cleanup phases 6

This commit is contained in:
hamid
2026-08-02 18:48:32 +03:30
parent e2db97392a
commit 51e86a1e5f
239 changed files with 118 additions and 70 deletions
@@ -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).