73 KiB
73 KiB
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:
- Envelope unwrapping. The b0 swagger shows every response wrapped in
ApiResult({ isSuccess, statusCode, message, requestId, data }). The frontend'sclientFetchcurrently returns the raw body, so domainclientApis read the payload viaunwrap()(data). Confirm this is the intended shape for all endpoints (i.e. payload always underdata), so the pattern is correct before f1+ copy it. - Wire casing. Observed swagger properties are camelCase (
isSuccess,serverTimeUtc) — not the snake_caseapi-conventions.mdimplies 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. - Pagination payload.
api-conventions.mdsays lists returnitems+total(+page/page_size). Confirm the exact field names/casing on the wire (we've typedPaginated<T>as{ items, total, page, pageSize }inclient/src/lib/api/types.ts).
- Envelope unwrapping. The b0 swagger shows every response wrapped in
- Why: These fix the shared
ApiEnvelope<T>/Paginated<T>types and theservices/{domain}reference pattern every later frontend phase inherits. - Proposed shape:
{ isSuccess: boolean, statusCode: number, message?: string, requestId?: string, data?: T }anddata: { items: T[], total: number, page: number, pageSize: number }for lists. - Status: confirmed in refinement-phase-3 — the
ApiResultenvelope (payload underdata, camelCase body, integerstatusCode) andPagedResult{ items, total, page, pageSize }are the intended shapes for all endpoints. Note: the list query param binds camelCasepageSize(case-insensitive); thepage_sizedoc occurrences were swept (REQ-010).
REQ-002 — OTP length + expiry in RequestOtpResult — filed by frontend-phase-1-b2 — 2026-07-02
- Need: Add
codeLength(int) andexpiresInSeconds(int) toRequestOtpResult. - Why: The A2/B2 OTP screen renders one box per digit and (later) a code-expiry hint.
RequestOtpResultcurrently exposes onlyotpSent+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: delivered in refinement-phase-3
REQ-003 — Machine-readable error codes for verify_otp failures — filed by frontend-phase-1-b2 — 2026-07-02
- Need: A stable
codeon 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 — aretryAfterSeconds(orlockedUntil) 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_lockedcode 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: delivered in refinement-phase-3
REQ-005 — Patient relation + conditions fields — filed by frontend-phase-2-b3 — 2026-07-02
- Need: Add two fields to
PatientDtoand thepatients/create+patients/updatebodies:relation(parent|spouse|child|self, nullable) — the care-recipient's relation to the payer.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(onlyinitialMedicalNotesfree-text). The client currently augments them behind theservices/patientsseam (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 forrelation;conditionsa stable code list (could also be a normalized child table). - Status: delivered in refinement-phase-3
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
IObjectStoragethat returns a stored URL, plus anavatarUrlfield onNurseProfileDto(and laterCustomerProfileDto). e.g.POST api/v1/nurse_profiles/avatar(multipart/form-data) →{ url }, and persistavatar_urlon 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/profilesseam (uploadAvatarreturns an object URL). The realprofilesClientApi.uploadAvatarthrows501until this lands. - Status: delivered in refinement-phase-3
REQ-007 — Customer name + preferred-language update — filed by frontend-phase-2-b3 — 2026-07-02
- Need: Either add
firstName/lastName/preferredLanguageto thecustomer_profiles/upsertbody +CustomerProfileDto, or confirm the customer name is only ever set elsewhere (and how).MeResultexposesfirstName/lastNameread-only with no update endpoint;CustomerProfileDtocarries 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/profilesseam (mock-persisted; the real upsert sends only the emergency contact). Confirm the intended home for these so the client stops augmenting. - Status: delivered in refinement-phase-3
REQ-004 — Confirm multi-role disambiguation (activeRole?) — filed by frontend-phase-1-b2 — 2026-07-02
- Need: Confirm whether
MeResultwill gain anactiveRole(the user's currently-selected actor) for a user who holds bothcustomerandnurse, or whether the client should keep owning that choice. - Why: The role router must pick one app for a dual-role user. Absent an
activeRolein 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 returnsrolesbut no userid(only/mehas it) — fine for now (context id is hydrated from/me), flagging in case that changes. - Resolution (refinement-phase-2, 2026-07-13): The client owns the active-role choice —
MeResultwill not gain anactiveRole. A dual customer+nurse session is disambiguated by the client-carried intended role (the A1 vs B1 login switch), defaulting to the family app;RoleGuardlets a dual-role user move freely between shells. No server change needed. If the backend ever wants to persist a "current role", file a new REQ and the router will prefer it. Confirmed the/meid-hydration note still holds. - Status: resolved (client owns it; no backend change)
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/createandcustomer_addresses/update/{id}accept optionallatitude/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 theIGeocoderwhen 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 sendslatitude/longitudein 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.CustomerAddressDtoalready returnslatitude/longitude. - Status: delivered in refinement-phase-3
REQ-009 — Add provinceId to CustomerAddressDto — filed by frontend-phase-3-b4 — 2026-07-02
- Need: Add
provinceId(long) toCustomerAddressDto(the province that owns the address'scityId). - 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 carriescityIdbut not its province, so the client can't drive the city query to preselect the city without the province id. The client currently augmentsprovinceIdbehind theservices/addressesseam (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.cityIdstill implies the province server-side — this is purely to prefill the client cascade. - Proposed shape:
CustomerAddressDto { …, provinceId: long }(join fromcities.province_id). - Status: delivered in refinement-phase-3
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.mdroute examples write?page=&page_size=(snake_case), but the working b4serviceAreasclient bindspageSize(camelCase, case-insensitive to the server'sPageSizeproperty) — the f3 report flagged this as thepage_size→pageSizegotcha. The f4 catalog client follows the provenpageSizeforcatalog/categoriesandnurse_variants/list; the domain filtercategory_idstays 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 bindspageSize, please update thepage_sizeoccurrences in the contract docs to match; if it bindspage_size, tell us and we'll switch the client (one line per list call). - Proposed shape: list query =
?page={1-based}&pageSize={≤100}; responsedata={ items, total, page, pageSize }. - Status: delivered in refinement-phase-3 — server binds
pageSize; thepage_sizeoccurrences indev/contracts/domains/*.md+conventions/api-conventions.mdwere swept topageSize. (One generated swagger endpoint still shows apage_sizequery name withx-originalName: pageSize; it bindspageSizecase-insensitively.)
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 optionallylicenseNumber,issuingAuthority,holderName,issuedAt,expiresAt. Proposed:POST api/v1/nurse_verification/credential_details(or fold into the manual-stepdocumentsconfirm 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 realverificationClientApi.submitCredentialDetailsno-ops (the document uploads it accompanies ARE contract-backed viaupload_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_detailsbody{ inoNumber, specialties: string[], licenseNumber?, issuingAuthority?, holderName?, issuedAt?, expiresAt? }→VerificationStatusDto. Alternatively, extend the manual-stepdocumentsconfirm body with these fields. - Also (minor): the contract's
VerificationStepDtohas noisRequired— the client treats every seeded step as required (the "X از Y" meter Y =steps.length). Confirm that holds, or addisRequired. - Status: delivered in refinement-phase-3
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:
- On the
search/nursesresult 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. - 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, givingisVerified+credentialTypes) and the b5 single-variant read are public — there is no name/bio/specialties/full services list/latest-review aggregation.
- On the
- Why: C2/C3 are the trust funnel — the family chooses a real, named, priced nurse here. The
services/searchdomain is mock-primary (USE_SEARCH_MOCK = true) precisely because these fields aren't available; the realsearchClientApimaps 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
NurseSearchResultDtowith{ nurseName, avatarUrl, distanceKm? }; addGET api/v1/nurses/{id}/profilereturning the object above.price/priceIrrstay IRR digit-strings. - Status: delivered in refinement-phase-3
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 carriesvariantLabel+variantPriceUnitbut 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 augmentsvariantPricebehind theservices/bookingRequestsseam (the mock supplies it from the chosen variant; the real client leaves itnull); 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: delivered in refinement-phase-3
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) toBookingRequestListItemDto. Today the nurse-inbox row carriescounterpartyName(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 carryvariantLabel) to see the service. SurfacingvariantLabelon 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: delivered in refinement-phase-3
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.tsclient unions stay wire-accurate:- Enum string codes. The b9 swagger types
status/evvStatus/sessionstatusas barestring(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.) checkInAddressMatchtri-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 returnsnull(notfalse) 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.
- Enum string codes. The b9 swagger types
- 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-nullconflation would mislabel a visit. Low-risk (mock-primary now), but worth locking before f9/f13 consume the same shapes. - Status: confirmed in refinement-phase-3 — booking/session/EVV statuses serialize as the exact snake_case string codes the client unions expect (verified in code);
checkInAddressMatchisbool?=nullwhen GPS was absent or the frozen address has no resolvable coordinate,false= advisory out-of-range (never a block),true= in range.
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_paymentrequest: 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
BookingRequestDtois 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/paymentmock 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 realpaymentClientApi.getCheckoutSummaryalready targets this slug and unwraps this exact shape (client/src/services/payment/types.ts: CheckoutSummaryDto). - Status: delivered in refinement-phase-3
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}orGET api/v1/bookings/{bookingRequestId}/payments/latest, owner-scoped) or, minimally, abookingIdfield onBookingRequestDtooncestatus = 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 mapconverted → succeeded/payment_deadline_expired → failed. That works, but it cannot distinguish a declined payment (stillaccepted_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-augmentedbookingId: 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?toBookingRequestDto(null until converted) and/orGET api/v1/bookings/{bookingRequestId}/payments/latest→{ transactionId, status, gatewayReferenceCode, bookingId? }. - Status: delivered in refinement-phase-3
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_invoicesalready is), or allow the owning customer to trigger the idempotent issue on firstGET 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'sGET 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: delivered in refinement-phase-3
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. therefunds/{id}/statusshape).sessionIdsomitted = 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 theservices/refundsseam: the mock flips the booking tocancelled(stamping the b9 cancellation snapshot), decomposes the refund across the two fee legs, and returns a card-immediate (succeeded) or BNPL-processingrefund. The realrefundsClientApi.cancelBookingalready targets this slug. - Proposed shape:
POST api/v1/bookings/{bookingId}/cancelbody 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: delivered in refinement-phase-3
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/refundableAmountIrron 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 theservices/refundsseam; 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 canonicalcancellation_policy_codeset so the client maps the real codes. - Proposed shape: as above. The
cancellationPolicyCodeset + the per-sessionreasonCodeset (un_started/ the blocking session status) should be documented as stable enum codes → i18n keys. - Status: delivered in refinement-phase-3
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:
- Reach a refund from its booking.
GET api/v1/refunds/by_booking/{bookingId}(owner-scoped) → therefunds/{id}/statusshape, or404when the booking has no refund. Today the only customer read isGET refunds/{id}/statuskeyed by a refund id the customer can't obtain (the refund id lives on the admin-onlyGET admin_refundsworklist). - Expose the decomposition to the customer. Add
platformFeeRefundedIrr,nursePayoutRefundedIrr,refundPercentageApplied,cancellationPolicyCode,createdAt,completedAtto the customerrefunds/{id}/statuspayload (they exist on the admin-onlyRefundListItem).
- Reach a refund from its booking.
- 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
refundsClientApileaves themnulland 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 b12IBnplProvidermock echoes it as nullable and the client treats any provider-commission figure as opaque/never customer-facing. - Status: delivered in refinement-phase-3
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:
- 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). - 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.
- Provider + plan options — proposed
- Why: b12 is order-centric (
eligibility/initiate/ status / webhook) and the contract explicitly does not model the customer's repayment schedule (installment_countis informational). But D1–D4 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/bnplmock computes the plan split + schedule with BigInt at the configured provider fees). When these land, the swap is a singleUSE_BNPL_MOCK=falseflip. - Note on provider set: the wireframe includes an in-house
balinyaarplan not in the b12provider_codeenum (snapppay|digipay|tara|torobpay). Please addbalinyaar(or state how the in-house plan is modelled) soproviderCodestays a closed set. - Status: partially delivered in refinement-phase-3 —
balinyaaradded to theprovider_codeenum. DEFERRED:checkout_bnpl/options/{id}+schedule(per-plan monthly/down-payment split + due-dated repayment table) — b12 deliberately does not model the customer repayment schedule and there is no installment ledger to serve it from; keep D1/D2/D4 mocked until a provider-schedule integration or schedule table lands.
REQ-023 — BNPL eligibility should accept the D3 credit-check inputs (national ID / mobile / consent) — filed by frontend-phase-11-b12 — 2026-07-10
- Need: Either extend
POST api/v1/checkout_bnpl/eligibilityto 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 carrieseligibilityStatus+creditCeilingIrr, which D3 renders — only the request inputs are the gap. - Status: delivered in refinement-phase-3 —
checkout_bnpl/eligibilitynow accepts{ nationalId, mobile, consent }(consent required when the KYC inputs are present; supplied mobile drives the inquiry, else the account mobile). Mock still uses only the mobile until the real KYC step exists.
REQ-024 — BNPL provider-reported installment status for the Wallet (D5) + customer bookingId link — filed by frontend-phase-11-b12 — 2026-07-10
- Need: Two additions for the Wallet installment view and the confirmation deep-link:
- 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);earlyPayUrlis a hand-off link to the provider (early-pay is a provider action, never a Balinyaar transaction). - Customer-readable
bookingIdon the settled BNPL order (BnplOrderStatussets 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). - A customer by-request order lookup — proposed
GET api/v1/checkout_bnpl/by_request/{bookingRequestId}→BnplOrderStatus. The b12GET 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 thetransaction_idcarried through the handoff, which is already contract-correct — only the standalone poll needs a by-request read.)
- Provider-reported installment status — proposed
- 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/bnplseam (seeded plan + a plan pushed on each settled checkout). When it lands the swap is one config flip. - Status: partially delivered in refinement-phase-3 — (2)
bookingIdon the settled order already present onBnplOrderStatusDto(confirmed); (3)GET checkout_bnpl/by_request/{bookingRequestId}added (owner-scoped). DEFERRED: (1)checkout_bnpl/wallet_installments— per-installment provider-reported status Balinyaar does not own/track (no installment ledger in b12); needs provider integration. Keep D5 mocked.
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/history→NursePayoutHistoryDto). The f12 earnings screen needs three more nurse-scoped, read-only reads (all tenancy-scoped to the caller):- Earnings balance (four buckets + signed net) — proposed
GET api/v1/nurse_payouts/earnings_balance→{ pendingTotalIrr, eligibleTotalIrr, paidTotalIrr, clawbackOutstandingIrr, netPayableBalanceIrr }.netPayableBalanceIrris the ledger-derived, SIGNED payable balance (may be negative = "owed back"; never clamp).paidTotalIrris a lifetime total and does not enter the net balance. - Per-booking earnings list + money-state — proposed
GET api/v1/nurse_payouts/earnings?state=&page=&pageSize=→PagedResult<NurseEarningsItem>whereNurseEarningsItem = { bookingId, patientName, scheduledDate, grossPriceIrr, balinyaarCommissionIrr, nursePayoutAmount, state: 'pending'|'eligible'|'paid'|'clawback_applied', disputeWindowEndsAt?, payoutEligibleAt?, paidAt?, transferReference?, nursePayoutId?, batchId?, clawbackAppliedIrr?, netAmountIrr? }.stateis derived server-side frombookings.status+dispute_window_ends_at < now+ the payout link + any clawback (the client must never compute eligibility). Filterable bystate. - Nurse-readable payout detail (batch context + booking links) — proposed
GET api/v1/nurse_payouts/{id}→ a nurse-scoped analogue of the adminPayoutBatchDetailDto/PayoutDto:{ id, batchId, status, grossEarningsIrr, clawbackAppliedIrr, netAmountIrr, amountIrr, maskedIban, transferReference?, paidAt?, failureReason?, batch: { id, periodStart, periodEnd, processingDate, status, totalAmount, payoutCount, processedAt? }, bookings: PayoutBookingLinkDto[] }. The adminbatches/{id}detail is admin-only; a nurse needs to reach their own payout's batch window + covered bookings for reconciliation. failureReasonon the nurse history DTO —NursePayoutHistoryDtohas nofailureReason(it's on the adminPayoutDto). Afailedpayout 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).
- Earnings balance (four buckets + signed net) — proposed
- 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. OnlygetNursePayoutHistorymaps a live route; the other three are mocked behind thePayoutsApiseam (real-shaped fixtures covering all four states + a negative net balance + a failed payout).payoutsClientApialready targets the proposed slugs — when these land the swap is a singleUSE_PAYOUTS_MOCK=falseflip; 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-linkpayout_amount_irrsum = itsgross_earnings_irr; the nurse amount is payment-method-invariant (BNPL provider commission never deducted). - Status: delivered in refinement-phase-3
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:
- 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:1409. - 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. - 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.
- Review eligibility — proposed
- Why: b14 serves the submit (
POST bookings/{id}/review), the publicGET 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 theservices/reviewsdomain 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 realreviewsClientApimapsgetNurseReviews/createReview1:1 and targets the two proposed slugs for the gaps — one config flip when they land. - Status: delivered in refinement-phase-3
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_recordsGET/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 nordata-model/10-reviews-and-records.mdmodels them). Proposed:- Family-owned record —
GET/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.) - Record access — proposed
GET api/v1/patients/{patientId}/record_access→{ canView, canEdit, canAppendNote, deniedReason? }or confirm the client should derive access purely from the403on the history read (the two-stage clinical-disclosure rule). Today the mock returns a403for a foreign patient id so the non-leaking access-denied card is demoable. - Structured task results on a visit note —
WriteCareRecordBodyis only{ bookingId?, body }. The nurse's ticked checklist is currently folded intobodyas a leading summary line. A structuredtaskResults: [{ label, done }]field would preserve it (and let the history render the checklist as chips).
- Family-owned record —
- 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: delivered in refinement-phase-3
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 becauseservices/ticketsis mock-primary —USE_TICKETS_MOCK = true; the realticketsClientApimaps every live b15 route 1:1 and leaves these blank):unreadCount+lastMessageAtonTicketSummaryDto. The inbox card shows an unread indicator and sorts/labels by last activity, but the wire summary carries onlycreatedAt(no unread, no last-message time). Today the client falls back tocreatedAtand hides the unread badge on the real path; the mock supplies both. Proposed:TicketSummaryDto { …, lastMessageAt: string (UTC), unreadCount: int }(unread = messages after the caller'slast_read_at, internal excluded).- A message author display label on
TicketMessageDto(or onTicketParticipantDto). The thread bubble labels each message by author, but the DTO carries onlysenderId— 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.) - A user-facing by-booking ticket lookup/filter.
GET /tickets(the user list) filters only byStatus/ReferenceCode— there's nobookingIdfilter (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 idempotentopenTicket(category:'coordination', bookingId)(returns the existing thread). Proposed: add abookingIdquery param toGET /tickets, orGET /tickets/by_booking/{bookingId}?category=coordination. - An idempotency /
clientMessageIdfield onPOST /tickets/{id}/messages. The optimistic send generates aclientMessageIdto 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 optionalclientMessageId/idempotencyKeyon the post-message body and dedupe on it, echoing it back onPostMessageResult.
- 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 = falseflip (no hook/component change) —ticketsClientApialready maps the live routes. - Status: delivered in refinement-phase-3 — (1)
unreadCount+lastMessageAtonTicketSummaryDto(unread = non-internal messages from others after the caller'slast_read_at, stamped when the participant fetches the thread; admin queue = 0). (3)bookingIdquery param onGET /tickets. (4) optionalclientMessageIdonPOST /tickets/{id}/messages(deduped, echoed onPostMessageResult). (2) Confirmed: the role-label approach is intended — no raw identity/name is added (privacy).
REQ-029 — Config updatedAt/updatedBy on PlatformConfigDto — filed by frontend-phase-15-b15 — 2026-07-10
- Need: the f15 config editor shows each row's last-changed meta ("updated {date} by {actor}"), but the b1
PlatformConfigDtocarries only{ key, value, dataType, description }. Proposed: addupdatedAt(UTC) +updatedBy(actor id or display) to the DTO (the audit trail already records the change; this surfaces the latest on the row without opening the drawer). Mock supplies both; the real row degrades gracefully without. - Why: finance needs the effective value + who last touched it at a glance.
services/adminis mock-primary (USE_ADMIN_MOCK = true);adminClientApi.listConfigsmaps the live route 1:1 and leaves these undefined. - Status: delivered in refinement-phase-3 —
updatedAt+updatedByonPlatformConfigDto(from the entity's audit fields; falls back to creation).
REQ-030 — Audit-trail filters: actor / action / date-range — filed by frontend-phase-15-b15 — 2026-07-10
- Need:
GET audit/get_audit_trailfilters only byentity_type+entity_id. The f15 audit viewer offers actor, action, and from/to date filters. Proposed: addactor_id,action,from,toquery params (the mock honours all four). Until then the real client passes only the supported two and the rest degrade. - Why: ops audits by actor and by time window, not only by a single entity. Mock-primary.
- Status: delivered in refinement-phase-3 —
GET audit/get_audit_trailnow also filters byactorId,action,from,to(all optional;entityType/entityIdare now optional too). Note: query params bind camelCase (actorId/from/to), likepageSize— notactor_id.
REQ-031 — RBAC role grant/revoke/list endpoints — filed by frontend-phase-15-b15 — 2026-07-10
- Need: the b15 contract exposes no role-management endpoints. The (optional, DEFERRED-IF-MISSING) admin
roles console needs:
GET admin_roles/list_roles[?user_id=]returningRoleGrant[]({ userId, role, grantedBy, grantedAt, revokedAt }),POST admin_roles/grant_role { userId, role },POST admin_roles/revoke_role { userId, role }, whereroleis one ofsuper_admin|admin|support|finance|moderation. The screen is built against the mock and flagged DEFERRED-IF-MISSING; swapUSE_ADMIN_MOCK=falseonce the routes land. - Why: to manage which users hold which admin scopes. Not on the testable acceptance path.
- Status: deferred in refinement-phase-3 — the RBAC
admin_roles/list|grant|revokeconsole. The admin sub-role vocabulary + phone-OTP admins are seeded (refinement-phase-2), but a full grant/revoke management surface is admin-console tooling not on the frontend acceptance path (flagged DEFERRED-IF-MISSING); keepUSE_ADMIN_MOCKfor/admin/roles. To deliver: 3 endpoints overuser_roles(grant/revoke audited) + aRoleGrant[]read.
REQ-032 — Partner-portal split reads + activate/suspend + IBAN write-then-masked — filed by frontend-phase-15-b15 — 2026-07-10
- Need: the b15 contract has admin partner-center CRUD/verify/sponsor + a single
GET /centers/{id}/dashboardportal endpoint. The f15 partner portal + admin management need: (1) a center-scoped "my center" resolution (GET centers/meor/centers/me/dashboard) so a center admin never passes a raw id; (2) split portal readsGET centers/me/nurses,GET centers/me/bookings?status=&page=,GET centers/me/settlement?page=(or these as fields on the dashboard); (3) an activate/suspend toggle (POST admin/partner-centers/{id}/set-active { isActive }) — verify only activates; (4) confirm the write-then-masked IBAN contract (PATCH accepts a fullsettlementIban, GET returns onlysettlementIbanMaskedlast-4). Also the admin roster read (GET admin/partner-centers/{id}/nurses).services/partnerCenteris mock-primary (USE_PARTNER_MOCK). - Why: the portal (separate authz scope, own-center tenancy) + the admin management screens render these.
- Status: partially delivered in refinement-phase-3 — (3) activate/suspend toggle
POST admin/partner-centers/{id}/set-active { isActive }added; (4) confirmed the write-then-masked IBAN contract (PATCH accepts fullsettlementIban; reads return only masked last-4). Route casing confirmed: the b15 admin partner-center routes are kebab-case (admin/partner-centers,.../set-active) — an intentional b15 divergence, so the frontend's kebab-case guess is CORRECT (no change needed). DEFERRED: (1)centers/me+ (2) the split portal reads (centers/me/nurses|bookings|settlement) — these need the user↔partner-center admin association that REQ-038 deferred;/partnerstays reachable by direct nav + the partnerCenter mock until that seed +/mesignal land.
REQ-033 — Partner settlement: per-booking commission invoices + invoice total — filed by frontend-phase-15-b15 — 2026-07-10
- Need: the merchant-of-record settlement view lists per-booking commission invoices (b11
Invoiceshape) filtered to the center, but there's no center-scoped invoice-list route (see REQ-032). Also the b11InvoiceDTO has nototal— the settlement row showsplatform commission + BNPL commission + VAT = total; the client currently sums the served legs for the total (a display sum, not a load-bearing re-derivation). Proposed: servetotalIrron the invoice (= commission + bnplCommission + vat), plus a center-scoped list. - Why: the settlement/invoice view (rendered only when
is_merchant_of_record) needs a reconciling total. VAT stays on the commission line only; the rate is config-driven (vat_rate), never hardcoded. - Status: partially delivered in refinement-phase-3 —
totalIrr(= platform commission + BNPL commission + VAT) added toInvoiceDto. DEFERRED: the center-scoped invoice list (depends on the partner portal split reads, REQ-032).
REQ-034 — Verification admin: nurse-level queue + on-demand document URL + whole-verification approve/reject — filed by frontend-phase-15-b15 — 2026-07-10
- Need: three gaps in the b6 admin surface for the f15 review queue: (1)
GET admin_verificationsreturns one row per pending step; the queue UI is nurse-level (progress "X of Y", next step, expiring flag) — a nurse-grouped list (or agroup_by=nurseparam) would avoid a lossy client fold. (2) A per-document on-demand signed-URL route (GET admin_verifications/documents/{documentId}/url) — signed URLs are short-lived and must be fetched on demand / re-requested on expiry, not read from the long-lived case payload. (3) whole- verification approve/reject (POST admin_verifications/{id}/approveand/reject) — today the aggregate flips implicitly when the last step is decided; the UI wants an explicit Approve (enabled only when all steps pass) / Reject action.services/verificationis mock-primary. - Why: the queue + per-nurse case + signed-URL document viewer render these. The client never writes
is_verified— the server flips it transactionally (§5). - Status: deferred in refinement-phase-3 — verification admin polish (nurse-grouped queue, on-demand signed document URL, explicit whole-verification approve/reject). The per-step admin surface + the transactional
is_verifiedflip already exist (b6); these are ergonomic refinements to the admin queue, not on the frontend acceptance path.
REQ-035 — Refund preview + explicit approve/reject — filed by frontend-phase-15-b15 — 2026-07-10
- Need: b11
POST admin_refundscreates and executes in one call, so there is no way to render a preview (tiered percentage + fee/payout decomposition + channel + BNPL ETA + will-create-clawback) before initiating. Proposed:GET admin_refunds/preview?booking_id=&ticket_id=returning the server-computed decomposition (the client renders it, never recomputes the split/percentage). Also explicitPOST admin_refunds/{id}/approveand/rejectfor the provider-revert-failure → retry and the decline paths (today the single POST both creates + executes).services/refundsadmin methods are mock-primary;initiateRefundmaps the livePOST admin_refunds. - Why: the ticket-linked refund panel shows the preview, then initiate → (retry on provider failure) / reject.
- Status: deferred in refinement-phase-3 — refund admin preview + explicit approve/reject (the single
POST admin_refundscreates+executes today). The customer preview (REQ-020) IS delivered and serves the same decomposition; the admin-side preview/approve/reject split is admin-console tooling.
REQ-036 — Payout single preview endpoint + holidayShifted flag + record-transfer-reference — filed by frontend-phase-15-b15 — 2026-07-10
- Need: the f15 payout dashboard wants (1) a single preview call returning eligible + skipped + the
holiday-shifted processing date together (b13
GET admin_payouts/eligibleis paged and returns only the eligible page — skipped nurses come back on the batchPOST); aGET admin_payouts/preview?periodStart=&periodEnd=returning{ eligible[], skipped[], processingDate, holidayShifted, totalNet }would drive the dry-run cleanly. (2) AholidayShiftedboolean onPayoutBatchDto/ the preview so the UI can badge a batch whose processing date moved off a bank-closed day (currently not on the DTO). (3) A record-transfer-reference route (POST admin_payouts/{payoutId}/transfer_reference { reference }) for finance to attach the real bank reference (b13 hasmark_failedbut no reconcile-reference write).services/payoutsadmin methods are mock-primary; the run/retry map the live routes with theIdempotency-Keyheader. - Why: preview → run (idempotency-keyed, no double-pay) → detail with per-nurse retry + transfer-ref reconcile.
- Status: deferred in refinement-phase-3 — payout admin single-preview endpoint +
holidayShiftedflag + record-transfer-reference route. The eligible/skipped data is already returned by the batch generate/GET admin_payouts/eligible; a consolidated dry-run preview + reconcile-reference write are admin-console refinements.
REQ-037 — Moderation queue tagCodes on ModerationQueueItemDto — filed by frontend-phase-15-b15 — 2026-07-10
- Need: the f15 moderation queue renders each review's tag chips, but
ModerationQueueItemDto(b14) doesn't listtagCodes(only the publicReviewListItemDtodoes). Proposed: addtagCodes: string[]to the moderation DTO. The client defaults to[]meanwhile.services/reviewsmoderation methods are mock-primary;moderateReviewmaps the livePATCH reviews/{id}/statusand the queue mapsGET admin/reviews/moderation_queue. - Why: moderators see the tags a review carries before publishing/hiding.
- Status: delivered in refinement-phase-3
REQ-038 — Signal on /me that the caller administers a partner center (partner auto-routing) — filed by refinement-phase-2 — 2026-07-13
- Need: add a boolean/id on
MeResult— e.g.administersPartnerCenterId: number | null(or a plainisPartnerCenterAdmin: bool) — indicating the signed-in user is the admin of apartner_center. - Why: the partner portal (
/partner) is a separate authz scope — a center admin is not a Balinyaar admin, and partner-ness is not derivable fromme.roles. SoresolveRoleDestination(the login role router) can't route a partner admin to/partneron login the way it routes customer/nurse/admin. Today/partneris only reachable by direct navigation (in dev theservices/partnerCentermock resolves a center foruseMyPartnerCenter, so the shell renders instead of access-denied); there is no seeded real partner-center↔user association and no/me-level signal, so login→/partnercan't be delivered. With this field the router gains a partner branch and the demo seed can associate a phone user with a seeded center. - Proposed shape:
MeResult.administersPartnerCenterId?: number | null; when non-null,resolveRoleDestinationroutes toROUTES.PARTNER. Pair with a Development seed (a partner-center + ademo_partner_*phone user linked as its admin) so the actor is reachable end-to-end like the others. - Status: open (partner login-routing deferred;
/partnerreachable by direct nav + the partnerCenter mock) - Status: open
REQ-040 — Index-row enrichment: variantDisplayName + optional topReviewTag — filed by ui-phase-4 — 2026-07-18
- Need: Two additions to
NurseSearchResultDto(GET search/nurses):variantDisplayName(required, string) — the variant's display name, so the C2 result card can name the exact service being bought (the row is a variant).topReviewTag(optional, string | null) — a one-line top review tag (e.g. «منظم و دقیق») from the review-tag vocabulary, for an optional one-line trust cue on the card.
- Why:
NurseResultCardv2 (ui-phase-4) is the four-second decision unit. Today the index row carries no variant label, so a nurse offering three variants renders as three near-identical cards differing only by price; the card falls back to the row's category name (passed in by the page from the cached catalog reference data) until this lands — a genuine wire gap, not a display choice. The client type (services/search/types.tsNurseSearchResult.topReviewTag) andNurseResultCardalready render the tag conditionally so this is a zero-code swap once served. - Proposed shape:
NurseSearchResultDto { …, variantDisplayName: string, topReviewTag?: string | null }. - Status: open
REQ-041 — Free-text search (q) over nurse/variant/category names — filed by ui-phase-4 — 2026-07-18
- Need: A
qquery param onGET search/nursesmatching against nurse display name, variant display name, and category name (case-insensitive substring or better). - Why: The Home search bar was a dead affordance — it pushed
?q=to C1, which silently discarded it (C1 reads onlycategory_id). ui-phase-4 replaced the free-text field with a tappable entry point straight to C1 (decision: the index has no text column, variant names aren't client-queryable, and the only matchable dataset client-side — 5–6 cached category names — is already better served by the category grid directly below; a half-working text field over-promises exactly where trust matters). This REQ is the upgrade path noted in the component's JSDoc: onceqis served, the Home search bar can become a real typeahead without changing its i18n keys. - Proposed shape:
GET search/nurses?...&q=<string>— matches folded into the existing filter set (AND'd with category/city/gender/price), samePaginated<NurseSearchResultDto>response shape. - Status: open
REQ-042 — nurseGender on NursePublicProfileDto — filed by ui-phase-4 — 2026-07-18
- Need: Add
nurseGender('male'|'female') to theGET nurses/{id}/profileresponse. - Why: C3's header would show a gender chip alongside years-of-experience/rating (same-gender
matching is load-bearing throughout the product), but the public profile DTO doesn't serve it — the
client's
nurseGender: 'female'is a placeholder stub (explicitly commented "unused by the C3 page"). ui-phase-4 keeps the C3 header gender chip omitted rather than render the stub as truth; the C2 card and the carriedrequired_genderquery param already cover the matching flow honestly in the meantime. - Proposed shape:
NursePublicProfileDto { …, nurseGender: 'male' | 'female' }. - Status: open
REQ-043 — Public per-step verification detail (step codes + decision dates) — filed by ui-phase-4 — 2026-07-18
- Need: Extend the public
GET nurses/{id}/trust_badgeread (or a sibling public endpoint) with per-step passed-check detail: stepcode,status, anddecidedAt/passedAtdate — not just the aggregatecredentialTypes[]+approvedAtit serves today. - Why: The new shared
VerificationPanelcomponent (ui-phase-4,src/components/VerificationPanel/) is the "what Balinyaar verified" explainer — reused by the C3 profile section, the tappableTrustBadgebottom-sheet/dialog, and (per the phase's cross-reference) phase 8's public-profile preview. Today it can only render one row percredentialTypes[]entry (identity/license/INO/background-check are folded into three generic credential-type codes) plus the single aggregateapprovedAt— it cannot list identity check, Shahkar match, and license individually with their own dates. Until this lands the panel stays honest (renders only what's served: the credential-type rows + the one approval date — no invented steps or fake dates). - Proposed shape:
TrustBadgeDto { …, steps?: [{ code: string, status: 'passed', decidedAt: string }] }(onlypassedsteps are ever publicly listed — never a pending/failed step's detail). - Status: open
REQ-039 — WebOTP-conformant OTP SMS template — filed by ui-phase-3 — 2026-07-17
- Need: the OTP SMS body must end with the origin-bound last line
@<domain> #<code>(the WebOTP / origin-bound one-time-code convention), e.g.:This is a server/SMS-template change on the Kavenegar adapter (refinement-phase-8) — zero API-shape impact, just the text body the SMS gateway sends.کد ورود بالینیار: 123456 @balinyaar.com #123456 - Why: the client now feature-detects
'OTPCredential' in windowand callsnavigator.credentials.get({ otp: { transport: ['sms'] } })(components/auth/useWebOtp.ts, wired intoOtpStep) plusautoComplete="one-time-code"on every OTP box (OtpInput.tsx) — but Chrome's WebOTP API and the iOS/Android keyboard "from SMS" suggestion only auto-read a code when the SMS ends with this exact origin-bound line. Without it, the client work still ships (autoComplete alone helps on iOS/some Android keyboards) but silently degrades to manual entry everywhere. - Proposed shape: append
\n@<domain> #<code>as the SMS's last line, where<domain>is the site's own host (no scheme, no trailing slash) and<code>is the exact code the user must enter. - Status: open — client-side WebOTP wiring ships regardless and degrades gracefully until this lands.
REQ-044 — Structured nurseRejectionReasonCode on BookingRequestDto — filed by ui-phase-5 — 2026-07-18
- Need: A stable, enumerated
nurseRejectionReasonCode(e.g.gender_preference|outside_coverage|schedule_conflict|other) alongside the existing freeformnurseRejectionReasontext onBookingRequestDto. - Why: C5's terminal-state recovery (phase §3.2) offers "request again with the same nurse, a different
time" only when the rejection reason isn't a hard gender/coverage block — otherwise only "similar
nurses" is offered, since re-requesting the same nurse would just fail again.
nurseRejectionReasonis nurse-authored free text with no code, so the client can only approximate this with a keyword heuristic over the (possibly translated/paraphrased) string — implemented inbookings/request/[id]/page.tsx'srejectionAllowsSameNurseRetry(), matchinggender/coverage/areaand their Persian equivalents. A real code would replace the heuristic with an exact check. - Proposed shape:
BookingRequestDto { …, nurseRejectionReasonCode: 'gender_preference' | 'outside_coverage' | 'schedule_conflict' | 'other' | null }, required whenevernurseRejectionReasonis set.reject_dialog's reason field would become a category select (+ optional free-text) instead of a single free-text field. - Status: open — the client ships the keyword heuristic in the meantime (documented as a known approximation in the phase report), never blocking the recovery UI.
REQ-045 — Typed addressSnapshot/variantSnapshot on BookingDetailDto — filed by ui-phase-5 — 2026-07-18
- Need: Replace (or supplement)
addressSnapshotJson/variantSnapshotJson(opaque JSON strings) with typed fields:addressSnapshot: { title, cityNameFa, cityNameEn, districtNameFa, districtNameEn, addressLine, postalCode } | nullandvariantSnapshot: { displayName, priceUnit }. - Why: The booking-detail hero (phase §3.4) needed the frozen visit address for its "next visit" card,
but the seed fixtures show the snapshot blob has no stable schema — field names drift across bookings
(
city/cityName/cityNameFa,line/addressLine; seeservices/bookings/apis/mockApi.ts'saddr5001vs the booking-5005 fixture). The client (BookingDetailView.tsx'saddressSnapshotLabel()) currently tries every candidate key and joins whatever resolves — a best-effort parse, not a contract. The nurse view already masks this field tonullserver-side (two-stage disclosure); only the shape within the customer's non-null value is the gap. - Proposed shape: as above — the same shape
BookingRequestDisplayContext's address fields already use elsewhere in the frontend, so the mapping is a straight carry-over from whatever snapshotting logic produces the JSON blob today. - Status: open — the client keeps the defensive multi-key parse until this lands; no user-facing defect today (the address still renders when any recognizable key is present), just an unenforced contract.
REQ-046 — Checkout/receipt enrichment: nurse identity on the summary + a client-readable payment reference — filed by ui-phase-6 — 2026-07-18
- Need: Two additions to the checkout/payment surfaces:
nurseAvatarUrl(string, nullable) andnurseVerified(bool) onCheckoutSummaryDto— the C6 identity moment ("who am I paying for").- A client-readable tracking code and paid-at timestamp for a captured payment — either on the
pending-callback poll target or a dedicated confirm-read:
trackingCode(string, nullable — a customer-quotable reference) andpaidAt(UTC ISO, nullable) onPaymentOutcomeDto(GET booking_requests/get/{id}-derived today per REQ-017).
- Why: C6 now shows the nurse's avatar + a verified badge next to the payable total, and the
confirmation screen is rebuilt as a screenshot-worthy receipt (کد پیگیری in a copyable row + Shamsi
payment date-time) — Iranian users screenshot payment receipts and expect a reference number to quote in
a dispute. Neither field exists on the wire today; the mock supplies both (nurse is always seed-verified,
no avatar in the f7 mock store;
trackingCode= the PSPgatewayReferenceCode,paidAt= the capture timestamp) and the realpaymentClientApileaves themnull— the receipt hides the row rather than render a fabricated code (extends REQ-016/017). - Proposed shape:
CheckoutSummaryDto { …, nurseAvatarUrl: string|null, nurseVerified: boolean };PaymentOutcomeDto { …, trackingCode: string|null, paidAt: string|null }. - Status: open — mock-only until served; real path hides the identity avatar/badge and the tracking code/paid-at rows gracefully.
REQ-047 — Customer payment-transactions list (card + BNPL) — filed by ui-phase-6 — 2026-07-18
- Need: A customer-scoped read of every payment transaction the caller made — proposed
GET api/v1/bookings/payment_history(owner-scoped) →[{ transactionId, bookingRequestId, bookingId, status, amountIrr, createdAt }]. - Why: The wallet's «پرداختها» tab is the fix for the audit's top finding — the Wallet tab was a
permanently empty installments-only shell for every card-paying customer (the default path; BNPL is
mock-gated). No endpoint serves a card payment history today. The client added
getPaymentHistoryto thePaymentApiseam; the mock returns the in-memory transaction list, the realpaymentClientApitargets this proposed route (404s until delivered — the tab renders its empty state until then). BNPL rows are sourced separately fromservices/bnpl's wallet installments (REQ-024) and merged client-side for display — this REQ covers the card half only. - Proposed shape: as above;
statusreuses the existingPaymentTransactionStatusenum (pending|succeeded|failed). - Status: open — wallet «پرداختها» renders the empty state on the real path until delivered.
REQ-048 — Customer "all my refunds" list — filed by ui-phase-6 — 2026-07-18
- Need: A customer-scoped read of every refund the caller owns — proposed
GET api/v1/refunds/my(owner-scoped) → the same thin shapeGET refunds/{id}/statusalready returns, as an array. - Why: The wallet's «استردادها» tab renders every refund via the shared
RefundStatusCard. b11's only customer-facing refund reads are by-booking (REQ-021) and by-id — there's no "list everything I've been refunded" read, and REQ-021's by-booking route requires already knowing which booking to ask about. The client addedgetMyRefundsto theRefundsApiseam (mock-primary perUSE_REFUNDS_MOCK, extends REQ-019/020/021); the realrefundsClientApitargets this proposed route. - Proposed shape:
GET api/v1/refunds/my→RefundStatusWire[](the same per-item shape asrefunds/{id}/status). - Status: open — wallet «استردادها» renders the empty state on the real path until delivered.
REQ-049 — Invoice fiscal fields: payment method, transaction reference, seller fiscal identity — filed by ui-phase-6 — 2026-07-18
- Need: Three additions to
InvoiceDtoso the invoice is a document a family can file for reimbursement/dispute:paymentMethod: 'card' | 'bnpl' | null.transactionReference: string | null(opaque, the settling payment/BNPL reference).sellerFiscalIdentity: { legalName, economicCode: string|null, address: string|null } | null— a platform-level fact (not per-invoice data), read from wherever the مودیان enrollment config lands.
- Why: The invoice page was print-capable but not audit-worthy (no payment method, no transaction
reference, no seller tax/economic identity — the things a real Iranian VAT invoice carries). Buyer name
and the service/visit-date recap are composed client-side today (a UI join over the customer's own
profile + the booking detail read — no money math, per phase §3.5), but payment method/reference and the
seller identity have no client-side source and must be served. The mock populates all three for card
payments (
paymentMethod: 'card',transactionReference= the gateway reference, a placeholdersellerFiscalIdentity); BNPL-settled invoices and the real path leave themnull— the invoice hides those rows rather than render fabricated fiscal data. Extends REQ-018. - Proposed shape: as above.
- Status: open — invoice renders the reconciling money breakdown + مودیان status unconditionally; the three new rows render only when served.