- deploy-stage.sh → stage поднят с новым кодом; /health/ready зелёный.
- Smoke: /api/me на стейдже содержит advancedMode.
- E2E stage-simple-mode — 7/7 passed, включая Playwright-шаг
«Недоступно в простом режиме».
- docs/simple-mode.md: пользовательская документация с таблицей
простого режима, перечнем расширенных модулей, инструкцией для
SuperAdmin (API + UI), ProblemDetails-примером и ссылкой на тесты.
- tests/e2e/scenarios/stage-simple-mode.yml: заквочен title с «:» для
js-yaml. steps.ts: правильный ключ localStorage SPA ('fm.access_token').
docs/simple-mode-progress.md — все 8 пунктов [x], итог.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
18 KiB
Simple Mode — прогресс
Задача: по умолчанию у всех организаций «Простой режим» — только базовые функции розничного учёта. Включить «Расширенный режим» может только SuperAdmin и только для конкретной организации. Данные при переключении не удаляются; при повторном включении всё видно.
Старт: 2026-10-04.
Чек-лист
- 1. Domain + миграция
Organization.IsAdvancedMode(bool, default false, backfill false) - 2. API
PUT /api/super-admin/organizations/{id}/advanced-mode+ аудит +/api/me.advancedMode; org-admin → 403 - 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: ActionTypeEnableAdvancedMode/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/LoyaltyProgramsControllerPromotions/PromotionsControllerReports/AbcReportControllerAdmin/OrgAuditLogControllerTelegram/TelegramBindingControllerCommon/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.loyaltyCardsnav.reportAbcnav.auditLogLanguageSwitcher(в простом режиме интерфейс только по-русски)
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→LanguageSwitcherDashboardPage→ виджеты «Топ товаров» и «Маржа»
Что отложено (новая функциональность, в этой задаче не трогаем):
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_trueForbids_with_ProblemDetails_when_simple_mode— проверяет ObjectResult + status 403 +extensions.code=advanced-mode-requiredSuperAdmin_bypasses_check_even_in_simple_orgInvalidateCache_makes_next_read_see_fresh_valueUnauthenticated_request_is_skipped_by_filterAuthenticated_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_defaultSimple_mode_gets_403_ProblemDetails_on_hidden_endpointOrg_admin_cannot_toggle_advanced_mode(403)SuperAdmin_can_toggle_and_writes_audit_entry(идемпотентно: повторный PUT →changed=false, в аудит не пишется)Me_advancedMode_switches_with_toggle_and_endpoint_opensData_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 → 403sm03_superadmin_enables_and_audit— SuperAdmin включает → 200 + запись в audit; идемпотентный повторsm04_me_reflects_switch_and_opens_endpoint— /api/me обновилось, demand открылсяsm05_data_survives_toggle_cycle— пресет виден после on→off→onsm06_archived_org_cannot_toggle— архивную → 409sm07_ui_shows_not_available_page— Playwright: простой админ на /sales/demands видит текст «Недоступно в простом режиме»
Quality-watchdog: проверка показала, что smoke дёргает только GET /api/catalog/price-types?pageSize=10 (не гейтится — только мутации). Патч смока не требуется.
2026-10-04 — Шаг 8 ✓
git push origin main(Forgejo).~/deploy-stage.sh— build+push api/web, redeploy на prod-vm →/health/readyзелёный.- Smoke на
https://test.admin.food-market.kz:/api/meдля SuperAdmin содержит"advancedMode": false✓- E2E сценарий
stage-simple-mode— 7/7 passed (включая Playwright-шаг «Недоступно в простом режиме»).
docs/simple-mode.mdсоздан: пользовательская документация с таблицей простого режима, перечнем модулей расширенного, инструкцией для SuperAdmin (API + UI), текстом ProblemDetails и ссылкой на тесты.
Итог
8/8 пунктов чек-листа ✓. Код в main (dc885f3..). Стейдж обновлён, тесты зелёные (unit 6/6, integration 7/7, e2e 7/7). Данные скрытых модулей сохраняются при переключении режима — это проверено явно в integration и e2e («пресет виден после on→off→on»).
Не трогал (по правилам задачи): прод admin.food-market.kz, global.json, POS WPF, gateway nginx. Новые функции (кассовые смены, оплаты, ценники, весы, Counterparty.IsSupplier/IsBuyer-галочки) не добавлял — следующий этап.