Перейти к содержимому

CrewsForge — Глобальный план поставки

Единственный источник истины о том, что уже сделано и что берётся следующим. Основание: docs/crewsforge-architecture-session-2026-08-21/. Протокол работы: START-HERE.md, скилл crewsforge-session.

28 единиц работы, нарезанных по срезам продукта, а не по слоям кода. Единица = одна ветка = один PR (крупные занимают две сессии). Каждая единица, кроме двух первых, заканчивается работающими эндпоинтами, под которые можно идти верстать фронт.

Инфраструктура не имеет собственных единиц — она прикреплена к первому срезу, которому нужна: машина состояний строится вместе со своей первой реальной машиной, очередь задач — вместе с первой задачей, уведомления — вместе с первым уведомлением. Это не стилистика: механизм, построенный без потребителя, всегда оказывается не тем механизмом.

Каждая карточка отвечает на пять вопросов до того, как вы решите её брать: какую бизнес-задачу решаем · какие сущности появятся · что появится для фронта · чего ещё нельзя · чем доказывается, что работает. Точные пути и DTO рождаются в сессии и показываются вам в бизнес-брифе перед началом работы — карточка называет операции, а не замораживает пути.

Статус живёт только в карточке. В обзорной таблице его нет намеренно — двум местам нечем разойтись.

СтатусСмысл
TODOне начата, свободна к захвату (если все зависимости DONE)
IN_PROGRESSзахвачена сессией; не трогать
DONEреализована, ревью пройдено, PR открыт, документация обновлена
BLOCKEDупёрлась во внешний ответ; причина — в OPEN-QUESTIONS.md
Окно терминала
grep -n '^\*\*Статус:\*\*' docs/delivery/ROADMAP.md | grep -v DONE # следующая единица

Префиксы поверхностей (шаг 4 §8): фаундер — /api/v1/customers/me/..., команда — /api/v1/teams/:teamId/..., оператор — admin-api /api/v1/operator/..., администратор — admin-api /api/v1/administrator/... и billing-api /api/v1/..., арбитр — admin-api /api/v1/arbiter/..., вебхуки — без JWT, по подписи провайдера.


#IDЕдиницаФронтРазмерЗависит от
1F1Ядро: деньги, события, календарьM
2F2Целевая схема, сиды, платформенные ролиM
3T1Команды: состав, приглашения, полномочияMF2
4T2Машины состояний и событияLF1, T1
5O1Операционный контур: задачи и таймерыLT2
6O2УведомленияMO1
7T3Выплатной аккаунт командыMT2, O1
8T4Публичная карточка командыST1
9P1Проект и спецификацияMT2, O1
10P2Мэтчинг и офферMP1, T1, T3
11P3Контрактинг: план, торг, KYCLP2
12P4Договоры: шаблоны и генерацияMP3
13P5Подписание: конверты и вебхукиMP4
14M1Этапы, критерии, артефактыMP5
15M2Фондирование этапаLM1, T3
16M3Сдача, приёмка, операторская поверхностьLM2
17M4Расчёт выплаты и подтверждениеMM3
18M5Исполнение выплатыLM4
19M6Разбор непринятияMM3, M4
20D1Спор и арбитражLM5, M6
21D2РасторжениеLD1
22D3Внешние chargeback’иSM5
23S1КонсьержSM3, O2
24S2Ночная сверкаSM5
25S3ai-agent и заглушка трекераMP1, O1
26G1Кросс-QAMD2, D3, S1, S2, S3
27G2Переключение на baseline и смоукSG1
28G3Нагрузочное (k6)SG2

Вертикальная нарезка почти всё выстраивает в цепочку — это цена за то, что каждая единица что-то даёт. Вне основной цепочки параллелятся O2, T3, T4, D3, S1, S2, S3: их можно брать, когда основная линия упёрлась. T4 свободна прямо сейчас — её единственная зависимость (T1) закрыта.

graph LR
F1 --> T2
F2 --> T1 --> T2 --> O1 --> O2
T1 --> T4
O1 --> T3
O1 --> P1 --> P2 --> P3 --> P4 --> P5 --> M1 --> M2 --> M3 --> M4 --> M5
T1 --> P2
T3 --> P2
T3 --> M2
M3 --> M6 --> D1
M5 --> D1 --> D2
M5 --> D3
M5 --> S2
M3 --> S1
O2 --> S1
P1 --> S3
D2 --> G1
D3 --> G1
S1 --> G1
S2 --> G1
S3 --> G1
G1 --> G2 --> G3

Две единицы без фронтовой поверхности. Дальше таких не будет.

Статус: DONE Ветка / PR: feat/f1-shared-core-libs / #29 — смержен Размер: M · Зависит от: — · Фронт: нет

Бизнес-задача. Ни одной. Это три расчётные либы, без которых любая денежная сумма и любой срок в продукте будут считаться по-разному в разных местах.

Источники: шаг 5 §4, шаг 7 §1, шаг 7 §3; решения №05, №07, №30.

Что делаем.

  • money — минорные единицы (BigInt), fee = ceil(amount × commissionRateBps / 10000), team = amount − fee; сплиты по долям (teamShareBps, completedCriteriaBps): доля команды вниз, разница фаундеру, комиссия по X-3 от доли команды; остаток минорной единицы — платформе.
  • domain-events — типизированный каталог routing keys и типы payload.
  • platform-calendar — UTC, 10:00–18:00, пн–пт, без праздников; «N рабочих часов/дней».

Приёмка.

  • Сумма компонент сплита строго равна сумме холда на наборе, включающем нечётные минорные остатки и teamShareBps вида 3333.
  • Комиссия по ceil, не round: остаток всегда платформе.
  • «4 рабочих часа» от пятницы 17:00 UTC дают понедельник; «5 рабочих дней» не проскакивают выходные.
  • В payload доменного события нет денежных полей — типы запрещают структурно (Q-3 на шине).

Что получилось. Группа Nx-проектов libs/apis/core/: api-core-money, api-core-domain-events, api-core-platform-calendar — без единой зависимости, в том числе от NestJS. Приёмка доказана: инвариант сплита прогнан на 539 комбинациях, направление округления проверено мутацией (ceil → floor валит 230 случаев), гейт CatalogIsMoneyFree проверен поломкой. Лог: 1634 + 31 + 2 теста зелёные.

Единица прошла по прежнему протоколу (спека + план + посекционное ревью) — она закрылась до перекроя. Начиная с F2 действует скилл crewsforge-session.


F2 — Целевая схема, сиды, платформенные роли

Заголовок раздела «F2 — Целевая схема, сиды, платформенные роли»

Статус: DONE Ветка / PR: feat/f2-baseline-schema / #31 Размер: M · Зависит от: — · Фронт: нет

Бизнес-задача. Ни одной напрямую. Но после неё поднимается Swagger, применяется миграция и существуют три платформенные роли — то есть появляется площадка, на которой всё остальное можно щупать.

Сущности. Вся целевая схема шага 2 сразу — 32 модели. Оживают в последующих срезах; здесь создаются таблицы, констрейнты и сиды.

Что появится для фронта. Ничего вызываемого. Появляется Swagger каждого сервиса и возможность залогиниться администратором, оператором и арбитром из сидов.

Источники: шаг 8 целиком; шаг 2 целиком; дельты — шаг 6 §2 (templateVersion, documentHash), шаг 7 §5 (ConciergeThread, ConciergeMessage), шаг 7 §2 (частичный индекс дедупа QueueItem); шаг 4 §1, §8; решения №12, №13, №19, №31.

Что делаем. Squash миграций в одну baseline-миграцию с целевой схемой целиком. Raw-SQL: CHECK на Settlement через денормализованный hold_amount_minor, onDelete: Restrict на всей сделочной цепочке. Claim platformRole в admin-JWT, три guard’а, запрет совмещения ролей. Сиды: справочники, платформенные роли, данные разработки.

Дельты enum’ов, найденные единицей F1 (ревизия шага 2 §9 внесена в step-документ, подтверждена Stark’ом) — обязаны попасть в baseline:

  • TimerKind — семь значений, а не пять: добавлены ENVELOPE_EXPIRY и MILESTONE_DEADLINE (шаг 7 §3 описывает семь таймеров).
  • SubjectType — одиннадцать значений плюс CONCIERGE_THREAD: консьерж-тред публикует concierge.escalated, а OutboxEvent.aggregateType типизирован именно SubjectType.

