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);
}