# Backend Phase 15 report — Messaging (tickets), partner centers & admin backoffice **The final backend phase.** It closes the operational loop: the ticket system, the licensed partner centers (merchant-of-record), and the consolidated admin backoffice. The backend chain is now complete. ## What was built ### Messaging (tickets) — new `messaging` schema - Entities `Domain/Entities/Messaging/`: `Ticket`, `TicketParticipant`, `TicketMessage` + `TicketStatus` / `TicketCategory` / `TicketParticipantRole` code sets. Configs in `Persistence/Configuration/MessagingConfig/`. - `ITicketRepository` (+ `TicketRepository`) on `IUnitOfWork`. - Features `Application/Features/Messaging/`: `OpenTicket`, `AutoCreateCoordinationTicket`, `PostMessage`, `AddParticipant`, `RemoveParticipant`, `CloseTicket`, `ReopenTicket`, `LogEmergencyTicket`, `GetTicketThread` (role-aware user/admin view), `ListMyTickets`, `ListTicketsForAdmin`. Shared helpers `TicketReferenceCode` (collision-checked mint) + `TicketRoleResolver` + `StaffRoles` (Application/Common). - Controllers `TicketsController` (authenticated) + `AdminTicketsController` (`support`/`admin`). ### Partner centers — new `partner` schema - Entity `Domain/Entities/PartnerCenters/PartnerCenter` (`IAuditable`; `settlement_iban` `[AuditRedacted]` + encrypted converter in `ApplicationDbContext`). Config in `Persistence/Configuration/PartnerCentersConfig/` (also adds the `nurse_profiles.partner_center_id` FK in place). `IPartnerCenterRepository` (+ impl). - Features: `CreatePartnerCenter`, `UpdatePartnerCenter`, `VerifyPartnerCenter`, `SponsorNurse`, `GetCenterForBooking` (the merchant-of-record resolver), `ListPartnerCenters`, `GetPartnerCenterById`, `GetCenterDashboard`. Controllers `AdminPartnerCentersController`, `CentersController` (portal), `InternalCentersController` (resolver). ### Seam - **`ILicenseVerificationService`** (`Application/Contracts/Common`) + `MockLicenseVerificationService` (`CrossCutting/Seams/`, registered in `AddCrossCuttingSeams`, config `Seams:LicenseVerification:AutoApprove`). ### Cross-phase wiring - b11 `IssueInvoiceCommand` now sets `invoices.issuing_entity_type` + `partner_center_id` from `ResolveCenterForBookingAsync` (the single merchant-of-record resolver). - b11 `CreateRefundCommand` auto-opens a `category=refund` ticket via `OpenTicketCommand` when the caller passes none, so `refunds.ticket_id` is always non-null (replaces the old config-gated "ticket required" check). - The card `ConfirmPaymentAndPostLedger` and BNPL `SettleBnplOrder` handlers dispatch `AutoCreateCoordinationTicketCommand` after a booking is confirmed (idempotent, one per booking). ### Reused, not rebuilt (admin backoffice consolidation) - Support-alert worklist (`ISupportAlertService` List/Assign/Resolve — `SupportAlertsController`) and the audit viewer (`GetAuditTrail` — `AuditController`) already existed since b1; verified as the backoffice surface. Verification/refund/payout/moderation queues are their own phases' endpoints. ## What is now testable and exactly how (the §7 steps) 1. **Open + message:** `POST /api/v1/tickets` (no links) → 200 with a `TKT-…` `referenceCode` + opener as participant; `POST /api/v1/tickets/{id}/messages` → the message appears in the thread. 2. **Internal boundary (proven by a test):** admin `POST …/messages {isInternal:true}` → 200; user `GET /api/v1/tickets/{id}` omits it; admin `GET /api/v1/admin/tickets/{id}` includes it; a non-staff `isInternal:true` → 403. (`MessagingApiTests.InternalNote_IsHiddenInUserView_ShownInAdminView` + `NonAdmin_CannotSetInternal`.) 3. **Participant uniqueness:** add a user → 200; add again → **409** (not 500); delete → 200. (`MessagingApiTests.AddParticipant_DuplicateIsConflict_NotServerError`.) 4. **Partner center + masked IBAN:** `POST /api/v1/admin/partner-centers {isMerchantOfRecord:true, settlementIban}` → 200 with `settlementIbanMasked` (last 4), never plaintext; `GET …/{id}` masks it too; created inactive. (`PartnerCentersApiTests.CreateMerchantOfRecord_MasksSettlementIban`, `Verify_ActivatesTheCenter`.) 5. **Merchant-of-record resolution:** `GET /api/v1/internal/bookings/{id}/center` → `partner_center` (+ id) for a nurse sponsored by a merchant-of-record center, `platform` otherwise. (`CenterForBookingTests`, 4 cases.) 6. **Refund anchors a ticket:** `CreateRefund` yields a non-null `refunds.ticket_id` (foundation refund tests pass with the auto-open wired via `TestSenders.WithTicketHooks()`). 7. **Admin worklists / RBAC:** support alerts + audit reachable under admin scope; a non-admin token on an admin route → 403 (`PartnerCentersApiTests.NonAdmin_IsForbidden`), unauthenticated → 401. 8. **Audit:** admin state changes (e.g. `VerifyPartnerCenter`) append an `audit_logs` row (`PartnerCenter` is `IAuditable`; `settlement_iban` is redacted in the diff). ## What is mocked / waiting on a real service - `ILicenseVerificationService` → manual-approve at MVP (no public eNamad/MoH B2B API). Make-it-real steps in `reports/mocks-registry.md` (🟡). No telephony seam — the emergency call is out-of-platform by design. ## Contracts produced - `dev/contracts/domains/messaging-notifications-admin.md`; `dev/contracts/openapi/swagger.v1.json` refreshed (now includes tickets, partner centers, the center resolver). ## Gate - `dotnet build Baya.sln` — 0 new code warnings. `dotnet test Baya.sln` — green: 4 identity + 240 foundation + 114 API (12 new tests this phase). Migration `MessagingAndPartnerCenters` scaffolds cleanly. ## Decisions / notes for the future - **Merchant-of-record** = `partner_center` issuer only when the sponsoring center `is_merchant_of_record`; a non-MoR sponsor leaves the platform as issuer (so a sponsored-but-platform-billed nurse is representable). - **Participant removal** is a soft `removed_at` stamp (not a hard delete / not `deleted_at`), so the `UNIQUE(ticket_id, user_id)` row survives and a re-add resurrects it. - **SQLite gotcha (again):** messages are ordered by the monotonic `Id` (== send order), never `ORDER BY sent_at` (`DateTimeOffset`), which the SQLite test provider can't translate. - **Follow-ups:** the invoice-issuer wire sets the columns but the downstream settlement rail (paying a center's IBAN when it is MoR) is not exercised end-to-end here; the center dashboard caps the sponsored-nurse list at 50 (count is exact) — paginate it if a center grows large. Bookings/invoices `partner_center_id` columns exist without a DB FK (only `nurse_profiles` got the FK, per the DoD).