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 | Единица | Фронт | Размер | Зависит от |
|---|---|---|---|---|---|
| 1 | F1 | Ядро: деньги, события, календарь | — | M | — |
| 2 | F2 | Целевая схема, сиды, платформенные роли | — | M | — |
| 3 | T1 | Команды: состав, приглашения, полномочия | ✓ | M | F2 |
| 4 | T2 | Машины состояний и события | ✓ | L | F1, T1 |
| 5 | O1 | Операционный контур: задачи и таймеры | ✓ | L | T2 |
| 6 | O2 | Уведомления | ✓ | M | O1 |
| 7 | T3 | Выплатной аккаунт команды | ✓ | M | T2, O1 |
| 8 | T4 | Публичная карточка команды | ✓ | S | T1 |
| 9 | P1 | Проект и спецификация | ✓ | M | T2, O1 |
| 10 | P2 | Мэтчинг и оффер | ✓ | M | P1, T1, T3 |
| 11 | P3 | Контрактинг: план, торг, KYC | ✓ | L | P2 |
| 12 | P4 | Договоры: шаблоны и генерация | ✓ | M | P3 |
| 13 | P5 | Подписание: конверты и вебхуки | ✓ | M | P4 |
| 14 | M1 | Этапы, критерии, артефакты | ✓ | M | P5 |
| 15 | M2 | Фондирование этапа | ✓ | L | M1, T3 |
| 16 | M3 | Сдача, приёмка, операторская поверхность | ✓ | L | M2 |
| 17 | M4 | Расчёт выплаты и подтверждение | ✓ | M | M3 |
| 18 | M5 | Исполнение выплаты | ✓ | L | M4 |
| 19 | M6 | Разбор непринятия | ✓ | M | M3, M4 |
| 20 | D1 | Спор и арбитраж | ✓ | L | M5, M6 |
| 21 | D2 | Расторжение | ✓ | L | D1 |
| 22 | D3 | Внешние chargeback’и | ✓ | S | M5 |
| 23 | S1 | Консьерж | ✓ | S | M3, O2 |
| 24 | S2 | Ночная сверка | ✓ | S | M5 |
| 25 | S3 | ai-agent и заглушка трекера | ✓ | M | P1, O1 |
| 26 | G1 | Кросс-QA | — | M | D2, D3, S1, S2, S3 |
| 27 | G2 | Переключение на baseline и смоук | — | S | G1 |
| 28 | G3 | Нагрузочное (k6) | — | S | G2 |
Вертикальная нарезка почти всё выстраивает в цепочку — это цена за то, что каждая единица
что-то даёт. Вне основной цепочки параллелятся 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Фундамент
Заголовок раздела «Фундамент»Две единицы без фронтовой поверхности. Дальше таких не будет.
F1 — Ядро: деньги, события, календарь
Заголовок раздела «F1 — Ядро: деньги, события, календарь»Статус: 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.
T2 — Машины состояний и события
Заголовок раздела «T2 — Машины состояний и события»Статус: 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 и поднимает алерт.
O2 — Уведомления
Заголовок раздела «O2 — Уведомления»Статус: 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 на шине).
T3 — Выплатной аккаунт команды
Заголовок раздела «T3 — Выплатной аккаунт команды»Статус: 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послеACTIVE→RESTRICTED+ задача администратору + уведомление команде. - Повторная доставка того же события:
SKIPPED_DUPLICATE, ноль переходов, ноль уведомлений. - Подпись чужим секретом отвергается до парсинга тела.
- Тест границ Nx: billing-модуль не импортируется в admin-api.
T4 — Публичная карточка команды
Заголовок раздела «T4 — Публичная карточка команды»Статус: 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по-прежнему требуют членства и по-прежнему отдают полномочия.
Путь до договора
Заголовок раздела «Путь до договора»P1 — Проект и спецификация
Заголовок раздела «P1 — Проект и спецификация»Статус: 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 на всех маршрутах фаундера.
P2 — Мэтчинг и оффер
Заголовок раздела «P2 — Мэтчинг и оффер»Статус: TODO Ветка / PR: — Размер: M · Зависит от: P1, T1, T3 · Фронт: ✓
Бизнес-задача. Администратор подбирает команду вручную и отправляет ей предложение. Команда принимает или отказывается с причиной. Только после согласия команды фаундер впервые её видит (P-1) — порядок зашит в машину, а не в интерфейс.
Сущности. Offer (машина SENT_TO_TEAM → TEAM_ACCEPTED → ACCEPTED_BY_FOUNDER, отказы
с обязательной причиной, отзыв администратором).
Что появится для фронта.
| Поверхность | Операции |
|---|---|
| Администратор | подобрать команду и отправить оффер · список офферов проекта · отозвать оффер |
| Команда | входящие офферы · карточка оффера с описанием проекта · принять · отклонить с причиной |
| Фаундер | увидеть предложенную команду (только после её согласия) · принять · отклонить с причиной |
Что можно собрать на фронте после. Экран входящих офферов у команды, экран «ваша команда»
у фаундера, админский мэтчинг. Проект доходит до статуса CONTRACTING.
Чего ещё нельзя. В CONTRACTING пока пусто — план и договоры в P3–P5.
Источники: шаг 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’ом конфликта интересов.
P3 — Контрактинг: план, торг, KYC
Заголовок раздела «P3 — Контрактинг: план, торг, KYC»Статус: 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.
P4 — Договоры: шаблоны и генерация
Заголовок раздела «P4 — Договоры: шаблоны и генерация»Статус: 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).
P5 — Подписание: конверты и вебхуки
Заголовок раздела «P5 — Подписание: конверты и вебхуки»Статус: 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).
Сделка в работе
Заголовок раздела «Сделка в работе»M1 — Этапы, критерии, артефакты
Заголовок раздела «M1 — Этапы, критерии, артефакты»Статус: 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падает на БД. - Операторский ответ по этапу не содержит ни одного денежного значения.
M2 — Фондирование этапа
Заголовок раздела «M2 — Фондирование этапа»Статус: 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не порождает перехода и не двигает деньги.
M4 — Расчёт выплаты и подтверждение
Заголовок раздела «M4 — Расчёт выплаты и подтверждение»Статус: 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.
M5 — Исполнение выплаты
Заголовок раздела «M5 — Исполнение выплаты»Статус: 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.paid→RELEASED→ этапPAID→ последний этап доводит проект доCOMPLETED, проект read-only. - «Выплачено» не показывается команде до
RELEASED— снапшот-тест трёх состояний. - Повторный запуск частично исполненного сплита не создаёт второй transfer и второй refund.
- Transfer на сумму больше холда невозможен — и нашим guard’ом, и
source_transaction. payout.failedне откатывает холд, а создаёт задачу.
M6 — Разбор непринятия
Заголовок раздела «M6 — Разбор непринятия»Статус: 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.
Конфликты
Заголовок раздела «Конфликты»D1 — Спор и арбитраж
Заголовок раздела «D1 — Спор и арбитраж»Статус: 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, при PARTIAL — teamShareBps. Точечная заморозка 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.
D2 — Расторжение
Заголовок раздела «D2 — Расторжение»Статус: 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).
D3 — Внешние chargeback’и
Заголовок раздела «D3 — Внешние chargeback’и»Статус: 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, ни в строках интерфейса.
Обвязка
Заголовок раздела «Обвязка»S1 — Консьерж
Заголовок раздела «S1 — Консьерж»Статус: 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обновлён так, чтобы исключение было явным, а не случайным.
S2 — Ночная сверка
Заголовок раздела «S2 — Ночная сверка»Статус: 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 «на чтение всего».
S3 — ai-agent и заглушка трекера
Заголовок раздела «S3 — ai-agent и заглушка трекера»Статус: 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-модуль — тест границ. - Невалидная вилка отвергается на входе, а не сохраняется частично.
- Падающий порт трекера не влияет ни на один переход машин.
Закалка
Заголовок раздела «Закалка»G1 — Кросс-QA
Заголовок раздела «G1 — Кросс-QA»Статус: TODO Ветка / PR: — Размер: M · Зависит от: D2, D3, S1, S2, S3 · Фронт: —
Бизнес-задача. Проверить, что срезы, написанные в разных сессиях с разницей в недели, действительно складываются в один продукт.
Источники: шаг 4 §9; шаги 5–7.
Что делаем. Только сквозная сборка — инвариант-тесты уже написаны внутри своих единиц. Матрица-раннер шага 4 §7 как параметризованный прогон по каждой клетке. Сводный escrow-линтер по всему репозиторию. Сквозные e2e: счастливый путь целиком, спор, расторжение.
Приёмка.
- Матрица-раннер покрывает каждую клетку таблицы доступа; пропуск клетки роняет тест.
- Счастливый путь проходит на sandbox-провайдерах без ручных шагов.
- Escrow-линтер зелёный на всём репозитории.
G2 — Переключение на baseline и смоук
Заголовок раздела «G2 — Переключение на baseline и смоук»Статус: TODO Ветка / PR: — Размер: S · Зависит от: G1 · Фронт: —
Источники: шаг 8; решение №31.
Что делаем. Окно обслуживания: drop + baseline + сиды на проде, редеплой всех сервисов, чек-лист 8.3, смоук.
Приёмка. Чек-лист 8.3 приложен к записи в SESSION-LOG.md; смоук зелёный на всех сервисах.
⚠️ Единственная единица, действующая на боевой контур. Только по явному подтверждению Stark’а в этой же сессии — не по факту того, что зависимости
DONE.
G3 — Нагрузочное (k6)
Заголовок раздела «G3 — Нагрузочное (k6)»Статус: TODO Ветка / PR: — Размер: S · Зависит от: G2 · Фронт: —
Что делаем. Сценарии k6: регистрация → контракт, фондирование, приёмка.
Приёмка. Три сценария дают отчёт с p95; пороги зафиксированы в документации операций.