Files
baya-monorepo/dev/shared-working-context/reports/mocks-registry.md
T
2026-07-19 17:13:32 +03:30

71 KiB
Raw Blame History

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 · 🟢real adapter shipped, config-selected (mock remains the default/fallback) — the refinement-phase-8 state.

Refinement-phase-9 — docs honesty + observability (2026-07-13). §7.6: pruned the stale duplicate 🔴 rows (IDistributedLock/INurseSearch/IPaymentProvider/ISettlementSplitProvider/IWebhookVerifier/IMoadianClient/ ILicenseVerificationService) that the detailed rows below already correct; the recurring-jobs row is the real in-process scheduler; IPaymentCaptureSimulator now reflects its 6.4 prod removal. No seam was un-mocked — the deferred Elasticsearch INurseSearch backend (row) and the SMS/push channels of INotificationDispatcher (row) stay explicitly out of MVP with the pull-triggers in their "Make it real →" columns (SQL search / in-app notifications are the real MVP). Also removed the unused Serilog.Sinks.Elasticsearch package (dead sink block); tracing/metrics unified on OpenTelemetry; audit-log retention is now a scheduled IRecurringJob; TicketMessage.Body is encrypted at rest (§9.5). See backend-phase-9-report.md (refinement) for the full observability + deferral list.

Refinement-phase-8 — external rails go real (2026-07-13). Every vendor rail below now has a real HTTP adapter behind the same seam, config-selected via a per-rail Seams:*:Provider selector (default = the mock, so an unconfigured environment is unchanged; a typo falls closed to the mock). Selecting a real provider swaps the adapter by a registration change only — no handler changed. All adapters are built on HttpClient + System.Text.Json + BCL crypto (zero new NuGet packages); credentials come from Seams:* (user-secrets/env). dotnet build 0 new warnings · dotnet test 402 pass. The rails made real, with their provider token and adapter (CrossCutting/Seams/Real/):

