# partner-center — nursing companies

> Client seam `client/src/services/partnerCenter/` · `USE_PARTNER_MOCK = true` (**mock is primary**) · 9 server ops
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).

Nursing companies («مراکز») that sponsor nurses onto the platform. The domain with the **widest gap between
what the client wants and what the server serves** — 6 of its client calls are phantom.

## Endpoints

| Method | Path | Verdict |
| --- | --- | --- |
| GET | `/api/v1/admin/partner-centers` | wired · paginated |
| POST | `/api/v1/admin/partner-centers` | wired |
| GET | `/api/v1/admin/partner-centers/{id}` | wired |
| PATCH | `/api/v1/admin/partner-centers/{id}` | wired |
| POST | `/api/v1/admin/partner-centers/{id}/set-active` | wired — REQ-032's delivered half |
| POST | `/api/v1/admin/partner-centers/{id}/sponsor-nurse` | wired |
| POST | `/api/v1/admin/partner-centers/{id}/verify` | wired |
| GET | `/api/v1/centers/{id}/dashboard` | **unwired** — the client wants `centers/me/*` splits instead |
| GET | `/api/v1/internal/bookings/{bookingId}/center` | **unwired** — the MoR resolver, server-internal |

The seven `admin/partner-centers` routes are `DynamicPermission` + `sensitive`; `centers` is `[Authorize]`;
`internal/bookings` is `DynamicPermission`.

This domain owns **three of the four route-shape exceptions** in the API: hyphens (`partner-centers`,
`set-active`, `sponsor-nurse`), a nested `admin/` segment, and an `internal/` namespace. It also has one of
only two `PATCH` verbs. See [index.md](index.md#route-shape-exceptions).

### Phantom — 6

| Client call | REQ | Note |
| --- | --- | --- |
| `GET /api/v1/centers/me` | REQ-032 | The portal's own-center read |
| `GET /api/v1/centers/me/nurses` | REQ-032 | Split read — the server serves one aggregate instead |
| `GET /api/v1/centers/me/bookings` | REQ-032 | Split read |
| `GET /api/v1/centers/me/bookings/{id}` | REQ-064 | Partner-scoped booking detail |
| `GET /api/v1/centers/me/settlement` | REQ-033 | Per-booking commission invoices |
| `GET /api/v1/admin/partner-centers/{id}/nurses` | REQ-032 | The admin-side sponsored-nurse list |

**The shape mismatch is the point.** The server serves **one aggregate**, `GET centers/{id}/dashboard` →
`CenterDashboardDto` with `sponsoredNurses` inline. The portal wants **`/me` plus paginated splits** — it
cannot page an inline array, and it does not know its own center id without REQ-038. Until REQ-032 lands,
the portal is mock-only.

## Merchant of record

The one business rule that changes where money goes:

- **`isMerchantOfRecord = true`** → the *center* is the seller. It invoices the customer, holds the
  commercial relationship, and Balinyaar's cut is a commission **against the center**.
- **`isMerchantOfRecord = false`** → the nurse is the seller and the center is a sponsor only.

`GET internal/bookings/{bookingId}/center` is the **MoR resolver** the invoice pipeline calls to decide
which entity issues the invoice — which is why `InvoiceDto.issuingEntityType` exists. See
[payment.md](payment.md). `commissionRate` on the center is a **per-center override** of the platform
default, snapshotted at compute time like every other rate.

## Shape rules the JSON does not express

- **The settlement IBAN is write-then-masked.** `settlementIbanMasked` is the only form on every read model;
  the full value never comes back after the write that set it. Same pattern as
  [nurse.md](nurse.md).
- **`verify` is a licence check behind a seam.** `ILicenseVerificationService` checks the eNamad code and the
  MoH establishment permit; by default it returns `NeedsManualReview`, so `verify` records a **human admin
  decision**. `Seams:LicenseVerification:AutoApprove` makes the mock return `Valid` to test the
  auto-approve path. A real eNamad/MoH registry adapter ignores the knob.
- **`technicalDirectorNurseUserId` links to a real verified nurse**, not a free-text name — Iranian
  regulation requires a named technical director («مدیر فنی») with a valid licence.
- **`set-active` is suspend/activate, not delete.** A suspended center's sponsored nurses and past bookings
  stay resolvable.
- `sponsoredNurseCount` is denormalised onto both the list item and the detail so the queue needs no
  per-row count.
- `legalEntityType` and `mohEstablishmentPermitNo` are the regulatory identity; `enamadCode` is the
  e-commerce trust seal. All three are distinct and none substitutes for another.

## Enums

| Vocabulary | Values |
| --- | --- |
| `CenterOnboardingState` | `draft` `pending_verification` `verified` `suspended` |
| `MoadianStatus` *(on the center's invoices)* | `pending` `submitted` `registered` `failed` |

`MoadianStatus` is shared with [payment.md](payment.md) — it is the سامانه مودیان submission state, and it
is the same vocabulary on both sides.

> `CenterOnboardingState` is the client's model of the center's position in onboarding. On the wire the
> server carries the **facts** it is derived from — `isActive` and `verifiedAt` on both
> `PartnerCenterListItemDto` and `PartnerCenterDetailDto` — not the state string itself. Derive, do not
> expect a field.

## Open REQs

| REQ | Status | Effect |
| --- | --- | --- |
| REQ-032 | partially delivered | `set-active` landed. The `/me` split reads and the IBAN write-then-masked flow are deferred → 5 phantom routes. **The main reason this seam is mocked** |
| REQ-033 | partially delivered | `totalIrr` is on `InvoiceDto`; the per-booking commission invoice list is deferred → 1 phantom |
| REQ-064 | open | No partner-scoped booking detail → 1 phantom |
| REQ-038 | open | `/me` carries no signal that the caller administers a center, so the portal cannot auto-route or discover its own center id. See [auth.md](auth.md) |
