docs(s29): simple-mode user guide + стейдж задеплоен + e2e 7/7 ✓ (шаг 8/8)
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

- 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>
This commit is contained in:
nns 2026-10-04 12:11:03 +05:00
parent 7f8750f30f
commit 8572875e7f
4 changed files with 175 additions and 4 deletions

View file

@ -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`-галочки) **не добавлял** — следующий этап.

154
docs/simple-mode.md Normal file
View file

@ -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 <token>
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 на стейдже).

View file

@ -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')

View file

@ -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 видит страницу «Недоступно»"