# 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).
