using System.Security.Cryptography; using System.Text.Json; using HrynCo.NotificationService.Contracts.Messages; using HrynCo.NotificationService.DAL.Abstract.Push; using HrynCo.NotificationService.DAL.Abstract.PushChannels; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; namespace HrynCo.NotificationService.Worker.Services.Push; public sealed class FcmPushSender( INotificationServiceRepository services, IPushChannelRepository channels, IFirebasePushClient client, IPushTemplateDefinitionRepository templates) : IPushSender { public async Task SendAsync(Guid messageId, SendPushMessageData data, CancellationToken cancellationToken) { if (!PushMessageValidator.IsSupportedVersion(data.Version)) return new PushDeliveryOutcome("Unsupported push payload version. Supported versions are 1 and 2."); if (!PushMessageValidator.IsValidContent(data)) return new PushDeliveryOutcome("Invalid push content for the requested version."); if (data.ExpiresAt <= DateTimeOffset.UtcNow) return new PushDeliveryOutcome("Push expired."); var service = await services.GetByNameAsync(data.ServiceName, cancellationToken); if (service is null) return new PushDeliveryOutcome("Push service is not configured."); RenderedPushMessageData rendered; try { rendered = await RenderAsync(service.Id, data, cancellationToken); } catch (EmailTemplateWriteException) { return new PushDeliveryOutcome("Push template data or action configuration is invalid."); } var channel = await channels.GetActiveForServiceAsync(service.Id, cancellationToken); if (channel is null) return new PushDeliveryOutcome("No active push channel is configured for this service."); var credentials = await channels.GetCredentialsAsync(channel.Id, cancellationToken); return await SendWithCredentialsAsync(messageId, rendered, channel.FirebaseProjectId, credentials, cancellationToken); } private async Task RenderAsync(Guid serviceId, SendPushMessageData data, CancellationToken ct) { if (data.Version == 1) return new RenderedPushMessageData(data, data.Title!, data.Body!, null, []); var template = await templates.GetAsync(serviceId, data.TemplateKey!, ct) ?? throw new EmailTemplateWriteException("Push template is not configured.", false); return PushTemplateRenderer.Render(template, data); } private async Task SendWithCredentialsAsync(Guid messageId, RenderedPushMessageData data, string projectId, string? json, CancellationToken ct) { try { var credentials = FirebaseServiceAccountCredentials.Parse(json ?? string.Empty, projectId); return await client.SendAsync(messageId, data, credentials, ct); } catch (Exception error) when (error is JsonException or FormatException or ArgumentException or CryptographicException) { // Never propagate credential parser details into broker retry logs or replies. return new PushDeliveryOutcome("The push channel Firebase configuration is invalid."); } } }