From 4342318c2cbe1b8a05b7f003e1f7a2d5965b71db Mon Sep 17 00:00:00 2001 From: nns Date: Sun, 4 Oct 2026 11:28:28 +0500 Subject: [PATCH] =?UTF-8?q?feat(s29):=20Organization.IsAdvancedMode=20+=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20Phase29a=20(?= =?UTF-8?q?=D1=88=D0=B0=D0=B3=201/8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simple/Advanced mode: по умолчанию у всех организаций «простой» режим (IsAdvancedMode=false) — виден только базовый розничный учёт. Включить «расширенный» режим может только SuperAdmin через отдельный API; в настройках магазина переключателя нет. Данные скрытых модулей при переключении НЕ удаляются; при повторном включении всё снова видно. - Organization.IsAdvancedMode (bool, default false) + XML-док - EF-миграция Phase29a — идемпотентный ADD COLUMN IF NOT EXISTS DEFAULT false - AppDbContextModelSnapshot обновлён - Применено на dev-БД: 5/5 организаций → IsAdvancedMode=false docs/simple-mode-progress.md — чек-лист + журнал. Co-Authored-By: Claude Opus 4.7 --- docs/simple-mode-progress.md | 55 +++++++++++++++++++ .../Organizations/Organization.cs | 10 ++++ .../Persistence/AppDbContext.cs | 4 ++ .../20261004100000_Phase29a_IsAdvancedMode.cs | 31 +++++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 4 ++ 5 files changed, 104 insertions(+) create mode 100644 docs/simple-mode-progress.md create mode 100644 src/food-market.infrastructure/Persistence/Migrations/20261004100000_Phase29a_IsAdvancedMode.cs diff --git a/docs/simple-mode-progress.md b/docs/simple-mode-progress.md new file mode 100644 index 0000000..bd1a579 --- /dev/null +++ b/docs/simple-mode-progress.md @@ -0,0 +1,55 @@ +# Simple Mode — прогресс + +Задача: по умолчанию у всех организаций «Простой режим» — только базовые +функции розничного учёта. Включить «Расширенный режим» может только +SuperAdmin и только для конкретной организации. Данные при переключении +не удаляются; при повторном включении всё видно. + +Старт: 2026-10-04. + +## Чек-лист + +- [x] 1. Domain + миграция `Organization.IsAdvancedMode` (bool, default false, backfill false) +- [ ] 2. API `PUT /api/superadmin/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 — старые, не наши). diff --git a/src/food-market.domain/Organizations/Organization.cs b/src/food-market.domain/Organizations/Organization.cs index 57f76a8..3ea8b2c 100644 --- a/src/food-market.domain/Organizations/Organization.cs +++ b/src/food-market.domain/Organizations/Organization.cs @@ -87,6 +87,16 @@ public class Organization : Entity /// просто захламляет карточку. public bool ShowDescriptionOnProduct { get; set; } + /// Расширенный режим для организации. По умолчанию false + /// — виден только «простой» набор функций (базовый розничный учёт). + /// Включить может только SuperAdmin через + /// PUT /api/superadmin/organizations/{id}/advanced-mode; в + /// настройках магазина переключателя нет. При выключении данные + /// скрытых модулей не удаляются; при повторном включении всё видно. + /// Серверная защита — атрибут [RequiresAdvancedMode] на + /// контроллерах скрытых модулей. + public bool IsAdvancedMode { get; set; } + // ─ Sprint 11: ОФД (фискализация чеков) ──────────────────────────── // Каждая организация выбирает СВОЕГО ОФД-оператора. Креды per-tenant: // у одной фирмы может быть Webkassa, у другой — Касса24. ApiKey/Secret diff --git a/src/food-market.infrastructure/Persistence/AppDbContext.cs b/src/food-market.infrastructure/Persistence/AppDbContext.cs index 9045738..426eb65 100644 --- a/src/food-market.infrastructure/Persistence/AppDbContext.cs +++ b/src/food-market.infrastructure/Persistence/AppDbContext.cs @@ -111,6 +111,10 @@ protected override void OnModelCreating(ModelBuilder builder) b.Property(o => o.CountryCode).HasMaxLength(2).IsRequired(); b.Property(o => o.Bin).HasMaxLength(20); b.Property(o => o.MoySkladToken).HasMaxLength(200); + // Simple/Advanced mode: default false (простой режим). Миграция + // Phase29a_IsAdvancedMode добавляет колонку, backfilling всех + // существующих организаций в false. + b.Property(o => o.IsAdvancedMode).HasDefaultValue(false); b.HasOne(o => o.DefaultCurrency).WithMany().HasForeignKey(o => o.DefaultCurrencyId).OnDelete(DeleteBehavior.Restrict); b.HasIndex(o => o.Name); b.HasIndex(o => o.IsArchived); diff --git a/src/food-market.infrastructure/Persistence/Migrations/20261004100000_Phase29a_IsAdvancedMode.cs b/src/food-market.infrastructure/Persistence/Migrations/20261004100000_Phase29a_IsAdvancedMode.cs new file mode 100644 index 0000000..2b7518c --- /dev/null +++ b/src/food-market.infrastructure/Persistence/Migrations/20261004100000_Phase29a_IsAdvancedMode.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using foodmarket.Infrastructure.Persistence; + +#nullable disable + +namespace foodmarket.Infrastructure.Persistence.Migrations +{ + /// Phase29a — колонка IsAdvancedMode на организациях. + /// Default false → все существующие и новые организации стартуют + /// в «простом» режиме, SuperAdmin переключает вручную через API. + /// Идемпотентно (ADD COLUMN IF NOT EXISTS) и без backfilling — default + /// в CREATE/ALTER COLUMN уже выставляет значение всем строкам. + [DbContext(typeof(AppDbContext))] + [Migration("20261004100000_Phase29a_IsAdvancedMode")] + public partial class Phase29a_IsAdvancedMode : Migration + { + protected override void Up(MigrationBuilder b) + { + b.Sql(@" + ALTER TABLE public.organizations + ADD COLUMN IF NOT EXISTS ""IsAdvancedMode"" boolean NOT NULL DEFAULT false; + "); + } + + protected override void Down(MigrationBuilder b) + { + b.Sql(@"ALTER TABLE public.organizations DROP COLUMN IF EXISTS ""IsAdvancedMode"";"); + } + } +} diff --git a/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index 48178e1..8ddd768 100644 --- a/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -1089,6 +1089,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsArchived") .HasColumnType("boolean"); + b.Property("IsAdvancedMode") + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("ArchivedAt") .HasColumnType("timestamp with time zone");