Initial import into Gitea
This commit is contained in:
96
Gateway/Controllers/ForecastController.cs
Normal file
96
Gateway/Controllers/ForecastController.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Gateway.Models;
|
||||
using Gateway.Security;
|
||||
using Gateway.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Gateway.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Channel forecast endpoint for the campaign wizard.
|
||||
///
|
||||
/// Routes to IntelligenceApi (category-aware engine container) when configured.
|
||||
/// Falls back to local ForecastService (General/rules-based) if IntelligenceApi
|
||||
/// is unreachable — ensuring the wizard never breaks during deployments.
|
||||
///
|
||||
/// ROUTING LOGIC:
|
||||
/// 1. Try IntelligenceApi — passes clientCategory so the engine router
|
||||
/// can select the correct model (General, Franchisee, Franchisor, etc.)
|
||||
/// 2. If unreachable / error → fall back to local ForecastService
|
||||
///
|
||||
/// SECURITY: Requires authenticated client session.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/forecast")]
|
||||
public sealed class ForecastController : ControllerBase
|
||||
{
|
||||
private readonly ForecastService _forecastService;
|
||||
private readonly IntelligenceApiClient _intelligenceClient;
|
||||
private readonly ClientContext _client;
|
||||
private readonly AuthorizationGuard _guard;
|
||||
private readonly ILogger<ForecastController> _log;
|
||||
|
||||
public ForecastController(
|
||||
ForecastService forecastService,
|
||||
IntelligenceApiClient intelligenceClient,
|
||||
ClientContext client,
|
||||
AuthorizationGuard guard,
|
||||
ILogger<ForecastController> log)
|
||||
{
|
||||
_forecastService = forecastService;
|
||||
_intelligenceClient = intelligenceClient;
|
||||
_client = client;
|
||||
_guard = guard;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate channel performance estimates for given targeting + budget.
|
||||
/// Called by the wizard AllocationStep when budget changes.
|
||||
///
|
||||
/// POST /api/forecast/channel-estimate
|
||||
/// </summary>
|
||||
[HttpPost("channel-estimate")]
|
||||
public async Task<IActionResult> ChannelEstimate(
|
||||
[FromBody] ChannelForecastRequest? request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
if (request == null)
|
||||
return BadRequest(new { ok = false, error = "Request body is required" });
|
||||
|
||||
if (request.MonthlyBudget <= 0)
|
||||
return BadRequest(new { ok = false, error = "monthlyBudget must be greater than zero" });
|
||||
|
||||
if (request.Keywords.Count == 0)
|
||||
return BadRequest(new { ok = false, error = "At least one keyword is required" });
|
||||
|
||||
_log.LogInformation(
|
||||
"[Forecast] Request | Category={Category} Budget={Budget} Objective={Obj}",
|
||||
_client.ClientCategory, request.MonthlyBudget, request.Objective);
|
||||
|
||||
try
|
||||
{
|
||||
// ── 1. Try IntelligenceApi (category-aware) ──
|
||||
var result = await _intelligenceClient.GetSpendDistributionAsync(
|
||||
request, _client.ClientCategory, ct);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
_log.LogInformation("[Forecast] Served by IntelligenceApi");
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// ── 2. Fallback: local ForecastService (General engine equivalent) ──
|
||||
_log.LogInformation("[Forecast] IntelligenceApi unavailable — using local ForecastService");
|
||||
var fallback = await _forecastService.ForecastAsync(request, ct);
|
||||
return Ok(fallback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "[Forecast] Error");
|
||||
return StatusCode(500, new { ok = false, error = "Forecast service error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user