Ввести единый контракт источника в домен Cards
Добавлены SourceItem/SourceRef/SourceContent/DataRef/ContactRef (строго типизированный контракт без подтипов вложений; файлы — ссылки на общий сервис данных). Удалены Telegram-ориентированные source-интерфейсы из Cards; Card/ICard переведены на SourceRef.
This commit is contained in:
@@ -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<string, string>? 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<DataRef> Data { get; init; } = []; // ссылки на файлы в Storage
|
||||
public IReadOnlyList<string>? Links { get; init; } // ссылки (не файлы)
|
||||
public IReadOnlyList<ContactRef>? Contacts { get; init; } // контакты
|
||||
public IReadOnlyDictionary<string, string>? 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<string, string>? 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 из ядра и задачи/этапы — везде.
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник «ИИ»: карточка создана/сгенерирована ИИ
|
||||
/// </summary>
|
||||
public interface IAiSource : ISource
|
||||
{
|
||||
/// <summary>
|
||||
/// Id провайдера ИИ
|
||||
/// </summary>
|
||||
public string ProviderId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Модель провайдера.
|
||||
/// </summary>
|
||||
public string Model { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Id ИИ-агента
|
||||
/// </summary>
|
||||
public string? AgentId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка на промпт/шаблон
|
||||
/// </summary>
|
||||
public string? PromptRef { get; }
|
||||
|
||||
/// <summary>
|
||||
/// API, через который работал агент
|
||||
/// </summary>
|
||||
public IApiSource? ViaApi { get; }
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник «внешний API»
|
||||
/// </summary>
|
||||
public interface IApiSource : ISource
|
||||
{
|
||||
public string ProviderId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Адрес эндпоинта
|
||||
/// </summary>
|
||||
public string? Endpoint { get; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,5 +20,5 @@ public interface ICard
|
||||
/// <summary>
|
||||
/// Источник: откуда карточка пришла
|
||||
/// </summary>
|
||||
public ISource Source { get; }
|
||||
public SourceRef Source { get; }
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Составной источник
|
||||
/// </summary>
|
||||
public interface ICompositeSource : ISource
|
||||
{
|
||||
/// <summary>
|
||||
/// Первоисточник контента.
|
||||
/// </summary>
|
||||
public ISource Origin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Цепочка обработки
|
||||
/// </summary>
|
||||
public IReadOnlyList<ISource> Pipeline { get; }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник «файл»
|
||||
/// </summary>
|
||||
public interface IFileSource : ISource
|
||||
{
|
||||
public string ObjectKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя файла.
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Размер файла в байтах.
|
||||
/// </summary>
|
||||
public long SizeBytes { get; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник «создано вручную/локально»
|
||||
/// </summary>
|
||||
public interface ILocalSource : ISource
|
||||
{
|
||||
/// <summary>
|
||||
/// Id автора (оператора тенанта), создавшего карточку; null — неизвестен.
|
||||
/// </summary>
|
||||
public string? AuthorId { get; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник «импорт данных»
|
||||
/// </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; }
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник карточки
|
||||
/// </summary>
|
||||
public interface ISource
|
||||
{
|
||||
/// <summary>
|
||||
/// Подпись источника в UI
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка на оригинал
|
||||
/// </summary>
|
||||
public string? OriginRef { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Сырое содержимое
|
||||
/// </summary>
|
||||
public string? RawPayload { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Время получения/создания исходных данных.
|
||||
/// </summary>
|
||||
public DateTimeOffset ReceivedAt { get; }
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник «Telegram»
|
||||
/// </summary>
|
||||
public interface ITelegramSource : ISource
|
||||
{
|
||||
/// <summary>
|
||||
/// Id диалога (peer) в Telegram.
|
||||
/// </summary>
|
||||
public string DialogId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Id сообщения в диалоге.
|
||||
/// </summary>
|
||||
public long MessageId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Handle канала
|
||||
/// </summary>
|
||||
public string? PeerHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя канала/группы/чата для отображения.
|
||||
/// </summary>
|
||||
public string PeerName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Id темы форума
|
||||
/// </summary>
|
||||
public string? TopicId { get; }
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Источник «ссылка на сайт/объявление»
|
||||
/// </summary>
|
||||
public interface IWebSource : ISource
|
||||
{
|
||||
/// <summary>
|
||||
/// Адрес страницы/объявления.
|
||||
/// </summary>
|
||||
public string Url { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя сайта (домен/бренд); null — не определено.
|
||||
/// </summary>
|
||||
public string? SiteName { get; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISource Source { get; init; } = null!;
|
||||
public SourceRef Source { get; init; } = new() { Kind = SourceKinds.Local };
|
||||
|
||||
// ── Модуль «содержимое» ──
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
/// <summary>
|
||||
/// Контакт, извлечённый из источника или указанный вручную.
|
||||
/// </summary>
|
||||
public sealed record ContactRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Вид контакта (например, телефон/почта/мессенджер/сайт).
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка на файл в общем сервисе данных.
|
||||
/// </summary>
|
||||
public sealed record DataRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Идентификатор объекта в сервисе данных.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка на скачивание/отображение.
|
||||
/// </summary>
|
||||
public required string Ref { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Тип, определённый сервисом данных (image/video/audio/document/archive/other).
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка на превью (если сервис данных сгенерировал).
|
||||
/// </summary>
|
||||
public string? PreviewRef { get; init; }
|
||||
|
||||
public string? Caption { get; init; }
|
||||
|
||||
public int? Order { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Прочие метаданные от сервиса данных.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Meta { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
/// <summary>
|
||||
/// Содержимое первичного сообщения источника.
|
||||
/// </summary>
|
||||
public sealed record SourceContent
|
||||
{
|
||||
public string? Text { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Форматированный текст (если источник его предоставил).
|
||||
/// </summary>
|
||||
public string? Html { get; init; }
|
||||
|
||||
public string? Author { get; init; }
|
||||
|
||||
public string? Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Вложения: ссылки на файлы в общем сервисе данных.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DataRef> Data { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Ссылки, не являющиеся файлами.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Links { get; init; }
|
||||
|
||||
public IReadOnlyList<ContactRef>? Contacts { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Прочие поля, не подходящие под текст/файл/ссылку/контакт.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Extra { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
/// <summary>
|
||||
/// Единый контракт входящей записи: источник + содержимое.
|
||||
/// </summary>
|
||||
public sealed record SourceItem
|
||||
{
|
||||
public required SourceRef Source { get; init; }
|
||||
|
||||
public required SourceContent Content { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
/// <summary>
|
||||
/// Источник данных: откуда пришла запись.
|
||||
/// </summary>
|
||||
public sealed record SourceRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Дискриминатор источника (значение задаёт сервис-владелец).
|
||||
/// </summary>
|
||||
public required string Kind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор записи в источнике (сообщение/строка/файл).
|
||||
/// </summary>
|
||||
public string? ExternalId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Подпись источника для интерфейса.
|
||||
/// </summary>
|
||||
public string? DisplayName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка на оригинал (url, deep-link, путь).
|
||||
/// </summary>
|
||||
public string? OriginRef { get; init; }
|
||||
|
||||
public string? Author { get; init; }
|
||||
|
||||
public DateTimeOffset ReceivedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Прочие метаданные источника.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Extra { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Дискриминаторы источников, владельцем которых является модуль.
|
||||
/// </summary>
|
||||
public static class SourceKinds
|
||||
{
|
||||
/// <summary>
|
||||
/// Запись создана вручную в системе.
|
||||
/// </summary>
|
||||
public const string Local = "local";
|
||||
}
|
||||
@@ -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<ICard>(card);
|
||||
Assert.IsAssignableFrom<IContentCard>(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<TelegramSourceStub>(source);
|
||||
Assert.Equal("@chan", ((ITelegramSource)source).PeerHandle);
|
||||
Assert.Equal("external", card.Source.Kind);
|
||||
Assert.Equal("42", card.Source.ExternalId);
|
||||
Assert.Equal("Канал", card.Source.DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user