Database Model
Revision 2 — 2026-06-20. This is a research-driven refinement of the original 13-domain model. It closes the financial-correctness gaps the previous version flagged in its own _Advices_ section, resolves the two open BNPL questions, and grounds every money decision in verified research on the Iranian payment landscape (SnappPay, Digipay, Tara, Torob Pay, Shaparak/پرداختیار rules). The previous revision is preserved in git history.
Companion documents: business requirements (per-section product requirements) and payments deep-dive (the BNPL/escrow deep-dive with sources).
Contents
- Diagrams
- Domain 1 — Identity & Access
- Domain 2 — Geographic Data
- Domain 3 — Services & Pricing
- Domain 4 — Verification & Credentials
- Domain 5 — Booking & Scheduling
- Domain 6 — Payments, Ledger & Refunds
- Domain 7 — Payouts to Nurses
- Domain 8 — BNPL / Installments
- Domain 9 — Messaging (Ticket System)
- Domain 10 — Reviews & Patient Records
- Domain 11 — Notifications
- Domain 12 — Audit, Config & Reference
- Domain 13 — Partner Centers (launch) & Future
Platform Summary
Balinyaar is a trust-first home-nursing marketplace in Iran. Independent, individually-verified nurses register, list configurable services with their own pricing, and pass a multi-step verification pipeline anchored on the MoH پروانه صلاحیت حرفهای (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 — see Principle 2), the nurse performs one or more EVV-verified visits, and the platform pays the nurse weekly, after the dispute window closes, minus a platform commission. All post-booking communication runs through an admin-readable ticket system.
At launch the platform operates under a partner licensed home-nursing center (مرکز مشاوره و ارائه مراقبتهای پرستاری در منزل) — the Asanism-style model — which is the legal vehicle and the likely merchant-of-record for payments while Balinyaar's own MoH permit is in process.
What changed in this revision (decision summary)
| # | Decision | Why | Schema effect |
|---|---|---|---|
| 1 | Escrow is a ledger _state_, not held cash | An Iranian پرداختیار (payment facilitator) is legally barred from custodying buyer funds; money flows card → PSP → Shaparak → registered IBANs. | New ledger_entries (double-entry); "escrow" derives from it. |
| 2 | BNPL = full-upfront single settlement | Verified: SnappPay/Digipay/Tara/Torob pay the merchant the whole amount minus commission in one lump and bear the customer-default risk. | Cut installment_plans/installment_entries; replace with one bnpl_transactions row. No customer-installment tracking. |
| 3 | Nurse paid by Balinyaar, on its own weekly schedule | The customer's installments are owned by the BNPL provider and decoupled from our payout. | Three-way money split on bookings; payout independent of BNPL. |
| 4 | Clawback is first-class | A booking can be disputed/refunded after the nurse was already paid; the old model had nowhere to record the receivable. | New nurse_clawbacks + dispute_window_ends_at gating. |
| 5 | Webhook idempotency before real money | PSP/BNPL callbacks are at-least-once and retried. | New payment_webhook_events keyed on external_event_id. |
| 6 | Multi-session engagements | Elder care is dominantly multi-day / شبانهروزی (live-in); one-visit-per-booking can't model it. | New booking_sessions; EVV + payout move to the session. |
| 7 | Partner licensed-center entity | The recommended launch path is subcontracting to an MoH-licensed center; it may be the merchant-of-record/invoice issuer. | New partner_centers + nurse_profiles.partner_center_id. |
| 8 | Structured credential registry | The "verified" trust badge and renewal alerts need queryable license numbers/expiries, not just opaque PDF uploads. | New nurse_credentials. |
| 9 | Cheaper search | Nurse search needed 4+ joins from day one. | New denormalized nurse_search_index. |
| 10 | Cancellation policy + tax/invoice | "Default 100% refund" is naive; Iranian commission marketplaces owe VAT on commission. | New cancellation_policies, invoices; VAT 10% (configurable). |
| 11 | Integrity hardening | Drift, double-pay, and tenancy-leak gaps the critiques found. | Drop duplicate verification_status & payout_released; add uniqueness/CHECK/tenancy invariants. |
Design Principles
- Money is
BIGINTin Iranian Rials (IRR). Toman is a display concern only; conversion happens only at a provider's API boundary (e.g. SnappPay quotes Toman) and never internally. No floats anywhere on the money path. - The platform never legally holds buyer cash. Funds settle through a licensed PSP/پرداختیار to registered IBANs (the platform's commission IBAN and the nurse's IBAN, via تسهیم settlement-sharing, or to one merchant-of-record account). "Escrow" and "nurse balance" are derived ledger states over money custodied at the provider/partner bank — represented in
ledger_entries, never as a Balinyaar-owned cash balance. ledger_entriesis the financial source of truth. Every capture, commission, payout, refund, and clawback posts balanced double-entry rows. Per-table money fields (e.g.bookings.gross_price_irr) remain the operational/pricing record; the ledger is the reconciliation truth that answers "how much do we owe nurses right now" and "how much is held but unreleased."- Fee split is captured per booking and never derived from live config, so historical reporting survives commission-schedule changes. The booking stores three distinct amounts:
gross_price_irr,balinyaar_commission_irr,nurse_payout_amount. - PII fields (national ID, IBAN, phone, addresses, clinical data) are marked (encrypted) — column- or application-level. Clinical data has stricter access than financial data.
- Two-stage clinical disclosure is a hard rule, not a convention. At the request stage the nurse sees only
booking_requests.customer_notes. The full encryptedbooking_care_instructionsare exposed only after the booking is confirmed. Enforced at the authorization layer. - Soft deletes on
users/nurse_profilesviadeleted_at. Audit, payment, ledger, and payout records are never deleted. - Audit trail is append-only. All state transitions on bookings, payments, refunds, payouts, verifications, reviews, and
platform_configsproduce anaudit_logsrow. - 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. - Idempotency is mandatory on the money path. Every PSP/BNPL callback is stored raw in
payment_webhook_eventsand deduplicated onexternal_event_idbefore any money-state mutation. - All timestamps are
DATETIME2(7)UTC. Persian-calendar display is a UI concern — except that bank-closure scheduling uses theiranian_holidaystable, because PAYA/SATNA transfers fail on holidays. - Derived flags must not drift.
nurse_profiles.is_verified, denormalized rating aggregates, and the search index are written only by the code path that owns their source of truth, inside the same transaction. - Invariants are enforced, not just 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 must belong to the same customer; its variant to the same nurse).
The two questions this revision answers
These were the two hardest open questions (from whatsInYourMind.txt). Both are resolved against verified research that all mainstream Iranian provider-financed BNPLs use full-upfront settlement — the provider pays the merchant the whole amount minus commission and owns the customer's installments and default risk.
Q1 — A booking paid by installments (BNPL) is cancelled or refunded mid-plan. What happens?
Money always flows customer ↔ BNPL provider ↔ Balinyaar — never nurse→customer, and never Balinyaar→customer directly for a BNPL order.
- Balinyaar initiates the reversal through the provider's API (SnappPay
revertfor full /cancel/updatefor partial, using the storedexternal_payment_token). - The provider then cancels the customer's unpaid installments, restores their credit, and refunds any already-paid installment to the customer's bank account in ~7–10 business days (asynchronous, owned by the provider).
- Balinyaar records a
refundsrow withrefund_channel = 'bnpl_revert', carryingexternal_revert_referenceandexpected_customer_refund_eta;refund_statusstaysprocessinguntil a reconciliation job confirms. - The refund decomposes across the two fee legs —
platform_fee_refunded_irrandnurse_payout_refunded_irr— and posts balanced ledger entries. - If the nurse has not yet been paid (still inside the dispute window / not in a processed batch): the
nurse_payableaccrual is simply reversed; nothing leaves Balinyaar. Clean. - If the nurse has already been paid: this is the clawback path — a
nurse_clawbacksreceivable + negative ledger entry; recovered from the next payout batch or written off.
A shortened/partial visit maps to the provider's update endpoint with a reduced amount; record refund_delta_irr and reduce bnpl_transactions.settled_amount_irr.
Q2 — Under BNPL, who pays the nurse and when?
Balinyaar pays the nurse, on its own normal weekly payout schedule, after EVV completion and after the dispute window closes — exactly the same path as a card-funded booking. The BNPL provider never pays the nurse and is indifferent to the internal split.
The nurse's payout is computed from the booking's gross_price_irr minus balinyaar_commission_irr — never from the BNPL provider's net settled_amount_irr. The provider's commission (bnpl_commission_irr) is a platform cost of accepting BNPL, borne by Balinyaar, and must never touch the nurse's payout. Hence three separately stored amounts:
| Amount | Meaning | Drives |
|---|---|---|
gross_price_irr | What the customer is charged (the booking price) | The invoice, the refund base |
balinyaar_commission_irr | Platform's own cut | Platform revenue |
bnpl_commission_irr | The BNPL provider's merchant discount (on bnpl_transactions) | Platform expense (never the nurse's) |
nurse_payout_amount = gross_price_irr − balinyaar_commission_irr. The nurse receives the identical amount and on the identical schedule whether the family paid by card or by SnappPay.
Relationship Summary
| 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 | CHANGED — 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 | CHANGED — 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 |
Final MVP table list
Identity & Access: users · user_sessions · roles · user_roles · nurse_profiles · customer_profiles · patients · customer_addresses · nurse_bank_accounts — all [CORE]
Geography: provinces · cities [CORE] · districts [MVP] · nurse_service_areas [CORE]
Services & Pricing: service_categories · service_option_groups · service_option_values · nurse_service_variants · nurse_service_variant_options · nurse_search_index (NEW) — [CORE]; nurse_availability_slots · nurse_availability_exceptions — [MVP]
Verification: nurse_verifications · verification_step_types · verification_steps · verification_documents [CORE]; nurse_credentials (NEW) [MVP]
Booking & Scheduling: booking_requests · bookings · booking_care_instructions · visit_verifications [CORE]; booking_sessions (NEW) · cancellation_policies (NEW) [MVP]
Payments & Ledger: payment_gateways · payment_transactions · payment_webhook_events (NEW) · refunds · ledger_entries (NEW) · nurse_clawbacks (NEW) · nurse_payout_batches · nurse_payouts · nurse_payout_booking_links [CORE]; invoices (NEW) [MVP]
BNPL: bnpl_transactions (NEW — replaces installment_plans) [MVP]; · installment_plans CUT; installment_entriesbnpl_settlement_entries [DEFERRED]
Messaging: tickets · ticket_participants · ticket_messages [CORE]
Reviews & Records: reviews [CORE]; review_tags_master · review_tag_links · patient_care_records [MVP]
Notifications: notifications · support_alerts [CORE]
Audit & Config: audit_logs [CORE] · system_events [MVP] · platform_configs [CORE] · iranian_holidays (NEW) [MVP]
Partner / Launch: partner_centers (NEW) [MVP]
Future (modeled, inactive): organizations · organization_nurses · fraud_flags · recurring_booking_schedules · bnpl_settlement_entries — all [DEFERRED]
Net change vs the original 45: −2 cut (installment_plans replaced, installment_entries removed), +10 added (ledger_entries, nurse_clawbacks, payment_webhook_events, nurse_search_index, booking_sessions, cancellation_policies, invoices, partner_centers, nurse_credentials, iranian_holidays), 1 replaced (bnpl_transactions). The financial core is now a single ledger, BNPL is one settlement row, and the clawback / dispute-window / idempotency / license / multi-session gaps are all closed.
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 modeled as 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 for Balinyaar 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 — it is 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 — confirm with sales.) The schema assumes one lump to Balinyaar/the center, internal allocation to nurses, so this is 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 the 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.
↑ Back to topConfirm 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. See the payments deep-dive for the full source-cited analysis.