# 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](../../product/business/13-tax-invoicing-and-legal.md) **Integration:** [docs/integration/domains/partner-center.md](../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`](../../client/src/app/%5Blocale%5D/(private-routes)/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](../../client/src/services/partnerCenter/hooks/useMyPartnerCenter.ts)) → `partnerCenterApi` selector (`apis/index.ts`) → **`USE_PARTNER_MOCK = true`** ([constants.ts:10](../../client/src/services/partnerCenter/constants.ts)) → `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](../integration/domains/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 `09120000030` → **404** (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 `09120000030` → **200**: `«مرکز پرستاری آرامش»`, `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 `09120000010` → **403 "This dashboard belongs to another center."** ([GetCenterDashboardQuery.Handler.cs:26-28](../../server/src/Core/Baya.Application/Features/PartnerCenters/Queries/GetCenterDashboard/GetCenterDashboardQuery.Handler.cs)) — keyed on `partner_centers.admin_user_id`, **not** a role | | MoR resolver (HTTP) | `GET /api/v1/internal/bookings/{bookingId}/center` | Exists ([InternalCentersController.cs:23](../../server/src/API/Baya.Web.Api/Controllers/V1/InternalCentersController.cs)) but is `DynamicPermission`. Probed as `super_admin` → **403 "Authorization Error"** — unreachable over HTTP in the demo | | MoR resolver (in-process) | — | **This is the path that actually runs.** [`IssueInvoiceCommand.Handler.cs:54`](../../server/src/Core/Baya.Application/Features/Invoices/Commands/IssueInvoice/IssueInvoiceCommand.Handler.cs) 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](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](../../server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PartnerCenterRepository.cs)). 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](../../product/business/13-tax-invoicing-and-legal.md) | | 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](../../product/business/13-tax-invoicing-and-legal.md) | | 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](../../product/business/13-tax-invoicing-and-legal.md) | | Commission rate | Per-center override of the platform default; seeded center = `0.05`. Snapshotted at compute time | [integration/partner-center.md](../integration/domains/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](../integration/domains/partner-center.md) | | Technical director | `technicalDirectorNurseUserId` links to a real verified nurse, not free text — regulation requires a named مدیر فنی | [integration/partner-center.md](../integration/domains/partner-center.md) | | `set-active` | Suspend/activate, **not** delete — sponsored nurses and past bookings stay resolvable | [integration/partner-center.md](../integration/domains/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](../integration/domains/partner-center.md) | ## How to test 1. Log in as **09120000030** (بهنام رستگار, the partner-center owner) — see [testing-setup.md](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 **09120000011** → `issuingEntityType: "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.