Initial import into Gitea
This commit is contained in:
153
Gateway/Controllers/RecommendationController.cs
Normal file
153
Gateway/Controllers/RecommendationController.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using Gateway.Data;
|
||||
using Gateway.Security;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Gateway.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Client-facing recommendation endpoints.
|
||||
///
|
||||
/// Clients can view, dismiss, and resolve recommendations for their
|
||||
/// own campaigns. All endpoints are scoped to the authenticated client.
|
||||
///
|
||||
/// Admin operations (rule CRUD, evaluate, cleanup) live in the
|
||||
/// Management API at /api/admin/recommendations.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/recommendations")]
|
||||
public sealed class RecommendationController : ControllerBase
|
||||
{
|
||||
private readonly SqlService _sql;
|
||||
private readonly ClientContext _client;
|
||||
private readonly AuthorizationGuard _guard;
|
||||
private readonly ILogger<RecommendationController> _log;
|
||||
|
||||
public RecommendationController(
|
||||
SqlService sql,
|
||||
ClientContext client,
|
||||
AuthorizationGuard guard,
|
||||
ILogger<RecommendationController> log)
|
||||
{
|
||||
_sql = sql;
|
||||
_client = client;
|
||||
_guard = guard;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Client-Facing: List Recommendations
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Get active recommendations for the authenticated client's dashboard.
|
||||
/// Returns recommendations sorted by severity (critical first).
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListByClient(
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
return await Exec(SqlNames.Procs.Recommendation, "listByClient",
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
clientId = _client.ClientId,
|
||||
status = status ?? "active",
|
||||
limit = limit ?? 50
|
||||
}), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get recommendations for a specific initiative (ownership verified).
|
||||
/// </summary>
|
||||
[HttpGet("initiative/{initiativeId:long}")]
|
||||
public async Task<IActionResult> ListByInitiative(
|
||||
long initiativeId,
|
||||
[FromQuery] string? status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
|
||||
return await Exec(SqlNames.Procs.Recommendation, "listByInitiative",
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
initiativeId,
|
||||
status = status ?? "active"
|
||||
}), ct);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Client-Facing: Manage Recommendations
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Dismiss a recommendation (user explicitly ignores it).
|
||||
/// </summary>
|
||||
[HttpPost("{recommendationId:long}/dismiss")]
|
||||
public async Task<IActionResult> Dismiss(long recommendationId, CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
// Ownership check: verify the recommendation belongs to this client
|
||||
// The SP itself filters by recId, but we pass userId for audit trail
|
||||
return await Exec(SqlNames.Procs.Recommendation, "dismiss",
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
recommendationId,
|
||||
userId = _client.UserId
|
||||
}), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a recommendation (action was taken to address it).
|
||||
/// </summary>
|
||||
[HttpPost("{recommendationId:long}/resolve")]
|
||||
public async Task<IActionResult> Resolve(long recommendationId, CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
return await Exec(SqlNames.Procs.Recommendation, "resolve",
|
||||
JsonSerializer.Serialize(new { recommendationId }), ct);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
private async Task<IActionResult> Exec(string proc, string action, string rqst, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = await _sql.ExecProcAsync(proc, action, rqst, ct: ct);
|
||||
if (string.IsNullOrWhiteSpace(resp))
|
||||
return StatusCode(500, new { ok = false, error = "Service unavailable" });
|
||||
|
||||
using var doc = JsonDocument.Parse(resp);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("ok", out var okProp) && !okProp.GetBoolean())
|
||||
{
|
||||
var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Unknown error";
|
||||
if (error?.Contains("not found", StringComparison.OrdinalIgnoreCase) == true)
|
||||
return NotFound(JsonSerializer.Deserialize<object>(resp));
|
||||
return BadRequest(JsonSerializer.Deserialize<object>(resp));
|
||||
}
|
||||
|
||||
return Content(resp, "application/json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Recommendation {Action} error", action);
|
||||
return StatusCode(500, new { ok = false, error = "Service error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user