Prisma-enum’ы обязаны совпасть значение в значение с union’ами @crewsforge-back/apis/core/domain-events: ActorType (3), SubjectType (11+1), TimerKind (7), WebhookProvider (3). Расхождение — молчаливый дефект на шине.

Приёмка.

  • prisma migrate reset на чистой БД даёт целевую схему; prisma validate зелёный.
  • Удаление проекта со сделочной цепочкой падает на уровне БД, не сервиса.
  • Settlement, чьи компоненты не суммируются в холд, отвергается БД.
  • Негативные тесты кросс-ролевого доступа: оператор → админский маршрут 403, арбитр → операторский 403, админ → арбитражный 403. Вторая платформенная роль пользователю не выдаётся.
  • Сверка enum’ов: тест сопоставляет значения Prisma-enum’ов со значениями union’ов domain-events — расхождение хотя бы в одном значении роняет прогон.
  • ArbiterGuard в billing-api не регистрируется (шаг 4 §6) — тест отсутствия маршрутов.
  • Чек-лист 8.3 пройден.

Прод эта единица не трогает. Дроп и накатка боевой БД — G2, в окне обслуживания.


T1 — Команды: состав, приглашения, полномочия

Заголовок раздела «T1 — Команды: состав, приглашения, полномочия»

Статус: DONE Ветка / PR: feat/t1-teams / #34 Размер: M · Зависит от: F2 · Фронт:

⚠️ Единица заведена сессией 0: домен команд спроектирован в шаге 2 §3 и присутствует в таблице задач шага 7 (TEAM_ONBOARDING), но эпика в шаге 9 не имел. Шаг 9 дополнен.

Бизнес-задача. Команда регистрируется на платформе, собирает состав и распределяет полномочия. Без этого некому предлагать проект, некому подписывать договор и некому сдавать работу — на домен команд опираются оффер, контрактинг, сдача и выплатной аккаунт.

Сущности. Team (статус, слаг, публичное описание, внешний опыт), TeamMember (две независимые оси: role — доступ, canSign/canSubmit — полномочия по 2.2 ФТ), TeamInvitation (токен, срок, статусы).

Что появится для фронта.

ПоверхностьОперации
Командасоздать команду · профиль команды: чтение и правка · список участников · пригласить по email · отозвать приглашение · изменить роль участника · выдать и снять canSign / canSubmit · убрать участника · выйти из команды
Employee (без команды)принять приглашение по токену · отклонить · список своих членств
Администраторсписок команд с фильтром по статусу · карточка команды · верифицировать (ONBOARDING → ACTIVE)

Что можно собрать на фронте после. Онбординг команды целиком: регистрация, экран состава, приглашения, матрица полномочий. Админский экран списка команд и верификации.

Чего ещё нельзя. Команда не может подключить выплаты (T3), не может получать офферы (P2). Смена статуса команды пока прямая, без журнала переходов — журнал придёт в T2.

Источники: шаг 2 §3; шаг 4 §7 (строка «Команда: состав, роли, полномочия»); шаг 7 §2 (строка TEAM_ONBOARDING); решение №08 (team — либы в user-api).

Что делаем. Либы team-*, монтируются в user-api (решение №08, выделение в сервис — после беты). Инвариант «последний ADMIN не может снять с себя роль и не может уйти» — service-level, единая точка изменения состава. Ушедший участник — status REMOVED, строка не удаляется (на членство ссылается история подписей и сдач). Мультичленство разрешено моделью.

Приёмка.

  • HTTP-e2e на реальной БД: создать команду → пригласить → принять приглашение → выдать canSign → снять роль ADMIN с себя при единственном админе (ожидается отказ) → убрать участника → проверить, что строка членства осталась со статусом REMOVED.
  • Оси независимы: участник с role: VIEWER и canSign: true существует и проходит валидацию.
  • Приглашение по истёкшему или уже использованному токену отвергается.
  • Чужой teamId в пути — 403, а не 404 с утечкой существования.
  • Swagger содержит все маршруты; примеры запроса и ответа приложены к PR.

Статус: IN_PROGRESS Ветка / PR: feat/t2-state-machines Размер: L (вероятно две сессии) · Зависит от: F1, T1 · Фронт:

Бизнес-задача. Администратор управляет жизненным циклом команды — верифицирует, приостанавливает, архивирует — и каждое такое действие оставляет неудаляемый след: кто, когда, на каком основании. Механизм, который это обеспечивает, здесь же строится и здесь же доказывается на первой настоящей машине.

Сущности. StateTransition (журнал переходов), OutboxEvent. Машина статуса команды: ONBOARDING → ACTIVE ⇄ SUSPENDED → ARCHIVED.

Что появится для фронта.

ПоверхностьОперации
Администраторверифицировать команду · приостановить с указанием причины · снять приостановку · архивировать · история переходов команды: кто, когда, основание
Командавидит свой статус и причину приостановки, если она есть

Что можно собрать на фронте после. Админский экран управления командой с историей действий. Баннер приостановки в интерфейсе команды.

Чего ещё нельзя. События уходят в outbox, но никем не потребляются — relay и консьюмеры появятся в O1. Журнал переходов клиентам не отдаётся вообще (L-4), только платформе.

Источники: шаг 3 §1.1–1.4; шаг 2 §9 (StateTransition, OutboxEvent, SubjectType включает TEAM); шаг 4 §2 (резолвер актора), §8; решения №07, №15, №19.

Что делаем. Либа outbox: модель, транзакционная запись в той же CLS-транзакции, контракт relay. Либа state-machine: TransitionDef / MachineDef, executor по алгоритму шага 3 §1.3 — CAS-захват (updateMany WHERE status IN (from)) → guards → effects → журнал → outbox, всё в одной транзакции. idempotentTarget, каскады с ограничением глубины, коды 409 TransitionConflict и 422 с кодом guard’а. Резолвер ActorSpec: роль платформы из JWT, сторона сделки через членство, SYSTEM, TIMER. Nx-теги границ (scope:billing запрещён в scope:admin). Машина статуса команды объявляется здесь — она первый настоящий потребитель механизма, и механизм принимается по ней, а не по синтетическому тесту.

Приёмка.

  • HTTP-e2e: два параллельных запроса на верификацию одной команды — один 200, второй 409; в журнале ровно одна строка.
  • Откат бизнес-транзакции не оставляет ни строки журнала, ни события в outbox; коммит гарантирует обе.
  • Повтор перехода с idempotentTarget на уже достигнутом статусе — 200 no-op, без второй строки журнала и второго события.
  • Провал guard’а откатывает транзакцию целиком.
  • Каскад пишет строку с actorType: SYSTEM и basisId исходного перехода; превышение глубины — явная ошибка, не переполнение стека.
  • Отказ актора попадает в журнал (L-2); журнал недоступен ни одному клиентскому маршруту.
  • SYSTEM / TIMER резолвятся без JWT и недоступны с HTTP.
  • Импорт billing-модуля в admin-api падает на линте.

O1 — Операционный контур: задачи и таймеры

Заголовок раздела «O1 — Операционный контур: задачи и таймеры»

Статус: TODO Ветка / PR:Размер: L (вероятно две сессии) · Зависит от: T2 · Фронт:

Бизнес-задача. У администратора появляется рабочая очередь: заявки команд на онбординг приземляются в неё сами, с дедлайном по SLA, и просрочка видна. Всё дальнейшее — мэтчинг, согласование цены, KYC, исполнение выплат — будет попадать в эту же очередь.

Сущности. QueueItem (тип, роль, субъект, SLA, статус), Timer (вид, срок, отмена). Приложение platform-worker.

Что появится для фронта.

ПоверхностьОперации
Администраторсвоя очередь задач: список с фильтрами по типу и статусу · карточка задачи со ссылкой на субъект · взять в работу · закрыть · метрики очереди: глубина, возраст старейшей, доля просроченных
Оператор / арбитрсвоя очередь тем же контрактом (пока пустая — задачи для них появятся в M3 и D1)

