using System.Security.Claims;
using Campaign_Tracker.Server.Audit;
using Campaign_Tracker.Server.Authorization;
using Campaign_Tracker.Server.LegacyData;
using Campaign_Tracker.Server.Municipalities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Campaign_Tracker.Server.Controllers;
///
/// Municipality account profile management (Story 1.10).
/// Accessible to ClientServices and Admin roles (HasAny check includes Admin bypass).
///
[ApiController]
[Authorize(Policy = ApplicationPolicy.ClientServicesAccess)]
[Route("api/municipalities/profiles")]
public sealed class MunicipalityProfileController : ControllerBase
{
private readonly IMunicipalityProfileRepository _profiles;
private readonly ILegacyDataAccess _legacyData;
private readonly IAuditService _audit;
private readonly TimeProvider _timeProvider;
public MunicipalityProfileController(
IMunicipalityProfileRepository profiles,
ILegacyDataAccess legacyData,
IAuditService audit,
TimeProvider timeProvider)
{
_profiles = profiles;
_legacyData = legacyData;
_audit = audit;
_timeProvider = timeProvider;
}
// ── Available legacy jurisdictions (JCode picker source) ─────────────────
[HttpGet("/api/municipalities/jurisdictions")]
public async Task>> GetJurisdictions(
CancellationToken cancellationToken)
{
var jurisdictions = await _legacyData.GetAllJurisdictionsAsync(cancellationToken);
return Ok(jurisdictions
.Select(j => new LegacyJurisdictionResponse(j.JCode, j.Name))
.ToArray());
}
// ── AC #1, AC #2: create and immediately return the combined view ─────────
[HttpPost]
public async Task> Create(
[FromBody] CreateMunicipalityProfileRequest request,
CancellationToken cancellationToken)
{
var actor = GetActor();
var result = await _profiles.CreateAsync(request.JCode, request.DisplayName, actor, cancellationToken);
if (!result.Saved || result.Profile is null)
return UnprocessableEntity(new MunicipalityProfileProblem(result.Error ?? "Save failed."));
// AC #3: audit the creation
_audit.Record(new AuditEvent(
EventType: "MUNICIPALITY_PROFILE_CREATED",
ActorIdentity: actor,
Resource: $"municipalities/profiles/{result.Profile.ProfileId}",
Outcome: $"created JCode={result.Profile.JCode}",
TraceIdentifier: HttpContext.TraceIdentifier,
RecordedAt: _timeProvider.GetUtcNow()));
var view = await _profiles.GetByIdAsync(result.Profile.ProfileId, cancellationToken);
if (view is null)
return StatusCode(500, new MunicipalityProfileProblem("Profile was saved but could not be retrieved."));
return Ok(MunicipalityProfileResponse.From(view));
}
// ── AC #2: list all profiles with resolved legacy data ───────────────────
[HttpGet]
public async Task>> GetAll(
CancellationToken cancellationToken)
{
var views = await _profiles.GetAllAsync(cancellationToken);
return Ok(views.Select(MunicipalityProfileResponse.From).ToArray());
}
[HttpGet("{profileId}")]
public async Task> GetById(
string profileId,
CancellationToken cancellationToken)
{
var view = await _profiles.GetByIdAsync(profileId, cancellationToken);
return view is null ? NotFound() : Ok(MunicipalityProfileResponse.From(view));
}
// ── AC #3: update with audit log ─────────────────────────────────────────
[HttpPut("{profileId}")]
public async Task> Update(
string profileId,
[FromBody] UpdateMunicipalityProfileRequest request,
CancellationToken cancellationToken)
{
var actor = GetActor();
var result = await _profiles.UpdateAsync(profileId, request.DisplayName, actor, cancellationToken);
if (!result.Saved || result.Profile is null)
{
if (result.IsNotFound)
return NotFound(new MunicipalityProfileProblem(result.Error ?? "Profile not found."));
return UnprocessableEntity(new MunicipalityProfileProblem(result.Error ?? "Update failed."));
}
_audit.Record(new AuditEvent(
EventType: "MUNICIPALITY_PROFILE_UPDATED",
ActorIdentity: actor,
Resource: $"municipalities/profiles/{profileId}",
Outcome: "updated display name",
TraceIdentifier: HttpContext.TraceIdentifier,
RecordedAt: _timeProvider.GetUtcNow()));
var view = await _profiles.GetByIdAsync(profileId, cancellationToken);
if (view is null)
return StatusCode(500, new MunicipalityProfileProblem("Profile was updated but could not be retrieved."));
return Ok(MunicipalityProfileResponse.From(view));
}
private string GetActor() =>
User.Identity?.Name
?? User.FindFirstValue(ClaimTypes.NameIdentifier)
?? "unknown";
}
public sealed record CreateMunicipalityProfileRequest(string JCode, string? DisplayName);
public sealed record UpdateMunicipalityProfileRequest(string? DisplayName);
public sealed record MunicipalityProfileResponse(
string ProfileId,
string JCode,
string? DisplayName,
string UpdatedAt,
string UpdatedBy,
string? LegacyName,
string? LegacyMailingAddress,
string? LegacyCityStateZip)
{
public static MunicipalityProfileResponse From(MunicipalityProfileView view) =>
new(view.Profile.ProfileId,
view.Profile.JCode,
view.Profile.DisplayName,
view.Profile.UpdatedAt.ToString("O"),
view.Profile.UpdatedBy,
view.LegacyName,
view.LegacyMailingAddress,
view.LegacyCityStateZip);
}
public sealed record MunicipalityProfileProblem(string Error);
public sealed record LegacyJurisdictionResponse(string JCode, string? Name);