From e1aebd1d78d3593e77f9d411060d53c3c812cd89 Mon Sep 17 00:00:00 2001 From: Rustam Khalimov Date: Fri, 11 Sep 2026 14:23:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B2=D0=B5=D1=81=D1=82=D0=B8=20=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=20Cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены SourceItem/SourceRef/SourceContent/DataRef/ContactRef (строго типизированный контракт без подтипов вложений; файлы — ссылки на общий сервис данных). Удалены Telegram-ориентированные source-интерфейсы из Cards; Card/ICard переведены на SourceRef. --- .../2026-09-11-source-contract-design.md | 129 ++++++++++++++++++ .../Application/Abstractions/IAiSource.cs | 32 ----- .../Application/Abstractions/IApiSource.cs | 14 -- .../Application/Abstractions/ICard.cs | 4 +- .../Abstractions/ICompositeSource.cs | 17 --- .../Application/Abstractions/IFileSource.cs | 19 --- .../Application/Abstractions/ILocalSource.cs | 12 -- .../Application/Abstractions/IRowSource.cs | 22 --- .../Application/Abstractions/ISource.cs | 27 ---- .../Abstractions/ITelegramSource.cs | 32 ----- .../Application/Abstractions/IWebSource.cs | 17 --- .../Application/Models/Card.cs | 3 +- .../Application/Sources/ContactRef.cs | 20 +++ .../Application/Sources/DataRef.cs | 48 +++++++ .../Application/Sources/SourceContent.cs | 35 +++++ .../Application/Sources/SourceItem.cs | 11 ++ .../Application/Sources/SourceRef.cs | 47 +++++++ .../Modules/Cards/CardsDomainTests.cs | 20 ++- .../Modules/Cards/LocalSourceStub.cs | 16 --- .../Modules/Cards/TelegramSourceStub.cs | 27 ---- 20 files changed, 308 insertions(+), 244 deletions(-) create mode 100644 docs/superpowers/specs/2026-09-11-source-contract-design.md delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/IAiSource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/IApiSource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/ICompositeSource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/IFileSource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/ILocalSource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/IRowSource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/ISource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/ITelegramSource.cs delete mode 100644 src/core/Deal.Modules.Cards/Application/Abstractions/IWebSource.cs create mode 100644 src/core/Deal.Modules.Cards/Application/Sources/ContactRef.cs create mode 100644 src/core/Deal.Modules.Cards/Application/Sources/DataRef.cs create mode 100644 src/core/Deal.Modules.Cards/Application/Sources/SourceContent.cs create mode 100644 src/core/Deal.Modules.Cards/Application/Sources/SourceItem.cs create mode 100644 src/core/Deal.Modules.Cards/Application/Sources/SourceRef.cs delete mode 100644 src/core/tests/Deal.Tests.Unit/Modules/Cards/LocalSourceStub.cs delete mode 100644 src/core/tests/Deal.Tests.Unit/Modules/Cards/TelegramSourceStub.cs diff --git a/docs/superpowers/specs/2026-09-11-source-contract-design.md b/docs/superpowers/specs/2026-09-11-source-contract-design.md new file mode 100644 index 0000000..3b27481 --- /dev/null +++ b/docs/superpowers/specs/2026-09-11-source-contract-design.md @@ -0,0 +1,129 @@ +# Дизайн: единый контракт источника + общий Storage-сервис данных + +Дата: 2026-09-11. Статус: на согласовании (контракт не плодит типы вложений; файлы — в общем Storage). + +## 1. Принцип + +1. **Единый строго типизированный контракт.** Любой источник (Telegram, WhatsApp, Avito, сайт, файл, + Excel) через адаптер приводит данные к одному типу `SourceItem`. Ядро, AI и ML работают только с ним. +2. **Данные файлов — в общем Storage-сервисе.** Каждый сервис-источник сам выгружает свои данные + (картинки, видео, аудио, документы, любые файлы) в общий Storage с **токеном валидации**. Storage сам + определяет тип и метаданные. В контракте хранится **ссылка** на файл, а не сам файл. +3. **Никаких подтипов вложений в контракте.** Не плодим `ImagePart/VideoPart/...`; есть универсальный + `DataRef` с полем `Kind`, которое заполняет Storage. +4. Ссылки, контакты и прочее, что **не является файлом**, идут отдельными полями контента. +5. В ядре нет Telegram-полей и слова Telegram (только в telegram-сервисе); в комментариях нет упоминаний + задач/этапов/ТЗ. + +## 2. Единый контракт (Deal.Modules.Cards) + +```csharp +public sealed record SourceItem +{ + public required SourceRef Source { get; init; } + public required SourceContent Content { get; init; } +} + +public sealed record SourceRef +{ + public required string Kind { get; init; } // "telegram", "whatsapp", "avito", "file", "excel", ... + public string? ExternalId { get; init; } // id в источнике (сообщение/строка/файл) + public string? DisplayName { get; init; } // подпись в UI + public string? OriginRef { get; init; } // url / deep-link / путь + public string? Author { get; init; } + public DateTimeOffset ReceivedAt { get; init; } + public IReadOnlyDictionary? Extra { get; init; } +} + +public sealed record SourceContent +{ + public string? Text { get; init; } // основной текст + public string? Html { get; init; } // разметка (если есть) + public string? Author { get; init; } // отправитель + public string? Subject { get; init; } // тема/заголовок + public IReadOnlyList Data { get; init; } = []; // ссылки на файлы в Storage + public IReadOnlyList? Links { get; init; } // ссылки (не файлы) + public IReadOnlyList? Contacts { get; init; } // контакты + public IReadOnlyDictionary? Extra { get; init; } // прочее (не файл/не ссылка/не контакт) +} +``` + +`DataRef` — ссылка на объект в Storage; тип и метаданные определил Storage (nullable, чтобы не плодить типы): + +```csharp +public sealed record DataRef +{ + public required string Id { get; init; } // идентификатор объекта в Storage + public required string Ref { get; init; } // ссылка (url/путь) для скачивания/отображения + public string? Kind { get; init; } // определил Storage: image/video/audio/document/archive/other + public string? MimeType { get; init; } + public string? FileName { get; init; } + public long? Size { get; init; } + public int? Width { get; init; } + public int? Height { get; init; } + public double? DurationSec { get; init; } + public string? PreviewRef { get; init; } // превью/thumbnail + public string? Caption { get; init; } + public int? Order { get; init; } + public IReadOnlyDictionary? Meta { get; init; } // прочие метаданные от Storage +} +``` + +`ContactRef`: `Name?`, `Phone?`, `Email?`, `Url?`, `Kind?` (контакт может быть квалифицирован). + +## 3. Storage-сервис (общий) + +Отдельный сервис (как ai/ml/telegram), владелец — данные. Источники и ядро только ссылаются на объекты. + +- **Загрузка:** `Upload(stream, token, fileName?) → DataRef`. Каждый сервис-источник выгружает свои данные + **сам**, передавая **токен валидации** (сервисный токен/mTLS — уже есть в gRPC-обвязке). +- **Определение типа:** Storage сам решает `Kind`/`MimeType`/размеры/длительность (контент-снифинг); + контракт типы не задаёт. +- **Чтение:** `Get(id) → (stream, DataRef)` либо выдача ссылки/временного URL. +- **Бэкенд:** объектное хранилище (MinIO/S3). Путь/бакет — по тенанту. +- **Владение:** единый общий сервис; каждый источник пишет в него со своим токеном, ядро/AI/ML читают по ссылке. + +## 4. Адаптеры источников + +```csharp +public interface ISourceAdapter { string Kind { get; } SourceItem Normalize(object native); } +``` + +Владельцы: `telegram` → telegram-сервис; `local` → ручное создание (Cards); `whatsapp`/`avito`/`web`/`file`/ +`excel` → соответствующий сервис. Файлы адаптер сам выгружает в Storage и кладёт в контракт `DataRef`. + +## 5. Загрузка исходника карточки + +Единый способ: по `SourceRef.Kind` — провайдер, возвращающий `SourceContent` (для файла — через Storage по +`DataRef.Ref`, для сообщения — у источника). `ISourceContentProvider { Kind; LoadAsync(SourceRef) }` + реестр. +API ядра: `GET /api/cards/{id}/source` → generic контент. + +## 6. Персистентность + +В карточках вместо плоских Telegram-колонок: + +- `SourceKind` (text); `SourceJson` (jsonb, `SourceRef`); +- `ContentJson` (jsonb, `SourceContent` — текст + `DataRef`-ссылки + прочее); +- `SourceText` (text, FTS); +- `SourceRefUrl` (text?, `OriginRef`). + +Конвертер контента общий (без per-source сериализаторов). Миграции: старые удаляем → новый init с нуля. + +## 7. Wire и фронт + +- `CardDto.Source` = `{ kind, displayName?, originRef?, receivedAt }`. +- `GET /api/cards/{id}/source` → `{ text?, html?, author?, subject?, data[], links[], contacts[], extra? }`. +- Фронт: generic блок источника + универсальный просмотрщик (по `DataRef.Kind` — картинка/видео/аудио/файл; + ссылки/контакты — списками). + +## 8. Этапы + +1. Домен: `SourceItem/SourceRef/SourceContent/DataRef/ContactRef`; удалить Telegram-маркеры из Cards. +2. Storage-сервис: контракт gRPC, определение типа, токен валидации, бэкенд MinIO; регистрация. +3. Персистентность: `SourceKind/SourceJson/ContentJson/SourceText/SourceRefUrl`, общий конвертер, новый init, + маппинг KanbanStore. +4. Pipeline: приём `SourceItem`, загрузка вложений в Storage адаптером, без Telegram-полей. +5. Wire/API: generic `Source` в `CardDto`, `GET /api/cards/{id}/source`, провайдеры. +6. Frontend: generic источник + универсальный просмотрщик. +7. Telegram: адаптер + провайдер исходника (только в telegram-сервисе) + выгрузка в Storage. +8. Комментарии: убрать упоминания Telegram из ядра и задачи/этапы — везде. diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/IAiSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/IAiSource.cs deleted file mode 100644 index f611765..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/IAiSource.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник «ИИ»: карточка создана/сгенерирована ИИ -/// -public interface IAiSource : ISource -{ - /// - /// Id провайдера ИИ - /// - public string ProviderId { get; } - - /// - /// Модель провайдера. - /// - public string Model { get; } - - /// - /// Id ИИ-агента - /// - public string? AgentId { get; } - - /// - /// Ссылка на промпт/шаблон - /// - public string? PromptRef { get; } - - /// - /// API, через который работал агент - /// - public IApiSource? ViaApi { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/IApiSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/IApiSource.cs deleted file mode 100644 index 98f199b..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/IApiSource.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник «внешний API» -/// -public interface IApiSource : ISource -{ - public string ProviderId { get; } - - /// - /// Адрес эндпоинта - /// - public string? Endpoint { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ICard.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ICard.cs index 5b6d219..d9b4179 100644 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/ICard.cs +++ b/src/core/Deal.Modules.Cards/Application/Abstractions/ICard.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Cards.Application.Sources; + namespace Deal.Modules.Cards.Application.Abstractions; /// @@ -18,5 +20,5 @@ public interface ICard /// /// Источник: откуда карточка пришла /// - public ISource Source { get; } + public SourceRef Source { get; } } diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ICompositeSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ICompositeSource.cs deleted file mode 100644 index 7ad7333..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/ICompositeSource.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Составной источник -/// -public interface ICompositeSource : ISource -{ - /// - /// Первоисточник контента. - /// - public ISource Origin { get; } - - /// - /// Цепочка обработки - /// - public IReadOnlyList Pipeline { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/IFileSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/IFileSource.cs deleted file mode 100644 index 0d0564e..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/IFileSource.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник «файл» -/// -public interface IFileSource : ISource -{ - public string ObjectKey { get; } - - /// - /// Имя файла. - /// - public string FileName { get; } - - /// - /// Размер файла в байтах. - /// - public long SizeBytes { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ILocalSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ILocalSource.cs deleted file mode 100644 index 3afa2f5..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/ILocalSource.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник «создано вручную/локально» -/// -public interface ILocalSource : ISource -{ - /// - /// Id автора (оператора тенанта), создавшего карточку; null — неизвестен. - /// - public string? AuthorId { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/IRowSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/IRowSource.cs deleted file mode 100644 index 3ab11b2..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/IRowSource.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник «импорт данных» -/// -public interface IRowSource : ISource -{ - /// - /// Id таблицы/импорта - /// - public string TableId { get; } - - /// - /// Id строки в таблице - /// - public string RowId { get; } - - /// - /// Id колонки, из которой взят заголовок/содержимое; null — колонка не применима. - /// - public string? ColumnId { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ISource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ISource.cs deleted file mode 100644 index f9b83c4..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/ISource.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник карточки -/// -public interface ISource -{ - /// - /// Подпись источника в UI - /// - public string DisplayName { get; } - - /// - /// Ссылка на оригинал - /// - public string? OriginRef { get; } - - /// - /// Сырое содержимое - /// - public string? RawPayload { get; } - - /// - /// Время получения/создания исходных данных. - /// - public DateTimeOffset ReceivedAt { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ITelegramSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ITelegramSource.cs deleted file mode 100644 index 70fd318..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/ITelegramSource.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник «Telegram» -/// -public interface ITelegramSource : ISource -{ - /// - /// Id диалога (peer) в Telegram. - /// - public string DialogId { get; } - - /// - /// Id сообщения в диалоге. - /// - public long MessageId { get; } - - /// - /// Handle канала - /// - public string? PeerHandle { get; } - - /// - /// Имя канала/группы/чата для отображения. - /// - public string PeerName { get; } - - /// - /// Id темы форума - /// - public string? TopicId { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/IWebSource.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/IWebSource.cs deleted file mode 100644 index 98f699a..0000000 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/IWebSource.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Deal.Modules.Cards.Application.Abstractions; - -/// -/// Источник «ссылка на сайт/объявление» -/// -public interface IWebSource : ISource -{ - /// - /// Адрес страницы/объявления. - /// - public string Url { get; } - - /// - /// Имя сайта (домен/бренд); null — не определено. - /// - public string? SiteName { get; } -} diff --git a/src/core/Deal.Modules.Cards/Application/Models/Card.cs b/src/core/Deal.Modules.Cards/Application/Models/Card.cs index 6ed171e..45a39f5 100644 --- a/src/core/Deal.Modules.Cards/Application/Models/Card.cs +++ b/src/core/Deal.Modules.Cards/Application/Models/Card.cs @@ -1,4 +1,5 @@ using Deal.Modules.Cards.Application.Abstractions; +using Deal.Modules.Cards.Application.Sources; namespace Deal.Modules.Cards.Application.Models; @@ -26,7 +27,7 @@ public sealed class Card : public string Title { get; init; } = string.Empty; /// - public ISource Source { get; init; } = null!; + public SourceRef Source { get; init; } = new() { Kind = SourceKinds.Local }; // ── Модуль «содержимое» ── diff --git a/src/core/Deal.Modules.Cards/Application/Sources/ContactRef.cs b/src/core/Deal.Modules.Cards/Application/Sources/ContactRef.cs new file mode 100644 index 0000000..174ebd1 --- /dev/null +++ b/src/core/Deal.Modules.Cards/Application/Sources/ContactRef.cs @@ -0,0 +1,20 @@ +namespace Deal.Modules.Cards.Application.Sources; + +/// +/// Контакт, извлечённый из источника или указанный вручную. +/// +public sealed record ContactRef +{ + /// + /// Вид контакта (например, телефон/почта/мессенджер/сайт). + /// + public string? Kind { get; init; } + + public string? Name { get; init; } + + public string? Phone { get; init; } + + public string? Email { get; init; } + + public string? Url { get; init; } +} diff --git a/src/core/Deal.Modules.Cards/Application/Sources/DataRef.cs b/src/core/Deal.Modules.Cards/Application/Sources/DataRef.cs new file mode 100644 index 0000000..08cdc28 --- /dev/null +++ b/src/core/Deal.Modules.Cards/Application/Sources/DataRef.cs @@ -0,0 +1,48 @@ +namespace Deal.Modules.Cards.Application.Sources; + +/// +/// Ссылка на файл в общем сервисе данных. +/// +public sealed record DataRef +{ + /// + /// Идентификатор объекта в сервисе данных. + /// + public required string Id { get; init; } + + /// + /// Ссылка на скачивание/отображение. + /// + public required string Ref { get; init; } + + /// + /// Тип, определённый сервисом данных (image/video/audio/document/archive/other). + /// + public string? Kind { get; init; } + + public string? MimeType { get; init; } + + public string? FileName { get; init; } + + public long? Size { get; init; } + + public int? Width { get; init; } + + public int? Height { get; init; } + + public double? DurationSec { get; init; } + + /// + /// Ссылка на превью (если сервис данных сгенерировал). + /// + public string? PreviewRef { get; init; } + + public string? Caption { get; init; } + + public int? Order { get; init; } + + /// + /// Прочие метаданные от сервиса данных. + /// + public IReadOnlyDictionary? Meta { get; init; } +} diff --git a/src/core/Deal.Modules.Cards/Application/Sources/SourceContent.cs b/src/core/Deal.Modules.Cards/Application/Sources/SourceContent.cs new file mode 100644 index 0000000..f1a1485 --- /dev/null +++ b/src/core/Deal.Modules.Cards/Application/Sources/SourceContent.cs @@ -0,0 +1,35 @@ +namespace Deal.Modules.Cards.Application.Sources; + +/// +/// Содержимое первичного сообщения источника. +/// +public sealed record SourceContent +{ + public string? Text { get; init; } + + /// + /// Форматированный текст (если источник его предоставил). + /// + public string? Html { get; init; } + + public string? Author { get; init; } + + public string? Subject { get; init; } + + /// + /// Вложения: ссылки на файлы в общем сервисе данных. + /// + public IReadOnlyList Data { get; init; } = []; + + /// + /// Ссылки, не являющиеся файлами. + /// + public IReadOnlyList? Links { get; init; } + + public IReadOnlyList? Contacts { get; init; } + + /// + /// Прочие поля, не подходящие под текст/файл/ссылку/контакт. + /// + public IReadOnlyDictionary? Extra { get; init; } +} diff --git a/src/core/Deal.Modules.Cards/Application/Sources/SourceItem.cs b/src/core/Deal.Modules.Cards/Application/Sources/SourceItem.cs new file mode 100644 index 0000000..ddddd39 --- /dev/null +++ b/src/core/Deal.Modules.Cards/Application/Sources/SourceItem.cs @@ -0,0 +1,11 @@ +namespace Deal.Modules.Cards.Application.Sources; + +/// +/// Единый контракт входящей записи: источник + содержимое. +/// +public sealed record SourceItem +{ + public required SourceRef Source { get; init; } + + public required SourceContent Content { get; init; } +} diff --git a/src/core/Deal.Modules.Cards/Application/Sources/SourceRef.cs b/src/core/Deal.Modules.Cards/Application/Sources/SourceRef.cs new file mode 100644 index 0000000..a8f1c1e --- /dev/null +++ b/src/core/Deal.Modules.Cards/Application/Sources/SourceRef.cs @@ -0,0 +1,47 @@ +namespace Deal.Modules.Cards.Application.Sources; + +/// +/// Источник данных: откуда пришла запись. +/// +public sealed record SourceRef +{ + /// + /// Дискриминатор источника (значение задаёт сервис-владелец). + /// + public required string Kind { get; init; } + + /// + /// Идентификатор записи в источнике (сообщение/строка/файл). + /// + public string? ExternalId { get; init; } + + /// + /// Подпись источника для интерфейса. + /// + public string? DisplayName { get; init; } + + /// + /// Ссылка на оригинал (url, deep-link, путь). + /// + public string? OriginRef { get; init; } + + public string? Author { get; init; } + + public DateTimeOffset ReceivedAt { get; init; } + + /// + /// Прочие метаданные источника. + /// + public IReadOnlyDictionary? Extra { get; init; } +} + +/// +/// Дискриминаторы источников, владельцем которых является модуль. +/// +public static class SourceKinds +{ + /// + /// Запись создана вручную в системе. + /// + public const string Local = "local"; +} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Cards/CardsDomainTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/CardsDomainTests.cs index 31615c4..0189473 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Cards/CardsDomainTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Cards/CardsDomainTests.cs @@ -1,6 +1,7 @@ using Deal.Modules.Cards; using Deal.Modules.Cards.Application.Abstractions; using Deal.Modules.Cards.Application.Models; +using Deal.Modules.Cards.Application.Sources; namespace Deal.Tests.Unit.Modules.Cards; @@ -65,7 +66,7 @@ public sealed class CardsDomainTests [Fact] public void Card_ImplementsCoreAndModuleRoles() { - Card card = new() { Id = "c_1", Title = "Заголовок", Source = new LocalSourceStub("u_1") }; + Card card = new() { Id = "c_1", Title = "Заголовок", Source = new SourceRef { Kind = SourceKinds.Local } }; Assert.IsAssignableFrom(card); Assert.IsAssignableFrom(card); @@ -84,7 +85,7 @@ public sealed class CardsDomainTests [Fact] public void Card_NewCard_DefaultsToInboxAndEmptyModules() { - Card card = new() { Id = "c_2", Title = "Новая", Source = new LocalSourceStub("u_1") }; + Card card = new() { Id = "c_2", Title = "Новая", Source = new SourceRef { Kind = SourceKinds.Local } }; Assert.Equal(CardIds.Inbox, card.ContainerId); Assert.True(card.IsNew); @@ -101,12 +102,17 @@ public sealed class CardsDomainTests } [Fact] - public void Card_Source_IsPolymorphic() + public void Card_Source_IsStored() { - Card card = new() { Id = "c_3", Title = "Из TG", Source = new TelegramSourceStub("100", 5, "@chan", "Канал", null) }; + Card card = new() + { + Id = "c_3", + Title = "Из источника", + Source = new SourceRef { Kind = "external", ExternalId = "42", DisplayName = "Канал" }, + }; - ISource source = card.Source; - Assert.IsType(source); - Assert.Equal("@chan", ((ITelegramSource)source).PeerHandle); + Assert.Equal("external", card.Source.Kind); + Assert.Equal("42", card.Source.ExternalId); + Assert.Equal("Канал", card.Source.DisplayName); } } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Cards/LocalSourceStub.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/LocalSourceStub.cs deleted file mode 100644 index 5a07a7c..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Cards/LocalSourceStub.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Deal.Modules.Cards.Application.Abstractions; - -namespace Deal.Tests.Unit.Modules.Cards; - -internal sealed class LocalSourceStub(string authorId) : ILocalSource -{ - public string? AuthorId { get; } = authorId; - - public string DisplayName => "создано вручную"; - - public string? OriginRef => null; - - public string? RawPayload => null; - - public DateTimeOffset ReceivedAt { get; } = DateTimeOffset.UtcNow; -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Cards/TelegramSourceStub.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/TelegramSourceStub.cs deleted file mode 100644 index bd47a15..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Cards/TelegramSourceStub.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Deal.Modules.Cards.Application.Abstractions; - -namespace Deal.Tests.Unit.Modules.Cards; - -internal sealed class TelegramSourceStub(string dialogId, long messageId, string? peerHandle, string peerName, string? topicId) - : ITelegramSource -{ - public string DialogId { get; } = dialogId; - - public long MessageId { get; } = messageId; - - public string? PeerHandle { get; } = peerHandle; - - public string PeerName { get; } = peerName; - - public string? TopicId { get; } = topicId; - - public string DisplayName => PeerHandle is null ? PeerName : "@" + PeerHandle; - - public string? OriginRef => PeerHandle is null - ? $"https://t.me/c/{DialogId.TrimStart('-', '1', '0', '0')}/{MessageId}" - : $"https://t.me/{PeerHandle}/{MessageId}"; - - public string? RawPayload => null; - - public DateTimeOffset ReceivedAt { get; } = DateTimeOffset.UtcNow; -}