Application разделён на Abstractions (25 интерфейсов), Models (13 доменных типов) и Dtos (CardMoveResultDto); namespace приведён к путям, using потребителей мигрированы.
403 lines
30 KiB
C#
403 lines
30 KiB
C#
using Deal.Modules.Kanban.Application.Models;
|
||
|
||
namespace Deal.Modules.Kanban.Application;
|
||
|
||
/// <summary>
|
||
/// Единый порт хранилища карточек и контейнеров (таблицы Cards/Containers/LeadComments/CardMoves тенанта;
|
||
/// жёсткое удаление карточки дополнительно чистит строки DedupEntries — Ruling 3), Ruling 1.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Объявлен в модуле Kanban — владельце единой сущности карточки (этап 9); реализация — EF-адаптер
|
||
/// <c>KanbanStore</c> в Deal.Infrastructure (регистрация в AddDealPersistence). Порт покрывает оба
|
||
/// пространства одной таблицы Cards: дашборд (служебные зоны/доски) и «Выбранные» (контейнеры-стадии),
|
||
/// поэтому отдельный порт карточек «Выбранных» упразднён. Порт оперирует DTO модуля; маппинг
|
||
/// DTO ↔ строки (включая JSON-поля и human-метки времени, Ruling 10) выполняет адаптер вручную
|
||
/// (эталон SettingsStore.cs). Набор методов — ровно тот, что нужен сервису карточек (YAGNI):
|
||
/// контейнеры, карточки и переносы, комментарии, журнал CardMoves, кандидаты правил хранения (Ruling 8),
|
||
/// пересчёт конверсий (Ruling 7) и выборка «Неразобранного» для эвристики (Ruling 3).
|
||
/// Id записей генерирует модуль (Ruling 12: короткие префиксные id через KanbanIdPrefixes) и передаёт
|
||
/// готовыми — хранилище id не создаёт. Чтения — AsNoTracking; сортировки (received_at DESC и т.п.) —
|
||
/// обязанность адаптера. Удаление карточки навсегда (DeleteForeverAsync/ClearColAsync/PurgeAsync)
|
||
/// снимает и «мягкие» dedup-ссылки (Ruling 3): строки таблицы DedupEntries чужого модуля адаптер удаляет
|
||
/// напрямую тем же TenantDbContext, цикла Kanban → Pipeline не возникает (Kanban о Pipeline не знает).
|
||
/// </remarks>
|
||
public interface ICardStore
|
||
{
|
||
// ── Контейнеры (колонки/стадии/зоны) ─────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Контейнеры пространства в порядке показа: ORDER BY space, position (этап 9, T4).
|
||
/// </summary>
|
||
/// <param name="space">Пространство (dashboard/selected) либо null — все контейнеры.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Контейнеры (DTO, счётчики заполняет сервис); пусто — контейнеров нет.</returns>
|
||
public Task<IReadOnlyList<ContainerDto>> ListContainersAsync(string? space, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Один контейнер по id (PATCH 404-семантика, переносы и валидация колонок).
|
||
/// </summary>
|
||
/// <param name="containerId">Id контейнера (inbox/archive/trash/стадия/<c>b_...</c>).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Контейнер или null, если строки нет.</returns>
|
||
public Task<ContainerDto?> GetContainerAsync(string containerId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Создаёт контейнер (id/позицию/цвет/дефолты вычисляет ContainersService).
|
||
/// </summary>
|
||
/// <param name="container">Полный контейнер для вставки, включая <see cref="ContainerDto.Order"/>.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task CreateContainerAsync(ContainerDto container, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Обновляет контейнер целиком (сервис читает Get + применяет ContainerPatchDto).
|
||
/// </summary>
|
||
/// <param name="container">Контейнер с изменёнными полями (идентифицируется по Id).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task UpdateContainerAsync(ContainerDto container, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Удаляет контейнер, предварительно перенося его карточки в «Неразобранное».
|
||
/// </summary>
|
||
/// <param name="containerId">Id удаляемого контейнера.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Сколько карточек перенесено в inbox (0 — контейнера/карточек не было; ответ {ok, movedToInbox}).</returns>
|
||
public Task<int> DeleteContainerAsync(string containerId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Переставляет контейнеры пространства: позиции 0..N-1 в порядке списка.
|
||
/// </summary>
|
||
/// <param name="space">Пространство переставляемых контейнеров.</param>
|
||
/// <param name="containerIds">Id контейнеров в новом порядке.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task ReorderContainersAsync(string space, IReadOnlyList<string> containerIds, CancellationToken ct);
|
||
|
||
// ── Карточки ──────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Карточки колонки или всех колонок дашборда, ORDER BY received_at DESC (list_leads L151–156).
|
||
/// </summary>
|
||
/// <param name="query">Фильтр: Col — конкретная колонка либо null (все колонки дашборда).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Полные карточки (DTO): JSON-поля разобраны, комментарии приложены, time посчитан.</returns>
|
||
public Task<IReadOnlyList<CardDto>> ListCardsAsync(CardsQuery query, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Карточки пространства «Выбранные», ORDER BY updated_at DESC (list_cards projects.py L58–63).
|
||
/// </summary>
|
||
/// <param name="containerId">Фильтр по контейнеру-стадии (id каталога <see cref="Deal.Modules.Cards.Application.Models.CardsDefaultContainers"/>); null — все стадии.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Полные карточки (DTO): JSON-поля разобраны, комментарии приложены, time посчитан.</returns>
|
||
public Task<IReadOnlyList<CardDto>> ListSelectedCardsAsync(string? containerId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Полнотекстовый поиск карточек (FTS + LIKE-дополнение; leads.py search L509–551, Ruling 6/Task 12).
|
||
/// </summary>
|
||
/// <param name="q">Поисковый запрос (уже Trim+lowercase, как в отсев-поиске; короче 2 символов сервис не пропускает).</param>
|
||
/// <param name="limit">Ограничение результата (эндпоинт шлёт 12, Ruling 6).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Полные карточки (комментарии приложены, time посчитан): FTS-кандидаты по
|
||
/// убыванию ts_rank (SearchTsv @@ plainto_tsquery), затем LIKE-дополнение, внутри — ReceivedAt DESC.</returns>
|
||
public Task<IReadOnlyList<CardDto>> SearchCardsAsync(string q, int limit, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Одна карточка по id (GET /api/cards/{id}, а также перечитывание после переноса).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Карточка или null, если строки нет.</returns>
|
||
public Task<CardDto?> GetCardAsync(string cardId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Карточка по исходному сообщению (диалог + id сообщения) — ручная разметка ML (§8).
|
||
/// </summary>
|
||
/// <remarks>Если сообщение давало несколько карточек (повторная разметка/пересоздание), берётся самая
|
||
/// свежая по ReceivedAt. Пустой dialogId — null (нечем искать).</remarks>
|
||
/// <param name="dialogId">Id диалога-источника сообщения.</param>
|
||
/// <param name="msgId">Id исходного сообщения в Telegram.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Карточка или null, если сообщение не становилось карточкой.</returns>
|
||
public Task<CardDto?> GetCardBySourceAsync(string dialogId, long msgId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Создаёт карточку из готового снимка (пайплайн этапа 4, ручное создание; CreatedAt — UTC-now).
|
||
/// </summary>
|
||
/// <param name="snapshot">Полное состояние новой карточки (id сгенерирован модулем, см. CardSnapshot).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task AddCardAsync(CardSnapshot snapshot, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Точечная правка полей по присутствующим в патче + bump UpdatedAt (patch_card projects.py L159–187).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="patch">Изменяемые поля (null — поле не меняется; JSON-поля — полная замена).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||
public Task<bool> PatchCardAsync(string cardId, CardPatch patch, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Атомарно дописывает ссылку в JSON-массив links ОДНИМ UPDATE (jsonb-append) + bump UpdatedAt.
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="link">Готовая ссылка {id,name,url} (id сгенерирован модулем).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||
public Task<bool> AddLinkAsync(string cardId, CardLinkDto link, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Атомарно убирает из JSON-массива links элемент с указанным id ОДНИМ UPDATE (jsonb-фильтрация) + bump UpdatedAt.
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="linkId">Id удаляемой ссылки (<c>pl_...</c>).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||
public Task<bool> RemoveLinkAsync(string cardId, string linkId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Атомарно дописывает метаданные файла в JSON-массив files ОДНИМ UPDATE (jsonb-append) + bump UpdatedAt.
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="file">Готовые метаданные {id,name,size,kind,label,objectKey} (id сгенерирован модулем).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||
public Task<bool> AddFileAsync(string cardId, CardFileDto file, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Атомарно убирает из JSON-массива files элемент с указанным id ОДНИМ UPDATE (jsonb-фильтрация) + bump UpdatedAt.
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="fileId">Id удаляемой записи файла (<c>pf_...</c>).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||
public Task<bool> RemoveFileAsync(string cardId, string fileId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Смена контейнера-стадии: один UPDATE (col, reminder_at=NULL, reminder_fired=false, updated_at=atMs)
|
||
/// + перезапись history-массива с добавленной записью (move_stage projects.py L202–216; Ruling 7).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="containerId">Новый контейнер-стадия (валидирует сервис каталогом <see cref="Deal.Modules.Cards.Application.Models.CardsDefaultContainers"/>).</param>
|
||
/// <param name="historyEntry">Готовая запись истории {id,at,stage} (id сгенерирован модулем).</param>
|
||
/// <param name="atMs">Время переноса, epoch-ms (пишется в updated_at и в запись истории).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||
public Task<bool> MoveCardStageAsync(string cardId, string containerId, CardHistoryDto historyEntry, long atMs, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Устанавливает напоминание: reminder_at, reminder_fired=false + bump UpdatedAt (set_reminder projects.py L236–243).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="atMs">Время напоминания, epoch-ms.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task SetReminderAsync(string cardId, long atMs, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Снимает напоминание: reminder_at=NULL, reminder_fired=false (clear_reminder projects.py L246–247).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task ClearReminderAsync(string cardId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Полная ручная очистка контейнера-стадии: DELETE строк (clear_stage projects.py L223–231).
|
||
/// </summary>
|
||
/// <param name="containerId">Очищаемая стадия (допустимость — только <c>rejected</c> — валидирует сервис).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Сколько карточек удалено (0 — стадия пуста).</returns>
|
||
public Task<int> ClearStageAsync(string containerId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Наступившие напоминания стадии hold: ReminderAt ≤ now, ReminderFired=false, ORDER BY ReminderAt (check_reminders projects.py L270–275).
|
||
/// </summary>
|
||
/// <param name="now">Текущий момент (UTC) для сравнения с ReminderAt.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Due-строки {id,title,containerId} в порядке наступления; пусто — сработавших нет.</returns>
|
||
public Task<IReadOnlyList<CardReminderDueDto>> ListDueRemindersAsync(DateTimeOffset now, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Помечает due-карточки сработавшими: ReminderFired=true по списку id (check_reminders projects.py L277–278).
|
||
/// </summary>
|
||
/// <param name="cardIds">Id карточек, чьи напоминания «выстрелили».</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task MarkRemindersFiredAsync(IReadOnlyList<string> cardIds, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Очищает протухшие напоминания при выключенной настройке: ReminderAt=NULL WHERE ReminderAt ≤ now (check_reminders L266–269).
|
||
/// </summary>
|
||
/// <param name="now">Текущий момент (UTC).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Сколько строк очищено.</returns>
|
||
public Task<int> ClearExpiredRemindersAsync(DateTimeOffset now, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Меняет колонку/состояние карточки (move/trash/restore/автоархив) — см. CardColumnUpdateDto.
|
||
/// </summary>
|
||
/// <param name="update">Новое состояние колонки карточки (matchHits пересчитан в модуле, Ruling 2).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Применяет результат ручной переклассификации к существующей карточке ОДНИМ обновлением
|
||
/// (leads.py reclassify_lead L346–367): тип/заголовок/суть/стек/бюджет/конверсия/контакты/matchHits + колонка.
|
||
/// </summary>
|
||
/// <param name="update">Поля классификации (полная замена; см. CardReclassificationDto).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||
public Task<bool> ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Снимает флаг «новое»: с одной карточки (cardId), с колонки (col) или со всех (оба null) — leads.py mark_seen L250–256.
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки либо null.</param>
|
||
/// <param name="col">Колонка либо null.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task UpdateSeenAsync(string? cardId, string? col, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Удаляет карточку навсегда: Cards + комментарии (FK cascade) + строки дедупа карточки
|
||
/// (DedupEntries WHERE LeadId=?, Ruling 3); журнал/outbox не трогает (leads.py _hard_delete L225–234).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task DeleteForeverAsync(string cardId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Полная очистка служебной колонки (только trash|archive — валидирует сервис), leads.py clear_col L237–247;
|
||
/// строки дедупа удаляемых карточек чистятся вместе с ними (Ruling 3).
|
||
/// </summary>
|
||
/// <param name="col">Очищаемая колонка (trash/archive).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Сколько карточек удалено (0 — колонка пуста).</returns>
|
||
public Task<int> ClearColAsync(string col, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Счётчики карточек по колонкам (count + new) для GET /api/cards/counts (leads.py counts L268–279).
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Словарь col → {count, new}; ключи — только колонки с карточками.</returns>
|
||
public Task<IReadOnlyDictionary<string, CardColumnCountDto>> CountCardsByColAsync(CancellationToken ct);
|
||
|
||
// ── Комментарии (LeadComments) ────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Комментарии карточки (для ответа add-comment и карточки; сортировка по времени добавления).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Комментарии (time — human-метка от CreatedAt); пусто — комментариев нет.</returns>
|
||
public Task<IReadOnlyList<CardCommentDto>> ListCommentsAsync(string cardId, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Добавляет комментарий (leads.py add_comment L259–265; CreatedAt — UTC-now).
|
||
/// </summary>
|
||
/// <param name="commentId">Готовый id (<c>cm_...</c>, генерирует модуль).</param>
|
||
/// <param name="cardId">Id карточки.</param>
|
||
/// <param name="by">Автор («Вы» — свои комментарии).</param>
|
||
/// <param name="text">Текст (непустой — валидирует сервис, 400 «Пустой комментарий»).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task AddCommentAsync(string commentId, string cardId, string by, string text, CancellationToken ct);
|
||
|
||
// ── Журнал действий (CardMoves = learning_log) ────────────────────────
|
||
|
||
/// <summary>
|
||
/// Пишет строку журнала действия пользователя (move/trash/restore/comment) — leads.py _log_learning L40–44.
|
||
/// </summary>
|
||
/// <param name="move">Запись журнала (id <c>lm_...</c> сгенерирован модулем).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task AddMoveAsync(CardMoveDto move, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Число записей журнала = счётчик learning (Ruling 4, Task 5 StatusAsync).
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Количество строк CardMoves.</returns>
|
||
public Task<int> CountMovesAsync(CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Свежие примеры разметки пользователя для few-shot ИИ-классификации (pipeline.py _learning_examples L201–215).
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Join журнала CardMoves с карточками (Cards.SourceMsg): действия move/restore, цель не служебная
|
||
/// (trash/archive), исходный текст непустой; ORDER BY created_at DESC, ≤ limit записей. Используется
|
||
/// контекст-билдером классификации (план Task 15, Ruling 5) — текст примера дополнительно режется
|
||
/// вызывающим до 500 кодовых точек (python L214).
|
||
/// </remarks>
|
||
/// <param name="limit">Максимум примеров (python L201: 8).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Примеры «текст → колонка», свежие первыми; пусто — истории разметки нет.</returns>
|
||
public Task<IReadOnlyList<AiMarkupExampleDto>> GetAiMarkupExamplesAsync(int limit, CancellationToken ct);
|
||
|
||
// ── Правила хранения (тик, Ruling 8) ──────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Кандидаты на автоархив: карточки досок и «Неразобранного» со ReceivedAt старше срока (tick_storage L462–467).
|
||
/// </summary>
|
||
/// <param name="receivedBeforeUtc">Граница: received_at < now − archiveAfterDays.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Id карточек-кандидатов (колонка при архивации пишется archive через UpdateColumnAsync).</returns>
|
||
public Task<IReadOnlyList<string>> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Архивирует пачку карточек ОДНИМ UPDATE: col=archive, is_new=false, archived_at=archivedAt,
|
||
/// matchHits — пустой массив (тик tick_storage L462–473; batch-замена по-карточных UpdateColumnAsync,
|
||
/// PrevCol при архивации не трогается — Ruling 8).
|
||
/// </summary>
|
||
/// <param name="cardIds">Id карточек-кандидатов (список из ListArchiveCandidatesAsync).</param>
|
||
/// <param name="archivedAt">Момент архивации (один «now» тика).</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Сколько карточек реально архивировано (0 — кандидатов не было/уже не в рабочих колонках).</returns>
|
||
public Task<int> ArchiveAsync(IReadOnlyList<string> cardIds, DateTimeOffset archivedAt, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Кандидаты на очистку архива: col='archive' и ArchivedAt старше срока (tick_storage L475–478).
|
||
/// </summary>
|
||
/// <param name="archivedBeforeUtc">Граница: archived_at < now − archiveClearDays.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Id карточек для жёсткого удаления.</returns>
|
||
public Task<IReadOnlyList<string>> ListExpiredArchiveCandidatesAsync(DateTimeOffset archivedBeforeUtc, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Кандидаты на очистку корзины: col='trash' и ReceivedAt старше срока (tick_storage L480–483).
|
||
/// </summary>
|
||
/// <param name="receivedBeforeUtc">Граница: received_at < now − trashClearDays.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Id карточек для жёсткого удаления.</returns>
|
||
public Task<IReadOnlyList<string>> ListTrashCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Жёстко удаляет пачку карточек: Cards + комментарии (FK cascade) + строки дедупа карточек
|
||
/// (DedupEntries WHERE LeadId IN …, Ruling 3) — для очисток тика и clear-col (Ruling 8).
|
||
/// </summary>
|
||
/// <param name="cardIds">Id карточек на удаление.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Сколько карточек удалено.</returns>
|
||
public Task<int> PurgeAsync(IReadOnlyList<string> cardIds, CancellationToken ct);
|
||
|
||
// ── Пересчёт конверсий (Ruling 7, Task 12) ─────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Карточки для пересчёта ConvFrom/ConvTo/ConvCur: budgetCur непуст и col NOT IN (archive, trash) — recompute_conversions L106–130.
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Карточки с бюджетом (используются Budget/бюджетные поля; служебные колонки исключены).</returns>
|
||
public Task<IReadOnlyList<CardDto>> ListCardsForConversionAsync(CancellationToken ct);
|
||
|
||
/// <summary>
|
||
/// Записывает пересчитанную конверсию бюджета карточки (обновляет только conv-поля).
|
||
/// </summary>
|
||
/// <param name="cardId">Id карточки.</param>
|
||
/// <param name="convFrom">Сконвертированная нижняя граница, либо null.</param>
|
||
/// <param name="convTo">Сконвертированная верхняя граница, либо null.</param>
|
||
/// <param name="convCur">Валюта конверсии (целевая валюта тенанта); пусто — конверсия снята.</param>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
public Task UpdateConversionAsync(string cardId, double? convFrom, double? convTo, string convCur, CancellationToken ct);
|
||
|
||
// ── Эвристика ИИ-предложений (Ruling 3, Task 14) ───────────────────────
|
||
|
||
/// <summary>
|
||
/// Карточки «Неразобранного» с исходным текстом — вход эвристики suggest (suggest.py, Ruling 3).
|
||
/// </summary>
|
||
/// <param name="ct">Токен отмены.</param>
|
||
/// <returns>Карточки col='inbox' с непустым SourceMsg (частотные темы считаются по source_msg).</returns>
|
||
public Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct);
|
||
}
|