using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Settings.Application; namespace Deal.Modules.Kanban.Application; /// /// Сервис правил хранения: автоархив и очистка архива/корзины по срокам — Ruling 8, план Task 10. /// /// /// Чистый сервис модуля (без EF/HTTP), повторяет leads.py tick_storage (L454–493). Настройки /// autoArchive/archiveAfterDays/archiveClearDays/trashClearDays читает типизированным снимком /// (C30: один GetAllAsync на тик; отсутствие/повреждение → дефолт из /// SettingsDefaults). Один «now» /// (UTC) на весь тик, как в прототипе. Последовательность 1:1 с tick_storage: /// (1) автоархив — карточки досок и «Неразобранного» со ReceivedAt старше archiveAfterDays → col=archive, /// is_new=false, archived_at=now (PrevCol НЕ трогается — Ruling 8: null в CardColumnUpdateDto), пачкой /// (один UPDATE вместо N); /// (2) очистка архива — col='archive' с ArchivedAt старше archiveClearDays (дефолт 90) → жёсткое удаление; /// (3) очистка корзины — col='trash' с ReceivedAt старше trashClearDays (дефолт 7) → жёсткое удаление. /// Удаление — пачкой (Cards + комментарии каскадом; журнал CardMoves /// и MlOutbox не трогаются, Ruling 10). purgedRejected — отсев пайплайна (этап 4); в этапе 3 всегда 0. /// SSE-тосты сервис НЕ публикует: это обязанность эндпоинтов Api (Ruling 5) по статистике TickAsync. /// /// Порт хранилища канбана: кандидаты тика, batch-перенос в архив, жёсткое удаление пачки. /// KV-хранилище настроек тенанта (снимок правил хранения, Ruling 8). public sealed class StorageTickService(ICardStore store, ISettingsStore settings) { /// /// Один тик правил хранения тенанта (Ruling 8; leads.py tick_storage L454–493). /// /// Токен отмены. /// Статистика тика: сколько карточек архивировано/очищено из архива и корзины (purgedRejected=0). public async Task TickAsync(CancellationToken ct) { // Все правила хранения — из одного типизированного снимка настроек (C30): один GetAllAsync на тик. TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(settings, ct); bool autoArchive = settingsSnapshot.GetBool(SettingsKeys.AutoArchive, SettingsDefaults.AutoArchive); int archiveAfterDays = settingsSnapshot.GetInt(SettingsKeys.ArchiveAfterDays, SettingsDefaults.ArchiveAfterDays); int archiveClearDays = settingsSnapshot.GetInt(SettingsKeys.ArchiveClearDays, SettingsDefaults.ArchiveClearDays); int trashClearDays = settingsSnapshot.GetInt(SettingsKeys.TrashClearDays, SettingsDefaults.TrashClearDays); // Один «now» на весь тик — границы автоархива и очисток от одного момента (tick_storage L459). DateTimeOffset now = DateTimeOffset.UtcNow; int archived = 0; if (autoArchive) { // Автоархив: карточки досок и «Неразобранного» (tick_storage L462–467). В архив — пачкой одним // UPDATE (архивация как системный перенос: col=archive, is_new=false, archived_at=now, matchHits // пусто; prev_col не трогается, Ruling 8); по-карточные UPDATE'ы тика заменены на batch. IReadOnlyList candidates = await store.ListArchiveCandidatesAsync( now.AddDays(-archiveAfterDays), ct); archived = candidates.Count == 0 ? 0 : await store.ArchiveAsync(candidates, now, ct); } // Очистка архива по сроку хранения (tick_storage L475–478). int purgedArchive = await PurgeAsync( await store.ListExpiredArchiveCandidatesAsync(now.AddDays(-archiveClearDays), ct), ct); // Очистка корзины по сроку хранения (tick_storage L480–483). int purgedTrash = await PurgeAsync( await store.ListTrashCandidatesAsync(now.AddDays(-trashClearDays), ct), ct); // purgedRejected — отсев пайплайна живёт в Pipeline (этап 4); в этапе 3 всегда 0 (Ruling 8). return new StorageTickStatsDto(archived, purgedArchive, purgedTrash, PurgedRejected: 0); } // Жёстко удаляет пачку кандидатов очистки (Ruling 8; KanbanStore.PurgeAsync — комментарии каскадом). // cardIds: Id карточек-кандидатов из выборки хранилища. // ct: Токен отмены. // Возвращает: Сколько карточек реально удалено (0 — кандидатов не было). private async Task PurgeAsync(IReadOnlyList cardIds, CancellationToken ct) { return cardIds.Count == 0 ? 0 : await store.PurgeAsync(cardIds, ct); } }