Что можно собрать на фронте после. Рабочий стол администратора целиком — он не изменится до конца стройки, будут только добавляться типы задач.

Чего ещё нельзя. Уведомлений нет (O2). Единственный работающий тип задачи — TEAM_ONBOARDING; остальные типы объявлены в enum, но их триггеры появятся вместе со своими срезами.

Источники: шаг 7 §1, §2, §3, §6; шаг 2 §9 (QueueItem, Timer, QueueItemType, TimerKind); решения №08, №28, №30.

Что делаем. platform-worker по конвенциям репозитория. Relay: outbox → RabbitMQ по routing key из domain-events → отметка доставки. Консьюмеры task-generator и timer-scheduler, DLX на каждом (пять падений → DLX + алерт: тихая потеря = нарушение Q-4). Декларативная таблица «переход-триггер → тип задачи → роль → SLA» как константа-массив, вторая колонка — маппинг закрытия. Дедуп открытых задач частичным уникальным индексом. timer-scheduler: тик раз в минуту, timer.fired через outbox, политика напоминаний 50%/80%. Расчёт «рабочих» величин — через platform-calendar. Автосанкций нет.

Приёмка.

  • HTTP-e2e: заявка команды на онбординг → задача TEAM_ONBOARDING появляется в очереди администратора с dueAt через 3 рабочих дня → верификация команды закрывает её автоматически.
  • Тест полноты Q-4: каждый переход всех объявленных машин либо присутствует в таблице маппинга, либо явно помечен как не порождающий задачу. Новый переход без записи роняет CI.
  • Дедуп: повторная доставка события не создаёт вторую открытую задачу того же типа по субъекту.
  • SLA по рабочему календарю: задача, созданная в пятницу 17:00 UTC с SLA 4 рабочих часа, имеет dueAt в понедельник.
  • Событие доходит до консьюмера ровно один раз и не теряется при рестарте воркера.
  • Падающий консьюмер после пяти попыток кладёт сообщение в DLX и поднимает алерт.

Статус: TODO Ветка / PR:Размер: M · Зависит от: O1 · Фронт:

Бизнес-задача. Люди узнают о том, что от них ждут действия, не заходя на платформу проверять. Команда получает письмо о приостановке, участник — о приглашении, администратор — о просроченной задаче.

Сущности. Notification (адресат, тип, requiresAction, прочитано).

Что появится для фронта.

ПоверхностьОперации
Все ролилента уведомлений с пагинацией · счётчик непрочитанных · отметить прочитанным · отметить все прочитанными

Что можно собрать на фронте после. Колокольчик с бейджем и выпадающая лента — во всех интерфейсах сразу.

Чего ещё нельзя. Дайджесты и push вне скоупа беты. Адресность покрывает только события, которые уже существуют (команда, задачи); каждый следующий срез добавляет свои строки в таблицу адресности.

Источники: шаг 7 §4; шаг 4 §4 (F-1), §5 (T-1); шаг 5 §3 (формулировки асинхронных состояний); решение №30.

Что делаем. Консьюмер notification-dispatcher. Декларативная таблица адресности (событие, переход) → [{ адресат-в-контексте, requiresAction, шаблон }], где адресат: founder, team.admins, team.submitter, team.signer, platform.role(X). Каналы: in-app всегда, email — при requiresAction и на терминальных событиях, через существующую либу email-sender. Линтер строк шаблонов: «escrow» запрещён, «оплачено»/«выплачено» запрещены до терминальных денежных состояний.

Приёмка.

  • HTTP-e2e: приостановка команды → уведомление в ленте у всех админов команды + письмо; отметка прочитанным убирает из счётчика.
  • Тест полноты адресности того же вида, что Q-4: событие без строки в таблице роняет CI, либо помечено как не порождающее уведомления.
  • Линтер строк падает на подложенном «escrow» и на преждевременном «выплачено».
  • Payload уведомления не содержит денежных сумм (Q-3 на шине).

Статус: TODO Ветка / PR:Размер: M · Зависит от: T2, O1 · Фронт:

Бизнес-задача. Команда подключает приём выплат: проходит онбординг Stripe, видит статус и — главное — заранее видит, какие документы Stripe от неё ещё хочет, а не узнаёт об этом в момент, когда деньги должны прийти (Y-4).

Сущности. PayoutAccount (провайдер, внешний id, состояние, requirements). Машина NOT_STARTED → PENDING → ACTIVE ⇄ RESTRICTED, целиком вебхук-управляемая.

Что появится для фронта.

ПоверхностьОперации
Команданачать онбординг (получить ссылку Stripe) · статус выплатного аккаунта · список того, что просит Stripe, человеческим языком · перезапросить ссылку
Администраторсостояние выплатных аккаунтов команд · задача при переходе в RESTRICTED
Вебхукиaccount.updated на отдельном signing secret

Что можно собрать на фронте после. Экран «Выплаты» в кабинете команды с реальным редиректом в Stripe и возвратом. Тестируется на Stripe test mode без единого живого платежа.

Чего ещё нельзя. Денег ещё нет: ни фондирования, ни выплат. RESTRICTED пока ничего не блокирует — блокировка приёма офферов включится в P2 (Y-2).

Источники: шаг 5 §1, §8; шаг 2 §7 (PayoutAccount); шаг 3 §8; решения №08, №09, №22.

Что делаем. Приложение billing-api по конвенциям репозитория, конфиг-модуль configs/billing/stripe с пиннингом версии API. Express-аккаунты: accounts.create → Account Link → машина PayoutAccount по account.updated, idempotentTarget. Сохранение requirements.currently_due — содержимое уведомления Y-4. Вебхук Connect: проверка подписи по raw body, дедуп через WebhookEvent @@unique([provider, externalEventId]).

Приёмка.

  • HTTP-e2e на Stripe test mode: запрос онбординга → ссылка → фикстура account.updated с charges_enabled && payouts_enabled && requirements.currently_due = [] → состояние ACTIVE; фикстура с непустым currently_due после ACTIVERESTRICTED + задача администратору + уведомление команде.
  • Повторная доставка того же события: SKIPPED_DUPLICATE, ноль переходов, ноль уведомлений.
  • Подпись чужим секретом отвергается до парсинга тела.
  • Тест границ Nx: billing-модуль не импортируется в admin-api.

Статус: TODO Ветка / PR:Размер: S · Зависит от: T1 · Фронт:

⚠️ Единица заведена 2026-08-31 по решению Stark’а (решение №32). Основание: T1 называет bio и externalExperience «публичным описанием», но прочитать карточку команды может только её участник — публичного чтения в продукте нет ни у кого. Матрица шага 4 §7 правлена той же правкой.

Бизнес-задача. У команды появляется витрина: страница по слагу, которую можно показать фаундеру, положить в подпись и открыть без входа в систему. Команда сама решает, показывать ли в ней состав. Без этого команду невозможно показать никому снаружи: T1 закрыл всю team-поверхность TeamMembershipGuard.

Сущности. Новое поле на Team — настройка видимости состава (флаг, по умолчанию состав показывается). Новых моделей нет; нужна миграция.

Что появится для фронта.

ПоверхностьОперации
Любой посетителькарточка команды по слагу: имя, описание, внешний опыт, статус верификации · состав команды, если он не скрыт
Команда (ADMIN)прочитать и изменить настройку видимости состава

Что можно собрать на фронте после. Публичная страница команды /{slug} — та самая ссылка, которой команда делится снаружи; переключатель «показывать состав» в настройках команды. P2 переиспользует эту же карточку там, где фаундер впервые видит согласившуюся команду.

Чего ещё нельзя. Публичного профиля сотрудника по-прежнему нет: увидеть, в каких командах состоит конкретный человек, снаружи нельзя — публичен состав команды, а не аффилиации человека. Поиск и листинг команд наружу не открываются (карточка достаётся только по известному слагу).

Источники: шаг 2 §3 (Team.slug, bio, externalExperience); шаг 4 §7 в редакции от 2026-08-31 и правка под таблицей; решение №32; запись T1 в SESSION-LOG.md.

