Files
baya-monorepo/dev/shared-working-context/frontend/requests/for-backend.md
T
2026-07-10 18:46:16 +03:30

43 KiB
Raw Blame History

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 clientApis 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<T> as { items, total, page, pageSize } in client/src/lib/api/types.ts).
  • Why: These fix the shared ApiEnvelope<T>/Paginated<T> 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_sizepageSize 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

REQ-022 — BNPL provider/plan options + repayment schedule (D1/D2/D4) — filed by frontend-phase-11-b12 — 2026-07-10

  • Need: Two customer-facing reads the installment checkout renders that b12 serves nothing for:
    1. Provider + plan options — proposed GET api/v1/checkout_bnpl/options/{bookingRequestId} (owner-scoped) → { bookingRequestId, requestStatus, orderAmountIrr, bnplEligible, providers: [{ providerCode, tagline?, plans: [{ planId, termMonths?, installmentCount, feePercent, downPaymentPercent, monthlyAmountIrr, downPaymentIrr, totalIrr }] }] } — the payable amount plus the provider set (دیجی‌پی/اسنپ‌پی/اقساط بالین‌یار) and, per plan, the served monthly amount / down-payment / total (all IRR digit-strings).
    2. Repayment schedule — proposed GET api/v1/checkout_bnpl/schedule/{bookingRequestId}?provider=&plan={ bookingRequestId, providerCode, planId, totalIrr, downPaymentIrr, installmentCount, rows: [{ sequence, kind: 'down_payment'|'installment', dueDate, amountIrr }] } — the D4 پیش‌پرداخت + قسط ۱…N table.
  • Why: b12 is order-centric (eligibility / initiate / status / webhook) and the contract explicitly does not model the customer's repayment schedule (installment_count is informational). But D1D4 must show a provider list, per-plan monthly/down-payment figures, and a due-dated repayment table — and the client is forbidden from computing any money figure. Today the whole D1/D2/D4 money surface is mocked (services/bnpl mock computes the plan split + schedule with BigInt at the configured provider fees). When these land, the swap is a single USE_BNPL_MOCK=false flip.
  • Note on provider set: the wireframe includes an in-house balinyaar plan not in the b12 provider_code enum (snapppay|digipay|tara|torobpay). Please add balinyaar (or state how the in-house plan is modelled) so providerCode stays a closed set.
  • Status: open
  • Need: Either extend POST api/v1/checkout_bnpl/eligibility to accept { nationalId, mobile, consent } alongside { bookingRequestId, providerCode }, or define a separate provider-KYC step that captures them.
  • Why: D3 (اعتبارسنجی) collects کد ملی + موبایل + a consent checkbox and submits them for the provider credit inquiry, but the b12 eligibility body is only { bookingRequestId, providerCode }. The client sends the extra fields today (ignored server-side until the KYC step exists) and the mock uses them for the deterministic declined-path demo. The response already carries eligibilityStatus + creditCeilingIrr, which D3 renders — only the request inputs are the gap.
  • Status: open
  • Need: Two additions for the Wallet installment view and the confirmation deep-link:
    1. Provider-reported installment status — proposed GET api/v1/checkout_bnpl/wallet_installments (owner-scoped) → [{ bnplTransactionId, bookingId?, providerCode, status, serviceLabel, outstandingBalanceIrr, installmentCount, nextDueDate?, nextAmountIrr?, earlyPayUrl?, installments: [{ sequence, kind, dueDate, amountIrr, status: 'paid'|'due_soon'|'upcoming'|'overdue' }] }] — the مانده بدهی + per-installment due list D5 renders. This is provider-reported status Balinyaar proxies, not a Balinyaar-managed ledger (the repayment schedule is out of the b12 contract's scope); earlyPayUrl is a hand-off link to the provider (early-pay is a provider action, never a Balinyaar transaction).
    2. Customer-readable bookingId on the settled BNPL order (BnplOrderStatus sets it at settle) so the return surface can deep-link the reused f9 confirmation → booking detail + invoice (same gap as REQ-017 on the card path).
    3. A customer by-request order lookup — proposed GET api/v1/checkout_bnpl/by_request/{bookingRequestId}BnplOrderStatus. The b12 GET checkout_bnpl/{id} is keyed by the order id, but the return-poll surface holds the request id (from the C6 handoff), not the order id. (The settle-on-return path reads the order by its own id via the transaction_id carried through the handoff, which is already contract-correct — only the standalone poll needs a by-request read.)
  • Why: D5 (پیگیری اقساط) is the load-bearing "provider owns the schedule, Balinyaar only displays it" surface, and b12 models none of the per-installment schedule/status. The client mocks the whole D5 read behind the services/bnpl seam (seeded plan + a plan pushed on each settled checkout). When it lands the swap is one config flip.
  • Status: open

REQ-025 — Nurse-read earnings surface: four-bucket balance + per-booking earnings list + nurse payout detail — filed by frontend-phase-12-b13 — 2026-07-10

  • Need: b13 serves the nurse exactly one endpoint (GET api/v1/nurse_payouts/historyNursePayoutHistoryDto). The f12 earnings screen needs three more nurse-scoped, read-only reads (all tenancy-scoped to the caller):
    1. Earnings balance (four buckets + signed net) — proposed GET api/v1/nurse_payouts/earnings_balance{ pendingTotalIrr, eligibleTotalIrr, paidTotalIrr, clawbackOutstandingIrr, netPayableBalanceIrr }. netPayableBalanceIrr is the ledger-derived, SIGNED payable balance (may be negative = "owed back"; never clamp). paidTotalIrr is a lifetime total and does not enter the net balance.
    2. Per-booking earnings list + money-state — proposed GET api/v1/nurse_payouts/earnings?state=&page=&pageSize=PagedResult<NurseEarningsItem> where NurseEarningsItem = { bookingId, patientName, scheduledDate, grossPriceIrr, balinyaarCommissionIrr, nursePayoutAmount, state: 'pending'|'eligible'|'paid'|'clawback_applied', disputeWindowEndsAt?, payoutEligibleAt?, paidAt?, transferReference?, nursePayoutId?, batchId?, clawbackAppliedIrr?, netAmountIrr? }. state is derived server-side from bookings.status + dispute_window_ends_at < now + the payout link + any clawback (the client must never compute eligibility). Filterable by state.
    3. Nurse-readable payout detail (batch context + booking links) — proposed GET api/v1/nurse_payouts/{id} → a nurse-scoped analogue of the admin PayoutBatchDetailDto/PayoutDto: { id, batchId, status, grossEarningsIrr, clawbackAppliedIrr, netAmountIrr, amountIrr, maskedIban, transferReference?, paidAt?, failureReason?, batch: { id, periodStart, periodEnd, processingDate, status, totalAmount, payoutCount, processedAt? }, bookings: PayoutBookingLinkDto[] }. The admin batches/{id} detail is admin-only; a nurse needs to reach their own payout's batch window + covered bookings for reconciliation.
    4. failureReason on the nurse history DTONursePayoutHistoryDto has no failureReason (it's on the admin PayoutDto). A failed payout in the nurse history should carry its reason so the read-only failure banner can show it (the nurse cannot retry — retry stays an admin action).
  • Why: the f12 nurse earnings screen (services/payouts) renders the net payable balance + four-bucket breakdown, the state-segmented earnings list (pending/eligible/paid/clawback_applied), and the payout/batch reconciliation detail. Only getNursePayoutHistory maps a live route; the other three are mocked behind the PayoutsApi seam (real-shaped fixtures covering all four states + a negative net balance + a failed payout). payoutsClientApi already targets the proposed slugs — when these land the swap is a single USE_PAYOUTS_MOCK=false flip; no hook/component change.
  • Note (money invariants the server owns): gross_price_irr = balinyaar_commission_irr + nurse_payout_amount; net_amount = gross_earnings clawback_applied; a payout's booking-link payout_amount_irr sum = its gross_earnings_irr; the nurse amount is payment-method-invariant (BNPL provider commission never deducted).
  • Status: open

REQ-026 — Review eligibility + my-review-for-booking reads (+ masked author confirmation) — filed by frontend-phase-13-b14 — 2026-07-10

  • Need: Three customer-facing additions the leave-a-review flow renders that b14 does not serve:
    1. Review eligibility — proposed GET api/v1/bookings/{bookingId}/review_eligibility (owner-scoped) → { canReview: boolean, reason?: 'not_completed'|'already_reviewed'|'not_owner'|'not_found' }. Lets the CTA/form gate on "this booking is completed/closed AND not already reviewed" without probing the 1:1 409.
    2. My review for a booking — proposed GET api/v1/bookings/{bookingId}/my_review (owner-scoped) → { moderationStatus: 'pending_moderation'|'published'|'hidden'|'rejected'|'none', rating?, body?, tagCodes[], createdAt? }. Lets the CTA show the persistent "under review" state across sessions (a returning customer must never see a second form) — today only the just-submitted result is known client-side.
    3. Masked author confirmation. ReviewListItemDto (the public list) carries no customer name. The client renders a generic masked author («کاربر بالین‌یار») on the profile reviews tab. Please confirm this omission is intentional (privacy), or add a server-masked display name field if a masked name is wanted.
  • Why: b14 serves the submit (POST bookings/{id}/review), the public GET nurses/{id}/reviews, and the tag rollup — but there is no eligibility read and no my-review read, and moderation (pending_moderation → published) is admin-only (f15). So the services/reviews domain is mock-primary (USE_REVIEWS_MOCK = true): the mock reads the shared f8 bookings store for completed-booking eligibility, tracks the submission for the under-review state, and seeds a published list per nurse. The real reviewsClientApi maps getNurseReviews/createReview 1:1 and targets the two proposed slugs for the gaps — one config flip when they land.
  • Status: open

REQ-027 — Family-owned care record (medications/routine/tasks) + record access + structured task results — filed by frontend-phase-13-b14 — 2026-07-10

  • Need: The b14 care_records GET/POST serve the nurse-authored visit-note history (سوابق) — that half is real and consumed. But the E2 record viewer's family-owned editable record and the nurse task checklist have no backend at all (neither the b14 contract nor data-model/10-reviews-and-records.md models them). Proposed:
    1. Family-owned recordGET/PUT api/v1/patients/{patientId}/care_record (owner-scoped for read; owning-customer for write) → { patientId, medications: [{ id, name, dosage?, frequency, timingNote? }], routine: [{ id, label, timeOfDay?, note? }], tasks: [{ id, label, done }] }. Patient-scoped, not booking-scoped; the customer owns/edits it; it persists across nurse changes. (New entity/table.)
    2. Record access — proposed GET api/v1/patients/{patientId}/record_access{ canView, canEdit, canAppendNote, deniedReason? } or confirm the client should derive access purely from the 403 on the history read (the two-stage clinical-disclosure rule). Today the mock returns a 403 for a foreign patient id so the non-leaking access-denied card is demoable.
    3. Structured task results on a visit noteWriteCareRecordBody is only { bookingId?, body }. The nurse's ticked checklist is currently folded into body as a leading summary line. A structured taskResults: [{ label, done }] field would preserve it (and let the history render the checklist as chips).
  • Why: E2 (داروها/روتین/سوابق/وظایف) + the nurse E3 task-checklist are the continuity-of-care surface. The visit-note history/append are real (b14) and mapped 1:1 in patientRecordsClientApi; the family-record + access methods target the proposed slugs (REQ-027) and the domain is mock-primary (USE_PATIENT_RECORDS_MOCK = true) until they land. Note: the wireframe's four-tab E2 record is not in the data model — please confirm whether the family-owned record is a real MVP entity or a future addition (the client treats it as forward-looking).
  • Status: open

REQ-028 — Ticket inbox enrichment (unread + last-activity), message author name, by-booking lookup, optimistic idempotency — filed by frontend-phase-14-b15 — 2026-07-10

  • Need: Four additions the f14 messaging UI renders that the b15 TicketSummaryDto/TicketThreadDto/message routes don't yet expose (the two ticket screens work today because services/tickets is mock-primaryUSE_TICKETS_MOCK = true; the real ticketsClientApi maps every live b15 route 1:1 and leaves these blank):
    1. unreadCount + lastMessageAt on TicketSummaryDto. The inbox card shows an unread indicator and sorts/labels by last activity, but the wire summary carries only createdAt (no unread, no last-message time). Today the client falls back to createdAt and hides the unread badge on the real path; the mock supplies both. Proposed: TicketSummaryDto { …, lastMessageAt: string (UTC), unreadCount: int } (unread = messages after the caller's last_read_at, internal excluded).
    2. A message author display label on TicketMessageDto (or on TicketParticipantDto). The thread bubble labels each message by author, but the DTO carries only senderId — no name. The client derives the label from the participant role (author_customer/author_nurse/author_support/author_system) and never shows a raw identity (privacy — the thread is deliberately not a contact directory). Please confirm this role-label approach is intended, or add a server-masked display name if a name is wanted. (No phone / no real name is the safe default.)
    3. A user-facing by-booking ticket lookup/filter. GET /tickets (the user list) filters only by Status/ReferenceCode — there's no bookingId filter (only the admin list has one). The "Get support" action on a booking is specced to jump to the existing coordination ticket if one exists; without a by-booking read the client can't find it. The mock approximates this via idempotent openTicket(category:'coordination', bookingId) (returns the existing thread). Proposed: add a bookingId query param to GET /tickets, or GET /tickets/by_booking/{bookingId}?category=coordination.
    4. An idempotency / clientMessageId field on POST /tickets/{id}/messages. The optimistic send generates a clientMessageId to reconcile the pending bubble; the server has no field for it, so a retried send (network flap) could create a duplicate server-side. The client does not send it today (reconciliation is client-only). Proposed: accept an optional clientMessageId/idempotencyKey on the post-message body and dedupe on it, echoing it back on PostMessageResult.
  • Why: the f14 inbox (unread + last-activity), the thread (author labels + jump-to-coordination), and the optimistic composer render these; the domain is mock-primary precisely because (1)/(3) aren't served and the linked bookings are themselves mock-primary. When they land the swap is a single USE_TICKETS_MOCK = false flip (no hook/component change) — ticketsClientApi already maps the live routes.
  • Status: open