Files
AdPlatform-Server/IntelligenceApi/Controllers/InternalController.cs
2026-03-14 13:50:09 -07:00

123 lines
4.8 KiB
C#

using IntelligenceApi.Engines;
using IntelligenceApi.Models;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
namespace IntelligenceApi.Controllers;
/// <summary>
/// Internal execution endpoint — the single entry point for Gateway-routed requests.
///
/// Called exclusively by the Gateway's ExecutionService via:
/// POST /internal/execute
/// Headers: X-Internal-Key, X-Request-Id
/// Body: { "operation": "Ping" | "SpendDistribution", "payload": { ... } }
///
/// Operations:
/// Ping — liveness check, no payload required
/// SpendDistribution — routes to EngineRouter; payload maps to SpendDistributionRequest
/// </summary>
[ApiController]
[Route("internal/execute")]
public sealed class InternalController : ControllerBase
{
private readonly EngineRouter _router;
private readonly ILogger<InternalController> _log;
private static readonly JsonSerializerOptions _jsonOpts =
new(JsonSerializerDefaults.Web);
public InternalController(EngineRouter router, ILogger<InternalController> log)
{
_router = router;
_log = log;
}
[HttpPost]
public async Task<IActionResult> Execute(
[FromBody] InternalExecuteRequest? request,
CancellationToken ct)
{
if (request == null)
return BadRequest(new { ok = false, error = "Request body required" });
var requestId = request.RequestId ?? HttpContext.Request.Headers["X-Request-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
var operation = (request.Operation ?? "").Trim();
_log.LogInformation(
"[Internal] Operation={Operation} RequestId={RequestId}",
operation, requestId);
return operation.ToLowerInvariant() switch
{
"ping" => Ok(new { ok = true, requestId, service = "IntelligenceApi", timestamp = DateTimeOffset.UtcNow }),
"spenddistribution" => await SpendDistribution(request.Payload, requestId, ct),
_ => BadRequest(new { ok = false, requestId, error = $"Unknown operation: '{operation}'" })
};
}
// ── Spend Distribution ───────────────────────────────────────────────────
private async Task<IActionResult> SpendDistribution(
JsonElement? payload,
string requestId,
CancellationToken ct)
{
if (payload == null || payload.Value.ValueKind == JsonValueKind.Null)
return BadRequest(new { ok = false, requestId, error = "Payload required for SpendDistribution" });
SpendDistributionRequest? distRequest;
try
{
distRequest = JsonSerializer.Deserialize<SpendDistributionRequest>(
payload.Value.GetRawText(), _jsonOpts);
}
catch (Exception ex)
{
_log.LogWarning("[Internal] Payload deserialize failed | RequestId={RequestId} Error={Error}",
requestId, ex.Message);
return BadRequest(new { ok = false, requestId, error = "Invalid payload shape for SpendDistribution" });
}
if (distRequest == null)
return BadRequest(new { ok = false, requestId, error = "Payload required for SpendDistribution" });
if (distRequest.MonthlyBudget <= 0)
return BadRequest(new { ok = false, requestId, error = "monthlyBudget must be greater than zero" });
if (distRequest.Keywords.Count == 0)
return BadRequest(new { ok = false, requestId, error = "At least one keyword is required" });
try
{
var engine = _router.Resolve(distRequest.ClientCategory);
var response = await engine.RecommendAsync(distRequest, ct);
_log.LogInformation(
"[Internal] SpendDistribution OK | RequestId={RequestId} Engine={Engine} Channels={N}",
requestId, response.Metadata.Engine, response.Channels.Count);
return Ok(response);
}
catch (Exception ex)
{
_log.LogError(ex, "[Internal] SpendDistribution error | RequestId={RequestId}", requestId);
return StatusCode(500, new { ok = false, requestId, error = "Intelligence service error" });
}
}
}
// ── Request model ────────────────────────────────────────────────────────────
/// <summary>
/// Shape sent by Gateway's ExecutionService to every provider container.
/// Matches the object built in ExecutionService.BuildProviderRequest().
/// </summary>
public sealed class InternalExecuteRequest
{
public string? Operation { get; set; }
public string? RequestId { get; set; }
public string? TenantId { get; set; }
public JsonElement? Payload { get; set; }
}