Что делаем. Поле видимости на Team + миграция. Публичные маршруты без авторизации в user-api (команда достаётся по слагу, не по id — id наружу не светится). Состав отдаётся проекцией: имя, аватар, специализация, роль внутри команды; canSign, canSubmit, employeeId, email и участники со статусом REMOVED в публичный ответ не попадают — отдельный DTO, а не переиспользование внутреннего. Настройка видимости — операция ADMIN команды в существующем контуре /api/v1/teams/:teamId.

Приёмка.

  • HTTP-e2e на реальной БД: карточка по слагу без единого токена отдаёт имя, описание и внешний опыт · при включённой видимости отдаёт состав · после выключения настройки тот же запрос отдаёт карточку без состава.
  • Публичный ответ не содержит canSign, canSubmit, employeeId, email и участников REMOVED — проверяется на объекте, где все эти поля присутствуют в источнике.
  • Несуществующий слаг — 404; id команды вместо слага — 404 (id наружу не адресуется).
  • Настройку меняет только ADMIN команды: VIEWER и EDITOR — 403, не участник — 403.
  • Внутренний контур не задет: GET /api/v1/teams/:teamId и .../members по-прежнему требуют членства и по-прежнему отдают полномочия.

Статус: TODO Ветка / PR:Размер: M · Зависит от: T2, O1 · Фронт:

Бизнес-задача. Фаундер заводит проект, получает спецификацию, принимает её и просит подобрать команду. Крупные бюджеты не отваливаются, а уходят на просмотр администратору (мягкий гейт, решение №06).

Сущности. Project (машина из 10 статусов, contractingStage заводится, но пока не живёт), Specification (версии PROPOSED → ACCEPTED, прочие SUPERSEDED).

Что появится для фронта.

ПоверхностьОперации
Фаундерсоздать проект · список своих проектов · карточка проекта со статусом · версии спецификации · принять версию спецификации · запросить подбор команды · отменить проект до контрактинга
Администраторсписок проектов · карточка · задача BUDGET_GATE_REVIEW · снять бюджет-гейт · задача TEAM_MATCHING

Что можно собрать на фронте после. Кабинет фаундера: создание проекта, просмотр спеки, принятие, кнопка «подобрать команду». Админский экран проектов и бюджет-гейта.

Чего ещё нельзя. Спецификацию пока никто не порождает — интеграция с ai-agent в S3, до неё версии заводятся администратором или фикстурой. Команду подобрать некому — P2.

Источники: шаг 2 §2.1, §4.1; шаг 3 §2 (P01–P03, P11), §9.3; шаг 4 §7; решения №02, №03, №06, №17.

Что делаем. Перевод проекта на state-machine: переходы P01–P03 и P11, поле contractingStage, производный DISPUTED объявляется, но ни один guard его не читает (решение №03 — витрина, не источник истины). Версионируемая Specification. Мягкий бюджет-гейт: belowBudgetGate не отвергает, а порождает задачу.

Приёмка.

  • HTTP-e2e: создать проект → добавить версию спеки → принять → запросить мэтчинг при belowBudgetGate = true (переход не проходит, создана задача) → админ снимает гейт → переход проходит, создана задача TEAM_MATCHING.
  • Отмена проекта (P11) невозможна, если существует хотя бы один Contract.
  • Ни один guard не читает Project.status === 'DISPUTED' — тест-греп по коду фичи.
  • Чужой проект — 403 на всех маршрутах фаундера.

Статус: TODO Ветка / PR:Размер: M · Зависит от: P1, T1, T3 · Фронт:

Бизнес-задача. Администратор подбирает команду вручную и отправляет ей предложение. Команда принимает или отказывается с причиной. Только после согласия команды фаундер впервые её видит (P-1) — порядок зашит в машину, а не в интерфейс.

Сущности. Offer (машина SENT_TO_TEAM → TEAM_ACCEPTED → ACCEPTED_BY_FOUNDER, отказы с обязательной причиной, отзыв администратором).

Что появится для фронта.

ПоверхностьОперации
Администраторподобрать команду и отправить оффер · список офферов проекта · отозвать оффер
Командавходящие офферы · карточка оффера с описанием проекта · принять · отклонить с причиной
Фаундерувидеть предложенную команду (только после её согласия) · принять · отклонить с причиной

Что можно собрать на фронте после. Экран входящих офферов у команды, экран «ваша команда» у фаундера, админский мэтчинг. Проект доходит до статуса CONTRACTING.

Чего ещё нельзя. В CONTRACTING пока пусто — план и договоры в P3P5.

Источники: шаг 2 §6 (Offer); шаг 3 §8, §2 (P04–P06); шаг 5 §1 (Y-2).

Что делаем. Модель и машина Offer, каскады P04 (P-1), P05 (возврат в TEAM_MATCHING), P06 (переход в CONTRACTING, фиксация teamId, contractingStage = PLAN_DRAFTING). Guard’ы: PayoutAccount ≠ RESTRICTED (Y-2 — здесь включается блокировка, объявленная в T3), запрет конфликта интересов «фаундер — участник команды того же проекта» (10.3.6).

Приёмка.

  • HTTP-e2e: админ шлёт оффер → фаундер не видит команду (негативная проверка на всех founder-маршрутах) → команда принимает → фаундер видит → фаундер принимает → проект CONTRACTING, teamId зафиксирован, contractingStage = PLAN_DRAFTING.
  • Отказ без причины отвергается обеими сторонами (P-2).
  • Команда в RESTRICTED не может принять оффер (Y-2).
  • Фаундер, состоящий в команде-получателе, ловится guard’ом конфликта интересов.

Статус: TODO Ветка / PR:Размер: L (вероятно две сессии) · Зависит от: P2 · Фронт:

Бизнес-задача. Команда составляет план этапов с суммами, сроками и критериями приёмки; администратор согласовывает цену; собираются юридические реквизиты обеих сторон; согласованный план уходит фаундеру — без истории торга (F-1). Фаундер принимает его бинарно или отклоняет с причиной, и это не тупик (C-4).

Сущности. PlanVersion (снапшот, машина DRAFT → INTERNAL_REVIEW → SENT → ACCEPTED | REJECTED, SUPERSEDED при новой версии), Party (снапшот реквизитов на проект, @@unique([projectId, side])), PartyDocument, суб-машина contractingStage.

Что появится для фронта.

ПоверхностьОперации
Командачерновик плана: этапы, суммы, сроки, критерии · отправить на согласование цены · внутренние заметки торга · реквизиты Party(TEAM) и документы
Администраторзадача PRICE_REVIEW · вернуть план на доработку · задача KYC_DOCUMENTS · принять документы · отправить план фаундеру
Фаундерсогласованный план (только версии SENT+) · diff версий · принять план · отклонить с причиной · реквизиты Party(FOUNDER)

Что можно собрать на фронте после. Конструктор плана у команды, экран согласования цены у администратора, экран «ваш план» у фаундера с diff между версиями.

Чего ещё нельзя. Договоры создаются записями DRAFT, но не генерируются и не подписываются — P4 и P5. Проект не доходит до ACTIVE.

Источники: шаг 2 §4.2, §6; шаг 3 §3 (C01–C06), §8; шаг 4 §4 (F-1 / L-4), §7; решения №11, №14.

Что делаем. Суб-машина contractingStage: PLAN_DRAFTING → PLAN_INTERNAL_REVIEW → KYC_PENDING → PLAN_SENT. PlanVersion как снапшот, diff версий (C-6). Party — снапшот на проект, KycState фаундера за конфиг-флагом (вопрос Q-02). Правила F-1 реализуются WHERE в репозитории founder-фичи, а не фильтром в маппере; internalNotes отсутствует как поле класса во всех клиентских DTO. Guard C04 не пропускает PLAN_SENT без заполненной Party(TEAM), PayoutAccount ≠ NOT_STARTED и Party(FOUNDER).kycState ∈ {NOT_REQUIRED, APPROVED} (C-5).

