Files
baya-monorepo/dev/shared-working-context/reports/mocks-registry.md
T
hamid 1c266523bc backend phase 6: nurse verification & credentials (mocked vendors)
The trust engine. New `verif` schema (5 tables) + a data-driven verification
pipeline: steps are rows (6 seeded step-types), not a code enum.

- nurse_verifications.status is the single source of verification truth;
  nurse_profiles.is_verified is flipped ONLY inside the finalize transaction
  (VerificationAggregator: tracked verification + tracked profile -> one commit)
  and reversed on suspension/expiry — no in-between state.
- is_automated snapshotted onto each step at submit; steps seeded from active
  required step-types; automated runs (identity-KYC, Shahkar, IBAN ownership)
  find their step by code.
- users.national_id populated only on identity-KYC pass; Shahkar + IBAN owner
  compare against it (money-mule guard); shared-SIM -> shared_sim support alert.
- Documents are metadata-only behind signed URLs; credential_number encrypted
  and never serialized; public trust badge exposes credential TYPES, not numbers;
  holder-name cross-checked against the verified identity before recording.
- Admin-triggered credential-expiry scan reverts lapsed steps, re-gates
  bookability, raises a verification_expired alert + verification_expiry_prompt
  notification (scheduled cron deferred; config key
  verification_expiry_scan_cadence_hours).

Three new mock vendor seams (IShahkarVerifier / IIdentityKycProvider /
ICredentialVerifier) behind DI; reuses b3 IBankAccountOwnershipVerifier and
b0 IObjectStorage/IFieldEncryptor. 15 endpoints across 4 controllers.

Two migrations (tables + step-type seed). 154 tests pass, zero new warnings.
Contract dev/contracts/domains/verification.md + swagger snapshot refreshed;
handoff/report/mocks-registry updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:39:32 +03:30

16 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.

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 — drives state machine, fake settle/revert tbd SnappPay/Digipay OAuth + verb set; encrypted creds in payment_gateways.config_json 🔴
ICurrencyNormalizer backend-phase-12 Toman↔IRR — ×10 tbd Config-driven per provider boundary 🔴
IBankTransferProvider backend-phase-13 PAYA/SATNA payout — fake transfer ref tbd Jibit/Vandar/Sadad payout; source account; PAYA vs SATNA 🔴
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) backend-phase-1 Scheduling — in-process interval BackgroundService running PurgeOldReadNotifications daily (NotificationRetentionHostedService, Persistence/Services/Notifications/) none Swap to Hangfire/Quartz; register the job there; keep the purge predicate (is_read=1 AND age>90d) 🟡
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 🔴

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 falsepatientsClientApi 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 falseprofilesClientApi 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 falsenurseBankClientApi 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 falsegeographyClientApi 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 falseaddressesClientApi 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 falseserviceAreasClientApi 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 onChangeAddressForm and every caller stay unchanged 🟡