offplan · online
Plan · stage1

Stage 1 — Wave 2 Chunk 6: §7 Edge cases

Approvedplanstage1priority P0
Ratified
2026-05-08
Created
2026-05-08
Priority
P0
Tags
ux, architecture, domain

Goal

Закрыть Wave 2 Chunk 6 — sweep по §7 Edge cases (Foundational Decisions раздел <div id="fd-edges">). Добавить 10 архитектурных bullets в 3 из 5 subsections (§7.1, §7.4, §7.5) — все decisions locked в CONV-27 interview. §7.2 GDPR / ADGM right-to-erasure + §7.3 Multi-jurisdiction compliance помечены как parking-lot до jurisdiction lock + подключения юриста + активации sub-plan plans/legal-multi-party-framework.md. Existing 5 subsections не переписываются — только additions / amendments.

Locked Decisions (CONV-27 interview)

§7.1 — User deactivation (4 additions)

# Тема Решение
A Buyer-records SA при deactivation Buyer-records остаются в DB, attribution к Sales Agent'у preserved (commission через handoff per CONV-22 attribution rules). Link voiding только через GDPR erasure (см. §7.2).
B (revised) Deactivated user UX Деактивированный пользователь = анонимный посетитель. Логин fail'ится generic'ом («не получилось войти», без указания причины — security: не leak'аем «kicked by colleague»); прямые ссылки на проекты работают по правилам Public Visibility per §5 (Private 404, Discovery limited, Full sales full, PIN-protected gate). Никаких спецбаннеров про suspension.
C Self-leave vs Owner-removes Self-leave (Settings → Leave Organisation): confirmation modal с consequences (lose access to N projects, M assigned units cascade up to SM/Admin per §4.4). Same downstream flow что Owner-removes — разница только в trigger / actor в audit log. Owner self-leave disabled — single Owner per Organisation, требуется ownership transfer first per §3.
D (amend) Re-invite no auto-restore Amend existing bullet 4: при re-invite same email → fresh membership; assignments / buyer-records НЕ auto-restore (manual reassignment by SM/Admin). Защита от accidental restore'ов после suspicious deactivation'ов.

§7.4 — Conflict resolution (3 additions; «Owner erasure» skipped)

# Тема Решение
A Race condition status-change в Open pool First-click-wins. Два Sales Agent'а одновременно жмут «Mark as Reserved» на same unit (Open pool — оба видят и могут продать). Победитель — первый запрос дошедший до server. Loser получает modal «Только что зарезервировал Алексей в 14:23 — обнови страницу». Audit log пишет обе попытки + winner. Critical после CONV-26 §6.1 verification form (раньше формы не было — race condition новая).
B Suspension Organisation с active buyer-tokens Tokens продолжают работать read-only до 90-day expiry. CTA «Связаться со мной» disabled с tooltip «Project temporarily unavailable, contact your Sales Agent directly». Полный block backfire'ит на buyer experience и сорвёт active deal flow.
C Owner erasure request Skipped — defer to GDPR sub-plan (parking-lot §7.2).
D Visibility flip с active tokens Buyer-tokens = pre-auth'ed visitor, bypass'ят Visibility setting (consistent с §5.1 PIN bypass + §6 token mechanics). Visibility flip применяется только к новым anonymous посетителям; existing token holders работают по своему правилу до 90d expiry.

§7.5 — Referral architecture (3 additions, all schema-level)

# Тема Решение
A Multi-Organisation referee Только первая Organisation Client'а с заполненным referred_by_user_id триггерит payout. Schema: Organisation.referred_by_user_id (nullable, set один раз при create). Защита от gaming: Client создал 5 Organisations через одну signup → payout × 1, не × 5.
B Cycle prevention Self-ref (sponsor_id == new_organisation.owner_id) или reverse-ref (sponsor_id ранее уже был referee этого owner'а) → запись в referrals создаётся с payout_status: ineligible_cycle. Audit запись остаётся для visibility / detection. Не блокируем insert hard — храним для transparency.
C (expanded) Sponsor visibility screen Detailed spec — см. ниже.

§7.5 C — Sponsor visibility detailed spec

Расположение: Settings → Referrals в Client'овой админке.

Top block (всегда виден):

Таблица referee Organisations (sort: signup_at desc):

Колонка Что показано
Organisation Anonymized identifier Org #abc12 (5-character hash от Organisation id)
Зарегистрирован дата signup
Текущий статус Free Guest / Trial / Paid Tier 1 / Paid Tier 2
Апгрейд дата первого upgrade на paid tier (или «—»)
Payout pending / paid / ineligible_cycle / refunded

Что НЕ показываем (privacy):

Stage 1 не в scope:

Cross-link: Phase 4.2 — payout-механика; §3 Billing — credit mechanics.

Parking-lot (deferred subsections)

§7.2 + §7.3 — DEFERRED до выполнения 3 условий:

  1. Jurisdiction lock — memory project_legal_entity_cyprus.md говорит «Cyprus decided», но user в CONV-27 сказал «есть вопрос где будет компания открываться» (под review). Нужна Roman ratification.
  2. Юрист подключён (Cyprus / EU / UAE — зависит от jurisdiction lock'а).
  3. Sub-plan активированplans/legal-multi-party-framework.md (status: stub) запущен отдельной /plan-сессией.

Visible parking-lot callout добавляется в начало §7 (после <h3> lead) — указывает что §7.2 + §7.3 не должны редактироваться до выполнения трёх условий.

Approach

Approach A — Surgical inline additions (pattern Chunks 3-5 для small additions, без новых HTML anchor'ов).

Без новых <h4 id="fd-edges-X"> anchor'ов — §7 edge cases пока ниоткуда не cross-link'ятся (если в будущих Chunks 7-8 phase callouts начнут на них ссылаться — добавим anchor'ы тогда).

Steps

A. Parking-lot callout в начале §7

A1. Insert после existing <h3> (line 1096), перед first <details> (line 1098).

<div style="margin:14px 0; padding:12px 16px; background:var(--sand-50); border:1px solid var(--gold); border-left:3px solid var(--gold); border-radius:0 6px 6px 0; font-size:13px; color:var(--text-mid);">
  <strong style="color:var(--navy);">⏸ Parking-lot (CONV-27):</strong> §7.2 GDPR / ADGM right-to-erasure + §7.3 Multi-jurisdiction compliance — <strong>DEFERRED</strong> до (1) lock'а jurisdiction (Cyprus default по memory, под review awaiting Roman ratification); (2) подключения юриста (Cyprus / EU / UAE); (3) активации sub-plan'а <code>plans/legal-multi-party-framework.md</code>. До этого момента не править. Архитектурные edge cases (§7.1 deactivation / §7.4 conflicts / §7.5 referrals) — closed in Chunk 6.
</div>

B. §7.1 — 4 additions

B1. В §7.1 <details> block — amend existing bullet 4 (line 1104) и append 3 new bullets перед closing </ul> (line 1105).

B1.1 — amend bullet 4 (D — re-invite no auto-restore):

B1.2 — A bullet (Buyer-records persist):

<li><strong>Buyer-records деактивированного Sales Agent'а:</strong> остаются в DB, attribution к SA preserved (commission через handoff к другому SA per CONV-22 attribution rules). Link не воидится — удаление только через GDPR erasure (см. <a href="#fd-edges">§7.2</a>).</li>

B1.3 — B revised bullet (Deactivated as anonymous):

<li><strong>Login attempt после deactivation:</strong> деактивированный = анонимный посетитель. Логин fail'ится generic'ом («не получилось войти», без причины — security: не leak'аем «kicked by colleague»). Прямые ссылки на проекты работают по правилам Public Visibility per <a href="#fd-visibility">§5</a> (Private = 404, Discovery = limited, Full sales = full, PIN-protected = PIN gate). Никаких спецбаннеров про suspension.</li>

B1.4 — C bullet (Self-leave vs Owner-removes):

<li><strong>Self-leave (user-initiated):</strong> Settings → Leave Organisation — confirmation modal с consequences (lose access to N projects, M assigned units cascade up to SM/Admin per <a href="#fd-access">§4.4</a>). Same downstream flow что Owner-removes — разница только в trigger / actor в audit log. <strong>Owner self-leave disabled</strong> — single Owner per Organisation, требуется ownership transfer first per <a href="#fd-billing-transfer">§3</a>.</li>

C. §7.4 — 3 additions

C1. В §7.4 <details> block — append 3 new bullets перед closing </ul> (line 1135).

C1.1 — A (Race condition Open pool):

<li><strong>Race condition при Mark as Reserved (Open pool):</strong> два Sales Agent'а одновременно жмут «Mark as Reserved» на тот же unit (Open pool — оба видят и могут). <strong>First-click-wins:</strong> победитель — первый запрос дошедший до server. Loser получает modal «Только что зарезервировал <em>Алексей</em> в 14:23 — обнови страницу». Audit log записывает обе попытки + winner. Появилось после CONV-26 §6.1 verification form — раньше status-change формы не было.</li>

C1.2 — B (Org suspension active tokens):

<li><strong>Suspension Organisation с активными buyer-tokens:</strong> Organisation заморожена (неуплата / chargeback) — tokens продолжают работать <strong>read-only до 90d expiry</strong>. CTA «Связаться со мной» disabled с tooltip «Project temporarily unavailable, contact your Sales Agent directly». Полный block ударил бы по buyer experience и сорвал active deal flow.</li>

C1.3 — D (Visibility flip token bypass):

<li><strong>Visibility setting flip с активными tokens:</strong> Owner меняет Public Visibility (Discovery → Private или PIN-protected → Full sales) пока есть active buyer-tokens. Tokens = pre-auth'ed visitor, <strong>bypass'ят Visibility setting</strong> (consistent с <a href="#fd-visibility-pin">§5.1 PIN bypass</a> + <a href="#fd-buyer">§6 token mechanics</a>). Новый Visibility применяется только к новым anonymous посетителям; existing token holders работают до 90d expiry по своему правилу.</li>

D. §7.5 — 3 additions (включая expanded C с table)

D1. В §7.5 <details> block — append 3 new bullets перед closing </ul> (line 1145).

D1.1 — A (Multi-Org referee):

<li><strong>Multi-Organisation referee:</strong> Client создал несколько Organisations через одну signup-сессию (с <code>?ref=</code>). Только <strong>первая Organisation</strong> с заполненным <code>referred_by_user_id</code> триггерит payout. Schema: <code>Organisation.referred_by_user_id</code> (nullable, set один раз при create). Защита от gaming: создал 5 Org'ов → payout × 1, не × 5.</li>

D1.2 — B (Cycle prevention):

<li><strong>Cycle prevention:</strong> self-ref (<code>sponsor_id == new_organisation.owner_id</code>) или reverse-ref (sponsor_id ранее уже был referee этого owner'а) → запись в <code>referrals</code> создаётся с <code>payout_status: ineligible_cycle</code>. Audit запись остаётся для visibility / detection — не блокируем insert hard.</li>

D1.3 — C expanded (Sponsor visibility screen) — <li> с inline <table> + sub-blocks:

<li><strong>Sponsor visibility:</strong> отдельный экран Settings → Referrals в Client'овой админке.
  <div style="margin:8px 0; padding:10px 14px; background:var(--sand-50); border-left:3px solid var(--gold); border-radius:0 6px 6px 0; font-size:13px;">
    <strong style="color:var(--navy);">Top block:</strong> свой ref-код + готовая URL <code>https://app.offplan.online/?ref=&lt;code&gt;</code> + кнопка «Скопировать ссылку». Агрегированные метрики «приглашено: N · апгрейднулись: M · в платном статусе: K».
  </div>
  <div style="margin:8px 0; padding:10px 14px; background:#fff; border:1px solid var(--border); border-radius:6px; font-size:13px;">
    <strong style="color:var(--navy);">Таблица referees</strong> (sort: signup_at desc):
    <table style="margin:6px 0 0; font-size:12.5px;">
      <thead><tr><th style="width:140px;">Колонка</th><th>Что показано</th></tr></thead>
      <tbody>
        <tr><td>Organisation</td><td>Anonymized <code>Org #abc12</code> (5-char hash от Organisation id)</td></tr>
        <tr><td>Зарегистрирован</td><td>дата signup</td></tr>
        <tr><td>Текущий статус</td><td>Free Guest / Trial / Paid Tier 1 / Paid Tier 2</td></tr>
        <tr><td>Апгрейд</td><td>дата первого upgrade на paid tier (или «—»)</td></tr>
        <tr><td>Payout</td><td><code>pending</code> / <code>paid</code> / <code>ineligible_cycle</code> / <code>refunded</code></td></tr>
      </tbody>
    </table>
  </div>
  <div style="margin:8px 0; padding:10px 14px; background:#fff; border:1px solid var(--border); border-radius:6px; font-size:13px;">
    <strong style="color:var(--navy);">НЕ показываем</strong> (privacy): email / имя Owner'а referee, имя Organisation / subdomain, проекты / юниты / metrics, activity log.
  </div>
  <div style="margin:8px 0 0; padding:10px 14px; background:#fff; border:1px solid var(--border); border-radius:6px; font-size:13px;">
    <strong style="color:var(--navy);">Stage 1 не в scope:</strong> генератор invite-ссылок (sponsor копирует URL вручную из top block); ручной revocation / edit <code>payout_status</code>; pagination (Stage 2 если &gt;50 referrals). Payout-логика → <a href="#fd-billing">§3 Billing</a> + Phase 4.2.
  </div>
</li>

E. Phase 1.4 callout — operator visibility for new edges

E1. Append v4.10 <li> к existing Phase 1.4 callout <ul> (после v4.9 entry на line 1707).

<li><em>v4.10 (CONV-27):</em> <strong>Operator visibility для новых edge cases (§7.1 + §7.4 + §7.5).</strong> Operator dashboard читает audit log entries для self-leave events (отдельный actor в log: «User X self-left Organisation Y at TIMESTAMP» vs «Owner removed user X»). Suspension state для buyer-tokens — operator видит «Project P has N active read-only tokens» в Project drawer (after Org suspension). Visibility-flip с active tokens — operator видит «Visibility changed from X to Y, M existing tokens still bypass» в Project log. Referrals: operator видит aggregate counter «N referrals в <code>ineligible_cycle</code> state» в operator overview (anti-fraud signal — детект cycle gaming через несколько Organisations). См. <a href="#fd-edges">§7</a>.</li>

F. Phase 1.10 callout — sales-app routing для новых edge cases

F1. Append v4.10 <li> к existing Phase 1.10 callout <ul> (после v4.9 entry на line 2844).

<li><em>v4.10 (CONV-27):</em> <strong>Routing для §7 edge cases.</strong> Status-change form сабмит в Open pool: server-side optimistic lock (compare-and-swap на <code>unit.status</code>) — если first-click пробежал, second-click получает 409 Conflict + frontend modal «Только что зарезервировал X в TIMESTAMP — обнови». Suspension Organisation: middleware проверяет <code>organisation.status</code> — если suspended, anonymous + token paths render content read-only (CTA disabled), authed paths block с suspension banner. Visibility flip с active tokens: middleware sequence (Phase 1.10 PIN gate routing extended) — buyer-token bypass'ит новую Visibility setting, anonymous посетители получают новый preset. См. <a href="#fd-edges">§7.4</a>.</li>

G. Changelog v4.10 entry

G1. Insert v4.10 entry поверх v4.9 в launch-plan-changelog.html (insert after <div class="wrap"> opening, перед v4.9 <div>).

Structure (full content):

H. Workstream update

H1. В workstreams/stage1-roman-integration.md:

I. Preview repo sync

I1. Sync 2 файла в ~/code/offplan-online/preview/plan/: launch-plan-stage-1.html + launch-plan-changelog.html. Commit + push.

Files

Dependencies

Testing

Risks

  1. Cyprus memory conflict с user'овским «under review»project_legal_entity_cyprus.md говорит «decided», но user в CONV-27 сказал «есть вопрос где будет компания открываться». Mitigate: parking-lot callout формулирует мягко («Cyprus default по memory, под review awaiting Roman ratification») — не противоречит ни одной интерпретации, /handoff flag'нет вопрос для clarification в next session.
  2. §7.5 C nested table может выглядеть тяжело внутри <details> block'а — sub-blocks внутри <li> внутри <ul> внутри <details>. Mitigate: pattern такой же как §6.2 channels (CONV-26) — там работало; styling consistent (sand-50 + border-left gold).
  3. Phase 1.4 + 1.10 v4.10 callouts могут дублировать v4.9 — все три (v4.8/4.9/4.10) затрагивают operator dashboard / sales-app routing. Mitigate: v4.10 фокусируется конкретно на §7 edges (self-leave audit, suspension token state, visibility-flip routing) — не дублирует v4.9 (status-flip / channel analytics).
  4. Parking-lot callout не активирует sub-plan автоматическиplans/legal-multi-party-framework.md остаётся status: stub, нужен trigger в next session. Mitigate: explicit пометка в workstream's What's Next + flag в /handoff'е.
  5. Cross-link #fd-edges для §7.2 reference в §7.1 A bullet — §7.2 не имеет отдельного anchor'а (только parent #fd-edges). Pointer работает (jump к началу §7), но не precise. Mitigate: acceptable pre Approach A (no new anchors); если Chunk 7+ Phase callouts начнут точечно cross-link'аться на §7.2 — добавим anchor'ы тогда.

Workstreams

Updates workstreams/stage1-roman-integration.md:

Не создаём новый workstream — pattern Chunks 3-5 (single feat commit + sync, no separate workstream).

Evaluation

Done when:

Definition of NOT done (deferred):

Open Questions

Q1 — Cyprus jurisdiction status (clarification needed)

Memory project_legal_entity_cyprus.md говорит «Cyprus decided (EU member, 12.5% corp tax, GDPR-native — no EU Rep needed)». User в CONV-27 сказал «есть вопрос где будет компания открываться» — значит decision re-opened или новая ambiguity появилась. Resolution: confirm в next session — Cyprus locked? Re-opened? Alternatives (ADGM / DIFC / DMCC / Mainland UAE) under consideration?

Q2 — Access expansion (parked)

В Roman call (CONV-26 или earlier? — не зафиксировано в plan) обсуждались decisions «расширить кому какие доступы надо дать». User отправил parking-lot до jurisdiction lock. Resolution: после jurisdiction lock — отдельная /plan сессия для Chunk 9 (Access expansion + legal alignment) или включить в Phase 1.9 sub-plan.

Q3 — §7.5 C tabле sorting / pagination

Stage 1: sort signup_at desc, no pagination. Если sponsor накопит >50 referrals → pagination нужна. Stage 2 enhancement.

Q4 — Race condition implementation detail

§7.4 A — first-click-wins via optimistic lock (compare-and-swap на unit.status). HTTP 409 для loser'а. Frontend modal triggered by 409 response. Detail: Phase 1.10 build implementation — не plan-level.