Files
baya-monorepo/product/data-model/01-identity-and-access.html
2026-06-24 01:32:46 +03:30

78 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>