Приёмка.

  • HTTP-e2e: команда составляет план → отправляет на согласование → админ возвращает с правкой → команда правит → админ принимает → KYC → план уходит фаундеру → фаундер отклоняет с причиной (задача PLAN_MEDIATION, не тупик) → новая версия → фаундер принимает.
  • Негативный e2e F-1: ни один founder-маршрут не отдаёт PlanVersion в DRAFT или INTERNAL_REVIEW — включая список, карточку, diff и payload уведомлений.
  • internalNotes отсутствует как поле класса в клиентских DTO — тест рефлексией, не ревью.
  • Diff строится только между версиями SENT+; первая видимая версия diff’а не имеет.
  • Guard C04 отвергает PLAN_SENT при незаполненной Party.

Статус: TODO Ветка / PR:Размер: M · Зависит от: P3 · Фронт:

Бизнес-задача. Из принятого плана собираются два договора — команда с платформой и фаундер с платформой — с приложением-планом, где критерии приёмки воспроизведены дословно, потому что они часть предмета договора.

Сущности. Contract (+ templateVersion, documentHash). Шаблоны в репозитории, версионируются как код.

Что появится для фронта.

ПоверхностьОперации
Администраторпредпросмотр сгенерированного договора A и B в PDF · версия шаблона и хэш документа
Команда / фаундерпредпросмотр своего договора до подписания

Что можно собрать на фронте после. Просмотрщик PDF договора в интерфейсе обеих сторон.

Чего ещё нельзя. Подписать нельзя — порт подписания подключается здесь, но конверты и вебхуки в P5. Юридические формулировки — плейсхолдеры до ответа юриста (Q-04).

Источники: шаг 6 §1, §2, §6; решение №25.

Что делаем. Шаблоны в libs/apis/providers/project-api/contracting/templates: рамочный A (Team ↔ Platform), рамочный B (Founder ↔ Platform), приложение-план из PlanVersion. Рендер HTML → PDF. templateVersion + documentHash (sha256 отправленного PDF). Интерфейс SigningProvider (createEnvelope, voidEnvelope, downloadSigned, parseWebhook) — объявляется здесь, реализуется в P5. Линтер строк: «escrow» запрещён в шаблонах договоров и уведомлений (7.6).

Приёмка.

  • HTTP-e2e: принятый план → предпросмотр PDF договора A и B → приложение-план содержит все этапы, суммы, сроки и критерии дословно, без усечения (снапшот-тест рендера).
  • documentHash считается до отправки и совпадает с хэшем сохранённого PDF.
  • Линтер «escrow» падает на подложенной строке в шаблоне договора и в шаблоне уведомления.
  • Фаундер не получает договор A ни одним маршрутом, команда — B (решение №26).

Статус: TODO Ветка / PR:Размер: M · Зависит от: P4 · Фронт:

Бизнес-задача. Обе стороны подписывают свои договоры не выходя из продукта, строго по очереди — сначала команда, потом фаундер (K-4), — и после второй подписи проект оживает: этапы материализуются, работа начинается.

Сущности. Машина Contract (DRAFT → SENT_FOR_SIGNING → SIGNED), конверты провайдера.

Что появится для фронта.

ПоверхностьОперации
Командаподписать договор A (embedded-виджет в интерфейсе) · статус конверта
Фаундерподписать договор B после команды · статус конверта
Администраторстатус подписания по проекту · перевыпустить истёкший конверт · задача при отказе от подписания
Вебхукиproject-api /webhooks/signing, дедуп, идемпотентные переходы

Что можно собрать на фронте после. Экран подписания с встроенным виджетом BoldSign у обеих сторон. Проект доходит до ACTIVE — сквозной путь от создания до старта работ собирается целиком.

Чего ещё нельзя. Этапы материализуются, но с ними ничего нельзя сделать — M1.

Источники: шаг 6 §1, §3, §5; шаг 3 §3 (C07, C08), §2 (P07), §8; решения №25, №26, №27.

Что делаем. Адаптер BoldSign на бесплатном Sandbox, embedded по умолчанию, email-ссылка — fallback конфигом. Последовательность K-4: конверт A → вебхук signed(A) → конверт B → вебхук signed(B) → каскад P07: материализация Milestone из принятого PlanVersion сразу в PLAN_APPROVED (M-4, журнальная строка на каждый), contractingStage = null, снапшот commissionRateBps. Обработка: смена подписанта в полёте (void + новый конверт), отказ (контракт → DRAFT, задача PLAN_MEDIATION), истечение 14 дней с напоминаниями на 7-й и 12-й, правка плана при живых конвертах (оба void, цикл с C01). Доп-этапы после старта — акцепт внутри продукта без перевыпуска конвертов (решение №27).

Приёмка.

  • HTTP-e2e через sandbox: оба конверта → обе подписи → проект ACTIVE → этапы материализованы в точном соответствии принятому PlanVersion по числу и суммам, журнал содержит строку на каждый.
  • Повторная доставка signed не двигает контракт дважды.
  • Смена canSign в полёте: старый конверт void, новый выпущен новому подписанту.
  • Попытка правки плана при живых конвертах отвергается — PlanVersion.ACCEPTED и подписываемый документ разойтись не могут.
  • Договор B не уходит фаундеру, пока A не подписан (K-4).

Статус: TODO Ветка / PR:Размер: M · Зависит от: P5 · Фронт:

Бизнес-задача. Обе стороны видят план работ как живой объект: этапы, критерии приёмки по каждому, загруженные артефакты. Артефакты оплаченных этапов остаются у фаундера навсегда (X-4).

Сущности. Milestone (11 статусов, включая терминальный CLOSED), AcceptanceCriterion, Artifact (файл в S3, ссылка, демо).

Что появится для фронта.

ПоверхностьОперации
Командасписок этапов со статусами · карточка этапа · критерии этапа · загрузить артефакт (файл, ссылка, демо) · отметить критерий выполненным
Фаундерсписок этапов · карточка · критерии · артефакты — чтение
Операторэтапы без денежных полей (первая операторская поверхность появится в M3)

Что можно собрать на фронте после. Доска этапов проекта у обеих сторон, чек-лист критериев, загрузка и просмотр артефактов.

Чего ещё нельзя. Этап нельзя профинансировать (M2), нельзя сдать (M3). Статусы дальше PLAN_APPROVED недостижимы.

Источники: шаг 2 §5; шаг 3 §4 (M01, M04), §9.1; решения №01, №11.

Что делаем. Модели этапа, критериев и артефактов. Канон из 11 статусов, включая CLOSED (решение №01). Переходы M01 (утверждение плана добавленного этапа оператором) и M04 (явный старт работ командой). Запрет удаления артефактов этапа в статусе ≥ FUNDED — service-level (X-4/L-3). @@unique([projectId, orderIndex]).

Приёмка.

  • HTTP-e2e: после ACTIVE обе стороны видят один и тот же список этапов; команда загружает файл в S3 и ссылку, обе видны фаундеру; отметка критерия в MET видна обеим.
  • Артефакт этапа FUNDED+ не удаляется ни одним маршрутом.
  • Порядок этапов держится уникальным индексом; попытка задвоить orderIndex падает на БД.
  • Операторский ответ по этапу не содержит ни одного денежного значения.

Статус: TODO Ветка / PR:Размер: L (вероятно две сессии) · Зависит от: M1, T3 · Фронт:

Бизнес-задача. Фаундер оплачивает этап, и обе стороны видят честное состояние платежа: банковский перевод идёт 3–5 дней, и всё это время нельзя писать «оплачено» (7.3). Когда деньги пришли — этап переходит в работу.

Сущности. Hold (машина PENDING_IN → HELD | VOID), WebhookEvent (дедуп входящих).

Что появится для фронта.

ПоверхностьОперации
Фаундерпрофинансировать этап (редирект в Stripe Checkout) · возврат с результатом · статус платежа по этапу · отменить незавершённый платёж
Командастатус фондирования этапа: «ожидает средств» / «средства поступили» · старт работ после FUNDED
Вебхукиbilling-api /webhooks/stripe: checkout, payment_intent, charge

Что можно собрать на фронте после. Кнопка «профинансировать» с реальным редиректом и возвратом, честные статусы платежа у обеих сторон, старт работ командой.

Чего ещё нельзя. Сдать этап нельзя (M3). Деньги лежат в холде и никуда не уходят.