Seam Seams:*:Provider Real adapter Notes
ISmsSender Sms:Provider=kavenegar KavenegarSmsSender launch-critical. OTP via verify/lookup template; the Dev OTP-in-logs bridge is disabled when a real provider is selected (OTP never logged).
IShahkarVerifier Shahkar:Provider=finnotech FinnotechShahkarVerifier shared Seams:Finnotech creds; شاهکار can't distinguish shared-SIM from mismatch (reported as plain mismatch).
IIdentityKycProvider IdentityKyc:Provider=finnotech FinnotechIdentityKycProvider nid+name inquiry; liveness extends the same adapter.
IBankAccountOwnershipVerifier BankOwnership:Provider=finnotech FinnotechBankAccountOwnershipVerifier استعلام شبا owner↔nid; fails closed (no nid returned = no match) — it is the first-payout gate.
IGeocoder Geocoding:Provider=neshan NeshanGeocoder outage degrades to the null-pin state, never blocks saving an address.
IObjectStorage ObjectStorage:Provider=s3 S3ObjectStorage MinIO/S3/ArvanCloud; manual AWS SigV4 (no SDK) — presigned GET = the real b6 signed-URL contract.
IPaymentProvider Payments:Provider=zarinpal ZarinPalPaymentProvider v4 request/verify/refund; mandatory server-side verify.
IWebhookVerifier (with Payments:Provider) HmacWebhookVerifier per-provider HMAC over the raw body; no-secret ⇒ the server-side verify re-check is the guard.
ISettlementSplitProvider (with Payments:Provider) ProviderSettlementSplitProvider تسهیم split-by-ratio to registered IBANs.
IBnplProvider/IBnplProviderResolver Bnpl:Provider=real SnappPayBnplProvider + DigipayBnplProvider + ConfiguredBnplProviderResolver one adapter per code; balinyaar = in-house (no external API), resolves to the net-of-fee model; tara/torobpay → null (unbuilt).
ICurrencyNormalizer Currency:TomanToIrrMultiplier MockCurrencyNormalizer (config-driven = the real impl) conversion only at the adapter boundary.
IBankTransferProvider BankTransfer:Provider=jibit JibitBankTransferProvider async rail — accepts as submitted; the reconciliation callback (POST webhooks/payouts/{provider}, ReconcilePayoutBatchCommand, HMAC-verified) flips submitted → paid/failed.
IMoadianClient Moadian:Provider=moadian MoadianClient submit + the MoadianReconciliationJob (IRecurringJob, 6 h) walks pending/submitted → registered.
IPaymentCaptureSimulator DisabledPaymentCaptureSimulator (prod) / MockPaymentCaptureSimulator (Dev/Testing) 6.4: removed from prod; the bookings/convert path is a Dev/Testing affordance (b10's webhook confirm is the real conversion).
ICredentialVerifier, ILicenseVerificationService mock (unchanged) 5.6: manual = intended MVP — MoH/INO/eNamad have no public B2B API; the manual admin review is the mechanism, not debt.
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. refinement-phase-0: in Development only, DevCapturingSmsSender decorates it (via AddDevelopmentOtpCapture, called from Program.cs inside IsDevelopment()) to also capture the code in DevOtpStore for the GET /api/v1/dev/last_otp/{phone} bring-up helper — not wired / 404 outside Development 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. refinement-phase-7: this is the >1-instance scale-out gate — a single-instance MVP intentionally keeps the in-proc cache (its generation-token invalidation is process-local); add Redis only when a second API instance runs 🟡 (in-proc is correct single-instance)
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 🟡
Recurring jobs (in-process scheduler) backend-phase-1 · re-homed refinement-phase-7 Scheduling — REAL single in-process scheduler RecurringJobSchedulerHostedService (Persistence/Services/Scheduling/) drives every IRecurringJob on its own (config-read) cadence: notification_retention (24 h) + booking_request_expiry (1 min) + verification_expiry_scan (verification_expiry_scan_cadence_hours) + no_show_sweep (no_show_scan_cadence_hours) + weekly_payout_generation (nurse_payout_interval_days, system-initiated draft only — processing stays admin). Each tick runs under IDistributedLock(scheduler:{name}); dormant under Testing. Admin manual triggers remain overrides. No new infra (SQL Server only). the cadence keys above (via IPlatformConfig) This is the intended MVP shape — not debt. Hangfire/Quartz only buys durable/cross-restart scheduling and is the >1-instance option alongside Redis (§7.2) — swap by re-registering the jobs behind it. Phase 8 adds the Moadian reconciliation + refund-settlement poll as new IRecurringJobs here. 🟢 real (single-instance)
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 🟡
IReviewModerationService backend-phase-14 AI review pre-screen — MockReviewModerationService (Baya.Infrastructure.CrossCutting/Seams/), no external call: ScreenAsync(reviewText) returns a ModerationVerdict(Decision, Reason) — a banned-word substring hit → Reject (banned_word:{w}); otherwise clean text → a human-review Flag by default (so the publish gate holds), or Approve when AutoApproveClean is set. The SubmitReview handler maps the verdict to the initial status (Approve→published, Reject→hidden, else pending) — decision authority stays with ModerateReviewCommand (human override). Registered singleton in AddCrossCuttingSeams Seams:ReviewModeration:AutoApproveClean (default false), Seams:ReviewModeration:BannedWords (default scam,fraud,کلاهبردار) 1) pick a text classifier / LLM moderation endpoint, add its client package to Directory.Packages.props; 2) add Seams:ReviewModeration:{ApiKey,BaseUrl} options; 3) implement ScreenAsync(reviewText) → map the provider's toxicity/spam scores to Approve/Flag/Reject + a reason; 4) swap the registration in AddCrossCuttingSeams (config-selected) — SubmitReviewCommand/ModerateReviewCommand unchanged, and the human moderation path always overrides; 5) test clean/flagged/rejected dispositions + that the publish gate still holds for a Flag 🟡
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 🟡
IPaymentCaptureSimulator backend-phase-9 → removed from prod refinement-phase-8 §6.4 The temporary conversion trigger that stood in for b10's real card capture. Prod now gets the fail-closed DisabledPaymentCaptureSimulator; only Dev/Testing re-register the succeeding MockPaymentCaptureSimulator (the bookings/convert path is a Dev/Testing affordance — prod converts via the b10 webhook confirm calling ConvertRequestToBooking directly). Registered in AddCrossCuttingSeams (prod) + re-registered in Dev/Testing from Program.cs Seams:PaymentCapture:ForceFailure (default false), Seams:PaymentCapture:PspFeeAmount (default unset) Nothing further for prod — the real conversion trigger is the b10 webhook confirm. The Dev/Testing mock stays as a test affordance; drop it only if/when bookings/convert is retired 🟢 (prod fail-closed; Dev/Testing mock is a test affordance)
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. refinement-phase-7: also the scheduler's per-tick lock (scheduler:{job}) uses this seam — once Redis-backed it serializes recurring-job ticks across instances (idempotency + DB uniques cover a double-run either way). Required only for >1 instance. 🟡 (in-proc is correct single-instance)
ISearchIndexMaintainer (the "ISearchIndexWriter" event shape) backend-phase-7 The index-maintenance seam (write side). The inline SQL path is REALSearchIndexMaintainer (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.) | 🟢 |

