122 lines
31 KiB
HTML
122 lines
31 KiB
HTML
<!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 f9–f15 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 (f0–f8), 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 (b0–b15), 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 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).</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<T>(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). S–M 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>). S–M 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 f9–f15 will hit</h2>
|
||
<p>Frontend phases f0–f8 are built (reports exist); <strong>f9–f15 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> |