clean and refine product docs structure

This commit is contained in:
hamid
2026-06-24 01:32:46 +03:30
parent be07c703ec
commit 1df3cd9f64
113 changed files with 6078 additions and 4973 deletions
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 1 — Identity &amp; Access — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a class="active" href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-1-identity-access">Domain 1 — Identity &amp; Access</h1>
<p><a href="index.html">← Database Model</a></p>
<p><strong>Related:</strong> business requirements — <a href="../business/01-actors-and-onboarding.html">Actors &amp; onboarding</a>.</p>
<h3 id="users-core"><code>users</code> [CORE] <a class="anchor" href="#users-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The single identity record for every human actor — nurse, customer, admin. <code>role</code> decides which profile sub-table is populated. Phone is the primary login credential (OTP); national ID is filled only after KYC.</p>
<p><strong>Why:</strong> One identity table avoids three near-duplicate user tables and lets auth, audit, and notifications treat everyone uniformly. Phone-as-primary matches Iranian OTP norms and is the key Shahkar matches against. National ID stays NULL until verified so an unverified registration can't masquerade as KYC-complete.</p>
<p>Fields unchanged from baseline: <code>id</code>, <code>email</code> (enc, nullable), <code>phone</code> (enc, unique), <code>national_id</code> (enc, nullable), <code>national_id_verified_at</code>, <code>first_name</code>, <code>last_name</code>, <code>gender</code> <em>(promoted here — see note)</em>, <code>avatar_url</code>, <code>role</code> (<code>nurse</code>/<code>customer</code>/<code>admin</code>), <code>is_active</code>, <code>email_verified_at</code>, <code>phone_verified_at</code>, <code>last_login_at</code>, <code>last_login_ip</code>, <code>preferred_language</code>, <code>created_at</code>, <code>updated_at</code>, <code>deleted_at</code>.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>gender</code></td><td>NVARCHAR(10) NULL</td><td><strong>NEW/clarified</strong><code>male</code>/<code>female</code>. Needed because <strong>same-gender caregiving is a near-hard requirement</strong> in Iranian bodily-care; nurse gender (from here) is matched against <code>booking_requests.required_caregiver_gender</code>.</td></tr>
<tr><td><code>shahkar_verified_at</code></td><td>DATETIME2 NULL</td><td><strong>NEW</strong> — when the phone↔national-id binding was confirmed via Shahkar. Re-set to NULL (re-verify) on phone change.</td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> 1:1 → <code>nurse_profiles</code> / <code>customer_profiles</code> (by role); 1:N → <code>user_sessions</code>, <code>user_roles</code>, <code>notifications</code>, <code>ticket_participants</code>. Admin users are referenced across the schema as <code>*_by_admin_id</code>.</p>
<h3 id="user_sessions-core"><code>user_sessions</code> [CORE] <a class="anchor" href="#user_sessions-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Refresh-token session records. <strong>Why:</strong> Enables logout-everywhere and stolen-token revocation without a heavyweight session store. Unchanged: <code>id</code>, <code>user_id</code>, <code>refresh_token_hash</code>, <code>device_info</code>, <code>ip_address</code>, <code>is_revoked</code>, <code>revoked_at</code>, <code>expires_at</code>, <code>created_at</code>. <strong>Relations:</strong> N:1 → <code>users</code>.</p>
<h3 id="roles-user_roles-core"><code>roles</code> / <code>user_roles</code> [CORE] <a class="anchor" href="#roles-user_roles-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> RBAC for admin staff only (nurses/customers use <code>users.role</code>). <strong>Why:</strong> A small admin team still needs separable finance/support/moderation permissions and a revocation history. <code>user_roles</code> keeps <code>granted_by</code>/<code>granted_at</code>/<code>revoked_at</code> for an audit trail. <strong>Relations:</strong> <code>users</code> N:N <code>roles</code> via <code>user_roles</code>.</p>
<h3 id="nurse_profiles-core"><code>nurse_profiles</code> [CORE] <a class="anchor" href="#nurse_profiles-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Extended data for nurses, plus denormalized search/quality aggregates. <strong>Why separated from <code>users</code>:</strong> keeps the base identity table lean and isolates the (large) nurse-only attributes and the aggregates that search reads on every query.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>user_id</code></td><td>BIGINT FK → users UNIQUE</td><td>1:1</td></tr>
<tr><td><code>partner_center_id</code></td><td>BIGINT FK → partner_centers NULL</td><td><strong>NEW</strong> — the licensed center that legally sponsors this nurse at launch (Asanism model). NULL once Balinyaar holds its own permit.</td></tr>
<tr><td><code>bio</code>, <code>years_of_experience</code>, <code>education_level</code>, <code>education_field</code>, <code>specializations_json</code></td><td></td><td>Unchanged.</td></tr>
<tr><td><code>is_verified</code></td><td>BIT NOT NULL DEFAULT 0</td><td><strong>Guarded</strong> — set <strong>only</strong> inside the transaction that confirms all required <code>verification_steps.status='passed'</code>. No direct write API (Principle 12).</td></tr>
<tr><td><del><code>verification_status</code></del></td><td></td><td><strong>CUT</strong> — duplicated <code>nurse_verifications.status</code>; two copies drifted. <code>nurse_verifications.status</code> is now the single source of truth.</td></tr>
<tr><td><code>is_accepting_bookings</code></td><td>BIT NOT NULL DEFAULT 0</td><td>Nurse can pause without losing verified status.</td></tr>
<tr><td><code>average_rating</code>, <code>total_reviews</code>, <code>total_completed_bookings</code></td><td></td><td>Denormalized. <strong>Recompute rule now documented</strong>: updated on <strong>every</strong> review status transition (publish → +, hide/reject/unpublish → ) and on booking completion/dispute-reversal, plus a nightly reconciliation job. Fixes the "hide a 1★ review → rating stays inflated" drift.</td></tr>
<tr><td><del><code>response_rate</code>, <code>avg_response_time_hours</code>, <code>profile_completion_score</code></del></td><td></td><td><strong>CUT for MVP</strong> — analytics columns on no money/safety path, each needing a maintenance job. Compute offline later.</td></tr>
<tr><td><code>created_at</code>, <code>updated_at</code>, <code>deleted_at</code></td><td></td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> 1:1 → <code>users</code>, <code>nurse_verifications</code>; 1:N → <code>nurse_service_variants</code>, <code>nurse_service_areas</code>, <code>nurse_bank_accounts</code>, <code>nurse_credentials</code>, <code>bookings</code>, <code>nurse_payouts</code>, <code>nurse_clawbacks</code>; N:1 → <code>partner_centers</code>.</p>
<h3 id="customer_profiles-core"><code>customer_profiles</code> [CORE] <a class="anchor" href="#customer_profiles-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Lightweight extension for customers. <strong>Why intentionally thin:</strong> most customer reality lives in their <code>patients</code>, <code>customer_addresses</code>, and <code>bookings</code>. KYC for customers is deferred. Unchanged: <code>id</code>, <code>user_id</code> (unique), <code>default_emergency_contact_name</code>/<code>_phone</code> (enc), <code>created_at</code>, <code>updated_at</code>. <strong>CUT for MVP:</strong> <code>national_id_verified_at</code> (anti-fraud customer KYC — add when actually built). <strong>Relations:</strong> 1:1 → <code>users</code>; 1:N → <code>patients</code>, <code>customer_addresses</code>, <code>booking_requests</code>, <code>bookings</code>.</p>
<h3 id="patients-core"><code>patients</code> [CORE] <a class="anchor" href="#patients-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The person receiving care, <strong>separate from the payer</strong>. <strong>Why:</strong> the payer (adult child, spouse) is usually not the patient (elderly parent, newborn, post-surgical adult); one customer registers many patients, each with its own clinical baseline and longitudinal record. Unchanged: <code>id</code>, <code>customer_id</code>, <code>display_name</code>, <code>first_name</code>, <code>last_name</code>, <code>birth_date</code>, <code>gender</code>, <code>blood_type</code>, <code>initial_medical_notes</code> (enc), <code>is_active</code>, timestamps. <strong>Relations:</strong> N:1 → <code>customer_profiles</code>; 1:N → <code>booking_requests</code>, <code>patient_care_records</code>. <strong>Tenancy invariant:</strong> a <code>booking_request.patient_id</code> must belong to the same <code>customer_id</code>.</p>
<h3 id="customer_addresses-core"><code>customer_addresses</code> [CORE] <a class="anchor" href="#customer_addresses-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Saved service locations; the encrypted address + coordinates for EVV distance checks. <strong>Why coordinates:</strong> EVV check-in compares the nurse's GPS against the booking address within tolerance. Unchanged fields, plus: <strong>filtered <code>UNIQUE(customer_id) WHERE is_primary=1</code></strong> so exactly one primary exists (prevents ambiguous default). <strong>Relations:</strong> N:1 → <code>customer_profiles</code>, <code>cities</code>, <code>districts</code>; referenced by <code>booking_requests</code>/<code>bookings</code>.</p>
<h3 id="nurse_bank_accounts-core"><code>nurse_bank_accounts</code> [CORE] <a class="anchor" href="#nurse_bank_accounts-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Payout destination (IBAN/Sheba). <strong>Why hardened:</strong> the IBAN is the single place real money leaves the platform — the original "admin eyeballs the IBAN" check is exactly the forgeable, money-mule-risk link the research warns about.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code>, <code>nurse_id</code>, <code>bank_name</code>, <code>account_holder_name</code> (enc), <code>iban</code> (enc), <code>is_primary</code>, <code>is_verified</code>, <code>verified_by_admin_id</code>, <code>verified_at</code>, timestamps</td><td></td><td>Baseline.</td></tr>
<tr><td><code>iban_hash</code></td><td>NVARCHAR(64)</td><td><strong>NEW</strong> — deterministic hash for a <strong>UNIQUE</strong> constraint (same IBAN must not silently serve two nurses).</td></tr>
<tr><td><code>matched_national_id</code></td><td>BIT NULL</td><td><strong>NEW</strong> — result of an automated <strong>IBAN-owner ↔ national-id inquiry (استعلام شبا)</strong> via a KYC vendor. First payout is gated on a match, not on admin eyeballing.</td></tr>
<tr><td><code>account_holder_from_bank</code></td><td>NVARCHAR(200) NULL</td><td><strong>NEW</strong> — name returned by the bank inquiry, snapshot.</td></tr>
<tr><td><code>ownership_vendor_ref</code></td><td>NVARCHAR(200) NULL</td><td><strong>NEW</strong> — vendor transaction id for audit.</td></tr>
</tbody></table></div>
<p>Constraints: <strong>filtered <code>UNIQUE(nurse_id) WHERE is_primary=1</code></strong>; <code>UNIQUE(iban_hash)</code>. <strong>Relations:</strong> N:1 → <code>nurse_profiles</code>; 1:N → <code>nurse_payouts</code>.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,65 @@
# Domain 1 — Identity & Access
[← Database Model](index.md)
**Related:** business requirements — [Actors & onboarding](../business/01-actors-and-onboarding.md).
### `users` [CORE]
**Role:** The single identity record for every human actor — nurse, customer, admin. `role` decides which profile sub-table is populated. Phone is the primary login credential (OTP); national ID is filled only after KYC.
**Why:** One identity table avoids three near-duplicate user tables and lets auth, audit, and notifications treat everyone uniformly. Phone-as-primary matches Iranian OTP norms and is the key Shahkar matches against. National ID stays NULL until verified so an unverified registration can't masquerade as KYC-complete.
Fields unchanged from baseline: `id`, `email` (enc, nullable), `phone` (enc, unique), `national_id` (enc, nullable), `national_id_verified_at`, `first_name`, `last_name`, `gender` *(promoted here — see note)*, `avatar_url`, `role` (`nurse`/`customer`/`admin`), `is_active`, `email_verified_at`, `phone_verified_at`, `last_login_at`, `last_login_ip`, `preferred_language`, `created_at`, `updated_at`, `deleted_at`.
| Field | Type | Notes |
|---|---|---|
| `gender` | NVARCHAR(10) NULL | **NEW/clarified**`male`/`female`. Needed because **same-gender caregiving is a near-hard requirement** in Iranian bodily-care; nurse gender (from here) is matched against `booking_requests.required_caregiver_gender`. |
| `shahkar_verified_at` | DATETIME2 NULL | **NEW** — when the phone↔national-id binding was confirmed via Shahkar. Re-set to NULL (re-verify) on phone change. |
**Relations:** 1:1 → `nurse_profiles` / `customer_profiles` (by role); 1:N → `user_sessions`, `user_roles`, `notifications`, `ticket_participants`. Admin users are referenced across the schema as `*_by_admin_id`.
### `user_sessions` [CORE]
**Role:** Refresh-token session records. **Why:** Enables logout-everywhere and stolen-token revocation without a heavyweight session store. Unchanged: `id`, `user_id`, `refresh_token_hash`, `device_info`, `ip_address`, `is_revoked`, `revoked_at`, `expires_at`, `created_at`. **Relations:** N:1 → `users`.
### `roles` / `user_roles` [CORE]
**Role:** RBAC for admin staff only (nurses/customers use `users.role`). **Why:** A small admin team still needs separable finance/support/moderation permissions and a revocation history. `user_roles` keeps `granted_by`/`granted_at`/`revoked_at` for an audit trail. **Relations:** `users` N:N `roles` via `user_roles`.
### `nurse_profiles` [CORE]
**Role:** Extended data for nurses, plus denormalized search/quality aggregates. **Why separated from `users`:** keeps the base identity table lean and isolates the (large) nurse-only attributes and the aggregates that search reads on every query.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `user_id` | BIGINT FK → users UNIQUE | 1:1 |
| `partner_center_id` | BIGINT FK → partner_centers NULL | **NEW** — the licensed center that legally sponsors this nurse at launch (Asanism model). NULL once Balinyaar holds its own permit. |
| `bio`, `years_of_experience`, `education_level`, `education_field`, `specializations_json` | … | Unchanged. |
| `is_verified` | BIT NOT NULL DEFAULT 0 | **Guarded** — set **only** inside the transaction that confirms all required `verification_steps.status='passed'`. No direct write API (Principle 12). |
| ~~`verification_status`~~ | — | **CUT** — duplicated `nurse_verifications.status`; two copies drifted. `nurse_verifications.status` is now the single source of truth. |
| `is_accepting_bookings` | BIT NOT NULL DEFAULT 0 | Nurse can pause without losing verified status. |
| `average_rating`, `total_reviews`, `total_completed_bookings` | … | Denormalized. **Recompute rule now documented**: updated on **every** review status transition (publish → +, hide/reject/unpublish → ) and on booking completion/dispute-reversal, plus a nightly reconciliation job. Fixes the "hide a 1★ review → rating stays inflated" drift. |
| ~~`response_rate`, `avg_response_time_hours`, `profile_completion_score`~~ | — | **CUT for MVP** — analytics columns on no money/safety path, each needing a maintenance job. Compute offline later. |
| `created_at`, `updated_at`, `deleted_at` | … | |
**Relations:** 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]
**Role:** Lightweight extension for customers. **Why intentionally thin:** most customer reality lives in their `patients`, `customer_addresses`, and `bookings`. KYC for customers is deferred. Unchanged: `id`, `user_id` (unique), `default_emergency_contact_name`/`_phone` (enc), `created_at`, `updated_at`. **CUT for MVP:** `national_id_verified_at` (anti-fraud customer KYC — add when actually built). **Relations:** 1:1 → `users`; 1:N → `patients`, `customer_addresses`, `booking_requests`, `bookings`.
### `patients` [CORE]
**Role:** The person receiving care, **separate from the payer**. **Why:** the payer (adult child, spouse) is usually not the patient (elderly parent, newborn, post-surgical adult); one customer registers many patients, each with its own clinical baseline and longitudinal record. Unchanged: `id`, `customer_id`, `display_name`, `first_name`, `last_name`, `birth_date`, `gender`, `blood_type`, `initial_medical_notes` (enc), `is_active`, timestamps. **Relations:** N:1 → `customer_profiles`; 1:N → `booking_requests`, `patient_care_records`. **Tenancy invariant:** a `booking_request.patient_id` must belong to the same `customer_id`.
### `customer_addresses` [CORE]
**Role:** Saved service locations; the encrypted address + coordinates for EVV distance checks. **Why coordinates:** EVV check-in compares the nurse's GPS against the booking address within tolerance. Unchanged fields, plus: **filtered `UNIQUE(customer_id) WHERE is_primary=1`** so exactly one primary exists (prevents ambiguous default). **Relations:** N:1 → `customer_profiles`, `cities`, `districts`; referenced by `booking_requests`/`bookings`.
### `nurse_bank_accounts` [CORE]
**Role:** Payout destination (IBAN/Sheba). **Why hardened:** the IBAN is the single place real money leaves the platform — the original "admin eyeballs the IBAN" check is exactly the forgeable, money-mule-risk link the research warns about.
| Field | Type | Notes |
|---|---|---|
| `id`, `nurse_id`, `bank_name`, `account_holder_name` (enc), `iban` (enc), `is_primary`, `is_verified`, `verified_by_admin_id`, `verified_at`, timestamps | … | Baseline. |
| `iban_hash` | NVARCHAR(64) | **NEW** — deterministic hash for a **UNIQUE** constraint (same IBAN must not silently serve two nurses). |
| `matched_national_id` | BIT NULL | **NEW** — result of an automated **IBAN-owner ↔ national-id inquiry (استعلام شبا)** via a KYC vendor. First payout is gated on a match, not on admin eyeballing. |
| `account_holder_from_bank` | NVARCHAR(200) NULL | **NEW** — name returned by the bank inquiry, snapshot. |
| `ownership_vendor_ref` | NVARCHAR(200) NULL | **NEW** — vendor transaction id for audit. |
Constraints: **filtered `UNIQUE(nurse_id) WHERE is_primary=1`**; `UNIQUE(iban_hash)`. **Relations:** N:1 → `nurse_profiles`; 1:N → `nurse_payouts`.
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 2 — Geographic Data — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a class="active" href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-2-geographic-data">Domain 2 — Geographic Data</h1>
<p><a href="index.html">← Database Model</a></p>
<h3 id="provinces-cities-districts-coremvp"><code>provinces</code> / <code>cities</code> / <code>districts</code> [CORE]/[MVP] <a class="anchor" href="#provinces-cities-districts-coremvp" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The geo hierarchy backing service areas, addresses, and search. <strong>Why a table, not a static list:</strong> new cities/districts launch without a deploy, and <code>sort_order</code>/<code>is_active</code> drive ordered, toggleable dropdowns. <code>districts</code> map to Tehran's 22 municipal districts or major neighborhoods elsewhere; they are <strong>optional</strong> (a nurse can cover a whole city). Fields unchanged. <strong>Relations:</strong> <code>provinces</code> 1:N <code>cities</code> 1:N <code>districts</code>; referenced by <code>customer_addresses</code> and <code>nurse_service_areas</code>.</p>
<h3 id="nurse_service_areas-core"><code>nurse_service_areas</code> [CORE] <a class="anchor" href="#nurse_service_areas-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Where a nurse will travel. A row with <code>district_id = NULL</code> means the entire city. <strong>Why a join table (not a radius):</strong> Iranian nurses think in named districts, not GPS radii; this also drives the geographic filter in search cheaply. Unchanged, with <code>UNIQUE(nurse_id, city_id, district_id)</code>. <strong>Relations:</strong> N:1 → <code>nurse_profiles</code>, <code>cities</code>, <code>districts</code>.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
# Domain 2 — Geographic Data
[← Database Model](index.md)
### `provinces` / `cities` / `districts` [CORE]/[MVP]
**Role:** The geo hierarchy backing service areas, addresses, and search. **Why a table, not a static list:** new cities/districts launch without a deploy, and `sort_order`/`is_active` drive ordered, toggleable dropdowns. `districts` map to Tehran's 22 municipal districts or major neighborhoods elsewhere; they are **optional** (a nurse can cover a whole city). Fields unchanged. **Relations:** `provinces` 1:N `cities` 1:N `districts`; referenced by `customer_addresses` and `nurse_service_areas`.
### `nurse_service_areas` [CORE]
**Role:** Where a nurse will travel. A row with `district_id = NULL` means the entire city. **Why a join table (not a radius):** Iranian nurses think in named districts, not GPS radii; this also drives the geographic filter in search cheaply. Unchanged, with `UNIQUE(nurse_id, city_id, district_id)`. **Relations:** N:1 → `nurse_profiles`, `cities`, `districts`.
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 3 — Services &amp; Pricing — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a class="active" href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-3-services-pricing">Domain 3 — Services &amp; Pricing</h1>
<p><a href="index.html">← Database Model</a></p>
<p>The service model keeps the original three admin layers (category → option group → option value) and two nurse layers (variant → variant option). This <strong>EAV-style configurability is deliberately kept</strong> — it lets admins add a new pricing dimension (e.g. "شبانه‌روزی / live-in", "number of patients") without a migration, and lets each nurse price every combination independently. The only addition is a denormalized read model for search.</p>
<h3 id="service_categories-core"><code>service_categories</code> [CORE] <a class="anchor" href="#service_categories-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Admin-managed top-level care types (Elderly, Post-Surgery, Infant, Chronic, Companionship). The primary search dimension. <strong>Why admin-managed rows:</strong> the catalog is a business lever, not a code constant. Fields unchanged. <strong>Relations:</strong> 1:N → <code>service_option_groups</code>, <code>nurse_service_variants</code>.</p>
<h3 id="service_option_groups-core-service_option_values-core"><code>service_option_groups</code> [CORE] / <code>service_option_values</code> [CORE] <a class="anchor" href="#service_option_groups-core-service_option_values-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The configurable dimensions (group, e.g. "نوع شیفت") and their concrete choices (value, e.g. "شبانه‌روزی"). A NULL <code>service_category_id</code> on a group = cross-category (e.g. shift type applies everywhere). <strong>Why two tables:</strong> separating dimension from choice lets a dimension be <code>is_required</code> and reused across categories. Fields unchanged. <strong>Relations:</strong> <code>service_categories</code> 1:N <code>service_option_groups</code> 1:N <code>service_option_values</code>.</p>
<h3 id="nurse_service_variants-core"><code>nurse_service_variants</code> [CORE] <a class="anchor" href="#nurse_service_variants-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The atomic <strong>bookable unit</strong> — a specific nurse offering a category with a chosen option combination at a price. <strong>Why this is the bookable unit (not the nurse):</strong> a nurse offers many priced combinations; search and booking operate on the exact thing the customer pays for. The <code>price_unit</code> (<code>per_hour</code>/<code>per_session</code>/<code>per_half_day</code>/<code>per_day</code>/<code>per_24h</code>) determines display and, with <code>session_count</code>, the engagement total. Fields unchanged. <strong>Consider</strong> a uniqueness strategy on <code>(nurse_id, category, option-set)</code> to prevent duplicate identical listings. <strong>Relations:</strong> N:1 → <code>nurse_profiles</code>, <code>service_categories</code>; 1:N → <code>nurse_service_variant_options</code>, <code>booking_requests</code>.</p>
<h3 id="nurse_service_variant_options-core"><code>nurse_service_variant_options</code> [CORE] <a class="anchor" href="#nurse_service_variant_options-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The option values that define a variant's configuration. <strong>Why:</strong> one row per dimension makes the variant's meaning explicit and queryable. <code>UNIQUE(variant_id, option_group_id)</code> — one value per dimension. <strong>Relations:</strong> N:1 → <code>nurse_service_variants</code>, <code>service_option_groups</code>, <code>service_option_values</code>.</p>
<h3 id="nurse_search_index-core-new"><code>nurse_search_index</code> [CORE] — <strong>NEW</strong> <a class="anchor" href="#nurse_search_index-core-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> A denormalized, one-row-per-bookable-variant read model holding every search-relevant field flat: nurse (verified + accepting), variant (category, price, unit), areas (city/district), gender, rating, partner center. <strong>Why:</strong> nurse search otherwise needs 4+ joins (<code>nurse_profiles → variants → variant_options → service_areas</code>) plus a rating sort from day one — slow at modest scale. A maintained-on-write flat table is far cheaper than adding Elasticsearch at MVP stage.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>variant_id</code></td><td>BIGINT FK → nurse_service_variants</td><td></td></tr>
<tr><td><code>nurse_id</code></td><td>BIGINT FK → nurse_profiles</td><td></td></tr>
<tr><td><code>service_category_id</code></td><td>BIGINT FK</td><td></td></tr>
<tr><td><code>price</code>, <code>price_unit</code></td><td></td><td>Copied from variant</td></tr>
<tr><td><code>city_id</code>, <code>district_id</code></td><td>BIGINT</td><td>One row per covered area (fan-out)</td></tr>
<tr><td><code>nurse_gender</code></td><td>NVARCHAR(10)</td><td>For same-gender filtering</td></tr>
<tr><td><code>average_rating</code>, <code>total_reviews</code>, <code>total_completed_bookings</code></td><td></td><td>Copied from profile</td></tr>
<tr><td><code>is_searchable</code></td><td>BIT</td><td>True <strong>only</strong> when nurse <code>is_verified=1</code>, not suspended, accepting, and variant <code>is_active=1</code></td></tr>
<tr><td><code>updated_at</code></td><td>DATETIME2</td><td></td></tr>
</tbody></table></div>
<p><strong>Relations (read-only projection):</strong> maintained on writes to <code>nurse_profiles</code>, <code>nurse_service_variants</code>, <code>nurse_service_areas</code>, <code>reviews</code>. <strong>Invariant:</strong> a row is <code>is_searchable=1</code> only when its source nurse/variant are bookable.</p>
<h3 id="nurse_availability_slots-mvp-nurse_availability_exceptions-mvp"><code>nurse_availability_slots</code> [MVP] / <code>nurse_availability_exceptions</code> [MVP] <a class="anchor" href="#nurse_availability_slots-mvp-nurse_availability_exceptions-mvp" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Recurring weekly windows + date overrides. <strong>Why soft-constraint:</strong> these are <strong>guidance only</strong> — the nurse still accepts/rejects each request; they inform search but never block a request. <code>day_of_week</code> uses the Shamsi week (0=Saturday … 6=Friday). Fields unchanged, with CHECK <code>end_time &gt; start_time</code>. <strong>Relations:</strong> N:1 → <code>nurse_profiles</code>.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,38 @@
# Domain 3 — Services & Pricing
[← Database Model](index.md)
The service model keeps the original three admin layers (category → option group → option value) and two nurse layers (variant → variant option). This **EAV-style configurability is deliberately kept** — it lets admins add a new pricing dimension (e.g. "شبانه‌روزی / live-in", "number of patients") without a migration, and lets each nurse price every combination independently. The only addition is a denormalized read model for search.
### `service_categories` [CORE]
**Role:** Admin-managed top-level care types (Elderly, Post-Surgery, Infant, Chronic, Companionship). The primary search dimension. **Why admin-managed rows:** the catalog is a business lever, not a code constant. Fields unchanged. **Relations:** 1:N → `service_option_groups`, `nurse_service_variants`.
### `service_option_groups` [CORE] / `service_option_values` [CORE]
**Role:** The configurable dimensions (group, e.g. "نوع شیفت") and their concrete choices (value, e.g. "شبانه‌روزی"). A NULL `service_category_id` on a group = cross-category (e.g. shift type applies everywhere). **Why two tables:** separating dimension from choice lets a dimension be `is_required` and reused across categories. Fields unchanged. **Relations:** `service_categories` 1:N `service_option_groups` 1:N `service_option_values`.
### `nurse_service_variants` [CORE]
**Role:** The atomic **bookable unit** — a specific nurse offering a category with a chosen option combination at a price. **Why this is the bookable unit (not the nurse):** a nurse offers many priced combinations; search and booking operate on the exact thing the customer pays for. The `price_unit` (`per_hour`/`per_session`/`per_half_day`/`per_day`/`per_24h`) determines display and, with `session_count`, the engagement total. Fields unchanged. **Consider** a uniqueness strategy on `(nurse_id, category, option-set)` to prevent duplicate identical listings. **Relations:** N:1 → `nurse_profiles`, `service_categories`; 1:N → `nurse_service_variant_options`, `booking_requests`.
### `nurse_service_variant_options` [CORE]
**Role:** The option values that define a variant's configuration. **Why:** one row per dimension makes the variant's meaning explicit and queryable. `UNIQUE(variant_id, option_group_id)` — one value per dimension. **Relations:** N:1 → `nurse_service_variants`, `service_option_groups`, `service_option_values`.
### `nurse_search_index` [CORE] — **NEW**
**Role:** A denormalized, one-row-per-bookable-variant read model holding every search-relevant field flat: nurse (verified + accepting), variant (category, price, unit), areas (city/district), gender, rating, partner center. **Why:** nurse search otherwise needs 4+ joins (`nurse_profiles → variants → variant_options → service_areas`) plus a rating sort from day one — slow at modest scale. A maintained-on-write flat table is far cheaper than adding Elasticsearch at MVP stage.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `variant_id` | BIGINT FK → nurse_service_variants | |
| `nurse_id` | BIGINT FK → nurse_profiles | |
| `service_category_id` | BIGINT FK | |
| `price`, `price_unit` | … | Copied from variant |
| `city_id`, `district_id` | BIGINT | One row per covered area (fan-out) |
| `nurse_gender` | NVARCHAR(10) | For same-gender filtering |
| `average_rating`, `total_reviews`, `total_completed_bookings` | … | Copied from profile |
| `is_searchable` | BIT | True **only** when nurse `is_verified=1`, not suspended, accepting, and variant `is_active=1` |
| `updated_at` | DATETIME2 | |
**Relations (read-only projection):** maintained on writes to `nurse_profiles`, `nurse_service_variants`, `nurse_service_areas`, `reviews`. **Invariant:** a row is `is_searchable=1` only when its source nurse/variant are bookable.
### `nurse_availability_slots` [MVP] / `nurse_availability_exceptions` [MVP]
**Role:** Recurring weekly windows + date overrides. **Why soft-constraint:** these are **guidance only** — the nurse still accepts/rejects each request; they inform search but never block a request. `day_of_week` uses the Shamsi week (0=Saturday … 6=Friday). Fields unchanged, with CHECK `end_time > start_time`. **Relations:** N:1 → `nurse_profiles`.
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 4 — Verification &amp; Credentials — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a class="active" href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-4-verification-credentials">Domain 4 — Verification &amp; Credentials</h1>
<p><a href="index.html">← Database Model</a></p>
<p><strong>Related:</strong> business requirements — <a href="../business/02-nurse-verification.html">Nurse verification</a>.</p>
<p>The pipeline stays <strong>data-driven</strong>: step types are rows, so a new regulatory requirement (e.g. professional liability insurance) is one INSERT. This revision adds a <strong>structured credential registry</strong> because the brand <em>is</em> "verified trust" and renewal tracking needs queryable license numbers, not opaque PDFs.</p>
<h3 id="nurse_verifications-core"><code>nurse_verifications</code> [CORE] <a class="anchor" href="#nurse_verifications-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The master per-nurse verification record; aggregates step outcomes into one status (the single source of truth for verification state). <strong>Why a header table:</strong> one place to drive the overall lifecycle and the <code>is_verified</code> flip. Fields unchanged: <code>id</code>, <code>nurse_id</code> (unique), <code>status</code> (<code>not_started</code>/<code>pending</code>/<code>in_review</code>/<code>approved</code>/<code>rejected</code>/<code>suspended</code>), <code>submitted_at</code>, <code>approved_at</code>, <code>rejected_at</code>, <code>suspended_at</code>, <code>rejection_reason</code>, <code>reviewed_by_admin_id</code>, <code>internal_notes</code>, timestamps. <strong>Relations:</strong> 1:1 → <code>nurse_profiles</code>; 1:N → <code>verification_steps</code>.</p>
<h3 id="verification_step_types-core"><code>verification_step_types</code> [CORE] <a class="anchor" href="#verification_step_types-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Admin catalog of pipeline steps with stable machine <code>code</code>s (<code>identity_kyc</code>, <code>shahkar_match</code>, <code>moh_competency_license</code>, <code>ino_membership</code>, <code>criminal_record</code>, <code>bank_account_verification</code>). <strong>Why rows + <code>is_automated</code>/<code>automation_provider</code>:</strong> the Iranian credential reality is fragmented across regulators and partly automatable (Shahkar, liveness) and partly manual (license PDF) — data-driving it absorbs that without code changes. Fields unchanged. <strong>Relations:</strong> 1:N → <code>verification_steps</code>.</p>
<h3 id="verification_steps-core"><code>verification_steps</code> [CORE] <a class="anchor" href="#verification_steps-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> One row per step per nurse; tracks status, the raw <code>external_response_json</code> (KYC vendor audit), and <code>expires_at</code> for time-limited steps (the عدم سوء پیشینه certificate expires → step reverts to <code>pending</code> + raises a <code>support_alert</code>). <strong>Why snapshot <code>is_automated</code>:</strong> historical records survive later step-type edits. Fields unchanged, with <code>UNIQUE(nurse_verification_id, step_type_id)</code>. <strong>Relations:</strong> N:1 → <code>nurse_verifications</code>, <code>verification_step_types</code>; 1:N → <code>verification_documents</code>.</p>
<h3 id="verification_documents-core"><code>verification_documents</code> [CORE] <a class="anchor" href="#verification_documents-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Uploaded evidence metadata (object-storage key + integrity hash); files live in S3-compatible storage behind signed URLs, never public. <strong>Why metadata-only:</strong> keeps PII bytes out of the DB and access controlled. Fields unchanged. <strong>Relations:</strong> N:1 → <code>verification_steps</code>.</p>
<h3 id="nurse_credentials-mvp-new"><code>nurse_credentials</code> [MVP] — <strong>NEW</strong> <a class="anchor" href="#nurse_credentials-mvp-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Structured, queryable registry of the actual Iranian credentials — beyond the opaque document uploads. <strong>Why:</strong> no public B2B API exists for MoH/INO, so an admin manually verifies an uploaded credential against the official portal — but the old model gave them <strong>nowhere to record the verified license number</strong> for renewal alerts, the public trust badge, or cross-check. This makes the badge and expiry monitoring real and survives a future INO/MoH API.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>nurse_id</code></td><td>BIGINT FK → nurse_profiles</td><td></td></tr>
<tr><td><code>credential_type</code></td><td>NVARCHAR(50)</td><td><code>moh_competency_license</code> (پروانه صلاحیت حرفه‌ای) / <code>ino_membership</code> (نظام پرستاری) / <code>criminal_record</code> (عدم سوء پیشینه)</td></tr>
<tr><td><code>credential_number</code></td><td>NVARCHAR(100) (enc)</td><td>License/membership number</td></tr>
<tr><td><code>holder_name_snapshot</code></td><td>NVARCHAR(200)</td><td>Name as printed, for ID cross-check</td></tr>
<tr><td><code>issuing_authority</code></td><td>NVARCHAR(200)</td><td></td></tr>
<tr><td><code>issued_at</code>, <code>expires_at</code></td><td>DATE NULL</td><td>Drives renewal alerts</td></tr>
<tr><td><code>verification_source</code></td><td>NVARCHAR(300) NULL</td><td>Portal URL / method</td></tr>
<tr><td><code>verification_method</code></td><td>NVARCHAR(20)</td><td><code>manual</code> / <code>portal</code> / <code>api</code></td></tr>
<tr><td><code>verified_by_admin_id</code></td><td>BIGINT FK → users NULL</td><td></td></tr>
<tr><td><code>created_at</code>, <code>updated_at</code></td><td></td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>nurse_profiles</code>. Cross-referenced by the relevant <code>verification_steps</code>.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,38 @@
# Domain 4 — Verification & Credentials
[← Database Model](index.md)
**Related:** business requirements — [Nurse verification](../business/02-nurse-verification.md).
The pipeline stays **data-driven**: step types are rows, so a new regulatory requirement (e.g. professional liability insurance) is one INSERT. This revision adds a **structured credential registry** because the brand *is* "verified trust" and renewal tracking needs queryable license numbers, not opaque PDFs.
### `nurse_verifications` [CORE]
**Role:** The master per-nurse verification record; aggregates step outcomes into one status (the single source of truth for verification state). **Why a header table:** one place to drive the overall lifecycle and the `is_verified` flip. Fields unchanged: `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`, timestamps. **Relations:** 1:1 → `nurse_profiles`; 1:N → `verification_steps`.
### `verification_step_types` [CORE]
**Role:** Admin catalog of pipeline steps with stable machine `code`s (`identity_kyc`, `shahkar_match`, `moh_competency_license`, `ino_membership`, `criminal_record`, `bank_account_verification`). **Why rows + `is_automated`/`automation_provider`:** the Iranian credential reality is fragmented across regulators and partly automatable (Shahkar, liveness) and partly manual (license PDF) — data-driving it absorbs that without code changes. Fields unchanged. **Relations:** 1:N → `verification_steps`.
### `verification_steps` [CORE]
**Role:** One row per step per nurse; tracks status, the raw `external_response_json` (KYC vendor audit), and `expires_at` for time-limited steps (the عدم سوء پیشینه certificate expires → step reverts to `pending` + raises a `support_alert`). **Why snapshot `is_automated`:** historical records survive later step-type edits. Fields unchanged, with `UNIQUE(nurse_verification_id, step_type_id)`. **Relations:** N:1 → `nurse_verifications`, `verification_step_types`; 1:N → `verification_documents`.
### `verification_documents` [CORE]
**Role:** Uploaded evidence metadata (object-storage key + integrity hash); files live in S3-compatible storage behind signed URLs, never public. **Why metadata-only:** keeps PII bytes out of the DB and access controlled. Fields unchanged. **Relations:** N:1 → `verification_steps`.
### `nurse_credentials` [MVP] — **NEW**
**Role:** Structured, queryable registry of the actual Iranian credentials — beyond the opaque document uploads. **Why:** no public B2B API exists for MoH/INO, so an admin manually verifies an uploaded credential against the official portal — but the old model gave them **nowhere to record the verified license number** for renewal alerts, the public trust badge, or cross-check. This makes the badge and expiry monitoring real and survives a future INO/MoH API.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `nurse_id` | BIGINT FK → nurse_profiles | |
| `credential_type` | NVARCHAR(50) | `moh_competency_license` (پروانه صلاحیت حرفه‌ای) / `ino_membership` (نظام پرستاری) / `criminal_record` (عدم سوء پیشینه) |
| `credential_number` | NVARCHAR(100) (enc) | License/membership number |
| `holder_name_snapshot` | NVARCHAR(200) | Name as printed, for ID cross-check |
| `issuing_authority` | NVARCHAR(200) | |
| `issued_at`, `expires_at` | DATE NULL | Drives renewal alerts |
| `verification_source` | NVARCHAR(300) NULL | Portal URL / method |
| `verification_method` | NVARCHAR(20) | `manual` / `portal` / `api` |
| `verified_by_admin_id` | BIGINT FK → users NULL | |
| `created_at`, `updated_at` | … | |
**Relations:** N:1 → `nurse_profiles`. Cross-referenced by the relevant `verification_steps`.
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 5 — Booking &amp; Scheduling — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a class="active" href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-5-booking-scheduling">Domain 5 — Booking &amp; Scheduling</h1>
<p><a href="index.html">← Database Model</a></p>
<p><strong>Related:</strong> business requirements — <a href="../business/05-booking-and-scheduling.html">Booking &amp; scheduling</a>.</p>
<p>Two distinct phases: the <strong>request phase</strong> (pre-payment intent) and the <strong>booking phase</strong> (post-payment commitment). The previous model's biggest domain gap — <strong>single-visit-only bookings</strong> — is fixed here with <code>booking_sessions</code>, because elder care is dominantly multi-day / live-in.</p>
<h3 id="booking_requests-core"><code>booking_requests</code> [CORE] <a class="anchor" href="#booking_requests-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> A customer's pre-payment intent for a nurse, service, date, and time. No money involved. <strong>Why separate from <code>bookings</code>:</strong> a request may be rejected, expire, or have its payment window lapse without a booking ever existing — merging would mean many nullable fields and tangled status logic. The deadlines are <strong>computed once and stored</strong> so they're immune to later config changes.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code>, <code>customer_id</code>, <code>nurse_id</code>, <code>patient_id</code>, <code>variant_id</code>, <code>customer_address_id</code></td><td></td><td>Baseline FKs. <strong>Tenancy invariant:</strong> patient &amp; address belong to <code>customer_id</code>; variant belongs to <code>nurse_id</code>.</td></tr>
<tr><td><code>required_caregiver_gender</code></td><td>NVARCHAR(10) NULL</td><td><strong>NEW</strong><code>male</code>/<code>female</code>/<code>any</code>. Same-gender care is decisive in Iranian bodily-care; surfaced as a first-class filter and matched against nurse gender.</td></tr>
<tr><td><code>requested_date</code>, <code>requested_time_start</code>, <code>requested_time_end</code></td><td></td><td>For multi-day engagements these are the engagement start; sessions carry the per-visit schedule.</td></tr>
<tr><td><code>customer_notes</code></td><td>NVARCHAR(1000) NULL</td><td><strong>Unencrypted, request-stage only</strong> — the <em>only</em> clinical context the nurse sees before accepting (Principle 6).</td></tr>
<tr><td><code>status</code></td><td>NVARCHAR(50)</td><td><code>pending_nurse_response</code><code>accepted_awaiting_payment</code><code>converted</code> / <code>rejected_by_nurse</code> / <code>expired_no_response</code> / <code>payment_deadline_expired</code> / <code>cancelled_by_customer</code></td></tr>
<tr><td><code>nurse_response_deadline_at</code></td><td>DATETIME2</td><td>From config at creation; auto-expire after.</td></tr>
<tr><td><code>payment_deadline_at</code></td><td>DATETIME2 NULL</td><td>Set on accept; 30-min window.</td></tr>
<tr><td><code>nurse_rejection_reason</code></td><td>NVARCHAR(500) NULL</td><td></td></tr>
<tr><td>timestamps</td><td></td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>customer_profiles</code>, <code>nurse_profiles</code>, <code>patients</code>, <code>nurse_service_variants</code>, <code>customer_addresses</code>; 1:1 → <code>bookings</code> (on conversion).</p>
<h3 id="bookings-core"><code>bookings</code> [CORE] <a class="anchor" href="#bookings-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The confirmed engagement — exists <strong>only</strong> when the nurse accepted <strong>and</strong> payment was captured. Source of truth for the service event and its money split. <strong>Why snapshots:</strong> <code>variant_snapshot_json</code> and <code>address_snapshot_json</code> freeze the service and address at booking time, so later edits/deletes can't corrupt history or disputes.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>booking_request_id</code></td><td>BIGINT FK UNIQUE</td><td>1:1 — the request that created it</td></tr>
<tr><td><code>customer_id</code>, <code>nurse_id</code>, <code>patient_id</code>, <code>variant_id</code>, <code>customer_address_id</code></td><td></td><td>Denormalized for query performance</td></tr>
<tr><td><code>variant_snapshot_json</code></td><td>NVARCHAR(MAX)</td><td>Variant + option labels at booking time</td></tr>
<tr><td><code>address_snapshot_json</code></td><td>NVARCHAR(MAX) (enc)</td><td>Full address at booking time</td></tr>
<tr><td><code>partner_center_id</code></td><td>BIGINT FK → partner_centers NULL</td><td><strong>NEW</strong> — the licensed center legally covering this visit / merchant-of-record</td></tr>
<tr><td><code>gross_price_irr</code></td><td>BIGINT</td><td><strong>CHANGED (renamed)</strong> — total charged to customer</td></tr>
<tr><td><code>balinyaar_commission_irr</code></td><td>BIGINT</td><td><strong>CHANGED (renamed from <code>platform_fee_amount</code>)</strong> — platform's own cut</td></tr>
<tr><td><code>platform_fee_rate</code></td><td>DECIMAL(5,4)</td><td>Rate snapshot for audit</td></tr>
<tr><td><code>nurse_payout_amount</code></td><td>BIGINT</td><td><code>= gross_price_irr balinyaar_commission_irr</code>. <strong>CHECK enforced.</strong></td></tr>
<tr><td><code>psp_fee_amount</code></td><td>BIGINT NULL</td><td><strong>NEW</strong> — gateway cost on this payment, for true margin/reconciliation</td></tr>
<tr><td><code>session_count</code></td><td>SMALLINT NOT NULL DEFAULT 1</td><td><strong>NEW</strong> — 1 = single visit; &gt;1 = multi-session engagement</td></tr>
<tr><td><code>scheduled_date</code>, <code>scheduled_time_start</code>, <code>scheduled_time_end</code></td><td></td><td>Engagement-level; per-visit lives in <code>booking_sessions</code></td></tr>
<tr><td><code>status</code></td><td>NVARCHAR(30)</td><td><code>pending_payment</code><code>confirmed</code><code>in_progress</code><code>completed</code> → (<code>disputed</code><code>closed</code>) / <code>cancelled</code>. <strong>Now guarded by an allowed-transition table/CHECK</strong> so it can't contradict EVV.</td></tr>
<tr><td><code>confirmed_at</code>, <code>cancelled_at</code>, <code>cancellation_reason</code>, <code>cancelled_by</code>, <code>completed_at</code></td><td></td><td></td></tr>
<tr><td><code>dispute_window_ends_at</code></td><td>DATETIME2 NULL</td><td><strong>NEW</strong><code>completed_at + config(dispute_window_hours, default 72)</code>. Payout eligible only after this passes with no open dispute.</td></tr>
<tr><td><del><code>payout_released</code></del></td><td></td><td><strong>CUT</strong> — second source of truth for "paid"; now derived from a <code>nurse_payout_booking_links</code> row + ledger.</td></tr>
<tr><td>timestamps</td><td></td><td></td></tr>
</tbody></table></div>
<p>CHECK: <code>gross_price_irr = balinyaar_commission_irr + nurse_payout_amount</code>; all amounts ≥ 0. <strong>Relations:</strong> 1:1 ← <code>booking_requests</code>; 1:N → <code>booking_sessions</code>, <code>payment_transactions</code>, <code>ledger_entries</code>; 1:1 → <code>booking_care_instructions</code>, <code>visit_verifications</code> <em>(see note)</em>, <code>reviews</code>, <code>invoices</code>; referenced by <code>nurse_payout_booking_links</code>, <code>refunds</code>, <code>nurse_clawbacks</code>.</p>
<h3 id="booking_sessions-mvp-new"><code>booking_sessions</code> [MVP] — <strong>NEW</strong> <a class="anchor" href="#booking_sessions-mvp-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> One row per <strong>visit</strong> within a booking. A 10-day post-op package or a month of nightly شبانه‌روزی care is <strong>one booking with N sessions</strong>, each with its own schedule, EVV, and payout eligibility. <strong>Why:</strong> the single-visit model literally cannot represent the dominant elder-care engagement; and money for a long engagement must release <strong>per completed session</strong>, not as one whole-month escrow held for weeks. Mid-engagement cancellation then cleanly refunds only the un-started sessions.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>booking_id</code></td><td>BIGINT FK → bookings</td><td></td></tr>
<tr><td><code>session_index</code></td><td>INT</td><td>1-based ordinal</td></tr>
<tr><td><code>scheduled_date</code>, <code>scheduled_time_start</code>, <code>scheduled_time_end</code></td><td></td><td>Per-visit</td></tr>
<tr><td><code>visit_payout_amount</code></td><td>BIGINT</td><td>Portion of <code>nurse_payout_amount</code> for this session</td></tr>
<tr><td><code>status</code></td><td>NVARCHAR(20)</td><td><code>scheduled</code> / <code>in_progress</code> / <code>completed</code> / <code>missed</code> / <code>cancelled</code></td></tr>
<tr><td><code>payout_eligible_at</code></td><td>DATETIME2 NULL</td><td>Per-session dispute-window close</td></tr>
<tr><td><code>cancellation_event_id</code></td><td>BIGINT NULL</td><td>If this session was cancelled</td></tr>
<tr><td>timestamps</td><td></td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>bookings</code>; 1:1 → <code>visit_verifications</code>. <strong>Note:</strong> for a single-visit booking, exactly one session is created so the EVV/payout path is uniform.</p>
<h3 id="booking_care_instructions-core"><code>booking_care_instructions</code> [CORE] <a class="anchor" href="#booking_care_instructions-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Encrypted clinical/logistical context provided at booking time, visible <strong>only post-confirmation</strong> to the assigned nurse and admin. <strong>Why separate + encrypted:</strong> keeps the financial/scheduling table clean and enforces the two-stage disclosure boundary (Principle 6) with stricter access controls. Fields unchanged (current conditions, medications, allergies, special instructions, emergency contact — all enc). <strong>Relations:</strong> 1:1 → <code>bookings</code>.</p>
<h3 id="visit_verifications-core"><code>visit_verifications</code> [CORE] <a class="anchor" href="#visit_verifications-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Electronic Visit Verification — the authoritative record that a visit happened and for how long; <strong>required for payout</strong>. <strong>Why:</strong> in-home care is unobserved; GPS + timestamped check-in/out is the proof that safely releases escrow. <code>check_in_address_match</code> is advisory (a mismatch triggers admin review, not auto-cancel). <strong>CHANGED:</strong> the FK moves to <strong><code>booking_session_id</code></strong> so each visit in a multi-session engagement is verified independently; the mapping between <code>visit_verifications.status</code> and the parent <code>bookings.status</code> is documented so the two state machines cannot silently diverge. Fields otherwise unchanged. <strong>Relations:</strong> 1:1 → <code>booking_sessions</code>.</p>
<h3 id="cancellation_policies-mvp-new"><code>cancellation_policies</code> [MVP] — <strong>NEW</strong> <a class="anchor" href="#cancellation_policies-mvp-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Config-driven, snapshot-able cancellation/refund tiers by lead time and initiating actor. <strong>Why:</strong> "default 100% refund" is naive — a free cancel 24h ahead, a 50% charge inside 24h, and a nurse-no-show full refund + nurse penalty are different money flows, and the applicable rule must be <strong>frozen onto the booking</strong> at cancel time (the same snapshot discipline used for <code>platform_fee_rate</code>).</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>code</code></td><td>NVARCHAR(50) UNIQUE</td><td>e.g. <code>standard_24h</code></td></tr>
<tr><td><code>applies_to</code></td><td>NVARCHAR(20)</td><td><code>customer</code> / <code>nurse</code> / <code>admin</code></td></tr>
<tr><td><code>hours_before_start_min</code>, <code>hours_before_start_max</code></td><td>INT NULL</td><td>Tier bounds</td></tr>
<tr><td><code>refund_percentage</code></td><td>DECIMAL(5,2)</td><td>0100</td></tr>
<tr><td><code>fee_amount_or_rate</code></td><td></td><td>Cancellation fee / nurse penalty</td></tr>
<tr><td><code>is_active</code>, timestamps</td><td></td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> referenced (snapshot) by <code>refunds</code> and the <code>cancellation_event</code> recorded on a session/booking.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,88 @@
# Domain 5 — Booking & Scheduling
[← Database Model](index.md)
**Related:** business requirements — [Booking & scheduling](../business/05-booking-and-scheduling.md).
Two distinct phases: the **request phase** (pre-payment intent) and the **booking phase** (post-payment commitment). The previous model's biggest domain gap — **single-visit-only bookings** — is fixed here with `booking_sessions`, because elder care is dominantly multi-day / live-in.
### `booking_requests` [CORE]
**Role:** A customer's pre-payment intent for a nurse, service, date, and time. No money involved. **Why separate from `bookings`:** a request may be rejected, expire, or have its payment window lapse without a booking ever existing — merging would mean many nullable fields and tangled status logic. The deadlines are **computed once and stored** so they're immune to later config changes.
| Field | Type | Notes |
|---|---|---|
| `id`, `customer_id`, `nurse_id`, `patient_id`, `variant_id`, `customer_address_id` | … | Baseline FKs. **Tenancy invariant:** patient & address belong to `customer_id`; variant belongs to `nurse_id`. |
| `required_caregiver_gender` | NVARCHAR(10) NULL | **NEW**`male`/`female`/`any`. Same-gender care is decisive in Iranian bodily-care; surfaced as a first-class filter and matched against nurse gender. |
| `requested_date`, `requested_time_start`, `requested_time_end` | … | For multi-day engagements these are the engagement start; sessions carry the per-visit schedule. |
| `customer_notes` | NVARCHAR(1000) NULL | **Unencrypted, request-stage only** — the *only* clinical context the nurse sees before accepting (Principle 6). |
| `status` | NVARCHAR(50) | `pending_nurse_response``accepted_awaiting_payment``converted` / `rejected_by_nurse` / `expired_no_response` / `payment_deadline_expired` / `cancelled_by_customer` |
| `nurse_response_deadline_at` | DATETIME2 | From config at creation; auto-expire after. |
| `payment_deadline_at` | DATETIME2 NULL | Set on accept; 30-min window. |
| `nurse_rejection_reason` | NVARCHAR(500) NULL | |
| timestamps | … | |
**Relations:** N:1 → `customer_profiles`, `nurse_profiles`, `patients`, `nurse_service_variants`, `customer_addresses`; 1:1 → `bookings` (on conversion).
### `bookings` [CORE]
**Role:** The confirmed engagement — exists **only** when the nurse accepted **and** payment was captured. Source of truth for the service event and its money split. **Why snapshots:** `variant_snapshot_json` and `address_snapshot_json` freeze the service and address at booking time, so later edits/deletes can't corrupt history or disputes.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `booking_request_id` | BIGINT FK UNIQUE | 1:1 — the request that created it |
| `customer_id`, `nurse_id`, `patient_id`, `variant_id`, `customer_address_id` | … | Denormalized for query performance |
| `variant_snapshot_json` | NVARCHAR(MAX) | Variant + option labels at booking time |
| `address_snapshot_json` | NVARCHAR(MAX) (enc) | Full address at booking time |
| `partner_center_id` | BIGINT FK → partner_centers NULL | **NEW** — the licensed center legally covering this visit / merchant-of-record |
| `gross_price_irr` | BIGINT | **CHANGED (renamed)** — total charged to customer |
| `balinyaar_commission_irr` | BIGINT | **CHANGED (renamed from `platform_fee_amount`)** — platform's own cut |
| `platform_fee_rate` | DECIMAL(5,4) | Rate snapshot for audit |
| `nurse_payout_amount` | BIGINT | `= gross_price_irr balinyaar_commission_irr`. **CHECK enforced.** |
| `psp_fee_amount` | BIGINT NULL | **NEW** — gateway cost on this payment, for true margin/reconciliation |
| `session_count` | SMALLINT NOT NULL DEFAULT 1 | **NEW** — 1 = single visit; >1 = multi-session engagement |
| `scheduled_date`, `scheduled_time_start`, `scheduled_time_end` | … | Engagement-level; per-visit lives in `booking_sessions` |
| `status` | NVARCHAR(30) | `pending_payment``confirmed``in_progress``completed` → (`disputed``closed`) / `cancelled`. **Now guarded by an allowed-transition table/CHECK** so it can't contradict EVV. |
| `confirmed_at`, `cancelled_at`, `cancellation_reason`, `cancelled_by`, `completed_at` | … | |
| `dispute_window_ends_at` | DATETIME2 NULL | **NEW**`completed_at + config(dispute_window_hours, default 72)`. Payout eligible only after this passes with no open dispute. |
| ~~`payout_released`~~ | — | **CUT** — second source of truth for "paid"; now derived from a `nurse_payout_booking_links` row + ledger. |
| timestamps | … | |
CHECK: `gross_price_irr = balinyaar_commission_irr + nurse_payout_amount`; all amounts ≥ 0. **Relations:** 1:1 ← `booking_requests`; 1:N → `booking_sessions`, `payment_transactions`, `ledger_entries`; 1:1 → `booking_care_instructions`, `visit_verifications` *(see note)*, `reviews`, `invoices`; referenced by `nurse_payout_booking_links`, `refunds`, `nurse_clawbacks`.
### `booking_sessions` [MVP] — **NEW**
**Role:** One row per **visit** within a booking. A 10-day post-op package or a month of nightly شبانه‌روزی care is **one booking with N sessions**, each with its own schedule, EVV, and payout eligibility. **Why:** the single-visit model literally cannot represent the dominant elder-care engagement; and money for a long engagement must release **per completed session**, not as one whole-month escrow held for weeks. Mid-engagement cancellation then cleanly refunds only the un-started sessions.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `booking_id` | BIGINT FK → bookings | |
| `session_index` | INT | 1-based ordinal |
| `scheduled_date`, `scheduled_time_start`, `scheduled_time_end` | … | Per-visit |
| `visit_payout_amount` | BIGINT | Portion of `nurse_payout_amount` for this session |
| `status` | NVARCHAR(20) | `scheduled` / `in_progress` / `completed` / `missed` / `cancelled` |
| `payout_eligible_at` | DATETIME2 NULL | Per-session dispute-window close |
| `cancellation_event_id` | BIGINT NULL | If this session was cancelled |
| timestamps | … | |
**Relations:** N:1 → `bookings`; 1:1 → `visit_verifications`. **Note:** for a single-visit booking, exactly one session is created so the EVV/payout path is uniform.
### `booking_care_instructions` [CORE]
**Role:** Encrypted clinical/logistical context provided at booking time, visible **only post-confirmation** to the assigned nurse and admin. **Why separate + encrypted:** keeps the financial/scheduling table clean and enforces the two-stage disclosure boundary (Principle 6) with stricter access controls. Fields unchanged (current conditions, medications, allergies, special instructions, emergency contact — all enc). **Relations:** 1:1 → `bookings`.
### `visit_verifications` [CORE]
**Role:** Electronic Visit Verification — the authoritative record that a visit happened and for how long; **required for payout**. **Why:** in-home care is unobserved; GPS + timestamped check-in/out is the proof that safely releases escrow. `check_in_address_match` is advisory (a mismatch triggers admin review, not auto-cancel). **CHANGED:** the FK moves to **`booking_session_id`** so each visit in a multi-session engagement is verified independently; the mapping between `visit_verifications.status` and the parent `bookings.status` is documented so the two state machines cannot silently diverge. Fields otherwise unchanged. **Relations:** 1:1 → `booking_sessions`.
### `cancellation_policies` [MVP] — **NEW**
**Role:** Config-driven, snapshot-able cancellation/refund tiers by lead time and initiating actor. **Why:** "default 100% refund" is naive — a free cancel 24h ahead, a 50% charge inside 24h, and a nurse-no-show full refund + nurse penalty are different money flows, and the applicable rule must be **frozen onto the booking** at cancel time (the same snapshot discipline used for `platform_fee_rate`).
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `code` | NVARCHAR(50) UNIQUE | e.g. `standard_24h` |
| `applies_to` | NVARCHAR(20) | `customer` / `nurse` / `admin` |
| `hours_before_start_min`, `hours_before_start_max` | INT NULL | Tier bounds |
| `refund_percentage` | DECIMAL(5,2) | 0100 |
| `fee_amount_or_rate` | … | Cancellation fee / nurse penalty |
| `is_active`, timestamps | … | |
**Relations:** referenced (snapshot) by `refunds` and the `cancellation_event` recorded on a session/booking.
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 6 — Payments, Ledger &amp; Refunds — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a class="active" href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-6-payments-ledger-refunds">Domain 6 — Payments, Ledger &amp; Refunds</h1>
<p><a href="index.html">← Database Model</a></p>
<p><strong>Related:</strong> business requirements — <a href="../business/08-payments-and-escrow.html">Payments &amp; escrow</a>. Ledger postings are explained in depth in <a href="../payments/escrow-ledger.html">Escrow ledger</a>.</p>
<p>This is the most-changed domain. The previous model <strong>inferred</strong> money state from scattered status flags; this revision makes a <strong>double-entry ledger</strong> the source of truth and adds the <strong>idempotency</strong> and <strong>clawback</strong> primitives that any real-money platform needs before launch.</p>
<h3 id="payment_gateways-core"><code>payment_gateways</code> [CORE] <a class="anchor" href="#payment_gateways-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Config per connected PSP/BNPL provider; credentials encrypted. <strong>Why <code>type</code>:</strong> multiple gateways run at once (standard IPG, a BNPL provider, a failover) and <code>type</code> (<code>standard</code>/<code>bnpl</code>) selects the flow. <strong>Why this matters for Iran:</strong> provider cut-offs happen (Toman/Jibit were abruptly suspended in Nov 2024), so the gateway is abstracted to be swappable. BNPL provider secrets (client_id/secret, merchant number, base_url, sandbox flag) live encrypted in <code>config_json</code>. Fields unchanged. <strong>Relations:</strong> 1:N → <code>payment_transactions</code>, <code>payment_webhook_events</code>.</p>
<h3 id="payment_transactions-core"><code>payment_transactions</code> [CORE] <a class="anchor" href="#payment_transactions-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Every payment attempt against a booking; the <code>succeeded</code> row triggers confirmation. Stores the full <code>gateway_response_json</code> and the <strong>Shaparak reference code</strong> (definitive proof) for reconciliation and chargebacks. <strong>Why hardened:</strong> a retried PSP webhook could otherwise insert a second <code>succeeded</code> row and double-confirm.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td>baseline fields</td><td></td><td><code>id</code>, <code>booking_id</code>, <code>customer_id</code>, <code>gateway_id</code>, <code>amount</code>, <code>currency</code>, <code>status</code>, <code>gateway_transaction_id</code>, <code>gateway_reference_code</code>, <code>gateway_response_code</code>, <code>gateway_response_json</code>, <code>is_installment</code>, <code>ip_address</code>, <code>user_agent</code>, timestamps</td></tr>
<tr><td><strong>constraints</strong></td><td></td><td><strong>NEW:</strong> <code>UNIQUE(gateway_reference_code) WHERE NOT NULL</code>; filtered <code>UNIQUE(booking_id) WHERE status='succeeded'</code> — at most one capturing transaction per booking.</td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>bookings</code>, <code>payment_gateways</code>; 1:1 → <code>bnpl_transactions</code> (if BNPL); 1:N → <code>refunds</code>, <code>ledger_entries</code>.</p>
<h3 id="payment_webhook_events-core-new"><code>payment_webhook_events</code> [CORE] — <strong>NEW</strong> <a class="anchor" href="#payment_webhook_events-core-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Raw, deduplicated store of every PSP/BNPL callback. <strong>Why mandatory:</strong> callbacks are at-least-once and retried; without a unique store keyed on the provider's event id, a replayed "payment succeeded" double-confirms a booking and a replayed "settled" double-counts money. The handler <strong>upserts here first</strong> and no-ops on a duplicate, inside the same transaction that mutates payment state.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>provider_code</code></td><td>NVARCHAR(50)</td><td><code>zarinpal</code> / <code>snapppay</code> / …</td></tr>
<tr><td><code>external_event_id</code></td><td>NVARCHAR(200)</td><td><strong>UNIQUE(provider_code, external_event_id)</strong></td></tr>
<tr><td><code>event_type</code></td><td>NVARCHAR(80)</td><td></td></tr>
<tr><td><code>signature_valid</code></td><td>BIT</td><td></td></tr>
<tr><td><code>payload_json</code></td><td>NVARCHAR(MAX)</td><td>Raw callback</td></tr>
<tr><td><code>processing_status</code></td><td>NVARCHAR(20)</td><td><code>received</code> / <code>processed</code> / <code>failed</code> / <code>ignored</code></td></tr>
<tr><td><code>related_payment_transaction_id</code></td><td>BIGINT NULL</td><td></td></tr>
<tr><td><code>received_at</code>, <code>processed_at</code></td><td>DATETIME2</td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>payment_gateways</code>; optional → <code>payment_transactions</code>.</p>
<h3 id="refunds-core"><code>refunds</code> [CORE] <a class="anchor" href="#refunds-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Admin-initiated refunds (no customer self-service), always linked to a support ticket. <strong>Why these changes:</strong> the previous model treated refunds as 1:1 with a transaction and as a single undecomposed amount — but partials exist (shortened visit) and a refund must say how much reverses the <strong>platform fee</strong> vs the <strong>nurse payout</strong>, and how the money actually moves (card vs BNPL revert).</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td>baseline</td><td></td><td><code>id</code>, <code>payment_transaction_id</code>, <code>booking_id</code>, <code>requested_by_customer_id</code>, <code>ticket_id</code>, <code>amount</code>, <code>refund_percentage</code>, <code>reason_category</code>, <code>reason_notes</code>, <code>status</code>, approval/rejection fields, <code>gateway_refund_reference</code>, <code>processed_at</code>, <code>admin_notes</code>, timestamps</td></tr>
<tr><td><strong>cardinality</strong></td><td></td><td><strong>CHANGED to 1:N</strong> per <code>payment_transaction</code> (app invariant: Σ refunded ≤ captured). The old "1:1" relationship summary was wrong.</td></tr>
<tr><td><code>platform_fee_refunded_irr</code></td><td>BIGINT</td><td><strong>NEW</strong> — fee-leg decomposition</td></tr>
<tr><td><code>nurse_payout_refunded_irr</code></td><td>BIGINT</td><td><strong>NEW</strong> — payout-leg decomposition (drives clawback if nurse already paid)</td></tr>
<tr><td><code>refund_channel</code></td><td>NVARCHAR(20)</td><td><strong>NEW</strong><code>psp_card</code> / <code>bnpl_revert</code> / <code>manual_bank</code></td></tr>
<tr><td><code>external_revert_reference</code></td><td>NVARCHAR(200) NULL</td><td><strong>NEW</strong> — BNPL revert id</td></tr>
<tr><td><code>expected_customer_refund_eta</code></td><td>DATE NULL</td><td><strong>NEW</strong> — the ~710 business-day BNPL window, surfaced in UI/reconciliation</td></tr>
<tr><td><code>cancellation_policy_code</code> / <code>refund_percentage_applied</code></td><td></td><td><strong>NEW</strong> — snapshot of the policy that produced this refund</td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>payment_transactions</code>, <code>bookings</code>, <code>customer_profiles</code>, <code>tickets</code>; 1:1 → <code>nurse_clawbacks</code> (only when refunding a booking whose nurse was already paid).</p>
<h3 id="ledger_entries-core-new"><code>ledger_entries</code> [CORE] — <strong>NEW</strong> <a class="anchor" href="#ledger_entries-core-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> The append-only, double-entry financial <strong>source of truth</strong>. Every money event posts <strong>balanced</strong> rows sharing a <code>transaction_group_id</code> (Σ debit = Σ credit). Per-nurse payable balance derives by filtering <code>account_type='nurse_payable'</code> + <code>nurse_id</code>. <strong>Why:</strong> a marketplace that holds escrow and pays out weekly minus commission, with refunds and clawbacks, is exactly the shape double-entry was invented for. The alternative (more money columns + status booleans) cannot answer "how much do we owe nurses right now" or reconcile against the bank, and makes refund/clawback second-class. Cost: one table + posting discipline.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>transaction_group_id</code></td><td>UNIQUEIDENTIFIER</td><td>Groups the balanced legs of one event</td></tr>
<tr><td><code>account_type</code></td><td>NVARCHAR(40)</td><td><code>escrow_held</code> / <code>platform_revenue</code> / <code>nurse_payable</code> / <code>refund_payable</code> / <code>bnpl_fee_expense</code> / <code>psp_fee_expense</code> / <code>nurse_clawback_receivable</code> / <code>bad_debt</code></td></tr>
<tr><td><code>nurse_id</code></td><td>BIGINT FK NULL</td><td>Set for <code>nurse_payable</code>/<code>nurse_clawback_receivable</code></td></tr>
<tr><td><code>direction</code></td><td>NVARCHAR(6)</td><td><code>debit</code> / <code>credit</code></td></tr>
<tr><td><code>amount_irr</code></td><td>BIGINT</td><td>Always positive; <code>direction</code> carries the sign</td></tr>
<tr><td><code>booking_id</code></td><td>BIGINT FK NULL</td><td></td></tr>
<tr><td><code>source_ref_type</code></td><td>NVARCHAR(40)</td><td><code>payment_transaction</code> / <code>refund</code> / <code>nurse_payout</code> / <code>bnpl_transaction</code> / <code>clawback</code></td></tr>
<tr><td><code>source_ref_id</code></td><td>BIGINT</td><td></td></tr>
<tr><td><code>memo</code></td><td>NVARCHAR(300) NULL</td><td></td></tr>
<tr><td><code>created_at</code></td><td>DATETIME2</td><td>Append-only; never updated</td></tr>
</tbody></table></div>
<p><strong>Canonical postings:</strong></p>
<div class="table-wrap"><table><thead><tr><th>Event</th><th>Debit</th><th>Credit</th></tr></thead><tbody>
<tr><td>Card capture</td><td><code>escrow_held</code> (gross)</td><td><code>platform_revenue</code> (commission) + <code>nurse_payable</code> (payout)</td></tr>
<tr><td>BNPL settle</td><td>as card, <strong>plus</strong> <code>bnpl_fee_expense</code> (provider commission)</td><td><code>escrow_held</code> (provider commission) → escrow reflects net cash actually received</td></tr>
<tr><td>Refund (pre-payout)</td><td><code>nurse_payable</code> + <code>platform_revenue</code> (decomposed legs)</td><td><code>refund_payable</code>; later <code>refund_payable</code><code>escrow_held</code> on confirm</td></tr>
<tr><td>Clawback (post-payout)</td><td><code>nurse_clawback_receivable</code> (+ <code>platform_revenue</code> leg)</td><td><code>refund_payable</code></td></tr>
<tr><td>Clawback recovered</td><td><code>nurse_payable</code> (next batch)</td><td><code>nurse_clawback_receivable</code></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>bookings</code>; logical links to <code>payment_transactions</code>/<code>refunds</code>/<code>nurse_payouts</code>/<code>bnpl_transactions</code> via <code>source_ref_*</code>.</p>
<h3 id="nurse_clawbacks-core-new"><code>nurse_clawbacks</code> [CORE] — <strong>NEW</strong> <a class="anchor" href="#nurse_clawbacks-core-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> A first-class receivable when a booking is refunded/disputed <strong>after</strong> the nurse was already paid. <strong>Why:</strong> this was the model's own flagged critical gap — Iranian payouts are real, hard-to-reverse bank transfers, so paying before the dispute window closes can create uncollectable loss with nowhere to record it. Closing it = gate payout on <code>dispute_window_ends_at</code> <strong>and</strong> record any post-payout recovery here.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>nurse_id</code></td><td>BIGINT FK → nurse_profiles</td><td></td></tr>
<tr><td><code>booking_id</code></td><td>BIGINT FK → bookings</td><td></td></tr>
<tr><td><code>refund_id</code></td><td>BIGINT FK → refunds</td><td></td></tr>
<tr><td><code>original_payout_id</code></td><td>BIGINT FK → nurse_payouts NULL</td><td></td></tr>
<tr><td><code>amount_irr</code></td><td>BIGINT</td><td></td></tr>
<tr><td><code>status</code></td><td>NVARCHAR(30)</td><td><code>pending</code> / <code>recovered</code> / <code>written_off</code></td></tr>
<tr><td><code>recovered_in_payout_id</code></td><td>BIGINT FK NULL</td><td>Batch that netted it</td></tr>
<tr><td><code>created_at</code>, <code>resolved_at</code></td><td>DATETIME2</td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>nurse_profiles</code>, <code>bookings</code>; 1:1 → <code>refunds</code>; → <code>nurse_payouts</code> (original and recovering).</p>
<h3 id="invoices-mvp-new"><code>invoices</code> [MVP] — <strong>NEW</strong> <a class="anchor" href="#invoices-mvp-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Minimal official-invoice/receipt record per booking. <strong>Why:</strong> Iranian commission marketplaces face VAT/مودیان obligations on <strong>their commission</strong> (the Snapp/Tapsi precedent: the provider's earnings are the provider's own income; the platform's commission is the platform's taxable revenue). The smallest footprint that satisfies bookkeeping without a full tax engine. <strong>VAT is 10%</strong> (rose from 9% in 1403) and stored as a <strong>config-driven rate</strong> so a future exemption ruling on the nursing service itself is just a value change. Nurse-side income tax is the nurse's own responsibility.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>booking_id</code></td><td>BIGINT FK → bookings</td><td></td></tr>
<tr><td><code>invoice_number</code></td><td>NVARCHAR(40) UNIQUE</td><td>Official sequential number</td></tr>
<tr><td><code>issuing_entity_type</code></td><td>NVARCHAR(20)</td><td><code>platform</code> / <code>partner_center</code></td></tr>
<tr><td><code>gross_irr</code></td><td>BIGINT</td><td></td></tr>
<tr><td><code>platform_commission_irr</code></td><td>BIGINT</td><td>The VAT-relevant line</td></tr>
<tr><td><code>bnpl_commission_irr</code></td><td>BIGINT NULL</td><td></td></tr>
<tr><td><code>vat_rate</code></td><td>DECIMAL(5,4)</td><td>Config-driven (0.10)</td></tr>
<tr><td><code>vat_irr</code></td><td>BIGINT</td><td></td></tr>
<tr><td><code>moadian_reference_number</code></td><td>NVARCHAR(40) NULL</td><td>سامانه مودیان 22-digit ref when issued</td></tr>
<tr><td><code>moadian_status</code></td><td>NVARCHAR(20) NULL</td><td></td></tr>
<tr><td><code>pdf_storage_key</code></td><td>NVARCHAR(512) NULL</td><td></td></tr>
<tr><td><code>issued_at</code></td><td>DATETIME2</td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> 1:1 → <code>bookings</code>; N:1 → <code>partner_centers</code> (when issuer).</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,120 @@
# Domain 6 — Payments, Ledger & Refunds
[← Database Model](index.md)
**Related:** business requirements — [Payments & escrow](../business/08-payments-and-escrow.md). Ledger postings are explained in depth in [Escrow ledger](../payments/escrow-ledger.md).
This is the most-changed domain. The previous model **inferred** money state from scattered status flags; this revision makes a **double-entry ledger** the source of truth and adds the **idempotency** and **clawback** primitives that any real-money platform needs before launch.
### `payment_gateways` [CORE]
**Role:** Config per connected PSP/BNPL provider; credentials encrypted. **Why `type`:** multiple gateways run at once (standard IPG, a BNPL provider, a failover) and `type` (`standard`/`bnpl`) selects the flow. **Why this matters for Iran:** provider cut-offs happen (Toman/Jibit were abruptly suspended in Nov 2024), so the gateway is abstracted to be swappable. BNPL provider secrets (client_id/secret, merchant number, base_url, sandbox flag) live encrypted in `config_json`. Fields unchanged. **Relations:** 1:N → `payment_transactions`, `payment_webhook_events`.
### `payment_transactions` [CORE]
**Role:** Every payment attempt against a booking; the `succeeded` row triggers confirmation. Stores the full `gateway_response_json` and the **Shaparak reference code** (definitive proof) for reconciliation and chargebacks. **Why hardened:** a retried PSP webhook could otherwise insert a second `succeeded` row and double-confirm.
| Field | Type | Notes |
|---|---|---|
| baseline fields | … | `id`, `booking_id`, `customer_id`, `gateway_id`, `amount`, `currency`, `status`, `gateway_transaction_id`, `gateway_reference_code`, `gateway_response_code`, `gateway_response_json`, `is_installment`, `ip_address`, `user_agent`, timestamps |
| **constraints** | | **NEW:** `UNIQUE(gateway_reference_code) WHERE NOT NULL`; filtered `UNIQUE(booking_id) WHERE status='succeeded'` — at most one capturing transaction per booking. |
**Relations:** N:1 → `bookings`, `payment_gateways`; 1:1 → `bnpl_transactions` (if BNPL); 1:N → `refunds`, `ledger_entries`.
### `payment_webhook_events` [CORE] — **NEW**
**Role:** Raw, deduplicated store of every PSP/BNPL callback. **Why mandatory:** callbacks are at-least-once and retried; without a unique store keyed on the provider's event id, a replayed "payment succeeded" double-confirms a booking and a replayed "settled" double-counts money. The handler **upserts here first** and no-ops on a duplicate, inside the same transaction that mutates payment state.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `provider_code` | NVARCHAR(50) | `zarinpal` / `snapppay` / … |
| `external_event_id` | NVARCHAR(200) | **UNIQUE(provider_code, external_event_id)** |
| `event_type` | NVARCHAR(80) | |
| `signature_valid` | BIT | |
| `payload_json` | NVARCHAR(MAX) | Raw callback |
| `processing_status` | NVARCHAR(20) | `received` / `processed` / `failed` / `ignored` |
| `related_payment_transaction_id` | BIGINT NULL | |
| `received_at`, `processed_at` | DATETIME2 | |
**Relations:** N:1 → `payment_gateways`; optional → `payment_transactions`.
### `refunds` [CORE]
**Role:** Admin-initiated refunds (no customer self-service), always linked to a support ticket. **Why these changes:** the previous model treated refunds as 1:1 with a transaction and as a single undecomposed amount — but partials exist (shortened visit) and a refund must say how much reverses the **platform fee** vs the **nurse payout**, and how the money actually moves (card vs BNPL revert).
| Field | Type | Notes |
|---|---|---|
| baseline | … | `id`, `payment_transaction_id`, `booking_id`, `requested_by_customer_id`, `ticket_id`, `amount`, `refund_percentage`, `reason_category`, `reason_notes`, `status`, approval/rejection fields, `gateway_refund_reference`, `processed_at`, `admin_notes`, timestamps |
| **cardinality** | | **CHANGED to 1:N** per `payment_transaction` (app invariant: Σ refunded ≤ captured). The old "1:1" relationship summary was wrong. |
| `platform_fee_refunded_irr` | BIGINT | **NEW** — fee-leg decomposition |
| `nurse_payout_refunded_irr` | BIGINT | **NEW** — payout-leg decomposition (drives clawback if nurse already paid) |
| `refund_channel` | NVARCHAR(20) | **NEW**`psp_card` / `bnpl_revert` / `manual_bank` |
| `external_revert_reference` | NVARCHAR(200) NULL | **NEW** — BNPL revert id |
| `expected_customer_refund_eta` | DATE NULL | **NEW** — the ~710 business-day BNPL window, surfaced in UI/reconciliation |
| `cancellation_policy_code` / `refund_percentage_applied` | … | **NEW** — snapshot of the policy that produced this refund |
**Relations:** N:1 → `payment_transactions`, `bookings`, `customer_profiles`, `tickets`; 1:1 → `nurse_clawbacks` (only when refunding a booking whose nurse was already paid).
### `ledger_entries` [CORE] — **NEW**
**Role:** The append-only, double-entry financial **source of truth**. Every money event posts **balanced** rows sharing a `transaction_group_id` (Σ debit = Σ credit). Per-nurse payable balance derives by filtering `account_type='nurse_payable'` + `nurse_id`. **Why:** a marketplace that holds escrow and pays out weekly minus commission, with refunds and clawbacks, is exactly the shape double-entry was invented for. The alternative (more money columns + status booleans) cannot answer "how much do we owe nurses right now" or reconcile against the bank, and makes refund/clawback second-class. Cost: one table + posting discipline.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `transaction_group_id` | UNIQUEIDENTIFIER | Groups the balanced legs of one event |
| `account_type` | NVARCHAR(40) | `escrow_held` / `platform_revenue` / `nurse_payable` / `refund_payable` / `bnpl_fee_expense` / `psp_fee_expense` / `nurse_clawback_receivable` / `bad_debt` |
| `nurse_id` | BIGINT FK NULL | Set for `nurse_payable`/`nurse_clawback_receivable` |
| `direction` | NVARCHAR(6) | `debit` / `credit` |
| `amount_irr` | BIGINT | Always positive; `direction` carries the sign |
| `booking_id` | BIGINT FK NULL | |
| `source_ref_type` | NVARCHAR(40) | `payment_transaction` / `refund` / `nurse_payout` / `bnpl_transaction` / `clawback` |
| `source_ref_id` | BIGINT | |
| `memo` | NVARCHAR(300) NULL | |
| `created_at` | DATETIME2 | Append-only; never updated |
**Canonical postings:**
| Event | Debit | Credit |
|---|---|---|
| Card capture | `escrow_held` (gross) | `platform_revenue` (commission) + `nurse_payable` (payout) |
| BNPL settle | as card, **plus** `bnpl_fee_expense` (provider commission) | `escrow_held` (provider commission) → escrow reflects net cash actually received |
| Refund (pre-payout) | `nurse_payable` + `platform_revenue` (decomposed legs) | `refund_payable`; later `refund_payable``escrow_held` on confirm |
| Clawback (post-payout) | `nurse_clawback_receivable` (+ `platform_revenue` leg) | `refund_payable` |
| Clawback recovered | `nurse_payable` (next batch) | `nurse_clawback_receivable` |
**Relations:** N:1 → `bookings`; logical links to `payment_transactions`/`refunds`/`nurse_payouts`/`bnpl_transactions` via `source_ref_*`.
### `nurse_clawbacks` [CORE] — **NEW**
**Role:** A first-class receivable when a booking is refunded/disputed **after** the nurse was already paid. **Why:** this was the model's own flagged critical gap — Iranian payouts are real, hard-to-reverse bank transfers, so paying before the dispute window closes can create uncollectable loss with nowhere to record it. Closing it = gate payout on `dispute_window_ends_at` **and** record any post-payout recovery here.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `nurse_id` | BIGINT FK → nurse_profiles | |
| `booking_id` | BIGINT FK → bookings | |
| `refund_id` | BIGINT FK → refunds | |
| `original_payout_id` | BIGINT FK → nurse_payouts NULL | |
| `amount_irr` | BIGINT | |
| `status` | NVARCHAR(30) | `pending` / `recovered` / `written_off` |
| `recovered_in_payout_id` | BIGINT FK NULL | Batch that netted it |
| `created_at`, `resolved_at` | DATETIME2 | |
**Relations:** N:1 → `nurse_profiles`, `bookings`; 1:1 → `refunds`; → `nurse_payouts` (original and recovering).
### `invoices` [MVP] — **NEW**
**Role:** Minimal official-invoice/receipt record per booking. **Why:** Iranian commission marketplaces face VAT/مودیان obligations on **their commission** (the Snapp/Tapsi precedent: the provider's earnings are the provider's own income; the platform's commission is the platform's taxable revenue). The smallest footprint that satisfies bookkeeping without a full tax engine. **VAT is 10%** (rose from 9% in 1403) and stored as a **config-driven rate** so a future exemption ruling on the nursing service itself is just a value change. Nurse-side income tax is the nurse's own responsibility.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `booking_id` | BIGINT FK → bookings | |
| `invoice_number` | NVARCHAR(40) UNIQUE | Official sequential number |
| `issuing_entity_type` | NVARCHAR(20) | `platform` / `partner_center` |
| `gross_irr` | BIGINT | |
| `platform_commission_irr` | BIGINT | The VAT-relevant line |
| `bnpl_commission_irr` | BIGINT NULL | |
| `vat_rate` | DECIMAL(5,4) | Config-driven (0.10) |
| `vat_irr` | BIGINT | |
| `moadian_reference_number` | NVARCHAR(40) NULL | سامانه مودیان 22-digit ref when issued |
| `moadian_status` | NVARCHAR(20) NULL | |
| `pdf_storage_key` | NVARCHAR(512) NULL | |
| `issued_at` | DATETIME2 | |
**Relations:** 1:1 → `bookings`; N:1 → `partner_centers` (when issuer).
+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 7 — Payouts to Nurses — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a class="active" href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-7-payouts-to-nurses">Domain 7 — Payouts to Nurses</h1>
<p><a href="index.html">← Database Model</a></p>
<p><strong>Related:</strong> business requirements — <a href="../business/10-payouts.html">Payouts</a>.</p>
<h3 id="nurse_payout_batches-core"><code>nurse_payout_batches</code> [CORE] <a class="anchor" href="#nurse_payout_batches-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Weekly aggregation of amounts owed for completed, payout-eligible, unpaid bookings/sessions. <strong>Why batched:</strong> matches the operational rhythm and the PAYA settlement cycle; an admin (or scheduled job) initiates it. <strong>Holiday-aware:</strong> <code>period_end</code>/processing dates shift off bank-closed days using <code>iranian_holidays</code> (a weekly payout landing on a multi-day Nowruz closure would otherwise fail). Fields unchanged: period, <code>total_amount</code>, <code>payout_count</code>, <code>status</code>, <code>initiated_by_admin_id</code>, <code>processed_at</code>, <code>failure_notes</code>, timestamps. CHECK: <code>total_amount = Σ payouts</code>. <strong>Relations:</strong> 1:N → <code>nurse_payouts</code>.</p>
<h3 id="nurse_payouts-core"><code>nurse_payouts</code> [CORE] <a class="anchor" href="#nurse_payouts-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> One row per nurse per batch — the exact amount transferred, the IBAN snapshot, and the bank transfer reference. <strong>Why these additions:</strong> a batch must be able to <strong>net prior clawbacks</strong> so it doesn't overpay a nurse who owes money back.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td>baseline</td><td></td><td><code>id</code>, <code>batch_id</code>, <code>nurse_id</code>, <code>bank_account_id</code>, <code>iban_snapshot</code> (enc), <code>amount</code>, <code>booking_count</code>, <code>status</code>, <code>transfer_reference</code>, <code>paid_at</code>, <code>failure_reason</code>, <code>created_at</code></td></tr>
<tr><td><code>gross_earnings_irr</code></td><td>BIGINT</td><td><strong>NEW</strong> — sum of eligible session/booking payouts</td></tr>
<tr><td><code>clawback_applied_irr</code></td><td>BIGINT</td><td><strong>NEW</strong> — clawbacks netted this batch</td></tr>
<tr><td><code>net_amount_irr</code></td><td>BIGINT</td><td><strong>NEW</strong><code>gross_earnings clawback</code>; <code>amount</code> = actually transferred net</td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> N:1 → <code>nurse_payout_batches</code>, <code>nurse_profiles</code>, <code>nurse_bank_accounts</code>; 1:N → <code>nurse_payout_booking_links</code>; referenced by <code>nurse_clawbacks</code>.</p>
<h3 id="nurse_payout_booking_links-core"><code>nurse_payout_booking_links</code> [CORE] <a class="anchor" href="#nurse_payout_booking_links-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Join from a payout to the specific bookings it covers, with <code>booking_id</code> <strong>UNIQUE</strong> to guarantee a booking is paid in exactly one batch. <strong>Why:</strong> per-booking reconciliation and the structural anti-double-pay guard (the previous model's strongest correctness feature — kept). Fields unchanged. <strong>Relations:</strong> N:1 → <code>nurse_payouts</code>; 1:1 → <code>bookings</code>.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
# Domain 7 — Payouts to Nurses
[← Database Model](index.md)
**Related:** business requirements — [Payouts](../business/10-payouts.md).
### `nurse_payout_batches` [CORE]
**Role:** Weekly aggregation of amounts owed for completed, payout-eligible, unpaid bookings/sessions. **Why batched:** matches the operational rhythm and the PAYA settlement cycle; an admin (or scheduled job) initiates it. **Holiday-aware:** `period_end`/processing dates shift off bank-closed days using `iranian_holidays` (a weekly payout landing on a multi-day Nowruz closure would otherwise fail). Fields unchanged: period, `total_amount`, `payout_count`, `status`, `initiated_by_admin_id`, `processed_at`, `failure_notes`, timestamps. CHECK: `total_amount = Σ payouts`. **Relations:** 1:N → `nurse_payouts`.
### `nurse_payouts` [CORE]
**Role:** One row per nurse per batch — the exact amount transferred, the IBAN snapshot, and the bank transfer reference. **Why these additions:** a batch must be able to **net prior clawbacks** so it doesn't overpay a nurse who owes money back.
| Field | Type | Notes |
|---|---|---|
| baseline | … | `id`, `batch_id`, `nurse_id`, `bank_account_id`, `iban_snapshot` (enc), `amount`, `booking_count`, `status`, `transfer_reference`, `paid_at`, `failure_reason`, `created_at` |
| `gross_earnings_irr` | BIGINT | **NEW** — sum of eligible session/booking payouts |
| `clawback_applied_irr` | BIGINT | **NEW** — clawbacks netted this batch |
| `net_amount_irr` | BIGINT | **NEW**`gross_earnings clawback`; `amount` = actually transferred net |
**Relations:** 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]
**Role:** Join from a payout to the specific bookings it covers, with `booking_id` **UNIQUE** to guarantee a booking is paid in exactly one batch. **Why:** per-booking reconciliation and the structural anti-double-pay guard (the previous model's strongest correctness feature — kept). Fields unchanged. **Relations:** N:1 → `nurse_payouts`; 1:1 → `bookings`.
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 8 — BNPL / Installments — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a class="active" href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-8-bnpl-installments">Domain 8 — BNPL / Installments</h1>
<p><a href="index.html">← Database Model</a></p>
<p><strong>Related:</strong> business requirements — <a href="../business/09-installments-bnpl.html">Installments / BNPL</a>. Settlement and reversal detail are explained in depth in the payments docs — <a href="../payments/bnpl-landscape.html">BNPL landscape</a> and <a href="../payments/cancellation-and-payout.html">Cancellation &amp; payout</a>.</p>
<p><strong>Resolved.</strong> Because verified research shows Iranian provider-financed BNPL settles the <strong>full amount to the merchant in one lump</strong> (provider owns the customer's installments and default risk), a BNPL order is — in our books — a card payment that lands net-of-fee. The original <code>installment_plans</code> + <code>installment_entries</code> subsystem (which tried to track the customer's repayment schedule and default) is <strong>deleted</strong>: it modeled a receivable Balinyaar never owns and a risk it never bears.</p>
<h3 id="bnpl_transactions-mvp-new-replaces-installment_plans"><code>bnpl_transactions</code> [MVP] — <strong>NEW (replaces <code>installment_plans</code>)</strong> <a class="anchor" href="#bnpl_transactions-mvp-new-replaces-installment_plans" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> One row per BNPL order, 1:1 with its <code>payment_transaction</code> — the single inbound settlement to reconcile, plus the revert path. <strong>Why one row, not a plan+entries tree:</strong> there is nothing to amortize on our side; we track the settlement, the provider's commission, and the reversal.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>payment_transaction_id</code></td><td>BIGINT FK UNIQUE</td><td>1:1</td></tr>
<tr><td><code>provider_code</code></td><td>NVARCHAR(50)</td><td><code>snapppay</code> / <code>digipay</code> / <code>tara</code> / <code>torobpay</code></td></tr>
<tr><td><code>merchant_of_record</code></td><td>NVARCHAR(40)</td><td>Balinyaar entity or partner center</td></tr>
<tr><td><code>external_payment_token</code></td><td>NVARCHAR(200)</td><td>For verify/settle/revert</td></tr>
<tr><td><code>external_transaction_id</code></td><td>NVARCHAR(200)</td><td></td></tr>
<tr><td><code>eligibility_status</code></td><td>NVARCHAR(30)</td><td></td></tr>
<tr><td><code>order_amount_irr</code></td><td>BIGINT</td><td>Gross order</td></tr>
<tr><td><code>settled_amount_irr</code></td><td>BIGINT</td><td>Net of provider commission actually received</td></tr>
<tr><td><code>bnpl_commission_irr</code></td><td>BIGINT</td><td>Provider's merchant discount = platform <strong>expense</strong> (never the nurse's)</td></tr>
<tr><td><code>currency</code></td><td>NVARCHAR(5)</td><td><code>IRR</code>/<code>TOMAN</code> at boundary; convert in</td></tr>
<tr><td><code>installment_count</code></td><td>TINYINT</td><td>Informational (default 4) — owned by the provider</td></tr>
<tr><td><code>status</code></td><td>NVARCHAR(30)</td><td>State machine: <code>eligible</code>/<code>token_issued</code>/<code>verified</code>/<code>settled</code>/<code>reverted</code>/<code>cancelled</code>/<code>failed</code></td></tr>
<tr><td><code>settled_at</code></td><td>DATETIME2 NULL</td><td><strong>Per-transaction</strong> — timing is contract-defined (daily/T+1-3/weekly), never assumed instant</td></tr>
<tr><td><code>revert_transaction_id</code>, <code>reverted_amount_irr</code>, <code>reverted_at</code></td><td></td><td>Reversal path</td></tr>
<tr><td><code>refund_channel</code></td><td>NVARCHAR(20)</td><td></td></tr>
<tr><td><code>callback_payload_json</code></td><td>NVARCHAR(MAX)</td><td>Raw verify/settle payload</td></tr>
<tr><td>timestamps</td><td></td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> 1:1 → <code>payment_transactions</code>. <strong>State-machine guard</strong> on <code>status</code> for idempotency.</p>
<h3 id="bnpl_settlement_entries-deferred"><code>bnpl_settlement_entries</code> [DEFERRED] <a class="anchor" href="#bnpl_settlement_entries-deferred" aria-hidden="true">#</a></h3>
<p><strong>Role/Why:</strong> Only needed if a future provider uses <strong>tranched</strong> settlement (pays the platform over time). No mainstream Iranian provider does today, so it's modeled-but-inactive; adding it later is a purely additive migration.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
# Domain 8 — BNPL / Installments
[← Database Model](index.md)
**Related:** business requirements — [Installments / BNPL](../business/09-installments-bnpl.md). Settlement and reversal detail are explained in depth in the payments docs — [BNPL landscape](../payments/bnpl-landscape.md) and [Cancellation & payout](../payments/cancellation-and-payout.md).
**Resolved.** Because verified research shows Iranian provider-financed BNPL settles the **full amount to the merchant in one lump** (provider owns the customer's installments and default risk), a BNPL order is — in our books — a card payment that lands net-of-fee. The original `installment_plans` + `installment_entries` subsystem (which tried to track the customer's repayment schedule and default) is **deleted**: it modeled a receivable Balinyaar never owns and a risk it never bears.
### `bnpl_transactions` [MVP] — **NEW (replaces `installment_plans`)**
**Role:** One row per BNPL order, 1:1 with its `payment_transaction` — the single inbound settlement to reconcile, plus the revert path. **Why one row, not a plan+entries tree:** there is nothing to amortize on our side; we track the settlement, the provider's commission, and the reversal.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `payment_transaction_id` | BIGINT FK UNIQUE | 1:1 |
| `provider_code` | NVARCHAR(50) | `snapppay` / `digipay` / `tara` / `torobpay` |
| `merchant_of_record` | NVARCHAR(40) | Balinyaar entity or partner center |
| `external_payment_token` | NVARCHAR(200) | For verify/settle/revert |
| `external_transaction_id` | NVARCHAR(200) | |
| `eligibility_status` | NVARCHAR(30) | |
| `order_amount_irr` | BIGINT | Gross order |
| `settled_amount_irr` | BIGINT | Net of provider commission actually received |
| `bnpl_commission_irr` | BIGINT | Provider's merchant discount = platform **expense** (never the nurse's) |
| `currency` | NVARCHAR(5) | `IRR`/`TOMAN` at boundary; convert in |
| `installment_count` | TINYINT | Informational (default 4) — owned by the provider |
| `status` | NVARCHAR(30) | State machine: `eligible`/`token_issued`/`verified`/`settled`/`reverted`/`cancelled`/`failed` |
| `settled_at` | DATETIME2 NULL | **Per-transaction** — timing is contract-defined (daily/T+1-3/weekly), never assumed instant |
| `revert_transaction_id`, `reverted_amount_irr`, `reverted_at` | … | Reversal path |
| `refund_channel` | NVARCHAR(20) | |
| `callback_payload_json` | NVARCHAR(MAX) | Raw verify/settle payload |
| timestamps | … | |
**Relations:** 1:1 → `payment_transactions`. **State-machine guard** on `status` for idempotency.
### `bnpl_settlement_entries` [DEFERRED]
**Role/Why:** Only needed if a future provider uses **tranched** settlement (pays the platform over time). No mainstream Iranian provider does today, so it's modeled-but-inactive; adding it later is a purely additive migration.
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 9 — Messaging (Ticket System) — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a class="active" href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-9-messaging-ticket-system">Domain 9 — Messaging (Ticket System)</h1>
<p><a href="index.html">← Database Model</a></p>
<h3 id="tickets-ticket_participants-ticket_messages-core"><code>tickets</code> / <code>ticket_participants</code> / <code>ticket_messages</code> [CORE] <a class="anchor" href="#tickets-ticket_participants-ticket_messages-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> All post-booking communication, admin-readable, with no direct nurse↔customer channel. <strong>Why intentionally constrained:</strong> it protects vulnerable patients, creates dispute evidence, and prevents disintermediation (families and nurses pairing off-platform). <code>ticket_messages.is_internal</code> keeps admin-only notes out of user view. <code>reference_code</code> is the human-facing support id. <strong>On-site emergencies</strong> stay an operational playbook ("call the emergency contact from the app, then open a ticket") — surfaced prominently in the booking UI; no schema change, but documented so nurses don't seek the family's number off-platform. Fields unchanged; <code>UNIQUE(ticket_id, user_id)</code> on participants. <strong>Relations:</strong> <code>tickets</code> 1:N <code>ticket_participants</code>/<code>ticket_messages</code>; <code>tickets</code> optionally ↔ <code>bookings</code>, <code>refunds</code>.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
# Domain 9 — Messaging (Ticket System)
[← Database Model](index.md)
### `tickets` / `ticket_participants` / `ticket_messages` [CORE]
**Role:** All post-booking communication, admin-readable, with no direct nurse↔customer channel. **Why intentionally constrained:** it protects vulnerable patients, creates dispute evidence, and prevents disintermediation (families and nurses pairing off-platform). `ticket_messages.is_internal` keeps admin-only notes out of user view. `reference_code` is the human-facing support id. **On-site emergencies** stay an operational playbook ("call the emergency contact from the app, then open a ticket") — surfaced prominently in the booking UI; no schema change, but documented so nurses don't seek the family's number off-platform. Fields unchanged; `UNIQUE(ticket_id, user_id)` on participants. **Relations:** `tickets` 1:N `ticket_participants`/`ticket_messages`; `tickets` optionally ↔ `bookings`, `refunds`.
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 10 — Reviews &amp; Patient Records — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a class="active" href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-10-reviews-patient-records">Domain 10 — Reviews &amp; Patient Records</h1>
<p><a href="index.html">← Database Model</a></p>
<h3 id="reviews-core"><code>reviews</code> [CORE] <a class="anchor" href="#reviews-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> One customer review per <strong>completed</strong> booking; enters <code>pending_moderation</code>, published only after admin/AI approval; a low rating raises a <code>support_alert</code>. <strong>Why these guards:</strong> (a) review creation is allowed <strong>only for completed/closed bookings</strong> (a review for a cancelled booking is nonsense); (b) <strong>every</strong> status transition (publish/hide/reject/unpublish) recomputes <code>nurse_profiles</code> aggregates, fixing the inflated-rating-after-hide drift. Fields unchanged (rating 15 with CHECK, body, status, moderation fields). <strong>Relations:</strong> 1:1 → <code>bookings</code>; N:1 → <code>customer_profiles</code>, <code>nurse_profiles</code>; 1:N → <code>review_tag_links</code>.</p>
<h3 id="review_tags_master-mvp-review_tag_links-mvp"><code>review_tags_master</code> [MVP] / <code>review_tag_links</code> [MVP] <a class="anchor" href="#review_tags_master-mvp-review_tag_links-mvp" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Standardized tags for quantitative aggregation of qualitative feedback (e.g. "% punctual"). <strong>Why MVP-not-core:</strong> free-text + rating is enough to launch; structured tags are a phase-2 analytics nicety (additive leaf tables). <strong>Relations:</strong> <code>reviews</code> N:N <code>review_tags_master</code>.</p>
<h3 id="patient_care_records-mvp"><code>patient_care_records</code> [MVP] <a class="anchor" href="#patient_care_records-mvp" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Nurse-authored clinical notes after a visit, accumulating into a <strong>patient-scoped longitudinal history</strong> (not booking-scoped). <strong>Why patient-scoped:</strong> when a different nurse takes over, they read the history before accepting, enabling continuity of care without the family repeating everything. Strict access: owning customer, nurses with a confirmed booking for that patient, admin. All fields encrypted. <strong>Relations:</strong> N:1 → <code>patients</code>, <code>bookings</code>, <code>nurse_profiles</code>.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,12 @@
# Domain 10 — Reviews & Patient Records
[← Database Model](index.md)
### `reviews` [CORE]
**Role:** One customer review per **completed** booking; enters `pending_moderation`, published only after admin/AI approval; a low rating raises a `support_alert`. **Why these guards:** (a) review creation is allowed **only for completed/closed bookings** (a review for a cancelled booking is nonsense); (b) **every** status transition (publish/hide/reject/unpublish) recomputes `nurse_profiles` aggregates, fixing the inflated-rating-after-hide drift. Fields unchanged (rating 15 with CHECK, body, status, moderation fields). **Relations:** 1:1 → `bookings`; N:1 → `customer_profiles`, `nurse_profiles`; 1:N → `review_tag_links`.
### `review_tags_master` [MVP] / `review_tag_links` [MVP]
**Role:** Standardized tags for quantitative aggregation of qualitative feedback (e.g. "% punctual"). **Why MVP-not-core:** free-text + rating is enough to launch; structured tags are a phase-2 analytics nicety (additive leaf tables). **Relations:** `reviews` N:N `review_tags_master`.
### `patient_care_records` [MVP]
**Role:** Nurse-authored clinical notes after a visit, accumulating into a **patient-scoped longitudinal history** (not booking-scoped). **Why patient-scoped:** when a different nurse takes over, they read the history before accepting, enabling continuity of care without the family repeating everything. Strict access: owning customer, nurses with a confirmed booking for that patient, admin. All fields encrypted. **Relations:** N:1 → `patients`, `bookings`, `nurse_profiles`.
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 11 — Notifications — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a class="active" href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-11-notifications">Domain 11 — Notifications</h1>
<p><a href="index.html">← Database Model</a></p>
<h3 id="notifications-core"><code>notifications</code> [CORE] <a class="anchor" href="#notifications-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> In-app notifications (no push at launch); <code>data_json</code> carries a typed payload for front-end navigation. <strong>Why polled, not pushed:</strong> push is out of MVP scope; a retention job hard-deletes read notifications older than 90 days to bound growth. Fields unchanged. <strong>Relations:</strong> N:1 → <code>users</code>.</p>
<h3 id="support_alerts-core"><code>support_alerts</code> [CORE] <a class="anchor" href="#support_alerts-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Internal-only alerts (low ratings, EVV no-shows, expired verification steps, EVV location mismatch, payment anomalies), with an owner and resolution trail. <strong>Why distinct from notifications:</strong> these are staff worklist items, never shown to users. Fields unchanged. <strong>Relations:</strong> polymorphic (<code>entity_type</code>,<code>entity_id</code>) — validated at the application layer; consider nullable typed FKs (<code>booking_id</code>,<code>review_id</code>) for the common cases.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
# Domain 11 — Notifications
[← Database Model](index.md)
### `notifications` [CORE]
**Role:** In-app notifications (no push at launch); `data_json` carries a typed payload for front-end navigation. **Why polled, not pushed:** push is out of MVP scope; a retention job hard-deletes read notifications older than 90 days to bound growth. Fields unchanged. **Relations:** N:1 → `users`.
### `support_alerts` [CORE]
**Role:** Internal-only alerts (low ratings, EVV no-shows, expired verification steps, EVV location mismatch, payment anomalies), with an owner and resolution trail. **Why distinct from notifications:** these are staff worklist items, never shown to users. Fields unchanged. **Relations:** polymorphic (`entity_type`,`entity_id`) — validated at the application layer; consider nullable typed FKs (`booking_id`,`review_id`) for the common cases.
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 12 — Audit, Config &amp; Reference — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a class="active" href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-12-audit-config-reference">Domain 12 — Audit, Config &amp; Reference</h1>
<p><a href="index.html">← Database Model</a></p>
<h3 id="audit_logs-core"><code>audit_logs</code> [CORE] <a class="anchor" href="#audit_logs-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Immutable, append-only record of every state change on sensitive entities — now explicitly <strong>including <code>platform_configs</code></strong> (so finance can prove the commission rate at any moment). <strong>Why:</strong> compliance and accountability; <code>changed_fields_json</code> enables fast filtering. Plan month-partitioning + 23yr cold-storage archival before launch. Fields unchanged. <strong>Relations:</strong> polymorphic, append-only.</p>
<h3 id="system_events-mvp"><code>system_events</code> [MVP] <a class="anchor" href="#system_events-mvp" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> High-volume behavioral/analytics event log. <strong>Why kept but de-emphasized:</strong> product analytics, not compliance. It grows unbounded — at scale, pipe it to an analytics sink/warehouse rather than the transactional DB. Fields unchanged.</p>
<h3 id="platform_configs-core"><code>platform_configs</code> [CORE] <a class="anchor" href="#platform_configs-core" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Key-value runtime business parameters — change without a deploy. <strong>Why typed values:</strong> <code>data_type</code> tells the app how to parse. <strong>New keys</strong> this revision: <code>dispute_window_hours</code> (default 72), <code>vat_rate</code> (0.10), <code>bnpl_merchant_of_record</code>, <code>bnpl_provider_commission_rate</code>, <code>bnpl_settlement_timing</code>, cancellation-tier defaults — alongside the existing <code>platform_fee_rate</code>, <code>booking_payment_deadline_minutes</code>, <code>nurse_response_deadline_hours</code>, <code>nurse_payout_interval_days</code>, <code>evv_location_tolerance_meters</code>, <code>min_rating_for_support_alert</code>. <strong>Relations:</strong> referenced everywhere; changes audited.</p>
<h3 id="iranian_holidays-mvp-new"><code>iranian_holidays</code> [MVP] — <strong>NEW</strong> <a class="anchor" href="#iranian_holidays-mvp-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> Shared official/religious holiday calendar (movable, partly lunar-Hijri), with a <code>is_bank_closed</code> flag. <strong>Why a real table:</strong> Iran's holidays are numerous and partly movable, and they drive <strong>payout bank-closure scheduling</strong> (PAYA/SATNA closed → a weekly payout shifts to the next business day), optional holiday pricing, and business-hour deadline math — none of which a purely manual per-nurse availability exception can express.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>holiday_date</code></td><td>DATE</td><td></td></tr>
<tr><td><code>name_fa</code></td><td>NVARCHAR(200)</td><td></td></tr>
<tr><td><code>type</code></td><td>NVARCHAR(20)</td><td><code>official</code> / <code>religious</code> / <code>national</code></td></tr>
<tr><td><code>is_bank_closed</code></td><td>BIT</td><td>Drives payout date shifting</td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> referenced by payout scheduling and (optionally) pricing.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,25 @@
# Domain 12 — Audit, Config & Reference
[← Database Model](index.md)
### `audit_logs` [CORE]
**Role:** Immutable, append-only record of every state change on sensitive entities — now explicitly **including `platform_configs`** (so finance can prove the commission rate at any moment). **Why:** compliance and accountability; `changed_fields_json` enables fast filtering. Plan month-partitioning + 23yr cold-storage archival before launch. Fields unchanged. **Relations:** polymorphic, append-only.
### `system_events` [MVP]
**Role:** High-volume behavioral/analytics event log. **Why kept but de-emphasized:** product analytics, not compliance. It grows unbounded — at scale, pipe it to an analytics sink/warehouse rather than the transactional DB. Fields unchanged.
### `platform_configs` [CORE]
**Role:** Key-value runtime business parameters — change without a deploy. **Why typed values:** `data_type` tells the app how to parse. **New keys** this revision: `dispute_window_hours` (default 72), `vat_rate` (0.10), `bnpl_merchant_of_record`, `bnpl_provider_commission_rate`, `bnpl_settlement_timing`, cancellation-tier defaults — alongside the existing `platform_fee_rate`, `booking_payment_deadline_minutes`, `nurse_response_deadline_hours`, `nurse_payout_interval_days`, `evv_location_tolerance_meters`, `min_rating_for_support_alert`. **Relations:** referenced everywhere; changes audited.
### `iranian_holidays` [MVP] — **NEW**
**Role:** Shared official/religious holiday calendar (movable, partly lunar-Hijri), with a `is_bank_closed` flag. **Why a real table:** Iran's holidays are numerous and partly movable, and they drive **payout bank-closure scheduling** (PAYA/SATNA closed → a weekly payout shifts to the next business day), optional holiday pricing, and business-hour deadline math — none of which a purely manual per-nurse availability exception can express.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `holiday_date` | DATE | |
| `name_fa` | NVARCHAR(200) | |
| `type` | NVARCHAR(20) | `official` / `religious` / `national` |
| `is_bank_closed` | BIT | Drives payout date shifting |
**Relations:** referenced by payout scheduling and (optionally) pricing.
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Domain 13 — Partner Centers (launch) &amp; Future — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a class="active" href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="domain-13-partner-centers-launch-future">Domain 13 — Partner Centers (launch) &amp; Future</h1>
<p><a href="index.html">← Database Model</a></p>
<p><strong>Related:</strong> business requirements — <a href="../business/13-tax-invoicing-and-legal.html">Tax, invoicing &amp; legal</a>.</p>
<h3 id="partner_centers-mvp-new"><code>partner_centers</code> [MVP] — <strong>NEW</strong> <a class="anchor" href="#partner_centers-mvp-new" aria-hidden="true">#</a></h3>
<p><strong>Role:</strong> A licensed home-nursing center (مرکز مشاوره و ارائه مراقبت‌های پرستاری در منزل) that <strong>sponsors</strong> nurses and is plausibly the <strong>merchant-of-record</strong> and invoice issuer at launch. <strong>Why this is the single most launch-critical addition:</strong> the research's #1 go-to-market recommendation is to operate by subcontracting to an already-licensed center (the Asanism model) while Balinyaar's own MoH permit is pending — the center is what makes the operation legal, may be who the IPG settles to, and clears the BNPL onboarding gate (which needs a جواز کسب + eNamad the company/center holds, not each nurse). Distinct from the future <code>organizations</code> (employer) table — this is the <strong>licensing/launch sponsor</strong>.</p>
<div class="table-wrap"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGINT PK</td><td></td></tr>
<tr><td><code>name</code></td><td>NVARCHAR(300)</td><td></td></tr>
<tr><td><code>legal_entity_type</code></td><td>NVARCHAR(30)</td><td></td></tr>
<tr><td><code>moh_establishment_permit_no</code></td><td>NVARCHAR(100)</td><td>پروانه تأسیس</td></tr>
<tr><td><code>technical_director_nurse_user_id</code></td><td>BIGINT FK → users NULL</td><td>مسئول فنی</td></tr>
<tr><td><code>technical_director_license_no</code></td><td>NVARCHAR(100) NULL</td><td></td></tr>
<tr><td><code>enamad_code</code></td><td>NVARCHAR(100) NULL</td><td></td></tr>
<tr><td><code>settlement_iban</code></td><td>NVARCHAR(34) (enc) NULL</td><td>If merchant-of-record</td></tr>
<tr><td><code>is_merchant_of_record</code></td><td>BIT</td><td></td></tr>
<tr><td><code>commission_rate</code></td><td>DECIMAL(5,4) NULL</td><td>Center's cut, if any</td></tr>
<tr><td><code>admin_user_id</code></td><td>BIGINT FK → users</td><td>Center's dashboard account</td></tr>
<tr><td><code>is_active</code>, <code>verified_at</code>, timestamps</td><td></td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> 1:N → <code>nurse_profiles</code> (sponsors), <code>bookings</code> (legally covered by), <code>invoices</code> (issuer).</p>
<h3 id="organizations-organization_nurses-deferred"><code>organizations</code> / <code>organization_nurses</code> [DEFERRED] <a class="anchor" href="#organizations-organization_nurses-deferred" aria-hidden="true">#</a></h3>
<p><strong>Role/Why:</strong> The future <strong>employer</strong> model (nursing companies adding their employed nurses). Modeled-but-inactive — no launch table references them, so adding them later is a pure additive migration. Kept distinct from <code>partner_centers</code> (launch licensing) to avoid conflating "sponsor for legality" with "employer."</p>
<h3 id="fraud_flags-deferred"><code>fraud_flags</code> [DEFERRED] <a class="anchor" href="#fraud_flags-deferred" aria-hidden="true">#</a></h3>
<p><strong>Role/Why:</strong> Output of a future ML fraud service. Premature for a no-traffic MVP; <code>support_alerts</code> (<code>fraud_signal</code> type) covers rule-based signals manually. Inactive stub.</p>
<h3 id="recurring_booking_schedules-deferred"><code>recurring_booking_schedules</code> [DEFERRED] <a class="anchor" href="#recurring_booking_schedules-deferred" aria-hidden="true">#</a></h3>
<p><strong>Role/Why:</strong> RFC-5545 recurrence for repeating care patterns. Note: the concrete multi-day need is now met by <code>booking_sessions</code>; this remains deferred for true open-ended recurrence. Inactive stub.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
@@ -0,0 +1,34 @@
# Domain 13 — Partner Centers (launch) & Future
[← Database Model](index.md)
**Related:** business requirements — [Tax, invoicing & legal](../business/13-tax-invoicing-and-legal.md).
### `partner_centers` [MVP] — **NEW**
**Role:** A licensed home-nursing center (مرکز مشاوره و ارائه مراقبت‌های پرستاری در منزل) that **sponsors** nurses and is plausibly the **merchant-of-record** and invoice issuer at launch. **Why this is the single most launch-critical addition:** the research's #1 go-to-market recommendation is to operate by subcontracting to an already-licensed center (the Asanism model) while Balinyaar's own MoH permit is pending — the center is what makes the operation legal, may be who the IPG settles to, and clears the BNPL onboarding gate (which needs a جواز کسب + eNamad the company/center holds, not each nurse). Distinct from the future `organizations` (employer) table — this is the **licensing/launch sponsor**.
| Field | Type | Notes |
|---|---|---|
| `id` | BIGINT PK | |
| `name` | NVARCHAR(300) | |
| `legal_entity_type` | NVARCHAR(30) | |
| `moh_establishment_permit_no` | NVARCHAR(100) | پروانه تأسیس |
| `technical_director_nurse_user_id` | BIGINT FK → users NULL | مسئول فنی |
| `technical_director_license_no` | NVARCHAR(100) NULL | |
| `enamad_code` | NVARCHAR(100) NULL | |
| `settlement_iban` | NVARCHAR(34) (enc) NULL | If merchant-of-record |
| `is_merchant_of_record` | BIT | |
| `commission_rate` | DECIMAL(5,4) NULL | Center's cut, if any |
| `admin_user_id` | BIGINT FK → users | Center's dashboard account |
| `is_active`, `verified_at`, timestamps | … | |
**Relations:** 1:N → `nurse_profiles` (sponsors), `bookings` (legally covered by), `invoices` (issuer).
### `organizations` / `organization_nurses` [DEFERRED]
**Role/Why:** The future **employer** model (nursing companies adding their employed nurses). Modeled-but-inactive — no launch table references them, so adding them later is a pure additive migration. Kept distinct from `partner_centers` (launch licensing) to avoid conflating "sponsor for legality" with "employer."
### `fraud_flags` [DEFERRED]
**Role/Why:** Output of a future ML fraud service. Premature for a no-traffic MVP; `support_alerts` (`fraud_signal` type) covers rule-based signals manually. Inactive stub.
### `recurring_booking_schedules` [DEFERRED]
**Role/Why:** RFC-5545 recurrence for repeating care patterns. Note: the concrete multi-day need is now met by `booking_sessions`; this remains deferred for true open-ended recurrence. Inactive stub.
+164
View File
@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Diagrams — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a href="index.html">Overview &amp; decisions</a></li><li><a class="active" href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="diagrams">Diagrams</h1>
<p><a href="index.html">← Database Model</a></p>
<h3 id="1-domain-map-how-the-clusters-relate">1. Domain map — how the clusters relate <a class="anchor" href="#1-domain-map-how-the-clusters-relate" aria-hidden="true">#</a></h3>
<pre class="mermaid">flowchart LR
PARTNER["🏥 Partner Centers (launch)&lt;br/&gt;partner_centers"]
IDENTITY["🧑 Identity &amp; Access&lt;br/&gt;users · nurse_profiles · customer_profiles&lt;br/&gt;patients · customer_addresses · nurse_bank_accounts"]
GEO["📍 Geography&lt;br/&gt;provinces · cities · districts · nurse_service_areas"]
VERIFY["✅ Verification&lt;br/&gt;nurse_verifications · step_types · steps&lt;br/&gt;documents · nurse_credentials"]
SERVICES["🩺 Services &amp; Pricing&lt;br/&gt;service_categories · option_groups · option_values&lt;br/&gt;variants · variant_options · search_index · availability"]
BOOKING["📅 Booking &amp; Scheduling&lt;br/&gt;booking_requests · bookings · booking_sessions&lt;br/&gt;care_instructions · visit_verifications · cancellation_policies"]
PAY["💳 Payments &amp; Ledger&lt;br/&gt;payment_gateways · payment_transactions · webhook_events&lt;br/&gt;refunds · ledger_entries · nurse_clawbacks · invoices"]
BNPL["🧾 BNPL&lt;br/&gt;bnpl_transactions"]
PAYOUT["🏦 Payouts&lt;br/&gt;payout_batches · payouts · booking_links"]
REVIEW["⭐ Reviews &amp; Records&lt;br/&gt;reviews · review_tags · patient_care_records"]
MSG["💬 Messaging&lt;br/&gt;tickets · participants · messages"]
NOTIFY["🔔 Notifications&lt;br/&gt;notifications · support_alerts"]
AUDITCFG["📜 Audit &amp; Config&lt;br/&gt;audit_logs · system_events&lt;br/&gt;platform_configs · iranian_holidays"]
PARTNER -. "sponsors / merchant-of-record" .-&gt; VERIFY
IDENTITY --&gt; VERIFY
VERIFY --&gt; SERVICES
SERVICES --&gt; GEO
IDENTITY --&gt; BOOKING
SERVICES --&gt; BOOKING
BOOKING --&gt; PAY
PAY --&gt; BNPL
PAY --&gt; PAYOUT
BOOKING --&gt; REVIEW
BOOKING --&gt; MSG
PAY --&gt; NOTIFY
PAY --&gt; AUDITCFG</pre>
<h3 id="2-core-booking-spine-who-books-whom">2. Core booking spine (who books whom) <a class="anchor" href="#2-core-booking-spine-who-books-whom" aria-hidden="true">#</a></h3>
<pre class="mermaid">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
}</pre>
<h3 id="3-payments-ledger-payouts">3. Payments, ledger &amp; payouts <a class="anchor" href="#3-payments-ledger-payouts" aria-hidden="true">#</a></h3>
<pre class="mermaid">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
}</pre>
<h3 id="4-financial-lifecycle-escrow-payout-clawback">4. Financial lifecycle — escrow → payout → clawback <a class="anchor" href="#4-financial-lifecycle-escrow-payout-clawback" aria-hidden="true">#</a></h3>
<pre class="mermaid">flowchart TD
A["Family submits booking_request"] --&gt; B{"Nurse responds in time?"}
B --&gt;|"reject / expire"| X["request closed — no money moved"]
B --&gt;|"accept"| C["30-min payment window"]
C --&gt; D{"Payment method"}
D --&gt;|"Card (IPG)"| E["payment_transactions = succeeded"]
D --&gt;|"BNPL (SnappPay)"| F["bnpl_transactions = settled&lt;br/&gt;full amount minus provider commission"]
E --&gt; G["Ledger posting:&lt;br/&gt;DR escrow_held / CR nurse_payable + platform_revenue"]
F --&gt; G
G --&gt; H["Booking confirmed (escrow held)"]
H --&gt; I["Nurse EVV check-in / check-out per session"]
I --&gt; J["Booking completed"]
J --&gt; K["dispute_window_ends_at = completed_at + 72h"]
K --&gt; L{"Window passed &amp; no dispute?"}
L --&gt;|"yes"| M["payout_eligible"]
M --&gt; N["Weekly batch → PAYA to nurse IBAN&lt;br/&gt;payout = gross balinyaar_commission"]
K -.-&gt;|"refund BEFORE payout"| O["Clean ledger reversal&lt;br/&gt;PSP refund / bnpl_revert"]
N --&gt; P{"Refund AFTER payout?"}
P --&gt;|"yes"| Q["nurse_clawbacks receivable&lt;br/&gt;netted next batch or written off"]
P --&gt;|"no"| Z["Settled and reconciled"]</pre>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
mermaid.initialize({ startOnLoad: true, theme: dark ? 'dark' : 'neutral' });
</script>
</body>
</html>
+147
View File
@@ -0,0 +1,147 @@
# Diagrams
[← Database Model](index.md)
### 1. Domain map — how the clusters relate
```mermaid
flowchart LR
PARTNER["🏥 Partner Centers (launch)<br/>partner_centers"]
IDENTITY["🧑 Identity & Access<br/>users · nurse_profiles · customer_profiles<br/>patients · customer_addresses · nurse_bank_accounts"]
GEO["📍 Geography<br/>provinces · cities · districts · nurse_service_areas"]
VERIFY["✅ Verification<br/>nurse_verifications · step_types · steps<br/>documents · nurse_credentials"]
SERVICES["🩺 Services & Pricing<br/>service_categories · option_groups · option_values<br/>variants · variant_options · search_index · availability"]
BOOKING["📅 Booking & Scheduling<br/>booking_requests · bookings · booking_sessions<br/>care_instructions · visit_verifications · cancellation_policies"]
PAY["💳 Payments & Ledger<br/>payment_gateways · payment_transactions · webhook_events<br/>refunds · ledger_entries · nurse_clawbacks · invoices"]
BNPL["🧾 BNPL<br/>bnpl_transactions"]
PAYOUT["🏦 Payouts<br/>payout_batches · payouts · booking_links"]
REVIEW["⭐ Reviews & Records<br/>reviews · review_tags · patient_care_records"]
MSG["💬 Messaging<br/>tickets · participants · messages"]
NOTIFY["🔔 Notifications<br/>notifications · support_alerts"]
AUDITCFG["📜 Audit & Config<br/>audit_logs · system_events<br/>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)
```mermaid
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
}
```
### 3. Payments, ledger & payouts
```mermaid
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
```mermaid
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<br/>full amount minus provider commission"]
E --> G["Ledger posting:<br/>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<br/>payout = gross balinyaar_commission"]
K -.->|"refund BEFORE payout"| O["Clean ledger reversal<br/>PSP refund / bnpl_revert"]
N --> P{"Refund AFTER payout?"}
P -->|"yes"| Q["nurse_clawbacks receivable<br/>netted next batch or written off"]
P -->|"no"| Z["Settled and reconciled"]
```
+195
View File
@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Database Model — Balinyaar docs</title>
<link rel="stylesheet" href="../assets/doc.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="../index.html"><span class="dot"></span> Balinyaar docs</a>
<p class="tagline">Trust-first home-nursing marketplace · Iran</p>
<nav><div class="group"><div class="label">Start here</div><ul><li><a href="../index.html">Docs home</a></li><li><a href="../overview/platform-summary.html">Platform summary &amp; ground truths</a></li></ul></div><div class="group"><div class="label">Business requirements</div><ul><li><a href="../business/index.html">Overview &amp; MVP scope</a></li><li><a href="../business/01-actors-and-onboarding.html">1. Actors &amp; onboarding</a></li><li><a href="../business/02-nurse-verification.html">2. Nurse verification</a></li><li><a href="../business/03-service-catalog-and-pricing.html">3. Service catalog &amp; pricing</a></li><li><a href="../business/04-search-and-matching.html">4. Search &amp; matching</a></li><li><a href="../business/05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="../business/06-evv-and-service-delivery.html">6. EVV / service delivery</a></li><li><a href="../business/07-cancellation-and-refunds.html">7. Cancellation &amp; refunds</a></li><li><a href="../business/08-payments-and-escrow.html">8. Payments &amp; escrow</a></li><li><a href="../business/09-installments-bnpl.html">9. Installments / BNPL</a></li><li><a href="../business/10-payouts.html">10. Payouts to nurses</a></li><li><a href="../business/11-reviews-trust-and-safety.html">11. Reviews, trust &amp; safety</a></li><li><a href="../business/12-messaging-and-emergencies.html">12. Messaging &amp; emergencies</a></li><li><a href="../business/13-tax-invoicing-and-legal.html">13. Tax, invoicing &amp; legal</a></li><li><a href="../business/14-notifications-and-admin.html">14. Notifications &amp; admin</a></li></ul></div><div class="group"><div class="label">Database model</div><ul><li><a class="active" href="index.html">Overview &amp; decisions</a></li><li><a href="diagrams.html">Diagrams</a></li><li><a href="01-identity-and-access.html">1. Identity &amp; access</a></li><li><a href="02-geography.html">2. Geography</a></li><li><a href="03-services-and-pricing.html">3. Services &amp; pricing</a></li><li><a href="04-verification-and-credentials.html">4. Verification &amp; credentials</a></li><li><a href="05-booking-and-scheduling.html">5. Booking &amp; scheduling</a></li><li><a href="06-payments-ledger-and-refunds.html">6. Payments, ledger &amp; refunds</a></li><li><a href="07-payouts.html">7. Payouts</a></li><li><a href="08-bnpl.html">8. BNPL / installments</a></li><li><a href="09-messaging.html">9. Messaging</a></li><li><a href="10-reviews-and-records.html">10. Reviews &amp; records</a></li><li><a href="11-notifications.html">11. Notifications</a></li><li><a href="12-audit-config-and-reference.html">12. Audit, config &amp; reference</a></li><li><a href="13-partner-centers-and-future.html">13. Partner centers &amp; future</a></li></ul></div><div class="group"><div class="label">Payments deep-dive</div><ul><li><a href="../payments/index.html">Overview &amp; exec summary</a></li><li><a href="../payments/iranian-payment-reality.html">Iranian payment reality</a></li><li><a href="../payments/escrow-ledger.html">Escrow as a ledger</a></li><li><a href="../payments/bnpl-landscape.html">BNPL landscape &amp; finding</a></li><li><a href="../payments/cancellation-and-payout.html">Cancellation &amp; nurse payout</a></li><li><a href="../payments/integration-notes.html">Integration &amp; schema touchpoints</a></li><li><a href="../payments/sources.html">Recommendations &amp; sources</a></li></ul></div><div class="group"><div class="label">Research &amp; strategy</div><ul><li><a href="../research/index.html">Overview &amp; exec summary</a></li><li><a href="../research/market-and-competitors.html">Market &amp; competitors</a></li><li><a href="../research/problems-and-risks.html">Problems &amp; risks</a></li><li><a href="../research/verification.html">Verification (research)</a></li><li><a href="../research/legal-landscape.html">Legal landscape</a></li><li><a href="../research/go-to-market.html">Go-to-market &amp; sources</a></li></ul></div><div class="group"><div class="label">Notes &amp; more</div><ul><li><a href="../notes/open-questions.html">Open questions</a></li><li><a href="../notes/future-ideas.html">Future ideas</a></li><li><a href="../wireframes/index.html">Wireframes</a></li><li><a href="../fa/index.html">Farsi documents</a></li></ul></div></nav>
</aside>
<main class="main"><div class="content">
<div class="topbar"><button class="theme-toggle" type="button" onclick="__t()">theme</button></div>
<h1 id="database-model">Database Model</h1>
<blockquote><p><strong>Revision 2 — 2026-06-20.</strong> 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.</p>
<p>Companion documents: <strong><a href="../business/index.html">business requirements</a></strong> (per-section product requirements) and <strong><a href="../payments/index.html">payments deep-dive</a></strong> (the BNPL/escrow deep-dive with sources).</p>
</blockquote>
<h2 id="contents">Contents <a class="anchor" href="#contents" aria-hidden="true">#</a></h2>
<ul>
<li><a href="diagrams.html">Diagrams</a></li>
<li><a href="01-identity-and-access.html">Domain 1 — Identity &amp; Access</a></li>
<li><a href="02-geography.html">Domain 2 — Geographic Data</a></li>
<li><a href="03-services-and-pricing.html">Domain 3 — Services &amp; Pricing</a></li>
<li><a href="04-verification-and-credentials.html">Domain 4 — Verification &amp; Credentials</a></li>
<li><a href="05-booking-and-scheduling.html">Domain 5 — Booking &amp; Scheduling</a></li>
<li><a href="06-payments-ledger-and-refunds.html">Domain 6 — Payments, Ledger &amp; Refunds</a></li>
<li><a href="07-payouts.html">Domain 7 — Payouts to Nurses</a></li>
<li><a href="08-bnpl.html">Domain 8 — BNPL / Installments</a></li>
<li><a href="09-messaging.html">Domain 9 — Messaging (Ticket System)</a></li>
<li><a href="10-reviews-and-records.html">Domain 10 — Reviews &amp; Patient Records</a></li>
<li><a href="11-notifications.html">Domain 11 — Notifications</a></li>
<li><a href="12-audit-config-and-reference.html">Domain 12 — Audit, Config &amp; Reference</a></li>
<li><a href="13-partner-centers-and-future.html">Domain 13 — Partner Centers (launch) &amp; Future</a></li>
</ul>
<hr>
<h2 id="platform-summary">Platform Summary <a class="anchor" href="#platform-summary" aria-hidden="true">#</a></h2>
<p>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 <strong>پروانه صلاحیت حرفه‌ای</strong> (professional-competency license). Families search — filtered by city/district <strong>and same-gender caregiver preference</strong> — pick a nurse and a service variant, submit a booking request, and pay <strong>through the platform</strong> after the nurse accepts. The platform records the money as an <strong>internal escrow ledger state</strong> (not platform-held cash — see Principle 2), the nurse performs one or more EVV-verified visits, and the platform pays the nurse <strong>weekly, after the dispute window closes</strong>, minus a platform commission. All post-booking communication runs through an admin-readable ticket system.</p>
<p>At launch the platform operates under a <strong>partner licensed home-nursing center (مرکز مشاوره و ارائه مراقبت‌های پرستاری در منزل)</strong> — the Asanism-style model — which is the legal vehicle and the likely <strong>merchant-of-record</strong> for payments while Balinyaar's own MoH permit is in process.</p>
<hr>
<h2 id="what-changed-in-this-revision-decision-summary">What changed in this revision (decision summary) <a class="anchor" href="#what-changed-in-this-revision-decision-summary" aria-hidden="true">#</a></h2>
<div class="table-wrap"><table><thead><tr><th>#</th><th>Decision</th><th>Why</th><th>Schema effect</th></tr></thead><tbody>
<tr><td>1</td><td><strong>Escrow is a ledger _state_, not held cash</strong></td><td>An Iranian پرداخت‌یار (payment facilitator) is legally barred from custodying buyer funds; money flows card → PSP → Shaparak → registered IBANs.</td><td>New <code>ledger_entries</code> (double-entry); "escrow" derives from it.</td></tr>
<tr><td>2</td><td><strong>BNPL = full-upfront single settlement</strong></td><td>Verified: SnappPay/Digipay/Tara/Torob pay the <em>merchant</em> the whole amount minus commission in one lump and bear the customer-default risk.</td><td><strong>Cut</strong> <code>installment_plans</code>/<code>installment_entries</code>; <strong>replace</strong> with one <code>bnpl_transactions</code> row. No customer-installment tracking.</td></tr>
<tr><td>3</td><td><strong>Nurse paid by Balinyaar, on its own weekly schedule</strong></td><td>The customer's installments are owned by the BNPL provider and decoupled from our payout.</td><td>Three-way money split on <code>bookings</code>; payout independent of BNPL.</td></tr>
<tr><td>4</td><td><strong>Clawback is first-class</strong></td><td>A booking can be disputed/refunded after the nurse was already paid; the old model had nowhere to record the receivable.</td><td>New <code>nurse_clawbacks</code> + <code>dispute_window_ends_at</code> gating.</td></tr>
<tr><td>5</td><td><strong>Webhook idempotency before real money</strong></td><td>PSP/BNPL callbacks are at-least-once and retried.</td><td>New <code>payment_webhook_events</code> keyed on <code>external_event_id</code>.</td></tr>
<tr><td>6</td><td><strong>Multi-session engagements</strong></td><td>Elder care is dominantly multi-day / شبانه‌روزی (live-in); one-visit-per-booking can't model it.</td><td>New <code>booking_sessions</code>; EVV + payout move to the session.</td></tr>
<tr><td>7</td><td><strong>Partner licensed-center entity</strong></td><td>The recommended launch path is subcontracting to an MoH-licensed center; it may be the merchant-of-record/invoice issuer.</td><td>New <code>partner_centers</code> + <code>nurse_profiles.partner_center_id</code>.</td></tr>
<tr><td>8</td><td><strong>Structured credential registry</strong></td><td>The "verified" trust badge and renewal alerts need queryable license numbers/expiries, not just opaque PDF uploads.</td><td>New <code>nurse_credentials</code>.</td></tr>
<tr><td>9</td><td><strong>Cheaper search</strong></td><td>Nurse search needed 4+ joins from day one.</td><td>New denormalized <code>nurse_search_index</code>.</td></tr>
<tr><td>10</td><td><strong>Cancellation policy + tax/invoice</strong></td><td>"Default 100% refund" is naive; Iranian commission marketplaces owe VAT on commission.</td><td>New <code>cancellation_policies</code>, <code>invoices</code>; VAT <strong>10%</strong> (configurable).</td></tr>
<tr><td>11</td><td><strong>Integrity hardening</strong></td><td>Drift, double-pay, and tenancy-leak gaps the critiques found.</td><td>Drop duplicate <code>verification_status</code> &amp; <code>payout_released</code>; add uniqueness/CHECK/tenancy invariants.</td></tr>
</tbody></table></div>
<hr>
<h2 id="design-principles">Design Principles <a class="anchor" href="#design-principles" aria-hidden="true">#</a></h2>
<ol>
<li><strong>Money is <code>BIGINT</code> in Iranian Rials (IRR).</strong> Toman is a display concern only; conversion happens <strong>only at a provider's API boundary</strong> (e.g. SnappPay quotes Toman) and never internally. No floats anywhere on the money path.</li>
<li><strong>The platform never legally holds buyer cash.</strong> Funds settle through a licensed PSP/پرداخت‌یار to <strong>registered IBANs</strong> (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 <strong>derived ledger states</strong> over money custodied at the provider/partner bank — represented in <code>ledger_entries</code>, never as a Balinyaar-owned cash balance.</li>
<li><strong><code>ledger_entries</code> is the financial source of truth.</strong> Every capture, commission, payout, refund, and clawback posts <strong>balanced</strong> double-entry rows. Per-table money fields (e.g. <code>bookings.gross_price_irr</code>) 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."</li>
<li><strong>Fee split is captured per booking and never derived from live config</strong>, so historical reporting survives commission-schedule changes. The booking stores three distinct amounts: <code>gross_price_irr</code>, <code>balinyaar_commission_irr</code>, <code>nurse_payout_amount</code>.</li>
<li><strong>PII fields</strong> (national ID, IBAN, phone, addresses, clinical data) are marked <strong>(encrypted)</strong> — column- or application-level. Clinical data has stricter access than financial data.</li>
<li><strong>Two-stage clinical disclosure is a hard rule, not a convention.</strong> At the request stage the nurse sees only <code>booking_requests.customer_notes</code>. The full encrypted <code>booking_care_instructions</code> are exposed <strong>only after</strong> the booking is confirmed. Enforced at the authorization layer.</li>
<li><strong>Soft deletes</strong> on <code>users</code>/<code>nurse_profiles</code> via <code>deleted_at</code>. Audit, payment, ledger, and payout records are <strong>never</strong> deleted.</li>
<li><strong>Audit trail is append-only.</strong> All state transitions on bookings, payments, refunds, payouts, verifications, reviews, and <code>platform_configs</code> produce an <code>audit_logs</code> row.</li>
<li><strong>Catalog/config tables are rows, not enums</strong> (service categories, verification step types, cancellation policies, Iranian holidays) so the business evolves without migrations. They carry <code>name_fa</code>/<code>name_en</code>.</li>
<li><strong>Idempotency is mandatory on the money path.</strong> Every PSP/BNPL callback is stored raw in <code>payment_webhook_events</code> and deduplicated on <code>external_event_id</code> <strong>before</strong> any money-state mutation.</li>
<li><strong>All timestamps are <code>DATETIME2(7)</code> UTC.</strong> Persian-calendar display is a UI concern — <strong>except</strong> that bank-closure scheduling uses the <code>iranian_holidays</code> table, because PAYA/SATNA transfers fail on holidays.</li>
<li><strong>Derived flags must not drift.</strong> <code>nurse_profiles.is_verified</code>, denormalized rating aggregates, and the search index are written <strong>only</strong> by the code path that owns their source of truth, inside the same transaction.</li>
<li><strong>Invariants are enforced, not just documented:</strong> CHECK constraints (<code>gross = commission + payout</code>, <code>rating BETWEEN 1 AND 5</code>, amounts ≥ 0, <code>end_time &gt; start_time</code>), 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).</li>
</ol>
<hr>
<h2 id="the-two-questions-this-revision-answers">The two questions this revision answers <a class="anchor" href="#the-two-questions-this-revision-answers" aria-hidden="true">#</a></h2>
<p>These were the two hardest open questions (from <code>whatsInYourMind.txt</code>). Both are resolved against <strong>verified</strong> research that all mainstream Iranian provider-financed BNPLs use <strong>full-upfront settlement</strong> — the provider pays the merchant the whole amount minus commission and owns the customer's installments and default risk.</p>
<h3 id="q1-a-booking-paid-by-installments-bnpl-is-cancelled-or-refunded-mid-plan-what-happens">Q1 — A booking paid by installments (BNPL) is cancelled or refunded mid-plan. What happens? <a class="anchor" href="#q1-a-booking-paid-by-installments-bnpl-is-cancelled-or-refunded-mid-plan-what-happens" aria-hidden="true">#</a></h3>
<p>Money <strong>always</strong> flows <code>customer ↔ BNPL provider ↔ Balinyaar</code><strong>never</strong> nurse→customer, and <strong>never</strong> Balinyaar→customer directly for a BNPL order.</p>
<ol>
<li>Balinyaar initiates the reversal through the provider's API (SnappPay <code>revert</code> for full / <code>cancel</code>/<code>update</code> for partial, using the stored <code>external_payment_token</code>).</li>
<li>The provider then <strong>cancels the customer's unpaid installments</strong>, restores their credit, and <strong>refunds any already-paid installment to the customer's bank account in ~710 business days</strong> (asynchronous, owned by the provider).</li>
<li>Balinyaar records a <code>refunds</code> row with <code>refund_channel = 'bnpl_revert'</code>, carrying <code>external_revert_reference</code> and <code>expected_customer_refund_eta</code>; <code>refund_status</code> stays <code>processing</code> until a reconciliation job confirms.</li>
<li>The refund <strong>decomposes</strong> across the two fee legs — <code>platform_fee_refunded_irr</code> and <code>nurse_payout_refunded_irr</code> — and posts <strong>balanced ledger entries</strong>.</li>
<li><strong>If the nurse has not yet been paid</strong> (still inside the dispute window / not in a processed batch): the <code>nurse_payable</code> accrual is simply reversed; nothing leaves Balinyaar. Clean.</li>
<li><strong>If the nurse has already been paid:</strong> this is the <strong>clawback</strong> path — a <code>nurse_clawbacks</code> receivable + negative ledger entry; recovered from the next payout batch or written off.</li>
</ol>
<p>A shortened/partial visit maps to the provider's <code>update</code> endpoint with a reduced amount; record <code>refund_delta_irr</code> and reduce <code>bnpl_transactions.settled_amount_irr</code>.</p>
<h3 id="q2-under-bnpl-who-pays-the-nurse-and-when">Q2 — Under BNPL, who pays the nurse and when? <a class="anchor" href="#q2-under-bnpl-who-pays-the-nurse-and-when" aria-hidden="true">#</a></h3>
<p><strong>Balinyaar pays the nurse</strong>, on its <strong>own normal weekly payout schedule, after EVV completion and after the dispute window closes</strong><em>exactly the same path as a card-funded booking</em>. The BNPL provider never pays the nurse and is indifferent to the internal split.</p>
<p>The nurse's payout is computed from the booking's <strong><code>gross_price_irr</code> minus <code>balinyaar_commission_irr</code></strong><strong>never</strong> from the BNPL provider's net <code>settled_amount_irr</code>. The provider's commission (<code>bnpl_commission_irr</code>) is a <strong>platform cost of accepting BNPL</strong>, borne by Balinyaar, and must never touch the nurse's payout. Hence three separately stored amounts:</p>
<div class="table-wrap"><table><thead><tr><th>Amount</th><th>Meaning</th><th>Drives</th></tr></thead><tbody>
<tr><td><code>gross_price_irr</code></td><td>What the customer is charged (the booking price)</td><td>The invoice, the refund base</td></tr>
<tr><td><code>balinyaar_commission_irr</code></td><td>Platform's own cut</td><td>Platform revenue</td></tr>
<tr><td><code>bnpl_commission_irr</code></td><td>The BNPL provider's merchant discount (on <code>bnpl_transactions</code>)</td><td>Platform <strong>expense</strong> (never the nurse's)</td></tr>
</tbody></table></div>
<p><code>nurse_payout_amount = gross_price_irr balinyaar_commission_irr</code>. The nurse receives the identical amount and on the identical schedule whether the family paid by card or by SnappPay.</p>
<hr>
<h2 id="relationship-summary">Relationship Summary <a class="anchor" href="#relationship-summary" aria-hidden="true">#</a></h2>
<div class="table-wrap"><table><thead><tr><th>Relationship</th><th>Type</th><th>Notes</th></tr></thead><tbody>
<tr><td><code>users</code><code>nurse_profiles</code> / <code>customer_profiles</code></td><td>1:1</td><td>by <code>role</code></td></tr>
<tr><td><code>partner_centers</code><code>nurse_profiles</code></td><td>1:N</td><td>launch sponsor (NEW)</td></tr>
<tr><td><code>customer_profiles</code><code>patients</code> / <code>customer_addresses</code></td><td>1:N</td><td></td></tr>
<tr><td><code>nurse_profiles</code><code>nurse_service_variants</code> / <code>nurse_service_areas</code> / <code>nurse_bank_accounts</code> / <code>nurse_credentials</code></td><td>1:N</td><td></td></tr>
<tr><td><code>nurse_service_variants</code><code>nurse_service_variant_options</code></td><td>1:N</td><td>option combination</td></tr>
<tr><td><code>nurse_profiles</code><code>nurse_verifications</code></td><td>1:1</td><td></td></tr>
<tr><td><code>nurse_verifications</code><code>verification_steps</code><code>verification_documents</code></td><td>1:N → 1:N</td><td></td></tr>
<tr><td><code>booking_requests</code><code>bookings</code></td><td>1:1</td><td>on nurse-accept + payment</td></tr>
<tr><td><code>bookings</code><code>booking_sessions</code></td><td>1:N</td><td><strong>NEW</strong> — multi-visit engagements</td></tr>
<tr><td><code>booking_sessions</code><code>visit_verifications</code></td><td>1:1</td><td><strong>CHANGED</strong> — EVV per session</td></tr>
<tr><td><code>bookings</code><code>booking_care_instructions</code> / <code>reviews</code> / <code>invoices</code></td><td>1:1</td><td></td></tr>
<tr><td><code>bookings</code><code>payment_transactions</code></td><td>1:N</td><td>attempts</td></tr>
<tr><td><code>payment_transactions</code><code>bnpl_transactions</code></td><td>1:1</td><td>if BNPL (<strong>replaces</strong> installment_plans)</td></tr>
<tr><td><code>payment_transactions</code><code>refunds</code></td><td><strong>1:N</strong></td><td><strong>CHANGED</strong> — partials allowed</td></tr>
<tr><td><code>payment_gateways</code><code>payment_webhook_events</code></td><td>1:N</td><td><strong>NEW</strong> — idempotency</td></tr>
<tr><td><code>bookings</code> / nurses → <code>ledger_entries</code></td><td>1:N</td><td><strong>NEW</strong> — money source of truth</td></tr>
<tr><td><code>refunds</code><code>nurse_clawbacks</code></td><td>1:1 (opt)</td><td><strong>NEW</strong> — refund-after-payout</td></tr>
<tr><td><code>nurse_payout_batches</code><code>nurse_payouts</code><code>nurse_payout_booking_links</code></td><td>1:N → 1:N</td><td><code>booking_id</code> UNIQUE</td></tr>
<tr><td><code>nurse_payout_booking_links</code><code>bookings</code></td><td>1:1</td><td>exactly one payout per booking</td></tr>
<tr><td><code>patients</code><code>patient_care_records</code></td><td>1:N</td><td>longitudinal history</td></tr>
<tr><td><code>tickets</code><code>ticket_participants</code> / <code>ticket_messages</code></td><td>1:N</td><td></td></tr>
<tr><td>Sensitive entities → <code>audit_logs</code></td><td>*:N</td><td>append-only</td></tr>
</tbody></table></div>
<hr>
<h2 id="final-mvp-table-list">Final MVP table list <a class="anchor" href="#final-mvp-table-list" aria-hidden="true">#</a></h2>
<p><strong>Identity &amp; Access:</strong> <code>users</code> · <code>user_sessions</code> · <code>roles</code> · <code>user_roles</code> · <code>nurse_profiles</code> · <code>customer_profiles</code> · <code>patients</code> · <code>customer_addresses</code> · <code>nurse_bank_accounts</code> — all <strong>[CORE]</strong></p>
<p><strong>Geography:</strong> <code>provinces</code> · <code>cities</code> <strong>[CORE]</strong> · <code>districts</code> <strong>[MVP]</strong> · <code>nurse_service_areas</code> <strong>[CORE]</strong></p>
<p><strong>Services &amp; Pricing:</strong> <code>service_categories</code> · <code>service_option_groups</code> · <code>service_option_values</code> · <code>nurse_service_variants</code> · <code>nurse_service_variant_options</code> · <strong><code>nurse_search_index</code></strong> <em>(NEW)</em><strong>[CORE]</strong>; <code>nurse_availability_slots</code> · <code>nurse_availability_exceptions</code><strong>[MVP]</strong></p>
<p><strong>Verification:</strong> <code>nurse_verifications</code> · <code>verification_step_types</code> · <code>verification_steps</code> · <code>verification_documents</code> <strong>[CORE]</strong>; <strong><code>nurse_credentials</code></strong> <em>(NEW)</em> <strong>[MVP]</strong></p>
<p><strong>Booking &amp; Scheduling:</strong> <code>booking_requests</code> · <code>bookings</code> · <code>booking_care_instructions</code> · <code>visit_verifications</code> <strong>[CORE]</strong>; <strong><code>booking_sessions</code></strong> <em>(NEW)</em> · <strong><code>cancellation_policies</code></strong> <em>(NEW)</em> <strong>[MVP]</strong></p>
<p><strong>Payments &amp; Ledger:</strong> <code>payment_gateways</code> · <code>payment_transactions</code> · <strong><code>payment_webhook_events</code></strong> <em>(NEW)</em> · <code>refunds</code> · <strong><code>ledger_entries</code></strong> <em>(NEW)</em> · <strong><code>nurse_clawbacks</code></strong> <em>(NEW)</em> · <code>nurse_payout_batches</code> · <code>nurse_payouts</code> · <code>nurse_payout_booking_links</code> <strong>[CORE]</strong>; <strong><code>invoices</code></strong> <em>(NEW)</em> <strong>[MVP]</strong></p>
<p><strong>BNPL:</strong> <strong><code>bnpl_transactions</code></strong> <em>(NEW — replaces <code>installment_plans</code>)</em> <strong>[MVP]</strong>; <del><code>installment_plans</code></del> · <del><code>installment_entries</code></del> <strong>CUT</strong>; <code>bnpl_settlement_entries</code> <strong>[DEFERRED]</strong></p>
<p><strong>Messaging:</strong> <code>tickets</code> · <code>ticket_participants</code> · <code>ticket_messages</code> <strong>[CORE]</strong></p>
<p><strong>Reviews &amp; Records:</strong> <code>reviews</code> <strong>[CORE]</strong>; <code>review_tags_master</code> · <code>review_tag_links</code> · <code>patient_care_records</code> <strong>[MVP]</strong></p>
<p><strong>Notifications:</strong> <code>notifications</code> · <code>support_alerts</code> <strong>[CORE]</strong></p>
<p><strong>Audit &amp; Config:</strong> <code>audit_logs</code> <strong>[CORE]</strong> · <code>system_events</code> <strong>[MVP]</strong> · <code>platform_configs</code> <strong>[CORE]</strong> · <strong><code>iranian_holidays</code></strong> <em>(NEW)</em> <strong>[MVP]</strong></p>
<p><strong>Partner / Launch:</strong> <strong><code>partner_centers</code></strong> <em>(NEW)</em> <strong>[MVP]</strong></p>
<p><strong>Future (modeled, inactive):</strong> <code>organizations</code> · <code>organization_nurses</code> · <code>fraud_flags</code> · <code>recurring_booking_schedules</code> · <code>bnpl_settlement_entries</code> — all <strong>[DEFERRED]</strong></p>
<p><strong>Net change vs the original 45:</strong> 2 cut (<code>installment_plans</code> replaced, <code>installment_entries</code> removed), +10 added (<code>ledger_entries</code>, <code>nurse_clawbacks</code>, <code>payment_webhook_events</code>, <code>nurse_search_index</code>, <code>booking_sessions</code>, <code>cancellation_policies</code>, <code>invoices</code>, <code>partner_centers</code>, <code>nurse_credentials</code>, <code>iranian_holidays</code>), 1 replaced (<code>bnpl_transactions</code>). 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.</p>
<hr>
<h2 id="key-design-decisions-the-reasoning-in-one-place">Key Design Decisions (the reasoning, in one place) <a class="anchor" href="#key-design-decisions-the-reasoning-in-one-place" aria-hidden="true">#</a></h2>
<ol>
<li><strong>Escrow as a ledger state, not platform cash</strong> — 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 <strong>verified, ownership-checked</strong> IBANs.</li>
</ol>
<ol>
<li><strong>A BNPL order is a net-of-fee inbound payment, full stop</strong> — the verified full-upfront settlement model means there is no customer receivable, no default risk, and no installment schedule for Balinyaar to track. Deleting <code>installment_entries</code> removed an entire fragile subsystem and replaced it with one reconciliation row.</li>
</ol>
<ol>
<li><strong>Three separate money amounts</strong> 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.</li>
</ol>
<ol>
<li><strong>Double-entry over status flags</strong> — 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 <code>nurse_clawbacks</code> receivable fixes both and makes bank/Shaparak reconciliation possible.</li>
</ol>
<ol>
<li><strong>Dispute window gates payout</strong> — preferring a <em>holding period</em> over a <em>clawback</em>, because clawback against an already-paid nurse IBAN is largely unenforceable. The clawback path exists for the cases that slip through.</li>
</ol>
<ol>
<li><strong>Idempotency before money</strong><code>payment_webhook_events</code> 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).</li>
</ol>
<ol>
<li><strong>Multi-session engagements are the norm, not an edge case</strong><code>booking_sessions</code> 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.</li>
</ol>
<ol>
<li><strong>Partner center is launch-critical</strong> — it is the legal vehicle and likely merchant-of-record; without it the recommended go-to-market and the money flow are not representable.</li>
</ol>
<ol>
<li><strong>Verified-trust must be queryable</strong><code>nurse_credentials</code> turns the brand promise into renewal alerts, a real badge, and audit defensibility, surviving the future arrival of an INO/MoH API.</li>
</ol>
<ol>
<li><strong>Keep the configurable service EAV; cut the analytics scaffolding</strong> — the category/option model earns its complexity (admin-extensible pricing dimensions without migrations); <code>response_rate</code>/<code>profile_completion_score</code>/<code>system_events</code>-in-SQL do not, at launch.</li>
</ol>
<hr>
<h2 id="open-items-to-confirm-before-building-not-schema-blockers">Open items to confirm before building (not schema blockers) <a class="anchor" href="#open-items-to-confirm-before-building-not-schema-blockers" aria-hidden="true">#</a></h2>
<ul>
<li><strong>BNPL provider contract:</strong> 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 <strong>one lump to Balinyaar/the center, internal allocation to nurses</strong>, so this is an ops confirmation, not a schema dependency.</li>
<li><strong>Commission %</strong> and <strong>settlement SLA</strong> per provider (and whether the provider returns its commission on a refund — full or pro-rata).</li>
<li><strong>PSP/تسهیم provider</strong> 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.</li>
<li><strong>VAT exemption</strong> ruling on the nursing service itself (the commission line is taxable regardless) — <code>vat_rate</code> is config-driven so either ruling is a value change.</li>
<li><strong>مودیان</strong> enrollment thresholds for the platform and high-earning nurses.</li>
</ul>
<blockquote><p>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 <a href="../payments/index.html">payments deep-dive</a> for the full source-cited analysis.</p>
</blockquote>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
<script>
(function(){var k='balinyaar-docs-theme';var s=localStorage.getItem(k);
if(s)document.documentElement.setAttribute('data-theme',s);
else if(matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.setAttribute('data-theme','dark');})();
function __t(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';
d.setAttribute('data-theme',n);localStorage.setItem('balinyaar-docs-theme',n);}
</script>
</body>
</html>
+196
View File
@@ -0,0 +1,196 @@
# 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.