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

197 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](../business/index.md)** (per-section product requirements) and **[payments deep-dive](../payments/index.md)** (the BNPL/escrow deep-dive with sources).
## Contents
- [Diagrams](diagrams.md)
- [Domain 1 — Identity & Access](01-identity-and-access.md)
- [Domain 2 — Geographic Data](02-geography.md)
- [Domain 3 — Services & Pricing](03-services-and-pricing.md)
- [Domain 4 — Verification & Credentials](04-verification-and-credentials.md)
- [Domain 5 — Booking & Scheduling](05-booking-and-scheduling.md)
- [Domain 6 — Payments, Ledger & Refunds](06-payments-ledger-and-refunds.md)
- [Domain 7 — Payouts to Nurses](07-payouts.md)
- [Domain 8 — BNPL / Installments](08-bnpl.md)
- [Domain 9 — Messaging (Ticket System)](09-messaging.md)
- [Domain 10 — Reviews & Patient Records](10-reviews-and-records.md)
- [Domain 11 — Notifications](11-notifications.md)
- [Domain 12 — Audit, Config & Reference](12-audit-config-and-reference.md)
- [Domain 13 — Partner Centers (launch) & Future](13-partner-centers-and-future.md)
---
## 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 ↔ Balinyaar`**never** 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 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`~~ · ~~`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 money**`payment_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 case**`booking_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 queryable**`nurse_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](../payments/index.md) for the full source-cited analysis.