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);