Files
baya-monorepo/archive/docs/flows/partner-center.md
T
2026-08-02 20:01:31 +03:30

12 KiB
Raw Blame History

Flow — partner-center

Last verified: 2026-08-02 against commit c841bde

Actor(s): partner-center owner / center admin («مدیر مرکز») · Status: mocked Client: mock · Server: partial Business source: product/business/13-tax-invoicing-and-legal.md Integration: docs/integration/domains/partner-center.md

What it does

A licensed nursing center («مرکز») sponsors nurses onto Balinyaar and — when it is the merchant of record — is the legal seller: it issues the customer invoice and receives settlement, with Balinyaar's cut booked as a commission against it. The portal is the center owner's own read-only window: who they sponsor, which bookings they legally cover, and what they are owed. It is a separate authz scope from /admin.

The core finding: there is no partner-facing controller. The server ships one admin controller (admin/partner-centers), one center-scoped aggregate (centers/{id}/dashboard), and an internal MoR resolver. The portal's five /centers/me* reads do not exist, so all six screens run on client fixtures.

Screens

All six sit inside PartnerLayout (5-tab MobileShell, no bell). Every page's access gate is useMyPartnerCenter() — which today resolves a mock center for any caller.

Step Route Component / notes
1 /fa/partner PartnerHomeScreen.tsx — MoR chip, onboarding banner (draft/pending/suspended), license block (permit · technical-director licence · Enamad · legal type), masked settlement IBAN (MoR only, :101-103)
2 /fa/partner/nurses Sponsored roster — name + verification StatusChip, unpaginated (bounded set)
3 /fa/partner/bookings Read-only sponsored-bookings list; URL-synced status filter over 7 wire codes; translated labels only
4 /fa/partner/bookings/[id] Scoped detail (REQ-064) — patient display name, date, StatusTimeline. No clinical content, no address, no money
5 /fa/partner/settlement MoR drives the whole view: non-MoR → settlement_not_mor state; MoR → per-booking commission invoices (PartnerSettlementRow), masked IBAN, PDF link, client-side CSV of the loaded page
6 /fa/partner/more Center identity + MoR chip, appearance/language, sign-out

layout.tsx:15 wraps the shell in a RoleGuard with no expected role — partner is deliberately not an AppRole; the guard only hardens /me hydration.

API

Chain traced: page → useMyPartnerCenter (hooks/useMyPartnerCenter.ts:14) → partnerCenterApi selector (apis/index.ts) → USE_PARTNER_MOCK = true (constants.ts:10) → mockApi.ts:276 centerById(MOCK_MY_CENTER_ID = 1). The real branch exists but points at routes that 404. Shapes live in partner-center.md — not restated here.

Call Endpoint Notes
Portal: my center / nurses / bookings / booking detail / settlement GET /api/v1/centers/me{,/nurses,/bookings,/bookings/{id},/settlement} Phantom — 5 routes. clientApi.ts:105-123. Probed live: GET /centers/me as owner 09120000030404 (no such route in swagger; only /centers/{id}/dashboard exists)
The one real portal read GET /api/v1/centers/{id}/dashboard Wired, and it works. Probed as 09120000030200: «مرکز پرستاری آرامش», isMerchantOfRecord true, settlementIbanMasked "••••7777", sponsoredNurseCount 1, sponsoredBookingCount 2, invoiceCount 2, one inline nurse (nurseProfileId 2). No client code calls it
Tenancy same Probed as customer 09120000010403 "This dashboard belongs to another center." (GetCenterDashboardQuery.Handler.cs:26-28) — keyed on partner_centers.admin_user_id, not a role
MoR resolver (HTTP) GET /api/v1/internal/bookings/{bookingId}/center Exists (InternalCentersController.cs:23) but is DynamicPermission. Probed as super_admin403 "Authorization Error" — unreachable over HTTP in the demo
MoR resolver (in-process) This is the path that actually runs. IssueInvoiceCommand.Handler.cs:54 calls ResolveCenterForBookingAsync, then sets IssuingEntityType (:63) and PartnerCenterId (:64)
Admin CRUD / verify / sponsor / set-active GET·POST·PATCH /api/v1/admin/partner-centers… 7 wired routes, all DynamicPermission + sensitive → 403 for every seeded admin (see testing-setup.md). Client half is behind the same mock flag
Admin roster read GET /api/v1/admin/partner-centers/{id}/nurses Phantom (clientApi.ts:101) — the server folds nurses into the dashboard aggregate instead

The resolver, traced: booking → NurseId → NurseProfile.PartnerCenterId → PartnerCenter.IsMerchantOfRecord (PartnerCenterRepository.cs:77-106). A non-MoR sponsor does not change the issuer — it falls through to platform.

Proof it works on live data: GET /api/v1/invoices/6 (booking 6, nurse علی کریمی, the sponsored nurse) as customer 09120000011"issuingEntityType": "partner_center", gross 3,200,000, commission 480,000, VAT 48,000, total 528,000. The same call for booking 1 (unsponsored nurse) → "platform". The MoR chain is real end-to-end on the server; only the portal that should surface it is fake.

Rules that must hold

