From 8572875e7f18a19d93bd0ac01eacfd486e31dd3c Mon Sep 17 00:00:00 2001 From: nns Date: Sun, 4 Oct 2026 12:11:03 +0500 Subject: [PATCH] =?UTF-8?q?docs(s29):=20simple-mode=20user=20guide=20+=20?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=B9=D0=B4=D0=B6=20=D0=B7=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=BF=D0=BB=D0=BE=D0=B5=D0=BD=20+=20e2e=207/7=20=E2=9C=93=20(?= =?UTF-8?q?=D1=88=D0=B0=D0=B3=208/8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/simple-mode-progress.md | 17 +- docs/simple-mode.md | 154 ++++++++++++++++++ .../e2e/scenarios/stage-simple-mode.steps.ts | 6 +- tests/e2e/scenarios/stage-simple-mode.yml | 2 +- 4 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 docs/simple-mode.md diff --git a/docs/simple-mode-progress.md b/docs/simple-mode-progress.md index 371d72e..eeff5b5 100644 --- a/docs/simple-mode-progress.md +++ b/docs/simple-mode-progress.md @@ -16,7 +16,7 @@ SuperAdmin и только для конкретной организации. - [x] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются - [x] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением - [x] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости -- [ ] 8. Деплой стейджа + `docs/simple-mode.md` +- [x] 8. Деплой стейджа + `docs/simple-mode.md` ## Простой режим — что видно @@ -179,3 +179,18 @@ SuperAdmin и только для конкретной организации. - `sm07_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`-галочки) **не добавлял** — следующий этап. diff --git a/docs/simple-mode.md b/docs/simple-mode.md new file mode 100644 index 0000000..492e2b2 --- /dev/null +++ b/docs/simple-mode.md @@ -0,0 +1,154 @@ +# Simple / Advanced mode + +Sprint 29, 2026-10-04. + +Food Market по умолчанию работает в «простом» режиме — виден только +минимальный набор функций розничного учёта, достаточный для +обычного магазина у дома. «Расширенный» режим включает дополнительные +модули (оптовые отгрузки, перемещения, лояльность и т.д.) и доступен +только после явного включения оператором платформы (SuperAdmin). + +Переключатель в настройках магазина отсутствует намеренно — +администратор организации не может ни включить, ни выключить +расширенный режим себе сам. + +## Простой режим — что видно + +| Раздел | Что внутри | +|---|---| +| **Главная** | KPI «Выручка сегодня / неделя / месяц», средний чек, график продаж, «Последние продажи», «Низкий остаток». | +| **Товары** | Карточка товара без артикула (генерируется автоматически), описания, нескольких типов цен, страны происхождения, НДС-галок, «Услуги», «Маркируемый», максимального остатка. Остаются: Название, Штрихкоды, Группа, Единица измерения, Цена продажи (одна), Себестоимость (только чтение), Минимальный остаток, Фасовка (шт/вес/разливной), Фото. | +| **Группы товаров** | Полный доступ. | +| **Единицы измерения** | Полный доступ. | +| **Контрагенты** | Единый список. Поля: Название, Тип лица, БИН/ИИН, Адрес, Телефон, Заметки. | +| **Склад** | Приёмка, Оприходование, Списание, Инвентаризация, Возврат поставщику, Остатки. | +| **Продажи** | Продажи (POS), Возвраты покупателей. | +| **Отчёты** | Продажи, Прибыль, Остатки на дату. | +| **Настройки** | Магазин, Склады, Кассы (RetailPoints), Сотрудники, Роли, ОФД, Импорт товаров (CSV / 1С / МойСклад). | +| **Профиль** | Смена пароля, 2FA. | + +Весь интерфейс по-русски (переключателя языка нет). + +## Расширенный режим — что добавляется + +- Оптовые отгрузки (`Demand`) +- Перемещения между точками (`Transfer`) +- Лояльность — программы и карты +- Промокоды / акции +- ABC-анализ +- Аудит организации (`/audit-log`) +- Несколько типов цен (не только retail) +- Bulk-операции в таблицах + сохранённые пресеты фильтров +- Telegram-бот владельца (ежедневная сводка) +- SSO (вход через Google / Microsoft) +- Выбор языка +- Виджеты «Топ товаров» и «Маржа» на дашборде + +На карточках/документах появляются дополнительные поля из `Organization` +(«Описание», «Эталонная цена», «Валюта закупки», «НДС», «Услуга», +«Маркируемый», «Макс. остаток»), которые управляются существующими +org-level `Show*` флагами. + +## Как SuperAdmin включает / выключает + +### Через API + +```http +PUT /api/super-admin/organizations/{id}/advanced-mode +Authorization: Bearer +Content-Type: application/json + +{ "enabled": true } +``` + +Ответ `200 OK`: + +```json +{ "id": "...", "isAdvancedMode": true, "changed": true } +``` + +Повторный вызов с тем же состоянием → `changed: false` и запись в +аудит **не** делается (идемпотентность). + +Переключение архивной организации → `409 Conflict`. +Эндпоинт разрешён только роли `SuperAdmin`; админ орги получает `403`. + +### Через SuperAdmin UI + +1. Открыть `/super-admin/organizations`. +2. В списке у каждой строки колонка **«Режим»** — бейдж «Простой» + (slate) или «Расширенный» (violet). +3. В экшн-колонке рядом с «Архивировать» — иконка-искра. Клик → + модалка подтверждения с описанием последствий. +4. Подтверждение → `PUT` на эндпоинт выше. После успеха список + автоматически перегружается. + +### Запись в журнал + +Каждое реальное переключение пишется в `super_admin_audit_log`: + +```json +{ + "actionType": "EnableAdvancedMode", // или DisableAdvancedMode + "organizationId": "...", + "description": "Включен расширенный режим «Org Name»", + "changesJson": { "before": false, "after": true }, + "ipAddress": "...", + "superAdminUserId": "..." +} +``` + +Просмотр: `/super-admin/audit-log?organizationId=...` или UI +«SuperAdmin → Журнал». + +## Что происходит с данными при выключении + +**Ничего не удаляется.** Серверный фильтр +`[RequiresAdvancedMode]` возвращает `403 ProblemDetails` на скрытых +эндпоинтах; данные остаются в БД. При повторном включении всё снова +видно и доступно без миграций и восстановлений. + +Пример `ProblemDetails` при 403: + +```json +{ + "type": "https://food-market.kz/errors/advanced-mode-required", + "title": "Функция доступна в расширенном режиме.", + "detail": "Этот раздел недоступен в простом режиме магазина. Обратитесь к администратору платформы, чтобы включить расширенный режим.", + "status": 403, + "instance": "/api/sales/demands", + "code": "advanced-mode-required" +} +``` + +Для UI это достаточный маркер, чтобы отличить этот 403 от обычного +«нет прав» и отрисовать страницу «Недоступно в простом режиме» вместо +общего сообщения. + +## Как это устроено на бэкенде (кратко) + +- `Organization.IsAdvancedMode` (bool, default `false`) — миграция + `20261004100000_Phase29a_IsAdvancedMode`. +- `[RequiresAdvancedMode]` — атрибут MVC + (`TypeFilterAttribute` → `AdvancedModeFilter : IAsyncAuthorizationFilter`), + читает флаг по `org_id`-claim через `AppDbContext`. + SuperAdmin bypass; аноним пропускается (отработает `[Authorize]`). + Кэш in-memory 30 с, инвалидация после переключения. +- Список скрытых контроллеров / действий — + см. `docs/simple-mode-progress.md` (шаг 3). +- `/api/me` расширен полем `advancedMode: boolean`. + +Веб: +- `useMe()` отдаёт `advancedMode`. +- `AppLayout.buildNav(roles, advancedMode)` фильтрует меню. +- `AdvancedModeGuard` оборачивает скрытые маршруты и показывает + «Недоступно в простом режиме» вместо белого экрана / 403 в консоли. +- Карточка товара, контрагенты и приёмка в простом режиме прячут + advanced-поля; значения существующих записей сохраняются и при + переключении режима снова видны. + +## Тесты + +- Unit: `tests/food-market.UnitTests/AdvancedModeFilterTests.cs` (6/6). +- Integration: `tests/food-market.IntegrationTests/AdvancedModeTests.cs` (7/7). +- E2E: `tests/e2e/scenarios/stage-simple-mode.yml` (7/7 на стейдже). diff --git a/tests/e2e/scenarios/stage-simple-mode.steps.ts b/tests/e2e/scenarios/stage-simple-mode.steps.ts index b5185e0..21f939d 100644 --- a/tests/e2e/scenarios/stage-simple-mode.steps.ts +++ b/tests/e2e/scenarios/stage-simple-mode.steps.ts @@ -235,10 +235,12 @@ export async function sm07_ui_shows_not_available_page({ ctx, step }: StepCtx) { const ctxBrowser = await browser.newContext({ ignoreHTTPSErrors: true }) const page = await ctxBrowser.newPage() - // Прокидываем access-token через localStorage, чтобы миновать форму логина. + // Прокидываем access-token через localStorage под реальным ключом SPA + // (см. src/food-market.web/src/lib/auth.ts — ACCESS = 'fm.access_token'), + // чтобы миновать форму логина. await page.goto(ADMIN_BASE + '/login') await page.evaluate((tok) => { - localStorage.setItem('accessToken', tok) + localStorage.setItem('fm.access_token', tok) }, org.token) await page.goto(ADMIN_BASE + '/sales/demands') diff --git a/tests/e2e/scenarios/stage-simple-mode.yml b/tests/e2e/scenarios/stage-simple-mode.yml index 9ea9274..77b946f 100644 --- a/tests/e2e/scenarios/stage-simple-mode.yml +++ b/tests/e2e/scenarios/stage-simple-mode.yml @@ -35,4 +35,4 @@ steps: - id: sm06_archived_org_cannot_toggle title: Архивную org переключать нельзя (409) - id: sm07_ui_shows_not_available_page - title: UI: простой админ на /sales/demands видит страницу «Недоступно» + title: "UI: простой админ на /sales/demands видит страницу «Недоступно»"