20 KiB
AURORA-1883: просмотр закрытых ВСП — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: В справочнике ВСП можно смотреть закрытые ВСП (статус «Действующие / Закрытые / Все», период закрытия, бейдж статуса, выгрузка по фильтру), а в листах формы 2 закрытый ВСП подписан рег. номером с пометкой «(закрыт)», а не числом.
Architecture: Бэк: параметр status (и период закрытия) в роутере /dict/info и в теле выгрузки переводится чистой функцией в is_active и доходит до VSPRepository.get_list; dropdown получает include_inactive. Фронт: чистые утилиты параметров списка и подписи ВСП, переключатель статуса на странице справочника, полный список ВСП (с закрытыми) в контексте RealtimeTable и выбор только действующих в редакторе ячейки. Миграции нет.
Tech Stack: FastAPI 0.111, SQLAlchemy 2 async, pydantic 2; React 18 (JSX), MUI, material-react-table, node --test, Biome.
Spec: openspec/changes/aurora-1883-closed-vsp/ — proposal.md, specs/vsp-directory/spec.md, design.md (решения D1–D8). Исполнитель читает design.md целиком перед своей задачей.
Global Constraints
- База — ветка
feat/330-aurora-1883-closed-vsp(поверх AURORA-1885, PR #328); PR —--base test. - Значения статуса — ровно
active,closed,all; по умолчаниюactive; иное — 422. - Параметры:
close_date_start,close_date_end(YYYY-MM-DD),include_inactive(bool, по умолчаниюfalse). - Без новых параметров поведение API и UI прежнее: только действующие ВСП.
- Удалённые ВСП (
is_deleted) не возвращаются ни из списка, ни из выгрузки, ни из dropdown. - Подписи: бейджи «Действует» / «Закрыт»; ячейка формы —
<рег. номер> (закрыт), ненайденный —ВСП #<id>; кнопки статуса «Действующие» / «Закрытые» / «Все»; поля «Закрыт с» / «по». - Цвет приглушённого текста закрытых —
#99a1af; цвета бейджей — как «Активен»/«Архив» вweb/src/pages/AdminPanel/SppsPage/SspTable.jsx. @mui/x-date-pickersне использовать (не объявлен в package.json).- Коммиты на русском, с
(AURORA-1883, #330)и строкойCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
Окружение
- Python:
/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin/pytest; пакеты не ставить. - Postgres
perf-pg(perf:perf@localhost:55432); на каждый прогон с--init_db— свежая БД:docker exec perf-pg psql -U perf -d perf -c "DROP DATABASE IF EXISTS verify_1883" -c "CREATE DATABASE verify_1883", затемcd api && DATABASE_URL="postgresql+asyncpg://perf:perf@localhost:55432/verify_1883" <venv>/bin/pytest --init_db <пути>. - Эталон полного backend на ветке 1885: 906 passed / 10 известных падений (form3 add_project, satellite spod ×3, upd_form_cells_subtree ×3, 0037_guard, export_service_query_params ×2).
- Фронт: в worktree
web/node_modulesнет — сделать симлинкln -s /Users/andrey.epifancev/projects/fomichev_claude/DFiP_Budget_planing/web/node_modules web/node_modules(как в worktree aurora-1896; симлинк не коммитить).npm installне запускать, реестр npm не подменять. - Фикстура
api/tests/sqls/fixture.sql: ВСП 1 (branch 2) уже закрыт (closed_at 2026-06-16,is_active=false), ВСП 2 и 3 (branch 2) действующие. Тесты создают свои ВСП через_create_vsp(...)вtest_vsp.py(уже есть, от 1885) с уникальным рег. номером и ищут их по рег. номеру — не завязываться на общее число строк.
Review Focus
- Ячейка ВСП с id, которого нет в загруженном списке (удалённый ВСП), при разрядности «тыс.»/«млн» — должна показывать
ВСП #<id>, а не0,1(Task 3, тестgetVspLabel+ проверка, что Cell не применяетdisplayFactor). - Редактор ячейки строки, где уже стоит закрытый ВСП, — поле не пустое (подпись закрытого), но в списке выбора его нет (Task 3).
- Переключение статуса при выбранном ССП и периоде — ССП сохраняется, период уходит в запрос только для «Закрытые»/«Все» (Task 4, тест
buildVspListParams). - Исполнитель с
status=all— только ВСП своих ССП (Task 1, интеграционный тест). - Выгрузка из режима «Закрытые» — файл содержит закрытые, а не действующие (Task 2, интеграционный тест; Task 4 — передача параметров из модалки).
Task 1: Бэк — статус и период закрытия в списке справочника
Files:
- Modify:
api/src/domain/schemas.py(рядом сVSPExportRequest, ~:741) - Modify:
api/src/services/vsp_service.py(модульная функция;get_list~:54-99) - Modify:
api/src/repository/vsp_repository.py(get_list~:51-134) - Modify:
api/src/api/v1/vsp.py(get_vsp~:23-57) - Test:
api/tests/integration/test_vsp.py,api/tests/unit/test_vsp_status.py(новый)
Interfaces:
-
Produces:
VSPStatus = Literal["active", "closed", "all"]вsrc.domain.schemas;vsp_status_to_is_active(status: VSPStatus) -> bool | Noneвsrc.services.vsp_service;VSPService.get_list(..., is_active, close_date_start: date | None = None, close_date_end: date | None = None, ...);VSPRepository.get_list(..., is_active, close_date_start: date | None = None, close_date_end: date | None = None, ...)— без параметраclose_date_is_null. -
Step 1: Unit-тест перевода статуса —
api/tests/unit/test_vsp_status.py:
import pytest
from src.services.vsp_service import vsp_status_to_is_active
@pytest.mark.parametrize("status, expected", [("active", True), ("closed", False), ("all", None)])
def test_vsp_status_to_is_active(status, expected):
assert vsp_status_to_is_active(status) is expected
-
Step 2: Интеграционные тесты в
test_vsp.py(каждый создаёт свои ВСП через_create_vsp, закрытый —close_dateв прошлом; ищет поregistration_numberвresult):test_vsp_list_status_default_active— действующий A есть, закрытый B (close_date="2021-03-15") нет;test_vsp_list_status_closed—?status=closed: B есть сis_active is False, A нет;test_vsp_list_status_all— A и B есть;test_vsp_list_status_invalid—?status=bogus→ 422;test_vsp_list_deleted_never_returned— ВСП C закрыт и удалён (DELETE), нет ни вactive, ни вclosed, ни вall;test_vsp_list_close_period— B (2021-03-15):?status=closed&close_date_start=2021-01-01&close_date_end=2021-12-31→ есть;?status=closed&close_date_start=2022-01-01→ нет;test_vsp_list_closed_order— B (2021-03-15) и D (2024-06-01): в?status=closedиндекс D меньше индекса B;test_vsp_list_closed_executor_scope— закрытый ВСП на ССП исполнителя (isp_tokens) виден ему в?status=closedи в?status=all, закрытый на чужом ССП — нет. ССП исполнителя и чужой ССП взять из фикстуры (api/tests/sqls/fixture.sql, привязки пользователя-исполнителя); если чужого активного ССП нет — тестpytest.skipс причиной.
-
Step 3: Прогнать — падают (
ImportErrorу unit, 200 вместо 422 и лишние/недостающие строки у интеграционных):<venv>/bin/pytest --init_db tests/unit/test_vsp_status.py tests/integration/test_vsp.py -q(свежая БД). -
Step 4: Реализация
schemas.py:VSPStatus.vsp_service.py:vsp_status_to_is_active;get_listпринимает и передаётclose_date_start,close_date_end.vsp_repository.pyget_list: даты вmatch(Vsp.closed_at >= start,<= end); сортировка по D3 (is_active is False→Vsp.closed_at.desc().nulls_last(), Vsp.id, иначеVsp.id); удалитьclose_date_is_nullи мёртвыйreturnв конце.vsp.pyget_vsp:status: VSPStatus = "active",close_date_start: date | None = None,close_date_end: date | None = None;is_active=vsp_status_to_is_active(status).
-
Step 5: Прогнать — зелёные (та же команда, свежая БД). Ожидается: все тесты
test_vsp.pyи новый unit — passed. -
Step 6: Коммит —
feat(vsp): фильтр справочника по статусу и периоду закрытия (AURORA-1883, #330).
Task 2: Бэк — выгрузка и dropdown с закрытыми
Files:
- Modify:
api/src/domain/schemas.py(VSPExportRequest) - Modify:
api/src/services/vsp_service.py(get_dropdown~:101-132,export_xlsx~:158-182) - Modify:
api/src/api/v1/vsp.py(get_vsp_dropdown,export_info) - Test:
api/tests/integration/test_vsp.py,api/tests/unit/test_vsp_status.py
Interfaces:
-
Consumes:
VSPStatus,vsp_status_to_is_active,VSPService.get_list(close_date_start, close_date_end)из Task 1. -
Produces:
VSPExportRequest.status: VSPStatus = "active",.close_date_start,.close_date_end: date | None = None;VSPService.get_dropdown(..., include_inactive: bool = False);VSPService.export_xlsx(..., status: VSPStatus = "active", close_date_start=None, close_date_end=None); queryGET /dict/info/dropdown?include_inactive=true. -
Step 1: Unit-тесты в
test_vsp_status.pyс мокомvsp_repo(как_serviceвtest_vsp_service_close_date.py; дляget_dropdownзамокать_get_scoped_ssp_ids,form_repo.get,_sync_closed_records):test_get_dropdown_include_inactive_passes_none—include_inactive=True→vsp_repo.get_listвызван сis_active=None;test_get_dropdown_default_active_only— без параметра →is_active=True;test_export_xlsx_status_closed—export_xlsx(user, status="closed", close_date_start=date(2021,1,1))→get_listвызван сis_active=False,close_date_start=date(2021,1,1)(замокатьget_listу сервиса, вернуть[]).
-
Step 2: Интеграционные тесты в
test_vsp.py:test_vsp_dropdown_include_inactive— закрытый ВСП на ССП 2 (_create_vsp(..., ssp_id=2, close_date="2021-03-15")):dropdown?form_id=2&include_inactive=trueсодержит его (is_active is False);dropdown?form_id=2— нет. Существующийtest_vsp_dropdown_with_filter(без параметра) должен остаться зелёным;test_vsp_export_status_closed—POST /dict/info/export{"ssp_ids":[1], "status":"closed"}→ в xlsx (openpyxl,load_workbook(io.BytesIO(response.content))) есть рег. номер закрытого ВСП и нет действующего; тот же запрос безstatus— наоборот.
-
Step 3: Прогнать — падают.
-
Step 4: Реализация по D4, D5 design.md.
-
Step 5: Прогнать — зелёные, затем полный backend на свежей БД:
<venv>/bin/pytest --init_db -q. Ожидается: те же 10 известных падений, новых нет (passed ≥ 906 + новые). -
Step 6: Коммит —
feat(vsp): выгрузка по статусу и периоду, закрытые ВСП в dropdown по include_inactive (AURORA-1883, #330).
Task 3: Фронт — подпись ВСП в листах форм
Files:
- Create:
web/src/components/RealtimeTable/utils/vspLabel.js,web/src/components/RealtimeTable/utils/vspLabel.test.js - Modify:
web/src/components/RealtimeTable/Cell/Cell/Cell.jsx(~:165-184) - Modify:
web/src/components/RealtimeTable/RealtimeTable.jsx(~:311getDropdownVsp) - Modify:
web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx - Modify (комментарий):
web/src/components/RealtimeTable/Modals/SelectVspModal.jsx
Interfaces:
-
Consumes:
GET /dict/info/dropdown?include_inactive=true(Task 2), поля ВСПid,registration_number,is_active. -
Produces:
getVspLabel(value, vspOptions) -> string | null;selectableVspOptions(vspOptions) -> array(толькоis_active !== false) — обе вvspLabel.js. -
Step 1: Тест
vspLabel.test.js(node:test,node:assert/strict, стиль соседних*.test.js):
const opts = [
{ id: 1, registration_number: '1234/567', is_active: true },
{ id: 2, registration_number: '7777/001', is_active: false },
];
// getVspLabel
assert.equal(getVspLabel(1, opts), '1234/567');
assert.equal(getVspLabel(2, opts), '7777/001 (закрыт)');
assert.equal(getVspLabel('1', opts), '1234/567');
assert.equal(getVspLabel(99, opts), 'ВСП #99');
assert.equal(getVspLabel(null, opts), null);
assert.equal(getVspLabel('', opts), null);
assert.equal(getVspLabel(5, undefined), 'ВСП #5');
// selectableVspOptions
assert.deepEqual(selectableVspOptions(opts).map((v) => v.id), [1]);
assert.deepEqual(selectableVspOptions(undefined), []);
-
Step 2: Прогнать — падает:
cd web && node --test src/components/RealtimeTable/utils/vspLabel.test.js→ ошибка импорта. -
Step 3: Реализация
vspLabel.js;Cell.jsx— дляisVspDropdownвернуть{ rawValue: value, displayValue: getVspLabel(value, vspOptions) ?? '', isNumeric: false, isNegative: false }до числового форматирования (D7);RealtimeTable.jsx—getDropdownVsp({ form_id: formId, include_inactive: true });VspDropdownEditCell.jsx— пункты списка изselectableVspOptions(vspOptions), отображение текущего значения черезgetVspLabel(закрытый не выбирается, но подписан);SelectVspModal.jsx— только комментарий, что модалка сама грузит dropdown безinclude_inactiveи поэтому получает только действующие (AURORA-1883). -
Step 4: Прогнать
node --test src/components/RealtimeTable/utils/*.test.js— ожидается: всё зелёное, кроме известного паденияSMETA.test.js, если он входит в набор (сравнить с прогоном до правок);npx biome checkпо изменённым файлам — без новых ошибок относительно исходных. -
Step 5: Коммит —
feat(forms): подпись закрытого ВСП «(закрыт)» в листах форм, выбор только действующих (AURORA-1883, #330).
Task 4: Фронт — справочник ВСП: статус, период, бейдж, выгрузка
Files:
- Create:
web/src/pages/DictVspPage/utils/vspListParams.js,web/src/pages/DictVspPage/utils/vspListParams.test.js - Modify:
web/src/pages/DictVspPage/DictVspPage.jsx(loadData~:34-65,handleEdit~:87-101) - Modify:
web/src/pages/DictVspPage/components/DictVspFilters.jsx - Modify:
web/src/pages/DictVspPage/components/TableDictVsp.jsx(колонки ~:54-236) - Modify:
web/src/pages/DictVspPage/components/Modals/ModalExport.jsx(~:45-57) и место, где страница её рендерит (DictVspModals.jsx)
Interfaces:
-
Consumes:
GET /dict/info?status&close_date_start&close_date_end(Task 1),POST /dict/info/exportс теми же полями (Task 2). -
Produces:
buildVspListParams({ status, closeDateStart, closeDateEnd }) -> { status, close_date_start?, close_date_end? };vspMatchesStatus(vsp, status) -> boolean. -
Step 1: Тест
vspListParams.test.js(запускnode --test src/pages/DictVspPage/utils/*.test.js, какfilterVspBySsp.test.jsрядом):
assert.deepEqual(buildVspListParams({ status: 'active', closeDateStart: '2021-01-01', closeDateEnd: '' }), { status: 'active' });
assert.deepEqual(buildVspListParams({ status: 'closed', closeDateStart: '2021-01-01', closeDateEnd: '2021-12-31' }),
{ status: 'closed', close_date_start: '2021-01-01', close_date_end: '2021-12-31' });
assert.deepEqual(buildVspListParams({ status: 'all', closeDateStart: '', closeDateEnd: '2021-12-31' }),
{ status: 'all', close_date_end: '2021-12-31' });
assert.equal(vspMatchesStatus({ is_active: true }, 'active'), true);
assert.equal(vspMatchesStatus({ is_active: false }, 'active'), false);
assert.equal(vspMatchesStatus({ is_active: false }, 'closed'), true);
assert.equal(vspMatchesStatus({ is_active: true }, 'closed'), false);
assert.equal(vspMatchesStatus({ is_active: false }, 'all'), true);
-
Step 2: Прогнать — падает (ошибка импорта).
-
Step 3: Реализация по D6 design.md: утилиты;
DictVspFilters—ToggleButtonGroup«Действующие»/«Закрытые»/«Все» и дваTextField type="date"«Закрыт с»/«по» приstatus !== 'active';DictVspPage— statestatus='active',closeDateStart='',closeDateEnd='', перезагрузка только списка ВСП по их изменению, выбранный ССП сохраняется,handleEditубирает строку при!vspMatchesStatus(updated, status);TableDictVsp— колонка «Статус» после «Дата закрытия ВСП» с бейджем, текст строки закрытого#99a1af;ModalExport— тело{ ssp_ids, ...buildVspListParams(...) }. -
Step 4: Прогнать
node --test src/pages/DictVspPage/utils/*.test.js(зелёные),npx biome checkпо изменённым файлам (без новых ошибок),npm run build(успешно). -
Step 5: Ручная проверка (если поднимается стенд: API на БД
verify_1883+npm run start): режимы статуса, период 2021, бейдж, закрытие ВСП в режиме «Действующие» убирает строку, выгрузка из «Закрытые»; лист FORM_2 со строкой на закрытом ВСП — подпись «(закрыт)», в выборе его нет. Если стенд не поднимается — отметить в отчёте как не выполненную. -
Step 6: Коммит —
feat(vsp): справочник ВСП — статус «Действующие/Закрытые/Все», период закрытия, бейдж и выгрузка по фильтру (AURORA-1883, #330).