# Frontend ↔ backend gaps — REQ reconciliation

**Audit date:** 2026-07-10 · **Sources:** `dev/shared-working-context/frontend/requests/for-backend.md`
(REQ-001…015), the frontend phase reports/STATUS (f0–f8), the client code's mock flags, the published
contracts (`dev/contracts/domains/*.md` + `dev/contracts/openapi/swagger.v1.json`), and the server code.
Every verdict was checked against **both** the contract surface and the actual DTO/handler/controller code.

**Headline:** the backend chain is complete (b0–b15), but of the 15 filed REQs only **REQ-001** and
**REQ-015** are effectively satisfied and **REQ-010** is a documentation fix — the other **12 are
undelivered**. Every REQ still reads `Status: open` in the tracker. As a direct consequence, **11 of the
client's 12 service domains still default to mock-primary** (only auth is real-default,
`client/src/services/auth/constants.ts:6`). Beyond the filed REQs, the unbuilt frontend phases f9–f15 will
consume backend surfaces that mostly exist — with one data gap (catalog option groups) and one pre-flagged
shape gap (checkout VAT line).

## Verdict summary

| REQ | Ask (short) | Verdict | One-line evidence |
| --- | --- | --- | --- |
| REQ-001 | Envelope / casing / pagination shape | **Done** (confirm + caveat) | `ApiResult` + `PagedResult` match the typed shape; `statusCode` is an **integer** enum |
| REQ-002 | `codeLength`/`expiresInSeconds` on RequestOtpResult | **Missing** | `RequestOtpResult.cs:7` has only `OtpSent`, `ResendAvailableInSeconds` |
| REQ-003 | Machine error codes for verify_otp | **Missing** | envelope has no `code` slot; lockout differs only by message text |
| REQ-004 | `activeRole` on MeResult (confirmation) | **Missing** (answer: client owns it) | `MeResult.cs:9` — no ActiveRole anywhere in the contract |
| REQ-005 | Patient `relation` + `conditions` | **Missing** | `PatientDto.cs:7`, create/update commands unchanged |
| REQ-006 | Avatar upload route + `avatarUrl` | **Missing** | zero `IFormFile`/avatar matches in `server/src` |
| REQ-007 | Customer name + preferred language | **Missing** | upsert body is emergency-contact only |
| REQ-008 | Accept client map pin on address create/update | **Missing** | commands have no lat/lng; always geocodes |
| REQ-009 | `provinceId` on CustomerAddressDto | **Missing** | DTO ends at RecipientPhone |
| REQ-010 | pageSize vs page_size | **Partial** | server binds `pageSize` (verified); contract docs still say `page_size` |
| REQ-011 | Nurse credential_details endpoint + `isRequired` | **Missing** | no such route/command; step DTO lacks isRequired |
| REQ-012 | Search row name/avatar/distance + `GET nurses/{id}/profile` | **Missing** | DTO ids-only; no profile action on NursesController |
| REQ-013 | `variantPrice` on BookingRequestDto | **Missing** | DTO has unit without price |
| REQ-014 | `variantLabel`/`patientAge` on list item | **Missing** | list DTO omits both |
| REQ-015 | Status enum codes + `checkInAddressMatch` tri-state (confirmation) | **Done** (verified in code) | snake_case string constants on the wire; null-when-no-GPS confirmed |

---

## Per-REQ detail

### REQ-001 — envelope, casing, pagination — **Done, needs a written confirmation + one caveat**

- **Frontend expects:** payload always under `data` in
  `{ isSuccess, statusCode, message, requestId, data }`; camelCase JSON; lists as
  `{ items, total, page, pageSize }`.
- **Backend ships:** exactly that. `ApiResult` at
  `server/src/Core/Baya.Application/Models/ApiResult/ApiResult.cs:8` (+ generic `Data` at `:28`);
  `PagedResult<T>(Items, Total, Page, PageSize)` at
  `server/src/Core/Baya.Application/Models/Common/PagedResult.cs:4`; camelCase is the System.Text.Json
  default (no naming-policy override exists in `server/src/API`); swagger confirms
  (`dev/contracts/openapi/swagger.v1.json:13448` envelope, `:14205` paged shape).
