Merge pull request 'Перевести оставшиеся «не найдено» в доменных сервисах на DealException' (#26) from t21_notfound_exceptions into main
ci / build-test (push) Successful in 2m56s
ci / build-test (push) Successful in 2m56s
Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
@@ -202,9 +202,7 @@ public static class CardDetailsEndpoints
|
||||
return EndpointResults.BadRequest(result.Error);
|
||||
}
|
||||
|
||||
return result.Card is null
|
||||
? EndpointResults.NotFound(CardNotFoundDetail)
|
||||
: await ReadCardAsync(context, cardId, ct);
|
||||
return await ReadCardAsync(context, cardId, ct);
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{cardId}/links/{linkId}: удалить ссылку. Ответ — карточка.
|
||||
@@ -220,10 +218,8 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
CardsService service = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardResultDto result = await service.RemoveLinkAsync(cardId, linkId, ct);
|
||||
return result.Card is null
|
||||
? EndpointResults.NotFound(CardNotFoundDetail)
|
||||
: await ReadCardAsync(context, cardId, ct);
|
||||
await service.RemoveLinkAsync(cardId, linkId, ct);
|
||||
return await ReadCardAsync(context, cardId, ct);
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/files: загрузка вложений (multipart/form-data, поле files).
|
||||
@@ -240,10 +236,7 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
if (await cardsService.GetCardAsync(cardId, ct) is null)
|
||||
{
|
||||
return EndpointResults.NotFound(CardNotFoundDetail);
|
||||
}
|
||||
await cardsService.GetCardAsync(cardId, ct);
|
||||
|
||||
IFormCollection form;
|
||||
try
|
||||
@@ -357,9 +350,7 @@ public static class CardDetailsEndpoints
|
||||
return EndpointResults.BadRequest(result.Error);
|
||||
}
|
||||
|
||||
return result.Card is null
|
||||
? EndpointResults.NotFound(CardNotFoundDetail)
|
||||
: await ReadCardAsync(context, cardId, ct);
|
||||
return await ReadCardAsync(context, cardId, ct);
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{cardId}/reminder: снять напоминание. Ответ — карточка.
|
||||
@@ -374,9 +365,8 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
CardsService service = context.RequestServices.GetRequiredService<CardsService>();
|
||||
return await service.ClearReminderAsync(cardId, ct)
|
||||
? await ReadCardAsync(context, cardId, ct)
|
||||
: EndpointResults.NotFound(CardNotFoundDetail);
|
||||
await service.ClearReminderAsync(cardId, ct);
|
||||
return await ReadCardAsync(context, cardId, ct);
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/reminder/snooze: «напомнить позже» (now + 24 ч). Ответ — карточка.
|
||||
@@ -391,9 +381,8 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
CardsService service = context.RequestServices.GetRequiredService<CardsService>();
|
||||
return await service.SnoozeReminderAsync(cardId, ct)
|
||||
? await ReadCardAsync(context, cardId, ct)
|
||||
: EndpointResults.NotFound(CardNotFoundDetail);
|
||||
await service.SnoozeReminderAsync(cardId, ct);
|
||||
return await ReadCardAsync(context, cardId, ct);
|
||||
}
|
||||
|
||||
// Читает карточку через единый сервис и возвращает её как ответ (404 — карточки нет).
|
||||
@@ -412,11 +401,7 @@ public static class CardDetailsEndpoints
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto? card = await cardsService.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
{
|
||||
return EndpointResults.NotFound(CardNotFoundDetail);
|
||||
}
|
||||
CardDto card = await cardsService.GetCardAsync(cardId, ct);
|
||||
|
||||
SourceContent content = await cardsService.ResolveSourceAsync(card, ct);
|
||||
return Results.Ok(content);
|
||||
@@ -428,10 +413,8 @@ public static class CardDetailsEndpoints
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto? card = await cardsService.GetCardAsync(cardId, ct);
|
||||
return card is null
|
||||
? EndpointResults.NotFound(CardNotFoundDetail)
|
||||
: Results.Ok(card);
|
||||
CardDto card = await cardsService.GetCardAsync(cardId, ct);
|
||||
return Results.Ok(card);
|
||||
}
|
||||
|
||||
// Имя файла для Content-Disposition без кавычек «"».
|
||||
|
||||
@@ -128,10 +128,8 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto? card = await cardsService.GetCardAsync(cardId, ct);
|
||||
return card is null
|
||||
? EndpointResults.NotFound(CardNotFoundDetail)
|
||||
: Results.Ok(card);
|
||||
CardDto card = await cardsService.GetCardAsync(cardId, ct);
|
||||
return Results.Ok(card);
|
||||
}
|
||||
|
||||
private static async Task<IResult> MarkAllSeenAsync(HttpContext context, CancellationToken ct)
|
||||
@@ -184,18 +182,11 @@ public static class CardsEndpoints
|
||||
return EndpointResults.BadRequest(outcome.Error);
|
||||
}
|
||||
|
||||
if (!outcome.Exists)
|
||||
{
|
||||
return EndpointResults.NotFound(CardNotFoundDetail);
|
||||
}
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardMoved, new { cardId, to = body.To }, ct);
|
||||
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto? unified = await cardsService.GetCardAsync(cardId, ct);
|
||||
return unified is null
|
||||
? EndpointResults.NotFound(CardNotFoundDetail)
|
||||
: Results.Ok(unified);
|
||||
CardDto unified = await cardsService.GetCardAsync(cardId, ct);
|
||||
return Results.Ok(unified);
|
||||
}
|
||||
|
||||
private static async Task<IResult> TrashAsync(
|
||||
@@ -353,11 +344,7 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto? card = await cardsService.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
{
|
||||
return EndpointResults.NotFound(CardNotFoundDetail);
|
||||
}
|
||||
CardDto card = await cardsService.GetCardAsync(cardId, ct);
|
||||
|
||||
CardReclassifier reclassifier = context.RequestServices.GetRequiredService<CardReclassifier>();
|
||||
ReclassifyResultDto result = await reclassifier.ReclassifyCardAsync(card, ct);
|
||||
|
||||
@@ -55,9 +55,6 @@ public static class DiscoveryEndpoints
|
||||
// Путь лога задачи (GET).
|
||||
private const string TaskLogPath = "/tasks/{task_id}/log";
|
||||
|
||||
// 404: кандидат не найден.
|
||||
private const string CandidateNotFoundDetail = "Кандидат не найден";
|
||||
|
||||
private const string AlreadyJoinedDetail = "Уже вступили в этот источник";
|
||||
|
||||
private const string JoinedRejectDetail = "Уже вступили — удалите источник из каналов";
|
||||
@@ -281,11 +278,7 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
DiscoveryCandidatesService candidates = context.RequestServices.GetRequiredService<DiscoveryCandidatesService>();
|
||||
DiscoveryCandidateDto? row = await candidates.GetAsync(dialog_id, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return EndpointResults.NotFound(CandidateNotFoundDetail);
|
||||
}
|
||||
DiscoveryCandidateDto row = await candidates.GetAsync(dialog_id, ct);
|
||||
|
||||
if (row.Status == DiscoveryCandidateStatuses.Joined)
|
||||
{
|
||||
@@ -314,8 +307,8 @@ public static class DiscoveryEndpoints
|
||||
|
||||
try
|
||||
{
|
||||
DiscoveryCandidateDto? joined = await candidates.MarkJoinedAsync(dialog_id, auto: false, ct);
|
||||
return joined is null ? EndpointResults.NotFound(CandidateNotFoundDetail) : Results.Ok(joined);
|
||||
DiscoveryCandidateDto joined = await candidates.MarkJoinedAsync(dialog_id, auto: false, ct);
|
||||
return Results.Ok(joined);
|
||||
}
|
||||
catch (DiscoveryValidationException exception)
|
||||
{
|
||||
@@ -334,11 +327,7 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
DiscoveryCandidatesService candidates = context.RequestServices.GetRequiredService<DiscoveryCandidatesService>();
|
||||
DiscoveryCandidateDto? row = await candidates.GetAsync(dialog_id, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return EndpointResults.NotFound(CandidateNotFoundDetail);
|
||||
}
|
||||
DiscoveryCandidateDto row = await candidates.GetAsync(dialog_id, ct);
|
||||
|
||||
if (row.Status == DiscoveryCandidateStatuses.Joined)
|
||||
{
|
||||
@@ -347,8 +336,8 @@ public static class DiscoveryEndpoints
|
||||
|
||||
try
|
||||
{
|
||||
DiscoveryCandidateDto? rejected = await candidates.MarkRejectedAsync(dialog_id, ManualRejectReason, ct);
|
||||
return rejected is null ? EndpointResults.NotFound(CandidateNotFoundDetail) : Results.Ok(rejected);
|
||||
DiscoveryCandidateDto rejected = await candidates.MarkRejectedAsync(dialog_id, ManualRejectReason, ct);
|
||||
return Results.Ok(rejected);
|
||||
}
|
||||
catch (DiscoveryValidationException exception)
|
||||
{
|
||||
|
||||
@@ -37,8 +37,6 @@ public static class MlEndpoints
|
||||
|
||||
private const string EnterTextDetail = "Введите текст";
|
||||
|
||||
private const string MessageNotFoundDetail = "Исходное сообщение не найдено";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует группу /api/ml
|
||||
/// </summary>
|
||||
@@ -141,12 +139,7 @@ public static class MlEndpoints
|
||||
}
|
||||
|
||||
MlReviewService review = context.RequestServices.GetRequiredService<MlReviewService>();
|
||||
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);
|
||||
|
||||
@@ -144,12 +144,7 @@ public static class OperatorTenantsEndpoints
|
||||
return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail);
|
||||
}
|
||||
|
||||
TenantDetailDto? tenant = await tenantAdminService.GetAsync(id, ct);
|
||||
if (tenant is null)
|
||||
{
|
||||
return EndpointResults.NotFound(TenantNotFoundDetail);
|
||||
}
|
||||
|
||||
TenantDetailDto tenant = await tenantAdminService.GetAsync(id, ct);
|
||||
return Results.Ok(tenant);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -140,11 +138,7 @@ public static class PipelineEndpoints
|
||||
}
|
||||
|
||||
PipelineProcessingService processing = context.RequestServices.GetRequiredService<PipelineProcessingService>();
|
||||
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)
|
||||
|
||||
@@ -22,8 +22,6 @@ public sealed class CardMover(CardsService cardsService) : ICardMover
|
||||
CardResultDto result = CardsDefaultContainers.Contains(toContainerId)
|
||||
? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct)
|
||||
: await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct);
|
||||
return result.Error is not null
|
||||
? new CardMoveResultDto(result.Error, Exists: true)
|
||||
: new CardMoveResultDto(null, Exists: result.Card is not null);
|
||||
return new CardMoveResultDto(result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Deal.Modules.Cards.Application.Dtos;
|
||||
using Deal.Modules.Cards.Application.Models;
|
||||
using Deal.SharedKernel.Errors;
|
||||
|
||||
namespace Deal.Modules.Cards.Application.Abstractions;
|
||||
|
||||
@@ -14,7 +15,8 @@ public interface ICardMover
|
||||
/// <param name="cardId">Id карточки.</param>
|
||||
/// <param name="toContainerId">Id контейнера назначения (стадия «Выбранных» либо дашборд-контейнер).</param>
|
||||
/// <param name="ctx">Контекст перехода (инициатор, причина, обучение).</param>
|
||||
/// <returns>Результат: Error (400-текст отказа) | Exists=false (карточки нет, 404) | успех (Exists=true).</returns>
|
||||
/// <returns>Результат: Error (400-текст отказа) либо null (успех).</returns>
|
||||
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
|
||||
public Task<CardMoveResultDto> MoveAsync(
|
||||
string cardId,
|
||||
string toContainerId,
|
||||
|
||||
@@ -5,6 +5,5 @@ namespace Deal.Modules.Cards.Application.Dtos;
|
||||
/// <summary>
|
||||
/// Результат перехода карточки единым механизмом <see cref="ICardMover"/>.
|
||||
/// </summary>
|
||||
/// <param name="Error">Текст 400-ошибки либо null.</param>
|
||||
/// <param name="Exists">True — карточка найдена и переход выполнен (либо перенос был no-op).</param>
|
||||
public sealed record CardMoveResultDto(string? Error, bool Exists);
|
||||
/// <param name="Error">Текст 400-ошибки либо null (успех).</param>
|
||||
public sealed record CardMoveResultDto(string? Error);
|
||||
|
||||
+46
-40
@@ -2,6 +2,7 @@ using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Discovery.Application.Abstractions;
|
||||
using Deal.Modules.Discovery.Application.Exceptions;
|
||||
using Deal.Modules.Discovery.Application.Models;
|
||||
using Deal.SharedKernel.Errors;
|
||||
|
||||
namespace Deal.Modules.Discovery.Application.Services;
|
||||
|
||||
@@ -13,6 +14,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
DiscoveryLogService log,
|
||||
DiscoveryBlacklistService blacklist)
|
||||
{
|
||||
private const string CandidateEntityName = "Кандидат";
|
||||
|
||||
private const string TaskEntityName = "Задача поиска";
|
||||
|
||||
/// <summary>
|
||||
/// 400 mark_rejected
|
||||
/// </summary>
|
||||
@@ -53,10 +58,12 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// Кандидат по dialog_id.
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <returns>Кандидат или null (404 «Кандидат не найден»).</returns>
|
||||
public Task<DiscoveryCandidateDto?> GetAsync(string dialogId, CancellationToken ct)
|
||||
/// <returns>Кандидат.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
public async Task<DiscoveryCandidateDto> GetAsync(string dialogId, CancellationToken ct)
|
||||
{
|
||||
return store.GetCandidateAsync(dialogId, ct);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,11 +85,8 @@ public sealed class DiscoveryCandidatesService(
|
||||
string hue,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false);
|
||||
if (task is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
DiscoveryTaskDto task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(TaskEntityName, taskId);
|
||||
|
||||
if (await store.IsDialogMonitoredAsync(dialogId, ct).ConfigureAwait(false))
|
||||
{
|
||||
@@ -125,7 +129,8 @@ public sealed class DiscoveryCandidatesService(
|
||||
};
|
||||
await store.CreateCandidateAsync(row, ct).ConfigureAwait(false);
|
||||
await store.BumpTaskCounterAsync(taskId, DiscoveryCounterField.Found, 1, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -134,18 +139,21 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="taskId">Id задачи.</param>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="patch">Изменяемые поля (null — не менять).</param>
|
||||
/// <returns>Обновлённый кандидат либо null — задачи/кандидата нет (или кандидат другой задачи).</returns>
|
||||
public async Task<DiscoveryCandidateDto?> SetAsync(
|
||||
/// <returns>Обновлённый кандидат.</returns>
|
||||
/// <exception cref="NotFoundException">Задача или кандидат не найдены.</exception>
|
||||
public async Task<DiscoveryCandidateDto> SetAsync(
|
||||
string taskId,
|
||||
string dialogId,
|
||||
DiscoveryCandidatePatch patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false);
|
||||
DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
if (task is null || current is null || current.TaskId != taskId)
|
||||
_ = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(TaskEntityName, taskId);
|
||||
DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
if (current.TaskId != taskId)
|
||||
{
|
||||
return null;
|
||||
throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
DiscoveryCandidatePatch normalized = NormalizePatch(patch, current);
|
||||
@@ -159,7 +167,8 @@ public sealed class DiscoveryCandidatesService(
|
||||
}
|
||||
|
||||
await store.PatchCandidateAsync(dialogId, normalized, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,9 +176,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="status">Новый статус: new|review.</param>
|
||||
/// <returns>Обновлённый кандидат либо null (кандидата нет).</returns>
|
||||
/// <returns>Обновлённый кандидат.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
/// <exception cref="DiscoveryValidationException">Статус не new/review.</exception>
|
||||
public async Task<DiscoveryCandidateDto?> SetStatusAsync(
|
||||
public async Task<DiscoveryCandidateDto> SetStatusAsync(
|
||||
string dialogId,
|
||||
string status,
|
||||
CancellationToken ct)
|
||||
@@ -179,11 +189,8 @@ public sealed class DiscoveryCandidatesService(
|
||||
throw new DiscoveryValidationException(string.Format(TransitionNotAllowedFormat, status));
|
||||
}
|
||||
|
||||
DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
if (current is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
|
||||
await store.SetCandidateStatusAsync(dialogId, status, ct).ConfigureAwait(false);
|
||||
if (status == DiscoveryCandidateStatuses.Review)
|
||||
@@ -195,7 +202,8 @@ public sealed class DiscoveryCandidatesService(
|
||||
ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -203,17 +211,15 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="auto">True — авто-вступление воркера; false — ручное.</param>
|
||||
/// <returns>Кандидат в joined либо null (кандидата нет). Повторный вызов для joined — идемпотентен.</returns>
|
||||
public async Task<DiscoveryCandidateDto?> MarkJoinedAsync(
|
||||
/// <returns>Кандидат в joined. Повторный вызов для joined — идемпотентен.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
public async Task<DiscoveryCandidateDto> MarkJoinedAsync(
|
||||
string dialogId,
|
||||
bool auto,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
if (current is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
|
||||
if (current.Status == DiscoveryCandidateStatuses.Joined)
|
||||
{
|
||||
@@ -224,7 +230,8 @@ public sealed class DiscoveryCandidatesService(
|
||||
await store.BumpTaskCounterAsync(current.TaskId, DiscoveryCounterField.Joined, 1, ct).ConfigureAwait(false);
|
||||
string logEvent = auto ? DiscoveryLogEvents.JoinAuto : DiscoveryLogEvents.JoinManual;
|
||||
await log.AddAsync(current.TaskId, logEvent, string.Format(JoinedLogFormat, dialogId), ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -232,18 +239,16 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// </summary>
|
||||
/// <param name="dialogId">Подписанный id источника.</param>
|
||||
/// <param name="reason">Причина отклонения (в лог/чёрный список; «отклонено вручную» — эндпоинт reject).</param>
|
||||
/// <returns>Кандидат в rejected либо null (кандидата нет). Повторный вызов для rejected — идемпотентен.</returns>
|
||||
/// <returns>Кандидат в rejected. Повторный вызов для rejected — идемпотентен.</returns>
|
||||
/// <exception cref="NotFoundException">Кандидат не найден.</exception>
|
||||
/// <exception cref="DiscoveryValidationException">Источник уже joined — отклонить нельзя.</exception>
|
||||
public async Task<DiscoveryCandidateDto?> MarkRejectedAsync(
|
||||
public async Task<DiscoveryCandidateDto> MarkRejectedAsync(
|
||||
string dialogId,
|
||||
string reason,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
if (current is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
DiscoveryCandidateDto current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
|
||||
if (current.Status == DiscoveryCandidateStatuses.Joined)
|
||||
{
|
||||
@@ -260,7 +265,8 @@ public sealed class DiscoveryCandidatesService(
|
||||
string text = string.IsNullOrEmpty(reason) ? string.Format(RejectedLogFormat, dialogId) : reason;
|
||||
await log.AddAsync(current.TaskId, DiscoveryLogEvents.Reject, text, ct).ConfigureAwait(false);
|
||||
await blacklist.AddAsync(dialogId, current.Name, reason ?? string.Empty, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
return await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false)
|
||||
?? throw new NotFoundException(CandidateEntityName, dialogId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -27,9 +27,16 @@ public sealed partial class CardsService
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <returns>Карточка или null — строки нет (эндпоинт отвечает 404 «Карточка не найдена»).</returns>
|
||||
public Task<CardDto?> GetCardAsync(string cardId, CancellationToken ct)
|
||||
/// <summary>
|
||||
/// Одна карточка по id.
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <returns>Карточка.</returns>
|
||||
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
|
||||
public async Task<CardDto> GetCardAsync(string cardId, CancellationToken ct)
|
||||
{
|
||||
return _store.GetCardAsync(cardId, ct);
|
||||
return await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +64,7 @@ public sealed partial class CardsService
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
{
|
||||
return new CardResultDto(null, null);
|
||||
throw new NotFoundException(CardEntityName, cardId);
|
||||
}
|
||||
|
||||
if (card.Col == CardIds.Archive || card.Col == CardIds.Trash)
|
||||
@@ -81,7 +88,10 @@ public sealed partial class CardsService
|
||||
await _mlClient.PushAsync(text, toCol, PushWeightUser, ct);
|
||||
}
|
||||
|
||||
return new CardResultDto(null, await _store.GetCardAsync(cardId, ct));
|
||||
return new CardResultDto(
|
||||
null,
|
||||
await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new NotFoundException(CardEntityName, cardId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.SharedKernel.Errors;
|
||||
|
||||
namespace Deal.Modules.Kanban.Application.Services;
|
||||
|
||||
@@ -22,17 +23,15 @@ public sealed partial class CardsService
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <param name="atMs">Время напоминания, epoch-ms.</param>
|
||||
/// <returns>Результат: Error <see cref="RemindersDisabledDetail"/> (400) | Card=null без Error (404) | Card — карточка с напоминанием.</returns>
|
||||
/// <returns>Результат: Error <see cref="RemindersDisabledDetail"/> (400) | Card — карточка с напоминанием.</returns>
|
||||
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
|
||||
public async Task<CardResultDto> SetReminderAsync(
|
||||
string cardId,
|
||||
long atMs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
{
|
||||
return new CardResultDto(null, null);
|
||||
}
|
||||
CardDto card = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
|
||||
if (!await ReadRemindersEnabledAsync(ct))
|
||||
{
|
||||
@@ -41,7 +40,7 @@ public sealed partial class CardsService
|
||||
|
||||
await _store.SetReminderAsync(cardId, atMs, ct);
|
||||
CardDto saved = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new InvalidOperationException("Карточка не прочиталась после установки напоминания: " + cardId);
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
return new CardResultDto(null, saved);
|
||||
}
|
||||
|
||||
@@ -49,35 +48,27 @@ public sealed partial class CardsService
|
||||
/// Снимает напоминание карточки.
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <returns>True — карточка есть и напоминание снято; false — карточки нет (404).</returns>
|
||||
public async Task<bool> ClearReminderAsync(string cardId, CancellationToken ct)
|
||||
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
|
||||
public async Task ClearReminderAsync(string cardId, CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_ = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
|
||||
await _store.ClearReminderAsync(cardId, ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// «Напомнить позже»
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <returns>True — карточка есть и напоминание отложено; false — карточки нет (404).</returns>
|
||||
public async Task<bool> SnoozeReminderAsync(string cardId, CancellationToken ct)
|
||||
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
|
||||
public async Task SnoozeReminderAsync(string cardId, CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_ = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
|
||||
long snoozedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + ReminderSnoozeMs;
|
||||
await _store.SetReminderAsync(cardId, snoozedAtMs, ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace Deal.Modules.Kanban.Application.Services;
|
||||
/// </summary>
|
||||
public sealed partial class CardsService
|
||||
{
|
||||
// Имя сущности для текста ошибки «не найдено».
|
||||
private const string CardLinkEntityName = "Ссылка карточки";
|
||||
|
||||
/// <summary>
|
||||
/// 400 перенос по стадии
|
||||
/// </summary>
|
||||
@@ -159,18 +162,16 @@ public sealed partial class CardsService
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <param name="name">Название ссылки; пустое после Trim → name = url.</param>
|
||||
/// <param name="url">URL ссылки (без схемы — добавится https://).</param>
|
||||
/// <returns>Результат: Error (400 «Пустая ссылка») | Card=null без Error (404) | Card — карточка со ссылкой.</returns>
|
||||
/// <returns>Результат: Error (400 «Пустая ссылка») | Card — карточка со ссылкой.</returns>
|
||||
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
|
||||
public async Task<CardResultDto> AddLinkAsync(
|
||||
string cardId,
|
||||
string name,
|
||||
string url,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
{
|
||||
return new CardResultDto(null, null);
|
||||
}
|
||||
CardDto card = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
|
||||
string normalizedUrl = (url ?? string.Empty).Trim();
|
||||
if (normalizedUrl.Length == 0)
|
||||
@@ -191,11 +192,11 @@ public sealed partial class CardsService
|
||||
normalizedUrl);
|
||||
if (!await _store.AddLinkAsync(cardId, link, ct))
|
||||
{
|
||||
return new CardResultDto(null, null);
|
||||
throw new NotFoundException(CardEntityName, cardId);
|
||||
}
|
||||
|
||||
CardDto saved = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new InvalidOperationException("Карточка не прочиталась после добавления ссылки: " + cardId);
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
return new CardResultDto(null, saved);
|
||||
}
|
||||
|
||||
@@ -204,7 +205,8 @@ public sealed partial class CardsService
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <param name="linkId">Id удаляемой ссылки (<c>pl_...</c>).</param>
|
||||
/// <returns>Результат: Card=null без Error (404) | Card — карточка без ссылки.</returns>
|
||||
/// <returns>Карточка без ссылки.</returns>
|
||||
/// <exception cref="NotFoundException">Карточка или ссылка не найдены.</exception>
|
||||
public async Task<CardResultDto> RemoveLinkAsync(
|
||||
string cardId,
|
||||
string linkId,
|
||||
@@ -212,11 +214,11 @@ public sealed partial class CardsService
|
||||
{
|
||||
if (!await _store.RemoveLinkAsync(cardId, linkId, ct))
|
||||
{
|
||||
return new CardResultDto(null, null);
|
||||
throw new NotFoundException(CardLinkEntityName, linkId);
|
||||
}
|
||||
|
||||
CardDto saved = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new InvalidOperationException("Карточка не прочиталась после удаления ссылки: " + cardId);
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
return new CardResultDto(null, saved);
|
||||
}
|
||||
|
||||
@@ -225,7 +227,8 @@ public sealed partial class CardsService
|
||||
/// </summary>
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <param name="containerId">Новый контейнер-стадия — id каталога <see cref="CardsDefaultContainers"/>.</param>
|
||||
/// <returns>Результат: Error «Неизвестная стадия» (400) | Card=null без Error (карточки нет, 404) | Card — карточка после переноса.</returns>
|
||||
/// <returns>Результат: Error «Неизвестная стадия» (400) | Card — карточка после переноса.</returns>
|
||||
/// <exception cref="NotFoundException">Карточка не найдена.</exception>
|
||||
public async Task<CardResultDto> MoveStageCardAsync(
|
||||
string cardId,
|
||||
string containerId,
|
||||
@@ -241,11 +244,11 @@ public sealed partial class CardsService
|
||||
bool moved = await _store.MoveCardStageAsync(cardId, containerId, entry, nowMs, ct);
|
||||
if (!moved)
|
||||
{
|
||||
return new CardResultDto(null, null);
|
||||
throw new NotFoundException(CardEntityName, cardId);
|
||||
}
|
||||
|
||||
CardDto card = await _store.GetCardAsync(cardId, ct)
|
||||
?? throw new InvalidOperationException("Карточка не прочиталась после move: " + cardId);
|
||||
?? throw new NotFoundException(CardEntityName, cardId);
|
||||
return new CardResultDto(null, card);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -157,8 +161,9 @@ public sealed class MlReviewService(
|
||||
/// <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>
|
||||
public async Task<MlApplyResult?> ApplyAsync(
|
||||
/// <returns>Результат решения.</returns>
|
||||
/// <exception cref="NotFoundException">Исходная запись не найдена.</exception>
|
||||
public async Task<MlApplyResult> 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)
|
||||
|
||||
@@ -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<string> SpamStages = new(StringComparer.Ordinal)
|
||||
{
|
||||
"spam_ml",
|
||||
@@ -154,17 +158,15 @@ public sealed class PipelineProcessingService(
|
||||
/// </summary>
|
||||
/// <param name="rejectedId">Id записи отсева (<c>r_...</c>).</param>
|
||||
/// <param name="reason">Причина возврата (trim, ≤500; пишется на запись для аудита).</param>
|
||||
/// <returns>null — записи нет (404); иначе результат: Error (400) либо {id, returned:true, returnedAt}.</returns>
|
||||
public async Task<RejectReturnResultDto?> ReturnAsync(
|
||||
/// <returns>Результат: Error (400) либо {id, returned:true, returnedAt}.</returns>
|
||||
/// <exception cref="NotFoundException">Запись отсева не найдена.</exception>
|
||||
public async Task<RejectReturnResultDto> 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)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.SharedKernel;
|
||||
using Deal.SharedKernel.Errors;
|
||||
using Deal.SharedKernel.Utilities;
|
||||
|
||||
namespace Deal.Modules.Tenants.Application.Services;
|
||||
@@ -17,6 +18,9 @@ public sealed class TenantAdminService(
|
||||
// Случайные байты одноразового пароля владельца: 12 → ровно 16 символов Base64Url (как InviteCodeGenerator).
|
||||
private const int InitialPasswordRandomByteCount = 12;
|
||||
|
||||
// Имя сущности для текста ошибки «не найдено» (пользователь = тенант с его окружением).
|
||||
private const string TenantEntityName = "Пользователь";
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт тенанта оператором
|
||||
/// </summary>
|
||||
@@ -103,14 +107,12 @@ public sealed class TenantAdminService(
|
||||
/// Детали тенанта с пользователями
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор тенанта.</param>
|
||||
/// <returns>Детали и пользователи тенанта (по CreatedAt) или null, если тенанта нет.</returns>
|
||||
public async Task<TenantDetailDto?> GetAsync(Guid id, CancellationToken ct)
|
||||
/// <returns>Детали и пользователи тенанта (по CreatedAt).</returns>
|
||||
/// <exception cref="NotFoundException">Тенант не найден.</exception>
|
||||
public async Task<TenantDetailDto> GetAsync(Guid id, CancellationToken ct)
|
||||
{
|
||||
var tenant = await tenantRepository.FindByIdAsync(id, ct);
|
||||
if (tenant is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var tenant = await tenantRepository.FindByIdAsync(id, ct)
|
||||
?? throw new NotFoundException(TenantEntityName, id.ToString("N"));
|
||||
|
||||
IReadOnlyList<UserIdentityDto> users = await authStore.ListUsersByTenantIdAsync(id, ct);
|
||||
return new TenantDetailDto(tenant.Id, tenant.Name, tenant.Status, tenant.CreatedAt, users);
|
||||
|
||||
@@ -53,13 +53,12 @@ public sealed class CardsServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCard_Missing_ReturnsNull()
|
||||
public async Task GetCard_Missing_ThrowsNotFound()
|
||||
{
|
||||
(CardsService service, _, _, _) = Create();
|
||||
|
||||
CardDto? card = await service.GetCardAsync("l_missing", CancellationToken.None);
|
||||
|
||||
Assert.Null(card);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.GetCardAsync("l_missing", CancellationToken.None));
|
||||
}
|
||||
|
||||
|
||||
@@ -146,15 +145,14 @@ public sealed class CardsServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Move_CardMissing_ReturnsNullLead()
|
||||
public async Task Move_CardMissing_ThrowsNotFound()
|
||||
{
|
||||
(CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
|
||||
store.SeedBoard(Board("b_py"));
|
||||
|
||||
CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None));
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.Null(result.Card); // эндпоинт отвечает 404 «Карточка не найдена»
|
||||
Assert.Empty(store.Moves);
|
||||
Assert.Empty(ml.Pushed);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ 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.Tests.Unit.Support;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.SharedKernel.Errors;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Infrastructure;
|
||||
|
||||
@@ -32,7 +33,6 @@ public sealed class CardMoverTests
|
||||
CardMoveResultDto result = await mover.MoveAsync("c_1", CardsDefaultContainers.Planned, UserMove, CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.True(result.Exists);
|
||||
CardDto card = Assert.Single(store.CardDtos);
|
||||
Assert.Equal(CardsDefaultContainers.Planned, card.Col);
|
||||
Assert.Null(card.Reminder); // move по стадии сбрасывает напоминание
|
||||
@@ -50,7 +50,6 @@ public sealed class CardMoverTests
|
||||
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_py", UserMove, CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.True(result.Exists);
|
||||
Assert.Equal("b_py", Assert.Single(store.CardDtos).Col);
|
||||
Assert.Single(store.Moves); // журнал CardMoves дашборд-переноса
|
||||
}
|
||||
@@ -64,18 +63,15 @@ public sealed class CardMoverTests
|
||||
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_ghost", UserMove, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CardsService.MoveTargetInvalidDetail, result.Error);
|
||||
Assert.True(result.Exists); // ошибка важнее признака наличия
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Move_ToStage_CardMissing_ReportsNotFound()
|
||||
public async Task Move_ToStage_CardMissing_ThrowsNotFound()
|
||||
{
|
||||
(CardMover mover, _) = Create();
|
||||
|
||||
CardMoveResultDto result = await mover.MoveAsync("c_ghost", CardsDefaultContainers.Planned, UserMove, CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.False(result.Exists);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => mover.MoveAsync("c_ghost", CardsDefaultContainers.Planned, UserMove, CancellationToken.None));
|
||||
}
|
||||
|
||||
private static (CardMover Mover, TestKanjStore Store) Create()
|
||||
|
||||
+14
-19
@@ -1,7 +1,8 @@
|
||||
using Deal.Modules.Discovery.Application.Exceptions;
|
||||
using Deal.Modules.Discovery.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Discovery.Application.Services;
|
||||
using Deal.SharedKernel.Errors;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Discovery;
|
||||
|
||||
@@ -114,14 +115,13 @@ public sealed class DiscoveryCandidatesServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Add_MissingTask_ReturnsNullWithoutLog()
|
||||
public async Task Add_MissingTask_ThrowsNotFoundWithoutLog()
|
||||
{
|
||||
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||
|
||||
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
||||
"dt_missing", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None);
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => service.AddAsync(
|
||||
"dt_missing", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None));
|
||||
|
||||
Assert.Null(candidate);
|
||||
Assert.Empty(store.Candidates);
|
||||
Assert.Empty(store.Log);
|
||||
}
|
||||
@@ -175,13 +175,12 @@ public sealed class DiscoveryCandidatesServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkJoined_MissingCandidate_ReturnsNull()
|
||||
public async Task MarkJoined_MissingCandidate_ThrowsNotFound()
|
||||
{
|
||||
(DiscoveryCandidatesService service, _) = Create();
|
||||
|
||||
DiscoveryCandidateDto? candidate = await service.MarkJoinedAsync("-1001", auto: true, CancellationToken.None);
|
||||
|
||||
Assert.Null(candidate);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.MarkJoinedAsync("-1001", auto: true, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -238,14 +237,12 @@ public sealed class DiscoveryCandidatesServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkRejected_MissingCandidate_ReturnsNull()
|
||||
public async Task MarkRejected_MissingCandidate_ThrowsNotFound()
|
||||
{
|
||||
(DiscoveryCandidatesService service, _) = Create();
|
||||
|
||||
DiscoveryCandidateDto? candidate = await service.MarkRejectedAsync(
|
||||
"-1001", "причина", CancellationToken.None);
|
||||
|
||||
Assert.Null(candidate);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.MarkRejectedAsync("-1001", "причина", CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -315,17 +312,15 @@ public sealed class DiscoveryCandidatesServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_WrongTask_ReturnsNull()
|
||||
public async Task Set_WrongTask_ThrowsNotFound()
|
||||
{
|
||||
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||
store.SeedTask(Task("dt_1"));
|
||||
store.SeedTask(Task("dt_2"));
|
||||
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
||||
|
||||
DiscoveryCandidateDto? candidate = await service.SetAsync(
|
||||
"dt_2", "-1001", new DiscoveryCandidatePatch { Participants = 10 }, CancellationToken.None);
|
||||
|
||||
Assert.Null(candidate); // кандидат другой задачи (python L465–466)
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => service.SetAsync(
|
||||
"dt_2", "-1001", new DiscoveryCandidatePatch { Participants = 10 }, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Kanban.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.SharedKernel.Errors;
|
||||
using Deal.Tests.Unit.Contracts;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Kanban;
|
||||
|
||||
@@ -47,14 +48,12 @@ public sealed class CardsServiceRemindersTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_MissingCard_Returns404EvenWhenDisabled()
|
||||
public async Task Set_MissingCard_ThrowsNotFoundEvenWhenDisabled()
|
||||
{
|
||||
(CardsService service, _, _) = Create(remindersEnabled: false);
|
||||
|
||||
CardResultDto result = await service.SetReminderAsync("c_missing", NowMs(), CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.Null(result.Card); // эндпоинт отвечает 404 «Карточка не найдена»
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.SetReminderAsync("c_missing", NowMs(), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -73,25 +72,23 @@ public sealed class CardsServiceRemindersTests
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_WithReminder_ClearsItAndReturnsTrue()
|
||||
public async Task Clear_WithReminder_ClearsIt()
|
||||
{
|
||||
(CardsService service, TestKanjStore store, _) = Create();
|
||||
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) });
|
||||
|
||||
bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None);
|
||||
await service.ClearReminderAsync("c_1", CancellationToken.None);
|
||||
|
||||
Assert.True(cleared);
|
||||
Assert.Null(Assert.Single(store.CardDtos).Reminder); // reminder_at=NULL, fired сброшен
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_MissingCard_ReturnsFalse()
|
||||
public async Task Clear_MissingCard_ThrowsNotFound()
|
||||
{
|
||||
(CardsService service, _, _) = Create();
|
||||
|
||||
bool cleared = await service.ClearReminderAsync("c_missing", CancellationToken.None);
|
||||
|
||||
Assert.False(cleared); // эндпоинт отвечает 404 «Карточка не найдена»
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.ClearReminderAsync("c_missing", CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -100,9 +97,8 @@ public sealed class CardsServiceRemindersTests
|
||||
(CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
|
||||
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() + DayMs) });
|
||||
|
||||
bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None);
|
||||
await service.ClearReminderAsync("c_1", CancellationToken.None);
|
||||
|
||||
Assert.True(cleared);
|
||||
Assert.Null(Assert.Single(store.CardDtos).Reminder);
|
||||
}
|
||||
|
||||
@@ -114,22 +110,20 @@ public sealed class CardsServiceRemindersTests
|
||||
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) });
|
||||
long beforeMs = NowMs();
|
||||
|
||||
bool snoozed = await service.SnoozeReminderAsync("c_1", CancellationToken.None);
|
||||
await service.SnoozeReminderAsync("c_1", CancellationToken.None);
|
||||
|
||||
Assert.True(snoozed);
|
||||
long afterMs = NowMs();
|
||||
CardReminderDto reminder = Assert.Single(store.CardDtos).Reminder!;
|
||||
Assert.InRange(reminder.At, beforeMs + DayMs, afterMs + DayMs); // now + 24 ч
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Snooze_MissingCard_ReturnsFalse()
|
||||
public async Task Snooze_MissingCard_ThrowsNotFound()
|
||||
{
|
||||
(CardsService service, _, _) = Create();
|
||||
|
||||
bool snoozed = await service.SnoozeReminderAsync("c_missing", CancellationToken.None);
|
||||
|
||||
Assert.False(snoozed); // эндпоинт отвечает 404 «Карточка не найдена»
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.SnoozeReminderAsync("c_missing", CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -138,9 +132,8 @@ public sealed class CardsServiceRemindersTests
|
||||
(CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
|
||||
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) });
|
||||
|
||||
bool snoozed = await service.SnoozeReminderAsync("c_1", CancellationToken.None);
|
||||
await service.SnoozeReminderAsync("c_1", CancellationToken.None);
|
||||
|
||||
Assert.True(snoozed);
|
||||
Assert.NotNull(Assert.Single(store.CardDtos).Reminder);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<NotFoundException>(
|
||||
() => service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NotFoundException>(
|
||||
() => service.ReturnAsync("r_missing", string.Empty, CancellationToken.None));
|
||||
|
||||
Assert.Null(result); // эндпоинт ответит 404 «Запись не найдена» (Ruling 10)
|
||||
Assert.Empty(store.Queue);
|
||||
Assert.Empty(ml.Pushed);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.SharedKernel.Errors;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||
|
||||
@@ -148,11 +149,12 @@ public sealed class TenantAdminServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_ForUnknownTenant_ReturnsNull()
|
||||
public async Task GetAsync_ForUnknownTenant_ThrowsNotFound()
|
||||
{
|
||||
var service = NewService(new TestTenantStore(), new TestAuthStore());
|
||||
|
||||
Assert.Null(await service.GetAsync(Guid.NewGuid(), CancellationToken.None));
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.GetAsync(Guid.NewGuid(), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -40,13 +40,12 @@ public sealed class CardsServiceSelectedTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_Missing_ReturnsNull()
|
||||
public async Task Get_Missing_ThrowsNotFound()
|
||||
{
|
||||
(CardsService service, _, _, _) = Create();
|
||||
|
||||
CardDto? card = await service.GetCardAsync("c_missing", CancellationToken.None);
|
||||
|
||||
Assert.Null(card);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.GetCardAsync("c_missing", CancellationToken.None));
|
||||
}
|
||||
|
||||
|
||||
@@ -319,14 +318,12 @@ public sealed class CardsServiceSelectedTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Move_CardMissing_ReturnsNullCard()
|
||||
public async Task Move_CardMissing_ThrowsNotFound()
|
||||
{
|
||||
(CardsService service, _, _, _) = Create();
|
||||
|
||||
CardResultDto result = await service.MoveStageCardAsync("c_missing", "work", CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.Null(result.Card); // 404 «Карточка не найдена» — текст у эндпоинта
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.MoveStageCardAsync("c_missing", "work", CancellationToken.None));
|
||||
}
|
||||
|
||||
|
||||
@@ -465,14 +462,13 @@ public sealed class CardsServiceSelectedTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddLink_CardMissing_ReturnsNullCardBeforeUrlValidation()
|
||||
public async Task AddLink_CardMissing_ThrowsBeforeUrlValidation()
|
||||
{
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
|
||||
CardResultDto result = await service.AddLinkAsync("c_missing", string.Empty, "example.com", CancellationToken.None);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.AddLinkAsync("c_missing", string.Empty, "example.com", CancellationToken.None));
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.Null(result.Card); // 404-семантика: карточки нет раньше валидации url
|
||||
Assert.Empty(store.CardDtos);
|
||||
}
|
||||
|
||||
@@ -514,14 +510,13 @@ public sealed class CardsServiceSelectedTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveLink_CardMissing_ReturnsNullCard()
|
||||
public async Task RemoveLink_CardMissing_ThrowsNotFound()
|
||||
{
|
||||
(CardsService service, TestKanjStore store, _, _) = Create();
|
||||
|
||||
CardResultDto result = await service.RemoveLinkAsync("c_missing", "pl_1", CancellationToken.None);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => service.RemoveLinkAsync("c_missing", "pl_1", CancellationToken.None));
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.Null(result.Card); // эндпоинт отвечает 404 «Карточка не найдена»
|
||||
Assert.Empty(store.CardDtos);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user