Balinyaar — Business & Data Model
A single, comprehensive walkthrough of what Balinyaar does and how its data is shaped. It walks the platform end to end — from a family searching for a nurse to the weekly bank transfer that pays that nurse — and, at every step, names the database entities that make the step real. A final reference assembles the entire ~53-table model across 13 domains, with diagrams.
Sections 1–14 are the business, in the order the platform actually runs. Each ends with a Data model block (teal header) listing the supporting tables and explaining how they connect to that step. The colored callouts recur throughout and always mean the same thing:
All monetary values are in IRR (Rials), stored as BIGINT. Toman is display-only and is converted to/from Rials only at an external provider's API boundary. Persian credential and product names are kept in their original script for fidelity.
Sources: business-requirements.md (14 business domains), database-model.md (Revision 2 — 53 tables / 13 domains / 4 diagrams), payments-and-installments.md (escrow, settlement & BNPL deep-dive), and the market/legal/verification research report. Document date basis: 2026-06-20.
What Balinyaar is
Balinyaar is a trust-first home-nursing marketplace in Iran. Independent, individually-verified nurses (and, later, nursing-company employees) register, list configurable services at their own prices, and pass a multi-step verification pipeline anchored on the Ministry of Health پروانه صلاحیت حرفهای (professional-competency license). Families search — filtered by city/district and same-gender caregiver preference — pick a nurse and a service variant, submit a booking request, and pay through the platform after the nurse accepts.
The platform records the money as an internal escrow ledger state (not platform-held cash), the nurse performs one or more EVV-verified visits, and the platform pays the nurse weekly, after a dispute window closes, minus a platform commission. All post-booking communication runs through an admin-readable ticket system — there is no direct nurse↔customer chat.
The three actors
Customer — the family member who pays.
Nurse — the independent caregiver who sells.
Admin — Balinyaar back-office (support, finance, moderation, super-admin).
The fourth entity: the patient
The patient (care recipient) is first-class and distinct from the payer, because the payer is usually not the patient (an adult child, a spouse vs. an elderly parent, a newborn, a post-surgical adult).
The launch legal vehicle
At launch the platform operates under a partner licensed home-nursing center (مرکز مشاوره و ارائه مراقبتهای پرستاری در منزل) — the Asanism-style model — the legal vehicle and likely merchant-of-record while Balinyaar's own MoH permit is in process.
This is not a generic marketplace. Iranian payment law forbids the platform from holding customer cash; same-gender bodily-care is culturally decisive; home nursing is a licensed healthcare activity; VAT is 10%; bank transfers are effectively irreversible; and credential verification is fragmented across regulators with no public B2B API. Each of these reshapes a requirement — so each business section carries its own Iran-specific callout.
Cross-cutting ground truths
Four facts hold in every section below. They are the load-bearing constraints the whole design is built around.
-
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
-
VAT is 10%, configurable
It rose from 9% to 10% in 1403 (7% government + 3% municipal) and is treated as a configurable rate — it has moved two years running, so it is never hardcoded.
-
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
-
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. The nurse's payout is always
gross_price_irr − balinyaar_commission_irr, never a BNPL provider's net settlement.
Data-model design principles
The schema (Revision 2) follows thirteen principles. They explain why the entity tables look the way they do, and recur in the data-model mappings.
| # | Principle |
|---|---|
| 1 | Money is BIGINT in IRR. Toman is display-only; conversion happens only at a provider's API boundary. No floats anywhere on the money path. |
| 2 | The platform never legally holds buyer cash. Funds settle through a licensed PSP to registered IBANs (commission IBAN + nurse IBAN via تسهیم settlement-sharing, or one merchant-of-record account). "Escrow"/"nurse balance" are derived ledger states, never a Balinyaar cash balance. |
| 3 | ledger_entries is the financial source of truth. Every capture, commission, payout, refund, and clawback posts balanced double-entry rows. Per-table money fields stay the operational/pricing record; the ledger answers "how much do we owe nurses right now" and "how much is held but unreleased." |
| 4 | Fee split is captured per booking, never derived from live config, so historical reporting survives commission-schedule changes. The booking stores gross_price_irr, balinyaar_commission_irr, nurse_payout_amount. |
| 5 | PII is marked (encrypted) — national ID, IBAN, phone, addresses, clinical data — column- or app-level. Clinical data has stricter access than financial data. |
| 6 | Two-stage clinical disclosure is a hard rule. At the request stage the nurse sees only booking_requests.customer_notes; the full encrypted booking_care_instructions are exposed only after the booking is confirmed. Enforced at the authorization layer. |
| 7 | Soft deletes on users/nurse_profiles via deleted_at. Audit, payment, ledger, and payout records are never deleted. |
| 8 | Audit trail is append-only. All state transitions on bookings, payments, refunds, payouts, verifications, reviews, and platform_configs produce an audit_logs row. |
| 9 | Catalog/config tables are rows, not enums (service categories, verification step types, cancellation policies, Iranian holidays) so the business evolves without migrations. They carry name_fa/name_en. |
| 10 | Idempotency is mandatory on the money path. Every PSP/BNPL callback is stored raw in payment_webhook_events and deduplicated on external_event_id before any money-state mutation. |
| 11 | All timestamps are DATETIME2(7) UTC. Persian-calendar display is a UI concern — except that bank-closure scheduling uses the iranian_holidays table, because PAYA/SATNA transfers fail on holidays. |
| 12 | Derived flags must not drift. nurse_profiles.is_verified, rating aggregates, and the search index are written only by the code path that owns their source of truth, inside the same transaction. |
| 13 | Invariants are enforced, not documented: CHECK constraints (gross = commission + payout, rating BETWEEN 1 AND 5, amounts ≥ 0, end_time > start_time), filtered-UNIQUE for "one primary"/"one active", and tenancy checks (a booking's patient/address belongs to the same customer; its variant to the same nurse). |
The platform at a glance
Before the detail, here is the end-to-end path a single engagement travels. Sections 1–14 expand each stage.
- Onboard — customer registers with phone-OTP and adds a patient + address; a nurse registers and enters verification. (§1)
- Verify the nurse — six-step pipeline (identity, Shahkar, MoH license, INO, criminal record, IBAN ownership); only then do the nurse's variants become bookable. (§2)
- List & price services — admin defines the catalog skeleton; each nurse creates priced variants. (§3)
- Search & match — family filters by category, city/district, price, rating, and same-gender preference against a denormalized search index. (§4)
- Request → accept → pay → confirm — request (no money) becomes a booking (payment captured), which owns N sessions. (§5)
- Deliver with EVV — the nurse clocks in/out per session with GPS; payout is gated on EVV completion. (§6)
- Handle cancellation/refund — tiered, snapshotted policy; admin-only refunds decomposed across fee legs. (§7)
- Move the money — card or BNPL capture posts a balanced ledger entry; escrow is a ledger state. (§8–9)
- Pay the nurse — weekly batch to the verified IBAN after the dispute window, with clawback as fallback. (§10)
- Review, communicate, invoice — one moderated review per booking; ticket-only messaging; a minimal commission invoice with 10% VAT. (§11–14)
The four reference diagrams (domain map, booking spine, payments & payouts, financial lifecycle) plus an ER overview render this visually.
1Actors & Onboarding
Who can use the platform, how they prove who they are, and when. KYC is staged by role and risk — not collected up-front for everyone.
How onboarding works
- Three actor types: customer (the family member / payer), nurse (the independent caregiver / seller), and admin (Balinyaar back-office: support, finance, moderation, super-admin).
- Phone number is the primary login credential. Authentication is phone-OTP (one-time SMS code). 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 is frequently not the patient. A customer may register multiple patients.
- Each successful login creates a refresh-token session that can be revoked (logout, stolen-token detection).
KYC timing is role- and risk-staged
- 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 (§2) before any of their service variants become bookable.
national_idis populated only after the identity step passes. - An admin is provisioned internally with RBAC roles.
- Phone-OTP is the dominant Iranian login norm and is also the anchor for Shahkar SIM↔national-ID binding (§2).
- Storing
national_idonly post-KYC matches the reality that identity is verified through gated vendor APIs, not collected casually at signup. - 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.
Phone-OTP login; customer/nurse/admin roles; customer→patient (1:N); session management; admin RBAC; nurse onboarding gated on verification.
Customer national-ID KYC (customer_profiles.national_id_verified_at exists but unused at launch); push notifications; social login; nursing-company (organization) self-onboarding.
- users is the single identity record for every human actor;
role(nurse/customer/admin) decides which profile sub-table is populated. Phone is unique & encrypted;national_idstays NULL until KYC;genderlives here because nurse gender is matched for same-gender care. - users 1:1 → nurse_profiles / customer_profiles by role; 1:N → user_sessions (revocable refresh tokens) and user_roles.
- roles / user_roles implement RBAC for admin staff only (nurses/customers use
users.role);user_roleskeepsgranted_by/revoked_atfor an audit trail. - customer_profiles 1:N → patients (the care recipients) and customer_addresses (saved, encrypted, with coordinates for EVV distance checks; exactly one
is_primaryvia filtered UNIQUE). - Tenancy invariant: a booking's
patient_idandcustomer_address_idmust belong to the same customer.
2Nurse Verification & Credentials
Verified trust is the entire brand. Vetting is platform-owned, non-optional, and performed at the authoritative source — never delegated to families, never marketed as a check the platform does not actually perform. A nurse is bookable only after all required steps pass.
A data-driven pipeline
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 is 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 six verification steps
-
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 stolen-identity / alias fraud.
-
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.
-
MoH پروانه صلاحیت حرفهای — the single most important credential
The MoH-mandated professional-competency license for in-home nursing. It 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 launch method is nurse-uploaded document + manual admin verification against the official record.
-
نظام پرستاری (INO) membership — cross-check
The Iranian Nursing Organization membership number is captured and cross-checked (ino.ir) as a second source. Manual at launch.
-
عدم سوء پیشینه (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.
-
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 money-mule payout diversion.
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.
- 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. (See the verification pipeline deep-dive.)
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.
Automated MoH/INO license lookup (pending a B2B API); ML-driven fraud scoring (fraud_flags modeled but inactive); a professional-liability-insurance step (addable as a row when required).
- nurse_verifications is the master per-nurse header (1:1 with nurse_profiles); its
statusis the single source of truth for verification state and drives theis_verifiedflip. - verification_step_types is the admin catalog of pipeline steps with stable machine
codes and anis_automatedflag; 1:N → verification_steps (one row per step per nurse, with rawexternal_response_jsonfrom the KYC vendor andexpires_atfor time-limited steps). - verification_steps 1:N → verification_documents (object-storage key + integrity hash; files behind signed URLs, never public).
- nurse_credentials is the structured registry:
credential_type, encryptedcredential_number,holder_name_snapshot,issued_at/expires_at— drives renewal alerts & the trust badge, cross-referenced by the relevant step. - nurse_bank_accounts carries the IBAN-ownership result (
matched_national_id,account_holder_from_bank,ownership_vendor_ref) that gates the first payout; expiring steps/credentials raise support_alerts; every state change writes audit_logs.
3Service Catalog & Pricing
An admin-defined skeleton that each nurse fills with their own priced offerings. The model is deliberately configurable (EAV-style) so admins add new pricing dimensions without a migration.
Three admin layers, two nurse layers
- Admin defines the catalog skeleton: top-level service categories (e.g. مراقبت از سالمند / Elderly Care, مراقبت پس از جراحی / Post-Surgery Recovery, مراقبت از نوزاد / Infant Care, مدیریت بیماری مزمن / Chronic Illness Management) and configurable option groups (e.g. تعداد بیمار / patient count, نوع شیفت / shift type) each with concrete option values (e.g. ۱ نفر, ۲ نفر, شبانهروزی). New dimensions need no 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 price independently.
Price units that match real home nursing
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.
- Iranian competitors sell exactly these shapes — hourly / daily / 24-hour (شبانهروزی) shifts and multi-day packages — so
per_24handper_dayare 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_enpairs (Persian primary).
Admin categories + option groups/values; nurse variants with own price + price unit across all five units; activate/deactivate; snapshotting.
Holiday/surge pricing rules; a lighter "companionship / daily-living" tier (modeled as a future category); dynamic/tiered commission per category.
- service_categories (admin-managed top-level care types) 1:N → service_option_groups (the configurable dimensions; a NULL
service_category_id= cross-category) 1:N → service_option_values (concrete choices). - nurse_service_variants is the atomic bookable unit — N:1 to nurse_profiles and service_categories; carries
priceandprice_unit. Search and booking operate on the exact thing the customer pays for, not on "the nurse." - nurse_service_variant_options records one option value per dimension that defines a variant's configuration (
UNIQUE(variant_id, option_group_id)); N:1 to service_option_groups / service_option_values. - The variant feeds the denormalized nurse_search_index (§4) and is frozen into
bookings.variant_snapshot_jsonat booking time.
4Search & Matching
Families search by category, geography, price, and availability, sortable by rating — and the search must be cheap from day one. Same-gender matching is a near-hard requirement, surfaced before booking.
How search works
- Families search by service category, geography (city, 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.
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 the single most Iran-specific matching constraint. In Iranian bodily-care (bathing, toileting, intimate post-surgical care) same-gender caregiving is culturally decisive, not optional — every real elder/post-surgical bodily-care request implies it. The customer specifies a required caregiver gender on the request (required_caregiver_gender), and nurse gender is an exposed search filter so families narrow to same-gender caregivers up front, not after.
- District granularity varies: in Tehran, districts map to the 22 official municipal مناطق; in smaller cities they are major neighborhoods. Districts are optional.
- 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).
Category + city/district geo search; nurse_search_index denormalization; same-gender filter via required_caregiver_gender; rating sort.
Map-based discovery; availability-window filtering as a hard constraint (slots are soft guidance at launch); algorithmic ranking beyond rating; continuity-of-carer "preferred nurse" suggestions.
- nurse_search_index is a read-only projection: one row per covered area per bookable variant, holding nurse (verified+accepting), variant (category, price, unit), city/district,
nurse_gender, rating, and partner center. Maintained on writes to nurse_profiles, nurse_service_variants, nurse_service_areas, and reviews;is_searchable=1only when its source nurse/variant are bookable. - nurse_service_areas declares where a nurse travels (a NULL
district_id= whole city); N:1 to cities / districts withUNIQUE(nurse_id, city_id, district_id). - Same-gender matching pairs
users.gender(the nurse, exposed via nurse_profiles) andpatients.genderagainstbooking_requests.required_caregiver_gender— the requested constraint.
5Booking & Scheduling
The lifecycle is split into two tables so each table's invariants stay clean: a request phase (no money) and a booking phase (always implies captured payment). Single-visit and long multi-session engagements must both be representable.
Request → accept → pay → confirm
- Customer submits a booking request (nurse, patient, variant, address, date/time, requested caregiver gender, customer notes). Status pending_nurse_response.
- 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, rejects → rejected_by_nurse, or the deadline passes → expired_no_response. - On accept, a 30-minute payment window opens (
payment_deadline_at). The customer pays within it → abookingsrow is created (confirmed). If the window lapses → payment_deadline_expired.
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 — the engagement-to-session split is the core scheduling model. For a single visit, exactly one session is created so the EVV/payout path stays uniform.
Booking lifecycle (guarded transitions)
Allowed transitions are guarded explicitly (an allowed-transition table/CHECK) 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.
- 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.
Request→accept→pay→confirm lifecycle with response deadline + 30-min payment window; single-visit bookings; booking_sessions for multi-session engagements with per-session EVV and payout; explicit status-transition guards; snapshots; soft availability slots/exceptions.
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.
- booking_requests is pre-payment intent — carries
required_caregiver_gender,nurse_response_deadline_at,payment_deadline_at, and the request-stage-onlycustomer_notes(the only clinical context the nurse sees before accepting). N:1 to customer/nurse/patient/variant/address; 1:1 → bookings on conversion. - bookings exists only when accepted + paid; holds the three-way money split (
gross_price_irr,balinyaar_commission_irr,nurse_payout_amountwith CHECKgross = commission + payout),session_count,dispute_window_ends_at, both snapshots, andpartner_center_id. 1:N → booking_sessions. - booking_sessions is one row per visit (per-visit schedule,
visit_payout_amount,payout_eligible_at, status); 1:1 → visit_verifications (EVV per session, §6). - booking_care_instructions (1:1, encrypted) holds clinical/logistical context visible only post-confirmation; nurse_availability_slots / nurse_availability_exceptions are soft guidance for search.
- Tenancy invariant: the request's patient & address belong to
customer_id; its variant belongs tonurse_id.
6EVV / Service Delivery
Electronic Visit Verification is the authoritative record that a visit actually happened, for how long, and where — and it is the trigger that releases escrow.
How EVV works
- 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 and 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 (§10). For a multi-session engagement, payout accrues per completed session.
- 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.
Per-session GPS check-in/out, timestamps, address-match tolerance flag, no-show alerting, payout gated on EVV completion + closed dispute window.
Continuous geofencing during a live-in shift; supervisory tele-check-ins; family-visible live care logs; consented in-home cameras.
- visit_verifications is 1:1 with booking_sessions (the FK moved to
booking_session_idso each visit in a multi-session engagement is verified independently). It stores check-in/out GPS + timestamps,check_in_address_match(advisory), and a status whose mapping tobookings.statusis documented so the two state machines cannot diverge. - support_alerts receives no-show and location-mismatch flags for staff triage.
- platform_configs supplies
evv_location_tolerance_meters(the tolerance radius) and the no-show threshold — tunable without a deploy.
7Cancellation & Refunds
Cancellation/refund rules are tiered and structured, not a single blunt "default 100%". The applicable policy is snapshotted onto the booking, and every refund decomposes across the two fee legs.
Tiered, snapshotted policy
The platform defines cancellation_policies tiers by lead time and initiating actor:
| Scenario | Outcome |
|---|---|
| Free cancellation > 24h before start | Full refund, no fee. |
| Partial refund under 24h | e.g. 50% charge. |
| 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 — no customer self-service. 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.
- 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 (§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 (§8/§9).
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.
Automated nurse no-show penalty (manual admin action at launch); self-service partial-refund UI; holiday-specific cancellation overrides.
- cancellation_policies is config-driven tiers (
applies_tocustomer/nurse/admin,hours_before_start_min/max,refund_percentage,fee_amount_or_rate); the resolved tier is snapshotted onto the booking and the refund. - refunds are admin-only, N:1 to a payment_transactions row (1:N — partials allowed) and always linked to a tickets row. They carry the fee-leg decomposition (
platform_fee_refunded_irr,nurse_payout_refunded_irr),refund_channel, and the policy snapshot. - bookings holds the policy snapshot +
dispute_window_ends_at; when the nurse was already paid, the refund spawns a nurse_clawbacks receivable; every leg posts balanced ledger_entries.
8Payments & Escrow
The family pays the gross booking price through the platform; the platform models the money as an internal double-entry ledger state, never as cash it holds. This is the most-changed domain — the ledger is the financial source of truth.
The money flow
- The family pays the gross booking price 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. A minimal double-entry
ledger_entriesledger models money state: each money event posts balanced legs grouped by atransaction_group_id(Σ debit = Σ credit). 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.
Account types in the ledger
| 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. |
| psp_fee_expense | Gateway/PSP cost on the card leg. |
| nurse_clawback_receivable | Money a nurse owes back after a refund-after-payout. |
| bad_debt | Written-off uncollectable clawback. |
The canonical postings
Amounts are always positive; direction (debit/credit) carries the sign. Posted once, idempotently, keyed on the settling transaction.
DEBIT escrow_held gross_price_irr CREDIT platform_revenue balinyaar_commission_irr CREDIT nurse_payable nurse_payout_amount (= gross − balinyaar_commission)
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)
DEBIT platform_revenue platform_fee_refunded_irr DEBIT nurse_payable nurse_payout_refunded_irr CREDIT refund_payable (sum)
DEBIT nurse_clawback_receivable amount_irr (nurse_id set; nurse now owes the platform) CREDIT refund_payable amount_irr
Clawback recovered: DEBIT nurse_payable (next batch) / CREDIT nurse_clawback_receivable. There are no installment-level postings — the customer's repayment schedule is the BNPL provider's ledger, not Balinyaar's.
The three amounts, never conflated
| Amount | Meaning | Drives |
|---|---|---|
| gross_price_irr | What the customer is charged (the booking price) | The invoice; the inbound escrow_held debit; the refund base |
| balinyaar_commission_irr | Platform's own cut (was platform_fee_amount) | platform_revenue; the nurse payout |
| bnpl_commission_irr | The BNPL provider's merchant discount (on bnpl_transactions) | bnpl_fee_expense — a platform expense, never the nurse's |
nurse_payout_amount = gross_price_irr − balinyaar_commission_irr, enforced by CHECK.
- Settlement-sharing (تسهیم). The compliant marketplace primitive splits one incoming card payment across multiple registered IBANs (nurse share + platform 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.
- 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_eventsbefore 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 — so a retried success webhook cannot double-confirm.
- Multi-provider failover. 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 (the Toman/Jibit Nov-2024 suspensions cut businesses off mid-cycle).
- 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: 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.
- 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
BIGINTIRR internally to avoid float/rounding bugs.
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.
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.
- ledger_entries is the append-only double-entry source of truth: balanced legs per
transaction_group_id,account_type,direction,amount_irr, optionalnurse_id/booking_id, andsource_ref_type/source_ref_id. Per-nurse payable balance derives by filter, never a drifting cached column. - payment_transactions records every attempt against a booking; the succeeded row triggers confirmation. Hardened with filtered
UNIQUE(gateway_reference_code)and filteredUNIQUE(booking_id) WHERE status='succeeded'. N:1 to payment_gateways; 1:N → refunds / ledger_entries; 1:1 → bnpl_transactions if BNPL. - payment_webhook_events stores every callback raw, deduplicated on
UNIQUE(provider_code, external_event_id), upserted first inside the money-mutating transaction. - payment_gateways abstracts each PSP/BNPL provider (type
standard/bnpl, encryptedconfig_json) for failover; bookings carries the three amounts +platform_fee_rate+psp_fee_amount; payouts target verified nurse_bank_accounts IBANs.
9Installments / BNPL
BNPL is an alternative checkout. The decisive, verified finding is full-upfront settlement: the provider pays Balinyaar the full booking amount in one lump (net of its commission) and bears 100% of customer-default risk. So a BNPL order is, in Balinyaar's books, identical to a card payment landing net-of-fee.
How BNPL is modeled
- 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 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). - 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.
What bnpl_transactions captures
Provider, merchant-of-record (Balinyaar/partner center), 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.
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. See the BNPL deep-dive for the exact Q1 cancellation flow and the Q2 three-amount split.
- 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 اینماد (eNamad) for the Balinyaar/partner entity; whether a multi-vendor re-disbursing marketplace qualifies as a single BNPL merchant is publicly undocumented — an ops/contracting task, not a schema dependency.
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.
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.
- bnpl_transactions (replaces the old
installment_plans;installment_entriesis cut) is 1:1 with a payment_transactions row — the single inbound settlement to reconcile, plus the revert path. State-machine guard onstatusfor idempotency. - refunds on a BNPL order carry
refund_channel = 'bnpl_revert',external_revert_reference, andexpected_customer_refund_eta(the ~7–10 business-day window surfaced in UI/reconciliation). - The settlement posts the same balanced ledger_entries as a card capture, plus the
bnpl_fee_expenseleg; callbacks are deduplicated via payment_webhook_events.
10Payouts to Nurses
Nurses are paid in weekly batches, gated on EVV completion and a closed dispute window — because an Iranian bank transfer, once sent, is effectively irreversible. Clawbacks handle the refund-after-payout case.
How payouts work
- 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'ANDdispute_window_ends_at < now()(config-driven, default 72h post-completion). This prevents paying a nurse before a dispute can surface, shrinking clawback frequency. - The nurse payout amount derives from
gross_price_irr − balinyaar_commission_irr— never from a BNPL provider's net settlement. - Each booking is paid at most once (the payout↔booking link is unique), preventing double-pay across batches.
- 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.
- Clawbacks (
nurse_clawbacks) handle the refund-after-payout case: 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); a batch nets prior clawbacks (gross_earnings,clawback_applied,net_amount). - Bank-holiday-aware scheduling. Payout period-end and processing dates are shifted off bank-closed days using a shared
iranian_holidayscalendar — a weekly payout landing on a multi-day Nowruz closure would otherwise fail, since PAYA/SATNA transfers do not settle on closed days.
- 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.
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.
On-demand / instant nurse withdrawal; per-nurse configurable payout frequency; automated clawback recovery beyond netting.
- nurse_payout_batches is the weekly aggregation (holiday-aware
period_end, CHECKtotal_amount = Σ payouts) 1:N → nurse_payouts (one row per nurse per batch, withgross_earnings_irr,clawback_applied_irr,net_amount_irr,iban_snapshot,transfer_reference). - nurse_payouts 1:N → nurse_payout_booking_links with
booking_idUNIQUE — the structural anti-double-pay guard (exactly one payout per booking). - nurse_clawbacks (1:1 to a refunds row; N:1 to nurse/booking; links to the original & recovering payout) carries
statuspending/recovered/written_off. - iranian_holidays (with
is_bank_closed) drives date-shifting; every transfer posts balanced ledger_entries; eligibility readsbookings.dispute_window_ends_at; the target is a verified nurse_bank_accounts IBAN.
11Reviews, Trust & Safety
One review per completed booking, moderated before it goes public, with aggregates recomputed on every transition — because the buyers are vulnerable people cared for unobserved, and a single incident can destroy a fragile, trust-first brand.
How reviews & safety work
- 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_alertsrow 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.
- 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).
One-per-completed-booking customer reviews; moderation with full recompute-on-every-transition; low-rating support_alerts; manual incident suspension.
Two-way (nurse-reviews-customer) double-blind reviews with timed reveal; structured review-tag aggregation (review_tags_master / review_tag_links modeled but phase-2); a dedicated incidents entity; ML fraud scoring.
- reviews is 1:1 with a bookings row (creation allowed only for completed/closed bookings); rating CHECK 1–5; every status transition recomputes the denormalized aggregates on nurse_profiles (
average_rating,total_reviews), fixing the inflated-rating-after-hide drift. - reviews N:N review_tags_master via review_tag_links for quantitative tag aggregation (phase-2 nicety).
- A low rating raises a support_alerts row; sensitive transitions write audit_logs.
12Messaging & On-Site Emergencies
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 — a deliberate anti-disintermediation and patient-safety design.
How communication works
- All post-booking communication runs through tickets admin can read in full — 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 (§7).
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). No schema change — an operational rule.
- 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.
Ticket-only messaging (admin-readable); auto-created booking-coordination ticket; internal notes; prominent in-app emergency contact + documented playbook.
Real-time chat; a first-class incidents/emergency-event entity with SLA; push/real-time alerting.
- tickets (human-facing
reference_code, optionally linked to a bookings or refunds row) 1:N → ticket_participants (UNIQUE(ticket_id, user_id)) and ticket_messages (is_internalkeeps admin-only notes out of user view). - The emergency contact number is read from the encrypted booking_care_instructions and surfaced in the booking UI.
- Escalations and anomalies become support_alerts for staff.
13Tax, Invoicing & Legal
The nurse is the taxable seller of the nursing service; Balinyaar is the taxable seller only of its commission. A partner licensed center is the launch legal vehicle that makes the whole money flow legal.
The tax/invoice split
- 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
invoicesrecord 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. Held by the legal launch entity.
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 (the Asanism model) 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. (See the legal landscape deep-dive.)
- 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.
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.
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.
- invoices is 1:1 with a bookings row:
invoice_number,issuing_entity_type(platform/partner_center),gross_irr,platform_commission_irr(the VAT-relevant line),bnpl_commission_irr, config-drivenvat_rate/vat_irr, and مودیان fields (moadian_reference_number,moadian_status) +pdf_storage_key. - partner_centers holds the MoH license (
moh_establishment_permit_no),enamad_code,settlement_iban, andis_merchant_of_record; 1:N → nurse_profiles (sponsors, vianurse_profiles.partner_center_id), bookings (legally covered by), and invoices (issuer). - payment_transactions supplies the Shaparak reference for reconciliation; platform_configs holds the VAT rate and merchant-of-record settings.
14Notifications & Admin / Backoffice
In-app notifications keep users informed; a back-office tooling spine lets admins verify nurses, refund, pay out, triage alerts, and audit everything — all scoped by RBAC.
Notifications
- 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 — 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.
Append-only audit trail
Every state-changing operation on sensitive entities (bookings, payments, refunds, verifications, reviews, users) and config changes (e.g. the platform fee rate) are auditable.
- 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_holidaysfor 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.
In-app notifications with 90-day retention; admin verification/refund/payout/alert tooling; RBAC; append-only audit_logs; config-change auditing.
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.
- notifications (N:1 to users) carries a typed
data_jsonpayload for front-end deep-linking; a retention job deletes read rows > 90 days. - support_alerts are staff worklist items (never shown to users); roles/user_roles scope admin permissions.
- audit_logs is append-only over every sensitive transition including platform_configs changes; system_events is high-volume analytics (piped to a warehouse at scale); platform_configs holds runtime business parameters (fee rate, deadlines, dispute window, VAT, tolerance).
- The tooling acts on the operational entities of every prior section: nurse_verifications/verification_steps/nurse_credentials, refunds, nurse_payout_batches/nurse_payouts/nurse_clawbacks, bookings.
Legal landscape (Iran)
You can legally build this in Iran — but it is a licensed healthcare activity, not a free-to-launch marketplace. Operating without a permit is what is illegal, and penalties escalate to permanent revocation and judicial referral.
The governing framework
Licensing flows through the MoH Treatment Deputy (معاونت درمان), after approval by the Article-20 medical-affairs commission (کمیسیون قانونی تشخیص امور پزشکی موضوع ماده ۲۰), under the Medical Affairs Law of 1334 (amended 1367) and the home-care bylaw approved 1378/7/17 (9 Oct 1999). Each center receives one establishment permit (پروانه تأسیس) and one technical-director license (پروانه مسئول فنی).
Two regulatory tracks — pick the nursing track
| Home Nursing Services Center (the vehicle) | Home Clinical Care Center | |
|---|---|---|
| Persian name | مرکز مشاوره و ارائه مراقبتهای پرستاری در منزل | مرکز خدمات و مراقبتهای بالینی در منزل |
| Governed via | Iranian Nursing Organization (نظام پرستاری) | MoH directly |
| Who can found / direct | A nurse — BSc nursing + ≥5 years clinical experience (can be both founder & technical director) | Both founder & technical director must be physicians |
| Fit | ✓ Elderly / post-surgery / infant / chronic home nursing | Only with a physician partner |
How the model must operate
- Care must be delivered in the patient's home; performing services at the center's HQ is prohibited. The licensed center is therefore a dispatch/coordination entity, not a walk-in clinic — which structurally fits a matchmaking/dispatch platform.
- After principal approval (موافقت اصولی), the founder has up to one year to ready the center for final inspection before operating.
- e-namad (نماد اعتماد الکترونیکی) is required for an Iranian site providing online services/sales and is de-facto mandatory for a monetized site, because PSP/Shaparak rules require e-namad to obtain an online payment gateway (IPG).
The #1 strategic recommendation: register a Home Nursing Services Center, then go to market fast via the Asanism model — partner with already-licensed centers while the platform's own permit is in process. This launches the tech/brand/marketplace layer legally and quickly, then brings supply in-house over time. The partner_centers entity makes this representable: it is the legal vehicle, plausibly the IPG merchant-of-record, and the BNPL onboarding gate (which needs a جواز کسب + eNamad the center holds, not each nurse).
- Current (1404–1405) registered-company count and the present status of the سخت و زیانآور (arduous-work) labor-law gap — home-care nurses fall outside the regime that benefits hospital nurses; confirm whether any legislation has closed it.
- Whether a tech-first marketplace can operate by subcontracting only to already-licensed partner centers without holding its own permit initially.
- Full capital, facility, staffing, and insurance requirements for the nursing-services-center track.
- Worker classification (neutral marketplace vs employer/agency) with labor counsel — the highest-stakes structural decision; the "control-for-quality + contractor-for-cost" middle is exactly what triggers misclassification liability. Tax/VAT/company-structure specifics with a local accountant.
Verification pipeline (the operative detail)
"Is this nurse really who they say, and really licensed?" splits into two checks that are separate pipeline stages: a license check (are they a registered nurse?) and an identity + background check (are they who they claim, with no disqualifying record?). The cautionary tale is the "imposter nurse" who defeated agencies with stolen identities and forged documents — so verify at source, bind to national ID + liveness, and never trust an uploaded PDF alone.
| Stage | Goal | Iran tool / how | Programmatic? |
|---|---|---|---|
| 0. Consent | Lawful basis to verify + store data | Explicit in-app consent at onboarding | n/a |
| 1. Identity | Match person ↔ کد ملی ↔ phone ↔ face | Shahkar + national-ID validity + video/photo liveness vs national card, via one KYC vendor (Finnotech / U-ID / Jibbit / Farashensa / Verify / Kavoshak) | Yes — off-the-shelf API |
| 2. License | Verify nursing credential at source | MoH پروانه صلاحیت حرفهای (Rn.behdasht.gov.ir) as primary + INO نظام پرستاری number (ino.ir) as cross-check | Manual — require upload + verify (no public API) |
| 3. Criminal record | No disqualifying record | عدم سوء پیشینه — nurse self-requests via adliran.ir / ثنا and uploads; partly covered by the MoH license | No company API — consent-gated, nurse-uploaded |
| 4. Ongoing monitoring | Catch revocations/expiry | Periodic re-verification of license validity + re-request of عدم سوء پیشینه; re-run Shahkar on phone change | Semi-manual; emulate Nursys e-Notify |
- Buy identity verification through one KYC provider — it shifts the regulator-gated Shahkar / ثبت احوال access burden onto a vendor that already holds the agreements.
- Anchor the license check on the MoH پروانه صلاحیت حرفهای — it is State-mandated for in-home nursing and bundles a criminal screen.
- Treat the criminal certificate as nurse-supplied + consent-gated.
- Build continuous monitoring, not one-and-done.
- Route through a licensed KYC intermediary to keep data-protection exposure compliant.
These five stages map onto the data-driven verification_step_types rows of §2, with the structured numbers captured in nurse_credentials, the raw vendor responses in verification_steps.external_response_json, and the IBAN-ownership inquiry result on nurse_bank_accounts.
Escrow as a ledger, not held cash
Because Balinyaar cannot custody buyer funds, "escrow" must be a software construct: a double-entry ledger state over money that legally sits at a licensed provider/bank. The original model had no ledger — escrow was only inferable by joining status flags, with no single answer to "how much do we owe nurses right now?" Three critiques rated this a critical gap. The fix is one append-only table.
The rails & the custody prohibition
- Every card payment is acquired by a licensed PSP and cleared through Shaparak, which settles to bank-registered IBANs (شِبا). There is no native marketplace-escrow construct that holds buyer cash in trust.
- A پرداختیار (payment facilitator) is explicitly forbidden from holding customer deposits, operating wallets, paying interest, granting credit/guarantees, or temporarily using merchant balances. Settlement goes only to merchant-registered accounts, and only Shaparak can withdraw from the special facilitator settlement account (حساب ویژه پرداختیاری). This is the single load-bearing constraint of the whole design.
- تسهیم (settlement-sharing) is the compliant primitive: one incoming card payment is split across multiple registered IBANs and credited directly by Shaparak/the provider — the platform never touches the split.
- The banned move: "collect into a platform pool, hold until EVV, then redistribute" — Shaparak banned inter-facilitator/inter-merchant transfers and wallet-style holding. A bank-grade escrow product (Vandar میندو / معاملات امن) is the only true hold/release/refund mechanism, and even its EVV-trigger is unverified.
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 + status booleans) 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; ledger_entries is the financial-truth / reconciliation layer posted alongside. The canonical postings (card capture, BNPL settle, refund pre-payout, clawback post-payout) are in §8.
In Nov 2024 the CBI abruptly cut Toman's and Jibit's settlement/withdrawal services with no stated cause, stranding businesses (including millions of Snapp drivers). Wallet/balance facilitator models have been blocked and re-permitted before (Vandar's gateway). Design for multi-provider failover and a reconciliation ledger that survives a provider being cut off mid-cycle — which is exactly what the provider-abstracted payment_gateways + the append-only ledger_entries deliver.
BNPL mechanics — the two hard questions
All mainstream Iranian provider-financed BNPLs (SnappPay, Digipay, Tara, Torob Pay, ZarinPlus) use full-upfront settlement: the provider pays the merchant the whole amount minus commission in one lump and bears default risk; the customer's installments are owned by the provider and decoupled from Balinyaar's escrow/payout. Lendo is the outlier (bank-financed, customer pays interest) — avoid for the MVP.
Provider comparison (the structurally important facts)
| Provider | Settlement | Who bears cost | Customer plan | Merchant fee |
|---|---|---|---|---|
| SnappPay | Full-upfront, single lump minus commission; provider bears default risk | Merchant (commission) | 4 installments / 4 months, interest-free | Undocumented (anecdotal ~7–15%); per-contract config |
| Digipay | Full-upfront to contracted merchant; provider bears default risk | Customer markup + merchant acquiring commission | 1-month + 4-installment + 3/6/9/12-mo loan | توافقی (negotiable); sells early-settlement as an add-on |
| Tara | Provider-financed, full amount to seller | Merchant (interest-free to customer) | 2 interest-free installments, starting 1 month after | Per-contract |
| Torob Pay | Full-upfront, cash to seller | Merchant | 4 equal installments, 25% down, interest-free | Concrete: 6% + VAT = 6.6% |
| Lendo avoid | Bank-financed (Bank Ayandeh) | Customer (~18–23% interest + ~5% upfront fee) | 6 / 9 / 12 months — a POS loan | — |
Q1 — Cancellation / refund of a BNPL booking mid-plan
Money always flows customer ↔ provider ↔ Balinyaar. Never refund the customer directly; never route a nurse→customer refund. Balinyaar initiates the reversal through the provider's API using the stored token:
- Full cancel/refund →
revert(full amount). - Partial / shortened-visit →
update(new amount strictly lower) orcancelper the provider's partial semantics.
The provider then, on its own ledger and asynchronously: (1) cancels the customer's remaining unpaid installments and credits the equivalent back to their credit wallet (reusable BNPL credit), and (2) refunds any already-paid installment to the customer's bank in ~7–10 business days. The merchant's only role is to authorize/cancel; the provider owns the unwind.
Balinyaar's internal bookkeeping
- Record a
refundsrow withrefund_channel = 'bnpl_revert',external_revert_reference,expected_customer_refund_eta;refund_statusstays processing until a reconciliation job confirms. Surface the 7–10-day window in UI/reconciliation. - Decompose across the two fee legs:
platform_fee_refunded_irrandnurse_payout_refunded_irr. - Post balanced ledger entries and record the revert reference on the
bnpl_transactionsrow (reverted_amount_irr,reverted_at). - If the nurse has NOT been paid: reverse the
nurse_payableaccrual — clean, nothing leaves Balinyaar (the common case if payout is gated on the dispute window). - If the nurse HAS been paid: the clawback path — a
nurse_clawbacksrow + anurse_clawback_receivableleg, recovered from the next batch or written off.
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 — never hardcode.
Q2 — Under BNPL, who pays the nurse, and when?
Balinyaar pays the nurse, on its own normal weekly schedule, after EVV check-out and after the dispute window closes — exactly the same path as a card-funded booking. The provider never pays the nurse and is indifferent to the internal split. The nurse's payout is computed from gross_price_irr − balinyaar_commission_irr, NOT from the BNPL-net amount.
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. The only difference a BNPL order makes is the extra bnpl_fee_expense leg that reduces Balinyaar's margin.
Integration notes
- SnappPay (primary) — API + IPG redirect; verified endpoint flow: OAuth token → eligible → payment token (redirect) → verify → settle → revert/cancel/update/status.
- Digipay (secondary) — unified UPG gateway; persist the gateway type per transaction (IPG=0, Wallet=11, Credit=5, BNPL=13, Credit-Card=24); deliver/refund calls must carry the matching code; gate
deliveron the nurse's EVV check-out; each purchase supports either refund OR manual reverse, not both. - Cross-cutting: webhook idempotency via
payment_webhook_eventskeyed onexternal_event_id, written first; never trust the callback alone — alwaysverifyserver-side and re-check amount + reference; amounts in IRRBIGINT, converting from Toman only at the boundary; a state-machine guard on BNPL status transitions. - Recommendation: integrate SnappPay first, Digipay second, avoid Lendo.
Market & competitors
The market is real and already competitive — but incumbents are heavily concentrated in Tehran/Karaj and run mostly as direct-dispatch staffing, not trust-first marketplaces. That is the gap. The hardest problem is trust and safety, not technology.
Iranian players
| Player | Model | Notable facts |
|---|---|---|
| Asanism (آسانیسم) | Matching/marketplace supplying caregivers through licensed partner centers (intermediary) | Markets identity-vetting, a reported ~40M-toman security note, 24–48hr trial periods. ~99% concentrated in Tehran/Karaj. The model the launch vehicle imitates. |
| Snapp Doctor | Health vertical of Snapp; managed dispatch | Operates in several cities. Holds a general online-medical-intermediary license — not a specific home-nursing MoH authorization. |
| Salamat Aval | Direct dispatch of its own nurses (not an open marketplace) | 3,000+ active personnel (self-reported), 24/7 call center, holds official MoH license no. 388180-3 (displays it prominently). Pricing توافقی. |
| Hirad | App-based managed staffing/dispatch | Shows both sides; advertises no placement fee; states it operates under MoH authorization. Modest adoption. |
What this tells you: the dominant model is direct/managed dispatch, not a true trust-first two-sided marketplace; geographic concentration is extreme (Tehran/Karaj dominate; second-tier cities thinly served — the clearest white space); pricing is opaque/negotiable (transparency is a differentiator); and "licensed" is a real, displayed trust signal. The closest international fit is the Homage model (curated marketplace + human matching), and India shows that where licensing infrastructure is weak, vetting and quality control are the product.
The risks that shape the build
Connecting strangers to vulnerable people without rigorous platform-owned vetting enables theft, abuse, and fraud — and the public blames the platform (Care.com's records-laden listings; the "imposter nurse" with 20+ aliases / 7 SSNs / forged docs). Own the vetting; verify at source; bind to national ID + liveness; re-verify periodically.
Worker misclassification ($10M TLC judgment), vicarious liability / negligent hiring, and insurance gaps stack. The dangerous middle — heavy control for "quality" but contractor classification for cost — is what triggers misclassification judgments. Keeping the nurse's per-request accept/reject autonomy is a deliberate hedge.
Extreme caregiver churn, no-shows that strand a patient, and disintermediation (families + nurses pairing off-platform). Beat leakage with retained value, not lock-in: EVV, a backup-coverage guarantee, in-platform escrow/dispute protection, and insurance that only applies on-platform — precisely the EVV, ticket-only messaging, escrow-ledger, and review mechanisms modeled above.
Gig-marketplace fraud runs ~2× elsewhere (>90% impersonation); financial elder abuse is real (1 in 9 known-perpetrator cases is a non-family caregiver). Tie reviews to verified, completed, on-platform bookings; strong identity verification at onboarding; in-platform escrow with dispute resolution.
The whole data model
The complete schema (Revision 2): ~53 tables across 13 domains. Each domain below lists its tables with key fields/columns and relationships (foreign keys). Net change vs the original 45: −2 cut (installment_plans replaced, installment_entries removed), +10 added, 1 replaced — the financial core is now a single ledger, BNPL is one settlement row, and the clawback / dispute-window / idempotency / license / multi-session gaps are closed.
Scope & change legend
Conventions: PII columns are (encrypted). Money is BIGINT IRR. Timestamps are DATETIME2(7) UTC. Most tables also carry created_at/updated_at (and deleted_at where soft-deleted); these are omitted from the key-field lists for brevity.
The 13 domains at a glance
Domain 1 — Identity & Access
One identity table for every human actor avoids three near-duplicate user tables; role decides which profile sub-table is populated. Phone-as-primary matches Iranian OTP norms and is what Shahkar matches against.
| Table | Key fields | Relationships (FK) |
|---|---|---|
users CORE |
id PK · email (enc, nullable) · phone (enc, UNIQUE) · national_id (enc, nullable) · national_id_verified_at · first_name/last_name · gender NEW · role (nurse/customer/admin) · shahkar_verified_at NEW · is_active · last_login_at/_ip · deleted_at | 1:1 → nurse_profiles / customer_profiles (by role); 1:N → user_sessions, user_roles, notifications, ticket_participants. Referenced as *_by_admin_id across the schema. |
user_sessions CORE |
id · user_id · refresh_token_hash · device_info · ip_address · is_revoked · revoked_at · expires_at | N:1 → users. Enables logout-everywhere & stolen-token revocation. |
roles CORE |
id · name (super_admin/admin/support/finance/moderator) · description | N:N users via user_roles. Admin staff only. |
user_roles CORE |
user_id · role_id · granted_by · granted_at · revoked_at | Join table; keeps a grant/revoke audit trail. |
nurse_profiles CORE |
id · user_id (UNIQUE) · partner_center_id NEW · bio · years_of_experience · education_level/_field · specializations_json · is_verified (guarded) · is_accepting_bookings · average_rating/total_reviews/total_completed_bookings (denorm). CUT verification_status, response_rate, profile_completion_score |
1:1 → users, nurse_verifications; 1:N → nurse_service_variants, nurse_service_areas, nurse_bank_accounts, nurse_credentials, bookings, nurse_payouts, nurse_clawbacks; N:1 → partner_centers. |
customer_profiles CORE |
id · user_id (UNIQUE) · default_emergency_contact_name/_phone (enc). CUT national_id_verified_at (deferred customer KYC) |
1:1 → users; 1:N → patients, customer_addresses, booking_requests, bookings. |
patients CORE |
id · customer_id · display_name · first_name/last_name · birth_date · gender · blood_type · initial_medical_notes (enc) · is_active | N:1 → customer_profiles; 1:N → booking_requests, patient_care_records. Tenancy: a request's patient must belong to the same customer. |
customer_addresses CORE |
id · customer_id · city_id · district_id · address lines (enc) · latitude/longitude (EVV) · is_primary — filtered UNIQUE(customer_id) WHERE is_primary=1 |
N:1 → customer_profiles, cities, districts; referenced by booking_requests/bookings. |
nurse_bank_accounts CORE |
id · nurse_id · bank_name · account_holder_name (enc) · iban (enc) · iban_hash NEW (UNIQUE) · matched_national_id NEW · account_holder_from_bank NEW · ownership_vendor_ref NEW · is_primary · is_verified — filtered UNIQUE(nurse_id) WHERE is_primary=1 |
N:1 → nurse_profiles; 1:N → nurse_payouts. The single place real money leaves the platform. |
Domain 2 — Geographic Data
A table (not a static list) so new cities/districts launch without a deploy; sort_order/is_active drive ordered, toggleable dropdowns. Districts are optional (a nurse can cover a whole city).
| Table | Key fields | Relationships (FK) |
|---|---|---|
provinces CORE | id · name_fa/name_en · sort_order · is_active | 1:N → cities. |
cities CORE | id · province_id · name_fa/name_en · sort_order · is_active | N:1 → provinces; 1:N → districts; referenced by customer_addresses, nurse_service_areas. |
districts MVP | id · city_id · name_fa/name_en — Tehran's 22 مناطق or major neighborhoods | N:1 → cities; referenced by customer_addresses, nurse_service_areas. |
nurse_service_areas CORE | id · nurse_id · city_id · district_id (NULL = whole city) — UNIQUE(nurse_id, city_id, district_id) | N:1 → nurse_profiles, cities, districts. Drives the geo filter in search cheaply. |
Domain 3 — Services & Pricing
Three admin layers (category → option group → option value) + two nurse layers (variant → variant option). This EAV-style configurability lets admins add a new pricing dimension without a migration; the only addition is a denormalized read model for search.
| Table | Key fields | Relationships (FK) |
|---|---|---|
service_categories CORE | id · name_fa/name_en · icon · sort_order · is_active | 1:N → service_option_groups, nurse_service_variants. The primary search dimension. |
service_option_groups CORE | id · service_category_id (NULL = cross-category) · name_fa/name_en · is_required | N:1 → service_categories; 1:N → service_option_values. |
service_option_values CORE | id · option_group_id · name_fa/name_en · sort_order | N:1 → service_option_groups. |
nurse_service_variants CORE | id · nurse_id · service_category_id · display_name · price · price_unit (per_hour/per_session/per_half_day/per_day/per_24h) · estimated_duration · is_active | N:1 → nurse_profiles, service_categories; 1:N → nurse_service_variant_options, booking_requests. The atomic bookable unit. |
nurse_service_variant_options CORE | id · variant_id · option_group_id · option_value_id — UNIQUE(variant_id, option_group_id) | N:1 → nurse_service_variants, service_option_groups, service_option_values. |
nurse_search_index CORE NEW | id · variant_id · nurse_id · service_category_id · price/price_unit · city_id/district_id (fan-out) · nurse_gender · average_rating/total_reviews · is_searchable | Read-only projection maintained on writes to nurse_profiles, nurse_service_variants, nurse_service_areas, reviews. is_searchable=1 only when source is bookable. |
nurse_availability_slots MVP | id · nurse_id · day_of_week (0=Sat…6=Fri) · start_time/end_time — CHECK end_time > start_time | N:1 → nurse_profiles. Soft guidance only. |
nurse_availability_exceptions MVP | id · nurse_id · exception_date · is_available · reason | N:1 → nurse_profiles. Date overrides; informs search, never blocks. |
Domain 4 — Verification & Credentials
Data-driven (step types are rows) plus a structured credential registry, because the brand is "verified trust" and renewal tracking needs queryable license numbers, not opaque PDFs.
| Table | Key fields | Relationships (FK) |
|---|---|---|
nurse_verifications CORE | id · nurse_id (UNIQUE) · status (not_started/pending/in_review/approved/rejected/suspended) · submitted_at/approved_at/rejected_at/suspended_at · rejection_reason · reviewed_by_admin_id · internal_notes | 1:1 → nurse_profiles; 1:N → verification_steps. The single source of truth for verification state. |
verification_step_types CORE | id · code (identity_kyc/shahkar_match/moh_competency_license/ino_membership/criminal_record/bank_account_verification) · name_fa/name_en · is_required · is_automated · automation_provider · sort_order | 1:N → verification_steps. Admin catalog — a new requirement is one INSERT. |
verification_steps CORE | id · nurse_verification_id · step_type_id · status · is_automated (snapshot) · external_response_json (KYC vendor audit) · expires_at · reviewed_by_admin_id — UNIQUE(nurse_verification_id, step_type_id) | N:1 → nurse_verifications, verification_step_types; 1:N → verification_documents. On expiry of a time-limited step it reverts to pending + raises a support_alerts. |
verification_documents CORE | id · verification_step_id · storage_key · integrity_hash · file_type · uploaded_at | N:1 → verification_steps. Metadata only; bytes live in S3-compatible storage behind signed URLs. |
nurse_credentials MVP NEW | id · nurse_id · credential_type (moh_competency_license / ino_membership / criminal_record) · credential_number (enc) · holder_name_snapshot · issuing_authority · issued_at/expires_at · verification_source · verification_method (manual/portal/api) · verified_by_admin_id | N:1 → nurse_profiles. Cross-referenced by the relevant verification_steps. Powers renewal alerts & the trust badge. |
Domain 5 — Booking & Scheduling
Two distinct phases — the request phase (pre-payment intent) and the booking phase (post-payment commitment). The previous model's biggest gap — single-visit-only bookings — is fixed with booking_sessions.
| Table | Key fields | Relationships (FK) |
|---|---|---|
booking_requests CORE | id · customer_id/nurse_id/patient_id/variant_id/customer_address_id · required_caregiver_gender NEW · requested_date/_time_start/_time_end · customer_notes (unenc, request-stage) · status · nurse_response_deadline_at · payment_deadline_at · nurse_rejection_reason | N:1 → customer_profiles, nurse_profiles, patients, nurse_service_variants, customer_addresses; 1:1 → bookings on conversion. Tenancy: patient/address belong to customer; variant to nurse. |
bookings CORE | id · booking_request_id (UNIQUE) · customer_id/nurse_id/patient_id/variant_id/customer_address_id · variant_snapshot_json · address_snapshot_json (enc) · partner_center_id NEW · gross_price_irr CHG · balinyaar_commission_irr CHG · platform_fee_rate · nurse_payout_amount · psp_fee_amount NEW · session_count NEW · status (guarded) · dispute_window_ends_at NEW · completed_at. CUT payout_released — CHECK gross = commission + payout | 1:1 ← booking_requests; 1:N → booking_sessions, payment_transactions, ledger_entries; 1:1 → booking_care_instructions, reviews, invoices; referenced by nurse_payout_booking_links, refunds, nurse_clawbacks. |
booking_sessions MVP NEW | id · booking_id · session_index (1-based) · scheduled_date/_time_start/_time_end · visit_payout_amount · status (scheduled/in_progress/completed/missed/cancelled) · payout_eligible_at · cancellation_event_id | N:1 → bookings; 1:1 → visit_verifications. One row per visit; a single-visit booking still gets exactly one session. |
booking_care_instructions CORE | id · booking_id · current conditions · medications · allergies · special instructions · emergency contact — all (enc) | 1:1 → bookings. Visible only post-confirmation (Principle 6). |
visit_verifications CORE | id · booking_session_id CHG · check_in_at/check_out_at · check_in_lat/_lng · check_out_lat/_lng · check_in_address_match (advisory) · status | 1:1 → booking_sessions. EVV per session; status maps to bookings.status (documented). |
cancellation_policies MVP NEW | id · code (UNIQUE, e.g. standard_24h) · applies_to (customer/nurse/admin) · hours_before_start_min/_max · refund_percentage · fee_amount_or_rate · is_active | Referenced (snapshot) by refunds and the cancellation event on a session/booking. |
Domain 6 — Payments, Ledger & Refunds
The most-changed domain. A double-entry ledger is the source of truth (replacing inference from scattered status flags), with the idempotency and clawback primitives any real-money platform needs before launch.
| Table | Key fields | Relationships (FK) |
|---|---|---|
payment_gateways CORE | id · name · type (standard/bnpl) · config_json (enc secrets: client_id/secret, merchant no, base_url, sandbox flag) · is_active | 1:N → payment_transactions, payment_webhook_events. Abstracted for failover (Toman/Jibit cut-off precedent). |
payment_transactions CORE | id · booking_id · customer_id · gateway_id · amount · currency · status · gateway_transaction_id · gateway_reference_code (Shaparak) · gateway_response_json · is_installment — filtered UNIQUE(gateway_reference_code) + filtered UNIQUE(booking_id) WHERE status='succeeded' | N:1 → bookings, payment_gateways; 1:1 → bnpl_transactions (if BNPL); 1:N → refunds, ledger_entries. |
payment_webhook_events CORE NEW | id · provider_code · external_event_id — UNIQUE(provider_code, external_event_id) · event_type · signature_valid · payload_json · processing_status (received/processed/failed/ignored) · related_payment_transaction_id · received_at/processed_at | N:1 → payment_gateways; optional → payment_transactions. Upserted first; no-ops on duplicate. |
refunds CORE | id · payment_transaction_id · booking_id · requested_by_customer_id · ticket_id · amount · refund_percentage · status · gateway_refund_reference · platform_fee_refunded_irr NEW · nurse_payout_refunded_irr NEW · refund_channel NEW (psp_card/bnpl_revert/manual_bank) · external_revert_reference NEW · expected_customer_refund_eta NEW · cancellation_policy_code NEW | N:1 (1:N per txn CHG) → payment_transactions, bookings, customer_profiles, tickets; 1:1 → nurse_clawbacks (only when nurse already paid). Admin-only. |
ledger_entries CORE NEW | id · transaction_group_id (UUID) · account_type (escrow_held/platform_revenue/nurse_payable/refund_payable/bnpl_fee_expense/psp_fee_expense/nurse_clawback_receivable/bad_debt) · nurse_id · direction (debit/credit) · amount_irr · booking_id · source_ref_type/source_ref_id · memo — append-only, balanced per group | N:1 → bookings; logical links to payment_transactions/refunds/nurse_payouts/bnpl_transactions via source_ref_*. The financial source of truth. |
nurse_clawbacks CORE NEW | id · nurse_id · booking_id · refund_id · original_payout_id · amount_irr · status (pending/recovered/written_off) · recovered_in_payout_id · resolved_at | N:1 → nurse_profiles, bookings; 1:1 → refunds; → nurse_payouts (original & recovering). |
invoices MVP NEW | id · booking_id · invoice_number (UNIQUE) · issuing_entity_type (platform/partner_center) · gross_irr · platform_commission_irr (VAT-relevant) · bnpl_commission_irr · vat_rate (0.10) · vat_irr · moadian_reference_number · moadian_status · pdf_storage_key | 1:1 → bookings; N:1 → partner_centers (when issuer). |
Domain 7 — Payouts to Nurses
Weekly aggregation matching the PAYA settlement cycle, holiday-aware, with a structural anti-double-pay guard.
| Table | Key fields | Relationships (FK) |
|---|---|---|
nurse_payout_batches CORE | id · period_start/period_end (holiday-shifted) · total_amount · payout_count · status · initiated_by_admin_id · processed_at · failure_notes — CHECK total_amount = Σ payouts | 1:N → nurse_payouts. |
nurse_payouts CORE | id · batch_id · nurse_id · bank_account_id · iban_snapshot (enc) · amount · booking_count · status · transfer_reference · gross_earnings_irr NEW · clawback_applied_irr NEW · net_amount_irr NEW | N:1 → nurse_payout_batches, nurse_profiles, nurse_bank_accounts; 1:N → nurse_payout_booking_links; referenced by nurse_clawbacks. |
nurse_payout_booking_links CORE | id · payout_id · booking_id (UNIQUE) · amount_irr | N:1 → nurse_payouts; 1:1 → bookings. Guarantees a booking is paid in exactly one batch. |
Domain 8 — BNPL / Installments
Because verified research shows Iranian provider-financed BNPL settles the full amount to the merchant in one lump, a BNPL order is — in these books — a card payment that lands net-of-fee. The old installment_plans + installment_entries subsystem (which tried to track the customer's repayment and default) is deleted.
| Table | Key fields | Relationships (FK) |
|---|---|---|
bnpl_transactions MVP NEW | id · payment_transaction_id (UNIQUE) · provider_code (snapppay/digipay/tara/torobpay) · merchant_of_record · external_payment_token · external_transaction_id · eligibility_status · order_amount_irr · settled_amount_irr · bnpl_commission_irr · currency · installment_count (info, default 4) · status (eligible/token_issued/verified/settled/reverted/cancelled/failed) · settled_at · revert_transaction_id/reverted_amount_irr/reverted_at · refund_channel · callback_payload_json | 1:1 → payment_transactions. State-machine guard on status for idempotency. Replaces installment_plans; installment_entries removed. |
bnpl_settlement_entries DEFERRED | Modeled-but-inactive: only needed if a future provider uses tranched settlement (pays the platform over time). No mainstream Iranian provider does today. | Would be N:1 → bnpl_transactions. Adding it later is a purely additive migration. |
Domain 9 — Messaging (Ticket System)
All post-booking communication, admin-readable, with no direct nurse↔customer channel — it protects vulnerable patients, creates dispute evidence, and prevents disintermediation.
| Table | Key fields | Relationships (FK) |
|---|---|---|
tickets CORE | id · reference_code (human-facing) · booking_id (opt) · subject · category (coordination/refund/support) · status · priority | 1:N → ticket_participants, ticket_messages; optionally ↔ bookings, refunds. |
ticket_participants CORE | id · ticket_id · user_id · role_in_ticket — UNIQUE(ticket_id, user_id) | N:1 → tickets, users. |
ticket_messages CORE | id · ticket_id · sender_user_id · body · is_internal (admin-only) · attachments_json | N:1 → tickets. is_internal keeps admin notes out of user view. |
Domain 10 — Reviews & Patient Records
One review per completed booking with recompute-on-every-transition, plus a patient-scoped longitudinal clinical history that enables continuity of care.
| Table | Key fields | Relationships (FK) |
|---|---|---|
reviews CORE | id · booking_id (1:1) · customer_id · nurse_id · rating (CHECK 1–5) · body · status (pending_moderation/published/hidden/rejected) · moderation fields | 1:1 → bookings; N:1 → customer_profiles, nurse_profiles; 1:N → review_tag_links. Every transition recomputes nurse_profiles aggregates. |
review_tags_master MVP | id · name_fa/name_en · sentiment | N:N reviews via review_tag_links. Phase-2 analytics nicety. |
review_tag_links MVP | id · review_id · tag_id | N:1 → reviews, review_tags_master. |
patient_care_records MVP | id · patient_id · booking_id · nurse_id · clinical notes (enc) · recorded_at | N:1 → patients, bookings, nurse_profiles. Patient-scoped (not booking-scoped) longitudinal history; strict access. |
Domain 11 — Notifications
| Table | Key fields | Relationships (FK) |
|---|---|---|
notifications CORE | id · user_id · type · title/body · data_json (deep-link payload) · is_read · read_at | N:1 → users. In-app only (no push at launch); read rows > 90 days are purged. |
support_alerts CORE | id · alert_type (low_rating/no_show/location_mismatch/credential_expiry/fraud_signal/payment_anomaly) · entity_type/entity_id · owner_admin_id · status · resolution_notes | Polymorphic (validated at app layer; consider nullable typed FKs booking_id/review_id). Staff worklist items, never shown to users. |
Domain 12 — Audit, Config & Reference
| Table | Key fields | Relationships (FK) |
|---|---|---|
audit_logs CORE | id · entity_type/entity_id · action · actor_user_id · changed_fields_json · created_at | Polymorphic, append-only. Covers every sensitive transition including platform_configs changes. Plan month-partitioning + 2–3yr archival. |
system_events MVP | id · event_name · user_id · properties_json · created_at | High-volume behavioral analytics; pipe to a warehouse at scale rather than the transactional DB. |
platform_configs CORE | key · value · data_type — keys: platform_fee_rate, booking_payment_deadline_minutes, nurse_response_deadline_hours, nurse_payout_interval_days, evv_location_tolerance_meters, min_rating_for_support_alert, dispute_window_hours NEW, vat_rate NEW, bnpl_merchant_of_record NEW, bnpl_provider_commission_rate NEW, bnpl_settlement_timing NEW, cancellation-tier defaults | Referenced everywhere; changes audited. |
iranian_holidays MVP NEW | id · holiday_date · name_fa · type (official/religious/national) · is_bank_closed | Referenced by payout scheduling (date-shifting) and optionally pricing. PAYA/SATNA fail on closed days. |
Domain 13 — Partner Centers (launch) & Future
partner_centers is the single most launch-critical addition: the legal vehicle, plausibly the merchant-of-record, and the BNPL onboarding gate. The remaining tables are modeled-but-inactive so adding them later is a pure additive migration.
| Table | Key fields | Relationships (FK) |
|---|---|---|
partner_centers MVP NEW | id · name · legal_entity_type · moh_establishment_permit_no (پروانه تأسیس) · technical_director_nurse_user_id (مسئول فنی) · technical_director_license_no · enamad_code · settlement_iban (enc) · is_merchant_of_record · commission_rate · admin_user_id · is_active/verified_at | 1:N → nurse_profiles (sponsors), bookings (legally covered by), invoices (issuer). N:1 → users (technical director, admin). |
organizations / organization_nurses DEFERRED | The future employer model (nursing companies adding employed nurses). Kept distinct from partner_centers (launch licensing) to avoid conflating "sponsor for legality" with "employer." | Modeled-but-inactive; no launch table references them. |
fraud_flags DEFERRED | Output of a future ML fraud service. support_alerts (fraud_signal type) covers rule-based signals manually for now. | Inactive stub. |
recurring_booking_schedules DEFERRED | RFC-5545 recurrence for repeating care patterns. The concrete multi-day need is met by booking_sessions; this remains for true open-ended recurrence. | Inactive stub. |
Relationship summary
The load-bearing relationships across the whole schema, at a glance.
| Relationship | Type | Notes |
|---|---|---|
| users → nurse_profiles / customer_profiles | 1:1 | by role |
| partner_centers → nurse_profiles | 1:N | launch sponsor NEW |
| customer_profiles → patients / customer_addresses | 1:N | |
| nurse_profiles → nurse_service_variants / nurse_service_areas / nurse_bank_accounts / nurse_credentials | 1:N | |
| nurse_service_variants → nurse_service_variant_options | 1:N | option combination |
| nurse_profiles → nurse_verifications | 1:1 | |
| nurse_verifications → verification_steps → verification_documents | 1:N → 1:N | |
| booking_requests → bookings | 1:1 | on nurse-accept + payment |
| bookings → booking_sessions | 1:N | NEW — multi-visit engagements |
| booking_sessions → visit_verifications | 1:1 | CHG — EVV per session |
| bookings → booking_care_instructions / reviews / invoices | 1:1 | |
| bookings → payment_transactions | 1:N | attempts |
| payment_transactions → bnpl_transactions | 1:1 | if BNPL (replaces installment_plans) |
| payment_transactions → refunds | 1:N | CHG — partials allowed |
| payment_gateways → payment_webhook_events | 1:N | NEW — idempotency |
| bookings / nurses → ledger_entries | 1:N | NEW — money source of truth |
| refunds → nurse_clawbacks | 1:1 (opt) | NEW — refund-after-payout |
| nurse_payout_batches → nurse_payouts → nurse_payout_booking_links | 1:N → 1:N | booking_id UNIQUE |
| nurse_payout_booking_links → bookings | 1:1 | exactly one payout per booking |
| patients → patient_care_records | 1:N | longitudinal history |
| tickets → ticket_participants / ticket_messages | 1:N | |
| Sensitive entities → audit_logs | *:N | append-only |
Key design decisions (the reasoning, in one place)
- Escrow as a ledger state, not platform cash — because an Iranian پرداختیار legally cannot custody buyer funds. Everything else in the money domain follows from honestly representing "we don't hold the cash; we hold a claim/obligation tracked in the ledger over funds at a licensed provider." This is also why payouts are provider-side settlement to verified, ownership-checked IBANs.
- A BNPL order is a net-of-fee inbound payment, full stop — the verified full-upfront settlement model means there is no customer receivable, no default risk, and no installment schedule to track. Deleting
installment_entriesremoved an entire fragile subsystem and replaced it with one reconciliation row. - Three separate money amounts so the platform's two fee deductions (its own commission, and the BNPL provider's discount) are never conflated, and the nurse is paid identically regardless of payment method.
- Double-entry over status flags — the previous model could not answer "how much do we owe nurses right now" without fragile joins, and had nowhere to record a refund-after-payout. One append-only ledger + a
nurse_clawbacksreceivable fixes both and makes bank/Shaparak reconciliation possible. - Dispute window gates payout — preferring a holding period over a clawback, because clawback against an already-paid nurse IBAN is largely unenforceable. The clawback path exists for the cases that slip through.
- Idempotency before money —
payment_webhook_eventskeyed on the provider event id, written first, is the cheapest insurance against the most damaging payments bug (double-confirm / double-settle on callback retries). - Multi-session engagements are the norm, not an edge case —
booking_sessionsmakes long elder-care arrangements representable, lets escrow release per completed visit instead of holding a month of money, and makes mid-engagement cancellation accounting clean. - Partner center is launch-critical — the legal vehicle and likely merchant-of-record; without it the recommended go-to-market and the money flow are not representable.
- Verified-trust must be queryable —
nurse_credentialsturns the brand promise into renewal alerts, a real badge, and audit defensibility, surviving the future arrival of an INO/MoH API. - Keep the configurable service EAV; cut the analytics scaffolding — the category/option model earns its complexity (admin-extensible pricing dimensions without migrations);
response_rate/profile_completion_score/system_events-in-SQL do not, at launch.
Open items to confirm before building (not schema blockers)
- BNPL provider contract: does SnappPay/Digipay permit a multi-vendor marketplace re-disbursing to many nurses as a single merchant? (Publicly undocumented.) The schema assumes one lump to Balinyaar/the center, internal allocation to nurses — an ops confirmation, not a schema dependency.
- Commission % and settlement SLA per provider (and whether the provider returns its commission on a refund — full or pro-rata).
- PSP / تسهیم provider for MVP (ZarinPal Multiplexing vs Vandar vs Jibit) and whether it permits hold-then-weekly-payout timing, or whether a bank-grade escrow (Vandar میندو) is needed.
- VAT exemption ruling on the nursing service itself (the commission line is taxable regardless) —
vat_rateis config-driven so either ruling is a value change. - مودیان enrollment thresholds for the platform and high-earning nurses.
Confirm decades-old regulations, provider fee/settlement specifics, and tax thresholds against current primary sources and the provider's compliance team before building the payment integration.
Diagrams
Five views of the same system, rendered with Mermaid (brand-themed). The first four are reproduced from the data-model document; the fifth is an entity-relationship overview of the core spine.
1 — Domain map: how the clusters relate
flowchart LR PARTNER["Partner Centers (launch)
partner_centers"] IDENTITY["Identity & Access
users · nurse_profiles · customer_profiles
patients · customer_addresses · nurse_bank_accounts"] GEO["Geography
provinces · cities · districts · nurse_service_areas"] VERIFY["Verification
nurse_verifications · step_types · steps
documents · nurse_credentials"] SERVICES["Services & Pricing
service_categories · option_groups · option_values
variants · variant_options · search_index · availability"] BOOKING["Booking & Scheduling
booking_requests · bookings · booking_sessions
care_instructions · visit_verifications · cancellation_policies"] PAY["Payments & Ledger
payment_gateways · payment_transactions · webhook_events
refunds · ledger_entries · nurse_clawbacks · invoices"] BNPL["BNPL
bnpl_transactions"] PAYOUT["Payouts
payout_batches · payouts · booking_links"] REVIEW["Reviews & Records
reviews · review_tags · patient_care_records"] MSG["Messaging
tickets · participants · messages"] NOTIFY["Notifications
notifications · support_alerts"] AUDITCFG["Audit & Config
audit_logs · system_events
platform_configs · iranian_holidays"] PARTNER -. "sponsors / merchant-of-record" .-> VERIFY IDENTITY --> VERIFY VERIFY --> SERVICES SERVICES --> GEO IDENTITY --> BOOKING SERVICES --> BOOKING BOOKING --> PAY PAY --> BNPL PAY --> PAYOUT BOOKING --> REVIEW BOOKING --> MSG PAY --> NOTIFY PAY --> AUDITCFG
2 — Core booking spine (who books whom)
erDiagram
users ||--o| nurse_profiles : "role=nurse"
users ||--o| customer_profiles : "role=customer"
partner_centers ||--o{ nurse_profiles : "sponsors"
customer_profiles ||--o{ patients : "registers"
customer_profiles ||--o{ customer_addresses : "saves"
nurse_profiles ||--o{ nurse_service_variants : "offers"
customer_profiles ||--o{ booking_requests : "submits"
nurse_profiles ||--o{ booking_requests : "receives"
patients ||--o{ booking_requests : "for patient"
nurse_service_variants ||--o{ booking_requests : "selects variant"
booking_requests ||--o| bookings : "converts on payment"
bookings ||--o{ booking_sessions : "has visits"
booking_sessions ||--o| visit_verifications : "EVV per visit"
bookings ||--o| booking_care_instructions : "clinical (encrypted)"
bookings ||--o| reviews : "one review"
booking_requests {
bigint id PK
string status
string required_caregiver_gender
datetime nurse_response_deadline_at
datetime payment_deadline_at
}
bookings {
bigint id PK
bigint gross_price_irr
bigint balinyaar_commission_irr
bigint nurse_payout_amount
smallint session_count
datetime dispute_window_ends_at
string status
}
booking_sessions {
bigint id PK
int session_index
date scheduled_date
string status
datetime payout_eligible_at
}
bookings.3 — Payments, ledger & payouts
erDiagram
bookings ||--o{ payment_transactions : "paid by (attempts)"
payment_gateways ||--o{ payment_transactions : "via"
payment_gateways ||--o{ payment_webhook_events : "emits"
payment_transactions ||--o| bnpl_transactions : "if BNPL"
payment_transactions ||--o{ refunds : "may be refunded"
refunds ||--o| nurse_clawbacks : "if after payout"
nurse_profiles ||--o{ nurse_clawbacks : "owes"
bookings ||--o{ ledger_entries : "money postings"
bookings ||--o| invoices : "billed"
nurse_payout_batches ||--o{ nurse_payouts : "groups"
nurse_profiles ||--o{ nurse_payouts : "receives"
nurse_bank_accounts ||--o{ nurse_payouts : "to IBAN"
nurse_payouts ||--o{ nurse_payout_booking_links : "covers"
bookings ||--o| nurse_payout_booking_links : "settled in one"
ledger_entries {
bigint id PK
uuid transaction_group_id
string account_type
string direction
bigint amount_irr
}
refunds {
bigint id PK
bigint platform_fee_refunded_irr
bigint nurse_payout_refunded_irr
string refund_channel
}
bnpl_transactions {
bigint id PK
string provider_code
bigint settled_amount_irr
bigint bnpl_commission_irr
string status
}
4 — Financial lifecycle: escrow → payout → clawback
flowchart TD
A["Family submits booking_request"] --> B{"Nurse responds in time?"}
B -->|"reject / expire"| X["request closed — no money moved"]
B -->|"accept"| C["30-min payment window"]
C --> D{"Payment method"}
D -->|"Card (IPG)"| E["payment_transactions = succeeded"]
D -->|"BNPL (SnappPay)"| F["bnpl_transactions = settled
full amount minus provider commission"]
E --> G["Ledger posting:
DR escrow_held / CR nurse_payable + platform_revenue"]
F --> G
G --> H["Booking confirmed (escrow held)"]
H --> I["Nurse EVV check-in / check-out per session"]
I --> J["Booking completed"]
J --> K["dispute_window_ends_at = completed_at + 72h"]
K --> L{"Window passed & no dispute?"}
L -->|"yes"| M["payout_eligible"]
M --> N["Weekly batch — PAYA to nurse IBAN
payout = gross − balinyaar_commission"]
K -.->|"refund BEFORE payout"| O["Clean ledger reversal
PSP refund / bnpl_revert"]
N --> P{"Refund AFTER payout?"}
P -->|"yes"| Q["nurse_clawbacks receivable
netted next batch or written off"]
P -->|"no"| Z["Settled and reconciled"]
5 — Entity-relationship overview (cross-domain)
A wider ER view tying the identity, verification, services, booking, payment, payout, BNPL, messaging, review, and partner clusters together — the relationships a reader needs to navigate the whole model.
erDiagram
users ||--o| nurse_profiles : "1:1"
users ||--o| customer_profiles : "1:1"
users ||--o{ user_sessions : "sessions"
users }o--o{ roles : "via user_roles"
partner_centers ||--o{ nurse_profiles : "sponsors"
partner_centers ||--o{ bookings : "covers"
partner_centers ||--o{ invoices : "issues"
nurse_profiles ||--|| nurse_verifications : "header"
nurse_verifications ||--o{ verification_steps : "steps"
verification_steps ||--o{ verification_documents : "evidence"
nurse_profiles ||--o{ nurse_credentials : "licenses"
nurse_profiles ||--o{ nurse_bank_accounts : "IBANs"
nurse_profiles ||--o{ nurse_service_variants : "offers"
nurse_profiles ||--o{ nurse_service_areas : "covers"
nurse_service_variants ||--o{ nurse_service_variant_options : "options"
nurse_service_variants ||--o{ nurse_search_index : "projected"
customer_profiles ||--o{ patients : "registers"
customer_profiles ||--o{ customer_addresses : "saves"
patients ||--o{ patient_care_records : "history"
customer_profiles ||--o{ booking_requests : "submits"
nurse_service_variants ||--o{ booking_requests : "variant"
booking_requests ||--o| bookings : "converts"
bookings ||--o{ booking_sessions : "visits"
booking_sessions ||--o| visit_verifications : "EVV"
bookings ||--o| booking_care_instructions : "clinical"
bookings ||--o| reviews : "one review"
reviews ||--o{ review_tag_links : "tags"
bookings ||--o| invoices : "billed"
bookings ||--o{ payment_transactions : "attempts"
payment_gateways ||--o{ payment_transactions : "via"
payment_gateways ||--o{ payment_webhook_events : "idempotency"
payment_transactions ||--o| bnpl_transactions : "if BNPL"
payment_transactions ||--o{ refunds : "partials"
refunds ||--o| nurse_clawbacks : "after payout"
bookings ||--o{ ledger_entries : "postings"
nurse_payout_batches ||--o{ nurse_payouts : "groups"
nurse_profiles ||--o{ nurse_payouts : "receives"
nurse_bank_accounts ||--o{ nurse_payouts : "to IBAN"
nurse_payouts ||--o{ nurse_payout_booking_links : "covers"
bookings ||--o| nurse_payout_booking_links : "one payout"
bookings ||--o{ tickets : "coordination"
tickets ||--o{ ticket_participants : "who"
tickets ||--o{ ticket_messages : "thread"
refunds ||--o{ tickets : "anchored"