- **Caveat to communicate:** `statusCode` serializes as an **integer** (`ApiResultStatusCode` enum,
  swagger `:13468-13469`) — matches the client's `number` typing, but worth stating so nobody expects an
  HTTP-status string.
- **Fix:** zero code. Write the confirmation into the REQ and mark it delivered.

### REQ-002 — OTP length + expiry — **Missing**

- **Expected:** `RequestOtpResult { otpSent, resendAvailableInSeconds, codeLength, expiresInSeconds }`.
- **Actual:** `server/src/Core/Baya.Application/Models/Identity/RequestOtpResult.cs:7` — two fields only;
  handler returns them at `Features/Identity/Commands/RequestOtp/RequestOtpCommand.Handler.cs:67`; swagger
  agrees (`swagger.v1.json:16539`). The client hardcodes `OTP_CODE_LENGTH = 6`
  (`client/src/services/auth/constants.ts:19`).
- **Fix:** add the two ints (code length is a constant today; TTL from the OTP provider options). S effort.

### REQ-003 — machine-readable verify_otp errors — **Missing**

- **Expected:** stable `code` (`otp_invalid` | `otp_expired` | `otp_locked`) + `retryAfterSeconds` on
  lockout.
- **Actual:** the envelope has no `code` slot (`ApiResult.cs:8`; `OperationResult` carries only boolean
  flags — `Models/Common/OperationResult.cs:14-31`). Wrong/expired share one anti-enumeration message
  (`Features/Identity/Commands/VerifyOtp/VerifyOtpCommand.Handler.cs:24,30,34,50`); lockout is a different
  English string only (`:38`). The client keys off the mock-only `otp_locked` code
  (`client/src/services/auth/constants.ts:29`).
- **Fix:** add an optional `code` (+ optional `data`) to the failure envelope — a small
  `OperationResult`/`ApiResult` extension — and emit `otp_locked` + `retryAfterSeconds` from the lockout
  branch; keep wrong-vs-expired collapsed if enumeration-safety is preferred (state that in the REQ
  answer). S–M effort (the only REQ touching a cross-cutting type).

### REQ-004 — activeRole confirmation — **Missing (recommend: answer "client owns it")**

- **Actual:** no `activeRole` on `MeResult`
  (`server/src/Core/Baya.Application/Models/Identity/MeResult.cs:9`) or anywhere in the contract (schema
  scan). No endpoint persists a current-role choice.
- **Fix:** zero code — write the decision (client-owned `intended_role` stands) into the REQ so the router
  behavior is contract-blessed. If the product later wants a persisted active role, it's a `me/select_role`
  extension.

### REQ-005 — patient relation + conditions — **Missing**

- **Actual:** `PatientDto` ends at `InitialMedicalNotes`/`IsActive`
  (`Models/Identity/PatientDto.cs:7,15-16`); create/update commands unchanged
  (`Features/Identity/Commands/CreatePatient/CreatePatientCommand.cs:12-19`,
  `UpdatePatient/UpdatePatientCommand.cs:8-16`); no `relation`/`conditions` in any schema.
- **Fix:** `relation` as a nullable code column; `conditions` as stable codes (JSON column or child table —
  child table if search/filtering is ever wanted). Gate: flips `USE_PATIENTS_MOCK`
  (`client/src/services/patients/constants.ts:8`). S–M effort.

### REQ-006 — avatar upload + avatarUrl — **Missing**

- **Actual:** zero `IFormFile`/multipart/avatar usage in `server/src` (repo-wide grep); no `avatarUrl` on
  `NurseProfileDto` (swagger `:18945`) or `CustomerProfileDto`
  (`Models/Identity/CustomerProfileDto.cs:7`). The client's real path deliberately throws 501
  (`client/src/services/profiles/apis/clientApi.ts:84`).
