fix(super-admin): новая org через UI получает полный bootstrap (как Demo)
Some checks failed
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Has been cancelled
Docker API / Build + push API (push) Successful in 46s
Docker API / Deploy API on stage (push) Successful in 17s

POST /api/super-admin/organizations создавал только Store + Admin role
в inline-коде — у новой орги не было единиц измерения, типов цен,
кастомных ролей шаблонов (Менеджер/Кладовщик/Закупщик/Бухгалтер),
кассы. Юзеру приходилось бы заводить всё руками.

Решение — переиспользовать DevDataSeeder.SeedTenantReferencesAsync,
который уже умеет всё это идемпотентно:
- 5 единиц измерения (штука/кг/л/м/упаковка по ОКЕИ)
- 2 типа цен (Розничная IsSystem+IsRequired+IsRetail / Оптовая)
- «Основной склад» MAIN
- «Касса 1» POS-1
- 6 ролей через SeedEmployeeRolesAsync (2 системных + 4 шаблона)

Helper повышен с private на public static. В контроллере убран
inline Store + AdminRole, после Add(org)+SaveChanges вызывается
seed, потом находим уже созданную «Администратор» роль для линковки
с Employee.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-26 14:58:23 +05:00
parent 982141598a
commit d553933580
2 changed files with 17 additions and 16 deletions

View file

@ -1,5 +1,5 @@
using foodmarket.Api.Seed;
using foodmarket.Application.Common; using foodmarket.Application.Common;
using foodmarket.Domain.Catalog;
using foodmarket.Domain.Organizations; using foodmarket.Domain.Organizations;
using foodmarket.Infrastructure.Identity; using foodmarket.Infrastructure.Identity;
using foodmarket.Infrastructure.Persistence; using foodmarket.Infrastructure.Persistence;
@ -100,21 +100,16 @@ public async Task<ActionResult<CreateOrgResult>> Create([FromBody] CreateOrgRequ
DefaultCurrencyId = input.Org.DefaultCurrencyId, DefaultCurrencyId = input.Org.DefaultCurrencyId,
}; };
_db.Organizations.Add(org); _db.Organizations.Add(org);
// Системные референсы (Stores, Cashiers, Roles) подсеет DevDataSeeder при await _db.SaveChangesAsync(ct);
// следующем рестарте; для немедленной готовности создаём минимум руками.
var mainStore = new Store
{
OrganizationId = org.Id, Name = "Основной склад", Code = "MAIN", IsMain = true,
};
_db.Stores.Add(mainStore);
var adminRole = new EmployeeRole // Полный bootstrap tenant-сущностей: единицы измерения, типы цен,
{ // «Основной склад», «Касса 1», 6 ролей (2 системные + 4 кастомные шаблона).
OrganizationId = org.Id, // Один helper и в DevDataSeeder, и здесь — гарантирует одинаковое
Name = "Администратор", Description = "Полный доступ ко всем разделам организации", // состояние новой орги независимо от пути создания.
IsSystem = true, SortOrder = 0, Permissions = RolePermissions.All(), await DevDataSeeder.SeedTenantReferencesAsync(_db, org.Id, ct);
};
_db.EmployeeRoles.Add(adminRole); var adminRole = await _db.EmployeeRoles.IgnoreQueryFilters()
.FirstAsync(r => r.OrganizationId == org.Id && r.IsSystem && r.Name == "Администратор", ct);
// AppUser админа // AppUser админа
var existing = await _userMgr.FindByEmailAsync(input.AdminEmail); var existing = await _userMgr.FindByEmailAsync(input.AdminEmail);

View file

@ -117,7 +117,13 @@ private static async Task SeedAdminEmployeeAsync(AppDbContext db, Guid orgId, Gu
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
} }
private static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, CancellationToken ct) /// <summary>Bootstrap минимально-достаточного набора tenant-сущностей для
/// новой организации: единицы измерения (ОКЕИ), типы цен (Розничная+Оптовая),
/// «Основной склад» MAIN, «Касса 1» POS-1, и системные роли через
/// SeedEmployeeRolesAsync. Идемпотентно: каждый блок проверяет существующие
/// записи. Используется и при первом старте Demo, и при создании org через
/// SuperAdmin UI.</summary>
public static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, CancellationToken ct)
{ {
var anyUnit = await db.UnitsOfMeasure.IgnoreQueryFilters().AnyAsync(u => u.OrganizationId == orgId, ct); var anyUnit = await db.UnitsOfMeasure.IgnoreQueryFilters().AnyAsync(u => u.OrganizationId == orgId, ct);
if (!anyUnit) if (!anyUnit)