food-market/docs/simple-mode-progress.md
nns 7f8750f30f
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
test(s29): unit (6) + integration (7) + e2e scenario для simple-mode (шаг 7/8)
Unit — AdvancedModeFilterTests (6/6, ~3с):
- allow when IsAdvancedMode=true
- 403 ProblemDetails с code=advanced-mode-required when simple
- SuperAdmin bypass
- InvalidateCache делает следующее чтение свежим
- unauthenticated — не трогаем ([Authorize] выше)
- authenticated без org — fail-closed 403

Integration — AdvancedModeTests (7/7, ~25с):
- /api/me.advancedMode=false по умолчанию
- GET на скрытый endpoint в простом режиме → 403 ProblemDetails
- org-admin не может переключить → 403
- SuperAdmin переключает → 200 + audit; идемпотент: повторный → changed=false
- /api/me обновилось, demand открылся
- данные пресета сохраняются через on→off→on
- архивную org переключать нельзя → 409

Доп. фикс: ApiFactory теперь прокидывает ConnectionStrings__Default через
Environment.SetEnvironmentVariable в InitializeAsync. ConfigureAppConfiguration
опаздывает к моменту чтения строки в Program.cs:86 (var poolTunedConnString=...)
— интеграционные тесты падали с «Connection refused к 127.0.0.1:5432». env-var
читает CreateBuilder сразу, строка подключения теперь корректно берётся из
Testcontainers. Это общий фикс для всей сюиты.

E2E — tests/e2e/scenarios/stage-simple-mode.{yml,steps.ts} (7 шагов):
sm01 signup→simple default; sm02 org-admin forbidden; sm03 SuperAdmin
toggle + audit; sm04 /api/me reflects + endpoint opens; sm05 data
survives toggle cycle; sm06 archived→409; sm07 Playwright UI показывает
страницу «Недоступно в простом режиме».

Quality-watchdog: GET /api/catalog/price-types не гейтится (только
мутации) — патч смока не требуется.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-04 12:02:44 +05:00

