161 lines
6.3 KiB
C#
161 lines
6.3 KiB
C#
using Gateway.Models;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Gateway.Services;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public sealed class ProviderStatusNormalizer
|
|
{
|
|
private readonly MultiChannelConfig _config;
|
|
private readonly ILogger<ProviderStatusNormalizer> _log;
|
|
|
|
/// <summary>The canonical set of platform-level statuses.</summary>
|
|
private static readonly HashSet<string> PlatformStatuses = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"draft", "staged", "pending", "active", "paused", "completed", "cancelled", "error"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Internal/transitional statuses used during launch orchestration.
|
|
/// These are not provider-specific but arise from the platform's own workflow.
|
|
/// </summary>
|
|
private static readonly Dictionary<string, string> 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<MultiChannelConfig> config,
|
|
ILogger<ProviderStatusNormalizer> log)
|
|
{
|
|
_config = config.Value;
|
|
_log = log;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalize a raw provider status into a platform status.
|
|
/// </summary>
|
|
/// <param name="channelType">Channel identifier (e.g. "google_ads", "meta", "tiktok").</param>
|
|
/// <param name="rawProviderStatus">The status string as returned by the provider.</param>
|
|
/// <returns>A valid platform status string.</returns>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve the platform status for a sync operation.
|
|
/// If an explicit platform status is provided, validate and use it.
|
|
/// Otherwise, normalize the provider status.
|
|
/// </summary>
|
|
/// <param name="channelType">Channel identifier.</param>
|
|
/// <param name="explicitStatus">Explicitly provided platform status (optional).</param>
|
|
/// <param name="rawProviderStatus">Raw provider status (optional).</param>
|
|
/// <returns>A valid platform status string.</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all configured mappings for a channel (for diagnostics / admin display).
|
|
/// </summary>
|
|
public Dictionary<string, string> GetMappings(string channelType)
|
|
{
|
|
var result = new Dictionary<string, string>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check whether a string is a valid platform status.
|
|
/// </summary>
|
|
public static bool IsValidPlatformStatus(string? status) =>
|
|
!string.IsNullOrWhiteSpace(status) && PlatformStatuses.Contains(status);
|
|
}
|