Перевести ядро на единый контракт источника
Карточка, очередь и отсев работают с SourceItem (SourceRef + SourceContent); Telegram-поля убраны из домена, персистентности, конвейера и wire, остались только в адаптере приёма. Tenant-миграции пересозданы с нуля. Фронт переведён на generic source/content с просмотрщиком вложений.
This commit is contained in:
@@ -57,6 +57,10 @@
|
|||||||
| TD-DBL-CLICK | Двойная перезагрузка доски у инициатора batch-reclassify (ответ + SSE) | этап 12, E | P3 | TECHDEBT |
|
| TD-DBL-CLICK | Двойная перезагрузка доски у инициатора batch-reclassify (ответ + SSE) | этап 12, E | P3 | TECHDEBT |
|
||||||
| TD-TEST-HARNESS | Историческая гонка `FreeTcpPort` — устранена; следить за новыми хост-хелперами | этап 12, E | P3 | TECHDEBT |
|
| TD-TEST-HARNESS | Историческая гонка `FreeTcpPort` — устранена; следить за новыми хост-хелперами | этап 12, E | P3 | TECHDEBT |
|
||||||
| TD-OLD-DOCS | Исторические доки несут старые термины под пометками (переписывать не нужно) | docs sweep | P3 | TECHDEBT |
|
| TD-OLD-DOCS | Исторические доки несут старые термины под пометками (переписывать не нужно) | docs sweep | P3 | TECHDEBT |
|
||||||
|
| TD-SOURCE-PROVIDER | `GET /api/cards/{id}/source` + `ISourceContentProvider` + реестр провайдеров; ленивая догрузка `SourceContent` (сейчас контент отдаётся прямо в `CardDto`) | generic source 2026-09-11 | P1 | BACKLOG |
|
||||||
|
| TD-STORE-ATTACH | Выгрузка вложений источника в Storage-сервис адаптером + `TelegramSourceContentProvider` (только telegram-сервис) + рендер `DataRef` в UI | generic source 2026-09-11 | P1 | BACKLOG |
|
||||||
|
| TD-TG-CORE-SPLIT | Перенести оставшуюся Telegram-специфику ядра (`TelegramStore`, таблицы `Dialogs`/`TgMessages`, Discovery, ingress-proto) в telegram-сервис; generic ingress-контракт источников | generic source 2026-09-11 | P2 | TECHDEBT |
|
||||||
|
| TD-SOURCE-CONTACTS | Квалификатор контактов знает форматы профилей (t.me/`@handle`) — вынести в расширяемые правила источников | generic source 2026-09-11 | P3 | BACKLOG |
|
||||||
| TD-APIMAP-COUNT | Ручной подсчёт числа операторских ручек в `api-map` (может расходиться с группировкой) | docs sweep | P3 | TECHDEBT |
|
| TD-APIMAP-COUNT | Ручной подсчёт числа операторских ручек в `api-map` (может расходиться с группировкой) | docs sweep | P3 | TECHDEBT |
|
||||||
|
|
||||||
## 6. Manual-проверки (нужны внешние условия)
|
## 6. Manual-проверки (нужны внешние условия)
|
||||||
|
|||||||
+8
-7
@@ -232,16 +232,13 @@
|
|||||||
"local": false, // создана локально, без внешнего источника
|
"local": false, // создана локально, без внешнего источника
|
||||||
"title": "Разработка интернет-магазина", // string ≤140
|
"title": "Разработка интернет-магазина", // string ≤140
|
||||||
"summary": "Компания: …\nЗадача: …",// блок «О заявке»
|
"summary": "Компания: …\nЗадача: …",// блок «О заявке»
|
||||||
"source": { "kind": "telegram", "displayName": "Канал заказов", "originRef": "123456789", "receivedAt": 1726000000000 },
|
"source": { "kind": "telegram", "externalId": "4242", "displayName": "Канал заказов", "originRef": "123456789", "author": "…", "receivedAt": "2026-09-11T10:00:00+00:00", "extra": {"hue": "#8b8ff8"} },
|
||||||
"sourceMsg": "Ищу разработчика…", // исходное сообщение
|
"content": { "text": "Ищу разработчика…", "html": null, "author": "…", "subject": null, "data": [], "links": [], "contacts": [] },
|
||||||
"sourceDialogId": "123456789",
|
|
||||||
"sourceMsgId": 4242,
|
|
||||||
"stack": ["vue", "dotnet"],
|
"stack": ["vue", "dotnet"],
|
||||||
"budget": {"from": 100000, "to": 200000, "cur": "RUB"},
|
"budget": {"from": 100000, "to": 200000, "cur": "RUB"},
|
||||||
"converted": {"from": 100000, "to": 200000, "cur": "RUB"},
|
"converted": {"from": 100000, "to": 200000, "cur": "RUB"},
|
||||||
"contact": "@client",
|
"contact": "@client",
|
||||||
"contacts": [{"type": "tg", "value": "@client"}],
|
"contacts": [{"type": "tg", "value": "@client"}],
|
||||||
"channel": {"name": "Канал заказов", "handle": "@orders", "hue": "#8b8ff8"}, // прежний ch
|
|
||||||
"matchHits": [{"label": "Стек", "term": "vue", "word": null}],
|
"matchHits": [{"label": "Стек", "term": "vue", "word": null}],
|
||||||
"comments": [{"id": "cm_…", "by": "Вы", "text": "Позвонил", "time": "5 мин"}],
|
"comments": [{"id": "cm_…", "by": "Вы", "text": "Позвонил", "time": "5 мин"}],
|
||||||
"links": [{"id": "pl_…", "name": "Бриф", "url": "https://example.com"}],
|
"links": [{"id": "pl_…", "name": "Бриф", "url": "https://example.com"}],
|
||||||
@@ -257,11 +254,15 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Ключевые поля: `id/containerId/(col)` — принадлежность; `source/sourceMsg/sourceDialogId/sourceMsgId` —
|
Ключевые поля: `id/containerId/(col)` — принадлежность; `source` (`SourceRef`: вид, внешний id, подпись,
|
||||||
происхождение; `stack/budget/converted/contact/contacts/channel/matchHits` — данные заявки; `comments/
|
ссылка на оригинал, цвет в `extra.hue`) и `content` (`SourceContent`: текст, разметка, ссылки, контакты,
|
||||||
|
вложения `data`) — происхождение; `stack/budget/converted/contact/contacts/matchHits` — данные заявки; `comments/
|
||||||
links/files/history/tzText/reminder` — модули; `isNew/prevCol/isVacancy/isVacancyKnown` — маркеры. Ровно
|
links/files/history/tzText/reminder` — модули; `isNew/prevCol/isVacancy/isVacancyKnown` — маркеры. Ровно
|
||||||
один из полей `history[].type`/`history[].stage` задан.
|
один из полей `history[].type`/`history[].stage` задан.
|
||||||
|
|
||||||
|
Строки очереди и отсева «Обработки» отдают тот же generic `source`/`content` (раньше — `ch`); решение
|
||||||
|
отсева — `decidedBy`/`decidedByLabel` (stop|ml|ai|stale|dup).
|
||||||
|
|
||||||
**counts** (`GET /api/cards/counts`): `{new: int, "<containerId>": {count: int, new: int}, learning: int, ml: int, ai: int}` — плоская форма (совместима с прежним `/api/leads/counts`).
|
**counts** (`GET /api/cards/counts`): `{new: int, "<containerId>": {count: int, new: int}, learning: int, ml: int, ai: int}` — плоская форма (совместима с прежним `/api/leads/counts`).
|
||||||
|
|
||||||
### 4.2 Контейнер (container) — `GET /api/containers`, `POST/PATCH` тела
|
### 4.2 Контейнер (container) — `GET /api/containers`, `POST/PATCH` тела
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
# Дейл (Deal) — Статус разработки и прогресс
|
# Дейл (Deal) — Статус разработки и прогресс
|
||||||
|
|
||||||
> Обновляется в конце каждого захода. Проект НЕ git — это главный борд состояния.
|
> Обновляется в конце каждого захода. Проект в git (ветка `main`, коммиты на русском) — борд состояния + git-история.
|
||||||
> Дата последнего обновления: 2026-09-10. **Все этапы 0–12 выполнены (100%)** — см. roadmap
|
> Дата последнего обновления: 2026-09-11.
|
||||||
|
>
|
||||||
|
> **2026-09-11 — единый контракт источника (generic source).** Ядро (домен Cards, Storage-сервис,
|
||||||
|
> персистентность, конвейер, wire, фронт) переведено с Telegram-полей карточки на generic-тип
|
||||||
|
> `SourceItem` (`SourceRef` + `SourceContent`, вложения — `DataRef` → общий Storage). Дашборды/канбан/конвейер
|
||||||
|
> больше не знают о Telegram; Telegram-специфика — только в тонком адаптере приёма. Tenant-миграции
|
||||||
|
> пересозданы с нуля (init). Ядро: build 4 sln 0/0, `Deal.Tests.Unit` **1277/1277 PASS**, фронт `build` +
|
||||||
|
> `lint:i18n` зелёные. Детали — `docs/superpowers/specs/2026-09-11-source-contract-design.md`.
|
||||||
|
> Осталось (в backlog): `GET /api/cards/{id}/source` + `ISourceContentProvider`, выгрузка вложений
|
||||||
|
> telegram-адаптером в Storage, `TelegramSourceContentProvider`, перенос оставшейся Telegram-специфики
|
||||||
|
> (`TelegramStore`, `Dialogs`/`TgMessages`, Discovery) в telegram-сервис.
|
||||||
|
|
||||||
|
**Все этапы 0–12 выполнены (100%)** — см. roadmap
|
||||||
> `docs/superpowers/plans/2026-09-05-deal-roadmap.md`, план этапа 10
|
> `docs/superpowers/plans/2026-09-05-deal-roadmap.md`, план этапа 10
|
||||||
> `docs/superpowers/plans/2026-09-10-deal-stage10-operator-analytics.md` и ledgers в `.superpowers/sdd/`.
|
> `docs/superpowers/plans/2026-09-10-deal-stage10-operator-analytics.md` и ledgers в `.superpowers/sdd/`.
|
||||||
> Live-приёмки на Docker Desktop выполнены: dev-smoke 14/14, SaaS-контур 15/15, prod-контур+mTLS+
|
> Live-приёмки на Docker Desktop выполнены: dev-smoke 14/14, SaaS-контур 15/15, prod-контур+mTLS+
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Дизайн: единый контракт источника + общий Storage-сервис данных
|
# Дизайн: единый контракт источника + общий Storage-сервис данных
|
||||||
|
|
||||||
Дата: 2026-09-11. Статус: на согласовании (контракт не плодит типы вложений; файлы — в общем Storage).
|
Дата: 2026-09-11. Статус: реализовано в ядре (домен, Storage-сервис, персистентность, конвейер, wire, фронт); адаптер/провайдер telegram-сервиса и Storage-выгрузка — следующие шаги. Контракт не плодит типы вложений; файлы — в общем Storage.
|
||||||
|
|
||||||
## 1. Принцип
|
## 1. Принцип
|
||||||
|
|
||||||
@@ -127,3 +127,45 @@ API ядра: `GET /api/cards/{id}/source` → generic контент.
|
|||||||
6. Frontend: generic источник + универсальный просмотрщик.
|
6. Frontend: generic источник + универсальный просмотрщик.
|
||||||
7. Telegram: адаптер + провайдер исходника (только в telegram-сервисе) + выгрузка в Storage.
|
7. Telegram: адаптер + провайдер исходника (только в telegram-сервисе) + выгрузка в Storage.
|
||||||
8. Комментарии: убрать упоминания Telegram из ядра и задачи/этапы — везде.
|
8. Комментарии: убрать упоминания Telegram из ядра и задачи/этапы — везде.
|
||||||
|
|
||||||
|
## 9. Реализация: зафиксированные сигнатуры
|
||||||
|
|
||||||
|
### Ядро: домен
|
||||||
|
|
||||||
|
- `SourceRefs` (Deal.Modules.Cards/Application/Sources): `Empty`, `DefaultHue = "#666"`,
|
||||||
|
`HueKey = "hue"`, расширения `DedupeKey()` (вид|оригинал|внешний id), `ResolveHue()`.
|
||||||
|
- `CardSnapshot`: вместо `ChannelName/ChannelHandle/ChannelHue/SourceMsg/SourceDialogId/SourceMsgId` —
|
||||||
|
`SourceRef Source` + `SourceContent Content`; `ReceivedAt` остаётся.
|
||||||
|
- `CardDto`: вместо `Channel`/`SourceMsg`/`SourceDialogId`/`SourceMsgId`/прежнего `Source` —
|
||||||
|
`SourceRef Source` + `SourceContent Content`; `ReceivedAtMs` остаётся. `CardChannelDto`/`CardSourceDto` удалены.
|
||||||
|
- `ICardStore.GetCardBySourceAsync(SourceRef source, CancellationToken ct)`.
|
||||||
|
|
||||||
|
### Ядро: конвейер
|
||||||
|
|
||||||
|
- `QueuedMessage { required SourceItem Item; bool Force; }`.
|
||||||
|
- `QueueItemDto { string Id; SourceRef Source; SourceContent Content; string Text; string Status;
|
||||||
|
long MsgAtMs; long QueuedAtMs; bool Force; }` (JsonIgnore на Force).
|
||||||
|
- `RejectRecord { SourceRef Source; SourceContent Content; string Text; long MsgAtMs;
|
||||||
|
string DecidedBy; string Stage; string Reason; string Kw; string? DeterministicId; }`
|
||||||
|
(`DeterministicId = r_{Kind}_{OriginRef}_{ExternalId}`).
|
||||||
|
- `RejectedItemDto`: `Source`/`Content`, `DecidedBy`/`DecidedByLabel` (решение), остальное как было.
|
||||||
|
- `IPipelineStore.ExistsDuplicateAsync(SourceRef source, CancellationToken ct)`.
|
||||||
|
- `PipelineChannelDto` удалён.
|
||||||
|
|
||||||
|
### Схема БД (схема тенанта)
|
||||||
|
|
||||||
|
- Cards: удалить `ChannelName/ChannelHandle/ChannelHue/SourceMsg/SourceDialogId/SourceMsgId`;
|
||||||
|
добавить `SourceKind`, `SourceExternalId`, `SourceOriginRef` (text, для запросов), `SourceJson` (text),
|
||||||
|
`ContentJson` (text), `SourceText` (text). FTS: Title+Summary+SourceText+Contact.
|
||||||
|
- QueueItems: удалить `DialogId/ChannelName/ChannelHandle/ChannelHue/MsgId`; добавить
|
||||||
|
`SourceKey` (text, уникальный ключ дедупа), `SourceJson`, `ContentJson`. `Text/MsgAt/Status/Force/CreatedAt/UpdatedAt` остаются.
|
||||||
|
- RejectedItems: удалить `DialogId/MsgId/ChannelName/ChannelHandle/ChannelHue`; добавить
|
||||||
|
`SourceKey`, `SourceJson`, `ContentJson`. FTS — по `Text`.
|
||||||
|
- Миграции tenant: старые удалить, сгенерировать новый init с нуля (данных нет).
|
||||||
|
|
||||||
|
### Маппинг источника
|
||||||
|
|
||||||
|
- Адаптер Telegram (в ядре — тонкий край приёма): `Kind="telegram"`, `ExternalId=MsgId`,
|
||||||
|
`OriginRef=DialogId`, `DisplayName=ChannelName`, `Extra["hue"]=ChannelHue` (иначе дефолт),
|
||||||
|
`ReceivedAt=msgAt`, `Content.Text=Text`, `Content.Author=ChannelName`.
|
||||||
|
- Дашборды/карточки/конвейер работают только с `SourceRef`/`SourceContent`; Telegram-поля не проходят дальше адаптера.
|
||||||
|
|||||||
@@ -816,6 +816,14 @@ Contact) + GIN-индекс; tsvector-колонки авто-актуальны
|
|||||||
(квалификация, ≤6, primary), поля канала `ch`, `sourceMsg`/`sourceDialogId`/`sourceMsgId`; колонка —
|
(квалификация, ≤6, primary), поля канала `ch`, `sourceMsg`/`sourceDialogId`/`sourceMsgId`; колонка —
|
||||||
inbox либо доска по `BoardAccepts`+правилам; после успешной классификации карточка получает
|
inbox либо доска по `BoardAccepts`+правилам; после успешной классификации карточка получает
|
||||||
`isVacancy`/`isVacancyKnown`; создание карточки связывает хэш в `DedupEntries` с её id.
|
`isVacancy`/`isVacancyKnown`; создание карточки связывает хэш в `DedupEntries` с её id.
|
||||||
|
|
||||||
|
> **Актуально с 2026-09-11: единый контракт источника.** Карточка, очередь и отсев работают с generic-типом
|
||||||
|
> `SourceItem` (`SourceRef` + `SourceContent`). В таблицах `Cards`/`QueueItems`/`RejectedItems` вместо
|
||||||
|
> Telegram-колонок (`ChannelName`/`SourceMsg`/`SourceMsgId`…) хранятся `SourceKind`/`SourceExternalId`/
|
||||||
|
> `SourceOriginRef`/`SourceJson`/`ContentJson` (карточка — ещё `SourceText` и FTS по нему; очередь/отсев —
|
||||||
|
> `SourceKey` для дедупа). Вложение — `DataRef` (ссылка на общий Storage-сервис); контакты/ссылки — поля
|
||||||
|
> `SourceContent`. Telegram-поля остались только в тонком адаптере приёма telegram-сервиса. Tenant-миграции
|
||||||
|
> пересозданы с нуля (данных нет). Детали — `docs/superpowers/specs/2026-09-11-source-contract-design.md`.
|
||||||
- Отсев — источник решения `stop|ml|ai|stale|dup` (подписи «правила/ML/ИИ/система») и этап
|
- Отсев — источник решения `stop|ml|ai|stale|dup` (подписи «правила/ML/ИИ/система») и этап
|
||||||
`length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup` (подписи UI: «короткое сообщение»,
|
`length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup` (подписи UI: «короткое сообщение»,
|
||||||
«стоп-фраза», «резюме соискателя», «нет суммы», «устарело», «повтор»…), причина ≤500, kw — сработавшая
|
«стоп-фраза», «резюме соискателя», «нет суммы», «устарело», «повтор»…), причина ≤500, kw — сработавшая
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ public static class CardsEndpoints
|
|||||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||||
IReadOnlyList<CardDto> leads = await cardsService.SearchCardsAsync(q, ct);
|
IReadOnlyList<CardDto> leads = await cardsService.SearchCardsAsync(q, ct);
|
||||||
|
|
||||||
// messages всегда []: telegram-сообщений здесь нет, фронт их не читает.
|
// messages всегда []: фронт их не читает.
|
||||||
return Results.Ok(new { cards = leads, messages = Array.Empty<object>() });
|
return Results.Ok(new { cards = leads, messages = Array.Empty<object>() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Api.Events;
|
using Deal.Api.Events;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
using Deal.Grpc.Telegram;
|
using Deal.Grpc.Telegram;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
@@ -70,17 +72,30 @@ public sealed class TelegramIngressService(
|
|||||||
tenantContext.SetTenant(new TenantId(tenant.Id.ToString("N")));
|
tenantContext.SetTenant(new TenantId(tenant.Id.ToString("N")));
|
||||||
PipelineIngestService ingest = tenantScope.ServiceProvider.GetRequiredService<PipelineIngestService>();
|
PipelineIngestService ingest = tenantScope.ServiceProvider.GetRequiredService<PipelineIngestService>();
|
||||||
|
|
||||||
PipelineIngestResultDto result = await ingest.EnqueueAsync(
|
string hue = string.IsNullOrWhiteSpace(request.ChannelHue) ? SourceRefs.DefaultHue : request.ChannelHue;
|
||||||
new QueuedMessage
|
DateTimeOffset receivedAt = request.HasMsgAt
|
||||||
|
? DateTimeOffset.FromUnixTimeMilliseconds(request.MsgAt)
|
||||||
|
: DateTimeOffset.UtcNow;
|
||||||
|
var item = new SourceItem
|
||||||
|
{
|
||||||
|
Source = new SourceRef
|
||||||
{
|
{
|
||||||
DialogId = request.DialogId,
|
Kind = "telegram",
|
||||||
ChannelName = request.ChannelName,
|
ExternalId = request.HasMsgId ? request.MsgId.ToString(CultureInfo.InvariantCulture) : null,
|
||||||
ChannelHandle = request.ChannelHandle,
|
OriginRef = request.DialogId,
|
||||||
ChannelHue = request.ChannelHue,
|
DisplayName = request.ChannelName,
|
||||||
MsgId = request.HasMsgId ? request.MsgId : null,
|
ReceivedAt = receivedAt,
|
||||||
Text = request.Text,
|
Extra = new Dictionary<string, string> { [SourceRefs.HueKey] = hue },
|
||||||
MsgAtMs = request.HasMsgAt ? request.MsgAt : null,
|
|
||||||
},
|
},
|
||||||
|
Content = new SourceContent
|
||||||
|
{
|
||||||
|
Text = request.Text,
|
||||||
|
Author = request.ChannelName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
PipelineIngestResultDto result = await ingest.EnqueueAsync(
|
||||||
|
new QueuedMessage { Item = item },
|
||||||
context.CancellationToken).ConfigureAwait(false);
|
context.CancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await SavePreviewSafelyAsync(tenantScope, tenant, request, context.CancellationToken).ConfigureAwait(false);
|
await SavePreviewSafelyAsync(tenantScope, tenant, request, context.CancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -88,9 +88,9 @@ ContainersService containersService) : IColumnSuggester
|
|||||||
List<string> texts = cards
|
List<string> texts = cards
|
||||||
.Where(card => card.Col != CardIds.Trash
|
.Where(card => card.Col != CardIds.Trash
|
||||||
&& card.Col != CardIds.Archive
|
&& card.Col != CardIds.Archive
|
||||||
&& card.SourceMsg.Trim().Length > 0)
|
&& (card.Content.Text ?? string.Empty).Trim().Length > 0)
|
||||||
.Take(SuggestHeuristics.KeywordsSampleLimit)
|
.Take(SuggestHeuristics.KeywordsSampleLimit)
|
||||||
.Select(card => card.SourceMsg.Trim())
|
.Select(card => (card.Content.Text ?? string.Empty).Trim())
|
||||||
.ToList();
|
.ToList();
|
||||||
if (texts.Count < SuggestHeuristics.MinKeywordsSample)
|
if (texts.Count < SuggestHeuristics.MinKeywordsSample)
|
||||||
{
|
{
|
||||||
@@ -115,7 +115,7 @@ ContainersService containersService) : IColumnSuggester
|
|||||||
.Select(card => card.Id)
|
.Select(card => card.Id)
|
||||||
.ToHashSet(StringComparer.Ordinal);
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
Dictionary<string, string> textByCardId = inbox
|
Dictionary<string, string> textByCardId = inbox
|
||||||
.ToDictionary(card => card.Id, card => card.SourceMsg, StringComparer.Ordinal);
|
.ToDictionary(card => card.Id, card => card.Content.Text ?? string.Empty, StringComparer.Ordinal);
|
||||||
|
|
||||||
int created = 0;
|
int created = 0;
|
||||||
foreach (SuggestedColumnPlan plan in plans)
|
foreach (SuggestedColumnPlan plan in plans)
|
||||||
|
|||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260905193010_InitialTenant")]
|
|
||||||
partial class InitialTenant
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class InitialTenant : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "settings",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Key = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
|
||||||
ValueJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_settings", x => x.Key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "settings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-307
@@ -1,307 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260906125058_TenantKanban")]
|
|
||||||
partial class TenantKanban
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.BoardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Width")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Boards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantKanban : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Boards",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Color = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Width = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Position = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
KeywordsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Prompt = table.Column<string>(type: "text", nullable: false),
|
|
||||||
VisibleFieldsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Collapsed = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
Suggested = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
RulesJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Note = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Boards", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "CardMoves",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
LeadId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Action = table.Column<string>(type: "text", nullable: false),
|
|
||||||
FromCol = table.Column<string>(type: "text", nullable: true),
|
|
||||||
ToCol = table.Column<string>(type: "text", nullable: true),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_CardMoves", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Cards",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Col = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
|
||||||
IsNew = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
IsVacancy = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
IsVacancyKnown = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Summary = table.Column<string>(type: "text", nullable: false),
|
|
||||||
StackJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
BudgetFrom = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
BudgetTo = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
BudgetCur = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ConvFrom = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
ConvTo = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
ConvCur = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Contact = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ContactsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelName = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelHandle = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelHue = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ReceivedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
SourceMsg = table.Column<string>(type: "text", nullable: false),
|
|
||||||
SourceDialogId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
SourceMsgId = table.Column<long>(type: "bigint", nullable: true),
|
|
||||||
PrevCol = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ArchivedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
|
||||||
MatchHitsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Cards", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "MlOutbox",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Label = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Delta = table.Column<double>(type: "double precision", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_MlOutbox", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "LeadComments",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CardId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
By = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_LeadComments", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_LeadComments_Cards_CardId",
|
|
||||||
column: x => x.CardId,
|
|
||||||
principalTable: "Cards",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Boards_Suggested_Position",
|
|
||||||
table: "Boards",
|
|
||||||
columns: new[] { "Suggested", "Position" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Cards_Col_IsNew",
|
|
||||||
table: "Cards",
|
|
||||||
columns: new[] { "Col", "IsNew" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Cards_Col_ReceivedAt",
|
|
||||||
table: "Cards",
|
|
||||||
columns: new[] { "Col", "ReceivedAt" },
|
|
||||||
descending: new[] { false, true });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_LeadComments_CardId",
|
|
||||||
table: "LeadComments",
|
|
||||||
column: "CardId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_MlOutbox_CreatedAt",
|
|
||||||
table: "MlOutbox",
|
|
||||||
column: "CreatedAt");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Boards");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "CardMoves");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "LeadComments");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "MlOutbox");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Cards");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-462
@@ -1,462 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260906165058_TenantPipeline")]
|
|
||||||
partial class TenantPipeline
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.BoardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Width")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Boards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DedupEntryEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Hash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Hash");
|
|
||||||
|
|
||||||
b.ToTable("DedupEntries", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.QueueItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Force")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Status", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("QueueItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.RejectedItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kw")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("RejectedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReturnReason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReturnedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Source")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RejectedAt");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.ToTable("RejectedItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantPipeline : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<NpgsqlTsVector>(
|
|
||||||
name: "SearchTsv",
|
|
||||||
table: "Cards",
|
|
||||||
type: "tsvector",
|
|
||||||
nullable: false,
|
|
||||||
computedColumnSql: "to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))",
|
|
||||||
stored: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "DedupEntries",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Hash = table.Column<string>(type: "text", nullable: false),
|
|
||||||
LeadId = table.Column<string>(type: "text", nullable: true),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_DedupEntries", x => x.Hash);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "QueueItems",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
DialogId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelName = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelHandle = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelHue = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "text", nullable: false),
|
|
||||||
MsgId = table.Column<long>(type: "bigint", nullable: true),
|
|
||||||
MsgAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
Status = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Force = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_QueueItems", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "RejectedItems",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
DialogId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
MsgId = table.Column<long>(type: "bigint", nullable: true),
|
|
||||||
ChannelName = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelHandle = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ChannelHue = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Stage = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Reason = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Kw = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Source = table.Column<string>(type: "text", nullable: false),
|
|
||||||
MsgAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
RejectedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
Returned = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
ReturnedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
|
||||||
ReturnReason = table.Column<string>(type: "text", nullable: false),
|
|
||||||
SearchTsv = table.Column<NpgsqlTsVector>(type: "tsvector", nullable: false, computedColumnSql: "to_tsvector('russian', coalesce(\"Text\",''))", stored: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_RejectedItems", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Cards_SearchTsv",
|
|
||||||
table: "Cards",
|
|
||||||
column: "SearchTsv")
|
|
||||||
.Annotation("Npgsql:IndexMethod", "gin");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_QueueItems_Status_CreatedAt",
|
|
||||||
table: "QueueItems",
|
|
||||||
columns: new[] { "Status", "CreatedAt" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RejectedItems_RejectedAt",
|
|
||||||
table: "RejectedItems",
|
|
||||||
column: "RejectedAt");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RejectedItems_SearchTsv",
|
|
||||||
table: "RejectedItems",
|
|
||||||
column: "SearchTsv")
|
|
||||||
.Annotation("Npgsql:IndexMethod", "gin");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "DedupEntries");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "QueueItems");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "RejectedItems");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_Cards_SearchTsv",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "SearchTsv",
|
|
||||||
table: "Cards");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-549
@@ -1,549 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260906200342_TenantProjects")]
|
|
||||||
partial class TenantProjects
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.BoardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Width")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Boards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DedupEntryEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Hash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Hash");
|
|
||||||
|
|
||||||
b.ToTable("DedupEntries", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.ProjectCardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("CommentsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FilesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("HistoryJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LinksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Local")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReminderAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("ReminderFired")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TzText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("LeadId")
|
|
||||||
.IsUnique()
|
|
||||||
.HasFilter("\"LeadId\" IS NOT NULL");
|
|
||||||
|
|
||||||
b.HasIndex("Stage");
|
|
||||||
|
|
||||||
b.HasIndex("UpdatedAt")
|
|
||||||
.IsDescending();
|
|
||||||
|
|
||||||
b.ToTable("ProjectCards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.QueueItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Force")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Status", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("QueueItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.RejectedItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kw")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("RejectedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReturnReason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReturnedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Source")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RejectedAt");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.ToTable("RejectedItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantProjects : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ProjectCards",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Stage = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Local = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
LeadId = table.Column<string>(type: "text", nullable: true),
|
|
||||||
Title = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Summary = table.Column<string>(type: "text", nullable: false),
|
|
||||||
StackJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
BudgetFrom = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
BudgetTo = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
BudgetCur = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Contact = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CommentsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
LinksJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
FilesJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
HistoryJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
TzText = table.Column<string>(type: "text", nullable: false),
|
|
||||||
ReminderAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
|
||||||
ReminderFired = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ProjectCards", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ProjectCards_LeadId",
|
|
||||||
table: "ProjectCards",
|
|
||||||
column: "LeadId",
|
|
||||||
unique: true,
|
|
||||||
filter: "\"LeadId\" IS NOT NULL");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ProjectCards_Stage",
|
|
||||||
table: "ProjectCards",
|
|
||||||
column: "Stage");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ProjectCards_UpdatedAt",
|
|
||||||
table: "ProjectCards",
|
|
||||||
column: "UpdatedAt",
|
|
||||||
descending: new bool[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ProjectCards");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-619
@@ -1,619 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260907141242_TenantTelegram")]
|
|
||||||
partial class TenantTelegram
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.BoardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Width")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Boards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DedupEntryEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Hash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Hash");
|
|
||||||
|
|
||||||
b.ToTable("DedupEntries", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DialogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Backfilled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Handle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasDefaultValue("#666");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LastAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LastText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Monitor")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Dialogs", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.ProjectCardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("CommentsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FilesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("HistoryJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LinksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Local")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReminderAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("ReminderFired")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TzText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("LeadId")
|
|
||||||
.IsUnique()
|
|
||||||
.HasFilter("\"LeadId\" IS NOT NULL");
|
|
||||||
|
|
||||||
b.HasIndex("Stage");
|
|
||||||
|
|
||||||
b.HasIndex("UpdatedAt")
|
|
||||||
.IsDescending();
|
|
||||||
|
|
||||||
b.ToTable("ProjectCards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.QueueItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Force")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Status", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("QueueItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.RejectedItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kw")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("RejectedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReturnReason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReturnedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Source")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RejectedAt");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.ToTable("RejectedItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TgMessageEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("DialogId", "MsgAt");
|
|
||||||
|
|
||||||
b.ToTable("TgMessages", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantTelegram : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Dialogs",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Handle = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Kind = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Hue = table.Column<string>(type: "text", nullable: false, defaultValue: "#666"),
|
|
||||||
Monitor = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
LastText = table.Column<string>(type: "text", nullable: false),
|
|
||||||
LastAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
|
||||||
Backfilled = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Dialogs", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "TgMessages",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
DialogId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "text", nullable: false),
|
|
||||||
MsgAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
LeadId = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_TgMessages", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_TgMessages_DialogId_MsgAt",
|
|
||||||
table: "TgMessages",
|
|
||||||
columns: new[] { "DialogId", "MsgAt" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Dialogs");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "TgMessages");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-801
@@ -1,801 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260907155333_TenantDiscovery")]
|
|
||||||
partial class TenantDiscovery
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.BoardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Width")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Boards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DedupEntryEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Hash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Hash");
|
|
||||||
|
|
||||||
b.ToTable("DedupEntries", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DialogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Backfilled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Handle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasDefaultValue("#666");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LastAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LastText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Monitor")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Dialogs", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscBlacklistEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.ToTable("DiscBlacklist", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscCandidateEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoined")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double?>("FitRatio")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("JoinFailures")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool?>("LangRu")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MarksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int?>("Participants")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TopicsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Username")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "Status");
|
|
||||||
|
|
||||||
b.ToTable("DiscCandidates", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscLogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Event")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("DiscLog", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscTaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoin")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Evaluated")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Found")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Joined")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Lang")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("MinSubscribers")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("PlanJoins")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Rejected")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("SampleSize")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<bool>("SearchDone")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("SearchIdx")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Threshold")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("DiscTasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.ProjectCardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("CommentsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FilesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("HistoryJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LinksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Local")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReminderAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("ReminderFired")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TzText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("LeadId")
|
|
||||||
.IsUnique()
|
|
||||||
.HasFilter("\"LeadId\" IS NOT NULL");
|
|
||||||
|
|
||||||
b.HasIndex("Stage");
|
|
||||||
|
|
||||||
b.HasIndex("UpdatedAt")
|
|
||||||
.IsDescending();
|
|
||||||
|
|
||||||
b.ToTable("ProjectCards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.QueueItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Force")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Status", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("QueueItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.RejectedItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kw")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("RejectedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReturnReason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReturnedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Source")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RejectedAt");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.ToTable("RejectedItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TgMessageEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("DialogId", "MsgAt");
|
|
||||||
|
|
||||||
b.ToTable("TgMessages", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantDiscovery : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "DiscBlacklist",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
DialogId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Reason = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_DiscBlacklist", x => x.DialogId);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "DiscCandidates",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
DialogId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
TaskId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Username = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Kind = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Hue = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Participants = table.Column<int>(type: "integer", nullable: true),
|
|
||||||
LangRu = table.Column<bool>(type: "boolean", nullable: true),
|
|
||||||
MarksJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
TopicsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
FitRatio = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
Status = table.Column<string>(type: "text", nullable: false),
|
|
||||||
AutoJoined = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
JoinFailures = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_DiscCandidates", x => x.DialogId);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "DiscLog",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
TaskId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Event = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Text = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_DiscLog", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "DiscTasks",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: false),
|
|
||||||
KeywordsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
MinSubscribers = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Lang = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Threshold = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
SampleSize = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
PlanJoins = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
AutoJoin = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
Status = table.Column<string>(type: "text", nullable: false),
|
|
||||||
SearchIdx = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
SearchDone = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
Found = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Evaluated = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Joined = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Rejected = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_DiscTasks", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_DiscCandidates_TaskId_Status",
|
|
||||||
table: "DiscCandidates",
|
|
||||||
columns: new[] { "TaskId", "Status" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_DiscLog_TaskId_CreatedAt",
|
|
||||||
table: "DiscLog",
|
|
||||||
columns: new[] { "TaskId", "CreatedAt" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "DiscBlacklist");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "DiscCandidates");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "DiscLog");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "DiscTasks");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-879
@@ -1,879 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260909222322_TenantContainers")]
|
|
||||||
partial class TenantContainers
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.BoardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Width")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Boards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.ContainerEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasColumnType("character varying(20)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PolicyJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Name\",'')||' '||coalesce(\"Description\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Space")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasColumnType("character varying(20)");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Space", "Position");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Containers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DedupEntryEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Hash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Hash");
|
|
||||||
|
|
||||||
b.ToTable("DedupEntries", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DialogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Backfilled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Handle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasDefaultValue("#666");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LastAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LastText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Monitor")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Dialogs", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscBlacklistEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.ToTable("DiscBlacklist", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscCandidateEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoined")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double?>("FitRatio")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("JoinFailures")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool?>("LangRu")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MarksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int?>("Participants")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TopicsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Username")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "Status");
|
|
||||||
|
|
||||||
b.ToTable("DiscCandidates", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscLogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Event")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("DiscLog", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscTaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoin")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Evaluated")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Found")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Joined")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Lang")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("MinSubscribers")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("PlanJoins")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Rejected")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("SampleSize")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<bool>("SearchDone")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("SearchIdx")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Threshold")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("DiscTasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.ProjectCardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("CommentsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FilesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("HistoryJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LinksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Local")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReminderAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("ReminderFired")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TzText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("LeadId")
|
|
||||||
.IsUnique()
|
|
||||||
.HasFilter("\"LeadId\" IS NOT NULL");
|
|
||||||
|
|
||||||
b.HasIndex("Stage");
|
|
||||||
|
|
||||||
b.HasIndex("UpdatedAt")
|
|
||||||
.IsDescending();
|
|
||||||
|
|
||||||
b.ToTable("ProjectCards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.QueueItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Force")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Status", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("QueueItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.RejectedItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kw")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("RejectedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReturnReason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReturnedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Source")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RejectedAt");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.ToTable("RejectedItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TgMessageEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("DialogId", "MsgAt");
|
|
||||||
|
|
||||||
b.ToTable("TgMessages", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantContainers : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Containers",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Color = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Position = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Kind = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
|
||||||
Space = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
|
||||||
KeywordsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
VisibleFieldsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Collapsed = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
Suggested = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
RulesJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Note = table.Column<string>(type: "text", nullable: false),
|
|
||||||
PolicyJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
SearchTsv = table.Column<NpgsqlTsVector>(type: "tsvector", nullable: false, computedColumnSql: "to_tsvector('russian', coalesce(\"Name\",'')||' '||coalesce(\"Description\",''))", stored: true),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Containers", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Containers_SearchTsv",
|
|
||||||
table: "Containers",
|
|
||||||
column: "SearchTsv")
|
|
||||||
.Annotation("Npgsql:IndexMethod", "gin");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Containers_Space_Position",
|
|
||||||
table: "Containers",
|
|
||||||
columns: new[] { "Space", "Position" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Containers_Suggested_Position",
|
|
||||||
table: "Containers",
|
|
||||||
columns: new[] { "Suggested", "Position" });
|
|
||||||
|
|
||||||
// Состав контейнеров по умолчанию НЕ сидируется миграцией: единственный источник —
|
|
||||||
// CardsDefaultContainers/CardIds, строки идемпотентно создаёт DefaultContainerProvisioner.EnsureAsync
|
|
||||||
// при провижининге тенанта (TenantProvisioningService) сразу после применения миграций.
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Containers");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-823
@@ -1,823 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260910132805_TenantUnifiedCard")]
|
|
||||||
partial class TenantUnifiedCard
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.BoardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Width")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Boards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FilesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("HistoryJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("LinksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Local")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReminderAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("ReminderFired")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TzText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("UpdatedAt")
|
|
||||||
.IsDescending();
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.ContainerEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasColumnType("character varying(20)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PolicyJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Name\",'')||' '||coalesce(\"Description\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Space")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasColumnType("character varying(20)");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Space", "Position");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Containers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DedupEntryEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Hash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Hash");
|
|
||||||
|
|
||||||
b.ToTable("DedupEntries", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DialogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Backfilled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Handle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasDefaultValue("#666");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LastAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LastText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Monitor")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Dialogs", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscBlacklistEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.ToTable("DiscBlacklist", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscCandidateEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoined")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double?>("FitRatio")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("JoinFailures")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool?>("LangRu")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MarksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int?>("Participants")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TopicsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Username")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "Status");
|
|
||||||
|
|
||||||
b.ToTable("DiscCandidates", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscLogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Event")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("DiscLog", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscTaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoin")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Evaluated")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Found")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Joined")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Lang")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("MinSubscribers")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("PlanJoins")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Rejected")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("SampleSize")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<bool>("SearchDone")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("SearchIdx")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Threshold")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("DiscTasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.QueueItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Force")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Status", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("QueueItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.RejectedItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kw")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("RejectedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReturnReason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReturnedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Source")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RejectedAt");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.ToTable("RejectedItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TgMessageEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("DialogId", "MsgAt");
|
|
||||||
|
|
||||||
b.ToTable("TgMessages", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantUnifiedCard : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ProjectCards");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "FilesJson",
|
|
||||||
table: "Cards",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "HistoryJson",
|
|
||||||
table: "Cards",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "LinksJson",
|
|
||||||
table: "Cards",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "Local",
|
|
||||||
table: "Cards",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
|
||||||
name: "ReminderAt",
|
|
||||||
table: "Cards",
|
|
||||||
type: "timestamp with time zone",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "ReminderFired",
|
|
||||||
table: "Cards",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "TzText",
|
|
||||||
table: "Cards",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
|
||||||
name: "UpdatedAt",
|
|
||||||
table: "Cards",
|
|
||||||
type: "timestamp with time zone",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Cards_UpdatedAt",
|
|
||||||
table: "Cards",
|
|
||||||
column: "UpdatedAt",
|
|
||||||
descending: new bool[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_Cards_UpdatedAt",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "FilesJson",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "HistoryJson",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "LinksJson",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Local",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "ReminderAt",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "ReminderFired",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "TzText",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "UpdatedAt",
|
|
||||||
table: "Cards");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ProjectCards",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
BudgetCur = table.Column<string>(type: "text", nullable: false),
|
|
||||||
BudgetFrom = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
BudgetTo = table.Column<double>(type: "double precision", nullable: true),
|
|
||||||
CommentsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Contact = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
FilesJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
HistoryJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
LeadId = table.Column<string>(type: "text", nullable: true),
|
|
||||||
LinksJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Local = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
ReminderAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
|
||||||
ReminderFired = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
StackJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Stage = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Summary = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "text", nullable: false),
|
|
||||||
TzText = table.Column<string>(type: "text", nullable: false),
|
|
||||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ProjectCards", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ProjectCards_LeadId",
|
|
||||||
table: "ProjectCards",
|
|
||||||
column: "LeadId",
|
|
||||||
unique: true,
|
|
||||||
filter: "\"LeadId\" IS NOT NULL");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ProjectCards_Stage",
|
|
||||||
table: "ProjectCards",
|
|
||||||
column: "Stage");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ProjectCards_UpdatedAt",
|
|
||||||
table: "ProjectCards",
|
|
||||||
column: "UpdatedAt",
|
|
||||||
descending: new bool[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-763
@@ -1,763 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Deal.Infrastructure.Persistence;
|
|
||||||
using Deal.Infrastructure.Persistence.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TenantDbContext))]
|
|
||||||
[Migration("20260910134104_TenantContainerRegistry")]
|
|
||||||
partial class TenantContainerRegistry
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "10.0.11")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("BudgetCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("BudgetTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Col")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Contact")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ContactsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ConvCur")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvFrom")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<double?>("ConvTo")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FilesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("HistoryJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("IsNew")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancy")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<bool>("IsVacancyKnown")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("LinksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Local")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MatchHitsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PrevCol")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReceivedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReminderAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("ReminderFired")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("SourceDialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("SourceMsg")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<long?>("SourceMsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("StackJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Summary")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TzText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("UpdatedAt")
|
|
||||||
.IsDescending();
|
|
||||||
|
|
||||||
b.HasIndex("Col", "IsNew");
|
|
||||||
|
|
||||||
b.HasIndex("Col", "ReceivedAt")
|
|
||||||
.IsDescending(false, true);
|
|
||||||
|
|
||||||
b.ToTable("Cards", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.CardMoveEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Action")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("FromCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ToCol")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("CardMoves", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.ContainerEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Collapsed")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Color")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasColumnType("character varying(20)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Note")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("PolicyJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Position")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("RulesJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Name\",'')||' '||coalesce(\"Description\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Space")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(20)
|
|
||||||
.HasColumnType("character varying(20)");
|
|
||||||
|
|
||||||
b.Property<bool>("Suggested")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("VisibleFieldsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.HasIndex("Space", "Position");
|
|
||||||
|
|
||||||
b.HasIndex("Suggested", "Position");
|
|
||||||
|
|
||||||
b.ToTable("Containers", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DedupEntryEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Hash")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Hash");
|
|
||||||
|
|
||||||
b.ToTable("DedupEntries", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DialogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Backfilled")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Handle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasDefaultValue("#666");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LastAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("LastText")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Monitor")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Dialogs", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscBlacklistEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.ToTable("DiscBlacklist", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscCandidateEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoined")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double?>("FitRatio")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Hue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("JoinFailures")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Kind")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool?>("LangRu")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MarksJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int?>("Participants")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TopicsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Username")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("DialogId");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "Status");
|
|
||||||
|
|
||||||
b.ToTable("DiscCandidates", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscLogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Event")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("TaskId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("TaskId", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("DiscLog", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.DiscTaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoJoin")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Evaluated")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Found")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Joined")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("KeywordsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Lang")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("MinSubscribers")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("PlanJoins")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Rejected")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("SampleSize")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<bool>("SearchDone")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("SearchIdx")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Threshold")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("DiscTasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("By")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("CardId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CardId");
|
|
||||||
|
|
||||||
b.ToTable("LeadComments", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.MlOutboxEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<double>("Delta")
|
|
||||||
.HasColumnType("double precision");
|
|
||||||
|
|
||||||
b.Property<string>("Label")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("MlOutbox", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.QueueItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Force")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Status", "CreatedAt");
|
|
||||||
|
|
||||||
b.ToTable("QueueItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.RejectedItemEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHandle")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelHue")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("ChannelName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Kw")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<long?>("MsgId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("RejectedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ReturnReason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("Returned")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ReturnedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<NpgsqlTsVector>("SearchTsv")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("tsvector")
|
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", true);
|
|
||||||
|
|
||||||
b.Property<string>("Source")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Stage")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RejectedAt");
|
|
||||||
|
|
||||||
b.HasIndex("SearchTsv");
|
|
||||||
|
|
||||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchTsv"), "gin");
|
|
||||||
|
|
||||||
b.ToTable("RejectedItems", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TenantSettingEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("ValueJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Key");
|
|
||||||
|
|
||||||
b.ToTable("settings", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.TgMessageEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("DialogId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LeadId")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("MsgAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Text")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("DialogId", "MsgAt");
|
|
||||||
|
|
||||||
b.ToTable("TgMessages", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Deal.Infrastructure.Persistence.Entities.LeadCommentEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Deal.Infrastructure.Persistence.Entities.CardEntity", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CardId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantContainerRegistry : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Boards");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Boards",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Collapsed = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
Color = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: false),
|
|
||||||
KeywordsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Note = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Position = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Prompt = table.Column<string>(type: "text", nullable: false),
|
|
||||||
RulesJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Suggested = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
VisibleFieldsJson = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Width = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Boards", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Boards_Suggested_Position",
|
|
||||||
table: "Boards",
|
|
||||||
columns: new[] { "Suggested", "Position" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class TenantContainerCleanup : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "KeywordsJson",
|
|
||||||
table: "Containers");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "VisibleFieldsJson",
|
|
||||||
table: "Containers");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "KeywordsJson",
|
|
||||||
table: "Containers",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "VisibleFieldsJson",
|
|
||||||
table: "Containers",
|
|
||||||
type: "text",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,9 @@ public sealed class CardConfiguration : IEntityTypeConfiguration<CardEntity>
|
|||||||
// Максимальная длина Col: служебные значения + id доски (короткие строки b_...).
|
// Максимальная длина Col: служебные значения + id доски (короткие строки b_...).
|
||||||
private const int ColMaxLength = 200;
|
private const int ColMaxLength = 200;
|
||||||
|
|
||||||
|
// Максимальная длина вида источника.
|
||||||
|
private const int SourceKindMaxLength = 64;
|
||||||
|
|
||||||
public void Configure(EntityTypeBuilder<CardEntity> builder)
|
public void Configure(EntityTypeBuilder<CardEntity> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("Cards");
|
builder.ToTable("Cards");
|
||||||
@@ -27,7 +30,12 @@ public sealed class CardConfiguration : IEntityTypeConfiguration<CardEntity>
|
|||||||
builder.Property(x => x.LinksJson).HasColumnType("text");
|
builder.Property(x => x.LinksJson).HasColumnType("text");
|
||||||
builder.Property(x => x.FilesJson).HasColumnType("text");
|
builder.Property(x => x.FilesJson).HasColumnType("text");
|
||||||
builder.Property(x => x.HistoryJson).HasColumnType("text");
|
builder.Property(x => x.HistoryJson).HasColumnType("text");
|
||||||
builder.Property(x => x.SourceMsg).HasColumnType("text");
|
builder.Property(x => x.SourceKind).HasMaxLength(SourceKindMaxLength);
|
||||||
|
builder.Property(x => x.SourceExternalId).HasColumnType("text");
|
||||||
|
builder.Property(x => x.SourceOriginRef).HasColumnType("text");
|
||||||
|
builder.Property(x => x.SourceJson).HasColumnType("text");
|
||||||
|
builder.Property(x => x.ContentJson).HasColumnType("text");
|
||||||
|
builder.Property(x => x.SourceText).HasColumnType("text");
|
||||||
|
|
||||||
// Выборка колонки сортируется по времени получения (received_at DESC); счётчик новых — по (col, is_new).
|
// Выборка колонки сортируется по времени получения (received_at DESC); счётчик новых — по (col, is_new).
|
||||||
builder.HasIndex(x => new { x.Col, x.ReceivedAt }).IsDescending(false, true);
|
builder.HasIndex(x => new { x.Col, x.ReceivedAt }).IsDescending(false, true);
|
||||||
@@ -38,7 +46,7 @@ public sealed class CardConfiguration : IEntityTypeConfiguration<CardEntity>
|
|||||||
|
|
||||||
builder.Property(x => x.SearchTsv)
|
builder.Property(x => x.SearchTsv)
|
||||||
.HasComputedColumnSql(
|
.HasComputedColumnSql(
|
||||||
"to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))",
|
"to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceText\",'')||' '||coalesce(\"Contact\",''))",
|
||||||
stored: true);
|
stored: true);
|
||||||
builder.HasIndex(x => x.SearchTsv).HasMethod("gin");
|
builder.HasIndex(x => x.SearchTsv).HasMethod("gin");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,14 @@ public sealed class QueueItemConfiguration : IEntityTypeConfiguration<QueueItemE
|
|||||||
builder.HasKey(x => x.Id);
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
builder.Property(x => x.Text).HasColumnType("text");
|
builder.Property(x => x.Text).HasColumnType("text");
|
||||||
|
builder.Property(x => x.SourceKey).HasColumnType("text");
|
||||||
|
builder.Property(x => x.SourceJson).HasColumnType("text");
|
||||||
|
builder.Property(x => x.ContentJson).HasColumnType("text");
|
||||||
|
|
||||||
// Выборка pump'а идёт по статусу и времени постановки (status='new', лимит 12).
|
// Выборка pump'а идёт по статусу и времени постановки (status='new', лимит 12).
|
||||||
builder.HasIndex(x => new { x.Status, x.CreatedAt });
|
builder.HasIndex(x => new { x.Status, x.CreatedAt });
|
||||||
|
|
||||||
|
// Дубль-гвард приёма ищет строку по ключу записи источника.
|
||||||
|
builder.HasIndex(x => x.SourceKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public sealed class RejectedItemConfiguration : IEntityTypeConfiguration<Rejecte
|
|||||||
builder.Property(x => x.Text).HasColumnType("text");
|
builder.Property(x => x.Text).HasColumnType("text");
|
||||||
builder.Property(x => x.Reason).HasColumnType("text");
|
builder.Property(x => x.Reason).HasColumnType("text");
|
||||||
builder.Property(x => x.Kw).HasColumnType("text");
|
builder.Property(x => x.Kw).HasColumnType("text");
|
||||||
|
builder.Property(x => x.SourceKey).HasColumnType("text");
|
||||||
|
builder.Property(x => x.SourceJson).HasColumnType("text");
|
||||||
|
builder.Property(x => x.ContentJson).HasColumnType("text");
|
||||||
|
|
||||||
builder.Property(x => x.SearchTsv)
|
builder.Property(x => x.SearchTsv)
|
||||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", stored: true);
|
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", stored: true);
|
||||||
|
|||||||
@@ -92,40 +92,40 @@ public sealed class CardEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ContactsJson { get; set; } = "[]";
|
public string ContactsJson { get; set; } = "[]";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Имя канала/диалога-источника.
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle канала-источника.
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHandle { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Цвет канала-источника
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHue { get; set; } = "#666";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время получения исходного сообщения
|
/// Время получения исходного сообщения
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset ReceivedAt { get; set; }
|
public DateTimeOffset ReceivedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текст исходного сообщения
|
/// Вид источника
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SourceMsg { get; set; } = string.Empty;
|
public string SourceKind { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога исходного сообщения
|
/// Идентификатор записи в источнике
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SourceDialogId { get; set; } = string.Empty;
|
public string SourceExternalId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id исходного сообщения в Telegram, либо null.
|
/// Ссылка на оригинал в источнике
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long? SourceMsgId { get; set; }
|
public string SourceOriginRef { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ссылка на источник, сериализованная в JSON
|
||||||
|
/// </summary>
|
||||||
|
public string SourceJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Содержимое источника, сериализованное в JSON
|
||||||
|
/// </summary>
|
||||||
|
public string ContentJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текст источника для полнотекстового поиска
|
||||||
|
/// </summary>
|
||||||
|
public string SourceText { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Предыдущая колонка
|
/// Предыдущая колонка
|
||||||
|
|||||||
@@ -11,35 +11,25 @@ public sealed class QueueItemEntity
|
|||||||
public string Id { get; set; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога-источника
|
/// Ключ дедупликации записи источника
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DialogId { get; set; } = string.Empty;
|
public string SourceKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Имя канала/диалога-источника.
|
/// Ссылка на источник, сериализованная в JSON
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ChannelName { get; set; } = string.Empty;
|
public string SourceJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle канала-источника.
|
/// Содержимое источника, сериализованное в JSON
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ChannelHandle { get; set; } = string.Empty;
|
public string ContentJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Цвет канала-источника
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHue { get; set; } = "#666";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текст сообщения
|
/// Текст сообщения
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Id исходного сообщения в Telegram, либо null
|
|
||||||
/// </summary>
|
|
||||||
public long? MsgId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время получения исходного сообщения
|
/// Время получения исходного сообщения
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -13,29 +13,19 @@ public sealed class RejectedItemEntity
|
|||||||
public string Id { get; set; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога-источника
|
/// Ключ дедупликации записи источника
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DialogId { get; set; } = string.Empty;
|
public string SourceKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id исходного сообщения в Telegram, либо null.
|
/// Ссылка на источник, сериализованная в JSON
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long? MsgId { get; set; }
|
public string SourceJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Имя канала/диалога-источника.
|
/// Содержимое источника, сериализованное в JSON
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ChannelName { get; set; } = string.Empty;
|
public string ContentJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle канала-источника.
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHandle { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Цвет канала-источника
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHue { get; set; } = "#666";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текст сообщения
|
/// Текст сообщения
|
||||||
|
|||||||
+744
-755
File diff suppressed because it is too large
Load Diff
+462
@@ -0,0 +1,462 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NpgsqlTypes;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Deal.Infrastructure.Persistence.Migrations.TenantDb
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialTenant : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CardMoves",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
LeadId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Action = table.Column<string>(type: "text", nullable: false),
|
||||||
|
FromCol = table.Column<string>(type: "text", nullable: true),
|
||||||
|
ToCol = table.Column<string>(type: "text", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CardMoves", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Cards",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Col = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
IsNew = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Local = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsVacancy = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsVacancyKnown = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Summary = table.Column<string>(type: "text", nullable: false),
|
||||||
|
StackJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
BudgetFrom = table.Column<double>(type: "double precision", nullable: true),
|
||||||
|
BudgetTo = table.Column<double>(type: "double precision", nullable: true),
|
||||||
|
BudgetCur = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ConvFrom = table.Column<double>(type: "double precision", nullable: true),
|
||||||
|
ConvTo = table.Column<double>(type: "double precision", nullable: true),
|
||||||
|
ConvCur = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Contact = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ContactsJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ReceivedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
SourceKind = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
SourceExternalId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceOriginRef = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ContentJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceText = table.Column<string>(type: "text", nullable: false),
|
||||||
|
PrevCol = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ArchivedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
MatchHitsJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
LinksJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
FilesJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
HistoryJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
TzText = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ReminderAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ReminderFired = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
SearchTsv = table.Column<NpgsqlTsVector>(type: "tsvector", nullable: false, computedColumnSql: "to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceText\",'')||' '||coalesce(\"Contact\",''))", stored: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Cards", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Containers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Color = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Position = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Kind = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
Space = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
Collapsed = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Suggested = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
RulesJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Note = table.Column<string>(type: "text", nullable: false),
|
||||||
|
PolicyJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SearchTsv = table.Column<NpgsqlTsVector>(type: "tsvector", nullable: false, computedColumnSql: "to_tsvector('russian', coalesce(\"Name\",'')||' '||coalesce(\"Description\",''))", stored: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Containers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DedupEntries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Hash = table.Column<string>(type: "text", nullable: false),
|
||||||
|
LeadId = table.Column<string>(type: "text", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DedupEntries", x => x.Hash);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Dialogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Handle = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Kind = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Hue = table.Column<string>(type: "text", nullable: false, defaultValue: "#666"),
|
||||||
|
Monitor = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
LastText = table.Column<string>(type: "text", nullable: false),
|
||||||
|
LastAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
Backfilled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Dialogs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DiscBlacklist",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
DialogId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Reason = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DiscBlacklist", x => x.DialogId);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DiscCandidates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
DialogId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
TaskId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Username = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Kind = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Hue = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Participants = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
LangRu = table.Column<bool>(type: "boolean", nullable: true),
|
||||||
|
MarksJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
TopicsJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
FitRatio = table.Column<double>(type: "double precision", nullable: true),
|
||||||
|
Status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
AutoJoined = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
JoinFailures = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DiscCandidates", x => x.DialogId);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DiscLog",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
TaskId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Event = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Text = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DiscLog", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DiscTasks",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "text", nullable: false),
|
||||||
|
KeywordsJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
MinSubscribers = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Lang = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Threshold = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SampleSize = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
PlanJoins = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
AutoJoin = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SearchIdx = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SearchDone = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Found = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Evaluated = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Joined = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Rejected = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DiscTasks", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MlOutbox",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Text = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Label = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Delta = table.Column<double>(type: "double precision", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MlOutbox", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "QueueItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ContentJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Text = table.Column<string>(type: "text", nullable: false),
|
||||||
|
MsgAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Status = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Force = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_QueueItems", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "RejectedItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
ContentJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Text = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Stage = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Reason = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Kw = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Source = table.Column<string>(type: "text", nullable: false),
|
||||||
|
MsgAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
RejectedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Returned = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
ReturnedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ReturnReason = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SearchTsv = table.Column<NpgsqlTsVector>(type: "tsvector", nullable: false, computedColumnSql: "to_tsvector('russian', coalesce(\"Text\",''))", stored: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_RejectedItems", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
ValueJson = table.Column<string>(type: "text", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_settings", x => x.Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TgMessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
DialogId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Text = table.Column<string>(type: "text", nullable: false),
|
||||||
|
MsgAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
LeadId = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TgMessages", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "LeadComments",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CardId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
By = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Text = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_LeadComments", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_LeadComments_Cards_CardId",
|
||||||
|
column: x => x.CardId,
|
||||||
|
principalTable: "Cards",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Cards_Col_IsNew",
|
||||||
|
table: "Cards",
|
||||||
|
columns: new[] { "Col", "IsNew" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Cards_Col_ReceivedAt",
|
||||||
|
table: "Cards",
|
||||||
|
columns: new[] { "Col", "ReceivedAt" },
|
||||||
|
descending: new[] { false, true });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Cards_SearchTsv",
|
||||||
|
table: "Cards",
|
||||||
|
column: "SearchTsv")
|
||||||
|
.Annotation("Npgsql:IndexMethod", "gin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Cards_UpdatedAt",
|
||||||
|
table: "Cards",
|
||||||
|
column: "UpdatedAt",
|
||||||
|
descending: new bool[0]);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Containers_SearchTsv",
|
||||||
|
table: "Containers",
|
||||||
|
column: "SearchTsv")
|
||||||
|
.Annotation("Npgsql:IndexMethod", "gin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Containers_Space_Position",
|
||||||
|
table: "Containers",
|
||||||
|
columns: new[] { "Space", "Position" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Containers_Suggested_Position",
|
||||||
|
table: "Containers",
|
||||||
|
columns: new[] { "Suggested", "Position" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DiscCandidates_TaskId_Status",
|
||||||
|
table: "DiscCandidates",
|
||||||
|
columns: new[] { "TaskId", "Status" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DiscLog_TaskId_CreatedAt",
|
||||||
|
table: "DiscLog",
|
||||||
|
columns: new[] { "TaskId", "CreatedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_LeadComments_CardId",
|
||||||
|
table: "LeadComments",
|
||||||
|
column: "CardId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MlOutbox_CreatedAt",
|
||||||
|
table: "MlOutbox",
|
||||||
|
column: "CreatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_QueueItems_SourceKey",
|
||||||
|
table: "QueueItems",
|
||||||
|
column: "SourceKey");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_QueueItems_Status_CreatedAt",
|
||||||
|
table: "QueueItems",
|
||||||
|
columns: new[] { "Status", "CreatedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RejectedItems_RejectedAt",
|
||||||
|
table: "RejectedItems",
|
||||||
|
column: "RejectedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RejectedItems_SearchTsv",
|
||||||
|
table: "RejectedItems",
|
||||||
|
column: "SearchTsv")
|
||||||
|
.Annotation("Npgsql:IndexMethod", "gin");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TgMessages_DialogId_MsgAt",
|
||||||
|
table: "TgMessages",
|
||||||
|
columns: new[] { "DialogId", "MsgAt" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CardMoves");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Containers");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DedupEntries");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Dialogs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DiscBlacklist");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DiscCandidates");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DiscLog");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DiscTasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "LeadComments");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MlOutbox");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "QueueItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "RejectedItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TgMessages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Cards");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+741
-752
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ public sealed partial class KanbanStore
|
|||||||
AND ("SearchTsv" @@ plainto_tsquery('russian', {query})
|
AND ("SearchTsv" @@ plainto_tsquery('russian', {query})
|
||||||
OR lower("Title") LIKE {pattern}
|
OR lower("Title") LIKE {pattern}
|
||||||
OR lower("Summary") LIKE {pattern}
|
OR lower("Summary") LIKE {pattern}
|
||||||
OR lower("SourceMsg") LIKE {pattern}
|
OR lower("SourceText") LIKE {pattern}
|
||||||
OR lower("Contact") LIKE {pattern})
|
OR lower("Contact") LIKE {pattern})
|
||||||
ORDER BY ts_rank("SearchTsv", plainto_tsquery('russian', {query})) DESC, "ReceivedAt" DESC
|
ORDER BY ts_rank("SearchTsv", plainto_tsquery('russian', {query})) DESC, "ReceivedAt" DESC
|
||||||
LIMIT {limit}
|
LIMIT {limit}
|
||||||
@@ -76,18 +77,21 @@ public sealed partial class KanbanStore
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<CardDto?> GetCardBySourceAsync(
|
public async Task<CardDto?> GetCardBySourceAsync(
|
||||||
string dialogId,
|
SourceRef source,
|
||||||
long msgId,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(dialogId))
|
if (string.IsNullOrEmpty(source.Kind))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string externalId = source.ExternalId ?? string.Empty;
|
||||||
|
string originRef = source.OriginRef ?? string.Empty;
|
||||||
CardEntity? entity = await _dbContext.Cards
|
CardEntity? entity = await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(card => card.SourceDialogId == dialogId && card.SourceMsgId == msgId)
|
.Where(card => card.SourceKind == source.Kind
|
||||||
|
&& card.SourceExternalId == externalId
|
||||||
|
&& card.SourceOriginRef == originRef)
|
||||||
.OrderByDescending(card => card.ReceivedAt)
|
.OrderByDescending(card => card.ReceivedAt)
|
||||||
.FirstOrDefaultAsync(ct);
|
.FirstOrDefaultAsync(ct);
|
||||||
if (entity is null)
|
if (entity is null)
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ public sealed partial class KanbanStore
|
|||||||
card => card.Id,
|
card => card.Id,
|
||||||
(move, card) => new { move, card })
|
(move, card) => new { move, card })
|
||||||
.Where(joined => (joined.move.Action == MoveAction || joined.move.Action == RestoreAction)
|
.Where(joined => (joined.move.Action == MoveAction || joined.move.Action == RestoreAction)
|
||||||
&& joined.card.SourceMsg.Length > 0)
|
&& joined.card.SourceText.Length > 0)
|
||||||
.OrderByDescending(joined => joined.move.CreatedAt)
|
.OrderByDescending(joined => joined.move.CreatedAt)
|
||||||
.Take(limit)
|
.Take(limit)
|
||||||
.Select(joined => new AiMarkupExampleDto(joined.card.SourceMsg, joined.move.ToCol!))
|
.Select(joined => new AiMarkupExampleDto(joined.card.SourceText, joined.move.ToCol!))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ public sealed partial class KanbanStore
|
|||||||
{
|
{
|
||||||
List<CardEntity> entities = await _dbContext.Cards
|
List<CardEntity> entities = await _dbContext.Cards
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(card => card.Col == CardIds.Inbox && card.SourceMsg != string.Empty)
|
.Where(card => card.Col == CardIds.Inbox && card.SourceText != string.Empty)
|
||||||
.OrderByDescending(card => card.ReceivedAt)
|
.OrderByDescending(card => card.ReceivedAt)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
return entities.Select(card => ToCardDto(card, Array.Empty<CardCommentDto>())).ToList();
|
return entities.Select(card => ToCardDto(card, Array.Empty<CardCommentDto>())).ToList();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Abstractions;
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
@@ -120,11 +121,8 @@ public sealed partial class KanbanStore : ICardStore
|
|||||||
IsVacancyKnown = entity.IsVacancyKnown,
|
IsVacancyKnown = entity.IsVacancyKnown,
|
||||||
Title = entity.Title,
|
Title = entity.Title,
|
||||||
Summary = entity.Summary,
|
Summary = entity.Summary,
|
||||||
Source = new CardSourceDto(
|
Source = FromJsonOr(entity.SourceJson, SourceRefs.Empty),
|
||||||
Kind: ResolveSourceKind(entity),
|
Content = FromJsonOr(entity.ContentJson, new SourceContent { Text = entity.SourceText }),
|
||||||
DisplayName: entity.ChannelName,
|
|
||||||
OriginRef: entity.SourceDialogId,
|
|
||||||
ReceivedAt: entity.ReceivedAt.ToUnixTimeMilliseconds()),
|
|
||||||
Stack = ToJsonList<string>(entity.StackJson),
|
Stack = ToJsonList<string>(entity.StackJson),
|
||||||
Budget = entity.BudgetCur.Length == 0
|
Budget = entity.BudgetCur.Length == 0
|
||||||
? null
|
? null
|
||||||
@@ -134,12 +132,8 @@ public sealed partial class KanbanStore : ICardStore
|
|||||||
: new CardBudgetDto(entity.ConvFrom, entity.ConvTo, entity.ConvCur),
|
: new CardBudgetDto(entity.ConvFrom, entity.ConvTo, entity.ConvCur),
|
||||||
Contact = entity.Contact,
|
Contact = entity.Contact,
|
||||||
Contacts = ToJsonList<CardContactDto>(entity.ContactsJson),
|
Contacts = ToJsonList<CardContactDto>(entity.ContactsJson),
|
||||||
Channel = new CardChannelDto(entity.ChannelName, entity.ChannelHandle, entity.ChannelHue),
|
|
||||||
Time = HumanAge(entity.ReceivedAt),
|
Time = HumanAge(entity.ReceivedAt),
|
||||||
ReceivedAtMs = entity.ReceivedAt.ToUnixTimeMilliseconds(),
|
ReceivedAtMs = entity.ReceivedAt.ToUnixTimeMilliseconds(),
|
||||||
SourceMsg = entity.SourceMsg,
|
|
||||||
SourceDialogId = entity.SourceDialogId,
|
|
||||||
SourceMsgId = entity.SourceMsgId,
|
|
||||||
PrevCol = entity.PrevCol,
|
PrevCol = entity.PrevCol,
|
||||||
MatchHits = ToJsonList<MatchHitDto>(entity.MatchHitsJson),
|
MatchHits = ToJsonList<MatchHitDto>(entity.MatchHitsJson),
|
||||||
Comments = comments,
|
Comments = comments,
|
||||||
@@ -152,17 +146,22 @@ public sealed partial class KanbanStore : ICardStore
|
|||||||
UpdatedAtMs = entity.UpdatedAt.ToUnixTimeMilliseconds(),
|
UpdatedAtMs = entity.UpdatedAt.ToUnixTimeMilliseconds(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Вид источника карточки (производная проекция): local/telegram/other.
|
// Разбирает одиночный JSON-объект; пустая/битая строка — запасное значение.
|
||||||
// entity: Строка карточки.
|
private static T FromJsonOr<T>(string json, T fallback)
|
||||||
// Возвращает: Вид источника (строка wire-контракта).
|
|
||||||
private static string ResolveSourceKind(CardEntity entity)
|
|
||||||
{
|
{
|
||||||
if (entity.Local)
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
return "local";
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity.ChannelName.Length > 0 || entity.SourceDialogId.Length > 0 ? "telegram" : "other";
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(json, JsonOptions) ?? fallback;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Маппинг снимка (write-модель создания карточки) в строку Cards; CreatedAt — UTC-now.
|
// Маппинг снимка (write-модель создания карточки) в строку Cards; CreatedAt — UTC-now.
|
||||||
@@ -185,13 +184,13 @@ public sealed partial class KanbanStore : ICardStore
|
|||||||
ConvCur = snapshot.ConvCur,
|
ConvCur = snapshot.ConvCur,
|
||||||
Contact = snapshot.Contact,
|
Contact = snapshot.Contact,
|
||||||
ContactsJson = ToJson(snapshot.Contacts),
|
ContactsJson = ToJson(snapshot.Contacts),
|
||||||
ChannelName = snapshot.ChannelName,
|
|
||||||
ChannelHandle = snapshot.ChannelHandle,
|
|
||||||
ChannelHue = snapshot.ChannelHue,
|
|
||||||
ReceivedAt = snapshot.ReceivedAt,
|
ReceivedAt = snapshot.ReceivedAt,
|
||||||
SourceMsg = snapshot.SourceMsg,
|
SourceKind = snapshot.Source.Kind,
|
||||||
SourceDialogId = snapshot.SourceDialogId,
|
SourceExternalId = snapshot.Source.ExternalId ?? string.Empty,
|
||||||
SourceMsgId = snapshot.SourceMsgId,
|
SourceOriginRef = snapshot.Source.OriginRef ?? string.Empty,
|
||||||
|
SourceJson = ToJson(snapshot.Source),
|
||||||
|
ContentJson = ToJson(snapshot.Content),
|
||||||
|
SourceText = snapshot.Content.Text ?? string.Empty,
|
||||||
PrevCol = snapshot.PrevCol,
|
PrevCol = snapshot.PrevCol,
|
||||||
ArchivedAt = snapshot.ArchivedAt,
|
ArchivedAt = snapshot.ArchivedAt,
|
||||||
MatchHitsJson = ToJson(snapshot.MatchHits),
|
MatchHitsJson = ToJson(snapshot.MatchHits),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using Deal.Infrastructure.Persistence.Entities;
|
using Deal.Infrastructure.Persistence.Entities;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
@@ -11,17 +13,21 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||||
{
|
{
|
||||||
// ── Очередь (QueueItems) ───────────────────────────────────────────────
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Очередь (QueueItems) ───────────────────────────────────────
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<bool> ExistsDuplicateAsync(
|
public Task<bool> ExistsDuplicateAsync(
|
||||||
string dialogId,
|
SourceRef source,
|
||||||
long? msgId,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
return msgId is null
|
string key = source.DedupeKey();
|
||||||
? Task.FromResult(false)
|
return dbContext.QueueItems.AnyAsync(item => item.SourceKey == key, ct);
|
||||||
: dbContext.QueueItems.AnyAsync(item => item.DialogId == dialogId && item.MsgId == msgId, ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -89,16 +95,19 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
|||||||
|
|
||||||
string id = record.DeterministicId ?? PrefixId.New(PipelineIdPrefixes.Rejected);
|
string id = record.DeterministicId ?? PrefixId.New(PipelineIdPrefixes.Rejected);
|
||||||
DateTimeOffset rejectedAt = DateTimeOffset.UtcNow;
|
DateTimeOffset rejectedAt = DateTimeOffset.UtcNow;
|
||||||
|
string sourceKey = record.Source.DedupeKey();
|
||||||
|
string sourceJson = ToJson(record.Source);
|
||||||
|
string contentJson = ToJson(record.Content);
|
||||||
|
|
||||||
await dbContext.Database.ExecuteSqlInterpolatedAsync(
|
await dbContext.Database.ExecuteSqlInterpolatedAsync(
|
||||||
$"""
|
$"""
|
||||||
INSERT INTO "RejectedItems" ("Id", "DialogId", "MsgId", "ChannelName", "ChannelHandle", "ChannelHue", "Text", "Stage", "Reason", "Kw", "Source", "MsgAt", "RejectedAt", "Returned", "ReturnReason")
|
INSERT INTO "RejectedItems" ("Id", "SourceKey", "SourceJson", "ContentJson", "Text", "Stage", "Reason", "Kw", "Source", "MsgAt", "RejectedAt", "Returned", "ReturnReason")
|
||||||
VALUES ({id}, {record.DialogId}, {record.MsgId}, {record.ChannelName}, {record.ChannelHandle}, {record.ChannelHue}, {record.Text}, {record.Stage}, {record.Reason}, {record.Kw}, {record.Source}, {FromEpochMs(record.MsgAtMs)}, {rejectedAt}, false, {string.Empty})
|
VALUES ({id}, {sourceKey}, {sourceJson}, {contentJson}, {record.Text}, {record.Stage}, {record.Reason}, {record.Kw}, {record.DecidedBy}, {FromEpochMs(record.MsgAtMs)}, {rejectedAt}, false, {string.Empty})
|
||||||
ON CONFLICT ("Id") DO UPDATE SET
|
ON CONFLICT ("Id") DO UPDATE SET
|
||||||
|
"SourceKey" = excluded."SourceKey",
|
||||||
|
"SourceJson" = excluded."SourceJson",
|
||||||
|
"ContentJson" = excluded."ContentJson",
|
||||||
"Text" = excluded."Text",
|
"Text" = excluded."Text",
|
||||||
"ChannelName" = excluded."ChannelName",
|
|
||||||
"ChannelHandle" = excluded."ChannelHandle",
|
|
||||||
"ChannelHue" = excluded."ChannelHue",
|
|
||||||
"Stage" = excluded."Stage",
|
"Stage" = excluded."Stage",
|
||||||
"Reason" = excluded."Reason",
|
"Reason" = excluded."Reason",
|
||||||
"Kw" = excluded."Kw",
|
"Kw" = excluded."Kw",
|
||||||
@@ -156,8 +165,7 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(item => EF.Functions.Like(item.Text.ToLower(), pattern)
|
.Where(item => EF.Functions.Like(item.Text.ToLower(), pattern)
|
||||||
|| EF.Functions.Like(item.Reason.ToLower(), pattern)
|
|| EF.Functions.Like(item.Reason.ToLower(), pattern)
|
||||||
|| EF.Functions.Like(item.Kw.ToLower(), pattern)
|
|| EF.Functions.Like(item.Kw.ToLower(), pattern))
|
||||||
|| EF.Functions.Like(item.ChannelName.ToLower(), pattern))
|
|
||||||
.OrderByDescending(item => item.RejectedAt)
|
.OrderByDescending(item => item.RejectedAt)
|
||||||
.Take(limitLike)
|
.Take(limitLike)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
@@ -274,11 +282,10 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
|||||||
private static QueueItemDto ToQueueItemDto(QueueItemEntity entity) => new()
|
private static QueueItemDto ToQueueItemDto(QueueItemEntity entity) => new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
DialogId = entity.DialogId,
|
Source = FromJsonOr(entity.SourceJson, SourceRefs.Empty),
|
||||||
MsgId = entity.MsgId,
|
Content = FromJsonOr(entity.ContentJson, new SourceContent()),
|
||||||
Text = entity.Text,
|
Text = entity.Text,
|
||||||
Status = entity.Status,
|
Status = entity.Status,
|
||||||
Channel = new PipelineChannelDto(entity.ChannelName, entity.ChannelHandle, entity.ChannelHue),
|
|
||||||
MsgAtMs = entity.MsgAt.ToUnixTimeMilliseconds(),
|
MsgAtMs = entity.MsgAt.ToUnixTimeMilliseconds(),
|
||||||
QueuedAtMs = entity.CreatedAt.ToUnixTimeMilliseconds(),
|
QueuedAtMs = entity.CreatedAt.ToUnixTimeMilliseconds(),
|
||||||
Force = entity.Force,
|
Force = entity.Force,
|
||||||
@@ -291,13 +298,11 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
|||||||
return new QueueItemEntity
|
return new QueueItemEntity
|
||||||
{
|
{
|
||||||
Id = item.Id,
|
Id = item.Id,
|
||||||
DialogId = item.DialogId,
|
SourceKey = item.Source.DedupeKey(),
|
||||||
MsgId = item.MsgId,
|
SourceJson = ToJson(item.Source),
|
||||||
|
ContentJson = ToJson(item.Content),
|
||||||
Text = item.Text,
|
Text = item.Text,
|
||||||
Status = item.Status,
|
Status = item.Status,
|
||||||
ChannelName = item.Channel.Name,
|
|
||||||
ChannelHandle = item.Channel.Handle,
|
|
||||||
ChannelHue = item.Channel.Hue,
|
|
||||||
MsgAt = FromEpochMs(item.MsgAtMs),
|
MsgAt = FromEpochMs(item.MsgAtMs),
|
||||||
CreatedAt = queuedAt,
|
CreatedAt = queuedAt,
|
||||||
UpdatedAt = queuedAt,
|
UpdatedAt = queuedAt,
|
||||||
@@ -308,16 +313,15 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
|||||||
private static RejectedItemDto ToRejectedItemDto(RejectedItemEntity entity) => new()
|
private static RejectedItemDto ToRejectedItemDto(RejectedItemEntity entity) => new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
DialogId = entity.DialogId,
|
Source = FromJsonOr(entity.SourceJson, SourceRefs.Empty),
|
||||||
MsgId = entity.MsgId,
|
Content = FromJsonOr(entity.ContentJson, new SourceContent()),
|
||||||
Text = entity.Text,
|
Text = entity.Text,
|
||||||
Stage = entity.Stage,
|
Stage = entity.Stage,
|
||||||
StageLabel = PipelineRejectConstants.StageLabel(entity.Stage),
|
StageLabel = PipelineRejectConstants.StageLabel(entity.Stage),
|
||||||
Reason = entity.Reason,
|
Reason = entity.Reason,
|
||||||
Kw = entity.Kw,
|
Kw = entity.Kw,
|
||||||
Source = entity.Source,
|
DecidedBy = entity.Source,
|
||||||
SourceLabel = PipelineRejectConstants.SourceLabel(entity.Source),
|
DecidedByLabel = PipelineRejectConstants.SourceLabel(entity.Source),
|
||||||
Channel = new PipelineChannelDto(entity.ChannelName, entity.ChannelHandle, entity.ChannelHue),
|
|
||||||
MsgAtMs = entity.MsgAt.ToUnixTimeMilliseconds(),
|
MsgAtMs = entity.MsgAt.ToUnixTimeMilliseconds(),
|
||||||
RejectedAtMs = entity.RejectedAt.ToUnixTimeMilliseconds(),
|
RejectedAtMs = entity.RejectedAt.ToUnixTimeMilliseconds(),
|
||||||
Returned = entity.Returned,
|
Returned = entity.Returned,
|
||||||
@@ -327,4 +331,25 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
|||||||
|
|
||||||
// epoch-ms → DateTimeOffset (UTC-момент для timestamptz-колонки).
|
// epoch-ms → DateTimeOffset (UTC-момент для timestamptz-колонки).
|
||||||
private static DateTimeOffset FromEpochMs(long epochMs) => DateTimeOffset.FromUnixTimeMilliseconds(epochMs);
|
private static DateTimeOffset FromEpochMs(long epochMs) => DateTimeOffset.FromUnixTimeMilliseconds(epochMs);
|
||||||
|
|
||||||
|
// Сериализует значение в JSON (camelCase, конвенция value_json).
|
||||||
|
private static string ToJson<T>(T value) => JsonSerializer.Serialize(value, JsonOptions);
|
||||||
|
|
||||||
|
// Разбирает одиночный JSON-объект; пустая/битая строка — запасное значение.
|
||||||
|
private static T FromJsonOr<T>(string json, T fallback)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(json, JsonOptions) ?? fallback;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
namespace Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Общие значения и производные поля ссылки на источник.
|
||||||
|
/// </summary>
|
||||||
|
public static class SourceRefs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Цвет источника по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public const string DefaultHue = "#666";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ прочих метаданных с цветом источника.
|
||||||
|
/// </summary>
|
||||||
|
public const string HueKey = "hue";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пустая ссылка на источник.
|
||||||
|
/// </summary>
|
||||||
|
public static SourceRef Empty { get; } = new() { Kind = string.Empty };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ уникальности записи в источнике: вид + оригинал + внешний id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Ссылка на источник.</param>
|
||||||
|
/// <returns>Стабильный ключ для дедупликации.</returns>
|
||||||
|
public static string DedupeKey(this SourceRef source) =>
|
||||||
|
string.Concat(source.Kind, "|", source.OriginRef ?? string.Empty, "|", source.ExternalId ?? string.Empty);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Цвет источника для интерфейса.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Ссылка на источник.</param>
|
||||||
|
/// <returns>Цвет из метаданных либо цвет по умолчанию.</returns>
|
||||||
|
public static string ResolveHue(this SourceRef source)
|
||||||
|
{
|
||||||
|
if (source.Extra is not null
|
||||||
|
&& source.Extra.TryGetValue(HueKey, out string? hue)
|
||||||
|
&& !string.IsNullOrWhiteSpace(hue))
|
||||||
|
{
|
||||||
|
return hue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultHue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
|
|
||||||
namespace Deal.Modules.Kanban.Application.Abstractions;
|
namespace Deal.Modules.Kanban.Application.Abstractions;
|
||||||
@@ -89,12 +90,10 @@ public interface ICardStore
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Карточка по исходному сообщению
|
/// Карточка по исходному сообщению
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dialogId">Id диалога-источника сообщения.</param>
|
/// <param name="source">Ссылка на источник: вид + оригинал + внешний id.</param>
|
||||||
/// <param name="msgId">Id исходного сообщения в Telegram.</param>
|
|
||||||
/// <returns>Карточка или null, если сообщение не становилось карточкой.</returns>
|
/// <returns>Карточка или null, если сообщение не становилось карточкой.</returns>
|
||||||
public Task<CardDto?> GetCardBySourceAsync(
|
public Task<CardDto?> GetCardBySourceAsync(
|
||||||
string dialogId,
|
SourceRef source,
|
||||||
long msgId,
|
|
||||||
CancellationToken ct);
|
CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -367,6 +366,6 @@ public interface ICardStore
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Карточки «Неразобранного» с исходным текстом — вход эвристики suggest.
|
/// Карточки «Неразобранного» с исходным текстом — вход эвристики suggest.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Карточки col='inbox' с непустым SourceMsg (частотные темы считаются по source_msg).</returns>
|
/// <returns>Карточки col='inbox' с непустым текстом источника.</returns>
|
||||||
public Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct);
|
public Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Deal.Modules.Kanban.Application.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Канал-источник карточки — объект <c>ch</c>.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record CardChannelDto(
|
|
||||||
string Name,
|
|
||||||
string Handle,
|
|
||||||
string Hue);
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
namespace Deal.Modules.Kanban.Application.Models;
|
namespace Deal.Modules.Kanban.Application.Models;
|
||||||
|
|
||||||
@@ -53,9 +54,14 @@ public sealed record CardDto
|
|||||||
public string Summary { get; init; } = string.Empty;
|
public string Summary { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Источник (производная проекция)
|
/// Источник записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CardSourceDto Source { get; init; } = new(string.Empty, string.Empty, string.Empty, 0);
|
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Содержимое записи источника.
|
||||||
|
/// </summary>
|
||||||
|
public SourceContent Content { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Стек/направления
|
/// Стек/направления
|
||||||
@@ -82,11 +88,6 @@ public sealed record CardDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<CardContactDto> Contacts { get; init; } = Array.Empty<CardContactDto>();
|
public IReadOnlyList<CardContactDto> Contacts { get; init; } = Array.Empty<CardContactDto>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Канал-источник
|
|
||||||
/// </summary>
|
|
||||||
public CardChannelDto Channel { get; init; } = new(string.Empty, string.Empty, string.Empty);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Человеческая метка возраста
|
/// Человеческая метка возраста
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -98,21 +99,6 @@ public sealed record CardDto
|
|||||||
[property: JsonPropertyName("receivedAt")]
|
[property: JsonPropertyName("receivedAt")]
|
||||||
public long ReceivedAtMs { get; init; }
|
public long ReceivedAtMs { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Исходное сообщение
|
|
||||||
/// </summary>
|
|
||||||
public string SourceMsg { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Id диалога исходного сообщения
|
|
||||||
/// </summary>
|
|
||||||
public string SourceDialogId { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Id исходного сообщения в Telegram, либо null.
|
|
||||||
/// </summary>
|
|
||||||
public long? SourceMsgId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Предыдущая колонка
|
/// Предыдущая колонка
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
namespace Deal.Modules.Kanban.Application.Models;
|
namespace Deal.Modules.Kanban.Application.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -91,40 +93,20 @@ public sealed record CardSnapshot
|
|||||||
public IReadOnlyList<CardContactDto> Contacts { get; init; } = Array.Empty<CardContactDto>();
|
public IReadOnlyList<CardContactDto> Contacts { get; init; } = Array.Empty<CardContactDto>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Имя канала/диалога-источника.
|
/// Источник записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ChannelName { get; init; } = string.Empty;
|
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle канала-источника.
|
/// Содержимое записи источника.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ChannelHandle { get; init; } = string.Empty;
|
public SourceContent Content { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Цвет канала-источника
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHue { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время получения исходного сообщения
|
/// Время получения исходного сообщения
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset ReceivedAt { get; init; }
|
public DateTimeOffset ReceivedAt { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Исходное сообщение
|
|
||||||
/// </summary>
|
|
||||||
public string SourceMsg { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Id диалога исходного сообщения.
|
|
||||||
/// </summary>
|
|
||||||
public string SourceDialogId { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Id исходного сообщения в Telegram, либо null.
|
|
||||||
/// </summary>
|
|
||||||
public long? SourceMsgId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Предыдущая колонка
|
/// Предыдущая колонка
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace Deal.Modules.Kanban.Application.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Источник карточки на wire — объект <c>source</c> карточки.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Kind">Вид источника: local/telegram/web/file/row/api/ai/composite/other.</param>
|
|
||||||
/// <param name="DisplayName">Имя источника (канал/диалог).</param>
|
|
||||||
/// <param name="OriginRef">Ссылка на первоисточник (id диалога/файла).</param>
|
|
||||||
/// <param name="ReceivedAt">Момент получения, epoch-ms.</param>
|
|
||||||
public sealed record CardSourceDto(
|
|
||||||
string Kind,
|
|
||||||
string DisplayName,
|
|
||||||
string OriginRef,
|
|
||||||
long ReceivedAt);
|
|
||||||
@@ -17,7 +17,7 @@ public sealed partial class CardsService
|
|||||||
|
|
||||||
private static string LearningText(CardDto card)
|
private static string LearningText(CardDto card)
|
||||||
{
|
{
|
||||||
string source = card.SourceMsg.Trim();
|
string source = (card.Content.Text ?? string.Empty).Trim();
|
||||||
return source.Length > 0 ? source : card.Title;
|
return source.Length > 0 ? source : card.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Abstractions;
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ public sealed partial class CardsService
|
|||||||
public async Task<CardDto> CreateLocalCardAsync(CardLocalCreateDto draft, CancellationToken ct)
|
public async Task<CardDto> CreateLocalCardAsync(CardLocalCreateDto draft, CancellationToken ct)
|
||||||
{
|
{
|
||||||
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
DateTimeOffset receivedAt = DateTimeOffset.FromUnixTimeMilliseconds(nowMs);
|
||||||
string containerId = draft.ContainerId is not null && CardsDefaultContainers.Contains(draft.ContainerId)
|
string containerId = draft.ContainerId is not null && CardsDefaultContainers.Contains(draft.ContainerId)
|
||||||
? draft.ContainerId
|
? draft.ContainerId
|
||||||
: PlannedStage;
|
: PlannedStage;
|
||||||
@@ -79,6 +81,9 @@ public sealed partial class CardsService
|
|||||||
Local = true,
|
Local = true,
|
||||||
Title = draft.Title.Trim(),
|
Title = draft.Title.Trim(),
|
||||||
Summary = draft.Summary,
|
Summary = draft.Summary,
|
||||||
|
Source = new SourceRef { Kind = SourceKinds.Local, ReceivedAt = receivedAt },
|
||||||
|
Content = new SourceContent { Text = draft.Summary },
|
||||||
|
ReceivedAt = receivedAt,
|
||||||
Stack = draft.Stack ?? Array.Empty<string>(),
|
Stack = draft.Stack ?? Array.Empty<string>(),
|
||||||
BudgetFrom = draft.Budget?.From,
|
BudgetFrom = draft.Budget?.From,
|
||||||
BudgetTo = draft.Budget?.To,
|
BudgetTo = draft.Budget?.To,
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ public static class SuggestHeuristics
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Планы колонок-предложений по карточкам «Неразобранного».
|
/// Планы колонок-предложений по карточкам «Неразобранного».
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inbox">Карточки «Неразобранного» с непустым source_msg (ICardStore.ListInboxWithSourceAsync).</param>
|
/// <param name="inbox">Карточки «Неразобранного» с непустым текстом источника (ICardStore.ListInboxWithSourceAsync).</param>
|
||||||
/// <param name="existingBoardNames">Имена существующих (suggested=false) досок — похожие темы не предлагаются.</param>
|
/// <param name="existingBoardNames">Имена существующих (suggested=false) досок — похожие темы не предлагаются.</param>
|
||||||
/// <returns>Планы колонок (≤4, каждая ≥2 карточки); пусто — мало карточек/нечего сгруппировать.</returns>
|
/// <returns>Планы колонок (≤4, каждая ≥2 карточки); пусто — мало карточек/нечего сгруппировать.</returns>
|
||||||
public static IReadOnlyList<SuggestedColumnPlan> PlanColumns(IReadOnlyList<CardDto> inbox, IReadOnlyList<string> existingBoardNames)
|
public static IReadOnlyList<SuggestedColumnPlan> PlanColumns(IReadOnlyList<CardDto> inbox, IReadOnlyList<string> existingBoardNames)
|
||||||
{
|
{
|
||||||
List<CardDto> window = inbox
|
List<CardDto> window = inbox
|
||||||
.Where(card => card.SourceMsg.Trim().Length > 0)
|
.Where(card => (card.Content.Text ?? string.Empty).Trim().Length > 0)
|
||||||
.OrderByDescending(card => card.ReceivedAtMs)
|
.OrderByDescending(card => card.ReceivedAtMs)
|
||||||
.ThenBy(card => card.Id, StringComparer.Ordinal)
|
.ThenBy(card => card.Id, StringComparer.Ordinal)
|
||||||
.Take(MaxText)
|
.Take(MaxText)
|
||||||
@@ -111,7 +111,7 @@ public static class SuggestHeuristics
|
|||||||
foreach (CardDto card in window)
|
foreach (CardDto card in window)
|
||||||
{
|
{
|
||||||
// Distinct: у карточки слово учитывается один раз, сколько бы раз ни встречалось в тексте.
|
// Distinct: у карточки слово учитывается один раз, сколько бы раз ни встречалось в тексте.
|
||||||
foreach (string term in Tokenize(card.SourceMsg).Distinct(StringComparer.Ordinal))
|
foreach (string term in Tokenize(card.Content.Text ?? string.Empty).Distinct(StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
if (!termCards.TryGetValue(term, out List<string>? cardIds))
|
if (!termCards.TryGetValue(term, out List<string>? cardIds))
|
||||||
{
|
{
|
||||||
@@ -166,7 +166,7 @@ public static class SuggestHeuristics
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Частотные слова-маркеры по текстам карточек.
|
/// Частотные слова-маркеры по текстам карточек.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="texts">Тексты source_msg карточек выборки (непустые, свежие ≤40).</param>
|
/// <param name="texts">Тексты источника карточек выборки (непустые, свежие ≤40).</param>
|
||||||
/// <returns>Слова-маркеры (≤60); пусто — нет слов, встречающихся в ≥2 текстах.</returns>
|
/// <returns>Слова-маркеры (≤60); пусто — нет слов, встречающихся в ≥2 текстах.</returns>
|
||||||
public static IReadOnlyList<string> SuggestDomainKeywords(IReadOnlyList<string> texts)
|
public static IReadOnlyList<string> SuggestDomainKeywords(IReadOnlyList<string> texts)
|
||||||
{
|
{
|
||||||
@@ -191,7 +191,7 @@ public static class SuggestHeuristics
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Слова текста: подряд букв (латиница/кириллица), нижний регистр, длина 3..40, минус стоп-слова.
|
// Слова текста: подряд букв (латиница/кириллица), нижний регистр, длина 3..40, минус стоп-слова.
|
||||||
// text: Исходный текст (source_msg карточки).
|
// text: Исходный текст (текст источника карточки).
|
||||||
// Возвращает: Уникальные слова текста в порядке появления (Ordinal).
|
// Возвращает: Уникальные слова текста в порядке появления (Ordinal).
|
||||||
private static List<string> Tokenize(string text)
|
private static List<string> Tokenize(string text)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
|
|
||||||
namespace Deal.Modules.Pipeline.Application.Abstractions;
|
namespace Deal.Modules.Pipeline.Application.Abstractions;
|
||||||
@@ -10,14 +11,12 @@ public interface IPipelineStore
|
|||||||
// ── Очередь (QueueItems) ───────────────────────────────────────────────
|
// ── Очередь (QueueItems) ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Есть ли строка очереди с тем же сообщением диалога — дубль-гвард приёма.
|
/// Есть ли строка очереди с той же записью источника — дубль-гвард приёма.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dialogId">Id диалога-источника.</param>
|
/// <param name="source">Ссылка на источник: вид + оригинал + внешний id.</param>
|
||||||
/// <param name="msgId">Id исходного сообщения; null — проверка не выполняется (вернёт false).</param>
|
/// <returns>true — строка с такой записью уже в очереди.</returns>
|
||||||
/// <returns>true — строка с DialogId+MsgId уже в очереди (Telethon-дубль не пишем).</returns>
|
|
||||||
public Task<bool> ExistsDuplicateAsync(
|
public Task<bool> ExistsDuplicateAsync(
|
||||||
string dialogId,
|
SourceRef source,
|
||||||
long? msgId,
|
|
||||||
CancellationToken ct);
|
CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
namespace Deal.Modules.Pipeline.Application.Models;
|
namespace Deal.Modules.Pipeline.Application.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сообщение-кандидат для ручной проверки ML.
|
/// Запись-кандидат для ручной проверки ML.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record MlCandidateDto
|
public sealed record MlCandidateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id исходного сообщения в Telegram
|
/// Источник записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long Id { get; init; }
|
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога-источника сообщения.
|
/// Содержимое записи источника.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DialogId { get; init; } = string.Empty;
|
public SourceContent Content { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текст сообщения.
|
/// Текст записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Text { get; init; } = string.Empty;
|
public string Text { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Deal.Modules.Pipeline.Application.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Канал-источник сообщения пайплайна — объект <c>ch</c> строк очереди/отсева.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Name">Имя канала/диалога.</param>
|
|
||||||
/// <param name="Handle">Handle канала/диалога (t.me/<handle>).</param>
|
|
||||||
/// <param name="Hue">Цвет канала.</param>
|
|
||||||
public sealed record PipelineChannelDto(string Name, string Handle, string Hue);
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
namespace Deal.Modules.Pipeline.Application.Models;
|
namespace Deal.Modules.Pipeline.Application.Models;
|
||||||
|
|
||||||
@@ -13,14 +14,14 @@ public sealed record QueueItemDto
|
|||||||
public string Id { get; init; } = string.Empty;
|
public string Id { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога-источника сообщения.
|
/// Источник записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DialogId { get; init; } = string.Empty;
|
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id исходного сообщения в Telegram
|
/// Содержимое записи источника.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long? MsgId { get; init; }
|
public SourceContent Content { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текст сообщения.
|
/// Текст сообщения.
|
||||||
@@ -33,25 +34,19 @@ public sealed record QueueItemDto
|
|||||||
public string Status { get; init; } = string.Empty;
|
public string Status { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Канал-источник
|
/// Время исходного сообщения, epoch-ms.
|
||||||
/// </summary>
|
|
||||||
[property: JsonPropertyName("ch")]
|
|
||||||
public PipelineChannelDto Channel { get; init; } = new(string.Empty, string.Empty, string.Empty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Время исходного сообщения, epoch-ms
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[property: JsonPropertyName("msgAt")]
|
[property: JsonPropertyName("msgAt")]
|
||||||
public long MsgAtMs { get; init; }
|
public long MsgAtMs { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время постановки в очередь
|
/// Время постановки в очередь, epoch-ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[property: JsonPropertyName("queuedAt")]
|
[property: JsonPropertyName("queuedAt")]
|
||||||
public long QueuedAtMs { get; init; }
|
public long QueuedAtMs { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Внутренний флаг «возвращено пользователем из отсева»
|
/// Внутренний флаг «возвращено пользователем из отсева».
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool Force { get; init; }
|
public bool Force { get; init; }
|
||||||
|
|||||||
@@ -1,47 +1,19 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
namespace Deal.Modules.Pipeline.Application.Models;
|
namespace Deal.Modules.Pipeline.Application.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Команда приёма входящего сообщения — аргумент PipelineIngestService.EnqueueAsync.
|
/// Команда приёма входящей записи — аргумент PipelineIngestService.EnqueueAsync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record QueuedMessage
|
public sealed record QueuedMessage
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога-источника сообщения
|
/// Запись источника: источник и содержимое.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DialogId { get; init; } = string.Empty;
|
public required SourceItem Item { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Имя канала/диалога-источника.
|
/// Флаг «возвращено из отсева».
|
||||||
/// </summary>
|
|
||||||
public string ChannelName { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle канала/диалога-источника.
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHandle { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Цвет канала-источника
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHue { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Id исходного сообщения в Telegram
|
|
||||||
/// </summary>
|
|
||||||
public long? MsgId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Текст сообщения
|
|
||||||
/// </summary>
|
|
||||||
public string Text { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Время исходного сообщения, epoch-ms; null — приём подставит now.
|
|
||||||
/// </summary>
|
|
||||||
public long? MsgAtMs { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Флаг «возвращено из отсева»
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Force { get; init; }
|
public bool Force { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
namespace Deal.Modules.Pipeline.Application.Models;
|
namespace Deal.Modules.Pipeline.Application.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -6,44 +8,29 @@ namespace Deal.Modules.Pipeline.Application.Models;
|
|||||||
public sealed record RejectRecord
|
public sealed record RejectRecord
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога-источника сообщения
|
/// Источник записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DialogId { get; init; } = string.Empty;
|
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id исходного сообщения в Telegram, либо null.
|
/// Содержимое записи источника.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long? MsgId { get; init; }
|
public SourceContent Content { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текст отсеянного сообщения
|
/// Текст отсеянного сообщения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Text { get; init; } = string.Empty;
|
public string Text { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Имя канала/диалога-источника.
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelName { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle канала/диалога-источника.
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHandle { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Цвет канала-источника
|
|
||||||
/// </summary>
|
|
||||||
public string ChannelHue { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время исходного сообщения, epoch-ms.
|
/// Время исходного сообщения, epoch-ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long MsgAtMs { get; init; }
|
public long MsgAtMs { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Источник решения
|
/// Кто вынес решение: stop|ml|ai|stale|dup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Source { get; init; } = string.Empty;
|
public string DecidedBy { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup.
|
/// Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup.
|
||||||
@@ -56,13 +43,15 @@ public sealed record RejectRecord
|
|||||||
public string Reason { get; init; } = string.Empty;
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Конкретное слово/фраза стоп-списка, сработавшая правилом
|
/// Конкретное слово/фраза стоп-списка, сработавшая правилом.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Kw { get; init; } = string.Empty;
|
public string Kw { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Детерминированный id записи
|
/// Детерминированный id записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DeterministicId =>
|
public string? DeterministicId =>
|
||||||
DialogId.Length > 0 && MsgId is not null ? $"r_{DialogId}_{MsgId}" : null;
|
Source.ExternalId is { Length: > 0 } externalId
|
||||||
|
? $"r_{Source.Kind}_{Source.OriginRef}_{externalId}"
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
namespace Deal.Modules.Pipeline.Application.Models;
|
namespace Deal.Modules.Pipeline.Application.Models;
|
||||||
|
|
||||||
@@ -13,14 +14,14 @@ public sealed record RejectedItemDto
|
|||||||
public string Id { get; init; } = string.Empty;
|
public string Id { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id диалога-источника сообщения.
|
/// Источник записи.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DialogId { get; init; } = string.Empty;
|
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id исходного сообщения в Telegram, либо null.
|
/// Содержимое записи источника.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long? MsgId { get; init; }
|
public SourceContent Content { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текст отсеянного сообщения.
|
/// Текст отсеянного сообщения.
|
||||||
@@ -28,12 +29,12 @@ public sealed record RejectedItemDto
|
|||||||
public string Text { get; init; } = string.Empty;
|
public string Text { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Этап отсева (см. словарь подписей <see cref="Deal.Modules.Pipeline.Application.Models.PipelineRejectConstants.StageLabels"/>).
|
/// Этап отсева.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Stage { get; init; } = string.Empty;
|
public string Stage { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Человекочитаемая подпись для UI
|
/// Человекочитаемая подпись для UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string StageLabel { get; init; } = string.Empty;
|
public string StageLabel { get; init; } = string.Empty;
|
||||||
|
|
||||||
@@ -43,51 +44,45 @@ public sealed record RejectedItemDto
|
|||||||
public string Reason { get; init; } = string.Empty;
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Конкретное слово/фраза стоп-списка, сработавшая правилом
|
/// Конкретное слово/фраза стоп-списка, сработавшая правилом.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Kw { get; init; } = string.Empty;
|
public string Kw { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Источник решения
|
/// Кто вынес решение: stop|ml|ai|stale|dup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Source { get; init; } = string.Empty;
|
public string DecidedBy { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Подпись источника
|
/// Подпись источника решения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SourceLabel { get; init; } = string.Empty;
|
public string DecidedByLabel { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Канал-источник
|
/// Время исходного сообщения, epoch-ms.
|
||||||
/// </summary>
|
|
||||||
[property: JsonPropertyName("ch")]
|
|
||||||
public PipelineChannelDto Channel { get; init; } = new(string.Empty, string.Empty, string.Empty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Время исходного сообщения, epoch-ms
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[property: JsonPropertyName("msgAt")]
|
[property: JsonPropertyName("msgAt")]
|
||||||
public long MsgAtMs { get; init; }
|
public long MsgAtMs { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время отсева, epoch-ms
|
/// Время отсева, epoch-ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[property: JsonPropertyName("rejectedAt")]
|
[property: JsonPropertyName("rejectedAt")]
|
||||||
public long RejectedAtMs { get; init; }
|
public long RejectedAtMs { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Флаг «возвращено в обработку»
|
/// Флаг «возвращено в обработку».
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Returned { get; init; }
|
public bool Returned { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время возврата, epoch-ms
|
/// Время возврата, epoch-ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[property: JsonPropertyName("returnedAt")]
|
[property: JsonPropertyName("returnedAt")]
|
||||||
public long? ReturnedAtMs { get; init; }
|
public long? ReturnedAtMs { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Причина возврата, указанная пользователем
|
/// Причина возврата, указанная пользователем.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ReturnReason { get; init; } = string.Empty;
|
public string ReturnReason { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ public static class ContactsQualifier
|
|||||||
|
|
||||||
private const int MaxContactLength = 300;
|
private const int MaxContactLength = 300;
|
||||||
|
|
||||||
private const int MinTgNameLength = 4;
|
private const int MinHandleLength = 4;
|
||||||
|
|
||||||
private const int MaxTgNameLength = 32;
|
private const int MaxHandleLength = 32;
|
||||||
|
|
||||||
internal const int MaxNormalizedPhoneLength = 18;
|
internal const int MaxNormalizedPhoneLength = 18;
|
||||||
|
|
||||||
private const string TgBotSuffix = "bot";
|
private const string BotSuffix = "bot";
|
||||||
|
|
||||||
private static readonly IReadOnlySet<string> TgServiceNames = new HashSet<string>(StringComparer.Ordinal)
|
private static readonly IReadOnlySet<string> ReservedProfileNames = new HashSet<string>(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
"joinchat", "share", "s", "c", "addstickers", "addtheme", "proxy", "bg", "login",
|
"joinchat", "share", "s", "c", "addstickers", "addtheme", "proxy", "bg", "login",
|
||||||
};
|
};
|
||||||
@@ -35,9 +35,9 @@ public static class ContactsQualifier
|
|||||||
"teletype.in", "forms.gle", "docs.google.com", "youtube.com", "youtu.be", "clck.ru",
|
"teletype.in", "forms.gle", "docs.google.com", "youtube.com", "youtu.be", "clck.ru",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Regex TelegramNameRe = new(@"\A[A-Za-z0-9_]{4,32}\z", RegexOptions.CultureInvariant);
|
private static readonly Regex ProfileHandleRe = new(@"\A[A-Za-z0-9_]{4,32}\z", RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
private static readonly Regex TelegramLinkRe = new(
|
private static readonly Regex ProfileLinkRe = new(
|
||||||
@"\Ahttps?://(?:www\.)?t\.me/([A-Za-z0-9_]{4,32})/?\z",
|
@"\Ahttps?://(?:www\.)?t\.me/([A-Za-z0-9_]{4,32})/?\z",
|
||||||
RegexOptions.CultureInvariant);
|
RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ public static class ContactsQualifier
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Классифицирует один сырой контакт → {type, value} или null.
|
/// Классифицирует один сырой контакт → {type, value} или null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="raw">Сырое значение контакта («@user», «https://t.me/x», телефон, email, ссылка).</param>
|
/// <param name="raw">Сырое значение контакта («@user», ссылка на профиль, телефон, email, ссылка).</param>
|
||||||
/// <returns>Квалифицированный контакт <see cref="CardContactDto"/> либо null — пусто/мусор/бот/сервисная ссылка/ «постовый» сайт.</returns>
|
/// <returns>Квалифицированный контакт <see cref="CardContactDto"/> либо null — пусто/мусор/бот/сервисная ссылка/ «постовый» сайт.</returns>
|
||||||
public static CardContactDto? Qualify(string? raw)
|
public static CardContactDto? Qualify(string? raw)
|
||||||
{
|
{
|
||||||
@@ -91,7 +91,7 @@ public static class ContactsQualifier
|
|||||||
if (s.StartsWith('@'))
|
if (s.StartsWith('@'))
|
||||||
{
|
{
|
||||||
string name = s[1..].Trim();
|
string name = s[1..].Trim();
|
||||||
if (TelegramNameRe.IsMatch(name) && !name.EndsWith(TgBotSuffix, StringComparison.OrdinalIgnoreCase))
|
if (ProfileHandleRe.IsMatch(name) && !name.EndsWith(BotSuffix, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return new CardContactDto("tg", "@" + name);
|
return new CardContactDto("tg", "@" + name);
|
||||||
}
|
}
|
||||||
@@ -99,12 +99,12 @@ public static class ContactsQualifier
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Match link = TelegramLinkRe.Match(s);
|
Match link = ProfileLinkRe.Match(s);
|
||||||
if (link.Success)
|
if (link.Success)
|
||||||
{
|
{
|
||||||
string name = link.Groups[1].Value;
|
string name = link.Groups[1].Value;
|
||||||
if (!TgServiceNames.Contains(name.ToLowerInvariant())
|
if (!ReservedProfileNames.Contains(name.ToLowerInvariant())
|
||||||
&& !name.EndsWith(TgBotSuffix, StringComparison.OrdinalIgnoreCase))
|
&& !name.EndsWith(BotSuffix, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return new CardContactDto("tg", "@" + name);
|
return new CardContactDto("tg", "@" + name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Abstractions;
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
using Deal.Modules.Kanban.Application.ColumnRules;
|
using Deal.Modules.Kanban.Application.ColumnRules;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
@@ -92,10 +93,11 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings)
|
|||||||
IReadOnlyList<CardContactDto> contacts = ContactsQualifier.Build(
|
IReadOnlyList<CardContactDto> contacts = ContactsQualifier.Build(
|
||||||
parsed.Contacts.Select(contact => contact.Value).ToList(), message.Text);
|
parsed.Contacts.Select(contact => contact.Value).ToList(), message.Text);
|
||||||
string contact = MessageTextCleaner.SliceCodePoints(ContactsQualifier.Primary(contacts), MaxPrimaryContactCodePoints);
|
string contact = MessageTextCleaner.SliceCodePoints(ContactsQualifier.Primary(contacts), MaxPrimaryContactCodePoints);
|
||||||
string hue = string.IsNullOrWhiteSpace(message.Channel.Hue) ? SourceDefaults.DefaultHue : message.Channel.Hue;
|
|
||||||
DateTimeOffset receivedAt = message.MsgAtMs != 0
|
DateTimeOffset receivedAt = message.MsgAtMs != 0
|
||||||
? DateTimeOffset.FromUnixTimeMilliseconds(message.MsgAtMs)
|
? DateTimeOffset.FromUnixTimeMilliseconds(message.MsgAtMs)
|
||||||
: DateTimeOffset.UtcNow; // python row.get("msg_at") or now (L501)
|
: message.Source.ReceivedAt != default
|
||||||
|
? message.Source.ReceivedAt
|
||||||
|
: DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
return new CardSnapshot
|
return new CardSnapshot
|
||||||
{
|
{
|
||||||
@@ -115,13 +117,9 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings)
|
|||||||
ConvCur = converted?.Cur ?? string.Empty,
|
ConvCur = converted?.Cur ?? string.Empty,
|
||||||
Contact = contact,
|
Contact = contact,
|
||||||
Contacts = contacts,
|
Contacts = contacts,
|
||||||
ChannelName = message.Channel.Name,
|
Source = message.Source,
|
||||||
ChannelHandle = message.Channel.Handle,
|
Content = message.Content with { Text = MessageTextCleaner.SliceCodePoints(message.Text, MaxSourceMsgCodePoints) },
|
||||||
ChannelHue = hue,
|
|
||||||
ReceivedAt = receivedAt,
|
ReceivedAt = receivedAt,
|
||||||
SourceMsg = MessageTextCleaner.SliceCodePoints(message.Text, MaxSourceMsgCodePoints),
|
|
||||||
SourceDialogId = message.DialogId,
|
|
||||||
SourceMsgId = message.MsgId,
|
|
||||||
PrevCol = CardIds.Inbox,
|
PrevCol = CardIds.Inbox,
|
||||||
ArchivedAt = null,
|
ArchivedAt = null,
|
||||||
MatchHits = matchHits,
|
MatchHits = matchHits,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public sealed class CardReclassifier(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Pass pass = await CreatePassAsync(ct);
|
Pass pass = await CreatePassAsync(ct);
|
||||||
if (string.IsNullOrWhiteSpace(card.SourceMsg))
|
if (string.IsNullOrWhiteSpace(card.Content.Text))
|
||||||
{
|
{
|
||||||
pass.Skipped++;
|
pass.Skipped++;
|
||||||
return Build(pass, attempted: 1, started: false, reason: NoSourceTextReason);
|
return Build(pass, attempted: 1, started: false, reason: NoSourceTextReason);
|
||||||
@@ -121,7 +121,7 @@ public sealed class CardReclassifier(
|
|||||||
Pass pass,
|
Pass pass,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
string text = card.SourceMsg;
|
string text = card.Content.Text ?? string.Empty;
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
{
|
{
|
||||||
pass.Skipped++;
|
pass.Skipped++;
|
||||||
@@ -225,16 +225,15 @@ public sealed class CardReclassifier(
|
|||||||
|
|
||||||
// ── Сборка входа/выхода ────────────────────────────────────────────────
|
// ── Сборка входа/выхода ────────────────────────────────────────────────
|
||||||
|
|
||||||
// Собирает строку-сообщение для CardComposer из полей карточки (канал/время/исходный текст).
|
// Собирает строку-сообщение для CardComposer из полей карточки (источник/время/исходный текст).
|
||||||
// card: Карточка-источник (алиасы канала/получателя/даты).
|
// card: Карточка-источник (источник и содержимое сообщения).
|
||||||
// text: Исходный текст (source_msg).
|
// text: Исходный текст записи источника.
|
||||||
// Возвращает: Строка очереди, эквивалентная исходному сообщению карточки.
|
// Возвращает: Строка очереди, эквивалентная исходному сообщению карточки.
|
||||||
private static QueueItemDto BuildMessage(CardDto card, string text) => new()
|
private static QueueItemDto BuildMessage(CardDto card, string text) => new()
|
||||||
{
|
{
|
||||||
DialogId = card.SourceDialogId,
|
Source = card.Source,
|
||||||
MsgId = card.SourceMsgId,
|
Content = card.Content with { Text = text },
|
||||||
Text = text,
|
Text = text,
|
||||||
Channel = new PipelineChannelDto(card.Channel.Name, card.Channel.Handle, card.Channel.Hue),
|
|
||||||
MsgAtMs = card.ReceivedAtMs,
|
MsgAtMs = card.ReceivedAtMs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
using System.Globalization;
|
||||||
using Deal.Contracts.Integrations.Abstractions;
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Abstractions;
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
@@ -10,7 +12,7 @@ using Deal.Modules.Pipeline.Application.Models;
|
|||||||
namespace Deal.Modules.Pipeline.Application.Services;
|
namespace Deal.Modules.Pipeline.Application.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ручная проверка/разметка ML на сообщениях канала.
|
/// Ручная проверка/разметка ML на записях источников.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MlReviewService(
|
public sealed class MlReviewService(
|
||||||
IPipelineStore pipelineStore,
|
IPipelineStore pipelineStore,
|
||||||
@@ -86,10 +88,10 @@ public sealed class MlReviewService(
|
|||||||
private const double UserPushWeight = 1.0;
|
private const double UserPushWeight = 1.0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Отбирает сообщения-кандидаты для проверки ML по каналу и/или размеру выборки.
|
/// Отбирает записи-кандидаты для проверки ML по источнику и/или размеру выборки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dialogId">Id канала/диалога; пусто — выборка по всем источникам тенанта.</param>
|
/// <param name="dialogId">Оригинал источника (OriginRef); пусто — выборка по всем источникам тенанта.</param>
|
||||||
/// <param name="limit">Сколько последних сообщений вернуть (кламп 1..<see cref="MaxCandidates"/>, дефолт вызывающего).</param>
|
/// <param name="limit">Сколько последних записей вернуть (кламп 1..<see cref="MaxCandidates"/>, дефолт вызывающего).</param>
|
||||||
/// <returns>Кандидаты (свежие первыми): текст, текущий вердикт и мнение ML по каждому.</returns>
|
/// <returns>Кандидаты (свежие первыми): текст, текущий вердикт и мнение ML по каждому.</returns>
|
||||||
public async Task<IReadOnlyList<MlCandidateDto>> CandidatesAsync(
|
public async Task<IReadOnlyList<MlCandidateDto>> CandidatesAsync(
|
||||||
string? dialogId,
|
string? dialogId,
|
||||||
@@ -103,36 +105,36 @@ public sealed class MlReviewService(
|
|||||||
IReadOnlyList<RejectedItemDto> rejected = await pipelineStore.ListPageAsync(offset: 0, MaxScan, ct);
|
IReadOnlyList<RejectedItemDto> rejected = await pipelineStore.ListPageAsync(offset: 0, MaxScan, ct);
|
||||||
IReadOnlyList<CardDto> cardList = await cardStore.ListCardsAsync(new CardsQuery(null), ct);
|
IReadOnlyList<CardDto> cardList = await cardStore.ListCardsAsync(new CardsQuery(null), ct);
|
||||||
|
|
||||||
// Объединение по (dialogId, msgId): очередь → отсев → карточка (последняя перекрывает предыдущие).
|
// Объединение по ключу дедупа источника: очередь → отсев → карточка (последняя перекрывает предыдущие).
|
||||||
var merged = new Dictionary<(string Dialog, long MsgId), MlCandidateDto>();
|
var merged = new Dictionary<string, MlCandidateDto>(StringComparer.Ordinal);
|
||||||
foreach (QueueItemDto row in queue)
|
foreach (QueueItemDto row in queue)
|
||||||
{
|
{
|
||||||
if (row.MsgId is not { } msgId || !MatchesDialog(dialog, row.DialogId))
|
if (!IsMessage(row.Source) || !MatchesDialog(dialog, row.Source.OriginRef))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
merged[(row.DialogId, msgId)] = BuildQueued(row, msgId);
|
merged[row.Source.DedupeKey()] = BuildQueued(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (RejectedItemDto row in rejected)
|
foreach (RejectedItemDto row in rejected)
|
||||||
{
|
{
|
||||||
if (row.MsgId is not { } msgId || !MatchesDialog(dialog, row.DialogId))
|
if (!IsMessage(row.Source) || !MatchesDialog(dialog, row.Source.OriginRef))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
merged[(row.DialogId, msgId)] = BuildRejected(row, msgId);
|
merged[row.Source.DedupeKey()] = BuildRejected(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (CardDto card in cardList)
|
foreach (CardDto card in cardList)
|
||||||
{
|
{
|
||||||
if (card.SourceMsgId is not { } msgId || !MatchesDialog(dialog, card.SourceDialogId))
|
if (!IsMessage(card.Source) || !MatchesDialog(dialog, card.Source.OriginRef))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
merged[(card.SourceDialogId, msgId)] = BuildCard(card, msgId);
|
merged[card.Source.DedupeKey()] = BuildCard(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MlCandidateDto> ordered = merged.Values
|
List<MlCandidateDto> ordered = merged.Values
|
||||||
@@ -150,12 +152,12 @@ public sealed class MlReviewService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяет ручное решение по сообщению
|
/// Применяет ручное решение по записи источника
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dialogId">Id канала/диалога сообщения.</param>
|
/// <param name="dialogId">Оригинал источника (OriginRef) записи.</param>
|
||||||
/// <param name="msgId">Id исходного сообщения.</param>
|
/// <param name="msgId">Внешний id записи в источнике.</param>
|
||||||
/// <param name="action">Действие: <c>skip</c> | <c>spam</c> | <c>board:<id></c>.</param>
|
/// <param name="action">Действие: <c>skip</c> | <c>spam</c> | <c>board:<id></c>.</param>
|
||||||
/// <returns>Результат решения; null — исходное сообщение не найдено (404-семантика эндпоинта).</returns>
|
/// <returns>Результат решения; null — исходная запись не найдена (404-семантика эндпоинта).</returns>
|
||||||
public async Task<MlApplyResult?> ApplyAsync(
|
public async Task<MlApplyResult?> ApplyAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
long msgId,
|
long msgId,
|
||||||
@@ -164,12 +166,19 @@ public sealed class MlReviewService(
|
|||||||
{
|
{
|
||||||
string normalized = (action ?? string.Empty).Trim();
|
string normalized = (action ?? string.Empty).Trim();
|
||||||
string dialog = (dialogId ?? string.Empty).Trim();
|
string dialog = (dialogId ?? string.Empty).Trim();
|
||||||
|
string externalId = msgId.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
CardDto? card = await cardStore.GetCardBySourceAsync(dialog, msgId, ct);
|
SourceRef? source = await ResolveSourceAsync(dialog, externalId, ct);
|
||||||
string? text = await FindTextAsync(dialog, msgId, card, ct);
|
if (source is null)
|
||||||
|
{
|
||||||
|
return null; // 404: исходная запись не найдена
|
||||||
|
}
|
||||||
|
|
||||||
|
CardDto? card = await cardStore.GetCardBySourceAsync(source, ct);
|
||||||
|
string? text = await FindTextAsync(dialog, externalId, card, ct);
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
{
|
{
|
||||||
return null; // 404: исходное сообщение не найдено
|
return null; // 404: исходная запись не найдена
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized == ActionSkip)
|
if (normalized == ActionSkip)
|
||||||
@@ -179,28 +188,28 @@ public sealed class MlReviewService(
|
|||||||
|
|
||||||
if (normalized == ActionSpam)
|
if (normalized == ActionSpam)
|
||||||
{
|
{
|
||||||
return await ApplySpamAsync(dialog, msgId, card, text, ct);
|
return await ApplySpamAsync(dialog, externalId, card, text, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.StartsWith(ActionBoardPrefix, StringComparison.Ordinal))
|
if (normalized.StartsWith(ActionBoardPrefix, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
string boardId = normalized[ActionBoardPrefix.Length..].Trim();
|
string boardId = normalized[ActionBoardPrefix.Length..].Trim();
|
||||||
return await ApplyBoardAsync(dialog, msgId, boardId, card, text, ct);
|
return await ApplyBoardAsync(boardId, card, text, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MlApplyResult(UnknownActionDetail, Ok: false, Learned: false, Moved: null, LeadId: null);
|
return new MlApplyResult(UnknownActionDetail, Ok: false, Learned: false, Moved: null, LeadId: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Действие «спам»: карточку — в корзину (с обучением), сообщение из очереди — в отсев; иначе учим ML.
|
// Действие «спам»: карточку — в корзину (с обучением), запись из очереди — в отсев; иначе учим ML.
|
||||||
// dialog: Id диалога.
|
// dialog: Оригинал источника.
|
||||||
// msgId: Id сообщения.
|
// externalId: Внешний id записи.
|
||||||
// card: Карточка сообщения (null — сообщение не становилось карточкой).
|
// card: Карточка сообщения (null — сообщение не становилось карточкой).
|
||||||
// text: Текст сообщения.
|
// text: Текст сообщения.
|
||||||
// ct: Токен отмены.
|
// ct: Токен отмены.
|
||||||
// Возвращает: Результат решения.
|
// Возвращает: Результат решения.
|
||||||
private async Task<MlApplyResult> ApplySpamAsync(
|
private async Task<MlApplyResult> ApplySpamAsync(
|
||||||
string dialog,
|
string dialog,
|
||||||
long msgId,
|
string externalId,
|
||||||
CardDto? card,
|
CardDto? card,
|
||||||
string text,
|
string text,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -215,19 +224,16 @@ public sealed class MlReviewService(
|
|||||||
await mlClient.PushAsync(text, MlLearningLabels.Spam, UserPushWeight, ct);
|
await mlClient.PushAsync(text, MlLearningLabels.Spam, UserPushWeight, ct);
|
||||||
|
|
||||||
// Сообщение ещё в очереди — отсеиваем его (решение пользователя), снимая строку.
|
// Сообщение ещё в очереди — отсеиваем его (решение пользователя), снимая строку.
|
||||||
QueueItemDto? queued = await FindQueuedAsync(dialog, msgId, ct);
|
QueueItemDto? queued = await FindQueuedAsync(dialog, externalId, ct);
|
||||||
if (queued is not null)
|
if (queued is not null)
|
||||||
{
|
{
|
||||||
await processing.RejectAsync(new RejectRecord
|
await processing.RejectAsync(new RejectRecord
|
||||||
{
|
{
|
||||||
DialogId = queued.DialogId,
|
Source = queued.Source,
|
||||||
MsgId = queued.MsgId,
|
Content = queued.Content,
|
||||||
Text = queued.Text,
|
Text = queued.Text,
|
||||||
ChannelName = queued.Channel.Name,
|
|
||||||
ChannelHandle = queued.Channel.Handle,
|
|
||||||
ChannelHue = queued.Channel.Hue,
|
|
||||||
MsgAtMs = queued.MsgAtMs,
|
MsgAtMs = queued.MsgAtMs,
|
||||||
Source = ManualSource,
|
DecidedBy = ManualSource,
|
||||||
Stage = ManualSpamStage,
|
Stage = ManualSpamStage,
|
||||||
Reason = ManualSpamReason,
|
Reason = ManualSpamReason,
|
||||||
Kw = string.Empty,
|
Kw = string.Empty,
|
||||||
@@ -239,16 +245,12 @@ public sealed class MlReviewService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Действие «в колонку»: карточку — переносим, уже в колонке — только учим; иначе учим ML.
|
// Действие «в колонку»: карточку — переносим, уже в колонке — только учим; иначе учим ML.
|
||||||
// dialog: Id диалога.
|
|
||||||
// msgId: Id сообщения.
|
|
||||||
// boardId: Id колонки-цели (inbox или b_...).
|
// boardId: Id колонки-цели (inbox или b_...).
|
||||||
// card: Карточка сообщения (null — сообщение не становилось карточкой).
|
// card: Карточка сообщения (null — сообщение не становилось карточкой).
|
||||||
// text: Текст сообщения.
|
// text: Текст сообщения.
|
||||||
// ct: Токен отмены.
|
// ct: Токен отмены.
|
||||||
// Возвращает: Результат решения.
|
// Возвращает: Результат решения.
|
||||||
private async Task<MlApplyResult> ApplyBoardAsync(
|
private async Task<MlApplyResult> ApplyBoardAsync(
|
||||||
string dialog,
|
|
||||||
long msgId,
|
|
||||||
string boardId,
|
string boardId,
|
||||||
CardDto? card,
|
CardDto? card,
|
||||||
string text,
|
string text,
|
||||||
@@ -281,24 +283,61 @@ public sealed class MlReviewService(
|
|||||||
return new MlApplyResult(null, Ok: true, Learned: true, Moved: boardId, LeadId: card.Id);
|
return new MlApplyResult(null, Ok: true, Learned: true, Moved: boardId, LeadId: card.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Текст исходного сообщения: source_msg карточки, иначе текст строки очереди/записи отсева.
|
// Ссылка на источник записи: очередь → карточка → отсев.
|
||||||
// dialog: Id диалога.
|
// dialog: Оригинал источника.
|
||||||
// msgId: Id сообщения.
|
// externalId: Внешний id записи.
|
||||||
|
// ct: Токен отмены.
|
||||||
|
// Возвращает: Ссылку на источник либо null, если записи нет.
|
||||||
|
private async Task<SourceRef?> ResolveSourceAsync(
|
||||||
|
string dialog,
|
||||||
|
string externalId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
QueueItemDto? queued = await FindQueuedAsync(dialog, externalId, ct);
|
||||||
|
if (queued is not null)
|
||||||
|
{
|
||||||
|
return queued.Source;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<CardDto> cardList = await cardStore.ListCardsAsync(new CardsQuery(null), ct);
|
||||||
|
foreach (CardDto card in cardList)
|
||||||
|
{
|
||||||
|
if (MatchesSource(card.Source, dialog, externalId))
|
||||||
|
{
|
||||||
|
return card.Source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<RejectedItemDto> rejected = await pipelineStore.ListPageAsync(offset: 0, MaxScan, ct);
|
||||||
|
foreach (RejectedItemDto row in rejected)
|
||||||
|
{
|
||||||
|
if (MatchesSource(row.Source, dialog, externalId))
|
||||||
|
{
|
||||||
|
return row.Source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Текст исходного сообщения: текст карточки, иначе текст строки очереди/записи отсева.
|
||||||
|
// dialog: Оригинал источника.
|
||||||
|
// externalId: Внешний id записи.
|
||||||
// card: Карточка сообщения (уже прочитана вызывающим).
|
// card: Карточка сообщения (уже прочитана вызывающим).
|
||||||
// ct: Токен отмены.
|
// ct: Токен отмены.
|
||||||
// Возвращает: Непустой текст либо null, если сообщения нет ни в одном источнике.
|
// Возвращает: Непустой текст либо null, если записи нет ни в одном источнике.
|
||||||
private async Task<string?> FindTextAsync(
|
private async Task<string?> FindTextAsync(
|
||||||
string dialog,
|
string dialog,
|
||||||
long msgId,
|
string externalId,
|
||||||
CardDto? card,
|
CardDto? card,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (card is not null && !string.IsNullOrWhiteSpace(card.SourceMsg))
|
if (card is not null && !string.IsNullOrWhiteSpace(card.Content.Text))
|
||||||
{
|
{
|
||||||
return card.SourceMsg;
|
return card.Content.Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueItemDto? queued = await FindQueuedAsync(dialog, msgId, ct);
|
QueueItemDto? queued = await FindQueuedAsync(dialog, externalId, ct);
|
||||||
if (queued is not null && !string.IsNullOrWhiteSpace(queued.Text))
|
if (queued is not null && !string.IsNullOrWhiteSpace(queued.Text))
|
||||||
{
|
{
|
||||||
return queued.Text;
|
return queued.Text;
|
||||||
@@ -307,7 +346,7 @@ public sealed class MlReviewService(
|
|||||||
IReadOnlyList<RejectedItemDto> rejected = await pipelineStore.ListPageAsync(offset: 0, MaxScan, ct);
|
IReadOnlyList<RejectedItemDto> rejected = await pipelineStore.ListPageAsync(offset: 0, MaxScan, ct);
|
||||||
foreach (RejectedItemDto row in rejected)
|
foreach (RejectedItemDto row in rejected)
|
||||||
{
|
{
|
||||||
if (row.MsgId == msgId && string.Equals(row.DialogId, dialog, StringComparison.Ordinal))
|
if (MatchesSource(row.Source, dialog, externalId))
|
||||||
{
|
{
|
||||||
return row.Text;
|
return row.Text;
|
||||||
}
|
}
|
||||||
@@ -316,20 +355,20 @@ public sealed class MlReviewService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Строка очереди сообщения (для отсева при ручной разметке «спам»).
|
// Строка очереди записи (для отсева при ручной разметке «спам»).
|
||||||
// dialog: Id диалога.
|
// dialog: Оригинал источника.
|
||||||
// msgId: Id сообщения.
|
// externalId: Внешний id записи.
|
||||||
// ct: Токен отмены.
|
// ct: Токен отмены.
|
||||||
// Возвращает: Строка очереди либо null — сообщение уже обработано/не в очереди.
|
// Возвращает: Строка очереди либо null — сообщение уже обработано/не в очереди.
|
||||||
private async Task<QueueItemDto?> FindQueuedAsync(
|
private async Task<QueueItemDto?> FindQueuedAsync(
|
||||||
string dialog,
|
string dialog,
|
||||||
long msgId,
|
string externalId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
IReadOnlyList<QueueItemDto> queue = await pipelineStore.ListAsync(status: null, MaxScan, ct);
|
IReadOnlyList<QueueItemDto> queue = await pipelineStore.ListAsync(status: null, MaxScan, ct);
|
||||||
foreach (QueueItemDto row in queue)
|
foreach (QueueItemDto row in queue)
|
||||||
{
|
{
|
||||||
if (row.MsgId == msgId && string.Equals(row.DialogId, dialog, StringComparison.Ordinal))
|
if (MatchesSource(row.Source, dialog, externalId))
|
||||||
{
|
{
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
@@ -360,21 +399,34 @@ public sealed class MlReviewService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Соответствует ли диалог фильтру канала (пустой фильтр — все диалоги).
|
// Идентифицирована ли запись источника (есть внешний id).
|
||||||
// filter: Запрошенный канал (пусто — без фильтра).
|
// source: Ссылка на источник.
|
||||||
// dialogId: Диалог сообщения.
|
// Возвращает: True — запись адресуема как сообщение источника.
|
||||||
|
private static bool IsMessage(SourceRef source) => source.ExternalId is { Length: > 0 };
|
||||||
|
|
||||||
|
// Соответствует ли источник фильтру оригинала (пустой фильтр — все источники).
|
||||||
|
// filter: Запрошенный оригинал (пусто — без фильтра).
|
||||||
|
// originRef: Оригинал источника записи.
|
||||||
// Возвращает: True — кандидат подходит.
|
// Возвращает: True — кандидат подходит.
|
||||||
private static bool MatchesDialog(string filter, string dialogId) =>
|
private static bool MatchesDialog(string filter, string? originRef) =>
|
||||||
filter.Length == 0 || string.Equals(filter, dialogId, StringComparison.Ordinal);
|
filter.Length == 0 || string.Equals(filter, originRef ?? string.Empty, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Совпадает ли источник с оригиналом и внешним id записи.
|
||||||
|
// source: Ссылка на источник.
|
||||||
|
// dialog: Оригинал источника.
|
||||||
|
// externalId: Внешний id записи.
|
||||||
|
// Возвращает: True — источник указывает на ту же запись.
|
||||||
|
private static bool MatchesSource(SourceRef source, string dialog, string externalId) =>
|
||||||
|
string.Equals(source.OriginRef ?? string.Empty, dialog, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(source.ExternalId ?? string.Empty, externalId, StringComparison.Ordinal);
|
||||||
|
|
||||||
// Кандидат из строки очереди (вердикт queued).
|
// Кандидат из строки очереди (вердикт queued).
|
||||||
// row: Строка очереди.
|
// row: Строка очереди.
|
||||||
// msgId: Id сообщения.
|
|
||||||
// Возвращает: Кандидат.
|
// Возвращает: Кандидат.
|
||||||
private static MlCandidateDto BuildQueued(QueueItemDto row, long msgId) => new()
|
private static MlCandidateDto BuildQueued(QueueItemDto row) => new()
|
||||||
{
|
{
|
||||||
Id = msgId,
|
Source = row.Source,
|
||||||
DialogId = row.DialogId,
|
Content = row.Content,
|
||||||
Text = Truncate(row.Text),
|
Text = Truncate(row.Text),
|
||||||
Time = row.MsgAtMs == 0 ? null : row.MsgAtMs,
|
Time = row.MsgAtMs == 0 ? null : row.MsgAtMs,
|
||||||
Lead = false,
|
Lead = false,
|
||||||
@@ -384,12 +436,11 @@ public sealed class MlReviewService(
|
|||||||
|
|
||||||
// Кандидат из записи отсева (вердикт rejected).
|
// Кандидат из записи отсева (вердикт rejected).
|
||||||
// row: Запись отсева.
|
// row: Запись отсева.
|
||||||
// msgId: Id сообщения.
|
|
||||||
// Возвращает: Кандидат.
|
// Возвращает: Кандидат.
|
||||||
private static MlCandidateDto BuildRejected(RejectedItemDto row, long msgId) => new()
|
private static MlCandidateDto BuildRejected(RejectedItemDto row) => new()
|
||||||
{
|
{
|
||||||
Id = msgId,
|
Source = row.Source,
|
||||||
DialogId = row.DialogId,
|
Content = row.Content,
|
||||||
Text = Truncate(row.Text),
|
Text = Truncate(row.Text),
|
||||||
Time = row.MsgAtMs == 0 ? null : row.MsgAtMs,
|
Time = row.MsgAtMs == 0 ? null : row.MsgAtMs,
|
||||||
Lead = false,
|
Lead = false,
|
||||||
@@ -400,13 +451,12 @@ public sealed class MlReviewService(
|
|||||||
|
|
||||||
// Кандидат из карточки (вердикт card).
|
// Кандидат из карточки (вердикт card).
|
||||||
// card: Карточка.
|
// card: Карточка.
|
||||||
// msgId: Id сообщения.
|
|
||||||
// Возвращает: Кандидат.
|
// Возвращает: Кандидат.
|
||||||
private static MlCandidateDto BuildCard(CardDto card, long msgId) => new()
|
private static MlCandidateDto BuildCard(CardDto card) => new()
|
||||||
{
|
{
|
||||||
Id = msgId,
|
Source = card.Source,
|
||||||
DialogId = card.SourceDialogId,
|
Content = card.Content,
|
||||||
Text = Truncate(card.SourceMsg),
|
Text = Truncate(card.Content.Text ?? string.Empty),
|
||||||
Time = card.ReceivedAtMs == 0 ? null : card.ReceivedAtMs,
|
Time = card.ReceivedAtMs == 0 ? null : card.ReceivedAtMs,
|
||||||
Lead = true,
|
Lead = true,
|
||||||
Verdict = VerdictCard,
|
Verdict = VerdictCard,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
@@ -6,42 +7,44 @@ using Deal.Modules.Pipeline.Application.Parse;
|
|||||||
namespace Deal.Modules.Pipeline.Application.Services;
|
namespace Deal.Modules.Pipeline.Application.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Приём входящих сообщений пайплайна — постановка сырого сообщения в очередь QueueItems.
|
/// Приём входящих сообщений пайплайна — постановка сырой записи источника в очередь QueueItems.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PipelineIngestService(IPipelineStore store)
|
public sealed class PipelineIngestService(IPipelineStore store)
|
||||||
{
|
{
|
||||||
private const int MaxQueueTextLength = 6000;
|
private const int MaxQueueTextLength = 6000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ставит входящее сообщение в очередь
|
/// Ставит входящую запись источника в очередь
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Сырое сообщение: диалог, канальные поля, текст, время; Force — возврат из отсева.</param>
|
/// <param name="message">Запись источника и флаг «возвращено из отсева».</param>
|
||||||
/// <returns>Результат: Id строки (<c>p_...</c>) либо null, если сообщение не принято; Duplicate — дубль диалога.</returns>
|
/// <returns>Результат: Id строки (<c>p_...</c>) либо null, если запись не принята; Duplicate — дубль источника.</returns>
|
||||||
public async Task<PipelineIngestResultDto> EnqueueAsync(QueuedMessage message, CancellationToken ct)
|
public async Task<PipelineIngestResultDto> EnqueueAsync(QueuedMessage message, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string text = (message.Text ?? string.Empty).Trim();
|
string text = (message.Item.Content.Text ?? string.Empty).Trim();
|
||||||
if (text.Length == 0 || string.IsNullOrEmpty(message.DialogId))
|
if (text.Length == 0)
|
||||||
{
|
{
|
||||||
return new PipelineIngestResultDto(null, false);
|
return new PipelineIngestResultDto(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
if (message.MsgId is not null
|
if (await store.ExistsDuplicateAsync(message.Item.Source, ct))
|
||||||
&& await store.ExistsDuplicateAsync(message.DialogId, message.MsgId, ct))
|
|
||||||
{
|
{
|
||||||
return new PipelineIngestResultDto(null, true);
|
return new PipelineIngestResultDto(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string sliced = MessageTextCleaner.SliceCodePoints(text, MaxQueueTextLength);
|
||||||
|
DateTimeOffset receivedAt = message.Item.Source.ReceivedAt;
|
||||||
|
long msgAtMs = receivedAt == default ? 0 : receivedAt.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
var item = new QueueItemDto
|
var item = new QueueItemDto
|
||||||
{
|
{
|
||||||
Id = PrefixId.New(PipelineIdPrefixes.Queue),
|
Id = PrefixId.New(PipelineIdPrefixes.Queue),
|
||||||
DialogId = message.DialogId,
|
Source = message.Item.Source,
|
||||||
MsgId = message.MsgId,
|
Content = message.Item.Content with { Text = sliced },
|
||||||
Text = MessageTextCleaner.SliceCodePoints(text, MaxQueueTextLength),
|
Text = sliced,
|
||||||
Status = PipelineQueueStatuses.New,
|
Status = PipelineQueueStatuses.New,
|
||||||
Channel = new PipelineChannelDto(message.ChannelName, message.ChannelHandle, message.ChannelHue),
|
MsgAtMs = msgAtMs,
|
||||||
MsgAtMs = message.MsgAtMs ?? nowMs,
|
|
||||||
QueuedAtMs = nowMs,
|
QueuedAtMs = nowMs,
|
||||||
Force = message.Force,
|
Force = message.Force,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Deal.Contracts.Integrations.Abstractions;
|
using Deal.Contracts.Integrations.Abstractions;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Parse;
|
using Deal.Modules.Pipeline.Application.Parse;
|
||||||
@@ -80,7 +80,6 @@ public sealed class PipelineProcessingService(
|
|||||||
return store.UpsertAsync(record with
|
return store.UpsertAsync(record with
|
||||||
{
|
{
|
||||||
Text = MessageTextCleaner.SliceCodePoints(record.Text, MaxRejectedTextLength),
|
Text = MessageTextCleaner.SliceCodePoints(record.Text, MaxRejectedTextLength),
|
||||||
ChannelHue = string.IsNullOrEmpty(record.ChannelHue) ? SourceDefaults.DefaultHue : record.ChannelHue,
|
|
||||||
Reason = MessageTextCleaner.SliceCodePoints(record.Reason, MaxReasonLength),
|
Reason = MessageTextCleaner.SliceCodePoints(record.Reason, MaxReasonLength),
|
||||||
Kw = MessageTextCleaner.SliceCodePoints(record.Kw, MaxKwLength),
|
Kw = MessageTextCleaner.SliceCodePoints(record.Kw, MaxKwLength),
|
||||||
}, ct);
|
}, ct);
|
||||||
@@ -172,7 +171,7 @@ public sealed class PipelineProcessingService(
|
|||||||
return new RejectReturnResultDto(AlreadyReturnedDetail, rejectedId, false, 0);
|
return new RejectReturnResultDto(AlreadyReturnedDetail, rejectedId, false, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.Source == DuplicateSource)
|
if (row.DecidedBy == DuplicateSource)
|
||||||
{
|
{
|
||||||
return new RejectReturnResultDto(DuplicateReturnDetail, rejectedId, false, 0);
|
return new RejectReturnResultDto(DuplicateReturnDetail, rejectedId, false, 0);
|
||||||
}
|
}
|
||||||
@@ -195,38 +194,21 @@ public sealed class PipelineProcessingService(
|
|||||||
DateTimeOffset.FromUnixTimeMilliseconds(nowMs),
|
DateTimeOffset.FromUnixTimeMilliseconds(nowMs),
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
long msgAtMs = row.MsgAtMs != 0 ? row.MsgAtMs : nowMs;
|
var extra = new Dictionary<string, string>(row.Source.Extra ?? new Dictionary<string, string>())
|
||||||
string hue = string.IsNullOrEmpty(row.Channel.Hue) ? SourceDefaults.DefaultHue : row.Channel.Hue;
|
|
||||||
if (row.DialogId.Length > 0 && row.MsgId is not null)
|
|
||||||
{
|
{
|
||||||
await ingest.EnqueueAsync(new QueuedMessage
|
[SourceRefs.HueKey] = row.Source.ResolveHue(),
|
||||||
{
|
};
|
||||||
DialogId = row.DialogId,
|
SourceRef source = row.Source with { Extra = extra };
|
||||||
ChannelName = row.Channel.Name,
|
|
||||||
ChannelHandle = row.Channel.Handle,
|
await ingest.EnqueueAsync(new QueuedMessage
|
||||||
ChannelHue = hue,
|
|
||||||
MsgId = row.MsgId,
|
|
||||||
Text = text,
|
|
||||||
MsgAtMs = msgAtMs,
|
|
||||||
Force = true,
|
|
||||||
}, ct);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var item = new QueueItemDto
|
Item = new SourceItem
|
||||||
{
|
{
|
||||||
Id = PrefixId.New(PipelineIdPrefixes.Queue),
|
Source = source,
|
||||||
DialogId = row.DialogId,
|
Content = row.Content with { Text = text },
|
||||||
MsgId = row.MsgId,
|
},
|
||||||
Text = MessageTextCleaner.SliceCodePoints(text, MaxRejectedTextLength),
|
Force = true,
|
||||||
Status = PipelineQueueStatuses.New,
|
}, ct);
|
||||||
Channel = new PipelineChannelDto(row.Channel.Name, row.Channel.Handle, hue),
|
|
||||||
MsgAtMs = msgAtMs,
|
|
||||||
QueuedAtMs = nowMs,
|
|
||||||
Force = true,
|
|
||||||
};
|
|
||||||
await store.AddAsync(item, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RejectReturnResultDto(null, rejectedId, true, nowMs);
|
return new RejectReturnResultDto(null, rejectedId, true, nowMs);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -20,14 +20,11 @@ public sealed partial class PipelineWorkerService
|
|||||||
{
|
{
|
||||||
return _processing.RejectAsync(new RejectRecord
|
return _processing.RejectAsync(new RejectRecord
|
||||||
{
|
{
|
||||||
DialogId = row.DialogId,
|
Source = row.Source,
|
||||||
MsgId = row.MsgId,
|
Content = row.Content,
|
||||||
Text = row.Text,
|
Text = row.Text,
|
||||||
ChannelName = row.Channel.Name,
|
|
||||||
ChannelHandle = row.Channel.Handle,
|
|
||||||
ChannelHue = row.Channel.Hue,
|
|
||||||
MsgAtMs = row.MsgAtMs,
|
MsgAtMs = row.MsgAtMs,
|
||||||
Source = source,
|
DecidedBy = source,
|
||||||
Stage = stage,
|
Stage = stage,
|
||||||
Reason = reason,
|
Reason = reason,
|
||||||
Kw = kw,
|
Kw = kw,
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
private static QueueItemDto QueueItem(string id) => new()
|
private static QueueItemDto QueueItem(string id) => new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
DialogId = "d_1",
|
|
||||||
Text = "текст",
|
Text = "текст",
|
||||||
Status = PipelineQueueStatuses.New,
|
Status = PipelineQueueStatuses.New,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System.Globalization;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
@@ -6,6 +8,7 @@ using Deal.Modules.Settings.Application.Models;
|
|||||||
using Deal.Modules.Settings.Application.Services;
|
using Deal.Modules.Settings.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -64,15 +67,17 @@ public sealed class CardComposerTests
|
|||||||
snapshot.Contacts);
|
snapshot.Contacts);
|
||||||
Assert.Equal("@ivan_py", snapshot.Contact);
|
Assert.Equal("@ivan_py", snapshot.Contact);
|
||||||
|
|
||||||
Assert.Equal(new CardChannelDto("Канал найма", "hire_ch", "#ababab"), Channel(snapshot));
|
Assert.Equal("test", snapshot.Source.Kind);
|
||||||
|
Assert.Equal("d_1", snapshot.Source.OriginRef);
|
||||||
|
Assert.Equal("123", snapshot.Source.ExternalId);
|
||||||
|
Assert.Equal("Канал найма", snapshot.Source.DisplayName);
|
||||||
|
Assert.Equal("#ababab", snapshot.Source.ResolveHue());
|
||||||
Assert.Equal(1_700_000_000_000L, snapshot.ReceivedAt.ToUnixTimeMilliseconds());
|
Assert.Equal(1_700_000_000_000L, snapshot.ReceivedAt.ToUnixTimeMilliseconds());
|
||||||
Assert.Equal("ООО Ромашка ищет Python-разработчика на бота для CRM, удалённо.", snapshot.SourceMsg);
|
Assert.Equal("ООО Ромашка ищет Python-разработчика на бота для CRM, удалённо.", snapshot.Content.Text);
|
||||||
Assert.Equal("d_1", snapshot.SourceDialogId);
|
|
||||||
Assert.Equal(123, snapshot.SourceMsgId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildAsync_LongSourceText_TruncatesSourceMsgTo4000CodePoints()
|
public async Task BuildAsync_LongSourceText_TruncatesContentTextTo4000CodePoints()
|
||||||
{
|
{
|
||||||
(CardComposer composer, _, _) = Create();
|
(CardComposer composer, _, _) = Create();
|
||||||
string longText = new string('а', 4500);
|
string longText = new string('а', 4500);
|
||||||
@@ -83,7 +88,7 @@ public sealed class CardComposerTests
|
|||||||
cardId: "l_test2",
|
cardId: "l_test2",
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(4000, snapshot.SourceMsg.EnumerateRunes().Count()); // python text[:4000] (L503)
|
Assert.Equal(4000, (snapshot.Content.Text ?? string.Empty).EnumerateRunes().Count()); // python text[:4000] (L503)
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -223,20 +228,6 @@ public sealed class CardComposerTests
|
|||||||
Assert.Equal("@client_py", snapshot.Contact); // primary_contact: tg важнее почты
|
Assert.Equal("@client_py", snapshot.Contact); // primary_contact: tg важнее почты
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task BuildAsync_EmptyChannelHue_DefaultsTo666()
|
|
||||||
{
|
|
||||||
(CardComposer composer, _, _) = Create();
|
|
||||||
|
|
||||||
CardSnapshot snapshot = await composer.BuildAsync(
|
|
||||||
Parsed(title: "Заголовок"),
|
|
||||||
Message("Сообщение", channelHue: string.Empty),
|
|
||||||
cardId: "l_test11",
|
|
||||||
CancellationToken.None);
|
|
||||||
|
|
||||||
Assert.Equal("#666", snapshot.ChannelHue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Хелперы ──────────────────────────────────────────────────────────────────────────
|
// ─── Хелперы ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Создаёт контекст теста: пустые хранилища (дефолты: конверсия включена, курсы — мок).
|
// Создаёт контекст теста: пустые хранилища (дефолты: конверсия включена, курсы — мок).
|
||||||
@@ -277,19 +268,17 @@ public sealed class CardComposerTests
|
|||||||
string dialogId = "d_1",
|
string dialogId = "d_1",
|
||||||
long? msgId = 123,
|
long? msgId = 123,
|
||||||
string channelName = "Канал найма",
|
string channelName = "Канал найма",
|
||||||
string channelHandle = "hire_ch",
|
|
||||||
string channelHue = "#ababab",
|
string channelHue = "#ababab",
|
||||||
long msgAtMs = 1_700_000_000_000) => new()
|
long msgAtMs = 1_700_000_000_000) => SourceItemFactory.Queue(
|
||||||
{
|
"p_1",
|
||||||
Id = "p_1",
|
text,
|
||||||
DialogId = dialogId,
|
PipelineQueueStatuses.Filtered,
|
||||||
MsgId = msgId,
|
origin: dialogId,
|
||||||
Text = text,
|
externalId: msgId?.ToString(CultureInfo.InvariantCulture),
|
||||||
Status = PipelineQueueStatuses.Filtered,
|
name: channelName,
|
||||||
Channel = new PipelineChannelDto(channelName, channelHandle, channelHue),
|
hue: channelHue,
|
||||||
MsgAtMs = msgAtMs,
|
msgAtMs: msgAtMs,
|
||||||
QueuedAtMs = msgAtMs,
|
queuedAtMs: msgAtMs);
|
||||||
};
|
|
||||||
|
|
||||||
// Доска с правилами (для сценариев ContainerAccepts-страховки).
|
// Доска с правилами (для сценариев ContainerAccepts-страховки).
|
||||||
// id: Id доски (b_...).
|
// id: Id доски (b_...).
|
||||||
@@ -316,8 +305,4 @@ public sealed class CardComposerTests
|
|||||||
private static CardBudgetDto? Converted(CardSnapshot snapshot) => snapshot.ConvCur.Length == 0
|
private static CardBudgetDto? Converted(CardSnapshot snapshot) => snapshot.ConvCur.Length == 0
|
||||||
? null
|
? null
|
||||||
: new CardBudgetDto(snapshot.ConvFrom, snapshot.ConvTo, snapshot.ConvCur);
|
: new CardBudgetDto(snapshot.ConvFrom, snapshot.ConvTo, snapshot.ConvCur);
|
||||||
|
|
||||||
// Канал снимка (как карточка CardDto.Channel после записи адаптера).
|
|
||||||
private static CardChannelDto Channel(CardSnapshot snapshot) =>
|
|
||||||
new(snapshot.ChannelName, snapshot.ChannelHandle, snapshot.ChannelHue);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
@@ -210,7 +211,7 @@ public sealed class CardsServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Move_EmptySourceMsg_UsesTitleForMatchHitsAndPush()
|
public async Task Move_EmptySourceText_UsesTitleForMatchHitsAndPush()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
|
(CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
|
||||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||||
@@ -677,7 +678,7 @@ public sealed class CardsServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Search_FindsByTitleSummaryContactSourceMsg_OrderedByReceivedAtDesc()
|
public async Task Search_FindsByTitleSummaryContactSourceText_OrderedByReceivedAtDesc()
|
||||||
{
|
{
|
||||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||||
store.SeedCard(Card("l_title", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300));
|
store.SeedCard(Card("l_title", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300));
|
||||||
@@ -759,7 +760,7 @@ public sealed class CardsServiceTests
|
|||||||
Title = title,
|
Title = title,
|
||||||
Summary = summary,
|
Summary = summary,
|
||||||
Contact = contact,
|
Contact = contact,
|
||||||
SourceMsg = sourceMsg,
|
Content = new SourceContent { Text = sourceMsg },
|
||||||
PrevCol = prevCol,
|
PrevCol = prevCol,
|
||||||
ReceivedAtMs = receivedAtMs,
|
ReceivedAtMs = receivedAtMs,
|
||||||
Comments = comments ?? Array.Empty<CardCommentDto>(),
|
Comments = comments ?? Array.Empty<CardCommentDto>(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Deal.Modules.Pipeline.Application.Services;
|
|||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
using Deal.Tests.Unit.Modules.Kanban;
|
using Deal.Tests.Unit.Modules.Kanban;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Contracts;
|
namespace Deal.Tests.Unit.Contracts;
|
||||||
|
|
||||||
@@ -110,15 +111,10 @@ public sealed class PipelineCardWriterTests
|
|||||||
private static QueueItemDto Message(
|
private static QueueItemDto Message(
|
||||||
string text,
|
string text,
|
||||||
string dialogId = "d_1",
|
string dialogId = "d_1",
|
||||||
long? msgId = 123) => new()
|
long? msgId = 123) => SourceItemFactory.Queue(
|
||||||
{
|
"p_1",
|
||||||
Id = "p_1",
|
text,
|
||||||
DialogId = dialogId,
|
PipelineQueueStatuses.Filtered,
|
||||||
MsgId = msgId,
|
origin: dialogId,
|
||||||
Text = text,
|
externalId: msgId?.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||||
Status = PipelineQueueStatuses.Filtered,
|
|
||||||
Channel = new PipelineChannelDto("Канал", "ch", "#666"),
|
|
||||||
MsgAtMs = 1_700_000_000_000,
|
|
||||||
QueuedAtMs = 1_700_000_000_000,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,18 +202,11 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
// id: Id строки (p_…).
|
// id: Id строки (p_…).
|
||||||
// text: Текст сообщения.
|
// text: Текст сообщения.
|
||||||
// Возвращает: Строка как если бы была переведена в filtered воркером.
|
// Возвращает: Строка как если бы была переведена в filtered воркером.
|
||||||
private static QueueItemDto QueueRowFiltered(string id, string text) => new()
|
private static QueueItemDto QueueRowFiltered(string id, string text) => SourceItemFactory.Queue(
|
||||||
{
|
id,
|
||||||
Id = id,
|
text,
|
||||||
DialogId = "d_1",
|
PipelineQueueStatuses.Filtered,
|
||||||
MsgId = 100,
|
msgAtMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||||
Text = text,
|
|
||||||
Status = PipelineQueueStatuses.Filtered,
|
|
||||||
Channel = new PipelineChannelDto("Канал А", "channel_a", "#333"),
|
|
||||||
MsgAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
||||||
QueuedAtMs = 1_700_000_000_000,
|
|
||||||
Force = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Неготовая модель: «не уверена» (ML-слот воркера не решает — сообщение уходит к ИИ).
|
// Неготовая модель: «не уверена» (ML-слот воркера не решает — сообщение уходит к ИИ).
|
||||||
// Возвращает: Фиксированный «не готов/не уверен».
|
// Возвращает: Фиксированный «не готов/не уверен».
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Infrastructure.Services;
|
using Deal.Infrastructure.Services;
|
||||||
using Deal.Modules.Cards.Application.Dtos;
|
using Deal.Modules.Cards.Application.Dtos;
|
||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Tests.Unit.Contracts;
|
using Deal.Tests.Unit.Contracts;
|
||||||
@@ -44,7 +45,7 @@ public sealed class CardMoverTests
|
|||||||
{
|
{
|
||||||
(CardMover mover, FakeKanjStore store) = Create();
|
(CardMover mover, FakeKanjStore store) = Create();
|
||||||
store.SeedBoard(new ContainerDto { Id = "b_py", Name = "Python" });
|
store.SeedBoard(new ContainerDto { Id = "b_py", Name = "Python" });
|
||||||
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, SourceMsg = "Нужен Python" });
|
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Нужен Python" } });
|
||||||
|
|
||||||
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_py", UserMove, CancellationToken.None);
|
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_py", UserMove, CancellationToken.None);
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ public sealed class CardMoverTests
|
|||||||
public async Task Move_UnknownTarget_ReturnsDashboardInvalidTargetError()
|
public async Task Move_UnknownTarget_ReturnsDashboardInvalidTargetError()
|
||||||
{
|
{
|
||||||
(CardMover mover, FakeKanjStore store) = Create();
|
(CardMover mover, FakeKanjStore store) = Create();
|
||||||
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, SourceMsg = "Текст" });
|
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Текст" } });
|
||||||
|
|
||||||
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_ghost", UserMove, CancellationToken.None);
|
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_ghost", UserMove, CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Modules.Cards.Application.Models;
|
using Deal.Modules.Cards.Application.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Abstractions;
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ public class FakeKanjStore : ICardStore
|
|||||||
.Where(card => card.Title.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
.Where(card => card.Title.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
||||||
|| card.Summary.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
|| card.Summary.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
||||||
|| card.Contact.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
|| card.Contact.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
||||||
|| card.SourceMsg.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal))
|
|| (card.Content.Text ?? string.Empty).ToLowerInvariant().Contains(lowered, StringComparison.Ordinal))
|
||||||
.OrderByDescending(card => card.ReceivedAtMs)
|
.OrderByDescending(card => card.ReceivedAtMs)
|
||||||
.Take(limit)
|
.Take(limit)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -190,17 +191,20 @@ public class FakeKanjStore : ICardStore
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<CardDto?> GetCardBySourceAsync(
|
public Task<CardDto?> GetCardBySourceAsync(
|
||||||
string dialogId,
|
SourceRef source,
|
||||||
long msgId,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(dialogId))
|
if (string.IsNullOrEmpty(source.Kind))
|
||||||
{
|
{
|
||||||
return Task.FromResult<CardDto?>(null);
|
return Task.FromResult<CardDto?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string externalId = source.ExternalId ?? string.Empty;
|
||||||
|
string originRef = source.OriginRef ?? string.Empty;
|
||||||
CardDto? match = _cards
|
CardDto? match = _cards
|
||||||
.Where(card => card.SourceDialogId == dialogId && card.SourceMsgId == msgId)
|
.Where(card => card.Source.Kind == source.Kind
|
||||||
|
&& (card.Source.ExternalId ?? string.Empty) == externalId
|
||||||
|
&& (card.Source.OriginRef ?? string.Empty) == originRef)
|
||||||
.OrderByDescending(card => card.ReceivedAtMs)
|
.OrderByDescending(card => card.ReceivedAtMs)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
return Task.FromResult(match);
|
return Task.FromResult(match);
|
||||||
@@ -233,12 +237,10 @@ public class FakeKanjStore : ICardStore
|
|||||||
: new CardBudgetDto(snapshot.ConvFrom, snapshot.ConvTo, snapshot.ConvCur),
|
: new CardBudgetDto(snapshot.ConvFrom, snapshot.ConvTo, snapshot.ConvCur),
|
||||||
Contact = snapshot.Contact,
|
Contact = snapshot.Contact,
|
||||||
Contacts = snapshot.Contacts,
|
Contacts = snapshot.Contacts,
|
||||||
Channel = new CardChannelDto(snapshot.ChannelName, snapshot.ChannelHandle, snapshot.ChannelHue),
|
Source = snapshot.Source,
|
||||||
|
Content = snapshot.Content,
|
||||||
Time = "только что",
|
Time = "только что",
|
||||||
ReceivedAtMs = snapshot.ReceivedAt.ToUnixTimeMilliseconds(),
|
ReceivedAtMs = snapshot.ReceivedAt.ToUnixTimeMilliseconds(),
|
||||||
SourceMsg = snapshot.SourceMsg,
|
|
||||||
SourceDialogId = snapshot.SourceDialogId,
|
|
||||||
SourceMsgId = snapshot.SourceMsgId,
|
|
||||||
PrevCol = snapshot.PrevCol,
|
PrevCol = snapshot.PrevCol,
|
||||||
MatchHits = snapshot.MatchHits,
|
MatchHits = snapshot.MatchHits,
|
||||||
TzText = snapshot.TzText,
|
TzText = snapshot.TzText,
|
||||||
@@ -418,12 +420,13 @@ public class FakeKanjStore : ICardStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
CardDto? card = _cards.FirstOrDefault(item => item.Id == move.LeadId);
|
CardDto? card = _cards.FirstOrDefault(item => item.Id == move.LeadId);
|
||||||
if (card is null || card.SourceMsg.Length == 0)
|
string sourceText = card?.Content.Text ?? string.Empty;
|
||||||
|
if (card is null || sourceText.Length == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
examples.Add(new AiMarkupExampleDto(card.SourceMsg, move.ToCol));
|
examples.Add(new AiMarkupExampleDto(sourceText, move.ToCol));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<IReadOnlyList<AiMarkupExampleDto>>(examples);
|
return Task.FromResult<IReadOnlyList<AiMarkupExampleDto>>(examples);
|
||||||
@@ -844,7 +847,7 @@ public class FakeKanjStore : ICardStore
|
|||||||
public Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct)
|
public Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
IReadOnlyList<CardDto> result = _cards
|
IReadOnlyList<CardDto> result = _cards
|
||||||
.Where(card => card.Col == KanbanColumns.Inbox && card.SourceMsg != string.Empty)
|
.Where(card => card.Col == KanbanColumns.Inbox && (card.Content.Text ?? string.Empty) != string.Empty)
|
||||||
.OrderByDescending(card => card.ReceivedAtMs)
|
.OrderByDescending(card => card.ReceivedAtMs)
|
||||||
.ToList();
|
.ToList();
|
||||||
return Task.FromResult(result);
|
return Task.FromResult(result);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
@@ -174,7 +175,7 @@ public sealed class AiClassifyContextBuilderTests
|
|||||||
string board,
|
string board,
|
||||||
string text)
|
string text)
|
||||||
{
|
{
|
||||||
kanj.SeedCard(new CardDto { Id = cardId, Col = board, SourceMsg = text });
|
kanj.SeedCard(new CardDto { Id = cardId, Col = board, Content = new SourceContent { Text = text } });
|
||||||
kanj.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult();
|
kanj.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
@@ -45,12 +46,11 @@ public class FakePipelineStore : IPipelineStore
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<bool> ExistsDuplicateAsync(
|
public Task<bool> ExistsDuplicateAsync(
|
||||||
string dialogId,
|
SourceRef source,
|
||||||
long? msgId,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
bool found = msgId is not null
|
string key = source.DedupeKey();
|
||||||
&& _queue.Any(item => item.DialogId == dialogId && item.MsgId == msgId);
|
bool found = _queue.Any(item => item.Source.DedupeKey() == key);
|
||||||
return Task.FromResult(found);
|
return Task.FromResult(found);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ public class FakePipelineStore : IPipelineStore
|
|||||||
.Where(item => LikeField(item.Text, query)
|
.Where(item => LikeField(item.Text, query)
|
||||||
|| LikeField(item.Reason, query)
|
|| LikeField(item.Reason, query)
|
||||||
|| LikeField(item.Kw, query)
|
|| LikeField(item.Kw, query)
|
||||||
|| LikeField(item.Channel.Name, query))
|
|| LikeField(item.Source.DisplayName ?? string.Empty, query))
|
||||||
.OrderByDescending(item => item.RejectedAtMs)
|
.OrderByDescending(item => item.RejectedAtMs)
|
||||||
.Take(limitLike)
|
.Take(limitLike)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -300,16 +300,15 @@ public class FakePipelineStore : IPipelineStore
|
|||||||
long rejectedAtMs) => new()
|
long rejectedAtMs) => new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
DialogId = record.DialogId,
|
Source = record.Source,
|
||||||
MsgId = record.MsgId,
|
Content = record.Content,
|
||||||
Text = record.Text,
|
Text = record.Text,
|
||||||
Stage = record.Stage,
|
Stage = record.Stage,
|
||||||
StageLabel = PipelineRejectConstants.StageLabel(record.Stage),
|
StageLabel = PipelineRejectConstants.StageLabel(record.Stage),
|
||||||
Reason = record.Reason,
|
Reason = record.Reason,
|
||||||
Kw = record.Kw,
|
Kw = record.Kw,
|
||||||
Source = record.Source,
|
DecidedBy = record.DecidedBy,
|
||||||
SourceLabel = PipelineRejectConstants.SourceLabel(record.Source),
|
DecidedByLabel = PipelineRejectConstants.SourceLabel(record.DecidedBy),
|
||||||
Channel = new PipelineChannelDto(record.ChannelName, record.ChannelHandle, record.ChannelHue),
|
|
||||||
MsgAtMs = record.MsgAtMs,
|
MsgAtMs = record.MsgAtMs,
|
||||||
RejectedAtMs = rejectedAtMs,
|
RejectedAtMs = rejectedAtMs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
@@ -5,6 +7,7 @@ using Deal.Modules.Pipeline.Application.Services;
|
|||||||
using Deal.Tests.Unit.Contracts;
|
using Deal.Tests.Unit.Contracts;
|
||||||
using Deal.Tests.Unit.Modules.Cards;
|
using Deal.Tests.Unit.Modules.Cards;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Kanban;
|
namespace Deal.Tests.Unit.Modules.Kanban;
|
||||||
|
|
||||||
@@ -30,31 +33,26 @@ public sealed class MlReviewServiceTests
|
|||||||
private static QueueItemDto QueueRow(
|
private static QueueItemDto QueueRow(
|
||||||
long msgId,
|
long msgId,
|
||||||
string text,
|
string text,
|
||||||
string dialog = Dialog) => new()
|
string dialog = Dialog) => SourceItemFactory.Queue(
|
||||||
{
|
$"p_{msgId}",
|
||||||
Id = $"p_{msgId}",
|
text,
|
||||||
DialogId = dialog,
|
PipelineQueueStatuses.New,
|
||||||
MsgId = msgId,
|
origin: dialog,
|
||||||
Text = text,
|
externalId: msgId.ToString(CultureInfo.InvariantCulture),
|
||||||
Status = PipelineQueueStatuses.New,
|
msgAtMs: 1_700_000_000_000 + msgId);
|
||||||
Channel = new PipelineChannelDto("Канал", "ch", "#333"),
|
|
||||||
MsgAtMs = 1_700_000_000_000 + msgId,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static RejectedItemDto RejectedRow(
|
private static RejectedItemDto RejectedRow(
|
||||||
long msgId,
|
long msgId,
|
||||||
string text,
|
string text,
|
||||||
string dialog = Dialog) => new()
|
string dialog = Dialog) => SourceItemFactory.Rejected(
|
||||||
{
|
$"r_{msgId}",
|
||||||
Id = $"r_{msgId}",
|
text,
|
||||||
DialogId = dialog,
|
stage: "spam_ml",
|
||||||
MsgId = msgId,
|
decidedBy: "ml",
|
||||||
Text = text,
|
reason: "спам",
|
||||||
Stage = "spam_ml",
|
origin: dialog,
|
||||||
Reason = "спам",
|
externalId: msgId.ToString(CultureInfo.InvariantCulture),
|
||||||
Source = "ml",
|
msgAtMs: 1_700_000_000_000 + msgId);
|
||||||
MsgAtMs = 1_700_000_000_000 + msgId,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static CardDto Card(
|
private static CardDto Card(
|
||||||
long msgId,
|
long msgId,
|
||||||
@@ -65,9 +63,10 @@ public sealed class MlReviewServiceTests
|
|||||||
Id = $"c_{msgId}",
|
Id = $"c_{msgId}",
|
||||||
Col = col,
|
Col = col,
|
||||||
Title = text,
|
Title = text,
|
||||||
SourceMsg = text,
|
Source = SourceItemFactory.Source(
|
||||||
SourceDialogId = dialog,
|
origin: dialog,
|
||||||
SourceMsgId = msgId,
|
externalId: msgId.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
Content = SourceItemFactory.Content(text),
|
||||||
ReceivedAtMs = 1_700_000_000_000 + msgId,
|
ReceivedAtMs = 1_700_000_000_000 + msgId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,9 +83,9 @@ public sealed class MlReviewServiceTests
|
|||||||
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(3, items.Count);
|
Assert.Equal(3, items.Count);
|
||||||
Assert.Contains(items, item => item.Id == 101 && item.Verdict == MlReviewService.VerdictQueued && !item.Lead);
|
Assert.Contains(items, item => item.Source.ExternalId == "101" && item.Verdict == MlReviewService.VerdictQueued && !item.Lead);
|
||||||
Assert.Contains(items, item => item.Id == 102 && item.Verdict == MlReviewService.VerdictRejected);
|
Assert.Contains(items, item => item.Source.ExternalId == "102" && item.Verdict == MlReviewService.VerdictRejected);
|
||||||
MlCandidateDto cardItem = Assert.Single(items, item => item.Id == 103);
|
MlCandidateDto cardItem = Assert.Single(items, item => item.Source.ExternalId == "103");
|
||||||
Assert.True(cardItem.Lead);
|
Assert.True(cardItem.Lead);
|
||||||
Assert.Equal(MlReviewService.VerdictCard, cardItem.Verdict);
|
Assert.Equal(MlReviewService.VerdictCard, cardItem.Verdict);
|
||||||
Assert.Equal("inbox", cardItem.Col);
|
Assert.Equal("inbox", cardItem.Col);
|
||||||
@@ -103,7 +102,7 @@ public sealed class MlReviewServiceTests
|
|||||||
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
||||||
|
|
||||||
MlCandidateDto only = Assert.Single(items);
|
MlCandidateDto only = Assert.Single(items);
|
||||||
Assert.Equal(101, only.Id);
|
Assert.Equal("101", only.Source.ExternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -186,7 +185,7 @@ public sealed class MlReviewServiceTests
|
|||||||
Assert.Empty(pipeline.Queue);
|
Assert.Empty(pipeline.Queue);
|
||||||
RejectedItemDto rejected = Assert.Single(pipeline.Rejected);
|
RejectedItemDto rejected = Assert.Single(pipeline.Rejected);
|
||||||
Assert.Equal("spam_ml", rejected.Stage);
|
Assert.Equal("spam_ml", rejected.Stage);
|
||||||
Assert.Equal("ml", rejected.Source);
|
Assert.Equal("ml", rejected.DecidedBy);
|
||||||
Assert.Contains(("рекламный текст", "spam", 1.0), ml.Pushed);
|
Assert.Contains(("рекламный текст", "spam", 1.0), ml.Pushed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
|
|
||||||
@@ -10,17 +11,17 @@ public sealed class SuggestHeuristicsTests
|
|||||||
{
|
{
|
||||||
// Карточка «Неразобранного» с текстом (helper; время — порядок окна, по умолчанию 0).
|
// Карточка «Неразобранного» с текстом (helper; время — порядок окна, по умолчанию 0).
|
||||||
// id: Id карточки.
|
// id: Id карточки.
|
||||||
// sourceMsg: Исходное сообщение (source_msg).
|
// sourceText: Исходное сообщение.
|
||||||
// receivedAtMs: Время получения, epoch-ms.
|
// receivedAtMs: Время получения, epoch-ms.
|
||||||
// Возвращает: Карточка для входа PlanColumns.
|
// Возвращает: Карточка для входа PlanColumns.
|
||||||
private static CardDto InboxCard(
|
private static CardDto InboxCard(
|
||||||
string id,
|
string id,
|
||||||
string sourceMsg,
|
string sourceText,
|
||||||
long receivedAtMs = 0) => new()
|
long receivedAtMs = 0) => new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Col = KanbanColumns.Inbox,
|
Col = KanbanColumns.Inbox,
|
||||||
SourceMsg = sourceMsg,
|
Content = new SourceContent { Text = sourceText },
|
||||||
ReceivedAtMs = receivedAtMs,
|
ReceivedAtMs = receivedAtMs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Kanban;
|
using Deal.Tests.Unit.Modules.Kanban;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Pipeline;
|
namespace Deal.Tests.Unit.Modules.Pipeline;
|
||||||
|
|
||||||
@@ -19,42 +21,43 @@ public sealed class PipelineIngestServiceTests
|
|||||||
|
|
||||||
PipelineIngestResultDto result = await service.EnqueueAsync(
|
PipelineIngestResultDto result = await service.EnqueueAsync(
|
||||||
Message(
|
Message(
|
||||||
dialogId: "d_1",
|
" Нужен Python-разработчик, удалёнка ",
|
||||||
msgId: 42,
|
origin: "d_1",
|
||||||
text: " Нужен Python-разработчик, удалёнка ",
|
externalId: "42",
|
||||||
channelName: "Канал",
|
name: "Канал",
|
||||||
channelHandle: "ch",
|
hue: "#ababab",
|
||||||
channelHue: "#ababab",
|
receivedAt: DateTimeOffset.FromUnixTimeMilliseconds(1_000)),
|
||||||
msgAtMs: 1_000),
|
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.StartsWith(PipelineIdPrefixes.Queue, result.Id);
|
Assert.StartsWith(PipelineIdPrefixes.Queue, result.Id);
|
||||||
Assert.False(result.Duplicate);
|
Assert.False(result.Duplicate);
|
||||||
QueueItemDto item = Assert.Single(store.Queue);
|
QueueItemDto item = Assert.Single(store.Queue);
|
||||||
Assert.Equal(result.Id, item.Id);
|
Assert.Equal(result.Id, item.Id);
|
||||||
Assert.Equal("d_1", item.DialogId);
|
Assert.Equal("d_1", item.Source.OriginRef);
|
||||||
Assert.Equal(42, item.MsgId);
|
Assert.Equal("42", item.Source.ExternalId);
|
||||||
Assert.Equal("Нужен Python-разработчик, удалёнка", item.Text); // trim краёв (enqueue L68)
|
Assert.Equal("Канал", item.Source.DisplayName);
|
||||||
|
Assert.Equal("#ababab", item.Source.ResolveHue());
|
||||||
|
Assert.Equal("Нужен Python-разработчик, удалёнка", item.Content.Text); // trim краёв (enqueue L68)
|
||||||
|
Assert.Equal("Нужен Python-разработчик, удалёнка", item.Text);
|
||||||
Assert.Equal(PipelineQueueStatuses.New, item.Status);
|
Assert.Equal(PipelineQueueStatuses.New, item.Status);
|
||||||
Assert.Equal("Канал", item.Channel.Name);
|
|
||||||
Assert.Equal("ch", item.Channel.Handle);
|
|
||||||
Assert.Equal("#ababab", item.Channel.Hue);
|
|
||||||
Assert.Equal(1_000, item.MsgAtMs);
|
Assert.Equal(1_000, item.MsgAtMs);
|
||||||
Assert.True(item.QueuedAtMs > 0, "queuedAt = now (CreatedAt/UpdatedAt строки)");
|
Assert.True(item.QueuedAtMs > 0, "queuedAt = now (CreatedAt/UpdatedAt строки)");
|
||||||
Assert.False(item.Force);
|
Assert.False(item.Force);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EnqueueAsync_MsgAtAbsent_UsesNow()
|
public async Task EnqueueAsync_MsgAtAbsent_ZeroMsgAtAndQueuedNow()
|
||||||
{
|
{
|
||||||
FakePipelineStore store = new();
|
FakePipelineStore store = new();
|
||||||
PipelineIngestService service = new(store);
|
PipelineIngestService service = new(store);
|
||||||
|
|
||||||
await service.EnqueueAsync(Message(text: "Сообщение без времени"), CancellationToken.None);
|
await service.EnqueueAsync(
|
||||||
|
Message("Сообщение без времени", receivedAt: default(DateTimeOffset)),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
QueueItemDto item = Assert.Single(store.Queue);
|
QueueItemDto item = Assert.Single(store.Queue);
|
||||||
Assert.True(item.MsgAtMs > 0, "msgAt = now, если время сообщения не передано (Ruling 2)");
|
Assert.Equal(0, item.MsgAtMs); // нет времени источника — нулевая отметка (Ruling 2)
|
||||||
Assert.Equal(item.MsgAtMs, item.QueuedAtMs);
|
Assert.True(item.QueuedAtMs > 0, "queuedAt = now");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -63,12 +66,12 @@ public sealed class PipelineIngestServiceTests
|
|||||||
FakePipelineStore store = new();
|
FakePipelineStore store = new();
|
||||||
PipelineIngestService service = new(store);
|
PipelineIngestService service = new(store);
|
||||||
|
|
||||||
await service.EnqueueAsync(Message(text: "Возврат из отсева", force: true), CancellationToken.None);
|
await service.EnqueueAsync(Message("Возврат из отсева", force: true), CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(Assert.Single(store.Queue).Force);
|
Assert.True(Assert.Single(store.Queue).Force);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Приём: no-op (пустой текст / нет диалога) ──────────────────────────
|
// ─── Приём: no-op (пустой текст) ────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EnqueueAsync_EmptyOrWhitespaceText_Noop()
|
public async Task EnqueueAsync_EmptyOrWhitespaceText_Noop()
|
||||||
@@ -85,63 +88,53 @@ public sealed class PipelineIngestServiceTests
|
|||||||
Assert.Empty(store.Queue); // пустой текст — no-op (enqueue L69)
|
Assert.Empty(store.Queue); // пустой текст — no-op (enqueue L69)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Приём: дубль-гвард (ключ записи источника) ─────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EnqueueAsync_MissingDialogId_Noop()
|
public async Task EnqueueAsync_SameSourceTwice_SecondIsDuplicate()
|
||||||
{
|
{
|
||||||
FakePipelineStore store = new();
|
FakePipelineStore store = new();
|
||||||
PipelineIngestService service = new(store);
|
PipelineIngestService service = new(store);
|
||||||
|
QueuedMessage first = Message("Сообщение", origin: "d_1", externalId: "7");
|
||||||
PipelineIngestResultDto result = await service.EnqueueAsync(Message(text: "Текст без диалога", dialogId: string.Empty), CancellationToken.None);
|
QueuedMessage second = Message("Сообщение снова", origin: "d_1", externalId: "7");
|
||||||
|
|
||||||
Assert.Null(result.Id);
|
|
||||||
Assert.Empty(store.Queue); // нет dialogId — no-op (enqueue L69–70)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Приём: дубль-гвард (dialogId + msgId) ──────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task EnqueueAsync_SameDialogAndMsgIdTwice_SecondIsDuplicate()
|
|
||||||
{
|
|
||||||
FakePipelineStore store = new();
|
|
||||||
PipelineIngestService service = new(store);
|
|
||||||
QueuedMessage first = Message(dialogId: "d_1", msgId: 7, text: "Сообщение");
|
|
||||||
QueuedMessage second = Message(dialogId: "d_1", msgId: 7, text: "Сообщение снова");
|
|
||||||
|
|
||||||
PipelineIngestResultDto accepted = await service.EnqueueAsync(first, CancellationToken.None);
|
PipelineIngestResultDto accepted = await service.EnqueueAsync(first, CancellationToken.None);
|
||||||
PipelineIngestResultDto duplicate = await service.EnqueueAsync(second, CancellationToken.None);
|
PipelineIngestResultDto duplicate = await service.EnqueueAsync(second, CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(accepted.Id);
|
Assert.NotNull(accepted.Id);
|
||||||
Assert.Null(duplicate.Id);
|
Assert.Null(duplicate.Id);
|
||||||
Assert.True(duplicate.Duplicate); // Telethon-повтор не пишем (enqueue L74–80)
|
Assert.True(duplicate.Duplicate); // повтор записи источника не пишем
|
||||||
Assert.Single(store.Queue);
|
Assert.Single(store.Queue);
|
||||||
Assert.Equal("Сообщение", Assert.Single(store.Queue).Text); // первая вставка не перезаписана
|
Assert.Equal("Сообщение", Assert.Single(store.Queue).Text); // первая вставка не перезаписана
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EnqueueAsync_SameDialogDifferentMsgIds_BothAccepted()
|
public async Task EnqueueAsync_SameSourceDifferentExternalIds_BothAccepted()
|
||||||
{
|
{
|
||||||
FakePipelineStore store = new();
|
FakePipelineStore store = new();
|
||||||
PipelineIngestService service = new(store);
|
PipelineIngestService service = new(store);
|
||||||
|
|
||||||
await service.EnqueueAsync(Message(dialogId: "d_1", msgId: 1, text: "Первое"), CancellationToken.None);
|
await service.EnqueueAsync(Message("Первое", origin: "d_1", externalId: "1"), CancellationToken.None);
|
||||||
PipelineIngestResultDto second = await service.EnqueueAsync(Message(dialogId: "d_1", msgId: 2, text: "Второе"), CancellationToken.None);
|
PipelineIngestResultDto second = await service.EnqueueAsync(Message("Второе", origin: "d_1", externalId: "2"), CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(second.Id);
|
Assert.NotNull(second.Id);
|
||||||
Assert.Equal(2, store.Queue.Count); // гвард — по паре dialog+msgId, разные сообщения проходят
|
Assert.Equal(2, store.Queue.Count); // гвард — по ключу источника, разные записи проходят
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EnqueueAsync_NoMsgId_SameDialogTwice_BothAccepted()
|
public async Task EnqueueAsync_SameOriginWithoutExternalId_SecondIsDuplicate()
|
||||||
{
|
{
|
||||||
FakePipelineStore store = new();
|
FakePipelineStore store = new();
|
||||||
PipelineIngestService service = new(store);
|
PipelineIngestService service = new(store);
|
||||||
|
|
||||||
PipelineIngestResultDto first = await service.EnqueueAsync(Message(dialogId: "d_1", text: "Первое"), CancellationToken.None);
|
PipelineIngestResultDto first = await service.EnqueueAsync(
|
||||||
PipelineIngestResultDto second = await service.EnqueueAsync(Message(dialogId: "d_1", text: "Второе"), CancellationToken.None);
|
Message("Первое", origin: "d_1", externalId: null), CancellationToken.None);
|
||||||
|
PipelineIngestResultDto second = await service.EnqueueAsync(
|
||||||
|
Message("Второе", origin: "d_1", externalId: null), CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(first.Id);
|
Assert.NotNull(first.Id);
|
||||||
Assert.NotNull(second.Id); // дубль-гвард работает только при msgId (enqueue L75: if msg_id)
|
Assert.True(second.Duplicate); // ключ источника без внешнего id совпадает
|
||||||
Assert.Equal(2, store.Queue.Count);
|
Assert.Single(store.Queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Приём: лимит текста 6000 (text[:6000]) ─────────────────────────────
|
// ─── Приём: лимит текста 6000 (text[:6000]) ─────────────────────────────
|
||||||
@@ -153,7 +146,7 @@ public sealed class PipelineIngestServiceTests
|
|||||||
PipelineIngestService service = new(store);
|
PipelineIngestService service = new(store);
|
||||||
string longText = new string('а', 7_000);
|
string longText = new string('а', 7_000);
|
||||||
|
|
||||||
await service.EnqueueAsync(Message(text: longText), CancellationToken.None);
|
await service.EnqueueAsync(Message(longText), CancellationToken.None);
|
||||||
|
|
||||||
QueueItemDto item = Assert.Single(store.Queue);
|
QueueItemDto item = Assert.Single(store.Queue);
|
||||||
Assert.Equal(6_000, item.Text.EnumerateRunes().Count()); // python text[:6000] — кодовые точки
|
Assert.Equal(6_000, item.Text.EnumerateRunes().Count()); // python text[:6000] — кодовые точки
|
||||||
@@ -164,21 +157,10 @@ public sealed class PipelineIngestServiceTests
|
|||||||
// Команда приёма со значениями по умолчанию (сценарий теста перекрывает нужные поля).
|
// Команда приёма со значениями по умолчанию (сценарий теста перекрывает нужные поля).
|
||||||
private static QueuedMessage Message(
|
private static QueuedMessage Message(
|
||||||
string text,
|
string text,
|
||||||
string dialogId = "d_1",
|
string? origin = SourceItemFactory.Origin,
|
||||||
long? msgId = null,
|
string? externalId = SourceItemFactory.ExternalId,
|
||||||
string channelName = "",
|
string? name = SourceItemFactory.Name,
|
||||||
string channelHandle = "",
|
string? hue = SourceItemFactory.Hue,
|
||||||
string channelHue = "",
|
DateTimeOffset? receivedAt = null,
|
||||||
long? msgAtMs = null,
|
bool force = false) => SourceItemFactory.Message(text, origin, externalId, name, hue, receivedAt, force);
|
||||||
bool force = false) => new()
|
|
||||||
{
|
|
||||||
DialogId = dialogId,
|
|
||||||
MsgId = msgId,
|
|
||||||
Text = text,
|
|
||||||
ChannelName = channelName,
|
|
||||||
ChannelHandle = channelHandle,
|
|
||||||
ChannelHue = channelHue,
|
|
||||||
MsgAtMs = msgAtMs,
|
|
||||||
Force = force,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
using Deal.Tests.Unit.Contracts;
|
using Deal.Tests.Unit.Contracts;
|
||||||
using Deal.Tests.Unit.Modules.Kanban;
|
using Deal.Tests.Unit.Modules.Kanban;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Pipeline;
|
namespace Deal.Tests.Unit.Modules.Pipeline;
|
||||||
|
|
||||||
@@ -16,7 +19,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, _) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, _) = Create();
|
||||||
|
|
||||||
await service.RejectAsync(new RejectRecord { Text = " ", Source = "stop", Stage = "length" }, CancellationToken.None);
|
await service.RejectAsync(new RejectRecord { Text = " ", DecidedBy = "stop", Stage = "length" }, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Empty(store.Rejected); // record L73–74: пустой текст — no-op
|
Assert.Empty(store.Rejected); // record L73–74: пустой текст — no-op
|
||||||
}
|
}
|
||||||
@@ -28,23 +31,23 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
|
|
||||||
await service.RejectAsync(new RejectRecord
|
await service.RejectAsync(new RejectRecord
|
||||||
{
|
{
|
||||||
DialogId = "d_1",
|
Source = SourceItemFactory.Source(origin: "d_1", externalId: "7", name: string.Empty, hue: null),
|
||||||
MsgId = 7,
|
Content = SourceItemFactory.Content(new string('а', 7_000), string.Empty),
|
||||||
Text = new string('а', 7_000),
|
Text = new string('а', 7_000),
|
||||||
Reason = new string('р', 600),
|
Reason = new string('р', 600),
|
||||||
Kw = new string('к', 300),
|
Kw = new string('к', 300),
|
||||||
Source = "stop",
|
DecidedBy = "stop",
|
||||||
Stage = "stop",
|
Stage = "stop",
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
|
|
||||||
RejectedItemDto row = Assert.Single(store.Rejected);
|
RejectedItemDto row = Assert.Single(store.Rejected);
|
||||||
Assert.Equal("r_d_1_7", row.Id); // детерминированный id по dialog+msgId (Ruling 1, L77)
|
Assert.Equal("r_test_d_1_7", row.Id); // детерминированный id по источнику (Ruling 1, L77)
|
||||||
Assert.Equal(6_000, row.Text.EnumerateRunes().Count()); // text[:6000]
|
Assert.Equal(6_000, row.Text.EnumerateRunes().Count()); // text[:6000]
|
||||||
Assert.Equal(500, row.Reason.EnumerateRunes().Count()); // reason[:500]
|
Assert.Equal(500, row.Reason.EnumerateRunes().Count()); // reason[:500]
|
||||||
Assert.Equal(200, row.Kw.EnumerateRunes().Count()); // kw[:200]
|
Assert.Equal(200, row.Kw.EnumerateRunes().Count()); // kw[:200]
|
||||||
Assert.Equal("#666", row.Channel.Hue); // row.get("ch_hue") or "#666" (L93)
|
Assert.Equal("#666", row.Source.ResolveHue()); // hue не задан — дефолт (L93)
|
||||||
Assert.Equal("stop", row.Stage);
|
Assert.Equal("stop", row.Stage);
|
||||||
Assert.Equal("stop", row.Source);
|
Assert.Equal("stop", row.DecidedBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -56,7 +59,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
await service.RejectAsync(Reject("d_1", 7, stage: "stop", reason: "стоп-фраза «реклама»"), CancellationToken.None);
|
await service.RejectAsync(Reject("d_1", 7, stage: "stop", reason: "стоп-фраза «реклама»"), CancellationToken.None);
|
||||||
|
|
||||||
RejectedItemDto row = Assert.Single(store.Rejected); // повторное отбрасывание обновляет запись (L79–101)
|
RejectedItemDto row = Assert.Single(store.Rejected); // повторное отбрасывание обновляет запись (L79–101)
|
||||||
Assert.Equal("r_d_1_7", row.Id);
|
Assert.Equal("r_test_d_1_7", row.Id);
|
||||||
Assert.Equal("stop", row.Stage);
|
Assert.Equal("stop", row.Stage);
|
||||||
Assert.Equal("стоп-фраза «реклама»", row.Reason);
|
Assert.Equal("стоп-фраза «реклама»", row.Reason);
|
||||||
}
|
}
|
||||||
@@ -127,23 +130,23 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ListRejectedAsync_QueryMatchesTextReasonKwAndChannel()
|
public async Task ListRejectedAsync_QueryMatchesTextReasonKwAndSourceName()
|
||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, _) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, _) = Create();
|
||||||
store.SeedRejected(Rejected("r_text", text: "Middle Python разработчик", rejectedAtMs: 1_000));
|
store.SeedRejected(Rejected("r_text", text: "Middle Python разработчик", rejectedAtMs: 1_000));
|
||||||
store.SeedRejected(Rejected("r_reason", reason: "стоп-фраза «реклама»", rejectedAtMs: 2_000));
|
store.SeedRejected(Rejected("r_reason", reason: "стоп-фраза «реклама»", rejectedAtMs: 2_000));
|
||||||
store.SeedRejected(Rejected("r_kw", kw: "спам", rejectedAtMs: 3_000));
|
store.SeedRejected(Rejected("r_kw", kw: "спам", rejectedAtMs: 3_000));
|
||||||
store.SeedRejected(Rejected("r_ch", text: "обычный текст", channelName: "Хороший чат", rejectedAtMs: 4_000));
|
store.SeedRejected(Rejected("r_ch", text: "обычный текст", name: "Хороший чат", rejectedAtMs: 4_000));
|
||||||
|
|
||||||
RejectedPageDto byText = await service.ListRejectedAsync("Python", 0, 10, CancellationToken.None);
|
RejectedPageDto byText = await service.ListRejectedAsync("Python", 0, 10, CancellationToken.None);
|
||||||
RejectedPageDto byReason = await service.ListRejectedAsync("реклама", 0, 10, CancellationToken.None);
|
RejectedPageDto byReason = await service.ListRejectedAsync("реклама", 0, 10, CancellationToken.None);
|
||||||
RejectedPageDto byKw = await service.ListRejectedAsync("спам", 0, 10, CancellationToken.None);
|
RejectedPageDto byKw = await service.ListRejectedAsync("спам", 0, 10, CancellationToken.None);
|
||||||
RejectedPageDto byChannel = await service.ListRejectedAsync("хороший", 0, 10, CancellationToken.None);
|
RejectedPageDto bySource = await service.ListRejectedAsync("хороший", 0, 10, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal("r_text", Assert.Single(byText.Items).Id); // LIKE по text; регистр q не важен
|
Assert.Equal("r_text", Assert.Single(byText.Items).Id); // LIKE по text; регистр q не важен
|
||||||
Assert.Equal("r_reason", Assert.Single(byReason.Items).Id); // LIKE по reason
|
Assert.Equal("r_reason", Assert.Single(byReason.Items).Id); // LIKE по reason
|
||||||
Assert.Equal("r_kw", Assert.Single(byKw.Items).Id); // LIKE по kw
|
Assert.Equal("r_kw", Assert.Single(byKw.Items).Id); // LIKE по kw
|
||||||
Assert.Equal("r_ch", Assert.Single(byChannel.Items).Id); // LIKE по ch_name
|
Assert.Equal("r_ch", Assert.Single(bySource.Items).Id); // LIKE по подписи источника
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -193,7 +196,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
public async Task ReturnAsync_DuplicateSource_Returns400()
|
public async Task ReturnAsync_DuplicateSource_Returns400()
|
||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
||||||
store.SeedRejected(Rejected("r_1", source: "dup", stage: "dup"));
|
store.SeedRejected(Rejected("r_1", decidedBy: "dup", stage: "dup"));
|
||||||
|
|
||||||
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
||||||
|
|
||||||
@@ -225,13 +228,13 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
"r_1",
|
"r_1",
|
||||||
text: " Продвижение в каналах ",
|
text: " Продвижение в каналах ",
|
||||||
stage: "spam_ai",
|
stage: "spam_ai",
|
||||||
source: "ai",
|
decidedBy: "ai",
|
||||||
dialogId: "d_9",
|
origin: "d_9",
|
||||||
msgId: 5,
|
externalId: "5",
|
||||||
channelName: "Спам",
|
name: "Спам",
|
||||||
channelHandle: "spam_ch",
|
hue: "#ff0000",
|
||||||
channelHue: "#ff0000",
|
msgAtMs: 5_000,
|
||||||
msgAtMs: 5_000));
|
receivedAt: DateTimeOffset.FromUnixTimeMilliseconds(5_000)));
|
||||||
|
|
||||||
RejectReturnResultDto? result = await service.ReturnAsync("r_1", "не спам, вернуть", CancellationToken.None);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_1", "не спам, вернуть", CancellationToken.None);
|
||||||
|
|
||||||
@@ -255,12 +258,11 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
Assert.StartsWith(PipelineIdPrefixes.Queue, queued.Id);
|
Assert.StartsWith(PipelineIdPrefixes.Queue, queued.Id);
|
||||||
Assert.Equal(PipelineQueueStatuses.New, queued.Status);
|
Assert.Equal(PipelineQueueStatuses.New, queued.Status);
|
||||||
Assert.True(queued.Force); // возврат: причины отсева игнорируются воркером (Ruling 5/10)
|
Assert.True(queued.Force); // возврат: причины отсева игнорируются воркером (Ruling 5/10)
|
||||||
Assert.Equal("d_9", queued.DialogId);
|
Assert.Equal("d_9", queued.Source.OriginRef);
|
||||||
Assert.Equal(5, queued.MsgId);
|
Assert.Equal("5", queued.Source.ExternalId);
|
||||||
Assert.Equal("Продвижение в каналах", queued.Text);
|
Assert.Equal("Продвижение в каналах", queued.Text);
|
||||||
Assert.Equal("Спам", queued.Channel.Name);
|
Assert.Equal("Спам", queued.Source.DisplayName);
|
||||||
Assert.Equal("spam_ch", queued.Channel.Handle);
|
Assert.Equal("#ff0000", queued.Source.ResolveHue());
|
||||||
Assert.Equal("#ff0000", queued.Channel.Hue);
|
|
||||||
Assert.Equal(5_000, queued.MsgAtMs); // msg_at исходного сообщения сохраняется (L171)
|
Assert.Equal(5_000, queued.MsgAtMs); // msg_at исходного сообщения сохраняется (L171)
|
||||||
Assert.True(queued.QueuedAtMs > 0);
|
Assert.True(queued.QueuedAtMs > 0);
|
||||||
}
|
}
|
||||||
@@ -273,10 +275,11 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
"r_1",
|
"r_1",
|
||||||
text: "Короткое сообщение",
|
text: "Короткое сообщение",
|
||||||
stage: "length",
|
stage: "length",
|
||||||
source: "stop",
|
decidedBy: "stop",
|
||||||
dialogId: "d_1",
|
origin: "d_1",
|
||||||
msgId: 3,
|
externalId: "3",
|
||||||
msgAtMs: 9_000));
|
msgAtMs: 9_000,
|
||||||
|
receivedAt: DateTimeOffset.FromUnixTimeMilliseconds(9_000)));
|
||||||
|
|
||||||
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
|
||||||
|
|
||||||
@@ -294,7 +297,7 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
public async Task ReturnAsync_LongReason_TruncatedTo500CodePointsOnRecord()
|
public async Task ReturnAsync_LongReason_TruncatedTo500CodePointsOnRecord()
|
||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, _) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, _) = Create();
|
||||||
store.SeedRejected(Rejected("r_1", text: "Текст", stage: "stale", source: "stale", dialogId: "d_1"));
|
store.SeedRejected(Rejected("r_1", text: "Текст", stage: "stale", decidedBy: "stale", origin: "d_1"));
|
||||||
|
|
||||||
await service.ReturnAsync("r_1", new string('п', 700), CancellationToken.None);
|
await service.ReturnAsync("r_1", new string('п', 700), CancellationToken.None);
|
||||||
|
|
||||||
@@ -304,14 +307,19 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnAsync_NoDialogOrMsgId_EnqueuesDirectlyWithFallbackNow()
|
public async Task ReturnAsync_NoSourceInfo_EnqueuesDirectlyWithDefaultHue()
|
||||||
{
|
{
|
||||||
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
||||||
store.SeedRejected(Rejected(
|
store.SeedRejected(Rejected(
|
||||||
"r_old",
|
"r_old",
|
||||||
text: "Старая запись до сохранения dialog_id",
|
text: "Старая запись без ссылки на исходное",
|
||||||
stage: "spam_ml",
|
stage: "spam_ml",
|
||||||
source: "ml"));
|
decidedBy: "ml",
|
||||||
|
origin: string.Empty,
|
||||||
|
externalId: null,
|
||||||
|
name: string.Empty,
|
||||||
|
hue: null,
|
||||||
|
receivedAt: default(DateTimeOffset)));
|
||||||
|
|
||||||
RejectReturnResultDto? result = await service.ReturnAsync("r_old", string.Empty, CancellationToken.None);
|
RejectReturnResultDto? result = await service.ReturnAsync("r_old", string.Empty, CancellationToken.None);
|
||||||
|
|
||||||
@@ -322,10 +330,10 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
QueueItemDto queued = Assert.Single(store.Queue); // прямая вставка без ссылки на исходное (L174–192)
|
QueueItemDto queued = Assert.Single(store.Queue); // прямая вставка без ссылки на исходное (L174–192)
|
||||||
Assert.StartsWith(PipelineIdPrefixes.Queue, queued.Id);
|
Assert.StartsWith(PipelineIdPrefixes.Queue, queued.Id);
|
||||||
Assert.True(queued.Force);
|
Assert.True(queued.Force);
|
||||||
Assert.Equal(string.Empty, queued.DialogId);
|
Assert.Equal(string.Empty, queued.Source.OriginRef);
|
||||||
Assert.Null(queued.MsgId);
|
Assert.Null(queued.Source.ExternalId);
|
||||||
Assert.Equal("#666", queued.Channel.Hue); // hue не сохранён — дефолт (L181)
|
Assert.Equal("#666", queued.Source.ResolveHue()); // hue не сохранён — дефолт (L181)
|
||||||
Assert.True(queued.MsgAtMs > 0); // msg_at нет — now (row.get("msg_at") or now)
|
Assert.Equal(0, queued.MsgAtMs); // времени источника нет — нулевая отметка
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -387,63 +395,57 @@ public sealed class PipelineProcessingServiceTests
|
|||||||
string dialogId,
|
string dialogId,
|
||||||
long? msgId,
|
long? msgId,
|
||||||
string stage,
|
string stage,
|
||||||
string reason) => new()
|
string reason) => SourceItemFactory.Reject(
|
||||||
{
|
"Текст сообщения",
|
||||||
DialogId = dialogId,
|
origin: dialogId,
|
||||||
MsgId = msgId,
|
externalId: msgId?.ToString(CultureInfo.InvariantCulture),
|
||||||
Text = "Текст сообщения",
|
name: string.Empty,
|
||||||
Stage = stage,
|
hue: null,
|
||||||
Source = "stop",
|
stage: stage,
|
||||||
Reason = reason,
|
decidedBy: "stop",
|
||||||
};
|
reason: reason);
|
||||||
|
|
||||||
// Строка очереди для сидирования (id/статус/время постановки).
|
// Строка очереди для сидирования (id/статус/время постановки).
|
||||||
private static QueueItemDto QueueItem(
|
private static QueueItemDto QueueItem(
|
||||||
string id,
|
string id,
|
||||||
string status,
|
string status,
|
||||||
long queuedAtMs) => new()
|
long queuedAtMs) => SourceItemFactory.Queue(
|
||||||
{
|
id,
|
||||||
Id = id,
|
"текст",
|
||||||
DialogId = "d_1",
|
status,
|
||||||
Text = "текст",
|
msgAtMs: queuedAtMs,
|
||||||
Status = status,
|
queuedAtMs: queuedAtMs);
|
||||||
Channel = new PipelineChannelDto(string.Empty, string.Empty, string.Empty),
|
|
||||||
MsgAtMs = queuedAtMs,
|
|
||||||
QueuedAtMs = queuedAtMs,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запись отсева для сидирования (как строка БД после маппинга адаптера).
|
// Запись отсева для сидирования (как строка БД после маппинга адаптера).
|
||||||
private static RejectedItemDto Rejected(
|
private static RejectedItemDto Rejected(
|
||||||
string id,
|
string id,
|
||||||
string text = "Текст сообщения",
|
string text = "Текст сообщения",
|
||||||
string stage = "length",
|
string stage = "length",
|
||||||
string source = "stop",
|
string decidedBy = "stop",
|
||||||
string dialogId = "",
|
|
||||||
long? msgId = null,
|
|
||||||
string reason = "",
|
string reason = "",
|
||||||
string kw = "",
|
string kw = "",
|
||||||
string channelName = "",
|
string? origin = SourceItemFactory.Origin,
|
||||||
string channelHandle = "",
|
string? externalId = SourceItemFactory.ExternalId,
|
||||||
string channelHue = "",
|
string? name = SourceItemFactory.Name,
|
||||||
|
string? hue = SourceItemFactory.Hue,
|
||||||
long msgAtMs = 0,
|
long msgAtMs = 0,
|
||||||
long rejectedAtMs = 1_000,
|
long rejectedAtMs = 1_000,
|
||||||
bool returned = false,
|
bool returned = false,
|
||||||
string returnReason = "") => new()
|
string returnReason = "",
|
||||||
{
|
DateTimeOffset? receivedAt = null) => SourceItemFactory.Rejected(
|
||||||
Id = id,
|
id,
|
||||||
DialogId = dialogId,
|
text,
|
||||||
MsgId = msgId,
|
stage,
|
||||||
Text = text,
|
decidedBy,
|
||||||
Stage = stage,
|
reason,
|
||||||
StageLabel = PipelineRejectConstants.StageLabel(stage),
|
kw,
|
||||||
Reason = reason,
|
origin,
|
||||||
Kw = kw,
|
externalId,
|
||||||
Source = source,
|
name,
|
||||||
SourceLabel = PipelineRejectConstants.SourceLabel(source),
|
hue,
|
||||||
Channel = new PipelineChannelDto(channelName, channelHandle, channelHue),
|
msgAtMs,
|
||||||
MsgAtMs = msgAtMs,
|
rejectedAtMs,
|
||||||
RejectedAtMs = rejectedAtMs,
|
returned,
|
||||||
Returned = returned,
|
returnReason,
|
||||||
ReturnReason = returnReason,
|
receivedAt);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,17 +275,11 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Строка очереди (status new; сценарий перекрывает текст/время).
|
// Строка очереди (status new; сценарий перекрывает текст/время).
|
||||||
private static QueueItemDto QueueRow(string id, string text) => new()
|
private static QueueItemDto QueueRow(string id, string text) => SourceItemFactory.Queue(
|
||||||
{
|
id,
|
||||||
Id = id,
|
text,
|
||||||
DialogId = "d_1",
|
PipelineQueueStatuses.New,
|
||||||
MsgId = 100,
|
msgAtMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||||
Text = text,
|
|
||||||
Status = PipelineQueueStatuses.New,
|
|
||||||
Channel = new PipelineChannelDto("Канал А", "channel_a", "#333"),
|
|
||||||
MsgAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
||||||
QueuedAtMs = 1_700_000_000_000,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Минимальный разбор карточки (карточку домыслит CardComposer; колонка не назначена → inbox).
|
// Минимальный разбор карточки (карточку домыслит CardComposer; колонка не назначена → inbox).
|
||||||
private static AiParsedCardDto Parsed(string title, bool isVacancy = false) => new(
|
private static AiParsedCardDto Parsed(string title, bool isVacancy = false) => new(
|
||||||
@@ -306,23 +300,16 @@ public sealed class AdminTickOrchestratorTests
|
|||||||
Board: null);
|
Board: null);
|
||||||
|
|
||||||
// Запись отсева для сидирования (как строка БД после маппинга адаптера).
|
// Запись отсева для сидирования (как строка БД после маппинга адаптера).
|
||||||
private static RejectedItemDto Rejected(string id, long rejectedAtMs) => new()
|
private static RejectedItemDto Rejected(string id, long rejectedAtMs) => SourceItemFactory.Rejected(
|
||||||
{
|
id,
|
||||||
Id = id,
|
origin: string.Empty,
|
||||||
DialogId = string.Empty,
|
externalId: null,
|
||||||
Text = "Текст сообщения",
|
name: string.Empty,
|
||||||
Stage = "length",
|
hue: null,
|
||||||
StageLabel = PipelineRejectConstants.StageLabel("length"),
|
stage: "length",
|
||||||
Reason = string.Empty,
|
decidedBy: "stop",
|
||||||
Kw = string.Empty,
|
msgAtMs: 0,
|
||||||
Source = "stop",
|
rejectedAtMs: rejectedAtMs);
|
||||||
SourceLabel = PipelineRejectConstants.SourceLabel("stop"),
|
|
||||||
Channel = new PipelineChannelDto(string.Empty, string.Empty, string.Empty),
|
|
||||||
MsgAtMs = 0,
|
|
||||||
RejectedAtMs = rejectedAtMs,
|
|
||||||
Returned = false,
|
|
||||||
ReturnReason = string.Empty,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Вычитывает все события канала: (тип, JSON) в порядке публикации.
|
// Вычитывает все события канала: (тип, JSON) в порядке публикации.
|
||||||
// subscription: Подписка канала тенанта.
|
// subscription: Подписка канала тенанта.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
@@ -50,19 +51,19 @@ public sealed class CardReclassifierTests
|
|||||||
|
|
||||||
// Карточка «Неразобранного» с исходным текстом (поля, нужные классификации).
|
// Карточка «Неразобранного» с исходным текстом (поля, нужные классификации).
|
||||||
// id: Id карточки.
|
// id: Id карточки.
|
||||||
// sourceMsg: Исходный текст.
|
// sourceText: Исходный текст.
|
||||||
// contact: Старый контакт карточки.
|
// contact: Старый контакт карточки.
|
||||||
// receivedAtMs: Время получения (для порядка батча).
|
// receivedAtMs: Время получения (для порядка батча).
|
||||||
// Возвращает: Карточка как если бы была сохранена в БД.
|
// Возвращает: Карточка как если бы была сохранена в БД.
|
||||||
private static CardDto Card(
|
private static CardDto Card(
|
||||||
string id,
|
string id,
|
||||||
string sourceMsg,
|
string sourceText,
|
||||||
string contact = "",
|
string contact = "",
|
||||||
long receivedAtMs = 1_700_000_000_000) => new()
|
long receivedAtMs = 1_700_000_000_000) => new()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Col = "inbox",
|
Col = "inbox",
|
||||||
SourceMsg = sourceMsg,
|
Content = new SourceContent { Text = sourceText },
|
||||||
Contact = contact,
|
Contact = contact,
|
||||||
ReceivedAtMs = receivedAtMs,
|
ReceivedAtMs = receivedAtMs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Deal.Contracts.Integrations.Models;
|
using Deal.Contracts.Integrations.Models;
|
||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Kanban.Application.Services;
|
using Deal.Modules.Kanban.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
@@ -34,7 +35,7 @@ public sealed class LocalColumnSuggesterTests
|
|||||||
Id = id,
|
Id = id,
|
||||||
Col = KanbanColumns.Inbox,
|
Col = KanbanColumns.Inbox,
|
||||||
IsNew = true,
|
IsNew = true,
|
||||||
SourceMsg = text,
|
Content = new SourceContent { Text = text },
|
||||||
ReceivedAtMs = receivedAtMs,
|
ReceivedAtMs = receivedAtMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,9 +226,9 @@ public sealed class LocalColumnSuggesterTests
|
|||||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||||
SeedInbox("l_1", "нужен python", store);
|
SeedInbox("l_1", "нужен python", store);
|
||||||
SeedInbox("l_2", "нужен vue", store);
|
SeedInbox("l_2", "нужен vue", store);
|
||||||
store.SeedCard(new CardDto { Id = "l_t1", Col = KanbanColumns.Trash, SourceMsg = "ищу такси" });
|
store.SeedCard(new CardDto { Id = "l_t1", Col = KanbanColumns.Trash, Content = new SourceContent { Text = "ищу такси" } });
|
||||||
store.SeedCard(new CardDto { Id = "l_t2", Col = KanbanColumns.Trash, SourceMsg = "ищу такси" });
|
store.SeedCard(new CardDto { Id = "l_t2", Col = KanbanColumns.Trash, Content = new SourceContent { Text = "ищу такси" } });
|
||||||
store.SeedCard(new CardDto { Id = "l_a1", Col = KanbanColumns.Archive, SourceMsg = "ищу такси" });
|
store.SeedCard(new CardDto { Id = "l_a1", Col = KanbanColumns.Archive, Content = new SourceContent { Text = "ищу такси" } });
|
||||||
|
|
||||||
SuggestKeywordsResultDto result = await suggester.SuggestKeywordsAsync(CancellationToken.None);
|
SuggestKeywordsResultDto result = await suggester.SuggestKeywordsAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -225,17 +225,12 @@ public sealed class PipelineWorkerSchedulerTests
|
|||||||
private static QueueItemDto QueueRow(
|
private static QueueItemDto QueueRow(
|
||||||
string id,
|
string id,
|
||||||
string text,
|
string text,
|
||||||
string dialogId) => new()
|
string dialogId) => SourceItemFactory.Queue(
|
||||||
{
|
id,
|
||||||
Id = id,
|
text,
|
||||||
DialogId = dialogId,
|
PipelineQueueStatuses.New,
|
||||||
MsgId = 100,
|
origin: dialogId,
|
||||||
Text = text,
|
msgAtMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||||
Status = PipelineQueueStatuses.New,
|
|
||||||
Channel = new PipelineChannelDto("Канал", "channel", "#333"),
|
|
||||||
MsgAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
||||||
QueuedAtMs = 1_700_000_000_000,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Минимальный разбор карточки (карточку домыслит CardComposer; колонка не назначена → inbox).
|
// Минимальный разбор карточки (карточку домыслит CardComposer; колонка не назначена → inbox).
|
||||||
private static AiParsedCardDto Parsed(string title, bool isVacancy = false) => new(
|
private static AiParsedCardDto Parsed(string title, bool isVacancy = false) => new(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,201 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сборка записей источника и строк конвейера для тестов.
|
||||||
|
/// </summary>
|
||||||
|
public static class SourceItemFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Вид источника по умолчанию для сценариев.
|
||||||
|
/// </summary>
|
||||||
|
public const string Kind = "test";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Оригинал источника по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public const string Origin = "d_1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Внешний id записи по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public const string ExternalId = "100";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подпись источника по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public const string Name = "Канал";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Цвет источника по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public const string Hue = "#333";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Время получения записи по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DateTimeOffset ReceivedAt = DateTimeOffset.FromUnixTimeMilliseconds(1_700_000_000_000);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ссылка на источник; сценарий перекрывает нужные поля.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="origin">Оригинал (url/путь/диалог); null — без оригинала.</param>
|
||||||
|
/// <param name="externalId">Внешний id записи; null — записи без id.</param>
|
||||||
|
/// <param name="name">Подпись источника; null — без подписи.</param>
|
||||||
|
/// <param name="hue">Цвет источника; null — без цвета.</param>
|
||||||
|
/// <param name="kind">Вид источника.</param>
|
||||||
|
/// <param name="receivedAt">Время получения; null — значение по умолчанию.</param>
|
||||||
|
/// <returns>Ссылка на источник.</returns>
|
||||||
|
public static SourceRef Source(
|
||||||
|
string? origin = Origin,
|
||||||
|
string? externalId = ExternalId,
|
||||||
|
string? name = Name,
|
||||||
|
string? hue = Hue,
|
||||||
|
string kind = Kind,
|
||||||
|
DateTimeOffset? receivedAt = null)
|
||||||
|
{
|
||||||
|
IReadOnlyDictionary<string, string>? extra = hue is null
|
||||||
|
? null
|
||||||
|
: new Dictionary<string, string> { [SourceRefs.HueKey] = hue };
|
||||||
|
return new SourceRef
|
||||||
|
{
|
||||||
|
Kind = kind,
|
||||||
|
OriginRef = origin,
|
||||||
|
ExternalId = externalId,
|
||||||
|
DisplayName = name,
|
||||||
|
ReceivedAt = receivedAt ?? ReceivedAt,
|
||||||
|
Extra = extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Содержимое записи источника.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">Основной текст.</param>
|
||||||
|
/// <param name="author">Отправитель; null — подпись источника по умолчанию.</param>
|
||||||
|
/// <returns>Содержимое записи.</returns>
|
||||||
|
public static SourceContent Content(string? text, string? author = null) => new()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Author = author ?? Name,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запись источника: ссылка + содержимое.
|
||||||
|
/// </summary>
|
||||||
|
public static SourceItem Item(
|
||||||
|
string? text,
|
||||||
|
string? origin = Origin,
|
||||||
|
string? externalId = ExternalId,
|
||||||
|
string? name = Name,
|
||||||
|
string? hue = Hue,
|
||||||
|
DateTimeOffset? receivedAt = null) => new()
|
||||||
|
{
|
||||||
|
Source = Source(origin, externalId, name, hue, Kind, receivedAt),
|
||||||
|
Content = Content(text, name),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Команда приёма записи.
|
||||||
|
/// </summary>
|
||||||
|
public static QueuedMessage Message(
|
||||||
|
string? text,
|
||||||
|
string? origin = Origin,
|
||||||
|
string? externalId = ExternalId,
|
||||||
|
string? name = Name,
|
||||||
|
string? hue = Hue,
|
||||||
|
DateTimeOffset? receivedAt = null,
|
||||||
|
bool force = false) => new()
|
||||||
|
{
|
||||||
|
Item = Item(text, origin, externalId, name, hue, receivedAt),
|
||||||
|
Force = force,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Строка очереди для сидирования.
|
||||||
|
/// </summary>
|
||||||
|
public static QueueItemDto Queue(
|
||||||
|
string id,
|
||||||
|
string? text,
|
||||||
|
string status,
|
||||||
|
string? origin = Origin,
|
||||||
|
string? externalId = ExternalId,
|
||||||
|
string? name = Name,
|
||||||
|
string? hue = Hue,
|
||||||
|
long msgAtMs = 1_700_000_000_000,
|
||||||
|
long queuedAtMs = 1_700_000_000_000,
|
||||||
|
bool force = false) => new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Source = Source(origin, externalId, name, hue),
|
||||||
|
Content = Content(text, name),
|
||||||
|
Text = text ?? string.Empty,
|
||||||
|
Status = status,
|
||||||
|
MsgAtMs = msgAtMs,
|
||||||
|
QueuedAtMs = queuedAtMs,
|
||||||
|
Force = force,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Команда записи отсева.
|
||||||
|
/// </summary>
|
||||||
|
public static RejectRecord Reject(
|
||||||
|
string? text,
|
||||||
|
string? origin = Origin,
|
||||||
|
string? externalId = ExternalId,
|
||||||
|
string? name = Name,
|
||||||
|
string? hue = Hue,
|
||||||
|
string stage = "length",
|
||||||
|
string decidedBy = "stop",
|
||||||
|
string reason = "",
|
||||||
|
string kw = "",
|
||||||
|
long msgAtMs = 0) => new()
|
||||||
|
{
|
||||||
|
Source = Source(origin, externalId, name, hue),
|
||||||
|
Content = Content(text, name),
|
||||||
|
Text = text ?? string.Empty,
|
||||||
|
Stage = stage,
|
||||||
|
DecidedBy = decidedBy,
|
||||||
|
Reason = reason,
|
||||||
|
Kw = kw,
|
||||||
|
MsgAtMs = msgAtMs,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запись отсева для сидирования.
|
||||||
|
/// </summary>
|
||||||
|
public static RejectedItemDto Rejected(
|
||||||
|
string id,
|
||||||
|
string? text = "Текст сообщения",
|
||||||
|
string stage = "length",
|
||||||
|
string decidedBy = "stop",
|
||||||
|
string reason = "",
|
||||||
|
string kw = "",
|
||||||
|
string? origin = Origin,
|
||||||
|
string? externalId = ExternalId,
|
||||||
|
string? name = Name,
|
||||||
|
string? hue = Hue,
|
||||||
|
long msgAtMs = 0,
|
||||||
|
long rejectedAtMs = 1_000,
|
||||||
|
bool returned = false,
|
||||||
|
string returnReason = "",
|
||||||
|
DateTimeOffset? receivedAt = null) => new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Source = Source(origin, externalId, name, hue, Kind, receivedAt),
|
||||||
|
Content = Content(text, name),
|
||||||
|
Text = text ?? string.Empty,
|
||||||
|
Stage = stage,
|
||||||
|
StageLabel = PipelineRejectConstants.StageLabel(stage),
|
||||||
|
Reason = reason,
|
||||||
|
Kw = kw,
|
||||||
|
DecidedBy = decidedBy,
|
||||||
|
DecidedByLabel = PipelineRejectConstants.SourceLabel(decidedBy),
|
||||||
|
MsgAtMs = msgAtMs,
|
||||||
|
RejectedAtMs = rejectedAtMs,
|
||||||
|
Returned = returned,
|
||||||
|
ReturnReason = returnReason,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тесты производных полей ссылки на источник — SourceRefs.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SourceRefsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ResolveHue_NoExtra_ReturnsDefault()
|
||||||
|
{
|
||||||
|
SourceRef source = SourceItemFactory.Source(hue: null);
|
||||||
|
|
||||||
|
Assert.Equal(SourceRefs.DefaultHue, source.ResolveHue());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveHue_EmptyOrWhitespaceExtra_ReturnsDefault()
|
||||||
|
{
|
||||||
|
SourceRef empty = SourceItemFactory.Source(hue: string.Empty);
|
||||||
|
SourceRef whitespace = SourceItemFactory.Source(hue: " ");
|
||||||
|
|
||||||
|
Assert.Equal(SourceRefs.DefaultHue, empty.ResolveHue());
|
||||||
|
Assert.Equal(SourceRefs.DefaultHue, whitespace.ResolveHue());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveHue_ExtraHue_ReturnsIt()
|
||||||
|
{
|
||||||
|
SourceRef source = SourceItemFactory.Source(hue: "#ababab");
|
||||||
|
|
||||||
|
Assert.Equal("#ababab", source.ResolveHue());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DedupeKey_JoinsKindOriginAndExternalId()
|
||||||
|
{
|
||||||
|
SourceRef source = SourceItemFactory.Source(origin: "d_1", externalId: "42", kind: "test");
|
||||||
|
|
||||||
|
Assert.Equal("test|d_1|42", source.DedupeKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Api.Events;
|
using Deal.Api.Events;
|
||||||
using Deal.Grpc.Telegram;
|
using Deal.Grpc.Telegram;
|
||||||
|
using Deal.Modules.Cards.Application.Sources;
|
||||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
@@ -54,12 +55,12 @@ public sealed class TelegramIngressServiceTests
|
|||||||
Assert.False(reply.Duplicate);
|
Assert.False(reply.Duplicate);
|
||||||
|
|
||||||
QueueItemDto row = Assert.Single(store.Queue);
|
QueueItemDto row = Assert.Single(store.Queue);
|
||||||
Assert.Equal(DialogId, row.DialogId);
|
Assert.Equal("telegram", row.Source.Kind);
|
||||||
Assert.Equal(7, row.MsgId);
|
Assert.Equal(DialogId, row.Source.OriginRef);
|
||||||
|
Assert.Equal("7", row.Source.ExternalId);
|
||||||
Assert.Equal("Вакансия Python-разработчика", row.Text);
|
Assert.Equal("Вакансия Python-разработчика", row.Text);
|
||||||
Assert.Equal("Канал", row.Channel.Name);
|
Assert.Equal("Канал", row.Source.DisplayName);
|
||||||
Assert.Equal("kanal_handle", row.Channel.Handle);
|
Assert.Equal("#a33", row.Source.ResolveHue());
|
||||||
Assert.Equal("#a33", row.Channel.Hue);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
colMeta,
|
colMeta,
|
||||||
fmtDateTime,
|
fmtDateTime,
|
||||||
} from '../../store.js'
|
} from '../../store.js'
|
||||||
import { cleanTitle, tgSourceUrl } from '../../utils.js'
|
import { cleanTitle, sourceOriginUrl } from '../../utils.js'
|
||||||
import { useCardDrag } from '../../composables/dnd.js'
|
import { useCardDrag } from '../../composables/dnd.js'
|
||||||
import Icon from '../Icon.vue'
|
import Icon from '../Icon.vue'
|
||||||
import MoveMenu from './MoveMenu.vue'
|
import MoveMenu from './MoveMenu.vue'
|
||||||
@@ -31,8 +31,8 @@ const props = defineProps({
|
|||||||
const isSelected = computed(() => props.mode === 'selected')
|
const isSelected = computed(() => props.mode === 'selected')
|
||||||
const meta = computed(() => colMeta(props.card.containerId))
|
const meta = computed(() => colMeta(props.card.containerId))
|
||||||
const clean = computed(() => cleanTitle(props.card.title))
|
const clean = computed(() => cleanTitle(props.card.title))
|
||||||
// ссылка на исходное сообщение в Telegram (null — данных для ссылки нет)
|
// ссылка на исходник (null — originRef не является абсолютным URL)
|
||||||
const sourceUrl = computed(() => tgSourceUrl(props.card))
|
const sourceUrl = computed(() => sourceOriginUrl(props.card))
|
||||||
const terminal = computed(() => meta.value?.kind === 'terminal' || !!meta.value?.policy?.isTerminal)
|
const terminal = computed(() => meta.value?.kind === 'terminal' || !!meta.value?.policy?.isTerminal)
|
||||||
// архив и корзина — не перетаскиваются и не комментируются, только восстановление
|
// архив и корзина — не перетаскиваются и не комментируются, только восстановление
|
||||||
const isService = computed(() => ['archive', 'trash'].includes(props.card.containerId))
|
const isService = computed(() => ['archive', 'trash'].includes(props.card.containerId))
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ import {
|
|||||||
fmtDateTime,
|
fmtDateTime,
|
||||||
} from '../../store.js'
|
} from '../../store.js'
|
||||||
import { CURRENCIES } from '../../data.js'
|
import { CURRENCIES } from '../../data.js'
|
||||||
import { contactInfo, renderSourceMessage, tgSourceUrl, copyText, humanSize, toFloat } from '../../utils.js'
|
import { contactInfo, sourceOriginUrl, copyText, humanSize, toFloat } from '../../utils.js'
|
||||||
|
import { FILE_ICON, FILE_STYLE } from '../ui/fileMeta.js'
|
||||||
import Icon from '../Icon.vue'
|
import Icon from '../Icon.vue'
|
||||||
import MoveMenu from './MoveMenu.vue'
|
import MoveMenu from './MoveMenu.vue'
|
||||||
import BaseModal from '../ui/BaseModal.vue'
|
import BaseModal from '../ui/BaseModal.vue'
|
||||||
import CommentSection from '../ui/CommentSection.vue'
|
import CommentSection from '../ui/CommentSection.vue'
|
||||||
|
import SourceContentView from '../ui/SourceContentView.vue'
|
||||||
import Field from '../ui/Field.vue'
|
import Field from '../ui/Field.vue'
|
||||||
|
|
||||||
const card = computed(() => cardById(state.drawerId))
|
const card = computed(() => cardById(state.drawerId))
|
||||||
@@ -88,9 +90,17 @@ function contactType(c) {
|
|||||||
function contactHref(c) {
|
function contactHref(c) {
|
||||||
return contactInfo(c?.value)?.href || null
|
return contactInfo(c?.value)?.href || null
|
||||||
}
|
}
|
||||||
const sourceUrl = computed(() => tgSourceUrl(card.value))
|
const sourceUrl = computed(() => sourceOriginUrl(card.value))
|
||||||
// HTML исходного сообщения — один раз на карточку, а не на каждый ре-рендер
|
// есть что показать в блоке исходника (текст/ссылки/контакты/вложения)
|
||||||
const sourceHtml = computed(() => renderSourceMessage(card.value?.sourceMsg || ''))
|
const hasSource = computed(() => {
|
||||||
|
const c = card.value?.content
|
||||||
|
if (!c) return false
|
||||||
|
return !!(c.text || c.links?.length || c.contacts?.length || c.data?.length)
|
||||||
|
})
|
||||||
|
const sourceSummary = computed(() => {
|
||||||
|
const name = card.value?.source?.displayName || t('drawer.kanal')
|
||||||
|
return `${name} · ${card.value?.time || ''} ${t('drawer.pokazat-tekst')}`.trim()
|
||||||
|
})
|
||||||
|
|
||||||
// ─── «Выбранные»: редактирование ─────────────────────────────────────────
|
// ─── «Выбранные»: редактирование ─────────────────────────────────────────
|
||||||
const historyReversed = computed(() => [...(card.value?.history || [])].reverse())
|
const historyReversed = computed(() => [...(card.value?.history || [])].reverse())
|
||||||
@@ -108,16 +118,6 @@ const stackInput = ref('')
|
|||||||
const tzText = ref('')
|
const tzText = ref('')
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
|
|
||||||
const FILE_ICON = { image: 'image', video: 'video', audio: 'audio', archive: 'archive', document: 'file', other: 'file' }
|
|
||||||
const FILE_STYLE = {
|
|
||||||
image: 'text-sky-400 bg-sky-400/10',
|
|
||||||
video: 'text-rose-400 bg-rose-400/10',
|
|
||||||
audio: 'text-amber-400 bg-amber-400/10',
|
|
||||||
archive: 'text-warn bg-warn/10',
|
|
||||||
document: 'text-brand bg-brand/10',
|
|
||||||
other: 'text-mid bg-white/5',
|
|
||||||
}
|
|
||||||
|
|
||||||
// страховка для ссылок из БД: открываем только http(s)
|
// страховка для ссылок из БД: открываем только http(s)
|
||||||
function safeUrl(u) {
|
function safeUrl(u) {
|
||||||
return /^https?:\/\//i.test(String(u || '')) ? u : null
|
return /^https?:\/\//i.test(String(u || '')) ? u : null
|
||||||
@@ -237,8 +237,8 @@ async function copyProjectContact() {
|
|||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-[13px] font-medium text-hi leading-tight">{{ meta?.name }}</div>
|
<div class="text-[13px] font-medium text-hi leading-tight">{{ meta?.name }}</div>
|
||||||
<div class="text-[11px] text-low num mt-0.5 flex items-center gap-1.5">
|
<div class="text-[11px] text-low num mt-0.5 flex items-center gap-1.5">
|
||||||
<span class="w-1.5 h-1.5 rounded-full inline-block" :style="{ background: card.channel?.hue }"></span>
|
<span class="w-1.5 h-1.5 rounded-full inline-block" :style="{ background: card.source?.extra?.hue || '#666' }"></span>
|
||||||
{{ card.channel?.name }} · {{ card.time }}
|
{{ card.source?.displayName }} · {{ card.time }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -516,7 +516,7 @@ async function copyProjectContact() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- сообщение-источник (дашборд) -->
|
<!-- сообщение-источник (дашборд) -->
|
||||||
<div v-if="!isSelected && card.sourceMsg" class="rounded-xl border border-edge bg-ink/40 p-3.5">
|
<div v-if="!isSelected && hasSource" class="rounded-xl border border-edge bg-ink/40 p-3.5">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<div class="text-[10.5px] uppercase tracking-wider text-low/80 font-medium">{{ $t('drawer.ishodnoe-soobshchenie') }}</div>
|
<div class="text-[10.5px] uppercase tracking-wider text-low/80 font-medium">{{ $t('drawer.ishodnoe-soobshchenie') }}</div>
|
||||||
<a
|
<a
|
||||||
@@ -525,21 +525,13 @@ async function copyProjectContact() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="ml-auto inline-flex items-center gap-1.5 h-8 px-3 rounded-lg bg-brand/12 text-brand border border-brand/25 text-[12px] font-medium hover:bg-brand/20 transition-colors"
|
class="ml-auto inline-flex items-center gap-1.5 h-8 px-3 rounded-lg bg-brand/12 text-brand border border-brand/25 text-[12px] font-medium hover:bg-brand/20 transition-colors"
|
||||||
:title="$t('drawer.otkryt-ishodnoe-soobshchenie-v-telegram')"
|
:title="$t('drawer.otkryt-ishodnoe-soobshchenie')"
|
||||||
>
|
>
|
||||||
<Icon name="external" :size="13" />
|
<Icon name="external" :size="13" />
|
||||||
{{ $t('drawer.otkryt-ishodnik') }}
|
{{ $t('drawer.otkryt-ishodnik') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<details class="group">
|
<SourceContentView :content="card.content" :summary="sourceSummary" />
|
||||||
<summary class="cursor-pointer list-none text-[11px] text-low/80 hover:text-mid transition-colors select-none">
|
|
||||||
{{ card.channel?.name || $t('drawer.kanal') }} · {{ card.time }} {{ $t('drawer.pokazat-tekst') }}
|
|
||||||
</summary>
|
|
||||||
<div
|
|
||||||
class="mt-2 rounded-lg bg-ink/60 border border-white/5 px-3.5 py-3 text-[12px] leading-relaxed text-mid tgmd whitespace-pre-wrap max-h-64 overflow-y-auto"
|
|
||||||
v-html="sourceHtml"
|
|
||||||
></div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ссылки (только «Выбранные») -->
|
<!-- ссылки (только «Выбранные») -->
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// Аватар канала/чата: инициалы + стабильный цвет по имени (без картинок).
|
// Аватар канала/чата: инициалы + стабильный цвет. Если источник передал
|
||||||
// Цвет детерминированный (hash от handle), чтобы аватар не «прыгал» между рендерами.
|
// собственный цвет (`source.extra.hue`) — используем его, иначе цвет
|
||||||
|
// детерминированный (hash от handle), чтобы аватар не «прыгал» между рендерами.
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: { type: String, default: '' },
|
name: { type: String, default: '' },
|
||||||
// handle без «@» — участвует в расчёте цвета
|
// handle без «@» — участвует в расчёте цвета, если свой цвет не задан
|
||||||
handle: { type: String, default: '' },
|
handle: { type: String, default: '' },
|
||||||
|
// цвет источника (напр. `extra.hue`); пусто — берём из hash(handle)
|
||||||
|
color: { type: String, default: '' },
|
||||||
// размер в Tailwind-классах (w-*/h-*), по умолчанию 24px
|
// размер в Tailwind-классах (w-*/h-*), по умолчанию 24px
|
||||||
size: { type: String, default: 'w-6 h-6' },
|
size: { type: String, default: 'w-6 h-6' },
|
||||||
// размер шрифта инициалов
|
// размер шрифта инициалов
|
||||||
@@ -25,16 +30,22 @@ function hue(handle) {
|
|||||||
for (const ch of handle || '?') h = (h * 31 + ch.codePointAt(0)) % 360
|
for (const ch of handle || '?') h = (h * 31 + ch.codePointAt(0)) % 360
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatarStyle = computed(() =>
|
||||||
|
props.color
|
||||||
|
? { background: props.color, color: '#fff' }
|
||||||
|
: {
|
||||||
|
background: `hsl(${hue(props.handle)} 28% 50% / 0.2)`,
|
||||||
|
color: `hsl(${hue(props.handle)} 45% 65%)`,
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
class="rounded-full grid place-items-center font-bold shrink-0"
|
class="rounded-full grid place-items-center font-bold shrink-0"
|
||||||
:class="[size, text]"
|
:class="[size, text]"
|
||||||
:style="{
|
:style="avatarStyle"
|
||||||
background: `hsl(${hue(handle)} 28% 50% / 0.2)`,
|
|
||||||
color: `hsl(${hue(handle)} 45% 65%)`,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
{{ initials(name) }}
|
{{ initials(name) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// «Подробнее» записи пайплайна (очередь/отсев): метаданные сообщения, ссылка
|
// «Подробнее» записи пайплайна (очередь/отсев): метаданные записи, ссылка
|
||||||
// на исходник в Telegram и само исходное сообщение с форматированием.
|
// на оригинал и универсальное содержимое источника. Одинаковый блок у карточек
|
||||||
// Одинаковый блок у карточек очереди и отсева — вынесен, чтобы шаблоны не
|
// очереди и отсева — вынесен, чтобы шаблоны не дублировали разметку.
|
||||||
// дублировали ~30 строк.
|
import { computed } from 'vue'
|
||||||
import Icon from '../Icon.vue'
|
import Icon from '../Icon.vue'
|
||||||
|
import SourceContentView from './SourceContentView.vue'
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
// [{ label, text, mono?, num? }] — строки метаданных (id, время…)
|
// [{ label, text, mono?, num? }] — строки метаданных (id, время…)
|
||||||
meta: { type: Array, default: () => [] },
|
meta: { type: Array, default: () => [] },
|
||||||
|
// SourceContent: текст/ссылки/контакты/вложения
|
||||||
|
content: { type: Object, default: null },
|
||||||
// HTML исходного сообщения (уже отрендерен и мемоизирован родителем)
|
// HTML исходного сообщения (уже отрендерен и мемоизирован родителем)
|
||||||
sourceHtml: { type: String, default: '' },
|
sourceHtml: { type: String, default: '' },
|
||||||
// ссылка «Открыть исходник в Telegram» (null — не показывать)
|
// ссылка на оригинал (null — не показывать)
|
||||||
sourceUrl: { type: String, default: null },
|
sourceUrl: { type: String, default: null },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// показываем блок содержимого, только если есть что рендерить
|
||||||
|
const hasContent = computed(() => {
|
||||||
|
if (props.sourceHtml) return true
|
||||||
|
const c = props.content
|
||||||
|
return !!(c && (c.text || c.links?.length || c.contacts?.length || c.data?.length))
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,16 +51,12 @@ defineProps({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details v-if="sourceHtml" class="mt-1.5 group">
|
<SourceContentView
|
||||||
<summary
|
v-if="hasContent"
|
||||||
class="cursor-pointer list-none text-[11px] text-low/70 hover:text-mid transition-colors select-none"
|
class="mt-1.5"
|
||||||
>
|
:content="content"
|
||||||
{{ $t('common.sourceMessage') }}
|
:html="sourceHtml"
|
||||||
</summary>
|
:summary="$t('common.sourceMessage')"
|
||||||
<div
|
/>
|
||||||
class="mt-1.5 rounded-lg bg-ink/60 border border-white/5 px-3 py-2.5 text-[12px] leading-relaxed text-mid tgmd whitespace-pre-wrap max-h-64 overflow-y-auto"
|
|
||||||
v-html="sourceHtml"
|
|
||||||
></div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<script setup>
|
||||||
|
// Универсальный просмотрщик исходника: текст под спойлером, ссылки, контакты и
|
||||||
|
// вложения (DataRef). Не знает про конкретные источники — работает с generic
|
||||||
|
// SourceContent. Одинаково используется в карточке и в деталях пайплайна.
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { renderSourceMessage, humanSize } from '../../utils.js'
|
||||||
|
import { fileIcon, fileStyle, safeHref } from './fileMeta.js'
|
||||||
|
import Icon from '../Icon.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// SourceContent: { text?, html?, links?, contacts?, data?, ... }
|
||||||
|
content: { type: Object, default: null },
|
||||||
|
// Заранее отрендеренный HTML текста (мемоизация у родителя); если null —
|
||||||
|
// рендерим из content.text внутри.
|
||||||
|
html: { type: String, default: null },
|
||||||
|
// Подпись спойлера с текстом («канал · время — показать текст»).
|
||||||
|
summary: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderedHtml = computed(() =>
|
||||||
|
props.html != null ? props.html : renderSourceMessage(props.content?.text || ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
const links = computed(() =>
|
||||||
|
(Array.isArray(props.content?.links) ? props.content.links : []).map((u) => String(u || '')).filter(Boolean),
|
||||||
|
)
|
||||||
|
|
||||||
|
const contacts = computed(() =>
|
||||||
|
Array.isArray(props.content?.contacts) ? props.content.contacts.filter(Boolean) : [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const attachments = computed(() => {
|
||||||
|
const list = Array.isArray(props.content?.data) ? [...props.content.data] : []
|
||||||
|
return list.sort((a, b) => (a?.order ?? 0) - (b?.order ?? 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
function isImage(d) {
|
||||||
|
return d?.kind === 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageSrc(d) {
|
||||||
|
return isImage(d) ? safeHref(d?.ref) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function telHref(phone) {
|
||||||
|
const digits = String(phone || '').replace(/[^+\d]/g, '')
|
||||||
|
return digits ? `tel:${digits}` : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDuration(sec) {
|
||||||
|
const total = Math.max(0, Math.round(Number(sec) || 0))
|
||||||
|
const m = Math.floor(total / 60)
|
||||||
|
const s = String(total % 60).padStart(2, '0')
|
||||||
|
return `${m}:${s}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentMeta(d) {
|
||||||
|
const parts = []
|
||||||
|
if (d?.mimeType) parts.push(d.mimeType)
|
||||||
|
if (d?.size) parts.push(humanSize(d.size))
|
||||||
|
if (d?.width && d?.height) parts.push(`${d.width}×${d.height}`)
|
||||||
|
if (d?.durationSec) parts.push(fmtDuration(d.durationSec))
|
||||||
|
return parts.join(' · ')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<!-- текст исходника -->
|
||||||
|
<details v-if="renderedHtml" class="group">
|
||||||
|
<summary
|
||||||
|
class="cursor-pointer list-none text-[11px] text-low/80 hover:text-mid transition-colors select-none"
|
||||||
|
>
|
||||||
|
{{ summary || $t('common.sourceMessage') }}
|
||||||
|
</summary>
|
||||||
|
<div
|
||||||
|
class="mt-2 rounded-lg bg-ink/60 border border-white/5 px-3.5 py-3 text-[12px] leading-relaxed text-mid tgmd whitespace-pre-wrap max-h-64 overflow-y-auto"
|
||||||
|
v-html="renderedHtml"
|
||||||
|
></div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- ссылки -->
|
||||||
|
<div v-if="links.length">
|
||||||
|
<div class="text-[10.5px] uppercase tracking-wider text-low/80 font-medium mb-1.5">
|
||||||
|
{{ $t('source.links') }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<a
|
||||||
|
v-for="(u, i) in links"
|
||||||
|
:key="i"
|
||||||
|
:href="safeHref(u)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="flex items-center gap-2 rounded-lg bg-ink/50 border border-white/5 px-3 py-1.5 text-[12px] text-brand hover:bg-brand/10 transition-colors"
|
||||||
|
>
|
||||||
|
<Icon name="link" :size="13" class="shrink-0" />
|
||||||
|
<span class="truncate">{{ u }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- контакты -->
|
||||||
|
<div v-if="contacts.length">
|
||||||
|
<div class="text-[10.5px] uppercase tracking-wider text-low/80 font-medium mb-1.5">
|
||||||
|
{{ $t('source.contacts') }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="(c, i) in contacts"
|
||||||
|
:key="i"
|
||||||
|
class="rounded-lg bg-ink/50 border border-white/5 px-3 py-1.5 text-[12px] flex flex-wrap items-center gap-x-3 gap-y-0.5"
|
||||||
|
>
|
||||||
|
<span class="text-hi">{{ c.name || c.kind || $t('source.contact') }}</span>
|
||||||
|
<span v-if="c.name && c.kind" class="text-[10.5px] text-low/70 uppercase tracking-wide">{{ c.kind }}</span>
|
||||||
|
<a v-if="c.email" :href="`mailto:${c.email}`" class="text-brand hover:underline">{{ c.email }}</a>
|
||||||
|
<a v-if="c.phone" :href="telHref(c.phone)" class="text-brand hover:underline">{{ c.phone }}</a>
|
||||||
|
<a
|
||||||
|
v-if="safeHref(c.url)"
|
||||||
|
:href="c.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="text-brand hover:underline truncate flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Icon name="external" :size="11" class="shrink-0" />
|
||||||
|
<span class="truncate">{{ c.url }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- вложения -->
|
||||||
|
<div v-if="attachments.length">
|
||||||
|
<div class="text-[10.5px] uppercase tracking-wider text-low/80 font-medium mb-1.5">
|
||||||
|
{{ $t('source.attachments') }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<div v-for="d in attachments" :key="d.id || d.ref" class="rounded-lg bg-ink/50 border border-white/5 overflow-hidden">
|
||||||
|
<a v-if="imageSrc(d)" :href="imageSrc(d)" target="_blank" rel="noreferrer" class="block">
|
||||||
|
<img :src="imageSrc(d)" :alt="d.fileName || d.caption || ''" class="max-h-56 w-full object-cover" />
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center gap-2.5 px-3 py-2">
|
||||||
|
<span class="w-8 h-8 rounded-lg grid place-items-center shrink-0" :class="fileStyle(d.kind)">
|
||||||
|
<Icon :name="fileIcon(d.kind)" :size="15" />
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0 flex-1">
|
||||||
|
<span class="block text-[12px] text-hi truncate">
|
||||||
|
{{ d.fileName || d.caption || $t('source.attachment') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="attachmentMeta(d)" class="block text-[10.5px] text-low mt-0.5 num">
|
||||||
|
{{ attachmentMeta(d) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
v-if="safeHref(d.ref)"
|
||||||
|
:href="d.ref"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="text-low hover:text-brand transition-colors shrink-0 p-1"
|
||||||
|
:title="$t('source.download')"
|
||||||
|
>
|
||||||
|
<Icon name="download" :size="14" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// ─── Типы вложений: иконка и цвет ─────────────────────────────────────────
|
||||||
|
// Kind заполняет сервис данных (image/video/audio/document/archive/other).
|
||||||
|
// Общая карта для файлов карточки и вложений исходника — без дублирования.
|
||||||
|
|
||||||
|
export const FILE_ICON = {
|
||||||
|
image: 'image',
|
||||||
|
video: 'video',
|
||||||
|
audio: 'audio',
|
||||||
|
archive: 'archive',
|
||||||
|
document: 'file',
|
||||||
|
other: 'file',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FILE_STYLE = {
|
||||||
|
image: 'text-sky-400 bg-sky-400/10',
|
||||||
|
video: 'text-rose-400 bg-rose-400/10',
|
||||||
|
audio: 'text-amber-400 bg-amber-400/10',
|
||||||
|
archive: 'text-warn bg-warn/10',
|
||||||
|
document: 'text-brand bg-brand/10',
|
||||||
|
other: 'text-mid bg-white/5',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileIcon(kind) {
|
||||||
|
return FILE_ICON[kind] || FILE_ICON.other
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileStyle(kind) {
|
||||||
|
return FILE_STYLE[kind] || FILE_STYLE.other
|
||||||
|
}
|
||||||
|
|
||||||
|
// Только абсолютный http(s)-URL годится для href/src (страховка от мусора).
|
||||||
|
export function safeHref(url) {
|
||||||
|
return /^https?:\/\//i.test(String(url || '')) ? url : null
|
||||||
|
}
|
||||||
@@ -148,7 +148,7 @@ export const ru = {
|
|||||||
"prev": "Назад",
|
"prev": "Назад",
|
||||||
"next": "Вперёд",
|
"next": "Вперёд",
|
||||||
"range": "{from}–{to} из {total}",
|
"range": "{from}–{to} из {total}",
|
||||||
"openSource": "Открыть исходник в Telegram",
|
"openSource": "Открыть исходник",
|
||||||
"sourceMessage": "Исходное сообщение (с форматированием)",
|
"sourceMessage": "Исходное сообщение (с форматированием)",
|
||||||
"comments": "Комментарии",
|
"comments": "Комментарии",
|
||||||
"noComments": "Комментариев пока нет",
|
"noComments": "Комментариев пока нет",
|
||||||
@@ -392,8 +392,19 @@ export const ru = {
|
|||||||
"colSaved": "Колонка «{name}» сохранена",
|
"colSaved": "Колонка «{name}» сохранена",
|
||||||
"colCreated": "Колонка «{name}» создана — заявки будут скапливаться здесь"
|
"colCreated": "Колонка «{name}» создана — заявки будут скапливаться здесь"
|
||||||
},
|
},
|
||||||
|
"source": {
|
||||||
|
"links": "Ссылки",
|
||||||
|
"contacts": "Контакты",
|
||||||
|
"contact": "Контакт",
|
||||||
|
"attachments": "Вложения",
|
||||||
|
"attachment": "Вложение",
|
||||||
|
"download": "Скачать"
|
||||||
|
},
|
||||||
"processing": {
|
"processing": {
|
||||||
"server-nedostupen-proverte-chto-backend": "Сервер недоступен — проверьте, что backend запущен (порт 8000)",
|
"server-nedostupen-proverte-chto-backend": "Сервер недоступен — проверьте, что backend запущен (порт 8000)",
|
||||||
|
"author": "Автор",
|
||||||
|
"externalId": "ID в источнике",
|
||||||
|
"originRef": "Оригинал",
|
||||||
"zapis-otseva-udalena": "Запись отсева удалена",
|
"zapis-otseva-udalena": "Запись отсева удалена",
|
||||||
"udaleno": "Удалено",
|
"udaleno": "Удалено",
|
||||||
"vozvrashcheno-v-obrabotku-prichiny-otseva": "Возвращено в обработку — причины отсева сняты",
|
"vozvrashcheno-v-obrabotku-prichiny-otseva": "Возвращено в обработку — причины отсева сняты",
|
||||||
@@ -1018,7 +1029,7 @@ export const ru = {
|
|||||||
"otkryt": "Открыть",
|
"otkryt": "Открыть",
|
||||||
"kopirovat": "Копировать",
|
"kopirovat": "Копировать",
|
||||||
"ishodnoe-soobshchenie": "Исходное сообщение",
|
"ishodnoe-soobshchenie": "Исходное сообщение",
|
||||||
"otkryt-ishodnoe-soobshchenie-v-telegram": "Открыть исходное сообщение в Telegram",
|
"otkryt-ishodnoe-soobshchenie": "Открыть исходное сообщение",
|
||||||
"otkryt-ishodnik": "Открыть исходник",
|
"otkryt-ishodnik": "Открыть исходник",
|
||||||
"pokazat-tekst": "— показать текст",
|
"pokazat-tekst": "— показать текст",
|
||||||
"udalit-ssylku": "Удалить ссылку",
|
"udalit-ssylku": "Удалить ссылку",
|
||||||
|
|||||||
+178
-182
@@ -1,183 +1,179 @@
|
|||||||
import { t } from '@/i18n/index.js'
|
import { t } from '@/i18n/index.js'
|
||||||
// ─── Мелкие утилиты: звук, буфер обмена ──────────────────────────────────
|
// ─── Мелкие утилиты: звук, буфер обмена ──────────────────────────────────
|
||||||
|
|
||||||
let audioCtx = null
|
let audioCtx = null
|
||||||
|
|
||||||
function ctx() {
|
function ctx() {
|
||||||
if (!audioCtx) {
|
if (!audioCtx) {
|
||||||
try {
|
try {
|
||||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
||||||
} catch {
|
} catch {
|
||||||
audioCtx = null
|
audioCtx = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume()
|
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume()
|
||||||
return audioCtx
|
return audioCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
function tone(freq, start, dur, vol) {
|
function tone(freq, start, dur, vol) {
|
||||||
const ac = ctx()
|
const ac = ctx()
|
||||||
if (!ac) return
|
if (!ac) return
|
||||||
const osc = ac.createOscillator()
|
const osc = ac.createOscillator()
|
||||||
const gain = ac.createGain()
|
const gain = ac.createGain()
|
||||||
osc.type = 'sine'
|
osc.type = 'sine'
|
||||||
osc.frequency.value = freq
|
osc.frequency.value = freq
|
||||||
gain.gain.setValueAtTime(0.0001, ac.currentTime + start)
|
gain.gain.setValueAtTime(0.0001, ac.currentTime + start)
|
||||||
gain.gain.exponentialRampToValueAtTime(vol, ac.currentTime + start + 0.02)
|
gain.gain.exponentialRampToValueAtTime(vol, ac.currentTime + start + 0.02)
|
||||||
gain.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + start + dur)
|
gain.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + start + dur)
|
||||||
osc.connect(gain).connect(ac.destination)
|
osc.connect(gain).connect(ac.destination)
|
||||||
osc.start(ac.currentTime + start)
|
osc.start(ac.currentTime + start)
|
||||||
osc.stop(ac.currentTime + start + dur + 0.05)
|
osc.stop(ac.currentTime + start + dur + 0.05)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Деликатный «пинг» новой карточки — два мягких тона
|
// Деликатный «пинг» новой карточки — два мягких тона
|
||||||
export function playLeadPing(volume = 0.35) {
|
export function playLeadPing(volume = 0.35) {
|
||||||
const v = Math.max(0.02, Math.min(1, volume)) * 0.35
|
const v = Math.max(0.02, Math.min(1, volume)) * 0.35
|
||||||
tone(880, 0, 0.18, v)
|
tone(880, 0, 0.18, v)
|
||||||
tone(1318.5, 0.16, 0.3, v * 0.8)
|
tone(1318.5, 0.16, 0.3, v * 0.8)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка звука в настройках
|
// Проверка звука в настройках
|
||||||
export function playTestPing(volume = 0.5) {
|
export function playTestPing(volume = 0.5) {
|
||||||
tone(660, 0, 0.12, Math.max(0.02, Math.min(1, volume)) * 0.4)
|
tone(660, 0, 0.12, Math.max(0.02, Math.min(1, volume)) * 0.4)
|
||||||
tone(990, 0.14, 0.25, Math.max(0.02, Math.min(1, volume)) * 0.3)
|
tone(990, 0.14, 0.25, Math.max(0.02, Math.min(1, volume)) * 0.3)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyText(text) {
|
export async function copyText(text) {
|
||||||
if (!text) return false
|
if (!text) return false
|
||||||
try {
|
try {
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* пробуем запасной путь */
|
/* пробуем запасной путь */
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const ta = document.createElement('textarea')
|
const ta = document.createElement('textarea')
|
||||||
ta.value = text
|
ta.value = text
|
||||||
ta.style.position = 'fixed'
|
ta.style.position = 'fixed'
|
||||||
ta.style.opacity = '0'
|
ta.style.opacity = '0'
|
||||||
document.body.appendChild(ta)
|
document.body.appendChild(ta)
|
||||||
ta.select()
|
ta.select()
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
document.body.removeChild(ta)
|
document.body.removeChild(ta)
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Рендер исходных сообщений (лёгкий Telegram/Markdown) ─────────────────
|
// ─── Рендер исходных сообщений (лёгкий Telegram/Markdown) ─────────────────
|
||||||
// Текст из каналов содержит ручную разметку: **жирный**, `код`, ```блоки```,
|
// Текст из каналов содержит ручную разметку: **жирный**, `код`, ```блоки```,
|
||||||
// [ссылка](url), ~~зачёркнуто~~, ||спойлер|| и голые ссылки.
|
// [ссылка](url), ~~зачёркнуто~~, ||спойлер|| и голые ссылки.
|
||||||
// Всё экранируется до вставки в HTML — рендер безопасен (без raw HTML).
|
// Всё экранируется до вставки в HTML — рендер безопасен (без raw HTML).
|
||||||
|
|
||||||
const _ESC_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
const _ESC_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
||||||
function _esc(s) {
|
function _esc(s) {
|
||||||
return s.replace(/[&<>"']/g, (c) => _ESC_MAP[c])
|
return s.replace(/[&<>"']/g, (c) => _ESC_MAP[c])
|
||||||
}
|
}
|
||||||
|
|
||||||
// рендер одного элемента в <a> с https-проверкой (только http/https/tg)
|
// рендер одного элемента в <a> с https-проверкой (только http/https/tg)
|
||||||
function _safeUrl(u) {
|
function _safeUrl(u) {
|
||||||
return /^(https?:|tg:)\/\//i.test(u) ? u : null
|
return /^(https?:|tg:)\/\//i.test(u) ? u : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderSourceMessage(text) {
|
export function renderSourceMessage(text) {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
let t = _esc(String(text))
|
let t = _esc(String(text))
|
||||||
const stash = []
|
const stash = []
|
||||||
const take = (html) => {
|
const take = (html) => {
|
||||||
const ph = `\u0001${stash.length}\u0001`
|
const ph = `\u0001${stash.length}\u0001`
|
||||||
stash.push(html)
|
stash.push(html)
|
||||||
return ph
|
return ph
|
||||||
}
|
}
|
||||||
// блоки кода ```…```
|
// блоки кода ```…```
|
||||||
t = t.replace(/```([^`]*)```/g, (_, code) => take(`<pre>${code}</pre>`))
|
t = t.replace(/```([^`]*)```/g, (_, code) => take(`<pre>${code}</pre>`))
|
||||||
// инлайн-код `…`
|
// инлайн-код `…`
|
||||||
t = t.replace(/`([^`\n]+)`/g, (_, c) => take(`<code>${c}</code>`))
|
t = t.replace(/`([^`\n]+)`/g, (_, c) => take(`<code>${c}</code>`))
|
||||||
// ссылки [текст](url)
|
// ссылки [текст](url)
|
||||||
t = t.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_, label, url) => {
|
t = t.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_, label, url) => {
|
||||||
const u = _safeUrl(url)
|
const u = _safeUrl(url)
|
||||||
return u ? take(`<a href="${u}" target="_blank" rel="noreferrer">${label}</a>`) : `[${label}]( ${url} )`
|
return u ? take(`<a href="${u}" target="_blank" rel="noreferrer">${label}</a>`) : `[${label}]( ${url} )`
|
||||||
})
|
})
|
||||||
// жирный **…**
|
// жирный **…**
|
||||||
t = t.replace(/\*\*([^*\n]+)\*\*/g, (_, m) => take(`<b>${m}</b>`))
|
t = t.replace(/\*\*([^*\n]+)\*\*/g, (_, m) => take(`<b>${m}</b>`))
|
||||||
// зачёркнутый ~~…~~
|
// зачёркнутый ~~…~~
|
||||||
t = t.replace(/~~([^~\n]+)~~/g, (_, m) => take(`<s>${m}</s>`))
|
t = t.replace(/~~([^~\n]+)~~/g, (_, m) => take(`<s>${m}</s>`))
|
||||||
// спойлер ||…||
|
// спойлер ||…||
|
||||||
t = t.replace(/\|\|([^|\n]+)\|\|/g, (_, m) => take(`<span class="tgmd-spoiler">${m}</span>`))
|
t = t.replace(/\|\|([^|\n]+)\|\|/g, (_, m) => take(`<span class="tgmd-spoiler">${m}</span>`))
|
||||||
// голые ссылки http(s)
|
// голые ссылки http(s)
|
||||||
t = t.replace(/(https?:\/\/[^\s<]+)/g, (u) => {
|
t = t.replace(/(https?:\/\/[^\s<]+)/g, (u) => {
|
||||||
let clean = u
|
let clean = u
|
||||||
while (/[),.;:!?]+$/.test(clean)) clean = clean.slice(0, -1)
|
while (/[),.;:!?]+$/.test(clean)) clean = clean.slice(0, -1)
|
||||||
const href = clean || u
|
const href = clean || u
|
||||||
return take(`<a href="${_esc(href)}" target="_blank" rel="noreferrer">${href}</a>`)
|
return take(`<a href="${_esc(href)}" target="_blank" rel="noreferrer">${href}</a>`)
|
||||||
})
|
})
|
||||||
// восстанавливаем отложенные фрагменты
|
// восстанавливаем отложенные фрагменты
|
||||||
return t.replace(/\u0001(\d+)\u0001/g, (_, i) => stash[+i] ?? '')
|
return t.replace(/\u0001(\d+)\u0001/g, (_, i) => stash[+i] ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function humanSize(bytes = 0) {
|
export function humanSize(bytes = 0) {
|
||||||
if (!bytes) return '—'
|
if (!bytes) return '—'
|
||||||
const units = [t('common.b'), t('common.kb'), t('common.mb'), t('common.gb')]
|
const units = [t('common.b'), t('common.kb'), t('common.mb'), t('common.gb')]
|
||||||
let v = bytes
|
let v = bytes
|
||||||
let i = 0
|
let i = 0
|
||||||
while (v >= 1024 && i < units.length - 1) {
|
while (v >= 1024 && i < units.length - 1) {
|
||||||
v /= 1024
|
v /= 1024
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
return `${v >= 100 ? Math.round(v) : Math.round(v * 10) / 10} ${units[i]}`
|
return `${v >= 100 ? Math.round(v) : Math.round(v * 10) / 10} ${units[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Контакты заказчика: по строке определяем, куда вести ────────────────
|
// ─── Контакты заказчика: по строке определяем, куда вести ────────────────
|
||||||
// Контакт может быть чем угодно: @username (Telegram), e-mail, телефон,
|
// Контакт может быть чем угодно: @username (Telegram), e-mail, телефон,
|
||||||
// whatsapp-ссылка, обычный URL.
|
// whatsapp-ссылка, обычный URL.
|
||||||
|
|
||||||
export function contactInfo(contact = '') {
|
export function contactInfo(contact = '') {
|
||||||
const raw = String(contact || '').trim()
|
const raw = String(contact || '').trim()
|
||||||
if (!raw) return null
|
if (!raw) return null
|
||||||
if (raw.startsWith('@')) {
|
if (raw.startsWith('@')) {
|
||||||
return { label: t('common.napisat-v-telegram'), href: `https://t.me/${raw.slice(1)}` }
|
return { label: t('common.napisat-v-telegram'), href: `https://t.me/${raw.slice(1)}` }
|
||||||
}
|
}
|
||||||
if (/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(raw)) {
|
if (/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(raw)) {
|
||||||
return { label: t('common.napisat-na-pochtu'), href: `mailto:${raw}` }
|
return { label: t('common.napisat-na-pochtu'), href: `mailto:${raw}` }
|
||||||
}
|
}
|
||||||
if (/^\+[\d\s\-()]{6,}$/.test(raw)) {
|
if (/^\+[\d\s\-()]{6,}$/.test(raw)) {
|
||||||
return { label: t('common.pozvonit'), href: `tel:${raw.replace(/[^+\d]/g, '')}` }
|
return { label: t('common.pozvonit'), href: `tel:${raw.replace(/[^+\d]/g, '')}` }
|
||||||
}
|
}
|
||||||
if (/^https?:\/\//i.test(raw)) {
|
if (/^https?:\/\//i.test(raw)) {
|
||||||
return { label: t('common.otkryt-ssylku'), href: raw }
|
return { label: t('common.otkryt-ssylku'), href: raw }
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ссылка на исходное сообщение в Telegram по сохранённым id (если доступна)
|
// Ссылка на исходник: отдаём `source.originRef`, только если это абсолютный
|
||||||
export function tgSourceUrl(lead) {
|
// http(s)-URL. Глубокие ссылки конкретных источников здесь не собираем — их
|
||||||
if (!lead || !lead.sourceMsgId) return null
|
// формирует сам источник и кладёт в originRef. Иначе ссылка недоступна.
|
||||||
const id = lead.sourceMsgId
|
export function sourceOriginUrl(item) {
|
||||||
const handle = lead.channel?.handle || lead.ch?.handle
|
const ref = item?.source?.originRef
|
||||||
if (handle) return `https://t.me/${handle}/${id}`
|
const url = typeof ref === 'string' ? ref.trim() : ''
|
||||||
// приватный канал: t.me/c/<peer>/<msg> (без префикса -100)
|
return /^https?:\/\//i.test(url) ? url : null
|
||||||
const dialog = String(lead.sourceDialogId || '')
|
}
|
||||||
const peer = dialog.startsWith('-100') ? dialog.slice(4) : dialog
|
|
||||||
if (/^\d+$/.test(peer)) return `https://t.me/c/${peer}/${id}`
|
// Чистый заголовок карточки: без markdown-ссылок и служебной разметки
|
||||||
return null
|
// (страховка для старых карточек из сырых сообщений).
|
||||||
}
|
export function cleanTitle(raw = '') {
|
||||||
|
return String(raw || '')
|
||||||
// Чистый заголовок карточки: без markdown-ссылок и служебной разметки
|
.replace(/\[([^\]]*)\]\([^)\s]+\)/g, '$1') // [текст](url) -> текст
|
||||||
// (страховка для старых карточек из сырых сообщений).
|
.replace(/https?:\/\/[^\s]*(?=\s|$)/g, '') // голые ссылки
|
||||||
export function cleanTitle(raw = '') {
|
.replace(/[*_`#>~]+/g, ' ')
|
||||||
return String(raw || '')
|
.replace(/\s+/g, ' ')
|
||||||
.replace(/\[([^\]]*)\]\([^)\s]+\)/g, '$1') // [текст](url) -> текст
|
.trim()
|
||||||
.replace(/https?:\/\/[^\s]*(?=\s|$)/g, '') // голые ссылки
|
}
|
||||||
.replace(/[*_`#>~]+/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
// «1 200,5» / «1 200.5» -> число (null, если не число). Парсинг сумм из полей.
|
||||||
.trim()
|
export function toFloat(v) {
|
||||||
}
|
const n = parseFloat(String(v ?? '').replace(/\s/g, '').replace(',', '.'))
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
// «1 200,5» / «1 200.5» -> число (null, если не число). Парсинг сумм из полей.
|
}
|
||||||
export function toFloat(v) {
|
|
||||||
const n = parseFloat(String(v ?? '').replace(/\s/g, '').replace(',', '.'))
|
|
||||||
return Number.isFinite(n) ? n : null
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
relTime,
|
relTime,
|
||||||
} from '../store.js'
|
} from '../store.js'
|
||||||
import Icon from '../components/Icon.vue'
|
import Icon from '../components/Icon.vue'
|
||||||
import { renderSourceMessage, tgSourceUrl } from '../utils.js'
|
import { renderSourceMessage, sourceOriginUrl } from '../utils.js'
|
||||||
import BaseModal from '../components/ui/BaseModal.vue'
|
import BaseModal from '../components/ui/BaseModal.vue'
|
||||||
import ChannelAvatar from '../components/ui/ChannelAvatar.vue'
|
import ChannelAvatar from '../components/ui/ChannelAvatar.vue'
|
||||||
import PipelineDetails from '../components/ui/PipelineDetails.vue'
|
import PipelineDetails from '../components/ui/PipelineDetails.vue'
|
||||||
@@ -50,8 +50,11 @@ function srcHtml(item) {
|
|||||||
// строки метаданных для «Подробнее» — очередь и отсев почти одинаковы
|
// строки метаданных для «Подробнее» — очередь и отсев почти одинаковы
|
||||||
function metaRows(item, extra = []) {
|
function metaRows(item, extra = []) {
|
||||||
const rows = []
|
const rows = []
|
||||||
if (item.dialogId) rows.push({ label: t('processing.dialog'), text: item.dialogId, mono: true })
|
const src = item.source || {}
|
||||||
if (item.msgId) rows.push({ label: t('processing.id-soobshcheniya'), text: item.msgId, mono: true })
|
if (src.displayName) rows.push({ label: t('processing.istochnik'), text: src.displayName })
|
||||||
|
if (src.author) rows.push({ label: t('processing.author'), text: src.author })
|
||||||
|
if (src.externalId) rows.push({ label: t('processing.externalId'), text: src.externalId, mono: true })
|
||||||
|
if (src.originRef) rows.push({ label: t('processing.originRef'), text: src.originRef, mono: true })
|
||||||
if (item.msgAt) rows.push({ label: t('processing.otpravleno'), text: fmtDateTimeCompact(item.msgAt), num: true })
|
if (item.msgAt) rows.push({ label: t('processing.otpravleno'), text: fmtDateTimeCompact(item.msgAt), num: true })
|
||||||
for (const e of extra) {
|
for (const e of extra) {
|
||||||
if (item[e.key]) rows.push({ label: e.label, text: fmtDateTimeCompact(item[e.key]), num: true })
|
if (item[e.key]) rows.push({ label: e.label, text: fmtDateTimeCompact(item[e.key]), num: true })
|
||||||
@@ -90,13 +93,9 @@ function toggleDetail(id) {
|
|||||||
detailOpen.value = s
|
detailOpen.value = s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ссылка на исходное сообщение в Telegram: @handle/msg или приватный канал
|
// ссылка на исходник: originRef источника, если это абсолютный URL
|
||||||
function sourceUrl(item) {
|
function sourceUrl(item) {
|
||||||
return tgSourceUrl({
|
return sourceOriginUrl(item)
|
||||||
ch: item.ch,
|
|
||||||
sourceDialogId: item.dialogId,
|
|
||||||
sourceMsgId: item.msgId,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadAll() {
|
async function reloadAll() {
|
||||||
@@ -134,7 +133,7 @@ async function onClearAll() {
|
|||||||
|
|
||||||
// можно ли вернуть запись в обработку: повтор возвращать нечего (карточка уже есть)
|
// можно ли вернуть запись в обработку: повтор возвращать нечего (карточка уже есть)
|
||||||
function canReturn(r) {
|
function canReturn(r) {
|
||||||
return !!r && !r.returned && r.source !== 'dup'
|
return !!r && !r.returned && r.decidedBy !== 'dup'
|
||||||
}
|
}
|
||||||
|
|
||||||
function openReturn(item) {
|
function openReturn(item) {
|
||||||
@@ -309,11 +308,11 @@ onBeforeUnmount(() => {
|
|||||||
:class="m.status === 'filtered' ? 'border-warn/20' : ''"
|
:class="m.status === 'filtered' ? 'border-warn/20' : ''"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 mb-1.5">
|
<div class="flex items-center gap-2 mb-1.5">
|
||||||
<ChannelAvatar :name="m.ch.name" :handle="m.ch.handle" />
|
<ChannelAvatar :name="m.source?.displayName || ''" :handle="m.source?.displayName || ''" :color="m.source?.extra?.hue" />
|
||||||
<span class="text-[12px] font-medium text-mid truncate" :title="m.ch.handle ? '@' + m.ch.handle : ''">
|
<span class="text-[12px] font-medium text-mid truncate" :title="m.source?.originRef || ''">
|
||||||
{{ m.ch.name }}
|
{{ m.source?.displayName || $t('processing.istochnik') }}
|
||||||
<template v-if="m.ch.handle">
|
<template v-if="m.source?.originRef">
|
||||||
<span class="text-low/60 font-mono">@{{ m.ch.handle }}</span>
|
<span class="text-low/60 font-mono">{{ m.source.originRef }}</span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -354,6 +353,7 @@ onBeforeUnmount(() => {
|
|||||||
<PipelineDetails
|
<PipelineDetails
|
||||||
v-if="detailOpen.has(m.id)"
|
v-if="detailOpen.has(m.id)"
|
||||||
:meta="metaRows(m, [{ key: 'queuedAt', label: $t('processing.v-ocheredi-2') }])"
|
:meta="metaRows(m, [{ key: 'queuedAt', label: $t('processing.v-ocheredi-2') }])"
|
||||||
|
:content="m.content"
|
||||||
:source-html="srcHtml(m)"
|
:source-html="srcHtml(m)"
|
||||||
:source-url="sourceUrl(m)"
|
:source-url="sourceUrl(m)"
|
||||||
/>
|
/>
|
||||||
@@ -391,18 +391,18 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<!-- Шапка: источник + причина -->
|
<!-- Шапка: источник + причина -->
|
||||||
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
|
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
|
||||||
<ChannelAvatar :name="r.ch.name" :handle="r.ch.handle" />
|
<ChannelAvatar :name="r.source?.displayName || ''" :handle="r.source?.displayName || ''" :color="r.source?.extra?.hue" />
|
||||||
<span class="text-[12px] font-medium text-mid truncate" :title="r.ch.handle ? '@' + r.ch.handle : ''">
|
<span class="text-[12px] font-medium text-mid truncate" :title="r.source?.originRef || ''">
|
||||||
{{ r.ch.name || $t('processing.istochnik') }}
|
{{ r.source?.displayName || $t('processing.istochnik') }}
|
||||||
<template v-if="r.ch.handle">
|
<template v-if="r.source?.originRef">
|
||||||
<span class="text-low/60 font-mono">@{{ r.ch.handle }}</span>
|
<span class="text-low/60 font-mono">{{ r.source.originRef }}</span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide rounded border px-1.5 py-px shrink-0"
|
class="inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide rounded border px-1.5 py-px shrink-0"
|
||||||
:class="srcStyle(r.source).cls"
|
:class="srcStyle(r.decidedBy).cls"
|
||||||
>
|
>
|
||||||
{{ srcStyle(r.source).label }}
|
{{ r.decidedByLabel || srcStyle(r.decidedBy).label }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="text-[10.5px] text-low bg-white/5 rounded px-1.5 py-px shrink-0"
|
class="text-[10.5px] text-low bg-white/5 rounded px-1.5 py-px shrink-0"
|
||||||
@@ -455,7 +455,7 @@ onBeforeUnmount(() => {
|
|||||||
<Icon name="restore" :size="11" />
|
<Icon name="restore" :size="11" />
|
||||||
{{ $t('processing.vozvrashcheno-v-obrabotku') }}
|
{{ $t('processing.vozvrashcheno-v-obrabotku') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="r.source === 'dup'" class="text-[10.5px] text-low/60 shrink-0">
|
<span v-else-if="r.decidedBy === 'dup'" class="text-[10.5px] text-low/60 shrink-0">
|
||||||
{{ $t('processing.povtor-kartochka-uzhe-est-v-sisteme') }}
|
{{ $t('processing.povtor-kartochka-uzhe-est-v-sisteme') }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -478,6 +478,7 @@ onBeforeUnmount(() => {
|
|||||||
<PipelineDetails
|
<PipelineDetails
|
||||||
v-if="detailOpen.has(r.id)"
|
v-if="detailOpen.has(r.id)"
|
||||||
:meta="metaRows(r, [{ key: 'rejectedAt', label: $t('processing.otseyano') }, { key: 'returnedAt', label: $t('processing.vozvrashcheno') }])"
|
:meta="metaRows(r, [{ key: 'rejectedAt', label: $t('processing.otseyano') }, { key: 'returnedAt', label: $t('processing.vozvrashcheno') }])"
|
||||||
|
:content="r.content"
|
||||||
:source-html="srcHtml(r)"
|
:source-html="srcHtml(r)"
|
||||||
:source-url="sourceUrl(r)"
|
:source-url="sourceUrl(r)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user