Источники: шаг 5 §2, §5, §8; шаг 3 §5 (H01–H03), §4 (M03); шаг 2 §7; решения №04, №16, №22.

Что делаем. Hosted Checkout Session (mode: payment, transfer_group = project:{projectId}, metadata с holdId/milestoneId/projectId, idempotency key hold:{holdId}:checkout), ACH по умолчанию, карты допускаются, издержки процессинга — из комиссии, без surcharge. Guard’ы H01: этап PLAN_APPROVED, PayoutAccount ≠ NOT_STARTED, M-1b (нет открытых Dispute и активной Termination — guard читает таблицы, которые появятся в D1/D2; до тех пор проверка объявлена и покрыта тестом на пустых таблицах). Вебхуки: подпись по raw body, дедуп через WebhookEvent, payment_intent.succeeded → H02 → каскад M03 (FUNDED), неуспех → H03 (VOID). Обработчики не предполагают порядок доставки.

Приёмка.

  • HTTP-e2e на Stripe test mode: фондирование → фикстура payment_intent.succeeded → холд HELD, этап FUNDED; команда стартует работы.
  • Перемешанный порядок доставки (succeeded раньше session.completed) не ломает состояние.
  • Повтор события: SKIPPED_DUPLICATE, ноль переходов.
  • Idempotency key детерминирован из id холда — двойной вызов не создаёт второй интент.
  • VOID не трогает статус этапа; повторное фондирование создаёт новый Hold, старый остаётся в истории.
  • Ни один клиентский DTO не говорит «оплачено» при PENDING_IN — снапшот-тест формулировок.

M3 — Сдача, приёмка, операторская поверхность

Заголовок раздела «M3 — Сдача, приёмка, операторская поверхность»

Статус: TODO Ветка / PR:Размер: L (вероятно две сессии) · Зависит от: M2 · Фронт:

Бизнес-задача. Команда сдаёт этап, предварительно подтвердив каждый критерий сама (M-3); фаундер принимает; оператор независимо проверяет, что заявленное действительно приложено (M-2). Двойное подтверждение — единственный путь к оплате.

Сущности. MilestoneSubmission (попытки без лимита, M-5). Операторская поверхность целиком: Operator*Dto, две операции, денежный линтер.

Что появится для фронта.

ПоверхностьОперации
Командасдать этап (блокируется, пока не все критерии в MET) · история попыток сдачи · сопроводительная записка
Фаундерпринять сдачу · отклонить (вход в разбор — сам разбор в M6)
Операторочередь задач SUBMISSION_VERIFICATION · карточка сдачи без единого денежного поля · approve · reject (не переход: задача администратору)
Администраторзадача при истечении молчания фаундера — решение, а не автовыплата

Что можно собрать на фронте после. Экран сдачи у команды, экран приёмки у фаундера, операторское рабочее место целиком. Этап доходит до VERIFIED.

Чего ещё нельзя. Деньги не двигаются — M4 и M5. Отклонение фаундером создаёт запись, но разбор ещё не проводится (M6).

Источники: шаг 2 §5; шаг 3 §4 (M05–M08, M11–M13); шаг 4 §3 (O-1 / O-2 / Q-3), §6, §7, §8; шаг 7 §3 (FOUNDER_SILENCE); решения №19, №21, №30.

Что делаем. Сдача с самопроверкой M-3: guard требует все критерии в MET, снапшот состояний пишется в metadata журнала. Таймер FOUNDER_SILENCE (5 рабочих дней); истечение — не переход, а задача FOUNDER_SILENCE_DECISION администратору (8.3: молчание не стоит денег автоматически). Приёмка M06, верификация оператором M11 с basisType = DUAL_APPROVAL, basisId = id перехода в журнале. Операторская поверхность по трём слоям O-1: границы Nx + отдельные Operator*Dto без денежных полей как полей класса + денежный линтер. O-2: ровно две мутирующие операции на тип задачи.

Приёмка.

  • HTTP-e2e: сдача при незакрытом критерии отвергается → закрыть критерии → сдать → фаундер принимает → задача оператору → оператор approve → этап VERIFIED, задача SETTLEMENT_EXECUTION администратору с основанием.
  • Денежный линтер: unit-тест обходит рефлексией все Operator*Dto и падает на поле, совпадающем с /amount|minor|budget|fee|commission|total|price|hold|settlement/i — доказать, временно добавив поле.
  • e2e-снапшот операторских ответов на фикстуре с деньгами: в теле ни одного денежного значения.
  • Третьей мутирующей операции в операторской поверхности не существует — тест перечисления маршрутов.
  • PAID недостижим иначе как через FOUNDER_APPROVED → VERIFIED (M-2).
  • Истечение FOUNDER_SILENCE не порождает перехода и не двигает деньги.

Статус: TODO Ветка / PR:Размер: M · Зависит от: M3 · Фронт:

Бизнес-задача. Администратор видит рассчитанный системой раздел суммы, основание и список необратимых последствий — и подтверждает. Ввести сумму руками он не может никогда (решение №20): считает система, решает основание, исполняет администратор.

Сущности. Settlement (основание, компоненты, статус PENDING → EXECUTED | FAILED), CHECK-инвариант суммы.

Что появится для фронта.

ПоверхностьОперации
Администратор (billing-api)очередь SETTLEMENT_EXECUTION · карточка расчёта: основание, доля команды, доля фаундера, комиссия, последствия · подтвердить исполнение · отклонить с эскалацией
Фаундер / командасвои холды и их состояние — чтение

Что можно собрать на фронте после. Экран исполнения выплаты в бэк-офисе. Обратите внимание: он живёт на втором бэкендеbilling-api, не admin-api (решение №09).

Чего ещё нельзя. Подтверждение пока ничего не отправляет провайдеру — исполнение в M5. Из четырёх оснований реализовано одно, DUAL_APPROVAL; остальные добавят M6, D1, D2.

Источники: шаг 4 §6; шаг 5 §3; шаг 2 §7; решения №13, №20.

Что делаем. Модель Settlement с денормализованным hold_amount_minor под CHECK. Расчёт на либе money по основанию DUAL_APPROVAL: команда = amount − fee, фаундер = 0, платформа = fee. Двухфазное исполнение D-6: показ расчёта и последствий → явное подтверждение (PENDING → EXECUTED). Отклонение создаёт эскалацию, но не право изменить сумму. ArbiterGuard к маршрутам settlement’а не применяется и не принимается.

Приёмка.

  • HTTP-e2e: этап VERIFIED → задача → карточка расчёта показывает три компоненты и основание → подтверждение переводит Settlement в EXECUTED.
  • Входные DTO settlement’а не содержат денежных полей — тест рефлексией. Попытка передать сумму в теле игнорируется схемой валидации, а не «затирается» в сервисе.
  • Сумма компонент строго равна холду; нарушение отвергается БД, а не кодом.
  • Арбитр к billing-поверхности не допускается — негативный e2e.

Статус: TODO Ветка / PR:Размер: L (вероятно две сессии) · Зависит от: M4 · Фронт:

Бизнес-задача. Деньги доходят до банковского счёта команды, и команда видит три честных состояния: «выплата инициирована» → «в пути» → «зачислено». Слово «выплачено» не появляется, пока деньги не на счету (решение №23).

Сущности. Машина холда HELD → RELEASING → RELEASED, а также REFUNDED и SPLIT.

Что появится для фронта.

ПоверхностьОперации
Командасостояние выплаты по этапу: три стадии · история выплат
Фаундерэтап PAID · проект COMPLETED после последнего этапа
Администраторзадача при payout.failed · повторный запуск частично исполненного сплита
Вебхукиpayout.*, charge.refunded на Connect-эндпоинте

Что можно собрать на фронте после. Счастливый путь продукта собирается целиком: от создания проекта до COMPLETED. Это первый момент, когда фронт можно провести по всему сценарию.

Чего ещё нельзя. Возврат и сплит реализованы механически, но оснований для них пока нет — они появятся с D1 и D2.

Источники: шаг 5 §3, §4, §5; шаг 3 §5 (H04–H07), §4 (M13), §2 (P08); решение №23.

