180 lines
6.7 KiB
C#
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; }
|
|
}
|