Status
RATIFIED CONV-33 · SPEC-AMEND PATCHES v4.17 + v4.18 CONV-34. Part 1 (Behavioural Spec, 10 разделов) ratified CONV-32. Part 2 (Decision Log, 11 разделов) ratified CONV-33 pack-mode. Step 4.4 (Business review) + Step 4.5 (Ratification sweep) + workstream phase-1-3-implementation (P0) — completed during CONV-33. 21 HIGH SPEC-AMEND findings closed CONV-34 via v4.17 (Themes 1+2: token mechanics + session re-eval — 10 findings) + v4.18 (Themes 3+4: audit immutability + billing precision — 11 findings) patches. ADR 0004 v2 (audit retention) + ADR 0013 (proration policy) + ADR 0014 (MCP wrapper auth, placeholder shell) ratified. Sub-plan status remains ratified — amendments are inline patches, не re-ratification.
Goal
Phase 1.3 implementation-ready spec — UX flows + edge cases + open questions resolution. Не tech-level (DB schema / API contracts / library choice — implementation choice тех команды).
Success Criteria
Roma read → понимает все user-facing flows + edge cases → его scaffold не разойдётся с ratified decisions. Ilya read → может дать tech estimate без новых вопросов «что должно происходить».
Scope
IN: behavioural specs, edge cases, permission matrix as business rules, open question resolution.
OUT: DB schema, API contracts, library choice, deployment topology, UI mockups (отдельный track — Skeleton White design).
Anchored ADRs + Foundational sections
- ADR 0009 — Tenancy & Permission Architecture (Full spec)
- ADR 0010 — Stock allocation strategy (Full spec)
- ADR 0008 — Tier model (Skeleton — structure ratified, numbers pending)
- ADR 0005 v3 — SSO methods (Microsoft + Custom SSO scope для Tier 3 Stage 2)
- Foundational §1 (Entities), §2 (Onboarding), §3 (Billing), §4 (Access), §5 (Visibility), §6 (Buyer flow), §7 (Edge cases) —
docs/rendered/launch-plan-stage-1.html
Part 1 — Behavioural Spec (RATIFIED CONV-32)
1.1 — Signup + onboarding
Three signup paths (Foundational §2 ratified):
Path A — Paid tier signup (Owner создаёт новую Organisation)
Trigger: visitor → offplan.online → «Get started» → tier selection на pricing page (T1/T2/T3, default T1).
- Tier selection на pricing page. Trial = T1 only (per ADR 0008); T2/T3 require commitment.
- Signup form — email + password OR Google SSO OR Microsoft OAuth.
- Email verification = magic link (для password signup; OAuth pre-verified). 60 min validity per Foundational §2.
- Welcome email — minimum content: verification link + ссылка обратно на entry-point screen (per Phase 1.2 line 1503).
- Entry-point choice screen (Foundational §2 line 725, ratified) — 3 опции, default-cursor on (1):
- (1) «Start your first project» (primary CTA) → Object Builder (Phase 1.2.4). Single-project Clients = most common path.
- (2) «Set up your organisation» → мини-визард: Organisation type multi-select (Studio/Agency/Developer) + brand (logo + accent colour) + invite команды. Затем landing на Organisation dashboard.
- (3) «Skip» → empty dashboard, всё через menu.
- Trial = automatic (T1 14d, no card per ADR 0001).
TRIAL — N days leftbadge в шапке. - Только Object Builder доступен; Full Admin залочен до payment (per ADR 0008).
→ User = Owner новой Organisation на {slug}.offplan.online.
Path B — Guest signup (reverse invite, Free Guest tier)
Trigger: invitee получил reverse invitation от существующей Organisation (e.g. VV приглашает Italian developer).
- Click invite link → landing с context: «{Inviter Org} invited you as Free Guest для viewing project {X}».
- Signup form — email + password OR Google/Microsoft OAuth. (NO tier selection — Free Guest auto.)
- Email verification (magic link).
- NO entry-point choice screen — Free Guest не может create projects, поэтому direct landing → invited project с guest access.
- User создаёт own Free Guest Organisation; может invite users в свою Organisation (но они тоже видят только guest content).
→ User = Owner of Free Guest Organisation, с guest membership в inviter's project.
Path C — Team invite (existing Organisation, специфичная роль)
Trigger: existing Org Owner/Admin invites person в команду с role = Admin / Sales Manager / Content Editor / Sales Agent.
C.1 — Email new (no account):
- Invitee receives email с link
/invite/{opaque_token}(Phase 1.3.7). - Click → landing с context: «{Inviter Name} invited you to join {Org Name} as {Role}». Signup form (email pre-filled + locked) + password OR OAuth.
- After signup → user immediately joins target Org as specified role.
C.2 — Email уже имеет account:
- Click → если not logged in: login required (
{slug}.offplan.online/loginbranded ORapp.offplan.online/logincentral). - After login → confirmation: «Accept invite to {Org Name} as {Role}? [Accept] [Decline]».
- Accept → membership added; multi-Org Client теперь (см. 1.3 workspace switcher).
- Decline → token consumed (single-use, ratified). Inviter sees decline event (audit log), может re-send.
Token mechanics (per Phase 1.3.7): opaque + 7d expiry + single-use (decline ALSO consumes).
Edge cases (1.1)
- Subdomain collision на entry-point (2) Org setup OR при first project creation в Object Builder — inline error «taken, try alternative».
- Reserved subdomain — inline error (full list — Part 2.7).
- Trial expired без оплаты → auto-downgrade Free Guest, projects read-only; public sales page работает 30d затем freeze; re-pay в любой момент = instant restore (per ADR 0008).
- Email mismatch on invite (Path C — invitee пытается signup с другим email) → error: «email приглашения не совпадает, свяжитесь с тем, кто пригласил» (Foundational §2 line 765 verbatim, Stage 1).
- Invitee already member of target Org → explicit message «You're already a member of {Org Name}», redirect to Org dashboard.
- Path A signup с email который уже Client (used в другой Org) → не creates duplicate account; login screen prompt вместо new signup. Per Foundational §2.
Deferred (1.1)
- Studio name / country auto-enrich from email domain → Phase 1.5.x AI.
- Multi-step onboarding wizard (>2 screens) — entry-point (2) wizard scope-bound.
- Stage 2 self-service email merging.
1.2 — Login surfaces + auth-scoped views
1.2.A — Две точки входа
(Foundational §2 «Login UX», утверждено.)
- Брендированный subdomain login (по умолчанию):
{slug}.offplan.online/login— страница входа с брендингом Organisation (логотип, accent colour). Первая точка для приглашённых team members. - Центральный fallback login:
app.offplan.online/login— общий вход «не помню свой адрес». После успешного входа показывает список всех Organisations для этого email.
1.2.B — Маршрутизация после входа
| Точка входа | Одно членство | Несколько членств |
|---|---|---|
{slug}.offplan.online/login |
Сразу в эту Organisation | В эту Organisation; переключатель в шапке (Slack-стиль) |
app.offplan.online/login |
Автоматический редирект | Страница выбора Organisation после входа |
1.2.C — Иконка входа на sales-app страницах
Анонимный посетитель на {slug}.offplan.online/projects/{project-slug} видит иконку входа в правом верхнем углу (реализуется в Phase 1.10.0). Клик → modal с Google / Microsoft / email-пароль. После входа страница перерисовывается из state #1 в state #4 (1.2.G). Logout → возврат к анонимному виду.
1.2.D — Методы аутентификации
(Foundational §2 «Auth methods» + ADR 0005 v3, утверждено.)
| Метод | Описание | Этап |
|---|---|---|
| Google OAuth | «Войти через Google» — primary, рекомендуемый CTA | Stage 1 |
| Email + пароль | Минимум 12 символов, NIST-compliant, проверка по haveibeenpwned | Stage 1 |
| Сброс пароля | Magic link, 60 минут, single-use, invalidates active sessions | Stage 1 |
| Microsoft OAuth (Azure AD / Entra ID) | Корпоративные пользователи Microsoft 365 / Azure AD | Stage 2 (Tier 3 gate) |
| Custom SSO (SAML 2.0 / OIDC) | Generic IdP integration через Okta / Entra ID / OneLogin | Stage 2 (Tier 3 gate) |
1.2.E — Сессия и cookie
- Сессия = 30 дней со скользящим обновлением.
- Один cookie на
*.offplan.online. - Logout = удаление cookie + редирект на login page.
1.2.F — 2FA
- Stage 1: опциональный для всех Clients. TOTP (Google Authenticator) или SMS fallback.
- Operator dashboard (
staff.offplan.online): обязательный всегда. - Stage 2: обязательный для Owner + Admin (после 100 проектов / первой compliance проверки).
2FA-loss recovery (v4.18 SPEC-AMEND — CS HIGH 2):
- Backup codes на enable. При активации 2FA TOTP — system generates 10 single-use backup codes (8-char base32, displayed once + PDF download CTA). Stored hashed в
user_2fa_backup_codes(user_id, code_hash, consumed_at). User warned: «Save these. If you lose your authenticator, these are your only self-service recovery path.» - Self-service recovery flow: Login screen → «Lost my authenticator» link → enter email + backup code → если valid + not consumed → 2FA disabled + email confirmation to account email + audit event
2fa_recovered_via_backup. Forced re-enrollment on next login (cannot proceed without 2FA setup if previously enabled). - Operator-mediated recovery (Phase 1.4.7 surface): если backup codes lost too → submit support ticket → operator opens user identity verification (per § 1.5.F protocol: 2FA challenge не applicable, fallback to email + phone + last-billing-detail match). Operator console action
disable_2fa_operator_overridewith mandatory reason + audit eventpii_class = sensitive. Target SLA: 1 business day. - Rate-limit на backup code attempts: 5 invalid attempts / 15 min per email + IP → 1h lockout. Same anti-brute-force pattern как login.
1.2.G — Auth-scoped views (4 viewer states)
(Foundational §5 line 964 — утверждено.)
| Состояние | Кто это | Что видит |
|---|---|---|
| #1 Anonymous | Нет логина, нет buyer-token | Public Visibility preset Organisation: Private / Discovery / Full sales / PIN-protected |
| #2 Buyer с tokenised URL | URL содержит ?b=<token>, токен валиден |
Полная unit-page: цены, availability, contact = атрибутированный SA, Heart + «Связаться» работают |
| #3 Buyer с истёкшим токеном | Token есть, но >90 дней | Редирект на main project page; attribution к SA сохранена в DB |
| #4 Organisation team member (logged in) | Залогинен, открыл публичную страницу subdomain'а | Всегда Full sales preset; S·1 stock allocation фильтрует юниты (SA — assigned only; CE/SM/Admin/Owner — всё) |
Substate (PIN-protected): visitor #1 с valid pin_<project_id> cookie от предыдущего PIN entry → After-PIN preset (Discovery или Full sales) без blur. Cookie 30 дней. (§5.1)
1.2.H — Reserved subdomain blacklist
Зарезервированы: staff / app / api / admin / www. Полный список — Part 2.7.
Edge cases (1.2)
- Login на
{slug}.offplan.online/loginгде subdomain не существует → error «Organisation not found» + ссылка наapp.offplan.online/login. - Login на брендированный subdomain с email не member этой Organisation → generic error без хинта на центральный login (security minimum — не leak'аем существование аккаунта). User видит
app.URL в reset-password / welcome email. - Cookie corruption / session expiry mid-action — silent re-auth attempt; если fail → redirect на login со сохранением intended URL.
- Logout = со всех subdomain'ов (single cookie на
*.offplan.online= автоматически). Per-subdomain logout «log out from this device only» → Stage 2 (Settings → Sessions).
Deferred (1.2)
- Custom SSO (SAML / OIDC) → Tier 3 Stage 2 (ADR 0005 v3).
- Постоянный логин Buyer'а → Stage 2 (Foundational §2 line 701).
- Operator impersonation flow → Stage 4.
1.3 — Workspace switcher (Organisation + Project)
1.3.A — Когда Organisation switcher появляется
(Foundational §1 line 602 — утверждено.)
- Client с одним Organisation membership → переключатель не показывается.
- Client с двумя или более memberships → переключатель в шапке (Slack-style).
1.3.B — Где живёт переключатель
| Surface | Локация | Реализация |
|---|---|---|
| Admin panel | Шапка рядом с avatar | Phase 1.3.12 + onboarding-trial-mode |
| Public sales-app | Avatar dropdown в иконке логина (top-right) | Phase 1.10.0 |
Operator dashboard (staff.offplan.online) |
Не применимо | Operator — отдельная identity |
1.3.C — Группировка
(Phase 1.2 line 1387 — утверждено: «Slack/Linear pattern с Guest orgs section».)
Две секции:
- Owned / paid Organisations — где Client = Owner или member платной Organisation (T1 / T2 / T3).
- Guest organisations — где Client = member Free Guest Organisation.
Внутри секций: Org name + tier badge + role badge.
1.3.D — Cross-Organisation login behaviour
(Phase 1.10.0 line 3039 — утверждено.)
- Логин со страницы Organisation A через брендированный subdomain → auto-resolve в Organisation A.
- Переключатель → редирект на
{B-slug}.offplan.online. - На центральном
app.offplan.online/login: одно membership = авто-редирект; ≥2 = picker page.
1.3.E — Wrong-Organisation edge case
(Phase 1.10.0 line 3040 — утверждено дословно.)
- Client логинится на subdomain Organisation B, но он member только Organisation A → error: «Вы не member этой организации. Открыть свою организацию →» (ссылка на правильный subdomain).
1.3.F — Real-world пример (Foundational §1 line 628)
Иван (Content Editor + Sales Agent): один Client
[email protected], два membership'а — Content Editor в VV и Sales Agent в Evgenia Realty. Switcher показывает обе.
1.3.G — Project switcher (внутри текущей Organisation)
(Phase 1.6.2 line 2178 + Phase 1.1.2 line 1321 — утверждено.)
Отдельный switcher — переключает фокус между проектами текущей Organisation (не путать с Organisation switcher).
Видимость по роли (permission decision этого sub-plan'а):
| Роль | Что видит в Project switcher |
|---|---|
| Owner / Admin | Все проекты Organisation |
| Sales Manager | Все проекты Organisation |
| Content Editor | Все проекты Organisation |
| Sales Agent | Только проекты где у него ≥1 assigned unit (S·1 Closed pool) |
| Free Guest member | Только guest projects |
Поведение:
- «+ Create new project» CTA — Owner / Admin / SM (visible, активна если tier позволяет; Free Guest — скрыта с upgrade prompt). CE / SA — скрыта.
- Лимиты по tier — кнопка залочена с tooltip «Лимит проектов tier {X} достигнут. Upgrade →».
- Trial state — только Object Builder доступен; switcher показывает существующие + «+ New».
- Сохранение progress per project (line 1321) — переключение не теряет state.
UI deep-spec — делегирован в plans/onboarding-trial-mode.md.
Edge cases (1.3)
- Sales Agent теряет последний assigned unit в проекте (v4.17 SPEC-AMEND — Studios HIGH 3): non-jarring banner заменяет forced redirect — SA остаётся на текущем экране, banner «Ваш доступ к {Project Name} был обновлён — последний assigned unit передан {Cascade Target}. Проект теперь read-only для вас на 24 часа, потом скроется из switcher.» Grace-period 24h read-only — даёт SA время скачать buyer-records / контакты / завершить in-flight conversations. После 24h project убирается из dropdown silently на next page load (no redirect). SA initiates manual leave через banner CTA «Выйти из проекта сейчас» если хочет mood-clear instantly. Audit: «SA {X} lost last assigned unit в Project {Y}; grace-period 24h started».
- Project удалён — все team members получают redirect при следующем page load. Audit log: «Project {X} deleted by {User}».
- Free Guest стал Tier 1 — Project switcher теперь показывает «+ Create new project». Existing guest memberships preserved.
- Client удалён из всех Organisations кроме одной — Organisation switcher скрывается автоматически.
- Logout из переключателя — единый logout (cookie
*.offplan.online).
Deferred (1.3)
- UI design switcher'а →
plans/onboarding-trial-mode.md. - Search в switcher'е (≥10 memberships) → Stage 2.
- Self-service Client identity merge → Stage 2.
1.4 — Cross-entity invitations (forward + reverse)
1.4.A — 4 типа приглашений
(Foundational §4.1 line 854 — утверждено дословно.)
| Type | Описание | Token |
|---|---|---|
| Personal invite | Client → Client в свою команду. Role: Admin / SM / CE / SA. См. 1.1 Path C. | Single-use, 7 дней |
| Organisation invite (forward) | Developer's Org → Studio/Agency как guest organisation. | Multi-use, 7 дней |
| Reverse invite | Studio's Org → Developer как guest organisation (с Free Guest creation если нет аккаунта). | Multi-use, 30 дней (v4.17 SPEC-AMEND — Developer B2B evaluation cycles 2-4 weeks) |
| Buyer email link | SA → Buyer с tokenised unit links. Не invitation. См. 1.7. | Per-unit token, 90 дней |
1.4.B — Forward invite (Developer → Studio / Agency)
Use-case: Italian Developer создал project Cote (его Organisation платит), приглашает VV как guest organisation в роли Studio.
Flow:
- Инициатор: Owner / Admin Developer's Organisation. SM / CE / SA — нельзя.
- Form: Project Settings → Guest Organisations → «+ Invite organisation». Email + Role (Studio / Agency, default Studio) + scope = per-project always.
- Token: multi-use, 7d, opaque.
- Email: «{Developer Org Name} invites your organisation to join project {Project Name} as {Role}».
- Acceptance: existing Owner / Free Guest Owner → confirmation; нет аккаунта → signup Path A или B с pre-attached invitation.
- Result: guest organisation получает per-project access. Stock allocation = Closed pool default (0 юнитов до явного assignment).
1.4.C — Reverse invite (Studio → Developer)
Use-case: VV сделала Cote как готовый проект, отправляет Italian Developer'у. Developer регистрируется → Free Guest Organisation → видит Cote → решает upgrade на Tier 1 → получает full ownership.
Flow:
- Инициатор: Owner / Admin Studio's Organisation. Tier 1+ только (Free Guest не может приглашать guest organisations per Foundational §3 line 787).
- Form: Project Settings → Guest Organisations → «+ Invite developer». Email + Role: Developer (default) + scope = per-project always.
- Token: multi-use, 30d (v4.17 SPEC-AMEND — extended из 7d для Developer B2B evaluation cycles 2-4 weeks; see § 1.4.A row 3), opaque.
- Email: «{Studio Org Name} invites you to view project {Project Name} you can claim as your own».
- Acceptance: нет аккаунта → signup Path B (Free Guest tier creation); existing Owner → его Organisation получает guest membership.
- Conversion к платному: «+ Create project» в dashboard ИЛИ ownership transfer (1.5.F). Stage 1: ownership transfer self-serve до target Organisation's
first_successful_payment_at(v4.17 SPEC-AMEND — Sales motion HIGH 4; см. § 1.5.F).
Funnel visibility surface (v4.17 SPEC-AMEND — Sales motion HIGH 2):
Studio видит «Invited Developers» tab в Project Settings → Guest Organisations со столбцами: Email · Sent at · Opened at · Signed up at · Free Guest tier · Upgraded at. Audit log events feed эту view (per § 2.4.A Membership lifecycle). Stage 2: per-step conversion rate metric. Stage 1 = list view + manual count.
Legal basis + IP-rights warranty (v4.17 SPEC-AMEND — Legal HIGH 3, deferred deep spec to legal sub-plan):
- ToS clause «Studio warrants ownership / authorisation для project content shared via Reverse Invite» — owner:
plans/legal-multi-party-framework.md. - Anti-spam: rate-limit invites per Studio Organisation (max 10 reverse invites / 24h Stage 1; Phase 1.4 operator can flag abuse).
- Mandatory
Unsubscribe / Report abusefooter на email per CAN-SPAM / GDPR Art. 14 (data controller = Studio Organisation). - Recipient takedown flow → email
[email protected](Phase 1.4 operator queue) — gate spec в legal sub-plan.
1.4.D — Token mechanics
(Foundational §4.1 line 867 — утверждено + v4.17 SPEC-AMEND — Security HIGH 4 + 6.)
- Format: opaque random string ≥128-bit entropy (CSPRNG, e.g.
crypto.randomBytes(32)URL-safe base64). Stored hashed (SHA-256 — token DB column = hash; raw token only in email URL). - DB row в
invitationstable (token_hash/type/client_id/invitee_email/role/scope/status/expires_at/created_by_user_id/consumed_at). - Server-side lookup на каждый click — constant-time hash compare (prevent timing attacks). Raw token never logged.
- Rate-limit: per-IP soft cap 10 invalid token attempts / 5 min, hard cap 30/h. Per-email lookup cap 5 valid attempts / 15 min (anti-enumeration). Excess → 429 + audit event «invite_token_rate_limit_hit».
- Alert on burst: > 50 invalid-token attempts in 10 min на single IP / token → operator notification (Phase 1.4.7 audit dashboard surfaces alert).
- TTL: Personal + Forward invite — 7d; Reverse invite — 30d (см. § 1.4.A + § 1.4.C); ownership transfer — 7d (см. § 1.5.F); Buyer email link — 90d (см. § 1.7.J).
- Revoke: Owner / Admin (cross-entity) или Owner / Admin / SM (personal) →
status='revoked',revoked_at+revoked_by_user_id. - Single-use vs multi-use — per § 1.4.A column 3; consumed token row preserved для audit (status
consumed). - Audit log per ADR 0004 retention + § 2.4.A Membership lifecycle category.
1.4.E — Scope правила
(Foundational §4.x line 896–899 — утверждено дословно.)
- Personal invite — org-level по умолчанию.
- Personal invite — project-level через toggle в invite form.
- Organisation invite (guest org) — всегда project-level.
- Owner / Admin / Sales Manager своей Organisation — bypass scope.
Edge cases (1.4)
- Email mismatch → error «email приглашения не совпадает, свяжитесь с тем, кто пригласил» (Foundational §2 line 765 verbatim).
- Expired token → «Срок действия приглашения истёк. Свяжитесь с {Inviter Name} для нового».
- Revoked token → «Приглашение отменено».
- Re-invite того же email в ту же роль — previous invite auto-revoked, new supersedes. Audit: «Previous invitation superseded by new».
- Forward / reverse invite от Free Guest → заблокировано на UI (кнопка скрыта).
- Self-invite (Owner приглашает member своей же Organisation как guest) → error «Cannot invite member of your own Organisation as guest».
- Invitee — guest organisation уже member project'а → silent skip + message «Organisation {X} уже member project {Y}», redirect.
Deferred (1.4)
- Self-service email merging → Stage 2.
- Bulk invite UX (CSV import) → Stage 2.
1.5 — Role transitions
1.5.A — Authority
(Foundational §1 line 616 + §4.2 line 876 — утверждено.)
| Действие | Owner | Admin | Sales Manager |
|---|---|---|---|
| Invite member | ✅ | ✅ | — |
| Promote / demote (Admin ↔ SM ↔ CE ↔ SA) | ✅ | ✅ | — |
| Remove member | ✅ | ✅ | — |
| Revoke personal invite (pending) | ✅ | ✅ | ✅ (только им же отправленный) |
| Promote to Owner = ownership transfer | ✅ | — | — |
| Demote / remove Owner | ❌ — single Owner per Org, требуется ownership transfer first |
1.5.B — Promote / Demote
- Owner или Admin → Settings → Team → пользователь → new role.
- Instant change
membership.role. (v4.17 SPEC-AMEND — Security HIGH 1 + CS HIGH 3): server-side session records для затронутого user'а invalidated cross-subdomain на role-change commit —sessions.revoked_at = now()на все rows whereuser_id = target(any subdomain). Next request → re-auth required (Google OAuth or email/password). Cookie стирается через standard expiry path. Permission check hits DB на каждый privileged action (write / billing / membership / audit-read) — role / scope NEVER cached в JWT / cookie payload (cookie несёт толькоuser_id+ signed CSRF token). UI-side toast «Your role was updated. Please sign in again» появляется при detected revoke. - При role change с участием Sales Agent (CE → SA, SM → SA, или demote-to-SA с любой роли) — pre-change confirmation modal (v4.17 SPEC-AMEND — Studios HIGH 2): диалог показывает (a) текущий count assigned units затронутого user'а, (b) cascade target (см. § 1.5.E), (c) preview button «Скачать snapshot assignments перед изменением». На submit — все stock allocation assignments автоматически clear'аются (clean slate per role change), но pre-change snapshot записан в audit JSONB metadata (
previous_assignments: [unit_ids…]) для manual restore Stage 1 (operator dashboard Phase 1.4.7). Restore UI = Stage 2. - Audit log: «Role of {User} changed from {Old} to {New} by {Actor}» + JSONB metadata:
{previous_role, new_role, sessions_revoked: N, previous_assignments?: [unit_ids…]}.
1.5.C — Remove member (deactivation)
(Foundational §7.1 line 1196–1206 — утверждено + v4.18 SPEC-AMEND — CS HIGH 1.)
- Confirmation modal с consequences.
- Active sessions: immediate logout (server-side
sessions.revoked_at = now()cross-subdomain, per § 1.5.B v4.17 amendment). - Assignments cascade up к SM / Admin / Owner (с triage view + batch digest per § 1.5.E v4.17 amendment).
- Buyer-records preserved, attribution preserved.
- Login attempt после deactivation: generic «не получилось войти» (security — не leak). (v4.18 SPEC-AMEND — CS HIGH 1): operator dashboard (Phase 1.4.7) per-email lookup screen позволяет CS видеть actual reason (deactivated / wrong subdomain / unverified email / locked / 2FA-locked / not-member) на support tickets. Tenant-facing message остаётся generic; operator-facing actionable. Same applies к § 1.2 Edge cases generic wrong-Org error.
- Audit log: «{User} removed from {Org Name} by {Actor}».
1.5.D — Self-leave
(Foundational §7.1 line 1204 — утверждено дословно.)
- Settings → Leave Organisation → confirmation modal с consequences.
- Same downstream flow что Owner-removes (cascade up + buyer-records preserved + immediate logout).
- Audit trigger: «left the Organisation».
- Owner self-leave заблокирован — single Owner per Organisation, требуется ownership transfer first.
1.5.E — Cascade rules
(Foundational §4.4 + line 921 — утверждено: «escalate up, never auto-revert в Internal pool».)
| Кого удалили | Куда переходят assignments |
|---|---|
| Sales Agent | Sales Manager → Admin → Owner |
| Content Editor | Не имеет assignments напрямую |
| Sales Manager | Не имеет assignments напрямую (bypass scope) |
| Guest organisation member | Owner-team owning Organisation |
| Auto-revert в Internal pool | ❌ rejected |
Audit log + email notification к новому target'у на каждом cascade event'е.
Cascade triage view (v4.17 SPEC-AMEND — Studios HIGH 1):
- При SA removal с N > 0 assignments → новому target'у (SM / Admin / Owner per chain) приходит batch digest email (NOT per-unit flood). Format: «{N} units inherited from {SA Name}: {first 5 names…} +{N-5} more. Review →».
- Link открывает «Inherited assignments» triage screen (Phase 1.4.8 surface, lazy-built — Stage 1 = filter в существующем Stock view с pre-applied filter
inherited_from_user_id = {SA}). - Triage screen позволяет: (a) bulk re-assign к другому SA / SM, (b) keep at current target, (c) move to Internal pool (Open mode only). Audit per unit change.
- Email rate-limit: max 1 digest / target / hour (debounce при large cascade). Subsequent SA removals в same hour append to существующий digest до отправки.
- Commission notes (v4.17 SPEC-AMEND — Sales motion MED, lifted к HIGH-adjacent): digest и audit log включают line «Commission preserved to {departing SA}» — commission attribution не cascade'ится, только operational assignment (per Foundational §4.5).
1.5.F — Ownership transfer
(Foundational §3 line 810–826 — утверждено + v4.17 SPEC-AMEND — Sales motion HIGH 4 + CS HIGH 3.)
Self-serve gate tied к target Organisation's first_successful_payment_at (NOT actor's payment state). Pre-payment (target Org in trial / Free Guest state) = self-serve flow. Post-payment (target Org has ≥1 successful Stripe charge) = [email protected] operator-mediated flow.
Field definition: organisations.first_successful_payment_at set on first Stripe webhook invoice.paid (or equivalent) with non-zero amount. Refunds / chargebacks do NOT roll back this field — once flipped, ownership transfer requires support route forever. Stage 1 = single timestamp; Stage 2 may model «payment dispute window» if abuse appears.
Self-serve flow (4 шага, pre-payment):
- Owner → Settings → Transfer Ownership → email получателя.
- Email-инвайт с одноразовым токеном (7d expiry, entropy + rate-limit per § 1.4.D).
- Получатель открывает email, регистрируется или logs in → acceptance screen с описанием Organisation.
- На acceptance: получатель выбирает что произойдёт со старым Owner — стать Admin или быть удалённым из команды. Confirm → ownership flips, audit log: «Ownership transferred from {X} to {Y}».
Operator-mediated flow (post-payment, v4.17 SPEC-AMEND — CS HIGH 3):
Owner clicks «Transfer Ownership» → modal: «Your Organisation has active billing. Transfer requires support assistance for fraud-prevention.» CTA «Open support ticket» → routes to [email protected] via в-app form pre-filling:
- Current Owner identity (verified via 2FA challenge при ticket open).
- Recipient email.
- Reason for transfer (free-text).
Operator SLA + identity-verification checklist (lives в Phase 1.4.7 operator playbook):
- SLA target: 2 business days from ticket receipt to completion (or explicit denial).
- Verification steps: (a) confirm current Owner via login + 2FA + match email-on-file; (b) confirm recipient через email-clickthrough + 2FA setup; (c) flag fraud signals (recent password change · IP-country mismatch · payment method ownership delta · subscription age <30d) — if ≥2 signals → 5-day cooldown + manual review; (d) commit transfer via operator console (Phase 1.4.7) with mandatory audit reason.
- Cool-down: New Owner cannot initiate another transfer for 30d (prevents serial laundering).
- Audit log:
ownership_transfer_operator_completedevent with operator_user_id, fraud_signals JSONB, verification trail.
Stage 2: full self-service ownership transfer post-payment per ADR (TBD).
Edge cases:
- Получатель отказывается / token expires → invite cancelled, ownership остаётся.
- Получатель уже Owner другой Organisation → nothing breaks, N memberships.
- Target Org
first_successful_payment_at= NULL but actor (current Owner) is Tier 1+ via different Org → still self-serve eligible (gate is target Org's state, не actor's).
Связь с reverse invite (1.4.C): главный sales motion — Studio создаёт project на trial → reverse invites Developer → Developer accepts ownership transfer (pre-payment self-serve) → Developer makes first payment.
1.5.G — Re-invite после deactivation
(Foundational §7.1 line 1201 — утверждено дословно.)
- Re-invite того же email → fresh personal-invite process (single-use token).
- Assignments и buyer-records НЕ auto-restore — manual reassignment by SM / Admin.
- «Restore deleted user» button → Stage 2.
Edge cases (1.5)
- Member removed mid-buyer flow — buyer link продолжает работать; commission attribution preserved через cascade target.
- Admin removes member mid-invitation acceptance — acceptance succeeds, membership добавляется → Admin removes сразу после.
- Ownership transfer recipient declines, Owner пытается удалить аккаунт — заблокировано (single Owner per Org). Retry transfer.
Deferred (1.5)
- Restore deleted user с auto-restore assignments → Stage 2.
- Self-service ownership transfer после first payment → Stage 2/3.
- Operator-mediated forced ownership transfer → Phase 1.4 / Stage 2.
1.6 — Stock assignment
1.6.A — Stock allocation mode (per-project setting)
(Foundational §4.4 line 904 — утверждено.)
- Per-project setting в Project Settings (рядом с Public Visibility).
- Default = Closed pool (secure-by-default).
- Open pool = opt-in для проектов с «open competition» моделью.
1.6.B — Closed pool: 3 unit states
(Foundational §4.4 line 906–914 — утверждено дословно.)
| State | Кто видит | Кто может продать |
|---|---|---|
| Internal pool (default) | Owner / Admin / SM / CE / любой Internal SA. Guest org-members — НЕТ. | Owner / Admin / SM / любой Internal SA |
| Assigned to guest org | Только members этой guest org + Owner/Admin/SM owning Org + CE | Только members этой guest org |
| Assigned to user | Только этот user + Owner/Admin/SM owning Org + CE | Только этот user |
1.6.C — Open pool delta
(Foundational §4.4 line 917 — утверждено дословно.)
Единственное отличие от Closed mode: «Internal pool» юниты видят и могут продать ВСЕ Sales Agents с project access, включая members guest organisations. Assigned-to-user и Assigned-to-org остаются exclusive как в Closed mode.
1.6.D — Bulk assignment UX
(Foundational §4.4 line 920 — утверждено дословно.)
- Filter list view + multi-select checkboxes + dropdown «Assign to» (grouped: organisations / users).
- CSV import → Phase 1.5.6 (AI file upload endpoint).
- Range-select на floorplate → Stage 2.
1.6.E — Per-unit assignment
(Phase 1.3.4 line 1556–1568 — утверждено.)
- Schema-level reference:
units.assigned_topolymorphic FK. - Admin UI: dropdown «Assigned to» при создании unit + bulk-edit.
- Validation: только members owning Organisation могут менять.
- Audit per change.
1.6.F — Authority
(Foundational §4.2 line 882 — утверждено.)
| Действие | Owner | Admin | SM | CE | SA |
|---|---|---|---|---|---|
| Manage stock allocation | ✅ | ✅ | ✅ | — | — |
1.6.G — S·1 visibility rules (3 sub-rules per ADR 0010)
- Owner / Admin / Sales Manager always sees all units.
- Content Editor sees all units regardless of allocation (content access ≠ sales access).
- On target removal — escalate up. Never auto-revert в Internal pool.
1.7 — Reserve unit flow + conflict modal
1.7.A — Status transitions
(Foundational §6.1 line 1121–1124 — утверждено.)
- Forward: Available → Reserved · Available → Sold · Reserved → Sold.
- Reverse: Sold → Reserved · Sold → Available · Reserved → Available.
- Permissions (per §4.2 line 885): Owner / Admin / SM / SA (own assigned).
1.7.B — Trigger UX (button-driven)
(Foundational §6.1 line 1099–1106 — утверждено дословно.)
- Trigger: SA → unit card → «Mark as Reserved» (амбер) или «Mark as Sold» (red).
- Required: buyer email (autocomplete из buyer-records SA, recent first).
- Conditional required: name + phone (если new buyer-record).
- Optional: notes + file upload.
- Submit: server validates → создаёт/links buyer-record → flips status → audit + email notification.
1.7.C — Smart-match logic (3-tier email lookup)
(Foundational §6.1 line 1110–1116 — утверждено дословно.)
- Lookup в buyer-records этого SA (org-scope) → match → auto-link.
- Lookup в Organisation-wide records → match → warning «attributed to {Other SA}, new record will be created».
- No match → form требует name + phone, новый record.
1.7.D — Reverse-flip policy
(Foundational §6.1 line 1122–1124 — утверждено дословно.)
- Same permissions как forward.
- Email notification одному уровню выше (SA → SM, SM → Admin/Owner). Forward — без notification.
- Buyer-record preserved; unit↔buyer link воидится. Re-flip forward → SA re-fills форму; same email = auto-link recreates.
1.7.E — Race condition (first-click-wins)
(Foundational §7.4 line 1234 — утверждено дословно.)
- Loser modal: «Только что зарезервировал {Name} в {TIMESTAMP} — обнови страницу».
- Audit оба attempts + winner.
1.7.F — Pessimistic lock (per ADR 0010)
- DB transaction lock на unit row при reserve action.
- Validates allocation (S·1) + status (must be
available). - Writes
reserved+reserved_by_user_id. - Lock release > 500ms (rare) → fall through to optimistic conflict modal (same UX как 1.7.E).
1.7.G — No reservation TTL (per CONV-18)
- Платформа НЕ управляет таймером.
- Organisation решает когда менять status сама — reservation процесс ведётся в external CRM.
1.7.H — Buyer view sold/reserved
(Foundational §6 line 1074 — утверждено.)
Buyer с tokenised URL → unit-page открывается нормально, status badge «Sold» / «Reserved». Никаких 404 / hard blocks.
1.7.I — Audit log
(Foundational §6.1 line 1129–1133 — утверждено.)
- Forward-flip: «User {X} marked unit {Y} as Reserved/Sold; linked to Buyer {Z} (existing | new) at {TIMESTAMP}. Notes. File».
- Reverse-flip: «User {X} reverted unit {Y} from Sold → Reserved at {TIMESTAMP}. Voided link to Buyer {Z}. Reason».
- Visible to: Owner / Admin / SM. SA — own actions on own assigned units.
1.7.J — Buyer tokenised URL mechanics (v4.17 SPEC-AMEND — Security HIGH 2)
(Замечание: anchors сразу к § 1.2.G row #2 «Buyer с tokenised URL» + Foundational §6 line 1074 + § 1.4.A row 4.)
- Format: opaque token ≥128-bit entropy (CSPRNG), URL-safe base64. Stored hashed в
buyer_unit_tokens(token_hash, buyer_id, unit_id, project_id, org_id, attributed_sa_user_id, created_at, expires_at, revoked_at, revoked_reason). Raw token only в email link. - Scope = single (buyer, unit) pair. One row per unit; buyer с N attributed units → N rows + N distinct URLs. No org-wide / multi-unit tokens.
- Lifetime: 90d sliding window from issuance. Re-issuable by attributed SA / SM / Admin / Owner (Owner/Admin can re-issue cross-SA per cascade events).
- Rotation (forced):
- SA removed / role-change-to-non-SA (§ 1.5.B/C/E cascade) → tokens issued by that SA → automatic
revoked_at+ reasonsa_lost_access. New attributed SA receives «Re-send buyer link» action item. - Unit re-attribution (buyer reassigned to different unit / different SA) → previous token revoked, new token issued.
- Buyer email change (Phase 1.4 operator-mediated) → all tokens for that buyer rotated.
- Manual revoke by Owner / Admin / SM / attributed SA → button «Revoke buyer link» в unit-card buyer panel.
- SA removed / role-change-to-non-SA (§ 1.5.B/C/E cascade) → tokens issued by that SA → automatic
- Lookup: constant-time hash compare; rate-limit 30 invalid token hits / 10 min per IP + alert on burst.
- HTTP headers on buyer page response:
X-Robots-Tag: noindex, nofollow, noarchive, nosnippet— prevents search-engine indexing.Referrer-Policy: no-referrer— prevents token leak via Referer header to embedded resources.Cache-Control: private, no-store— prevents proxy caching.Content-Security-Policy: frame-ancestors 'none'— prevent embed/clickjack.
- Token URL format:
https://{slug}.offplan.online/{project-slug}/units/{unit-slug}?b={token}. Token never appears в HTML body; consumed server-side в session middleware (sets buyer cookie scoped to single (buyer_id,org_id) request → never*.offplan.online). - Suspension state (§ 1.8.C): tokens работают read-only до expiry; CTA disabled.
- Audit log: issuance, rotation, revocation, lookup-failure burst alerts (per § 2.4.A new sub-category «Buyer tokens»).
- Storage / retention: revoked token rows kept 12mo (audit window per ADR 0004); auto-purge after.
1.8 — Tier transitions
1.8.A — Trial → Paid (Path A signup)
(Foundational §3 line 843 — утверждено.)
- Click on
TRIAL — N days leftbadge → pricing modal → tier select → Stripe → first payment → trial badge исчезает, Full Admin открывается.
1.8.B — Trial expired без оплаты
(Foundational §3 line 804 — утверждено + v4.18 SPEC-AMEND — Finance HIGH 1.)
- Auto-downgrade Free Guest, projects read-only.
- Public site работает 30 дней потом freeze.
- Re-pay в любой момент = instant restore.
Stripe customer cleanup (v4.18 SPEC-AMEND — Finance HIGH 1):
- Free Guest = no Stripe
subscriptionobject. При auto-downgrade existing trial subscription →cancel_at_period_end → immediate cancellationчерез Stripe API; webhookcustomer.subscription.deletedtriggers Org tier flip (Trial → Free Guest). - Stripe
customerobject retained для re-pay restore path — preserves payment method history, tax / VAT metadata, dispute history, addresses. Orgexternal_customer_idstays linked (per Learning «PaymentProvider abstraction»). - Re-pay flow: Free Guest Org → «+ Create project» → upgrade modal → Stripe Checkout reuses saved customer → new subscription created → tier flips Free Guest → Tier 1/2/3 (per § 2.8.A trigger).
- Audit: Tier transitions category logs
trial_auto_downgrade_free_guestevent +subscription_cancelledJSONB metadata. - Failed-card edge case: trial → first payment fails (card decline) → 3 retry attempts (Stripe automatic) → если all fail → same Free Guest auto-downgrade path; customer object preserved; CS notification email sent to Org Owner.
1.8.C — Paid → Suspended (просрочка / chargeback)
(Foundational §3 line 832–836 — утверждено + v4.18 SPEC-AMEND — Finance HIGH 3.)
- 14d grace period для просрочки (payment failure).
- Chargeback overrides grace immediately (v4.18 SPEC-AMEND — Finance HIGH 3, cross-link ADR 0006): Stripe webhook
charge.dispute.created(илиradar.early_fraud_warning.created) → tier → Suspended immediately, NO grace. ADR 0006 auto-freeze policy wins. Audit eventchargeback_auto_freeze(pii_class = sensitive). - Hybrid block: login OK, edit/invitations/публикация blocked. Public site работает 30d потом freeze.
- Юниты статусы не меняются.
- Buyer tokens работают read-only до 90d expiry (Foundational §7.4 line 1235). CTA «Связаться со мной» disabled.
- Revenue recognition (v4.18 SPEC-AMEND — Finance HIGH 3 follow-on): cash-basis Stage 1. Revenue recognised on Stripe
payment_intent.succeeded(net of fees); refunds + chargebacks deducted oncharge.refunded/charge.dispute.funds_withdrawnevents. Stage 2 / pre-audit milestone: switch to accrual basis with deferred-revenue schedule per subscription term. Cross-linkplans/onboarding-trial-mode.md+ ADR 0008.
1.8.D — Suspended → Restore
(Foundational §3 line 836 — утверждено.)
- Pay → full access сразу.
- Audit log «reactivated».
- Email notifications команде + guest organisations.
1.8.E — Free Guest → Paid (Path B conversion)
(Foundational §1 line 629 — утверждено.)
- «+ Create project» в Free Guest dashboard.
- Upgrade modal → tier select → Stripe → tier flips на Tier 1+.
- Existing guest memberships preserved в Guest section workspace switcher'а.
1.8.F — Tier upgrade / downgrade (T1 ↔ T2 ↔ T3)
- Upgrade (T1 → T2 / T3): Settings → Billing → Change tier → Stripe proration → новые лимиты + features instantly.
- Downgrade (T2 → T1): разрешить только если Organisation в пределах лимитов нового tier'а. Если нет (например projects > T1 limit) → блокировка с message «Удалите {N} projects / {M} team members перед downgrade'ом» (explicit user action). Auto-freeze лишних → не делаем.
Proration policy (v4.18 SPEC-AMEND — Finance HIGH 2, anchored ADR 0013):
- Upgrade (T1 → T2 / T3): Stripe proration (default) — credit for unused T1 period, charge prorated T2/T3 from upgrade date. Customer-facing: «Charged {amount} today (prorated); next renewal {date}.»
- Downgrade (T2 → T1, или T3 → T2 / T1): end-of-period switch — current period billed at higher tier до renewal; downgrade takes effect on next renewal date; no refund / credit for the unused higher-tier days. UI message: «Your tier will change to {New Tier} on {renewal_date}. Until then you keep {Current Tier} access.»
- Trial → Paid: full new-tier period (no proration) — clean billing cycle starts на Path A conversion.
- Reverse-invite ownership transfer (§ 1.5.F + § 1.4.C): new Owner's first paid period starts fresh (resets billing clock) — old Owner's prior trial/Free Guest state does not transfer billing history; Stripe customer object can be reused (linked to new Owner) OR new customer created (decision at transfer ratification, defaults to new customer for clean audit trail).
- Implementation: Stage 1 = Stripe default proration on upgrade, scheduled subscription change on downgrade. See ADR 0013 for rationale + alternatives.
Cross-reference: ADR 0013 (Proration policy) ratified CONV-34 v4.18 SPEC-AMEND.
1.9 — Internal vs External Sales Agent
1.9.A — Same role, different operational context
(Foundational §4.5 line 934 — утверждено дословно.)
Sales Agent — одна role с двумя operational контекстами:
- Internal SA — Client из team самой Organisation.
- External SA — Client из guest organisation, приглашённой через guest-org invite.
Базовые permissions в §4.2 (Section 1.10) одинаковы.
(Foundational §4.5 line 936–944 — утверждено дословно.)
| Ось | Internal SA | External SA |
|---|---|---|
| Stock pool default (Closed mode) | Видит Internal pool | Не видит Internal pool — только assigned |
| Cross-team visibility | Видит коллег-Internal SA в своей Organisation | Видит только своих в guest organisation |
| Invitation source | Personal invite от Owner / Admin / SM | Член guest organisation, через org-invite |
| Removal authority | Owner / Admin / SM (revoke personal invite) | Admin owning Org (revoke guest-org membership) или Admin guest org |
| Reports / analytics | Aggregate Org-level (own + team, без других guest orgs) | Own продажи + own guest organisation aggregate |
1.9.B — Membership_type derivation
(Phase 1.3 v4.7 callout line 1530 — утверждено.)
membership_type = internal | external — derived через organisation_id link на Sales Agent's home Organisation:
home_org == owning_org→ Internalhome_org != owning_org→ External
RBAC должен экспонировать обе variants через API + UI permissions filter.
1.9.C — UI distinction
Badge «External» рядом с именем external SA в Team list owning Organisation (admin UX clarity — кто сотрудник, кто guest-org member).
1.10 — Permission matrix (5 ролей × actions)
1.10.A — Hierarchy
(Foundational §4.2 line 870 — утверждено дословно.)
Owner ⊃ Admin ⊃ {Sales Manager, Content Editor, Sales Agent}
SM / CE / SA — параллельны (не subset друг друга). Один Client = одна role per Organisation (если нужно и контент, и продажи → Admin).
1.10.B — Полная permission matrix
(Foundational §4.2 line 873–890 — утверждено дословно.)
| Действие | Owner | Admin | Sales Manager | Content Editor | Sales Agent |
|---|---|---|---|---|---|
| Billing / cancel / transfer | ✅ | — | — | — | — |
| Add / remove team members | ✅ | ✅ | — | — | — |
| Invite / remove Internal Sales Agents (own team) | ✅ | ✅ | ✅ | — | — |
| Invite / remove External Sales Agents (via guest organisation) | ✅ | ✅ | — | — | — |
| Invite guest organisations | ✅ | ✅ | — | — | — |
| Edit content (gallery, renders, floorplates, descriptions, переводы) | ✅ | ✅ | ✅ | ✅ | — |
| Edit unit pricing | ✅ | ✅ | ✅ | ✅ | — |
| Manage stock allocation (assign / unassign units) | ✅ | ✅ | ✅ | — | — |
| Create / edit buyer profiles | ✅ | ✅ | ✅ | — | ✅ (own) |
| Send presentation emails to buyers | ✅ | ✅ | ✅ | — | ✅ (own) |
| Change unit status (available → reserved → sold) | ✅ | ✅ | ✅ | — | ✅ (own assigned) |
| Public Visibility settings (Private / Discovery / Full sales / PIN-protected) | ✅ | ✅ | — | — | — |
| Branding / theme customisation (project-level) | ✅ | ✅ | — | ✅ | — |
| Branding / theme customisation (Organisation-level) | ✅ | ✅ | — | — | — |
Reverse-flip note (CONV-26): reverse направление status change (Sold → Reserved/Available, Reserved → Available) — same permissions как forward. Каждый reverse-flip → audit log + email notification одному уровню выше. Forward — audit only.
1.10.C — View-as-Agent (admin debug + training)
(Foundational §4.6 line 948–953 — утверждено дословно.)
- Доступен: Owner / Admin / Sales Manager. Sales Agent — нет.
- UI: в шапке админки toggle/dropdown «View as…» → Sales Agent / другой Sales Manager. Banner: «Viewing as {Name} ({Role}) · Exit view-as-mode →».
- Read-only mode: все действия залочены. НЕ impersonation — для actual impersonation operator flow в Phase 1.4, Stage 4.
Part 2 — Decision Log (RATIFIED CONV-33)
2.1 — Workspace switcher mechanics
Closed as cross-ref. Поведенческий спек workspace switcher закрыт в Part 1 § 1.3 (A когда появляется / B routing / C группировка / D cross-Organisation / E wrong-Organisation / G Project switcher). UI deep-spec (визуальный layout, dropdown rendering) делегирован в plans/onboarding-trial-mode.md. Нет открытых вопросов для Phase 1.3 implementation.
2.2 — Ownership transfer detail
Closed as cross-ref. Полностью покрыто в Part 1 § 1.5.F + Foundational §3 lines 811-826 (4-step flow + cross-link с reverse invite sales motion). Token mechanics = single-use + 7d (consistent с personal invite per § 1.4.D). Audit log entry («Ownership transferred from {X} to {Y} at {TIMESTAMP}. Old owner outcome: {Admin | Removed}») + email notifications (new Owner + old Owner + team broadcast; guest organisations silent) — details в § 2.4 (Audit log scope).
2.3 — Referral attribution rules
Real design. Scaffolding в Foundational §7.5 lines 1241-1273 даёт 7 ratified bullets; добавляем 6 + 2 v4.18 SPEC-AMEND picks для Stage 1 ratification:
- Credit accrual = dual condition. Запись в
referralsпоявляется при signup referee (signup_at).payout_status: pending → eligibleкогда обе стороны выполнены: (a) referee's Organisation first upgrade на paid tier (upgraded_to_client_at), (b) sponsor на paid tier на момент upgrade'а referee. Если sponsor всё ещё Free Guest — статус остаётсяpending, активируется когда sponsor upgrades. (v4.18 SPEC-AMEND — Finance HIGH 4): retroactive eligibility cap = 12 months from referee'supgraded_to_client_at. Если sponsor upgrades > 12mo после referee upgrade →payout_status = ineligible_expired(audit entry + sponsor-facing note «Referral expired — sponsor upgrade exceeded 12-month cap»). Prevents unbounded retroactive payouts. - Payout amount calculation — Stage 2 / Phase 4.2. Stage 1 хранит
payout_amount = null. - Referral code generation — auto-generated при первом access экрана Settings → Referrals. Lazy creation (не при signup всех Client'ов — экономия записей). Format: 6 chars base32 (~1B variants — достаточно при <10k активных users в Stage 1). Stored в
referral_codes(user_id, code)уникально. - Self-ref + reverse-ref detection — already specified §7.5 line 1248: запись с
payout_status: ineligible_cycle. Подтверждение: detection runs server-side на момент Organisation creation (cycle = sponsor попадает в свой же reverse-chain). ?ref=URL param scope — primary attribution; signup form backup field («Referred by code» optional). NO cookie tracking. Если оба заполнены и различаются → wins URL param (newer signal); audit log entry «conflicting referral params: URL={X}, form={Y}; URL chosen». (v4.18 SPEC-AMEND — Sales motion HIGH 3): existing-account signup attribution. Sponsor binding tied к Organisationcreated_at(НЕ usersignup_at). Если referee — existing user creating new Organisation → reference?ref=или form value attaches sponsor к new Organisation'screated_at. Existing-user re-using account для new Org with valid?ref=→ referral row created (no longer drops silently). Audit: «Referral attributed на Organisation creation: sponsor={X}, referee_org={Y}, referee_user={Z}».- Stage 1 visibility UI для Free Guest — экран Settings → Referrals показывается всем (включая Free Guest) с disabled top block «Upgrade to start referring» + locked tooltip на «Скопировать ссылку». Мотивация upgrade. Rejected: скрывать до upgrade (confusion «зачем мне это, я ещё не плачу»).
2.4 — Audit log scope для Phase 1.3 events
Real design. Phase 1.4.7 закрывает operator-side audit; здесь org-side enumeration для Phase 1.3.
2.4.A — Logged event categories
| Категория | События |
|---|---|
| Membership lifecycle | invite sent · invite accepted · invite declined · invite revoked · invite superseded · member added · role changed · member removed · member self-left |
| Authentication | login success · logout · failed login (per email + IP, anti-brute-force aggregate) · 2FA enabled / disabled · password reset triggered · password changed |
| Stock allocation | unit assigned (to user / org) · unit unassigned · cascade event (escalate up) · pool mode flipped (Open ↔ Closed) |
| Ownership transfer | initiated · accepted · declined / expired · completed |
| Tier transitions | trial started · trial expired auto-downgrade · paid upgrade · downgrade attempt (blocked / completed) · suspension entered · suspension exited |
| Status flips (Phase 1.7) | forward (Available → Reserved/Sold) · reverse (Sold → Reserved / Reserved → Available) — per Foundational §6.1 lines 1129-1133 |
| View-as-Agent (§4.6) | enter · exit · duration recorded |
| Public Visibility | preset changed (Private / Discovery / Full sales / PIN) · PIN attempts (rate-limit aggregate) |
| Referrals | code generated · referral_signup · cycle blocked (ineligible_cycle) · payout status change |
2.4.B — Visibility per role
| Категория | Owner | Admin | Sales Manager | Content Editor | Sales Agent |
|---|---|---|---|---|---|
| Membership lifecycle | ✅ | ✅ | own-team events | — | own (re self) |
| Authentication | ✅ (own + team) | ✅ (team) | — | — | own only |
| Stock allocation | ✅ | ✅ | ✅ | — | own assigned |
| Ownership transfer | ✅ | ✅ (read-only) | — | — | — |
| Tier transitions | ✅ | ✅ (read-only) | — | — | — |
| Status flips | ✅ | ✅ | ✅ | — | own actions |
| View-as | ✅ | ✅ | ✅ (own session) | — | — |
| Public Visibility | ✅ | ✅ | — | — | — |
| Referrals | ✅ (own + Org) | ✅ (Org events) | — | — | own only |
2.4.C — Storage + retention
- Append-only table
audit_events(id, org_id, actor_user_id, action, target_type, target_id, metadata JSONB, created_at, ip, user_agent, pii_class). - Tenant-scoped queries via
org_id. Per-role filter на query layer. - Retention per ADR 0004 (amended v3 CONV-34) — 12 months active + archive до 7 years (Cyprus statutory limitation), pseudonymisation на archive boundary.
- Operator dashboard (Phase 1.4.7) reads same table cross-tenant.
Immutability + tamper-evidence (v4.18 SPEC-AMEND — Security HIGH 3):
- Insert-only DB role — application connects через
audit_writerPostgres role сINSERTgrant only наaudit_events(noUPDATE/DELETE/TRUNCATEgrants). Even с stolen DB credentials atttacker cannot modify history. Schema migrations / archival jobs run under separateaudit_adminrole rotated через secrets manager. - Monthly hash-chain seal — на конец каждого месяца cron job вычисляет
seal_hash = sha256(prev_seal_hash || rows_concat(sorted by id))для каждогоorg_id. Stored вaudit_seals(org_id, period_yyyymm, seal_hash, row_count, first_id, last_id, sealed_at). ЛюбаяUPDATE/DELETE(если grant'ы compromised) breaks chain — verification job runs weekly + alerts operator dashboard. Stage 2: hardware WORM media (per LOW Stage 2 backlog). - Off-box archive shipping — еженедельный job streams
audit_eventsrows older than 30d в separate AWS account / S3 bucket (Object Lock = Compliance mode, immutable до retention expiry). Cross-account write-only IAM role; no read-back from production. Stage 1 = same Cyprus region (eu-south-1 / eu-central-1 depending on hosting decision). - No UPDATE/DELETE grants verified в Phase 1.3 implementation via boot-time check (app refuses to start если
audit_writerhasUPDATEилиDELETEprivilege наaudit_events).
PII classification + GDPR-compliant retention (v4.18 SPEC-AMEND — Legal HIGH 2 + Legal HIGH 1):
pii_classcolumn (enum:none/personal_meta/personal_content/sensitive) populated по action category:none= pool flips, public visibility changes (no PII).personal_meta= login events, role changes (user_id + IP, no content).personal_content= stock allocation, status flips with buyer link, View-as-Agent events (touches buyer PII).sensitive= ownership transfer with bank/billing detail, 2FA enable/disable.
- JSONB metadata PII tagging — each metadata key tagged in code via central registry (
audit_metadata_schema.ts); archival shipping job redactssensitivefields > 90d (Stage 1) или pseudonymises (Stage 2 per LOW backlog). - Pseudonymisation post-12mo — после 12mo active retention
actor_user_idreplaced с deterministic hash (hmac_sha256(user_id || global_pepper)) на archive shipping. Original mapping preserved в separate vault DB (operator-access only, retained for fraud investigation до 7yr Cyprus limitation period). Joining vault + archive requires explicit legal-basis ticket + operator dual-approval. - Data-controller annotation —
metadata.controller_org_idfield auto-populated на every audit event touching buyer PII (per Foundational §1 + Legal HIGH 1 joint-controller flag). Detailed Art. 26 controller-of-record framework deferred кplans/legal-multi-party-framework.md(Phase 1.9 sub-plan); этот sub-plan записывает только thecontroller_org_idfield requirement. - Buyer joint-controller gate — for any audit event touching
buyer_*table data, sub-plan defers controller-of-record determination к legal sub-plan; current behaviour:controller_org_id = primary_assigned_sa.org_id(operationally correct but legally provisional).
2.4.D — UI surface для Phase 1.3
- Settings → Audit log на Organisation level (Owner / Admin view).
- Project Settings → Activity log на per-project level (Owner / Admin / SM view, filtered по
project_id). - Sales Agent dashboard → «My activity» — own actions.
Deferred (2.4)
- Full-text search → Stage 2 (mirror Phase 1.4.7 iteration 2).
- CSV export для compliance → Stage 2.
- JSON diff display для PATCH events → Stage 2.
2.5 — Naming finalisation (Sales Manager + Content Editor)
Closed без изменений. Имена Sales Manager + Content Editor ratified в v4.7 (CONV-24); consistent across Foundational §1 lines 615-619, §4.2 line 870, Phase 1.3.9, permission matrix Part 1 § 1.10.B. Альтернатив не предложено. Закрыт.
2.6 — Email mismatch wording
Closed as verbatim. «Email приглашения не совпадает, свяжитесь с тем, кто пригласил» (Foundational §2 line 765, verbatim в Part 1 § 1.1 + § 1.4.E). EN/AR i18n — Stage 2 per ADR 0007.
2.7 — Subdomain reservation list
Real design. Полный list (58 entries, categorised):
2.7.A — Платформа (operator + auth)
staff · app · api · admin · www · auth · accounts · signup · login · signin · register · console · dashboard
2.7.B — Инфра / DNS
mail · email · smtp · mx · ftp · webhook · webhooks · cdn · assets · static · media · files
2.7.C — Environments
staging · dev · test · qa · preview · sandbox
2.7.D — Functional / brand
blog · news · press · docs · developers · kb · help · support · status · health · legal · terms · privacy · billing · payments · pay · checkout · analytics · metrics · pricing · about · contact
2.7.E — Brand / vendor (typosquat protection)
offplan · offplanonline · volumevision · stripe · paddle · notion
2.7.F — Validation rules
- Length: 3-63 chars (DNS standard).
- Allowed charset:
[a-z0-9-], не начинается / заканчивается дефисом. - Не reserved выше.
- Case-insensitive uniqueness check.
2.7.G — Storage + maintenance
- Static config file (
config/reserved-subdomains.json) — не DB. - Adding new reservations = code change + deploy.
- Self-serve operator UI «Reserved subdomains management» → Stage 2 (Phase 1.4.x backlog).
2.8 — Free Guest → paid conversion
Real design. Триггер + миграционные правила.
2.8.A — Триггер (dual)
- Lazy — «+ Create project» CTA в Free Guest dashboard → upgrade modal → tier select → Stripe → conversion completes. Primary path per Foundational §3 line 844.
- Explicit — Settings → Billing → «Upgrade plan» button. Same modal.
2.8.B — Что мигрирует (preserved as-is)
- Organisation name + slug + subdomain (
{slug}.offplan.online— Free Guest имел дефолтный subdomain с момента signup Path B). - Brand (logo + accent colour, если установлены в Free Guest state).
- Members (если были invited per Foundational §3 line 787).
- Existing guest memberships в чужих projects (Foundational §1 line 629 verbatim).
- Settings (Organisation type multi-select, locale preference).
- Audit log / activity trail (continuous).
- Client identity (email + password / OAuth).
2.8.C — Что новое (activated on conversion)
- Tier badge (Free Guest → Tier 1 / 2 / 3).
- Billing record (Stripe customer + subscription).
- Project creation unlock.
- Guest organisation invitation unlock (Free Guest blocked per §3 line 787, теперь allowed для Tier 2+).
- 2FA mandatory window opens на Stage 2 для Owner+Admin (per Part 1 § 1.2.F).
2.8.D — Edge cases
- Conversion without own projects — Free Guest accounts с guest memberships но без own projects upgrade → unlock «+ Create project» surface; existing guest memberships preserved unchanged.
- Conversion during expired-trial state — если account был Trial → auto-downgrade Free Guest → re-pay = restore (per § 1.8.B), это технически тот же path, не «Free Guest conversion».
- Conversion отменена mid-Stripe — нет state change; повторный trigger из same source.
2.8.E — Backwards (paid → Free Guest)
- Только auto: trial expired без оплаты OR suspension (per § 1.8.B / § 1.8.C).
- Self-trigger downgrade Tier 1+ → Free Guest — blocked. Avoids data-loss flows. Downgrade allowed только между paid tiers per § 1.8.F.
2.9 — Sales Agent dual-Org membership
Real design. Подтверждение модели + home_org formalisation.
2.9.A — Allowed combinations
Один Client (один email) может быть members в N разных Organisations с разными ролями per Organisation (per Foundational §1 line 628 verbatim example: Иван CE в VV + SA в Evgenia Realty). Explicitly ratified:
- Internal SA в А + External SA в Б (через guest org invite) — allowed;
membership_typederived per Part 1 § 1.9.B. - Owner в А + Sales Agent в Б — allowed.
- CE в А + Owner в Б — allowed.
2.9.B — Forbidden
- Two roles per same Organisation (Foundational §4.2 line 870 verbatim: «Один Client = одна role per Organisation»). Если нужно и content и sales → роль Admin. Validation: invite form rejects если email уже member target Org.
2.9.C — home_org formalisation (new)
home_org_idper Client = Organisation, в которой Client был создан (signup Path A — own Org; Path B — own Free Guest Org; Path C — Org инвайтера).home_org_idустанавливается один раз при first Organisation creation / first membership и не меняется.- Если Client удалён из home_org но остался в других → флаг
home_org_orphaned: true(audit + warning в admin; cascade rules § 1.5.E применяются). - Membership type derivation:
membership.org_id == client.home_org_id→ Internal.membership.org_id != client.home_org_id→ External (badge displayed).
2.9.D — Switcher rendering (dual-Org SA)
- Иван home_org = VV. Switcher группы (per § 1.3.C):
- Owned / paid — все memberships в paid Organisations (Owner OR не-Owner). Если Evgenia paid → попадает сюда.
- Guest organisations — memberships в Free Guest Organisations.
- Per-Org role + tier badge inline.
2.9.E — Reports / analytics scope (per Foundational §4.5 line 943)
- В VV (Internal): Иван видит aggregate VV team (Internal SAs, не external guest orgs).
- В Evgenia (External): Иван видит only own продажи + Evgenia's guest org aggregate.
2.10 — Open/Closed pool reversibility
Real design. Per-project flip rules.
2.10.A — Both directions allowed
Closed → OpenиOpen → Closedв любой момент. No one-way lock.
2.10.B — Permission
- Owner / Admin / Sales Manager (matching «Manage stock allocation» row Part 1 § 1.10.B / Foundational §4.2 line 882).
2.10.C — UI + confirmation
- Project Settings → Stock allocation mode (рядом с Public Visibility).
- Toggle с confirmation modal showing consequences («N units сейчас в Internal pool — после flip станут видны / спрятаны для guest organisations»).
2.10.D — Audit + notifications
- Audit log entry per flip: «Stock allocation mode flipped from {Old} to {New} by {User} at {TIMESTAMP}».
- Email notification на Open → Closed flip — guest org members: «Your access to Internal pool in project {X} has been revoked. Your assigned units (N) remain accessible.»
- Closed → Open flip — без email (расширение visibility, не loss).
2.10.E — NO auto-assign на flip
- Units не auto-assigned to guest orgs based on mid-Open activity. Security default.
2.10.F — Edge case: unit Reserved by External SA в Open mode, проект flipped to Closed
Variant A (chosen): Status preserved (Reserved сохранён в DB); External SA теряет visibility unless unit explicitly assigned to его guest org. Audit log warns: «External SA {X} from {Guest Org Y} loses visibility to unit {Z} after pool mode flip. If commission cycle ongoing — admin must manually assign unit to {Guest Org Y} OR handover to internal team.»
Rejected:
- Variant B: auto-assign unit to guest org based on Reserved state — security risk (implicit access escalation).
- Variant C: sticky access exception — усложняет permission model.
2.10.G — Universal status preservation
- Flip pool mode никогда не меняет unit statuses. Только visibility/permission меняется по § 4.4 rules.
2.11 — Microsoft OAuth resolution
ADR 0005 v3 (2026-05-09, CONV-31) wins — более свежий + authoritative.
2.11.A — Authoritative Stage 1 auth methods
- Google OAuth — primary, рекомендуемый CTA.
- Email + password — equal-weight fallback (NIST-compliant per ADR 0005 v3).
2.11.B — Tier 3 Stage 2 SSO
- Microsoft (Azure AD / Entra ID) OAuth2/OIDC.
- Custom SSO (SAML 2.0 / OIDC через Okta / Entra ID / generic IdP).
- Trigger: Stage 2 SSO build starts после первого Tier 3 customer commitment (revenue trigger).
2.11.C — Patches применённые во время Part 2 commit
- Foundational §2 lines 745-753 в
docs/rendered/launch-plan-stage-1.html— section retitled «Auth methods — Google + Microsoft + email/password (5)» → «Auth methods — Google + email/password (4)». Удалён bullet про Microsoft (line 748). Footer note (line 753) уже корректно ссылается на ADR 0005 v3. - Part 1 § 1.2.D table — Microsoft OAuth row: «Этап = Stage 1» → «Stage 2 (Tier 3 gate)». ⚠️ warning «Part 2.X — несоответствие» удалён. Custom SSO row formatted parallel.
- Phase 1.3.11 line 1644 open question «Какие SSO обязательны на старте?» — resolved per ADR 0005 v3.
- Phase 1.3.11 line 1685 open question «Studio-level SSO (Google Organisation / Microsoft)» — resolved per ADR 0005 v3.
Risks & Edge Cases (RATIFIED CONV-33)
Architectural risks
- Tenant isolation at MCP tool-call level — per Learning «MCP wrapper prerequisites» (2026-04-27). MCP wrapper (Phase 1.5.6) must scope auth at
org_idtool-call level, не connection level. Без этого — cross-tenant data leak risk. (v4.18 SPEC-AMEND — Security HIGH 5): Phase 1.5.6 implementation is blocked до ratification ADR 0014 «MCP wrapper auth + prompt-injection sanitisation model» (placeholder ADR shell ratified CONV-34; spec deferred к sub-plan / dedicated /plan session). Implementation gate enforced through workstreamphase-1-3-implementationtask 1.3.2 (multi-tenant scoping) — Roma scaffold may includeorg_idpropagation interface, but actual MCP wrapper code blocked до ADR. - Audit log table size growth — per ADR 0004 v3 (12 months active + archive до 7yr Cyprus statutory). Estimated volume: ~50-200 events/day per Org × 100 Orgs × 365 = 1.8-7.3M rows/year. Postgres partition by month recommended (Phase 1.3.x implementation choice).
- Audit log immutability vs disaster recovery — insert-only DB role + monthly hash-chain seal (§ 2.4.C v4.18 amendment) means schema migrations on
audit_eventsrequire coordinatedaudit_adminrole escalation + chain re-seal. Phase 1.3.x implementation must document this в DBA runbook. - home_org_id immutability constraint — если Owner self-leave его home_org заблокирован (Part 1 § 1.5.D); если ownership transfer и new Owner был member home_org → его home_org остаётся unchanged. Edge case: Client уволен из home_org but остаётся в N other Orgs →
home_org_orphaned: trueflag (Part 2 § 2.9.C). - Referral cycle detection performance — на каждый new Organisation creation требуется reverse-chain lookup в
referralstable. Index on(sponsor_id, referee_id)— must для < 100ms latency. - Pseudonymisation vault key custody (v4.18 SPEC-AMEND — Legal HIGH 2 derived) —
global_pepperдля actor_user_id HMAC must be stored в KMS (AWS KMS / Cloudflare Secrets) с rotation policy. Lost pepper = lost ability to de-anonymise → operator fraud-investigation path broken. Documented in Phase 1.7 security checklist.
Cross-stage dependencies
- Stage 2 Microsoft / Custom SSO — provider abstraction interface (ADR 0005 v3) must быть встроен на Stage 1. Phase 1.3.3 implementation choice: SSO provider plugin architecture.
- Stage 2 referral payouts — Stage 1 хранит data model only; payout logic в Phase 4.2. Schema extensible:
payout_amount,payout_statusfields ready. - Stage 2 audit log full-text search + CSV export — schema extensible: JSONB
metadatacolumn allows future indexing.
From Business Review
Process: Step 4.4 ratification — 6 parallel concern agents (Studios / Sales motion / Customer Success / Legal+Compliance / Security / Finance+Billing) reviewed full sub-plan на 2026-05-11. Consolidated finding counts: 21 HIGH · 29 MED · 15 LOW.
Treatment policy: HIGH findings that mark behavioural-spec gaps → Phase 1.3 implementation gates (Roma must address before scaffold ships); high findings flagged as (SPEC-AMEND) trigger Part 1 / Part 2 amendments в CONV-34 (separate session). MED → documented для post-launch follow-up. LOW → deferred (Stage 2 backlog).
HIGH findings (21)
Studios
- (SPEC-AMEND) Cascade-up on SA removal silently dumps stock onto SM/Admin/Owner с no triage UI (§1.5.E). Add «Cascade triage view» requirement; batch email digest вместо per-unit flood.
- (SPEC-AMEND) Role-change-to-SA wipes all stock assignments без undo / preview (§1.5.B). Add hard confirmation modal + pre-change snapshot в audit JSONB для manual restore.
- (SPEC-AMEND) Project switcher auto-redirects SA away when last assigned unit lost — fires constantly во время rebalancing. Replace forced redirect с non-jarring banner OR grace-period read-only access.
Sales motion
- (SPEC-AMEND) Reverse-invite token TTL 7d fatal для B2B Developer evaluation cycles (2-4 weeks typical). Extend reverse-invite token to 30d OR auto-renew on Studio activity OR revocation-only model. Affects §1.4.C + §1.4.D.
- (SPEC-AMEND) No reverse-invite funnel visibility для Studio (sent / opened / signed-up / Free Guest / upgraded). Add «Invited Developers» surface в Project Settings → Guest Organisations.
- (SPEC-AMEND) Referral attribution dies on existing-account signup. Bind sponsor to Organisation
created_at, не usersignup_at. Affects §2.3 pick 5. - (SPEC-AMEND) Ownership-transfer self-serve gate broken: tied to actor's payment state, должен быть к target Organisation's first-payment state. Affects §1.5.F.
Customer Success
- (SPEC-AMEND) Generic «не получилось войти» (§1.5.C.5) + generic wrong-Org login error (§1.2 edge cases) blind CS — 30-40% «can't login» tickets unresolvable without DB access. Operator dashboard (Phase 1.4) must surface per-email lookup screen.
- (SPEC-AMEND) No 2FA-loss recovery flow specified (§1.2.F). Add backup codes generated on 2FA enable + operator-mediated reset через Phase 1.4.x.
- (SPEC-AMEND) Ownership transfer post-payment routes to
support@(§1.5.F) без SLA / escalation / identity-verification protocol. Define checklist + target 2-day SLA + pull operator-mediated transfer из Stage 4 into Phase 1.4.
Legal + Compliance
- (SPEC-AMEND) Buyer-records data-controller ambiguity при multi-party access (§1.5.C/D + §1.6 + §2.10.F). Joint-controller arrangement requires GDPR Art. 26 agreement. Defer detailed spec to
plans/legal-multi-party-framework.md; mark gate в этом sub-plan. - (SPEC-AMEND) Audit log indefinite archive vs GDPR Art. 5(1)(e) storage limitation + Art. 17 erasure. Anchor retention to documented legal basis + pseudonymise
actor_user_idpost-12mo. Affects ADR 0004 amendment + §2.4.C. - (SPEC-AMEND) Reverse-invite (§1.4.C) lacks legal basis для unsolicited Developer outreach + IP rights warranty. Add (a) Studio warranty-of-rights clause в ToS, (b) rate-limit + opt-out, (c) takedown flow. Owner: Legal sub-plan + ToS.
Security
- (SPEC-AMEND) Role-change does not force session/cookie re-eval («next action» reload, §1.5.B). Permission check must hit DB on every privileged action; on demotion revoke all server-side session records cross-subdomain. Aligns с CS HIGH.
- (SPEC-AMEND) Buyer token mechanics under-specified — 90d lifetime без rotation, binding rules, scope. Spec: ≥128-bit opaque server-side row, scope = single unit, rotate on SA removal или buyer reassign,
noindex+Referrer-Policy: no-referrerheaders. Affects §1.7 + Foundational §6. - (SPEC-AMEND) Audit log не declared immutable / tamper-evident — only «append-only» (§2.4.C). Add insert-only DB role + monthly hash-chain seal + off-box archive shipping + no UPDATE/DELETE grants. Affects ADR 0004 + §2.4.C.
- (SPEC-AMEND) Cross-tenant MCP wrapper leak risk — already в Architectural risks. Block Phase 1.5.6 build до ADR «MCP wrapper auth + sanitisation model» ratified.
Finance + Billing
- (SPEC-AMEND) Trial → Free Guest auto-downgrade — Stripe customer/subscription cleanup unspecified (§1.8.B). Define: Free Guest = no Stripe subscription object; Stripe customer record retained для re-pay restore. Phase 1.3 billing schema.
- (SPEC-AMEND) T2 → T1 downgrade — proration / refund policy missing (§1.8.F). Decide: end-of-period (no refund) OR Stripe proration credit. New ADR on proration policy.
- (SPEC-AMEND) Suspension + grace + chargeback timing conflict (§1.8.C vs ADR 0006). Define: chargeback auto-freeze overrides grace immediately; revenue recognition = cash basis Stage 1. Cross-link ADR 0006.
- (SPEC-AMEND) Referral payout retroactive eligibility unbounded (§2.3 pick 1) — sponsor upgrades 2yr later → all dormant referrals retroactively eligible. Cap look-back window (12 months from referee
upgraded_to_client_at).
MED findings (29) — full table
| Function | Finding | Suggested fix / owner |
|---|---|---|
| Studios | No bulk team invite Stage 1 (§1.4 CSV defer) — pain for studios с 15-30 agents | CS expectation: white-glove team-import для first 20 studios; defer acceptable |
| Studios | Owner self-leave blocked без in-product «request transfer» path | Add in-app «Request ownership transfer» form pre-filling support ticket |
| Studios | home_org_orphaned flag (§2.9.C) — no Studio-side remediation UI |
Spec orphan-Client UX; can они convert guest membership в new home_org? |
| Sales motion | Free Guest cannot reverse-invite (§1.4.C, §3 line 787) — blocks viral expansion | Allow Free Guest forward-invite; keep reverse-invite Tier 1+ |
| Sales motion | Trial = T1 only (§1.1 Path A) — T2/T3 conversion blind | T2/T3 demo sandbox OR tier-switch mid-trial |
| Sales motion | SA cascade silent on commission impact (§1.5.E) | Add «commission preserved to {departing SA}» line в cascade audit + notification |
| Sales motion | No referral CTA в Free Guest onboarding | Add post-Path-B welcome surface promoting referral after upgrade |
| CS | Invite-decline events log'ed но inviter не receives proactive notification (§1.1 Path C.2) | Email-to-inviter on decline + token expiry |
| CS | Email mismatch error «свяжитесь с тем, кто пригласил» не actionable (§1.1, §1.4.E, §2.6) | Show last-4 / masked hint of invited email (s***y@o***.online) |
| CS | Trial auto-downgrade + 30-day public-site freeze без warning emails (§1.1, §1.8.B) | Reminder schedule: T-7 / T-1 / day-of, then day-23 / day-29 freeze countdown |
| CS | Operator workflow «resend fresh invite» missing (§1.4 edge cases) | Operator dashboard one-click resend action |
| CS | Cookie corruption mid-action loses unsaved form state (§1.2) | Preserve form state в localStorage pre-redirect OR explicit «session expired, changes saved» |
| CS | Role-change does not force logout (§1.5.B) — UI shows new permissions only on next action | In-page toast «role updated, refresh to see changes» |
| Legal | home_org_orphaned no defined controller-of-record fallback |
Define fallback rule + surface в privacy notice |
| Legal | View-as-Agent не logged at buyer-PII access layer (§1.10.C) — GDPR Art. 30 gap | Add per-record view event when View-as touches buyer PII |
| Legal | Cross-jurisdiction subdomain hosting — no data-residency commitment / SCC | Add residency disclosure + SCCs в multi-party agreement |
| Legal | Referral table sponsor↔referee link retained indefinitely (§2.3) | Retention = lifetime of payout claim + statutory limitation (Cyprus = 6yr) |
| Legal | Trial auto-downgrade — buyer PII collected during trial остаётся indefinitely on unpaid Free Guest | Dormant-account purge schedule (24mo inactivity → notice → delete) |
| Security | Account enumeration on central login picker (§1.3.D) reveals Org-membership graph pre-auth | Render generic response on login submit; show picker post-password+2FA |
| Security | No anti-brute-force / lockout policy specified — only «aggregate» PIN attempts mentioned | Per-account exponential backoff after 5 fails; CAPTCHA threshold; PIN 5/15min then 1h lockout |
| Security | OAuth callback validation not specified — no state CSRF, PKCE, redirect_uri allowlist |
Mandate PKCE + signed state + strict redirect_uri allowlist; subdomain DNS lifecycle policy |
| Security | Password reset invalidates sessions но role-change не invalidates (inconsistent) | Align — role-change + member-removal invalidate all session records |
| Security | Single *.offplan.online cookie scope = blast radius on subdomain XSS |
Strict CSP per subdomain; cookieless assets.offplan.online; separate staff. cookie scope |
| Security | Invite token entropy / rate-limit unspecified | ≥128-bit CSPRNG, constant-time DB lookup via hash, rate-limit per IP, alert on >N invalid attempts |
| Security | 2FA optional Stage 1 для Owner с billing + transfer + downgrade access | Step-up auth для sensitive actions OR mandate 2FA at Owner role assignment |
| Finance | Ownership transfer pre-/post-payment boundary brittle для refund/chargeback edge cases | Define first_successful_payment_at; refunds/chargebacks не roll back self-serve eligibility |
| Finance | Free Guest Organisation cost accounting (storage / audit rows / API events) unaddressed | Add Free Guest cost telemetry to Org record |
| Finance | Tier upgrade Stripe proration unclear для trial-to-paid vs in-cycle | Trial conversion = full new-tier period (no proration); paid-to-paid = Stripe default |
| Finance | Reverse-invite ownership transfer creates billing-handover trial-clock ambiguity (§1.5.F + §1.4.C) | Specify: transfer resets billing clock; new Owner adds card + new trial OR continues remaining days |
LOW findings (15) — deferred Stage 2
- View-as-Agent читается-only (§1.10.C) — Studios expect impersonation for training; CS docs note (no code change).
- SA self-leave buyer-record inheritance silent (§1.5.D); confirm SM inheritance via cascade rules в Part 1 §1.5.E.
- Referral
?ref=vs form-field conflict silent to user — show inline notice при conflict. - Reverse-invite email copy «you can claim as your own» ambiguous — reword pre-Stripe expectation.
- 2FA only mandatory Stage 2 — risk-accept memo by Legal.
- Audit-log immutability — WORM/hash-chain explicit Stage 2 enhancement note в ADR 0004.
- View-as duration retention — confirm feeds §2.4.A category + transparency для viewed SA per GDPR.
- Reserved subdomain list misses phishing vectors:
claude,vv,verify,secure,account,update,me,id. Add Stage 1. ?ref=URL attacker-controllable hijack — fraud-score signal в Stage 2.- Audit log JSONB metadata PII classification — redact on archival shipping в Stage 2.
- Reverse-flip email cascade fallback when SM missing (§1.7.D) — document explicit fallback to Admin/Owner.
- Referrals Free Guest screen «upgrade to refer» FAQ entry — pre-empt CS confusion.
- «Why can't I leave?» inline help link на Owner self-leave block (§1.5.D).
- Audit archive cost-cap policy (cold storage S3 Glacier) — Stage 2 ops budget.
- Tax/VAT MOSS dependency на Cyprus entity finalisation — flag для Phase 1.3 billing checklist.
Treatment plan
- 21 HIGH (SPEC-AMEND) → CLOSED CONV-34 via v4.17 + v4.18 patches.
- v4.17 — Theme 1 (Token mechanics hardening): Sales 1 (Reverse TTL 7d→30d, § 1.4.A + 1.4.C); Sales 2 (Reverse-invite funnel visibility, § 1.4.C); Security 2 (Buyer token mechanics, new § 1.7.J); Security 4 (Invite token entropy + rate-limit, § 1.4.D); Legal 3 (Reverse-invite legal basis stub + gate, § 1.4.C).
- v4.17 — Theme 2 (Session/permission re-eval policy): Security 1 + CS HIGH 3 (Role-change session re-eval, § 1.5.B); Sales 4 (Ownership transfer target Org gate, § 1.5.F); Studios 1 (Cascade triage view, § 1.5.E); Studios 2 (Stock-wipe modal + audit snapshot, § 1.5.B); Studios 3 (SA project-switcher non-jarring banner, § 1.3 Edge cases).
- v4.18 — Theme 3 (Audit immutability + GDPR retention): Security 3 (Audit immutability, § 2.4.C + ADR 0004 v2); Legal 2 (GDPR retention + pseudonymisation, § 2.4.C + ADR 0004 v2); Security 5 (MCP wrapper auth block, Architectural risks + ADR 0014 placeholder); Legal 1 (Buyer joint-controller annotation, § 2.4.C + gate to legal sub-plan).
- v4.18 — Theme 4 (Ownership transfer + billing precision): Finance 1 (Stripe customer cleanup, § 1.8.B); Finance 2 (T2→T1 proration, § 1.8.F + ADR 0013); Finance 3 (Chargeback overrides grace + cash-basis recognition, § 1.8.C); Finance 4 (Referral 12mo cap, § 2.3 pick 1); Sales 3 (Referral attribution Organisation-bound, § 2.3 pick 5); CS 3 (Ownership transfer SLA + identity-verification, § 1.5.F); CS 1 (Operator dashboard per-email lookup, § 1.5.C); CS 2 (2FA-loss recovery flow, § 1.2.F).
- 29 MED → documented; reviewed during
phase-1-3-implementationworkstream kickoff; non-blocking для Stage 1 launch. - 15 LOW → Stage 2 backlog; some (subdomain phishing vector additions) cheap enough для Stage 1 cleanup pass.
From Learnings DB
- «MCP wrapper prerequisites: tenant isolation + prompt-injection sanitisation» (Learning 2026-04-27) [architecture, security, vendor, tooling] — applies to Phase 1.3.x → 1.5.6 boundary. Tenant isolation must operate at
org_idtool-call level. Prompt-injection sanitisation для API responses returned via MCP (project names, unit descriptions могут содержать user-controlled content). - «Multi-level access: 5-tier model для B2B2C white-label SaaS» (Learning 2026-04-24) [architecture, domain] — STALE. Superseded by current flat-5-role model (Owner / Admin / SM / CE / SA) + guest organisations pattern. Deprecation note pending в Notion (carried over CONV-31 / CONV-32 / CONV-33).
- «Invite клиента уже есть в админке» (Learning 2026-04-22) [domain, architecture] — VV legacy fact; superseded by new cross-entity invitation model (Part 1 § 1.4). Deprecation note pending в Notion.
- «Платёжные провайдеры: Stripe / Paddle / Checkout.com для кипрской компании» (Learning 2026-04-29) [billing, vendor, domain, architecture] — applies to Phase 1.3 billing schema. Confirms Finance review HIGH: PaymentProvider interface with
createSubscription/cancelSubscription/updatePlan/getInvoices/createPaymentLinkmethods needed day 1. Stage 1 = StripeProvider; PaddleProvider + CheckoutProvider added Stage 2/3 by volume threshold. Org schema must carrypayment_provider+external_customer_idcolumns from Phase 1.3.11 (Signup & tenant data model). Free Guest tier → no Stripe subscription object; Stripe customer record preserved для re-pay restore.
Workstreams (RATIFIED CONV-33)
| Name | Priority | Depends On | Tags | Tasks |
|---|---|---|---|---|
phase-1-3-implementation |
P0 | — | architecture, ux, security, domain | Все 12 numbered Phase 1.3.x tasks в docs/rendered/launch-plan-stage-1.html (1.3.1-1.3.12); реализация behavioural spec Part 1 + decision log Part 2 |
Evaluation
| Field | Description |
|---|---|
| Artefact | Phase 1.3.x scaffold (Roma's code); Roma + Ilya self-report «понимает все user-facing flows + edge cases без новых вопросов» |
| 2-min check | Sergey + Ilya открывают plans/permission-and-tenancy-model.md, scan headings — все 21 раздел (10 Part 1 + 11 Part 2) присутствуют, status: ratified, audit-log table заполнена, edge cases для каждой section покрыты |
| Agent self-check | Все 11 пунктов Part 2 имеют либо cross-ref close, либо real design с ratified picks. Все cross-refs (Foundational §X line Y / ADR NNNN / Part 1 § N.M) проверены grep'ом. Microsoft OAuth Foundational §2 patches применены |
Changelog
- 2026-05-09 (CONV-32) — Part 1 (1.1-1.10) ratified. Status: stub → draft. New decisions: magic link verification, single-use decline tokens, Sales Agent project switcher visibility (≥1 assigned unit), forward invite Owner/Admin only, re-invite auto-revoke + supersede, self-invite blocked, role change SA-involving = clear assignments, badge «External» SA, tier downgrade explicit blocking, pessimistic lock 500ms timeout fall-through.
- 2026-05-11 (CONV-33) — Part 2 (Decision Log, 2.1-2.11) ratified pack-mode. Status: draft → ratified. 6 new picks Part 2.3 (referral credit dual condition, lazy code generation, conflicting referral params resolution, base32 6-char format, Free Guest UI motivation pattern, dual-condition payout eligibility); full audit log scope Phase 1.3 events (9 categories × 5 roles visibility matrix); subdomain reservation list (58 entries в 5 categories); Free Guest → paid conversion split (preserved vs activated, dual trigger); home_org_id formalisation для dual-Org membership; pool mode reversibility (Variant A — visibility loss preserves status). Microsoft OAuth inconsistency resolved per ADR 0005 v3 — Foundational §2 patched, Part 1 § 1.2.D updated. Workstream
phase-1-3-implementation(P0) created. Risks section populated с Learnings DB sweep findings (MCP tenant isolation tool-call level scoping). - 2026-05-11 (CONV-34, v4.17 SPEC-AMEND patches — Themes 1 + 2 = 10 HIGH closed) — Token mechanics hardening + Session/permission re-eval policy. Status:
ratified(unchanged — patches, не re-ratification). § 1.4.A reverse-invite TTL 7d → 30d. § 1.4.C funnel visibility surface («Invited Developers» tab) + legal basis stub (rate-limit, ToS warranty, abuse takedown route) + gate к legal sub-plan. § 1.4.D token mechanics expanded: ≥128-bit CSPRNG entropy, hashed storage, constant-time hash compare, per-IP rate-limit (10/5min soft, 30/h hard), per-email lookup cap (5/15min), burst alert on >50 invalid/10min. § 1.7.J (new) buyer tokenised URL mechanics: ≥128-bit opaque + (buyer, unit) single-pair scope + forced rotation on SA loss / reattribution +X-Robots-Tag noindex+Referrer-Policy no-referrer+Content-Security-Policy frame-ancestors none. § 1.5.B server-side session revocation cross-subdomain on role-change + per-action DB permission check (no JWT/cookie caching of role) + stock-wipe pre-change confirmation modal + audit JSONB snapshot для restore. § 1.5.E cascade triage view requirement (batch digest email vs per-unit flood) + commission preservation note in audit. § 1.5.F ownership transfer self-serve gate tied кorganisations.first_successful_payment_at(target Org, не actor); operator-mediated post-payment flow + 2-day SLA + fraud-signal identity-verification checklist + 30d cooldown. § 1.3 Edge cases SA project-switcher: non-jarring banner + 24h read-only grace вместо forced redirect. - 2026-05-11 (CONV-34, v4.18 SPEC-AMEND patches — Themes 3 + 4 = 11 HIGH closed) — Audit immutability + GDPR retention + Ownership transfer billing precision. Status:
ratified(unchanged). § 2.4.C insert-onlyaudit_writerDB role + monthly hash-chain seal + off-box S3 Object Lock archive + boot-time grant check +pii_classcolumn + JSONB metadata key registry; pseudonymisation на 12mo boundary (HMACactor_user_idс vault DB mapping); buyer joint-controllercontroller_org_idannotation (gate к legal sub-plan). ADR 0004 v2 retention amended: 12mo active + 7yr archive (Cyprus statutory) + pseudonymisation + insert-only role + monthly hash-chain seal + KMS pepper custody. ADR 0014 (new, placeholder shell) MCP wrapper auth + prompt-injection sanitisation — Phase 1.5.6 implementation gate; spec deferred to dedicated /plan session. Architectural risks expanded: MCP wrapper block; audit DR runbook; pseudonymisation key custody. § 1.8.B Stripe customer object retention + subscription cancellation on Trial → Free Guest + failed-card edge case. § 1.8.C chargeback overrides 14d grace immediately (ADR 0006 wins) + cash-basis revenue recognition Stage 1. § 1.8.F proration policy: upgrade prorated, downgrade end-of-period (no refund); ADR 0013 anchor. ADR 0013 (new) Tier change proration policy — asymmetric (upgrade prorated, downgrade end-of-period); rationale + alternatives. § 2.3 pick 1 referral payout retroactive eligibility cap 12mo from referee upgrade. § 2.3 pick 5 referral attribution bound к Organisationcreated_at(NOT usersignup_at) — existing-user new-Org signup attaches sponsor properly. § 1.2.F 2FA-loss recovery: 10 backup codes on enable + self-service recovery flow + operator-mediateddisable_2fa_operator_override+ rate-limit. § 1.5.C operator dashboard per-email lookup cross-link (tenant-facing message stays generic; operator-facing actionable). Treatment plan section flipped from «pending» к «closed CONV-34».