Перевести ядро на единый контракт источника
Карточка, очередь и отсев работают с 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-TEST-HARNESS | Историческая гонка `FreeTcpPort` — устранена; следить за новыми хост-хелперами | этап 12, E | 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 |
|
||||
|
||||
## 6. Manual-проверки (нужны внешние условия)
|
||||
|
||||
+8
-7
@@ -232,16 +232,13 @@
|
||||
"local": false, // создана локально, без внешнего источника
|
||||
"title": "Разработка интернет-магазина", // string ≤140
|
||||
"summary": "Компания: …\nЗадача: …",// блок «О заявке»
|
||||
"source": { "kind": "telegram", "displayName": "Канал заказов", "originRef": "123456789", "receivedAt": 1726000000000 },
|
||||
"sourceMsg": "Ищу разработчика…", // исходное сообщение
|
||||
"sourceDialogId": "123456789",
|
||||
"sourceMsgId": 4242,
|
||||
"source": { "kind": "telegram", "externalId": "4242", "displayName": "Канал заказов", "originRef": "123456789", "author": "…", "receivedAt": "2026-09-11T10:00:00+00:00", "extra": {"hue": "#8b8ff8"} },
|
||||
"content": { "text": "Ищу разработчика…", "html": null, "author": "…", "subject": null, "data": [], "links": [], "contacts": [] },
|
||||
"stack": ["vue", "dotnet"],
|
||||
"budget": {"from": 100000, "to": 200000, "cur": "RUB"},
|
||||
"converted": {"from": 100000, "to": 200000, "cur": "RUB"},
|
||||
"contact": "@client",
|
||||
"contacts": [{"type": "tg", "value": "@client"}],
|
||||
"channel": {"name": "Канал заказов", "handle": "@orders", "hue": "#8b8ff8"}, // прежний ch
|
||||
"matchHits": [{"label": "Стек", "term": "vue", "word": null}],
|
||||
"comments": [{"id": "cm_…", "by": "Вы", "text": "Позвонил", "time": "5 мин"}],
|
||||
"links": [{"id": "pl_…", "name": "Бриф", "url": "https://example.com"}],
|
||||
@@ -257,11 +254,15 @@
|
||||
}
|
||||
```
|
||||
|
||||
Ключевые поля: `id/containerId/(col)` — принадлежность; `source/sourceMsg/sourceDialogId/sourceMsgId` —
|
||||
происхождение; `stack/budget/converted/contact/contacts/channel/matchHits` — данные заявки; `comments/
|
||||
Ключевые поля: `id/containerId/(col)` — принадлежность; `source` (`SourceRef`: вид, внешний id, подпись,
|
||||
ссылка на оригинал, цвет в `extra.hue`) и `content` (`SourceContent`: текст, разметка, ссылки, контакты,
|
||||
вложения `data`) — происхождение; `stack/budget/converted/contact/contacts/matchHits` — данные заявки; `comments/
|
||||
links/files/history/tzText/reminder` — модули; `isNew/prevCol/isVacancy/isVacancyKnown` — маркеры. Ровно
|
||||
один из полей `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`).
|
||||
|
||||
### 4.2 Контейнер (container) — `GET /api/containers`, `POST/PATCH` тела
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
# Дейл (Deal) — Статус разработки и прогресс
|
||||
|
||||
> Обновляется в конце каждого захода. Проект НЕ git — это главный борд состояния.
|
||||
> Дата последнего обновления: 2026-09-10. **Все этапы 0–12 выполнены (100%)** — см. roadmap
|
||||
> Обновляется в конце каждого захода. Проект в git (ветка `main`, коммиты на русском) — борд состояния + git-история.
|
||||
> Дата последнего обновления: 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-10-deal-stage10-operator-analytics.md` и ledgers в `.superpowers/sdd/`.
|
||||
> Live-приёмки на Docker Desktop выполнены: dev-smoke 14/14, SaaS-контур 15/15, prod-контур+mTLS+
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Дизайн: единый контракт источника + общий Storage-сервис данных
|
||||
|
||||
Дата: 2026-09-11. Статус: на согласовании (контракт не плодит типы вложений; файлы — в общем Storage).
|
||||
Дата: 2026-09-11. Статус: реализовано в ядре (домен, Storage-сервис, персистентность, конвейер, wire, фронт); адаптер/провайдер telegram-сервиса и Storage-выгрузка — следующие шаги. Контракт не плодит типы вложений; файлы — в общем Storage.
|
||||
|
||||
## 1. Принцип
|
||||
|
||||
@@ -127,3 +127,45 @@ API ядра: `GET /api/cards/{id}/source` → generic контент.
|
||||
6. Frontend: generic источник + универсальный просмотрщик.
|
||||
7. Telegram: адаптер + провайдер исходника (только в telegram-сервисе) + выгрузка в Storage.
|
||||
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`; колонка —
|
||||
inbox либо доска по `BoardAccepts`+правилам; после успешной классификации карточка получает
|
||||
`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/ИИ/система») и этап
|
||||
`length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup` (подписи UI: «короткое сообщение»,
|
||||
«стоп-фраза», «резюме соискателя», «нет суммы», «устарело», «повтор»…), причина ≤500, kw — сработавшая
|
||||
|
||||
@@ -423,7 +423,7 @@ public static class CardsEndpoints
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
IReadOnlyList<CardDto> leads = await cardsService.SearchCardsAsync(q, ct);
|
||||
|
||||
// messages всегда []: telegram-сообщений здесь нет, фронт их не читает.
|
||||
// messages всегда []: фронт их не читает.
|
||||
return Results.Ok(new { cards = leads, messages = Array.Empty<object>() });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Deal.Api.Events;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Grpc.Telegram;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
@@ -70,17 +72,30 @@ public sealed class TelegramIngressService(
|
||||
tenantContext.SetTenant(new TenantId(tenant.Id.ToString("N")));
|
||||
PipelineIngestService ingest = tenantScope.ServiceProvider.GetRequiredService<PipelineIngestService>();
|
||||
|
||||
PipelineIngestResultDto result = await ingest.EnqueueAsync(
|
||||
new QueuedMessage
|
||||
string hue = string.IsNullOrWhiteSpace(request.ChannelHue) ? SourceRefs.DefaultHue : request.ChannelHue;
|
||||
DateTimeOffset receivedAt = request.HasMsgAt
|
||||
? DateTimeOffset.FromUnixTimeMilliseconds(request.MsgAt)
|
||||
: DateTimeOffset.UtcNow;
|
||||
var item = new SourceItem
|
||||
{
|
||||
Source = new SourceRef
|
||||
{
|
||||
DialogId = request.DialogId,
|
||||
ChannelName = request.ChannelName,
|
||||
ChannelHandle = request.ChannelHandle,
|
||||
ChannelHue = request.ChannelHue,
|
||||
MsgId = request.HasMsgId ? request.MsgId : null,
|
||||
Text = request.Text,
|
||||
MsgAtMs = request.HasMsgAt ? request.MsgAt : null,
|
||||
Kind = "telegram",
|
||||
ExternalId = request.HasMsgId ? request.MsgId.ToString(CultureInfo.InvariantCulture) : null,
|
||||
OriginRef = request.DialogId,
|
||||
DisplayName = request.ChannelName,
|
||||
ReceivedAt = receivedAt,
|
||||
Extra = new Dictionary<string, string> { [SourceRefs.HueKey] = hue },
|
||||
},
|
||||
Content = new SourceContent
|
||||
{
|
||||
Text = request.Text,
|
||||
Author = request.ChannelName,
|
||||
},
|
||||
};
|
||||
|
||||
PipelineIngestResultDto result = await ingest.EnqueueAsync(
|
||||
new QueuedMessage { Item = item },
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
await SavePreviewSafelyAsync(tenantScope, tenant, request, context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -88,9 +88,9 @@ ContainersService containersService) : IColumnSuggester
|
||||
List<string> texts = cards
|
||||
.Where(card => card.Col != CardIds.Trash
|
||||
&& card.Col != CardIds.Archive
|
||||
&& card.SourceMsg.Trim().Length > 0)
|
||||
&& (card.Content.Text ?? string.Empty).Trim().Length > 0)
|
||||
.Take(SuggestHeuristics.KeywordsSampleLimit)
|
||||
.Select(card => card.SourceMsg.Trim())
|
||||
.Select(card => (card.Content.Text ?? string.Empty).Trim())
|
||||
.ToList();
|
||||
if (texts.Count < SuggestHeuristics.MinKeywordsSample)
|
||||
{
|
||||
@@ -115,7 +115,7 @@ ContainersService containersService) : IColumnSuggester
|
||||
.Select(card => card.Id)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
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;
|
||||
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_...).
|
||||
private const int ColMaxLength = 200;
|
||||
|
||||
// Максимальная длина вида источника.
|
||||
private const int SourceKindMaxLength = 64;
|
||||
|
||||
public void Configure(EntityTypeBuilder<CardEntity> builder)
|
||||
{
|
||||
builder.ToTable("Cards");
|
||||
@@ -27,7 +30,12 @@ public sealed class CardConfiguration : IEntityTypeConfiguration<CardEntity>
|
||||
builder.Property(x => x.LinksJson).HasColumnType("text");
|
||||
builder.Property(x => x.FilesJson).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).
|
||||
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)
|
||||
.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);
|
||||
builder.HasIndex(x => x.SearchTsv).HasMethod("gin");
|
||||
}
|
||||
|
||||
@@ -16,8 +16,14 @@ public sealed class QueueItemConfiguration : IEntityTypeConfiguration<QueueItemE
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
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).
|
||||
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.Reason).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)
|
||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Text\",''))", stored: true);
|
||||
|
||||
@@ -92,40 +92,40 @@ public sealed class CardEntity
|
||||
/// </summary>
|
||||
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>
|
||||
public DateTimeOffset ReceivedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Текст исходного сообщения
|
||||
/// Вид источника
|
||||
/// </summary>
|
||||
public string SourceMsg { get; set; } = string.Empty;
|
||||
public string SourceKind { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id диалога исходного сообщения
|
||||
/// Идентификатор записи в источнике
|
||||
/// </summary>
|
||||
public string SourceDialogId { get; set; } = string.Empty;
|
||||
public string SourceExternalId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id исходного сообщения в Telegram, либо null.
|
||||
/// Ссылка на оригинал в источнике
|
||||
/// </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>
|
||||
/// Предыдущая колонка
|
||||
|
||||
@@ -11,35 +11,25 @@ public sealed class QueueItemEntity
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id диалога-источника
|
||||
/// Ключ дедупликации записи источника
|
||||
/// </summary>
|
||||
public string DialogId { get; set; } = string.Empty;
|
||||
public string SourceKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Имя канала/диалога-источника.
|
||||
/// Ссылка на источник, сериализованная в JSON
|
||||
/// </summary>
|
||||
public string ChannelName { get; set; } = string.Empty;
|
||||
public string SourceJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Handle канала-источника.
|
||||
/// Содержимое источника, сериализованное в JSON
|
||||
/// </summary>
|
||||
public string ChannelHandle { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Цвет канала-источника
|
||||
/// </summary>
|
||||
public string ChannelHue { get; set; } = "#666";
|
||||
public string ContentJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Текст сообщения
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id исходного сообщения в Telegram, либо null
|
||||
/// </summary>
|
||||
public long? MsgId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Время получения исходного сообщения
|
||||
/// </summary>
|
||||
|
||||
@@ -13,29 +13,19 @@ public sealed class RejectedItemEntity
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id диалога-источника
|
||||
/// Ключ дедупликации записи источника
|
||||
/// </summary>
|
||||
public string DialogId { get; set; } = string.Empty;
|
||||
public string SourceKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id исходного сообщения в Telegram, либо null.
|
||||
/// Ссылка на источник, сериализованная в JSON
|
||||
/// </summary>
|
||||
public long? MsgId { get; set; }
|
||||
public string SourceJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Имя канала/диалога-источника.
|
||||
/// Содержимое источника, сериализованное в JSON
|
||||
/// </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";
|
||||
public string ContentJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Текст сообщения
|
||||
|
||||
+41
-52
@@ -1,7 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Deal.Infrastructure.Persistence;
|
||||
using Deal.Infrastructure.Persistence.Configurations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -11,11 +10,11 @@ using NpgsqlTypes;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
namespace Deal.Infrastructure.Persistence.Migrations.TenantDb
|
||||
{
|
||||
[DbContext(typeof(TenantDbContext))]
|
||||
[Migration("20260910134358_TenantContainerCleanup")]
|
||||
partial class TenantContainerCleanup
|
||||
[Migration("20260911114400_InitialTenant")]
|
||||
partial class InitialTenant
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -45,18 +44,6 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
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)
|
||||
@@ -70,6 +57,10 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ContentJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConvCur")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -128,18 +119,28 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("tsvector")
|
||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceText\",'')||' '||coalesce(\"Contact\",''))", true);
|
||||
|
||||
b.Property<string>("SourceDialogId")
|
||||
b.Property<string>("SourceExternalId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceMsg")
|
||||
b.Property<string>("SourceJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long?>("SourceMsgId")
|
||||
.HasColumnType("bigint");
|
||||
b.Property<string>("SourceKind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("SourceOriginRef")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceText")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StackJson")
|
||||
.IsRequired()
|
||||
@@ -574,33 +575,26 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChannelHandle")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChannelHue")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChannelName")
|
||||
b.Property<string>("ContentJson")
|
||||
.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>("SourceJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
@@ -615,6 +609,8 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SourceKey");
|
||||
|
||||
b.HasIndex("Status", "CreatedAt");
|
||||
|
||||
b.ToTable("QueueItems", (string)null);
|
||||
@@ -625,19 +621,7 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
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")
|
||||
b.Property<string>("ContentJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -648,9 +632,6 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
b.Property<DateTimeOffset>("MsgAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("MsgId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -678,6 +659,14 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Stage")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
+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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-50
@@ -1,7 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Deal.Infrastructure.Persistence;
|
||||
using Deal.Infrastructure.Persistence.Configurations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -10,7 +9,7 @@ using NpgsqlTypes;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
namespace Deal.Infrastructure.Persistence.Migrations.TenantDb
|
||||
{
|
||||
[DbContext(typeof(TenantDbContext))]
|
||||
partial class TenantDbContextModelSnapshot : ModelSnapshot
|
||||
@@ -42,18 +41,6 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
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)
|
||||
@@ -67,6 +54,10 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ContentJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConvCur")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -125,18 +116,28 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("tsvector")
|
||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", true);
|
||||
.HasComputedColumnSql("to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceText\",'')||' '||coalesce(\"Contact\",''))", true);
|
||||
|
||||
b.Property<string>("SourceDialogId")
|
||||
b.Property<string>("SourceExternalId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceMsg")
|
||||
b.Property<string>("SourceJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long?>("SourceMsgId")
|
||||
.HasColumnType("bigint");
|
||||
b.Property<string>("SourceKind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("SourceOriginRef")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceText")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StackJson")
|
||||
.IsRequired()
|
||||
@@ -571,33 +572,26 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChannelHandle")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChannelHue")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ChannelName")
|
||||
b.Property<string>("ContentJson")
|
||||
.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>("SourceJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
@@ -612,6 +606,8 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SourceKey");
|
||||
|
||||
b.HasIndex("Status", "CreatedAt");
|
||||
|
||||
b.ToTable("QueueItems", (string)null);
|
||||
@@ -622,19 +618,7 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
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")
|
||||
b.Property<string>("ContentJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -645,9 +629,6 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
b.Property<DateTimeOffset>("MsgAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("MsgId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -675,6 +656,14 @@ namespace Deal.Infrastructure.Migrations.TenantDb
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Stage")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -1,4 +1,5 @@
|
||||
using Deal.Infrastructure.Persistence.Entities;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -49,7 +50,7 @@ public sealed partial class KanbanStore
|
||||
AND ("SearchTsv" @@ plainto_tsquery('russian', {query})
|
||||
OR lower("Title") LIKE {pattern}
|
||||
OR lower("Summary") LIKE {pattern}
|
||||
OR lower("SourceMsg") LIKE {pattern}
|
||||
OR lower("SourceText") LIKE {pattern}
|
||||
OR lower("Contact") LIKE {pattern})
|
||||
ORDER BY ts_rank("SearchTsv", plainto_tsquery('russian', {query})) DESC, "ReceivedAt" DESC
|
||||
LIMIT {limit}
|
||||
@@ -76,18 +77,21 @@ public sealed partial class KanbanStore
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CardDto?> GetCardBySourceAsync(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
SourceRef source,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dialogId))
|
||||
if (string.IsNullOrEmpty(source.Kind))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string externalId = source.ExternalId ?? string.Empty;
|
||||
string originRef = source.OriginRef ?? string.Empty;
|
||||
CardEntity? entity = await _dbContext.Cards
|
||||
.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)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (entity is null)
|
||||
|
||||
@@ -71,10 +71,10 @@ public sealed partial class KanbanStore
|
||||
card => card.Id,
|
||||
(move, card) => new { move, card })
|
||||
.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)
|
||||
.Take(limit)
|
||||
.Select(joined => new AiMarkupExampleDto(joined.card.SourceMsg, joined.move.ToCol!))
|
||||
.Select(joined => new AiMarkupExampleDto(joined.card.SourceText, joined.move.ToCol!))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed partial class KanbanStore
|
||||
{
|
||||
List<CardEntity> entities = await _dbContext.Cards
|
||||
.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)
|
||||
.ToListAsync(ct);
|
||||
return entities.Select(card => ToCardDto(card, Array.Empty<CardCommentDto>())).ToList();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Infrastructure.Persistence.Entities;
|
||||
using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Abstractions;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
@@ -120,11 +121,8 @@ public sealed partial class KanbanStore : ICardStore
|
||||
IsVacancyKnown = entity.IsVacancyKnown,
|
||||
Title = entity.Title,
|
||||
Summary = entity.Summary,
|
||||
Source = new CardSourceDto(
|
||||
Kind: ResolveSourceKind(entity),
|
||||
DisplayName: entity.ChannelName,
|
||||
OriginRef: entity.SourceDialogId,
|
||||
ReceivedAt: entity.ReceivedAt.ToUnixTimeMilliseconds()),
|
||||
Source = FromJsonOr(entity.SourceJson, SourceRefs.Empty),
|
||||
Content = FromJsonOr(entity.ContentJson, new SourceContent { Text = entity.SourceText }),
|
||||
Stack = ToJsonList<string>(entity.StackJson),
|
||||
Budget = entity.BudgetCur.Length == 0
|
||||
? null
|
||||
@@ -134,12 +132,8 @@ public sealed partial class KanbanStore : ICardStore
|
||||
: new CardBudgetDto(entity.ConvFrom, entity.ConvTo, entity.ConvCur),
|
||||
Contact = entity.Contact,
|
||||
Contacts = ToJsonList<CardContactDto>(entity.ContactsJson),
|
||||
Channel = new CardChannelDto(entity.ChannelName, entity.ChannelHandle, entity.ChannelHue),
|
||||
Time = HumanAge(entity.ReceivedAt),
|
||||
ReceivedAtMs = entity.ReceivedAt.ToUnixTimeMilliseconds(),
|
||||
SourceMsg = entity.SourceMsg,
|
||||
SourceDialogId = entity.SourceDialogId,
|
||||
SourceMsgId = entity.SourceMsgId,
|
||||
PrevCol = entity.PrevCol,
|
||||
MatchHits = ToJsonList<MatchHitDto>(entity.MatchHitsJson),
|
||||
Comments = comments,
|
||||
@@ -152,17 +146,22 @@ public sealed partial class KanbanStore : ICardStore
|
||||
UpdatedAtMs = entity.UpdatedAt.ToUnixTimeMilliseconds(),
|
||||
};
|
||||
|
||||
// Вид источника карточки (производная проекция): local/telegram/other.
|
||||
// entity: Строка карточки.
|
||||
// Возвращает: Вид источника (строка wire-контракта).
|
||||
private static string ResolveSourceKind(CardEntity entity)
|
||||
// Разбирает одиночный JSON-объект; пустая/битая строка — запасное значение.
|
||||
private static T FromJsonOr<T>(string json, T fallback)
|
||||
{
|
||||
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.
|
||||
@@ -185,13 +184,13 @@ public sealed partial class KanbanStore : ICardStore
|
||||
ConvCur = snapshot.ConvCur,
|
||||
Contact = snapshot.Contact,
|
||||
ContactsJson = ToJson(snapshot.Contacts),
|
||||
ChannelName = snapshot.ChannelName,
|
||||
ChannelHandle = snapshot.ChannelHandle,
|
||||
ChannelHue = snapshot.ChannelHue,
|
||||
ReceivedAt = snapshot.ReceivedAt,
|
||||
SourceMsg = snapshot.SourceMsg,
|
||||
SourceDialogId = snapshot.SourceDialogId,
|
||||
SourceMsgId = snapshot.SourceMsgId,
|
||||
SourceKind = snapshot.Source.Kind,
|
||||
SourceExternalId = snapshot.Source.ExternalId ?? string.Empty,
|
||||
SourceOriginRef = snapshot.Source.OriginRef ?? string.Empty,
|
||||
SourceJson = ToJson(snapshot.Source),
|
||||
ContentJson = ToJson(snapshot.Content),
|
||||
SourceText = snapshot.Content.Text ?? string.Empty,
|
||||
PrevCol = snapshot.PrevCol,
|
||||
ArchivedAt = snapshot.ArchivedAt,
|
||||
MatchHitsJson = ToJson(snapshot.MatchHits),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Infrastructure.Persistence.Entities;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
@@ -11,17 +13,21 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
||||
/// </summary>
|
||||
public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
{
|
||||
// ── Очередь (QueueItems) ───────────────────────────────────────────────
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
// ── Очередь (QueueItems) ───────────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ExistsDuplicateAsync(
|
||||
string dialogId,
|
||||
long? msgId,
|
||||
SourceRef source,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return msgId is null
|
||||
? Task.FromResult(false)
|
||||
: dbContext.QueueItems.AnyAsync(item => item.DialogId == dialogId && item.MsgId == msgId, ct);
|
||||
string key = source.DedupeKey();
|
||||
return dbContext.QueueItems.AnyAsync(item => item.SourceKey == key, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -89,16 +95,19 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
|
||||
string id = record.DeterministicId ?? PrefixId.New(PipelineIdPrefixes.Rejected);
|
||||
DateTimeOffset rejectedAt = DateTimeOffset.UtcNow;
|
||||
string sourceKey = record.Source.DedupeKey();
|
||||
string sourceJson = ToJson(record.Source);
|
||||
string contentJson = ToJson(record.Content);
|
||||
|
||||
await dbContext.Database.ExecuteSqlInterpolatedAsync(
|
||||
$"""
|
||||
INSERT INTO "RejectedItems" ("Id", "DialogId", "MsgId", "ChannelName", "ChannelHandle", "ChannelHue", "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})
|
||||
INSERT INTO "RejectedItems" ("Id", "SourceKey", "SourceJson", "ContentJson", "Text", "Stage", "Reason", "Kw", "Source", "MsgAt", "RejectedAt", "Returned", "ReturnReason")
|
||||
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
|
||||
"SourceKey" = excluded."SourceKey",
|
||||
"SourceJson" = excluded."SourceJson",
|
||||
"ContentJson" = excluded."ContentJson",
|
||||
"Text" = excluded."Text",
|
||||
"ChannelName" = excluded."ChannelName",
|
||||
"ChannelHandle" = excluded."ChannelHandle",
|
||||
"ChannelHue" = excluded."ChannelHue",
|
||||
"Stage" = excluded."Stage",
|
||||
"Reason" = excluded."Reason",
|
||||
"Kw" = excluded."Kw",
|
||||
@@ -156,8 +165,7 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
.AsNoTracking()
|
||||
.Where(item => EF.Functions.Like(item.Text.ToLower(), pattern)
|
||||
|| EF.Functions.Like(item.Reason.ToLower(), pattern)
|
||||
|| EF.Functions.Like(item.Kw.ToLower(), pattern)
|
||||
|| EF.Functions.Like(item.ChannelName.ToLower(), pattern))
|
||||
|| EF.Functions.Like(item.Kw.ToLower(), pattern))
|
||||
.OrderByDescending(item => item.RejectedAt)
|
||||
.Take(limitLike)
|
||||
.ToListAsync(ct);
|
||||
@@ -274,11 +282,10 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
private static QueueItemDto ToQueueItemDto(QueueItemEntity entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
DialogId = entity.DialogId,
|
||||
MsgId = entity.MsgId,
|
||||
Source = FromJsonOr(entity.SourceJson, SourceRefs.Empty),
|
||||
Content = FromJsonOr(entity.ContentJson, new SourceContent()),
|
||||
Text = entity.Text,
|
||||
Status = entity.Status,
|
||||
Channel = new PipelineChannelDto(entity.ChannelName, entity.ChannelHandle, entity.ChannelHue),
|
||||
MsgAtMs = entity.MsgAt.ToUnixTimeMilliseconds(),
|
||||
QueuedAtMs = entity.CreatedAt.ToUnixTimeMilliseconds(),
|
||||
Force = entity.Force,
|
||||
@@ -291,13 +298,11 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
return new QueueItemEntity
|
||||
{
|
||||
Id = item.Id,
|
||||
DialogId = item.DialogId,
|
||||
MsgId = item.MsgId,
|
||||
SourceKey = item.Source.DedupeKey(),
|
||||
SourceJson = ToJson(item.Source),
|
||||
ContentJson = ToJson(item.Content),
|
||||
Text = item.Text,
|
||||
Status = item.Status,
|
||||
ChannelName = item.Channel.Name,
|
||||
ChannelHandle = item.Channel.Handle,
|
||||
ChannelHue = item.Channel.Hue,
|
||||
MsgAt = FromEpochMs(item.MsgAtMs),
|
||||
CreatedAt = queuedAt,
|
||||
UpdatedAt = queuedAt,
|
||||
@@ -308,16 +313,15 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
private static RejectedItemDto ToRejectedItemDto(RejectedItemEntity entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
DialogId = entity.DialogId,
|
||||
MsgId = entity.MsgId,
|
||||
Source = FromJsonOr(entity.SourceJson, SourceRefs.Empty),
|
||||
Content = FromJsonOr(entity.ContentJson, new SourceContent()),
|
||||
Text = entity.Text,
|
||||
Stage = entity.Stage,
|
||||
StageLabel = PipelineRejectConstants.StageLabel(entity.Stage),
|
||||
Reason = entity.Reason,
|
||||
Kw = entity.Kw,
|
||||
Source = entity.Source,
|
||||
SourceLabel = PipelineRejectConstants.SourceLabel(entity.Source),
|
||||
Channel = new PipelineChannelDto(entity.ChannelName, entity.ChannelHandle, entity.ChannelHue),
|
||||
DecidedBy = entity.Source,
|
||||
DecidedByLabel = PipelineRejectConstants.SourceLabel(entity.Source),
|
||||
MsgAtMs = entity.MsgAt.ToUnixTimeMilliseconds(),
|
||||
RejectedAtMs = entity.RejectedAt.ToUnixTimeMilliseconds(),
|
||||
Returned = entity.Returned,
|
||||
@@ -327,4 +331,25 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
|
||||
// epoch-ms → DateTimeOffset (UTC-момент для timestamptz-колонки).
|
||||
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;
|
||||
|
||||
namespace Deal.Modules.Kanban.Application.Abstractions;
|
||||
@@ -89,12 +90,10 @@ public interface ICardStore
|
||||
/// <summary>
|
||||
/// Карточка по исходному сообщению
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Id диалога-источника сообщения.</param>
|
||||
/// <param name="msgId">Id исходного сообщения в Telegram.</param>
|
||||
/// <param name="source">Ссылка на источник: вид + оригинал + внешний id.</param>
|
||||
/// <returns>Карточка или null, если сообщение не становилось карточкой.</returns>
|
||||
public Task<CardDto?> GetCardBySourceAsync(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
SourceRef source,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
@@ -367,6 +366,6 @@ public interface ICardStore
|
||||
/// <summary>
|
||||
/// Карточки «Неразобранного» с исходным текстом — вход эвристики suggest.
|
||||
/// </summary>
|
||||
/// <returns>Карточки col='inbox' с непустым SourceMsg (частотные темы считаются по source_msg).</returns>
|
||||
/// <returns>Карточки col='inbox' с непустым текстом источника.</returns>
|
||||
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 Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Kanban.Application.Models;
|
||||
|
||||
@@ -53,9 +54,14 @@ public sealed record CardDto
|
||||
public string Summary { get; init; } = string.Empty;
|
||||
|
||||
/// <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>
|
||||
/// Стек/направления
|
||||
@@ -82,11 +88,6 @@ public sealed record CardDto
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -98,21 +99,6 @@ public sealed record CardDto
|
||||
[property: JsonPropertyName("receivedAt")]
|
||||
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>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Kanban.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -91,40 +93,20 @@ public sealed record CardSnapshot
|
||||
public IReadOnlyList<CardContactDto> Contacts { get; init; } = Array.Empty<CardContactDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Имя канала/диалога-источника.
|
||||
/// Источник записи.
|
||||
/// </summary>
|
||||
public string ChannelName { get; init; } = string.Empty;
|
||||
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Handle канала-источника.
|
||||
/// Содержимое записи источника.
|
||||
/// </summary>
|
||||
public string ChannelHandle { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Цвет канала-источника
|
||||
/// </summary>
|
||||
public string ChannelHue { get; init; } = string.Empty;
|
||||
public SourceContent Content { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Время получения исходного сообщения
|
||||
/// </summary>
|
||||
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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
string source = card.SourceMsg.Trim();
|
||||
string source = (card.Content.Text ?? string.Empty).Trim();
|
||||
return source.Length > 0 ? source : card.Title;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Abstractions;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
|
||||
@@ -68,6 +69,7 @@ public sealed partial class CardsService
|
||||
public async Task<CardDto> CreateLocalCardAsync(CardLocalCreateDto draft, CancellationToken ct)
|
||||
{
|
||||
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
DateTimeOffset receivedAt = DateTimeOffset.FromUnixTimeMilliseconds(nowMs);
|
||||
string containerId = draft.ContainerId is not null && CardsDefaultContainers.Contains(draft.ContainerId)
|
||||
? draft.ContainerId
|
||||
: PlannedStage;
|
||||
@@ -79,6 +81,9 @@ public sealed partial class CardsService
|
||||
Local = true,
|
||||
Title = draft.Title.Trim(),
|
||||
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>(),
|
||||
BudgetFrom = draft.Budget?.From,
|
||||
BudgetTo = draft.Budget?.To,
|
||||
|
||||
@@ -90,13 +90,13 @@ public static class SuggestHeuristics
|
||||
/// <summary>
|
||||
/// Планы колонок-предложений по карточкам «Неразобранного».
|
||||
/// </summary>
|
||||
/// <param name="inbox">Карточки «Неразобранного» с непустым source_msg (ICardStore.ListInboxWithSourceAsync).</param>
|
||||
/// <param name="inbox">Карточки «Неразобранного» с непустым текстом источника (ICardStore.ListInboxWithSourceAsync).</param>
|
||||
/// <param name="existingBoardNames">Имена существующих (suggested=false) досок — похожие темы не предлагаются.</param>
|
||||
/// <returns>Планы колонок (≤4, каждая ≥2 карточки); пусто — мало карточек/нечего сгруппировать.</returns>
|
||||
public static IReadOnlyList<SuggestedColumnPlan> PlanColumns(IReadOnlyList<CardDto> inbox, IReadOnlyList<string> existingBoardNames)
|
||||
{
|
||||
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)
|
||||
.ThenBy(card => card.Id, StringComparer.Ordinal)
|
||||
.Take(MaxText)
|
||||
@@ -111,7 +111,7 @@ public static class SuggestHeuristics
|
||||
foreach (CardDto card in window)
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
@@ -166,7 +166,7 @@ public static class SuggestHeuristics
|
||||
/// <summary>
|
||||
/// Частотные слова-маркеры по текстам карточек.
|
||||
/// </summary>
|
||||
/// <param name="texts">Тексты source_msg карточек выборки (непустые, свежие ≤40).</param>
|
||||
/// <param name="texts">Тексты источника карточек выборки (непустые, свежие ≤40).</param>
|
||||
/// <returns>Слова-маркеры (≤60); пусто — нет слов, встречающихся в ≥2 текстах.</returns>
|
||||
public static IReadOnlyList<string> SuggestDomainKeywords(IReadOnlyList<string> texts)
|
||||
{
|
||||
@@ -191,7 +191,7 @@ public static class SuggestHeuristics
|
||||
}
|
||||
|
||||
// Слова текста: подряд букв (латиница/кириллица), нижний регистр, длина 3..40, минус стоп-слова.
|
||||
// text: Исходный текст (source_msg карточки).
|
||||
// text: Исходный текст (текст источника карточки).
|
||||
// Возвращает: Уникальные слова текста в порядке появления (Ordinal).
|
||||
private static List<string> Tokenize(string text)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
namespace Deal.Modules.Pipeline.Application.Abstractions;
|
||||
@@ -10,14 +11,12 @@ public interface IPipelineStore
|
||||
// ── Очередь (QueueItems) ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Есть ли строка очереди с тем же сообщением диалога — дубль-гвард приёма.
|
||||
/// Есть ли строка очереди с той же записью источника — дубль-гвард приёма.
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Id диалога-источника.</param>
|
||||
/// <param name="msgId">Id исходного сообщения; null — проверка не выполняется (вернёт false).</param>
|
||||
/// <returns>true — строка с DialogId+MsgId уже в очереди (Telethon-дубль не пишем).</returns>
|
||||
/// <param name="source">Ссылка на источник: вид + оригинал + внешний id.</param>
|
||||
/// <returns>true — строка с такой записью уже в очереди.</returns>
|
||||
public Task<bool> ExistsDuplicateAsync(
|
||||
string dialogId,
|
||||
long? msgId,
|
||||
SourceRef source,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение-кандидат для ручной проверки ML.
|
||||
/// Запись-кандидат для ручной проверки ML.
|
||||
/// </summary>
|
||||
public sealed record MlCandidateDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Id исходного сообщения в Telegram
|
||||
/// Источник записи.
|
||||
/// </summary>
|
||||
public long Id { get; init; }
|
||||
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id диалога-источника сообщения.
|
||||
/// Содержимое записи источника.
|
||||
/// </summary>
|
||||
public string DialogId { get; init; } = string.Empty;
|
||||
public SourceContent Content { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Текст сообщения.
|
||||
/// Текст записи.
|
||||
/// </summary>
|
||||
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 Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
@@ -13,14 +14,14 @@ public sealed record QueueItemDto
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id диалога-источника сообщения.
|
||||
/// Источник записи.
|
||||
/// </summary>
|
||||
public string DialogId { get; init; } = string.Empty;
|
||||
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id исходного сообщения в Telegram
|
||||
/// Содержимое записи источника.
|
||||
/// </summary>
|
||||
public long? MsgId { get; init; }
|
||||
public SourceContent Content { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Текст сообщения.
|
||||
@@ -33,25 +34,19 @@ public sealed record QueueItemDto
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Канал-источник
|
||||
/// </summary>
|
||||
[property: JsonPropertyName("ch")]
|
||||
public PipelineChannelDto Channel { get; init; } = new(string.Empty, string.Empty, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Время исходного сообщения, epoch-ms
|
||||
/// Время исходного сообщения, epoch-ms.
|
||||
/// </summary>
|
||||
[property: JsonPropertyName("msgAt")]
|
||||
public long MsgAtMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Время постановки в очередь
|
||||
/// Время постановки в очередь, epoch-ms.
|
||||
/// </summary>
|
||||
[property: JsonPropertyName("queuedAt")]
|
||||
public long QueuedAtMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Внутренний флаг «возвращено пользователем из отсева»
|
||||
/// Внутренний флаг «возвращено пользователем из отсева».
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool Force { get; init; }
|
||||
|
||||
@@ -1,47 +1,19 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Команда приёма входящего сообщения — аргумент PipelineIngestService.EnqueueAsync.
|
||||
/// Команда приёма входящей записи — аргумент PipelineIngestService.EnqueueAsync.
|
||||
/// </summary>
|
||||
public sealed record QueuedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Id диалога-источника сообщения
|
||||
/// Запись источника: источник и содержимое.
|
||||
/// </summary>
|
||||
public string DialogId { get; init; } = string.Empty;
|
||||
public required SourceItem Item { get; init; }
|
||||
|
||||
/// <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>
|
||||
public bool Force { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -6,44 +8,29 @@ namespace Deal.Modules.Pipeline.Application.Models;
|
||||
public sealed record RejectRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Id диалога-источника сообщения
|
||||
/// Источник записи.
|
||||
/// </summary>
|
||||
public string DialogId { get; init; } = string.Empty;
|
||||
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id исходного сообщения в Telegram, либо null.
|
||||
/// Содержимое записи источника.
|
||||
/// </summary>
|
||||
public long? MsgId { get; init; }
|
||||
public SourceContent Content { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Текст отсеянного сообщения
|
||||
/// Текст отсеянного сообщения.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Время исходного сообщения, epoch-ms.
|
||||
/// </summary>
|
||||
public long MsgAtMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Источник решения
|
||||
/// Кто вынес решение: stop|ml|ai|stale|dup.
|
||||
/// </summary>
|
||||
public string Source { get; init; } = string.Empty;
|
||||
public string DecidedBy { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Этап отсева: 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;
|
||||
|
||||
/// <summary>
|
||||
/// Конкретное слово/фраза стоп-списка, сработавшая правилом
|
||||
/// Конкретное слово/фраза стоп-списка, сработавшая правилом.
|
||||
/// </summary>
|
||||
public string Kw { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Детерминированный id записи
|
||||
/// Детерминированный id записи.
|
||||
/// </summary>
|
||||
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 Deal.Modules.Cards.Application.Sources;
|
||||
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
@@ -13,14 +14,14 @@ public sealed record RejectedItemDto
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id диалога-источника сообщения.
|
||||
/// Источник записи.
|
||||
/// </summary>
|
||||
public string DialogId { get; init; } = string.Empty;
|
||||
public SourceRef Source { get; init; } = SourceRefs.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id исходного сообщения в Telegram, либо null.
|
||||
/// Содержимое записи источника.
|
||||
/// </summary>
|
||||
public long? MsgId { get; init; }
|
||||
public SourceContent Content { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Текст отсеянного сообщения.
|
||||
@@ -28,12 +29,12 @@ public sealed record RejectedItemDto
|
||||
public string Text { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Этап отсева (см. словарь подписей <see cref="Deal.Modules.Pipeline.Application.Models.PipelineRejectConstants.StageLabels"/>).
|
||||
/// Этап отсева.
|
||||
/// </summary>
|
||||
public string Stage { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Человекочитаемая подпись для UI
|
||||
/// Человекочитаемая подпись для UI.
|
||||
/// </summary>
|
||||
public string StageLabel { get; init; } = string.Empty;
|
||||
|
||||
@@ -43,51 +44,45 @@ public sealed record RejectedItemDto
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Конкретное слово/фраза стоп-списка, сработавшая правилом
|
||||
/// Конкретное слово/фраза стоп-списка, сработавшая правилом.
|
||||
/// </summary>
|
||||
public string Kw { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Источник решения
|
||||
/// Кто вынес решение: stop|ml|ai|stale|dup.
|
||||
/// </summary>
|
||||
public string Source { get; init; } = string.Empty;
|
||||
public string DecidedBy { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Подпись источника
|
||||
/// Подпись источника решения.
|
||||
/// </summary>
|
||||
public string SourceLabel { get; init; } = string.Empty;
|
||||
public string DecidedByLabel { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Канал-источник
|
||||
/// </summary>
|
||||
[property: JsonPropertyName("ch")]
|
||||
public PipelineChannelDto Channel { get; init; } = new(string.Empty, string.Empty, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Время исходного сообщения, epoch-ms
|
||||
/// Время исходного сообщения, epoch-ms.
|
||||
/// </summary>
|
||||
[property: JsonPropertyName("msgAt")]
|
||||
public long MsgAtMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Время отсева, epoch-ms
|
||||
/// Время отсева, epoch-ms.
|
||||
/// </summary>
|
||||
[property: JsonPropertyName("rejectedAt")]
|
||||
public long RejectedAtMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Флаг «возвращено в обработку»
|
||||
/// Флаг «возвращено в обработку».
|
||||
/// </summary>
|
||||
public bool Returned { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Время возврата, epoch-ms
|
||||
/// Время возврата, epoch-ms.
|
||||
/// </summary>
|
||||
[property: JsonPropertyName("returnedAt")]
|
||||
public long? ReturnedAtMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Причина возврата, указанная пользователем
|
||||
/// Причина возврата, указанная пользователем.
|
||||
/// </summary>
|
||||
public string ReturnReason { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@ public static class ContactsQualifier
|
||||
|
||||
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;
|
||||
|
||||
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",
|
||||
};
|
||||
@@ -35,9 +35,9 @@ public static class ContactsQualifier
|
||||
"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",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
@@ -78,7 +78,7 @@ public static class ContactsQualifier
|
||||
/// <summary>
|
||||
/// Классифицирует один сырой контакт → {type, value} или null.
|
||||
/// </summary>
|
||||
/// <param name="raw">Сырое значение контакта («@user», «https://t.me/x», телефон, email, ссылка).</param>
|
||||
/// <param name="raw">Сырое значение контакта («@user», ссылка на профиль, телефон, email, ссылка).</param>
|
||||
/// <returns>Квалифицированный контакт <see cref="CardContactDto"/> либо null — пусто/мусор/бот/сервисная ссылка/ «постовый» сайт.</returns>
|
||||
public static CardContactDto? Qualify(string? raw)
|
||||
{
|
||||
@@ -91,7 +91,7 @@ public static class ContactsQualifier
|
||||
if (s.StartsWith('@'))
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -99,12 +99,12 @@ public static class ContactsQualifier
|
||||
return null;
|
||||
}
|
||||
|
||||
Match link = TelegramLinkRe.Match(s);
|
||||
Match link = ProfileLinkRe.Match(s);
|
||||
if (link.Success)
|
||||
{
|
||||
string name = link.Groups[1].Value;
|
||||
if (!TgServiceNames.Contains(name.ToLowerInvariant())
|
||||
&& !name.EndsWith(TgBotSuffix, StringComparison.OrdinalIgnoreCase))
|
||||
if (!ReservedProfileNames.Contains(name.ToLowerInvariant())
|
||||
&& !name.EndsWith(BotSuffix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new CardContactDto("tg", "@" + name);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Abstractions;
|
||||
using Deal.Modules.Kanban.Application.ColumnRules;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
@@ -92,10 +93,11 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings)
|
||||
IReadOnlyList<CardContactDto> contacts = ContactsQualifier.Build(
|
||||
parsed.Contacts.Select(contact => contact.Value).ToList(), message.Text);
|
||||
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.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
|
||||
{
|
||||
@@ -115,13 +117,9 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings)
|
||||
ConvCur = converted?.Cur ?? string.Empty,
|
||||
Contact = contact,
|
||||
Contacts = contacts,
|
||||
ChannelName = message.Channel.Name,
|
||||
ChannelHandle = message.Channel.Handle,
|
||||
ChannelHue = hue,
|
||||
Source = message.Source,
|
||||
Content = message.Content with { Text = MessageTextCleaner.SliceCodePoints(message.Text, MaxSourceMsgCodePoints) },
|
||||
ReceivedAt = receivedAt,
|
||||
SourceMsg = MessageTextCleaner.SliceCodePoints(message.Text, MaxSourceMsgCodePoints),
|
||||
SourceDialogId = message.DialogId,
|
||||
SourceMsgId = message.MsgId,
|
||||
PrevCol = CardIds.Inbox,
|
||||
ArchivedAt = null,
|
||||
MatchHits = matchHits,
|
||||
|
||||
@@ -94,7 +94,7 @@ public sealed class CardReclassifier(
|
||||
try
|
||||
{
|
||||
Pass pass = await CreatePassAsync(ct);
|
||||
if (string.IsNullOrWhiteSpace(card.SourceMsg))
|
||||
if (string.IsNullOrWhiteSpace(card.Content.Text))
|
||||
{
|
||||
pass.Skipped++;
|
||||
return Build(pass, attempted: 1, started: false, reason: NoSourceTextReason);
|
||||
@@ -121,7 +121,7 @@ public sealed class CardReclassifier(
|
||||
Pass pass,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string text = card.SourceMsg;
|
||||
string text = card.Content.Text ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
pass.Skipped++;
|
||||
@@ -225,16 +225,15 @@ public sealed class CardReclassifier(
|
||||
|
||||
// ── Сборка входа/выхода ────────────────────────────────────────────────
|
||||
|
||||
// Собирает строку-сообщение для CardComposer из полей карточки (канал/время/исходный текст).
|
||||
// card: Карточка-источник (алиасы канала/получателя/даты).
|
||||
// text: Исходный текст (source_msg).
|
||||
// Собирает строку-сообщение для CardComposer из полей карточки (источник/время/исходный текст).
|
||||
// card: Карточка-источник (источник и содержимое сообщения).
|
||||
// text: Исходный текст записи источника.
|
||||
// Возвращает: Строка очереди, эквивалентная исходному сообщению карточки.
|
||||
private static QueueItemDto BuildMessage(CardDto card, string text) => new()
|
||||
{
|
||||
DialogId = card.SourceDialogId,
|
||||
MsgId = card.SourceMsgId,
|
||||
Source = card.Source,
|
||||
Content = card.Content with { Text = text },
|
||||
Text = text,
|
||||
Channel = new PipelineChannelDto(card.Channel.Name, card.Channel.Handle, card.Channel.Hue),
|
||||
MsgAtMs = card.ReceivedAtMs,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Globalization;
|
||||
using Deal.Contracts.Integrations.Abstractions;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Abstractions;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
@@ -10,7 +12,7 @@ using Deal.Modules.Pipeline.Application.Models;
|
||||
namespace Deal.Modules.Pipeline.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Ручная проверка/разметка ML на сообщениях канала.
|
||||
/// Ручная проверка/разметка ML на записях источников.
|
||||
/// </summary>
|
||||
public sealed class MlReviewService(
|
||||
IPipelineStore pipelineStore,
|
||||
@@ -86,10 +88,10 @@ public sealed class MlReviewService(
|
||||
private const double UserPushWeight = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Отбирает сообщения-кандидаты для проверки ML по каналу и/или размеру выборки.
|
||||
/// Отбирает записи-кандидаты для проверки ML по источнику и/или размеру выборки.
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Id канала/диалога; пусто — выборка по всем источникам тенанта.</param>
|
||||
/// <param name="limit">Сколько последних сообщений вернуть (кламп 1..<see cref="MaxCandidates"/>, дефолт вызывающего).</param>
|
||||
/// <param name="dialogId">Оригинал источника (OriginRef); пусто — выборка по всем источникам тенанта.</param>
|
||||
/// <param name="limit">Сколько последних записей вернуть (кламп 1..<see cref="MaxCandidates"/>, дефолт вызывающего).</param>
|
||||
/// <returns>Кандидаты (свежие первыми): текст, текущий вердикт и мнение ML по каждому.</returns>
|
||||
public async Task<IReadOnlyList<MlCandidateDto>> CandidatesAsync(
|
||||
string? dialogId,
|
||||
@@ -103,36 +105,36 @@ public sealed class MlReviewService(
|
||||
IReadOnlyList<RejectedItemDto> rejected = await pipelineStore.ListPageAsync(offset: 0, MaxScan, 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)
|
||||
{
|
||||
if (row.MsgId is not { } msgId || !MatchesDialog(dialog, row.DialogId))
|
||||
if (!IsMessage(row.Source) || !MatchesDialog(dialog, row.Source.OriginRef))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
merged[(row.DialogId, msgId)] = BuildQueued(row, msgId);
|
||||
merged[row.Source.DedupeKey()] = BuildQueued(row);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
merged[(row.DialogId, msgId)] = BuildRejected(row, msgId);
|
||||
merged[row.Source.DedupeKey()] = BuildRejected(row);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
merged[(card.SourceDialogId, msgId)] = BuildCard(card, msgId);
|
||||
merged[card.Source.DedupeKey()] = BuildCard(card);
|
||||
}
|
||||
|
||||
List<MlCandidateDto> ordered = merged.Values
|
||||
@@ -150,12 +152,12 @@ public sealed class MlReviewService(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Применяет ручное решение по сообщению
|
||||
/// Применяет ручное решение по записи источника
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Id канала/диалога сообщения.</param>
|
||||
/// <param name="msgId">Id исходного сообщения.</param>
|
||||
/// <param name="dialogId">Оригинал источника (OriginRef) записи.</param>
|
||||
/// <param name="msgId">Внешний id записи в источнике.</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(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
@@ -164,12 +166,19 @@ public sealed class MlReviewService(
|
||||
{
|
||||
string normalized = (action ?? string.Empty).Trim();
|
||||
string dialog = (dialogId ?? string.Empty).Trim();
|
||||
string externalId = msgId.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
CardDto? card = await cardStore.GetCardBySourceAsync(dialog, msgId, ct);
|
||||
string? text = await FindTextAsync(dialog, msgId, card, ct);
|
||||
SourceRef? source = await ResolveSourceAsync(dialog, externalId, 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))
|
||||
{
|
||||
return null; // 404: исходное сообщение не найдено
|
||||
return null; // 404: исходная запись не найдена
|
||||
}
|
||||
|
||||
if (normalized == ActionSkip)
|
||||
@@ -179,28 +188,28 @@ public sealed class MlReviewService(
|
||||
|
||||
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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// Действие «спам»: карточку — в корзину (с обучением), сообщение из очереди — в отсев; иначе учим ML.
|
||||
// dialog: Id диалога.
|
||||
// msgId: Id сообщения.
|
||||
// Действие «спам»: карточку — в корзину (с обучением), запись из очереди — в отсев; иначе учим ML.
|
||||
// dialog: Оригинал источника.
|
||||
// externalId: Внешний id записи.
|
||||
// card: Карточка сообщения (null — сообщение не становилось карточкой).
|
||||
// text: Текст сообщения.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Результат решения.
|
||||
private async Task<MlApplyResult> ApplySpamAsync(
|
||||
string dialog,
|
||||
long msgId,
|
||||
string externalId,
|
||||
CardDto? card,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
@@ -215,19 +224,16 @@ public sealed class MlReviewService(
|
||||
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)
|
||||
{
|
||||
await processing.RejectAsync(new RejectRecord
|
||||
{
|
||||
DialogId = queued.DialogId,
|
||||
MsgId = queued.MsgId,
|
||||
Source = queued.Source,
|
||||
Content = queued.Content,
|
||||
Text = queued.Text,
|
||||
ChannelName = queued.Channel.Name,
|
||||
ChannelHandle = queued.Channel.Handle,
|
||||
ChannelHue = queued.Channel.Hue,
|
||||
MsgAtMs = queued.MsgAtMs,
|
||||
Source = ManualSource,
|
||||
DecidedBy = ManualSource,
|
||||
Stage = ManualSpamStage,
|
||||
Reason = ManualSpamReason,
|
||||
Kw = string.Empty,
|
||||
@@ -239,16 +245,12 @@ public sealed class MlReviewService(
|
||||
}
|
||||
|
||||
// Действие «в колонку»: карточку — переносим, уже в колонке — только учим; иначе учим ML.
|
||||
// dialog: Id диалога.
|
||||
// msgId: Id сообщения.
|
||||
// boardId: Id колонки-цели (inbox или b_...).
|
||||
// card: Карточка сообщения (null — сообщение не становилось карточкой).
|
||||
// text: Текст сообщения.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Результат решения.
|
||||
private async Task<MlApplyResult> ApplyBoardAsync(
|
||||
string dialog,
|
||||
long msgId,
|
||||
string boardId,
|
||||
CardDto? card,
|
||||
string text,
|
||||
@@ -281,24 +283,61 @@ public sealed class MlReviewService(
|
||||
return new MlApplyResult(null, Ok: true, Learned: true, Moved: boardId, LeadId: card.Id);
|
||||
}
|
||||
|
||||
// Текст исходного сообщения: source_msg карточки, иначе текст строки очереди/записи отсева.
|
||||
// dialog: Id диалога.
|
||||
// msgId: Id сообщения.
|
||||
// Ссылка на источник записи: очередь → карточка → отсев.
|
||||
// dialog: Оригинал источника.
|
||||
// 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: Карточка сообщения (уже прочитана вызывающим).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Непустой текст либо null, если сообщения нет ни в одном источнике.
|
||||
// Возвращает: Непустой текст либо null, если записи нет ни в одном источнике.
|
||||
private async Task<string?> FindTextAsync(
|
||||
string dialog,
|
||||
long msgId,
|
||||
string externalId,
|
||||
CardDto? card,
|
||||
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))
|
||||
{
|
||||
return queued.Text;
|
||||
@@ -307,7 +346,7 @@ public sealed class MlReviewService(
|
||||
IReadOnlyList<RejectedItemDto> rejected = await pipelineStore.ListPageAsync(offset: 0, MaxScan, ct);
|
||||
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;
|
||||
}
|
||||
@@ -316,20 +355,20 @@ public sealed class MlReviewService(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Строка очереди сообщения (для отсева при ручной разметке «спам»).
|
||||
// dialog: Id диалога.
|
||||
// msgId: Id сообщения.
|
||||
// Строка очереди записи (для отсева при ручной разметке «спам»).
|
||||
// dialog: Оригинал источника.
|
||||
// externalId: Внешний id записи.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Строка очереди либо null — сообщение уже обработано/не в очереди.
|
||||
private async Task<QueueItemDto?> FindQueuedAsync(
|
||||
string dialog,
|
||||
long msgId,
|
||||
string externalId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
IReadOnlyList<QueueItemDto> queue = await pipelineStore.ListAsync(status: null, MaxScan, ct);
|
||||
foreach (QueueItemDto row in queue)
|
||||
{
|
||||
if (row.MsgId == msgId && string.Equals(row.DialogId, dialog, StringComparison.Ordinal))
|
||||
if (MatchesSource(row.Source, dialog, externalId))
|
||||
{
|
||||
return row;
|
||||
}
|
||||
@@ -360,21 +399,34 @@ public sealed class MlReviewService(
|
||||
}
|
||||
}
|
||||
|
||||
// Соответствует ли диалог фильтру канала (пустой фильтр — все диалоги).
|
||||
// filter: Запрошенный канал (пусто — без фильтра).
|
||||
// dialogId: Диалог сообщения.
|
||||
// Идентифицирована ли запись источника (есть внешний id).
|
||||
// source: Ссылка на источник.
|
||||
// Возвращает: True — запись адресуема как сообщение источника.
|
||||
private static bool IsMessage(SourceRef source) => source.ExternalId is { Length: > 0 };
|
||||
|
||||
// Соответствует ли источник фильтру оригинала (пустой фильтр — все источники).
|
||||
// filter: Запрошенный оригинал (пусто — без фильтра).
|
||||
// originRef: Оригинал источника записи.
|
||||
// Возвращает: True — кандидат подходит.
|
||||
private static bool MatchesDialog(string filter, string dialogId) =>
|
||||
filter.Length == 0 || string.Equals(filter, dialogId, StringComparison.Ordinal);
|
||||
private static bool MatchesDialog(string filter, string? originRef) =>
|
||||
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).
|
||||
// row: Строка очереди.
|
||||
// msgId: Id сообщения.
|
||||
// Возвращает: Кандидат.
|
||||
private static MlCandidateDto BuildQueued(QueueItemDto row, long msgId) => new()
|
||||
private static MlCandidateDto BuildQueued(QueueItemDto row) => new()
|
||||
{
|
||||
Id = msgId,
|
||||
DialogId = row.DialogId,
|
||||
Source = row.Source,
|
||||
Content = row.Content,
|
||||
Text = Truncate(row.Text),
|
||||
Time = row.MsgAtMs == 0 ? null : row.MsgAtMs,
|
||||
Lead = false,
|
||||
@@ -384,12 +436,11 @@ public sealed class MlReviewService(
|
||||
|
||||
// Кандидат из записи отсева (вердикт rejected).
|
||||
// row: Запись отсева.
|
||||
// msgId: Id сообщения.
|
||||
// Возвращает: Кандидат.
|
||||
private static MlCandidateDto BuildRejected(RejectedItemDto row, long msgId) => new()
|
||||
private static MlCandidateDto BuildRejected(RejectedItemDto row) => new()
|
||||
{
|
||||
Id = msgId,
|
||||
DialogId = row.DialogId,
|
||||
Source = row.Source,
|
||||
Content = row.Content,
|
||||
Text = Truncate(row.Text),
|
||||
Time = row.MsgAtMs == 0 ? null : row.MsgAtMs,
|
||||
Lead = false,
|
||||
@@ -400,13 +451,12 @@ public sealed class MlReviewService(
|
||||
|
||||
// Кандидат из карточки (вердикт card).
|
||||
// card: Карточка.
|
||||
// msgId: Id сообщения.
|
||||
// Возвращает: Кандидат.
|
||||
private static MlCandidateDto BuildCard(CardDto card, long msgId) => new()
|
||||
private static MlCandidateDto BuildCard(CardDto card) => new()
|
||||
{
|
||||
Id = msgId,
|
||||
DialogId = card.SourceDialogId,
|
||||
Text = Truncate(card.SourceMsg),
|
||||
Source = card.Source,
|
||||
Content = card.Content,
|
||||
Text = Truncate(card.Content.Text ?? string.Empty),
|
||||
Time = card.ReceivedAtMs == 0 ? null : card.ReceivedAtMs,
|
||||
Lead = true,
|
||||
Verdict = VerdictCard,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
@@ -6,42 +7,44 @@ using Deal.Modules.Pipeline.Application.Parse;
|
||||
namespace Deal.Modules.Pipeline.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Приём входящих сообщений пайплайна — постановка сырого сообщения в очередь QueueItems.
|
||||
/// Приём входящих сообщений пайплайна — постановка сырой записи источника в очередь QueueItems.
|
||||
/// </summary>
|
||||
public sealed class PipelineIngestService(IPipelineStore store)
|
||||
{
|
||||
private const int MaxQueueTextLength = 6000;
|
||||
|
||||
/// <summary>
|
||||
/// Ставит входящее сообщение в очередь
|
||||
/// Ставит входящую запись источника в очередь
|
||||
/// </summary>
|
||||
/// <param name="message">Сырое сообщение: диалог, канальные поля, текст, время; Force — возврат из отсева.</param>
|
||||
/// <returns>Результат: Id строки (<c>p_...</c>) либо null, если сообщение не принято; Duplicate — дубль диалога.</returns>
|
||||
/// <param name="message">Запись источника и флаг «возвращено из отсева».</param>
|
||||
/// <returns>Результат: Id строки (<c>p_...</c>) либо null, если запись не принята; Duplicate — дубль источника.</returns>
|
||||
public async Task<PipelineIngestResultDto> EnqueueAsync(QueuedMessage message, CancellationToken ct)
|
||||
{
|
||||
string text = (message.Text ?? string.Empty).Trim();
|
||||
if (text.Length == 0 || string.IsNullOrEmpty(message.DialogId))
|
||||
string text = (message.Item.Content.Text ?? string.Empty).Trim();
|
||||
if (text.Length == 0)
|
||||
{
|
||||
return new PipelineIngestResultDto(null, false);
|
||||
}
|
||||
|
||||
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
if (message.MsgId is not null
|
||||
&& await store.ExistsDuplicateAsync(message.DialogId, message.MsgId, ct))
|
||||
if (await store.ExistsDuplicateAsync(message.Item.Source, ct))
|
||||
{
|
||||
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
|
||||
{
|
||||
Id = PrefixId.New(PipelineIdPrefixes.Queue),
|
||||
DialogId = message.DialogId,
|
||||
MsgId = message.MsgId,
|
||||
Text = MessageTextCleaner.SliceCodePoints(text, MaxQueueTextLength),
|
||||
Source = message.Item.Source,
|
||||
Content = message.Item.Content with { Text = sliced },
|
||||
Text = sliced,
|
||||
Status = PipelineQueueStatuses.New,
|
||||
Channel = new PipelineChannelDto(message.ChannelName, message.ChannelHandle, message.ChannelHue),
|
||||
MsgAtMs = message.MsgAtMs ?? nowMs,
|
||||
MsgAtMs = msgAtMs,
|
||||
QueuedAtMs = nowMs,
|
||||
Force = message.Force,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Deal.Contracts.Integrations.Abstractions;
|
||||
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.Models;
|
||||
using Deal.Modules.Pipeline.Application.Parse;
|
||||
@@ -80,7 +80,6 @@ public sealed class PipelineProcessingService(
|
||||
return store.UpsertAsync(record with
|
||||
{
|
||||
Text = MessageTextCleaner.SliceCodePoints(record.Text, MaxRejectedTextLength),
|
||||
ChannelHue = string.IsNullOrEmpty(record.ChannelHue) ? SourceDefaults.DefaultHue : record.ChannelHue,
|
||||
Reason = MessageTextCleaner.SliceCodePoints(record.Reason, MaxReasonLength),
|
||||
Kw = MessageTextCleaner.SliceCodePoints(record.Kw, MaxKwLength),
|
||||
}, ct);
|
||||
@@ -172,7 +171,7 @@ public sealed class PipelineProcessingService(
|
||||
return new RejectReturnResultDto(AlreadyReturnedDetail, rejectedId, false, 0);
|
||||
}
|
||||
|
||||
if (row.Source == DuplicateSource)
|
||||
if (row.DecidedBy == DuplicateSource)
|
||||
{
|
||||
return new RejectReturnResultDto(DuplicateReturnDetail, rejectedId, false, 0);
|
||||
}
|
||||
@@ -195,38 +194,21 @@ public sealed class PipelineProcessingService(
|
||||
DateTimeOffset.FromUnixTimeMilliseconds(nowMs),
|
||||
ct);
|
||||
|
||||
long msgAtMs = row.MsgAtMs != 0 ? row.MsgAtMs : nowMs;
|
||||
string hue = string.IsNullOrEmpty(row.Channel.Hue) ? SourceDefaults.DefaultHue : row.Channel.Hue;
|
||||
if (row.DialogId.Length > 0 && row.MsgId is not null)
|
||||
var extra = new Dictionary<string, string>(row.Source.Extra ?? new Dictionary<string, string>())
|
||||
{
|
||||
await ingest.EnqueueAsync(new QueuedMessage
|
||||
{
|
||||
DialogId = row.DialogId,
|
||||
ChannelName = row.Channel.Name,
|
||||
ChannelHandle = row.Channel.Handle,
|
||||
ChannelHue = hue,
|
||||
MsgId = row.MsgId,
|
||||
Text = text,
|
||||
MsgAtMs = msgAtMs,
|
||||
Force = true,
|
||||
}, ct);
|
||||
}
|
||||
else
|
||||
[SourceRefs.HueKey] = row.Source.ResolveHue(),
|
||||
};
|
||||
SourceRef source = row.Source with { Extra = extra };
|
||||
|
||||
await ingest.EnqueueAsync(new QueuedMessage
|
||||
{
|
||||
var item = new QueueItemDto
|
||||
Item = new SourceItem
|
||||
{
|
||||
Id = PrefixId.New(PipelineIdPrefixes.Queue),
|
||||
DialogId = row.DialogId,
|
||||
MsgId = row.MsgId,
|
||||
Text = MessageTextCleaner.SliceCodePoints(text, MaxRejectedTextLength),
|
||||
Status = PipelineQueueStatuses.New,
|
||||
Channel = new PipelineChannelDto(row.Channel.Name, row.Channel.Handle, hue),
|
||||
MsgAtMs = msgAtMs,
|
||||
QueuedAtMs = nowMs,
|
||||
Force = true,
|
||||
};
|
||||
await store.AddAsync(item, ct);
|
||||
}
|
||||
Source = source,
|
||||
Content = row.Content with { Text = text },
|
||||
},
|
||||
Force = true,
|
||||
}, ct);
|
||||
|
||||
return new RejectReturnResultDto(null, rejectedId, true, nowMs);
|
||||
}
|
||||
|
||||
+3
-6
@@ -20,14 +20,11 @@ public sealed partial class PipelineWorkerService
|
||||
{
|
||||
return _processing.RejectAsync(new RejectRecord
|
||||
{
|
||||
DialogId = row.DialogId,
|
||||
MsgId = row.MsgId,
|
||||
Source = row.Source,
|
||||
Content = row.Content,
|
||||
Text = row.Text,
|
||||
ChannelName = row.Channel.Name,
|
||||
ChannelHandle = row.Channel.Handle,
|
||||
ChannelHue = row.Channel.Hue,
|
||||
MsgAtMs = row.MsgAtMs,
|
||||
Source = source,
|
||||
DecidedBy = source,
|
||||
Stage = stage,
|
||||
Reason = reason,
|
||||
Kw = kw,
|
||||
|
||||
@@ -70,7 +70,6 @@ public sealed class RuntimeDepthsCollectorTests
|
||||
private static QueueItemDto QueueItem(string id) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = "d_1",
|
||||
Text = "текст",
|
||||
Status = PipelineQueueStatuses.New,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Globalization;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
@@ -6,6 +8,7 @@ using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Settings.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Contracts;
|
||||
|
||||
@@ -64,15 +67,17 @@ public sealed class CardComposerTests
|
||||
snapshot.Contacts);
|
||||
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("ООО Ромашка ищет Python-разработчика на бота для CRM, удалённо.", snapshot.SourceMsg);
|
||||
Assert.Equal("d_1", snapshot.SourceDialogId);
|
||||
Assert.Equal(123, snapshot.SourceMsgId);
|
||||
Assert.Equal("ООО Ромашка ищет Python-разработчика на бота для CRM, удалённо.", snapshot.Content.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_LongSourceText_TruncatesSourceMsgTo4000CodePoints()
|
||||
public async Task BuildAsync_LongSourceText_TruncatesContentTextTo4000CodePoints()
|
||||
{
|
||||
(CardComposer composer, _, _) = Create();
|
||||
string longText = new string('а', 4500);
|
||||
@@ -83,7 +88,7 @@ public sealed class CardComposerTests
|
||||
cardId: "l_test2",
|
||||
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]
|
||||
@@ -223,20 +228,6 @@ public sealed class CardComposerTests
|
||||
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",
|
||||
long? msgId = 123,
|
||||
string channelName = "Канал найма",
|
||||
string channelHandle = "hire_ch",
|
||||
string channelHue = "#ababab",
|
||||
long msgAtMs = 1_700_000_000_000) => new()
|
||||
{
|
||||
Id = "p_1",
|
||||
DialogId = dialogId,
|
||||
MsgId = msgId,
|
||||
Text = text,
|
||||
Status = PipelineQueueStatuses.Filtered,
|
||||
Channel = new PipelineChannelDto(channelName, channelHandle, channelHue),
|
||||
MsgAtMs = msgAtMs,
|
||||
QueuedAtMs = msgAtMs,
|
||||
};
|
||||
long msgAtMs = 1_700_000_000_000) => SourceItemFactory.Queue(
|
||||
"p_1",
|
||||
text,
|
||||
PipelineQueueStatuses.Filtered,
|
||||
origin: dialogId,
|
||||
externalId: msgId?.ToString(CultureInfo.InvariantCulture),
|
||||
name: channelName,
|
||||
hue: channelHue,
|
||||
msgAtMs: msgAtMs,
|
||||
queuedAtMs: msgAtMs);
|
||||
|
||||
// Доска с правилами (для сценариев ContainerAccepts-страховки).
|
||||
// id: Id доски (b_...).
|
||||
@@ -316,8 +305,4 @@ public sealed class CardComposerTests
|
||||
private static CardBudgetDto? Converted(CardSnapshot snapshot) => snapshot.ConvCur.Length == 0
|
||||
? null
|
||||
: 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.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Cards;
|
||||
@@ -210,7 +211,7 @@ public sealed class CardsServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Move_EmptySourceMsg_UsesTitleForMatchHitsAndPush()
|
||||
public async Task Move_EmptySourceText_UsesTitleForMatchHitsAndPush()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
|
||||
@@ -677,7 +678,7 @@ public sealed class CardsServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_FindsByTitleSummaryContactSourceMsg_OrderedByReceivedAtDesc()
|
||||
public async Task Search_FindsByTitleSummaryContactSourceText_OrderedByReceivedAtDesc()
|
||||
{
|
||||
(CardsService service, FakeKanjStore store, _, _) = Create();
|
||||
store.SeedCard(Card("l_title", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300));
|
||||
@@ -759,7 +760,7 @@ public sealed class CardsServiceTests
|
||||
Title = title,
|
||||
Summary = summary,
|
||||
Contact = contact,
|
||||
SourceMsg = sourceMsg,
|
||||
Content = new SourceContent { Text = sourceMsg },
|
||||
PrevCol = prevCol,
|
||||
ReceivedAtMs = receivedAtMs,
|
||||
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.Kanban;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Contracts;
|
||||
|
||||
@@ -110,15 +111,10 @@ public sealed class PipelineCardWriterTests
|
||||
private static QueueItemDto Message(
|
||||
string text,
|
||||
string dialogId = "d_1",
|
||||
long? msgId = 123) => new()
|
||||
{
|
||||
Id = "p_1",
|
||||
DialogId = dialogId,
|
||||
MsgId = msgId,
|
||||
Text = text,
|
||||
Status = PipelineQueueStatuses.Filtered,
|
||||
Channel = new PipelineChannelDto("Канал", "ch", "#666"),
|
||||
MsgAtMs = 1_700_000_000_000,
|
||||
QueuedAtMs = 1_700_000_000_000,
|
||||
};
|
||||
long? msgId = 123) => SourceItemFactory.Queue(
|
||||
"p_1",
|
||||
text,
|
||||
PipelineQueueStatuses.Filtered,
|
||||
origin: dialogId,
|
||||
externalId: msgId?.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
@@ -202,18 +202,11 @@ public sealed class PipelineWorkerGrpcAiTests
|
||||
// id: Id строки (p_…).
|
||||
// text: Текст сообщения.
|
||||
// Возвращает: Строка как если бы была переведена в filtered воркером.
|
||||
private static QueueItemDto QueueRowFiltered(string id, string text) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = "d_1",
|
||||
MsgId = 100,
|
||||
Text = text,
|
||||
Status = PipelineQueueStatuses.Filtered,
|
||||
Channel = new PipelineChannelDto("Канал А", "channel_a", "#333"),
|
||||
MsgAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
QueuedAtMs = 1_700_000_000_000,
|
||||
Force = false,
|
||||
};
|
||||
private static QueueItemDto QueueRowFiltered(string id, string text) => SourceItemFactory.Queue(
|
||||
id,
|
||||
text,
|
||||
PipelineQueueStatuses.Filtered,
|
||||
msgAtMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
|
||||
// Неготовая модель: «не уверена» (ML-слот воркера не решает — сообщение уходит к ИИ).
|
||||
// Возвращает: Фиксированный «не готов/не уверен».
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Deal.Infrastructure.Services;
|
||||
using Deal.Modules.Cards.Application.Dtos;
|
||||
using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
@@ -44,7 +45,7 @@ public sealed class CardMoverTests
|
||||
{
|
||||
(CardMover mover, FakeKanjStore store) = Create();
|
||||
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);
|
||||
|
||||
@@ -58,7 +59,7 @@ public sealed class CardMoverTests
|
||||
public async Task Move_UnknownTarget_ReturnsDashboardInvalidTargetError()
|
||||
{
|
||||
(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);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Abstractions;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
|
||||
@@ -175,7 +176,7 @@ public class FakeKanjStore : ICardStore
|
||||
.Where(card => card.Title.ToLowerInvariant().Contains(lowered, StringComparison.Ordinal)
|
||||
|| card.Summary.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)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
@@ -190,17 +191,20 @@ public class FakeKanjStore : ICardStore
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CardDto?> GetCardBySourceAsync(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
SourceRef source,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dialogId))
|
||||
if (string.IsNullOrEmpty(source.Kind))
|
||||
{
|
||||
return Task.FromResult<CardDto?>(null);
|
||||
}
|
||||
|
||||
string externalId = source.ExternalId ?? string.Empty;
|
||||
string originRef = source.OriginRef ?? string.Empty;
|
||||
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)
|
||||
.FirstOrDefault();
|
||||
return Task.FromResult(match);
|
||||
@@ -233,12 +237,10 @@ public class FakeKanjStore : ICardStore
|
||||
: new CardBudgetDto(snapshot.ConvFrom, snapshot.ConvTo, snapshot.ConvCur),
|
||||
Contact = snapshot.Contact,
|
||||
Contacts = snapshot.Contacts,
|
||||
Channel = new CardChannelDto(snapshot.ChannelName, snapshot.ChannelHandle, snapshot.ChannelHue),
|
||||
Source = snapshot.Source,
|
||||
Content = snapshot.Content,
|
||||
Time = "только что",
|
||||
ReceivedAtMs = snapshot.ReceivedAt.ToUnixTimeMilliseconds(),
|
||||
SourceMsg = snapshot.SourceMsg,
|
||||
SourceDialogId = snapshot.SourceDialogId,
|
||||
SourceMsgId = snapshot.SourceMsgId,
|
||||
PrevCol = snapshot.PrevCol,
|
||||
MatchHits = snapshot.MatchHits,
|
||||
TzText = snapshot.TzText,
|
||||
@@ -418,12 +420,13 @@ public class FakeKanjStore : ICardStore
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
examples.Add(new AiMarkupExampleDto(card.SourceMsg, move.ToCol));
|
||||
examples.Add(new AiMarkupExampleDto(sourceText, move.ToCol));
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AiMarkupExampleDto>>(examples);
|
||||
@@ -844,7 +847,7 @@ public class FakeKanjStore : ICardStore
|
||||
public Task<IReadOnlyList<CardDto>> ListInboxWithSourceAsync(CancellationToken ct)
|
||||
{
|
||||
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)
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
@@ -174,7 +175,7 @@ public sealed class AiClassifyContextBuilderTests
|
||||
string board,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
@@ -45,12 +46,11 @@ public class FakePipelineStore : IPipelineStore
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ExistsDuplicateAsync(
|
||||
string dialogId,
|
||||
long? msgId,
|
||||
SourceRef source,
|
||||
CancellationToken ct)
|
||||
{
|
||||
bool found = msgId is not null
|
||||
&& _queue.Any(item => item.DialogId == dialogId && item.MsgId == msgId);
|
||||
string key = source.DedupeKey();
|
||||
bool found = _queue.Any(item => item.Source.DedupeKey() == key);
|
||||
return Task.FromResult(found);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ public class FakePipelineStore : IPipelineStore
|
||||
.Where(item => LikeField(item.Text, query)
|
||||
|| LikeField(item.Reason, query)
|
||||
|| LikeField(item.Kw, query)
|
||||
|| LikeField(item.Channel.Name, query))
|
||||
|| LikeField(item.Source.DisplayName ?? string.Empty, query))
|
||||
.OrderByDescending(item => item.RejectedAtMs)
|
||||
.Take(limitLike)
|
||||
.ToList();
|
||||
@@ -300,16 +300,15 @@ public class FakePipelineStore : IPipelineStore
|
||||
long rejectedAtMs) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = record.DialogId,
|
||||
MsgId = record.MsgId,
|
||||
Source = record.Source,
|
||||
Content = record.Content,
|
||||
Text = record.Text,
|
||||
Stage = record.Stage,
|
||||
StageLabel = PipelineRejectConstants.StageLabel(record.Stage),
|
||||
Reason = record.Reason,
|
||||
Kw = record.Kw,
|
||||
Source = record.Source,
|
||||
SourceLabel = PipelineRejectConstants.SourceLabel(record.Source),
|
||||
Channel = new PipelineChannelDto(record.ChannelName, record.ChannelHandle, record.ChannelHue),
|
||||
DecidedBy = record.DecidedBy,
|
||||
DecidedByLabel = PipelineRejectConstants.SourceLabel(record.DecidedBy),
|
||||
MsgAtMs = record.MsgAtMs,
|
||||
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.Services;
|
||||
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.Modules.Cards;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Kanban;
|
||||
|
||||
@@ -30,31 +33,26 @@ public sealed class MlReviewServiceTests
|
||||
private static QueueItemDto QueueRow(
|
||||
long msgId,
|
||||
string text,
|
||||
string dialog = Dialog) => new()
|
||||
{
|
||||
Id = $"p_{msgId}",
|
||||
DialogId = dialog,
|
||||
MsgId = msgId,
|
||||
Text = text,
|
||||
Status = PipelineQueueStatuses.New,
|
||||
Channel = new PipelineChannelDto("Канал", "ch", "#333"),
|
||||
MsgAtMs = 1_700_000_000_000 + msgId,
|
||||
};
|
||||
string dialog = Dialog) => SourceItemFactory.Queue(
|
||||
$"p_{msgId}",
|
||||
text,
|
||||
PipelineQueueStatuses.New,
|
||||
origin: dialog,
|
||||
externalId: msgId.ToString(CultureInfo.InvariantCulture),
|
||||
msgAtMs: 1_700_000_000_000 + msgId);
|
||||
|
||||
private static RejectedItemDto RejectedRow(
|
||||
long msgId,
|
||||
string text,
|
||||
string dialog = Dialog) => new()
|
||||
{
|
||||
Id = $"r_{msgId}",
|
||||
DialogId = dialog,
|
||||
MsgId = msgId,
|
||||
Text = text,
|
||||
Stage = "spam_ml",
|
||||
Reason = "спам",
|
||||
Source = "ml",
|
||||
MsgAtMs = 1_700_000_000_000 + msgId,
|
||||
};
|
||||
string dialog = Dialog) => SourceItemFactory.Rejected(
|
||||
$"r_{msgId}",
|
||||
text,
|
||||
stage: "spam_ml",
|
||||
decidedBy: "ml",
|
||||
reason: "спам",
|
||||
origin: dialog,
|
||||
externalId: msgId.ToString(CultureInfo.InvariantCulture),
|
||||
msgAtMs: 1_700_000_000_000 + msgId);
|
||||
|
||||
private static CardDto Card(
|
||||
long msgId,
|
||||
@@ -65,9 +63,10 @@ public sealed class MlReviewServiceTests
|
||||
Id = $"c_{msgId}",
|
||||
Col = col,
|
||||
Title = text,
|
||||
SourceMsg = text,
|
||||
SourceDialogId = dialog,
|
||||
SourceMsgId = msgId,
|
||||
Source = SourceItemFactory.Source(
|
||||
origin: dialog,
|
||||
externalId: msgId.ToString(CultureInfo.InvariantCulture)),
|
||||
Content = SourceItemFactory.Content(text),
|
||||
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);
|
||||
|
||||
Assert.Equal(3, items.Count);
|
||||
Assert.Contains(items, item => item.Id == 101 && item.Verdict == MlReviewService.VerdictQueued && !item.Lead);
|
||||
Assert.Contains(items, item => item.Id == 102 && item.Verdict == MlReviewService.VerdictRejected);
|
||||
MlCandidateDto cardItem = Assert.Single(items, item => item.Id == 103);
|
||||
Assert.Contains(items, item => item.Source.ExternalId == "101" && item.Verdict == MlReviewService.VerdictQueued && !item.Lead);
|
||||
Assert.Contains(items, item => item.Source.ExternalId == "102" && item.Verdict == MlReviewService.VerdictRejected);
|
||||
MlCandidateDto cardItem = Assert.Single(items, item => item.Source.ExternalId == "103");
|
||||
Assert.True(cardItem.Lead);
|
||||
Assert.Equal(MlReviewService.VerdictCard, cardItem.Verdict);
|
||||
Assert.Equal("inbox", cardItem.Col);
|
||||
@@ -103,7 +102,7 @@ public sealed class MlReviewServiceTests
|
||||
IReadOnlyList<MlCandidateDto> items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
|
||||
|
||||
MlCandidateDto only = Assert.Single(items);
|
||||
Assert.Equal(101, only.Id);
|
||||
Assert.Equal("101", only.Source.ExternalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -186,7 +185,7 @@ public sealed class MlReviewServiceTests
|
||||
Assert.Empty(pipeline.Queue);
|
||||
RejectedItemDto rejected = Assert.Single(pipeline.Rejected);
|
||||
Assert.Equal("spam_ml", rejected.Stage);
|
||||
Assert.Equal("ml", rejected.Source);
|
||||
Assert.Equal("ml", rejected.DecidedBy);
|
||||
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.Services;
|
||||
|
||||
@@ -10,17 +11,17 @@ public sealed class SuggestHeuristicsTests
|
||||
{
|
||||
// Карточка «Неразобранного» с текстом (helper; время — порядок окна, по умолчанию 0).
|
||||
// id: Id карточки.
|
||||
// sourceMsg: Исходное сообщение (source_msg).
|
||||
// sourceText: Исходное сообщение.
|
||||
// receivedAtMs: Время получения, epoch-ms.
|
||||
// Возвращает: Карточка для входа PlanColumns.
|
||||
private static CardDto InboxCard(
|
||||
string id,
|
||||
string sourceMsg,
|
||||
string sourceText,
|
||||
long receivedAtMs = 0) => new()
|
||||
{
|
||||
Id = id,
|
||||
Col = KanbanColumns.Inbox,
|
||||
SourceMsg = sourceMsg,
|
||||
Content = new SourceContent { Text = sourceText },
|
||||
ReceivedAtMs = receivedAtMs,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Pipeline;
|
||||
|
||||
@@ -19,42 +21,43 @@ public sealed class PipelineIngestServiceTests
|
||||
|
||||
PipelineIngestResultDto result = await service.EnqueueAsync(
|
||||
Message(
|
||||
dialogId: "d_1",
|
||||
msgId: 42,
|
||||
text: " Нужен Python-разработчик, удалёнка ",
|
||||
channelName: "Канал",
|
||||
channelHandle: "ch",
|
||||
channelHue: "#ababab",
|
||||
msgAtMs: 1_000),
|
||||
" Нужен Python-разработчик, удалёнка ",
|
||||
origin: "d_1",
|
||||
externalId: "42",
|
||||
name: "Канал",
|
||||
hue: "#ababab",
|
||||
receivedAt: DateTimeOffset.FromUnixTimeMilliseconds(1_000)),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.StartsWith(PipelineIdPrefixes.Queue, result.Id);
|
||||
Assert.False(result.Duplicate);
|
||||
QueueItemDto item = Assert.Single(store.Queue);
|
||||
Assert.Equal(result.Id, item.Id);
|
||||
Assert.Equal("d_1", item.DialogId);
|
||||
Assert.Equal(42, item.MsgId);
|
||||
Assert.Equal("Нужен Python-разработчик, удалёнка", item.Text); // trim краёв (enqueue L68)
|
||||
Assert.Equal("d_1", item.Source.OriginRef);
|
||||
Assert.Equal("42", item.Source.ExternalId);
|
||||
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("Канал", item.Channel.Name);
|
||||
Assert.Equal("ch", item.Channel.Handle);
|
||||
Assert.Equal("#ababab", item.Channel.Hue);
|
||||
Assert.Equal(1_000, item.MsgAtMs);
|
||||
Assert.True(item.QueuedAtMs > 0, "queuedAt = now (CreatedAt/UpdatedAt строки)");
|
||||
Assert.False(item.Force);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_MsgAtAbsent_UsesNow()
|
||||
public async Task EnqueueAsync_MsgAtAbsent_ZeroMsgAtAndQueuedNow()
|
||||
{
|
||||
FakePipelineStore store = new();
|
||||
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);
|
||||
Assert.True(item.MsgAtMs > 0, "msgAt = now, если время сообщения не передано (Ruling 2)");
|
||||
Assert.Equal(item.MsgAtMs, item.QueuedAtMs);
|
||||
Assert.Equal(0, item.MsgAtMs); // нет времени источника — нулевая отметка (Ruling 2)
|
||||
Assert.True(item.QueuedAtMs > 0, "queuedAt = now");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -63,12 +66,12 @@ public sealed class PipelineIngestServiceTests
|
||||
FakePipelineStore store = new();
|
||||
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);
|
||||
}
|
||||
|
||||
// ─── Приём: no-op (пустой текст / нет диалога) ──────────────────────────
|
||||
// ─── Приём: no-op (пустой текст) ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_EmptyOrWhitespaceText_Noop()
|
||||
@@ -85,63 +88,53 @@ public sealed class PipelineIngestServiceTests
|
||||
Assert.Empty(store.Queue); // пустой текст — no-op (enqueue L69)
|
||||
}
|
||||
|
||||
// ─── Приём: дубль-гвард (ключ записи источника) ─────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_MissingDialogId_Noop()
|
||||
public async Task EnqueueAsync_SameSourceTwice_SecondIsDuplicate()
|
||||
{
|
||||
FakePipelineStore store = new();
|
||||
PipelineIngestService service = new(store);
|
||||
|
||||
PipelineIngestResultDto result = await service.EnqueueAsync(Message(text: "Текст без диалога", dialogId: string.Empty), CancellationToken.None);
|
||||
|
||||
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: "Сообщение снова");
|
||||
QueuedMessage first = Message("Сообщение", origin: "d_1", externalId: "7");
|
||||
QueuedMessage second = Message("Сообщение снова", origin: "d_1", externalId: "7");
|
||||
|
||||
PipelineIngestResultDto accepted = await service.EnqueueAsync(first, CancellationToken.None);
|
||||
PipelineIngestResultDto duplicate = await service.EnqueueAsync(second, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(accepted.Id);
|
||||
Assert.Null(duplicate.Id);
|
||||
Assert.True(duplicate.Duplicate); // Telethon-повтор не пишем (enqueue L74–80)
|
||||
Assert.True(duplicate.Duplicate); // повтор записи источника не пишем
|
||||
Assert.Single(store.Queue);
|
||||
Assert.Equal("Сообщение", Assert.Single(store.Queue).Text); // первая вставка не перезаписана
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_SameDialogDifferentMsgIds_BothAccepted()
|
||||
public async Task EnqueueAsync_SameSourceDifferentExternalIds_BothAccepted()
|
||||
{
|
||||
FakePipelineStore store = new();
|
||||
PipelineIngestService service = new(store);
|
||||
|
||||
await service.EnqueueAsync(Message(dialogId: "d_1", msgId: 1, text: "Первое"), CancellationToken.None);
|
||||
PipelineIngestResultDto second = await service.EnqueueAsync(Message(dialogId: "d_1", msgId: 2, text: "Второе"), CancellationToken.None);
|
||||
await service.EnqueueAsync(Message("Первое", origin: "d_1", externalId: "1"), CancellationToken.None);
|
||||
PipelineIngestResultDto second = await service.EnqueueAsync(Message("Второе", origin: "d_1", externalId: "2"), CancellationToken.None);
|
||||
|
||||
Assert.NotNull(second.Id);
|
||||
Assert.Equal(2, store.Queue.Count); // гвард — по паре dialog+msgId, разные сообщения проходят
|
||||
Assert.Equal(2, store.Queue.Count); // гвард — по ключу источника, разные записи проходят
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_NoMsgId_SameDialogTwice_BothAccepted()
|
||||
public async Task EnqueueAsync_SameOriginWithoutExternalId_SecondIsDuplicate()
|
||||
{
|
||||
FakePipelineStore store = new();
|
||||
PipelineIngestService service = new(store);
|
||||
|
||||
PipelineIngestResultDto first = await service.EnqueueAsync(Message(dialogId: "d_1", text: "Первое"), CancellationToken.None);
|
||||
PipelineIngestResultDto second = await service.EnqueueAsync(Message(dialogId: "d_1", text: "Второе"), CancellationToken.None);
|
||||
PipelineIngestResultDto first = await service.EnqueueAsync(
|
||||
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(second.Id); // дубль-гвард работает только при msgId (enqueue L75: if msg_id)
|
||||
Assert.Equal(2, store.Queue.Count);
|
||||
Assert.True(second.Duplicate); // ключ источника без внешнего id совпадает
|
||||
Assert.Single(store.Queue);
|
||||
}
|
||||
|
||||
// ─── Приём: лимит текста 6000 (text[:6000]) ─────────────────────────────
|
||||
@@ -153,7 +146,7 @@ public sealed class PipelineIngestServiceTests
|
||||
PipelineIngestService service = new(store);
|
||||
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);
|
||||
Assert.Equal(6_000, item.Text.EnumerateRunes().Count()); // python text[:6000] — кодовые точки
|
||||
@@ -164,21 +157,10 @@ public sealed class PipelineIngestServiceTests
|
||||
// Команда приёма со значениями по умолчанию (сценарий теста перекрывает нужные поля).
|
||||
private static QueuedMessage Message(
|
||||
string text,
|
||||
string dialogId = "d_1",
|
||||
long? msgId = null,
|
||||
string channelName = "",
|
||||
string channelHandle = "",
|
||||
string channelHue = "",
|
||||
long? msgAtMs = null,
|
||||
bool force = false) => new()
|
||||
{
|
||||
DialogId = dialogId,
|
||||
MsgId = msgId,
|
||||
Text = text,
|
||||
ChannelName = channelName,
|
||||
ChannelHandle = channelHandle,
|
||||
ChannelHue = channelHue,
|
||||
MsgAtMs = msgAtMs,
|
||||
Force = force,
|
||||
};
|
||||
string? origin = SourceItemFactory.Origin,
|
||||
string? externalId = SourceItemFactory.ExternalId,
|
||||
string? name = SourceItemFactory.Name,
|
||||
string? hue = SourceItemFactory.Hue,
|
||||
DateTimeOffset? receivedAt = null,
|
||||
bool force = false) => SourceItemFactory.Message(text, origin, externalId, name, hue, receivedAt, 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.Services;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Kanban;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Pipeline;
|
||||
|
||||
@@ -16,7 +19,7 @@ public sealed class PipelineProcessingServiceTests
|
||||
{
|
||||
(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
|
||||
}
|
||||
@@ -28,23 +31,23 @@ public sealed class PipelineProcessingServiceTests
|
||||
|
||||
await service.RejectAsync(new RejectRecord
|
||||
{
|
||||
DialogId = "d_1",
|
||||
MsgId = 7,
|
||||
Source = SourceItemFactory.Source(origin: "d_1", externalId: "7", name: string.Empty, hue: null),
|
||||
Content = SourceItemFactory.Content(new string('а', 7_000), string.Empty),
|
||||
Text = new string('а', 7_000),
|
||||
Reason = new string('р', 600),
|
||||
Kw = new string('к', 300),
|
||||
Source = "stop",
|
||||
DecidedBy = "stop",
|
||||
Stage = "stop",
|
||||
}, CancellationToken.None);
|
||||
|
||||
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(500, row.Reason.EnumerateRunes().Count()); // reason[:500]
|
||||
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.Source);
|
||||
Assert.Equal("stop", row.DecidedBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -56,7 +59,7 @@ public sealed class PipelineProcessingServiceTests
|
||||
await service.RejectAsync(Reject("d_1", 7, stage: "stop", reason: "стоп-фраза «реклама»"), CancellationToken.None);
|
||||
|
||||
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("стоп-фраза «реклама»", row.Reason);
|
||||
}
|
||||
@@ -127,23 +130,23 @@ public sealed class PipelineProcessingServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListRejectedAsync_QueryMatchesTextReasonKwAndChannel()
|
||||
public async Task ListRejectedAsync_QueryMatchesTextReasonKwAndSourceName()
|
||||
{
|
||||
(PipelineProcessingService service, FakePipelineStore store, _) = Create();
|
||||
store.SeedRejected(Rejected("r_text", text: "Middle Python разработчик", rejectedAtMs: 1_000));
|
||||
store.SeedRejected(Rejected("r_reason", reason: "стоп-фраза «реклама»", rejectedAtMs: 2_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 byReason = 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_reason", Assert.Single(byReason.Items).Id); // LIKE по reason
|
||||
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]
|
||||
@@ -193,7 +196,7 @@ public sealed class PipelineProcessingServiceTests
|
||||
public async Task ReturnAsync_DuplicateSource_Returns400()
|
||||
{
|
||||
(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);
|
||||
|
||||
@@ -225,13 +228,13 @@ public sealed class PipelineProcessingServiceTests
|
||||
"r_1",
|
||||
text: " Продвижение в каналах ",
|
||||
stage: "spam_ai",
|
||||
source: "ai",
|
||||
dialogId: "d_9",
|
||||
msgId: 5,
|
||||
channelName: "Спам",
|
||||
channelHandle: "spam_ch",
|
||||
channelHue: "#ff0000",
|
||||
msgAtMs: 5_000));
|
||||
decidedBy: "ai",
|
||||
origin: "d_9",
|
||||
externalId: "5",
|
||||
name: "Спам",
|
||||
hue: "#ff0000",
|
||||
msgAtMs: 5_000,
|
||||
receivedAt: DateTimeOffset.FromUnixTimeMilliseconds(5_000)));
|
||||
|
||||
RejectReturnResultDto? result = await service.ReturnAsync("r_1", "не спам, вернуть", CancellationToken.None);
|
||||
|
||||
@@ -255,12 +258,11 @@ public sealed class PipelineProcessingServiceTests
|
||||
Assert.StartsWith(PipelineIdPrefixes.Queue, queued.Id);
|
||||
Assert.Equal(PipelineQueueStatuses.New, queued.Status);
|
||||
Assert.True(queued.Force); // возврат: причины отсева игнорируются воркером (Ruling 5/10)
|
||||
Assert.Equal("d_9", queued.DialogId);
|
||||
Assert.Equal(5, queued.MsgId);
|
||||
Assert.Equal("d_9", queued.Source.OriginRef);
|
||||
Assert.Equal("5", queued.Source.ExternalId);
|
||||
Assert.Equal("Продвижение в каналах", queued.Text);
|
||||
Assert.Equal("Спам", queued.Channel.Name);
|
||||
Assert.Equal("spam_ch", queued.Channel.Handle);
|
||||
Assert.Equal("#ff0000", queued.Channel.Hue);
|
||||
Assert.Equal("Спам", queued.Source.DisplayName);
|
||||
Assert.Equal("#ff0000", queued.Source.ResolveHue());
|
||||
Assert.Equal(5_000, queued.MsgAtMs); // msg_at исходного сообщения сохраняется (L171)
|
||||
Assert.True(queued.QueuedAtMs > 0);
|
||||
}
|
||||
@@ -273,10 +275,11 @@ public sealed class PipelineProcessingServiceTests
|
||||
"r_1",
|
||||
text: "Короткое сообщение",
|
||||
stage: "length",
|
||||
source: "stop",
|
||||
dialogId: "d_1",
|
||||
msgId: 3,
|
||||
msgAtMs: 9_000));
|
||||
decidedBy: "stop",
|
||||
origin: "d_1",
|
||||
externalId: "3",
|
||||
msgAtMs: 9_000,
|
||||
receivedAt: DateTimeOffset.FromUnixTimeMilliseconds(9_000)));
|
||||
|
||||
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()
|
||||
{
|
||||
(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);
|
||||
|
||||
@@ -304,14 +307,19 @@ public sealed class PipelineProcessingServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnAsync_NoDialogOrMsgId_EnqueuesDirectlyWithFallbackNow()
|
||||
public async Task ReturnAsync_NoSourceInfo_EnqueuesDirectlyWithDefaultHue()
|
||||
{
|
||||
(PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
|
||||
store.SeedRejected(Rejected(
|
||||
"r_old",
|
||||
text: "Старая запись до сохранения dialog_id",
|
||||
text: "Старая запись без ссылки на исходное",
|
||||
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);
|
||||
|
||||
@@ -322,10 +330,10 @@ public sealed class PipelineProcessingServiceTests
|
||||
QueueItemDto queued = Assert.Single(store.Queue); // прямая вставка без ссылки на исходное (L174–192)
|
||||
Assert.StartsWith(PipelineIdPrefixes.Queue, queued.Id);
|
||||
Assert.True(queued.Force);
|
||||
Assert.Equal(string.Empty, queued.DialogId);
|
||||
Assert.Null(queued.MsgId);
|
||||
Assert.Equal("#666", queued.Channel.Hue); // hue не сохранён — дефолт (L181)
|
||||
Assert.True(queued.MsgAtMs > 0); // msg_at нет — now (row.get("msg_at") or now)
|
||||
Assert.Equal(string.Empty, queued.Source.OriginRef);
|
||||
Assert.Null(queued.Source.ExternalId);
|
||||
Assert.Equal("#666", queued.Source.ResolveHue()); // hue не сохранён — дефолт (L181)
|
||||
Assert.Equal(0, queued.MsgAtMs); // времени источника нет — нулевая отметка
|
||||
}
|
||||
|
||||
|
||||
@@ -387,63 +395,57 @@ public sealed class PipelineProcessingServiceTests
|
||||
string dialogId,
|
||||
long? msgId,
|
||||
string stage,
|
||||
string reason) => new()
|
||||
{
|
||||
DialogId = dialogId,
|
||||
MsgId = msgId,
|
||||
Text = "Текст сообщения",
|
||||
Stage = stage,
|
||||
Source = "stop",
|
||||
Reason = reason,
|
||||
};
|
||||
string reason) => SourceItemFactory.Reject(
|
||||
"Текст сообщения",
|
||||
origin: dialogId,
|
||||
externalId: msgId?.ToString(CultureInfo.InvariantCulture),
|
||||
name: string.Empty,
|
||||
hue: null,
|
||||
stage: stage,
|
||||
decidedBy: "stop",
|
||||
reason: reason);
|
||||
|
||||
// Строка очереди для сидирования (id/статус/время постановки).
|
||||
private static QueueItemDto QueueItem(
|
||||
string id,
|
||||
string status,
|
||||
long queuedAtMs) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = "d_1",
|
||||
Text = "текст",
|
||||
Status = status,
|
||||
Channel = new PipelineChannelDto(string.Empty, string.Empty, string.Empty),
|
||||
MsgAtMs = queuedAtMs,
|
||||
QueuedAtMs = queuedAtMs,
|
||||
};
|
||||
long queuedAtMs) => SourceItemFactory.Queue(
|
||||
id,
|
||||
"текст",
|
||||
status,
|
||||
msgAtMs: queuedAtMs,
|
||||
queuedAtMs: queuedAtMs);
|
||||
|
||||
// Запись отсева для сидирования (как строка БД после маппинга адаптера).
|
||||
private static RejectedItemDto Rejected(
|
||||
string id,
|
||||
string text = "Текст сообщения",
|
||||
string stage = "length",
|
||||
string source = "stop",
|
||||
string dialogId = "",
|
||||
long? msgId = null,
|
||||
string decidedBy = "stop",
|
||||
string reason = "",
|
||||
string kw = "",
|
||||
string channelName = "",
|
||||
string channelHandle = "",
|
||||
string channelHue = "",
|
||||
string? origin = SourceItemFactory.Origin,
|
||||
string? externalId = SourceItemFactory.ExternalId,
|
||||
string? name = SourceItemFactory.Name,
|
||||
string? hue = SourceItemFactory.Hue,
|
||||
long msgAtMs = 0,
|
||||
long rejectedAtMs = 1_000,
|
||||
bool returned = false,
|
||||
string returnReason = "") => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = dialogId,
|
||||
MsgId = msgId,
|
||||
Text = text,
|
||||
Stage = stage,
|
||||
StageLabel = PipelineRejectConstants.StageLabel(stage),
|
||||
Reason = reason,
|
||||
Kw = kw,
|
||||
Source = source,
|
||||
SourceLabel = PipelineRejectConstants.SourceLabel(source),
|
||||
Channel = new PipelineChannelDto(channelName, channelHandle, channelHue),
|
||||
MsgAtMs = msgAtMs,
|
||||
RejectedAtMs = rejectedAtMs,
|
||||
Returned = returned,
|
||||
ReturnReason = returnReason,
|
||||
};
|
||||
string returnReason = "",
|
||||
DateTimeOffset? receivedAt = null) => SourceItemFactory.Rejected(
|
||||
id,
|
||||
text,
|
||||
stage,
|
||||
decidedBy,
|
||||
reason,
|
||||
kw,
|
||||
origin,
|
||||
externalId,
|
||||
name,
|
||||
hue,
|
||||
msgAtMs,
|
||||
rejectedAtMs,
|
||||
returned,
|
||||
returnReason,
|
||||
receivedAt);
|
||||
}
|
||||
|
||||
@@ -275,17 +275,11 @@ public sealed class AdminTickOrchestratorTests
|
||||
}
|
||||
|
||||
// Строка очереди (status new; сценарий перекрывает текст/время).
|
||||
private static QueueItemDto QueueRow(string id, string text) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = "d_1",
|
||||
MsgId = 100,
|
||||
Text = text,
|
||||
Status = PipelineQueueStatuses.New,
|
||||
Channel = new PipelineChannelDto("Канал А", "channel_a", "#333"),
|
||||
MsgAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
QueuedAtMs = 1_700_000_000_000,
|
||||
};
|
||||
private static QueueItemDto QueueRow(string id, string text) => SourceItemFactory.Queue(
|
||||
id,
|
||||
text,
|
||||
PipelineQueueStatuses.New,
|
||||
msgAtMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
|
||||
// Минимальный разбор карточки (карточку домыслит CardComposer; колонка не назначена → inbox).
|
||||
private static AiParsedCardDto Parsed(string title, bool isVacancy = false) => new(
|
||||
@@ -306,23 +300,16 @@ public sealed class AdminTickOrchestratorTests
|
||||
Board: null);
|
||||
|
||||
// Запись отсева для сидирования (как строка БД после маппинга адаптера).
|
||||
private static RejectedItemDto Rejected(string id, long rejectedAtMs) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = string.Empty,
|
||||
Text = "Текст сообщения",
|
||||
Stage = "length",
|
||||
StageLabel = PipelineRejectConstants.StageLabel("length"),
|
||||
Reason = string.Empty,
|
||||
Kw = string.Empty,
|
||||
Source = "stop",
|
||||
SourceLabel = PipelineRejectConstants.SourceLabel("stop"),
|
||||
Channel = new PipelineChannelDto(string.Empty, string.Empty, string.Empty),
|
||||
MsgAtMs = 0,
|
||||
RejectedAtMs = rejectedAtMs,
|
||||
Returned = false,
|
||||
ReturnReason = string.Empty,
|
||||
};
|
||||
private static RejectedItemDto Rejected(string id, long rejectedAtMs) => SourceItemFactory.Rejected(
|
||||
id,
|
||||
origin: string.Empty,
|
||||
externalId: null,
|
||||
name: string.Empty,
|
||||
hue: null,
|
||||
stage: "length",
|
||||
decidedBy: "stop",
|
||||
msgAtMs: 0,
|
||||
rejectedAtMs: rejectedAtMs);
|
||||
|
||||
// Вычитывает все события канала: (тип, JSON) в порядке публикации.
|
||||
// subscription: Подписка канала тенанта.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
@@ -50,19 +51,19 @@ public sealed class CardReclassifierTests
|
||||
|
||||
// Карточка «Неразобранного» с исходным текстом (поля, нужные классификации).
|
||||
// id: Id карточки.
|
||||
// sourceMsg: Исходный текст.
|
||||
// sourceText: Исходный текст.
|
||||
// contact: Старый контакт карточки.
|
||||
// receivedAtMs: Время получения (для порядка батча).
|
||||
// Возвращает: Карточка как если бы была сохранена в БД.
|
||||
private static CardDto Card(
|
||||
string id,
|
||||
string sourceMsg,
|
||||
string sourceText,
|
||||
string contact = "",
|
||||
long receivedAtMs = 1_700_000_000_000) => new()
|
||||
{
|
||||
Id = id,
|
||||
Col = "inbox",
|
||||
SourceMsg = sourceMsg,
|
||||
Content = new SourceContent { Text = sourceText },
|
||||
Contact = contact,
|
||||
ReceivedAtMs = receivedAtMs,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Globalization;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Infrastructure.Integrations.Services;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
@@ -34,7 +35,7 @@ public sealed class LocalColumnSuggesterTests
|
||||
Id = id,
|
||||
Col = KanbanColumns.Inbox,
|
||||
IsNew = true,
|
||||
SourceMsg = text,
|
||||
Content = new SourceContent { Text = text },
|
||||
ReceivedAtMs = receivedAtMs,
|
||||
});
|
||||
|
||||
@@ -225,9 +226,9 @@ public sealed class LocalColumnSuggesterTests
|
||||
(FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext();
|
||||
SeedInbox("l_1", "нужен python", store);
|
||||
SeedInbox("l_2", "нужен vue", store);
|
||||
store.SeedCard(new CardDto { Id = "l_t1", Col = KanbanColumns.Trash, SourceMsg = "ищу такси" });
|
||||
store.SeedCard(new CardDto { Id = "l_t2", Col = KanbanColumns.Trash, SourceMsg = "ищу такси" });
|
||||
store.SeedCard(new CardDto { Id = "l_a1", Col = KanbanColumns.Archive, 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, Content = new SourceContent { Text = "ищу такси" } });
|
||||
store.SeedCard(new CardDto { Id = "l_a1", Col = KanbanColumns.Archive, Content = new SourceContent { Text = "ищу такси" } });
|
||||
|
||||
SuggestKeywordsResultDto result = await suggester.SuggestKeywordsAsync(CancellationToken.None);
|
||||
|
||||
|
||||
@@ -225,17 +225,12 @@ public sealed class PipelineWorkerSchedulerTests
|
||||
private static QueueItemDto QueueRow(
|
||||
string id,
|
||||
string text,
|
||||
string dialogId) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = dialogId,
|
||||
MsgId = 100,
|
||||
Text = text,
|
||||
Status = PipelineQueueStatuses.New,
|
||||
Channel = new PipelineChannelDto("Канал", "channel", "#333"),
|
||||
MsgAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
QueuedAtMs = 1_700_000_000_000,
|
||||
};
|
||||
string dialogId) => SourceItemFactory.Queue(
|
||||
id,
|
||||
text,
|
||||
PipelineQueueStatuses.New,
|
||||
origin: dialogId,
|
||||
msgAtMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
|
||||
// Минимальный разбор карточки (карточку домыслит CardComposer; колонка не назначена → inbox).
|
||||
private static AiParsedCardDto Parsed(string title, bool isVacancy = false) => new(
|
||||
|
||||
@@ -102,18 +102,12 @@ public sealed class PipelineWorkerServiceTests
|
||||
string id,
|
||||
string text,
|
||||
long? msgAtMs = null,
|
||||
bool force = false) => new()
|
||||
{
|
||||
Id = id,
|
||||
DialogId = "d_1",
|
||||
MsgId = 100,
|
||||
Text = text,
|
||||
Status = PipelineQueueStatuses.New,
|
||||
Channel = new PipelineChannelDto("Канал А", "channel_a", "#333"),
|
||||
MsgAtMs = msgAtMs ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
QueuedAtMs = 1_700_000_000_000,
|
||||
Force = force,
|
||||
};
|
||||
bool force = false) => SourceItemFactory.Queue(
|
||||
id,
|
||||
text,
|
||||
PipelineQueueStatuses.New,
|
||||
msgAtMs: msgAtMs ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
force: force);
|
||||
|
||||
private static MlPredictResultDto NotReadyPrediction() => new(
|
||||
Take: false,
|
||||
@@ -168,7 +162,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("length", rejected.Stage);
|
||||
Assert.Equal("stop", rejected.Source);
|
||||
Assert.Equal("stop", rejected.DecidedBy);
|
||||
Assert.Equal("короче 24 символов", rejected.Reason);
|
||||
Assert.Empty(rejected.Kw);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
@@ -190,7 +184,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("stop", rejected.Stage);
|
||||
Assert.Equal("stop", rejected.Source);
|
||||
Assert.Equal("stop", rejected.DecidedBy);
|
||||
Assert.Equal("взаимный пиар", rejected.Kw);
|
||||
Assert.Equal("стоп-фраза «взаимный пиар»", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
@@ -211,7 +205,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("exclude_kw", rejected.Stage);
|
||||
Assert.Equal("stop", rejected.Source);
|
||||
Assert.Equal("stop", rejected.DecidedBy);
|
||||
Assert.Equal("крипт", rejected.Kw);
|
||||
Assert.Equal("глобальное исключение: слово/технология «крипт»", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
@@ -234,7 +228,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("exclude_budget", rejected.Stage);
|
||||
Assert.Equal("stop", rejected.Source);
|
||||
Assert.Equal("stop", rejected.DecidedBy);
|
||||
Assert.Equal(0, ctx.MlClient.PredictCalls);
|
||||
}
|
||||
|
||||
@@ -254,7 +248,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("resume", rejected.Stage);
|
||||
Assert.Equal("stop", rejected.Source);
|
||||
Assert.Equal("stop", rejected.DecidedBy);
|
||||
Assert.Equal("готов к собеседованию", rejected.Kw);
|
||||
Assert.Equal("резюме соискателя («готов к собеседованию»)", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
@@ -276,7 +270,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("dup", rejected.Stage);
|
||||
Assert.Equal("dup", rejected.Source);
|
||||
Assert.Equal("dup", rejected.DecidedBy);
|
||||
Assert.Equal("сообщение уже в системе: карточка создана ранее или этот текст уже обрабатывается", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
Assert.Equal(1, result.Staged);
|
||||
@@ -300,7 +294,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
Assert.Equal(1, store.ClaimCalls); // конфликт claim'а обработан, а не пропущен молча
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("dup", rejected.Stage);
|
||||
Assert.Equal("dup", rejected.Source);
|
||||
Assert.Equal("dup", rejected.DecidedBy);
|
||||
Assert.Equal("сообщение уже в системе: карточка создана ранее или этот текст уже обрабатывается", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
Assert.Empty(ctx.KanjStore.CardDtos); // второй карточки нет
|
||||
@@ -326,7 +320,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("stale", rejected.Stage);
|
||||
Assert.Equal("stale", rejected.Source);
|
||||
Assert.Equal("stale", rejected.DecidedBy);
|
||||
Assert.Equal("сообщение старше 1 дн. (срок до автоархива) — не заводим в систему", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
Assert.Empty(result.CreatedCards); // устаревшие не попадают в систему ни карточкой, ни ML/ИИ
|
||||
@@ -377,7 +371,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("budget", rejected.Stage);
|
||||
Assert.Equal("stop", rejected.Source);
|
||||
Assert.Equal("stop", rejected.DecidedBy);
|
||||
Assert.Equal("включён фильтр «не создавать карточку без суммы» — в тексте не указан бюджет", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
Assert.Empty(ctx.KanjStore.CardDtos);
|
||||
@@ -409,7 +403,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("spam_ml", rejected.Stage);
|
||||
Assert.Equal("ml", rejected.Source);
|
||||
Assert.Equal("ml", rejected.DecidedBy);
|
||||
Assert.Equal("ML уверен, что это спам/не заявка (score 0.90)", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
Assert.Empty(ctx.KanjStore.CardDtos);
|
||||
@@ -556,7 +550,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("spam_ai", rejected.Stage);
|
||||
Assert.Equal("ai", rejected.Source);
|
||||
Assert.Equal("ai", rejected.DecidedBy);
|
||||
Assert.Equal("ИИ: не заявка — спам, реклама, скам или служебное сообщение", rejected.Reason);
|
||||
Assert.Empty(ctx.PipelineStore.Queue);
|
||||
Assert.Empty(ctx.KanjStore.CardDtos);
|
||||
@@ -599,7 +593,7 @@ public sealed class PipelineWorkerServiceTests
|
||||
|
||||
RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected);
|
||||
Assert.Equal("filter_ai", rejected.Stage);
|
||||
Assert.Equal("ai", rejected.Source);
|
||||
Assert.Equal("ai", rejected.DecidedBy);
|
||||
Assert.Equal("ИИ-фильтр: реклама и скам", rejected.Reason);
|
||||
Assert.Equal(0, ctx.AiClassifier.ClassifyCalls); // заблокировано фильтром — классификацию не звали (L1108)
|
||||
Assert.Equal(1, result.AiDrop);
|
||||
|
||||
@@ -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 Deal.Api.Events;
|
||||
using Deal.Grpc.Telegram;
|
||||
using Deal.Modules.Cards.Application.Sources;
|
||||
using Deal.Modules.Pipeline.Application.Abstractions;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
@@ -54,12 +55,12 @@ public sealed class TelegramIngressServiceTests
|
||||
Assert.False(reply.Duplicate);
|
||||
|
||||
QueueItemDto row = Assert.Single(store.Queue);
|
||||
Assert.Equal(DialogId, row.DialogId);
|
||||
Assert.Equal(7, row.MsgId);
|
||||
Assert.Equal("telegram", row.Source.Kind);
|
||||
Assert.Equal(DialogId, row.Source.OriginRef);
|
||||
Assert.Equal("7", row.Source.ExternalId);
|
||||
Assert.Equal("Вакансия Python-разработчика", row.Text);
|
||||
Assert.Equal("Канал", row.Channel.Name);
|
||||
Assert.Equal("kanal_handle", row.Channel.Handle);
|
||||
Assert.Equal("#a33", row.Channel.Hue);
|
||||
Assert.Equal("Канал", row.Source.DisplayName);
|
||||
Assert.Equal("#a33", row.Source.ResolveHue());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
colMeta,
|
||||
fmtDateTime,
|
||||
} from '../../store.js'
|
||||
import { cleanTitle, tgSourceUrl } from '../../utils.js'
|
||||
import { cleanTitle, sourceOriginUrl } from '../../utils.js'
|
||||
import { useCardDrag } from '../../composables/dnd.js'
|
||||
import Icon from '../Icon.vue'
|
||||
import MoveMenu from './MoveMenu.vue'
|
||||
@@ -31,8 +31,8 @@ const props = defineProps({
|
||||
const isSelected = computed(() => props.mode === 'selected')
|
||||
const meta = computed(() => colMeta(props.card.containerId))
|
||||
const clean = computed(() => cleanTitle(props.card.title))
|
||||
// ссылка на исходное сообщение в Telegram (null — данных для ссылки нет)
|
||||
const sourceUrl = computed(() => tgSourceUrl(props.card))
|
||||
// ссылка на исходник (null — originRef не является абсолютным URL)
|
||||
const sourceUrl = computed(() => sourceOriginUrl(props.card))
|
||||
const terminal = computed(() => meta.value?.kind === 'terminal' || !!meta.value?.policy?.isTerminal)
|
||||
// архив и корзина — не перетаскиваются и не комментируются, только восстановление
|
||||
const isService = computed(() => ['archive', 'trash'].includes(props.card.containerId))
|
||||
|
||||
@@ -28,11 +28,13 @@ import {
|
||||
fmtDateTime,
|
||||
} from '../../store.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 MoveMenu from './MoveMenu.vue'
|
||||
import BaseModal from '../ui/BaseModal.vue'
|
||||
import CommentSection from '../ui/CommentSection.vue'
|
||||
import SourceContentView from '../ui/SourceContentView.vue'
|
||||
import Field from '../ui/Field.vue'
|
||||
|
||||
const card = computed(() => cardById(state.drawerId))
|
||||
@@ -88,9 +90,17 @@ function contactType(c) {
|
||||
function contactHref(c) {
|
||||
return contactInfo(c?.value)?.href || null
|
||||
}
|
||||
const sourceUrl = computed(() => tgSourceUrl(card.value))
|
||||
// HTML исходного сообщения — один раз на карточку, а не на каждый ре-рендер
|
||||
const sourceHtml = computed(() => renderSourceMessage(card.value?.sourceMsg || ''))
|
||||
const sourceUrl = computed(() => sourceOriginUrl(card.value))
|
||||
// есть что показать в блоке исходника (текст/ссылки/контакты/вложения)
|
||||
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())
|
||||
@@ -108,16 +118,6 @@ const stackInput = ref('')
|
||||
const tzText = ref('')
|
||||
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)
|
||||
function safeUrl(u) {
|
||||
return /^https?:\/\//i.test(String(u || '')) ? u : null
|
||||
@@ -237,8 +237,8 @@ async function copyProjectContact() {
|
||||
<div class="min-w-0">
|
||||
<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">
|
||||
<span class="w-1.5 h-1.5 rounded-full inline-block" :style="{ background: card.channel?.hue }"></span>
|
||||
{{ card.channel?.name }} · {{ card.time }}
|
||||
<span class="w-1.5 h-1.5 rounded-full inline-block" :style="{ background: card.source?.extra?.hue || '#666' }"></span>
|
||||
{{ card.source?.displayName }} · {{ card.time }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -516,7 +516,7 @@ async function copyProjectContact() {
|
||||
</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="text-[10.5px] uppercase tracking-wider text-low/80 font-medium">{{ $t('drawer.ishodnoe-soobshchenie') }}</div>
|
||||
<a
|
||||
@@ -525,21 +525,13 @@ async function copyProjectContact() {
|
||||
target="_blank"
|
||||
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"
|
||||
:title="$t('drawer.otkryt-ishodnoe-soobshchenie-v-telegram')"
|
||||
:title="$t('drawer.otkryt-ishodnoe-soobshchenie')"
|
||||
>
|
||||
<Icon name="external" :size="13" />
|
||||
{{ $t('drawer.otkryt-ishodnik') }}
|
||||
</a>
|
||||
</div>
|
||||
<details class="group">
|
||||
<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>
|
||||
<SourceContentView :content="card.content" :summary="sourceSummary" />
|
||||
</div>
|
||||
|
||||
<!-- ссылки (только «Выбранные») -->
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script setup>
|
||||
// Аватар канала/чата: инициалы + стабильный цвет по имени (без картинок).
|
||||
// Цвет детерминированный (hash от handle), чтобы аватар не «прыгал» между рендерами.
|
||||
// Аватар канала/чата: инициалы + стабильный цвет. Если источник передал
|
||||
// собственный цвет (`source.extra.hue`) — используем его, иначе цвет
|
||||
// детерминированный (hash от handle), чтобы аватар не «прыгал» между рендерами.
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
name: { type: String, default: '' },
|
||||
// handle без «@» — участвует в расчёте цвета
|
||||
// handle без «@» — участвует в расчёте цвета, если свой цвет не задан
|
||||
handle: { type: String, default: '' },
|
||||
// цвет источника (напр. `extra.hue`); пусто — берём из hash(handle)
|
||||
color: { type: String, default: '' },
|
||||
// размер в Tailwind-классах (w-*/h-*), по умолчанию 24px
|
||||
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
|
||||
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>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="rounded-full grid place-items-center font-bold shrink-0"
|
||||
:class="[size, text]"
|
||||
:style="{
|
||||
background: `hsl(${hue(handle)} 28% 50% / 0.2)`,
|
||||
color: `hsl(${hue(handle)} 45% 65%)`,
|
||||
}"
|
||||
:style="avatarStyle"
|
||||
>
|
||||
{{ initials(name) }}
|
||||
</span>
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
<script setup>
|
||||
// «Подробнее» записи пайплайна (очередь/отсев): метаданные сообщения, ссылка
|
||||
// на исходник в Telegram и само исходное сообщение с форматированием.
|
||||
// Одинаковый блок у карточек очереди и отсева — вынесен, чтобы шаблоны не
|
||||
// дублировали ~30 строк.
|
||||
// «Подробнее» записи пайплайна (очередь/отсев): метаданные записи, ссылка
|
||||
// на оригинал и универсальное содержимое источника. Одинаковый блок у карточек
|
||||
// очереди и отсева — вынесен, чтобы шаблоны не дублировали разметку.
|
||||
import { computed } from 'vue'
|
||||
import Icon from '../Icon.vue'
|
||||
import SourceContentView from './SourceContentView.vue'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
// [{ label, text, mono?, num? }] — строки метаданных (id, время…)
|
||||
meta: { type: Array, default: () => [] },
|
||||
// SourceContent: текст/ссылки/контакты/вложения
|
||||
content: { type: Object, default: null },
|
||||
// HTML исходного сообщения (уже отрендерен и мемоизирован родителем)
|
||||
sourceHtml: { type: String, default: '' },
|
||||
// ссылка «Открыть исходник в Telegram» (null — не показывать)
|
||||
// ссылка на оригинал (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>
|
||||
|
||||
<template>
|
||||
@@ -41,16 +51,12 @@ defineProps({
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<details v-if="sourceHtml" class="mt-1.5 group">
|
||||
<summary
|
||||
class="cursor-pointer list-none text-[11px] text-low/70 hover:text-mid transition-colors select-none"
|
||||
>
|
||||
{{ $t('common.sourceMessage') }}
|
||||
</summary>
|
||||
<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>
|
||||
<SourceContentView
|
||||
v-if="hasContent"
|
||||
class="mt-1.5"
|
||||
:content="content"
|
||||
:html="sourceHtml"
|
||||
:summary="$t('common.sourceMessage')"
|
||||
/>
|
||||
</div>
|
||||
</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": "Назад",
|
||||
"next": "Вперёд",
|
||||
"range": "{from}–{to} из {total}",
|
||||
"openSource": "Открыть исходник в Telegram",
|
||||
"openSource": "Открыть исходник",
|
||||
"sourceMessage": "Исходное сообщение (с форматированием)",
|
||||
"comments": "Комментарии",
|
||||
"noComments": "Комментариев пока нет",
|
||||
@@ -392,8 +392,19 @@ export const ru = {
|
||||
"colSaved": "Колонка «{name}» сохранена",
|
||||
"colCreated": "Колонка «{name}» создана — заявки будут скапливаться здесь"
|
||||
},
|
||||
"source": {
|
||||
"links": "Ссылки",
|
||||
"contacts": "Контакты",
|
||||
"contact": "Контакт",
|
||||
"attachments": "Вложения",
|
||||
"attachment": "Вложение",
|
||||
"download": "Скачать"
|
||||
},
|
||||
"processing": {
|
||||
"server-nedostupen-proverte-chto-backend": "Сервер недоступен — проверьте, что backend запущен (порт 8000)",
|
||||
"author": "Автор",
|
||||
"externalId": "ID в источнике",
|
||||
"originRef": "Оригинал",
|
||||
"zapis-otseva-udalena": "Запись отсева удалена",
|
||||
"udaleno": "Удалено",
|
||||
"vozvrashcheno-v-obrabotku-prichiny-otseva": "Возвращено в обработку — причины отсева сняты",
|
||||
@@ -1018,7 +1029,7 @@ export const ru = {
|
||||
"otkryt": "Открыть",
|
||||
"kopirovat": "Копировать",
|
||||
"ishodnoe-soobshchenie": "Исходное сообщение",
|
||||
"otkryt-ishodnoe-soobshchenie-v-telegram": "Открыть исходное сообщение в Telegram",
|
||||
"otkryt-ishodnoe-soobshchenie": "Открыть исходное сообщение",
|
||||
"otkryt-ishodnik": "Открыть исходник",
|
||||
"pokazat-tekst": "— показать текст",
|
||||
"udalit-ssylku": "Удалить ссылку",
|
||||
|
||||
@@ -152,17 +152,13 @@ export function contactInfo(contact = '') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Ссылка на исходное сообщение в Telegram по сохранённым id (если доступна)
|
||||
export function tgSourceUrl(lead) {
|
||||
if (!lead || !lead.sourceMsgId) return null
|
||||
const id = lead.sourceMsgId
|
||||
const handle = lead.channel?.handle || lead.ch?.handle
|
||||
if (handle) return `https://t.me/${handle}/${id}`
|
||||
// приватный канал: t.me/c/<peer>/<msg> (без префикса -100)
|
||||
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}`
|
||||
return null
|
||||
// Ссылка на исходник: отдаём `source.originRef`, только если это абсолютный
|
||||
// http(s)-URL. Глубокие ссылки конкретных источников здесь не собираем — их
|
||||
// формирует сам источник и кладёт в originRef. Иначе ссылка недоступна.
|
||||
export function sourceOriginUrl(item) {
|
||||
const ref = item?.source?.originRef
|
||||
const url = typeof ref === 'string' ? ref.trim() : ''
|
||||
return /^https?:\/\//i.test(url) ? url : null
|
||||
}
|
||||
|
||||
// Чистый заголовок карточки: без markdown-ссылок и служебной разметки
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
relTime,
|
||||
} from '../store.js'
|
||||
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 ChannelAvatar from '../components/ui/ChannelAvatar.vue'
|
||||
import PipelineDetails from '../components/ui/PipelineDetails.vue'
|
||||
@@ -50,8 +50,11 @@ function srcHtml(item) {
|
||||
// строки метаданных для «Подробнее» — очередь и отсев почти одинаковы
|
||||
function metaRows(item, extra = []) {
|
||||
const rows = []
|
||||
if (item.dialogId) rows.push({ label: t('processing.dialog'), text: item.dialogId, mono: true })
|
||||
if (item.msgId) rows.push({ label: t('processing.id-soobshcheniya'), text: item.msgId, mono: true })
|
||||
const src = item.source || {}
|
||||
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 })
|
||||
for (const e of extra) {
|
||||
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
|
||||
}
|
||||
|
||||
// ссылка на исходное сообщение в Telegram: @handle/msg или приватный канал
|
||||
// ссылка на исходник: originRef источника, если это абсолютный URL
|
||||
function sourceUrl(item) {
|
||||
return tgSourceUrl({
|
||||
ch: item.ch,
|
||||
sourceDialogId: item.dialogId,
|
||||
sourceMsgId: item.msgId,
|
||||
})
|
||||
return sourceOriginUrl(item)
|
||||
}
|
||||
|
||||
async function reloadAll() {
|
||||
@@ -134,7 +133,7 @@ async function onClearAll() {
|
||||
|
||||
// можно ли вернуть запись в обработку: повтор возвращать нечего (карточка уже есть)
|
||||
function canReturn(r) {
|
||||
return !!r && !r.returned && r.source !== 'dup'
|
||||
return !!r && !r.returned && r.decidedBy !== 'dup'
|
||||
}
|
||||
|
||||
function openReturn(item) {
|
||||
@@ -309,11 +308,11 @@ onBeforeUnmount(() => {
|
||||
:class="m.status === 'filtered' ? 'border-warn/20' : ''"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<ChannelAvatar :name="m.ch.name" :handle="m.ch.handle" />
|
||||
<span class="text-[12px] font-medium text-mid truncate" :title="m.ch.handle ? '@' + m.ch.handle : ''">
|
||||
{{ m.ch.name }}
|
||||
<template v-if="m.ch.handle">
|
||||
<span class="text-low/60 font-mono">@{{ m.ch.handle }}</span>
|
||||
<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.source?.originRef || ''">
|
||||
{{ m.source?.displayName || $t('processing.istochnik') }}
|
||||
<template v-if="m.source?.originRef">
|
||||
<span class="text-low/60 font-mono">{{ m.source.originRef }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
@@ -354,6 +353,7 @@ onBeforeUnmount(() => {
|
||||
<PipelineDetails
|
||||
v-if="detailOpen.has(m.id)"
|
||||
:meta="metaRows(m, [{ key: 'queuedAt', label: $t('processing.v-ocheredi-2') }])"
|
||||
:content="m.content"
|
||||
:source-html="srcHtml(m)"
|
||||
:source-url="sourceUrl(m)"
|
||||
/>
|
||||
@@ -391,18 +391,18 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<!-- Шапка: источник + причина -->
|
||||
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
|
||||
<ChannelAvatar :name="r.ch.name" :handle="r.ch.handle" />
|
||||
<span class="text-[12px] font-medium text-mid truncate" :title="r.ch.handle ? '@' + r.ch.handle : ''">
|
||||
{{ r.ch.name || $t('processing.istochnik') }}
|
||||
<template v-if="r.ch.handle">
|
||||
<span class="text-low/60 font-mono">@{{ r.ch.handle }}</span>
|
||||
<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.source?.originRef || ''">
|
||||
{{ r.source?.displayName || $t('processing.istochnik') }}
|
||||
<template v-if="r.source?.originRef">
|
||||
<span class="text-low/60 font-mono">{{ r.source.originRef }}</span>
|
||||
</template>
|
||||
</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="srcStyle(r.source).cls"
|
||||
:class="srcStyle(r.decidedBy).cls"
|
||||
>
|
||||
{{ srcStyle(r.source).label }}
|
||||
{{ r.decidedByLabel || srcStyle(r.decidedBy).label }}
|
||||
</span>
|
||||
<span
|
||||
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" />
|
||||
{{ $t('processing.vozvrashcheno-v-obrabotku') }}
|
||||
</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') }}
|
||||
</span>
|
||||
<button
|
||||
@@ -478,6 +478,7 @@ onBeforeUnmount(() => {
|
||||
<PipelineDetails
|
||||
v-if="detailOpen.has(r.id)"
|
||||
:meta="metaRows(r, [{ key: 'rejectedAt', label: $t('processing.otseyano') }, { key: 'returnedAt', label: $t('processing.vozvrashcheno') }])"
|
||||
:content="r.content"
|
||||
:source-html="srcHtml(r)"
|
||||
:source-url="sourceUrl(r)"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user