another step just a little remaining
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
# Balinyaar — Business Requirements
|
||||
|
||||
> **Purpose.** This document specifies the business requirements for **Balinyaar**, an MVP home-nursing marketplace in Iran where independent, individually-verified nurses list configurable services, families search and request a nurse, the nurse accepts, the family pays *through* the platform, the platform holds the money as an internal escrow ledger state, the nurse performs one or more visits with Electronic Visit Verification (EVV) check-in/out, and the platform pays the nurse weekly minus a commission. It is grounded in the verified payment/settlement research, the adversarial fact-checks, the database-model critiques, and the market/legal/verification research. It is written to be an MVP that is decisive but **not naive** about Iranian payment law, tax, and the realities of caring for vulnerable people at home. All monetary values are in **IRR (Rials)**; Toman is a display concern only and is converted to/from Rials solely at an external provider's API boundary.
|
||||
|
||||
**Date:** 2026-06-20
|
||||
|
||||
---
|
||||
|
||||
## How to read this document
|
||||
|
||||
Each section covers one business area and states, in order:
|
||||
|
||||
- **(a) Business requirements** — what the platform must do.
|
||||
- **(b) Iran-specific considerations** — the local legal, fiscal, cultural, and infrastructural realities that shape the requirement.
|
||||
- **(c) MVP vs DEFERRED** — an explicit callout of what ships at launch and what is intentionally postponed.
|
||||
- **(d) Supporting database entities** — the entities (using the final names from the refined data model) that implement the requirement.
|
||||
|
||||
**Cross-cutting ground truths (true in every section):**
|
||||
|
||||
1. **Balinyaar cannot legally custody customer cash.** Under Iranian rules a پرداختیار (payment facilitator) is forbidden from holding deposits, running wallets, or moving money between merchants. Money always flows card → licensed PSP → Shaparak settlement → **bank-registered IBANs**. "Escrow" is therefore an **internal ledger state** over funds custodied at a licensed provider/partner bank — never a Balinyaar-owned cash balance. (`ledger_entries`)
|
||||
2. **VAT is 10%**, not 9% — it rose from 9% to 10% in 1403 (7% government + 3% municipal) and is treated as a configurable rate.
|
||||
3. **BNPL is full-upfront.** A BNPL provider settles **one full-amount lump (net of its commission) to the merchant-of-record**, bears 100% of customer-default risk, and owns the customer's installment repayment entirely. A BNPL order behaves in Balinyaar's books exactly like a card payment landing net-of-fee. (`bnpl_transactions`)
|
||||
4. **The nurse is paid by Balinyaar, weekly, on Balinyaar's own schedule** — gated on EVV completion and a closed dispute window — regardless of how the family paid.
|
||||
|
||||
---
|
||||
|
||||
## 1. Actors & Onboarding
|
||||
|
||||
### (a) Business requirements
|
||||
- Three actor types: **customer** (the family member / payer), **nurse** (the independent caregiver / seller), and **admin** (Balinyaar back-office staff: support, finance, moderation, super-admin).
|
||||
- **Phone number is the primary login credential.** Authentication is **phone-OTP** (one-time code by SMS). Email is optional/secondary (required only for admin accounts).
|
||||
- The **patient** (care recipient) is a first-class entity distinct from the customer, because the payer (an adult child, a spouse) is frequently not the patient (an elderly parent, a newborn, a post-surgical adult). A customer may register multiple patients.
|
||||
- **KYC timing is role- and risk-staged, not up-front-for-everyone:**
|
||||
- A **customer** can register and browse with only a verified phone (OTP). National-ID KYC for customers is anti-fraud only and is **deferred** at launch.
|
||||
- A **nurse** must complete the full verification pipeline (Section 2) before any of their service variants become bookable. `national_id` is populated only after the identity step passes.
|
||||
- An **admin** is provisioned internally with RBAC roles.
|
||||
- Each successful login creates a refresh-token session that can be revoked (logout, stolen-token detection).
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- Phone-OTP is the dominant Iranian login norm and is also the anchor for **Shahkar** SIM↔national-ID binding (Section 2).
|
||||
- Storing `national_id` only post-KYC matches the reality that identity is verified through gated vendor APIs, not collected casually at signup.
|
||||
- Cultural reality: the booking flow must let a family member act on behalf of a patient who cannot self-advocate (infant, dementia, post-anesthesia). The customer/patient split is essential, not cosmetic.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** phone-OTP login; customer/nurse/admin roles; customer→patient (1:N); session management; admin RBAC; nurse onboarding gated on verification.
|
||||
- **DEFERRED:** customer national-ID KYC (`customer_profiles.national_id_verified_at` exists but is optional/unused at launch); push notifications; social login; nursing-company (organization) self-onboarding.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`users`, `user_sessions`, `roles`, `user_roles`, `nurse_profiles`, `customer_profiles`, `patients`, `customer_addresses`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Nurse Verification & Credentials
|
||||
|
||||
### (a) Business requirements
|
||||
Verified trust is the **entire brand**. Vetting is **platform-owned, non-optional, and performed at the authoritative source** — never delegated to families, and never marketed as a check the platform does not actually perform. A nurse is bookable only after all *required* verification steps pass.
|
||||
|
||||
The pipeline is **data-driven**: the set of steps lives as rows in `verification_step_types` (not a code enum), so a new regulatory requirement (e.g., professional liability insurance) is one INSERT, not a migration. Each step can be **automated** (a KYC vendor API call) or **manual** (admin reviews an uploaded document). The aggregate `nurse_verifications` record rolls the step outcomes into a single status; `nurse_profiles.is_verified` flips to true **only inside the same transaction** that confirms every required step is `passed`.
|
||||
|
||||
The verification steps:
|
||||
|
||||
1. **Identity (KYC) — automated.** Match person ↔ کد ملی (national ID) ↔ phone ↔ face via one Iranian KYC vendor: national-ID validity/name match + photo/video **liveness** against the national-card / civil-registry (ثبت احوال) photo. Binds the profile to a real identity and a liveness selfie to defeat the stolen-identity / alias fraud pattern.
|
||||
2. **Shahkar phone↔national-id binding — automated.** Confirm the login SIM is registered to the nurse's own کد ملی. The binding result (when, which vendor, the reference) is recorded, and **re-verification is triggered on phone change**. The shared-SIM failure mode (a SIM owned by a family member) is an explicit, handled state, not an undefined edge case.
|
||||
3. **MoH پروانه صلاحیت حرفهای (professional-competency license) — the single most important credential.** It is the MoH-mandated license for in-home nursing and **already bundles the criminal-record (سوء پیشینه) screen** plus scientific/ethical/health vetting. Verified against the MoH source (Rn.behdasht.gov.ir). No public B2B API exists, so the realistic method at launch is **nurse-uploaded document + manual admin verification against the official record**.
|
||||
4. **نظام پرستاری (Iranian Nursing Organization / INO) membership — cross-check.** The INO membership number is captured and cross-checked (ino.ir) as a second source. Manual at launch.
|
||||
5. **عدم سوء پیشینه (criminal-record certificate).** Consent-gated to the individual (obtained by the nurse via adliran.ir / their own ثنا password); **no company/employer API exists**. The nurse uploads it; it is **time-limited** — on expiry the step reverts to pending and a support alert is raised. Partly covered already by credential #3.
|
||||
6. **IBAN ownership verification.** The payout IBAN (Sheba) must be proven to belong to the verified nurse — the account-holder national ID must equal the verified nurse national ID. Done via automated IBAN-ownership inquiry (استعلام شبا) where available, gating the **first payout**, not merely an admin eyeballing the number. Prevents paying a nurse's earnings into a third party's account (money-mule risk).
|
||||
|
||||
**Structured credential registry.** Beyond opaque uploaded files, the actual license **numbers**, issuing authority, holder-name-as-printed, and issue/expiry dates are stored as typed, queryable rows in `nurse_credentials`. This powers renewal/expiry alerts, the public "verified" trust badge, cross-checking against official portals, and audit defensibility — and survives the future arrival of an MoH/INO API.
|
||||
|
||||
**Continuous monitoring**, not one-and-done: license validity and the criminal-record certificate are periodically re-verified; Shahkar is re-run on phone change. Expiring credentials raise `support_alerts`.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- The license layer is **fragmented across regulators** (MoH vs INO) and has **no public B2B API** — manual verification against the official portal is the realistic MVP method; the structured registry makes that defensible and renewable.
|
||||
- The criminal-record check is **consent-gated to the person** and cannot be pulled by a company — hence nurse-uploaded + re-requested periodically, leaning on the MoH license which already embeds it.
|
||||
- Identity (Shahkar, liveness, national-ID match) is the **easy** layer because a competitive market of Iranian e-KYC vendors (Finnotech, U-ID, Jibbit, Farashensa, Verify, Kavoshak) already holds the regulator-gated upstream agreements. **Buy this, don't build it.**
|
||||
- Document forgery is the documented attack (the "imposter nurse" pattern): verify at source, bind to national ID + liveness, never trust an uploaded PDF alone.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** all six steps; data-driven `verification_step_types`; structured `nurse_credentials` registry; manual MoH/INO verification; nurse-uploaded عدم سوء پیشینه with expiry; automated identity + Shahkar + IBAN-ownership via one KYC vendor; expiry-driven re-verification alerts; transactional `is_verified`.
|
||||
- **DEFERRED:** automated MoH/INO license lookup (pending a B2B API); ML-driven fraud scoring (`fraud_flags` is modeled but inactive); professional-liability-insurance step (addable as a row when required).
|
||||
|
||||
### (d) Supporting database entities
|
||||
`nurse_verifications`, `verification_step_types`, `verification_steps`, `verification_documents`, **`nurse_credentials`** (structured license registry), `nurse_bank_accounts` (IBAN ownership), `support_alerts` (expiry/renewal), `audit_logs`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Service Catalog & Pricing
|
||||
|
||||
### (a) Business requirements
|
||||
- **Admin defines the catalog skeleton:** top-level **service categories** (e.g., مراقبت از سالمند / Elderly Care, مراقبت پس از جراحی / Post-Surgery Recovery, مراقبت از نوزاد / Infant Care, مدیریت بیماری مزمن / Chronic Illness Management) and **configurable option dimensions** as admin-managed **option groups** (e.g., تعداد بیمار / patient count, نوع شیفت / shift type) each with concrete **option values** (e.g., ۱ نفر, ۲ نفر, شبانهروزی). Admin can add new dimensions without a schema change.
|
||||
- **Each nurse defines their own offerings as variants.** A **variant** is the atomic bookable unit: a category + a chosen combination of option values + the nurse's **own price** and **price unit**. A nurse may have many variants per category, one per combination they choose to offer and price independently.
|
||||
- **Price units** must support the real shapes of home nursing: `per_hour`, `per_session`, `per_half_day`, `per_day`, and `per_24h` (شبانهروزی / live-in). For hourly variants an estimated duration helps the customer estimate total cost.
|
||||
- The variant `display_name` auto-generates from option labels but is nurse-editable. Nurses can deactivate (not delete) a variant; deactivated variants cannot be booked.
|
||||
- Catalog and prices are **snapshotted onto the booking** at booking time (`variant_snapshot_json`) so historical records survive later edits.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- Iranian competitors sell exactly these shapes — hourly / daily / 24-hour (شبانهروزی) shifts and multi-day packages — so `per_24h` and `per_day` are first-class, not edge cases.
|
||||
- Competitor pricing is opaque and "توافقی" (negotiable); **transparent, upfront, nurse-set pricing is a deliberate differentiator** families value.
|
||||
- All catalog tables carry `name_fa` / `name_en` pairs (Persian primary).
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** admin categories + option groups/values; nurse variants with own price + price unit across all five units; activate/deactivate; snapshotting.
|
||||
- **DEFERRED:** holiday/surge pricing rules; a lighter "companionship / daily-living" tier (modeled as a future category); dynamic/tiered commission per category.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`service_categories`, `service_option_groups`, `service_option_values`, `nurse_service_variants` (carries `price`, `price_unit`), `nurse_service_variant_options`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Search & Matching
|
||||
|
||||
### (a) Business requirements
|
||||
- Families search by **service category**, **geography** (city, and optionally district), price, and availability, with results sortable by rating.
|
||||
- **Geography** is driven by nurse-declared **service areas**: a nurse covers one or more cities, optionally specific districts; a city-level row (no district) means the whole city.
|
||||
- **Search must be cheap from day one.** The naive query joins nurse profile (verified + accepting) → variants (category/price) → variant options → service areas → rating across 4+ tables. Instead a **denormalized `nurse_search_index`** holds one flat row per active, bookable variant with all search-relevant fields, maintained on write. A row exists **only** when the nurse is `is_verified` and not suspended and the variant `is_active`. This is far cheaper than introducing Elasticsearch at MVP stage.
|
||||
- **Same-gender caregiver matching** is a first-class filter and a near-hard requirement: in Iranian bodily-care (bathing, toileting, intimate post-surgical care) same-gender caregiving is culturally decisive, not optional. The customer specifies a required caregiver gender on the booking request (`required_caregiver_gender`), and nurse gender is an exposed search filter so families can narrow to same-gender caregivers up front. The patient's gender (`patients.gender`) and the nurse's gender support this matching.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- District granularity varies: in Tehran, districts map to the 22 official municipal مناطق; in smaller cities they are major neighborhoods. Districts are optional.
|
||||
- **Same-gender matching is the single most Iran-specific matching constraint** — every real elder/post-surgical bodily-care request implies it. It must be surfaced before booking, not discovered after.
|
||||
- White-space opportunity: incumbents concentrate ~99% in Tehran/Karaj; the search/area model must work for under-served second-tier cities (Mashhad, Isfahan, Shiraz, Tabriz, Ahvaz, Qom).
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** category + city/district geo search; `nurse_search_index` denormalization; same-gender filter via `required_caregiver_gender`; rating sort.
|
||||
- **DEFERRED:** map-based discovery; availability-window filtering as a hard constraint (availability slots are soft guidance at launch); algorithmic ranking beyond rating; continuity-of-carer "preferred nurse" suggestions.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`nurse_service_areas`, `cities`, `districts`, **`nurse_search_index`**, `nurse_service_variants`, `nurse_profiles` (rating, gender via `users`), `patients.gender`; `booking_requests.required_caregiver_gender` (the requested constraint).
|
||||
|
||||
---
|
||||
|
||||
## 5. Booking & Scheduling
|
||||
|
||||
### (a) Business requirements
|
||||
The lifecycle has two phases separated into two tables so each table's invariants stay clean: a **request phase** (no money) and a **booking phase** (always implies captured payment).
|
||||
|
||||
**Request → accept → pay → confirm lifecycle:**
|
||||
1. Customer submits a **booking request** (nurse, patient, variant, address, date/time, requested caregiver gender, customer notes). Status `pending_nurse_response`.
|
||||
2. The nurse must respond before a **response deadline** (`nurse_response_deadline_at`, computed from config and frozen on the request). The nurse **accepts** → `accepted_awaiting_payment`, or **rejects** → `rejected_by_nurse`, or the deadline passes → `expired_no_response`.
|
||||
3. On accept, a **30-minute payment window** opens (`payment_deadline_at`). The customer pays within it → a `bookings` row is created (`confirmed`). If the window lapses → `payment_deadline_expired`.
|
||||
|
||||
**Single-visit AND multi-session / long-duration engagements must both be representable.** Home nursing is frequently multi-visit: post-surgery daily visits for ten days, month-long nightly or شبانهروزی (24h live-in) care. A booking therefore carries a `session_count` and owns **N `booking_sessions`** (one row per scheduled visit), each with its own schedule, its own EVV check-in/out, and its own payout eligibility. A single EVV per booking cannot represent a multi-day engagement, so the engagement-to-session split is the core scheduling model.
|
||||
|
||||
**Booking lifecycle:** `pending_payment` → `confirmed` (payment captured) → `in_progress` (first/relevant session check-in) → `completed` (sessions checked out) → optionally `disputed` → `closed`; or `cancelled` before service. Allowed transitions are guarded explicitly so the booking and EVV state machines cannot silently contradict.
|
||||
|
||||
**Snapshots:** `variant_snapshot_json` and `address_snapshot_json` freeze the service and address at booking time.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- Multi-session and شبانهروزی live-in care is the **dominant** elder-care shape in Iran, not a niche — modeling only single visits would fail to represent demand.
|
||||
- Heavy platform control over multi-visit scheduling **strengthens a worker-misclassification argument** under labor law; this is flagged for counsel, and the platform deliberately keeps the nurse's accept/reject autonomy per request.
|
||||
- Availability slots/exceptions are **soft guidance only** (informing search), not hard blocks — the nurse still individually accepts or rejects each request, which also fits the Shamsi week and holiday rhythm.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** request→accept→pay→confirm lifecycle with response deadline + 30-min payment window; single-visit bookings; `booking_sessions` for multi-session/long-duration engagements with per-session EVV and payout; explicit status-transition guards; snapshots; soft availability slots/exceptions.
|
||||
- **DEFERRED:** open-ended recurring schedules (`recurring_booking_schedules` modeled, inactive — launch is all finite engagements); milestone/progress-payment UX beyond per-session accrual; hard availability-based booking blocks.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`booking_requests` (carries `nurse_response_deadline_at`, `payment_deadline_at`, `required_caregiver_gender`), `bookings` (carries `session_count`, `dispute_window_ends_at`, fee split), **`booking_sessions`**, `booking_care_instructions`, `nurse_availability_slots`, `nurse_availability_exceptions`, `nurse_service_variants`, `patients`, `customer_addresses`.
|
||||
|
||||
---
|
||||
|
||||
## 6. EVV / Service Delivery
|
||||
|
||||
### (a) Business requirements
|
||||
- **Electronic Visit Verification (EVV)** is the authoritative record that a visit actually happened, for how long, and where. The nurse **clocks in and out via the app per session**, capturing GPS coordinates and timestamps.
|
||||
- An **address-match tolerance** check computes whether the nurse's GPS at check-in falls within an acceptable radius of the booking address (`evv_location_tolerance_meters`). A mismatch is **advisory** — it raises a support alert / review flag but does **not** auto-cancel; it does not silently block the visit.
|
||||
- If the nurse has not checked in by a configurable threshold after the scheduled start, a **no-show / late support alert** is created and the family is notified.
|
||||
- **Payout is gated on EVV completion.** A session/booking becomes payout-eligible only after EVV check-out **and** the dispute window has closed (Section 10). EVV completion is the trigger that lets the booking enter the next weekly payout batch; for a multi-session engagement, payout accrues per completed session.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- EVV is the core operational-trust mitigation for **unobserved in-home care** of vulnerable patients who often cannot reliably report what happened (infants, dementia, post-anesthesia) — the platform compensates for unobservability with structured proof of service.
|
||||
- Releasing escrow against proof of service is also a financial-correctness requirement under the Iranian "hold then pay weekly" model — the platform must not pay a nurse for a visit that has no EVV evidence.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** per-session GPS check-in/out, timestamps, address-match tolerance flag, no-show alerting, payout gated on EVV completion + closed dispute window.
|
||||
- **DEFERRED:** continuous geofencing during a live-in shift; supervisory tele-check-ins; family-visible live care logs; consented in-home cameras.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`visit_verifications` (per session, with check-in/out GPS, timestamps, `check_in_address_match`, status), `booking_sessions`, `support_alerts` (no-show / location-mismatch), `platform_configs` (`evv_location_tolerance_meters`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Cancellation & Refunds
|
||||
|
||||
### (a) Business requirements
|
||||
- Cancellation/refund rules are **tiered and structured**, not a single blunt "default 100%". The platform defines `cancellation_policies` tiers by **lead time** and **initiating actor**:
|
||||
- **Free** cancellation more than 24h before start.
|
||||
- **Partial** refund (e.g., 50%) under 24h.
|
||||
- **Customer no-show:** up to 100% charge.
|
||||
- **Nurse no-show:** full refund to the customer **and** a penalty/forfeiture for the nurse.
|
||||
- The **applicable policy is snapshotted onto the booking** at booking time (mirroring the per-booking fee-rate snapshot), so later policy edits never rewrite history. The **resolved** cancellation fee / refund percentage is recorded on the refund event.
|
||||
- For multi-session engagements, **cancellation is per remaining session:** cancelling mid-engagement refunds only the un-started sessions, while completed-and-verified sessions remain payout-eligible.
|
||||
- **Refunds are admin-only** — there is no customer self-service refund. A refund is initiated by an admin and **must be linked to a support ticket** (`tickets`) that holds the conversation and dispute evidence.
|
||||
- A refund **decomposes across the two fee legs** — how much of the platform commission and how much of the nurse payout is being reversed — because the booking gross is `platform commission + nurse payout`.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- A flat percentage is too blunt for شبانهروزی live-in engagements and Iranian holiday-period bookings; tiered, snapshotted policy reduces dispute load.
|
||||
- **The refund money path depends on whether the nurse has already been paid** (Section 8/10): pre-payout it is a clean reversal; post-payout it becomes a platform-funded refund plus a nurse clawback, because an Iranian bank transfer to a nurse's IBAN is effectively irreversible.
|
||||
- For BNPL bookings, the refund **never** goes nurse→customer or Balinyaar→customer directly — it is initiated through the BNPL provider's revert/cancel API (Section 8/9).
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** tiered `cancellation_policies`; per-booking policy snapshot; admin-only, ticket-linked refunds; per-session cancellation for engagements; nurse-no-show vs customer-no-show handling; fee-leg decomposition on refunds.
|
||||
- **DEFERRED:** automated nurse no-show penalty (manual admin action at launch); self-service partial-refund UI; holiday-specific cancellation overrides.
|
||||
|
||||
### (d) Supporting database entities
|
||||
**`cancellation_policies`**, `bookings` (policy snapshot, `dispute_window_ends_at`), `refunds` (admin-only, `ticket_id`, fee-leg decomposition, `refund_channel`), `tickets`, `nurse_clawbacks` (post-payout case), `ledger_entries`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Payments & Escrow
|
||||
|
||||
### (a) Business requirements
|
||||
- The family pays the **gross** booking price **through the platform** by card via a licensed PSP's IPG. The platform is the **merchant-of-record**; the payment lands net of provider/Shaparak fees.
|
||||
- **Escrow is an internal ledger state, not platform-held cash.** The platform models money state with a minimal **double-entry `ledger_entries`** ledger: each money event posts **balanced** legs grouped by a transaction group. Account types: `escrow_held`, `platform_revenue`, `nurse_payable`, `refund_payable`, `bnpl_fee_expense`, `nurse_clawback_receivable`. The ledger is the **single source of truth** for "how much is held," "how much do we owe nurses now," and "what is our commission income" — replacing fragile inference from scattered status booleans.
|
||||
- On a successful card payment: debit `escrow_held` (gross), credit `platform_revenue` (Balinyaar commission), credit `nurse_payable` (nurse payout).
|
||||
- **Settlement-sharing (تسهیم).** The compliant marketplace primitive is splitting one incoming card payment across multiple **registered IBANs** (the nurse's share and the platform's commission) at settlement, performed by Shaparak/the provider — the platform never touches the actual split. The internal ledger mirrors this split; the per-booking fee snapshot freezes it.
|
||||
- **Per-booking the three amounts are stored separately and never conflated:** `gross_price_irr` (what the customer is charged), `balinyaar_commission_irr` (platform's cut — drives the nurse payout), and (for BNPL) `bnpl_commission_irr` (the provider's merchant discount — a platform expense). `nurse_payout_amount = gross_price_irr − balinyaar_commission_irr`.
|
||||
- **Webhook idempotency is mandatory before money moves.** Every PSP/BNPL callback is stored raw and **deduplicated by a unique external event id** in `payment_webhook_events` before any money state mutates — preventing double-confirmed bookings and double-settlements from at-least-once, retried callbacks.
|
||||
- **Payment uniqueness:** at most one `succeeded` payment transaction per booking, and the Shaparak reference is unique — enforced so a retried success webhook cannot double-confirm.
|
||||
- **Multi-provider failover.** Provider settlement cut-offs are a real continuity risk (the Toman/Jibit Nov-2024 suspensions cut businesses off mid-cycle). The payment layer abstracts the provider behind configuration so a blocked provider can be swapped, and the reconciliation ledger survives a provider being cut off.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- **The load-bearing legal constraint:** a پرداختیار may **not** hold customer deposits, run wallets, or move money between merchants; the Shaparak ban on inter-merchant/inter-facilitator transfers means the "delay the تسهیم and redistribute later from a platform pool" pattern is regulatory grey-to-prohibited. The compliant posture is: collect via the provider, model escrow as an **internal ledger over funds custodied at the licensed provider/partner bank**, and pay out by provider-side settlement to **verified, registered nurse IBANs**. A bank-grade escrow product (e.g., Vandar میندو / معاملات امن) is the only true hold/release/refund mechanism, and its EVV-triggered hold is unverified — so the platform never assumes it can lawfully custody the cash itself.
|
||||
- **PSP received ≠ cash in bank.** Iranian PAYA settlement is cyclic (T+0/T+1, holiday-deferred), so the ledger separates a clearing/receivable state from settled cash, making bank reconciliation possible.
|
||||
- Toman/PSP units differ from internal Rials; convert only at the API boundary. Amounts are BIGINT IRR internally to avoid float/rounding bugs.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** card payment via one licensed PSP; internal double-entry `ledger_entries` escrow; per-booking three-way amount split; تسهیم-style commission/nurse-share modeling; `payment_webhook_events` idempotency; single-succeeded-transaction-per-booking guard; provider abstraction for failover.
|
||||
- **DEFERRED:** a nurse-facing wallet with on-demand withdrawal (facilitator wallet prohibition risk); multiple simultaneous live PSPs at launch (abstraction is built, second provider added later); bank-grade EVV-triggered escrow product integration.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`payment_gateways`, `payment_transactions` (unique Shaparak ref, single-succeeded-per-booking), **`payment_webhook_events`**, **`ledger_entries`**, `bookings` (`gross_price_irr`, `balinyaar_commission_irr`, `platform_fee_rate`, `nurse_payout_amount`), `refunds`, `nurse_bank_accounts` (verified registered IBANs).
|
||||
|
||||
---
|
||||
|
||||
## 9. Installments / BNPL
|
||||
|
||||
### (a) Business requirements
|
||||
- BNPL is offered as an alternative checkout. The decisive, verified model is **full-upfront settlement**: on approval the BNPL provider pays Balinyaar the **full booking amount in one lump, net of the provider's merchant commission**, and **bears 100% of customer-default risk**. The customer's interest-free installment repayment (typically a 4-installment plan) is **owned entirely by the provider** and is **decoupled** from Balinyaar's escrow/EVV/payout cycle.
|
||||
- **Therefore a BNPL order is, in Balinyaar's books, identical to a card payment that lands net-of-fee in one inbound settlement.** Balinyaar **does NOT track customer installments, per-installment webhooks, or default propagation** — that fragile subsystem is intentionally not built.
|
||||
- A BNPL order is recorded once as a single inbound settlement in `bnpl_transactions` (1:1 with a payment transaction), capturing the provider, the merchant-of-record (Balinyaar), the external payment token / transaction id, `order_amount_irr`, `settled_amount_irr` (net of provider commission), `bnpl_commission_irr`, currency (converted at the boundary), an idempotent status state-machine (`eligible`/`token_issued`/`verified`/`settled`/`reverted`/`cancelled`/`failed`), `installment_count` (informational, default 4), `settled_at`, and the revert fields.
|
||||
- **BNPL refunds flow only customer ↔ provider ↔ Balinyaar** — never nurse→customer or Balinyaar→customer directly. Balinyaar initiates the reversal via the provider's revert (full) / cancel/update (partial, new amount strictly lower) API using the stored token; the provider cancels the customer's unpaid installments, restores their credit, and refunds any already-paid installment to the customer's bank in ~7–10 business days (asynchronous, owned by the provider). The refund still decomposes across the platform-fee and nurse-payout legs in Balinyaar's ledger.
|
||||
- **The nurse's payout is unchanged by BNPL:** computed from `gross_price_irr − balinyaar_commission_irr`, paid weekly after EVV + dispute window — the provider's commission is a **platform cost of accepting BNPL** and is **never** passed through to the nurse.
|
||||
|
||||
> **This is a summary. Deep BNPL provider mechanics, the exact revert/cancel/settle API flows, commission-as-config, settlement-timing nuances, and provider-specific behavior are specified in `payments-and-installments.md` — cross-reference it for implementation detail.**
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- Provider-financed Iranian BNPLs (SnappPay, Digipay, Tara, Torob Pay) are uniformly **full-upfront, provider-bears-risk, interest-free-to-customer**; only bank-financed POS loans (Lendo) charge the customer interest and are a poor fit for short, cancellable nursing visits.
|
||||
- **Settlement timing is contract-defined and may be gated on the customer's first installment** (daily / T+1-3 / weekly / 15-day) — "full amount" does not mean "instant cash." Timing is config + a per-transaction `settled_at`; weekly nurse payout may key off settlement actually received, never an assumption.
|
||||
- **Commission rate is per-contract and not public** (anecdotal 7–15% for SnappPay; Torob Pay's published 6.6%) — always a config field read from the actual settlement, never hardcoded.
|
||||
- Onboarding requires جواز کسب **and** اینماد for the Balinyaar/partner entity, and whether a multi-vendor re-disbursing marketplace qualifies as a single BNPL merchant is publicly undocumented — an ops/contracting task, not a schema dependency.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** full-upfront BNPL via one provider modeled as a single inbound settlement (`bnpl_transactions`); provider-mediated revert/cancel refunds; nurse payout decoupled from BNPL; commission + settlement timing as config.
|
||||
- **DEFERRED:** customer installment tracking (`installment_entries` — **cut**, owned by the provider); tranched settlement (`bnpl_settlement_entries` modeled-only, added if a future provider tranches); multiple BNPL providers.
|
||||
|
||||
### (d) Supporting database entities
|
||||
**`bnpl_transactions`** (replaces the old `installment_plans`; the old `installment_entries` is cut), `payment_transactions`, `payment_webhook_events`, `refunds` (`refund_channel = 'bnpl_revert'`, `external_revert_reference`, `expected_customer_refund_eta`), `ledger_entries`. See `payments-and-installments.md`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Payouts to Nurses
|
||||
|
||||
### (a) Business requirements
|
||||
- Nurses are paid in **weekly batches**. A batch aggregates the amounts owed for completed, payout-eligible bookings/sessions and produces one payout per nurse with earnings in that window.
|
||||
- **Payout eligibility is gated on EVV completion AND a closed dispute window.** A booking/session enters a batch only when `status = 'completed'` AND `dispute_window_ends_at < now()` (the dispute window is config-driven, default 72h post-completion). This deliberately prevents paying a nurse before a dispute can surface, shrinking clawback frequency — important because an Iranian bank transfer, once sent, is effectively irreversible.
|
||||
- The nurse payout amount derives from `gross_price_irr − balinyaar_commission_irr` (never from a BNPL provider's net settlement).
|
||||
- **Clawbacks** (`nurse_clawbacks`) handle the refund-after-payout case: if a booking is refunded/disputed **after** the nurse was already paid, a clawback receivable is recorded (negative ledger entry against the nurse) and recovered by **netting against the nurse's next weekly batch**, or written off if uncollectable. The nurse's payable balance is **derived from the ledger** (it may go negative), and a batch can net prior clawbacks (`gross_earnings`, `clawback_applied`, `net_amount`).
|
||||
- **Each booking is paid at most once** (the payout↔booking link is unique), preventing double-pay across batches.
|
||||
- **Bank-holiday-aware scheduling.** Payout period-end and processing dates are shifted off bank-closed days using a shared `iranian_holidays` calendar — a weekly payout landing on a multi-day Nowruz closure would otherwise fail, since PAYA/SATNA transfers do not settle on closed days.
|
||||
- Payouts go to the nurse's **verified, registered primary IBAN**, with the IBAN snapshotted and a transfer reference stored for reconciliation. Each payout item carries a unique track id + (for batches) a batch id.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- Payouts are **real bank transfers to registered IBANs** (PAYA/SATNA cycles, next-business-day on holidays) — there is no chargeback-style reversal, which is *why* the dispute window must close before payout and why clawback is a netting/receivable mechanism rather than an automatic reversal.
|
||||
- Provider settlement cut-offs (Toman/Jibit) mean payout must tolerate a provider being unavailable mid-cycle; the batch + reconciliation references survive a swap.
|
||||
- Each nurse must have a Shahkar/KYC-verified, IBAN-ownership-checked account registered as a beneficiary before any payout targets it.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** weekly batches; EVV + dispute-window gating; per-session accrual for engagements; `nurse_clawbacks` with next-batch netting and write-off; unique booking↔payout link; `iranian_holidays`-aware scheduling; verified-IBAN payouts with reconciliation references.
|
||||
- **DEFERRED:** on-demand / instant nurse withdrawal; per-nurse configurable payout frequency; automated clawback recovery beyond netting.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`nurse_payout_batches`, `nurse_payouts` (with `gross_earnings_irr`, `clawback_applied_irr`, `net_amount_irr`, `iban_snapshot`), `nurse_payout_booking_links` (unique per booking), **`nurse_clawbacks`**, `ledger_entries`, **`iranian_holidays`**, `bookings.dispute_window_ends_at`, `nurse_bank_accounts`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Reviews, Trust & Safety
|
||||
|
||||
### (a) Business requirements
|
||||
- A customer can leave **one review per completed booking** (rating 1–5 + free text), tied to a verified, completed, on-platform booking.
|
||||
- **Moderation:** reviews enter `pending_moderation` and are not public until approved by an admin (or an AI moderator). Aggregate nurse rating/counts are recomputed on **every** review status transition — publish, **hide**, reject, unpublish — so hiding a 1-star review never leaves a stale, inflated average.
|
||||
- **Low-rating alerting:** a rating at or below a configurable threshold (default ≤ 2) with negative content automatically raises a `support_alerts` row for the support team to investigate.
|
||||
- **Incident handling:** rapid-response protocols with immediate suspension on credible complaints; structured family check-ins and easy in-app concern flagging (the patient is not the sole information source); high-acuity cases routed only to appropriately verified nurses.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- The buyers are **vulnerable people** cared for **unobserved at home**; a single incident can destroy a fragile, trust-first brand — so moderation, low-rating alerting, and immediate suspension are core, not optional.
|
||||
- Verified-trust is the brand; reviews must be bound to real completed bookings to resist fake-review fraud (gig-marketplace fraud is ~2× elsewhere, mostly impersonation).
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** one-per-completed-booking customer reviews; moderation with full recompute-on-every-transition; low-rating `support_alerts`; manual incident suspension.
|
||||
- **DEFERRED:** two-way (nurse-reviews-customer) double-blind reviews with timed reveal; structured review-tag aggregation (`review_tags_master` / `review_tag_links` modeled but a phase-2 nicety); a dedicated `incidents` entity; ML fraud scoring.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`reviews` (moderation status, recompute triggers), `review_tags_master`, `review_tag_links`, `support_alerts` (low-rating, fraud-signal), `nurse_profiles` (denormalized aggregates), `audit_logs`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Messaging & On-Site Emergencies
|
||||
|
||||
### (a) Business requirements
|
||||
- **There is no live chat and no direct nurse↔customer messaging.** All post-booking communication runs through a structured **ticket** system that admin can read in full. This is a deliberate **anti-disintermediation** and **patient-safety** design: it protects vulnerable patients, creates a dispute paper trail, and prevents families and nurses pairing off-platform.
|
||||
- A **booking-scoped coordination ticket** is auto-created so the nurse and customer can coordinate logistics (arrival time, room location) under admin visibility. Internal admin-only notes are supported and never shown to users.
|
||||
- Tickets also carry refund conversations and any support request, and are the mandatory anchor for admin refunds (Section 7).
|
||||
- **On-site emergency playbook.** The ticket system is async and has no real-time channel, so the operational playbook is explicit: **in an emergency (no answer at the door, a medical emergency), the nurse calls the emergency-contact number surfaced in the app, then opens a ticket.** The emergency contact number is surfaced prominently in the booking UI (drawn from encrypted care instructions), so a nurse never needs to find the family's number by other means (which would break the platform's communication control).
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- Disintermediation is the predictable failure mode of recurring, relationship-based care; the ticket-only model retains value (escrow, dispute protection, backup coverage, insurance that only applies on-platform) instead of relying on punitive lock-in.
|
||||
- For unobserved in-home care of patients who cannot self-report, the controlled-but-auditable communication channel plus a clear emergency escalation path is a safety requirement.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** ticket-only messaging (admin-readable); auto-created booking-coordination ticket; internal notes; prominent in-app emergency contact + documented playbook.
|
||||
- **DEFERRED:** real-time chat; a first-class `incidents`/emergency-event entity with SLA; push/real-time alerting.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`tickets`, `ticket_participants`, `ticket_messages`, `booking_care_instructions` (encrypted emergency contact), `support_alerts`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Tax, Invoicing & Legal
|
||||
|
||||
### (a) Business requirements
|
||||
- **The nurse is the taxable seller of the nursing service; Balinyaar is the taxable seller only of its commission.** This mirrors the Snapp/Tapsi sharing-economy precedent: the nurse's fee is the nurse's income (the nurse files their own income tax — out of Balinyaar's scope), and Balinyaar's commission is the company's VAT-relevant revenue.
|
||||
- **VAT is 10%** (configurable), applied to Balinyaar's commission line. The home-nursing **service's** own VAT treatment is **unconfirmed** (medical services are commonly exempt) — so the VAT field is config-driven and can be 0/exempt, keeping the model correct whichever way the ruling lands. Confirm with an Iranian tax advisor before launch.
|
||||
- **سامانه مودیان (taxpayer system) readiness, minimal footprint.** The platform produces a minimal `invoices` record per booking capturing the gross, the platform commission, any BNPL commission, VAT, and a place for the مودیان reference fields (22-digit fiscal number, memory tax id, status) and PDF. The seller issues the invoice (the buyer cannot), so Balinyaar issues only its own **commission** invoice; it does not issue the nurse's service invoice.
|
||||
- **e-namad (نماد اعتماد الکترونیکی)** is de-facto mandatory: a monetized Iranian site needs e-namad to obtain an online payment gateway from PSP/Shaparak. It is held by the legal launch entity.
|
||||
- **Partner licensed-center (Asanism-style) as the launch legal vehicle.** Home nursing is a **licensed healthcare activity** (MoH establishment permit پروانه تأسیس + technical-director license پروانه مسئول فنی via the Article-20 commission), in the **home-nursing-services-center** track (a nurse with BSc + ≥5 yrs experience can found/direct it). The fast, legal go-to-market is to **partner with already-licensed centers** while Balinyaar's own permit is pending. A `partner_centers` entity represents the licensed center that holds the جواز کسب + اینماد + MoH license, sponsors nurses, and **may be the merchant-of-record / invoice issuer** for payments — making BNPL and online payment legally feasible without each nurse holding a license.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- Operating **without** a permit is the real legal risk (penalty ladder up to permanent revocation + judicial referral). The partner-center vehicle is the launch-critical mechanism that makes the whole money flow legal.
|
||||
- مودیان obligation phases in by revenue thresholds; most individual nurses fall below mandatory thresholds early, but the **platform's commission line is VAT/e-invoice-relevant** — so per-nurse مودیان obligation is a configurable flag and the platform's own commission invoicing is the in-scope obligation.
|
||||
- The licensed center (not Balinyaar-the-tech-company, initially) is plausibly the IPG merchant-of-record and the invoice issuer — the data model represents this explicitly.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** `partner_centers` as the launch legal vehicle with merchant-of-record flag and nurse sponsorship; minimal per-booking `invoices` with 10% configurable VAT on commission and مودیان reference fields; e-namad held by the launch entity; nurse-as-taxable-seller / platform-as-commission-seller split.
|
||||
- **DEFERRED:** full مودیان e-invoice automation / digital-signature pipeline; nurse-side service-invoice issuance on the nurse's behalf; insurer/B2B-payor invoicing; the future employer-style `organizations` model.
|
||||
|
||||
### (d) Supporting database entities
|
||||
**`invoices`** (minimal, commission-focused, مودیان fields, VAT), **`partner_centers`** (MoH license, اینماد, merchant-of-record), `nurse_profiles.partner_center_id`, `payment_transactions` (Shaparak reference for reconciliation), `platform_configs` (VAT rate, merchant-of-record).
|
||||
|
||||
---
|
||||
|
||||
## 14. Notifications & Admin / Backoffice
|
||||
|
||||
### (a) Business requirements
|
||||
- **In-app notifications** to all user types for booking, payment, payout, review, verification, and alert events. Carried as typed in-app records the front-end fetches on load and uses to deep-link to the relevant entity. **No push notifications at launch.**
|
||||
- A retention job hard-deletes read notifications older than 90 days to keep the table bounded.
|
||||
- **Admin / backoffice tooling** must cover the operational spine:
|
||||
- **Verification queue** — review uploaded MoH/INO/criminal-record documents, record structured credential numbers/expiries, pass/fail steps, and flip `is_verified` transactionally.
|
||||
- **Refund tooling** — initiate admin-only, ticket-linked refunds with tiered policy application and fee-leg decomposition; for BNPL, trigger the provider revert/cancel.
|
||||
- **Payout tooling** — initiate/inspect weekly batches, see eligibility (EVV + closed dispute window), apply clawback netting, schedule around bank holidays, and reconcile transfer references.
|
||||
- **Support-alert console** — triage low-rating, no-show, location-mismatch, expiry, and fraud-signal alerts.
|
||||
- **RBAC** — admin roles (super_admin / admin / support / finance / moderator) scope who can verify, refund, pay out, and moderate.
|
||||
- An **append-only audit trail** records every state-changing operation on sensitive entities (bookings, payments, refunds, verifications, reviews, users), and config changes (e.g., the platform fee rate) are auditable.
|
||||
|
||||
### (b) Iran-specific considerations
|
||||
- No push at launch reflects a pragmatic MVP and the in-app polling norm; SMS-OTP already covers the critical auth path.
|
||||
- Back-office must reason over the Shamsi calendar and `iranian_holidays` for payout scheduling and deadline computation, and over the verification realities (manual MoH/INO checks, expiry-driven re-verification).
|
||||
- High-volume logs (`audit_logs`, `system_events`, `notifications`) need partitioning/retention planned before launch to avoid unbounded growth.
|
||||
|
||||
### (c) MVP vs DEFERRED
|
||||
- **MVP:** in-app notifications with 90-day retention; admin verification/refund/payout/alert tooling; RBAC; append-only `audit_logs`; config-change auditing.
|
||||
- **DEFERRED:** push notifications; SMS/email notification channels beyond OTP; a full analytics warehouse (`system_events` piped out rather than queried in the transactional DB); ML fraud console.
|
||||
|
||||
### (d) Supporting database entities
|
||||
`notifications`, `support_alerts`, `roles`, `user_roles`, `audit_logs`, `system_events`, `platform_configs`, plus the operational entities each tool acts on (`nurse_verifications` / `verification_steps` / `nurse_credentials`, `refunds`, `nurse_payout_batches` / `nurse_payouts` / `nurse_clawbacks`, `bookings`).
|
||||
|
||||
---
|
||||
|
||||
## Appendix — MVP vs Deferred at a glance
|
||||
|
||||
| Area | MVP | Deferred |
|
||||
|---|---|---|
|
||||
| Onboarding | phone-OTP; customer/nurse/admin; patient split | customer KYC; org self-onboarding; push |
|
||||
| Verification | 6-step data-driven pipeline; `nurse_credentials`; IBAN ownership | MoH/INO API; liability-insurance step; ML fraud |
|
||||
| Catalog | admin categories/options; nurse variants & price units | holiday/surge pricing; companionship tier |
|
||||
| Search | geo + category; `nurse_search_index`; same-gender filter | map discovery; availability hard-filter |
|
||||
| Booking | request→accept→pay→confirm; `booking_sessions` multi-visit | recurring schedules; milestone-payment UX |
|
||||
| EVV | per-session GPS check-in/out; payout gating | geofencing; tele-check-ins; cameras |
|
||||
| Cancellation | tiered policy + snapshot; admin/ticket refunds; per-session | auto no-show penalty; self-service refunds |
|
||||
| Payments/Escrow | ledger escrow; `payment_webhook_events`; provider abstraction | nurse wallet; multi-PSP live; bank escrow product |
|
||||
| BNPL | full-upfront `bnpl_transactions`; provider-revert refunds | installment tracking (cut); tranched settlement; multi-provider |
|
||||
| Payouts | weekly batches; clawbacks; holiday-aware; verified IBAN | instant withdrawal; per-nurse frequency |
|
||||
| Reviews | one-per-booking; moderation; low-rating alerts | two-way reviews; tag aggregation; `incidents` |
|
||||
| Messaging | ticket-only; coordination ticket; emergency playbook | live chat; emergency-event entity |
|
||||
| Tax/Legal | `partner_centers`; minimal `invoices`; 10% VAT on commission; e-namad | full مودیان automation; nurse-side invoicing; B2B |
|
||||
| Notifications/Admin | in-app + retention; verify/refund/payout tooling; RBAC | push; analytics warehouse; ML console |
|
||||
+657
-1078
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,347 @@
|
||||
# Balinyaar — Payments, Escrow, Settlement & Installments (BNPL)
|
||||
|
||||
> **Purpose.** This is the fintech deep-dive for Balinyaar, an MVP home-nursing marketplace in Iran. It pins down how the platform collects money from families, holds an *internal escrow ledger state* (not custodied cash), pays nurses weekly minus commission, and integrates Iranian Buy-Now-Pay-Later (خرید اقساطی / BNPL). It answers the two questions the team cares most about — how a BNPL booking is cancelled/refunded mid-plan, and who pays the nurse (and when) under BNPL — and it grounds every decision in verified research, separating **VERIFIED** facts from **CONFIGURABLE / UNCERTAIN** items that must be confirmed at contracting. All money is **IRR (Rials), stored as `BIGINT`**; Toman is display-only and converted only at the provider API boundary.
|
||||
|
||||
**Date:** 2026-06-20
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
- **Balinyaar legally CANNOT custody buyer funds.** In Iran money always flows card → licensed PSP → Shaparak settlement → bank-registered IBANs. A پرداختیار (payment facilitator — the license class an MVP rides on) is explicitly **barred from holding deposits, running wallets, paying interest, or moving money between merchants**. "Platform holds escrow" must therefore be implemented as an **internal double-entry ledger state** over funds custodied at a licensed provider — never as cash in a Balinyaar bank account. *(VERIFIED — multiple independent sources + CBI/Shaparak directives.)*
|
||||
|
||||
- **The compliant marketplace primitive is تسهیم (settlement-sharing):** one incoming card payment is split by the provider/Shaparak across multiple registered IBANs (the nurse's share, the platform's commission) and deposited directly — the platform never touches the split. Shaparak has separately **banned inter-merchant / inter-facilitator transfers and wallet-style holding**, which makes a "delay-then-redistribute" pool legally grey-to-prohibited. *(VERIFIED.)*
|
||||
|
||||
- **The decisive BNPL finding: full-upfront settlement.** Provider-financed Iranian BNPLs (SnappPay, Digipay, Tara, Torob Pay, ZarinPlus) pay the **merchant the FULL amount in ONE lump, minus a merchant commission, and bear 100% of customer-default risk.** The customer's installments are owned entirely by the provider and are **decoupled** from Balinyaar's escrow/EVV/payout cycle. *(VERIFIED for SnappPay and Torob Pay from credible sources; consistent for Digipay and Tara.)*
|
||||
|
||||
- **Consequence — a BNPL order = a card payment landing net-of-fee.** Therefore Balinyaar **does NOT track customer installments** (no `installment_entries`, no per-installment webhooks, no default propagation). This deletes an entire fragile subsystem.
|
||||
|
||||
- **But settlement TIMING is not instant.** The "full amount" is true in *amount*, not *timing*: cadence is contract-defined (daily / T+1–3 / weekly / 15-day), and at least one authoritative SnappPay source gates settlement on the customer's **first installment** (پس از واریز اولین قسط). Model a per-transaction `settled_at`; never assume instant. *(VERIFIED correction to the original research.)*
|
||||
|
||||
- **Under BNPL the nurse is paid by Balinyaar, on Balinyaar's own weekly schedule, exactly as for a card booking** — after EVV check-out and the dispute window. The nurse's payout is computed from `gross_price_irr − balinyaar_commission_irr`, **NOT** from the BNPL-net amount. The BNPL commission is a **platform expense** that must never touch the nurse's payout.
|
||||
|
||||
- **Avoid Lendo for the MVP.** It is bank-financed (Bank Ayandeh): the **customer** pays ~18–23% interest plus a ~5% (often non-refundable) service fee over 6–12 months — a POS loan, a poor fit for short, cancellable nursing visits.
|
||||
|
||||
- **Two corrections the research forced:** VAT in Iran is **10%** (rose from 9% in 1403, = govt 7% + municipal 3%), not 9% — and make it configurable since it has moved two years running. And the Digipay "24h-pay / collect-in-5–25-days-from-business" sentence describes the **early-settlement/factoring** product, **not** BNPL — right conclusion, wrong evidence; the correct first-party BNPL source is `mydigipay.com/bpg/`.
|
||||
|
||||
- **Provider continuity is a real risk.** In **Nov 2024 the CBI abruptly cut Toman and Jibit's settlement/withdrawal services** with no stated cause, stranding businesses (including millions of Snapp drivers). Design for multi-provider failover and a reconciliation ledger that survives a provider being cut mid-cycle.
|
||||
|
||||
- **Recommendation:** integrate **SnappPay first**, **Digipay second**, **avoid Lendo**. Onboarding the Balinyaar entity needs **both جواز کسب AND eNamad (اینماد)**. Whether a multi-vendor marketplace re-disbursing to many independent nurses qualifies as a single merchant is **publicly UNCONFIRMED** — confirm with provider sales before relying on it.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Iranian payment reality
|
||||
|
||||
### 2.1 The rails: card → PSP → Shaparak → registered IBANs
|
||||
|
||||
Every card payment in Iran is acquired by a licensed **PSP** and cleared through **Shaparak** (the national switch), which settles to **bank-registered IBANs (شِبا)** of the merchant/beneficiaries. There is **no native marketplace-escrow construct** the way a US/EU platform would hold buyer cash in trust. The platform does not — and legally may not — sit in the money path as a custodian.
|
||||
|
||||
### 2.2 The license class: پرداختیار (payment facilitator) and the custody prohibition
|
||||
|
||||
An MVP marketplace like Balinyaar rides on a **پرداختیار (payment facilitator / aggregator)** arrangement under a contracted PSP and the CBI/Shaparak agreement. A facilitator is **explicitly forbidden** from:
|
||||
|
||||
- holding customer deposits,
|
||||
- operating wallets,
|
||||
- paying interest,
|
||||
- granting credit / guarantees,
|
||||
- temporarily using merchant balances.
|
||||
|
||||
Settlement must go **only** to merchant-registered bank accounts, and **only Shaparak** can withdraw from the special facilitator settlement account (حساب ویژه پرداختیاری). Unauthorized fund-holding draws penalties, license suspension, and AML exposure. **This is the single load-bearing constraint of the whole design: Balinyaar cannot be the custodian of buyer funds.** *(VERIFIED — way2pay, Zibal legal blog, finolaw, peivast.)*
|
||||
|
||||
### 2.3 تسهیم (settlement-sharing) — the compliant marketplace primitive
|
||||
|
||||
The legitimate way to pay many providers is **تسهیم / تسویه اشتراکی (settlement-sharing)**: a single incoming card payment is split across multiple registered IBANs (the nurse's net share + the platform's commission) and **credited directly by Shaparak/the provider** to each party. The platform never touches the actual split. ZarinPal markets this for marketplaces (بازارگاه) with split-by-ratio to each partner's registered Sheba; Zibal, Sadad, SizPay, Vandar, Jibit, Zibal, PayPing, IDPay offer variants. *(VERIFIED.)*
|
||||
|
||||
> **Caveat (CONFIGURABLE):** ZarinPal's تسهیم appears gated to a "golden" (طلایی) membership tier; all timing/minimum/limit numbers (e.g. ~100,000 IRR minimum, processing windows, "no beneficiary limit") came from single doc reads and must be reconfirmed at contracting.
|
||||
|
||||
### 2.4 The banned move: inter-merchant / inter-facilitator transfers and held pools
|
||||
|
||||
A tempting design — "collect into a platform pool, hold until EVV check-out, then redistribute" — is **regulatory grey-to-prohibited.** Shaparak **explicitly banned inter-facilitator and inter-merchant fund transfers and wallet-style holding.** A *delayed but pre-fixed* split to the **same registered IBANs** may be tolerable; **moving/holding funds in a platform-controlled pool to release conditionally is the banned behavior.** The only clean hold/release/refund mechanism is a **bank-grade escrow product** (e.g. Vandar میندو / معاملات امن — buyer pays into trust, released to seller on confirmation, refundable on seller failure), but even that is flagged as regulatorily fragile and its API-level EVV trigger is unverified. **Practical conclusion: model escrow as an internal ledger over whichever provider primitive you contract; do not assume you can lawfully custody cash yourself.** *(VERIFIED ban; the "delay-then-hold" pattern is the OVERSTATED part of the original research.)*
|
||||
|
||||
### 2.5 Provider cut-off continuity risk (Toman / Jibit, Nov 2024)
|
||||
|
||||
In **November 2024 the CBI abruptly cut Toman's and Jibit's settlement/withdrawal services** with no stated cause, disrupting businesses including millions of Snapp drivers who could not settle. Wallet/balance facilitator models have been blocked and re-permitted before (Vandar's gateway was blocked then unblocked by Shaparak). **Design for multi-provider failover and a reconciliation ledger that survives a provider being cut off mid-cycle.** *(VERIFIED — zoomit, way2pay, ecoiran.)*
|
||||
|
||||
### 2.6 Tax: سامانه مودیان and VAT = 10%
|
||||
|
||||
- Iran's **سامانه مودیان (taxpayer / e-invoicing system)** requires electronic invoices (صورتحساب الکترونیکی) with a **22-digit number**, a **memory tax-id (شناسه یکتای حافظه مالیاتی)**, and a digital signature. The **SELLER issues the invoice** and remits VAT; the buyer cannot. For a marketplace this maps cleanly: **each nurse is the taxable seller of the nursing service; Balinyaar's taxable supply is ONLY its commission** (the Snapp/Tapsi precedent). *(VERIFIED.)*
|
||||
- **VAT is 10%**, not 9%: the standard rate **rose from 9% to 10% in 1403** (govt 7% + municipal 3%) and remains 10% in 1404. Any VAT field hardcoded at 9% is stale — **use 10% and make it a configurable parameter**, since it has changed two years running. *(VERIFIED correction.)*
|
||||
- مودیان enrollment is phased in by revenue threshold (individuals with sales above ~144bn IRR through end of 1404 must issue e-invoices from Tir 1405). Whether the home-nursing **service** itself is VAT-exempt (medical exemption) is **UNCERTAIN** — do not assume; model a config-driven VAT rate that can be 0.
|
||||
|
||||
---
|
||||
|
||||
## 3. Escrow as an internal ledger, not held cash
|
||||
|
||||
Because Balinyaar cannot custody buyer funds (§2.2), **"escrow" must be a software construct: a double-entry ledger STATE over money that legally sits at a licensed provider/bank.** The original ~45-table model had **no ledger** — escrow was only inferable by joining `bookings.status`, `bookings.payout_released`, `payment_transactions.status`, and `refunds`, with no single answer to "how much do we owe nurses right now?" Three independent critiques rated this a **critical** gap. The fix is one append-only table.
|
||||
|
||||
### 3.1 `ledger_entries` — the financial source of truth
|
||||
|
||||
Append-only, never updated or deleted. Every money event posts **balanced** rows sharing a `transaction_group_id` (Σ debit = Σ credit). Per-nurse balances are *derived by filter*, never cached in a drifting wallet-balance column.
|
||||
|
||||
**Account types:**
|
||||
|
||||
| account_type | Meaning |
|
||||
|---|---|
|
||||
| `escrow_held` | Funds received and held (over provider custody) not yet released or refunded |
|
||||
| `platform_revenue` | Balinyaar's own commission income |
|
||||
| `nurse_payable` | What the platform owes the nurse (accrued, awaiting weekly payout) |
|
||||
| `refund_payable` | Amount owed back to the customer / in-flight reversal |
|
||||
| `bnpl_fee_expense` | The BNPL provider's merchant commission — a platform expense |
|
||||
| `nurse_clawback_receivable` | Money a nurse owes back after a refund-after-payout |
|
||||
|
||||
### 3.2 The postings
|
||||
|
||||
Amounts are positive; `direction` carries the sign. The three-amount split (`gross_price_irr`, `balinyaar_commission_irr`, `bnpl_commission_irr`) is defined in §7.
|
||||
|
||||
**(a) Card payment capture (inbound):**
|
||||
```
|
||||
DEBIT escrow_held gross_price_irr
|
||||
CREDIT platform_revenue balinyaar_commission_irr
|
||||
CREDIT nurse_payable nurse_payout_amount (= gross − balinyaar_commission)
|
||||
```
|
||||
|
||||
**(b) BNPL settle (inbound) — identical to a card capture, plus the provider-fee leg:**
|
||||
```
|
||||
DEBIT escrow_held gross_price_irr
|
||||
CREDIT platform_revenue balinyaar_commission_irr
|
||||
CREDIT nurse_payable nurse_payout_amount
|
||||
DEBIT bnpl_fee_expense bnpl_commission_irr
|
||||
CREDIT escrow_held bnpl_commission_irr (escrow reflects NET cash actually received)
|
||||
```
|
||||
Posted **once**, idempotently, keyed on the settling transaction. **No installment-level postings** — the customer's repayment schedule is SnappPay's ledger, not ours.
|
||||
|
||||
**(c) Refund — BEFORE the nurse is paid out (clean reversal):**
|
||||
```
|
||||
DEBIT platform_revenue platform_fee_refunded_irr
|
||||
DEBIT nurse_payable nurse_payout_refunded_irr
|
||||
CREDIT refund_payable (sum)
|
||||
```
|
||||
Clear `refund_payable` when the PSP / SnappPay confirms the customer cash-back. Nothing leaves Balinyaar toward the nurse — the `nurse_payable` accrual is simply reversed.
|
||||
|
||||
**(d) Clawback — refund AFTER the nurse was already paid:**
|
||||
The nurse's `nurse_payable` was already drained by a processed payout batch, so there is nothing left to reverse. Instead the platform books a receivable:
|
||||
```
|
||||
DEBIT nurse_clawback_receivable amount_irr (nurse_id set; nurse now owes the platform)
|
||||
CREDIT refund_payable amount_irr
|
||||
```
|
||||
Recovered by **netting against the nurse's next `nurse_payable`** at batch time, or marked `written_off` if uncollectable. A `nurse_clawbacks` row carries the lifecycle (`pending` / `recovered` / `written_off`). This is unavoidable because **Iranian payouts are real bank transfers — hard/impossible to reverse** — so the right defense is *gating payout on the dispute window*, with clawback as the fallback.
|
||||
|
||||
### 3.3 Why the ledger, not more columns
|
||||
|
||||
A marketplace that holds escrow, pays out weekly minus commission, and handles refunds + clawbacks has exactly the shape double-entry was invented for. The MVP cost is **one table + posting discipline**. The alternative (more money columns on bookings/payouts) cannot answer "how much is held but unreleased" without fragile joins and makes bank/Shaparak reconciliation nearly impossible. Keep the per-booking fee snapshot as the *pricing* record; the ledger is the *financial-truth / reconciliation* layer posted alongside.
|
||||
|
||||
---
|
||||
|
||||
## 4. BNPL landscape — comparison
|
||||
|
||||
All six are Iranian. The structurally important fact (full-upfront-to-merchant, provider bears default risk) holds for every **provider-financed** option; **Lendo is the outlier (bank-financed, customer pays interest).**
|
||||
|
||||
| Provider | Settlement model | Who bears financing cost | Customer plan (installments / tenor / interest) | Credit ceiling | Merchant fee | Service / marketplace eligibility | Integration | Confidence |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **SnappPay (اسنپپی)** | **Full-upfront**, single lump minus commission; provider bears default risk | **Merchant** (commission) | 4 installments / 4 months (1 at purchase + 3 monthly); **interest-free** | ~20M Toman (→~50M good payers); separate bank-credit product up to 100M Toman, 12–24 mo, **carries bank interest** | **Undocumented** (anecdotal ~7–15% + ~10% VAT; one merchant cited 15%); per-contract config | Services incl. "بعضی خدمات پزشکی" eligible; in-person/appointment OK. **Multi-vendor re-disbursement UNCONFIRMED** | API + IPG redirect (OAuth → eligible → token → verify → settle → revert/cancel/update/status) | High (model) / Medium (fee, eligibility) |
|
||||
| **Digipay (دیجیپی)** | **Full-upfront** to contracted merchant; provider bears default risk | **Customer** bears markup; merchant pays acquiring commission | 1-month (interest-free) + 4-installment (~4–8% on installments 2–4) + 3/6/9/12-mo loan | Varies widely by product/campaign (monthly ~1–30M Toman; 4-inst up to ~50–200M; loan up to 2bn IRR) | **توافقی (negotiable)**, settlement-speed dependent; sells *early settlement* as a paid add-on | Explicit **services** (travel, hotels, insurance, **dental**); single contracted merchant, **no sub-merchant split** | UPG: ticket → redirect → callback → verify; deliver-confirm then refund. Type codes IPG=0/Wallet=11/Credit=5/BNPL=13/Credit-Card=24 | High (model, API) / Uncertain (fee, mid-plan mechanics) |
|
||||
| **Tara (تارا)** | Provider-financed, full amount to seller | Merchant (interest-free to customer) | 2 interest-free installments, starting 1 month after purchase | **Up to 20M Toman** (research's "10M / 2-month" is OUTDATED); other tiers 5M / 10M / 100M / 150M | Per-contract | Interest-free (بدون سود) standard tier | API/gateway | Medium |
|
||||
| **Torob Pay** | **Full-upfront**, cash to seller | Merchant | 4 equal installments, **25% down**, interest-free | New users ~1–2M Toman → 3–5M | **Concrete: 6% + VAT = 6.6%** per order | Third-party explainers (not first-party docs) | Gateway | Medium (firm fee) |
|
||||
| **ZarinPlus (ZarinPal)** | Provider-financed BNPL inside the ZarinPal gateway; standard ZarinPal merchant settlement ~T+1 | Merchant | 4 installments | ~2M Toman (ID only) → 5–15M with history → up to 20M with cheque/promissory note | ZarinPal gateway ~1% (cap ~4000 Toman) + per-txn; BNPL-specific terms not confirmed | Inside ZarinPal IPG | Gateway / تسهیم | Medium |
|
||||
| **Lendo** | Bank-financed (Bank Ayandeh); merchant effectively gets full value | **CUSTOMER** (~18–23% interest **+ ~5% upfront service fee**, often non-refundable) | 6 / 9 / 12 months — a **POS loan**, not interest-free BNPL | Bank-set | — | POS loan | — | **AVOID for MVP** |
|
||||
|
||||
> **Key contrast:** Torob Pay's **6.6% (6% + VAT)** is the only *published* rate; SnappPay's true rate **cannot be inferred** from it and must be treated as negotiated per-contract config. Lendo's customer-borne interest + non-refundable fee make mid-engagement cancellations leave the customer out of pocket — a poor fit for short, cancellable nursing visits.
|
||||
|
||||
---
|
||||
|
||||
## 5. The decisive finding: full-upfront settlement
|
||||
|
||||
**The provider pays the merchant the full amount minus commission in ONE lump and bears default risk. The customer's installments are owned by the provider and are decoupled from Balinyaar's escrow/payout.** *(VERIFIED — SnappPay CEO: "ارایهدهنده سرویس تمام پول پذیرنده را پرداخت کرده و هیچ ریسکی سمت پذیرنده نیست"; Digipay first-party bpg page: "مبلغ حاصل از فروش را یکجا دریافت کنید … نگران ریسک عدم بازپرداخت اقساط نباشید". No source described any tranched-to-merchant model among provider-financed BNPLs.)*
|
||||
|
||||
**Therefore, in Balinyaar's books, a BNPL order is identical to a card payment that lands net-of-fee in one inbound settlement.** We **DO NOT model customer-installment tracking** (`installment_entries`, per-installment webhooks, default propagation). This removes the fragile subsystem the original model flagged as unresolved.
|
||||
|
||||
### Verified caveats that must remain CONFIGURABLE / UNCERTAIN
|
||||
|
||||
- **Settlement TIMING is NOT instant.** Cadence is contract-defined (daily / T+1–3 / weekly / 15-day / occasionally longer — one merchant alleged ~4 months), and the most concrete SnappPay statement gates settlement on the customer's **first installment** ("پس از واریز اولین قسط"). Digipay even sells *early settlement* as a paid feature, implying its baseline is delayed. **Model a per-transaction `settled_at` and a per-provider settlement-lag; gate weekly nurse payouts on settlement actually received, so you never pay a nurse before you hold the cash.**
|
||||
- **Commission rate is per-contract config, never hardcoded.** SnappPay publishes no public rate; read the actual deducted amount from each settlement record.
|
||||
- **Marketplace re-disbursement eligibility is publicly UNCONFIRMED.** SnappPay's and Digipay's documented model is **single-merchant-receiver**. Design so the provider pays **Balinyaar (the merchant-of-record) one lump**, and Balinyaar does the internal escrow→nurse allocation itself. Confirm any per-nurse routing directly with provider sales. Even Snapp Doctor's own home-nursing page does not advertise SnappPay installments — service eligibility is *plausible, not confirmed* for an in-home individual-nurse marketplace.
|
||||
- **Onboarding requires BOTH جواز کسب AND eNamad (اینماد)** for the Balinyaar entity (the original research omitted eNamad). For Digipay, an activity license is mandatory only for "sensitive trades" (صنوف حساس); home-healthcare may be treated as one — confirm.
|
||||
|
||||
---
|
||||
|
||||
## 6. Q1 — Cancellation / refund of a BNPL booking mid-plan
|
||||
|
||||
**Decisive rule: money ALWAYS flows `customer ↔ SnappPay ↔ Balinyaar`. Never refund the customer directly, and never route a nurse→customer refund.** Balinyaar initiates the reversal through SnappPay's API using the stored payment token/transaction id:
|
||||
|
||||
- **Full cancel/refund → `revert`** (full amount).
|
||||
- **Partial / shortened-visit → `update`** (new amount must be strictly lower than the original settled amount) — or `cancel` per the provider's partial semantics.
|
||||
|
||||
SnappPay then, on its own ledger and asynchronously:
|
||||
1. **cancels the customer's remaining UNPAID installments** and credits their equivalent back to the customer's **credit wallet** (reusable BNPL credit — not merely "wiped"),
|
||||
2. **refunds any already-PAID installment** to the customer's **bank account in ~7–10 business days.**
|
||||
|
||||
The merchant's only role is to authorize/cancel; **SnappPay owns the unwind.** *(VERIFIED verbatim: "اقساط پرداختنشده لغو و معادل آن به موجودی حساب اعتباری شما برگشت داده میشود"; "مبلغ قسط پرداختشده به حساب بانکی شما برگشت داده خواهد شد (۷ تا ۱۰ روز کاری)".)*
|
||||
|
||||
### Balinyaar's internal bookkeeping
|
||||
|
||||
1. Record a **`refund` row** with `refund_channel = 'bnpl_revert'`, `external_revert_reference`, `expected_customer_refund_eta`, and a `refund_status` that stays `processing` until SnappPay confirms (a reconciliation job clears it). **Surface the asynchronous 7–10-day window in the UI and reconciliation** — never assume instant.
|
||||
2. **Decompose the refund** across the two fee legs: `platform_fee_refunded_irr` and `nurse_payout_refunded_irr` (the booking gross = platform fee + nurse payout; the refund must say how much of each is reversed).
|
||||
3. **Post balanced ledger entries** (§3.2c/d): debit the decomposed `platform_revenue` / `nurse_payable`, credit `refund_payable`; record the revert reference on the `bnpl_transactions` row (`reverted_amount_irr`, `reverted_at`, `refund_channel`).
|
||||
4. **If the nurse has NOT been paid** (booking still inside the dispute window / not in a processed batch): reverse the `nurse_payable` accrual — clean, nothing leaves Balinyaar. *(This is the common case if you gate payout on the dispute window.)*
|
||||
5. **If the nurse HAS been paid** (refund-after-payout): take the **clawback** path — a `nurse_clawbacks` row + a `nurse_clawback_receivable` ledger leg (§3.2d), recovered from the next payout batch or written off.
|
||||
|
||||
**Partial / shortened-visit** maps to the `update` endpoint with a reduced amount: record `refund_delta_irr`, reduce `settled_amount_irr` on the `bnpl_transactions` row, and apply the same fee-leg decomposition.
|
||||
|
||||
> **UNCERTAIN (confirm at contracting):** whether the provider returns *its* merchant commission on a full vs partial refund (full / pro-rata / not at all) is **undocumented** and directly affects platform P&L on cancellations. Model `provider_commission_reversed_amount` as nullable and reconcile from the provider's refund response — do not hardcode. Digipay's exact mid-installment proration mechanics are likewise undocumented and contract-dependent.
|
||||
|
||||
---
|
||||
|
||||
## 7. Q2 — Under BNPL, who pays the nurse, and when?
|
||||
|
||||
**Balinyaar pays the nurse, on Balinyaar's own normal weekly payout schedule, after EVV check-out and after the dispute window closes — exactly the same path as a card-funded booking.** SnappPay **never** pays the nurse and is indifferent to Balinyaar's internal split. The customer's BNPL repayment timeline is completely decoupled from the nurse payout cycle.
|
||||
|
||||
### The crucial accounting rule — the three-amount split
|
||||
|
||||
The nurse's payout is computed from the booking's **own price and Balinyaar's own commission**, **NOT** from the BNPL-net amount. SnappPay's commission is a **cost of accepting BNPL, borne by Balinyaar**, and must **never** be passed through to the nurse. Store three separate amounts so the two fee deductions are never conflated:
|
||||
|
||||
| Amount | Meaning | Drives |
|
||||
|---|---|---|
|
||||
| `gross_price_irr` | What the customer is charged (booking price) | The invoice; the inbound `escrow_held` debit |
|
||||
| `balinyaar_commission_irr` | Balinyaar's own cut (was `platform_fee_amount`) | `platform_revenue`; **the nurse payout** |
|
||||
| `bnpl_commission_irr` | The BNPL provider's merchant discount | `bnpl_fee_expense` (platform expense) — **never the nurse** |
|
||||
|
||||
```
|
||||
nurse_payout_amount = gross_price_irr − balinyaar_commission_irr
|
||||
```
|
||||
|
||||
**The nurse receives the identical amount whether the family paid by card or by SnappPay, and on the identical weekly timing** (batch, gated on `dispute_window_ends_at < now()`). The only difference a BNPL order makes to the books is the extra `bnpl_fee_expense` leg that reduces *Balinyaar's* margin — not the nurse's pay.
|
||||
|
||||
**Worked example** (illustrative; rates are config): gross `5,000,000` IRR, Balinyaar commission 15% = `750,000`, nurse payout = `4,250,000`. If paid via SnappPay at a 10% merchant commission, `bnpl_commission_irr = 500,000` is a Balinyaar expense; SnappPay settles `4,500,000` net to Balinyaar; the **nurse still receives `4,250,000`**, and Balinyaar's net margin is `750,000 − 500,000 = 250,000` (before PSP/VAT). The nurse payout is invariant to the payment method.
|
||||
|
||||
> **Timing guard (CONFIGURABLE):** because BNPL settlement can lag, optionally key weekly-payout eligibility off `bnpl_transactions.settled_at` (settlement actually received) in addition to EVV + dispute window, so the platform never advances a nurse before it holds the cash.
|
||||
|
||||
---
|
||||
|
||||
## 8. Integration notes
|
||||
|
||||
### 8.1 SnappPay (اسنپپی) — primary
|
||||
|
||||
API-based with an IPG redirect. Endpoint paths are **VERIFIED** against the open-source Laravel package and match exactly:
|
||||
|
||||
```
|
||||
POST api/online/v1/oauth/token → OAuth bearer token
|
||||
GET api/online/offer/v1/eligible → eligibility / credit check on the customer
|
||||
POST api/online/payment/v1/token → payment token → redirect customer to SnappPay
|
||||
POST api/online/payment/v1/verify → verify after callback
|
||||
POST api/online/payment/v1/settle → settle (capture the merchant lump)
|
||||
POST api/online/payment/v1/revert → full reversal
|
||||
POST api/online/payment/v1/cancel → cancel
|
||||
POST api/online/payment/v1/update → partial (new amount strictly lower)
|
||||
GET api/online/payment/v1/status → status
|
||||
```
|
||||
|
||||
Credentials issued only after a signed contract + business-license review: `user_name`, `password`, `client_id`, `client_secret`, merchant/customer number, security code, `base_url`. Sandbox availability is plausible (issued by sales) but **TO BE CONFIRMED** — the public package does not evidence it.
|
||||
|
||||
> **WARNING:** the `SnapPayInc/open-api-java-sdk` GitHub repo is the **unrelated CANADIAN SnapPay** (snappay.ca, CAD) — **do NOT use it**. Likewise, English searches for "digipay split payment" return **DigiPay.Guru**, an unrelated white-label vendor — not the Iranian Digipay.
|
||||
|
||||
### 8.2 Digipay (دیجیپی) — secondary / fallback
|
||||
|
||||
Unified **UPG** gateway, server-side + hosted redirect:
|
||||
|
||||
```
|
||||
POST /digipay/api/tickets/business?type=… → ticket + redirectUrl (type MUST match product)
|
||||
(callback to merchant)
|
||||
POST /digipay/api/purchases/verify → verify (re-check amount + providerId before trusting)
|
||||
POST /digipay/api/purchases/deliver?type=… → delivery confirmation (Credit=5 / BNPL=13) — GATE ON EVV CHECK-OUT
|
||||
POST /digipay/api/refunds?type=… → refund (providerId, amount, saleTrackingCode)
|
||||
GET /digipay/api/refunds/{InquiryId} → poll refund status
|
||||
POST /digipay/api/reverse → manual reverse (~25 min, IPG/DPG only)
|
||||
```
|
||||
|
||||
**Type codes (VERIFIED, first-party):** IPG=0, Wallet=11, Credit=5, BNPL=13, Credit-Card=24 — **persist the gateway type per transaction**; deliver/refund calls must carry the matching code. Each purchase supports **EITHER refund OR manual reverse, not both** — store a mutually-exclusive reversal-mode flag. For a *service*, the "delivery" is the completed visit, so **gate `deliver` on the nurse's EVV check-out.** A BNPL refund returns to the customer's Digipay credit/wallet (or bank/SHEBA), **not** the original card.
|
||||
|
||||
### 8.3 Cross-cutting integration rules
|
||||
|
||||
- **Webhook idempotency:** every PSP/BNPL callback is at-least-once and retried. Upsert into **`payment_webhook_events`** keyed `UNIQUE(external_event_id)` **first**, inside the same transaction that mutates money state, and **no-op on duplicate** — prevents double-confirm / double-settle / double-refund.
|
||||
- **Never trust the callback alone** — always `verify` server-side and re-check `amount` + `providerId`/reference before treating funds as captured.
|
||||
- **Amounts in IRR Rials as `BIGINT`** everywhere; SnappPay/Digipay quote in **Toman** at the API boundary — store a `currency` field on the BNPL row and **convert only at the boundary, never internally.**
|
||||
- **State-machine guard** on BNPL status transitions (`eligible → token_issued → verified → settled → reverted`) so callbacks/retries cannot double-settle or double-refund.
|
||||
|
||||
---
|
||||
|
||||
## 9. Schema touchpoints
|
||||
|
||||
Final, aligned table/field names (these supersede `installment_plans` / `installment_entries`):
|
||||
|
||||
- **`bnpl_transactions`** (new, **replaces `installment_plans`**; `installment_entries` **CUT**) — 1:1 with a `payment_transaction`. Fields: `payment_transaction_id` FK UNIQUE, `provider_code`, `merchant_of_record`, `external_payment_token`, `external_transaction_id`, `eligibility_status`, `order_amount_irr`, `settled_amount_irr` (net of provider commission), `bnpl_commission_irr`, `currency` (`IRR`/`TOMAN`), `status` (`eligible`/`token_issued`/`verified`/`settled`/`reverted`/`cancelled`/`failed`), `installment_count` (default 4, informational only), `settled_at`, `revert_transaction_id`, `reverted_amount_irr`, `reverted_at`, `refund_channel`, `callback_payload_json`.
|
||||
- **`payment_transactions`** — keep full gateway response + Shaparak reference; **ADD** a filtered `UNIQUE(gateway_reference_code) WHERE NOT NULL` and a filtered `UNIQUE(booking_id) WHERE status='succeeded'` (single capture per booking; idempotent retries).
|
||||
- **`payment_webhook_events`** (new) — `provider_code`, `event_type`, `external_event_id UNIQUE`, `payload_json`, `signature_valid`, `processing_status` (`received`/`processed`/`failed`/`ignored`), `related_payment_transaction_id` NULL, `received_at`, `processed_at`.
|
||||
- **`refunds`** — **1:N** per `payment_transaction` (the original "1:1" claim is wrong); **ADD** `platform_fee_refunded_irr`, `nurse_payout_refunded_irr` (fee-leg decomposition), `refund_channel` (`psp_card`/`bnpl_revert`/`manual_bank`), `external_revert_reference`, `expected_customer_refund_eta`; app invariant `Σ refunded ≤ captured`.
|
||||
- **`ledger_entries`** (new) — `transaction_group_id`, `account_type` (`escrow_held`/`platform_revenue`/`nurse_payable`/`refund_payable`/`bnpl_fee_expense`/`nurse_clawback_receivable`), `nurse_id` NULL, `direction`, `amount_irr`, `booking_id` NULL, `source_ref_type`, `source_ref_id`, `memo`, `created_at`. Append-only; balanced per group.
|
||||
- **`nurse_clawbacks`** (new) — `nurse_id`, `booking_id`, `refund_id`, `amount_irr`, `status` (`pending`/`recovered`/`written_off`), `recovered_in_payout_id` NULL, `created_at`, `resolved_at`.
|
||||
- **`payment_gateways`** — encrypted provider config in `config_json` / secrets: SnappPay `client_id`, `client_secret`/`username`+`password`, merchant number, security code, `base_url`, `sandbox` flag. **Never** store credentials per-transaction.
|
||||
|
||||
**Supporting changes:** `bookings` gets the three-way split (`gross_price_irr`, `balinyaar_commission_irr`, `nurse_payout_amount`) and `dispute_window_ends_at`; `payout_released` BIT is **CUT** (derive from `nurse_payout_booking_links` + ledger). `nurse_payouts` gets `gross_earnings_irr`, `clawback_applied_irr`, `net_amount_irr`. An **`invoices`** table (minimal) captures the commission VAT line.
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommendations & open questions to confirm at contracting
|
||||
|
||||
### Recommendations
|
||||
|
||||
1. **Integrate SnappPay first, Digipay second, avoid Lendo.** SnappPay has the largest reach, explicit service-merchant support, true full-upfront settlement, full default-risk transfer, and a coded API. Digipay is the redundancy/fallback with the broadest healthcare/service coverage. Lendo's customer-borne interest + non-refundable fee is wrong for short, cancellable visits.
|
||||
2. **Treat a BNPL order as one net inbound settlement** identical to a card payment net-of-fee. **Do not** build customer-installment tracking.
|
||||
3. **Make escrow an internal double-entry ledger** over funds custodied at a single licensed provider; **abstract the provider** behind config so it can be swapped if blocked (Toman/Jibit precedent).
|
||||
4. **Pay the nurse from `gross − balinyaar_commission`, weekly, after EVV + dispute window** — identical for card and BNPL; the BNPL commission is a platform expense only.
|
||||
5. **Gate payout on the dispute window** (default 72h) rather than relying on clawback — Iranian bank transfers are effectively irreversible; keep clawback as the modeled fallback.
|
||||
6. **Build webhook idempotency before touching real money**, and store all amounts in IRR `BIGINT`, converting from Toman only at the API boundary.
|
||||
7. **Use 10% VAT, configurable.** Treat each nurse as the taxable seller; invoice only Balinyaar's commission.
|
||||
|
||||
### Open questions to confirm with provider sales / at contracting
|
||||
|
||||
- **Marketplace eligibility:** does the provider's merchant contract permit a multi-vendor home-services marketplace that re-disburses to many independent nurses as a **single** merchant-of-record? (Publicly undocumented; their known model is single-receiver.)
|
||||
- **Commission rate (%):** the actual rate for the health/home-services category (SnappPay publishes none; ~7–15% is anecdotal; Torob Pay's 6.6% is not a proxy).
|
||||
- **Settlement SLA / timing:** daily vs T+1–3 vs weekly vs 15-day, and whether it is gated on the customer's first installment. Get it in writing; do not assume same-day.
|
||||
- **Commission-clawback-on-refund behavior:** on a full vs partial refund, does the provider return its merchant commission fully, pro-rata, or not at all?
|
||||
- **Onboarding documents:** confirm جواز کسب **and** eNamad suffice for the Balinyaar entity, and whether home-healthcare is a "sensitive trade" needing a sectoral license.
|
||||
- **Sandbox credentials:** request early; confirm availability (not evidenced publicly).
|
||||
- **Settlement-provider (تسهیم/payout) choice for the card leg:** which licensed provider (ZarinPal تسهیم / Vandar / Jibit), its fee schedule, batch caps, minimums, and whether delayed settlement / a bank-grade escrow product (Vandar میندو) is permissible for the EVV-gated hold.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
**Iranian payment-facilitator / escrow / settlement legality**
|
||||
- finolaw.net — مقررات پرداختیاری (facilitator rules): `https://finolaw.net/مقررات-پرداخت-یاری/`
|
||||
- way2pay.ir — CBI facilitator framework: `https://way2pay.ir/480525/`, `https://way2pay.ir/484056/`
|
||||
- Zibal legal blog — internet-payment rules: `https://zibal.ir/blog/قوانین-پرداخت-اینترنتی-درگاه-پرداخت-ک/`
|
||||
- peivast.com — Shaparak inter-merchant/wallet ban: `https://peivast.com/p/148655`
|
||||
- ZarinPal تسهیم (split-payment): `https://zarinpal.com/split-payment.html`, `https://www.zarinpal.com/blog/درگاه-پرداخت-اشتراکی-چیست؟/`, `https://next.zarinpal.com/paymentGateway/setshare.html`
|
||||
- Vandar — facilitator / میندو escrow / Bank Ayandeh custody: `https://vandar.io/blog/پرداختیاری-چیست-و-پرداختیار-کیست؟/`, `https://vandar.io/miando/`, `https://docs.vandar.io/payout_service/settlement`
|
||||
- Jibit transferor / payout: `https://www.jibit.io/transferor/`
|
||||
- Toman/Jibit Nov-2024 cut-off: `https://www.zoomit.ir/tech-iran/429145-banning-payment-services-on-toman-and-jibit/`, `https://way2pay.ir/389544/`
|
||||
|
||||
**Tax / مودیان / VAT (10%)**
|
||||
- systemgroup.net — مودیان registration: `https://www.systemgroup.net/knowledge-network/registration-in-the-tax-system/`
|
||||
- hesabandish.com — taxpayer rules: `https://hesabandish.com/rules-taxpayer-system/`
|
||||
- sepidarsystem.com — VAT rate: `https://www.sepidarsystem.com/blog/vat-rate/`
|
||||
- Tapsi/Snapp commission-tax precedent: `https://ip30.ir/tapsi-taxation-challenge/`, `https://drhesaab.ir/how-is-digital-platform-tax-calculated/`
|
||||
|
||||
**SnappPay**
|
||||
- Merchant settlement (full-upfront, risk): `https://limoo.host/blog/snap-pay-merchant-settlement/`, `https://www.portal.ir/snappay-payment-method`, `https://way2pay.ir/278219/`
|
||||
- Product / CEO revenue model: `https://see5.net/blog/what-is-snappay`, `https://ideaagency.net/snapppay-the-correct-revenue-model-landtechs/`, `https://snapppay.ir/`, `https://pay.snapp.ir/`
|
||||
- Refund/cancel FAQ (7–10 business days): `https://allsport.ir/faq/5/8.html`, `https://sourmeh.ir/common-question-about-snapppay/`
|
||||
- API (Laravel package) + eligibility: `https://github.com/backendprogramer/laravel-snapp-pay`, `https://payzito.net/docs/gateways/snapppay`, `https://snapppay.ir/merchant-acquisition/`
|
||||
|
||||
**Digipay**
|
||||
- BNPL full-upfront (credit gateway): `https://www.mydigipay.com/bpg/`, `https://matson.online/digipay-seller/`, `https://digiato.com/tech/digipay-business-solutions-pr`
|
||||
- Services / merchants: `https://www.mydigipay.com/credit/merchants/`, `https://www.mydigipay.com/credit/c-credit/`, `https://www.mydigipay.com/bnpl/c-bnpl/`
|
||||
- UPG dev docs (type codes, deliver/refund/reverse): `https://www.mydigipay.com/developers/docs/upg/`
|
||||
- Onboarding: `https://limoo.host/blog/signup-on-digipay/`
|
||||
|
||||
**Tara / Torob Pay / ZarinPlus / Lendo**
|
||||
- Tara: `https://tara360.ir/bnpl/`, `https://tara360.ir/`, `https://itresan.com/384039/`
|
||||
- Torob Pay (6% + VAT): `https://blupoz.com/`, `https://ranginstore.com/`
|
||||
- ZarinPlus: `https://www.zarinpal.com/blog/bnpl-زرین-پلاس/`, `https://www.zarinpal.com/payment-gateway`
|
||||
- Lendo (bank-financed): `https://lendo.ir/blog/`, `https://lendo.ir/`
|
||||
|
||||
**Internal**
|
||||
- Existing research: `c:\Users\Lenovo\Desktop\balinyaar\product\Home-Nursing-Platform-Research.md`
|
||||
- Database model to refine: `c:\Users\Lenovo\Desktop\balinyaar\product\database-model.md`
|
||||
|
||||
> **Confidence legend.** VERIFIED = survived adversarial verification against multiple/first-party sources. CONFIGURABLE = real but contract-/campaign-dependent (store as config, read actuals from provider). UNCERTAIN = plausible but unconfirmed publicly — confirm at contracting before depending on it.
|
||||
+24
-5
@@ -1,14 +1,33 @@
|
||||
add no unused var rule to client lint rules and agent rules,
|
||||
for both projects,
|
||||
read agent specific files, and if it's not specified, specify a place which define project arcitecture and also add rules for subsequent agents to update that file, if their task changes the project in a way that the description should be updated
|
||||
|
||||
# cleanup layout
|
||||
# add rules and conventions and structure again for both projects
|
||||
=======================================================================
|
||||
|
||||
|
||||
the auth flow should be revised,
|
||||
we send the cookie to the server which contains the auth cookies, so we can pass that down as props also and use it as initial data for the AppStore, also the AppStore name should change to AuthContext, thats better
|
||||
and the flow of authentication and authorization should be reviewed to ensure it is configured with regaurd to best practices,
|
||||
|
||||
=======================================================================
|
||||
|
||||
rules should be added to the projects for agents to not to add verbose comment, if some where there is a really decision made that the code does not tells us ( me and agent) why that piece of code is like that,
|
||||
comment should be added
|
||||
|
||||
|
||||
===================================================================
|
||||
|
||||
in client project if there is still javascript code, rewrite it with ts, with reguard to type rules, no type error and mismatch
|
||||
|
||||
|
||||
=====================================================================================
|
||||
|
||||
|
||||
|
||||
# add proper lint and type rules in client project and add rules for agents to do not violate those rules
|
||||
in product folder, read all the docs, and extract the full information without summarizing and skipping data ( obviousely you can skip duplicate data), and create a single file,
|
||||
which is a coprehensive step by step explaination to the bussiness and data model with clear descriptions.
|
||||
write it in as html file with styles matched to the client project theme,
|
||||
just a single file, not more. so, long story short:
|
||||
# do not skip or ignore data
|
||||
# clear and comprehensive step by step explanation
|
||||
# map each step of the bussines and its description to the data model.
|
||||
# at the end of the file consider a whole section for all data models together with their realtions and the diagram( you can use canvas or anyhting that you can attach using cdn in the file)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
ticket page,
|
||||
backoffice,
|
||||
terms of services page ( needs research )
|
||||
privacy and policy page ( needs research )
|
||||
verify the registration code
|
||||
rate limit
|
||||
workbox for cache ( maybe )
|
||||
|
||||
Reference in New Issue
Block a user