food-market/docs/simple-mode-progress.md
nns 921bca076c feat(s29): [RequiresAdvancedMode] + гейтинг скрытых модулей (шаг 3/8)
- RequiresAdvancedModeAttribute (TypeFilterAttribute → IAsyncAuthorizationFilter):
  читает Organization.IsAdvancedMode по org_id-claim через AppDbContext,
  кэш 30 с, инвалидация после переключения. SuperAdmin bypass. На отказ
  возвращает 403 ProblemDetails с code=advanced-mode-required.
- Атрибут навешен на контроллеры:
  Sales/DemandsController, Inventory/TransfersController,
  Loyalty/LoyaltyCardsController + LoyaltyProgramsController,
  Promotions/PromotionsController, Reports/AbcReportController,
  Admin/OrgAuditLogController, Telegram/TelegramBindingController,
  Common/UserPresetsController.
- И на отдельные действия:
  Catalog/PriceTypesController POST/PUT/DELETE (GET доступен — товары
  используют retail-прайс),
  Catalog/ProductsController POST /bulk-update.
- SuperAdmin toggle (шаг 2) инвалидирует кэш фильтра после SaveChangesAsync.
- SSO (Auth/ExternalAuthController) гейтится UI, а не атрибутом: endpoint
  инициируется анонимом без org_id в токене.

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

89 lines
7.3 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 на скрытых модулях
- [ ] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
- [ ] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
- [ ] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
- [ ] 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).