feat(s29): Organization.IsAdvancedMode + миграция Phase29a (шаг 1/8)
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 <noreply@anthropic.com>
This commit is contained in:
parent
dc885f36b4
commit
4342318c2c
55
docs/simple-mode-progress.md
Normal file
55
docs/simple-mode-progress.md
Normal file
|
|
@ -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 — старые, не наши).
|
||||||
|
|
@ -87,6 +87,16 @@ public class Organization : Entity
|
||||||
/// просто захламляет карточку.</summary>
|
/// просто захламляет карточку.</summary>
|
||||||
public bool ShowDescriptionOnProduct { get; set; }
|
public bool ShowDescriptionOnProduct { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Расширенный режим для организации. По умолчанию <c>false</c>
|
||||||
|
/// — виден только «простой» набор функций (базовый розничный учёт).
|
||||||
|
/// Включить может только SuperAdmin через
|
||||||
|
/// <c>PUT /api/superadmin/organizations/{id}/advanced-mode</c>; в
|
||||||
|
/// настройках магазина переключателя нет. При выключении данные
|
||||||
|
/// скрытых модулей не удаляются; при повторном включении всё видно.
|
||||||
|
/// Серверная защита — атрибут <c>[RequiresAdvancedMode]</c> на
|
||||||
|
/// контроллерах скрытых модулей.</summary>
|
||||||
|
public bool IsAdvancedMode { get; set; }
|
||||||
|
|
||||||
// ─ Sprint 11: ОФД (фискализация чеков) ────────────────────────────
|
// ─ Sprint 11: ОФД (фискализация чеков) ────────────────────────────
|
||||||
// Каждая организация выбирает СВОЕГО ОФД-оператора. Креды per-tenant:
|
// Каждая организация выбирает СВОЕГО ОФД-оператора. Креды per-tenant:
|
||||||
// у одной фирмы может быть Webkassa, у другой — Касса24. ApiKey/Secret
|
// у одной фирмы может быть Webkassa, у другой — Касса24. ApiKey/Secret
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,10 @@ protected override void OnModelCreating(ModelBuilder builder)
|
||||||
b.Property(o => o.CountryCode).HasMaxLength(2).IsRequired();
|
b.Property(o => o.CountryCode).HasMaxLength(2).IsRequired();
|
||||||
b.Property(o => o.Bin).HasMaxLength(20);
|
b.Property(o => o.Bin).HasMaxLength(20);
|
||||||
b.Property(o => o.MoySkladToken).HasMaxLength(200);
|
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.HasOne(o => o.DefaultCurrency).WithMany().HasForeignKey(o => o.DefaultCurrencyId).OnDelete(DeleteBehavior.Restrict);
|
||||||
b.HasIndex(o => o.Name);
|
b.HasIndex(o => o.Name);
|
||||||
b.HasIndex(o => o.IsArchived);
|
b.HasIndex(o => o.IsArchived);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using foodmarket.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>Phase29a — колонка <c>IsAdvancedMode</c> на организациях.
|
||||||
|
/// Default <c>false</c> → все существующие и новые организации стартуют
|
||||||
|
/// в «простом» режиме, SuperAdmin переключает вручную через API.
|
||||||
|
/// Идемпотентно (ADD COLUMN IF NOT EXISTS) и без backfilling — default
|
||||||
|
/// в CREATE/ALTER COLUMN уже выставляет значение всем строкам.</summary>
|
||||||
|
[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"";");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1089,6 +1089,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
b.Property<bool>("IsArchived")
|
b.Property<bool>("IsArchived")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAdvancedMode")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<DateTime?>("ArchivedAt")
|
b.Property<DateTime?>("ArchivedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue