Files
baya-monorepo/dev/post-phase/server/frontend-backend-gaps.html
T
2026-07-10 11:49:55 +03:30

122 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Frontend ↔ backend gaps — REQ reconciliation</title>
<style>
:root{
--bg:#faf9f6; --fg:#26221c; --muted:#6d675e; --panel:#ffffff; --border:#ddd7cc;
--accent:#0e7a63; --accent-soft:#e4f2ee; --code-bg:#f1ede5; --th-bg:#efeadf;
--warn:#a04b12; color-scheme: light dark;
}
@media (prefers-color-scheme: dark){
:root{
--bg:#191714; --fg:#e8e3da; --muted:#a29a8d; --panel:#211e1a; --border:#3a352d;
--accent:#4fc3a8; --accent-soft:#1e3630; --code-bg:#2a261f; --th-bg:#2d2921;
--warn:#e09355;
}
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);
font:16px/1.62 ui-sans-serif,system-ui,"Segoe UI",Roboto,"Vazirmatn",sans-serif;}
main{max-width:72rem;margin:0 auto;padding:2.5rem 1.5rem 5rem;}
h1{font-size:1.75rem;line-height:1.25;margin:.2rem 0 1rem;}
h2{font-size:1.35rem;margin:2.4rem 0 .7rem;padding-top:1rem;border-top:1px solid var(--border);}
h3{font-size:1.08rem;margin:1.8rem 0 .5rem;color:var(--accent);}
p{margin:.6rem 0;}
a{color:var(--accent);text-decoration:none;} a:hover{text-decoration:underline;}
code{background:var(--code-bg);border-radius:4px;padding:.1em .35em;
font:.86em ui-monospace,"Cascadia Code",Consolas,monospace;overflow-wrap:anywhere;}
pre{background:var(--code-bg);border:1px solid var(--border);border-radius:8px;
padding: .9rem 1rem;overflow-x:auto;}
pre code{background:none;padding:0;}
hr{border:none;border-top:1px solid var(--border);margin:2rem 0;}
.tblwrap{overflow-x:auto;margin:1rem 0;border:1px solid var(--border);border-radius:8px;}
table{border-collapse:collapse;width:100%;font-size:.92rem;}
th{background:var(--th-bg);text-align:start;position:sticky;top:0;}
th,td{border-bottom:1px solid var(--border);padding:.5rem .7rem;vertical-align:top;}
td:not(:last-child),th:not(:last-child){border-inline-end:1px solid var(--border);}
tbody tr:last-child td{border-bottom:none;}
ul,ol{margin:.6rem 0;padding-inline-start:1.5rem;}
li{margin:.35rem 0;}
li>code:first-child{font-weight:600;}
nav.toc{background:var(--panel);border:1px solid var(--border);border-radius:10px;
padding:1rem 1.3rem;margin:1.4rem 0 2rem;font-size:.92rem;}
nav.toc strong{display:block;margin-bottom:.4rem;}
nav.toc ul{margin:.2rem 0;padding-inline-start:1.1rem;list-style:none;}
nav.toc>ul{padding-inline-start:0;}
nav.toc li{margin:.2rem 0;}
nav.toc .l3{padding-inline-start:1.1rem;font-size:.88em;color:var(--muted);}
nav.toc .l3 a{color:var(--muted);}
.crumbs{font-size:.85rem;color:var(--muted);margin-bottom:.3rem;}
.crumbs a{color:var(--muted);}
.stamp{font-size:.85rem;color:var(--muted);margin:-.4rem 0 1rem;}
figure.diagram{margin:1.5rem 0;padding:1rem;background:var(--panel);
border:1px solid var(--border);border-radius:10px;overflow-x:auto;}
figure.diagram svg{display:block;min-width:900px;width:100%;height:auto;}
details.src{margin:.6rem 0 1.6rem;font-size:.85rem;color:var(--muted);}
details.src summary{cursor:pointer;}
svg text{fill:var(--fg);font:13px ui-sans-serif,system-ui,"Segoe UI",sans-serif;}
svg .t2{font-size:11px;fill:var(--muted);}
svg .grp-title{font-size:12px;font-weight:600;fill:var(--muted);letter-spacing:.04em;}
svg .box{fill:var(--panel);stroke:var(--fg);stroke-opacity:.55;rx:8;}
svg .box.live{stroke:var(--accent);stroke-opacity:1;stroke-width:1.6;}
svg .box.mock{stroke-dasharray:5 4;}
svg .grp{fill:none;stroke:var(--border);stroke-width:1.2;rx:12;}
svg .edge{fill:none;stroke-width:1.7;}
svg .edge.real{stroke:var(--accent);}
svg .edge.mock{stroke:var(--muted);stroke-dasharray:6 4;}
svg .lbl{font-size:10.5px;fill:var(--muted);}
svg .arr-real{fill:var(--accent);} svg .arr-mock{fill:var(--muted);}
</style>
</head>
<body><main>
<div class="crumbs"><a href="index.html">← Post-phase server audit</a></div>
<h1 id="frontend-backend-gaps-req-reconciliation">Frontend ↔ backend gaps — REQ reconciliation</h1><p class="stamp">Generated from the canonical Markdown — do not hand-edit. Audit date 2026-07-10.</p><nav class="toc"><strong>Contents</strong><ul><li class="l2"><a href="#verdict-summary">Verdict summary</a></li><li class="l2"><a href="#per-req-detail">Per-REQ detail</a></li><li class="l3"><a href="#req-001-envelope-casing-pagination-done-needs-a-written-confirmation-one-caveat">REQ-001 — envelope, casing, pagination — <strong>Done, needs a written confirmation + one caveat</strong></a></li><li class="l3"><a href="#req-002-otp-length-expiry-missing">REQ-002 — OTP length + expiry — <strong>Missing</strong></a></li><li class="l3"><a href="#req-003-machine-readable-verify-otp-errors-missing">REQ-003 — machine-readable verify_otp errors — <strong>Missing</strong></a></li><li class="l3"><a href="#req-004-activerole-confirmation-missing-recommend-answer-client-owns-it">REQ-004 — activeRole confirmation — <strong>Missing (recommend: answer "client owns it")</strong></a></li><li class="l3"><a href="#req-005-patient-relation-conditions-missing">REQ-005 — patient relation + conditions — <strong>Missing</strong></a></li><li class="l3"><a href="#req-006-avatar-upload-avatarurl-missing">REQ-006 — avatar upload + avatarUrl — <strong>Missing</strong></a></li><li class="l3"><a href="#req-007-customer-name-preferred-language-missing">REQ-007 — customer name + preferred language — <strong>Missing</strong></a></li><li class="l3"><a href="#req-008-accept-the-client-map-pin-missing">REQ-008 — accept the client map pin — <strong>Missing</strong></a></li><li class="l3"><a href="#req-009-provinceid-on-customeraddressdto-missing">REQ-009 — provinceId on CustomerAddressDto — <strong>Missing</strong></a></li><li class="l3"><a href="#req-010-pagesize-param-name-partial-server-verified-docs-stale">REQ-010 — pageSize param name — <strong>Partial (server verified; docs stale)</strong></a></li><li class="l3"><a href="#req-011-nurse-credential-details-isrequired-missing">REQ-011 — nurse credential_details + isRequired — <strong>Missing</strong></a></li><li class="l3"><a href="#req-012-search-enrichment-public-nurse-profile-missing-highest-leverage-gap">REQ-012 — search enrichment + public nurse profile — <strong>Missing (highest-leverage gap)</strong></a></li><li class="l3"><a href="#req-013-variantprice-on-bookingrequestdto-missing">REQ-013 — variantPrice on BookingRequestDto — <strong>Missing</strong></a></li><li class="l3"><a href="#req-014-variantlabel-patientage-on-the-inbox-row-missing">REQ-014 — variantLabel/patientAge on the inbox row — <strong>Missing</strong></a></li><li class="l3"><a href="#req-015-enum-codes-checkinaddressmatch-tri-state-done-verified-needs-a-written-confirmation">REQ-015 — enum codes + checkInAddressMatch tri-state — <strong>Done (verified), needs a written confirmation</strong></a></li><li class="l2"><a href="#beyond-the-filed-reqs-what-f9-f15-will-hit">Beyond the filed REQs — what f9f15 will hit</a></li><li class="l2"><a href="#frontend-unblock-priority">Frontend-unblock priority</a></li></ul></nav>
<p><strong>Audit date:</strong> 2026-07-10 · <strong>Sources:</strong> <code>dev/shared-working-context/frontend/requests/for-backend.md</code> (REQ-001…015), the frontend phase reports/STATUS (f0f8), the client code's mock flags, the published contracts (<code>dev/contracts/domains/*.md</code> + <code>dev/contracts/openapi/swagger.v1.json</code>), and the server code. Every verdict was checked against <strong>both</strong> the contract surface and the actual DTO/handler/controller code.</p>
<p><strong>Headline:</strong> the backend chain is complete (b0b15), but of the 15 filed REQs only <strong>REQ-001</strong> and <strong>REQ-015</strong> are effectively satisfied and <strong>REQ-010</strong> is a documentation fix — the other <strong>12 are undelivered</strong>. Every REQ still reads <code>Status: open</code> in the tracker. As a direct consequence, <strong>11 of the client's 12 service domains still default to mock-primary</strong> (only auth is real-default, <code>client/src/services/auth/constants.ts:6</code>). Beyond the filed REQs, the unbuilt frontend phases f9f15 will consume backend surfaces that mostly exist — with one data gap (catalog option groups) and one pre-flagged shape gap (checkout VAT line).</p>
<h2 id="verdict-summary">Verdict summary</h2>
<div class="tblwrap"><table><thead><tr><th>REQ</th><th>Ask (short)</th><th>Verdict</th><th>One-line evidence</th></tr></thead><tbody><tr><td>REQ-001</td><td>Envelope / casing / pagination shape</td><td><strong>Done</strong> (confirm + caveat)</td><td><code>ApiResult</code> + <code>PagedResult</code> match the typed shape; <code>statusCode</code> is an <strong>integer</strong> enum</td></tr><tr><td>REQ-002</td><td><code>codeLength</code>/<code>expiresInSeconds</code> on RequestOtpResult</td><td><strong>Missing</strong></td><td><code>RequestOtpResult.cs:7</code> has only <code>OtpSent</code>, <code>ResendAvailableInSeconds</code></td></tr><tr><td>REQ-003</td><td>Machine error codes for verify_otp</td><td><strong>Missing</strong></td><td>envelope has no <code>code</code> slot; lockout differs only by message text</td></tr><tr><td>REQ-004</td><td><code>activeRole</code> on MeResult (confirmation)</td><td><strong>Missing</strong> (answer: client owns it)</td><td><code>MeResult.cs:9</code> — no ActiveRole anywhere in the contract</td></tr><tr><td>REQ-005</td><td>Patient <code>relation</code> + <code>conditions</code></td><td><strong>Missing</strong></td><td><code>PatientDto.cs:7</code>, create/update commands unchanged</td></tr><tr><td>REQ-006</td><td>Avatar upload route + <code>avatarUrl</code></td><td><strong>Missing</strong></td><td>zero <code>IFormFile</code>/avatar matches in <code>server/src</code></td></tr><tr><td>REQ-007</td><td>Customer name + preferred language</td><td><strong>Missing</strong></td><td>upsert body is emergency-contact only</td></tr><tr><td>REQ-008</td><td>Accept client map pin on address create/update</td><td><strong>Missing</strong></td><td>commands have no lat/lng; always geocodes</td></tr><tr><td>REQ-009</td><td><code>provinceId</code> on CustomerAddressDto</td><td><strong>Missing</strong></td><td>DTO ends at RecipientPhone</td></tr><tr><td>REQ-010</td><td>pageSize vs page_size</td><td><strong>Partial</strong></td><td>server binds <code>pageSize</code> (verified); contract docs still say <code>page_size</code></td></tr><tr><td>REQ-011</td><td>Nurse credential_details endpoint + <code>isRequired</code></td><td><strong>Missing</strong></td><td>no such route/command; step DTO lacks isRequired</td></tr><tr><td>REQ-012</td><td>Search row name/avatar/distance + <code>GET nurses/{id}/profile</code></td><td><strong>Missing</strong></td><td>DTO ids-only; no profile action on NursesController</td></tr><tr><td>REQ-013</td><td><code>variantPrice</code> on BookingRequestDto</td><td><strong>Missing</strong></td><td>DTO has unit without price</td></tr><tr><td>REQ-014</td><td><code>variantLabel</code>/<code>patientAge</code> on list item</td><td><strong>Missing</strong></td><td>list DTO omits both</td></tr><tr><td>REQ-015</td><td>Status enum codes + <code>checkInAddressMatch</code> tri-state (confirmation)</td><td><strong>Done</strong> (verified in code)</td><td>snake_case string constants on the wire; null-when-no-GPS confirmed</td></tr></tbody></table></div>
<hr>
<h2 id="per-req-detail">Per-REQ detail</h2>
<h3 id="req-001-envelope-casing-pagination-done-needs-a-written-confirmation-one-caveat">REQ-001 — envelope, casing, pagination — <strong>Done, needs a written confirmation + one caveat</strong></h3>
<ul><li><strong>Frontend expects:</strong> payload always under <code>data</code> in <code>{ isSuccess, statusCode, message, requestId, data }</code>; camelCase JSON; lists as <code>{ items, total, page, pageSize }</code>.</li><li><strong>Backend ships:</strong> exactly that. <code>ApiResult</code> at <code>server/src/Core/Baya.Application/Models/ApiResult/ApiResult.cs:8</code> (+ generic <code>Data</code> at <code>:28</code>); <code>PagedResult&lt;T&gt;(Items, Total, Page, PageSize)</code> at <code>server/src/Core/Baya.Application/Models/Common/PagedResult.cs:4</code>; camelCase is the System.Text.Json default (no naming-policy override exists in <code>server/src/API</code>); swagger confirms (<code>dev/contracts/openapi/swagger.v1.json:13448</code> envelope, <code>:14205</code> paged shape).</li><li><strong>Caveat to communicate:</strong> <code>statusCode</code> serializes as an <strong>integer</strong> (<code>ApiResultStatusCode</code> enum, swagger <code>:13468-13469</code>) — matches the client's <code>number</code> typing, but worth stating so nobody expects an HTTP-status string.</li><li><strong>Fix:</strong> zero code. Write the confirmation into the REQ and mark it delivered.</li></ul>
<h3 id="req-002-otp-length-expiry-missing">REQ-002 — OTP length + expiry — <strong>Missing</strong></h3>
<ul><li><strong>Expected:</strong> <code>RequestOtpResult { otpSent, resendAvailableInSeconds, codeLength, expiresInSeconds }</code>.</li><li><strong>Actual:</strong> <code>server/src/Core/Baya.Application/Models/Identity/RequestOtpResult.cs:7</code> — two fields only; handler returns them at <code>Features/Identity/Commands/RequestOtp/RequestOtpCommand.Handler.cs:67</code>; swagger agrees (<code>swagger.v1.json:16539</code>). The client hardcodes <code>OTP_CODE_LENGTH = 6</code> (<code>client/src/services/auth/constants.ts:19</code>).</li><li><strong>Fix:</strong> add the two ints (code length is a constant today; TTL from the OTP provider options). S effort.</li></ul>
<h3 id="req-003-machine-readable-verify-otp-errors-missing">REQ-003 — machine-readable verify_otp errors — <strong>Missing</strong></h3>
<ul><li><strong>Expected:</strong> stable <code>code</code> (<code>otp_invalid</code> | <code>otp_expired</code> | <code>otp_locked</code>) + <code>retryAfterSeconds</code> on lockout.</li><li><strong>Actual:</strong> the envelope has no <code>code</code> slot (<code>ApiResult.cs:8</code>; <code>OperationResult</code> carries only boolean flags — <code>Models/Common/OperationResult.cs:14-31</code>). Wrong/expired share one anti-enumeration message (<code>Features/Identity/Commands/VerifyOtp/VerifyOtpCommand.Handler.cs:24,30,34,50</code>); lockout is a different English string only (<code>:38</code>). The client keys off the mock-only <code>otp_locked</code> code (<code>client/src/services/auth/constants.ts:29</code>).</li><li><strong>Fix:</strong> add an optional <code>code</code> (+ optional <code>data</code>) to the failure envelope — a small <code>OperationResult</code>/<code>ApiResult</code> extension — and emit <code>otp_locked</code> + <code>retryAfterSeconds</code> from the lockout branch; keep wrong-vs-expired collapsed if enumeration-safety is preferred (state that in the REQ answer). SM effort (the only REQ touching a cross-cutting type).</li></ul>
<h3 id="req-004-activerole-confirmation-missing-recommend-answer-client-owns-it">REQ-004 — activeRole confirmation — <strong>Missing (recommend: answer "client owns it")</strong></h3>
<ul><li><strong>Actual:</strong> no <code>activeRole</code> on <code>MeResult</code> (<code>server/src/Core/Baya.Application/Models/Identity/MeResult.cs:9</code>) or anywhere in the contract (schema scan). No endpoint persists a current-role choice.</li><li><strong>Fix:</strong> zero code — write the decision (client-owned <code>intended_role</code> stands) into the REQ so the router behavior is contract-blessed. If the product later wants a persisted active role, it's a <code>me/select_role</code> extension.</li></ul>
<h3 id="req-005-patient-relation-conditions-missing">REQ-005 — patient relation + conditions — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> <code>PatientDto</code> ends at <code>InitialMedicalNotes</code>/<code>IsActive</code> (<code>Models/Identity/PatientDto.cs:7,15-16</code>); create/update commands unchanged (<code>Features/Identity/Commands/CreatePatient/CreatePatientCommand.cs:12-19</code>, <code>UpdatePatient/UpdatePatientCommand.cs:8-16</code>); no <code>relation</code>/<code>conditions</code> in any schema.</li><li><strong>Fix:</strong> <code>relation</code> as a nullable code column; <code>conditions</code> as stable codes (JSON column or child table — child table if search/filtering is ever wanted). Gate: flips <code>USE_PATIENTS_MOCK</code> (<code>client/src/services/patients/constants.ts:8</code>). SM effort.</li></ul>
<h3 id="req-006-avatar-upload-avatarurl-missing">REQ-006 — avatar upload + avatarUrl — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> zero <code>IFormFile</code>/multipart/avatar usage in <code>server/src</code> (repo-wide grep); no <code>avatarUrl</code> on <code>NurseProfileDto</code> (swagger <code>:18945</code>) or <code>CustomerProfileDto</code> (<code>Models/Identity/CustomerProfileDto.cs:7</code>). The client's real path deliberately throws 501 (<code>client/src/services/profiles/apis/clientApi.ts:84</code>).</li><li><strong>Fix:</strong> <code>POST api/v1/{nurse|customer}_profiles/avatar</code> (multipart, size/type-validated) storing via <code>IObjectStorage</code> + <code>avatar_url</code> 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).</li></ul>
<h3 id="req-007-customer-name-preferred-language-missing">REQ-007 — customer name + preferred language — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> upsert body is emergency-contact only (<code>Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.cs:11</code>); <code>MeResult</code> exposes name read-only (<code>MeResult.cs:12</code>); no <code>preferredLanguage</code> anywhere (schema scan).</li><li><strong>Fix:</strong> decide the home (recommend: extend the upsert to write <code>Users.FirstName/LastName</code> + <code>preferred_language</code> on the customer profile) and answer the REQ. S effort.</li></ul>
<h3 id="req-008-accept-the-client-map-pin-missing">REQ-008 — accept the client map pin — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> create/update commands have no coordinates; the server always geocodes (<code>Features/Addresses/Commands/CreateAddress/CreateAddressCommand.cs:9-12</code>, <code>UpdateAddress/UpdateAddressCommand.cs:8-9</code>; swagger <code>:18051</code>). The user's pin is silently discarded on the real path — exactly what the REQ warned. This also degrades <strong>EVV accuracy</strong> (b9 measures distance to the stored coordinate; a mock/geocoded centroid is ±5 km off — <code>CrossCutting/Seams/MockGeocoder.cs:52</code>).</li><li><strong>Fix:</strong> optional <code>latitude</code>/<code>longitude</code> on both bodies; when present store as source <code>user_pin</code>, else geocode as today. S effort; do before the real geocoder swap (plan §5.4).</li></ul>
<h3 id="req-009-provinceid-on-customeraddressdto-missing">REQ-009 — provinceId on CustomerAddressDto — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> DTO fields run <code>Id..RecipientPhone</code> (<code>Models/Addresses/CustomerAddressDto.cs:9-24</code>); no <code>provinceId</code> (swagger <code>:17983</code> — the property exists only on <code>CityDto</code>).</li><li><strong>Fix:</strong> join <code>cities.province_id</code> into the address projections. S effort. Gate (with REQ-008): <code>USE_ADDRESSES_MOCK</code> (<code>client/src/services/addresses/constants.ts:10</code>).</li></ul>
<h3 id="req-010-pagesize-param-name-partial-server-verified-docs-stale">REQ-010 — pageSize param name — <strong>Partial (server verified; docs stale)</strong></h3>
<ul><li><strong>Actual:</strong> every list binds a <code>PageSize</code> record property via <code>[FromQuery]</code> — so the working wire name is camelCase <code>pageSize</code> (case-insensitive), and <code>page_size</code> <strong>silently does not bind</strong>. Verified: <code>Features/ServiceAreas/Queries/ListMyServiceAreas/ListMyServiceAreasQuery.cs:8</code>, <code>Features/Variants/Queries/ListMyVariants/ListMyVariantsQuery.cs:8</code>, <code>Controllers/V1/NurseServiceAreasController.cs:35</code>, <code>AdminPayoutsController.cs:54</code>; swagger names the parameter <code>pageSize</code> (<code>swagger.v1.json:3104</code>). But the requested deliverable — fixing the docs — never happened: <code>dev/contracts/domains/catalog.md:41</code> and <code>config-reference.md:11</code> (and others, e.g. bookings-evv.md, verification.md) still write <code>page_size</code>.</li><li><strong>Fix:</strong> sweep the contract docs to <code>pageSize</code>, answer the REQ. Zero server code.</li></ul>
<h3 id="req-011-nurse-credential-details-isrequired-missing">REQ-011 — nurse credential_details + isRequired — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> the nurse-facing controller exposes only submit/status/upload_url/documents/run (<code>Controllers/V1/NurseVerificationController.cs:26-57</code>); repo-wide grep for <code>credential_details|SubmitCredential</code> finds nothing. <code>VerificationStepDto</code> has no <code>IsRequired</code> (<code>Models/Verification/VerificationDtos.cs:17</code>); the flag exists only on the admin step-type catalog. Consequence on the real path: the INO number + specialties a nurse types are <strong>silently dropped</strong> (<code>verificationClientApi.submitCredentialDetails</code> no-ops — <code>dev/shared-working-context/reports/frontend-phase-5-report.md:101</code>).</li><li><strong>Fix:</strong> <code>POST api/v1/nurse_verification/credential_details</code> writing the structured <code>nurse_credentials</code> fields (the registry table already stores number/authority/expiry), + project <code>isRequired</code> onto the step DTO. M effort. Gate: <code>USE_VERIFICATION_MOCK</code> (<code>client/src/services/verification/constants.ts:9</code>).</li></ul>
<h3 id="req-012-search-enrichment-public-nurse-profile-missing-highest-leverage-gap">REQ-012 — search enrichment + public nurse profile — <strong>Missing (highest-leverage gap)</strong></h3>
<ul><li><strong>Actual:</strong> <code>NurseSearchResultDto</code> carries ids + price/rating/gender/geo only (<code>Models/Search/NurseSearchResultDto.cs:8-19</code>); the public <code>NursesController</code> has trust_badge, reviews, review_tags — <strong>no <code>/profile</code></strong> (<code>Controllers/V1/NursesController.cs:24-36</code>); no <code>avatarUrl</code>/<code>distanceKm</code> anywhere in the contract (schema scan).</li><li><strong>Why it leads the priority list:</strong> C2/C3 are the trust funnel — the family picks a <em>named, faced, priced</em> nurse here; this single REQ keeps <code>services/search</code> mock-primary (<code>client/src/services/search/constants.ts:9</code>) and blocks the whole discovery→request→booking real-path chain (search feeds C4's nurse/variant ids).</li><li><strong>Fix:</strong> (a) denormalize <code>nurse_name</code>/<code>avatar_url</code> into <code>nurse_search_index</code> (the maintainer already re-derives rows from source — <code>Persistence/Services/Search/SearchIndexMaintainer.cs:25</code>; add columns + reindex-on-profile-change) or join at query time in <code>SqlNurseSearch</code>; <code>distanceKm</code> is optional — the district model makes it derived-if-cheap. (b) an aggregated <code>GET nurses/{id}/profile</code> composing existing reads (profile + variants + trust badge + latest published review). M effort; depends on REQ-006 for the avatar itself.</li></ul>
<h3 id="req-013-variantprice-on-bookingrequestdto-missing">REQ-013 — variantPrice on BookingRequestDto — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> the DTO has <code>VariantLabel</code> + <code>VariantPriceUnit</code> but no price and no nurse avatar (<code>Models/Booking/BookingRequestDto.cs:20-21</code>, full list <code>:11-42</code>; swagger <code>:16674</code>).</li><li><strong>Fix:</strong> join the variant's <code>Price</code> (IRR digit-string, consistent with the money convention) into the projection. The money-free rule stays intact — this is the display <em>rate</em>, not an engagement total (the request row still stores no money). S effort.</li></ul>
<h3 id="req-014-variantlabel-patientage-on-the-inbox-row-missing">REQ-014 — variantLabel/patientAge on the inbox row — <strong>Missing</strong></h3>
<ul><li><strong>Actual:</strong> <code>BookingRequestListItemDto</code> has neither (<code>Models/Booking/BookingRequestListItemDto.cs:10-21</code>; swagger <code>:16903</code>) — the nurse inbox can't show <em>which service</em> was requested without opening the detail.</li><li><strong>Fix:</strong> add <code>variantLabel</code> (already on the detail DTO); <code>patientAge</code> as a coarse band if product wants it. S effort. Gate (with REQ-013): <code>USE_BOOKING_REQUESTS_MOCK</code> (<code>client/src/services/bookingRequests/constants.ts:14</code>) — though that flip also needs the upstream domains real (see below).</li></ul>
<h3 id="req-015-enum-codes-checkinaddressmatch-tri-state-done-verified-needs-a-written-confirmation">REQ-015 — enum codes + checkInAddressMatch tri-state — <strong>Done (verified), needs a written confirmation</strong></h3>
<ul><li><strong>Verified in code:</strong> statuses are stored/projected as snake_case <strong>string constants</strong> — exactly the client unions: <code>Domain/Entities/Booking/BookingStatus.cs:11-30</code>, <code>BookingSessionStatus.cs:10-19</code>, <code>VisitVerificationStatus.cs:11-17</code>; DTOs copy them verbatim (<code>Models/Booking/BookingDtos.cs:97</code>), so no PascalCase/int ever hits the wire. <code>checkInAddressMatch</code> is <code>bool?</code> (<code>BookingDtos.cs:104</code>) assigned only inside the lat/lng-present branch (<code>Features/Bookings/Commands/CheckInVisit/CheckInVisitCommand.Handler.cs:64-77</code>) → <strong>null when GPS is absent</strong>; a <code>false</code> is advisory only (support alert + notification, no block — <code>:90-109</code>).</li><li><strong>One nuance to include in the answer:</strong> <code>null</code> also occurs when GPS <em>was</em> sent but the frozen booking address has no resolvable coordinates — the client copy for «موقعیت ثبت نشد» should tolerate that.</li><li><strong>Fix:</strong> zero code; write the confirmation, mark delivered.</li></ul>
<hr>
<h2 id="beyond-the-filed-reqs-what-f9-f15-will-hit">Beyond the filed REQs — what f9f15 will hit</h2>
<p>Frontend phases f0f8 are built (reports exist); <strong>f9f15 are specs only</strong>. Reconciling their declared consumption against the shipped backend:</p>
<div class="tblwrap"><table><thead><tr><th>Upcoming phase</th><th>Consumes</th><th>Backend reality</th><th>Verdict</th></tr></thead><tbody><tr><td>f9 checkout/card</td><td>b10 <code>payments.md</code> + b11 invoice</td><td>endpoints exist (initiate/webhook/<code>GET invoices/{booking_id}</code>), but <strong>no checkout-summary read with the VAT line</strong> — f8 already flagged <code>BookingDetailDto</code> has no tax field (<code>reports/frontend-phase-8-report.md:113</code>); f9's spec expects <code>vat_irr</code>/<code>vat_rate</code>/<code>redirect_url</code> shapes (<code>dev/phases/frontend/frontend-phase-9-b10.md:117,330</code>)</td><td><strong>Partial — pre-file the checkout-summary REQ now</strong></td></tr><tr><td>f10 refund status</td><td>b11 <code>refunds-invoices.md</code></td><td><code>GET refunds/{id}/status</code> + <code>GET invoices/{booking_id}</code> shipped (<code>Controllers/V1/RefundsController</code>, <code>InvoicesController</code>)</td><td>Done (verify shapes when f10 runs)</td></tr><tr><td>f11 BNPL</td><td>b12 <code>bnpl.md</code></td><td>full eligibility→initiate→status surface shipped (<code>CheckoutBnplController</code>)</td><td>Done (verify shapes)</td></tr><tr><td>f12 nurse earnings</td><td>b13 <code>payouts.md</code></td><td><code>nurse_payouts/history</code> + admin console shipped (<code>NursePayoutsController</code>, <code>AdminPayoutsController</code>)</td><td>Done (verify shapes)</td></tr><tr><td>f13 reviews/care records</td><td>b14 <code>reviews-records.md</code></td><td>submit/list/tags/moderation + care records shipped (5 controllers)</td><td>Done (verify shapes)</td></tr><tr><td>f14 tickets + notifications</td><td>b15 + <strong>b1 notifications</strong></td><td>tickets shipped; notifications <strong>verified present</strong>: <code>GET notifications/get_notifications</code>/<code>get_unread_count</code>, <code>POST mark_notification_read</code>/<code>mark_all_read</code> (<code>Controllers/V1/NotificationsController.cs:24-42</code>) — the f14 spec's worry about missing b1 endpoints is unfounded</td><td>Done</td></tr><tr><td>f15 admin/partner consoles</td><td>admin endpoints across b1/b6/b11/b13/b14/b15</td><td>all shipped per the chain (verification queue, refunds, payouts, moderation, config/holidays/audit/support-alerts, partner centers)</td><td>Done (expect shape-polish REQs when f15 runs)</td></tr></tbody></table></div>
<p><strong>Data gap (not a contract gap):</strong> flipping <code>USE_CATALOG_MOCK</code> against a fresh backend yields categories with <strong>no option groups</strong> — only the 5 categories are seeded; groups/values are admin-authored and the admin catalog UI is f15 (<code>reports/frontend-phase-4-report.md:92</code>). 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.</p>
<p><strong>Tracker hygiene:</strong> all 15 REQs read <code>Status: open</code> (<code>for-backend.md:32…216</code>) 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.</p>
<hr>
<h2 id="frontend-unblock-priority">Frontend-unblock priority</h2>
<ol><li><strong>REQ-012</strong> (search row enrichment + public profile) — unlocks the discovery funnel; everything downstream needs C2/C3 real. Include the <code>nurse_search_index</code> columns + reindex.</li><li><strong>REQ-005, REQ-008, REQ-009</strong> — the booking-request <em>inputs</em> (patients, addresses) go real; REQ-008 also protects EVV accuracy.</li><li><strong>REQ-013, REQ-014</strong> — the request flow prices/labels itself; with (1)+(2) the whole search→request→accept chain can flip to real.</li><li><strong>REQ-006, REQ-007</strong> — profile/avatar polish; REQ-006 also feeds (1) and (3)'s avatar fields.</li><li><strong>REQ-011</strong> — verification detail capture (stops silent INO/specialty data loss).</li><li><strong>REQ-002, REQ-003</strong> — auth UX polish (real path already works without them).</li><li><strong>Zero-code batch: REQ-001, REQ-004, REQ-010, REQ-015</strong> — written confirmations + contract-doc <code>page_size</code> sweep + tracker statuses.</li><li><strong>Pre-file the f9 checkout-summary REQ</strong> (VAT line, redirect_url, idempotency header echo) so b-side work can precede the f9 build.</li></ol>
</main></body></html>