Что делаем. Transfer с source_transaction = chargeId холда — инвариант «не больше холда» на стороне Stripe. Idempotency keys settlement:{id}:transfer / settlement:{id}:refund. H04 (guard Y-1: PayoutAccount ACTIVE), H05 строго по payout.paid через сопоставление balance transactions с нашими transfer id, H06 (REFUNDED), H07 (SPLIT — терминален только после подтверждения обеих операций; частичное исполнение = Settlement.FAILED + задача, повторный запуск идемпотентен). payout.failed → холд остаётся RELEASING + задача + уведомление команде. Комиссия отдельной проводкой не двигается — это остаток на балансе платформы.

Приёмка.

  • HTTP-e2e с test clocks: подтверждение → transfer → RELEASING → фикстура payout.paidRELEASED → этап PAID → последний этап доводит проект до COMPLETED, проект read-only.
  • «Выплачено» не показывается команде до RELEASED — снапшот-тест трёх состояний.
  • Повторный запуск частично исполненного сплита не создаёт второй transfer и второй refund.
  • Transfer на сумму больше холда невозможен — и нашим guard’ом, и source_transaction.
  • payout.failed не откатывает холд, а создаёт задачу.

Статус: TODO Ветка / PR:Размер: M · Зависит от: M3, M4 · Фронт:

Бизнес-задача. Фаундер не может отклонить работу «просто так»: отказ обязан быть привязан к конкретному критерию (R-1). Администратор разбирает и выносит один из двух вердиктов — либо критерий действительно не соблюдён и команда дорабатывает бесплатно, либо критерии соблюдены, а требование фаундера — это новый платный этап (R-2, штатная операция роста GMV).

Сущности. AcceptanceReview, ReviewRejectionItem (обязательная привязка к критерию), пауза дедлайна deadlinePausedAt.

Что появится для фронта.

ПоверхностьОперации
Фаундеротклонить сдачу, выбрав конкретные критерии и добавив комментарии · видеть разбор и вердикт
Командавидеть, какие критерии оспорены · дорабатывать после возврата
Администраторзадача ACCEPTANCE_REVIEW · вердикт CRITERIA_UNMET_RETURN · вердикт CRITERIA_MET_NEW_SCOPE с созданием нового этапа
Операторзадача PLAN_APPROVAL на добавленный этап (M01)

Что можно собрать на фронте после. Экран отклонения с чек-листом критериев, экран разбора у администратора, появление нового платного этапа в плане.

Чего ещё нельзя. Эскалация разбора в арбитраж — D1.

Источники: шаг 2 §5 «Разбор непринятия»; шаг 3 §4 (M07, M09, M10); шаг 4 §6 (основание REVIEW_VERDICT); решение №27.

Что делаем. AcceptanceReview не создаётся без хотя бы одного ReviewRejectionItem (R-1, service-level в транзакции). Вердикт без reasoning отвергается (L-2). CRITERIA_UNMET_RETURN: спорные критерии → UNCHECKED, этап → IN_PROGRESS, дедлайн размораживается dueAt += now − deadlinePausedAt. CRITERIA_MET_NEW_SCOPE: этап принят обычным путём, требование становится новым PLANNED-этапом со ссылкой newMilestoneId. Основание REVIEW_VERDICT добавляется в расчёт M4.

Приёмка.

  • HTTP-e2e: отклонение без выбранного критерия отвергается → отклонение с критерием создаёт разбор и ставит паузу дедлайна → вердикт возврата размораживает дедлайн ровно на время паузы → вердикт нового объёма создаёт новый этап и доводит текущий до приёмки.
  • Вердикт без обоснования отвергается.
  • Число попыток сдачи не ограничено (M-5) — три круга сдача/отказ проходят.
  • Расчёт по основанию REVIEW_VERDICT совпадает с DUAL_APPROVAL.

Статус: TODO Ветка / PR:Размер: L (вероятно две сессии) · Зависит от: M5, M6 · Фронт:

Бизнес-задача. Любая сторона может открыть спор по фондированному этапу. Обе подают позиции и доказательства, не видя чужих до решения (T-1). Арбитр выносит вердикт с обоснованием; деньги двигает не он, а администратор по его решению.

Сущности. Dispute (машина OPENED → POSITIONS_GATHERING → UNDER_ARBITRATION → RESOLVED, плюс ESCALATED_LCIA), DisputePosition, DisputeEvidence.

Что появится для фронта.

ПоверхностьОперации
Обе стороныоткрыть спор по этапу · подать свою позицию и доказательства · видеть только факт подачи чужой позиции · после решения — обе позиции целиком
Арбитрочередь DISPUTE_ARBITRATION · обе позиции и доказательства · вердикт с обоснованием: в пользу команды, фаундера, частично с долей · эскалация в LCIA · журнал переходов по субъекту спора
Администраторзадача SETTLEMENT_EXECUTION с основанием ARBITER_DECISION

Что можно собрать на фронте после. Экран спора у обеих сторон с взаимной слепотой, арбитражное рабочее место целиком. Спор доводится до движения денег.

Чего ещё нельзя. Расторжение — D2.

Источники: шаг 2 §8; шаг 3 §6 (D01–D06), §4 (M14–M17), §2 (P09, P10); шаг 4 §5 (T-1), §6, §7; шаг 7 §3 (окно позиций); решения №03, №18.

Что делаем. Машина спора целиком. Окно позиций (5 календарных дней): по истечении спор идёт в арбитраж по имеющимся материалам, факт неответа — строка журнала с меткой (S-2, L-5). Вердикт требует outcome + reasoning, при PARTIALteamShareBps. Точечная заморозка 7.5: этап → DISPUTED, каскад P09, пауза дедлайна; разморозка наступает после исполнения денег, не в момент вердикта. Реализация T-1: WHERE по стороне в репозитории клиентской поверхности + отдельные DisputeOwnViewDto / DisputeResolvedViewDto. Основание ARBITER_DECISION в расчёте M4; исходы ведут этап в PAID (M15), CLOSED (M16) или обратно в IN_PROGRESS (M17).

Приёмка.

  • Параметризованный HTTP-e2e T-1, один и тот же для обеих сторон: до RESOLVED видна своя позиция целиком и только факт подачи чужой без содержимого; после RESOLVED — обе целиком.
  • Спор по нефондированному этапу отвергается (M-6); второй открытый спор по тому же этапу отвергается.
  • Истечение окна позиций уводит спор в арбитраж и пишет факт неответа в журнал.
  • Вердикт без обоснования или PARTIAL без доли отвергается.
  • Арбитр не имеет доступа к billing-поверхности — негативный e2e.
  • Разморозка этапа наступает после RELEASED / REFUNDED / SPLIT, а не после вердикта.
  • При открытом споре фондирование новых этапов отвергается guard’ом M-1b, объявленным в M2.

Статус: TODO Ветка / PR:Размер: L (вероятно две сессии) · Зависит от: D1 · Фронт:

Бизнес-задача. Сторона может расторгнуть отношения подписанным заявлением. Вторая сторона имеет окно на возражение, и возражение превращается в спор. Администратор оценивает выполненное по доле закрытых критериев — произвольная оценка запрещена (X-2) — и исполняет раздел по каждому активному холду.

Сущности. Termination (машина REQUESTED → UNDER_REVIEW → EXECUTED, плюс WITHDRAWN), основания расторжения.

Что появится для фронта.

ПоверхностьОперации
Обе стороныподать заявление о расторжении · отозвать до рассмотрения · возразить внутри окна (создаёт спор) · видеть паузу проекта и её причину
Администраторзадача TERMINATION_ASSESSMENT · рассчитанная системой доля выполненного · основание и обоснование · исполнить
Обе стороныитог: договоры расторгнуты, нефондированные этапы закрыты, артефакты оплаченных этапов остаются у фаундера

Что можно собрать на фронте после. Экран расторжения у обеих сторон, экран оценки у администратора, баннер паузы проекта.

Чего ещё нельзя. Ничего из основного сценария — после D2 продуктовый контур замкнут.

Источники: шаг 2 §8; шаг 3 §7 (T01–T05), §4 (M18), §2 (P13), §9.4; шаг 4 §6 (основание TERMINATION_ASSESSMENT); шаг 7 §3 (окно возражений); решение №18.

