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

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 }

Критерии приёмки (из карточки):

  1. prisma migrate reset на чистой БД даёт целевую схему; prisma validate зелёный.
  2. Удаление проекта со сделочной цепочкой падает на уровне БД, не сервиса.
  3. Settlement, чьи компоненты не суммируются в холд, отвергается БД.
  4. Негативные тесты кросс-ролевого доступа: оператор → админский маршрут 403, арбитр → операторский 403, админ → арбитражный 403. Вторая платформенная роль пользователю не выдаётся.
  5. Сверка enum’ов с union’ами domain-events — расхождение в одном значении роняет прогон.
  6. ArbiterGuard в billing-api не регистрируется — тест отсутствия маршрутов.
  7. Чек-лист шага 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 — три платформенные роли с детерминированными id
  • apps/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.ts
  • libs/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.tsOPERATOR_ROLE_ID, ARBITER_ROLE_ID
  • libs/apis/providers/auth-api/features/token/src/lib/token.service.tsplatformRole в TokenPayload
  • libs/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/**AdministratorGuard
  • libs/apis/providers/project-api/**, admin-project.service.ts — удаление таблиц переходов статуса
  • apps/admin-api/src/app/app.module.ts — подключение WorkspaceModule
  • package.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/
  • 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 отвергает пропуск обязательного, геттеры типизированы
  • T4. Baseline-миграция: squash 15 папок в одну, raw-SQL (CHECK Settlement, частичный индекс QueueItem, частичный индекс одной платформенной роли) — тест: migrate reset на чистой БД зелёный
  • T5. Сиды: роли, справочники, dev-фикстуры — тест: повторный прогон идемпотентен
  • T6. Удаление старой машины статусов проекта (PROJECT_STATUS_TRANSITIONS, ProjectUpdateStatusDto, PATCH-маршруты) — тесты соседних сервисов остаются зелёными
  • T7. platformRole в JWT: TokenPayload, выдача в AdminAuthVerificationService, отказ при двух платформенных ролях — тесты: claim присутствует, отказ логина, warn без секретов
  • T8. Три guard’а в shared — тесты: пропускают свою роль, дают 403 чужой, не подменяют аутентификацию
  • T9. Библиотека admin-api-feature-workspace: три контроллера /me, DTO ответа, Swagger
  • T10. AdministratorGuard на существующие /projects и /users; admin-contact не трогается
  • T11. Интеграционные тесты схемы: Restrict на сделочной цепочке, CHECK Settlement, частичный индекс дедупа, вторая платформенная роль отвергается БД
  • 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 и /users admin-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 на сделочной цепочке, CHECK Settlement, частичный индекс дедупа 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’ов — третья; тесты — четвёртая.