Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
@@ -0,0 +1,99 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Агрегат карточки: ядро + опциональные модули (единая сущность всех дашбордов).
/// </summary>
/// <remarks>
/// Один класс на карточку (не иерархия видов): вид определяется контейнером и наполненностью модулей,
/// а не типом. Модули — это секции данных (роли интерфейсов), пустые, пока карточка «входящая»,
/// наполняются, когда карточка «в работе» (файлы/ссылки/ТЗ/история/напоминания). Класс неизменяемый
/// (init/свойства только для чтения) — изменения применяются сервисами через порт и возвращают новый
/// снимок, как единая CardDto (wire 1:1 сохраняется).
/// </remarks>
public sealed class Card :
ICard,
IContentCard,
IBudgetedCard,
IContactCard,
IAttributedCard,
ICommentableCard,
ILinkCard,
IFileCard,
ITzCard,
ITraceableCard,
IRemindableCard,
ILocatedCard
{
/// <inheritdoc />
public string Id { get; init; } = string.Empty;
/// <inheritdoc />
public string Title { get; init; } = string.Empty;
/// <inheritdoc />
public ISource Source { get; init; } = null!;
// ── Модуль «содержимое» ──
/// <inheritdoc />
public string Summary { get; init; } = string.Empty;
// ── Модуль «бюджет» ──
/// <inheritdoc />
public CardBudget? Budget { get; init; }
// ── Модуль «контакты» ──
/// <inheritdoc />
public IReadOnlyList<CardContact> Contacts { get; init; } = Array.Empty<CardContact>();
/// <inheritdoc />
public string ContactText { get; init; } = string.Empty;
// ── Модуль «атрибуты» ──
/// <inheritdoc />
public IReadOnlyList<CardAttribute> Attributes { get; init; } = Array.Empty<CardAttribute>();
// ── Модуль «комментарии» ──
/// <inheritdoc />
public IReadOnlyList<CardComment> Comments { get; init; } = Array.Empty<CardComment>();
// ── Модуль «ссылки» ──
/// <inheritdoc />
public IReadOnlyList<CardLink> Links { get; init; } = Array.Empty<CardLink>();
// ── Модуль «файлы» ──
/// <inheritdoc />
public IReadOnlyList<CardFile> Files { get; init; } = Array.Empty<CardFile>();
// ── Модуль «ТЗ» ──
/// <inheritdoc />
public string TzText { get; init; } = string.Empty;
// ── Модуль «история движения» ──
/// <inheritdoc />
public IReadOnlyList<CardHistoryEntry> History { get; init; } = Array.Empty<CardHistoryEntry>();
// ── Модуль «напоминание» ──
/// <inheritdoc />
public CardReminder? Reminder { get; init; }
// ── Модуль «размещение» ──
/// <inheritdoc />
public string ContainerId { get; init; } = CardIds.Inbox;
/// <inheritdoc />
public string? PrevContainerId { get; init; }
/// <inheritdoc />
public bool IsNew { get; init; } = true;
}
@@ -0,0 +1,27 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Настраиваемый атрибут карточки: имя, значение и (опционально) единица измерения.
/// </summary>
/// <remarks>
/// Стек/грейд/локация/сроки/площадь и прочее — не «зашитые» поля карточки, а атрибуты из справочника
/// тенанта (пользователь настраивает в UI, «стек» — лишь частый атрибут). Один атрибут = одна запись
/// «ключ → значение»; ключом служит строковый id из справочника атрибутов.
/// </remarks>
public sealed record CardAttribute
{
/// <summary>
/// Id атрибута (ключ справочника: <c>stack</c>, <c>grade</c>, <c>location</c>, …).
/// </summary>
public string Key { get; init; } = string.Empty;
/// <summary>
/// Значение атрибута (например, «Python», «Middle», «Москва»).
/// </summary>
public string Value { get; init; } = string.Empty;
/// <summary>
/// Единица измерения (₽/₽/час/м²/…); null — безразмерный.
/// </summary>
public string? Unit { get; init; }
}
@@ -0,0 +1,27 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Бюджет/цена карточки в исходной валюте и (опционально) сконвертированный в целевую.
/// </summary>
public sealed record CardBudget
{
/// <summary>
/// Нижняя граница; null — не указана.
/// </summary>
public double? From { get; init; }
/// <summary>
/// Верхняя граница; null — не указана.
/// </summary>
public double? To { get; init; }
/// <summary>
/// Валюта (код/символ из исходных данных); пусто — бюджет не задан.
/// </summary>
public string Cur { get; init; } = string.Empty;
/// <summary>
/// Пересчитанный в целевую валюту тенанта бюджет; null — конверсия не сделана.
/// </summary>
public CardBudget? Converted { get; init; }
}
@@ -0,0 +1,27 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Комментарий карточки.
/// </summary>
public sealed record CardComment
{
/// <summary>
/// Короткий id комментария.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Автор (имя/«Вы»).
/// </summary>
public string By { get; init; } = string.Empty;
/// <summary>
/// Текст комментария.
/// </summary>
public string Text { get; init; } = string.Empty;
/// <summary>
/// Время создания, epoch-ms.
/// </summary>
public long AtMs { get; init; }
}
@@ -0,0 +1,17 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Контакт заказчика: квалифицированная запись (тип + значение).
/// </summary>
public sealed record CardContact
{
/// <summary>
/// Тип контакта: tg/phone/email/linkedin/whatsapp/site/other.
/// </summary>
public string Type { get; init; } = "other";
/// <summary>
/// Значение: @username, +7…, name@mail, url.
/// </summary>
public string Value { get; init; } = string.Empty;
}
@@ -0,0 +1,32 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Метаданные прикреплённого файла карточки (объект хранится в S3/MinIO или локальной папке).
/// </summary>
public sealed record CardFile
{
/// <summary>
/// Короткий id записи файла.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Имя файла.
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// Размер в байтах.
/// </summary>
public long SizeBytes { get; init; }
/// <summary>
/// Тип контента: image/video/audio/archive/document/other (определяет детектор).
/// </summary>
public string Kind { get; init; } = "other";
/// <summary>
/// Ключ объекта в хранилище.
/// </summary>
public string ObjectKey { get; init; } = string.Empty;
}
@@ -0,0 +1,22 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Запись истории движения карточки.
/// </summary>
public sealed record CardHistoryEntry
{
/// <summary>
/// Короткий id записи.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Время события, epoch-ms.
/// </summary>
public long AtMs { get; init; }
/// <summary>
/// Тип события: <c>created</c>|<c>createdLocal</c>|id контейнера назначения (движение).
/// </summary>
public string Type { get; init; } = string.Empty;
}
@@ -0,0 +1,34 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Реестр коротких id модуля Cards и служебных контейнеров (единый владелец карточки).
/// </summary>
/// <remarks>
/// Единый префикс карточек <c>c_</c> приходит на смену <c>l_</c> (канбан) и <c>pr_</c> (проекты) — при
/// слиянии таблиц у карточки один id, не зависящий от пространства. Служебные контейнеры
/// (<c>inbox</c>/<c>archive</c>/<c>trash</c>) — фиксированные зоны любого пространства-дашборда.
/// Контейнеры-доски (<c>b_…</c>) создаёт пользователь/ИИ; контейнеры-стадии — предзаданный каталог
/// (см. CardsDefaultContainers). Id генерирует модуль (PrefixId), хранилище получает готовые.
/// </remarks>
public static class CardIds
{
/// <summary>
/// Префикс id карточки (единый для всех дашбордов).
/// </summary>
public const string CardPrefix = "c_";
/// <summary>
/// Контейнер «Неразобранное»: новые карточки пайплайна до раскладки по колонкам.
/// </summary>
public const string Inbox = "inbox";
/// <summary>
/// Контейнер «Архив»: карточки старше срока архивации (автоочистка по политике).
/// </summary>
public const string Archive = "archive";
/// <summary>
/// Контейнер «Корзина»: удалённые карточки (автоочистка по политике).
/// </summary>
public const string Trash = "trash";
}
@@ -0,0 +1,22 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Прикреплённая ссылка карточки.
/// </summary>
public sealed record CardLink
{
/// <summary>
/// Короткий id ссылки.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Подпись ссылки (пусто — url как подпись).
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// Адрес ссылки (http/https).
/// </summary>
public string Url { get; init; } = string.Empty;
}
@@ -0,0 +1,13 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Результат перехода карточки единым механизмом <see cref="ICardMover"/>.
/// </summary>
/// <remarks>
/// Error — 400-текст отказа (несуществующий контейнер/исходная колонка, запрет политики); Exists=false —
/// карточки нет (404). При успехе оба поля пусты/true — снимок карточки эндпоинт перечитывает единым
/// чтением (CardDto), чтобы наружу всегда уходила одна форма.
/// </remarks>
/// <param name="Error">Текст 400-ошибки либо null.</param>
/// <param name="Exists">True — карточка найдена и переход выполнен (либо перенос был no-op).</param>
public sealed record CardMoveResultDto(string? Error, bool Exists);
@@ -0,0 +1,17 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Напоминание об «Отложено» (карточка в контейнере hold).
/// </summary>
public sealed record CardReminder
{
/// <summary>
/// Время срабатывания, epoch-ms.
/// </summary>
public long AtMs { get; init; }
/// <summary>
/// Признак «напоминание выстрелило» (повторно не срабатывает до переноса/переустановки).
/// </summary>
public bool Fired { get; init; }
}
@@ -0,0 +1,90 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Каталог контейнеров по умолчанию пространства «Выбранные» (стадии) и служебных зон.
/// </summary>
/// <remarks>
/// Приходит на смену ProjectStages модуля Projects: стадии — это контейнеры kind=stage предзаданного
/// каталога (терминальные — finished/rejected). Хранилище провижинит их тенанту как строки Containers;
/// каталог — единственный источник имён/цветов/порядка (как ProjectStages.All). Служебные зоны
/// (inbox/archive/trash) — фиксированные контейнеры CardIds. Доски (kind=board) каталогу не принадлежат:
/// их создаёт пользователь/ИИ.
/// </remarks>
public static class CardsDefaultContainers
{
/// <summary>
/// Id стадии «Запланировано» (цель перехода «взять в работу»).
/// </summary>
public const string Planned = "planned";
/// <summary>
/// Id стадии «Отклик».
/// </summary>
public const string Reply = "reply";
/// <summary>
/// Id стадии «Согласование».
/// </summary>
public const string Agree = "agree";
/// <summary>
/// Id стадии «В работе».
/// </summary>
public const string Work = "work";
/// <summary>
/// Id стадии «Проверка».
/// </summary>
public const string Review = "review";
/// <summary>
/// Id стадии «Готово».
/// </summary>
public const string Ready = "ready";
/// <summary>
/// Id стадии «Отложено» (напоминания сбрасываются переносом).
/// </summary>
public const string Hold = "hold";
/// <summary>
/// Id терминальной стадии «Выполнено».
/// </summary>
public const string Finished = "finished";
/// <summary>
/// Id терминальной стадии «Отклонено» (очистка вручную, без возврата).
/// </summary>
public const string Rejected = "rejected";
/// <summary>
/// Все контейнеры по умолчанию (стадии пространства «Выбранные»), порядок канбана.
/// </summary>
public static IReadOnlyList<DefaultContainer> All { get; } = new List<DefaultContainer>
{
new(Planned, "Запланировано", "#818cf8", 0, terminal: false, restore: false),
new(Reply, "Отклик", "#38bdf8", 1, terminal: false, restore: false),
new(Agree, "Согласование", "#a78bfa", 2, terminal: false, restore: false),
new(Work, "В работе", "#fbbf24", 3, terminal: false, restore: false),
new(Review, "Проверка", "#f97316", 4, terminal: false, restore: false),
new(Ready, "Готово", "#4ade80", 5, terminal: false, restore: false),
new(Hold, "Отложено", "#94a3b8", 6, terminal: false, restore: false),
new(Finished, "Выполнено", "#2bd576", 7, terminal: true, restore: false),
new(Rejected, "Отклонено", "#ff6b6b", 8, terminal: true, restore: false),
};
/// <summary>
/// Id всех контейнеров-стадий «Выбранных» (для фильтра пространства в выборках хранилища).
/// </summary>
public static IReadOnlyList<string> Ids { get; } = All.Select(container => container.Id).ToArray();
/// <summary>
/// Проверка: контейнер с таким id есть в каталоге (валидация move/ручного создания).
/// </summary>
/// <param name="containerId">Проверяемый id.</param>
/// <returns>True — контейнер известен каталогу.</returns>
public static bool Contains(string containerId)
{
return All.Any(s => s.Id == containerId);
}
}
@@ -0,0 +1,56 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Описание контейнера по умолчанию (запись каталога CardsDefaultContainers).
/// </summary>
public sealed record DefaultContainer
{
/// <summary>
/// Создаёт запись каталога контейнеров по умолчанию.
/// </summary>
/// <param name="id">Короткий id контейнера (стадии).</param>
/// <param name="name">Имя для отображения.</param>
/// <param name="color">Цвет (hex).</param>
/// <param name="order">Позиция в канбане.</param>
/// <param name="terminal">Терминальная зона (только ручная очистка, без возврата).</param>
/// <param name="restore">Возврат из контейнера разрешён.</param>
public DefaultContainer(string id, string name, string color, int order, bool terminal, bool restore)
{
Id = id;
Name = name;
Color = color;
Order = order;
IsTerminal = terminal;
CanRestore = restore;
}
/// <summary>
/// Короткий id контейнера.
/// </summary>
public string Id { get; }
/// <summary>
/// Имя для отображения.
/// </summary>
public string Name { get; }
/// <summary>
/// Цвет (hex).
/// </summary>
public string Color { get; }
/// <summary>
/// Позиция в канбане.
/// </summary>
public int Order { get; }
/// <summary>
/// Терминальная зона.
/// </summary>
public bool IsTerminal { get; }
/// <summary>
/// Возврат разрешён.
/// </summary>
public bool CanRestore { get; }
}
@@ -0,0 +1,33 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник «ИИ»: карточка создана/сгенерирована ИИ (агент, генератор, разборщик).
/// </summary>
/// <remarks>Может быть составной частью <see cref="ICompositeSource"/>: первоисточник данных + ИИ-обработка.</remarks>
public interface IAiSource : ISource
{
/// <summary>
/// Id провайдера ИИ (deepseek/openai/ollama/…).
/// </summary>
public string ProviderId { get; }
/// <summary>
/// Модель провайдера.
/// </summary>
public string Model { get; }
/// <summary>
/// Id ИИ-агента (если источник — агент, ищущий/обрабатывающий данные); null — не агент.
/// </summary>
public string? AgentId { get; }
/// <summary>
/// Ссылка на промпт/шаблон (если применимо); null — нет.
/// </summary>
public string? PromptRef { get; }
/// <summary>
/// API, через который работал агент (для составных сценариев); null — нет.
/// </summary>
public IApiSource? ViaApi { get; }
}
@@ -0,0 +1,17 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник «внешний API»: карточка пришла из интеграции по API.
/// </summary>
public interface IApiSource : ISource
{
/// <summary>
/// Ключ интеграции/провайдера (конфигурация подключения живёт в настройках тенанта).
/// </summary>
public string ProviderId { get; }
/// <summary>
/// Адрес эндпоинта (или иной указатель в рамках провайдера); null — не применимо.
/// </summary>
public string? Endpoint { get; }
}
@@ -0,0 +1,13 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «атрибуты»: настраиваемые пользователем характеристики (стек, грейд, локация, сроки…).
/// </summary>
/// <remarks>Справочник атрибутов живёт в настройках тенанта; карточка хранит только значения по ключам.</remarks>
public interface IAttributedCard
{
/// <summary>
/// Значения атрибутов карточки (пусто — не заполнены).
/// </summary>
public IReadOnlyList<CardAttribute> Attributes { get; }
}
@@ -0,0 +1,12 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «бюджет/цена»: деньги заявки (валюта исходная + сконвертированная).
/// </summary>
public interface IBudgetedCard
{
/// <summary>
/// Бюджет карточки; null — сумма не указана.
/// </summary>
public CardBudget? Budget { get; }
}
@@ -0,0 +1,31 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Ядро карточки: единственное, что есть у любой карточки во всех дашбордах.
/// </summary>
/// <remarks>
/// Никаких «полей заявки» в ядре: стек/бюджет/контакты и прочее — опциональные модули-роли
/// (см. <c>IContentCard</c>, <c>IBudgetedCard</c> и др.), которые реализует агрегат карточки
/// по мере наполнения. <see cref="Source"/> — откуда карточка пришла (см. иерархию <c>ISource</c>);
/// у карточки, созданной пайплайном из сообщения канала, источник составной:
/// <c>ICompositeSource { Origin: ITelegramSource, Pipeline: [IAiSource/IMl] }</c>.
/// Типизированный доступ к конкретному источнику — дискриминация через иерархию ISource
/// (паттерн-матчинг), отдельный generic-интерфейс не нужен.
/// </remarks>
public interface ICard
{
/// <summary>
/// Короткий id карточки (единый префикс карточек, напр. <c>c_</c>).
/// </summary>
public string Id { get; }
/// <summary>
/// Заголовок карточки (очищенный).
/// </summary>
public string Title { get; }
/// <summary>
/// Источник: откуда карточка пришла (полиморфный, не enum).
/// </summary>
public ISource Source { get; }
}
@@ -0,0 +1,25 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Единый механизм перехода карточки между контейнерами.
/// </summary>
/// <remarks>
/// Заменяет маршрутизацию перехода по эндпоинтам: одна точка знает и контейнеры-стадии пространства
/// «Выбранные», и контейнеры дашборда (доски/служебные зоны). Валидацию «откуда → куда можно» выполняют
/// политики контейнеров и «ворота» между пространствами (например, дашборд → «Выбранные» — только через
/// действие «взять в работу»; «Выбранные» → дашборд — запрещено). Побочные эффекты (запись истории для
/// стадий, сброс напоминания, журнал/обучение ML для дашборда) выполняет реализация перехода.
/// </remarks>
public interface ICardMover
{
/// <summary>
/// Перемещает карточку в контейнер назначения с побочными эффектами перехода.
/// </summary>
/// <param name="cardId">Id карточки.</param>
/// <param name="toContainerId">Id контейнера назначения (стадия «Выбранных» либо дашборд-контейнер).</param>
/// <param name="ctx">Контекст перехода (инициатор, причина, обучение).</param>
/// <param name="ct">Токен отмены.</param>
/// <returns>Результат: Error (400-текст отказа) | Exists=false (карточки нет, 404) | успех (Exists=true).</returns>
public Task<CardMoveResultDto> MoveAsync(
string cardId, string toContainerId, TransitionContext ctx, CancellationToken ct);
}
@@ -0,0 +1,12 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «комментарии» карточки.
/// </summary>
public interface ICommentableCard
{
/// <summary>
/// Комментарии карточки (пусто — комментариев нет).
/// </summary>
public IReadOnlyList<CardComment> Comments { get; }
}
@@ -0,0 +1,22 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Составной источник: цепочка «где взято → как обработано/доставлено».
/// </summary>
/// <remarks>
/// У карточки из пайплайна источник составной: <see cref="Origin"/> — первоисточник контента
/// (например, <c>ITelegramSource</c>), <see cref="Pipeline"/> — кто/что его обрабатывал
/// (например, ИИ-классификатор). Позволяет строить «третьи» источники без правки ядра.
/// </remarks>
public interface ICompositeSource : ISource
{
/// <summary>
/// Первоисточник контента.
/// </summary>
public ISource Origin { get; }
/// <summary>
/// Цепочка обработки (пусто — обработки не было).
/// </summary>
public IReadOnlyList<ISource> Pipeline { get; }
}
@@ -0,0 +1,17 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «контакты заказчика».
/// </summary>
public interface IContactCard
{
/// <summary>
/// Квалифицированные контакты (пусто — контактов нет).
/// </summary>
public IReadOnlyList<CardContact> Contacts { get; }
/// <summary>
/// Контактная строка «как в исходных данных» (fallback, если Contacts пуст).
/// </summary>
public string ContactText { get; }
}
@@ -0,0 +1,44 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Контейнер карточки: колонка дашборда, стадия «Выбранных» или служебная зона.
/// </summary>
/// <remarks>
/// Общая база колонок/стадий/зон: пользовательские колонки (kind=board, создаёт пользователь/ИИ),
/// стадии (kind=stage, предзаданный каталог), «Неразобранное», архив, корзина, терминальные зоны.
/// Поведение контейнера — через <see cref="Policy"/> (роль, не enum-свойства): правила попадания,
/// допустимость возврата, автоочистка, терминальность. Пространство (дашборд/«Выбранные»/третий вид) —
/// свойство SpaceId: карточке безразличен вид, у неё только ContainerId.
/// </remarks>
public interface IContainer
{
/// <summary>
/// Короткий id контейнера (доски <c>b_…</c>, стадии <c>planned…</c>, служебные inbox/archive/trash).
/// </summary>
public string Id { get; }
/// <summary>
/// Имя для отображения («WPF», «В работе», «Архив»).
/// </summary>
public string Name { get; }
/// <summary>
/// Цвет контейнера (hex).
/// </summary>
public string Color { get; }
/// <summary>
/// Позиция в пространстве (порядок показа слева направо).
/// </summary>
public int Order { get; }
/// <summary>
/// Id пространства (доски), которому принадлежит контейнер.
/// </summary>
public string SpaceId { get; }
/// <summary>
/// Политика контейнера: что можно/нельзя и что происходит (роль).
/// </summary>
public IContainerPolicy Policy { get; }
}
@@ -0,0 +1,33 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Политика контейнера: правила размещения карточек и жизненного цикла зоны.
/// </summary>
/// <remarks>
/// Роль контейнера (не enum): пользовательская колонка-фильтр, стадия, «Неразобранное», архив/корзина,
/// терминальная зона — каждая реализует политику по-своему. Политика отвечает на два вопроса:
/// (1) можно ли карточке попасть сюда (правила фильтрации) и (2) что происходит с карточкой здесь
/// (возврат, автоочистка, терминальность, напоминания).
/// </remarks>
public interface IContainerPolicy
{
/// <summary>
/// Правила попадания карточки в контейнер (null — фильтра нет, карточки кладутся вручную/ИИ).
/// </summary>
public IContainerRules? Rules { get; }
/// <summary>
/// Можно ли вернуть карточку из контейнера на доску пространства (архив/корзина — да; терминальные — нет).
/// </summary>
public bool CanRestore { get; }
/// <summary>
/// Терминальная зона: карточка завершила жизненный путь, ручная очистка без возврата.
/// </summary>
public bool IsTerminal { get; }
/// <summary>
/// Автоочистка контейнера: срок хранения карточек (null — автоочистки нет).
/// </summary>
public TimeSpan? Retention { get; }
}
@@ -0,0 +1,68 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Правила попадания карточки в контейнер (набор опциональных фильтров).
/// </summary>
/// <remarks>
/// Соответствует «правилам колонки» (§6.3): направление/стек/ключевые слова/грейд/уровень/цена/бюджет/
/// локация/тип и отрицательные исключения. Все группы опциональны; режим All/Any решает, как группы
/// сочетаются. Пустые правила — контейнер без фильтра (карточки кладутся вручную или ИИ/ML по контексту).
/// Проверка — по содержимому карточки (атрибуты, бюджет, текст), чистая функция.
/// </remarks>
public interface IContainerRules
{
/// <summary>
/// Режим сочетания групп: <c>all</c> — должны совпасть все включённые группы; <c>any</c> — хотя бы одна.
/// </summary>
public string Mode { get; }
/// <summary>
/// Ключевые слова/фразы (пусто — группа не участвует).
/// </summary>
public IReadOnlyList<string> Keywords { get; }
/// <summary>
/// Ключевые технологии/стек-атрибуты (пусто — не участвует).
/// </summary>
public IReadOnlyList<string> Stack { get; }
/// <summary>
/// Направление/тема (пусто — не участвует).
/// </summary>
public IReadOnlyList<string> Directions { get; }
/// <summary>
/// Грейд/уровень (пусто — не участвует).
/// </summary>
public IReadOnlyList<string> Grades { get; }
/// <summary>
/// Уровень (отдельная группа §6.3; пусто — не участвует).
/// </summary>
public IReadOnlyList<string> Levels { get; }
/// <summary>
/// Локация (пусто — не участвует).
/// </summary>
public IReadOnlyList<string> Locations { get; }
/// <summary>
/// Тип заявки: вакансия/фриланс/объявление (пусто — не участвует).
/// </summary>
public IReadOnlyList<string> Types { get; }
/// <summary>
/// Бюджетный диапазон (null — не участвует).
/// </summary>
public CardBudget? Budget { get; }
/// <summary>
/// Диапазон цены (отдельная группа §6.3; null — не участвует).
/// </summary>
public CardBudget? Prices { get; }
/// <summary>
/// Исключения: карточка не попадает, если в тексте/атрибутах есть хотя бы одно (veto).
/// </summary>
public IReadOnlyList<string> Exclude { get; }
}
@@ -0,0 +1,16 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «содержимое»: структурированный блок «О заявке» карточки.
/// </summary>
/// <remarks>
/// Единая структура у всех карточек (Компания → Формат → О задаче → Требования → Будет плюсом → Условия);
/// у карточки без разобранного содержимого модуль пуст (Summary = "").
/// </remarks>
public interface IContentCard
{
/// <summary>
/// Блок «О заявке» (строки с метками, ≤2000).
/// </summary>
public string Summary { get; }
}
@@ -0,0 +1,12 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «файлы» карточки (медиа/документы; объекты — в S3/MinIO).
/// </summary>
public interface IFileCard
{
/// <summary>
/// Метаданные файлов карточки (пусто — файлов нет).
/// </summary>
public IReadOnlyList<CardFile> Files { get; }
}
@@ -0,0 +1,22 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник «файл» (карточка создана из загруженного/импортированного файла).
/// </summary>
public interface IFileSource : ISource
{
/// <summary>
/// Ключ объекта в хранилище (MinIO/локальная папка).
/// </summary>
public string ObjectKey { get; }
/// <summary>
/// Имя файла.
/// </summary>
public string FileName { get; }
/// <summary>
/// Размер файла в байтах.
/// </summary>
public long SizeBytes { get; }
}
@@ -0,0 +1,12 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «ссылки» карточки.
/// </summary>
public interface ILinkCard
{
/// <summary>
/// Прикреплённые ссылки (пусто — ссылок нет).
/// </summary>
public IReadOnlyList<CardLink> Links { get; }
}
@@ -0,0 +1,13 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник «создано вручную/локально» (карточка без внешнего первоисточника).
/// </summary>
/// <remarks>Покрывает ручное создание карточки пользователем (в UI «Выбранных» и будущих дашбордов).</remarks>
public interface ILocalSource : ISource
{
/// <summary>
/// Id автора (оператора тенанта), создавшего карточку; null — неизвестен.
/// </summary>
public string? AuthorId { get; }
}
@@ -0,0 +1,27 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «размещение»: контейнер карточки и флаги состояния на доске.
/// </summary>
/// <remarks>
/// Контейнер (колонка/стадия/зона) — единственное «место» карточки; смена контейнера = переход карточки
/// (см. <see cref="ICardMover"/>). «Архив/корзина/отклонено/выполнено» — такие же контейнеры со своими
/// политиками, а не отдельные сущности. PrevContainerId — для возврата из архива/корзины.
/// </remarks>
public interface ILocatedCard
{
/// <summary>
/// Id контейнера, в котором лежит карточка.
/// </summary>
public string ContainerId { get; }
/// <summary>
/// Предыдущий контейнер (для возврата); null — возврат неприменим.
/// </summary>
public string? PrevContainerId { get; }
/// <summary>
/// Признак «новое» (подсветка на доске; снимается просмотром/переходом).
/// </summary>
public bool IsNew { get; }
}
@@ -0,0 +1,12 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «напоминание»: отложенная карточка (контейнер hold).
/// </summary>
public interface IRemindableCard
{
/// <summary>
/// Напоминание об «Отложено»; null — напоминания нет.
/// </summary>
public CardReminder? Reminder { get; }
}
@@ -0,0 +1,22 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник «импорт данных»: карточка создана из строки/колонки таблицы (csv/sheet/БД).
/// </summary>
public interface IRowSource : ISource
{
/// <summary>
/// Id таблицы/импорта (внешний идентификатор источника данных).
/// </summary>
public string TableId { get; }
/// <summary>
/// Id строки в таблице (первичный ключ строки-источника).
/// </summary>
public string RowId { get; }
/// <summary>
/// Id колонки, из которой взят заголовок/содержимое; null — колонка не применима.
/// </summary>
public string? ColumnId { get; }
}
@@ -0,0 +1,33 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник карточки: всё, откуда карточка может прийти.
/// </summary>
/// <remarks>
/// Полиморфная иерархия вместо enum-свойства «тип источника»: конкретные варианты несут свои поля
/// (Telegram — диалог/сообщение/тема, Web — url, AI — провайдер/модель и т.д.), а общий контракт —
/// только подпись, ссылку на оригинал и сырое содержимое. Составной источник (<see cref="ICompositeSource"/>)
/// описывает цепочку «первоисточник → обработка» (например, сообщение Telegram, разобранное ИИ).
/// </remarks>
public interface ISource
{
/// <summary>
/// Подпись источника в UI: «@freelance», «hh.ru», «файл leads.csv»…
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Ссылка на оригинал: t.me/…, https://…, objectKey; null — оригинала нет (локальное создание).
/// </summary>
public string? OriginRef { get; }
/// <summary>
/// Сырое содержимое (текст/JSON), если хранится; null — не хранится.
/// </summary>
public string? RawPayload { get; }
/// <summary>
/// Время получения/создания исходных данных.
/// </summary>
public DateTimeOffset ReceivedAt { get; }
}
@@ -0,0 +1,37 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник «Telegram»: канал/группа/чат/форум, конкретное сообщение.
/// </summary>
/// <remarks>
/// Покрывает все варианты источника Telegram: канал (DialogId=peer, TopicId=null), группа с темами
/// (TopicId задан), личный чат. Ссылка на оригинал строится из PeerHandle/PeerId и MessageId
/// (см. контракт tgSourceUrl текущего фронта).
/// </remarks>
public interface ITelegramSource : ISource
{
/// <summary>
/// Id диалога (peer) в Telegram.
/// </summary>
public string DialogId { get; }
/// <summary>
/// Id сообщения в диалоге.
/// </summary>
public long MessageId { get; }
/// <summary>
/// Handle канала (без «@»); null — диалог без username (приватный).
/// </summary>
public string? PeerHandle { get; }
/// <summary>
/// Имя канала/группы/чата для отображения.
/// </summary>
public string PeerName { get; }
/// <summary>
/// Id темы форума (для групп с темами); null — темы нет.
/// </summary>
public string? TopicId { get; }
}
@@ -0,0 +1,12 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «история движения» карточки (создание и смены контейнера).
/// </summary>
public interface ITraceableCard
{
/// <summary>
/// История движения, свежие записи — в конце списка (пусто — истории нет).
/// </summary>
public IReadOnlyList<CardHistoryEntry> History { get; }
}
@@ -0,0 +1,12 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Модуль «ТЗ»: текст технического задания/заметки-задания по карточке.
/// </summary>
public interface ITzCard
{
/// <summary>
/// Текст ТЗ (пусто — ТЗ нет).
/// </summary>
public string TzText { get; }
}
@@ -0,0 +1,17 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Источник «ссылка на сайт/объявление» (карточка создана из веб-страницы).
/// </summary>
public interface IWebSource : ISource
{
/// <summary>
/// Адрес страницы/объявления.
/// </summary>
public string Url { get; }
/// <summary>
/// Имя сайта (домен/бренд); null — не определено.
/// </summary>
public string? SiteName { get; }
}
@@ -0,0 +1,25 @@
namespace Deal.Modules.Cards.Application;
/// <summary>
/// Контекст перехода карточки: кто инициировал и что делать с обучением.
/// </summary>
/// <remarks>Переход — единый механизм (<see cref="ICardMover"/>); контекст отличает действие пользователя
/// (обучать ML), системы (автоархив/очистка) и ИИ/ML (классификация). Reason — для возвратов из отсева
/// и ручных исключений, чтобы ML/ИИ учились на решении.</remarks>
public sealed record TransitionContext
{
/// <summary>
/// Инициатор перехода: <c>user</c>|<c>system</c>|<c>ai</c>|<c>ml</c>.
/// </summary>
public required string Actor { get; init; }
/// <summary>
/// Причина (возврат из отсева, ручная отмена); null — нет.
/// </summary>
public string? Reason { get; init; }
/// <summary>
/// Обучать ML на этом действии (действия пользователя — да; системные — нет).
/// </summary>
public bool Learn { get; init; }
}