- **Fix:** `POST api/v1/{nurse|customer}_profiles/avatar` (multipart, size/type-validated) storing via
  `IObjectStorage` + `avatar_url` column on both profiles. Note it also feeds REQ-012 (search card avatar)
  and REQ-013 (nurse avatar on request detail) — deliver before or with those. M effort (first multipart
  endpoint; pairs with the object-storage swap, plan §5.5).

### REQ-007 — customer name + preferred language — **Missing**

- **Actual:** upsert body is emergency-contact only
  (`Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.cs:11`); `MeResult`
  exposes name read-only (`MeResult.cs:12`); no `preferredLanguage` anywhere (schema scan).
- **Fix:** decide the home (recommend: extend the upsert to write `Users.FirstName/LastName` +
  `preferred_language` on the customer profile) and answer the REQ. S effort.

### REQ-008 — accept the client map pin — **Missing**

- **Actual:** create/update commands have no coordinates; the server always geocodes
  (`Features/Addresses/Commands/CreateAddress/CreateAddressCommand.cs:9-12`,
  `UpdateAddress/UpdateAddressCommand.cs:8-9`; swagger `:18051`). The user's pin is silently discarded on
  the real path — exactly what the REQ warned. This also degrades **EVV accuracy** (b9 measures distance to
  the stored coordinate; a mock/geocoded centroid is ±5 km off — `CrossCutting/Seams/MockGeocoder.cs:52`).
- **Fix:** optional `latitude`/`longitude` on both bodies; when present store as source `user_pin`, else
  geocode as today. S effort; do before the real geocoder swap (plan §5.4).

### REQ-009 — provinceId on CustomerAddressDto — **Missing**

- **Actual:** DTO fields run `Id..RecipientPhone` (`Models/Addresses/CustomerAddressDto.cs:9-24`); no
  `provinceId` (swagger `:17983` — the property exists only on `CityDto`).
- **Fix:** join `cities.province_id` into the address projections. S effort. Gate (with REQ-008):
  `USE_ADDRESSES_MOCK` (`client/src/services/addresses/constants.ts:10`).

### REQ-010 — pageSize param name — **Partial (server verified; docs stale)**

- **Actual:** every list binds a `PageSize` record property via `[FromQuery]` — so the working wire name is
  camelCase `pageSize` (case-insensitive), and `page_size` **silently does not bind**. Verified:
  `Features/ServiceAreas/Queries/ListMyServiceAreas/ListMyServiceAreasQuery.cs:8`,
  `Features/Variants/Queries/ListMyVariants/ListMyVariantsQuery.cs:8`,
  `Controllers/V1/NurseServiceAreasController.cs:35`, `AdminPayoutsController.cs:54`; swagger names the
  parameter `pageSize` (`swagger.v1.json:3104`). But the requested deliverable — fixing the docs — never
  happened: `dev/contracts/domains/catalog.md:41` and `config-reference.md:11` (and others, e.g.
  bookings-evv.md, verification.md) still write `page_size`.
- **Fix:** sweep the contract docs to `pageSize`, answer the REQ. Zero server code.

### REQ-011 — nurse credential_details + isRequired — **Missing**

- **Actual:** the nurse-facing controller exposes only submit/status/upload_url/documents/run
  (`Controllers/V1/NurseVerificationController.cs:26-57`); repo-wide grep for
  `credential_details|SubmitCredential` finds nothing. `VerificationStepDto` has no `IsRequired`
  (`Models/Verification/VerificationDtos.cs:17`); the flag exists only on the admin step-type catalog.
  Consequence on the real path: the INO number + specialties a nurse types are **silently dropped**
  (`verificationClientApi.submitCredentialDetails` no-ops —
  `dev/shared-working-context/reports/frontend-phase-5-report.md:101`).
