frontend phase 9
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Post-phase server audit — 2026-07-10
|
||||
|
||||
A read-only audit of the completed backend chain (backend-phase-0 → 15), produced after the final backend
|
||||
phase shipped. Three deliverables, each in canonical Markdown with a matching self-contained HTML view
|
||||
(`index.html` is the browsable entry point):
|
||||
|
||||
| Deliverable | What it answers |
|
||||
| --- | --- |
|
||||
| [post-phase-backend-plan.md](post-phase-backend-plan.md) · [html](post-phase-backend-plan.html) | What backend work remains — 8 prioritized, runnable "post-phases" (security hygiene → money-path fixes → contract batch → scheduler/Redis → trust rails → money rails → observability → later) |
|
||||
| [frontend-backend-gaps.md](frontend-backend-gaps.md) · [html](frontend-backend-gaps.html) | REQ-001…015 reconciled against the shipped contract + code: 2 done, 1 doc-fix, 12 missing; plus what unbuilt f9–f15 will hit, and the unblock priority |
|
||||
| [runtime-services.md](runtime-services.md) · [html](runtime-services.html) | The deployment topology: 17 services/rails derived from the seams + config, a dependency graph, per-service defaults/config keys/health notes |
|
||||
|
||||
**Executive summary.** The chain is genuinely complete against its own specs — 358 green tests, and the
|
||||
load-bearing invariants (balanced ledger groups, four money DB CHECKs, webhook idempotency, tenancy 404s,
|
||||
forward-only status machines) all verifiably exist in code. The API's only real external dependency today
|
||||
is SQL Server; all 18 vendor/infra seams are deterministic in-process mocks, which is the designed MVP
|
||||
posture. What the audit surfaced beyond that design: **(1)** committed live credentials — a real `sa`
|
||||
connection string in `appsettings*.json`, placeholder JWE/encryption keys, and a seeded `admin`/`qw123321`
|
||||
user — that block any deployment; **(2)** one genuine money-correctness hole — the BNPL/manual refund
|
||||
settlement path is unreachable (`Refund.MarkSucceededAsync` has zero callers), so those refunds strand
|
||||
`refund_payable`/`escrow_held` forever; **(3)** the frontend is still 11/12 domains mock-primary because 12
|
||||
of its 15 filed REQs were never delivered and none were answered; **(4)** unattended operation doesn't
|
||||
exist yet — payout batches, credential-expiry scans, no-show sweeps, and Moadian reconciliation are
|
||||
admin-click-only while their cadence config keys sit unread; and **(5)** the promised forward-dep FKs
|
||||
(refunds→tickets, clawbacks→payouts, invoices→partner_centers) were never added after their target tables
|
||||
shipped. Full evidence and the fix-by-fix plan are in the three documents.
|
||||
@@ -0,0 +1,122 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,250 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Post-phase server audit — 2026-07-10</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>
|
||||
|
||||
<h1 id="post-phase-server-audit-2026-07-10">Post-phase server audit — 2026-07-10</h1><p class="stamp">Generated from the canonical Markdown — do not hand-edit. Audit date 2026-07-10.</p>
|
||||
<p>A read-only audit of the completed backend chain (backend-phase-0 → 15), produced after the final backend phase shipped. Three deliverables, each in canonical Markdown with a matching self-contained HTML view (<code>index.html</code> is the browsable entry point):</p>
|
||||
<div class="tblwrap"><table><thead><tr><th>Deliverable</th><th>What it answers</th></tr></thead><tbody><tr><td><a href="post-phase-backend-plan.html">post-phase-backend-plan.md</a> · <a href="post-phase-backend-plan.html">html</a></td><td>What backend work remains — 8 prioritized, runnable "post-phases" (security hygiene → money-path fixes → contract batch → scheduler/Redis → trust rails → money rails → observability → later)</td></tr><tr><td><a href="frontend-backend-gaps.html">frontend-backend-gaps.md</a> · <a href="frontend-backend-gaps.html">html</a></td><td>REQ-001…015 reconciled against the shipped contract + code: 2 done, 1 doc-fix, 12 missing; plus what unbuilt f9–f15 will hit, and the unblock priority</td></tr><tr><td><a href="runtime-services.html">runtime-services.md</a> · <a href="runtime-services.html">html</a></td><td>The deployment topology: 17 services/rails derived from the seams + config, a dependency graph, per-service defaults/config keys/health notes</td></tr></tbody></table></div>
|
||||
<p><strong>Executive summary.</strong> The chain is genuinely complete against its own specs — 358 green tests, and the load-bearing invariants (balanced ledger groups, four money DB CHECKs, webhook idempotency, tenancy 404s, forward-only status machines) all verifiably exist in code. The API's only real external dependency today is SQL Server; all 18 vendor/infra seams are deterministic in-process mocks, which is the designed MVP posture. What the audit surfaced beyond that design: <strong>(1)</strong> committed live credentials — a real <code>sa</code> connection string in <code>appsettings*.json</code>, placeholder JWE/encryption keys, and a seeded <code>admin</code>/<code>qw123321</code> user — that block any deployment; <strong>(2)</strong> one genuine money-correctness hole — the BNPL/manual refund settlement path is unreachable (<code>Refund.MarkSucceededAsync</code> has zero callers), so those refunds strand <code>refund_payable</code>/<code>escrow_held</code> forever; <strong>(3)</strong> the frontend is still 11/12 domains mock-primary because 12 of its 15 filed REQs were never delivered and none were answered; <strong>(4)</strong> unattended operation doesn't exist yet — payout batches, credential-expiry scans, no-show sweeps, and Moadian reconciliation are admin-click-only while their cadence config keys sit unread; and <strong>(5)</strong> the promised forward-dep FKs (refunds→tickets, clawbacks→payouts, invoices→partner_centers) were never added after their target tables shipped. Full evidence and the fix-by-fix plan are in the three documents.</p>
|
||||
</main></body></html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,601 @@
|
||||
# Post-development backend plan — fixes & improvements
|
||||
|
||||
**Audit date:** 2026-07-10 · **Scope:** the completed backend chain (backend-phase-0 → 15) under `server/`
|
||||
· **Method:** read-only audit of code + `dev/` docs; every finding cites the file/line it was verified at.
|
||||
Companion documents: [frontend-backend-gaps.md](frontend-backend-gaps.md) (the REQ-by-REQ contract
|
||||
reconciliation) and [runtime-services.md](runtime-services.md) (the deployment topology).
|
||||
|
||||
The chain is genuinely complete against its own specs: all 16 backend phases shipped, 358 tests are green,
|
||||
and the load-bearing invariants (balanced ledger, DB CHECKs, idempotency uniques, tenancy 404s, forward-only
|
||||
status machines) verifiably exist in code. What remains falls into eight coherent "post-phases", ordered so
|
||||
that each is a runnable unit of work: deployment blockers first, then a code-level money-correctness fix,
|
||||
then the contract batch that unblocks the frontend lane, then the infrastructure and vendor swaps the seam
|
||||
architecture was built for.
|
||||
|
||||
| Bucket | Theme | Blocking what? |
|
||||
| --- | --- | --- |
|
||||
| post-phase-1 | Security & config hygiene | Any non-local deployment |
|
||||
| post-phase-2 | Money-path correctness completion | Ledger ⇄ bank reconciliation |
|
||||
| post-phase-3 | Frontend-unblock contract batch | 11 of 12 client domains are still mock-primary |
|
||||
| post-phase-4 | Scheduling, locking & multi-instance readiness | Unattended operation; >1 API instance |
|
||||
| post-phase-5 | Identity & trust rails go real | Real nurses onboarding (OTP, KYC, docs) |
|
||||
| post-phase-6 | Money rails go real | Real payments, payouts, tax |
|
||||
| post-phase-7 | Observability, audit & ops hardening | Production diagnosability |
|
||||
| post-phase-8 | Scale & later | Search scale, analytics, doc debt |
|
||||
|
||||
Status legend used below — **Current state** always cites what the code does *today*.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-1 — Security & config hygiene (do before anything is deployed)
|
||||
|
||||
Everything in this bucket is small (S) and none of it changes behavior — but each item is a deployment
|
||||
blocker, and two of them are live credential leaks sitting in git today.
|
||||
|
||||
### 1.1 Rotate and remove the committed SQL Server `sa` connection string
|
||||
|
||||
- **Why:** `appsettings.json` and `appsettings.Development.json` both commit a real connection string —
|
||||
public IP `87.107.152.16`, login `sa`, plaintext password — for the app DB *and* the log DB. Anyone with
|
||||
repo access owns the database (all PII ciphertext + the encryption keys sit in the same repo, see 1.2).
|
||||
This directly violates root `CLAUDE.md` working agreement #6 ("Never commit secrets").
|
||||
- **Current state:** `server/src/API/Baya.Web.Api/appsettings.json:3-4` and
|
||||
`appsettings.Development.json:3-4` (byte-identical files); consumed at
|
||||
`server/src/Infrastructure/Baya.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:41`
|
||||
and by the Serilog sink at
|
||||
`server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs:44`.
|
||||
- **Change:** rotate the `sa` password on that server (assume compromised); create a least-privilege app
|
||||
login; move both connection strings to user-secrets (dev) / environment variables (deploy); commit only a
|
||||
placeholder. Consider `git filter-repo` history scrubbing, and add a secret-scanning pre-commit hook.
|
||||
- **Files/layers:** `appsettings*.json` only (config).
|
||||
- **Effort:** S · **Risk:** low (config move) · **Deps:** none. **Do this first.**
|
||||
|
||||
### 1.2 Replace placeholder JWE signing/encryption keys and field-encryption keys
|
||||
|
||||
- **Why:** the JWT/JWE `SecretKey`/`Encryptkey` are starter-template placeholders and the PII
|
||||
field-encryption keys are committed `local-dev-…-change-me` strings. If these defaults reach any shared
|
||||
environment, every token is forgeable and every encrypted PII column is decryptable. The access-token
|
||||
lifetime is also ~7 days (`ExpirationMinutes: 10000`) and `RequireHttpsMetadata = false`.
|
||||
- **Current state:** `server/src/API/Baya.Web.Api/appsettings.json:7-8` (keys), `:12` (expiry);
|
||||
`Seams:FieldEncryption` at `appsettings.json:15-18`; the JWE decryption key is wired at
|
||||
`server/src/Infrastructure/Baya.Infrastructure.Identity/ServiceConfiguration/ServiceCollectionExtension.cs:135`
|
||||
and `RequireHttpsMetadata = false` at `:139`. `SymmetricFieldEncryptor` derives a real AES-256-CBC key
|
||||
from whatever string is configured
|
||||
(`server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/SymmetricFieldEncryptor.cs:25`) — the
|
||||
crypto is fine; the key *management* is dev-grade (mocks-registry row `IFieldEncryptor` 🟡 agrees).
|
||||
- **Change:** per-environment secrets (env vars / Key Vault / KMS per the registry's "make it real"), a
|
||||
sane access-token lifetime (≤ 60 min; refresh flow already exists), `RequireHttpsMetadata = true` outside
|
||||
Development, real `Issuer`/`Audience` values (currently `"MyWebsite"`). Note: rotating the field key
|
||||
requires a re-encryption migration for existing rows — do it before real PII exists.
|
||||
- **Effort:** S (config) + M if key-rotation tooling is wanted · **Risk:** medium (existing dev-DB
|
||||
ciphertext becomes unreadable — acceptable pre-launch) · **Deps:** 1.1.
|
||||
|
||||
### 1.3 Remove or environment-gate the seeded `admin` / `qw123321` user
|
||||
|
||||
- **Why:** every non-Testing boot creates a well-known admin account with a hardcoded weak password and the
|
||||
full admin role — in production too.
|
||||
- **Current state:** `server/src/Infrastructure/Baya.Infrastructure.Identity/Identity/SeedDatabaseService/SeedDataBase.cs:48`
|
||||
(`CreateAsync(user, "qw123321")`), invoked from `server/src/API/Baya.Web.Api/Program.cs:102`.
|
||||
- **Change:** read the bootstrap admin credentials from configuration and only seed when explicitly
|
||||
configured (or Development-only); force a password change on first login.
|
||||
- **Effort:** S · **Risk:** low · **Deps:** none.
|
||||
|
||||
### 1.4 Environment-gate the auto-seeded sandbox ZarinPal payment gateway
|
||||
|
||||
- **Why:** `SeedPaymentGatewaysAsync` idempotently inserts an **active** sandbox ZarinPal gateway row
|
||||
(all-zeros merchant id) on every boot — a production DB would silently contain an active sandbox money
|
||||
gateway.
|
||||
- **Current state:** `server/src/API/Baya.Web.Api/Program.cs:103` →
|
||||
`server/src/Infrastructure/Baya.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:106-117`
|
||||
(sandbox row, `config_json` encrypted via the DbContext converter).
|
||||
- **Change:** seed only in Development/Testing, or seed `is_active = false` and require an admin to
|
||||
activate a real gateway (`payment_gateways` is already admin data).
|
||||
- **Effort:** S · **Risk:** low · **Deps:** none; pairs with 6.1.
|
||||
|
||||
### 1.5 Fix the Kestrel HTTP/2-only default
|
||||
|
||||
- **Why:** `Kestrel:EndpointDefaults:Protocols = "Http2"` (set for the gRPC plugin) makes every endpoint
|
||||
HTTP/2-only. Browsers can't speak h2c, so any non-TLS hop — container health probes, an HTTP/1.1 reverse
|
||||
proxy leg, plain-HTTP Swagger — breaks. Locally it only works because `https://localhost:5002` negotiates
|
||||
via ALPN.
|
||||
- **Current state:** `server/src/API/Baya.Web.Api/appsettings.json:29-33` (both files). The gRPC plugin that
|
||||
motivated it serves a single duplicate OTP/token service
|
||||
(`server/src/API/Plugins/Baya.Web.Plugins.Grpc/Services/UserGrpcServices.cs:18`).
|
||||
- **Change:** default `Http1AndHttp2`; give gRPC its own `Http2` endpoint if kept (see 7.5 for the
|
||||
keep-or-remove decision).
|
||||
- **Effort:** S · **Risk:** low · **Deps:** none.
|
||||
|
||||
### 1.6 Make rate limiting proxy-aware and align the two payment webhooks
|
||||
|
||||
- **Why:** all rate-limit partitions key on `RemoteIpAddress` and no `ForwardedHeaders` middleware is
|
||||
registered — behind any reverse proxy every client shares one 100 req/min bucket (self-DoS). Separately,
|
||||
the two webhook siblings disagree: the BNPL webhook runs the 20/min `sensitive` policy while the card
|
||||
webhook has only the global 100/min fallback — one of them is wrong on purpose or both by accident.
|
||||
- **Current state:** partition key at
|
||||
`server/src/API/Baya.WebFramework/ServiceConfiguration/RateLimitingServiceExtension.cs:69`; global limiter
|
||||
`:35`; `Program.cs` has no `UseForwardedHeaders` (checked `server/src/API/Baya.Web.Api/Program.cs`).
|
||||
Webhooks: `Controllers/V1/WebhooksBnplController.cs:28` (`sensitive`) vs `Controllers/V1/WebhooksController.cs:25`
|
||||
(`[AllowAnonymous]`, global only).
|
||||
- **Change:** add `ForwardedHeaders` middleware (trusting only the known proxy), partition on the resolved
|
||||
client IP, and pick one deliberate webhook policy (PSP callbacks are bursty — a dedicated `webhook`
|
||||
policy keyed per-provider is safer than `sensitive`).
|
||||
- **Effort:** S · **Risk:** low–medium (limiter behavior changes) · **Deps:** deployment topology decision.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-2 — Money-path correctness completion
|
||||
|
||||
The ledger invariants verified clean (balanced groups throw at
|
||||
`server/src/Core/Baya.Domain/Entities/Payments/LedgerPosting.cs:26,65,178,204`; the four DB CHECKs exist —
|
||||
`CK_Bookings_AmountSplit`, `CK_NursePayouts_NetSplit`, `CK_Refunds_LegSplit`, `CK_BnplTransactions_SettleSplit`
|
||||
— per the EF configs and `Migrations/ApplicationDbContextModelSnapshot.cs:345,3543,3842,206`). This bucket
|
||||
closes the holes *around* those invariants.
|
||||
|
||||
### 2.1 Wire the unreachable BNPL/manual refund settlement (dead-end money state) — **top code fix**
|
||||
|
||||
- **Why:** a card refund posts its `refund_payable ↔ escrow_held` clearing immediately. A BNPL-revert or
|
||||
manual-bank refund is left in `processing` with the clearing "deferred to reconciliation" — but **no
|
||||
reconciliation path exists anywhere**: `Refund.MarkSucceededAsync` has zero call sites, no admin
|
||||
endpoint/webhook/job performs `processing → succeeded`, and `LedgerPosting.RefundPayableClearing` has
|
||||
exactly one call site (the immediate card path). Every BNPL/manual refund permanently overstates
|
||||
`escrow_held` and strands `refund_payable` — the ledger will never reconcile with the bank.
|
||||
- **Current state:** `server/src/Core/Baya.Domain/Entities/Refunds/Refund.cs:105` (uncalled);
|
||||
`server/src/Core/Baya.Application/Features/Refunds/Commands/CreateRefund/CreateRefundCommand.Handler.cs:139-143`
|
||||
(clearing only when already `Succeeded`); `:226-231` (BNPL/manual → `MarkProcessing`, never succeeded);
|
||||
the `Processing → Succeeded` edge exists unused in
|
||||
`server/src/Core/Baya.Domain/Entities/Refunds/RefundTransitions.cs:16`; admin surface is create+list only
|
||||
(`Controllers/V1/AdminRefundsController.cs:33`). The b11 handoff promised at least a manual trigger
|
||||
(`dev/shared-working-context/backend/handoff/after-backend-phase-11.md:37`).
|
||||
- **Change:** a `ConfirmRefundSettlementCommand` (admin `POST admin_refunds/{id}/confirm_settlement`, plus a
|
||||
BNPL-callback branch when the provider confirms customer cash-back) that transitions
|
||||
`processing → succeeded`, stamps `settled_at`, and posts `LedgerPosting.RefundPayableClearing` in the same
|
||||
commit; a `mark_failed` counterpart. Tests for both channels.
|
||||
- **Files/layers:** Application `Features/Refunds/`, API `AdminRefundsController`, Domain (rename
|
||||
`MarkSucceededAsync` — it's not async), tests.
|
||||
- **Effort:** M · **Risk:** medium (money path — but additive) · **Deps:** none; do before real BNPL (6.2).
|
||||
|
||||
### 2.2 Add the promised-but-missing FKs on the forward-dep columns
|
||||
|
||||
- **Why:** b11 created `refunds.ticket_id`, `nurse_clawbacks.original_payout_id` /
|
||||
`recovered_in_payout_id`, and `invoices.partner_center_id` as FK-less nullable columns "until the target
|
||||
table ships". The targets all shipped (b13 `nurse_payouts`, b15 `tickets`/`partner_centers`) and the
|
||||
**values** are wired, but no phase added the constraints — referential integrity rests on application
|
||||
discipline, and the config comments are now false.
|
||||
- **Current state:** stale comment "no FK yet (tickets does not exist)" at
|
||||
`server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/RefundsConfig/RefundConfig.cs:41`;
|
||||
`NurseClawbackConfig.cs:12` (comment says b13 "wires the FKs" — it didn't; b13's `NursePayoutEngine`
|
||||
migration touches no clawback FK); `InvoicesConfig/InvoiceConfig.cs:11` (`partner_center_id` has **no FK
|
||||
and no index**); the b15 migration adds only `FK_NurseProfiles_PartnerCenters_PartnerCenterId`
|
||||
(`Migrations/20260709232741_MessagingAndPartnerCenters.cs:293`). Value-side wiring confirmed:
|
||||
`CreateRefundCommand.Handler.cs:53-61` (auto-ticket), `Refunds/NurseClawback.cs:50` (`MarkRecovered`),
|
||||
`IssueInvoiceCommand.Handler.cs:63-64` (issuer/center).
|
||||
- **Change:** one additive migration adding the three FK sets (`ON DELETE NO ACTION`) + an index on
|
||||
`invoices.partner_center_id`; update the three config comments.
|
||||
- **Effort:** S · **Risk:** low (data is young; verify no orphans first) · **Deps:** none.
|
||||
|
||||
### 2.3 Extend `IAuditable` to the admin-decided money & trust entities
|
||||
|
||||
- **Why:** the append-only `audit_logs` diff interceptor covers exactly three entities — `PlatformConfig`,
|
||||
`PartnerCenter`, `Review`. Admin decisions on refunds (approve/reject), payouts (process/retry/fail), and
|
||||
**nurse verification** (the trust-critical `is_verified` flip / suspend) leave no audit-diff row. For a
|
||||
trust-first escrow platform these are precisely the actions an auditor asks about. (The ledger itself is
|
||||
fine — append-only by construction, `LedgerEntry` is `IEntity`-only at
|
||||
`server/src/Core/Baya.Domain/Entities/Payments/LedgerEntry.cs:13`.)
|
||||
- **Current state:** the three implementors — `Domain/Entities/Configuration/PlatformConfig.cs:12`,
|
||||
`Domain/Entities/PartnerCenters/PartnerCenter.cs:16`, `Domain/Entities/Reviews/Review.cs:15`. `Refund`
|
||||
(`Domain/Entities/Refunds/Refund.cs:77`) and `NursePayout` (`Domain/Entities/Payouts/NursePayout.cs:62`)
|
||||
are not `IAuditable`; `NurseVerification` isn't either.
|
||||
- **Change:** add `IAuditable` to `Refund`, `NurseClawback`, `NursePayout`, `NursePayoutBatch`,
|
||||
`NurseVerification` (the interceptor at `Persistence/Interceptors/AuditFieldInterceptor` already handles
|
||||
any `IAuditable`); confirm `[AuditRedacted]` covers `iban_snapshot` before enabling.
|
||||
- **Effort:** S–M · **Risk:** low (write-volume growth on `audit_logs`; see 7.4 archival) · **Deps:** none.
|
||||
|
||||
### 2.4 Close the refund channel-execute-before-commit crash window
|
||||
|
||||
- **Why:** `CreateRefundCommand` executes the external channel call (PSP refund / BNPL revert) **before**
|
||||
the first DB commit — a crash between provider success and commit loses the record of an executed refund.
|
||||
The idempotency key means a *retry* won't double-refund, but nothing retries automatically and no record
|
||||
exists to reconcile against.
|
||||
- **Current state:** `Features/Refunds/Commands/CreateRefund/CreateRefundCommand.Handler.cs:106-111`
|
||||
(channel executes), commit later in the same handler; idempotency key at `:85`.
|
||||
- **Change:** persist the refund row in `pending` state (commit) *before* the channel call, then execute and
|
||||
update — the standard two-phase intent/confirm shape the webhook handler already uses
|
||||
(claim-key-first at `Features/Payments/Commands/HandlePaymentWebhook/HandlePaymentWebhookCommand.Handler.cs:76-88`).
|
||||
- **Effort:** M · **Risk:** medium (touches the refund state machine; full test pass required) · **Deps:**
|
||||
do together with 2.1.
|
||||
|
||||
### 2.5 Test the untested admin money paths
|
||||
|
||||
- **Why:** `WriteOffClawbackCommand` — an admin action that posts a `bad_debt` ledger group — has **zero
|
||||
tests** (grep across `server/src/Tests` finds no `WriteOff` match). The webhook duplicate-race path
|
||||
(`DbUpdateException` on a concurrent same-key insert) is only exercised sequentially. Messaging has no
|
||||
Foundation-level handler tests (the `is_internal` boundary is covered only end-to-end in
|
||||
`Tests/Baya.Test.Api/MessagingApiTests.cs:33`).
|
||||
- **Current state:** handler at
|
||||
`Features/Refunds/Commands/WriteOffClawback/WriteOffClawbackCommand.Handler.cs:26`; coverage inventory:
|
||||
49 Foundation + 33 Api test files; payout retry / webhook replay / clawback fork / rebuild convergence
|
||||
**are** tested (`Tests/Baya.Test.Foundation/Payouts/PayoutHandlerTests.cs:211`,
|
||||
`Payments/PaymentWebhookTests.cs:64`, `Refunds/RefundHandlerTests.cs:81`, `Search/SearchIndexTests.cs:184`).
|
||||
- **Change:** add write-off unit + API tests (balanced group, idempotency, 404/409 paths), a true racing
|
||||
webhook-insert test, and Foundation tests for `PostMessage`/`GetTicketThread` internal-flag handling.
|
||||
- **Effort:** S–M · **Risk:** none · **Deps:** none.
|
||||
|
||||
### 2.6 Retire the orphaned `refund_ticket_required` config key
|
||||
|
||||
- **Why:** b15 superseded the config-gated rule by unconditionally auto-opening a refund ticket, so the
|
||||
seeded key has zero production consumers and a now-false description ("Off until b15 ships the tickets
|
||||
table") — a small honesty debt that will mislead an operator.
|
||||
- **Current state:** seed row at
|
||||
`Persistence/Configuration/ConfigurationConfig/PlatformConfigConfig.cs:49`; auto-open at
|
||||
`CreateRefundCommand.Handler.cs:53-61`; only remaining reference is a test stub
|
||||
(`Tests/Baya.Test.Foundation/Refunds/RefundsTestHost.cs:174`).
|
||||
- **Change:** delete the seed row (migration) or repurpose it to gate whether a *customer-visible* ticket is
|
||||
required; update the description either way.
|
||||
- **Effort:** S · **Risk:** low · **Deps:** none.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-3 — Frontend-unblock contract batch
|
||||
|
||||
Full field-level detail, evidence, and priority ordering live in
|
||||
[frontend-backend-gaps.md](frontend-backend-gaps.md). Summary: of the 15 filed REQs, **REQ-001 and
|
||||
REQ-015 are effectively done, REQ-010 is a doc fix, and the other 12 are undelivered**; 11 of the client's
|
||||
12 service domains still default to mock-primary, most gated on exactly these items. This bucket is one
|
||||
backend phase-sized batch of small DTO/endpoint additions:
|
||||
|
||||
- **3.1 Booking-surface fields (S):** `variantPrice` on `BookingRequestDto`, `variantLabel` (+ optional
|
||||
`patientAge`) on `BookingRequestListItemDto` (REQ-013/014).
|
||||
- **3.2 Identity/profile fields (S–M):** patient `relation` + `conditions` (REQ-005); customer
|
||||
name/preferred-language upsert (REQ-007); avatar upload endpoint + `avatarUrl` (REQ-006 — the only item
|
||||
needing multipart + `IObjectStorage`).
|
||||
- **3.3 Address fields (S):** accept the client map pin on create/update (REQ-008 — matters for EVV
|
||||
accuracy later) + `provinceId` on `CustomerAddressDto` (REQ-009).
|
||||
- **3.4 Search & public profile (M — the single highest-leverage item):** enrich `NurseSearchResultDto`
|
||||
with `nurseName`/`avatarUrl` (+ optional `distanceKm`) and add the aggregated public
|
||||
`GET nurses/{id}/profile` (REQ-012). Unblocks the discovery funnel (C2/C3).
|
||||
- **3.5 Verification details (S–M):** nurse-facing `credential_details` command + `isRequired` on
|
||||
`VerificationStepDto` (REQ-011 — without it the INO number/specialties are silently dropped).
|
||||
- **3.6 Auth polish (S):** `codeLength`/`expiresInSeconds` on `RequestOtpResult` (REQ-002); machine-readable
|
||||
`code` (+ `retryAfterSeconds`) on OTP failures (REQ-003 — needs a small `OperationResult`/envelope
|
||||
extension, the only cross-cutting piece).
|
||||
- **3.7 Zero-code confirmations & doc fixes (S):** answer REQ-001/004/015 in the tracker; fix the stale
|
||||
`page_size` occurrences in `dev/contracts/domains/*.md` (REQ-010 — the server verifiably binds camelCase
|
||||
`pageSize`); mark every delivered REQ `delivered in …` (all 15 currently read `Status: open`).
|
||||
|
||||
**Effort:** one M-sized phase overall · **Risk:** low (additive DTO fields; regenerate
|
||||
`dev/contracts/openapi/swagger.v1.json` after) · **Deps:** none — can run in parallel with post-phase-1/2.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-4 — Scheduling, locking & multi-instance readiness
|
||||
|
||||
### 4.1 Real job scheduler + register the four deferred crons
|
||||
|
||||
- **Why:** only two recurring jobs exist (booking-request expiry every 1 min, notification retention every
|
||||
24 h). The verification credential-expiry scan, the EVV no-show sweep, the **weekly payout batch**, and
|
||||
the Moadian reconciliation poll are admin-manual-only — their cadence config keys are seeded but nothing
|
||||
reads them on a schedule. Operationally today: credentials never re-expire, no-shows are never flagged,
|
||||
and **nurses are not paid unless an operator clicks**.
|
||||
- **Current state:** the two hosted services at
|
||||
`Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:63,67` (intervals hardcoded at
|
||||
`Services/Notifications/NotificationRetentionHostedService.cs:19` and
|
||||
`Services/Booking/BookingRequestExpiryHostedService.cs:21`). Manual triggers:
|
||||
`Controllers/V1/AdminVerificationsController.cs:49` (`scan_expiring`), `AdminEvvController.cs:37`
|
||||
(`detect_no_shows`), `AdminPayoutsController.cs:44` (batch generate). Unconsumed cadence keys:
|
||||
`verification_expiry_scan_cadence_hours`, `no_show_scan_cadence_hours`, `nurse_payout_interval_days`
|
||||
(`PlatformConfigConfig.cs:46,48,36`). Note: **no `IJobScheduler` interface exists** — the registry row is
|
||||
aspirational naming; there is nothing to swap behind, only hosted services to re-home. No
|
||||
Hangfire/Quartz package is referenced (`server/Directory.Packages.props` — verified absent).
|
||||
- **Change:** adopt Hangfire (SQL Server storage — no new infra) or Quartz; move the two existing sweeps
|
||||
and add recurring jobs for expiry-scan, no-show, payout-batch generation (+ 2.1's reconciliation and 6.5's
|
||||
Moadian poll), each reading its seeded cadence key; keep the admin manual triggers as overrides. Payout
|
||||
processing (money-moving) can stay human-approved — schedule *generation*, keep `process` manual until
|
||||
trust is earned.
|
||||
- **Effort:** M–L · **Risk:** medium (new infra dependency in-app; jobs must stay idempotent — they already
|
||||
are by design) · **Deps:** none hard; pairs with 4.2 for multi-instance.
|
||||
|
||||
### 4.2 Redis for `ICacheService` + `IDistributedLock`
|
||||
|
||||
- **Why:** both are in-process today. Cache: fine single-instance, silently wrong (stale geo/catalog/config
|
||||
reads, generation-token invalidation not shared) the moment a second instance runs. Lock: the money-path
|
||||
mutex (`booking:{id}:payment` / `:refund`) is a per-key `SemaphoreSlim` — no cross-instance protection
|
||||
(DB uniques remain the correctness backstop, as designed, but the lock is doing nothing across nodes).
|
||||
- **Current state:** `CrossCutting/Seams/MemoryCacheService.cs:11` and `InProcessDistributedLock.cs:14-22`,
|
||||
registered at `CrossCutting/ServiceConfiguration/ServiceCollectionExtension.cs:27,61`. No Redis package
|
||||
referenced.
|
||||
- **Change:** add `StackExchange.Redis`; `RedisCacheService` (same key/TTL scheme) +
|
||||
`RedisDistributedLock` (SET NX PX + token-checked release, lease ≥ the longest money handler);
|
||||
config-selected registration per the registry's make-it-real steps (rows `ICacheService`,
|
||||
`IDistributedLock`).
|
||||
- **Effort:** M · **Risk:** medium (lock semantics under expiry; keep DB uniques authoritative) ·
|
||||
**Deps:** Redis service (see runtime-services.md).
|
||||
|
||||
### 4.3 Separate migrations from boot (multi-instance + least privilege)
|
||||
|
||||
- **Why:** every non-Testing boot runs `MigrateAsync` + three seeders — concurrent instance start-ups race
|
||||
on DDL (no distributed lock exists at boot), and the app login needs permanent DDL rights.
|
||||
- **Current state:** `server/src/API/Baya.Web.Api/Program.cs:99-104`;
|
||||
`Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:92` (`MigrateAsync` unconditional).
|
||||
- **Change:** a deploy-time migration step (`dotnet ef database update` in CI, or a `--migrate` one-shot
|
||||
mode) and boot-time schema *check* instead of apply; seeders become idempotent deploy steps.
|
||||
- **Effort:** S–M · **Risk:** low · **Deps:** CI/CD pipeline exists.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-5 — Identity & trust rails go real
|
||||
|
||||
Ordered by user impact: nobody can log in without 5.1.
|
||||
|
||||
### 5.1 Real SMS gateway behind `ISmsSender` — **launch-critical**
|
||||
|
||||
- **Why:** OTP delivery is a log statement; no real user can ever log in. This is the single seam standing
|
||||
between the platform and its first real session.
|
||||
- **Current state:** `CrossCutting/Seams/LoggingSmsSender.cs:12-16` (logs the code, phone last-4),
|
||||
registered at `ServiceCollectionExtension.cs:32`. No `Seams:Sms` options exist yet (verified:
|
||||
`SeamOptions.cs` has no Sms group).
|
||||
- **Change:** per registry row `ISmsSender`: pick Kavenegar/Ghasedak/SMS.ir, add `Seams:Sms:{ApiKey,
|
||||
SenderLine,BaseUrl}` options + package, implement pattern/template OTP send, config-selected
|
||||
registration. Keep the per-phone resend window and `otp` rate policy untouched.
|
||||
- **Effort:** M · **Risk:** low (isolated seam) · **Deps:** vendor account.
|
||||
|
||||
### 5.2 Real Shahkar + e-KYC vendors (`IShahkarVerifier`, `IIdentityKycProvider`)
|
||||
|
||||
- **Why:** nurse verification currently passes any well-formed input (mock passes everything except two
|
||||
configured magic values) — the trust engine's automated steps assert nothing real.
|
||||
- **Current state:** `CrossCutting/Seams/MockShahkarVerifier.cs:17-37`, `MockIdentityKycProvider.cs:17-25`;
|
||||
registrations `:45-46`; config defaults `SeamOptions.cs:157-184`. The pipeline already persists
|
||||
`external_response_json` and handles shared-SIM/mismatch as explicit states — the handler side is ready.
|
||||
- **Change:** per registry rows: one Finnotech-style bridge client for both استعلامها; keep shared-SIM as a
|
||||
handled failure; persist real vendor refs. The phone-change re-trigger already works
|
||||
(`ShahkarVerifiedAt` reset on phone change — b2).
|
||||
- **Effort:** M–L (vendor onboarding dominates) · **Risk:** medium (real-world failure modes) · **Deps:**
|
||||
vendor contract; 5.1 not required but sensible first.
|
||||
|
||||
### 5.3 Real استعلام شبا (`IBankAccountOwnershipVerifier`)
|
||||
|
||||
- **Why:** the b13 first-payout gate (`matched_national_id = 1`) is currently satisfied by a mock that
|
||||
matches every IBAN except one magic value — real money would flow against unverified account ownership.
|
||||
- **Current state:** `CrossCutting/Seams/MockBankAccountOwnershipVerifier.cs:17-26` (nurseNationalId
|
||||
deliberately ignored, comment `:21-22`); registration `:36`; defaults `SeamOptions.cs:204`.
|
||||
- **Change:** per registry row; verify the payout gate end-to-end (`is_primary=1 AND is_verified=1 AND
|
||||
matched_national_id=1` skip-with-reason path already exists in b13).
|
||||
- **Effort:** M · **Risk:** medium (money gate) · **Deps:** same vendor family as 5.2 — bundle them.
|
||||
|
||||
### 5.4 Real geocoder (`IGeocoder`) behind Neshan
|
||||
|
||||
- **Why:** address coordinates (and therefore the EVV distance check) are deterministic fakes jittered ±5 km
|
||||
around 8 hardcoded city centroids — EVV mismatch alerts are currently noise. Accepting the client pin
|
||||
(REQ-008, post-phase-3) reduces but doesn't remove the need.
|
||||
- **Current state:** `CrossCutting/Seams/MockGeocoder.cs:15-52`; registration `:40`; `Seams:Geocoding` is
|
||||
one of only three seam sections present in `appsettings.json:22-26`.
|
||||
- **Change:** per registry row (Neshan client, rate-limit/retry, keep the null-coordinate path).
|
||||
- **Effort:** M · **Risk:** low · **Deps:** REQ-008 first (pin > geocode for EVV).
|
||||
|
||||
### 5.5 Real object storage (`IObjectStorage`) — MinIO/S3/ArvanCloud
|
||||
|
||||
- **Why:** verification documents (and future avatars/invoice PDFs) live on the API host's local disk under
|
||||
a temp root — non-durable, non-shared, and `GetUrl` returns a `file://` URI rather than a presigned URL,
|
||||
so the b6 "short-lived signed URL" contract is only shape-deep.
|
||||
- **Current state:** `CrossCutting/Seams/LocalDiskObjectStorage.cs:12-56` (temp-dir fallback `:20`,
|
||||
`file://` URL); `Seams:ObjectStorage:RootPath` empty in `appsettings.json:19-21`.
|
||||
- **Change:** per registry row: S3-compatible client, presigned PUT/GET with expiry, bucket + creds from
|
||||
config; migrate any existing dev files or reset.
|
||||
- **Effort:** M · **Risk:** low · **Deps:** storage service; REQ-006 (avatar) builds on this.
|
||||
|
||||
### 5.6 Accept manual as the real path for MoH/INO credentials and partner licensing (document, don't build)
|
||||
|
||||
- **Why:** `ICredentialVerifier` and `ILicenseVerificationService` return "needs manual review" by design —
|
||||
there is **no public MoH/INO/eNamad B2B API** today. The admin review flows are the real mechanism; the
|
||||
seams exist so a portal API can slot in if one appears.
|
||||
- **Current state:** `CrossCutting/Seams/MockCredentialVerifier.cs:18`,
|
||||
`MockLicenseVerificationService.cs:26`; registry rows agree ("no public B2B API today").
|
||||
- **Change:** none in code. Mark these 🟡 rows as "manual = intended MVP state" in the registry so they stop
|
||||
reading as debt.
|
||||
- **Effort:** S (docs) · **Risk:** none.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-6 — Money rails go real
|
||||
|
||||
The seam shapes are faithful (idempotency keys, server-side re-verify, upsert-first webhooks are already the
|
||||
handler behavior — verified at `HandlePaymentWebhookCommand.Handler.cs:31-88`), so each swap is an adapter,
|
||||
not a redesign. **Do 2.1 first** so the BNPL refund path is complete before real money uses it.
|
||||
|
||||
### 6.1 Real PSP/IPG + webhook signatures + تسهیم (`IPaymentProvider`, `IWebhookVerifier`, `ISettlementSplitProvider`)
|
||||
|
||||
- **Why:** card capture, callback authenticity, and the settlement split are all deterministic mocks
|
||||
(`VerifyAsync` always succeeds echoing the expected amount; any callback is "validly signed" unless it
|
||||
contains a magic marker; any balanced split "settles"). The legal تسهیم model (provider splits to
|
||||
registered IBANs; platform never holds funds) only exists as an interface.
|
||||
- **Current state:** `CrossCutting/Seams/MockPaymentProvider.cs:14-24`, `MockWebhookVerifier.cs:15-21`,
|
||||
`MockSettlementSplitProvider.cs:13`; registrations `:58-60`; merchant creds intended to come from the
|
||||
encrypted `payment_gateways.config_json` (seed at `Persistence/ServiceCollectionExtensions.cs:109-117`).
|
||||
- **Change:** per registry rows 39–41: one acquirer-with-تسهیم (ZarinPal/Sadad/Vandar/Jibit); real
|
||||
`InitPaymentAsync`/`VerifyAsync`/`RefundAsync`; per-provider HMAC verification of the raw body; a
|
||||
provider registry/factory selected per gateway row; persist full gateway responses.
|
||||
- **Effort:** L · **Risk:** high (real money; Shaparak certification lead time) · **Deps:** merchant
|
||||
registration; 1.4; 2.1; 4.2 (real lock) strongly recommended.
|
||||
|
||||
### 6.2 Real BNPL adapters (`IBnplProvider` / `IBnplProviderResolver`)
|
||||
|
||||
- **Why:** the full BNPL state machine runs against one mock provider; per-contract commission and
|
||||
non-instant settlement are simulated by config.
|
||||
- **Current state:** `CrossCutting/Seams/MockBnplProvider.cs:17-53`, `MockBnplProviderResolver.cs:14-17`;
|
||||
registrations `:72-74`; `Seams:Bnpl:*` defaults `SeamOptions.cs:98-120`. `bnpl_settlement_entries`
|
||||
(tranched settlement) is modeled-but-not-built by design
|
||||
(`dev/shared-working-context/backend/handoff/after-backend-phase-12.md:40`).
|
||||
- **Change:** per registry row 46: SnappPay (OAuth verb set) and/or Digipay adapters, creds from encrypted
|
||||
gateway config, Toman↔IRR only via `ICurrencyNormalizer`, per-contract commission read from the settle
|
||||
response, resolver registered per `provider_code`.
|
||||
- **Effort:** L · **Risk:** high · **Deps:** 2.1 (revert clearing), 6.1 patterns, provider contracts.
|
||||
|
||||
### 6.3 Real PAYA/SATNA payout rail (`IBankTransferProvider`) + async reconciliation
|
||||
|
||||
- **Why:** payouts "settle" instantly in the mock, collapsing the real `submitted → paid/failed` async
|
||||
reconciliation; no money reaches nurses.
|
||||
- **Current state:** `CrossCutting/Seams/MockBankTransferProvider.cs:18-31`; registration `:82`;
|
||||
`Seams:BankTransfer` defaults `SeamOptions.cs:61-72`. The status machine, batch idempotency key,
|
||||
`nurse_payout_booking_links` UNIQUE (`Persistence/Configuration/PayoutsConfig/NursePayoutBookingLinkConfig.cs:22`),
|
||||
and PAYA/SATNA threshold selection already exist.
|
||||
- **Change:** per registry row 23: Jibit/Vandar/Sadad payout API, source settlement account config, the
|
||||
async callback that flips `submitted → paid/failed`, batch caps/minimums. Keep whole-batch/single-row
|
||||
failure + retry semantics (already tested at `Tests/.../Payouts/PayoutHandlerTests.cs:211`).
|
||||
- **Effort:** L · **Risk:** high (irreversible transfers — the UNIQUE link + ledger-exists guard are the
|
||||
backstops, and they're in place) · **Deps:** 5.3 (real ownership check) before real runs; 4.1 for the
|
||||
weekly trigger.
|
||||
|
||||
### 6.4 Remove `IPaymentCaptureSimulator` (the registry's own end-state)
|
||||
|
||||
- **Why:** the b9 temporary conversion trigger was supposed to be removed once b10's real capture shipped
|
||||
(registry row 37, "make it real" step 3). b10 shipped; the seam and registration remain. Harmless as a
|
||||
test trigger, but it's a second way to mint a booking without a payment row — undesirable once real money
|
||||
exists.
|
||||
- **Current state:** registered at `CrossCutting/ServiceConfiguration/ServiceCollectionExtension.cs:52`;
|
||||
`Seams:PaymentCapture` options `SeamOptions.cs:142`.
|
||||
- **Change:** move the simulator into the test host (b9's tests are its only legitimate consumer) and drop
|
||||
the production registration; or gate the registration to Development/Testing.
|
||||
- **Effort:** S · **Risk:** low (b9 Convert tests must keep a path) · **Deps:** none.
|
||||
|
||||
### 6.5 Real Moadian submission + reconciliation (`IMoadianClient`)
|
||||
|
||||
- **Why:** e-invoicing to سامانه مودیان is a legal obligation; today every invoice stays
|
||||
`moadian_status = pending` forever (mock leaves it pending; **no reconciliation job or endpoint exists** —
|
||||
the only `ApplyMoadianResult` caller is issue-time, `Features/Invoices/Commands/IssueInvoice/IssueInvoiceCommand.Handler.cs:72`).
|
||||
- **Current state:** `CrossCutting/Seams/MockMoadianClient.cs:15-25`; registration `:65`;
|
||||
`Seams:Moadian:ForceRegistered` default false.
|
||||
- **Change:** enrollment (memory/economic code + signing cert), real `SubmitAsync`, and the
|
||||
`pending → submitted → registered/failed` reconciliation job (register it under 4.1). The invoice
|
||||
number sequence and VAT-on-commission math are already correct and tested.
|
||||
- **Effort:** M–L (enrollment dominates) · **Risk:** medium · **Deps:** 4.1 (scheduler) for the poll.
|
||||
|
||||
### 6.6 Decide the partner-center settlement rail (currently: resolver without money)
|
||||
|
||||
- **Why:** b15 resolves merchant-of-record per booking and stores each center's encrypted
|
||||
`settlement_iban` + `commission_rate`, but no money path pays a center or applies its rate — the b15
|
||||
report itself lists the settlement rail as a follow-up.
|
||||
- **Current state:** resolver + invoice wiring real
|
||||
(`Persistence/Repositories/PartnerCenterRepository.cs:118`, `IssueInvoiceCommand.Handler.cs:63`);
|
||||
`IBankTransferProvider` consumed only by nurse payouts (`Controllers/V1/AdminPayoutsController.cs:44`);
|
||||
follow-up noted at `dev/shared-working-context/reports/backend-phase-15-report.md:83`.
|
||||
- **Change:** product decision first (does a merchant-of-record center receive the commission split at
|
||||
launch, or is it bookkeeping-only?). If money moves: a center-settlement ledger account + payout command
|
||||
reusing the b13 machinery.
|
||||
- **Effort:** M–L (if built) · **Risk:** medium · **Deps:** product decision; 6.3.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-7 — Observability, audit & ops hardening
|
||||
|
||||
### 7.1 Add tracing and consolidate the two metric stacks
|
||||
|
||||
- **Why:** OTel is metrics-only (no `WithTracing`, no OTLP exporter) — cross-service money flows (webhook →
|
||||
confirm → ledger) can't be traced in production. Two overlapping Prometheus stacks run simultaneously
|
||||
(OTel's `AddPrometheusExporter` + prometheus-net's `UseMetricServer`/`UseHttpMetrics`).
|
||||
- **Current state:** `Monitoring/Configurations/OpenTelemetryConfigurations.cs:11-21`;
|
||||
`PrometheusMetricsConfigurations.cs:11`; W3C activity format set but unexported
|
||||
(`Program.cs:32`); Serilog already enriches with span ids (`LoggingConfiguration.cs:24`).
|
||||
- **Change:** add `WithTracing` (AspNetCore + EF instrumentation) exporting OTLP; pick **one** metrics
|
||||
stack; wire trace-id into the `ApiResult.requestId` for support correlation.
|
||||
- **Effort:** S–M · **Risk:** low · **Deps:** an OTLP-capable collector (optional at MVP; Prometheus alone
|
||||
is acceptable — see runtime-services.md).
|
||||
|
||||
### 7.2 Broaden health checks and split readiness/liveness
|
||||
|
||||
- **Why:** the single check is app-DB connectivity; the log DB, object-storage root, and (future)
|
||||
Redis/PSP get no signal. A deploy can pass health while logging or uploads are broken.
|
||||
- **Current state:** `Monitoring/Configurations/HealthCheckConfigurations.cs:17` (SQL Server only),
|
||||
`/HealthCheck` endpoint `:28`, dead `currentUrl` variable `:20`.
|
||||
- **Change:** add checks for `logDb`, object storage (write probe), Redis when 4.2 lands; tag checks and
|
||||
expose `/healthz/live` (process) vs `/healthz/ready` (dependencies); remove the dead line.
|
||||
- **Effort:** S · **Risk:** low · **Deps:** tracks new infra as it arrives.
|
||||
|
||||
### 7.3 Revisit production log levels and the notification-channel plan
|
||||
|
||||
- **Why:** deployed environments write **only Warning+** to the SQL sink — every Information-level audit
|
||||
trail (logins, money operations context) is dropped in production while Development keeps it. Also note:
|
||||
OTP codes are currently logged by design (`LoggingSmsSender`) — that must not survive 5.1.
|
||||
- **Current state:** `CrossCutting/Logging/LoggingConfiguration.cs:40-53`; Elasticsearch sink referenced
|
||||
but commented out (`:58-70`; package still pinned at `server/Directory.Packages.props:51`).
|
||||
- **Change:** Information+ to the sink with table retention (or a file/OTLP sink), structured category
|
||||
filters; delete the dead Elastic sink block + package (or revive it deliberately); verify no PII is
|
||||
logged (the SMS mock's OTP log disappears with 5.1).
|
||||
- **Effort:** S · **Risk:** low · **Deps:** none.
|
||||
|
||||
### 7.4 Audit-log growth & archival
|
||||
|
||||
- **Why:** `audit_logs` is append-only with no archival or retention (deferred since b1); 2.3 will grow it
|
||||
faster. The notification purge job is the only retention job in the system.
|
||||
- **Current state:** deferral recorded at `dev/phases/backend/backend-phase-1.md:124`; no purge/archive job
|
||||
exists for `ops.AuditLogs` (only `NotificationRetentionHostedService`).
|
||||
- **Change:** a retention/archival policy (cold table or export) as a 4.1 job; define legal retention for
|
||||
money/verification audit rows first.
|
||||
- **Effort:** S–M · **Risk:** low · **Deps:** 4.1.
|
||||
|
||||
### 7.5 Decide TicketMessage.Body encryption and the gRPC plugin's fate
|
||||
|
||||
- **Why (tickets):** ticket messages are the refund/dispute paper trail — users will type phone numbers,
|
||||
addresses, and clinical details. `TicketMessage.Body` is plaintext with **no documented decision**,
|
||||
unlike `BookingRequest.CustomerNotes` which carries an explicit "deliberately plaintext" comment
|
||||
(`Domain/Entities/Booking/BookingRequest.cs:45`).
|
||||
- **Why (gRPC):** the plugin duplicates the OTP/token flow only, forces the HTTP/2 posture (1.5), and
|
||||
enables reflection unconditionally — cost without a consumer (the Next.js client is HTTP/JSON only).
|
||||
- **Current state:** `Domain/Entities/Messaging/TicketMessage.cs:20`;
|
||||
`Plugins/Baya.Web.Plugins.Grpc/GrpcPluginStartup.cs:14-23`, `Services/UserGrpcServices.cs:18-53`.
|
||||
- **Change:** (a) either encrypt `Body` via the existing converter pattern (accepting the search/ops cost —
|
||||
admin thread reads already decrypt per-row elsewhere) or add the explicit "deliberately plaintext"
|
||||
decision comment + docs; recommend encrypting. (b) remove the gRPC plugin or disable reflection outside
|
||||
Development and give it a dedicated HTTP/2 endpoint.
|
||||
- **Effort:** S–M · **Risk:** low · **Deps:** 1.5 pairs with (b).
|
||||
|
||||
### 7.6 Keep the docs honest (registry + tracker + map)
|
||||
|
||||
- **Why:** the mocks-registry contains stale duplicate rows — the early block still says 🔴 "not built" for
|
||||
`IDistributedLock`/`INurseSearch`/`IPaymentProvider`/`ISettlementSplitProvider`/`IWebhookVerifier`/
|
||||
`IMoadianClient`/`ILicenseVerificationService` while later rows correct all seven (e.g. rows 15/16 vs
|
||||
38/42). `IJobScheduler` is listed as a seam but no such interface exists. All 15 REQs read `Status: open`.
|
||||
Stale instructions are worse than none (root CLAUDE.md rule 7).
|
||||
- **Current state:** `dev/shared-working-context/reports/mocks-registry.md:15-19,32,36` (stale block) vs
|
||||
`:38-50` (corrected rows); `dev/shared-working-context/frontend/requests/for-backend.md` (all open).
|
||||
- **Change:** prune the stale registry block, rename the `IJobScheduler` row to "recurring jobs (hosted
|
||||
services)", mark delivered/answered REQs, and note `IPaymentCaptureSimulator`'s intended removal (6.4).
|
||||
- **Effort:** S · **Risk:** none · **Deps:** post-phase-3 outcomes.
|
||||
|
||||
---
|
||||
|
||||
## post-phase-8 — Scale & later (explicitly not MVP)
|
||||
|
||||
- **8.1 Elasticsearch read backend + outbox feeder** — `SqlNurseSearch` is real and correct
|
||||
(`Persistence/Services/Search/SqlNurseSearch.cs:18`); `Search:Backend` fails fast on any non-`sql` value
|
||||
(`ServiceCollectionExtensions.cs:74-78`). Build `ElasticNurseSearch` + the outbox/CDC feeder per registry
|
||||
rows 38/43 only when SQL search shows strain. **Effort:** L.
|
||||
- **8.2 Analytics pipeline** — `IAnalyticsSink` writes `ops.SystemEvents` rows fire-and-forget
|
||||
(`Persistence/Services/Analytics/AnalyticsSink.cs:15-35`); pipe to a warehouse/stream when product needs
|
||||
it. **Effort:** M.
|
||||
- **8.3 Holiday-calendar feed** — the table is manually maintained; a lunar-Hijri drift shifts payout dates
|
||||
(`Persistence/Services/Holidays/HolidayCalendarService.cs:25-45`). A yearly ops checklist item is an
|
||||
acceptable alternative to a feed. **Effort:** S.
|
||||
- **8.4 Push/SMS notification channels** — `InAppNotificationDispatcher` silently drops non-InApp channels
|
||||
(`Persistence/Services/Notifications/InAppNotificationDispatcher.cs:17`); add channel fan-out (SMS via
|
||||
5.1's sender, FCM push) when the mobile/notification UX demands it. **Effort:** M.
|
||||
- **8.5 Deferred product tables** — `organizations`, `organization_nurses`, `fraud_flags`,
|
||||
`recurring_booking_schedules` (b15), `bnpl_settlement_entries` (b12), nurse availability slots (b5/b8),
|
||||
customer national-ID KYC (b3), geo bulk import (b4) — all verified absent and all pure additive
|
||||
migrations when product pulls them (evidence: `dev/phases/backend/backend-phase-15.md:333-336`,
|
||||
`after-backend-phase-12.md:40`, `backend-phase-3.md:188`, `backend-phase-4.md:194`; model snapshot clean).
|
||||
|
||||
---
|
||||
|
||||
## Suggested sequencing
|
||||
|
||||
```
|
||||
post-phase-1 (security) ──┬──► post-phase-2 (money correctness) ──► post-phase-6 (money rails)
|
||||
│
|
||||
post-phase-3 (contract batch — parallel, unblocks frontend f9–f15)
|
||||
│
|
||||
post-phase-4 (scheduler/Redis) ──► needed by 6.3/6.5 triggers
|
||||
post-phase-5 (trust rails; 5.1 SMS is launch-critical, schedule early)
|
||||
post-phase-7 (observability — start anytime, finish before launch)
|
||||
post-phase-8 (later)
|
||||
```
|
||||
|
||||
The two items that should not wait for their bucket: **1.1 (rotate the committed sa credentials — today)**
|
||||
and **2.1 (the unreachable refund clearing — before any real BNPL/manual refund exists)**.
|
||||
@@ -0,0 +1,223 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Runtime services — deployment topology</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="runtime-services-deployment-topology">Runtime services — deployment topology</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="#service-inventory">Service inventory</a></li><li class="l2"><a href="#dependency-graph">Dependency graph</a></li><li class="l2"><a href="#per-service-notes">Per-service notes</a></li><li class="l3"><a href="#1-2-sql-server-baya-baya-logs">1–2 · SQL Server (<code>Baya</code> + <code>Baya_Logs</code>)</a></li><li class="l3"><a href="#3-reverse-proxy-tls">3 · Reverse proxy / TLS</a></li><li class="l3"><a href="#4-prometheus-grafana">4 · Prometheus (+ Grafana)</a></li><li class="l3"><a href="#5-redis">5 · Redis</a></li><li class="l3"><a href="#6-minio-s3-arvancloud">6 · MinIO / S3 / ArvanCloud</a></li><li class="l3"><a href="#7-job-scheduler">7 · Job scheduler</a></li><li class="l3"><a href="#8-sms-gateway">8 · SMS gateway</a></li><li class="l3"><a href="#9-psp-ipg-shaparak-تسهیم">9 · PSP / IPG + Shaparak (تسهیم)</a></li><li class="l3"><a href="#10-bnpl-providers">10 · BNPL providers</a></li><li class="l3"><a href="#11-bank-transfer-rail-paya-satna">11 · Bank-transfer rail (PAYA/SATNA)</a></li><li class="l3"><a href="#12-سامانه-مودیان">12 · سامانه مودیان</a></li><li class="l3"><a href="#13-kyc-bridge-shahkar-e-kyc-استعلام-شبا">13 · KYC bridge (Shahkar / e-KYC / استعلام شبا)</a></li><li class="l3"><a href="#14-geocoding-neshan">14 · Geocoding (Neshan)</a></li><li class="l3"><a href="#15-review-moderation">15 · Review moderation</a></li><li class="l3"><a href="#16-moh-ino-enamad-a-process-not-a-service">16 · MoH / INO / eNamad — a process, not a service</a></li><li class="l3"><a href="#17-elasticsearch-deliberately-later">17 · Elasticsearch — deliberately later</a></li><li class="l2"><a href="#deployment-notes-from-the-code-not-aspiration">Deployment notes (from the code, not aspiration)</a></li></ul></nav>
|
||||
<p><strong>Audit date:</strong> 2026-07-10 · <strong>Derivation:</strong> every entry below is justified from the code — the DI seam that depends on it, the config key that names it, or the package/startup wiring that talks to it. Nothing is invented; "not needed" claims are backed by the absence of the package/code. Make-it-real steps live in <code>dev/shared-working-context/reports/mocks-registry.md</code> (row references below).</p>
|
||||
<p><strong>The shape in one sentence:</strong> today the API binary talks to exactly <strong>one external system — SQL Server</strong> (app DB + log DB); everything else (18 seams) is an in-process mock, so "deployment" today is one container + one database — and the table below is the roadmap of what must exist as each seam goes real.</p>
|
||||
<h2 id="service-inventory">Service inventory</h2>
|
||||
<div class="tblwrap"><table><thead><tr><th>#</th><th>Service</th><th>Purpose</th><th>Depends via (seam / config)</th><th>MVP?</th><th>Registry row</th></tr></thead><tbody><tr><td>1</td><td><strong>SQL Server</strong> (app DB <code>Baya</code>)</td><td>System of record — 12 schemas (<code>usr ops geo catalog verif search booking payments payouts reviews messaging partner</code>)</td><td>EF Core; <code>ConnectionStrings:SqlServer</code></td><td><strong>Required now</strong></td><td>—</td></tr><tr><td>2</td><td><strong>SQL Server</strong> (log DB <code>Baya_Logs</code>)</td><td>Serilog sink in deployed envs (Warning+, auto-created <code>log.LogEvents</code>)</td><td><code>ConnectionStrings:logDb</code></td><td><strong>Required now</strong> (deployed)</td><td>—</td></tr><tr><td>3</td><td><strong>Reverse proxy / TLS</strong> (nginx·caddy·traefik)</td><td>TLS termination, HTTP/1.1+2, forwarded headers</td><td>Kestrel config; JWE bearer</td><td><strong>Required now</strong></td><td>—</td></tr><tr><td>4</td><td><strong>Prometheus</strong> (+ Grafana)</td><td>Scrapes <code>/metrics</code>; health forwarded to gauges</td><td><code>UseMetricServer</code> + OTel exporter</td><td><strong>Recommended now</strong></td><td>—</td></tr><tr><td>5</td><td><strong>Redis</strong></td><td><code>ICacheService</code> + <code>IDistributedLock</code> (money-path mutex)</td><td><code>Seams:*</code> (keys TBD; none today)</td><td>Before >1 API instance</td><td>rows 14, 42</td></tr><tr><td>6</td><td><strong>MinIO / S3 / ArvanCloud</strong></td><td><code>IObjectStorage</code> — verification docs, avatars (REQ-006), invoice PDFs</td><td><code>Seams:ObjectStorage:*</code></td><td>Before real verification</td><td>row 13</td></tr><tr><td>7</td><td><strong>Job scheduler</strong> (Hangfire/Quartz, in-app on SQL)</td><td>The deferred crons: payout batch, expiry scan, no-show, Moadian poll</td><td>hosted services (no interface exists)</td><td>Before unattended ops</td><td>row 26</td></tr><tr><td>8</td><td><strong>SMS gateway</strong> (Kavenegar·Ghasedak·SMS.ir)</td><td><code>ISmsSender</code> — OTP delivery (login is impossible without it)</td><td><code>Seams:Sms:*</code> (to be added)</td><td><strong>Launch-critical</strong></td><td>row 12</td></tr><tr><td>9</td><td><strong>PSP / IPG + Shaparak</strong> (ZarinPal·Sadad·Vandar·Jibit)</td><td><code>IPaymentProvider</code> + <code>IWebhookVerifier</code> + <code>ISettlementSplitProvider</code> (تسهیم)</td><td>encrypted <code>payment_gateways.config_json</code></td><td>Real payments</td><td>rows 39–41</td></tr><tr><td>10</td><td><strong>BNPL providers</strong> (SnappPay·Digipay)</td><td><code>IBnplProvider</code> / <code>IBnplProviderResolver</code> / <code>ICurrencyNormalizer</code></td><td><code>Seams:Bnpl:*</code>, <code>Seams:Currency:*</code>, gateway config</td><td>Optional at launch</td><td>rows 46–47</td></tr><tr><td>11</td><td><strong>Bank-transfer rail</strong> (Jibit·Vandar·Sadad payout API → PAYA/SATNA)</td><td><code>IBankTransferProvider</code> — weekly nurse payouts</td><td><code>Seams:BankTransfer:*</code></td><td>Real payouts</td><td>row 23</td></tr><tr><td>12</td><td><strong>سامانه مودیان</strong> (tax e-invoicing)</td><td><code>IMoadianClient</code> — legal invoice registration</td><td><code>Seams:Moadian:*</code> + signing cert</td><td>Legal — soon after launch</td><td>row 45</td></tr><tr><td>13</td><td><strong>KYC bridge vendor</strong> (Finnotech-class)</td><td><code>IShahkarVerifier</code> + <code>IIdentityKycProvider</code> + <code>IBankAccountOwnershipVerifier</code></td><td><code>Seams:Shahkar:*</code>, <code>Seams:IdentityKyc:*</code>, <code>Seams:BankOwnership:*</code></td><td>Real verification + payout gate</td><td>rows 27, 28, 30</td></tr><tr><td>14</td><td><strong>Geocoding</strong> (Neshan)</td><td><code>IGeocoder</code> — address → coordinates (EVV distance)</td><td><code>Seams:Geocoding:*</code></td><td>With real EVV</td><td>row 31</td></tr><tr><td>15</td><td><strong>Review-moderation classifier</strong> (LLM/API)</td><td><code>IReviewModerationService</code> — AI pre-screen</td><td><code>Seams:ReviewModeration:*</code></td><td>Optional (human queue is default)</td><td>row 33</td></tr><tr><td>16</td><td><strong>MoH / INO / eNamad</strong></td><td><code>ICredentialVerifier</code> + <code>ILicenseVerificationService</code> — <strong>manual admin process; no public B2B API exists</strong></td><td><code>Seams:LicenseVerification:*</code></td><td>Manual = the MVP design</td><td>rows 29, 50</td></tr><tr><td>17</td><td><strong>Elasticsearch</strong></td><td><code>INurseSearch</code> alt backend + outbox feeder</td><td><code>Search:Backend</code> (non-<code>sql</code> throws today)</td><td><strong>Not MVP</strong> — SQL search is real</td><td>rows 38, 43</td></tr></tbody></table></div>
|
||||
<p><strong>Explicitly not needed</strong> (verified absent from <code>server/Directory.Packages.props</code> and code): message broker (no RabbitMQ/Kafka), Redis today (no <code>StackExchange.Redis</code>), Hangfire/Quartz today, Elasticsearch client (<code>Elastic.Clients.*</code>), any cloud SDK, any payment/SMS vendor SDK. The only externally-pointing package beyond SQL Server is <code>Serilog.Sinks.Elasticsearch</code> — its wiring is commented out (<code>Baya.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs:58-70</code>).</p>
|
||||
<hr>
|
||||
<h2 id="dependency-graph">Dependency graph</h2>
|
||||
<p>Solid edges are live today; dashed edges are behind a mocked seam (the arrow exists in code, the wire does not). One line per node below the graph.</p>
|
||||
<figure class="diagram"><svg viewBox="0 0 1160 830" role="img" aria-label="Balinyaar runtime dependency graph">
|
||||
<defs>
|
||||
<marker id="arr-real" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="arr-real" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="arr-mock" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="arr-mock" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect class="box live" x="20" y="150" width="210" height="62" rx="8"/><text x="125" y="178" text-anchor="middle" font-weight="600">Next.js client</text><text class="t2" x="125" y="195" text-anchor="middle">HTTPS · JSON (camelCase)</text>
|
||||
<rect class="box live" x="20" y="430" width="210" height="62" rx="8"/><text x="125" y="458" text-anchor="middle" font-weight="600">Prometheus (+ Grafana)</text><text class="t2" x="125" y="475" text-anchor="middle">scrapes /metrics + health</text>
|
||||
|
||||
<rect class="grp" x="290" y="90" width="270" height="330" rx="12"/>
|
||||
<text class="grp-title" x="425" y="114" text-anchor="middle">API HOST · Baya.Web.Api (:5002)</text>
|
||||
<rect class="box live" x="310" y="132" width="230" height="78" rx="8"/><text x="425" y="150" text-anchor="middle" font-weight="600">ASP.NET Core API</text><text class="t2" x="425" y="166" text-anchor="middle">REST /api/v1 · Swagger</text><text class="t2" x="425" y="180" text-anchor="middle">/metrics · /HealthCheck · gRPC</text>
|
||||
<rect class="box live" x="310" y="250" width="230" height="62" rx="8"/><text x="425" y="278" text-anchor="middle" font-weight="600">In-proc interval jobs</text><text class="t2" x="425" y="295" text-anchor="middle">expiry 1min · retention 24h</text>
|
||||
<path class="edge real" d="M 425 250 L 425 210"/>
|
||||
<text class="t2" x="425" y="345" text-anchor="middle">18 vendor seams resolve</text>
|
||||
<text class="t2" x="425" y="361" text-anchor="middle">in-process (mocks) today —</text>
|
||||
<text class="t2" x="425" y="377" text-anchor="middle">dashed edges = the wire to build</text>
|
||||
|
||||
<rect class="grp" x="640" y="20" width="500" height="216" rx="12"/>
|
||||
<text class="grp-title" x="660" y="44">DATA & PLATFORM INFRA</text>
|
||||
<rect class="box live" x="660" y="58" width="225" height="62" rx="8"/><text x="772.5" y="86" text-anchor="middle" font-weight="600">SQL Server «Baya»</text><text class="t2" x="772.5" y="103" text-anchor="middle">12 schemas · system of record</text>
|
||||
<rect class="box live" x="900" y="58" width="220" height="62" rx="8"/><text x="1010" y="86" text-anchor="middle" font-weight="600">SQL Server «Baya_Logs»</text><text class="t2" x="1010" y="103" text-anchor="middle">Serilog sink (deployed envs)</text>
|
||||
<rect class="box mock" x="660" y="146" width="225" height="62" rx="8"/><text x="772.5" y="174" text-anchor="middle" font-weight="600">Redis</text><text class="t2" x="772.5" y="191" text-anchor="middle">cache + dist. lock — in-proc now</text>
|
||||
<rect class="box mock" x="900" y="146" width="220" height="62" rx="8"/><text x="1010" y="174" text-anchor="middle" font-weight="600">MinIO / S3</text><text class="t2" x="1010" y="191" text-anchor="middle">object storage — local disk now</text>
|
||||
|
||||
<rect class="grp" x="640" y="260" width="500" height="216" rx="12"/>
|
||||
<text class="grp-title" x="660" y="284">MONEY RAILS (ALL MOCKED TODAY)</text>
|
||||
<rect class="box mock" x="660" y="298" width="225" height="62" rx="8"/><text x="772.5" y="326" text-anchor="middle" font-weight="600">PSP / IPG + Shaparak</text><text class="t2" x="772.5" y="343" text-anchor="middle">capture · webhook · تسهیم</text>
|
||||
<rect class="box mock" x="900" y="298" width="220" height="62" rx="8"/><text x="1010" y="326" text-anchor="middle" font-weight="600">BNPL</text><text class="t2" x="1010" y="343" text-anchor="middle">SnappPay / Digipay</text>
|
||||
<rect class="box mock" x="660" y="386" width="225" height="62" rx="8"/><text x="772.5" y="414" text-anchor="middle" font-weight="600">PAYA / SATNA rail</text><text class="t2" x="772.5" y="431" text-anchor="middle">weekly nurse payouts</text>
|
||||
<rect class="box mock" x="900" y="386" width="220" height="62" rx="8"/><text x="1010" y="414" text-anchor="middle" font-weight="600">سامانه مودیان</text><text class="t2" x="1010" y="431" text-anchor="middle">tax e-invoicing</text>
|
||||
|
||||
<rect class="grp" x="640" y="500" width="500" height="300" rx="12"/>
|
||||
<text class="grp-title" x="660" y="524">TRUST & IDENTITY RAILS (ALL MOCKED TODAY)</text>
|
||||
<rect class="box mock" x="660" y="538" width="225" height="62" rx="8"/><text x="772.5" y="566" text-anchor="middle" font-weight="600">SMS gateway (OTP)</text><text class="t2" x="772.5" y="583" text-anchor="middle">launch-critical — logs only now</text>
|
||||
<rect class="box mock" x="900" y="538" width="220" height="62" rx="8"/><text x="1010" y="566" text-anchor="middle" font-weight="600">KYC bridge</text><text class="t2" x="1010" y="583" text-anchor="middle">Shahkar · e-KYC · استعلام شبا</text>
|
||||
<rect class="box mock" x="660" y="626" width="225" height="62" rx="8"/><text x="772.5" y="654" text-anchor="middle" font-weight="600">Neshan geocoding</text><text class="t2" x="772.5" y="671" text-anchor="middle">EVV distance accuracy</text>
|
||||
<rect class="box mock" x="900" y="626" width="220" height="62" rx="8"/><text x="1010" y="654" text-anchor="middle" font-weight="600">Review moderation</text><text class="t2" x="1010" y="671" text-anchor="middle">optional — human queue default</text>
|
||||
<rect class="box mock" x="660" y="714" width="460" height="62" rx="8"/><text x="890" y="742" text-anchor="middle" font-weight="600">MoH / INO / eNamad</text><text class="t2" x="890" y="759" text-anchor="middle">manual admin review — no public API exists</text>
|
||||
|
||||
<path class="edge real" d="M 230 181 C 270 181, 270 171, 310 171" marker-end="url(#arr-real)"/><text class="lbl" x="270" y="170" text-anchor="middle">HTTPS/JSON</text>
|
||||
<path class="edge real" d="M 230 455 C 260 455, 260 400, 290 400" marker-end="url(#arr-real)"/><text class="lbl" x="260" y="419.5" text-anchor="middle">scrape</text>
|
||||
<path class="edge real" d="M 560 130 C 600 130, 600 100, 640 100" marker-end="url(#arr-real)"/><text class="lbl" x="600" y="99" text-anchor="middle">EF Core · Serilog</text>
|
||||
<path class="edge mock" d="M 560 170 C 600 170, 600 185, 640 185" marker-end="url(#arr-mock)"/><text class="lbl" x="600" y="193.5" text-anchor="middle">cache · lock · files</text>
|
||||
<path class="edge mock" d="M 560 245 C 600 245, 600 320, 640 320" marker-end="url(#arr-mock)"/><text class="lbl" x="600" y="268.5" text-anchor="middle">capture · payouts · invoices</text>
|
||||
<path class="edge mock" d="M 640 395 C 600 395, 600 290, 560 290" marker-end="url(#arr-mock)"/><text class="lbl" x="600" y="356.5" text-anchor="middle">webhooks / callbacks</text>
|
||||
<path class="edge mock" d="M 560 335 C 600 335, 600 610, 640 610" marker-end="url(#arr-mock)"/><text class="lbl" x="600" y="466.5" text-anchor="middle">OTP · استعلام · geocode · moderation</text>
|
||||
|
||||
<rect class="grp" x="20" y="600" width="210" height="130" rx="12"/>
|
||||
<text class="grp-title" x="125" y="624" text-anchor="middle">LEGEND</text>
|
||||
<path class="edge real" d="M 40 646 L 90 646"/><text class="t2" x="100" y="650">live today</text>
|
||||
<path class="edge mock" d="M 40 674 L 90 674"/><text class="t2" x="100" y="678">mocked seam — to build</text>
|
||||
<rect class="box mock" x="40" y="694" width="50" height="20" rx="6"/><text class="t2" x="100" y="708">service to provision</text>
|
||||
</svg></figure>
|
||||
<details class="src"><summary>Mermaid source (canonical, in the .md)</summary><pre><code>flowchart LR
|
||||
subgraph fe[Frontend]
|
||||
WEB["Next.js client"]
|
||||
end
|
||||
subgraph host["API host — Baya.Web.Api (:5002)"]
|
||||
API["ASP.NET Core API\nREST /api/v1 · /metrics · /HealthCheck"]
|
||||
JOBS["In-proc interval jobs\n(→ Hangfire/Quartz later)"]
|
||||
end
|
||||
subgraph data[Data & platform infra]
|
||||
SQL[("SQL Server 'Baya'\n12 schemas · migrations on boot")]
|
||||
LOG[("SQL Server 'Baya_Logs'\nSerilog sink, Warning+")]
|
||||
REDIS[("Redis — cache + dist. lock\n(in-proc today)")]
|
||||
S3[("MinIO / S3 — object storage\n(local disk today)")]
|
||||
end
|
||||
subgraph money["Money rails (all mocked today)"]
|
||||
PSP["PSP / IPG + Shaparak\ncapture · webhook · تسهیم"]
|
||||
BNPL["BNPL — SnappPay / Digipay"]
|
||||
BANK["PAYA / SATNA payout rail"]
|
||||
MOAD["سامانه مودیان e-invoicing"]
|
||||
end
|
||||
subgraph trust["Trust & identity rails (all mocked today)"]
|
||||
SMS["SMS gateway (OTP)"]
|
||||
KYC["Shahkar · e-KYC · استعلام شبا"]
|
||||
GEOC["Neshan geocoding"]
|
||||
MODAI["Review-moderation classifier"]
|
||||
MANUAL["MoH / INO / eNamad\n(manual admin review)"]
|
||||
end
|
||||
subgraph obs[Observability]
|
||||
PROM["Prometheus (+ Grafana)"]
|
||||
end
|
||||
WEB -->|HTTPS/JSON| API
|
||||
API --> SQL
|
||||
API --> LOG
|
||||
JOBS --- API
|
||||
API -.-> REDIS
|
||||
API -.-> S3
|
||||
API -.-> SMS
|
||||
API <-.-> PSP
|
||||
API <-.-> BNPL
|
||||
API -.-> BANK
|
||||
API -.-> MOAD
|
||||
API -.-> KYC
|
||||
API -.-> GEOC
|
||||
API -.-> MODAI
|
||||
MANUAL -.- API
|
||||
PROM -->|scrape /metrics| API</code></pre></details>
|
||||
<ul><li><strong>Next.js client</strong> — the only API consumer; reads <code>NEXT_PUBLIC_API_URL</code> (root <code>CLAUDE.md</code>).</li><li><strong>API</strong> — single ASP.NET Core host; all seams resolve in-process today.</li><li><strong>In-proc jobs</strong> — the two <code>BackgroundService</code> sweeps (booking-request expiry 1 min, notification retention 24 h); the scheduler upgrade re-homes them (plan §4.1).</li><li><strong>SQL Server <code>Baya</code></strong> — system of record; migrations + seeding run on every non-Testing boot.</li><li><strong>SQL Server <code>Baya_Logs</code></strong> — deployed-env Serilog sink (auto-creates DB/table).</li><li><strong>Redis</strong> — target for <code>ICacheService</code>/<code>IDistributedLock</code>; nothing speaks Redis yet.</li><li><strong>MinIO/S3</strong> — target for <code>IObjectStorage</code>; local disk + <code>file://</code> URLs today.</li><li><strong>PSP/IPG</strong> — card capture, callback signatures, تسهیم settlement split.</li><li><strong>BNPL</strong> — provider-financed installments; settle/revert callbacks.</li><li><strong>PAYA/SATNA</strong> — weekly nurse payout batches + async reconciliation.</li><li><strong>مودیان</strong> — legal e-invoice registration (pending→registered poll).</li><li><strong>KYC vendor</strong> — Shahkar phone↔NID, identity+liveness, Sheba ownership (payout gate).</li><li><strong>Neshan</strong> — geocoding for address coordinates / EVV distance.</li><li><strong>Moderation classifier</strong> — optional AI pre-screen; human moderation is the default gate.</li><li><strong>MoH/INO/eNamad</strong> — human verification workflows, by design (no API exists).</li><li><strong>Prometheus</strong> — scrapes <code>/metrics</code>; health check results forwarded as gauges.</li></ul>
|
||||
<hr>
|
||||
<h2 id="per-service-notes">Per-service notes</h2>
|
||||
<h3 id="1-2-sql-server-baya-baya-logs">1–2 · SQL Server (<code>Baya</code> + <code>Baya_Logs</code>)</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>UseSqlServer</code> at <code>server/src/Infrastructure/Baya.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:41</code>; sink at <code>…CrossCutting/Logging/LoggingConfiguration.cs:44-45</code> (schema <code>log</code>, auto-create); the 12 schemas via per-entity <code>ToTable(name, schema)</code> (e.g. <code>PaymentsConfig/LedgerEntryConfig.cs:18</code>, <code>PayoutsConfig/NursePayoutConfig.cs:19</code>).</li><li><strong>Default:</strong> <code>mcr.microsoft.com/mssql/server:2022-latest</code> (Developer for dev; licensed edition in prod). Both DBs fit one instance; <code>Baya_Logs</code> can move later.</li><li><strong>Config:</strong> <code>ConnectionStrings:SqlServer</code>, <code>ConnectionStrings:logDb</code> — <strong>rotate + externalize first</strong> (plan §1.1; live <code>sa</code> credentials are committed today).</li><li><strong>Health/readiness:</strong> the app's only health check (<code>/HealthCheck</code>, <code>Monitoring/Configurations/HealthCheckConfigurations.cs:17</code>); <code>logDb</code> has none (plan §7.2). Boot runs <code>MigrateAsync</code> + 3 seeders (<code>Program.cs:99-104</code>) → the login needs DDL rights and concurrent multi-node boot races (plan §4.3).</li></ul>
|
||||
<h3 id="3-reverse-proxy-tls">3 · Reverse proxy / TLS</h3>
|
||||
<ul><li><strong>Evidence of need:</strong> JWE bearer auth (<code>RequireHttpsMetadata</code> must be true in prod — <code>Identity/ServiceConfiguration/ServiceCollectionExtension.cs:139</code>); Kestrel <code>EndpointDefaults=Http2</code> breaks non-TLS HTTP/1.1 (<code>appsettings.json:29-33</code>, plan §1.5); the rate limiter partitions on <code>RemoteIpAddress</code> with <strong>no ForwardedHeaders middleware</strong> (<code>WebFramework/ServiceConfiguration/RateLimitingServiceExtension.cs:69</code>, plan §1.6) — the proxy must pass <code>X-Forwarded-For</code> <em>and</em> the app must be taught to honor it.</li><li><strong>Default:</strong> caddy 2 / nginx 1.27; terminate TLS, h2 to clients, HTTP/1.1 (or h2c) upstream once §1.5 lands.</li></ul>
|
||||
<h3 id="4-prometheus-grafana">4 · Prometheus (+ Grafana)</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>/metrics</code> via prometheus-net <code>UseMetricServer</code> + OTel <code>AddPrometheusExporter</code> (two stacks — consolidate, plan §7.1) at <code>Monitoring/Configurations/PrometheusMetricsConfigurations.cs:11</code> and <code>OpenTelemetryConfigurations.cs:21</code>; health forwarded (<code>HealthCheckConfigurations.cs:18</code>).</li><li><strong>Default:</strong> <code>prom/prometheus:v2.53</code> + <code>grafana/grafana:11</code>. No tracing backend exists yet (metrics-only); an OTLP collector becomes relevant with plan §7.1.</li></ul>
|
||||
<h3 id="5-redis">5 · Redis</h3>
|
||||
<ul><li><strong>Evidence of the gap:</strong> <code>MemoryCacheService</code> and <code>InProcessDistributedLock</code> (<code>CrossCutting/Seams/MemoryCacheService.cs:11</code>, <code>InProcessDistributedLock.cs:14</code>) — single-process only; the money-path lock convention <code>booking:{id}:payment|refund</code> is already in the handlers.</li><li><strong>Default:</strong> <code>redis:7-alpine</code> (AOF on). <strong>Required the moment a second API instance runs</strong> (shared cache invalidation generation-tokens + cross-instance money mutex). Config keys to be introduced with the swap (plan §4.2); none exist today.</li></ul>
|
||||
<h3 id="6-minio-s3-arvancloud">6 · MinIO / S3 / ArvanCloud</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>LocalDiskObjectStorage</code> writes under a temp root and returns <code>file://</code> URLs (<code>CrossCutting/Seams/LocalDiskObjectStorage.cs:20,56</code>); consumers: b6 verification documents (signed-URL upload flow), future avatars (REQ-006), invoice <code>PdfStorageKey</code> (<code>InvoicesConfig/InvoiceConfig.cs</code> — <code>pdf_storage_key</code> column).</li><li><strong>Default:</strong> <code>minio/minio:latest</code> (S3-compatible; ArvanCloud object storage is the Iran-hosted option).</li><li><strong>Config:</strong> <code>Seams:ObjectStorage:RootPath</code> today → bucket/endpoint/keys with the swap. Presigned PUT/GET with expiry is the contract the frontend already codes against.</li></ul>
|
||||
<h3 id="7-job-scheduler">7 · Job scheduler</h3>
|
||||
<ul><li><strong>Evidence:</strong> two <code>PeriodicTimer</code> hosted services only (<code>Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:63,67</code>); the payout/expiry/no-show/ Moadian crons are admin-manual with seeded-but-unread cadence keys (plan §4.1). <strong>No <code>IJobScheduler</code> interface exists</strong> — the registry name is aspirational.</li><li><strong>Default:</strong> Hangfire on the existing SQL Server (no new container) — dashboard behind admin auth; or Quartz with SQL persistence. Not a separate service to "spin up", but it changes the SQL footprint (schema) and ops (dashboard, retries).</li></ul>
|
||||
<h3 id="8-sms-gateway">8 · SMS gateway</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>LoggingSmsSender</code> logs OTPs instead of sending (<code>CrossCutting/Seams/LoggingSmsSender.cs:16</code>) — <strong>no real user can log in</strong>; per-phone resend window + <code>otp</code> rate policy already enforced upstream.</li><li><strong>Default:</strong> Kavenegar / Ghasedak / SMS.ir (SaaS — API key, no container). Keys to add: <code>Seams:Sms:{ApiKey,SenderLine,BaseUrl}</code> (registry row 12). Template/pattern OTP send for deliverability.</li></ul>
|
||||
<h3 id="9-psp-ipg-shaparak-تسهیم">9 · PSP / IPG + Shaparak (تسهیم)</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>MockPaymentProvider</code> (instant success, <code>VerifyAsync</code> echoes the expected amount — <code>CrossCutting/Seams/MockPaymentProvider.cs:24</code>), <code>MockWebhookVerifier</code> (marker-based "signature" — <code>MockWebhookVerifier.cs:21</code>), <code>MockSettlementSplitProvider</code> (<code>MockSettlementSplitProvider.cs:13</code>). Real merchant credentials belong in the <strong>encrypted</strong> <code>payment_gateways.config_json</code> (seeded sandbox row: <code>Persistence/ServiceCollectionExtensions.cs:109-117</code> — environment-gate it, plan §1.4).</li><li><strong>Default:</strong> SaaS (ZarinPal / Sadad / Vandar / Jibit) — needs merchant + terminal registration and تسهیم (settlement-split) setup to registered IBANs; webhook endpoint is already public (<code>POST webhooks/payments/{provider}</code>) with upsert-first idempotency in place.</li></ul>
|
||||
<h3 id="10-bnpl-providers">10 · BNPL providers</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>MockBnplProvider</code> drives the full verb set + state machine (<code>CrossCutting/Seams/MockBnplProvider.cs:17-53</code>); callback endpoint <code>WebhooksBnplController</code> (signed, rate-limited). <code>Seams:Bnpl:*</code> + <code>Seams:Currency:TomanToIrrMultiplier</code> (<code>SeamOptions.cs:98-120,79</code>).</li><li><strong>Default:</strong> SnappPay and/or Digipay (SaaS, OAuth). <strong>Do plan §2.1 first</strong> — the revert-clearing path is currently unreachable, so real BNPL refunds would strand ledger state.</li></ul>
|
||||
<h3 id="11-bank-transfer-rail-paya-satna">11 · Bank-transfer rail (PAYA/SATNA)</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>MockBankTransferProvider</code> settles every instruction instantly (<code>CrossCutting/Seams/MockBankTransferProvider.cs:18-31</code>); the handler already chooses PAYA vs SATNA by <code>payout_satna_threshold_irr</code> (<code>platform_configs</code> seed row 22) and the irreversibility backstops (unconditional <code>UNIQUE(booking_id)</code> link, forward-only payout machine) are in place.</li><li><strong>Default:</strong> Jibit / Vandar / Sadad payout API (SaaS). Needs the source settlement account + the async <code>submitted → paid/failed</code> reconciliation callback the mock collapses (registry row 23, steps 3–4).</li></ul>
|
||||
<h3 id="12-سامانه-مودیان">12 · سامانه مودیان</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>MockMoadianClient</code> leaves invoices <code>pending</code> forever (<code>CrossCutting/Seams/MockMoadianClient.cs:21</code>); no reconciliation job exists (plan §6.5). VAT-on-commission + sequential invoice numbers are already correct server-side.</li><li><strong>Default:</strong> government SaaS — enrollment (memory/economic code) + a signing certificate; the certificate is a deploy-time secret.</li></ul>
|
||||
<h3 id="13-kyc-bridge-shahkar-e-kyc-استعلام-شبا">13 · KYC bridge (Shahkar / e-KYC / استعلام شبا)</h3>
|
||||
<ul><li><strong>Evidence:</strong> three deterministic mocks with magic-value failure cases (<code>MockShahkarVerifier.cs:26-37</code>, <code>MockIdentityKycProvider.cs:25</code>, <code>MockBankAccountOwnershipVerifier.cs:26</code>); handlers already persist <code>external_response_json</code> and treat shared-SIM as a handled state. The Sheba-ownership result gates first payouts (<code>matched_national_id</code>).</li><li><strong>Default:</strong> one Finnotech-class bridge covers all three استعلامها (SaaS; API keys under <code>Seams:{Shahkar,IdentityKyc,BankOwnership}:*</code>).</li></ul>
|
||||
<h3 id="14-geocoding-neshan">14 · Geocoding (Neshan)</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>MockGeocoder</code> jitters ±5 km around 8 hardcoded centroids (<code>MockGeocoder.cs:52</code>) — EVV distance checks are noise until this (and/or REQ-008's user pin) is real. <code>Seams:Geocoding:*</code> is one of only three seam sections present in <code>appsettings.json</code> (<code>:22-26</code>).</li><li><strong>Default:</strong> Neshan (Iran coverage; SaaS API key), rate-limit/retry per registry row 31.</li></ul>
|
||||
<h3 id="15-review-moderation">15 · Review moderation</h3>
|
||||
<ul><li><strong>Evidence:</strong> keyword-list mock; clean text stays in the human queue by default (<code>MockReviewModerationService.cs:23</code>; <code>Seams:ReviewModeration:*</code>). Human <code>ModerateReviewCommand</code> retains decision authority — so this can stay mocked indefinitely at low volume.</li><li><strong>Default:</strong> any text-moderation API / LLM endpoint when review volume outgrows the human queue.</li></ul>
|
||||
<h3 id="16-moh-ino-enamad-a-process-not-a-service">16 · MoH / INO / eNamad — a process, not a service</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>MockCredentialVerifier</code> always returns <code>RequiresManualReview</code> (<code>MockCredentialVerifier.cs:18</code>); <code>MockLicenseVerificationService</code> likewise (<code>MockLicenseVerificationService.cs:26</code>). The registry itself records <strong>no public B2B API exists</strong> — the admin review queue <em>is</em> the real implementation. Provision: admin staffing + the f15 console.</li></ul>
|
||||
<h3 id="17-elasticsearch-deliberately-later">17 · Elasticsearch — deliberately later</h3>
|
||||
<ul><li><strong>Evidence:</strong> <code>SqlNurseSearch</code> is the real MVP backend; <code>Search:Backend</code> ≠ <code>sql</code> throws at startup (<code>Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:74-78</code>); no ES client package.</li><li><strong>Default when needed:</strong> <code>elasticsearch:8.x</code> + the outbox feeder (registry rows 38/43; plan §8.1).</li></ul>
|
||||
<hr>
|
||||
<h2 id="deployment-notes-from-the-code-not-aspiration">Deployment notes (from the code, not aspiration)</h2>
|
||||
<ol><li><strong>Boot = migrate + seed.</strong> Every non-Testing start applies EF migrations and seeds roles, the <code>admin</code>/<code>qw123321</code> user, and an <strong>active sandbox ZarinPal gateway</strong> (<code>Program.cs:99-104</code>). Until plan §1.3/§1.4/§4.3 land: single-instance start-up, DDL-privileged login, and clean up the seeded credentials per environment.</li><li><strong>Environment files:</strong> <code>appsettings.json</code> ≡ <code>appsettings.Development.json</code> (byte-identical); <strong>no Production/Staging file exists.</strong> All non-secret env differences ride on ~14 <code>Seams:*</code> groups whose defaults live in code (<code>SeamOptions.cs</code>), not in config files.</li><li><strong>HTTP posture:</strong> HTTP/2-only Kestrel default (plan §1.5), gRPC plugin + reflection always on (plan §7.5), TLS required for JWE sanity.</li><li><strong>Single-instance constraints today:</strong> in-memory cache, in-proc money lock, in-proc sweeps, per-instance rate-limit buckets. Scaling past one instance requires plan §4.2 (Redis) + §4.3 (migrations) first — the DB uniques keep money <em>correct</em> either way, but locks/cache/limits silently degrade.</li><li><strong>Logs:</strong> deployed envs write Warning+ to <code>Baya_Logs</code> only (Information dropped — plan §7.3); dev writes console + <code>logs/log.json</code>.</li><li><strong>Client:</strong> the Next.js app needs <code>NEXT_PUBLIC_API_URL</code> pointing at the proxy; wire casing camelCase; snake_case routes.</li></ol>
|
||||
</main></body></html>
|
||||
@@ -0,0 +1,280 @@
|
||||
# Runtime services — deployment topology
|
||||
|
||||
**Audit date:** 2026-07-10 · **Derivation:** every entry below is justified from the code — the DI seam
|
||||
that depends on it, the config key that names it, or the package/startup wiring that talks to it. Nothing
|
||||
is invented; "not needed" claims are backed by the absence of the package/code. Make-it-real steps live in
|
||||
`dev/shared-working-context/reports/mocks-registry.md` (row references below).
|
||||
|
||||
**The shape in one sentence:** today the API binary talks to exactly **one external system — SQL Server**
|
||||
(app DB + log DB); everything else (18 seams) is an in-process mock, so "deployment" today is one container
|
||||
+ one database — and the table below is the roadmap of what must exist as each seam goes real.
|
||||
|
||||
## Service inventory
|
||||
|
||||
| # | Service | Purpose | Depends via (seam / config) | MVP? | Registry row |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | **SQL Server** (app DB `Baya`) | System of record — 12 schemas (`usr ops geo catalog verif search booking payments payouts reviews messaging partner`) | EF Core; `ConnectionStrings:SqlServer` | **Required now** | — |
|
||||
| 2 | **SQL Server** (log DB `Baya_Logs`) | Serilog sink in deployed envs (Warning+, auto-created `log.LogEvents`) | `ConnectionStrings:logDb` | **Required now** (deployed) | — |
|
||||
| 3 | **Reverse proxy / TLS** (nginx·caddy·traefik) | TLS termination, HTTP/1.1+2, forwarded headers | Kestrel config; JWE bearer | **Required now** | — |
|
||||
| 4 | **Prometheus** (+ Grafana) | Scrapes `/metrics`; health forwarded to gauges | `UseMetricServer` + OTel exporter | **Recommended now** | — |
|
||||
| 5 | **Redis** | `ICacheService` + `IDistributedLock` (money-path mutex) | `Seams:*` (keys TBD; none today) | Before >1 API instance | rows 14, 42 |
|
||||
| 6 | **MinIO / S3 / ArvanCloud** | `IObjectStorage` — verification docs, avatars (REQ-006), invoice PDFs | `Seams:ObjectStorage:*` | Before real verification | row 13 |
|
||||
| 7 | **Job scheduler** (Hangfire/Quartz, in-app on SQL) | The deferred crons: payout batch, expiry scan, no-show, Moadian poll | hosted services (no interface exists) | Before unattended ops | row 26 |
|
||||
| 8 | **SMS gateway** (Kavenegar·Ghasedak·SMS.ir) | `ISmsSender` — OTP delivery (login is impossible without it) | `Seams:Sms:*` (to be added) | **Launch-critical** | row 12 |
|
||||
| 9 | **PSP / IPG + Shaparak** (ZarinPal·Sadad·Vandar·Jibit) | `IPaymentProvider` + `IWebhookVerifier` + `ISettlementSplitProvider` (تسهیم) | encrypted `payment_gateways.config_json` | Real payments | rows 39–41 |
|
||||
| 10 | **BNPL providers** (SnappPay·Digipay) | `IBnplProvider` / `IBnplProviderResolver` / `ICurrencyNormalizer` | `Seams:Bnpl:*`, `Seams:Currency:*`, gateway config | Optional at launch | rows 46–47 |
|
||||
| 11 | **Bank-transfer rail** (Jibit·Vandar·Sadad payout API → PAYA/SATNA) | `IBankTransferProvider` — weekly nurse payouts | `Seams:BankTransfer:*` | Real payouts | row 23 |
|
||||
| 12 | **سامانه مودیان** (tax e-invoicing) | `IMoadianClient` — legal invoice registration | `Seams:Moadian:*` + signing cert | Legal — soon after launch | row 45 |
|
||||
| 13 | **KYC bridge vendor** (Finnotech-class) | `IShahkarVerifier` + `IIdentityKycProvider` + `IBankAccountOwnershipVerifier` | `Seams:Shahkar:*`, `Seams:IdentityKyc:*`, `Seams:BankOwnership:*` | Real verification + payout gate | rows 27, 28, 30 |
|
||||
| 14 | **Geocoding** (Neshan) | `IGeocoder` — address → coordinates (EVV distance) | `Seams:Geocoding:*` | With real EVV | row 31 |
|
||||
| 15 | **Review-moderation classifier** (LLM/API) | `IReviewModerationService` — AI pre-screen | `Seams:ReviewModeration:*` | Optional (human queue is default) | row 33 |
|
||||
| 16 | **MoH / INO / eNamad** | `ICredentialVerifier` + `ILicenseVerificationService` — **manual admin process; no public B2B API exists** | `Seams:LicenseVerification:*` | Manual = the MVP design | rows 29, 50 |
|
||||
| 17 | **Elasticsearch** | `INurseSearch` alt backend + outbox feeder | `Search:Backend` (non-`sql` throws today) | **Not MVP** — SQL search is real | rows 38, 43 |
|
||||
|
||||
**Explicitly not needed** (verified absent from `server/Directory.Packages.props` and code): message
|
||||
broker (no RabbitMQ/Kafka), Redis today (no `StackExchange.Redis`), Hangfire/Quartz today, Elasticsearch
|
||||
client (`Elastic.Clients.*`), any cloud SDK, any payment/SMS vendor SDK. The only externally-pointing
|
||||
package beyond SQL Server is `Serilog.Sinks.Elasticsearch` — its wiring is commented out
|
||||
(`Baya.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs:58-70`).
|
||||
|
||||
---
|
||||
|
||||
## Dependency graph
|
||||
|
||||
Solid edges are live today; dashed edges are behind a mocked seam (the arrow exists in code, the wire does
|
||||
not). One line per node below the graph.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph fe[Frontend]
|
||||
WEB["Next.js client"]
|
||||
end
|
||||
subgraph host["API host — Baya.Web.Api (:5002)"]
|
||||
API["ASP.NET Core API\nREST /api/v1 · /metrics · /HealthCheck"]
|
||||
JOBS["In-proc interval jobs\n(→ Hangfire/Quartz later)"]
|
||||
end
|
||||
subgraph data[Data & platform infra]
|
||||
SQL[("SQL Server 'Baya'\n12 schemas · migrations on boot")]
|
||||
LOG[("SQL Server 'Baya_Logs'\nSerilog sink, Warning+")]
|
||||
REDIS[("Redis — cache + dist. lock\n(in-proc today)")]
|
||||
S3[("MinIO / S3 — object storage\n(local disk today)")]
|
||||
end
|
||||
subgraph money["Money rails (all mocked today)"]
|
||||
PSP["PSP / IPG + Shaparak\ncapture · webhook · تسهیم"]
|
||||
BNPL["BNPL — SnappPay / Digipay"]
|
||||
BANK["PAYA / SATNA payout rail"]
|
||||
MOAD["سامانه مودیان e-invoicing"]
|
||||
end
|
||||
subgraph trust["Trust & identity rails (all mocked today)"]
|
||||
SMS["SMS gateway (OTP)"]
|
||||
KYC["Shahkar · e-KYC · استعلام شبا"]
|
||||
GEOC["Neshan geocoding"]
|
||||
MODAI["Review-moderation classifier"]
|
||||
MANUAL["MoH / INO / eNamad\n(manual admin review)"]
|
||||
end
|
||||
subgraph obs[Observability]
|
||||
PROM["Prometheus (+ Grafana)"]
|
||||
end
|
||||
WEB -->|HTTPS/JSON| API
|
||||
API --> SQL
|
||||
API --> LOG
|
||||
JOBS --- API
|
||||
API -.-> REDIS
|
||||
API -.-> S3
|
||||
API -.-> SMS
|
||||
API <-.-> PSP
|
||||
API <-.-> BNPL
|
||||
API -.-> BANK
|
||||
API -.-> MOAD
|
||||
API -.-> KYC
|
||||
API -.-> GEOC
|
||||
API -.-> MODAI
|
||||
MANUAL -.- API
|
||||
PROM -->|scrape /metrics| API
|
||||
```
|
||||
|
||||
- **Next.js client** — the only API consumer; reads `NEXT_PUBLIC_API_URL` (root `CLAUDE.md`).
|
||||
- **API** — single ASP.NET Core host; all seams resolve in-process today.
|
||||
- **In-proc jobs** — the two `BackgroundService` sweeps (booking-request expiry 1 min, notification
|
||||
retention 24 h); the scheduler upgrade re-homes them (plan §4.1).
|
||||
- **SQL Server `Baya`** — system of record; migrations + seeding run on every non-Testing boot.
|
||||
- **SQL Server `Baya_Logs`** — deployed-env Serilog sink (auto-creates DB/table).
|
||||
- **Redis** — target for `ICacheService`/`IDistributedLock`; nothing speaks Redis yet.
|
||||
- **MinIO/S3** — target for `IObjectStorage`; local disk + `file://` URLs today.
|
||||
- **PSP/IPG** — card capture, callback signatures, تسهیم settlement split.
|
||||
- **BNPL** — provider-financed installments; settle/revert callbacks.
|
||||
- **PAYA/SATNA** — weekly nurse payout batches + async reconciliation.
|
||||
- **مودیان** — legal e-invoice registration (pending→registered poll).
|
||||
- **KYC vendor** — Shahkar phone↔NID, identity+liveness, Sheba ownership (payout gate).
|
||||
- **Neshan** — geocoding for address coordinates / EVV distance.
|
||||
- **Moderation classifier** — optional AI pre-screen; human moderation is the default gate.
|
||||
- **MoH/INO/eNamad** — human verification workflows, by design (no API exists).
|
||||
- **Prometheus** — scrapes `/metrics`; health check results forwarded as gauges.
|
||||
|
||||
---
|
||||
|
||||
## Per-service notes
|
||||
|
||||
### 1–2 · SQL Server (`Baya` + `Baya_Logs`)
|
||||
|
||||
- **Evidence:** `UseSqlServer` at
|
||||
`server/src/Infrastructure/Baya.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:41`;
|
||||
sink at `…CrossCutting/Logging/LoggingConfiguration.cs:44-45` (schema `log`, auto-create); the 12 schemas
|
||||
via per-entity `ToTable(name, schema)` (e.g. `PaymentsConfig/LedgerEntryConfig.cs:18`,
|
||||
`PayoutsConfig/NursePayoutConfig.cs:19`).
|
||||
- **Default:** `mcr.microsoft.com/mssql/server:2022-latest` (Developer for dev; licensed edition in prod).
|
||||
Both DBs fit one instance; `Baya_Logs` can move later.
|
||||
- **Config:** `ConnectionStrings:SqlServer`, `ConnectionStrings:logDb` — **rotate + externalize first**
|
||||
(plan §1.1; live `sa` credentials are committed today).
|
||||
- **Health/readiness:** the app's only health check (`/HealthCheck`,
|
||||
`Monitoring/Configurations/HealthCheckConfigurations.cs:17`); `logDb` has none (plan §7.2). Boot runs
|
||||
`MigrateAsync` + 3 seeders (`Program.cs:99-104`) → the login needs DDL rights and concurrent multi-node
|
||||
boot races (plan §4.3).
|
||||
|
||||
### 3 · Reverse proxy / TLS
|
||||
|
||||
- **Evidence of need:** JWE bearer auth (`RequireHttpsMetadata` must be true in prod —
|
||||
`Identity/ServiceConfiguration/ServiceCollectionExtension.cs:139`); Kestrel `EndpointDefaults=Http2`
|
||||
breaks non-TLS HTTP/1.1 (`appsettings.json:29-33`, plan §1.5); the rate limiter partitions on
|
||||
`RemoteIpAddress` with **no ForwardedHeaders middleware**
|
||||
(`WebFramework/ServiceConfiguration/RateLimitingServiceExtension.cs:69`, plan §1.6) — the proxy must pass
|
||||
`X-Forwarded-For` *and* the app must be taught to honor it.
|
||||
- **Default:** caddy 2 / nginx 1.27; terminate TLS, h2 to clients, HTTP/1.1 (or h2c) upstream once §1.5
|
||||
lands.
|
||||
|
||||
### 4 · Prometheus (+ Grafana)
|
||||
|
||||
- **Evidence:** `/metrics` via prometheus-net `UseMetricServer` + OTel `AddPrometheusExporter` (two stacks —
|
||||
consolidate, plan §7.1) at `Monitoring/Configurations/PrometheusMetricsConfigurations.cs:11` and
|
||||
`OpenTelemetryConfigurations.cs:21`; health forwarded (`HealthCheckConfigurations.cs:18`).
|
||||
- **Default:** `prom/prometheus:v2.53` + `grafana/grafana:11`. No tracing backend exists yet (metrics-only);
|
||||
an OTLP collector becomes relevant with plan §7.1.
|
||||
|
||||
### 5 · Redis
|
||||
|
||||
- **Evidence of the gap:** `MemoryCacheService` and `InProcessDistributedLock`
|
||||
(`CrossCutting/Seams/MemoryCacheService.cs:11`, `InProcessDistributedLock.cs:14`) — single-process only;
|
||||
the money-path lock convention `booking:{id}:payment|refund` is already in the handlers.
|
||||
- **Default:** `redis:7-alpine` (AOF on). **Required the moment a second API instance runs** (shared cache
|
||||
invalidation generation-tokens + cross-instance money mutex). Config keys to be introduced with the swap
|
||||
(plan §4.2); none exist today.
|
||||
|
||||
### 6 · MinIO / S3 / ArvanCloud
|
||||
|
||||
- **Evidence:** `LocalDiskObjectStorage` writes under a temp root and returns `file://` URLs
|
||||
(`CrossCutting/Seams/LocalDiskObjectStorage.cs:20,56`); consumers: b6 verification documents
|
||||
(signed-URL upload flow), future avatars (REQ-006), invoice `PdfStorageKey`
|
||||
(`InvoicesConfig/InvoiceConfig.cs` — `pdf_storage_key` column).
|
||||
- **Default:** `minio/minio:latest` (S3-compatible; ArvanCloud object storage is the Iran-hosted option).
|
||||
- **Config:** `Seams:ObjectStorage:RootPath` today → bucket/endpoint/keys with the swap. Presigned PUT/GET
|
||||
with expiry is the contract the frontend already codes against.
|
||||
|
||||
### 7 · Job scheduler
|
||||
|
||||
- **Evidence:** two `PeriodicTimer` hosted services only
|
||||
(`Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:63,67`); the payout/expiry/no-show/
|
||||
Moadian crons are admin-manual with seeded-but-unread cadence keys (plan §4.1). **No `IJobScheduler`
|
||||
interface exists** — the registry name is aspirational.
|
||||
- **Default:** Hangfire on the existing SQL Server (no new container) — dashboard behind admin auth; or
|
||||
Quartz with SQL persistence. Not a separate service to "spin up", but it changes the SQL footprint
|
||||
(schema) and ops (dashboard, retries).
|
||||
|
||||
### 8 · SMS gateway
|
||||
|
||||
- **Evidence:** `LoggingSmsSender` logs OTPs instead of sending (`CrossCutting/Seams/LoggingSmsSender.cs:16`)
|
||||
— **no real user can log in**; per-phone resend window + `otp` rate policy already enforced upstream.
|
||||
- **Default:** Kavenegar / Ghasedak / SMS.ir (SaaS — API key, no container). Keys to add:
|
||||
`Seams:Sms:{ApiKey,SenderLine,BaseUrl}` (registry row 12). Template/pattern OTP send for deliverability.
|
||||
|
||||
### 9 · PSP / IPG + Shaparak (تسهیم)
|
||||
|
||||
- **Evidence:** `MockPaymentProvider` (instant success, `VerifyAsync` echoes the expected amount —
|
||||
`CrossCutting/Seams/MockPaymentProvider.cs:24`), `MockWebhookVerifier` (marker-based "signature" —
|
||||
`MockWebhookVerifier.cs:21`), `MockSettlementSplitProvider` (`MockSettlementSplitProvider.cs:13`). Real
|
||||
merchant credentials belong in the **encrypted** `payment_gateways.config_json` (seeded sandbox row:
|
||||
`Persistence/ServiceCollectionExtensions.cs:109-117` — environment-gate it, plan §1.4).
|
||||
- **Default:** SaaS (ZarinPal / Sadad / Vandar / Jibit) — needs merchant + terminal registration and
|
||||
تسهیم (settlement-split) setup to registered IBANs; webhook endpoint is already public
|
||||
(`POST webhooks/payments/{provider}`) with upsert-first idempotency in place.
|
||||
|
||||
### 10 · BNPL providers
|
||||
|
||||
- **Evidence:** `MockBnplProvider` drives the full verb set + state machine
|
||||
(`CrossCutting/Seams/MockBnplProvider.cs:17-53`); callback endpoint `WebhooksBnplController` (signed,
|
||||
rate-limited). `Seams:Bnpl:*` + `Seams:Currency:TomanToIrrMultiplier` (`SeamOptions.cs:98-120,79`).
|
||||
- **Default:** SnappPay and/or Digipay (SaaS, OAuth). **Do plan §2.1 first** — the revert-clearing path is
|
||||
currently unreachable, so real BNPL refunds would strand ledger state.
|
||||
|
||||
### 11 · Bank-transfer rail (PAYA/SATNA)
|
||||
|
||||
- **Evidence:** `MockBankTransferProvider` settles every instruction instantly
|
||||
(`CrossCutting/Seams/MockBankTransferProvider.cs:18-31`); the handler already chooses PAYA vs SATNA by
|
||||
`payout_satna_threshold_irr` (`platform_configs` seed row 22) and the irreversibility backstops
|
||||
(unconditional `UNIQUE(booking_id)` link, forward-only payout machine) are in place.
|
||||
- **Default:** Jibit / Vandar / Sadad payout API (SaaS). Needs the source settlement account + the async
|
||||
`submitted → paid/failed` reconciliation callback the mock collapses (registry row 23, steps 3–4).
|
||||
|
||||
### 12 · سامانه مودیان
|
||||
|
||||
- **Evidence:** `MockMoadianClient` leaves invoices `pending` forever (`CrossCutting/Seams/MockMoadianClient.cs:21`);
|
||||
no reconciliation job exists (plan §6.5). VAT-on-commission + sequential invoice numbers are already
|
||||
correct server-side.
|
||||
- **Default:** government SaaS — enrollment (memory/economic code) + a signing certificate; the certificate
|
||||
is a deploy-time secret.
|
||||
|
||||
### 13 · KYC bridge (Shahkar / e-KYC / استعلام شبا)
|
||||
|
||||
- **Evidence:** three deterministic mocks with magic-value failure cases
|
||||
(`MockShahkarVerifier.cs:26-37`, `MockIdentityKycProvider.cs:25`,
|
||||
`MockBankAccountOwnershipVerifier.cs:26`); handlers already persist `external_response_json` and treat
|
||||
shared-SIM as a handled state. The Sheba-ownership result gates first payouts (`matched_national_id`).
|
||||
- **Default:** one Finnotech-class bridge covers all three استعلامها (SaaS; API keys under
|
||||
`Seams:{Shahkar,IdentityKyc,BankOwnership}:*`).
|
||||
|
||||
### 14 · Geocoding (Neshan)
|
||||
|
||||
- **Evidence:** `MockGeocoder` jitters ±5 km around 8 hardcoded centroids (`MockGeocoder.cs:52`) — EVV
|
||||
distance checks are noise until this (and/or REQ-008's user pin) is real. `Seams:Geocoding:*` is one of
|
||||
only three seam sections present in `appsettings.json` (`:22-26`).
|
||||
- **Default:** Neshan (Iran coverage; SaaS API key), rate-limit/retry per registry row 31.
|
||||
|
||||
### 15 · Review moderation
|
||||
|
||||
- **Evidence:** keyword-list mock; clean text stays in the human queue by default
|
||||
(`MockReviewModerationService.cs:23`; `Seams:ReviewModeration:*`). Human `ModerateReviewCommand` retains
|
||||
decision authority — so this can stay mocked indefinitely at low volume.
|
||||
- **Default:** any text-moderation API / LLM endpoint when review volume outgrows the human queue.
|
||||
|
||||
### 16 · MoH / INO / eNamad — a process, not a service
|
||||
|
||||
- **Evidence:** `MockCredentialVerifier` always returns `RequiresManualReview`
|
||||
(`MockCredentialVerifier.cs:18`); `MockLicenseVerificationService` likewise
|
||||
(`MockLicenseVerificationService.cs:26`). The registry itself records **no public B2B API exists** —
|
||||
the admin review queue *is* the real implementation. Provision: admin staffing + the f15 console.
|
||||
|
||||
### 17 · Elasticsearch — deliberately later
|
||||
|
||||
- **Evidence:** `SqlNurseSearch` is the real MVP backend; `Search:Backend` ≠ `sql` throws at startup
|
||||
(`Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:74-78`); no ES client package.
|
||||
- **Default when needed:** `elasticsearch:8.x` + the outbox feeder (registry rows 38/43; plan §8.1).
|
||||
|
||||
---
|
||||
|
||||
## Deployment notes (from the code, not aspiration)
|
||||
|
||||
1. **Boot = migrate + seed.** Every non-Testing start applies EF migrations and seeds roles, the
|
||||
`admin`/`qw123321` user, and an **active sandbox ZarinPal gateway** (`Program.cs:99-104`). Until plan
|
||||
§1.3/§1.4/§4.3 land: single-instance start-up, DDL-privileged login, and clean up the seeded credentials
|
||||
per environment.
|
||||
2. **Environment files:** `appsettings.json` ≡ `appsettings.Development.json` (byte-identical); **no
|
||||
Production/Staging file exists.** All non-secret env differences ride on ~14 `Seams:*` groups whose
|
||||
defaults live in code (`SeamOptions.cs`), not in config files.
|
||||
3. **HTTP posture:** HTTP/2-only Kestrel default (plan §1.5), gRPC plugin + reflection always on
|
||||
(plan §7.5), TLS required for JWE sanity.
|
||||
4. **Single-instance constraints today:** in-memory cache, in-proc money lock, in-proc sweeps, per-instance
|
||||
rate-limit buckets. Scaling past one instance requires plan §4.2 (Redis) + §4.3 (migrations) first — the
|
||||
DB uniques keep money *correct* either way, but locks/cache/limits silently degrade.
|
||||
5. **Logs:** deployed envs write Warning+ to `Baya_Logs` only (Information dropped — plan §7.3); dev writes
|
||||
console + `logs/log.json`.
|
||||
6. **Client:** the Next.js app needs `NEXT_PUBLIC_API_URL` pointing at the proxy; wire casing camelCase;
|
||||
snake_case routes.
|
||||
@@ -12,6 +12,41 @@ for awareness.
|
||||
- **Requests filed:** frontend/requests/for-backend.md (yes/no)
|
||||
-->
|
||||
|
||||
## frontend-phase-9-b10 — Checkout, card payment & invoice — 2026-07-10
|
||||
- **Shipped:** the money moment — a **new `services/payment` domain** (types/keys/constants/
|
||||
apis[client+mock]/invalidations/5 hooks + barrel) and the customer checkout flow: **C6 خلاصه و پرداخت**
|
||||
`/bookings/checkout?request_id=` (acceptance badge, served **reconciling** service-cost/کارمزد/مالیات/مبلغ کل
|
||||
breakdown, verbatim **escrow notice**, payment-window countdown, «ادامه پرداخت ←» with an
|
||||
**idempotency-key-per-attempt**, disabled BNPL seam for f11), the **card states** (initiating → redirect →
|
||||
dev **mock-gateway harness** `/bookings/checkout/gateway` → return `/bookings/checkout/return` with a
|
||||
**backoff pending-callback poll** → succeeded/failed/expired), the **confirmation**
|
||||
`/bookings/checkout/confirmation` («مشاهده رزرو» + «دانلود فاکتور»), and the **invoice**
|
||||
`/bookings/[id]/invoice` (VAT-on-commission line, read-only مودیان state, pdfUrl download or print
|
||||
receipt). Three shared tested composites: `PriceBreakdown`, `EscrowNotice`, `PaymentStatusBadge`. New
|
||||
`payment` i18n namespace (53 keys, both locales).
|
||||
- **Load-bearing rules honored:** money = IRR digit-strings, **BigInt only** (integer parts-per-10000 rate
|
||||
math in the mock — zero floats); the breakdown **must reconcile** (`PriceBreakdown` dev-guards it; rows
|
||||
are served, VAT never derived client-side); **VAT on the commission only** (invoice line labelled
|
||||
accordingly); escrow copy **verbatim** in fa (pinned by test against fa.json), info tone never error;
|
||||
**409 = benign convergence** (re-reads the outcome, never an error toast); poll uses **geometric backoff,
|
||||
stops on terminal + bounded attempts**; success flips the booking **by cache invalidation** (exact keys,
|
||||
no refetch storm).
|
||||
- **Consumes:** dev/contracts/domains/payments.md (b10 — initiate route + `Idempotency-Key` header +
|
||||
`InitiatePaymentResult`; status enum `pending|succeeded|failed`) and the invoice slice of
|
||||
refunds-invoices.md (b11 — `GET invoices/{bookingId}`, `InvoiceDto`), casing verified against
|
||||
swagger.v1.json. **Not served by any contract:** a checkout summary, a client transaction read, and the
|
||||
converted request's booking id → REQ-016/017/018.
|
||||
- **Mocked client-side:** `services/payment` via `paymentMockApi` (**USE_PAYMENT_MOCK=true, primary**) —
|
||||
it is the missing **conversion trigger bridging the f7 ↔ f8 mock stores**: capture converts the request
|
||||
(`converted` + client-augmented `bookingId`), inserts a **confirmed** booking into the f8 store, and
|
||||
auto-issues the b11-shaped invoice, so C5 → C6 → gateway → confirmation → booking detail → invoice runs
|
||||
end-to-end in one session. The dev **mock-gateway page is a test harness, not a product feature**. Real
|
||||
`paymentClientApi`: initiate/invoice = published contract; summary targets the REQ-016 proposed slug;
|
||||
outcome maps `booking_requests/get` (REQ-017).
|
||||
- **Gate:** npm run check green · npm run test:ci green (204 tests, +9) · production build green.
|
||||
- **Requests filed:** frontend/requests/for-backend.md — yes (REQ-016 checkout summary, REQ-017 payment
|
||||
outcome + bookingId, REQ-018 customer invoice availability post-capture).
|
||||
|
||||
## frontend-phase-8-b9 — Booking detail, sessions & nurse EVV — 2026-07-10
|
||||
- **Shipped:** the post-payment engagement — a **new** `services/bookings` domain (the sibling of
|
||||
`bookingRequests`, NOT a rename): types/keys/constants/apis[client(1:1 b9)+mock+serverApi]/8 hooks +
|
||||
|
||||
@@ -214,3 +214,52 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
a casing/int drift or a `false`-vs-`null` conflation would mislabel a visit. Low-risk (mock-primary now),
|
||||
but worth locking before f9/f13 consume the same shapes.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-016 — Checkout summary for C6 (served gross/commission/VAT breakdown) — filed by frontend-phase-9-b10 — 2026-07-10
|
||||
- **Need:** A customer-facing read that serves the C6 «خلاصه و پرداخت» money rows for an
|
||||
`accepted_awaiting_payment` request: the three b10 amounts (`grossPriceIrr`,
|
||||
`balinyaarCommissionIrr`, `nursePayoutAmount`) **plus the display decomposition** — service cost,
|
||||
commission **net of VAT**, `vatIrr` + `vatRate` — and the nurse/variant/schedule mini-info +
|
||||
`paymentDeadlineAt`.
|
||||
- **Why:** The b8 `BookingRequestDto` is money-free by design and no checkout-summary endpoint exists,
|
||||
but C6 must show a breakdown that **reconciles to the rial** (service + commission + VAT = total) and
|
||||
the client is forbidden from deriving commission or tax itself (no float math, rates are server
|
||||
config). Today the whole C6 money surface is mocked (`services/payment` mock computes the split from
|
||||
the mock variant price at the configured 12% fee / 10% VAT).
|
||||
- **Proposed shape:** `GET api/v1/booking_requests/checkout_summary/{id}` (owner-scoped) →
|
||||
`{ bookingRequestId, requestStatus, nurseName, patientName, variantLabel, variantPriceUnit,
|
||||
sessionCount, requestedDate, requestedTimeStart, requestedTimeEnd, paymentDeadlineAt,
|
||||
serviceCostIrr, commissionIrr, vatIrr, vatRate, totalIrr, grossPriceIrr, balinyaarCommissionIrr,
|
||||
nursePayoutAmount }` — the client's real `paymentClientApi.getCheckoutSummary` already targets this
|
||||
slug and unwraps this exact shape (`client/src/services/payment/types.ts: CheckoutSummaryDto`).
|
||||
- **Status:** open
|
||||
|
||||
## REQ-017 — Client-readable payment outcome + `bookingId` on a converted request — filed by frontend-phase-9-b10 — 2026-07-10
|
||||
- **Need:** After the gateway redirect returns, the client needs to learn (a) the payment transaction's
|
||||
status (`pending|succeeded|failed`) and (b) **which booking** the capture created. Either a
|
||||
transaction read (e.g. `GET api/v1/payment_transactions/{id}` or
|
||||
`GET api/v1/bookings/{bookingRequestId}/payments/latest`, owner-scoped) or, minimally, a
|
||||
`bookingId` field on `BookingRequestDto` once `status = converted`.
|
||||
- **Why:** b10 confirms captures inside the PSP webhook (correct — the client is never trusted), so the
|
||||
frontend's pending-callback state can only poll `booking_requests/get/{id}` and map
|
||||
`converted → succeeded` / `payment_deadline_expired → failed`. That works, but it cannot distinguish a
|
||||
*declined* payment (still `accepted_awaiting_payment`, retry allowed) from a *slow* callback, and the
|
||||
confirmation screen cannot deep-link «مشاهده رزرو» or «دانلود فاکتور» because the converted request
|
||||
never reveals its booking id. The client DTO already carries a client-augmented
|
||||
`bookingId: number | null` (mock fills it; real path returns null and the UI falls back to the
|
||||
bookings list, hiding the invoice link).
|
||||
- **Proposed shape:** add `bookingId: long?` to `BookingRequestDto` (null until converted) **and/or**
|
||||
`GET api/v1/bookings/{bookingRequestId}/payments/latest` → `{ transactionId, status,
|
||||
gatewayReferenceCode, bookingId? }`.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-018 — Customer invoice availability after capture (auto-issue or owner-issue) — filed by frontend-phase-9-b10 — 2026-07-10
|
||||
- **Need:** Make the b11 invoice reachable by the paying customer right after capture: auto-issue the
|
||||
commission invoice on card capture (idempotent per booking, as `POST admin_invoices` already is), or
|
||||
allow the owning customer to trigger the idempotent issue on first `GET api/v1/invoices/{bookingId}`.
|
||||
- **Why:** The f9 confirmation screen offers «دانلود فاکتور», but b11 issues invoices only via the
|
||||
admin-only `POST api/v1/admin_invoices`, so a customer's `GET invoices/{bookingId}` 404s until an
|
||||
admin acts. The UI handles the 404 as a "فاکتور هنوز صادر نشده است" state (and the mock auto-issues at
|
||||
capture to demo the full flow), but on the real rails every fresh payment would land on that empty
|
||||
state.
|
||||
- **Status:** open
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
# Frontend phase 9 report — Checkout, card payment & invoice (consumes b10 + the invoice slice of b11)
|
||||
|
||||
**Status:** complete · gate green (`npm run check`, `npm run test:ci` — 204 tests, production build) · 2026-07-10
|
||||
**Scope shipped:** the C6 خلاصه و پرداخت checkout, the card-payment state machine (initiate → redirect →
|
||||
pending-callback → succeeded→confirmed / failed→retry), the confirmation screen, the invoice view, the
|
||||
`services/payment` domain, and three shared money composites.
|
||||
|
||||
---
|
||||
|
||||
## 1. What was built
|
||||
|
||||
### `services/payment` (new domain — mirrors the `auth`/`bookings` shape)
|
||||
|
||||
| File | What it is |
|
||||
| --- | --- |
|
||||
| `types.ts` | Contract-derived DTOs + the `PaymentApi` seam. `PaymentTransactionStatus = pending\|succeeded\|failed` (the b10 enum — the phase file's illustrative `initiated/cancelled` states do **not** exist on the wire and were not used). `CheckoutSummaryDto` (REQ-016 shape), `InitiatePaymentResult` (b10 swagger), `PaymentOutcomeDto` (poll target), `InvoiceDto` (b11 swagger, flat totals — **no line-items array exists in the contract**). |
|
||||
| `keys.ts` | `paymentKeys.summary(requestId)` / `.outcome(requestId)` / `.invoice(bookingId)`. |
|
||||
| `constants.ts` | `USE_PAYMENT_MOCK=true` (why documented in-file), `BNPL_ENABLED=false` (the f11 seam gate), poll backoff tuning, checkout query-param names, mock money rates. |
|
||||
| `apis/clientApi.ts` | Real impl: **initiate** = `POST api/v1/bookings/{id}/payments` + **`Idempotency-Key` header** (no body — contract-exact); **invoice** = `GET api/v1/invoices/{bookingId}`; **summary** targets the REQ-016 proposed slug; **outcome** maps `GET booking_requests/get/{id}` (`converted`→succeeded, `payment_deadline_expired`→failed, else pending — REQ-017). |
|
||||
| `apis/mockApi.ts` | Mock-primary state machine — see §3. |
|
||||
| `invalidations.ts` | `invalidateAfterPaymentSuccess` — the one post-capture cache transition (request detail/lists + bookings lists/detail + this request's summary/outcome). Exact keys, never a blanket refetch. |
|
||||
| `hooks/` | `useCheckoutSummary` (short stale), `useInitiatePayment` (caller owns the per-attempt key), `useConfirmGatewayReturn` (primes the outcome key; invalidates on immediate success), `usePaymentOutcome` (**geometric-backoff poll**: 2s → ×1.5 → cap 15s, stops on terminal outcome or 40 attempts; exports `isTerminalPaymentOutcome`), `useInvoice` (immutable → long stale; **404 = "not issued", not retried**). |
|
||||
|
||||
### Screens (customer shell)
|
||||
|
||||
- **C6** `/bookings/checkout?request_id=` — «✓ پرستار تایید کرد» badge (reuses `booking.accepted_badge`),
|
||||
nurse/service/schedule mini-summary, the payment-window `CountdownTimer`, the **served reconciling
|
||||
breakdown** (هزینه خدمت / کارمزد بالینیار / مالیات بر ارزش افزوده / **مبلغ کل**) via `PriceBreakdown`,
|
||||
the verbatim `EscrowNotice`, «ادامه پرداخت ←» (terracotta `secondary`), and the **BNPL seam** — a
|
||||
disabled outlined «یا پرداخت اقساطی» + "coming soon" caption gated by `BNPL_ENABLED` for f11 to wire to D1.
|
||||
Non-payable statuses render convergence cards (already-paid → outcome; window-expired / other terminal → back to C5).
|
||||
- **Gateway harness** `/bookings/checkout/gateway` — **test-only** fake PSP (labelled آزمایشی): success +
|
||||
failure buttons so both return branches are drivable without a gateway. The phase file suggested
|
||||
auto-success; buttons were chosen instead so §7 step 5 (failed attempt → new idempotency key) is testable by a human.
|
||||
- **Return** `/bookings/checkout/return` — fires `useConfirmGatewayReturn` once per mount (ref-guarded; a
|
||||
refresh replays it and converges idempotently), then the pending-callback poll. Succeeded → invalidate +
|
||||
`router.replace` to confirmation (once, ref-guarded, no double invalidation between mutation and poll
|
||||
paths); failed → retry (back to a fresh C6 mount = **new attempt, new key**); window lapsed → back to C5.
|
||||
A manual «بررسی دوباره» covers the bounded poll giving up.
|
||||
- **Confirmation** `/bookings/checkout/confirmation` — success state, amount-paid card, «مشاهده رزرو» →
|
||||
`/bookings/{bookingId}` (falls back to the list without a bookingId — REQ-017), «دانلود فاکتور» →
|
||||
`/bookings/{bookingId}/invoice` (hidden without a bookingId).
|
||||
- **Invoice** `/bookings/[id]/invoice` — `invoiceNumber` + Shamsi issue date, `PriceBreakdown` rows where
|
||||
the **service line is the exact integer remainder** (gross − commission − VAT, via `parseIrr` BigInt) so
|
||||
the lines reconcile by construction, the VAT line labelled **«مالیات بر ارزش افزوده (بر کارمزد
|
||||
بالینیار)»** (product rule: the nurse is never implied to be taxed), read-only **مودیان** state chip
|
||||
(`moadian_*`), and `pdfUrl` download **or** a print receipt (`window.print()` + a print-scoped
|
||||
visibility rule isolating the invoice card). 404 renders «فاکتور هنوز صادر نشده است» (REQ-018).
|
||||
|
||||
### Shared composites (each with a co-located test)
|
||||
|
||||
- **`PriceBreakdown`** — typed rows + total, all IRR digit-strings through `formatIrrToToman`; dev-guard
|
||||
`console.error`s if rows ≠ total (test proves rows render, total = Σ rows, and the guard fires on mismatch).
|
||||
- **`EscrowNotice`** — wraps `AppAlert` (info severity, `--bal-primary` text on `--bal-primary-soft`, lock icon). Its test mocks next-intl to
|
||||
read **the real `fa.json`**, pinning the mandated copy verbatim: a rewording fails the suite.
|
||||
- **`PaymentStatusBadge`** — full `PaymentTransactionStatus` → `StatusChip` kind map (`Record` typed, so an
|
||||
enum change breaks the build); labels from `payment.pstatus_*`.
|
||||
|
||||
### Extensions to prior phases (in place, per operating rules)
|
||||
|
||||
- `services/bookingRequests`: client-augmented **`bookingId: number | null`** on `BookingRequestDto`
|
||||
(REQ-017 twin of REQ-013's `variantPrice`; real client maps it to `null`), and the mock-only
|
||||
`mockMarkBookingRequestConverted(id, bookingId)` capture bridge.
|
||||
- `services/bookings/apis/mockApi.ts`: mock-only `mockInsertConvertedBooking(seed)` — inserts a confirmed
|
||||
single-session booking (ids 6001+/80001+, distinct from the 5001/5002 seeds).
|
||||
- C5 (`bookings/request/[id]`): the `converted` terminal card now deep-links the booking when
|
||||
`bookingId` is present (list fallback otherwise).
|
||||
- `constants/routes.ts`: `CHECKOUT_GATEWAY/RETURN/CONFIRMATION` + `bookingInvoicePath()`.
|
||||
- i18n: new **`payment`** namespace — 53 keys, both locales, inserted textually (no reformat of existing lines).
|
||||
|
||||
## 2. Contract deltas the implementation honors (vs the phase file's illustrative design)
|
||||
|
||||
The phase file sketched `getCheckoutSummary`/`verifyPayment`/`getTransaction` endpoints and an
|
||||
`initiated…cancelled` status enum. The **published contract wins**:
|
||||
|
||||
1. **Status enum is `pending|succeeded|failed`** (b10 `payment_transactions.status`). Client unions match.
|
||||
2. **There is no client verify/transaction endpoint.** The server re-verifies inside the webhook handler;
|
||||
"verify on return" is therefore modelled as `confirmGatewayReturn` (real impl = an outcome *read* — the
|
||||
PSP already hit the webhook before redirecting) + the outcome poll. Filed as REQ-017.
|
||||
3. **There is no checkout-summary endpoint** and the b8 request DTO is money-free — the C6 breakdown
|
||||
cannot be served today. Filed as REQ-016; mocked behind the seam; the real client targets the proposed slug.
|
||||
4. **The invoice is flat totals** (`grossIrr`/`platformCommissionIrr`/`vatRate`/`vatIrr`…, no line-items
|
||||
array) and only exists after the **admin-only** issue action. Filed as REQ-018; the UI has a not-issued state.
|
||||
5. **Idempotency is a header** (`Idempotency-Key`), not a body field — the contract's exact casing.
|
||||
|
||||
## 3. Mocks in this phase (recorded in mocks-registry.md)
|
||||
|
||||
- **`paymentMockApi` (mock-primary)** — the missing **conversion trigger between the f7 and f8 mock
|
||||
stores** (their `converted`/seeded-booking states were previously unconnected): capture flips the f7
|
||||
request to `converted` + stamps `bookingId`, inserts a **confirmed** f8 booking, and auto-issues the
|
||||
b11-shaped invoice. Money split uses integer parts-per-10000 BigInt math (12% fee, 10% VAT,
|
||||
`vat = commission_net × rate` per b11) so `service + commission + vat = total` **exactly** and
|
||||
`gross = balinyaarCommission + payout` holds. Idempotency mirrors b10: same-key retry reuses the
|
||||
attempt, post-capture initiate → `409`, replayed returns converge.
|
||||
- **The mock-gateway page** — test harness only (see registry row for the deletion story).
|
||||
- **Why mock-primary:** upstream ids are mock-primary (f7), REQ-016/017 are unserved, and nothing fires
|
||||
the PSP webhook in dev (b10's "webhook simulator" is a manual server-side POST — after a real initiate,
|
||||
nothing would ever confirm). Swap = deliver REQ-016/017/018 + real upstreams, then `USE_PAYMENT_MOCK=false`.
|
||||
|
||||
## 4. What is now testable, exactly (mock path — `npm run dev`)
|
||||
|
||||
Prereq: an `accepted_awaiting_payment` request — either seed-driven (open `/fa/nurse/requests`, accept a
|
||||
seeded pending request) or full-flow (C4 create → nurse accept). Same browser tab throughout (module-singleton mocks).
|
||||
|
||||
1. **C6:** from C5's «ادامه پرداخت» (or `/fa/bookings/checkout?request_id={id}`) — badge, mini-summary,
|
||||
30-min countdown, breakdown that sums to the rial (2,800,000×1 IRR gross → service 2,436,000 +
|
||||
commission 336,000 − VAT split), escrow notice in info tone. `/en` flips `dir`, translates, still Toman.
|
||||
2. **Pay:** «ادامه پرداخت ←» → spinner → the آزمایشی gateway → «پرداخت موفق» → return surface briefly shows
|
||||
«در حال تایید پرداخت…» → confirmation screen.
|
||||
3. **Booking flips:** «مشاهده رزرو» lands on `/bookings/{id}` showing **confirmed** (React Query Devtools:
|
||||
only request-detail/lists, bookings lists/detail, and this request's payment keys invalidated). C5 now
|
||||
shows the converted card deep-linking the same booking; the bookings list has the new row.
|
||||
4. **Invoice:** «دانلود فاکتور» → number `INV-…`, Shamsi date, service/commission/VAT-on-commission/total
|
||||
reconciling to C6, مودیان «در انتظار ثبت», print button (mock serves no `pdfUrl` so the print path runs).
|
||||
5. **Idempotency/retry:** double-tap pay (one attempt, same key — mock returns the same transaction);
|
||||
re-initiate after success → `409` → converges to confirmation, no error toast. Gateway «شبیهسازی
|
||||
پرداخت ناموفق» → failed card → «تلاش دوباره» → fresh C6 mount issues a **new** key (observable in the
|
||||
mock's `gatewayReferenceCode` suffix).
|
||||
6. **Window expiry:** wait out the 30-min window (or re-enter later) → C6/return show «مهلت پرداخت به
|
||||
پایان رسید» and C5 shows its terminal card; initiate after expiry → `409` handled as state, not error.
|
||||
7. **Invoice not-issued state:** `/fa/bookings/5001/invoice` (a seeded booking that never went through
|
||||
checkout) → «فاکتور هنوز صادر نشده است».
|
||||
|
||||
## 5. Follow-ups for the next phases
|
||||
|
||||
- **f10 (refunds/cancellation):** reuse `PriceBreakdown` (fee disclosure), `EscrowNotice` (identical trust
|
||||
copy), `PaymentStatusBadge`; `RefundStatusDto`/`refunds/{id}/status` is live in b11 and unconsumed;
|
||||
`refundableAmountIrr`/`cancellationRefundPercentage` already ride on `BookingDetailDto`.
|
||||
- **f11 (BNPL):** flip `BNPL_ENABLED` and wire the C6 secondary to D1. The b12 contract
|
||||
(`checkout_bnpl/eligibility|initiate|{id}` + `Idempotency-Key` header) parallels this domain's shapes;
|
||||
`InvoiceDto.bnplCommissionIrr` is already typed. The gateway-harness pattern extends to the BNPL redirect.
|
||||
- **Backend:** REQ-016 (checkout summary), REQ-017 (outcome/bookingId — until then the real poll can't
|
||||
distinguish *declined* from *slow*, and the confirmation can't deep-link), REQ-018 (invoice reachable
|
||||
post-capture). Also note: the PSP's return-URL config must deep-link `/{locale}/bookings/checkout/return`.
|
||||
|
||||
## 6. Post-review hardening (multi-agent adversarial review before close)
|
||||
|
||||
A 26-agent review/verify pass over the diff confirmed and fixed, pre-merge:
|
||||
- **Stale-outcome guard (major):** the return surface now trusts the outcome cache only after *this*
|
||||
mount's return report settles — a previous attempt's cached `failed` outcome can no longer flash a
|
||||
false «پرداخت ناموفق» (with a live retry) while the current attempt's capture is in flight.
|
||||
- **No dead-end retries:** malformed `request_id`/booking-id links render a navigation card instead of a
|
||||
`refetch()` that bypasses `enabled` and would request `checkout_summary/undefined`.
|
||||
- **Unpriced request fails loudly:** the mock throws `409 unpriced_request` instead of silently serving a
|
||||
reconciling 0-rial checkout when `variantPrice` is null (REQ-013 edge).
|
||||
- **i18n/UX:** inline initiate errors always use the localized copy (never raw `ApiError.message`); en
|
||||
`cta_pay` arrow points → (fa keeps ←); fa `error_body` matches the app's «بارگذاری … ممکن نشد» pattern;
|
||||
the C6 service-cost row carries the quantity (`row_service_cost_with_count`); the invoice issuer line
|
||||
uses the product spelling «بالینیار» (note: fa `common.brand` reads «بلینیار» — a pre-existing
|
||||
wordmark/product-spelling divergence worth a product decision).
|
||||
- **Dark scheme:** EscrowNotice text/border use `--bal-primary` (the info token is an alert *background*
|
||||
and is illegible as dark-mode text); the print button temporarily flips `data-mui-color-scheme` to
|
||||
light around `window.print()` (restored on `afterprint`) so a dark-mode user prints paper colors.
|
||||
- **Contract hygiene:** the domain barrel is hooks-only again (`isTerminalPaymentOutcome` moved to
|
||||
`types.ts`, mirroring `isTerminalBookingRequestStatus`); the gateway harness scopes `dir="ltr"` to the
|
||||
reference code, not the Persian label; the invoice VAT percent formats fractional rates
|
||||
(`maximumFractionDigits: 2`).
|
||||
|
||||
## 7. Gate
|
||||
|
||||
`npm run check` green · `npm run test:ci` green (46 suites, 204 tests — +9: PriceBreakdown 4, EscrowNotice 2,
|
||||
PaymentStatusBadge 3) · `npm run build` green. `en.json`/`fa.json` in sync (53-key `payment` namespace).
|
||||
`client/CLAUDE.md` Project Structure updated (checkout subtree, invoice route, `services/payment`, three
|
||||
components, `payment` namespace entry). Gotcha for future phases: **BigInt literals (`0n`) don't compile**
|
||||
(tsconfig target ES2017) — use the `BigInt(...)` constructor like `utils/money.ts`.
|
||||
@@ -71,3 +71,5 @@ the frontend can build before the backend phase merges, and swap to the real HTT
|
||||
| `VerificationApi` | `client/src/services/verification/apis/mockApi.ts` | The whole nurse trust journey (b6). Seeds the six required steps on `start` (idempotent); `runIdentityKyc` passes any well-formed 10-digit id **except** `0000000000` (→ `failed`/`kyc_no_match`, matches backend `MockIdentityKycProvider`); `runShahkarMatch` requires identity passed, fails **shared-SIM** when the bound national id is `1111111111` (→ `failed`/`shared_sim`); `runBankVerification` passes (assumes a primary bank account); `uploadStepDocument` simulates signed-URL PUT progress then moves the step to `in_review` (metadata only); `submitCredentialDetails` validates the INO number. Re-aggregates like the server (`approved` only when every step passes). **Dev-only** `__mockApproveAll()`/`__mockRejectStep(code,reason)` stand in for the deferred (f15) admin review queue so a human can watch `is_verified`/the trust badge/the publish gate flip — reachable from B3/B6 only while the flag is true | `USE_VERIFICATION_MOCK` (`services/verification/constants.ts`, default `true`) | b6 `nurse_verification/*` + `nurses/{id}/trust_badge` are live; set flag `false` — `verificationClientApi` is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). **Caveat:** the real `submitCredentialDetails` no-ops pending REQ-011 (no nurse-facing endpoint for the structured INO/specialties fields yet) — the document uploads it accompanies are contract-backed. No hook/component change | 🟡 |
|
||||
| `BookingsApi` | `client/src/services/bookings/apis/mockApi.ts` | The post-payment engagement (b9). Seeds **2 confirmed bookings** (one 3-session multi-day, one single-visit) + `booking_care_instructions` + a per-session **EVV state machine** — `checkInVisit` flips the session→`in_progress`/`checked_in` (booking→`in_progress`) and computes the **advisory** `checkInAddressMatch` (haversine vs the seeded address ± `MOCK_EVV_TOLERANCE_METERS`, `null` when GPS was absent); `checkOutVisit` requires an open check-in (**`400 no_open_check_in`** otherwise), completes the session (stamps `payoutEligibleAt`), and completes the booking + opens the dispute window once **all** sessions settle. `getCareInstructions` **404s any viewer but the assigned nurse** (the two-stage-disclosure boundary; the UI `enabled` gate means the customer never even calls it). Money stays IRR digit-strings with `gross = commission + payout` and `Σ visitPayout = payout` | `USE_BOOKINGS_MOCK` (`services/bookings/constants.ts`, default `true`) | b9 `bookings/*` + `booking_sessions/*` are live, but a booking only exists after `bookings/convert` runs on a **paid** request — both upstreams (`bookingRequests` mock, card capture b10) aren't real client-side yet. Once conversion is live, set flag `false` — `bookingsClientApi` maps the routes 1:1 (+ `bookingsServerApi` for the RSC prefetch). No hook/component change | 🟡 |
|
||||
| `ILocationProvider` | `client/src/services/bookings/evv/locationProvider.ts` | **EVV GPS capture** — the only client seam f8 introduces. `getCurrentPosition()` never rejects (denied/unavailable → `null`, so a GPS problem is **advisory, never a block**). The **real** provider wraps `navigator.geolocation.getCurrentPosition`; the **mock** returns canned coordinates per mode so the in-range / advisory-out-of-range / denied paths are all demoable without a device (the mock `BookingsApi` computes the match against the same seeded reference point) | `NEXT_PUBLIC_EVV_MOCK_GPS` = `in_range` \| `out_of_range` \| `denied` \| `off` (default `in_range` while `USE_BOOKINGS_MOCK`, else `off`) | Set `NEXT_PUBLIC_EVV_MOCK_GPS=off` (or flip `USE_BOOKINGS_MOCK`) → the real `navigator.geolocation` provider is selected. Real **address-match math** stays server-side (backend geocoding seam), not here — this seam only *captures* the position | 🟡 |
|
||||
| `PaymentApi` | `client/src/services/payment/apis/mockApi.ts` | **The f9 checkout money path** — plays the PSP + webhook roles the client can't reach: `getCheckoutSummary` serves the unserved C6 breakdown (REQ-016; commission-net/VAT/service split via **integer parts-per-10000 BigInt math**, 12% fee / 10% VAT, reconciles to the rial); `initiatePayment` enforces b10 idempotency (same `Idempotency-Key` → same attempt; repeat after capture / lapsed window → **`409`**) and returns a `redirectUrl` into the local mock-gateway harness; `confirmGatewayReturn` on success is the **webhook-confirm stand-in and the missing f7↔f8 bridge** — flips the request `converted` (+ client-augmented `bookingId`, via `mockMarkBookingRequestConverted` in the f7 mock), inserts a **confirmed** booking into the f8 store (`mockInsertConvertedBooking`), and auto-issues the b11-shaped invoice (`moadianStatus: pending`, `pdfUrl: null` so the print path exercises); replayed returns converge idempotently; `getInvoice` 404s until issued | `USE_PAYMENT_MOCK` (`services/payment/constants.ts`, default `true`) | b10 initiate + b11 invoice are live and `paymentClientApi` maps them 1:1 (`Idempotency-Key` header, `GET invoices/{bookingId}`); deliver **REQ-016** (checkout summary — the real client already targets the proposed `booking_requests/checkout_summary/{id}` slug) + **REQ-017** (transaction status / `bookingId`; until then the real outcome poll maps `booking_requests/get` statuses and can't distinguish declined from slow) + **REQ-018** (invoice reachable post-capture), make the upstream `bookingRequests` flow real, then set flag `false`. No hook/component change | 🟡 |
|
||||
| Mock-gateway page (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the PSP's hosted payment page so the initiate → redirect → return round-trip is exercisable without a gateway: the mock `redirectUrl` points here, and its success/failure buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «درگاه پرداخت آزمایشی», dashed border | _none — only reachable via the mock's `redirectUrl`_ | On the real path b10's `redirectUrl` is the PSP's **absolute** URL (the checkout does a full `window.location.assign` for `http(s)` URLs), so this page is simply never linked; delete it when `USE_PAYMENT_MOCK` retires. The PSP's return deep-link into `/bookings/checkout/return` is backend/PSP config | 🟡 |
|
||||
|
||||
Reference in New Issue
Block a user