182 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Simple Mode — прогресс
Задача: по умолчанию у всех организаций «Простой режим» — только базовые
функции розничного учёта. Включить «Расширенный режим» может только
SuperAdmin и только для конкретной организации. Данные при переключении
не удаляются; при повторном включении всё видно.
Старт: 2026-10-04.
## Чек-лист
- [x] 1. Domain + миграция `Organization.IsAdvancedMode` (bool, default false, backfill false)
- [x] 2. API `PUT /api/super-admin/organizations/{id}/advanced-mode` + аудит + `/api/me.advancedMode`; org-admin → 403
- [x] 3. Серверная защита: `[RequiresAdvancedMode]` → 403 ProblemDetails на скрытых модулях
- [x] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
- [x] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
- [x] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
- [x] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости
- [ ] 8. Деплой стейджа + `docs/simple-mode.md`
## Простой режим — что видно
- Главная: выручка за сегодня, что заканчивается, последние продажи
- Товары и группы товаров
- Контрагенты (единый список — поставщики и покупатели)
- Склад: Приёмка, Оприходование, Списание, Инвентаризация, Возврат поставщику, Остатки
- Продажи: Продажи, Возвраты покупателей
- Отчёты: Продажи, Прибыль, Остатки на дату
- Сотрудники и роли, Настройки магазина (включая ОФД), Импорт товаров
- 2FA в профиле — остаётся
## Расширенный режим — что добавляется
- Оптовые отгрузки (Demand)
- Перемещения между точками
- Лояльность
- Промокоды / акции
- ABC-анализ
- Аудит организации
- Несколько типов цен (в простом — одна цена продажи)
- Bulk-операции и сохранённые пресеты в таблицах
- Telegram-бот владельца
- SSO
- Переключатель языка (в простом только русский)
- Продвинутые виджеты дашборда
## Журнал
### 2026-10-04 — Шаг 1 ✓
- `Organization.IsAdvancedMode` (bool, default false) + XML-док, что переключает только SuperAdmin.
- Миграция `20261004100000_Phase29a_IsAdvancedMode` — идемпотентный `ADD COLUMN IF NOT EXISTS` с `DEFAULT false`; все существующие орги получают `false`.
- Snapshot обновлён (`AppDbContextModelSnapshot.cs`).
- Применено на dev-БД: 5/5 организаций `IsAdvancedMode = false`.
- `dotnet build food-market.api` — ✓ (0 error, warnings — старые, не наши).
### 2026-10-04 — Шаг 2 ✓
- `PUT /api/super-admin/organizations/{id}/advanced-mode { "enabled": bool }` в `SuperAdminOrganizationsController` — `[Authorize(Roles="SuperAdmin")]`. Админ орги получает 403 от стандартного Authorize-фильтра (роль Admin ≠ SuperAdmin); в контроллере сверху уже `[Authorize(Roles="SuperAdmin")]` — не нужно отдельного `Forbid()`.
- Запись в `SuperAdminAuditLog`: ActionType `EnableAdvancedMode` / `DisableAdvancedMode`, Description, Changes `{"before":…,"after":…}`, IpAddress. Идемпотентный переключатель: если состояние совпадает — 200 без аудит-записи.
- Архивную орг переключать нельзя — 409.
- `/api/me` отдаёт `advancedMode` (bool). SuperAdmin без org → false (это ОК — он работает через `/api/super-admin/...`).
- `OrgRow` / `OrgDetail` в SuperAdmin list / detail — добавлено `IsAdvancedMode`.
- Маршрут: `/api/super-admin/...` (с дефисом), так как существующий контроллер уже использует этот стиль; задача упоминала `superadmin` без дефиса — выбрал существующий стиль.
- `dotnet build food-market.api` — ✓ (0 error).
### 2026-10-04 — Шаг 3 ✓
- `src/food-market.api/Infrastructure/Authorization/RequiresAdvancedModeAttribute.cs`:
- `[RequiresAdvancedMode]` — `TypeFilterAttribute` вокруг `AdvancedModeFilter : IAsyncAuthorizationFilter`.
- Читает `IsAdvancedMode` по `org_id`-claim через `AppDbContext` (без доверия к клиентским заголовкам). Кэш in-memory 30 с, инвалидация после переключения.
- SuperAdmin bypass (чтобы он мог проверять скрытые модули через «войти как»).
- Отказ — `403 ProblemDetails` с `type`, `title`, `detail` на русском и `extensions.code = "advanced-mode-required"` — фронт различает это 403 от обычного «нет прав».
- Атрибут навешен на (контроллеры):
- `Sales/DemandsController` (оптовые отгрузки)
- `Inventory/TransfersController` (перемещения)
- `Loyalty/LoyaltyCardsController`, `Loyalty/LoyaltyProgramsController`
- `Promotions/PromotionsController`
- `Reports/AbcReportController`
- `Admin/OrgAuditLogController`
- `Telegram/TelegramBindingController`
- `Common/UserPresetsController` (пресеты фильтров)
- И на отдельные действия:
- `Catalog/PriceTypesController` — POST / PUT / DELETE (чтение остаётся — товары используют системный retail-прайс)
- `Catalog/ProductsController.BulkUpdate` (POST `/products/bulk-update`)
- `Auth/ExternalAuthController` (SSO) — атрибут НЕ навешен: endpoint инициирует OAuth-челлендж для анонима, `org_id`-claim ещё не выставлен. Контроль — на уровне UI (не показывать кнопку «Войти через Google/Microsoft» в простом режиме). Также SSO в простом режиме не ломается при включении — просто кнопка появляется.
- `SuperAdminOrganizationsController.SetAdvancedMode` вызывает `AdvancedModeFilter.InvalidateCache(orgId)` после `SaveChangesAsync`, чтобы следующий запрос этой org сразу увидел новое значение.
- `dotnet build food-market.api` — ✓ (0 error).
### 2026-10-04 — Шаг 4 ✓
- `src/food-market.web/src/components/AdvancedModeGuard.tsx` — render-guard: `me.advancedMode = false` + не-SuperAdmin → страница «Недоступно в простом режиме» с текстом + кнопкой «На главную». SuperAdmin и расширенный режим — пропускает.
- `useMe.ts` + локальные `MeResponse` в AppLayout/DashboardPage расширены полем `advancedMode: boolean`.
- `AppLayout.buildNav(roles, advancedMode)` — в простом режиме скрыто:
- `nav.priceTypes` (справочник типов цен)
- `nav.transfers` (перемещения между точками)
- `nav.demands` / `nav.promotions` / `nav.loyaltyPrograms` / `nav.loyaltyCards`
- `nav.reportAbc`
- `nav.auditLog`
- `LanguageSwitcher` (в простом режиме интерфейс только по-русски)
- `App.tsx` — скрытые маршруты обёрнуты в `<AdvancedModeGuard>`:
- `/catalog/price-types`
- `/inventory/transfers`, `/inventory/transfers/new`, `/inventory/transfers/:id`
- `/sales/demands`, `/sales/demands/new`, `/sales/demands/:id`
- `/loyalty/programs`, `/loyalty/cards`, `/promotions`
- `/audit-log`
- `/reports/abc`
- `DashboardPage` — в простом режиме скрыты виджеты «Топ товаров» и «Маржа». Остаются KPI (выручка today/week/month, средний чек), график продаж, «Последние продажи», «Низкий остаток» — ровно то, что в чек-листе.
- Данные НЕ удаляются при выключении режима: это только UI-гейтинг + серверный 403; при повторном включении всё видно.
- `pnpm run build` — ✓ (2.60s, 0 TS errors).
### 2026-10-04 — Шаг 5 ✓
- Везде формула простого режима: `simpleMode = me && !me.advancedMode && !roles.includes('SuperAdmin')`. SuperAdmin не ощущает — ему доступно всё.
- **Карточка товара** (`ProductEditPage.tsx`) в простом режиме прячет (значения в state сохраняются, при save не стираются):
- «Артикул» (генерируется автоматически, Название на 3 колонки)
- «Описание», «Страна происхождения», «Эталонная цена»
- «Ставка НДС», чекбокс «В том числе НДС»
- «Валюта закупки» (мульти-валюта)
- Чекбоксы «Услуга», «Маркируемый»
- «Максимальный остаток» — остаётся только «Минимальный»
- Все не-retail типы цен в блоке «Цены продажи» — рендерится только retail/system price type.
- **Контрагенты** (`CounterpartiesPage.tsx`) — в простом режиме скрыты Юр. название, Страна, Email, Банк/Р.счёт/БИК, Контактное лицо. Остаются: Название, Тип лица, БИН/ИИН, Адрес, Телефон, Заметки.
- **Приёмка** (`SupplyEditPage.tsx`) — колонка «Валюта» всегда скрыта в простом режиме (одна валюта по умолчанию).
- Другие документы (Enter, Loss, Inventory, SupplierReturn) используют те же `org.ShowXxx` / `org.MultiCurrencyEnabled` флаги — уже скрывают advanced-поля для магазинов с «простым» org-настройками.
Доп. список спрятанного за `simpleMode` (из шагов 4/5, не вошёл напрямую в чек-лист):
- `AppLayout` → `LanguageSwitcher`
- `DashboardPage` → виджеты «Топ товаров» и «Маржа»
Что отложено (новая функциональность, в этой задаче не трогаем):
- `Counterparty.IsSupplier` / `IsBuyer` — задача просит галочки «Поставщик/Покупатель», но на `Counterparty` таких bool-колонок нет. Это новая миграция и расширение API — отдельная задача.
- `pnpm run build` — ✓ (2.36s, 0 TS errors).
### 2026-10-04 — Шаг 6 ✓
- `SuperAdminOrganizationsPage.tsx`:
- В `OrgRow` добавлен `isAdvancedMode: boolean` (бэк уже отдаёт из шага 2).
- Новая колонка **«Режим»** с бейджем «Простой» (slate-пилюля) / «Расширенный» (violet-пилюля).
- Новая кнопка-экшн рядом с «Архивировать» (иконка Sparkles): при клике открывается модалка подтверждения с разной копией для включения/выключения:
- *Включить:* перечисляет, что станет доступно (demand, loyalty, promos, abc, audit, несколько цен, пресеты, bulk-ops, telegram, sso, язык).
- *Выключить:* предупреждает что данные НЕ удаляются и переключение можно вернуть; напоминает проверить активные операции.
- Переключение бьёт `PUT /api/super-admin/organizations/{id}/advanced-mode { enabled }` и после успеха `list.refetch()`.
- Переключение из карточки организации (`SuperAdminOrgEmployeesPage` / detail-view) не делаем в этой итерации — список-страница с бейджем и кнопкой уже закрывает UX.
- `pnpm run build` — ✓ (2.02s, 0 TS errors).
### 2026-10-04 — Шаг 7 ✓
**Unit — `tests/food-market.UnitTests/AdvancedModeFilterTests.cs`** (6 тестов, все зелёные):
- `Allows_when_IsAdvancedMode_true`
- `Forbids_with_ProblemDetails_when_simple_mode` — проверяет ObjectResult + status 403 + `extensions.code=advanced-mode-required`
- `SuperAdmin_bypasses_check_even_in_simple_org`
- `InvalidateCache_makes_next_read_see_fresh_value`
- `Unauthenticated_request_is_skipped_by_filter`
- `Authenticated_without_org_claim_is_forbidden` (fail-closed)
Запуск: `dotnet test tests/food-market.UnitTests --filter "FullyQualifiedName~AdvancedModeFilterTests"` → 6/6 passed, ~3 с.
**Integration — `tests/food-market.IntegrationTests/AdvancedModeTests.cs`** (7 тестов):
- `Me_returns_advancedMode_false_by_default`
- `Simple_mode_gets_403_ProblemDetails_on_hidden_endpoint`
- `Org_admin_cannot_toggle_advanced_mode` (403)
- `SuperAdmin_can_toggle_and_writes_audit_entry` (идемпотентно: повторный PUT → `changed=false`, в аудит не пишется)
- `Me_advancedMode_switches_with_toggle_and_endpoint_opens`
- `Data_in_hidden_module_survives_toggle_cycle` (пресет виден после `on → off → on`)
- `Toggle_fails_for_archived_org_with_409`
Запуск: `dotnet test tests/food-market.IntegrationTests --filter "FullyQualifiedName~AdvancedModeTests"` → 7/7 passed, ~25 с.
Доп. фикс: `tests/food-market.IntegrationTests/Support/ApiFactory.cs` теперь прокидывает `ConnectionStrings__Default` через `Environment.SetEnvironmentVariable` в `InitializeAsync` — `WebApplicationFactory.ConfigureAppConfiguration` опаздывает к моменту чтения строки в `Program.cs:86`, а env-var читается `CreateBuilder` сразу. Благодаря этому вся интеграционная сюита снова нормально работает на этой dev-vm (не только мои тесты).
**E2E — `tests/e2e/scenarios/stage-simple-mode.yml` + `.steps.ts`** (7 шагов, запускаются после деплоя шага 8):
- `sm01_signup_defaults_to_simple` — /api/me.advancedMode=false + demand 403 с кодом
- `sm02_org_admin_cannot_toggle` — админ орги PUT advanced-mode → 403
- `sm03_superadmin_enables_and_audit` — SuperAdmin включает → 200 + запись в audit; идемпотентный повтор
- `sm04_me_reflects_switch_and_opens_endpoint` — /api/me обновилось, demand открылся
- `sm05_data_survives_toggle_cycle` — пресет виден после on→off→on
- `sm06_archived_org_cannot_toggle` — архивную → 409
- `sm07_ui_shows_not_available_page` — Playwright: простой админ на /sales/demands видит текст «Недоступно в простом режиме»
**Quality-watchdog:** проверка показала, что smoke дёргает только `GET /api/catalog/price-types?pageSize=10` (не гейтится — только мутации). Патч смока **не требуется**.