using System.Security.Claims; using Campaign_Tracker.Server.Audit; using Campaign_Tracker.Server.Authorization; 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 IAuditService _audit; private readonly TimeProvider _timeProvider; public MunicipalityProfileController( IMunicipalityProfileRepository profiles, IAuditService audit, TimeProvider timeProvider) { _profiles = profiles; _audit = audit; _timeProvider = timeProvider; } // ── 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); 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) 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); 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);