F2 — Целевая схема, сиды, платформенные роли
Цель: в базе появляются все 32 модели сделочного контура одной baseline-миграцией, а платформа получает три раздельные роли — оператор, администратор, арбитр — с запретом совмещения.
Источники: шаг 8 целиком · шаг 2 целиком · шаг 6 §2 (templateVersion, documentHash) ·
шаг 7 §2 (частичный индекс дедупа QueueItem), §5 (ConciergeThread, ConciergeMessage) ·
шаг 4 §1, §7, §8 · решения №12, №13, №19, №31.
API для фронта:
| Метод | Путь | Кто вызывает | Назначение | Возвращает |
|---|---|---|---|---|
| POST | /api/v1/auth/admin/login + /login/verify | сотрудник платформы | существующий 2FA-логин, теперь кладёт в JWT claim platformRole | пара токенов; в payload platformRole |
| GET | /api/v1/operator/me | оператор | кто я и какое у меня рабочее место | { userId, email, platformRole: OPERATOR } |
| GET | /api/v1/administrator/me | администратор | то же | { ..., platformRole: ADMIN } |
| GET | /api/v1/arbiter/me | арбитр | то же | { ..., platformRole: ARBITER } |
Критерии приёмки (из карточки):
prisma migrate resetна чистой БД даёт целевую схему;prisma validateзелёный.- Удаление проекта со сделочной цепочкой падает на уровне БД, не сервиса.
Settlement, чьи компоненты не суммируются в холд, отвергается БД.- Негативные тесты кросс-ролевого доступа: оператор → админский маршрут 403, арбитр → операторский 403, админ → арбитражный 403. Вторая платформенная роль пользователю не выдаётся.
- Сверка enum’ов с union’ами
domain-events— расхождение в одном значении роняет прогон. ArbiterGuardв billing-api не регистрируется — тест отсутствия маршрутов.- Чек-лист шага 8 §3 пройден.
Создать
docker-compose.test.yml— PostgreSQL 16 на 55432 для интеграционных прогоновapps/core-api/prisma/migrations/20260830000000_baseline/migration.sql— единственная миграцияapps/core-api/prisma/seeds/platform-role.seed.ts— три платформенные роли с детерминированными idapps/core-api/prisma/seeds/dev-fixtures.seed.ts— пользователи платформы, фаундер, командаlibs/apis/configs/shared/platform/**— конфиг-модуль параметров платформы (окна, SLA, комиссия, пороги)libs/apis/shared/src/lib/guards/operator.guard.ts,administrator.guard.ts,arbiter.guard.tslibs/apis/providers/admin-api/features/workspace/**— три контроллера/meи общий сервисapps/core-api-e2e/jest-integration.config.ts+src/schema/*.spec.ts— инварианты схемы и сверка enum’овapps/admin-api-e2e/jest-integration.config.ts+src/admin-api/role-matrix.spec.ts— HTTP-матрица ролей
Изменить
apps/core-api/prisma/schema.prisma— целевая схема (32 модели, 34 новых enum’а)apps/core-api/prisma/seeds/init.seed.ts— подключение новых сидовlibs/apis/shared/src/lib/constants/roles.constants.ts—OPERATOR_ROLE_ID,ARBITER_ROLE_IDlibs/apis/providers/auth-api/features/token/src/lib/token.service.ts—platformRoleвTokenPayloadlibs/apis/providers/auth-api/features/admin-auth/src/lib/admin-auth-verification.service.ts— выдача claim’а и отказ при двух платформенных роляхlibs/apis/providers/admin-api/features/admin-project/**,admin-user/**—AdministratorGuardlibs/apis/providers/project-api/**,admin-project.service.ts— удаление таблиц переходов статусаapps/admin-api/src/app/app.module.ts— подключениеWorkspaceModulepackage.json— скриптыtest:db:up/test:db:down- документация:
docs/05-data/database-schema.md,migrations.md,seeds.md,docs/04-shared-and-utils/shared-lib.md,configs.md,docs/06-operations/environment.md,local-dev.md,docs/03-services/
Группа 1 (параллельно)
Заголовок раздела «Группа 1 (параллельно)»- T1. Целевая
schema.prisma: 32 модели, enum’ы, политикаRestrict, дельты шагов 6 §2 и 7 §5 — проверка:prisma validate,migrate diffдаёт SQL без ошибок - T2. Тестовая БД и интеграционный обвес:
docker-compose.test.yml, npm-скрипты, дваjest-integration.config.ts, таргетыintegration— проверка: пустой прогон зелёный на поднятой БД - T3. Конфиг-модуль
configs/shared/platform: окна 5/5/5, конверт 14 дней, комиссия 1500 bps, порог бюджета, порог LCIA (без дефолта) — тесты: Joi отвергает пропуск обязательного, геттеры типизированы
Группа 2 (после T1)
Заголовок раздела «Группа 2 (после T1)»- T4. Baseline-миграция: squash 15 папок в одну, raw-SQL (CHECK
Settlement, частичный индексQueueItem, частичный индекс одной платформенной роли) — тест:migrate resetна чистой БД зелёный - T5. Сиды: роли, справочники, dev-фикстуры — тест: повторный прогон идемпотентен
- T6. Удаление старой машины статусов проекта (
PROJECT_STATUS_TRANSITIONS,ProjectUpdateStatusDto, PATCH-маршруты) — тесты соседних сервисов остаются зелёными
Группа 3 (после T4)
Заголовок раздела «Группа 3 (после T4)»- T7.
platformRoleв JWT:TokenPayload, выдача вAdminAuthVerificationService, отказ при двух платформенных ролях — тесты: claim присутствует, отказ логина,warnбез секретов - T8. Три guard’а в
shared— тесты: пропускают свою роль, дают 403 чужой, не подменяют аутентификацию
Группа 4 (после T7, T8)
Заголовок раздела «Группа 4 (после T7, T8)»- T9. Библиотека
admin-api-feature-workspace: три контроллера/me, DTO ответа, Swagger - T10.
AdministratorGuardна существующие/projectsи/users;admin-contactне трогается
Группа 5 (после всех)
Заголовок раздела «Группа 5 (после всех)»- T11. Интеграционные тесты схемы:
Restrictна сделочной цепочке, CHECKSettlement, частичный индекс дедупа, вторая платформенная роль отвергается БД - T12. Сверка Prisma-enum’ов с union’ами
domain-events— рантайм и компиляция - T13. HTTP-матрица ролей по реальной БД: 200 на своём
/me, 403 на чужих, 403 оператору на/projects; тест отсутствияArbiterGuardвне admin-api - T14. Документация по
documentation-rules.mdи чек-лист шага 8 §3
Решения реализации
Заголовок раздела «Решения реализации»Проверены субагентом
crewsforge-decision-reviewerв два круга. Первый круг: 6 возражений — все приняты и блок переписан. Второй круг: возражений нет, четыре пропуска (логин не-ADMIN, потеря claim’а при refresh, unit-спеки снятой поверхности, свежесть сгенерированного клиента) — все закрыты решениями ниже.
- Squash миграций: 15 существующих папок в
apps/core-api/prisma/migrations/удаляются, вместо них одна20260830000000_baseline/migration.sql— шаг 8 §1 п.2 («история схлопывается в один baseline»), решение №31; данных беты нет, expand/backfill снят. - Способ генерации baseline-SQL:
prisma migrate diff --from-empty --to-schema-datamodelв файл, затем ручная дописка raw-SQL блоков — генератор Prisma не умеет CHECK и частичные индексы (шаг 8 §1 п.3); ручной SQL целиком отвергнут: 32 модели руками — источник опечаток, которые не ловитprisma validate. @@mapна всех новых моделях и@mapна всех полях: имена таблиц — snake_case во множественном числе (teams,team_members,state_transitions) —coding-rules.md§1.11; step-документ 2 их не называет, потому что описывает домен, а не физические имена.- CHECK суммы Settlement: денормализованное поле
holdAmountMinor(hold_amount_minor) +CHECK (team_amount_minor + founder_amount_minor + platform_fee_minor = hold_amount_minor)в baseline — решение №13, шаг 2 §7; сверка значения с самим холдом остаётся в транзакции сервиса (M4). - Частичный индекс дедупа QueueItem:
CREATE UNIQUE INDEX ... ON queue_items (type, subject_type, subject_id) WHERE status IN ('OPEN','IN_PROGRESS')в raw-SQL baseline — шаг 7 §2, решение №28. - Политика удаления:
onDelete: Restrictна всей сделочной цепочке и наCustomer → Project,User → Customer/Employee; профильные сателлиты остаютсяCascade— шаг 2 §2.3, решение №12.TeamInvitation → TeamостаётсяCascade(приглашение не сделочная запись) — так в шаге 2 §3. - Запрет совмещения платформенных ролей — на уровне БД, а не только сервиса: id ролей
детерминированы (
ADMIN_ROLE_IDуже константа вshared; добавляютсяOPERATOR_ROLE_ID,ARBITER_ROLE_ID), поэтому в baseline ложится частичный уникальный индексUNIQUE (user_id) WHERE role_id IN (<три платформенных uuid>). Основание: решение №19 («одна платформенная роль на пользователя»); сервисная проверка одна не годится — назначение роли живёт в двух местах (registerVerify, сиды), и третье появится в админке. platformRoleв JWT несёт значенияRoleType, неPlatformRole: claim —OPERATOR | ADMIN | ARBITERдословно по шагу 4 §1, выводится изUserOnRoleвAdminAuthVerificationService.PlatformRoleсхемы шага 2 §9 используетADMINISTRATORвместоADMIN— это другое множество, оно типизируетQueueItem.targetRole, а не токен. ОтображениеRoleType.ADMIN → PlatformRole.ADMINISTRATORоформляется картой вsharedи покрывается тестом; потребители появятся вO1.TokenPayloadрасширяется необязательным полем: тот же тип обслуживает customer- и employee-токены.- Отказ логина при двух платформенных ролях:
ForbiddenExceptionв момент выдачи токена,warnв лог — вторая линия к индексу БД на случай данных, залитых в обход (решение №19). - Три guard’а:
OperatorGuard,AdministratorGuard,ArbiterGuardвlibs/apis/shared/src/lib/guards/, каждый читаетrequest.user.platformRoleи применяется в паре сAdminAccessTokenGuard— шаг 4 §1; собственной аутентификации не делают, только роль. - Фича
/me: одна библиотекаlibs/apis/providers/admin-api/features/workspace(Nx-имяadmin-api-feature-workspace, алиас@crewsforge-back/apis/providers/admin-api/features/workspace) с тремя контроллерами по префиксамoperator/administrator/arbiter— шаг 4 §8; три отдельные библиотеки отвергнуты: у них общий сервис и общий DTO. AdministratorGuardна существующие/projectsи/usersadmin-api: администраторская поверхность по матрице шага 4 §7; без этого оператор и арбитр получают админские данные.admin-contactне трогается — это публичные формы лендинга.- Тестовая БД:
docker-compose.test.ymlв корне, PostgreSQL 16 на порту 55432 (5432 и 5433 заняты), npm-скриптыtest:db:up/test:db:down; интеграционные прогоны требуют поднятой БД и падают с внятным сообщением, если её нет. Testcontainers отвергнут: новая зависимость ради того, что делает один compose-файл. - Где живут БД-тесты: новый таргет
integrationуcore-api-e2e(инварианты схемы:Restrictна сделочной цепочке, CHECKSettlement, частичный индекс дедупаQueueItem, частичный индекс одной платформенной роли, SQL-проверка отсутствияCASCADEна всём путиProject → Settlementпоpg_constraint.confdeltype, применимость сидов) и уadmin-api-e2e(HTTP-матрица ролей поверх реальногоAppModuleи реальной БД). Отдельный таргет, а неtest:nx affected -t lint,test,buildне должен требовать Docker. - Сверка enum’ов — обычный unit-тест под таргетом
test, а неintegration: базы она не требует, а в Docker-зависимом таргете перестала бы падать в штатном прогоне, чего карточка прямо требует («расхождение хотя бы в одном значении роняет прогон»). Для этого уcore-apiзаводитсяjest.config.tsи таргетtest, спек лежит рядом со схемой. Форма: рантайм-сравнениеObject.values(SubjectType)(top-level импорт из@prisma/client, неPrisma.SubjectType— такого свойства в сгенерированном клиенте нет) с массивом, типизированным какSubjectType[]изdomain-events, плюсRecord<SubjectType, true>— ловит расхождение в обе стороны: лишнее значение в Prisma валит рантайм-assert, лишнее в union’е — компиляцию ts-jest. - Тест «
ArbiterGuardне регистрируется вне admin-api» — статический unit под тем же таргетомtestвapi-shared(там, где guard объявлен): читаетapps/*/src/app/app.module.tsи барели провайдеров и падает, еслиArbiterGuardимпортируется приложением, отличным от admin-api. Базы и Docker’а не требует;T3расширяет его наbilling-apiпри создании сервиса (согласовано Stark’ом). - HTTP-матрица ролей: supertest поверх
Test.createTestingModule({ imports: [AppModule] })admin-api с реальной БД и реальными guard’ами; токены выпускаются настоящимTokenAdminService. Моки guard’ов и транзакций запрещены протоколом. Матрица — новый спек; судьба существующего мок-спека — отдельным решением ниже. - Сиды:
apps/core-api/prisma/seeds/пополняетсяplatform-role.seed.ts(пять ролей шага 8 §2 —CUSTOMER,EMPLOYEE,ADMIN,OPERATOR,ARBITER— по детерминированным id: два существующих изuser-api/data-access/role.constants.ts,ADMIN_ROLE_IDизshared, два новых) иdev-fixtures.seed.ts(три пользователя платформы, фаундер, команда с ролями и полномочиями, проект в двух опорных состояниях — шаг 8 §2 дословно). Dev-фикстуры подAPP_ENV in (development, stage), как уже устроенinit.seed.ts; оба сида идемпотентны (upsertпо детерминированным id). - Конфиг-модуль параметров платформы в
F2не заводится: шаг 8 §2 перечисляет конфиги (календарь, SLA, окна 5/5/5, конверт 14 дней, 1500 bps, пороги) среди сидов, но карточкаF2(«Что делаем») их не называет, ни одного потребителя в этой единице нет (таймеры —O1, очередь —O1, деньги —M2/M4), а календарь уже имеет форму константыapi-core-platform-calendar, сданнойF1. Заводить второй источник истины и модуль без потребителя запрещает правило роадмапа «инфраструктура прикреплена к первому срезу, которому нужна». Параметры приезжают со своими потребителями; таблицы для них не вводится — её нет в схеме шага 2. Расхождение фиксируется в «Отклонениях» лога. - Старый
ProjectStatusи поверхность смены статуса: enum переписывается на 10 значений шага 2 §2.1. Удаляются обе копииPROJECT_STATUS_TRANSITIONS(project-api/data-access/.../project-status-transitions.tsи константа вadmin-project.service.ts),ProjectUpdateStatusDto, founder-маршрутPATCH /api/v1/customers/me/projects/:projectId/statusи полеstatusвAdminProjectUpdateDto(отдельного PATCH-маршрута статуса в admin-api нет — статус менялся полем общего PATCH). Подтверждено Stark’ом 2026-08-30; строка шага 8 §3 уже правлена с «входит в эпик A1» на «выполнено вF2». Смена статуса возвращается вP1переходами машины, с журналом и событиями. Вместе с поверхностью правятся её unit-спеки под таргетомtest, иначеnx affected -t testкрасный:admin-project.service.spec.ts(кейсы переходовIN_PROGRESS → REVIEW,COMPLETED → PUBLISHED, таблицы валидных/невалидных пар) иadmin-project.controller.spec.ts(?status=IN_PROGRESS). Кейсы валидации переходов удаляются вместе с самой валидацией; фильтр по статусу переводится на значение из нового enum’а. admin-e2e-flows.spec.tsчинится, а не остаётся как есть: спек на моках ломается двумя решениями этой единицы —overrideGuardне знает проAdministratorGuard(403 вместо 200) и посылаетstatus: 'IN_PROGRESS', которого не станет в enum’е (400 вместо 200). Правка минимальная: переопределение нового guard’а и актуальные значения статуса. Замена его матрицей на реальной БД — не здесь: новая матрица заводится отдельным спеком, старый мок-спек остаётся как регрессия контроллерных цепочек.- Логин admin-api пускает все три платформенные роли: в
AdminAuthService.loginусловиеhasAdminRole(жёсткоеrole.type === 'ADMIN') заменяется на «есть ровно одна роль изADMIN | OPERATOR | ARBITER». Без этого сидовые оператор и арбитр не получают даже кода подтверждения, а карточкаF2обещает логин всеми тремя. Захардкоженныйroles: ['ADMIN']в payload’еAdminAuthVerificationServiceзаменяется на реальные роли пользователя — для оператора он сейчас лжёт. platformRoleперевыводится при каждом выпуске пары, включая refresh:AdminAuthRefreshTokenServiceсегодня собирает payload из воздуха (roles: ['ADMIN'], без обращения к БД), поэтому после первого рефреша claim исчез бы и все три guard’а начали бы отдавать 403 — включая/projectsи/users. Роли иplatformRoleчитаются изUserOnRoleв момент выпуска; если платформенной роли у пользователя больше нет, рефреш отвечает 403, а не выдаёт токен без claim’а. Вывод claim’а — общий хелпер вauth-api/features/token, чтобы три точки выпуска не разъехались.- Свежесть сгенерированного клиента в сверке enum’ов: тест сверяет ТРИ источника — текст
apps/core-api/prisma/schema.prisma(парсится регуляркой по блокамenum), значения из@prisma/clientи union’ыdomain-events. Плюс таргетtestуcore-apiполучаетdependsOn: ["prisma:schema:generate"]. Одного сравнения со сгенерированным клиентом мало: правка схемы безprisma generateдала бы молчаливо зелёный прогон — ровно тот дефект, против которого написан критерий карточки. - Разбиение на группы агентов: schema.prisma — один файл, один агент, первая группа; guard’ы,
JWT-claim, тестовая инфраструктура и сиды не пересекаются по файлам и идут параллельно во второй;
контроллеры
/meи навешивание guard’ов — третья; тесты — четвёртая.