backend phase 15 & frontend phase 8
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user