- **Fix:** `POST api/v1/nurse_verification/credential_details` writing the structured
  `nurse_credentials` fields (the registry table already stores number/authority/expiry), + project
  `isRequired` onto the step DTO. M effort. Gate: `USE_VERIFICATION_MOCK`
  (`client/src/services/verification/constants.ts:9`).

### REQ-012 — search enrichment + public nurse profile — **Missing (highest-leverage gap)**

- **Actual:** `NurseSearchResultDto` carries ids + price/rating/gender/geo only
  (`Models/Search/NurseSearchResultDto.cs:8-19`); the public `NursesController` has trust_badge, reviews,
  review_tags — **no `/profile`** (`Controllers/V1/NursesController.cs:24-36`); no
  `avatarUrl`/`distanceKm` anywhere in the contract (schema scan).
- **Why it leads the priority list:** C2/C3 are the trust funnel — the family picks a *named, faced,
  priced* nurse here; this single REQ keeps `services/search` mock-primary
  (`client/src/services/search/constants.ts:9`) and blocks the whole discovery→request→booking real-path
  chain (search feeds C4's nurse/variant ids).
- **Fix:** (a) denormalize `nurse_name`/`avatar_url` into `nurse_search_index` (the maintainer already
  re-derives rows from source — `Persistence/Services/Search/SearchIndexMaintainer.cs:25`; add columns +
  reindex-on-profile-change) or join at query time in `SqlNurseSearch`; `distanceKm` is optional — the
  district model makes it derived-if-cheap. (b) an aggregated `GET nurses/{id}/profile` composing existing
  reads (profile + variants + trust badge + latest published review). M effort; depends on REQ-006 for the
  avatar itself.

### REQ-013 — variantPrice on BookingRequestDto — **Missing**

- **Actual:** the DTO has `VariantLabel` + `VariantPriceUnit` but no price and no nurse avatar
  (`Models/Booking/BookingRequestDto.cs:20-21`, full list `:11-42`; swagger `:16674`).
- **Fix:** join the variant's `Price` (IRR digit-string, consistent with the money convention) into the
  projection. The money-free rule stays intact — this is the display *rate*, not an engagement total (the
  request row still stores no money). S effort.

### REQ-014 — variantLabel/patientAge on the inbox row — **Missing**

- **Actual:** `BookingRequestListItemDto` has neither (`Models/Booking/BookingRequestListItemDto.cs:10-21`;
  swagger `:16903`) — the nurse inbox can't show *which service* was requested without opening the detail.
- **Fix:** add `variantLabel` (already on the detail DTO); `patientAge` as a coarse band if product wants
  it. S effort. Gate (with REQ-013): `USE_BOOKING_REQUESTS_MOCK`
  (`client/src/services/bookingRequests/constants.ts:14`) — though that flip also needs the upstream
  domains real (see below).

### REQ-015 — enum codes + checkInAddressMatch tri-state — **Done (verified), needs a written confirmation**

- **Verified in code:** statuses are stored/projected as snake_case **string constants** — exactly the
  client unions: `Domain/Entities/Booking/BookingStatus.cs:11-30`, `BookingSessionStatus.cs:10-19`,
  `VisitVerificationStatus.cs:11-17`; DTOs copy them verbatim (`Models/Booking/BookingDtos.cs:97`), so no
  PascalCase/int ever hits the wire. `checkInAddressMatch` is `bool?` (`BookingDtos.cs:104`) assigned only
  inside the lat/lng-present branch
  (`Features/Bookings/Commands/CheckInVisit/CheckInVisitCommand.Handler.cs:64-77`) → **null when GPS is
  absent**; a `false` is advisory only (support alert + notification, no block — `:90-109`).
- **One nuance to include in the answer:** `null` also occurs when GPS *was* sent but the frozen booking
  address has no resolvable coordinates — the client copy for «موقعیت ثبت نشد» should tolerate that.
- **Fix:** zero code; write the confirmation, mark delivered.

---

## Beyond the filed REQs — what f9–f15 will hit

Frontend phases f0–f8 are built (reports exist); **f9–f15 are specs only**. Reconciling their declared
consumption against the shipped backend:

| Upcoming phase | Consumes | Backend reality | Verdict |
| --- | --- | --- | --- |
| f9 checkout/card | b10 `payments.md` + b11 invoice | endpoints exist (initiate/webhook/`GET invoices/{booking_id}`), but **no checkout-summary read with the VAT line** — f8 already flagged `BookingDetailDto` has no tax field (`reports/frontend-phase-8-report.md:113`); f9's spec expects `vat_irr`/`vat_rate`/`redirect_url` shapes (`dev/phases/frontend/frontend-phase-9-b10.md:117,330`) | **Partial — pre-file the checkout-summary REQ now** |
| f10 refund status | b11 `refunds-invoices.md` | `GET refunds/{id}/status` + `GET invoices/{booking_id}` shipped (`Controllers/V1/RefundsController`, `InvoicesController`) | Done (verify shapes when f10 runs) |
| f11 BNPL | b12 `bnpl.md` | full eligibility→initiate→status surface shipped (`CheckoutBnplController`) | Done (verify shapes) |
| f12 nurse earnings | b13 `payouts.md` | `nurse_payouts/history` + admin console shipped (`NursePayoutsController`, `AdminPayoutsController`) | Done (verify shapes) |
| f13 reviews/care records | b14 `reviews-records.md` | submit/list/tags/moderation + care records shipped (5 controllers) | Done (verify shapes) |
| f14 tickets + notifications | b15 + **b1 notifications** | tickets shipped; notifications **verified present**: `GET notifications/get_notifications`/`get_unread_count`, `POST mark_notification_read`/`mark_all_read` (`Controllers/V1/NotificationsController.cs:24-42`) — the f14 spec's worry about missing b1 endpoints is unfounded | Done |
| f15 admin/partner consoles | admin endpoints across b1/b6/b11/b13/b14/b15 | all shipped per the chain (verification queue, refunds, payouts, moderation, config/holidays/audit/support-alerts, partner centers) | Done (expect shape-polish REQs when f15 runs) |

**Data gap (not a contract gap):** flipping `USE_CATALOG_MOCK` against a fresh backend yields categories
with **no option groups** — only the 5 categories are seeded; groups/values are admin-authored and the
admin catalog UI is f15 (`reports/frontend-phase-4-report.md:92`). Until f15 (or a seed migration), the
variant builder's required-option step has nothing to render on the real path. Recommend: a small
representative option-group seed, or prioritize the f15 catalog manager.

**Tracker hygiene:** all 15 REQs read `Status: open` (`for-backend.md:32…216`) and the mocks-registry's
early block contradicts its own later rows (see plan §7.6). Whoever lands this batch should update both in
the same change.

---

## Frontend-unblock priority

1. **REQ-012** (search row enrichment + public profile) — unlocks the discovery funnel; everything
   downstream needs C2/C3 real. Include the `nurse_search_index` columns + reindex.
2. **REQ-005, REQ-008, REQ-009** — the booking-request *inputs* (patients, addresses) go real; REQ-008
   also protects EVV accuracy.
3. **REQ-013, REQ-014** — the request flow prices/labels itself; with (1)+(2) the whole
   search→request→accept chain can flip to real.
4. **REQ-006, REQ-007** — profile/avatar polish; REQ-006 also feeds (1) and (3)'s avatar fields.
5. **REQ-011** — verification detail capture (stops silent INO/specialty data loss).
6. **REQ-002, REQ-003** — auth UX polish (real path already works without them).
7. **Zero-code batch: REQ-001, REQ-004, REQ-010, REQ-015** — written confirmations + contract-doc
   `page_size` sweep + tracker statuses.
8. **Pre-file the f9 checkout-summary REQ** (VAT line, redirect_url, idempotency header echo) so b-side
   work can precede the f9 build.
