diff --git a/src/core/Deal.Api/Endpoints/MlEndpoints.cs b/src/core/Deal.Api/Endpoints/MlEndpoints.cs index 64a4a4d..b7d816a 100644 --- a/src/core/Deal.Api/Endpoints/MlEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/MlEndpoints.cs @@ -37,8 +37,6 @@ public static class MlEndpoints private const string EnterTextDetail = "Введите текст"; - private const string MessageNotFoundDetail = "Исходное сообщение не найдено"; - /// /// Регистрирует группу /api/ml /// @@ -141,12 +139,7 @@ public static class MlEndpoints } MlReviewService review = context.RequestServices.GetRequiredService(); - MlApplyResult? result = await review.ApplyAsync(body.DialogId, body.MsgId, body.Action, ct); - if (result is null) - { - return EndpointResults.NotFound(MessageNotFoundDetail); - } - + MlApplyResult result = await review.ApplyAsync(body.DialogId, body.MsgId, body.Action, ct); if (result.Error is not null) { return EndpointResults.BadRequest(result.Error); diff --git a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs index f7ea7cb..24166af 100644 --- a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs @@ -33,8 +33,6 @@ public static class PipelineEndpoints // Путь возврата записи отсева в обработку (POST). private const string RejectedReturnPath = "/rejected/{rejId}/return"; - private const string RejectedNotFoundDetail = "Запись не найдена"; - private const int DefaultPageSize = 100; /// @@ -140,11 +138,7 @@ public static class PipelineEndpoints } PipelineProcessingService processing = context.RequestServices.GetRequiredService(); - RejectReturnResultDto? result = await processing.ReturnAsync(rejId, body.Reason ?? string.Empty, ct); - if (result is null) - { - return EndpointResults.NotFound(RejectedNotFoundDetail); - } + RejectReturnResultDto result = await processing.ReturnAsync(rejId, body.Reason ?? string.Empty, ct); return result.Error is not null ? EndpointResults.BadRequest(result.Error) diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs index 2fabdbf..14c2426 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs @@ -8,6 +8,7 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Pipeline.Application.Services; @@ -34,6 +35,9 @@ public sealed class MlReviewService( // Размер одного чтения из очереди/отсева при объединении кандидатов. private const int MaxScan = 500; + // Имя сущности для текста ошибки «не найдено». + private const string MessageEntityName = "Исходное сообщение"; + private const int TextPreviewLength = 600; /// @@ -157,8 +161,9 @@ public sealed class MlReviewService( /// Оригинал источника (OriginRef) записи. /// Внешний id записи в источнике. /// Действие: skip | spam | board:<id>. - /// Результат решения; null — исходная запись не найдена (404-семантика эндпоинта). - public async Task ApplyAsync( + /// Результат решения. + /// Исходная запись не найдена. + public async Task ApplyAsync( string dialogId, long msgId, string? action, @@ -168,17 +173,14 @@ public sealed class MlReviewService( string dialog = (dialogId ?? string.Empty).Trim(); string externalId = msgId.ToString(CultureInfo.InvariantCulture); - SourceRef? source = await ResolveSourceAsync(dialog, externalId, ct); - if (source is null) - { - return null; // 404: исходная запись не найдена - } + SourceRef source = await ResolveSourceAsync(dialog, externalId, ct) + ?? throw new NotFoundException(MessageEntityName, externalId); CardDto? card = await cardStore.GetCardBySourceAsync(source, ct); string? text = await FindTextAsync(dialog, externalId, card, ct); if (string.IsNullOrWhiteSpace(text)) { - return null; // 404: исходная запись не найдена + throw new NotFoundException(MessageEntityName, externalId); } if (normalized == ActionSkip) diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs index d845dd0..2a79770 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs @@ -4,6 +4,7 @@ using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Pipeline.Application.Abstractions; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Parse; +using Deal.SharedKernel.Errors; namespace Deal.Modules.Pipeline.Application.Services; @@ -48,6 +49,9 @@ public sealed class PipelineProcessingService( private const string DuplicateSource = "dup"; + // Имя сущности для текста ошибки «не найдено». + private const string RejectedEntityName = "Запись отсева"; + private static readonly HashSet SpamStages = new(StringComparer.Ordinal) { "spam_ml", @@ -154,17 +158,15 @@ public sealed class PipelineProcessingService( /// /// Id записи отсева (r_...). /// Причина возврата (trim, ≤500; пишется на запись для аудита). - /// null — записи нет (404); иначе результат: Error (400) либо {id, returned:true, returnedAt}. - public async Task ReturnAsync( + /// Результат: Error (400) либо {id, returned:true, returnedAt}. + /// Запись отсева не найдена. + public async Task ReturnAsync( string rejectedId, string reason, CancellationToken ct) { - RejectedItemDto? row = await store.GetAsync(rejectedId, ct); - if (row is null) - { - return null; - } + RejectedItemDto row = await store.GetAsync(rejectedId, ct) + ?? throw new NotFoundException(RejectedEntityName, rejectedId); if (row.Returned) { diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs index 9a2e827..9551349 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs @@ -4,6 +4,7 @@ using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Support; @@ -241,12 +242,11 @@ public sealed class MlReviewServiceTests } [Fact] - public async Task Apply_MessageNotFound_ReturnsNull() + public async Task Apply_MessageNotFound_ThrowsNotFound() { MlReviewService service = Create(new TestPipelineStore(), new TestKanjStore(), new TestMlClient(), out _); - MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None); - - Assert.Null(result); + await Assert.ThrowsAsync( + () => service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None)); } } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs index 6ea34d0..6b31631 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs @@ -2,6 +2,7 @@ using System.Globalization; using Deal.Modules.Cards.Application.Sources; using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Services; +using Deal.SharedKernel.Errors; using Deal.Tests.Unit.Contracts; using Deal.Tests.Unit.Support; @@ -165,13 +166,13 @@ public sealed class PipelineProcessingServiceTests [Fact] - public async Task ReturnAsync_RecordNotFound_ReturnsNull() + public async Task ReturnAsync_RecordNotFound_ThrowsNotFound() { (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create(); - RejectReturnResultDto? result = await service.ReturnAsync("r_missing", string.Empty, CancellationToken.None); + await Assert.ThrowsAsync( + () => service.ReturnAsync("r_missing", string.Empty, CancellationToken.None)); - Assert.Null(result); // эндпоинт ответит 404 «Запись не найдена» (Ruling 10) Assert.Empty(store.Queue); Assert.Empty(ml.Pushed); }