chore(openspec): архив change slice-0-skeleton
Срез 0 реализован и влит (PR #3), change перенесён в openspec/changes/archive, спеки — в openspec/specs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
adb9ba47dc
commit
db7df33517
31
openspec/specs/audit-trail/spec.md
Normal file
31
openspec/specs/audit-trail/spec.md
Normal file
@ -0,0 +1,31 @@
|
||||
# audit-trail Specification
|
||||
|
||||
## Purpose
|
||||
Все действия из enum `audit_action` пишутся в `audit_event` одним способом — через сервис аудита; журнал только
|
||||
дополняется, записи неизменяемы (ТЗ 8.2).
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Единая точка записи
|
||||
Сервис аудита SHALL предоставлять метод записи события с полями `action` (из enum `audit_action`), `section`
|
||||
(из `audit_section`), `object_type`, `object_id`, `user_id` (NULL для системных действий), `consolidation_version_id`,
|
||||
`description`, `before_value`, `after_value` (jsonb), `comment`, `result`, `changed_field`, `ip_address`, `user_agent`;
|
||||
`occurred_at` SHALL проставляться сервером (UTC). Запись SHALL выполняться в той же транзакции, что и изменение данных.
|
||||
|
||||
NOTE: В Срезе 0 обмен через recognition_job аудит не пишет (рулинг R-7, design #20); сервис готов к использованию Срезом 2+.
|
||||
|
||||
#### Scenario: Системное событие
|
||||
- **WHEN** системный процесс (без пользователя) вызывает сервис аудита
|
||||
- **THEN** в `audit_event` появляется строка с `user_id = NULL`, `before_value`/`after_value` содержат старый и новый статус
|
||||
|
||||
#### Scenario: Откат транзакции
|
||||
- **WHEN** изменение данных и запись аудита выполняются в одной транзакции, и она откатывается
|
||||
- **THEN** строка `audit_event` не сохраняется
|
||||
|
||||
### Requirement: Журнал только дополняется
|
||||
ORM-модель `audit_event` MUST NOT использоваться для `UPDATE` или `DELETE` в коде ядра; тест SHALL проверять, что в
|
||||
`src/` нет обращений, изменяющих или удаляющих `audit_event`, а сервис аудита не имеет методов изменения.
|
||||
|
||||
#### Scenario: Попытка изменить запись
|
||||
- **WHEN** код вызывает обновление строки `audit_event` через сервис аудита
|
||||
- **THEN** такого метода нет (ошибка на этапе типизации/теста), запись остаётся прежней
|
||||
66
openspec/specs/authn-authz/spec.md
Normal file
66
openspec/specs/authn-authz/spec.md
Normal file
@ -0,0 +1,66 @@
|
||||
# authn-authz Specification
|
||||
|
||||
## Purpose
|
||||
Вход в ядро по JWT внешнего SSO (РАИСА): пользователь находится по email в `app_user`, его эффективные права — объединение
|
||||
прав его ролей из БД; любой защищённый маршрут объявляет требуемое право одной зависимостью. Для разработки — эмитент
|
||||
токенов за флагом.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Личность из JWT, пользователь из БД
|
||||
Защищённый маршрут SHALL требовать заголовок `Authorization: Bearer <jwt>`. Ядро SHALL проверить подпись и срок токена
|
||||
настроенным проверяющим (dev HS256 при `AUTH_DEV_ISSUER=true`, иначе JWKS/публичный ключ по `AUTH_*`), извлечь email из
|
||||
claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_user` с этим `email` и `is_active = true`. Нет или невалидный
|
||||
токен → 401; пользователь не найден или неактивен → 403. Тело ошибки — `{"detail": "<причина по-русски>"}`.
|
||||
|
||||
#### Scenario: Валидный токен активного пользователя
|
||||
- **WHEN** запрос несёт корректный токен с `email = ka_rf@armka.local`, и такой активный пользователь есть
|
||||
- **THEN** маршрут выполняется от имени этого `app_user`
|
||||
|
||||
#### Scenario: Пользователя нет в системе
|
||||
- **WHEN** токен валиден, но `app_user` с таким email отсутствует
|
||||
- **THEN** ответ 403, `detail` сообщает, что пользователь не зарегистрирован в АРМ
|
||||
|
||||
#### Scenario: Истёкший или подделанный токен
|
||||
- **WHEN** срок токена истёк или подпись не проходит
|
||||
- **THEN** ответ 401, тело `{"detail": …}`
|
||||
|
||||
### Requirement: Эффективные права и зависимость `require`
|
||||
`effective_permissions(user)` SHALL быть объединением `permission.code` по всем ролям пользователя
|
||||
(`user_role → role_permission → permission`); код роли в проверках MUST NOT использоваться. Зависимость
|
||||
`require("<PERMISSION_CODE>")` SHALL возвращать 403, если кода нет в эффективных правах, и MUST NOT обращаться к БД
|
||||
повторно в рамках одного запроса.
|
||||
|
||||
#### Scenario: Право есть
|
||||
- **WHEN** пользователь с ролью `KA_GO` вызывает маршрут с `require("KFO_VERIFY")`
|
||||
- **THEN** маршрут выполняется
|
||||
|
||||
#### Scenario: Права нет
|
||||
- **WHEN** пользователь с ролью `EXPERT` вызывает маршрут с `require("DOCUMENT_EDIT")`
|
||||
- **THEN** ответ 403, `detail` называет недостающее право
|
||||
|
||||
#### Scenario: Две роли
|
||||
- **WHEN** пользователю назначены роли `KA_RF` и `CURATOR_GO`
|
||||
- **THEN** его эффективные права — объединение прав обеих ролей без дубликатов
|
||||
|
||||
### Requirement: `GET /api/v1/me`
|
||||
Маршрут SHALL возвращать текущего пользователя: `id`, `email`, `full_name`, `regional_office {id, name, is_head_office}`,
|
||||
`business_segment`, `roles` (список `code`), `permissions` (отсортированный список `code`). Это единственный маршрут
|
||||
Среза 0, доступный любому аутентифицированному пользователю без `require`.
|
||||
|
||||
#### Scenario: Ответ для dev-пользователя
|
||||
- **WHEN** `ka_rf@armka.local` (роль `KA_RF`) запрашивает `/api/v1/me`
|
||||
- **THEN** ответ 200 содержит `roles = ["KA_RF"]` и `permissions`, равные правам `KA_RF` из матрицы §10
|
||||
|
||||
### Requirement: Dev-эмитент токенов только в режиме разработки
|
||||
При `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` ядро SHALL предоставлять `POST /api/v1/dev/token` с телом `{"email"}`,
|
||||
возвращающий HS256 JWT (срок 8 часов) для существующего активного пользователя. При любом другом сочетании флагов
|
||||
маршрут MUST NOT существовать (404), а dev-проверяющий MUST NOT принимать токены.
|
||||
|
||||
#### Scenario: Получение dev-токена
|
||||
- **WHEN** `AUTH_DEV_ISSUER=true`, `DEBUG=true` и запрошен токен для `admin@armka.local`
|
||||
- **THEN** ответ 200 `{access_token, token_type: "bearer"}`, токен принимается `/api/v1/me`
|
||||
|
||||
#### Scenario: Прод-конфигурация
|
||||
- **WHEN** `DEBUG=false`
|
||||
- **THEN** `POST /api/v1/dev/token` возвращает 404, токен, подписанный dev-секретом, отвергается с 401
|
||||
56
openspec/specs/data-model/spec.md
Normal file
56
openspec/specs/data-model/spec.md
Normal file
@ -0,0 +1,56 @@
|
||||
# data-model Specification
|
||||
|
||||
## Purpose
|
||||
Схема базы данных ядра совпадает с моделью данных DBML v5.13 один в один: таблицы, колонки, типы, enum'ы, ключи и
|
||||
индексы; миграции применяются к пустой базе и полностью откатываются; расхождение между ORM и DBML невозможно пропустить.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Схема БД равна DBML v5.13
|
||||
ORM-метаданные SHALL содержать ровно 43 таблицы и 27 enum'ов из `../docs/model/data-model-v5.13.dbml` в схеме `dkb`;
|
||||
имена таблиц, колонок, enum'ов и их значений SHALL совпадать с DBML буквально; nullability, PK, UNIQUE, FK, значения
|
||||
по умолчанию и индексы SHALL соответствовать DBML, включая частичные уникальные индексы (`related_group_member`
|
||||
WHERE `is_current`, `consolidation_lock` WHERE `released_at IS NULL`) и `NULLS NOT DISTINCT` на
|
||||
`document (stored_file_id, document_type, osv_account_code)`. Поля, отсутствующие в DBML, MUST NOT существовать.
|
||||
|
||||
#### Scenario: Сверка ORM с DBML
|
||||
- **WHEN** выполняется `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.13.dbml`
|
||||
- **THEN** вывод пуст и код возврата 0
|
||||
|
||||
#### Scenario: Лишняя колонка в ORM
|
||||
- **WHEN** в ORM добавлена колонка, которой нет в DBML (или удалена существующая)
|
||||
- **THEN** скрипт печатает `таблица.колонка: отсутствует в DBML` (или `отсутствует в ORM`) и возвращает код 1
|
||||
|
||||
#### Scenario: Расхождение типа или nullability
|
||||
- **WHEN** колонка `osv_entry.opening_debit` объявлена в ORM как `Numeric(18, 4)` или `NOT NULL`
|
||||
- **THEN** скрипт сообщает `ожидалось numeric(18,2) NULL / фактически numeric(18,4) NOT NULL` и возвращает код 1
|
||||
|
||||
### Requirement: Миграции применяются с нуля и откатываются
|
||||
Единственная ревизия `0001` SHALL создавать схему `dkb` (если нет), все enum'ы, таблицы, ограничения и индексы на пустой
|
||||
базе PostgreSQL 16 (требуется ≥ 15); `downgrade` SHALL удалять всё созданное, оставляя пустую схему; повторный `upgrade` после `downgrade`
|
||||
SHALL проходить без ошибок.
|
||||
|
||||
#### Scenario: Полный цикл
|
||||
- **WHEN** на пустой БД выполняются `alembic upgrade head`, `alembic downgrade base`, `alembic upgrade head`
|
||||
- **THEN** все три команды завершаются успешно, после последней в `information_schema.tables` схемы `dkb` 43 таблицы модели (+ служебная `alembic_version`)
|
||||
и в `pg_type` 27 enum-типов
|
||||
|
||||
#### Scenario: Автогенерация ничего не находит
|
||||
- **WHEN** после `upgrade head` выполняется `alembic revision --autogenerate` на той же БД
|
||||
- **THEN** сгенерированная ревизия не содержит операций (пустые `upgrade`/`downgrade`)
|
||||
|
||||
### Requirement: Версия DBML зафиксирована в коде
|
||||
Пакет моделей SHALL объявлять константу версии DBML (`5.13`); скрипт сверки SHALL сравнивать её с версией в шапке файла
|
||||
DBML и падать при расхождении — смена модели без осознанного обновления ORM невозможна.
|
||||
|
||||
#### Scenario: Новая версия DBML без обновления ORM
|
||||
- **WHEN** в `docs/model/` появляется `data-model-v5.14.dbml`, а константа в ORM — `5.13`
|
||||
- **THEN** сверка против нового файла падает с сообщением о версии до сравнения таблиц
|
||||
|
||||
### Requirement: Деньги и проценты — точные типы
|
||||
Все денежные колонки SHALL быть `numeric(18,2)`, проценты — `numeric(7,4)`/`numeric(9,4)`, уверенность — `numeric(5,2)`;
|
||||
ORM SHALL отдавать их как `Decimal`, `float` в моделях MUST NOT встречаться.
|
||||
|
||||
#### Scenario: Чтение денежной колонки
|
||||
- **WHEN** из `osv_entry` читается строка со `closing_debit = 106311116.46`
|
||||
- **THEN** значение имеет тип `Decimal` и равно `Decimal("106311116.46")`
|
||||
100
openspec/specs/recognition-job-exchange/spec.md
Normal file
100
openspec/specs/recognition-job-exchange/spec.md
Normal file
@ -0,0 +1,100 @@
|
||||
# recognition-job-exchange Specification
|
||||
|
||||
## Purpose
|
||||
Единственная точка обмена ядра с сервисами распознавания — таблица `recognition_job` (DECISION-4): outbox публикует
|
||||
запросы в NATS, inbox принимает подтверждения и результаты идемпотентно, потеря сообщений без JetStream компенсируется
|
||||
повторной публикацией и таймаутами, файл результата регистрируется в `s3_object`.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Outbox публикует задания из `queued`
|
||||
Фоновый публикатор SHALL с периодом `OUTBOX_POLL_SECONDS` (по умолчанию 5) выбирать задания `status = queued`
|
||||
(с блокировкой `FOR UPDATE SKIP LOCKED`), формировать сообщение по `recognition-request.schema.json` v1.0 из
|
||||
`recognition_job ⋈ stored_file ⋈ s3_object` и публиковать в subject по `pipeline` с заголовком
|
||||
`Nats-Msg-Id = <job_id>:<attempt>`. После успешной публикации задание SHALL получить `status = published`,
|
||||
`published_at`, `request_msg_id`, `request_subject`; при ошибке публикации задание SHALL остаться `queued`.
|
||||
|
||||
#### Scenario: Публикация запроса парсеру
|
||||
- **WHEN** в `recognition_job` вставлена строка `queued` с `pipeline = tabular`, связанная со `stored_file` (XLSX) и его `s3_object`
|
||||
- **THEN** в течение 10 с в `recognition.tabular.request` появляется сообщение, проходящее схему запроса, с `job_id`,
|
||||
`file_id`, `s3.key` исходника, `reply_deadline`, `attempt = 1`, а строка переходит в `published` с заполненным `request_msg_id`
|
||||
|
||||
#### Scenario: NATS недоступен
|
||||
- **WHEN** публикация завершается ошибкой соединения
|
||||
- **THEN** задание остаётся `queued`, ошибка залогирована, следующий цикл повторяет публикацию
|
||||
|
||||
### Requirement: Ack сервиса переводит задание в `processing`
|
||||
Получив в `recognition.result` конверт со `status = processing` для задания в `published`, ядро SHALL установить
|
||||
`status = processing`, `started_at`, `service_name`, `service_version`. Повторный ack для задания в `processing` SHALL
|
||||
игнорироваться; ack для задания в другом статусе SHALL логироваться и не менять задание.
|
||||
|
||||
#### Scenario: Сервис подтвердил
|
||||
- **WHEN** сервис опубликовал конверт `processing` с `job_id` опубликованного задания
|
||||
- **THEN** задание имеет `status = processing`, `started_at` заполнен, `service_name = tabular-parser`
|
||||
|
||||
### Requirement: Запрос без ack публикуется повторно, затем — `dlq`
|
||||
Если задание находится в `published` дольше `ACK_TIMEOUT_SECONDS` (по умолчанию 30) без ack, публикатор SHALL
|
||||
опубликовать запрос повторно с тем же `Nats-Msg-Id` и тем же `attempt`, обновив `published_at` и увеличив счётчик
|
||||
`metrics.republish_count`. После `OUTBOX_MAX_REPUBLISH` (по умолчанию 3) повторов без ack задание SHALL получить
|
||||
`status = dlq`, `error_code = NO_ACK`, `finished_at`.
|
||||
|
||||
#### Scenario: Сервис недоступен
|
||||
- **WHEN** ни один инстанс `tabular-parser` не подписан, задание опубликовано
|
||||
- **THEN** запрос публикуется повторно примерно каждые 30 с; после трёх повторов задание имеет `status = dlq`,
|
||||
`error_code = NO_ACK`, `request_msg_id` не изменился
|
||||
|
||||
#### Scenario: Сервис появился после первого повтора
|
||||
- **WHEN** сервис подписался между первой и второй публикацией и прислал ack
|
||||
- **THEN** задание переходит в `processing`, дальнейшие повторы не выполняются
|
||||
|
||||
### Requirement: Inbox принимает результат идемпотентно
|
||||
Для конверта со `status ∈ {done, needs_check, error}` ядро SHALL валидировать его по
|
||||
`recognition-result-message.schema.json`, найти задание по `job_id` и: если `result_msg_id` задания уже равен
|
||||
`Nats-Msg-Id` сообщения — ничего не менять; иначе зарегистрировать файл результата в `s3_object` (`bucket_role = results`,
|
||||
`created_by = <service.name>`; только при наличии `result_s3`), обновить задание (`status`, `service_name`,
|
||||
`service_version`, `finished_at`, `metrics`, `error_code`, `error_message`, `result_schema_version`, `result_s3_object_id`,
|
||||
`result_msg_id`) и зафиксировать транзакцию. Неизвестный `job_id` SHALL быть залогирован. Результат для задания, уже
|
||||
находящегося в финальном статусе (`done`, `error`, `timeout`, `dlq`), SHALL логироваться и не применяться.
|
||||
|
||||
#### Scenario: Успешный результат
|
||||
- **WHEN** сервис публикует конверт `status = done` с `result_s3 = {arm-ka, results/<file_id>/<job_id>/result.json}` и
|
||||
`Nats-Msg-Id = result:<job_id>:1`
|
||||
- **THEN** задание получает `status = done`, `result_msg_id = result:<job_id>:1`, `result_s3_object_id` указывает на новую
|
||||
строку `s3_object` с этим ключом, `finished_at` заполнен
|
||||
|
||||
#### Scenario: Повторная доставка
|
||||
- **WHEN** то же сообщение получено второй раз
|
||||
- **THEN** строка `recognition_job` и `s3_object` не меняются
|
||||
|
||||
#### Scenario: Ошибка сервиса
|
||||
- **WHEN** конверт содержит `status = error`, `error = {NOT_IMPLEMENTED, …}` без `result_s3`
|
||||
- **THEN** задание получает `status = error`, `error_code = NOT_IMPLEMENTED`, `error_message` из конверта, `result_s3_object_id` пуст
|
||||
|
||||
#### Scenario: Результат пришёл после таймаута
|
||||
- **WHEN** задание уже `timeout`, и приходит конверт `done`
|
||||
- **THEN** задание не меняется, событие залогировано
|
||||
|
||||
### Requirement: Таймаут ожидания результата
|
||||
Фоновый вахтёр SHALL с периодом 30 с переводить задания `processing` и `published` с `reply_deadline < now()` в
|
||||
`status = timeout`, `error_code = TIMEOUT`, `finished_at = now()`. Replay задания (новая попытка, `attempt + 1`) — вне Среза 0.
|
||||
|
||||
#### Scenario: Сервис взял, но не ответил
|
||||
- **WHEN** задание в `processing` и `reply_deadline` прошёл без результата
|
||||
- **THEN** не позднее чем через 30 с задание имеет `status = timeout`
|
||||
|
||||
### Requirement: Соединение с NATS по шаблону, без JetStream
|
||||
Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы `OPENBAO__NATS__NATS_SEED/NATS_JWT`, и без учётных данных,
|
||||
если они пусты; подписываться на `recognition.result` в queue group `arm-core`; переподключаться автоматически.
|
||||
Stream'ы и consumer'ы JetStream MUST NOT требоваться. Фоновые задачи SHALL запускаться при `NATS_ENABLED=true`.
|
||||
|
||||
#### Scenario: Dev-окружение compose
|
||||
- **WHEN** ядро стартует с `DEBUG=true`, `NATS_ENABLED=true` и пустыми учётными данными против NATS без JetStream
|
||||
- **THEN** подписка активна, `/healthcheck` отдаёт `nats = ok`, в логах нет ошибок подключения
|
||||
|
||||
#### Scenario: Два инстанса ядра
|
||||
- **WHEN** запущены два инстанса с `NATS_ENABLED=true` и опубликован один результат
|
||||
- **THEN** результат обработан ровно одним инстансом (queue group), строка задания изменена один раз
|
||||
|
||||
#### Scenario: NATS выключен
|
||||
- **WHEN** `NATS_ENABLED=false`
|
||||
- **THEN** приложение стартует, фоновые задачи не запускаются, `/healthcheck` отвечает 200 с `nats = off`
|
||||
58
openspec/specs/reference-data-seed/spec.md
Normal file
58
openspec/specs/reference-data-seed/spec.md
Normal file
@ -0,0 +1,58 @@
|
||||
# reference-data-seed Specification
|
||||
|
||||
## Purpose
|
||||
Справочники продукта (роли, права и их матрица, план счетов, правила элиминации, состав обязательных документов) и
|
||||
dev-данные загружаются из файлов репозитория одной идемпотентной командой; повторный запуск ничего не дублирует.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Идемпотентный seed справочников продукта
|
||||
Команда `python -m scripts.seed` (из `api/`) SHALL загрузить из `api/seed/core/`: `regional_office` (Головной офис,
|
||||
`is_head_office = true`), `role` — 7 системных ролей ТЗ 8.1 (`KA_RF`, `KA_GO`, `RKA`, `CLIENT_MANAGER`, `CURATOR_GO`,
|
||||
`EXPERT`, `ADMIN`, `is_system = true`), `permission` — 16 сочетаний объект × действие из §10 правил с кодами
|
||||
`<OBJECT>_<ACTION>` (`EXPORT` при совпадении), `role_permission` по матрице §10 (ASSUMPTION(OQ-16)), `chart_of_account`,
|
||||
`elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`.
|
||||
Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться.
|
||||
|
||||
#### Scenario: Первый запуск на пустой схеме
|
||||
- **WHEN** после `alembic upgrade head` выполняется `python -m scripts.seed`
|
||||
- **THEN** в `role` 7 строк, в `permission` 16, в `role_permission` — число строк равно сумме прав по матрице §10,
|
||||
в `elimination_rule` 8 строк с `statement_type = balance`, `version_no = 1`, в `required_document_rule` для `BASIC`
|
||||
10 строк (BALANCE, INCOME, OSV × 01, 58, 60, 62, 66, 67, 76, 90) и для `EXTENDED` 27 строк
|
||||
|
||||
#### Scenario: Повторный запуск
|
||||
- **WHEN** `python -m scripts.seed` выполняется второй раз без изменения файлов
|
||||
- **THEN** число строк во всех затронутых таблицах не меняется, команда завершается с кодом 0
|
||||
|
||||
#### Scenario: Изменение названия роли в файле
|
||||
- **WHEN** в `roles.yaml` изменено `name` роли `EXPERT` и seed запущен снова
|
||||
- **THEN** строка `role` с `code = EXPERT` обновлена, новая не создана
|
||||
|
||||
### Requirement: Счета правил и чек-листа существуют в плане счетов
|
||||
Все коды счетов, на которые ссылаются `elimination_rules_v1.yaml` и `required_document_rules.yaml`, SHALL резолвиться в
|
||||
`chart_of_account.code` после seed; `elimination_rule_account.is_excluded = true` SHALL быть у исключений вида
|
||||
«76 кроме 76.07.5, 76.07.9»; `chart_of_account.kind` SHALL быть задан для всех синтетических счетов, `is_off_balance = true`
|
||||
для 001–011.
|
||||
|
||||
#### Scenario: Правило № 6 (основная пара)
|
||||
- **WHEN** seed завершён
|
||||
- **THEN** у правила `rule_no = 6` сторона 1 содержит счета 60.02, 60.22, 60.32, 62.01, 62.03, 62.21, 62.31, 76 и исключения
|
||||
76.07.5, 76.07.9; сторона 2 — 60.01, 60.03, 60.21, 60.31, 62.02, 62.22, 62.32, 76 и исключение 76.07.1;
|
||||
`elimination_mode = min`, `measure = closing`, `side_1 = D`
|
||||
|
||||
#### Scenario: Ссылка на несуществующий счёт
|
||||
- **WHEN** в файл правил добавлен счёт, которого нет в `chart_of_accounts.csv`
|
||||
- **THEN** seed падает до записи в БД с сообщением, какой счёт и в каком правиле не найден
|
||||
|
||||
### Requirement: Dev-данные только по явному флагу
|
||||
`python -m scripts.seed --dev` SHALL дополнительно загрузить из `api/seed/dev/`: примеры региональных филиалов и по одному
|
||||
`app_user` на каждую системную роль (`<role>@armka.local`, активные, с назначенной ролью через `user_role`). Без флага
|
||||
`--dev` эти данные MUST NOT загружаться.
|
||||
|
||||
#### Scenario: Dev-пользователи
|
||||
- **WHEN** выполнен `python -m scripts.seed --dev`
|
||||
- **THEN** в `app_user` 7 строк с email вида `ka_rf@armka.local`, каждая связана ровно с одной ролью через `user_role`
|
||||
|
||||
#### Scenario: Прод-режим
|
||||
- **WHEN** выполнен `python -m scripts.seed` без `--dev`
|
||||
- **THEN** таблица `app_user` пуста, `regional_office` содержит только Головной офис
|
||||
Loading…
x
Reference in New Issue
Block a user