| ILicenseVerificationService | backend-phase-15 | Partner-center licensing (eNamad / MoH establishment-permit پروانه تأسیس) — MockLicenseVerificationService (Baya.Infrastructure.CrossCutting/Seams/), no external call: VerifyEstablishmentPermitAsync/VerifyENamadAsync return NeedsManualReview (no automated registry → a human admin decides), so VerifyPartnerCenterCommand records the manual approval and activates the center. A config toggle makes clean checks return Valid (auto-approve path); an explicit Invalid verdict blocks activation. Registered singleton in AddCrossCuttingSeams | Seams:LicenseVerification:AutoApprove (default false) | 1) obtain access to a real eNamad status endpoint and/or the MoH establishment-permit registry (no public B2B API today — likely a manual/partner data feed at launch); 2) implement the two methods to look up the permit/eNamad code and return Valid/Invalid + a reason; 3) swap the registration (config-selected) — VerifyPartnerCenter is unchanged (it keeps the human-override decision authority) | 🟡 |

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.

Refinement-phase-4 de-mock (2026-07-13) — 14 domains flipped to REAL (USE_*_MOCK = false), verified against the regenerated swagger + npm run check/test:ci green: geography, patients, profiles, nurse (bank), addresses, serviceAreas, catalog, search, bookingRequests, bookings, payment, reviews, tickets, notifications (auth was already real). The flip was not a pure flag flip for most: Phase-3 delivered fields the clientApi.ts mappers were written to null-override, so each mapper was updated to consume/send them (search name/avatar/distance + nurses/{id}/profile; patient relation/conditions; address provinceId; booking-request variantPrice/bookingId; ticket unreadCount/lastMessageAt + clientMessageId; review my_review; profile avatarUrl/preferredLanguage + a real multipart avatar uploadclientFetch now passes FormData through; the customer profile sources name from /me). The payment mock-gateway harness page was deleted; EVV_GPS_MODE auto-selects off (real geolocation).

