using foodmarket.Api.Infrastructure.Tenancy; using foodmarket.Application.Common.Tenancy; using foodmarket.Infrastructure.Integrations.MoySklad; using foodmarket.Infrastructure.Persistence; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace foodmarket.Api.Controllers.Admin; // Временные эндпоинты для очистки данных после кривых импортов. // Удалять только свой tenant — query-filter на DbSets это обеспечивает. [ApiController] [Authorize(Policy = "AdminAccess")] [Route("api/admin/cleanup")] public class AdminCleanupController : ControllerBase { private readonly AppDbContext _db; private readonly IServiceScopeFactory _scopes; private readonly ImportJobRegistry _jobs; private readonly ITenantContext _tenant; public AdminCleanupController( AppDbContext db, IServiceScopeFactory scopes, ImportJobRegistry jobs, ITenantContext tenant) { _db = db; _scopes = scopes; _jobs = jobs; _tenant = tenant; } public record CleanupStats( int Counterparties, int Products, int ProductGroups, int ProductBarcodes, int ProductPrices, int Supplies, int RetailSales, int Stocks, int StockMovements); public record CleanupResult(string Scope, CleanupStats Deleted); [HttpGet("stats")] public async Task> GetStats(CancellationToken ct) => new CleanupStats( await _db.Counterparties.CountAsync(ct), await _db.Products.CountAsync(ct), await _db.ProductGroups.CountAsync(ct), await _db.ProductBarcodes.CountAsync(ct), await _db.ProductPrices.CountAsync(ct), await _db.Supplies.CountAsync(ct), await _db.RetailSales.CountAsync(ct), await _db.Stocks.CountAsync(ct), await _db.StockMovements.CountAsync(ct)); /// Удалить всех контрагентов текущей организации. Чтобы не нарваться на FK, /// сначала обнуляем ссылки (Product.DefaultSupplier, RetailSale.Customer) и сносим /// поставки (они жёстко ссылаются на supplier). [HttpDelete("counterparties")] public async Task> WipeCounterparties(CancellationToken ct) { var before = await SnapshotAsync(ct); // 1. Обнуляем nullable-FK await _db.Products .Where(p => p.DefaultSupplierId != null) .ExecuteUpdateAsync(u => u.SetProperty(p => p.DefaultSupplierId, (Guid?)null), ct); await _db.RetailSales .Where(s => s.CustomerId != null) .ExecuteUpdateAsync(u => u.SetProperty(s => s.CustomerId, (Guid?)null), ct); // 2. Сносим поставки (NOT NULL supplier) + их stock movements/stocks await _db.StockMovements .Where(m => m.DocumentType == "supply" || m.DocumentType == "supply-reversal") .ExecuteDeleteAsync(ct); await _db.SupplyLines.ExecuteDeleteAsync(ct); await _db.Supplies.ExecuteDeleteAsync(ct); // 3. Контрагенты await _db.Counterparties.ExecuteDeleteAsync(ct); var after = await SnapshotAsync(ct); return new CleanupResult("counterparties", Diff(before, after)); } // Async-версия полной очистки: возвращает jobId, реальные DELETE в фоне, прогресс в Stage+Deleted. [HttpPost("all/async")] public ActionResult WipeAllAsync() { var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant."); var job = _jobs.Create("cleanup-all"); job.Stage = "Подготовка…"; _ = Task.Run(async () => { try { using var tenantScope = HttpContextTenantContext.UseOverride(orgId); using var scope = _scopes.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var steps = new (string Stage, Func> Run)[] { ("Движения склада", () => db.StockMovements.ExecuteDeleteAsync()), ("Остатки", () => db.Stocks.ExecuteDeleteAsync()), ("Строки поставок", () => db.SupplyLines.ExecuteDeleteAsync()), ("Поставки", () => db.Supplies.ExecuteDeleteAsync()), ("Строки продаж", () => db.RetailSaleLines.ExecuteDeleteAsync()), ("Продажи", () => db.RetailSales.ExecuteDeleteAsync()), ("Изображения товаров", () => db.ProductImages.ExecuteDeleteAsync()), ("Цены товаров", () => db.ProductPrices.ExecuteDeleteAsync()), ("Штрихкоды", () => db.ProductBarcodes.ExecuteDeleteAsync()), ("Товары", () => db.Products.ExecuteDeleteAsync()), ("Группы товаров", () => db.ProductGroups.ExecuteDeleteAsync()), ("Контрагенты", () => db.Counterparties.ExecuteDeleteAsync()), }; foreach (var (stage, run) in steps) { job.Stage = $"Удаление: {stage}…"; job.Deleted += await run(); } job.Stage = "Готово"; job.Message = $"Удалено записей: {job.Deleted}."; job.Status = ImportJobStatus.Succeeded; } catch (Exception ex) { job.Status = ImportJobStatus.Failed; job.Message = ex.Message; job.Errors.Add(ex.ToString()); } finally { job.FinishedAt = DateTime.UtcNow; } }); return Ok(new { jobId = job.Id }); } /// Полная очистка данных текущей организации — всё кроме настроек: /// остаются Organization, пользователи, справочники (Country, Currency, UnitOfMeasure, /// PriceType), Store и RetailPoint. Удаляются: Product*, ProductGroup, Counterparty, /// Supply*, RetailSale*, Stock, StockMovement. [HttpDelete("all")] public async Task> WipeAll(CancellationToken ct) { var before = await SnapshotAsync(ct); // Documents first — they reference products, counterparties, stores. await _db.StockMovements.ExecuteDeleteAsync(ct); await _db.Stocks.ExecuteDeleteAsync(ct); await _db.SupplyLines.ExecuteDeleteAsync(ct); await _db.Supplies.ExecuteDeleteAsync(ct); await _db.RetailSaleLines.ExecuteDeleteAsync(ct); await _db.RetailSales.ExecuteDeleteAsync(ct); // Product composites. await _db.ProductImages.ExecuteDeleteAsync(ct); await _db.ProductPrices.ExecuteDeleteAsync(ct); await _db.ProductBarcodes.ExecuteDeleteAsync(ct); // Products reference counterparty.DefaultSupplier — FK Restrict, but we're about // to delete products anyway, so order products → counterparties. await _db.Products.ExecuteDeleteAsync(ct); await _db.ProductGroups.ExecuteDeleteAsync(ct); await _db.Counterparties.ExecuteDeleteAsync(ct); var after = await SnapshotAsync(ct); return new CleanupResult("all", Diff(before, after)); } private async Task SnapshotAsync(CancellationToken ct) => new( await _db.Counterparties.CountAsync(ct), await _db.Products.CountAsync(ct), await _db.ProductGroups.CountAsync(ct), await _db.ProductBarcodes.CountAsync(ct), await _db.ProductPrices.CountAsync(ct), await _db.Supplies.CountAsync(ct), await _db.RetailSales.CountAsync(ct), await _db.Stocks.CountAsync(ct), await _db.StockMovements.CountAsync(ct)); private static CleanupStats Diff(CleanupStats a, CleanupStats b) => new( a.Counterparties - b.Counterparties, a.Products - b.Products, a.ProductGroups - b.ProductGroups, a.ProductBarcodes - b.ProductBarcodes, a.ProductPrices - b.ProductPrices, a.Supplies - b.Supplies, a.RetailSales - b.RetailSales, a.Stocks - b.Stocks, a.StockMovements - b.StockMovements); }