# Заметки для исполнителей (Codex / Antigravity) Автор: Grok (стенд тестов и независимая приёмка). Я не меняю `src/`, `apps/`, `migrations/`, `deploy/`; всё, что требует изменения продукта, описано здесь. Как запускать: ```sh # bootstrap-админ PostgreSQL 17 (только создаёт роли и БД; сервер и миграции под ним не работают) export E2E_PG_ADMIN_URL=postgres://:@127.0.0.1:55433/postgres node scripts/test/run-e2e.mjs # храповик known-defects (как в CI) node scripts/test/run-e2e.mjs --no-known-defects # строгий режим: все дефекты красные E2E_MIGRATIONS=roundtrip node scripts/test/run-e2e.mjs # + откат всех миграций перед запуском ./scripts/test/ci-local.sh # локальный эквивалент verify/format/e2e ``` **Правило храповика.** Исправили дефект → его сценарий позеленел → `run-e2e.mjs` падает с «дефект исправлен, обновите known-defects.json» — удалите ID из `tests/e2e/known-defects.json` в том же коммите. Новый красный сценарий, которого нет в списке, — регрессия. ## 1. Предполагаемый контракт для решений владельца (OWN-01..11) — Codex, этап 4 API пока нет, поэтому сценарии `tests/e2e/30-owner-decisions.e2e.ts` написаны против наиболее вероятного контракта. Если выбираете другой — поменяйте тесты и этот раздел в том же PR, смысл проверок (кто что может и когда) менять нельзя. Все пути относительно `/api/v1/staff`. ### C1. Роли точки/бухгалтера/директора (standalone-аналог Core RBAC) - `POST /access-grants` (только главный менеджер, `staff:employee:write`) тело `{ "role": "point_manager" | "accountant" | "director", "org_unit_ids": [""], "display_name": "..." }` → `201 { "id", "role", "org_unit_ids", "access_token" }`. Токен показывается один раз, хранится как хеш (как ключи сотрудников), используется как `Authorization: Bearer`. - `point_manager` видит/считает только свои `org_unit_ids`; чужая точка → `403` (и для нового `POST /payroll/batches`, и для старого `POST /payroll/calculate`). - В platform-режиме те же роли приходят из Core (User ≠ Employee, AGENTS.md). ### C2. Пакет зарплаты точки и согласование | Метод и путь | Кто | Результат | | ------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `POST /payroll/batches` `{org_unit_id, from, to}` | accountant; point_manager своей точки | `201 {id, status:"draft", org_unit_id, lines:[{work_session_id, employee_id, org_unit_id, amount_minor}]}` | | `POST /payroll/batches/:id/submit` | только accountant | `200 {status:"submitted"}`; point_manager → `403`; повтор после подписи → `409` | | `GET /notifications` | любой пользователь | `200 {notifications:[{type:"payroll.batch.submitted", subject_id:}]}`; каждому директору ровно одно уведомление на отправку | | `POST /payroll/batches/:id/sign` | только director | `200 {status:"signed"}`; accountant → `403`; если смена пакета изменилась после отправки → `409`, пакет получает `status:"returned"` | | `POST /payroll/batches/:id/return` `{comment}` | director | `200 {status:"returned", return_comment}` | | `GET /payroll/batches/:id` | accountant, director | `200 {status, return_comment, lines:[...]}` | Инварианты, которые проверяют тесты: - до подписи в `staff_payroll_ledger` для сотрудников пакета нет ни `accrual`, ни `payout` (OWN-05); - подпись атомарно пишет `accrual` и `payout`; после подписи `PATCH /work-sessions/:id` для смен пакета → `409`, пакет не отправляется повторно (OWN-06); - строка пакета принадлежит точке, где отработана смена (`staff_work_sessions.org_unit_id`), а не основной точке сотрудника (OWN-09). ### C3. Согласие на биометрию - `PUT /employees/:id/biometric-consent` `{ "status": "granted" | "revoked", "document_ref": "..." }` (главный менеджер) → `200`. - `GET /terminal/employees/:id/enrollment` (терминальный токен) → `200 { "employee_id", "registration_allowed": boolean, "reason": "consent_missing" | "consent_revoked" | null }`. Без записи о согласии — `false/consent_missing` (OWN-10); после отзыва — `false/consent_revoked` (OWN-11). - Шаблоны отпечатков на сервер не передаются (docs/TERMINAL-API-REQUIREMENTS.md); сервер хранит только факт согласия. Тот же флаг стоит отдать в `/sync/pull` (employee payload). Сейчас все OWN красные с `404 Route ... not found` (C1–C3 не реализованы); OWN-03..09 дополнительно упрутся в N2 (подтверждение смены падает 500). ## 2. Дефекты продукта, найденные сценариями (все красные сейчас) Команда и фактический вывод — `tests/e2e/evidence/e2e-run.txt`; таблица — `tests/e2e/REPORT.md`. | ID | Файл | Что исправить | | -------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | N1 | `src/portal.ts` POST /employees | `42P08 inconsistent types deduced for parameter $2`: явные `$2::varchar(26)`, `$5::text` или отдельный запрос профессии | | N2 | `src/portal.ts` PATCH /work-sessions, `src/domain/payroll/service.ts` calculate/approve | `staff_outbox.occurred_at` NULL (23502): передавать `occurred_at` или `DEFAULT now()` новой миграцией | | N4 | `service.ts` | после N2 проверить: пересекающийся период не должен начислять смену повторно (409 или исключение уже начисленных смен) | | N3 | push-подписки | allowlist push-сервисов / запрет приватных адресов, `redirect:'manual'`; сейчас сервер подключается к `127.0.0.1` | | N9 | `push-dispatcher.ts` | `ON CONFLICT ... DO UPDATE SET employee_id` → 409 для чужого endpoint | | N10 | GET /professions, /org-units | требовать право справочника; терминал сейчас получает 200 | | N11 | импорт | валидировать `kind` и схему `items` → 400 вместо 500 (23514) | | N12 | PUT pay-assignment | `additionalProperties:false`, верхние границы сумм | | F2 | `config.ts`/`identity.ts` | standalone без токенов не должен выдавать права (процесс без токенов отдаёт 200 на /portal, /payroll, POST /professions) | | F3 | POST/DELETE /session | в cookie — идентификатор сессии, а не сырой токен; logout отзывает сессию | | F4 | вход | rate limit (тест ждёт 429 не позже 30 неверных попыток; идёт на отдельном процессе, чтобы лимит не мешал другим сценариям) | | F7 | POST /attendance/events | использовать `Idempotency-Key` / `staff_idempotency_keys` | | F10 | `assertSameOrigin` | мутации по cookie без `Origin` отклонять | | SYNC-A/B | `src/domain/sync/service.ts` | устройство не должно выставлять `confirmed`/`approved` (сейчас `applied`) | | SYNC-C0 | `pull()` bootstrap | `SELECT ... status FROM staff_org_units` — такого столбца нет → 500 на любой начальной загрузке | | SYNC-C | `pull()` bootstrap | при `has_more=true` отдаётся `next_cursor=latest`, остаток после 100 сущностей теряется; нужен курсор снимка | | SYNC-D | портал/импорт | изменения вне sync не пишутся в `staff_sync_change_log` → терминал не узнаёт о новых/изменённых сотрудниках | | SYNC-E | `executeCommand` | проверять `status='active'` сотрудника | | SYNC-F | `executeCommand` | сохранять `org_unit_id` в `staff_attendance_events`; payload не может переопределить точку устройства | | SYNC-G | `push()` | ошибки БД одной команды → `rejected` для неё, а не 500 и откат всего пакета | | SYNC-H | `/sync/devices` | `assertCookieMutationOrigin` как в портале | | SYNC-I | `requireManagerContext` | без токенов не выдавать права (как F2) | | SYNC-J | `executeCommand` | окно времени отметки как в POST /attendance/events (−180 дней … +5 мин) | | SYNC-K | `acknowledge()` | отклонять курсор больше `MAX(cursor_seq)` | ## 3. deploy/ (PILOT-1, PILOT-2) — Codex - PILOT-1: все подстановки в `deploy/pilot/compose.yaml` → `${VAR:?VAR is required}` (сейчас 14 без `:?`: пустой `STANDALONE_MANAGER_TOKEN` молча превращает pilot в F2). - PILOT-2: `NODE_ENV: development` в `app`. Конфликт: `config.ts` запрещает `STAFF_RUNTIME_MODE=standalone` при `NODE_ENV=production`. Нужно решение: либо разрешить standalone в production при обязательных токенах и HTTPS, либо отдельный режим для pilot. Тест требует только «не development». - N6: `scripts/create-app-role.mjs` не работает мигратором без SUPERUSER («Only roles with SUPERUSER may change the SUPERUSER attribute»). CI и e2e обходятся `scripts/test/provision-pg.mjs` (роли создаёт bootstrap-админ, как `docker/postgres/init-app-role.sh`). В pilot мигратор = `POSTGRES_USER` = суперпользователь; рекомендую схему как в provision-pg.mjs. ## 4. CI - Задаче `docker-build` нужен раннер с меткой `docker-host` (Docker daemon). Без него задача ждёт в очереди; остальные задачи не блокируются. - `format` красная, пока не исправлен F14 (25 файлов, в основном `src/`, `docs/`, `tests/unit/`). - Реальный Forgejo-раннер мной не запускался (нет API-токена) — не проверено.