Files
hrynco-notification-service/HrynCo.NotificationService.Worker.Services/Push/FcmPushSender.cs
T
agrynco aba4173827 feat(templates): add localized push templates and actions
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery.

Ref: IT-1627
2026-10-31 09:38:05 +02:00

55 lines
3.2 KiB
C#

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<PushDeliveryOutcome> 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<RenderedPushMessageData> 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<PushDeliveryOutcome> 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.");
}
}
}