6.5 KiB
6.5 KiB
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/TicketParticipantRolecode sets. Configs inPersistence/Configuration/MessagingConfig/. ITicketRepository(+TicketRepository) onIUnitOfWork.- Features
Application/Features/Messaging/:OpenTicket,AutoCreateCoordinationTicket,PostMessage,AddParticipant,RemoveParticipant,CloseTicket,ReopenTicket,LogEmergencyTicket,GetTicketThread(role-aware user/admin view),ListMyTickets,ListTicketsForAdmin. Shared helpersTicketReferenceCode(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 inApplicationDbContext). Config inPersistence/Configuration/PartnerCentersConfig/(also adds thenurse_profiles.partner_center_idFK in place).IPartnerCenterRepository(+ impl). - Features:
CreatePartnerCenter,UpdatePartnerCenter,VerifyPartnerCenter,SponsorNurse,GetCenterForBooking(the merchant-of-record resolver),ListPartnerCenters,GetPartnerCenterById,GetCenterDashboard. ControllersAdminPartnerCentersController,CentersController(portal),InternalCentersController(resolver).
Seam
ILicenseVerificationService(Application/Contracts/Common) +MockLicenseVerificationService(CrossCutting/Seams/, registered inAddCrossCuttingSeams, configSeams:LicenseVerification:AutoApprove).
Cross-phase wiring
- b11
IssueInvoiceCommandnow setsinvoices.issuing_entity_type+partner_center_idfromResolveCenterForBookingAsync(the single merchant-of-record resolver). - b11
CreateRefundCommandauto-opens acategory=refundticket viaOpenTicketCommandwhen the caller passes none, sorefunds.ticket_idis always non-null (replaces the old config-gated "ticket required" check). - The card
ConfirmPaymentAndPostLedgerand BNPLSettleBnplOrderhandlers dispatchAutoCreateCoordinationTicketCommandafter a booking is confirmed (idempotent, one per booking).
Reused, not rebuilt (admin backoffice consolidation)
- Support-alert worklist (
ISupportAlertServiceList/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)
- Open + message:
POST /api/v1/tickets(no links) → 200 with aTKT-…referenceCode+ opener as participant;POST /api/v1/tickets/{id}/messages→ the message appears in the thread. - Internal boundary (proven by a test): admin
POST …/messages {isInternal:true}→ 200; userGET /api/v1/tickets/{id}omits it; adminGET /api/v1/admin/tickets/{id}includes it; a non-staffisInternal:true→ 403. (MessagingApiTests.InternalNote_IsHiddenInUserView_ShownInAdminView+NonAdmin_CannotSetInternal.) - Participant uniqueness: add a user → 200; add again → 409 (not 500); delete → 200.
(
MessagingApiTests.AddParticipant_DuplicateIsConflict_NotServerError.) - Partner center + masked IBAN:
POST /api/v1/admin/partner-centers {isMerchantOfRecord:true, settlementIban}→ 200 withsettlementIbanMasked(last 4), never plaintext;GET …/{id}masks it too; created inactive. (PartnerCentersApiTests.CreateMerchantOfRecord_MasksSettlementIban,Verify_ActivatesTheCenter.) - Merchant-of-record resolution:
GET /api/v1/internal/bookings/{id}/center→partner_center(+ id) for a nurse sponsored by a merchant-of-record center,platformotherwise. (CenterForBookingTests, 4 cases.) - Refund anchors a ticket:
CreateRefundyields a non-nullrefunds.ticket_id(foundation refund tests pass with the auto-open wired viaTestSenders.WithTicketHooks()). - Admin worklists / RBAC: support alerts + audit reachable under admin scope; a non-admin token on an admin
route → 403 (
PartnerCentersApiTests.NonAdmin_IsForbidden), unauthenticated → 401. - Audit: admin state changes (e.g.
VerifyPartnerCenter) append anaudit_logsrow (PartnerCenterisIAuditable;settlement_ibanis 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 inreports/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.jsonrefreshed (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). MigrationMessagingAndPartnerCentersscaffolds cleanly.
Decisions / notes for the future
- Merchant-of-record =
partner_centerissuer only when the sponsoring centeris_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_atstamp (not a hard delete / notdeleted_at), so theUNIQUE(ticket_id, user_id)row survives and a re-add resurrects it. - SQLite gotcha (again): messages are ordered by the monotonic
Id(== send order), neverORDER 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_idcolumns exist without a DB FK (onlynurse_profilesgot the FK, per the DoD).