DFiP_Budget_planing/docs/superpowers/plans/2026-11-12-aurora-1883-closed-vsp.md
Andrey Epifancev 99ab7598c9 docs(vsp): OpenSpec change и план AURORA-1883 — просмотр закрытых ВСП (#330)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 11:27:51 +04:00

20 KiB
Raw Blame History

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

  1. Ячейка ВСП с id, которого нет в загруженном списке (удалённый ВСП), при разрядности «тыс.»/«млн» — должна показывать ВСП #<id>, а не 0,1 (Task 3, тест getVspLabel + проверка, что Cell не применяет displayFactor).
  2. Редактор ячейки строки, где уже стоит закрытый ВСП, — поле не пустое (подпись закрытого), но в списке выбора его нет (Task 3).
  3. Переключение статуса при выбранном ССП и периоде — ССП сохраняется, период уходит в запрос только для «Закрытые»/«Все» (Task 4, тест buildVspListParams).
  4. Исполнитель с status=all — только ВСП своих ССП (Task 1, интеграционный тест).
  5. Выгрузка из режима «Закрытые» — файл содержит закрытые, а не действующие (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.py get_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.py get_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); query GET /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 (~:311 getDropdownVsp)
  • 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 — state status='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).