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

180 lines
6.8 KiB
C#

using Gateway.Data;
using Gateway.Security;
using Gateway.Services;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
namespace Gateway.Controllers;
/// <summary>
/// Census demographic data endpoints for the campaign wizard.
///
/// GET /api/demographics/{zcta} — fetch census data from DB, forward to
/// Intelligence container for derived
/// recommendations. Falls back to raw census
/// data if Intelligence is unreachable.
/// POST /api/demographics/list — multiple ZCTAs (raw data only)
/// POST /api/demographics/search — find ZCTAs by criteria (raw data only)
/// </summary>
[ApiController]
[Route("api/demographics")]
public sealed class DemographicsController : ControllerBase
{
private readonly SqlService _sql;
private readonly AuthorizationGuard _guard;
private readonly IntelligenceApiClient _intelligence;
private readonly ILogger<DemographicsController> _log;
public DemographicsController(
SqlService sql,
AuthorizationGuard guard,
IntelligenceApiClient intelligence,
ILogger<DemographicsController> log)
{
_sql = sql;
_guard = guard;
_intelligence = intelligence;
_log = log;
}
/// <summary>
/// Fetch raw census data for a ZCTA, then forward to Intelligence container
/// for derived audience recommendations (age chips, income tiers, insights).
/// Falls back to raw census data only if Intelligence is unreachable.
/// </summary>
[HttpGet("{zcta}")]
public async Task<IActionResult> Get(string zcta, CancellationToken ct)
{
var (ok, err) = _guard.RequireAuth();
if (!ok) return Unauthorized(new { ok = false, error = err });
if (string.IsNullOrWhiteSpace(zcta) || zcta.Length != 5 || !zcta.All(char.IsDigit))
return BadRequest(new { ok = false, error = "Valid 5-digit ZIP code is required" });
try
{
// 1. Fetch raw census data from DB
var censusJson = await _sql.ExecProcAsync(
SqlNames.Procs.Demographics, "get",
JsonSerializer.Serialize(new { zcta }),
ct: ct);
if (string.IsNullOrWhiteSpace(censusJson))
return NotFound(new { ok = false, error = "ZIP code not found" });
using var doc = JsonDocument.Parse(censusJson);
var censusRoot = doc.RootElement;
if (censusRoot.TryGetProperty("ok", out var okProp) && !okProp.GetBoolean())
return NotFound(new { ok = false, error = "ZIP code not found" });
// 2. Forward to Intelligence container for market analysis derivation
var analysis = await _intelligence.GetDemographicAnalysisAsync(zcta, censusRoot, ct);
if (analysis != null)
{
_log.LogInformation("[Demographics] Analysis by Intelligence container | ZCTA={Zcta}", zcta);
return Content(analysis, "application/json");
}
// 3. Fallback: return raw census data
_log.LogInformation("[Demographics] Intelligence unavailable — raw census | ZCTA={Zcta}", zcta);
return Content(censusJson, "application/json");
}
catch (Exception ex)
{
_log.LogError(ex, "Demographics get error for ZCTA {Zcta}", zcta);
return StatusCode(500, new { ok = false, error = "Demographics service error" });
}
}
/// <summary>Get demographics for multiple ZCTAs (raw census data).</summary>
[HttpPost("list")]
public async Task<IActionResult> List([FromBody] ZctaListRequest? request, CancellationToken ct)
{
var (ok, err) = _guard.RequireAuth();
if (!ok) return Unauthorized(new { ok = false, error = err });
if (request?.Zctas == null || request.Zctas.Length == 0)
return BadRequest(new { ok = false, error = "zctas array is required" });
try
{
var rqst = JsonSerializer.Serialize(new
{
zctas = request.Zctas,
page = request.Page ?? 1,
pageSize = request.PageSize ?? 50
});
var resp = await _sql.ExecProcAsync(SqlNames.Procs.Demographics, "list", rqst, ct: ct);
if (string.IsNullOrWhiteSpace(resp))
return StatusCode(500, new { ok = false, error = "Demographics service unavailable" });
return Content(resp, "application/json");
}
catch (Exception ex)
{
_log.LogError(ex, "Demographics list error");
return StatusCode(500, new { ok = false, error = "Demographics service error" });
}
}
/// <summary>Search ZCTAs by demographic criteria (raw census data).</summary>
[HttpPost("search")]
public async Task<IActionResult> Search([FromBody] DemographicSearchRequest? request, CancellationToken ct)
{
var (ok, err) = _guard.RequireAuth();
if (!ok) return Unauthorized(new { ok = false, error = err });
try
{
var rqst = JsonSerializer.Serialize(new
{
zctaPrefix = request?.ZctaPrefix,
minIncome = request?.MinIncome,
maxIncome = request?.MaxIncome,
minPopulation = request?.MinPopulation,
minBachelorPct = request?.MinBachelorPct,
minAge25to34Pct = request?.MinAge25to34Pct,
minHomeValue = request?.MinHomeValue,
page = request?.Page ?? 1,
pageSize = request?.PageSize ?? 50
});
var resp = await _sql.ExecProcAsync(SqlNames.Procs.Demographics, "search", rqst, ct: ct);
if (string.IsNullOrWhiteSpace(resp))
return StatusCode(500, new { ok = false, error = "Demographics service unavailable" });
return Content(resp, "application/json");
}
catch (Exception ex)
{
_log.LogError(ex, "Demographics search error");
return StatusCode(500, new { ok = false, error = "Demographics service error" });
}
}
}
// ── DTOs ──
public sealed class ZctaListRequest
{
public string[]? Zctas { get; set; }
public int? Page { get; set; }
public int? PageSize { get; set; }
}
public sealed class DemographicSearchRequest
{
public string? ZctaPrefix { get; set; }
public int? MinIncome { get; set; }
public int? MaxIncome { get; set; }
public int? MinPopulation { get; set; }
public decimal? MinBachelorPct { get; set; }
public decimal? MinAge25to34Pct { get; set; }
public int? MinHomeValue { get; set; }
public int? Page { get; set; }
public int? PageSize { get; set; }
}