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; [ApiController] [Authorize(Policy = ApplicationPolicy.ClientServicesAccess)] [Route("api/municipalities/{profileId}/addresses")] public sealed class MunicipalityAddressesController : ControllerBase { private readonly IMunicipalityAddressRepository _addresses; private readonly IAuditService _audit; private readonly TimeProvider _timeProvider; public MunicipalityAddressesController( IMunicipalityAddressRepository addresses, IAuditService audit, TimeProvider timeProvider) { _addresses = addresses; _audit = audit; _timeProvider = timeProvider; } [HttpGet] public async Task>> GetAll( string profileId, CancellationToken cancellationToken) { var addresses = await _addresses.GetByProfileIdAsync(profileId, cancellationToken); return Ok(addresses.Select(MunicipalityAddressResponse.From).ToArray()); } [HttpGet("{addressId}")] public async Task> GetById( string profileId, string addressId, CancellationToken cancellationToken) { var address = await _addresses.GetByIdAsync(addressId, cancellationToken); return address is null ? NotFound() : Ok(MunicipalityAddressResponse.From(address)); } [HttpPost] public async Task> Add( string profileId, [FromBody] AddMunicipalityAddressRequest request, CancellationToken cancellationToken) { var actor = GetActor(); var result = await _addresses.AddAsync( profileId, request.AddressType, request.Street, request.City, request.State, request.ZipCode, request.EffectiveDate, actor, cancellationToken); if (!result.Saved || result.Address is null) return UnprocessableEntity(new MunicipalityAddressProblem(result.Error ?? "Save failed.")); _audit.Record(new AuditEvent( EventType: "MUNICIPALITY_ADDRESS_ADDED", ActorIdentity: actor, Resource: $"municipalities/{profileId}/addresses/{result.Address.AddressId}", Outcome: $"added {result.Address.AddressType} address", TraceIdentifier: HttpContext.TraceIdentifier, RecordedAt: _timeProvider.GetUtcNow())); return CreatedAtAction(nameof(GetById), new { profileId, addressId = result.Address.AddressId }, MunicipalityAddressResponse.From(result.Address)); } [HttpPut("{addressId}")] public async Task> Update( string profileId, string addressId, [FromBody] UpdateMunicipalityAddressRequest request, CancellationToken cancellationToken) { var actor = GetActor(); var result = await _addresses.UpdateAsync( addressId, request.AddressType, request.Street, request.City, request.State, request.ZipCode, request.EffectiveDate, actor, cancellationToken); if (!result.Saved || result.Address is null) { if (result.IsNotFound) return NotFound(new MunicipalityAddressProblem(result.Error ?? "Address not found.")); return UnprocessableEntity(new MunicipalityAddressProblem(result.Error ?? "Update failed.")); } _audit.Record(new AuditEvent( EventType: "MUNICIPALITY_ADDRESS_UPDATED", ActorIdentity: actor, Resource: $"municipalities/{profileId}/addresses/{addressId}", Outcome: $"updated {result.Address.AddressType} address — new record {result.Address.AddressId}", TraceIdentifier: HttpContext.TraceIdentifier, RecordedAt: _timeProvider.GetUtcNow())); return Ok(MunicipalityAddressResponse.From(result.Address)); } [HttpDelete("{addressId}")] public async Task Delete( string profileId, string addressId, CancellationToken cancellationToken) { var actor = GetActor(); var result = await _addresses.SoftDeleteAsync(addressId, actor, cancellationToken); if (!result.Saved) return result.IsNotFound ? NotFound() : UnprocessableEntity(); _audit.Record(new AuditEvent( EventType: "MUNICIPALITY_ADDRESS_DELETED", ActorIdentity: actor, Resource: $"municipalities/{profileId}/addresses/{addressId}", Outcome: "soft-deleted", TraceIdentifier: HttpContext.TraceIdentifier, RecordedAt: _timeProvider.GetUtcNow())); return NoContent(); } private string GetActor() => User.Identity?.Name ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "unknown"; } public sealed record AddMunicipalityAddressRequest( string AddressType, string Street, string City, string State, string ZipCode, DateTimeOffset EffectiveDate); public sealed record UpdateMunicipalityAddressRequest( string AddressType, string Street, string City, string State, string ZipCode, DateTimeOffset EffectiveDate); public sealed record MunicipalityAddressResponse( string AddressId, string ProfileId, string AddressType, string Street, string City, string State, string ZipCode, string EffectiveDate, bool IsCurrent, string CreatedAt, string CreatedBy, string UpdatedAt, string UpdatedBy) { public static MunicipalityAddressResponse From(MunicipalityAddress a) => new(a.AddressId, a.ProfileId, a.AddressType, a.Street, a.City, a.State, a.ZipCode, a.EffectiveDate.ToString("O"), a.IsCurrent, a.CreatedAt.ToString("O"), a.CreatedBy, a.UpdatedAt.ToString("O"), a.UpdatedBy); } public sealed record MunicipalityAddressProblem(string Error);