using Management.Data; using Management.Security; using Microsoft.AspNetCore.Mvc; using System.Text.Json; namespace Management.Controllers.Admin; /// /// 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. /// [ApiController] [Route("api/admin/recommendations")] public sealed class AdminRecommendationsController : AdminControllerBase { public AdminRecommendationsController( SqlService sql, ClientContext client, ILogger log) : base(sql, client, log) { } // ──────────────────────────────────────────────── // Rule Management // ──────────────────────────────────────────────── [HttpGet("rules")] public Task ListRules( [FromQuery] string? category, [FromQuery] string? channel, CancellationToken ct) => CallProc("spRecommendation", "rules.list", new { category, channel }, ct); [HttpGet("rules/{ruleId:int}")] public Task GetRule(int ruleId, CancellationToken ct) => CallProc("spRecommendation", "rules.get", new { ruleId }, ct); [HttpPost("rules")] public Task 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 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 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 // ──────────────────────────────────────────────── /// /// Trigger rule evaluation for a campaign, initiative, client, or all active campaigns. /// [HttpPost("evaluate")] public Task 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); } /// /// Cleanup expired and old recommendations. /// [HttpPost("cleanup")] public Task 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; } }