197 lines
20 KiB
Markdown
197 lines
20 KiB
Markdown
# 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 ~7–10 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.
|