using FrameworkBLL.EIPConversation.EIPHandlers.EIPEngine.ResponseEngine; using FrameworkBLL.Notification; using FrameworkDAL.DTO.EIPConversation; using FrameworkDAL.DTO.SystemJob; using FrameworkDAL.Query.SysJobRun; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.PushNotification; using GB5Shared.PushNotification; using GB5Shared.QueryExecutor; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace FrameworkBLL.SystemJob { public class SysJobNotificationDispatcher : ISysJobNotificationDispatcher { private readonly IQueryExecutor _queryExecutor; private readonly IEIPResponseEngine _responseEngine; private readonly INotificationBLL _notificationBLL; private readonly IPushNotificationBLL _pushNotificationBLL; private readonly ILogger _logger; public SysJobNotificationDispatcher( IQueryExecutor queryExecutor, IEIPResponseEngine responseEngine, INotificationBLL notificationBLL, IPushNotificationBLL pushNotificationBLL, ILogger logger) { _queryExecutor = queryExecutor; _responseEngine = responseEngine; _notificationBLL = notificationBLL; _pushNotificationBLL = pushNotificationBLL; _logger = logger; } public async Task DispatchAsync( int sysJobId, int sysJobRunId, SysJobDeliveryChannel channel, string destination, int submittedById, int runStatus, LoginDTO loginDTO, CancellationToken cancellationToken = default) { try { GB5Trace.Step("sysjob-dispatch", new { sysJobId, sysJobRunId, channel }); string statusText = runStatus == 2 ? "completed" : "failed"; string message = $"Job {sysJobId} {statusText}."; string logSuffix; switch (channel) { case SysJobDeliveryChannel.Email: case SysJobDeliveryChannel.WhatsApp: var context = new EIPResponseContext { Channel = channel == SysJobDeliveryChannel.Email ? "EMAIL" : "WHATSAPP", Recipient = destination, Message = message, TenantId = loginDTO.ClientId, FlowCode = "SYSJOB_NOTIFY", // A job-completion notice must never be silently dropped by the // per-recipient anti-spam cooldown — same reasoning as OTP delivery. BypassRateLimit = true }; var result = await _responseEngine .GenerateAsync(context, loginDTO, cancellationToken) .ConfigureAwait(false); logSuffix = result.IsSent ? $"Sent via {channel} to {destination}: {message}" : $"Failed to send via {channel} to {destination}: {result.ErrorMessage ?? result.Status}"; break; case SysJobDeliveryChannel.AppChat: logSuffix = await NotifyAppChatAsync(sysJobId, submittedById, message, loginDTO, cancellationToken) .ConfigureAwait(false); break; // SMS: no real provider configured anywhere in this system yet (Twilio // credentials/TwilioClient.Init are both absent) — log-only until that exists. case SysJobDeliveryChannel.SMS: default: logSuffix = $"Would send via {channel} to {destination}: {message}"; break; } _logger.LogInformation( "SysJobNotificationDispatcher: {Message} | SysJobId={SysJobId} SysJobRunId={SysJobRunId}", logSuffix, sysJobId, sysJobRunId); await _queryExecutor.ExecuteAsync( loginDTO, SysJobRunQB.APPEND_SYSJOBRUN_LOG, new { SysJobRunId = sysJobRunId, LogSuffix = " | " + logSuffix }, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("sysjob-dispatch-failed", ex); _logger.LogError(ex, "SysJobNotificationDispatcher failed | SysJobId={SysJobId} SysJobRunId={SysJobRunId}", sysJobId, sysJobRunId); } } // Writes a real TNOTIFICATION bell/inbox entry for the job's submitter and sends a real // Firebase push (best-effort — silently no-ops for a user with no registered device // token, same as every other push caller in this codebase). The SignalR JobCompleted/ // JobFailed push already fires unconditionally elsewhere and is not duplicated here. private async Task NotifyAppChatAsync( int sysJobId, int submittedById, string message, LoginDTO loginDTO, CancellationToken ct) { // -1 is this codebase's "system/none" sentinel and 0 means unset/unmapped — real user // ids are commonly large negative numbers (e.g. -1499998836), so this must NOT be a // generic <= 0 check. if (submittedById == -1 || submittedById == 0) return $"In-app notify skipped (no submitter): {message}"; await _notificationBLL.SaveNotification( message, fromUserId: -1, dataId: sysJobId, new List { submittedById }, loginDTO, ct).ConfigureAwait(false); try { await _pushNotificationBLL.SendNotificationToUsers(new PushNotificationDTO { NotificationTitle = "GB5 Job Update", NotificationBody = message, UserIds = new List { submittedById }, LoginDTO = loginDTO }).ConfigureAwait(false); } catch (Exception ex) { // Push is best-effort on top of the bell notification already saved above — // a missing/expired device token must never fail the in-app notify as a whole. _logger.LogWarning(ex, "SysJobNotificationDispatcher: push notification failed | SysJobId={SysJobId}", sysJobId); } return $"Notified in-app (bell + push): {message}"; } } }