7 domains stay 🟡 mocked — precondition REQ deferred/unsafe (documented, not forgotten): verification (REQ-034 admin queue/doc-URL/approve — nurse flow ready, admin half blocks the shared flag), refunds (REQ-035 admin preview/approve — customer cancel/policy ready), payouts (REQ-036 admin preview/holidayShifted/transfer-ref — nurse earnings ready), admin (REQ-031 RBAC roles — config/audit/ holidays/alerts ready), bnpl (REQ-022 options/schedule + REQ-024 wallet_installments deferred), partnerCenter (REQ-032/033 portal split reads + REQ-038 /me signal deferred), patientRecords (REQ-027 endpoints exist but the client family-record id model is string vs the wire's int → the customer-edit PUT is write-unsafe until the id types are reconciled; the nurse visit-note history half is contract-real).

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 falsepatientsClientApi is already wired to the b3 patients/* routes 🟢 (real, refinement-phase-4)
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. ui-phase-8: setAcceptingBookings(accepting) now mirrors the flip in-memory (isAcceptingBookings, never touching isVerified) — this is the real endpoint (POST nurse_profiles/set_accepting_bookings, previously unwired), not a mock-only gap; the mock exists only so USE_PROFILES_MOCK=true local dev still demos the go-live toggle USE_PROFILES_MOCK (services/profiles/constants.ts, default true) b3 customer_profiles/* + nurse_profiles/* + nurse_profiles/set_accepting_bookings are all live; deliver REQ-006 (avatar route/field) + REQ-007 (customer name/language) then set flag falseprofilesClientApi is wired (its uploadAvatar throws 501 until REQ-006; setAcceptingBookings is fully wired already) 🟢 (real, refinement-phase-4)
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 falsenurseBankClientApi is wired 🟢 (real, refinement-phase-4)
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 falsegeographyClientApi is wired to the snake_case-param lookups. No hook/component change 🟢 (real, refinement-phase-4)
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 falseaddressesClientApi is wired (sends the pin + pageSize, echoes provinceId locally) 🟢 (real, refinement-phase-4)
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 falseserviceAreasClientApi is wired (maps the server 409 to the same inline message). No hook/component change 🟢 (real, refinement-phase-4)
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 onChangeAddressForm 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 15, sortOrder 04). 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 falsecatalogClientApi 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 🟢 (real, refinement-phase-4)
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. ui-phase-8: VerificationStatus gained two mock-tolerant fields the nurse-facing wire doesn't serve yet — submittedAt (stamped once when start() first seeds the steps, REQ-055) and credentialSubmission (stamped by submitCredentialDetailsinoNumberSubmitted boolean + specialties/registry fields, never the raw INO number, REQ-056) — so B6's timestamp and B5's hydrate-on-return both demo pre-REQ USE_VERIFICATION_MOCK (services/verification/constants.ts, default true) b6 nurse_verification/* + nurses/{id}/trust_badge are live; set flag falseverificationClientApi is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). Caveats: 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); submittedAt (REQ-055) and credentialSubmission (REQ-056) are both undefined until served, degrading gracefully (B6 omits the timestamp line; B5 falls back to blank fields). 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 machinecheckInVisit 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. ui-phase-7: forViewer now unmasks addressSnapshotJson for the nurse once the booking is confirmed+ (simulating REQ-051, still delivered) instead of unconditionally nulling it — addr5001 gained latitude/longitude matching the EVV reference point so the new address-card map link is demoable; listTodaySessions stamps a variantLabel off the booking's frozen variant snapshot (REQ-052) USE_BOOKINGS_MOCK (services/bookings/constants.ts, default false) 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 falsebookingsClientApi maps the routes 1:1 (+ bookingsServerApi for the RSC prefetch). No hook/component change. Deliver REQ-051 (nurse-view address post-confirmation) + REQ-052 (today-feed variantLabel) 🟢 (real, refinement-phase-4)
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 🟢 (real, refinement-phase-4)
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. ui-phase-6: initiatePayment's redirectUrl is now null (was a stale pointer to the deleted card-gateway harness page — a latent bug, since the harness itself was already removed in refinement-phase-4; the checkout page's !redirectUrl branch already reads the outcome directly, no behavior change); getCheckoutSummary adds mock nurseAvatarUrl: null/nurseVerified: true (REQ-046, the C6 identity moment); the capture path stamps capturedAt/createdAt on the transaction so PaymentOutcomeDto serves trackingCode/paidAt (REQ-046, the confirmation receipt) and the new getPaymentHistory reads the same transaction list (REQ-047, wallet «پرداخت‌ها»); invoice creation adds mock paymentMethod: 'card'/transactionReference/sellerFiscalIdentity (REQ-049, fiscal-grade invoice) 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) + REQ-046 (nurse identity + tracking code/paid-at) + REQ-047 (payment history) + REQ-049 (invoice fiscal fields), make the upstream bookingRequests flow real, then set flag false. No hook/component change 🟢 (real, refinement-phase-4)
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 🗑 removed in refinement-phase-4 (payment flipped real)
RefundsApi client/src/services/refunds/apis/mockApi.ts The f10 customer cancel + refund surface b11 doesn't serve (refunds are admin-only; no customer cancel command, no policy preview, no refund-by-booking, no fee-leg decomposition on the customer status → REQ-019/020/021). Reads the shared f8 bookings store (mockGetBookingForRefund) to resolve the tier by lead time (free_24h >24h / partial_under_24h <24h / customer_no_show started — client-invented codes → i18n keys) and the per-session refundable(un-started)/locked(completed-and-verified) breakdown, decomposing the refund across the two fee legs via integer parts-per-10000 BigInt math (refundAmount + fee = refundableGross to the rial). cancelBooking flips the booking → cancelled (mockMarkBookingCancelled stamps the b9 snapshot + cancels only un-started sessions) and creates a refund: card → succeeded immediately (no ETA); BNPL → approvedprocessingsucceeded over status polls with a expected_customer_refund_eta ~10 business days out (Fridays skipped) so the ~710-day banner renders. Enforces the outside-policy 409 (already-cancelled / nothing-refundable / non-refundable session). Seeds a failed refund on the cancelled booking 5004 so the contact-support state demos; booking 5002 is pinned to the BNPL channel; booking 5003 (new, mid-engagement) demos the mixed refundable/locked breakdown. Also adds bookings-store seeds 5003/5004 + the two non-seam exports. ui-phase-6: getMyRefunds (REQ-048) returns every in-memory refund newest-first — the wallet «استردادها» tab USE_REFUNDS_MOCK (services/refunds/constants.ts, default true) Deliver REQ-019 (customer cancel command — the real refundsClientApi.cancelBooking already targets POST bookings/{id}/cancel) + REQ-020 (cancellation-policy preview → GET bookings/{id}/cancellation_policy, incl. the canonical cancellation_policy_code set) + REQ-021 (GET refunds/by_booking/{id} + the decomposition fields on the customer refunds/{id}/status) + REQ-048 (GET refunds/my), then set flag false — the real client maps the published refunds/{id}/status 1:1 and targets the proposed slugs for the rest. No hook/component change 🟡
BnplApi client/src/services/bnpl/apis/mockApi.ts The f11 BNPL installment checkout (D1D5) b12 doesn't serve client-side (b12 is order-centric — eligibility/initiate/status/webhook — and explicitly does not model the repayment schedule; no provider/plan options, no wallet installment status → REQ-022/023/024). Reads the frozen request gross from the shared f7 store and plays the provider: getBnplOptions builds the provider set as data (دیجی‌پی 3/6/12 · اسنپ‌پی ۴ · اقساط بالین‌یار; per-plan monthly/down-payment/total via integer parts-per-10000 BigInt math, never a hardcoded fee in the UI); checkEligibility returns eligible unless the national-id last digit is 0 (→not_eligible) or the order exceeds MOCK_CREDIT_CEILING_IRR (→ceiling_exceeded) so both declined paths demo; getBnplSchedule serves the down-payment + N-installment rows (last absorbs the remainder → rows sum to total); issueBnplToken enforces b12 idempotency (same key → same token; repeat after settle / lapsed window → 409) + a redirectUrl into the local provider-handoff harness; acceptBnplSchedule on success is the settle stand-in and reuses the f9 conversion bridge — flips the request converted (mockMarkBookingRequestConverted), inserts a confirmed booking (mockInsertConvertedBooking; a settled BNPL order = a card payment net-of-fee, payout invariant to method), and seeds a provider-reported Wallet plan; getWalletInstallments serves D5 (seeded active دیجی‌پی ۶-ماهه with paid/due-soon/upcoming rows + each settled checkout's plan). Money = served IRR digit-strings end-to-end (components only format) USE_BNPL_MOCK (services/bnpl/constants.ts, default true) Deliver REQ-022 (options + schedule — real bnplClientApi targets checkout_bnpl/options/{id} + checkout_bnpl/schedule/{id}), REQ-023 (eligibility accepts the D3 national-id/mobile/consent), REQ-024 (checkout_bnpl/wallet_installments provider-reported status + a customer bookingId on the settled order), and make the upstream bookingRequests flow real, then set flag falsecheckEligibility/issueBnplToken(Idempotency-Key)/getBnplOrder already map the live b12 routes 1:1; the settle-on-return reads the order (the real settle is the provider webhook). No hook/component change 🟡
BNPL provider-handoff harness (test harness) client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/gateway/page.tsx Not a product feature — a dev stand-in for the provider's hosted BNPL page so the initiate → redirect → return round-trip is exercisable without a provider: the mock redirectUrl points here, and its pay/cancel buttons drive both branches of the return surface (?outcome=success|failure). Clearly labelled «در حال انتقال به ارائه‌دهنده», dashed border. ui-phase-6: was reachable by direct URL in a production build (the equivalent card-gateway harness was already deleted, this one never got the same guard since BNPL stays mock-primary) — now notFound()-gated outside NODE_ENV=development, still fully reachable in next dev none — only reachable via the mock's redirectUrl On the real path b12's redirectUrl is the provider's absolute URL (the wizard does a full window.location.assign for http(s)), so this page is never linked; delete it when USE_BNPL_MOCK retires. The provider's return deep-link into /bookings/checkout/bnpl/return is backend/provider config 🟡
PayoutsApi client/src/services/payouts/apis/mockApi.ts The f12 nurse earnings surface b13 doesn't serve read-side for a nurse (b13's only nurse route is GET nurse_payouts/history; the four-bucket earnings summary, the per-booking earnings list + money-state, and a nurse-readable payout detail with batch context + booking links are gaps → REQ-025). Self-contained, money-correct fixtures exercising every UI state: all four earnings states (pending/eligible/paid/clawback_applied; booking ids 50015004 align with the f8 bookings-store seeds so "view booking" deep-links land), all four PayoutStatus values in history (pending/submitted/paid/failed, incl. a failed payout with failureReason: 'invalid_sheba' for the read-only failure banner), payout details that reconcile (gross clawback = net = amount, Σ booking-link amounts = grossEarnings), and a signed net balance computed with BigInt via a MOCK_SCENARIO toggle (standard = positive; clawback_heavy = negative "owed back" for phase §7 step 3). Timestamps are relative to now so the pending dispute-window countdown always ticks; money stays IRR digit-strings end-to-end (components only format). getNurseEarnings filters by state + paginates. ui-phase-7: buildSummary() adds nextPayoutDate (+3 days) + nextPayoutEligibleAmountIrr (= the eligible bucket) for the earnings/dashboard «برداشت بعدی» forecast line (REQ-053); the client also gained a failureReasonLabelKey() helper (services/payouts/failureReasons.ts) mapping known failureReason codes (today: invalid_sheba) to i18n labels so PayoutHistoryRow/the payout detail never show the raw vendor string as the headline USE_PAYOUTS_MOCK (services/payouts/constants.ts, default true) + MOCK_SCENARIO in constants.ts Deliver REQ-025 (earnings_balance + earnings list + nurse nurse_payouts/{id} detail + failureReason on the history DTO) + REQ-053 (nextPayoutDate/nextPayoutEligibleAmountIrr), then set flag falsepayoutsClientApi already maps the live GET nurse_payouts/history 1:1 and targets the proposed slugs for the rest. No hook/component change 🟡
ReviewsApi client/src/services/reviews/apis/mockApi.ts The f13 moderated-review trust loop. b14 serves the review submit (POST bookings/{id}/review), the public nurse reviews page (GET nurses/{id}/reviews), and the tag rollup — those are mapped 1:1 in reviewsClientApi. But there is no review-eligibility read and no my-review-for-booking read (REQ-026), and the whole moderation transition (pending_moderation → published) is admin-only (f15). The mock reads a booking from the shared f8 bookings store (mockGetBookingForReview) to gate eligibility on a completed/closed booking (aligns with the new completed seed 5005 / nurse 1 / patient 905), tracks the customer's submission as pending_moderation so eligibility flips already_reviewed + getMyReviewForBooking returns the persistent "under review" state, and seeds a published list per nurse (nurse 1 has 7 → the profile tab paginates; nurses 5/6 empty → empty state). The aggregate is recomputed from the published list (never a stored sum). A submitted review never enters any public list. Dev-only __mockPublishSubmittedReview(bookingId) stands in for the deferred (f15) admin queue so a human can watch a review appear on the profile. Money-free USE_REVIEWS_MOCK (services/reviews/constants.ts, default true) Deliver REQ-026 (review_eligibility + my_review reads; confirm masked-author omission), then set flag falsereviewsClientApi.getNurseReviews/createReview already map the live b14 routes 1:1 and target the two proposed slugs for the gaps. Moderation UI itself is f15 (admin). No hook/component change 🟢 (real, refinement-phase-4)
PatientRecordsApi client/src/services/patientRecords/apis/mockApi.ts The f13 continuity-of-care surface. Two very different things: (1) the nurse-authored visit-note history (getPatientHistory/createVisitNote) is REAL b14 (GET/POST patients/{id}/care_records), mapped 1:1 in patientRecordsClientApi (the append composes the ticked task checklist into the note body since the wire has no structured task field); (2) the family-owned editable record (medications/routine/tasks — the داروها/روتین/وظایف tabs) and the access check have NO backend at all (neither the b14 contract nor data-model/10-reviews-and-records.md model them → REQ-027). The mock is patient-scoped and lazily seeds a coherent default per patient: a default family record (customer edits it), a multi-nurse continuity history (two prior notes from different nurses, proving the history persists across nurse changes; a nurse append prepends to the same patient's history), and a foreign-patient access-denied path (MOCK_FOREIGN_PATIENT_ID = 8888canView:false + a 403 on every read) so the non-leaking access-denied card is demoable. Clinical text is fixture data (never logged) USE_PATIENT_RECORDS_MOCK (services/patientRecords/constants.ts, default true) Deliver REQ-027 (family-owned care_record GET/PUT + record_access + structured taskResults), then set flag false — the history/append methods already map the real b14 routes; only the family-record/access methods flip. Confirm whether the family-owned record is a real MVP entity 🟡
f8 bookings mock — completed-booking seed 5005 + f13 cross-mock reads client/src/services/bookings/apis/mockApi.ts Non-seam additions (mirrors the f10 refunds precedent). The f8 seeds had no completed booking (only confirmed/in_progress/cancelled), so f13's review flow needs one: added booking 5005 (status: 'completed', nurse 1, patient 905, one completed EVV session) so the customer can open a completed booking and leave a review. Also added a cross-mock read helpermockGetBookingForReview(id) (single booking, clone) — imported by the reviews mock to gate eligibility and read the patient/nurse snapshot for a submission (the listBookings seam row omits patientId/nurseId). One-way edge INTO bookings (the bookings mock never imports f13), so no cycle — (part of USE_BOOKINGS_MOCK) When the bookings flow goes real (b9/b10 conversion live), 5005 stops being a static seed and the cross-mock helpers retire with the reviews/records mocks 🟢 (real, refinement-phase-4)
TicketsApi client/src/services/tickets/apis/mockApi.ts The f14 ticket channel (b15). b15 serves open/list/thread/message and ticketsClientApi maps them 1:1; REQ-028 is now delivered (unreadCount/lastMessageAt + clientMessageId idempotency are real on the wire), so USE_TICKETS_MOCK = false by default — the mock stays available for offline/demo use. It seeds 3 tickets (a booking-5001 coordination ticket with a stored internal admin note the user view NEVER returns — the no-leak demo — plus a support + a closed refund ticket), returns them newest-activity first with a per-ticket unread count that clears on open; openTicket is idempotent for coordination + bookingId; postMessage appends as the current viewer, throws 403 on a closed ticket, and throws 500 on the dev sentinel body '/fail' (the optimistic failure→retry path — ui-phase-10: a failed send now flips the bubble to sendStatus:'failed' in place instead of rolling it back, and a retry re-mutates the same clientMessageId). MOCK_VIEWER_USER_ID (per-role "me") drives isMine; isInternal is never modelled in the user-app types. ui-phase-10 additions: toSummary() now also computes lastMessagePreview (first ~80 chars of the last non-internal message) + lastAuthorRole (REQ-059 gap — real path maps both null, card degrades gracefully); a new getUnreadTotal() sums unread across every seeded ticket for the chrome support-badge (REQ-059 — real path returns null, badge doesn't render) USE_TICKETS_MOCK (services/tickets/constants.ts, default false — REQ-028 delivered) Deliver REQ-059 (lastMessagePreview/lastAuthorRole on the summary + a cheap unread-total read) to fully retire the mock's enrichment role; REQ-060 (message photo attachments) gates the composer's designed-but-off attachment affordance (TICKETS_ATTACHMENTS_ENABLED, default false). ticketsClientApi already maps the live b15 routes 1:1 (drops any leaked internal message defensively) 🟢 real by default (REQ-028); 🟡 REQ-059/060 gaps remain mock-only
NotificationsApi client/src/services/notifications/apis/mockApi.ts The f14 notification center + polled bell (b1). The b1 endpoints are live and notificationsClientApi maps them 1:1, but a notification only exists once some other backend domain dispatches one (INotificationDispatcher) — none run client-side while the upstream flows are mock-primary — so there'd be nothing to show. The mock seeds a realistic unread-first feed spanning every deep-link class (ticket_message/booking_confirmed/refund_processed/payment_captured/payout_paid/review_published + one unknown-type/no-payload row that degrades to no deep-link), each with a snake_case dataJson string the list maps through the real parseNotificationData; getUnreadCount/markRead/markAllRead mutate the in-memory feed. Dev-only __mockPushNotification(type,title,dataJson?,body?) prepends a fresh unread row so a human can watch the bell badge increment within the poll interval (phase §7 step 4). Ids align with the f8 bookings + tickets mocks so a deep-link lands on a real screen USE_NOTIFICATIONS_MOCK (services/notifications/constants.ts, default true) When the upstream domains dispatch real notifications, set flag falsenotificationsClientApi already maps the live b1 notifications/* routes 1:1 (page/pageSize, {count}, {notificationId}). No hook/component change 🟢 (real, refinement-phase-4)
AdminApi client/src/services/admin/apis/mockApi.ts The f15 backoffice-owned data (b1 + b15). Fixtures engineered to exercise every console state: one config per data_type (decimal/int/bool/json/string — so the typed inputs + the 01 rate validation are all reachable) with a change-history trail; holidays with bank-closed days; a paged audit log with changedFields diffs (one row <redacted> for a PII field); a support-alert list spanning every type (low_rating/evv_no_show/evv_location_mismatch/verification_expired/shared_sim/payment_anomaly/fraud_signal/nurse_clawback/emergency) and all three statuses so the worklist filters are testable; and RBAC grants. Mutations mutate the in-memory arrays (a config save writes a history row; assign/resolve advance an alert; grant/revoke flip a role). Timestamps relative to now USE_ADMIN_MOCK (services/admin/constants.ts, default true) b1 config/holiday/audit/support-alert routes are live and adminClientApi maps them 1:1 — deliver REQ-029 (config updatedAt/updatedBy) + REQ-030 (audit actor/action/date filters) + REQ-031 (the RBAC admin_roles/* endpoints, which don't exist yet), then set flag false. No hook/component change 🟡
PartnerCenterApi client/src/services/partnerCenter/apis/mockApi.ts The f15 partner centers (b15) — admin management + the center-scoped portal. Returns center #1 = merchant-of-record (the settlement/invoice view renders) and #2 = non-MoR (the "settlement runs through Balinyaar" state) and a draft #3 (unverified banner); sponsored nurses (verified + unverified), sponsored bookings, and commission invoices whose platform commission + BNPL commission + VAT = total (VAT on the commission line only) with a fake 22-digit moadianReferenceNumber + a stub PDF url. settlementIbanMasked is last-4 only (write-then-masked: create/edit submit a full IBAN, only last-4 ever returns). Admin CRUD/verify/set-active/assign-nurse + the portal "my center" reads all mutate/read the in-memory world; "my center" resolves to MOCK_MY_CENTER_ID (=1, MoR) USE_PARTNER_MOCK (services/partnerCenter/constants.ts, default true) + MOCK_MY_CENTER_ID b15 admin partner-center CRUD/verify/sponsor are live; deliver REQ-032 (portal split reads `centers/me[/nurses /bookings
Admin-endpoint additions to existing domain mocks (verification/refunds/payouts/reviews/tickets) the same apis/mockApi.ts files (+ their clientApi.ts) The f15 staff lens over prior domains — new admin methods added behind the existing seams (no new seam, no hook/component change on swap). verification: a nurse-level review queue (pending/in_review, one with an expiring credential) + a per-nurse case whose manual credential steps carry a document, and getDocumentSignedUrl that returns a fresh short-lived URL each call (sentinel documentId 9999 throws → viewer error/re-request path); decideStep/approve/reject re-aggregate. refunds: a getRefundPreview with the fee/payout split reconciling to the rial per booking (a normal card, a BNPL w/ ETA, a post-payout w/ clawback notice, and a provider-decline sentinel that fails then retries succeeds). payouts: batches spanning completed/partially_failed/processing (one holiday-shifted), a preview w/ eligible + skipped(no-IBAN) + clawback line + holiday-shifted date, an idempotency-keyed run/retry (same key → same result, never double-pays), a failed payout to retry, and record-transfer-reference. reviews: a moderation queue incl. a low-rating flagged review; moderateReview returns a plausible recomputed aggregate. tickets: a global admin queue + a thread that includes the seeded internal note (the no-leak inverse demo) + postAdminMessage w/ isInternal; a refund-linked ticket (bookingId+refundId) so the RefundPanel opens from it the owning domain's flag (USE_VERIFICATION_MOCK / USE_REFUNDS_MOCK / USE_PAYOUTS_MOCK / USE_REVIEWS_MOCK / USE_TICKETS_MOCK, all default true) Deliver the per-domain admin gaps — REQ-034 (verification nurse-queue + on-demand doc URL + whole-verification approve/reject), REQ-035 (refund preview + explicit approve/reject), REQ-036 (payout single-preview + holidayShifted + record-transfer-reference), REQ-037 (moderation tagCodes) — then flip the owning domain's flag. The real clientApi methods already map the live admin routes 1:1 and target the proposed slugs for the gaps 🟢 (real, refinement-phase-4)