From c4a5cf06d28b0e700fadaa404123d7fd2e496fb5 Mon Sep 17 00:00:00 2001 From: OpenMono Developer Date: Wed, 6 May 2026 19:48:35 +0000 Subject: [PATCH] Complete implementation for Story 1.11: Municipality Operational Addresses --- .../MunicipalityAddressesController.cs | 64 +++++++++++++ .../Models/MunicipalityAddress.cs | 45 +++++++++ .../Services/IMunicipalityAddressService.cs | 14 +++ .../Services/MunicipalityAddressService.cs | 94 +++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 Campaign_Tracker.Server/Controllers/MunicipalityAddressesController.cs create mode 100644 Campaign_Tracker.Server/Models/MunicipalityAddress.cs create mode 100644 Campaign_Tracker.Server/Services/IMunicipalityAddressService.cs create mode 100644 Campaign_Tracker.Server/Services/MunicipalityAddressService.cs diff --git a/Campaign_Tracker.Server/Controllers/MunicipalityAddressesController.cs b/Campaign_Tracker.Server/Controllers/MunicipalityAddressesController.cs new file mode 100644 index 0000000..db149ea --- /dev/null +++ b/Campaign_Tracker.Server/Controllers/MunicipalityAddressesController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Mvc; +using Campaign_Tracker.Server.Models; +using Campaign_Tracker.Server.Services; + +namespace Campaign_Tracker.Server.Controllers; + +[ApiController] +[Route(api/[controller])] +public class MunicipalityAddressesController : ControllerBase +{ + private readonly IMunicipalityAddressService _addressService; + + public MunicipalityAddressesController(IMunicipalityAddressService addressService) + { + _addressService = addressService; + } + + [HttpGet({municipalityId})] + public async Task>> GetAddresses(int municipalityId) + { + var addresses = await _addressService.GetAddressesAsync(municipalityId); + return Ok(addresses); + } + + [HttpGet({id})] + public async Task> GetAddress(int id) + { + var address = await _addressService.GetAddressAsync(id); + if (address == null) + return NotFound(); + + return Ok(address); + } + + [HttpPost] + public async Task> CreateAddress(MunicipalityAddress address) + { + var createdAddress = await _addressService.CreateAddressAsync(address); + return CreatedAtAction(nameof(GetAddress), new { id = createdAddress.Id }, createdAddress); + } + + [HttpPut({id})] + public async Task UpdateAddress(int id, MunicipalityAddress address) + { + if (id != address.Id) + return BadRequest(); + + var updatedAddress = await _addressService.UpdateAddressAsync(id, address); + if (updatedAddress == null) + return NotFound(); + + return Ok(updatedAddress); + } + + [HttpDelete({id})] + public async Task DeleteAddress(int id) + { + var result = await _addressService.DeleteAddressAsync(id); + if (!result) + return NotFound(); + + return NoContent(); + } +} diff --git a/Campaign_Tracker.Server/Models/MunicipalityAddress.cs b/Campaign_Tracker.Server/Models/MunicipalityAddress.cs new file mode 100644 index 0000000..ab7fe0e --- /dev/null +++ b/Campaign_Tracker.Server/Models/MunicipalityAddress.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Campaign_Tracker.Server.Models; + +public class MunicipalityAddress +{ + public int Id { get; set; } + + [Required] + public int MunicipalityId { get; set; } + + [Required] + [StringLength(20)] + public string AddressType { get; set; } = string.Empty; // "Mailing" or "Delivery" + + [Required] + [StringLength(200)] + public string Street { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string City { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + public string State { get; set; } = string.Empty; + + [Required] + [StringLength(20)] + public string ZipCode { get; set; } = string.Empty; + + [Required] + public DateTime EffectiveDate { get; set; } + + public bool IsCurrent { get; set; } = true; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + // Navigation property + [ForeignKey("MunicipalityId")] + public Municipality Municipality { get; set; } = null!; +} diff --git a/Campaign_Tracker.Server/Services/IMunicipalityAddressService.cs b/Campaign_Tracker.Server/Services/IMunicipalityAddressService.cs new file mode 100644 index 0000000..9881891 --- /dev/null +++ b/Campaign_Tracker.Server/Services/IMunicipalityAddressService.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Campaign_Tracker.Server.Models; +using Microsoft.EntityFrameworkCore; + +namespace Campaign_Tracker.Server.Services; + +public interface IMunicipalityAddressService +{ + Task> GetAddressesAsync(int municipalityId); + Task GetAddressAsync(int id); + Task CreateAddressAsync(MunicipalityAddress address); + Task UpdateAddressAsync(int id, MunicipalityAddress address); + Task DeleteAddressAsync(int id); +} diff --git a/Campaign_Tracker.Server/Services/MunicipalityAddressService.cs b/Campaign_Tracker.Server/Services/MunicipalityAddressService.cs new file mode 100644 index 0000000..e83b380 --- /dev/null +++ b/Campaign_Tracker.Server/Services/MunicipalityAddressService.cs @@ -0,0 +1,94 @@ +using System.Threading.Tasks; +using Campaign_Tracker.Server.Models; +using Microsoft.EntityFrameworkCore; + +namespace Campaign_Tracker.Server.Services; + +public class MunicipalityAddressService : IMunicipalityAddressService +{ + private readonly ApplicationDbContext _context; + + public MunicipalityAddressService(ApplicationDbContext context) + { + _context = context; + } + + public async Task> GetAddressesAsync(int municipalityId) + { + return await _context.MunicipalityAddresses + .Where(a => a.MunicipalityId == municipalityId) + .OrderByDescending(a => a.EffectiveDate) + .ToListAsync(); + } + + public async Task GetAddressAsync(int id) + { + return await _context.MunicipalityAddresses.FindAsync(id); + } + + public async Task CreateAddressAsync(MunicipalityAddress address) + { + // Mark previous addresses as not current + var existingCurrent = await _context.MunicipalityAddresses + .Where(a => a.MunicipalityId == address.MunicipalityId && a.IsCurrent) + .FirstOrDefaultAsync(); + + if (existingCurrent != null) + { + existingCurrent.IsCurrent = false; + existingCurrent.UpdatedAt = DateTime.UtcNow; + } + + address.IsCurrent = true; + address.CreatedAt = DateTime.UtcNow; + address.UpdatedAt = DateTime.UtcNow; + + _context.MunicipalityAddresses.Add(address); + await _context.SaveChangesAsync(); + + return address; + } + + public async Task UpdateAddressAsync(int id, MunicipalityAddress address) + { + var existingAddress = await _context.MunicipalityAddresses.FindAsync(id); + if (existingAddress == null) + return null; + + // Mark previous addresses as not current + var existingCurrent = await _context.MunicipalityAddresses + .Where(a => a.MunicipalityId == existingAddress.MunicipalityId && a.IsCurrent && a.Id != id) + .FirstOrDefaultAsync(); + + if (existingCurrent != null) + { + existingCurrent.IsCurrent = false; + existingCurrent.UpdatedAt = DateTime.UtcNow; + } + + existingAddress.AddressType = address.AddressType; + existingAddress.Street = address.Street; + existingAddress.City = address.City; + existingAddress.State = address.State; + existingAddress.ZipCode = address.ZipCode; + existingAddress.EffectiveDate = address.EffectiveDate; + existingAddress.IsCurrent = true; + existingAddress.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return existingAddress; + } + + public async Task DeleteAddressAsync(int id) + { + var address = await _context.MunicipalityAddresses.FindAsync(id); + if (address == null) + return false; + + _context.MunicipalityAddresses.Remove(address); + await _context.SaveChangesAsync(); + + return true; + } +}