using System.Collections.Concurrent; namespace foodmarket.Infrastructure.Integrations.MoySklad; public enum ImportJobStatus { Running, Succeeded, Failed, Cancelled } public class ImportJobProgress { public Guid Id { get; init; } = Guid.NewGuid(); public string Kind { get; init; } = ""; // "products" | "counterparties" public DateTime StartedAt { get; init; } = DateTime.UtcNow; public DateTime? FinishedAt { get; set; } public ImportJobStatus Status { get; set; } = ImportJobStatus.Running; public string? Stage { get; set; } // человекочитаемое описание текущего шага public int Total { get; set; } // входящих записей от MS (растёт по мере пейджинга) public int Created { get; set; } public int Updated { get; set; } public int Skipped { get; set; } public int Deleted { get; set; } // для cleanup public int GroupsCreated { get; set; } public string? Message { get; set; } // последняя ошибка / финальное сообщение public List Errors { get; set; } = []; } // Process-memory реестр прогресса фоновых импортов. Один процесс API — одно DI singleton. // При рестарте контейнера история импортов теряется — для просмотра «вчерашнего» надо // смотреть логи. На MVP достаточно. public class ImportJobRegistry { private readonly ConcurrentDictionary _jobs = new(); public ImportJobProgress Create(string kind) { var job = new ImportJobProgress { Kind = kind }; _jobs[job.Id] = job; return job; } public ImportJobProgress? Get(Guid id) => _jobs.TryGetValue(id, out var j) ? j : null; public IReadOnlyList RecentlyFinished(int take = 10) => _jobs.Values .Where(j => j.FinishedAt is not null) .OrderByDescending(j => j.FinishedAt) .Take(take) .ToList(); }