frontend phase 5 & backend phase 12
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# Backend phase 12 — BNPL: provider-financed installments (mocked) — report
|
||||
|
||||
**Mission:** let a family pay for a booking with a provider-financed BNPL plan, and record it correctly — a BNPL
|
||||
order is, in Balinyaar's books, **a card payment that lands net-of-fee**.
|
||||
|
||||
## What was built
|
||||
|
||||
### Domain (`Baya.Domain/Entities/Bnpl/`)
|
||||
- **`BnplTransaction`** — one row per order, **1:1 with its `payment_transaction`**. Guarded `Status` mutated only
|
||||
through cohesive `MarkTokenIssued`/`MarkVerified`/`MarkSettled`/`MarkReverted`/`MarkCancelled`/`MarkFailed`;
|
||||
`MarkSettled` enforces `settled = order − commission` (≥0). Money is IRR `long`; `settled_at`,
|
||||
`provider_commission_reversed_amount`, and all settle/revert amounts are nullable.
|
||||
- **`BnplStatus`** + **`BnplTransitions`** — the forward-only state machine (`eligible → token_issued → verified →
|
||||
settled → reverted/cancelled/failed`), the idempotency spine.
|
||||
- **`BnplEligibilityStatus`** (`eligible`/`not_eligible`/`ceiling_exceeded`), **`BnplProviderCodes`**
|
||||
(`snapppay`/`digipay`/`tara`/`torobpay`).
|
||||
- **`LedgerPosting.BnplSettle`** — the net-of-fee group (card-capture legs **plus** `DEBIT bnpl_fee_expense /
|
||||
CREDIT escrow_held`), one balanced `transaction_group_id`, `SourceRefType = bnpl_transaction`. Throws if the
|
||||
capture legs don't reconcile.
|
||||
|
||||
### Application (`Baya.Application/Features/Bnpl/`)
|
||||
- Queries: **`CheckBnplEligibilityQuery`** (records `eligibility_status` on a created/updated row),
|
||||
**`GetBnplOrderStatusQuery`** (admin/customer, tenancy-scoped).
|
||||
- Commands: **`InitiateBnplOrderCommand`** (token + `eligible → token_issued`, under
|
||||
`lock(booking-request:{id}:payment)`, idempotency-keyed), **`VerifyBnplOrderCommand`**, **`SettleBnplOrderCommand`**
|
||||
(net-of-fee ledger + booking conversion, under `lock(bnpl:{id}:settle)`), **`RevertBnplOrderCommand`** (reuses
|
||||
b11 `CreateRefundCommand`), **`HandleBnplCallbackCommand`** (webhook dedup + dispatch by event type).
|
||||
- **`BnplOrderInitializer`** (shared find-or-create of the pending `payment_transaction` + 1:1 BNPL row) and the
|
||||
extracted **`BookingConversion`** helper (shared by b10 card capture + b12 settle — b10 was refactored to use it).
|
||||
- New seams in `Contracts/Payments/`: **`IBnplProvider`** (full verb set, supersedes the b11 revert-only stub),
|
||||
**`IBnplProviderResolver`**, **`ICurrencyNormalizer`**; `IBnplRepository` on `IUnitOfWork`.
|
||||
|
||||
### Infrastructure
|
||||
- **`Persistence`**: `BnplTransactionConfig` (`payments.BnplTransactions`, `UNIQUE(payment_transaction_id)`,
|
||||
`CK_BnplTransactions_SettleSplit`, filtered token index), `BnplRepository`, `UnitOfWork` wiring, one migration
|
||||
**`BnplTransactions`**. `IRefundRepository.GetExternalRevertReferenceAsync` added for the revert audit.
|
||||
- **`CrossCutting/Seams`**: `MockBnplProvider` (deterministic full state machine), `MockBnplProviderResolver`,
|
||||
`MockCurrencyNormalizer`; `SeamOptions` extended (`BnplOptions` + `CurrencyOptions`); DI registration in
|
||||
`AddCrossCuttingSeams`.
|
||||
- **`API`**: `CheckoutBnplController`, `WebhooksBnplController`, `AdminBnplController`.
|
||||
|
||||
## What is now testable and exactly how (per phase §7)
|
||||
Seed a `pending_payment`/accepted booking request with a known three-amount split and a `payment_gateways` row
|
||||
`type='bnpl', provider_code='snapppay'`; mock commission % via `Seams:Bnpl:CommissionRate` (default 10%).
|
||||
1. **Eligibility** — `POST api/v1/checkout_bnpl/eligibility` → `eligible` + plan summary; a `bnpl_transactions`
|
||||
row exists with `eligibility_status` set and `status='eligible'`. *(Covered:
|
||||
`BnplHandlerTests.Eligibility_creates_row_eligible_and_returns_plan`, `BnplApiTests` eligibility.)*
|
||||
2. **Initiate** — `POST api/v1/checkout_bnpl/initiate` → `status='token_issued'`, deterministic token + redirect;
|
||||
1:1 with the `payment_transaction`; a second initiate reuses the same row.
|
||||
*(`BnplHandlerTests.Initiate_issues_token_and_is_strictly_one_to_one`.)*
|
||||
3. **Verify → settle (the ledger)** — drive `POST api/v1/webhooks_bnpl/snapppay` (`order.verified` then
|
||||
`order.settled`) or the admin settle → `verified → settled`; `settled_amount = order − commission`, ledger shows
|
||||
the balanced net-of-fee group and net `escrow_held = settled_amount`.
|
||||
*(`BnplHandlerTests.Settle_posts_net_of_fee_group_…`, `BnplApiTests.Full_flow_…`, `BnplLedgerAndStateTests`.)*
|
||||
4. **Payout invariance** — `nurse_payable` credited = `gross − balinyaar_commission`, **identical to the card path**
|
||||
and independent of the BNPL commission. *(Asserted in both the handler + api full-flow tests, compared against
|
||||
`LedgerPosting.CardCapture`.)*
|
||||
5. **Replayed settle is a no-op** — re-deliver the same settle callback → webhook dedup + state guard reject it;
|
||||
no second ledger group. *(`BnplApiTests.Replayed_settle_callback_is_idempotent_no_second_ledger`,
|
||||
`BnplHandlerTests.Replayed_settle_is_a_noop_…`.)*
|
||||
6. **Revert** — `POST api/v1/admin_bnpl/{id}/revert` → `status='reverted'`, revert audit set; a `refunds` row with
|
||||
`refund_channel='bnpl_revert'` + `expected_customer_refund_eta`; reversal ledger posts.
|
||||
*(`BnplApiTests.Admin_revert_reverses_a_settled_order_…`.)*
|
||||
7. **Status** — `GET api/v1/admin_bnpl/{id}` (admin) / `GET api/v1/checkout_bnpl/{id}` (customer, own only).
|
||||
8. **Guards** — settle-before-verify is a 409; the state machine rejects illegal edges.
|
||||
*(`BnplHandlerTests.Settle_before_verify_…`, `BnplLedgerAndStateTests.State_machine_…`.)*
|
||||
|
||||
Full suite: **314 pass** (4 identity + 214 foundation + 96 api). Build clean, 0 new code warnings.
|
||||
|
||||
## What is mocked + how to make it real
|
||||
- **`IBnplProvider`** (per `provider_code` via **`IBnplProviderResolver`**) — deterministic mock, no network;
|
||||
settle returns `order − round(order × Seams:Bnpl:CommissionRate)` with the commission read from the response.
|
||||
Real: one adapter per 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`; creds from the encrypted
|
||||
`payment_gateways.config_json`; per-contract commission read from the settle response. **Do not use the unrelated
|
||||
Canadian `SnapPayInc/open-api-java-sdk`.**
|
||||
- **`ICurrencyNormalizer`** — mock ×10 Toman→IRR (`Seams:Currency:TomanToIrrMultiplier`). Real: read the
|
||||
multiplier/unit from provider config at the boundary.
|
||||
- **`settled_at` is nullable / non-instant** (`Seams:Bnpl:SettlementInstant`) — the mock models the
|
||||
daily/T+1–3/weekly reality. **b13 must not assume BNPL cash funds a payout.**
|
||||
|
||||
## Contracts produced / consumed
|
||||
- **Produced:** `dev/contracts/domains/bnpl.md`; `dev/contracts/openapi/swagger.v1.json` refreshed
|
||||
(386K → 411K, all BNPL paths present).
|
||||
- **Consumed:** b10 `payment_transactions`/`ledger_entries`/`payment_webhook_events`/`IWebhookVerifier`/
|
||||
`IDistributedLock`/`LedgerPosting`; b11 `refunds`/`CreateRefundCommand`/`refund_channel`; b9 booking split +
|
||||
`BookingFactory`; b1 typed config accessor.
|
||||
|
||||
## Follow-ups
|
||||
- **b13:** the `settled_at`-gates-payout coupling — add `require_bnpl_settlement_for_payout` and gate a BNPL
|
||||
booking's payout on `bnpl_transactions.settled_at`.
|
||||
- **DEFERRED:** `bnpl_settlement_entries` (tranched settlement — modeled-but-not-built, additive migration later);
|
||||
multi-provider routing/failover (one active route today); `provider_commission_reversed_amount` reconciliation
|
||||
on revert (left null when the b11 refund path drives it).
|
||||
- The `BnplTransactions` migration was **not applied to a live DB** this session (no reachable SQL Server in the
|
||||
agent env); apply with `dotnet ef database update` before the next live run, and seed a `type='bnpl'` gateway.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Frontend Phase 5 — Nurse verification flow (trust engine) — Report (2026-07-09)
|
||||
|
||||
Builds the trust engine's front end: the staged, platform-owned verification a nurse walks before any
|
||||
service can go live. A nurse lands on a **status checklist (B3)**, submits **identity (B4)**, submits
|
||||
**professional credentials (B5)**, and waits on **under-review (B6)** until an admin decides — then a
|
||||
**trust badge** renders and the **publish gate** unlocks. Consumes the b6 `verification` contract. Unlocks
|
||||
a bookable verified nurse + the `<TrustBadge>` f6 reuses.
|
||||
|
||||
## What was built
|
||||
|
||||
### `services/verification` domain (`client/src/services/verification/`)
|
||||
- **`types.ts`** — DTOs mirrored from [`verification.md`](../../contracts/domains/verification.md)
|
||||
(**camelCase** wire): `VerificationStatus` (aggregate `status` + `isBookable` + `blockingSteps` +
|
||||
ordered `steps[]`), `VerificationStep`, `RunStepResult`, `UploadUrlResult`, `DocumentConfirmedResult`,
|
||||
`VerificationDocument` (metadata only), `NurseCredential`, `TrustBadge`, the `VerificationApi` seam, and
|
||||
the string-literal enums (aggregate/per-step status, the six step codes, credential type, verification
|
||||
method, `BadgeState`). Helpers: `isApproved`, `ownBadgeState` (own-profile, computes `expired`),
|
||||
`publicBadgeState` (public/search — verified/unverified only), `SPECIALTY_PRESETS`.
|
||||
- **`validation.ts`** — `isValidNationalId` (10-digit mod-11 checksum; rejects all-same-digit).
|
||||
- **`keys.ts`** — `verificationKeys.status()` (the single cached source B3+B6 read), `.documents(code)`,
|
||||
`.badge(nurseId)`.
|
||||
- **`constants.ts`** — `USE_VERIFICATION_MOCK` (**default true**), the status `staleTime` (30 s) + badge
|
||||
`staleTime`/`gcTime`, the document type/size caps (jpg/png/pdf, 5 MB), `NATIONAL_ID_LENGTH`.
|
||||
- **`apis/`** — `clientApi.ts` (real HTTP, action-style routes, XHR signed-URL PUT for upload progress +
|
||||
SHA-256 integrity hash), `mockApi.ts` (**primary**, full journey + dev-only admin sim), selecting
|
||||
`index.ts`. Both implement `VerificationApi`; swap is one line.
|
||||
- **`hooks/`** — one per file: `useVerificationStatus` (query), `useStartVerification` (setQueryData),
|
||||
`useSubmitIdentity` (runs KYC → chained Shahkar), `useRunBankVerification`, `useUploadVerificationDocument`
|
||||
(progress via vars), `useSubmitCredentials`, `useNurseTrustBadge`. **Every mutation invalidates
|
||||
`status()`** (badge where relevant), so the checklist re-renders from cache with no manual refetch.
|
||||
|
||||
### Screens — nurse verification route subtree (`app/[locale]/(private-routes)/nurse/verification/`)
|
||||
- **B3 `page.tsx`** — the hub: loading skeleton, error+retry, `not_started` start-CTA, the in-progress
|
||||
**"X از Y" meter + data-driven checklist** (`VerificationChecklist` reusing the shared `StatusChip`), a
|
||||
**blocking summary**, a single **continue CTA** that routes to the next actionable step (calls
|
||||
`useStartVerification` first when `not_started`), and the terminal **`approved`** state (publish link).
|
||||
- **B4 `identity/page.tsx`** — national-ID field (checksum) + national-ID card + liveness selfie **local
|
||||
captures** (identity stores no server document — they feed the automated KYC), the honest auto-registry
|
||||
note, submit → `useSubmitIdentity`. Handles national-ID mismatch (`failed`) and the **non-accusatory
|
||||
shared-SIM** Shahkar failure distinctly.
|
||||
- **B5 `credentials/page.tsx`** — INO number + specialty chips (presets + add-your-own) + **a
|
||||
`<DocumentUpload>` per manual credential step (data-driven from `status`)** each moving its step to
|
||||
`in_review`, + an optional education-cert local upload + optional registry fields; submit →
|
||||
`useSubmitCredentials`, lands on B6. Copy reflects **manual review**, never an automated authority check.
|
||||
- **B6 `review/page.tsx`** — the under-review resting screen: "در حال بررسی" + the 24–48h note + a
|
||||
**condensed mini-checklist** (reusing `StatusChip`) — a focused **second view of the same cached
|
||||
`status()` query**, not a second fetch. CTA back to B3.
|
||||
- **`verificationSteps.ts`** — the data-driven glue: `displaySteps` (prepends a synthetic passed **mobile**
|
||||
step), `progressCounts`, `stepLabelKey`/`stepDescriptionKey`, `stepStatusChip` (the green/amber/grey/red
|
||||
legend), `routeForStep`, `nextActionRoute`/`nextActionableIndex`.
|
||||
- **Journey stepper:** B4/B5/B6 reuse the shared `StepperHeader` (identity → credentials → review).
|
||||
|
||||
### Shared components (with co-located tests)
|
||||
- **`<DocumentUpload>`** — the reusable uploader: client type/size validation **before** upload, the full
|
||||
idle → uploading(%) → success(✓ + name / local image preview) → error(retry) machine, re-upload on
|
||||
reject, server-metadata as the "uploaded" truth, and a **local-capture mode** (B4). Its state chrome uses
|
||||
the `verification` namespace; the caller passes the field label/hint.
|
||||
- **`<TrustBadge state=…>`** — verified / unverified / expired off `--bal-*` tokens. Rendered on the nurse's
|
||||
own profile; **exported so f6 reuses it** in search results + the public profile.
|
||||
|
||||
### Wiring
|
||||
- **Publish gate** — `nurse/services/PublishGate.tsx` (in `MyServicesList`): the go-live CTA is **disabled
|
||||
with a blocked-until-verified explanation** (+ link to B3) until the aggregate is `approved`. Mirrors the
|
||||
server's guarded `is_verified` flip.
|
||||
- **Trust badge on the nurse profile** — sourced from the own `VerificationStatus` via `ownBadgeState`; the
|
||||
unverified banner now shows only until `verified`.
|
||||
- **6 AppIcons** (`upload/document/refresh/identity/license/publish`), **4 route constants**, the
|
||||
`verification` **i18n namespace (123 keys)** in both locales in sync.
|
||||
|
||||
## What is now testable, and exactly how
|
||||
Run `npm run dev`, sign in as a **nurse** (mock auth code `123456` if `USE_AUTH_MOCK`), open
|
||||
**/nurse/verification**:
|
||||
1. **Checklist:** B3 shows "X از Y" with the mobile step already green, the rest `not_started`. The continue
|
||||
CTA calls `start` (seeds steps) then routes to B4.
|
||||
2. **Identity (B4):** a valid کد ملی + card image (watch the uploader progress → ✓) + selfie → submit → the
|
||||
`identity_kyc` + `shahkar_match` steps update on B3 **without a refresh**. National-ID `0000000000` →
|
||||
failed KYC; national-ID `1111111111` → the non-accusatory **shared-SIM** Shahkar message.
|
||||
3. **Credentials (B5):** INO number + upload each manual document (→ `in_review`) + specialty chips → submit
|
||||
→ lands on **B6** ("در حال بررسی", 24–48h, mini-checklist).
|
||||
4. **Approval flips verified:** the **dev-only "Simulate admin review" panel** on B3/B6 (mock-flag-gated —
|
||||
stands in for the deferred f15 admin queue) → *Approve all* → B3 shows `approved`, the **trust badge**
|
||||
on the nurse profile shows **verified**, and the **publish CTA on /nurse/services is enabled**.
|
||||
5. **Rejected step:** *Reject a document* → the step shows its **rejection reason** + a working re-upload
|
||||
path; the publish CTA stays blocked.
|
||||
6. React Query Devtools shows **one `verification.status` query** feeding B3 + B6 and invalidating on each
|
||||
mutation. Toggle `/en`↔`/fa` (RTL) and dark mode — strings + layout flip.
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
`services/verification` runs behind **`verificationMockApi`** (`USE_VERIFICATION_MOCK = true`) — b6 isn't
|
||||
reachable in this environment, so the mock drives the full journey (identity/Shahkar/bank runs, document
|
||||
uploads, admin-decision sim). The **swap is one line** (`USE_VERIFICATION_MOCK = false`): `verificationClientApi`
|
||||
is wired to every b6 route (`nurse_verification/*`, `nurses/{id}/trust_badge`), same hook signatures + query
|
||||
keys, no call-site change. See the mocks-registry row for the deterministic test triggers. Vendor calls
|
||||
(KYC/Shahkar/credential/IBAN/object-storage) are mocked **server-side** by b6 — the front end consumes them
|
||||
as if real.
|
||||
|
||||
## Contracts consumed + gaps filed
|
||||
- **Consumed:** [`verification.md`](../../contracts/domains/verification.md) + `openapi/swagger.v1.json` (b6).
|
||||
- **Filed:** **REQ-011** — a nurse-facing endpoint for the structured credential details (INO number,
|
||||
specialties, license fields) B5 collects; the contract records these only on the admin `decide`, so the
|
||||
real `submitCredentialDetails` no-ops until it lands (the document uploads are contract-backed). Also
|
||||
flagged: `VerificationStepDto` has no `isRequired` (the client treats every seeded step as required).
|
||||
|
||||
## Follow-ups for later phases
|
||||
- **f6** reuses `<TrustBadge>` (import from `@/components`) on search results (C2) + the public nurse profile
|
||||
(C3), sourced from `useNurseTrustBadge(nurseId)` / `publicBadgeState`. The `expired` state is own-profile
|
||||
only (the public badge payload carries `isVerified` only).
|
||||
- **f12 payout** depends on the `bank_account_verification` step (this phase deep-links it to the f2 bank screen).
|
||||
- The **admin verification review queue** (pass/reject, doc viewer, credential entry) is **f15** — the
|
||||
dev-only mock sim here is a stand-in, gated on `USE_VERIFICATION_MOCK`.
|
||||
- Deliver **REQ-011** then wire `submitCredentialDetails` + flip `USE_VERIFICATION_MOCK=false`.
|
||||
@@ -17,8 +17,9 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
||||
| `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 | 🔴 |
|
||||
| `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 — 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 | 🟡 |
|
||||
@@ -42,7 +43,8 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
||||
| `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` | **owned by backend-phase-12**; thin local stub added in **b11** | BNPL revert/update — `MockBnplProvider` (`Baya.Infrastructure.CrossCutting/Seams/`), a **temporary stub so b11's `bnpl_revert` refund path runs before b12 merges**. `RevertAsync`/`UpdateAsync` succeed, echo a deterministic `external_revert_reference`, and report a **nullable** `provider_commission_reversed_amount` (null by default — reconciled from the response, never hardcoded). Registered singleton in `AddCrossCuttingSeams` | `Seams:Bnpl:ForceFailure` (default `false`), `Seams:Bnpl:ReverseProviderCommission` (default `false`) | **b12 owns the real seam definition + adapter** (SnappPay/Tara): settle flow, order lifecycle, real `RevertAsync`(full)/`UpdateAsync`(strictly-lower partial). When b12 lands its real registration supersedes this stub and the refund `bnpl_revert` path calls the real client unchanged | 🟡 (pre-b12 stub) |
|
||||
| `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; **b13** owns the real impl) | "Was the nurse already paid for this booking?" — `NursePayoutStatusService` (`Persistence/Services/Payments/`) derives it from the booking's `dispute_window_ends_at` close (the same gate b13 pays out on), with a `refund_assume_nurse_paid` config override. Not a mock of an external — a **temporary derivation** standing in for the b13 `nurse_payout_booking_links` lookup. Registered scoped in `AddPersistenceServices` | `refund_assume_nurse_paid` (`platform_configs`, default `false`) | In b13: implement `IsNursePaidForBookingAsync` as a real `nurse_payout_booking_links` join (a booking linked to a paid-out `nurse_payouts` batch ⇒ paid), swap the registration — the refund pre-payout/clawback fork is unchanged | 🟡 |
|
||||
|
||||
> Exact config keys and file paths get filled in by the phase that builds each seam. Keep the
|
||||
@@ -64,3 +66,4 @@ the frontend can build before the backend phase merges, and swap to the real HTT
|
||||
| `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 | 🟡 |
|
||||
|
||||
Reference in New Issue
Block a user