186 lines
7.6 KiB
C#
186 lines
7.6 KiB
C#
using Gateway.Data;
|
|
using Gateway.Security;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using System.Text.Json;
|
|
|
|
namespace Gateway.Controllers;
|
|
|
|
/// <summary>
|
|
/// Campaign intelligence endpoints: health overview, budget pacing,
|
|
/// and post-campaign analysis.
|
|
///
|
|
/// SECURITY MODEL:
|
|
/// - Every endpoint requires authenticated session (via middleware)
|
|
/// - Initiative endpoints verify ownership before data access
|
|
/// - Client-level endpoints scoped via injected ClientContext
|
|
/// - ClientId is always injected server-side, never from request body
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/intelligence")]
|
|
public sealed class CampaignIntelligenceController : ControllerBase
|
|
{
|
|
private readonly SqlService _sql;
|
|
private readonly ClientContext _client;
|
|
private readonly AuthorizationGuard _guard;
|
|
private readonly ILogger<CampaignIntelligenceController> _log;
|
|
|
|
public CampaignIntelligenceController(
|
|
SqlService sql,
|
|
ClientContext client,
|
|
AuthorizationGuard guard,
|
|
ILogger<CampaignIntelligenceController> log)
|
|
{
|
|
_sql = sql;
|
|
_client = client;
|
|
_guard = guard;
|
|
_log = log;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────
|
|
// Campaign Health Overview
|
|
// ────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Get health overview for all active initiatives.
|
|
/// Returns green/yellow/red status per channel campaign based on active recommendations.
|
|
/// </summary>
|
|
[HttpGet("health")]
|
|
public async Task<IActionResult> Health(CancellationToken ct)
|
|
{
|
|
var (ok, err) = _guard.RequireAuth();
|
|
if (!ok) return Unauthorized(new { ok = false, error = err });
|
|
|
|
return await Exec(SqlNames.Procs.CampaignIntelligence, "health",
|
|
JsonSerializer.Serialize(new { clientId = _client.ClientId }), ct);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────
|
|
// Budget Pacing
|
|
// ────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Get budget pacing analysis for an initiative.
|
|
/// Shows actual vs expected spend velocity with projections.
|
|
/// </summary>
|
|
[HttpGet("{initiativeId:long}/pacing")]
|
|
public async Task<IActionResult> Pacing(long initiativeId, CancellationToken ct)
|
|
{
|
|
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
|
if (!ownership.IsAllowed)
|
|
return NotFound(new { ok = false, error = ownership.Error });
|
|
|
|
return await Exec(SqlNames.Procs.CampaignIntelligence, "pacing",
|
|
JsonSerializer.Serialize(new { initiativeId }), ct);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────
|
|
// Post-Campaign Report
|
|
// ────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Comprehensive post-campaign analysis.
|
|
/// Cross-platform comparison with daily trends, efficiency metrics,
|
|
/// and recommendation history.
|
|
/// </summary>
|
|
[HttpGet("{initiativeId:long}/report")]
|
|
public async Task<IActionResult> PostCampaignReport(long initiativeId, CancellationToken ct)
|
|
{
|
|
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
|
if (!ownership.IsAllowed)
|
|
return NotFound(new { ok = false, error = ownership.Error });
|
|
|
|
return await Exec(SqlNames.Procs.CampaignIntelligence, "postCampaign",
|
|
JsonSerializer.Serialize(new { initiativeId }), ct);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────
|
|
// Metric Snapshots (internal / polling service)
|
|
// ────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Record an intraday metric snapshot for pacing analysis.
|
|
/// Called by the background polling service between daily aggregations.
|
|
/// Admin-only endpoint.
|
|
/// </summary>
|
|
[HttpPost("snapshot")]
|
|
public async Task<IActionResult> Snapshot([FromBody] SnapshotRequest request, CancellationToken ct)
|
|
{
|
|
var (ok, err) = _guard.RequireServiceKey();
|
|
if (!ok) return StatusCode(403, new { ok = false, error = err });
|
|
|
|
return await Exec(SqlNames.Procs.CampaignIntelligence, "snapshot",
|
|
JsonSerializer.Serialize(new
|
|
{
|
|
channelCampaignId = request.ChannelCampaignId,
|
|
date = request.Date,
|
|
impressions = request.Impressions,
|
|
clicks = request.Clicks,
|
|
spend = request.Spend,
|
|
conversions = request.Conversions
|
|
}), ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch insert intraday snapshots.
|
|
/// Admin-only endpoint.
|
|
/// </summary>
|
|
[HttpPost("snapshot/batch")]
|
|
public async Task<IActionResult> SnapshotBatch([FromBody] SnapshotBatchRequest request, CancellationToken ct)
|
|
{
|
|
var (ok, err) = _guard.RequireServiceKey();
|
|
if (!ok) return StatusCode(403, new { ok = false, error = err });
|
|
|
|
return await Exec(SqlNames.Procs.CampaignIntelligence, "snapshotBatch",
|
|
JsonSerializer.Serialize(new { snapshots = request.Snapshots }), ct);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────
|
|
// Helpers
|
|
// ────────────────────────────────────────────────
|
|
|
|
private async Task<IActionResult> Exec(string proc, string action, string rqst, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var resp = await _sql.ExecProcAsync(proc, action, rqst, ct: ct);
|
|
if (string.IsNullOrWhiteSpace(resp))
|
|
return StatusCode(500, new { ok = false, error = "Service unavailable" });
|
|
|
|
using var doc = JsonDocument.Parse(resp);
|
|
var root = doc.RootElement;
|
|
|
|
if (root.TryGetProperty("ok", out var okProp) && !okProp.GetBoolean())
|
|
{
|
|
var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Unknown error";
|
|
if (error?.Contains("not found", StringComparison.OrdinalIgnoreCase) == true)
|
|
return NotFound(JsonSerializer.Deserialize<object>(resp));
|
|
return BadRequest(JsonSerializer.Deserialize<object>(resp));
|
|
}
|
|
|
|
return Content(resp, "application/json");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogError(ex, "CampaignIntelligence {Action} error", action);
|
|
return StatusCode(500, new { ok = false, error = "Service error" });
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── DTOs ──
|
|
|
|
public sealed class SnapshotRequest
|
|
{
|
|
public long? ChannelCampaignId { get; set; }
|
|
public string? Date { get; set; }
|
|
public long? Impressions { get; set; }
|
|
public long? Clicks { get; set; }
|
|
public decimal? Spend { get; set; }
|
|
public decimal? Conversions { get; set; }
|
|
}
|
|
|
|
public sealed class SnapshotBatchRequest
|
|
{
|
|
public object[]? Snapshots { get; set; }
|
|
}
|