using Dapper; using GmRelay.Shared.Domain; using GmRelay.Shared.Features.Notifications; using GmRelay.Shared.Platform; using Microsoft.Extensions.Logging; using Npgsql; namespace GmRelay.Shared.Features.Reminders.SendOneHourReminder; internal sealed record OneHourReminderSessionRow( Guid Id, string Title, string JoinLink, DateTime ScheduledAt, string NotificationMode); internal sealed record OneHourReminderRecipientRow( string Platform, string ExternalUserId, string DisplayName, string? ExternalUsername); public sealed class SendOneHourReminderHandler( NpgsqlDataSource dataSource, PlatformDirectNotificationSender directSender, ILogger logger) : ISendOneHourReminderHandler { public async Task HandleAsync(Guid sessionId, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); var session = await connection.QuerySingleOrDefaultAsync( """ SELECT id, title, join_link AS JoinLink, scheduled_at AS ScheduledAt, notification_mode AS NotificationMode FROM sessions WHERE id = @SessionId AND status IN (@Confirmed, @ConfirmationSent) AND one_hour_reminder_processed_at IS NULL """, new { SessionId = sessionId, Confirmed = SessionStatus.Confirmed, ConfirmationSent = SessionStatus.ConfirmationSent }); if (session is null) { logger.LogWarning("Session {SessionId} not eligible for one-hour reminder", sessionId); return; } var recipients = (await connection.QueryAsync( """ SELECT COALESCE(p.platform, 'Telegram') AS Platform, COALESCE(p.external_user_id, p.telegram_id::TEXT) AS ExternalUserId, p.display_name AS DisplayName, COALESCE(p.external_username, p.telegram_username) AS ExternalUsername FROM session_participants sp JOIN players p ON p.id = sp.player_id WHERE sp.session_id = @SessionId AND sp.is_gm = false AND sp.registration_status = @Active AND sp.rsvp_status != @Declined """, new { SessionId = sessionId, Active = ParticipantRegistrationStatus.Active, Declined = RsvpStatus.Declined })) .Select(row => new PlatformUser( ParsePlatform(row.Platform), row.ExternalUserId, row.DisplayName, row.ExternalUsername)) .ToList(); var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode); if (mode.ShouldSendDirectMessages() && recipients.Count > 0) { await directSender.SendAsync( PlatformDirectSessionNotificationKind.OneHourReminder, recipients, session.Id, session.Title, session.ScheduledAt, session.JoinLink, actorDisplayName: null, reason: null, ct); } await connection.ExecuteAsync( """ UPDATE sessions SET one_hour_reminder_processed_at = now(), updated_at = now() WHERE id = @SessionId AND one_hour_reminder_processed_at IS NULL """, new { SessionId = sessionId }); logger.LogInformation( "One-hour reminder processed for session {SessionId} ({Title}) with mode {NotificationMode}", sessionId, session.Title, session.NotificationMode); } private static PlatformKind ParsePlatform(string platform) => Enum.Parse(platform, ignoreCase: true); }