Files
Deal/src/core/Deal.Modules.Kanban/Application/ICardStore.cs
T
Rustam Khalimov 31c434ed93 Разбить Deal.Modules.Cards по назначению
Application разделён на Abstractions (25 интерфейсов), Models (13
доменных типов) и Dtos (CardMoveResultDto); namespace приведён к путям,
using потребителей мигрированы.
2026-09-11 13:16:57 +03:00

403 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 L151156).
/// </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 L5863).
/// </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 L509551, 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 L159187).
/// </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 L202216; 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 L236243).
/// </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 L246247).
/// </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 L223231).
/// </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 L270275).
/// </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 L277278).
/// </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 L266269).
/// </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 L346367): тип/заголовок/суть/стек/бюджет/конверсия/контакты/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 L250256.
/// </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 L225234).
/// </summary>
/// <param name="cardId">Id карточки.</param>
/// <param name="ct">Токен отмены.</param>
public Task DeleteForeverAsync(string cardId, CancellationToken ct);
/// <summary>
/// Полная очистка служебной колонки (только trash|archive — валидирует сервис), leads.py clear_col L237247;
/// строки дедупа удаляемых карточек чистятся вместе с ними (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 L268279).
/// </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 L259265; 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 L4044.
/// </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 L201215).
/// </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 L462467).
/// </summary>
/// <param name="receivedBeforeUtc">Граница: received_at &lt; 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 L462473; 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 L475478).
/// </summary>
/// <param name="archivedBeforeUtc">Граница: archived_at &lt; 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 L480483).
/// </summary>
/// <param name="receivedBeforeUtc">Граница: received_at &lt; 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 L106130.
/// </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);
}