e6e6d17b72
PR Checks / test-and-build (pull_request) Successful in 3m12s
- Добавлена абстракция ISystemClock + SystemClock / FakeSystemClock для тестируемого scheduling. - Добавлена миграция V014: confirmation_sent_at в sessions. - Обновлен SendConfirmationHandler: записывает confirmation_sent_at. - Обновлен SessionSchedulerService: - выделен ISessionTriggerStore / DbSessionTriggerStore - SQL-запросы используют параметр @Now вместо now() - добавлен публичный TickAsync для тестов - защита от дублей через confirmation_sent_at IS NULL - Обновлен RescheduleVotingDeadlineService: использует ISystemClock. - Добавлены интерфейсы ISendConfirmationHandler, ISendOneHourReminderHandler, ISendJoinLinkHandler для unit-тестируемости. - Добавлены 8 unit-тестов SessionSchedulerService: - все 3 триггера (T-24h, T-1h, T-5min) - идемпотентность при повторном запуске - ошибки handler не падают и не блокируют другие сессии - ошибки store логируются без падения worker-а Bump version -> 1.13.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
87 lines
3.0 KiB
C#
87 lines
3.0 KiB
C#
using Dapper;
|
|
using GmRelay.Shared.Domain;
|
|
using Npgsql;
|
|
|
|
namespace GmRelay.Bot.Infrastructure.Scheduling;
|
|
|
|
public interface ISessionTriggerStore
|
|
{
|
|
Task<IReadOnlyList<Guid>> GetSessionsNeedingConfirmationAsync(DateTimeOffset now, CancellationToken ct);
|
|
Task<IReadOnlyList<Guid>> GetSessionsNeedingOneHourReminderAsync(DateTimeOffset now, CancellationToken ct);
|
|
Task<IReadOnlyList<Guid>> GetSessionsNeedingJoinLinkAsync(DateTimeOffset now, CancellationToken ct);
|
|
}
|
|
|
|
public sealed class DbSessionTriggerStore(NpgsqlDataSource dataSource) : ISessionTriggerStore
|
|
{
|
|
private static readonly TimeSpan ConfirmationLeadTime = TimeSpan.FromHours(24);
|
|
private static readonly TimeSpan OneHourReminderLeadTime = TimeSpan.FromHours(1);
|
|
private static readonly TimeSpan JoinLinkLeadTime = TimeSpan.FromMinutes(5);
|
|
|
|
public async Task<IReadOnlyList<Guid>> GetSessionsNeedingConfirmationAsync(DateTimeOffset now, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
var results = await connection.QueryAsync<Guid>(
|
|
"""
|
|
SELECT id
|
|
FROM sessions
|
|
WHERE status = @Planned
|
|
AND scheduled_at - @LeadTime <= @Now
|
|
AND confirmation_sent_at IS NULL
|
|
""",
|
|
new
|
|
{
|
|
Planned = SessionStatus.Planned,
|
|
LeadTime = ConfirmationLeadTime,
|
|
Now = now.UtcDateTime
|
|
});
|
|
|
|
return results.ToList();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Guid>> GetSessionsNeedingOneHourReminderAsync(DateTimeOffset now, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
var results = await connection.QueryAsync<Guid>(
|
|
"""
|
|
SELECT id
|
|
FROM sessions
|
|
WHERE status IN (@Confirmed, @ConfirmationSent)
|
|
AND scheduled_at - @LeadTime <= @Now
|
|
AND one_hour_reminder_processed_at IS NULL
|
|
""",
|
|
new
|
|
{
|
|
Confirmed = SessionStatus.Confirmed,
|
|
ConfirmationSent = SessionStatus.ConfirmationSent,
|
|
LeadTime = OneHourReminderLeadTime,
|
|
Now = now.UtcDateTime
|
|
});
|
|
|
|
return results.ToList();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Guid>> GetSessionsNeedingJoinLinkAsync(DateTimeOffset now, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
var results = await connection.QueryAsync<Guid>(
|
|
"""
|
|
SELECT id
|
|
FROM sessions
|
|
WHERE status = @Confirmed
|
|
AND scheduled_at - @LeadTime <= @Now
|
|
AND link_message_id IS NULL
|
|
""",
|
|
new
|
|
{
|
|
Confirmed = SessionStatus.Confirmed,
|
|
LeadTime = JoinLinkLeadTime,
|
|
Now = now.UtcDateTime
|
|
});
|
|
|
|
return results.ToList();
|
|
}
|
|
}
|