# Frontend → Backend requests (append-only) The frontend lane appends here when it needs a contract that doesn't exist yet, finds a shape mismatch, or needs a new field/filter/endpoint. The backend agent reads this at the start of each phase and delivers fixes in its own change. **Frontend never edits backend code to "fix" a gap — it requests it.** ## REQ-001 — Confirm response envelope, wire casing & pagination shape — filed by frontend-phase-0 — 2026-07-02 - **Need:** Authoritative confirmation of three things the frontend types depend on: 1. **Envelope unwrapping.** The b0 swagger shows every response wrapped in `ApiResult` (`{ isSuccess, statusCode, message, requestId, data }`). The frontend's `clientFetch` currently returns the **raw body**, so domain `clientApi`s read the payload via `unwrap()` (`data`). Confirm this is the intended shape for all endpoints (i.e. payload always under `data`), so the pattern is correct before f1+ copy it. 2. **Wire casing.** Observed swagger properties are **camelCase** (`isSuccess`, `serverTimeUtc`) — not the snake_case `api-conventions.md` implies for URL segments. Please confirm JSON body casing is camelCase (and, if so, we can note it in the convention doc), or state where it differs. 3. **Pagination payload.** `api-conventions.md` says lists return `items` + `total` (+ `page`/`page_size`). Confirm the exact field names/casing on the wire (we've typed `Paginated` as `{ items, total, page, pageSize }` in `client/src/lib/api/types.ts`). - **Why:** These fix the shared `ApiEnvelope`/`Paginated` types and the `services/{domain}` reference pattern every later frontend phase inherits. - **Proposed shape:** `{ isSuccess: boolean, statusCode: number, message?: string, requestId?: string, data?: T }` and `data: { items: T[], total: number, page: number, pageSize: number }` for lists. - **Status:** open ## REQ-002 — OTP length + expiry in RequestOtpResult — filed by frontend-phase-1-b2 — 2026-07-02 - **Need:** Add `codeLength` (int) and `expiresInSeconds` (int) to `RequestOtpResult`. - **Why:** The A2/B2 OTP screen renders one box per digit and (later) a code-expiry hint. `RequestOtpResult` currently exposes only `otpSent` + `resendAvailableInSeconds`, so the frontend hardcodes the box count (`OTP_CODE_LENGTH = 6`, inferred from the live 6-digit verify example, not the 4-box wireframe). Surfacing the length makes the box count contract-driven; the expiry lets us show "code expires in …". - **Proposed shape:** `{ otpSent: boolean, resendAvailableInSeconds: number, codeLength: number, expiresInSeconds: number }` - **Status:** open ## REQ-003 — Machine-readable error codes for verify_otp failures — filed by frontend-phase-1-b2 — 2026-07-02 - **Need:** A stable `code` on the 400 envelope for verify_otp that distinguishes wrong code vs expired code vs max-attempts lockout (e.g. `otp_invalid` | `otp_expired` | `otp_locked`), and — for lockout — a `retryAfterSeconds` (or `lockedUntil`) field. - **Why:** The OTP screen has explicit **wrong-code**, **expired-code**, and **max-attempts-lockout** states (per the phase spec), but the contract returns the same safe 400 message for all of them, so the frontend can't reliably tell them apart. Today it maps a lockout only when it sees the mock's `otp_locked` code and otherwise degrades to a generic "incorrect or expired" message. A stable machine code (kept generic enough to avoid account enumeration) would let the UI render the precise state + the unlock countdown. - **Proposed shape:** `{ isSuccess: false, statusCode: 400, message: "…", code: "otp_locked", data: { retryAfterSeconds: 60 } }` - **Status:** open ## REQ-005 — Patient `relation` + `conditions` fields — filed by frontend-phase-2-b3 — 2026-07-02 - **Need:** Add two fields to `PatientDto` and the `patients/create` + `patients/update` bodies: 1. `relation` (`parent`|`spouse`|`child`|`self`, nullable) — the care-recipient's relation to the payer. 2. `conditions` (string[] of stable codes, e.g. `elderly`/`post_surgery`/`diabetes`/`mobility`/`dementia`). - **Why:** The A3 onboarding step captures the relation, and the A4 form + E1 patient cards show condition chips. Neither field exists on the wire `PatientDto` (only `initialMedicalNotes` free-text). The client currently augments them behind the `services/patients` seam (the mock persists them; `USE_PATIENTS_MOCK=true`) and drops them on the real path. Adding the columns lets the client flip the flag to the live endpoints. - **Proposed shape:** `PatientDto { …, relation: string|null, conditions: string[] }`; same fields accepted on create/update. Enum for `relation`; `conditions` a stable code list (could also be a normalized child table). - **Status:** open ## REQ-006 — Avatar / object-storage upload route (nurse & customer) — filed by frontend-phase-2-b3 — 2026-07-02 - **Need:** A multipart image-upload endpoint backed by `IObjectStorage` that returns a stored URL, plus an `avatarUrl` field on `NurseProfileDto` (and later `CustomerProfileDto`). e.g. `POST api/v1/nurse_profiles/avatar` (multipart/form-data) → `{ url }`, and persist `avatar_url` on the profile. - **Why:** The B7 nurse profile bootstrap and the customer profile both take a profile photo. The b3 contract has no avatar field or upload route, and the client fetch layer is JSON-only (can't send multipart). The client mocks this behind the `services/profiles` seam (`uploadAvatar` returns an object URL). The real `profilesClientApi.uploadAvatar` throws `501` until this lands. - **Status:** open ## REQ-007 — Customer name + preferred-language update — filed by frontend-phase-2-b3 — 2026-07-02 - **Need:** Either add `firstName`/`lastName`/`preferredLanguage` to the `customer_profiles/upsert` body + `CustomerProfileDto`, or confirm the customer name is only ever set elsewhere (and how). `MeResult` exposes `firstName`/`lastName` read-only with no update endpoint; `CustomerProfileDto` carries only the emergency contact. - **Why:** The customer profile screen edits first/last name + preferred language alongside the emergency contact. Absent a wire field/endpoint, the client augments name/language behind the `services/profiles` seam (mock-persisted; the real upsert sends only the emergency contact). Confirm the intended home for these so the client stops augmenting. - **Status:** open ## REQ-004 — Confirm multi-role disambiguation (activeRole?) — filed by frontend-phase-1-b2 — 2026-07-02 - **Need:** Confirm whether `MeResult` will gain an `activeRole` (the user's currently-selected actor) for a user who holds **both** `customer` and `nurse`, or whether the client should keep owning that choice. - **Why:** The role router must pick one app for a dual-role user. Absent an `activeRole` in the contract, it currently uses the **intended role** carried from the login switch (A1 vs B1), defaulting to the family app. If the backend intends to persist a "current role", the router should prefer it. Also note: verify_otp returns `roles` but no user `id` (only `/me` has it) — fine for now (context id is hydrated from `/me`), flagging in case that changes. - **Status:** open ## REQ-008 — Accept the client-picked map pin on address create/update — filed by frontend-phase-3-b4 — 2026-07-02 - **Need:** Let `customer_addresses/create` and `customer_addresses/update/{id}` accept optional `latitude`/`longitude` (decimals) from the request body — the coordinates the user dropped with the map-pin picker — and persist those when provided, only falling back to the `IGeocoder` when the client sends none. - **Why:** The b4 contract's create body geocodes server-side from `addressLine`+city and does **not** accept client coordinates, but f3 requires the user to **drop a pin** on the map (a hard client-side validation) so the stored coordinate is the user's exact door location for the later EVV distance check (b9) — a geocoded street centroid is coarser. The client already sends `latitude`/`longitude` in the create/update body and echoes them locally; until the server accepts them, the real path silently ignores them and geocodes instead. - **Proposed shape:** create/update body gains `latitude?: number, longitude?: number`; when both present, store them (and mark the geocode source as "user-pin"); when absent, geocode as today. `CustomerAddressDto` already returns `latitude`/`longitude`. - **Status:** open ## REQ-009 — Add `provinceId` to `CustomerAddressDto` — filed by frontend-phase-3-b4 — 2026-07-02 - **Need:** Add `provinceId` (long) to `CustomerAddressDto` (the province that owns the address's `cityId`). - **Why:** The address book's **edit** form prefills the cascading province → city → district dropdowns from a saved address, and the city list is fetched **per province** (`geo/cities?province_id=`). The DTO carries `cityId` but not its province, so the client can't drive the city query to preselect the city without the province id. The client currently augments `provinceId` behind the `services/addresses` seam (the mock persists it; the real client echoes the just-saved choice), so editing an address that was **loaded fresh from the server** can't prefill the province until this lands. `cityId` still implies the province server-side — this is purely to prefill the client cascade. - **Proposed shape:** `CustomerAddressDto { …, provinceId: long }` (join from `cities.province_id`). - **Status:** open ## REQ-010 — Confirm/align the list pagination query-param name (catalog + all lists) — filed by frontend-phase-4-b5 — 2026-07-05 - **Need:** Confirm the exact query-param name the paginated list endpoints bind for page size. The `catalog.md` route examples write `?page=&page_size=` (snake_case), but the **working** b4 `serviceAreas` client binds `pageSize` (camelCase, case-insensitive to the server's `PageSize` property) — the f3 report flagged this as the `page_size`→`pageSize` gotcha. The f4 catalog client follows the proven `pageSize` for `catalog/categories` and `nurse_variants/list`; the domain filter `category_id` stays snake_case per the doc. - **Why:** So the real-endpoint swap (f6 flips `USE_CATALOG_MOCK=false`) doesn't silently paginate wrong. If the server truly binds `pageSize`, please update the `page_size` occurrences in the contract docs to match; if it binds `page_size`, tell us and we'll switch the client (one line per list call). - **Proposed shape:** list query = `?page={1-based}&pageSize={≤100}`; response `data` = `{ items, total, page, pageSize }`. - **Status:** open ## REQ-011 — Nurse-facing endpoint for structured professional-credential details — filed by frontend-phase-5-b6 — 2026-07-09 - **Need:** A nurse-facing command to submit the **structured** credential fields B5 collects alongside the document uploads: `inoNumber` (شماره نظام پرستاری), `specialties` (string[] — stable codes + free-text), and optionally `licenseNumber`, `issuingAuthority`, `holderName`, `issuedAt`, `expiresAt`. Proposed: `POST api/v1/nurse_verification/credential_details` (or fold into the manual-step `documents` confirm body). - **Why:** The b6 contract records these structured fields **only on the admin `decide`** — there is no nurse-facing endpoint to capture them. B5 needs them at submission time (the INO number is the nurse's primary registry key, specialties feed f6 search facets). Today the client collects them and the mock persists them; the real `verificationClientApi.submitCredentialDetails` **no-ops** (the document uploads it accompanies ARE contract-backed via `upload_url` → PUT → `documents`). Without this, a swap to the real backend silently drops the INO number + specialties until an admin re-enters them. - **Proposed shape:** `POST api/v1/nurse_verification/credential_details` body `{ inoNumber, specialties: string[], licenseNumber?, issuingAuthority?, holderName?, issuedAt?, expiresAt? }` → `VerificationStatusDto`. Alternatively, extend the manual-step `documents` confirm body with these fields. - **Also (minor):** the contract's `VerificationStepDto` has no `isRequired` — the client treats **every** seeded step as required (the "X از Y" meter Y = `steps.length`). Confirm that holds, or add `isRequired`. - **Status:** open ## REQ-012 — Search result + nurse-profile enrichment for discovery (C2/C3) — filed by frontend-phase-6-b7 — 2026-07-09 - **Need:** Two extra read surfaces the discovery UI renders but b7/b6/b5 don't yet expose: 1. **On the `search/nurses` result row** (`NurseSearchResultDto`): the nurse's **display name** and **avatar URL** (the C2 card's identity) and a **distance** value (km from the searched area) — b7's index row today carries only ids + price/rating/gender/geo ids. Without the name/avatar the card falls back to a generic label + initials; distance is simply hidden. 2. **An aggregated public nurse-profile endpoint** for C3 — proposed `GET api/v1/nurses/{id}/profile` → `{ nurseId, nurseName, avatarUrl, bio, yearsExperience, averageRating, totalReviews, totalCompletedBookings, isVerified, inoMembership, attributeChips: string[] (specialty codes), services: [{ variantId, displayName, priceIrr (string), priceUnit, sessionCount? }], latestReview?: { rating, body, authorMasked, createdAt } }`. Today only the b6 public **trust badge** (`nurses/{id}/trust_badge`, giving `isVerified` + `credentialTypes`) and the b5 single-variant read are public — there is no name/bio/specialties/**full services list**/latest-review aggregation. - **Why:** C2/C3 are the trust funnel — the family chooses a real, named, priced nurse here. The `services/search` domain is **mock-primary** (`USE_SEARCH_MOCK = true`) precisely because these fields aren't available; the real `searchClientApi` maps everything b7/b6 do provide and leaves the above blank. When both land, the swap is a single config flip (no hook/component change). - **Proposed shape:** enrich `NurseSearchResultDto` with `{ nurseName, avatarUrl, distanceKm? }`; add `GET api/v1/nurses/{id}/profile` returning the object above. `price`/`priceIrr` stay IRR digit-strings. - **Status:** open ## REQ-013 — Variant price on `BookingRequestDto` — filed by frontend-phase-7-b8 — 2026-07-09 - **Need:** Add the variant's **price** (IRR digit-string) to `BookingRequestDto` (and ideally the nurse's **avatar URL**). The b8 DTO carries `variantLabel` + `variantPriceUnit` but no price, so the request summary card (C5 + the nurse detail + later f8 booking detail) can't show the priced rate from the DTO. - **Why:** The C5 awaiting screen and the nurse request detail render the shared `BookingRequestSummaryCard`, which prices the service via the f0 money util + i18n unit label. Without a price on the DTO the summary hides the amount on the real path. The client augments `variantPrice` behind the `services/bookingRequests` seam (the mock supplies it from the chosen variant; the real client leaves it `null`); adding the field lets the summary price the service once the domain flips to the real endpoint. - **Proposed shape:** `BookingRequestDto { …, variantPrice: string (IRR digits), nurseAvatarUrl?: string }`. (Money-free rule intact — this is the *rate* of the chosen variant for display, not an engagement total.) - **Status:** open ## REQ-014 — Enrich the nurse-inbox list item (variant label + patient age) — filed by frontend-phase-7-b8 — 2026-07-09 - **Need:** Add `variantLabel` (and optionally the patient's **age/age-band**) to `BookingRequestListItemDto`. Today the nurse-inbox row carries `counterpartyName` (patient) + `customerNotes` + gender + times + deadline, but **not** which service was requested. - **Why:** The f7 nurse inbox card is specced to show the requested **service variant** and the patient's age alongside the gender chip + countdown. The list item omits both, so the card shows a notes preview and the nurse must open the detail (`get/{id}`, which *does* carry `variantLabel`) to see the service. Surfacing `variantLabel` on the row makes the inbox self-describing; a coarse age is a nice-to-have for triage. - **Proposed shape:** `BookingRequestListItemDto { …, variantLabel: string, patientAge?: int }`. - **Status:** open ## REQ-015 — Confirm the booking/session/EVV enum codes + `checkInAddressMatch` tri-state — filed by frontend-phase-8-b9 — 2026-07-10 - **Need:** Two confirmations so the f8 `services/bookings/types.ts` client unions stay wire-accurate: 1. **Enum string codes.** The b9 swagger types `status`/`evvStatus`/session `status` as bare `string` (no enum constraint). Confirm the stable wire codes match the client unions: `BookingStatus` = `pending_payment|confirmed|in_progress|completed|disputed|closed|cancelled`; `BookingSessionStatus` = `scheduled|in_progress|completed|missed|cancelled`; `VisitVerificationStatus` = `pending|checked_in|completed`. (They match the contract doc's "Enums used" — this just asks that the serialized JSON emits these exact snake_case codes, not PascalCase/int.) 2. **`checkInAddressMatch` tri-state semantics.** The EVV banner keys off it as: `true` = in range («موقعیت تایید شد»), `false` = out-of-range/advisory-under-review («موقعیت خارج از محدوده»), `null` = GPS unavailable/denied («موقعیت ثبت نشد»). Confirm the server returns `null` (not `false`) when the nurse checked in **without** coordinates (GPS denied), so the UI can distinguish "flagged mismatch" from "no position captured". A mismatch stays advisory server-side (support alert, never a block) — the UI mirrors that. - **Why:** f8 renders the status timeline, per-session chips, and the EVV banner strictly off these codes; a casing/int drift or a `false`-vs-`null` conflation would mislabel a visit. Low-risk (mock-primary now), but worth locking before f9/f13 consume the same shapes. - **Status:** open ## REQ-016 — Checkout summary for C6 (served gross/commission/VAT breakdown) — filed by frontend-phase-9-b10 — 2026-07-10 - **Need:** A customer-facing read that serves the C6 «خلاصه و پرداخت» money rows for an `accepted_awaiting_payment` request: the three b10 amounts (`grossPriceIrr`, `balinyaarCommissionIrr`, `nursePayoutAmount`) **plus the display decomposition** — service cost, commission **net of VAT**, `vatIrr` + `vatRate` — and the nurse/variant/schedule mini-info + `paymentDeadlineAt`. - **Why:** The b8 `BookingRequestDto` is money-free by design and no checkout-summary endpoint exists, but C6 must show a breakdown that **reconciles to the rial** (service + commission + VAT = total) and the client is forbidden from deriving commission or tax itself (no float math, rates are server config). Today the whole C6 money surface is mocked (`services/payment` mock computes the split from the mock variant price at the configured 12% fee / 10% VAT). - **Proposed shape:** `GET api/v1/booking_requests/checkout_summary/{id}` (owner-scoped) → `{ bookingRequestId, requestStatus, nurseName, patientName, variantLabel, variantPriceUnit, sessionCount, requestedDate, requestedTimeStart, requestedTimeEnd, paymentDeadlineAt, serviceCostIrr, commissionIrr, vatIrr, vatRate, totalIrr, grossPriceIrr, balinyaarCommissionIrr, nursePayoutAmount }` — the client's real `paymentClientApi.getCheckoutSummary` already targets this slug and unwraps this exact shape (`client/src/services/payment/types.ts: CheckoutSummaryDto`). - **Status:** open ## REQ-017 — Client-readable payment outcome + `bookingId` on a converted request — filed by frontend-phase-9-b10 — 2026-07-10 - **Need:** After the gateway redirect returns, the client needs to learn (a) the payment transaction's status (`pending|succeeded|failed`) and (b) **which booking** the capture created. Either a transaction read (e.g. `GET api/v1/payment_transactions/{id}` or `GET api/v1/bookings/{bookingRequestId}/payments/latest`, owner-scoped) or, minimally, a `bookingId` field on `BookingRequestDto` once `status = converted`. - **Why:** b10 confirms captures inside the PSP webhook (correct — the client is never trusted), so the frontend's pending-callback state can only poll `booking_requests/get/{id}` and map `converted → succeeded` / `payment_deadline_expired → failed`. That works, but it cannot distinguish a *declined* payment (still `accepted_awaiting_payment`, retry allowed) from a *slow* callback, and the confirmation screen cannot deep-link «مشاهده رزرو» or «دانلود فاکتور» because the converted request never reveals its booking id. The client DTO already carries a client-augmented `bookingId: number | null` (mock fills it; real path returns null and the UI falls back to the bookings list, hiding the invoice link). - **Proposed shape:** add `bookingId: long?` to `BookingRequestDto` (null until converted) **and/or** `GET api/v1/bookings/{bookingRequestId}/payments/latest` → `{ transactionId, status, gatewayReferenceCode, bookingId? }`. - **Status:** open ## REQ-018 — Customer invoice availability after capture (auto-issue or owner-issue) — filed by frontend-phase-9-b10 — 2026-07-10 - **Need:** Make the b11 invoice reachable by the paying customer right after capture: auto-issue the commission invoice on card capture (idempotent per booking, as `POST admin_invoices` already is), or allow the owning customer to trigger the idempotent issue on first `GET api/v1/invoices/{bookingId}`. - **Why:** The f9 confirmation screen offers «دانلود فاکتور», but b11 issues invoices only via the admin-only `POST api/v1/admin_invoices`, so a customer's `GET invoices/{bookingId}` 404s until an admin acts. The UI handles the 404 as a "فاکتور هنوز صادر نشده است" state (and the mock auto-issues at capture to demo the full flow), but on the real rails every fresh payment would land on that empty state. - **Status:** open ## REQ-019 — Customer-initiated booking cancellation command — filed by frontend-phase-10-b11 — 2026-07-10 - **Need:** A **customer-facing** command to cancel a booking (post-payment) and open its refund, e.g. `POST api/v1/bookings/{bookingId}/cancel` (owner-scoped) with body `{ sessionIds?: long[], reasonCategory: string, reasonNotes?: string }` → the created refund summary (`{ id, bookingId, status, refundChannel, amount, expectedCustomerRefundEta, reference }`, i.e. the `refunds/{id}/status` shape). `sessionIds` omitted = cancel all un-started (remaining) sessions; completed-and-verified sessions stay payout-eligible. - **Why:** b11 shipped refunds **admin-only** (`POST admin_refunds`) — there is **no** customer path to request a cancellation, but f10's whole cancel flow is customer-initiated (the customer *requests*; an admin still *approves/processes* the money). The client mocks this behind the `services/refunds` seam: the mock flips the booking to `cancelled` (stamping the b9 cancellation snapshot), decomposes the refund across the two fee legs, and returns a card-immediate (`succeeded`) or BNPL-`processing` refund. The real `refundsClientApi.cancelBooking` already targets this slug. - **Proposed shape:** `POST api/v1/bookings/{bookingId}/cancel` body as above → `RefundStatusDto`. The server resolves the snapshotted policy, enforces the outside-policy/state rules (`409`), posts the balanced reversal, and (per the admin-only rule) may route the refund through an admin/ticket step — the customer surface just needs to *create* the cancellation request and read the resulting refund. - **Status:** open ## REQ-020 — Cancellation-policy preview (pre-cancel, per-session) — filed by frontend-phase-10-b11 — 2026-07-10 - **Need:** A read that **resolves the applicable cancellation policy by current lead time** *before* the customer confirms, incl. the per-session refundability breakdown. Proposed `GET api/v1/bookings/{bookingId}/cancellation_policy` (owner-scoped) → `{ bookingId, cancellable, cancellationPolicyCode, refundPercentageApplied, feePercentage, refundAmountIrr, feeAmountIrr, refundableAmountIrr, platformFeeRefundedIrr, nursePayoutRefundedIrr, appliesTo, leadTimeLabel, refundChannel, expectedCustomerRefundEta, sessions: [{ bookingSessionId, sessionIndex, scheduledDate, refundable, reasonCode }] }` (IRR fields are digit-strings; `refundAmountIrr + feeAmountIrr = refundableAmountIrr`; the fee-leg split is served, never client-derived). - **Why:** The phase's load-bearing rule is **disclose the fee/refund % before confirm** — an outside-policy fee is never a surprise. b9 snapshots `cancellationPolicyCode`/`cancellationRefundPercentage`/ `refundableAmountIrr` on the booking only *after* a cancel; there is no pre-cancel preview that resolves the tier by current lead time and enumerates which sessions are refundable (un-started) vs locked (completed-and-verified). The client mocks the whole preview behind the `services/refunds` seam; the tier **codes** (`free_24h` / `partial_under_24h` / `customer_no_show`) are client-invented placeholders (the product doc pins no wire codes) mapped to i18n keys — please define the canonical `cancellation_policy_code` set so the client maps the real codes. - **Proposed shape:** as above. The `cancellationPolicyCode` set + the per-session `reasonCode` set (`un_started` / the blocking session status) should be documented as stable enum codes → i18n keys. - **Status:** open ## REQ-021 — Customer refund lookup-by-booking + fee-leg decomposition on the customer status — filed by frontend-phase-10-b11 — 2026-07-10 - **Need:** Two additions to the customer refund surface: 1. **Reach a refund from its booking.** `GET api/v1/refunds/by_booking/{bookingId}` (owner-scoped) → the `refunds/{id}/status` shape, or `404` when the booking has no refund. Today the only customer read is `GET refunds/{id}/status` keyed by a **refund id** the customer can't obtain (the refund id lives on the admin-only `GET admin_refunds` worklist). 2. **Expose the decomposition to the customer.** Add `platformFeeRefundedIrr`, `nursePayoutRefundedIrr`, `refundPercentageApplied`, `cancellationPolicyCode`, `createdAt`, `completedAt` to the customer `refunds/{id}/status` payload (they exist on the admin-only `RefundListItem`). - **Why:** f10's refund-status screen deep-links from a booking and renders (where the design calls for it) a **fee-leg transparency split** (Balinyaar-fee-refunded vs service-cost-refunded). Without (1) the customer can't find their refund; without (2) the split can't render on the real path (the client mock fills all six fields; the real `refundsClientApi` leaves them `null` and the split is hidden). - **Also (minor):** please confirm `provider_commission_reversed_amount` (the BNPL provider's own commission on a revert) is **nullable** on the refund shape and reconciled from the provider response — the b12 `IBnplProvider` mock echoes it as nullable and the client treats any provider-commission figure as opaque/never customer-facing. - **Status:** open