Initial import into Gitea
This commit is contained in:
160
Gateway/Services/ProviderStatusNormalizer.cs
Normal file
160
Gateway/Services/ProviderStatusNormalizer.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user