using Gateway.Models; using Microsoft.Extensions.Options; namespace Gateway.Services; /// /// Normalizes provider-specific campaign statuses into platform-standard statuses. /// /// Each advertising channel (Google Ads, Meta, TikTok, etc.) reports campaign state /// using its own vocabulary. This service translates those into the platform's /// unified status set: draft, staged, pending, active, paused, completed, cancelled, error. /// /// Mapping priority: /// 1. Channel-specific mapping from config (e.g. google_ads → ENABLED → active) /// 2. Common/internal mappings (e.g. submitted → active, pending_review → pending) /// 3. Pass-through if the raw status is already a valid platform status /// 4. "error" fallback with a warning log for truly unknown statuses /// public sealed class ProviderStatusNormalizer { private readonly MultiChannelConfig _config; private readonly ILogger _log; /// The canonical set of platform-level statuses. private static readonly HashSet PlatformStatuses = new(StringComparer.OrdinalIgnoreCase) { "draft", "staged", "pending", "active", "paused", "completed", "cancelled", "error" }; /// /// Internal/transitional statuses used during launch orchestration. /// These are not provider-specific but arise from the platform's own workflow. /// private static readonly Dictionary CommonMappings = new(StringComparer.OrdinalIgnoreCase) { // Launch service assigns these during dispatch ["submitted"] = "active", ["pending_review"] = "pending", ["stub_provider"] = "pending", // Webhook / callback transitional states ["approved"] = "active", ["rejected"] = "error", ["suspended"] = "paused", ["budget_depleted"] = "paused", ["expired"] = "completed", ["archived"] = "completed", ["deleted"] = "cancelled", ["in_process"] = "pending", ["in_review"] = "pending", ["learning"] = "active", // Meta "learning phase" ["limited"] = "active", // Google "limited by budget" etc. }; public ProviderStatusNormalizer( IOptions config, ILogger log) { _config = config.Value; _log = log; } /// /// Normalize a raw provider status into a platform status. /// /// Channel identifier (e.g. "google_ads", "meta", "tiktok"). /// The status string as returned by the provider. /// A valid platform status string. public string Normalize(string? channelType, string? rawProviderStatus) { if (string.IsNullOrWhiteSpace(rawProviderStatus)) return "error"; var raw = rawProviderStatus.Trim(); // 1. Try channel-specific mapping from config if (!string.IsNullOrWhiteSpace(channelType)) { var provider = _config.GetChannel(channelType); if (provider?.StatusMappings != null && provider.StatusMappings.TryGetValue(raw, out var mapped)) { _log.LogDebug("[StatusNorm] {Channel}/{RawStatus} → {PlatformStatus} (config)", channelType, raw, mapped); return mapped; } } // 2. Try common/internal mappings if (CommonMappings.TryGetValue(raw, out var common)) { _log.LogDebug("[StatusNorm] {RawStatus} → {PlatformStatus} (common)", raw, common); return common; } // 3. If the raw value is already a valid platform status, pass through if (PlatformStatuses.Contains(raw)) { _log.LogDebug("[StatusNorm] {RawStatus} → pass-through (already platform status)", raw); return raw.ToLowerInvariant(); } // 4. Unknown — log warning and return "error" _log.LogWarning( "[StatusNorm] Unknown provider status: channel={Channel}, raw={RawStatus}. Defaulting to 'error'. " + "Add a mapping in MultiChannel.Channels[].StatusMappings or CommonMappings.", channelType ?? "(none)", raw); return "error"; } /// /// Resolve the platform status for a sync operation. /// If an explicit platform status is provided, validate and use it. /// Otherwise, normalize the provider status. /// /// Channel identifier. /// Explicitly provided platform status (optional). /// Raw provider status (optional). /// A valid platform status string. public string Resolve(string? channelType, string? explicitStatus, string? rawProviderStatus) { // If an explicit platform status was given, validate it if (!string.IsNullOrWhiteSpace(explicitStatus)) { if (PlatformStatuses.Contains(explicitStatus)) return explicitStatus.ToLowerInvariant(); _log.LogWarning("[StatusNorm] Invalid explicit status '{Status}', normalizing as provider status instead.", explicitStatus); // Fall through to normalization } return Normalize(channelType, rawProviderStatus); } /// /// Get all configured mappings for a channel (for diagnostics / admin display). /// public Dictionary GetMappings(string channelType) { var result = new Dictionary(CommonMappings, StringComparer.OrdinalIgnoreCase); var provider = _config.GetChannel(channelType); if (provider?.StatusMappings != null) { foreach (var kv in provider.StatusMappings) result[kv.Key] = kv.Value; // Channel-specific overrides common } return result; } /// /// Check whether a string is a valid platform status. /// public static bool IsValidPlatformStatus(string? status) => !string.IsNullOrWhiteSpace(status) && PlatformStatuses.Contains(status); }