34 KiB
Mock & integration registry
The master list of every external dependency that is mocked behind a DI seam in this build, and the exact steps to make each one real. Backend lane owns this file; every phase that introduces or touches a seam updates its row. This is the checklist the team works through to go from "MVP with mocks" to "production with real providers".
Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢 real integration live.
| Seam (interface) | Introduced in | What it fakes | Config keys | Make it real → | Status |
|---|---|---|---|---|---|
ISmsSender |
backend-phase-2 | OTP/SMS delivery — LoggingSmsSender (Baya.Infrastructure.CrossCutting/Seams/) logs the OTP code (phone shown as last-4 only) and returns success; registered singleton in AddCrossCuttingSeams |
none today; real client will need Seams:Sms:ApiKey + Seams:Sms:SenderLine (+ gateway base URL) |
1) pick a gateway (Kavenegar/Ghasedak/SMS.ir), add its client package to Directory.Packages.props; 2) implement ISmsSender.SendOtpAsync/SendAsync against it (template/pattern-based OTP send); 3) bind the new Seams:Sms options; 4) swap the registration in AddCrossCuttingSeams (config-selected) — handlers unchanged; 5) keep the per-phone resend window + otp rate-limit policy exactly as-is; test with a real SIM |
🟡 |
IObjectStorage |
backend-phase-0/6 | File storage — local-disk store under a scratch root (LocalDiskObjectStorage, Baya.Infrastructure.CrossCutting/Seams/) |
Seams:ObjectStorage:RootPath (default: temp dir) |
Point at MinIO/S3/ArvanCloud; presigned upload/download; bucket + creds | 🟡 |
ICacheService |
backend-phase-0 | Caching — in-memory IMemoryCache (MemoryCacheService, Baya.Infrastructure.CrossCutting/Seams/) |
none | Swap to Redis (StackExchange.Redis); keep key/TTL scheme |
🟡 |
IDistributedLock |
backend-phase-10 | Money-path locks — no-op/in-proc | tbd | Redis lock (RedLock); DB constraint remains the backstop | 🔴 |
INurseSearch |
backend-phase-7 | Search — SQL over nurse_search_index |
tbd | Elasticsearch index + feeder; reimplement the interface | 🔴 |
IPaymentProvider |
backend-phase-10 | Card PSP/IPG — deterministic success | tbd | ZarinPal/Sadad/Vandar/Jibit + Shaparak; merchant/terminal/تسهیم | 🔴 |
ISettlementSplitProvider |
backend-phase-10 | تسهیم split — accepts any balanced legs | tbd | Provider split-by-ratio to registered Shebas | 🔴 |
IWebhookVerifier |
backend-phase-10 | Callback auth — always valid | tbd | Per-provider HMAC/signature + server-side re-verify | 🔴 |
IBnplProvider |
backend-phase-12 | BNPL — MockBnplProvider drives the full state machine (eligible→settled→reverted), settle returns order − commission% |
Seams:Bnpl:{CommissionRate,SettlementInstant,CreditCeilingIrr,NotEligibleMobile,ForceFailure,ReverseProviderCommission} |
SnappPay/Digipay OAuth + verb set; encrypted creds in payment_gateways.config_json |
🟡 |
IBnplProviderResolver |
backend-phase-12 | Per-provider_code selection — maps every known code to the one mock |
none | One concrete adapter per code; resolver returns the right one | 🟡 |
ICurrencyNormalizer |
backend-phase-12 | Toman↔IRR — ×10 at the boundary | Seams:Currency:TomanToIrrMultiplier (default 10) |
Config-driven per provider boundary | 🟡 |
IBankTransferProvider |
backend-phase-13 | PAYA/SATNA payout rail — MockBankTransferProvider (Baya.Infrastructure.CrossCutting/Seams/), no external call, no money moves: SubmitPayoutBatchAsync(batchId, instructions, idempotencyKey) returns a deterministic externalBatchRef + a per-instruction transfer_reference and settles every row Paid (collapsing the real submitted → paid reconciliation); it honours the PAYA/SATNA method the handler chose by the payout_satna_threshold_irr config and echoes it. A config switch forces deterministic failures so partially_failed/retry are testable: ForceFailure fails the whole batch, FailIban fails one destination. GetPayoutStatusAsync echoes Paid. Registered singleton in AddCrossCuttingSeams |
Seams:BankTransfer:ForceFailure (default false), Seams:BankTransfer:FailIban (default empty) |
1) pick a transferor (Jibit/Vandar/Sadad payout API), add its client package to Directory.Packages.props; 2) add Seams:BankTransfer:{ApiKey,BaseUrl,SourceSettlementAccount}; 3) implement SubmitPayoutBatchAsync to register the batch against the registered source settlement account and route each transfer PAYA (batch, low-value) vs SATNA (real-time, above the threshold) to each nurse's verified Sheba (the b3 matched_national_id gate), honouring batch caps/minimums; 4) implement the async reconciliation callback that flips a payout submitted → paid/failed (the mock collapses this — the real rail is async); 5) swap the registration (config-selected) — the payout status machine + nurse_payout_booking_links UNIQUE remain the irreversible-transfer backstop; 6) test PAYA/SATNA selection, whole-batch + single-row failure → retry |
🟡 |
IHolidayCalendar |
backend-phase-1 | Bank holidays — reads the seeded ops.IranianHolidays table; lookups cached (HolidayCalendarService, Persistence/Services/Holidays/); Iranian banking weekend = Friday |
none | Add a sync job/feed that maintains the (partly lunar-Hijri) calendar table; the read interface stays | 🟡 |
IAnalyticsSink |
backend-phase-1 | Behavioural events — inserts an ops.SystemEvents row, fire-and-forget (AnalyticsSink, Persistence/Services/Analytics/) |
none | Pipe to a warehouse/stream (e.g. Kafka→ClickHouse); keep fire-and-forget semantics | 🟡 |
IJobScheduler (retention + booking expiry) |
backend-phase-1 | Scheduling — in-process interval BackgroundServices: PurgeOldReadNotifications daily (NotificationRetentionHostedService, Persistence/Services/Notifications/) and b8 BookingRequestExpiryHostedService (Persistence/Services/Booking/) running the idempotent booking-request expiry sweep every minute |
none | Swap to Hangfire/Quartz; register both jobs there; keep the purge predicate (is_read=1 AND age>90d) and the booking-expiry command |
🟡 |
IShahkarVerifier |
backend-phase-6 | شاهکار phone↔national-id binding — MockShahkarVerifier (Baya.Infrastructure.CrossCutting/Seams/) returns a deterministic result + fake vendor ref + external_response_json: matches every pair except the configured shared-SIM phone (→ the explicit shared-SIM failure state, which the handler turns into a shared_sim support alert) and the mismatch national id (→ plain mismatch); registered singleton in AddCrossCuttingSeams. No real Shahkar call |
Seams:Shahkar:SharedSimPhone (default 09120000000), Seams:Shahkar:MismatchNationalId (default 1111111111) |
1) pick a Finnotech / KYC Shahkar-bridge vendor, add its client package to Directory.Packages.props; 2) add Seams:Shahkar:{ApiKey,BaseUrl} options; 3) implement MatchAsync(phone, nationalId) against the real استعلام شاهکار, mapping to ShahkarMatchResult and persisting the raw response into the step's external_response_json; 4) keep shared-SIM as the explicit handled failure (IsSharedSim=true); 5) swap the registration in AddCrossCuttingSeams (config-selected) — handlers unchanged; 6) test match / shared-SIM / mismatch + that a phone change re-runs it (shahkar_verified_at resets upstream on phone change) |
🟡 |
IIdentityKycProvider |
backend-phase-6 | Identity KYC (national-id validity + name match + liveness) — MockIdentityKycProvider (.../Seams/) passes any well-formed 10-digit national id except the configured fail id, returning a matched name + fake vendor ref + external_response_json; on pass the handler populates users.national_id + national_id_verified_at. No real OCR/liveness; registered singleton |
Seams:IdentityKyc:FailNationalId (default 0000000000), Seams:IdentityKyc:MatchedName (default Verified Nurse) |
1) pick an Iranian e-KYC vendor (Finnotech / U-ID / Jibbit / Farashensa / Verify / Kavoshak), add its client package to Directory.Packages.props; 2) add Seams:IdentityKyc:{ApiKey,BaseUrl} options; 3) implement VerifyAsync(nationalId, livenessPayload) → national-id validity + name match + photo/video liveness against ثبت احوال, mapping to IdentityKycResult and persisting external_response_json; 4) swap the registration (config-selected) — handlers unchanged; 5) test pass/fail by national id + that national_id is populated only on pass |
🟡 |
ICredentialVerifier |
backend-phase-6 | MoH پروانه صلاحیت حرفهای / INO / عدم سوء پیشینه verification — MockCredentialVerifier (.../Seams/) is the manual-admin default: every call returns RequiresManualReview with verification_method=manual (an admin verifies the uploaded document against the official portal in AdminReviewStep). No portal call; registered singleton. There is no public B2B API for MoH/INO, so this stays manual until one appears |
none | 1) when an MoH/INO portal or API becomes available, implement VerifyAsync(credentialType, credentialNumber) to return Verified/Failed with `verification_method=portal |
api(+external_response_json); 2) swap the registration (config-selected) for those credential types — the manual path stays the fallback; 3) the structured nurse_credentials` registry already stores number/authority/expiry so cross-check + renewal survive the swap. MoH/INO have no public B2B API today |
IBankAccountOwnershipVerifier |
backend-phase-3 | استعلام شبا IBAN-owner ↔ national-id inquiry — MockBankAccountOwnershipVerifier (Baya.Infrastructure.CrossCutting/Seams/) returns a deterministic fake: every IBAN matches (matched_national_id=true, echoes a holder name + MOCK-SHEBA-{sha} vendor ref) except the configured mismatch IBAN which returns false; registered singleton in AddCrossCuttingSeams. No real bank/KYC call, no money moves |
Seams:BankOwnership:MismatchIban (default IR000000000000000000000000), Seams:BankOwnership:MatchedHolderName, Seams:BankOwnership:MismatchHolderName |
1) pick a Finnotech / banking-bridge استعلام شبا provider, add its client package to Directory.Packages.props; 2) add Seams:BankOwnership:{ApiKey,BaseUrl} options; 3) implement VerifyOwnershipAsync(iban, nurseNationalId) against the real Sheba-owner inquiry, mapping to OwnershipInquiryResult; 4) persist the real ownership_vendor_ref (+ raw response if a column is added); 5) swap the registration in AddCrossCuttingSeams (config-selected) — handlers unchanged; 6) test match/mismatch + that the b13 first-payout gate honours matched_national_id=true |
🟡 |
IGeocoder |
backend-phase-4 | Address→lat/lng — MockGeocoder (Baya.Infrastructure.CrossCutting/Seams/) returns deterministic decimal coordinates jittered (FNV-1a, ~±5 km) around the known city centroid (unknown city → Iran centroid) plus formatted_address + confidence; no network call. A global switch or a per-address marker forces the null-coordinate ("no map pin") path; registered singleton in AddCrossCuttingSeams |
Seams:Geocoding:ReturnNullCoordinates (default false), Seams:Geocoding:LowConfidenceMarker (default NO_GEO), Seams:Geocoding:ResolvedConfidence (default 0.9) |
1) pick Neshan (or Google) geocoding, add its client package to Directory.Packages.props; 2) add Seams:Geocoding:{ApiKey,BaseUrl} options; 3) implement IGeocoder.GeocodeAsync(addressText, cityName, districtName?) against it, mapping to (lat, lng, formatted_address, confidence) with decimal coords; 4) add rate-limit/retry; 5) swap the registration in AddCrossCuttingSeams (config-selected) — handlers unchanged; 6) test a known Tehran address resolves within expected bounds |
🟡 |
IMoadianClient |
backend-phase-11 | سامانه مودیان e-invoice — leaves ref pending | tbd | Real مودیان submission → 22-digit ref | 🔴 |
IReviewModerationService |
backend-phase-14 | AI moderation — keyword/pass-through | tbd | Real classifier/LLM endpoint | 🔴 |
IFieldEncryptor |
backend-phase-0 | PII encryption — AES-256-CBC + HMAC hash from a local symmetric key (SymmetricFieldEncryptor, Baya.Infrastructure.CrossCutting/Seams/) |
Seams:FieldEncryption:Key, Seams:FieldEncryption:HashKey |
KMS / column encryption / Key Vault / HSM | 🟡 |
INotificationDispatcher |
backend-phase-0/1 | Notification channels — in-app write is now real (InAppNotificationDispatcher, Persistence/Services/Notifications/, writes an ops.Notifications row); b0 log stub removed. SMS/push channels still deferred (no-op) behind the same seam |
none | Add SMS (ISmsSender) / push (FCM) channels; polling → Redis pub/sub or SignalR later |
🟡 |
ILicenseVerificationService |
backend-phase-15 | eNamad / MoH establishment-permit — manual approve | tbd | Real registry/API | 🔴 |
IPaymentCaptureSimulator |
backend-phase-9 | The temporary conversion trigger standing in for b10's real card capture. MockPaymentCaptureSimulator (Baya.Infrastructure.CrossCutting/Seams/) returns a deterministic succeeded capture (a fake gateway_reference + a configurable psp_fee_amount) so ConvertRequestToBookingCommand is exercisable now; a config switch forces a failed capture (→ no booking is created). This is the trigger, not a parallel money path — registered singleton in AddCrossCuttingSeams |
Seams:PaymentCapture:ForceFailure (default false), Seams:PaymentCapture:PspFeeAmount (default unset) |
In b10: 1) build the real card capture (payment_transactions, PSP/IPG client, webhook verify); 2) on a real payment_transactions.succeeded, call ConvertRequestToBooking directly (the same conversion command that computes the three-amount split + generates sessions) instead of this seam; 3) remove the IPaymentCaptureSimulator registration + MockPaymentCaptureSimulator; the conversion/idempotency logic is unchanged |
🟡 |
INurseSearch |
backend-phase-7 | The search-service seam (read side). The MVP impl SqlNurseSearch (Persistence/Services/Search/) is REAL, not a mock — it reads the maintained nurse_search_index WHERE is_searchable=1, applies the category/city/district (NULL=whole-city)/gender/price filters + rating sort + pagination, projected & AsNoTracking. Registered by AddPersistenceServices, config-selected. Only the DEFERRED Elasticsearch backend is unbuilt |
Search:Backend (default sql; any other value throws until Elastic ships) |
1) add an Elasticsearch client package (Elastic.Clients.Elasticsearch) to Directory.Packages.props; 2) define the index mapping (the NurseSearchResultDto fields + is_searchable); 3) implement ElasticNurseSearch : INurseSearch (same filters/sort/paging) reading the ES index; 4) build the feeder that consumes the ISearchIndexMaintainer change events via an outbox/CDC stream into ES (see the next row); 5) point Search:Backend=elastic in config — callers unchanged; 6) keep the SQL index as the projection/fallback + the reconciliation source (RebuildAsync); 7) test filter/sort/paging parity vs SqlNurseSearch |
🟢 SQL real; Elastic 🟡 |
IPaymentProvider |
backend-phase-10 | Card PSP acquirer — MockPaymentProvider (Baya.Infrastructure.CrossCutting/Seams/), no external call: InitPaymentAsync → a deterministic gatewayReferenceCode (mock-ref-{requestId}-{key}) + a fake redirect URL; VerifyAsync → instant Succeeded echoing the expected amount (the server-side re-check); RefundAsync(ref, amount, idempotencyKey, ct) → always Succeeded, echoes a deterministic refund ref (b11 refunds carry the booking+refund idempotency key so a retry never double-refunds). Registered singleton in AddCrossCuttingSeams |
none today; real client needs merchant id + terminal/IBAN registration + sandbox flag from payment_gateways.config_json (encrypted), not appsettings |
1) pick ZarinPal/Sadad/Vandar/Jibit as an acquirer-with-تسهیم, add its client package to Directory.Packages.props; 2) implement InitPaymentAsync (open the IPG session, return the Shaparak-routed redirect + reference), VerifyAsync (the mandatory server-side verify re-check of amount + reference — never trust the callback alone), RefundAsync; 3) read merchant id/terminal from the encrypted payment_gateways.config_json; 4) a config-driven IProviderRegistry/factory selects the concrete provider per gateway so a cut-off provider swaps without code change; 5) persist the full gateway response into gateway_response_json; 6) swap the registration (config-selected) — handlers unchanged |
🟡 |
ISettlementSplitProvider |
backend-phase-10 | تسهیم settlement-sharing — MockSettlementSplitProvider (.../Seams/) records the split intent and returns Settled for any legs whose sum is positive; the platform never moves money. Registered singleton in AddCrossCuttingSeams |
none today; real client needs each beneficiary's registered SHEBA + split-by-ratio config | 1) pick the acquirer's تسهیم API, implement RegisterSplitAsync(bookingId, legs) to register the split-by-ratio to each beneficiary's registered IBAN (nurse payout + platform commission), honouring the ~100,000 IRR min-amount caveat; 2) resolve each nurse's SHEBA from nurse_bank_accounts (the b3 matched_national_id gate) and the platform SHEBA from config; 3) GetSplitStatusAsync polls the provider; the provider credits IBANs directly — the ledger only mirrors it; 4) swap the registration (config-selected) |
🟡 |
IWebhookVerifier |
backend-phase-10 | PSP callback signature verify — MockWebhookVerifier (.../Seams/) treats the signature as valid unless the body carries Seams:Payments:InvalidSignatureMarker, and extracts external_event_id/event_type/gateway_reference_code from a small JSON body (so tests can replay duplicates + exercise the invalid-signature path). Registered singleton in AddCrossCuttingSeams |
Seams:Payments:InvalidSignatureMarker (default INVALID_SIGNATURE) |
1) implement the per-provider HMAC/signature scheme (verify the raw body against the provider's signing key from the gateway config); 2) where a provider offers no signature, fall back to the mandatory server-side verify re-check (amount + reference) via IPaymentProvider.VerifyAsync; 3) parse the real provider event shape into WebhookVerification; 4) swap the registration (config-selected) — the HandlePaymentWebhook upsert-first/no-op-on-duplicate ordering is unchanged |
🟡 |
IDistributedLock |
backend-phase-10 | Money-path mutex — InProcessDistributedLock (.../Seams/): a per-key SemaphoreSlim so the capture path runs the same acquire/release shape it will with real Redis, within one process only. Not a cross-instance correctness guarantee — the DB uniques/state-machine are the authoritative backstop. Registered singleton in AddCrossCuttingSeams |
none today; real client needs a Redis connection string | 1) add StackExchange.Redis to Directory.Packages.props; 2) implement AcquireAsync(key) with a lease/expiry (RedLock-style SET NX PX + a token-checked release), key convention booking:{id}:payment; 3) bind Seams:Payments:Redis (or reuse the ICacheService Redis swap); 4) swap the registration (config-selected) — handlers unchanged, and correctness still rests on the DB uniques if Redis is down/expired |
🟡 |
ISearchIndexMaintainer (the "ISearchIndexWriter" event shape) |
backend-phase-7 | The index-maintenance seam (write side). The inline SQL path is REAL — SearchIndexMaintainer (Persistence/Services/Search/) re-derives nurse_search_index from source and stages it inside the owning source write's unit of work (single CommitAsync), invoked from the b3/b4/b5/b6 handlers (ReindexVariantAsync/ReindexNurseAsync/FanOutServiceAreaAsync/RemoveServiceAreaRowsAsync/RebuildAsync). Only the outbox/queue routing for an async Elastic feeder is deferred — the seam is shaped so the same change events can later be emitted to an outbox instead of an inline upsert |
none | 1) introduce an outbox table + a SaveChanges interceptor that captures each maintainer change as an event row in the same transaction; 2) a background feeder (Hangfire/Quartz or a hosted service) reads the outbox and applies to ElasticNurseSearch; 3) keep the inline SQL upsert as the projection/fallback so RebuildAsync stays the reconciliation path; 4) test that an outbox replay converges to the same rows as the inline path |
🟡 outbox deferred (inline real) |
| IMoadianClient | backend-phase-11 | سامانه مودیان e-invoicing — MockMoadianClient (Baya.Infrastructure.CrossCutting/Seams/), no external call: SubmitAsync leaves a new invoice moadian_status = pending with moadian_reference_number = null; a config switch forces a deterministic registered result with a fake 22-digit reference so the reconciliation/registered path is testable. Registered singleton in AddCrossCuttingSeams | Seams:Moadian:ForceRegistered (default false) | 1) enroll the platform in سامانه مودیان (memory/economic code + signing certificate); 2) implement SubmitAsync to POST the معاملات/invoice (صورتحساب) to the مودیان API, sign the payload, map the 22-digit reference_number; 3) walk the async pending → submitted → registered/failed states via a reconciliation callback/poll (cron deferred/manual today — a job flips moadian_status + fills the ref); 4) swap the registration (config-selected) — the IssueInvoice handler is unchanged | 🟡 |
| IBnplProvider | backend-phase-12 (superset of the b11 revert-only stub) | BNPL provider — MockBnplProvider (Baya.Infrastructure.CrossCutting/Seams/), no external call, drives the full SnappPay-superset verb set CheckEligibilityAsync/CreatePaymentTokenAsync/VerifyAsync/SettleAsync/GetStatusAsync/CancelAsync/RevertAsync/UpdateAsync and the eligible → token_issued → verified → settled → reverted/cancelled machine. Eligibility is eligible unless the mobile = NotEligibleMobile (→not_eligible) or the order exceeds CreditCeilingIrr (→ceiling_exceeded); token/redirect are deterministic; settle returns settledAmountIrr = order − round(order × CommissionRate) + the commission read from the response (never hardcoded) + a nullable settledAt (null when SettlementInstant=false, modelling non-instant settlement); revert echoes a deterministic external_revert_reference + nullable provider_commission_reversed_amount. Selected per provider_code by IBnplProviderResolver (MockBnplProviderResolver → the one mock for every known code); the b11 refund bnpl_revert path still injects IBnplProvider directly. Registered singleton in AddCrossCuttingSeams | Seams:Bnpl:CommissionRate (default 0.10), Seams:Bnpl:SettlementInstant (default true), Seams:Bnpl:CreditCeilingIrr (default 2000000000), Seams:Bnpl:NotEligibleMobile (default 09120000099), Seams:Bnpl:ForceFailure (default false), Seams:Bnpl:ReverseProviderCommission (default false) | 1) implement one concrete adapter per provider_code (SnappPay OAuth api/online/v1/oauth/token + offer/v1/eligible + payment/v1/token\|verify\|settle\|revert\|cancel\|update\|status, or Digipay UPG tickets/business?type=13 + purchases/verify + purchases/deliver?type=13 + refunds/reverse); 2) read credentials from the encrypted payment_gateways.config_json; 3) do Toman↔Rial via ICurrencyNormalizer at the adapter boundary; 4) read the per-contract commission from the settle response, never hardcode; 5) map the provider event shape into the callback so HandleBnplCallback dispatch is unchanged; 6) register per-code in IBnplProviderResolver (config-selected) — handlers unchanged. Warn: do NOT use the unrelated Canadian SnapPayInc/open-api-java-sdk. | 🟡 |
| ICurrencyNormalizer | backend-phase-12 | Toman↔IRR at the provider boundary — MockCurrencyNormalizer (Baya.Infrastructure.CrossCutting/Seams/): ToIrr(amount,"TOMAN") = amount × TomanToIrrMultiplier, IRR passes through; ToDisplayToman divides back. Conversion happens ONLY here, never internally. Registered singleton in AddCrossCuttingSeams | Seams:Currency:TomanToIrrMultiplier (default 10) | Read the multiplier (or a per-provider unit) from provider config; the interface stays — a currency redenomination is a config change | 🟡 |
| INursePayoutStatus | backend-phase-11 (interim) → backend-phase-13 (authoritative) | "Was the nurse already paid for this booking?" — b13 shipped the real NursePayoutLinkStatusService (Persistence/Services/Payments/): a booking is paid iff a nurse_payout_booking_links row ties it to a nurse_payouts row in status paid. This supersedes the interim NursePayoutStatusService (dispute-window derivation, now deleted); the refund_assume_nurse_paid config override still forces the paid answer for ops/testing. Not a mock of an external — a real ledger-backed derivation. Registered scoped in AddPersistenceServices. The refund pre-payout/clawback fork is unchanged | refund_assume_nurse_paid (platform_configs, default false) | Nothing further — this is the real implementation. (A future on-demand-withdrawal model would extend the "paid?" definition, not replace it.) | 🟢 |
Exact config keys and file paths get filled in by the phase that builds each seam. Keep the "Make it real →" column actionable enough that a developer can pick up any single row and ship it.
Frontend client-side mocks (not backend DI seams)
These are in-browser mocks behind a services/{domain} interface, selected by a config flag. They exist so
the frontend can build before the backend phase merges, and swap to the real HTTP client in one line.
| Seam (interface) | File | What it fakes | Config flag | Make it real → | Status |
|---|---|---|---|---|---|
PatientsApi |
client/src/services/patients/apis/mockApi.ts |
In-memory patient CRUD (list/get/create/update/soft-archive), seeded empty so onboarding + the empty state both demo; persists the client-augmented relation/conditions the wire PatientDto lacks (REQ-005) |
USE_PATIENTS_MOCK (services/patients/constants.ts, default true) |
Deliver REQ-005 (relation/conditions on PatientDto + create/update), then set flag false — patientsClientApi is already wired to the b3 patients/* routes |
🟡 |
ProfilesApi |
client/src/services/profiles/apis/mockApi.ts |
Customer + nurse profile get/upsert and avatar upload (echoes an object-URL). Keeps guarded read-only fields (isVerified=false, zero aggregates). Augments customer name/language (REQ-007) + nurse avatarUrl (REQ-006) the wire DTOs lack |
USE_PROFILES_MOCK (services/profiles/constants.ts, default true) |
b3 customer_profiles/* + nurse_profiles/* are live; deliver REQ-006 (avatar route/field) + REQ-007 (customer name/language) then set flag false — profilesClientApi is wired (its uploadAvatar throws 501 until REQ-006) |
🟡 |
NurseBankAccountsApi |
client/src/services/nurse/apis/mockApi.ts |
Bank-account list/add/set-primary/verify-ownership. Drives the استعلام شبا pending→verified/mismatch transition over 2 list reads (so the poll shows it), single-primary enforcement, masked-IBAN (last-4); the configured mismatch IBAN (IR000000000000000000000000, matches backend default) resolves to matchedNationalId=false |
USE_NURSE_BANK_MOCK (services/nurse/constants.ts, default true) |
b3 nurse_bank_accounts/* are live (the real add resolves the inquiry synchronously — no client poll needed); set flag false — nurseBankClientApi is wired |
🟡 |
AuthApi |
client/src/services/auth/apis/mockApi.ts (authMockApi) |
Phone-OTP login offline: requestOtp→{otpSent,resendAvailableInSeconds:120}; verifyOtp accepts dev code 123456 and locks after 3 wrong tries (otp_locked); getMe/selectRole/refresh from a MOCK_SCENARIO toggle (customer/nurse_unverified/no_role) to exercise all router branches |
USE_AUTH_MOCK (services/auth/constants.ts, default false — b2 is live) + MOCK_SCENARIO in mockApi.ts |
The real authClientApi is already wired to the live b2 routes; set USE_AUTH_MOCK = false (already the default) — no hook/screen change |
🟢 real by default, 🟡 mock available |
GeographyApi |
client/src/services/geography/apis/mockApi.ts (+ apis/seed.ts) |
The province→city→district reference hierarchy — a faithful subset of the b4 seed: 8 provinces, Tehran (city 101) with its 22 مناطق (1001…1022), and the white-space cities Mashhad/Isfahan/Shiraz/Tabriz/Ahvaz/Qom/Karaj as whole-city-only. Active-only, sortOrder-ordered. seed.ts also resolves a saved cityId/districtId back to names for the addresses & serviceAreas mocks |
USE_GEOGRAPHY_MOCK (services/geography/constants.ts, default true) |
b4 geo/{provinces,cities,districts} are live; set flag false — geographyClientApi is wired to the snake_case-param lookups. No hook/component change |
🟡 |
AddressesApi |
client/src/services/addresses/apis/mockApi.ts |
Customer address CRUD (list primary-first / create / update / set-primary / soft-delete) with the exactly-one-primary invariant enforced in-memory (first address auto-primary; promoting clears the prior; deleting the primary promotes the next). Persists the client-augmented provinceId (REQ-009) and the picked latitude/longitude (REQ-008) the wire DTO/create-body lack |
USE_ADDRESSES_MOCK (services/addresses/constants.ts, default true) |
b4 customer_addresses/* are live; deliver REQ-008 (accept the pin) + REQ-009 (provinceId on the DTO), then set flag false — addressesClientApi is wired (sends the pin + pageSize, echoes provinceId locally) |
🟡 |
ServiceAreasApi |
client/src/services/serviceAreas/apis/mockApi.ts |
Nurse coverage areas (list whole-city-first / add / remove). Enforces UNIQUE(cityId, districtId) exactly as the server — a duplicate (incl. a second whole-city row) throws the same 409 (area_duplicate) so the coverage editor's inline dup handling is demonstrable |
USE_SERVICE_AREAS_MOCK (services/serviceAreas/constants.ts, default true) |
b4 nurse_service_areas/* are live; set flag false — serviceAreasClientApi is wired (maps the server 409 to the same inline message). No hook/component change |
🟡 |
AddressMapPicker (map stand-in) |
client/src/components/geography/AddressMapPicker.tsx |
Not a real map — a bounded, tappable/draggable marker canvas (CSS grid, no Neshan/Google tiles, no network) that maps the pointer position to { latitude, longitude } around the chosen city's centroid (CITY_CENTROIDS/IRAN_CENTROID in services/geography/constants.ts). Emits real coordinates for the create/update request |
none (component boundary) | Replace the canvas internals with a real map widget (Neshan/Google, inlined per the client CSP) that emits the same { latitude, longitude } via onChange — AddressForm and every caller stay unchanged |
🟡 |
CatalogApi |
client/src/services/catalog/apis/mockApi.ts (+ apis/seed.ts) |
The catalog skeleton + nurse pricing layer. Categories mirror the b5 seed exactly (5 categories, ids 1–5, sortOrder 0–4). Seeds representative option groups/values the fresh backend does not (an admin authors them per category) — incl. required + optional groups and one cross-category (serviceCategoryId=null) group — so the builder's required-option gate + cross-category rendering demo. Enforces the server's create validation in-memory: 400 missing required dimension / bad price, and the (nurse, category, option-set) duplicate 409 (via optionSetSignature). Variant store seeded empty so the offerings empty-state demos; the nurse builds variants live (across price units). create/update/set_active/list(active-first, paginated)/get. Money stays an IRR digit-string end-to-end |
USE_CATALOG_MOCK (services/catalog/constants.ts, default true) |
b5 catalog/* + nurse_variants/* are live; set flag false — catalogClientApi is wired to the action-style routes (camelCase bodies, pageSize pagination per REQ-010, category_id snake_case filter). When swapped, categories will have NO option groups until an admin authors them (the mock's groups were illustrative). No hook/component change |
🟡 |
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 |
🟡 |