Files
AdPlatform-Server/Management/Controllers/Admin/AdminRecommendationsController.cs
2026-03-14 13:50:09 -07:00

180 lines
6.7 KiB
C#

using Management.Data;
using Management.Security;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
namespace Management.Controllers.Admin;
/// <summary>
/// Admin endpoints for the recommendation engine.
///
/// ENDPOINTS:
/// GET /api/admin/recommendations/rules - List all rules
/// GET /api/admin/recommendations/rules/{id} - Get rule
/// POST /api/admin/recommendations/rules - Create rule
/// PUT /api/admin/recommendations/rules/{id} - Update rule
/// DELETE /api/admin/recommendations/rules/{id} - Delete rule
/// POST /api/admin/recommendations/evaluate - Trigger evaluation
/// POST /api/admin/recommendations/cleanup - Cleanup old records
///
/// Client-facing endpoints (list, dismiss, resolve) remain on Gateway
/// at /api/recommendations — scoped to the authenticated CIAM session.
/// </summary>
[ApiController]
[Route("api/admin/recommendations")]
public sealed class AdminRecommendationsController : AdminControllerBase
{
public AdminRecommendationsController(
SqlService sql, ClientContext client, ILogger<AdminRecommendationsController> log)
: base(sql, client, log) { }
// ────────────────────────────────────────────────
// Rule Management
// ────────────────────────────────────────────────
[HttpGet("rules")]
public Task<IActionResult> ListRules(
[FromQuery] string? category,
[FromQuery] string? channel,
CancellationToken ct)
=> CallProc("spRecommendation", "rules.list", new { category, channel }, ct);
[HttpGet("rules/{ruleId:int}")]
public Task<IActionResult> GetRule(int ruleId, CancellationToken ct)
=> CallProc("spRecommendation", "rules.get", new { ruleId }, ct);
[HttpPost("rules")]
public Task<IActionResult> CreateRule([FromBody] RuleRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request?.Code))
return Task.FromResult(ValidationError("code is required"));
if (string.IsNullOrWhiteSpace(request.Name))
return Task.FromResult(ValidationError("name is required"));
Logger.LogInformation("[Admin] CreateRecommendationRule {Code} | By={User}", request.Code, Client.Email);
return CallProc("spRecommendation", "rules.create", new
{
request.Code,
request.Name,
request.Category,
request.Metric,
request.Operator,
request.Threshold,
request.ThresholdType,
request.Severity,
request.Channel,
request.Objective,
request.Message,
request.AdminNotes,
request.LookbackDays,
request.MinDataDays,
request.CooldownHours,
request.SortOrder
}, ct);
}
[HttpPut("rules/{ruleId:int}")]
public Task<IActionResult> UpdateRule(int ruleId, [FromBody] RuleRequest request, CancellationToken ct)
{
Logger.LogInformation("[Admin] UpdateRecommendationRule {Id} | By={User}", ruleId, Client.Email);
return CallProc("spRecommendation", "rules.update", new
{
ruleId,
request.Name,
request.Category,
request.Metric,
request.Operator,
request.Threshold,
request.ThresholdType,
request.Severity,
request.Channel,
request.Objective,
request.Message,
request.AdminNotes,
request.LookbackDays,
request.MinDataDays,
request.CooldownHours,
request.IsActive,
request.SortOrder
}, ct);
}
[HttpDelete("rules/{ruleId:int}")]
public Task<IActionResult> DeleteRule(int ruleId, CancellationToken ct)
{
Logger.LogInformation("[Admin] DeleteRecommendationRule {Id} | By={User}", ruleId, Client.Email);
return CallProc("spRecommendation", "rules.delete", new { ruleId }, ct);
}
// ────────────────────────────────────────────────
// Evaluation Engine
// ────────────────────────────────────────────────
/// <summary>
/// Trigger rule evaluation for a campaign, initiative, client, or all active campaigns.
/// </summary>
[HttpPost("evaluate")]
public Task<IActionResult> Evaluate([FromBody] EvaluateRequest? request, CancellationToken ct)
{
Logger.LogInformation("[Admin] Evaluate | ClientId={Client} InitiativeId={Init} | By={User}",
request?.ClientId, request?.InitiativeId, Client.Email);
return CallProc("spRecommendation", "evaluate", new
{
channelCampaignId = request?.ChannelCampaignId,
initiativeId = request?.InitiativeId,
clientId = request?.ClientId
}, ct);
}
/// <summary>
/// Cleanup expired and old recommendations.
/// </summary>
[HttpPost("cleanup")]
public Task<IActionResult> Cleanup([FromBody] CleanupRequest? request, CancellationToken ct)
{
Logger.LogInformation("[Admin] RecommendationCleanup daysToKeep={Days} | By={User}",
request?.DaysToKeep ?? 90, Client.Email);
return CallProc("spRecommendation", "cleanup",
new { daysToKeep = request?.DaysToKeep ?? 90 }, ct);
}
}
// ── Request DTOs ──
public sealed class RuleRequest
{
public string? Code { get; set; }
public string? Name { get; set; }
public string? Category { get; set; }
public string? Metric { get; set; }
public string? Operator { get; set; }
public decimal? Threshold { get; set; }
public string? ThresholdType { get; set; }
public string? Severity { get; set; }
public string? Channel { get; set; }
public string? Objective { get; set; }
public string? Message { get; set; }
public string? AdminNotes { get; set; }
public int? LookbackDays { get; set; }
public int? MinDataDays { get; set; }
public int? CooldownHours { get; set; }
public bool? IsActive { get; set; }
public int? SortOrder { get; set; }
}
public sealed class EvaluateRequest
{
public long? ChannelCampaignId { get; set; }
public long? InitiativeId { get; set; }
public string? ClientId { get; set; }
}
public sealed class CleanupRequest
{
public int? DaysToKeep { get; set; }
}