Rule Value / invariant Source
Merchant of record isMerchantOfRecord = true → the center is the taxable seller and invoice issuer; false → sponsor only, platform issues business/13 §Decisions
Why the vehicle exists Home nursing is a licensed activity (MoH پروانه تأسیس + مسئول فنی). Partnering with a licensed center is what makes online payment and BNPL legal pre-permit business/13
VAT 10% on the commission only, additive (480,000 × 0.10 = 48,000, confirmed on invoice 6) — never on the nurse's fee business/13
Commission rate Per-center override of the platform default; seeded center = 0.05. Snapshotted at compute time integration/partner-center.md
Settlement IBAN Write-then-masked — the full value never comes back on any read (Mask.IbanTail, repository :142) integration/partner-center.md
Technical director technicalDirectorNurseUserId links to a real verified nurse, not free text — regulation requires a named مدیر فنی integration/partner-center.md
set-active Suspend/activate, not delete — sponsored nurses and past bookings stay resolvable integration/partner-center.md
Portal scope Center admin sees no clinical content, no address, no customer money — only patient display name, date, status partner/bookings/[id]/page.tsx
onboardingState Client-derived from isActive + verifiedAt (deriveCenterState) — the wire never carries the string integration/partner-center.md

How to test

  1. Log in as 09120000030 (بهنام رستگار, the partner-center owner) — see testing-setup.md.
  2. Expect: you land on the customer home. /me returns roles: ["customer"] only — there is no partner signal (REQ-038), and services/auth/routing.ts:60 returns null for /partner, so even ?next=/fa/partner is discarded. Nothing anywhere links to the portal.
  3. Type /fa/partner in the address bar (locale prefix required).
  4. Expect (this is the tell): the header reads «مرکز پرستاری آسان‌گستر», permit MOH-12345, Enamad EN-999, IBAN ••••0001 — the mock fixture (mockApi.ts:46-59), not the seeded «مرکز پرستاری آرامش» / MOH-C-1001 / ENAMAD-DEMO-771 / ••••7777. PASS for the flow = you see the mock center. Seeing the seeded one would mean the flag flipped.
  5. /fa/partner/nurses → three fixture nurses (زهرا موسوی، مریم رضایی، سارا کاظمی). The seeded reality is one sponsored nurse, علی کریمی (09120000002).
  6. /fa/partner/bookings → three rows with ids 5001·5002·5003; open one. Expect: a detail screen that resolves — those ids exist in no database. The seeded sponsored bookings are 6 and 5, both completed.
  7. /fa/partner/settlement → two commission invoices with a fake مودیان reference and a stub PDF. The real center's invoices (invoiceCount 2) are unreachable from here.
  8. Server-side truth check (no UI path exists): with the owner's token, GET /api/v1/centers/1/dashboard → 200 with the seeded center. With a customer's token → 403.
  9. MoR check: GET /api/v1/invoices/6 as customer 09120000011issuingEntityType: "partner_center"; GET /api/v1/invoices/1 as 09120000010"platform". This is the only place the MoR rule is observable today.

Seeded-world caveat: the owner must be told center id 1 out of band — nothing in the API tells a caller which center they administer. Any admin-side action (create / verify / suspend / sponsor) is 403 for both demo admin accounts, so the center cannot be administered at all in this build.

Known gaps

  • USE_PARTNER_MOCK = true (partnerCenter/constants.ts:10) — all 6 portal screens render fixtures; the demo shows a center, nurses, bookings and invoices that exist in no database.
  • 5 portal routes are phantom (GET /centers/me, /me/nurses, /me/bookings, /me/bookings/{id}, /me/settlement, clientApi.ts:105-123) — probed 404. REQ-032 / REQ-033 / REQ-064.
  • MOCK_MY_CENTER_ID = 1 (constants.ts:13, used at mockApi.ts:276) resolves "my center" for any signed-in caller — the portal has zero tenancy today, and layout.tsx:15 passes no expected role.
  • No partner signal in /me (REQ-038): /fa/partner is an orphan route reachable only by typing the URL, and services/auth/routing.ts:60 makes it an invalid ?next= target.
  • Shape mismatch: the server serves one aggregate (centers/{id}/dashboard, nurses inline and capped by DashboardNurseCap); the portal needs /me + paginated splits. It cannot page an inline array.
  • GET /centers/{id}/dashboard requires a center id the portal cannot discover, and no client code calls it — the one working portal endpoint is unused.
  • The HTTP MoR resolver (internal/bookings/{id}/center) is DynamicPermission and 403s for both seeded admins — reachable only in-process from IssueInvoice.
  • All 7 admin/partner-centers routes 403 for the seeded super_admin/finance accounts, and the admin console (/fa/admin/partners) is behind the same mock flag — there is no working path to create, verify, suspend or sponsor.
  • A center owner cannot read the invoices it issues: GetInvoiceQuery.Handler.cs:25-27 authorizes the admin role or the booking's customer only. The settlement view has no legal read even per booking.
  • Two MoR resolvers can disagree: BNPL reads the bnpl_merchant_of_record config (platform) while invoicing uses ResolveCenterForBookingAsync — a MoR center's BNPL order and its invoice would name different sellers.
  • Seeded invoices carry moadianStatus: "pending", moadianReferenceNumber: null, pdfUrl: null — the settlement view's PDF/CSV columns have nothing real to render.
  • Center self-onboarding is deferred (product/business/01-actors-and-onboarding.md:24); the write-then-masked IBAN flow has never been exercised on a real route.