правки

This commit is contained in:
gorbenko.pavel
2026-08-27 15:21:34 +05:00
parent cfc2b393bd
commit 554f9b2528
5 changed files with 96 additions and 16 deletions
@@ -0,0 +1,16 @@
namespace Tnsu.ApprovalRoute.Application.Reminders;
/// <summary>
/// Какой вид напоминаний запросить у <see cref="ReminderService.GetRemindersAsync(DateTimeOffset, ReminderFeed, CancellationToken)"/>.
///
/// GetRemindersAsync изначально (до появления ProbationEnding) отдавал только
/// просроченные шаги согласования — этот вид и остаётся значением по умолчанию
/// (0), чтобы старые потребители (Power Automate), не указывающие feed явно,
/// продолжали получать ровно то же, что и раньше.
/// </summary>
public enum ReminderFeed
{
None = 0,
ApprovalOverdue = 1,
ProbationEnding = 2
}
@@ -24,6 +24,7 @@ public sealed class ReminderService(
ISharePointClient sharePoint,
ListPartitionRegistry partitionRegistry,
ITnsuPortalClient portalClient,
TimeProvider timeProvider,
IOptions<RemindersOptions> options,
ILogger<ReminderService> logger)
{
@@ -32,17 +33,30 @@ public sealed class ReminderService(
private const string ActiveStatusFilter =
$"$filter=ApprovalStatus eq '{ApprovalStatuses.OnApproval}' or ApprovalStatus eq '{ApprovalStatuses.OnExecution}'";
/// <summary>Оба вида напоминаний разом — то, что реально отдаёт контроллер.</summary>
public async Task<Result<IReadOnlyList<ReminderDto>>> GetRemindersAsync(
DateTimeOffset now, CancellationToken cancellationToken)
/// <summary>
/// Оверлоад без feed — для обратной совместимости со старыми вызовами: раньше
/// это был единственный вид напоминаний, и без явного выбора им и остаётся.
/// </summary>
public Task<Result<IReadOnlyList<ReminderDto>>> GetRemindersAsync(CancellationToken cancellationToken) =>
GetRemindersAsync(ReminderFeed.ApprovalOverdue, cancellationToken);
/// <summary>
/// То, что реально отдаёт контроллер — один вид напоминаний по выбору вызывающей
/// стороны. Момент «сейчас» берётся здесь же, из <see cref="TimeProvider"/>, а не
/// пробрасывается вызывающей стороной — во всех проде это <c>TimeProvider.System</c>
/// (см. Program.cs), в тестах подменяется, а нижестоящие GetOverdueAsync/
/// GetProbationEndingAsync по-прежнему принимают <c>now</c> явным параметром: так
/// их сценарии остаются детерминированными без своего TimeProvider на каждый тест.
/// </summary>
public Task<Result<IReadOnlyList<ReminderDto>>> GetRemindersAsync(
ReminderFeed feed, CancellationToken cancellationToken)
{
var overdue = await GetOverdueAsync(now, cancellationToken);
if (overdue.IsFailed) return overdue;
var probation = await GetProbationEndingAsync(now, cancellationToken);
if (probation.IsFailed) return probation;
return Result.Ok<IReadOnlyList<ReminderDto>>(overdue.Value.Concat(probation.Value).ToList());
var now = timeProvider.GetUtcNow();
return feed switch
{
ReminderFeed.ProbationEnding => GetProbationEndingAsync(now, cancellationToken),
_ => GetOverdueAsync(now, cancellationToken),
};
}
public async Task<Result<IReadOnlyList<ReminderDto>>> GetOverdueAsync(
@@ -191,9 +205,17 @@ public sealed class ReminderService(
// ДО появления колонки (см. ApprovalRow) или если request-portal
// почему-то не проставил её при активации. Без даты старта SLA не
// от чего отсчитывать — пропускаем, а не подставляем сейчас.
//
// RequestTypeId — всегда, чтобы искать TicketId в правильном типе
// заявок. RequestUniqueCode — то, по чему заявку реально ищут в
// портале, добавляется, только если он уже есть (строка данных
// могла ещё не появиться или заявка старше появления колонки).
var identifier = uniqueCodes.GetValueOrDefault(row.TicketId) is { Length: > 0 } code
? $"requestTypeId={type.Id}, code={code}"
: $"requestTypeId={type.Id}";
logger.LogWarning(
"Строка {Row} заявки {Ticket} активна, но дата активации (Date) не заполнена — пропущена",
row.Id, row.TicketId);
"Строка {Row} заявки {Ticket} ({Identifier}) активна, но дата активации (Date) не заполнена — пропущена",
row.Id, row.TicketId, identifier);
continue;
}