Files
2026-06-24 01:32:46 +03:30

20 KiB
Raw Permalink Blame History

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


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

  1. Money is BIGINT in 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.
  2. 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.
  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 (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."
  4. 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.
  5. PII fields (national ID, IBAN, phone, addresses, clinical data) are marked (encrypted) — column- or application-level. Clinical data has stricter access than financial data.
  6. 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 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, denormalized 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 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 ↔ Balinyaarnever nurse→customer, and never Balinyaar→customer directly for a BNPL order.

  1. Balinyaar initiates the reversal through the provider's API (SnappPay revert for full / cancel/update for partial, using the stored external_payment_token).
  2. 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 ~710 business days (asynchronous, owned by the provider).
  3. Balinyaar records a refunds row with refund_channel = 'bnpl_revert', carrying external_revert_reference and expected_customer_refund_eta; refund_status stays processing until a reconciliation job confirms.
  4. The refund decomposes across the two fee legs — platform_fee_refunded_irr and nurse_payout_refunded_irr — and posts balanced ledger entries.
  5. If the nurse has not yet been paid (still inside the dispute window / not in a processed batch): the nurse_payable accrual is simply reversed; nothing leaves Balinyaar. Clean.
  6. If the nurse has already been paid: this is the clawback path — a nurse_clawbacks receivable + 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 closesexactly 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_irrnever 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
usersnurse_profiles / customer_profiles 1:1 by role
partner_centersnurse_profiles 1:N launch sponsor (NEW)
customer_profilespatients / customer_addresses 1:N
nurse_profilesnurse_service_variants / nurse_service_areas / nurse_bank_accounts / nurse_credentials 1:N
nurse_service_variantsnurse_service_variant_options 1:N option combination
nurse_profilesnurse_verifications 1:1
nurse_verificationsverification_stepsverification_documents 1:N → 1:N
booking_requestsbookings 1:1 on nurse-accept + payment
bookingsbooking_sessions 1:N NEW — multi-visit engagements
booking_sessionsvisit_verifications 1:1 CHANGED — EVV per session
bookingsbooking_care_instructions / reviews / invoices 1:1
bookingspayment_transactions 1:N attempts
payment_transactionsbnpl_transactions 1:1 if BNPL (replaces installment_plans)
payment_transactionsrefunds 1:N CHANGED — partials allowed
payment_gatewayspayment_webhook_events 1:N NEW — idempotency
bookings / nurses → ledger_entries 1:N NEW — money source of truth
refundsnurse_clawbacks 1:1 (opt) NEW — refund-after-payout
nurse_payout_batchesnurse_payoutsnurse_payout_booking_links 1:N → 1:N booking_id UNIQUE
nurse_payout_booking_linksbookings 1:1 exactly one payout per booking
patientspatient_care_records 1:N longitudinal history
ticketsticket_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 · installment_entries CUT; bnpl_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)

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

  2. 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_entries removed an entire fragile subsystem and replaced it with one reconciliation row.

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

  4. 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_clawbacks receivable fixes both and makes bank/Shaparak reconciliation possible.

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

  6. Idempotency before moneypayment_webhook_events keyed on the provider event id, written first, is the cheapest insurance against the most damaging payments bug (double-confirm / double-settle on callback retries).

  7. Multi-session engagements are the norm, not an edge casebooking_sessions makes 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.

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

  9. Verified-trust must be queryablenurse_credentials turns the brand promise into renewal alerts, a real badge, and audit defensibility, surviving the future arrival of an INO/MoH API.

  10. 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_rate is 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. See the payments deep-dive for the full source-cited analysis.