Что делаем. Машина расторжения целиком. Пауза по семантике §9.4: блокируются фондирование, сдачи и приёмки; работа IN_PROGRESS продолжается на риск команды; дедлайны замирают. Окно возражений 5 календарных дней; молчание ≠ согласие — факт неответа в журнал с меткой (X-6). completedCriteriaBps считается системой из доли критериев в MET/CONFIRMED; при основании DISPUTE_DECISION повторной оценки нет — исполняется решение арбитра. Раздел: комиссия не возвращается при инициативе фаундера без вины команды (X-3); при TEAM_FAULT холды полностью фаундеру. Оба договора → TERMINATED (K-6), нефондированные этапы → CLOSED (M18), каскад P13.

Приёмка.

  • HTTP-e2e: заявление → пауза (фондирование, сдача и приёмка отвергаются guard’ами) → окно истекает без возражения → задача → оценка → исполнение → холды разделены, договоры TERMINATED, нефондированные этапы CLOSED, проект CANCELLED.
  • Второй сценарий: возражение внутри окна создаёт спор, расторжение ждёт его исхода.
  • completedCriteriaBps не принимается из тела запроса ни на одном маршруте — тест рефлексией DTO.
  • Отзыв заявления возможен до UNDER_REVIEW и невозможен после.
  • Артефакты оплаченных этапов доступны фаундеру после расторжения (X-4).

Статус: TODO Ветка / PR:Размер: S · Зависит от: M5 · Фронт:

Бизнес-задача. Фаундер оспорил списание у своего банка, минуя платформу. Это не наш арбитраж, и путать их в коде опасно. Администратор получает приоритетную задачу и собранный пакет доказательств для ответа Stripe.

Сущности. ExternalPaymentDispute — модель заводится здесь.

⚠️ Модель упомянута в шаге 5 §6, но не объявлена в схеме шага 2. Единица её вводит; дельта схемы фиксируется в step-документе.

Что появится для фронта.

ПоверхностьОперации
Администраторприоритетная задача · карточка внешнего спора · собранный evidence-пакет: договор B, критерии, артефакты, журнал приёмки
Вебхукиcharge.dispute.created

Что можно собрать на фронте после. Экран внешнего спора в бэк-офисе.

Источники: шаг 5 §6; решение №24.

Что делаем. Модель и обработчик. Имя ExternalPaymentDisputeникогда не Dispute. Evidence-пакет собирается из существующих таблиц, ничего дополнительно не пишется. Лимит карточных платежей на этап — конфиг.

Приёмка.

  • HTTP-e2e: фикстура charge.dispute.created после RELEASED → приоритетная задача, собранный пакет, состояние холда не сломано.
  • Тест-греп по именам: внешний chargeback не называется Dispute ни в коде, ни в DTO, ни в строках интерфейса.

Статус: TODO Ветка / PR:Размер: S · Зависит от: M3, O2 · Фронт:

Бизнес-задача. У пользователя есть куда написать. Первая линия — оператор с SLA 8 часов; если вопрос не его, он эскалирует, не отвечая.

Сущности. ConciergeThread, ConciergeMessage.

Что появится для фронта.

ПоверхностьОперации
Все клиентские ролисоздать обращение (с привязкой к проекту или без) · переписка в треде · список своих обращений
Операторочередь CONCIERGE_FIRST_LINE · ответ в треде · эскалация — третья операторская операция
Администраторочередь ESCALATED_INQUIRY · ответ · вернуть оператору или закрыть

Что можно собрать на фронте после. Виджет обращений во всех интерфейсах.

Источники: шаг 7 §5; решение №29.

Что делаем. Треды и сообщения, задача оператору при создании, эскалация как переход OPEN → ESCALATED с событием concierge.escalated. Денежные системные объекты в консьерж-поверхности не отображаются. Walrider в консьерже на бете не участвует.

Приёмка.

  • HTTP-e2e: обращение → задача оператору → ответ → эскалация → задача администратору → возврат оператору → закрытие.
  • Эскалация не меняет ничего в домене сделки.
  • SLA 8 часов считается по рабочему календарю.
  • Эскалация — единственное исключение из «ровно двух операторских операций»; тест перечисления маршрутов из M3 обновлён так, чтобы исключение было явным, а не случайным.

Статус: TODO Ветка / PR:Размер: S · Зависит от: M5 · Фронт:

Бизнес-задача. Расхождение между нашим представлением о деньгах и реальностью Stripe обнаруживается ночью автоматически, а не через жалобу команды.

Что появится для фронта.

ПоверхностьОперации
Администраторотчёт сверки за ночь · задачи по расхождениям · список зависших состояний

Источники: шаг 5 §7.

Что делаем. Cron в platform-worker: все HELD-холды имеют succeeded-интент на ту же сумму · все EXECUTED-settlements имеют terminal transfer/refund · баланс платформы ≈ Σ(HELD) + нераспределённая комиссия − списания · зависшие состояния (PENDING_IN > 7 дней, RELEASING > 10 дней). Любое расхождение → задача + алерт. Пороги — конфиг.

Приёмка.

  • Каждая из четырёх проверок покрыта тестом на подложенном расхождении: рождается задача и алерт.
  • Зелёный прогон не создаёт ни одной задачи.
  • Сверка — единственный потребитель Stripe API «на чтение всего».

Статус: TODO Ветка / PR:Размер: M · Зависит от: P1, O1 · Фронт:

Бизнес-задача. Спецификацию проекта наконец порождает агент, а не фикстура: вилки решений типизированы, версии пишутся через доменное ядро. Трекер подключается портом-заглушкой, чтобы его отсутствие ничего не ломало.

Что появится для фронта.

ПоверхностьОперации
Фаундердиалог с агентом порождает версию спецификации с типизированными вилками
Вебхукиприёмник трекера (no-op)

Источники: шаг 2 §4.1, §10; ФТ C-1; решение №10.

Что делаем. Контракт вилок C-1, запись версий Specification через core-модуль project-api, а не прямыми запросами. Порт трекера (no-op), TrackerLink, вебхук-приёмник, integration-dispatcher в воркере.

Приёмка.

  • HTTP-e2e: диалог с агентом создаёт версию спеки, которую фаундер принимает штатным путём.
  • Агент не пишет в Specification иначе как через core-модуль — тест границ.
  • Невалидная вилка отвергается на входе, а не сохраняется частично.
  • Падающий порт трекера не влияет ни на один переход машин.

Статус: TODO Ветка / PR:Размер: M · Зависит от: D2, D3, S1, S2, S3 · Фронт:

Бизнес-задача. Проверить, что срезы, написанные в разных сессиях с разницей в недели, действительно складываются в один продукт.

Источники: шаг 4 §9; шаги 5–7.

Что делаем. Только сквозная сборка — инвариант-тесты уже написаны внутри своих единиц. Матрица-раннер шага 4 §7 как параметризованный прогон по каждой клетке. Сводный escrow-линтер по всему репозиторию. Сквозные e2e: счастливый путь целиком, спор, расторжение.

Приёмка.

  • Матрица-раннер покрывает каждую клетку таблицы доступа; пропуск клетки роняет тест.
  • Счастливый путь проходит на sandbox-провайдерах без ручных шагов.
  • Escrow-линтер зелёный на всём репозитории.

Статус: TODO Ветка / PR:Размер: S · Зависит от: G1 · Фронт:

Источники: шаг 8; решение №31.

Что делаем. Окно обслуживания: drop + baseline + сиды на проде, редеплой всех сервисов, чек-лист 8.3, смоук.

Приёмка. Чек-лист 8.3 приложен к записи в SESSION-LOG.md; смоук зелёный на всех сервисах.

⚠️ Единственная единица, действующая на боевой контур. Только по явному подтверждению Stark’а в этой же сессии — не по факту того, что зависимости DONE.


Статус: TODO Ветка / PR:Размер: S · Зависит от: G2 · Фронт:

Что делаем. Сценарии k6: регистрация → контракт, фондирование, приёмка.

Приёмка. Три сценария дают отчёт с p95; пороги зафиксированы в документации операций.