# Story 1.12: Municipality Service Contacts Status: done ## Story As a a client services staff member, I want to manage municipality service-contact records including primary and secondary contacts, so that the right people can be reached during election operations without consulting legacy records. ## Acceptance Criteria 1. **Given** a municipality profile exists **When** a client services user adds a contact **Then** they can designate it as primary or secondary contact and save name, role/title, phone, and email 2. **Given** a municipality has both a primary and secondary contact **When** the profile is viewed **Then** both contacts are displayed with clear primary/secondary designation labels 3. **Given** a contact is updated or removed **When** the change is saved **Then** it is recorded in the audit log with actor identity and timestamp 4. **Given** a user attempts to save a contact without required fields (name, contact type) **When** the form is submitted **Then** the save is rejected with inline validation errors identifying the missing fields ## Tasks / Subtasks - [x] Implement story behavior in aligned backend/frontend modules (AC: #1) - [x] Add or update API/service/UI components required by the story scope - [x] Keep legacy Access entities read-only and route writes to extension-layer structures - [x] Cover acceptance criteria #2 in implementation and tests (AC: #2) - [x] Add validation/error handling and UX state updates as needed - [x] Cover acceptance criteria #3 in implementation and tests (AC: #3) - [x] Add validation/error handling and UX state updates as needed - [x] Cover acceptance criteria #4 in implementation and tests (AC: #4) - [x] Add validation/error handling and UX state updates as needed - [x] Validate and document completion evidence - [x] Verify build/tests for touched modules - [x] Capture changed files and any migration/config implications ### Review Findings - [x] [Review][Patch] Scope contact APIs to an existing parent municipality profile before create/read/update/delete [Campaign_Tracker.Server/Controllers/MunicipalityContactsController.cs:49] ## Dev Notes - Follow Epic 1 architecture constraints: ASP.NET Core + React separation, RBAC-aware patterns, and immutable legacy tables. - Reuse shared component and workflow patterns defined in UX and architecture docs; avoid parallel custom implementations. - Keep changes scoped to this story; do not pull forward Epic 2+ features. ### Project Structure Notes - Backend: `Campaign_Tracker.Server/` - Frontend: `campaign-tracker-client/` - Story artifacts: `_bmad-output/implementation-artifacts/` ### References - Story source: `_bmad-output/planning-artifacts/epics.md` (Epic 1 / Story 1.12) - Architecture constraints: `_bmad-output/planning-artifacts/architecture.md` - UX patterns: `_bmad-output/planning-artifacts/ux-design-specification.md` ## Dev Agent Record ### Agent Model Used GPT-5 Codex ### Debug Log References - Story generated from epic source and architecture/UX planning artifacts. - 2026-05-07: Added failing backend repository/controller tests and frontend contract tests for municipality service contacts before implementation. - 2026-05-07: Verified targeted contact tests, full backend test suite, frontend unit tests, frontend lint, and frontend production build. ### Completion Notes List - Story context created and marked ready-for-dev. - Implemented extension-layer municipality service contact storage with primary/secondary designation, required name/contact-type validation, and soft delete behavior. - Added authenticated client-services REST endpoints for listing, adding, updating, and removing service contacts; add/update/delete write shared audit events with actor identity and timestamp. - Added frontend municipality contact contracts and a service-contact management modal on the municipality profile panel with primary/secondary labels, inline required-field validation, edit, and remove flows. - Code review patch: contact endpoints now require an existing parent profile and reject read/update/delete attempts through a different municipality profile route. - Validation evidence: `dotnet test Campaign_Tracker.Server.Tests\Campaign_Tracker.Server.Tests.csproj /p:UseAppHost=false /p:BaseOutputPath="..\_bmad-output\test-bin\"` passed 150 tests; `npm test` passed 42 tests; `npm run lint` passed; `npm run build` passed. - Review patch validation: `dotnet test Campaign_Tracker.Server.Tests\Campaign_Tracker.Server.Tests.csproj --filter "FullyQualifiedName~MunicipalityContact" /p:UseAppHost=false /p:BaseOutputPath="..\_bmad-output\test-bin\"` passed 14 tests. ### File List - `_bmad-output/implementation-artifacts/1-12-municipality-service-contacts.md` - `_bmad-output/implementation-artifacts/sprint-status.yaml` - `Campaign_Tracker.Server/Program.cs` - `Campaign_Tracker.Server/Controllers/MunicipalityContactsController.cs` - `Campaign_Tracker.Server/Municipalities/IMunicipalityContactRepository.cs` - `Campaign_Tracker.Server/Municipalities/InMemoryMunicipalityContactRepository.cs` - `Campaign_Tracker.Server/Municipalities/MunicipalityContact.cs` - `Campaign_Tracker.Server/Municipalities/MunicipalityContactSaveResult.cs` - `Campaign_Tracker.Server.Tests/MunicipalityContactControllerTests.cs` - `Campaign_Tracker.Server.Tests/MunicipalityContactRepositoryTests.cs` - `campaign-tracker-client/src/municipalities/MunicipalityProfilePanel.tsx` - `campaign-tracker-client/src/municipalities/municipalityContracts.ts` - `campaign-tracker-client/src/municipalities/municipalityContracts.test.ts` ### Change Log - 2026-05-07: Implemented municipality service contacts and moved story to review.