using StellaOps.Notify.Models; namespace StellaOps.Notifier.Worker.Channels; /// /// Contract implemented by channel adapters to dispatch notifications. /// public interface IChannelAdapter { /// /// Channel type handled by this adapter. /// NotifyChannelType ChannelType { get; } /// /// Dispatches a notification delivery through the channel. /// Task DispatchAsync( ChannelDispatchContext context, CancellationToken cancellationToken); /// /// Checks channel health/connectivity. /// Task CheckHealthAsync( NotifyChannel channel, CancellationToken cancellationToken); } /// /// Context for dispatching a notification through a channel. /// public sealed record ChannelDispatchContext( string DeliveryId, string TenantId, NotifyChannel Channel, NotifyDelivery Delivery, string RenderedBody, string? Subject, IReadOnlyDictionary Metadata, DateTimeOffset Timestamp, string TraceId); /// /// Result of a channel dispatch attempt. /// public sealed record ChannelDispatchResult { public required bool Success { get; init; } public required ChannelDispatchStatus Status { get; init; } public string? Message { get; init; } public string? ExternalId { get; init; } public int? HttpStatusCode { get; init; } public TimeSpan? Duration { get; init; } public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); public Exception? Exception { get; init; } public static ChannelDispatchResult Succeeded( string? externalId = null, string? message = null, TimeSpan? duration = null, IReadOnlyDictionary? metadata = null) => new() { Success = true, Status = ChannelDispatchStatus.Sent, ExternalId = externalId, Message = message ?? "Delivery dispatched successfully.", Duration = duration, Metadata = metadata ?? new Dictionary() }; public static ChannelDispatchResult Failed( string message, ChannelDispatchStatus status = ChannelDispatchStatus.Failed, int? httpStatusCode = null, Exception? exception = null, TimeSpan? duration = null, IReadOnlyDictionary? metadata = null) => new() { Success = false, Status = status, Message = message, HttpStatusCode = httpStatusCode, Exception = exception, Duration = duration, Metadata = metadata ?? new Dictionary() }; public static ChannelDispatchResult Throttled( string message, TimeSpan? retryAfter = null, IReadOnlyDictionary? metadata = null) { var meta = metadata is not null ? new Dictionary(metadata) : new Dictionary(); if (retryAfter.HasValue) { meta["retryAfterSeconds"] = retryAfter.Value.TotalSeconds.ToString("F0"); } return new() { Success = false, Status = ChannelDispatchStatus.Throttled, Message = message, HttpStatusCode = 429, Metadata = meta }; } } /// /// Dispatch attempt status. /// public enum ChannelDispatchStatus { Sent, Failed, Throttled, InvalidConfiguration, Timeout, NetworkError } /// /// Result of a channel health check. /// public sealed record ChannelHealthCheckResult { public required bool Healthy { get; init; } public required string Status { get; init; } public string? Message { get; init; } public TimeSpan? Latency { get; init; } public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); public static ChannelHealthCheckResult Ok(string? message = null, TimeSpan? latency = null) => new() { Healthy = true, Status = "healthy", Message = message ?? "Channel is operational.", Latency = latency }; public static ChannelHealthCheckResult Degraded(string message, TimeSpan? latency = null) => new() { Healthy = true, Status = "degraded", Message = message, Latency = latency }; public static ChannelHealthCheckResult Unhealthy(string message) => new() { Healthy = false, Status = "unhealthy", Message = message }; }