Files
baya-monorepo/product/database-model.md
T
2026-06-18 01:42:37 +03:30

67 KiB
Raw Blame History

Balinyaar — Database Model

Platform Summary

Balinyaar is a hybrid home nursing marketplace in Iran. Independent nurses (and in future, nursing company employees) register, list configurable services with their own pricing, and undergo a multi-step verification pipeline. Families search, pick a nurse and service variant, submit a booking request, and pay through the platform after the nurse accepts. The platform holds payment in escrow and releases weekly payouts to nurses after service completion is confirmed via EVV. All post-booking communication runs through a ticket system inspectable by admins.


Design Principles

  1. Monetary values stored as BIGINT in Iranian Rials (IRR). Toman conversion is a display concern only.
  2. PII fields (national ID, IBAN, phone, addresses, clinical data) are marked (encrypted) — column-level or application-level encryption; actual mechanism is implementation-specific.
  3. Soft deletes on users and nurse_profiles via deleted_at. Audit and payment records are never deleted.
  4. Audit trail is append-only. All state transitions on bookings, payments, verifications, and reviews produce a row in audit_logs.
  5. Enum-like fields use NVARCHAR(50) with application-enforced constraints. Admin-managed catalog tables (ServiceCategory, VerificationStepType) are rows, not enums — they can be extended without schema changes.
  6. Platform fee is captured at booking time and never derived from current config, so historical records remain accurate after fee changes.
  7. Snapshot fields (e.g., variant_snapshot_json, address_snapshot_json on bookings) protect historical accuracy from future edits to linked records.
  8. Multi-language — all admin-managed catalog tables carry name_fa / name_en pairs.
  9. All timestamps are DATETIME2(7) in UTC. Persian calendar display is a UI concern.
  10. String fields use NVARCHAR throughout to support Persian/Arabic characters.

Entity Catalog


Domain 1 — Identity & Access


users

The single identity record for every human actor on the platform: nurses, customers, and admin staff. Role determines which profile sub-table is populated. Phone number is the primary login credential (OTP); email is secondary. National ID is populated only after the KYC step completes.

Field Type Notes
id BIGINT PK IDENTITY
email NVARCHAR(255) UNIQUE NULL (encrypted) — optional at registration; required for admin accounts
phone NVARCHAR(20) UNIQUE NOT NULL (encrypted) — used for OTP login and Shahkar matching
national_id NVARCHAR(20) UNIQUE NULL (encrypted) — populated after KYC identity step passes
national_id_verified_at DATETIME2 NULL
first_name NVARCHAR(100) NOT NULL
last_name NVARCHAR(100) NOT NULL
avatar_url NVARCHAR(512) NULL Object-storage URL
role NVARCHAR(20) NOT NULL nurse / customer / admin
is_active BIT NOT NULL DEFAULT 1 False = account suspended (not deleted)
email_verified_at DATETIME2 NULL
phone_verified_at DATETIME2 NULL Set on first successful OTP
last_login_at DATETIME2 NULL
last_login_ip NVARCHAR(45) NULL IPv4 or IPv6
preferred_language NVARCHAR(5) NOT NULL DEFAULT 'fa' fa / en
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL
deleted_at DATETIME2 NULL Soft delete — NULL = active

user_sessions

Auth session records for refresh token management. Each login creates a session. Logging out or detecting a stolen token invalidates the session.

Field Type Notes
id BIGINT PK IDENTITY
user_id BIGINT FK → users NOT NULL
refresh_token_hash NVARCHAR(64) NOT NULL SHA-256 of the issued refresh token
device_info NVARCHAR(500) NULL User-agent string
ip_address NVARCHAR(45) NULL
is_revoked BIT NOT NULL DEFAULT 0
revoked_at DATETIME2 NULL
expires_at DATETIME2 NOT NULL
created_at DATETIME2 NOT NULL

roles

RBAC roles for admin staff. Nurses and customers do not use this table — their permissions are determined by the users.role field.

Field Type Notes
id BIGINT PK IDENTITY
name NVARCHAR(50) UNIQUE NOT NULL super_admin / admin / support / finance / moderator
description NVARCHAR(500) NULL
created_at DATETIME2 NOT NULL

user_roles

Assignment of admin roles to admin users. A user may hold multiple roles. Revoked roles retain history.

Field Type Notes
id BIGINT PK IDENTITY
user_id BIGINT FK → users NOT NULL Must be a user with role=admin
role_id BIGINT FK → roles NOT NULL
granted_by_user_id BIGINT FK → users NULL
granted_at DATETIME2 NOT NULL
revoked_at DATETIME2 NULL NULL = still active

nurse_profiles

Extended data for users with role = nurse. Separated from users to avoid a bloated base table. Denormalized aggregates (average_rating, total_completed_bookings) are maintained on every review publication and booking completion — never calculated on-the-fly in search queries.

Field Type Notes
id BIGINT PK IDENTITY
user_id BIGINT FK → users UNIQUE NOT NULL 1:1
bio NVARCHAR(2000) NULL Displayed on nurse profile page
years_of_experience SMALLINT NULL
education_level NVARCHAR(50) NULL bachelor / master / phd / associate
education_field NVARCHAR(200) NULL e.g., "پرستاری — دانشگاه تهران"
specializations_json NVARCHAR(MAX) NULL JSON array of specialization tag strings
is_verified BIT NOT NULL DEFAULT 0 True only when all required verification steps pass
verification_status NVARCHAR(30) NOT NULL DEFAULT 'not_started' not_started / pending / in_review / approved / rejected / suspended
is_accepting_bookings BIT NOT NULL DEFAULT 0 Nurse can toggle without losing verified status
average_rating DECIMAL(3,2) NULL Denormalized; recalculated on each published review
total_reviews INT NOT NULL DEFAULT 0 Denormalized count of published reviews
total_completed_bookings INT NOT NULL DEFAULT 0 Denormalized
response_rate DECIMAL(5,2) NULL % of booking requests responded to before expiry
avg_response_time_hours DECIMAL(5,2) NULL Rolling average for display on profile
profile_completion_score SMALLINT NOT NULL DEFAULT 0 0100, shown in backoffice to surface incomplete profiles
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL
deleted_at DATETIME2 NULL Soft delete

customer_profiles

Extended profile for users with role = customer. Intentionally lightweight — most customer data lives in their bookings and patients.

Field Type Notes
id BIGINT PK IDENTITY
user_id BIGINT FK → users UNIQUE NOT NULL 1:1
national_id_verified_at DATETIME2 NULL Anti-fraud KYC for high-value customers (optional at launch)
default_emergency_contact_name NVARCHAR(200) NULL (encrypted) — overridable per booking
default_emergency_contact_phone NVARCHAR(20) NULL (encrypted)
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

patients

The person receiving care. Fully separated from the customer because the payer (adult child, spouse) is often not the patient (elderly parent, newborn, post-surgical adult). A customer may register multiple patients. Patient medical data is encrypted at rest.

Field Type Notes
id BIGINT PK IDENTITY
customer_id BIGINT FK → customer_profiles NOT NULL Record owner
display_name NVARCHAR(200) NOT NULL e.g., "پدر", "مادر بزرگ", "نوزاد"
first_name NVARCHAR(100) NULL
last_name NVARCHAR(100) NULL
birth_date DATE NULL
gender NVARCHAR(10) NULL male / female
blood_type NVARCHAR(5) NULL A+, A-, B+, B-, O+, O-, AB+, AB-
initial_medical_notes NVARCHAR(MAX) NULL (encrypted) — baseline context set by the family
is_active BIT NOT NULL DEFAULT 1 Soft-archivable
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

customer_addresses

Saved service locations for a customer. Used in booking requests to tell the nurse where to go. The full address is encrypted. Coordinates are stored for EVV distance matching at check-in.

Field Type Notes
id BIGINT PK IDENTITY
customer_id BIGINT FK → customer_profiles NOT NULL
title NVARCHAR(100) NOT NULL e.g., "خانه", "خانه‌ی پدری"
address_line1 NVARCHAR(500) NOT NULL (encrypted)
address_line2 NVARCHAR(500) NULL (encrypted)
city_id BIGINT FK → cities NOT NULL
district_id BIGINT FK → districts NULL
postal_code NVARCHAR(20) NULL
latitude DECIMAL(10,7) NULL For EVV match tolerance check
longitude DECIMAL(10,7) NULL
access_notes NVARCHAR(500) NULL (encrypted) — door code, building instructions, unit number
is_primary BIT NOT NULL DEFAULT 0 One primary per customer
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

nurse_bank_accounts

Banking details for nurse payout. IBAN (Sheba) is encrypted. Multiple accounts allowed; exactly one marked primary receives weekly payouts. Verified by admin before first payout.

Field Type Notes
id BIGINT PK IDENTITY
nurse_id BIGINT FK → nurse_profiles NOT NULL
bank_name NVARCHAR(100) NOT NULL
account_holder_name NVARCHAR(200) NOT NULL (encrypted)
iban NVARCHAR(34) NOT NULL (encrypted) — IR + 24 digits (Sheba)
is_primary BIT NOT NULL DEFAULT 0
is_verified BIT NOT NULL DEFAULT 0 Admin confirmed the account exists and matches nurse identity
verified_by_admin_id BIGINT FK → users NULL
verified_at DATETIME2 NULL
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

Domain 2 — Geographic Data


provinces

Field Type Notes
id BIGINT PK IDENTITY
name_fa NVARCHAR(100) NOT NULL
name_en NVARCHAR(100) NOT NULL
is_active BIT NOT NULL DEFAULT 1

cities

Field Type Notes
id BIGINT PK IDENTITY
province_id BIGINT FK → provinces NOT NULL
name_fa NVARCHAR(100) NOT NULL
name_en NVARCHAR(100) NOT NULL
is_active BIT NOT NULL DEFAULT 1
sort_order SMALLINT NOT NULL DEFAULT 0 For ordered dropdowns

districts

Sub-city units (محله / منطقه). Granularity varies: in Tehran these map to the 22 official municipal districts; in smaller cities they may be major neighborhoods. Districts are optional — nurses can cover a whole city without specifying districts.

Field Type Notes
id BIGINT PK IDENTITY
city_id BIGINT FK → cities NOT NULL
name_fa NVARCHAR(200) NOT NULL
name_en NVARCHAR(200) NULL
is_active BIT NOT NULL DEFAULT 1

nurse_service_areas

Defines where a nurse is willing to travel. A nurse can cover multiple cities and optionally specific districts within each. A row with district_id = NULL means the nurse covers the entire city. This table drives the geographic filter in nurse search.

Field Type Notes
id BIGINT PK IDENTITY
nurse_id BIGINT FK → nurse_profiles NOT NULL
city_id BIGINT FK → cities NOT NULL
district_id BIGINT FK → districts NULL NULL = entire city
created_at DATETIME2 NOT NULL

Index: (nurse_id, city_id, district_id) UNIQUE — prevents duplicate area declarations.


Domain 3 — Nurse Services & Pricing

The service model has three admin-defined layers (category → option groups → option values) and two nurse-defined layers (variant → variant options). This design lets admins add new configurable dimensions (e.g., "shift length", "number of patients") without schema changes, and lets each nurse price every combination they choose to offer independently.


service_categories

Admin-managed top-level care types. These are the primary search dimension for customers.

Examples: مراقبت از سالمند (Elderly Care), مراقبت پس از جراحی (Post-Surgery Recovery), مراقبت از نوزاد (Infant/Newborn Care), مدیریت بیماری مزمن (Chronic Illness Management), همراهی و کمک روزمره (Companionship/Daily Living — future tier).

Field Type Notes
id BIGINT PK IDENTITY
name_fa NVARCHAR(200) NOT NULL
name_en NVARCHAR(200) NOT NULL
description_fa NVARCHAR(1000) NULL Shown to customers in search
icon_url NVARCHAR(512) NULL
is_active BIT NOT NULL DEFAULT 1
sort_order SMALLINT NOT NULL DEFAULT 0
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

service_option_groups

Admin-defined configurable dimensions per category. Each group represents one axis the nurse can configure. A NULL service_category_id means the option group applies across all categories (e.g., shift type).

Field Type Notes
id BIGINT PK IDENTITY
service_category_id BIGINT FK → service_categories NULL NULL = cross-category
name_fa NVARCHAR(200) NOT NULL e.g., "تعداد بیمار", "نوع شیفت"
name_en NVARCHAR(200) NOT NULL
description_fa NVARCHAR(500) NULL Instructions shown to nurse when setting up a listing
is_required BIT NOT NULL DEFAULT 1 If true, nurse must select a value from this group per variant
sort_order SMALLINT NOT NULL DEFAULT 0
is_active BIT NOT NULL DEFAULT 1

service_option_values

Concrete choices within an option group. The nurse selects from these when creating a variant.

Field Type Notes
id BIGINT PK IDENTITY
option_group_id BIGINT FK → service_option_groups NOT NULL
label_fa NVARCHAR(200) NOT NULL e.g., "۱ نفر", "۲ نفر", "شبانه‌روزی"
label_en NVARCHAR(200) NULL
sort_order SMALLINT NOT NULL DEFAULT 0
is_active BIT NOT NULL DEFAULT 1

nurse_service_variants

The atomic bookable unit on the platform. Each variant is a specific offering by a nurse: a service category with a particular combination of option values, at a specific price. These rows are indexed for search and displayed on the nurse's public profile. A nurse may have many variants per category (one per combination they choose to offer).

Example: Nurse A → Elderly Care → 2 patients → Full-day shift → 3,200,000 IRR per day

The display_name is auto-generated from the selected option labels but can be customized by the nurse. The price_unit determines how the price is shown (per hour, per session, per day, etc.). For hourly variants, estimated_duration_hours gives the minimum or typical commitment so customers can estimate total cost.

Field Type Notes
id BIGINT PK IDENTITY
nurse_id BIGINT FK → nurse_profiles NOT NULL
service_category_id BIGINT FK → service_categories NOT NULL
display_name NVARCHAR(300) NOT NULL Shown on profile and search results
price BIGINT NOT NULL In IRR. Never null — every variant must have a price.
price_unit NVARCHAR(20) NOT NULL per_hour / per_session / per_half_day / per_day / per_24h
estimated_duration_hours DECIMAL(5,2) NULL For hourly variants, helps customer estimate total cost
description NVARCHAR(1000) NULL Nurse's optional notes about this specific variant
is_active BIT NOT NULL DEFAULT 1 Nurse can deactivate without deleting; deactivated variants cannot be booked
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

nurse_service_variant_options

Records which option values apply to a variant. Together, these rows define the full configuration of a variant. A variant covering two option groups (e.g., patient count + shift type) will have two rows here.

Field Type Notes
id BIGINT PK IDENTITY
variant_id BIGINT FK → nurse_service_variants NOT NULL
option_group_id BIGINT FK → service_option_groups NOT NULL
option_value_id BIGINT FK → service_option_values NOT NULL

Constraint: (variant_id, option_group_id) UNIQUE — one value per dimension per variant.


nurse_availability_slots

Recurring weekly availability windows declared by the nurse. These inform the customer's search (e.g., "available Saturdays 8am6pm") but are soft constraints — the nurse still accepts or rejects each individual booking request. They do not block booking requests for times outside these slots; they are guidance only.

Field Type Notes
id BIGINT PK IDENTITY
nurse_id BIGINT FK → nurse_profiles NOT NULL
day_of_week TINYINT NOT NULL 0=Saturday, 1=Sunday, 2=Monday, 3=Tuesday, 4=Wednesday, 5=Thursday, 6=Friday (Shamsi week)
start_time TIME NOT NULL
end_time TIME NOT NULL
is_active BIT NOT NULL DEFAULT 1
created_at DATETIME2 NOT NULL

nurse_availability_exceptions

Date-specific overrides to the recurring weekly schedule. Used for vacations, public holidays the nurse wants to block, or one-time extra availability on a normally-off day.

Field Type Notes
id BIGINT PK IDENTITY
nurse_id BIGINT FK → nurse_profiles NOT NULL
exception_date DATE NOT NULL
is_available BIT NOT NULL False = day blocked; True = day open (even if absent from weekly slots)
reason NVARCHAR(200) NULL e.g., "تعطیلات عید نوروز"

Constraint: (nurse_id, exception_date) UNIQUE.


Domain 4 — Nurse Verification

The verification pipeline is a sequence of steps. Each step type is admin-configurable (new steps can be added as rows, no code change). Steps can be automated (KYC API call) or manual (admin reviews uploaded document). The overall nurse_verifications record aggregates the step outcomes into a single status.


nurse_verifications

Master verification record for a nurse — one row per nurse, created when the nurse first submits for verification. Every status transition here is captured in audit_logs. When all required verification_steps reach passed, this record's status moves to approved and nurse_profiles.is_verified flips to true.

Field Type Notes
id BIGINT PK IDENTITY
nurse_id BIGINT FK → nurse_profiles UNIQUE NOT NULL 1:1
status NVARCHAR(30) NOT NULL DEFAULT 'not_started' not_started / pending / in_review / approved / rejected / suspended
submitted_at DATETIME2 NULL First submission timestamp
approved_at DATETIME2 NULL
rejected_at DATETIME2 NULL
suspended_at DATETIME2 NULL
rejection_reason NVARCHAR(1000) NULL Shown to the nurse in the app
reviewed_by_admin_id BIGINT FK → users NULL Last admin to act on this record
internal_notes NVARCHAR(2000) NULL Admin-only; not shown to nurse
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

verification_step_types

Admin-configurable catalog of what steps exist in the pipeline. Adding a new step type (e.g., "professional liability insurance") requires only inserting a row here — no code change needed. The code is a stable machine key referenced by application logic for automated steps.

Field Type Notes
id BIGINT PK IDENTITY
code NVARCHAR(100) UNIQUE NOT NULL e.g., identity_kyc, moh_license, ino_membership, criminal_record, bank_account_verification
name_fa NVARCHAR(200) NOT NULL Shown in backoffice
name_en NVARCHAR(200) NOT NULL
description_fa NVARCHAR(500) NULL Instructions shown to nurse for this step
is_required BIT NOT NULL DEFAULT 1 Non-required steps are advisory; they don't block approval
is_automated BIT NOT NULL DEFAULT 0 If true, platform calls an external API for this step
automation_provider NVARCHAR(100) NULL e.g., finnotech, u_id, jibbit, manual
requires_document_upload BIT NOT NULL DEFAULT 0 If true, nurse must upload at least one document
sort_order SMALLINT NOT NULL DEFAULT 0 Processing order shown to nurse
is_active BIT NOT NULL DEFAULT 1
created_at DATETIME2 NOT NULL

verification_steps

One row per step per nurse. Tracks individual step status as the nurse moves through the pipeline. The external_response_json stores the raw API response for automated steps — critical for dispute and compliance audit. For manual steps, reviewed_by_admin_id and review_notes capture who reviewed and what they decided.

expires_at supports time-limited steps: the criminal record certificate (گواهی عدم سوء پیشینه) is valid for a limited period; when it expires, the step reverts to pending and a support alert is raised.

Field Type Notes
id BIGINT PK IDENTITY
nurse_verification_id BIGINT FK → nurse_verifications NOT NULL
step_type_id BIGINT FK → verification_step_types NOT NULL
status NVARCHAR(30) NOT NULL DEFAULT 'pending' pending / submitted / processing / passed / failed / expired
is_automated BIT NOT NULL Snapshot from step_type at creation; stored so historical records survive step_type edits
external_provider NVARCHAR(100) NULL KYC vendor that processed this step
external_reference_id NVARCHAR(200) NULL Provider's session/transaction ID
external_response_json NVARCHAR(MAX) NULL Full API response for audit
reviewed_by_admin_id BIGINT FK → users NULL NULL for automated steps
review_notes NVARCHAR(1000) NULL Admin rationale for pass/fail decision
submitted_at DATETIME2 NULL When nurse submitted this step
completed_at DATETIME2 NULL When step reached terminal state
expires_at DATETIME2 NULL For time-limited steps (e.g., criminal record cert)
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

Constraint: (nurse_verification_id, step_type_id) UNIQUE.


verification_documents

Documents uploaded as evidence for a verification step. File contents live in object storage (S3-compatible). This table holds the metadata, reference key, and integrity hash. Access to the actual file must be through signed/time-limited URLs — never a direct public URL.

Field Type Notes
id BIGINT PK IDENTITY
verification_step_id BIGINT FK → verification_steps NOT NULL
document_type NVARCHAR(50) NOT NULL national_id_front / national_id_back / moh_license / ino_certificate / criminal_record / selfie / liveness_video
storage_key NVARCHAR(512) NOT NULL Object-storage path — access-controlled, no public URL
file_hash NVARCHAR(64) NOT NULL SHA-256 of raw file bytes for integrity verification
file_size_bytes INT NULL
mime_type NVARCHAR(100) NULL
is_valid BIT NULL NULL = pending review; True/False after admin assessment
uploaded_at DATETIME2 NOT NULL

Domain 5 — Booking & Scheduling

The booking lifecycle has two distinct phases: the request phase (before payment) and the booking phase (after payment). Separating them into two tables keeps each table focused and makes lifecycle transitions explicit. A booking only exists if payment succeeded.

Request lifecycle: pending_nurse_response → nurse accepts → accepted_awaiting_payment → payment received → converted (Booking created) pending_nurse_response → nurse rejects → rejected_by_nurse pending_nurse_response → deadline passes → expired_no_response accepted_awaiting_payment → 30-min payment window passes → payment_deadline_expired Any pre-payment state → customer cancels → cancelled_by_customer

Booking lifecycle: pending_payment → payment captured → confirmed confirmed → nurse checks in → in_progress in_progress → nurse checks out → completed confirmed → cancelled before service → cancelled completed → disputed → disputed disputed → resolved → closed


booking_requests

A customer's intent to book a nurse for a specific service, date, and time slot. Created when the customer submits a request. Multiple requests can exist for the same customer/nurse pair (e.g., first one rejected, a new one submitted later). No money is involved at this stage.

The nurse_response_deadline_at is computed from platform_configs.nurse_response_deadline_hours at creation time and stored so it is immune to config changes. Similarly, payment_deadline_at is set when the nurse accepts and reflects the 30-minute window from that moment.

Field Type Notes
id BIGINT PK IDENTITY
customer_id BIGINT FK → customer_profiles NOT NULL
nurse_id BIGINT FK → nurse_profiles NOT NULL
patient_id BIGINT FK → patients NOT NULL
variant_id BIGINT FK → nurse_service_variants NOT NULL The specific service + options the customer wants
customer_address_id BIGINT FK → customer_addresses NOT NULL
requested_date DATE NOT NULL
requested_time_start TIME NOT NULL
requested_time_end TIME NOT NULL
customer_notes NVARCHAR(1000) NULL Additional context the customer sends with the request (visible to nurse)
status NVARCHAR(50) NOT NULL DEFAULT 'pending_nurse_response' See lifecycle above
nurse_response_deadline_at DATETIME2 NOT NULL After this, job transitions to expired_no_response automatically
payment_deadline_at DATETIME2 NULL Set when nurse accepts; 30-minute countdown starts
nurse_rejection_reason NVARCHAR(500) NULL Optional message from nurse on rejection
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

bookings

Confirmed service engagement. Created when nurse accepts AND payment is successfully captured within the deadline. This is the source of truth for the actual service event. The variant_snapshot_json and address_snapshot_json freeze the service details and address at booking time, protecting historical records from future edits to those referenced entities.

The fee fields capture the exact split at the moment of booking: gross_amount (what the customer pays), platform_fee_amount (platform's share), and nurse_payout_amount (what the nurse will receive). The platform_fee_rate is stored so auditors can reconstruct how the split was calculated even after the fee schedule changes.

Field Type Notes
id BIGINT PK IDENTITY
booking_request_id BIGINT FK → booking_requests UNIQUE NOT NULL 1:1 — the request that created this booking
customer_id BIGINT FK → customer_profiles NOT NULL Denormalized for query performance
nurse_id BIGINT FK → nurse_profiles NOT NULL
patient_id BIGINT FK → patients NOT NULL
variant_id BIGINT FK → nurse_service_variants NOT NULL Reference — see snapshot below for historical accuracy
variant_snapshot_json NVARCHAR(MAX) NOT NULL Full JSON of variant + option labels at booking time
customer_address_id BIGINT FK → customer_addresses NOT NULL
address_snapshot_json NVARCHAR(MAX) NOT NULL (encrypted) — full address at booking time
scheduled_date DATE NOT NULL
scheduled_time_start TIME NOT NULL
scheduled_time_end TIME NOT NULL
gross_amount BIGINT NOT NULL Total charged to customer (IRR)
platform_fee_amount BIGINT NOT NULL Platform's cut
platform_fee_rate DECIMAL(5,4) NOT NULL e.g., 0.1500 = 15% — captured at booking time
nurse_payout_amount BIGINT NOT NULL gross_amount - platform_fee_amount
status NVARCHAR(30) NOT NULL DEFAULT 'pending_payment' See lifecycle above
confirmed_at DATETIME2 NULL When payment was captured
cancelled_at DATETIME2 NULL
cancellation_reason NVARCHAR(500) NULL
cancelled_by NVARCHAR(20) NULL customer / nurse / admin / system
completed_at DATETIME2 NULL When booking status moved to completed
payout_released BIT NOT NULL DEFAULT 0 True when this booking is included in a processed payout batch
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

booking_care_instructions

Clinical and logistical context provided by the family at booking time. Separated from bookings to keep the financial/scheduling table clean and because this data has stricter access controls (encrypted, visible only to the assigned nurse and admin). The nurse reads this before the visit.

Field Type Notes
id BIGINT PK IDENTITY
booking_id BIGINT FK → bookings UNIQUE NOT NULL 1:1
current_conditions NVARCHAR(MAX) NULL (encrypted) — active diagnoses
current_medications NVARCHAR(MAX) NULL (encrypted) — drug names, doses, schedule
allergies NVARCHAR(1000) NULL (encrypted)
special_instructions NVARCHAR(2000) NULL (encrypted) — care protocol, family preferences
emergency_contact_name NVARCHAR(200) NULL (encrypted)
emergency_contact_phone NVARCHAR(20) NULL (encrypted)
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

visit_verifications

Electronic Visit Verification (EVV). The nurse clocks in and out via the app; GPS coordinates and timestamps are recorded. This is the authoritative record of whether a visit occurred and for how long. It is required for payout release. If a nurse does not check in by a configurable threshold after the scheduled start time, a support_alerts record is created and the family is notified.

check_in_address_match is a computed boolean: did the nurse's GPS at check-in fall within an acceptable radius of the booking address? This flag is advisory for admin — a mismatch triggers a review but does not automatically cancel the booking.

Field Type Notes
id BIGINT PK IDENTITY
booking_id BIGINT FK → bookings UNIQUE NOT NULL 1:1
check_in_at DATETIME2 NULL
check_in_latitude DECIMAL(10,7) NULL
check_in_longitude DECIMAL(10,7) NULL
check_in_address_match BIT NULL Did GPS match booking address within tolerance?
check_out_at DATETIME2 NULL
check_out_latitude DECIMAL(10,7) NULL
check_out_longitude DECIMAL(10,7) NULL
actual_duration_minutes INT NULL Computed: check_out_at - check_in_at
status NVARCHAR(20) NOT NULL DEFAULT 'pending' pending / checked_in / completed / missed / disputed
nurse_checkout_notes NVARCHAR(500) NULL Optional note from nurse at checkout
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

Domain 6 — Payments


payment_gateways

Configuration for each PSP connected to the platform. Credentials are encrypted. Multiple gateways may be active simultaneously (e.g., standard IPG for direct payment, a BNPL gateway for installments, a failover gateway). The type field determines which payment flow applies.

Field Type Notes
id BIGINT PK IDENTITY
code NVARCHAR(50) UNIQUE NOT NULL e.g., zarinpal, idpay, payping, bnpl_hamrah, bnpl_toranj
name NVARCHAR(100) NOT NULL Display name
type NVARCHAR(30) NOT NULL standard / bnpl
is_active BIT NOT NULL DEFAULT 1
config_json NVARCHAR(MAX) NULL (encrypted) — merchant ID, API keys, webhook secret, callback URLs
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

payment_transactions

Records every payment attempt against a booking. A booking may have multiple rows here (failed first attempt, successful retry). The row where status = 'succeeded' is the one that triggers booking confirmation. The gateway_response_json stores the full PSP response — essential for dispute, chargebacks, and Shaparak reconciliation.

ip_address and user_agent at payment time are retained for fraud detection and dispute evidence.

Field Type Notes
id BIGINT PK IDENTITY
booking_id BIGINT FK → bookings NOT NULL
customer_id BIGINT FK → customer_profiles NOT NULL
gateway_id BIGINT FK → payment_gateways NOT NULL
amount BIGINT NOT NULL Amount charged in this transaction (IRR)
currency NVARCHAR(5) NOT NULL DEFAULT 'IRR'
status NVARCHAR(30) NOT NULL DEFAULT 'initiated' initiated / pending / succeeded / failed / cancelled / refunded / partially_refunded
gateway_transaction_id NVARCHAR(200) NULL PSP's transaction identifier
gateway_reference_code NVARCHAR(200) NULL Shaparak reference code — the definitive payment proof
gateway_response_code NVARCHAR(50) NULL PSP result code
gateway_response_json NVARCHAR(MAX) NULL Full PSP response
is_installment BIT NOT NULL DEFAULT 0 True if funded via a BNPL plan
initiated_at DATETIME2 NOT NULL When the payment flow was started
paid_at DATETIME2 NULL When PSP confirmed success
failed_at DATETIME2 NULL
failure_reason NVARCHAR(500) NULL Human-readable from PSP
ip_address NVARCHAR(45) NULL Customer's IP at payment time
user_agent NVARCHAR(500) NULL
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

refunds

Refund requests and their resolution. Refunds are always admin-initiated — there is no customer self-service refund. They are typically triggered by a family request communicated through a support ticket. Default refund is 100% of the payment; admin may set a partial amount (e.g., cancellation fee deducted).

The ticket_id links to the support conversation where the refund was discussed, providing full context for dispute evidence.

Field Type Notes
id BIGINT PK IDENTITY
payment_transaction_id BIGINT FK → payment_transactions NOT NULL The original successful payment being refunded
booking_id BIGINT FK → bookings NOT NULL
requested_by_customer_id BIGINT FK → customer_profiles NOT NULL
ticket_id BIGINT FK → tickets NULL Support conversation that led to this refund
amount BIGINT NOT NULL Refund amount (IRR). Default = full gross_amount.
refund_percentage DECIMAL(5,2) NOT NULL For audit display (e.g., 100.00, 50.00)
reason_category NVARCHAR(50) NOT NULL nurse_no_show / quality_complaint / scheduling_error / customer_cancellation / platform_error / other
reason_notes NVARCHAR(1000) NULL
status NVARCHAR(30) NOT NULL DEFAULT 'pending' pending / approved / rejected / processing / completed / failed
approved_by_admin_id BIGINT FK → users NULL
approved_at DATETIME2 NULL
rejected_by_admin_id BIGINT FK → users NULL
rejected_at DATETIME2 NULL
rejection_reason NVARCHAR(500) NULL Communicated to customer
gateway_refund_reference NVARCHAR(200) NULL PSP's refund transaction ID
processed_at DATETIME2 NULL When PSP confirmed refund success
admin_notes NVARCHAR(1000) NULL Internal only — not shown to customer
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

nurse_payout_batches

Weekly aggregation of all amounts owed to nurses for completed, unpaid bookings. Admin initiates a batch (manually or via a scheduled job). Each batch covers a specific period and produces one nurse_payouts row per nurse with earnings in that window.

Field Type Notes
id BIGINT PK IDENTITY
period_start DATE NOT NULL
period_end DATE NOT NULL
total_amount BIGINT NOT NULL Sum of all nurse_payout_amount in this batch
payout_count INT NOT NULL Number of individual nurse payouts in this batch
status NVARCHAR(30) NOT NULL DEFAULT 'draft' draft / processing / completed / partially_failed / failed
initiated_by_admin_id BIGINT FK → users NULL
processed_at DATETIME2 NULL
failure_notes NVARCHAR(1000) NULL
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

nurse_payouts

One row per nurse per batch. Records the exact amount transferred, which bank account was used (snapshot, not FK, because account may change), and the transfer reference for bank reconciliation.

Field Type Notes
id BIGINT PK IDENTITY
batch_id BIGINT FK → nurse_payout_batches NOT NULL
nurse_id BIGINT FK → nurse_profiles NOT NULL
bank_account_id BIGINT FK → nurse_bank_accounts NOT NULL Primary account at time of payout
iban_snapshot NVARCHAR(34) NOT NULL (encrypted) — IBAN at time of payout; in case account is later changed
amount BIGINT NOT NULL Total paid to nurse in this batch (IRR)
booking_count INT NOT NULL How many bookings this covers
status NVARCHAR(30) NOT NULL DEFAULT 'pending' pending / processing / paid / failed
transfer_reference NVARCHAR(200) NULL Bank/ACH reference for reconciliation
paid_at DATETIME2 NULL
failure_reason NVARCHAR(500) NULL
created_at DATETIME2 NOT NULL

Join table linking each payout to the specific bookings it covers. Enables per-booking reconciliation and prevents double-paying the same booking in a future batch.

Field Type Notes
id BIGINT PK IDENTITY
payout_id BIGINT FK → nurse_payouts NOT NULL
booking_id BIGINT FK → bookings UNIQUE NOT NULL UNIQUE ensures a booking can only appear in one payout
amount BIGINT NOT NULL nurse_payout_amount for this booking

Domain 7 — BNPL / Installments

When a customer pays via BNPL, the third-party provider approves the installment plan and pays the platform. The customer repays the BNPL provider directly. From the platform's perspective, the payment_transactions row is succeeded with is_installment = true. The tables below track the BNPL plan details for reconciliation, reporting, and potential future collections escalation.

⚠️ Open decision before implementation: Two BNPL settlement models exist and the choice affects this schema:

  • Immediate full settlement — the provider pays the platform the full booking amount (possibly net of their own fee) in one transfer at approval time. In this case payment_transactions.amount is the booking's gross_amount, and installment_plans.total_amount may be higher (the provider charges the customer more than the booking price to cover their financing fee). The current schema handles this correctly — the two amounts live in separate fields on separate tables.

  • Tranched settlement — the provider pays the platform in installments as the customer pays them. In this case a single payment_transactions row is insufficient; you would need either multiple rows in payment_transactions (one per provider settlement) or a dedicated bnpl_settlement_entries table linked to installment_plans. The current schema does not handle this — it would require an additive migration.

Confirm which model your chosen BNPL provider uses before building the payment integration.


installment_plans

Metadata about a BNPL approval linked to a specific payment transaction.

Field Type Notes
id BIGINT PK IDENTITY
payment_transaction_id BIGINT FK → payment_transactions UNIQUE NOT NULL 1:1
bnpl_provider_code NVARCHAR(50) NOT NULL Matches payment_gateways.code
external_plan_id NVARCHAR(200) NOT NULL Provider's plan identifier
total_amount BIGINT NOT NULL Total the customer owes the provider (IRR), may include provider fee
installment_count TINYINT NOT NULL Number of installments
monthly_fee_rate DECIMAL(5,4) NULL Provider's monthly fee rate
plan_start_date DATE NOT NULL
plan_end_date DATE NOT NULL
status NVARCHAR(30) NOT NULL DEFAULT 'pending_approval' pending_approval / approved / rejected / active / completed / defaulted
approved_at DATETIME2 NULL
last_webhook_at DATETIME2 NULL When last webhook was received from provider
webhook_payload_json NVARCHAR(MAX) NULL Last webhook payload
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

installment_entries

Individual installment records. Updated via webhook from the BNPL provider when a payment occurs or is missed.

Field Type Notes
id BIGINT PK IDENTITY
plan_id BIGINT FK → installment_plans NOT NULL
installment_number TINYINT NOT NULL 1-based
amount BIGINT NOT NULL Amount due for this installment (IRR)
due_date DATE NOT NULL
paid_at DATETIME2 NULL NULL = not yet paid
status NVARCHAR(20) NOT NULL DEFAULT 'upcoming' upcoming / due / paid / overdue / defaulted
external_reference NVARCHAR(200) NULL Provider's reference for this specific installment
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

Domain 8 — Messaging (Ticket System)

There is no live chat and no direct messaging between nurse and customer. All post-booking communication runs through a structured ticket system where admin can read every message. This is intentional: it protects vulnerable patients, creates a paper trail for disputes, and prevents disintermediation. Tickets can also be used by customers or nurses to contact support for any reason.


tickets

A support or communication thread. May be linked to a booking, refund, or stand alone. Admin assigns, responds, and resolves tickets. Customers and nurses communicate through messages on their assigned ticket.

The reference_code is a human-readable identifier (e.g., TKT-2025-001234) communicated to users for support follow-up.

Field Type Notes
id BIGINT PK IDENTITY
reference_code NVARCHAR(30) UNIQUE NOT NULL Human-readable code for support communication
subject NVARCHAR(300) NOT NULL
type NVARCHAR(50) NOT NULL booking_coordination / booking_issue / refund_request / nurse_complaint / review_dispute / verification_question / general
status NVARCHAR(30) NOT NULL DEFAULT 'open' open / pending_customer / pending_nurse / pending_admin / resolved / closed
priority NVARCHAR(20) NOT NULL DEFAULT 'medium' low / medium / high / urgent
booking_id BIGINT FK → bookings NULL Context link — not required
refund_id BIGINT FK → refunds NULL
created_by_user_id BIGINT FK → users NOT NULL Who opened the ticket
assigned_to_admin_id BIGINT FK → users NULL Current admin owner
resolved_at DATETIME2 NULL
closed_at DATETIME2 NULL
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

ticket_participants

Controls who has access to a ticket. Every ticket has at minimum its creator and an admin. Post-booking tickets may include both the customer and the nurse.

Field Type Notes
id BIGINT PK IDENTITY
ticket_id BIGINT FK → tickets NOT NULL
user_id BIGINT FK → users NOT NULL
participant_role NVARCHAR(20) NOT NULL customer / nurse / admin / support
added_at DATETIME2 NOT NULL
removed_at DATETIME2 NULL NULL = still active participant

Constraint: (ticket_id, user_id) UNIQUE.


ticket_messages

Messages within a ticket. is_internal = true messages are admin-only notes — they are never shown to the customer or nurse. This allows admins to coordinate internally without leaving the ticket context.

Field Type Notes
id BIGINT PK IDENTITY
ticket_id BIGINT FK → tickets NOT NULL
sender_user_id BIGINT FK → users NOT NULL
body NVARCHAR(MAX) NOT NULL
is_internal BIT NOT NULL DEFAULT 0 True = admin-only note
attachments_json NVARCHAR(MAX) NULL JSON array of {storage_key, filename, mime_type}
created_at DATETIME2 NOT NULL

Domain 9 — Reviews & Patient Records


review_tags_master

Admin-managed standardized tags for reviews. Allows quantitative aggregation of qualitative feedback (e.g., "% of reviews tagged 'punctual'"). Nurses can later see their most common tags on their profile.

Field Type Notes
id BIGINT PK IDENTITY
name_fa NVARCHAR(100) NOT NULL e.g., "وقت‌شناس", "مهربان", "متخصص", "غیرحرفه‌ای"
name_en NVARCHAR(100) NULL
sentiment NVARCHAR(10) NOT NULL positive / negative / neutral
is_active BIT NOT NULL DEFAULT 1
sort_order SMALLINT NOT NULL DEFAULT 0

reviews

Post-booking review by the customer about the nurse. Only one review per booking. Submitted reviews enter pending_moderation and are not shown publicly until an admin (or AI moderator) approves them. A rating of 1 or 2 with negative content automatically triggers a support_alerts row so the support team can investigate.

When a review is published, nurse_profiles.average_rating and total_reviews are updated.

Field Type Notes
id BIGINT PK IDENTITY
booking_id BIGINT FK → bookings UNIQUE NOT NULL One review per booking
customer_id BIGINT FK → customer_profiles NOT NULL
nurse_id BIGINT FK → nurse_profiles NOT NULL Denormalized for query performance
rating TINYINT NOT NULL 15
body NVARCHAR(2000) NULL Free-text content
status NVARCHAR(30) NOT NULL DEFAULT 'pending_moderation' pending_moderation / published / rejected / hidden
moderated_by_admin_id BIGINT FK → users NULL Admin or AI agent that approved/rejected
moderation_notes NVARCHAR(500) NULL Internal reason for rejection or hiding
published_at DATETIME2 NULL
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

Tags applied to a specific review.

Field Type Notes
id BIGINT PK IDENTITY
review_id BIGINT FK → reviews NOT NULL
tag_id BIGINT FK → review_tags_master NOT NULL

Constraint: (review_id, tag_id) UNIQUE.


patient_care_records

Nurse-authored clinical notes written after completing a booking. These accumulate over time and form a longitudinal care history for the patient. When a new nurse receives a booking request for the same patient, they can read this history to understand prior care, medications, and observations — enabling continuity of care across multiple nurses.

Access to these records is strictly controlled: only the patient's owning customer, nurses with a confirmed upcoming or in-progress booking for that patient, and admin may read them. All fields are encrypted.

Field Type Notes
id BIGINT PK IDENTITY
patient_id BIGINT FK → patients NOT NULL
booking_id BIGINT FK → bookings NOT NULL The specific visit that generated this record
nurse_id BIGINT FK → nurse_profiles NOT NULL Who authored the record
observations NVARCHAR(MAX) NULL (encrypted) — general notes on patient state during the visit
vitals_json NVARCHAR(MAX) NULL (encrypted) — structured: {blood_pressure, temperature, pulse, oxygen_saturation, ...}
medications_json NVARCHAR(MAX) NULL (encrypted) — medications administered or observed
conditions_note NVARCHAR(MAX) NULL (encrypted) — new or changed conditions noted by the nurse
recorded_at DATETIME2 NOT NULL When nurse submitted this record
created_at DATETIME2 NOT NULL

Domain 10 — Notifications


notifications

In-app notification records for all user types. No push notifications at launch — these are polled on page load or explicitly fetched. The data_json carries a typed payload that the front-end uses to navigate to the relevant entity (e.g., open a specific booking, ticket, or payout screen).

Field Type Notes
id BIGINT PK IDENTITY
user_id BIGINT FK → users NOT NULL Recipient
type NVARCHAR(80) NOT NULL e.g., booking_request_received, booking_accepted, booking_rejected, booking_request_expired, payment_succeeded, payment_failed, booking_confirmed, review_published, review_rejected, payout_processed, ticket_new_message, verification_update, nurse_no_show_alert
title_fa NVARCHAR(300) NOT NULL
body_fa NVARCHAR(500) NULL
data_json NVARCHAR(MAX) NULL e.g., {"entity_type": "booking", "entity_id": 1234}
is_read BIT NOT NULL DEFAULT 0
read_at DATETIME2 NULL
created_at DATETIME2 NOT NULL

support_alerts

Internal alerts visible only to the support and admin team. Not shown to customers or nurses. Automatically generated by platform rules (low review ratings, EVV no-shows, payment anomalies) or manually by admin. Each alert has an owner and a resolution trail.

Field Type Notes
id BIGINT PK IDENTITY
alert_type NVARCHAR(50) NOT NULL low_rating_review / nurse_no_show / payment_anomaly / verification_step_expired / evv_location_mismatch / fraud_signal
severity NVARCHAR(20) NOT NULL info / warning / critical
entity_type NVARCHAR(50) NOT NULL e.g., review, booking, payment_transaction, verification_step
entity_id BIGINT NOT NULL
description NVARCHAR(500) NOT NULL Human-readable summary
status NVARCHAR(20) NOT NULL DEFAULT 'open' open / acknowledged / resolved / dismissed
assigned_to_admin_id BIGINT FK → users NULL NULL = unassigned
resolved_at DATETIME2 NULL
resolved_by_admin_id BIGINT FK → users NULL
resolution_notes NVARCHAR(500) NULL
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

Domain 11 — Audit & Logging


audit_logs

Immutable, append-only record of all state-changing operations on sensitive entities. Every insert, update, or status change on bookings, payments, refunds, verifications, reviews, and user accounts produces a row here. This table must never be updated or deleted — archive to cold storage after a retention period.

changed_fields_json stores just the names of fields that changed, enabling fast filtering ("show me all booking status changes") without parsing full old/new value diffs.

Field Type Notes
id BIGINT PK IDENTITY
entity_type NVARCHAR(100) NOT NULL Table name: bookings, payment_transactions, refunds, nurse_verifications, verification_steps, reviews, users, nurse_profiles
entity_id BIGINT NOT NULL PK of the affected row
action NVARCHAR(20) NOT NULL created / updated / status_changed / deleted
old_values_json NVARCHAR(MAX) NULL Full row snapshot before change (NULL for creates)
new_values_json NVARCHAR(MAX) NULL Full row snapshot after change (NULL for deletes)
changed_fields_json NVARCHAR(MAX) NULL JSON array of field names that changed
performed_by_user_id BIGINT FK → users NULL NULL for system/background jobs
performed_by_role NVARCHAR(50) NULL Captured at action time
ip_address NVARCHAR(45) NULL
user_agent NVARCHAR(500) NULL
request_id NVARCHAR(100) NULL Correlation ID from API request for distributed tracing
created_at DATETIME2 NOT NULL

system_events

Higher-volume behavioral event log for analytics, funnel analysis, and future fraud detection feature engineering. Distinct from audit_logs — audit is for compliance and accountability; system_events is for product analytics and monitoring. May be partitioned by month or archived to a data warehouse.

Field Type Notes
id BIGINT PK IDENTITY
event_type NVARCHAR(100) NOT NULL e.g., nurse_search_performed, nurse_profile_viewed, booking_request_submitted, payment_initiated, verification_step_started
user_id BIGINT FK → users NULL NULL for anonymous or system events
session_id NVARCHAR(100) NULL App session identifier
payload_json NVARCHAR(MAX) NULL Event-specific properties (search filters, variant viewed, etc.)
source NVARCHAR(30) NOT NULL api / background_job / admin_panel / webhook
ip_address NVARCHAR(45) NULL
created_at DATETIME2 NOT NULL

Domain 12 — Platform Configuration


platform_configs

Key-value store for runtime-tunable business parameters. Changing a parameter (e.g., raising the platform fee, adjusting the nurse response deadline) does not require a deployment — admin edits a row and the application picks it up. Values are stored as strings; the data_type field tells the application how to parse them.

Representative keys and their purpose:

Key Description
platform_fee_rate Decimal — e.g., 0.1500 (15%). Used for new bookings only.
booking_payment_deadline_minutes Integer — how long the customer has to pay after nurse accepts. Default 30.
nurse_response_deadline_hours Integer — how long before a booking request auto-expires.
nurse_payout_interval_days Integer — how many days after period_end before a batch is created.
default_refund_percentage Decimal — default % refunded when admin approves. Default 100.
min_rating_for_support_alert Integer — reviews with rating ≤ this value trigger a support_alert. Default 2.
evv_location_tolerance_meters Integer — GPS match tolerance for check-in address verification.
review_requires_admin_approval Boolean — whether all reviews need moderation before publish.
Field Type Notes
id BIGINT PK IDENTITY
key NVARCHAR(100) UNIQUE NOT NULL
value NVARCHAR(500) NOT NULL
data_type NVARCHAR(20) NOT NULL integer / decimal / boolean / string / json
description NVARCHAR(500) NULL
updated_by_admin_id BIGINT FK → users NULL
updated_at DATETIME2 NOT NULL
created_at DATETIME2 NOT NULL

Domain 13 — Future Extensions (modeled now, inactive at launch)

These tables are defined now to avoid a painful migration when the features ship. They carry no data at launch but the FK relationships are already established so the rest of the schema doesn't need to change.


organizations

Nursing companies that may register on the platform and add their employed nurses. Each nurse remains individually verified and responsible. The organization acts as a sponsor/guarantor for its nurses and may have a separate dashboard.

Field Type Notes
id BIGINT PK IDENTITY
name NVARCHAR(300) NOT NULL
type NVARCHAR(30) NOT NULL nursing_company / staffing_agency
moh_license_number NVARCHAR(100) NULL MoH establishment permit (پروانه تأسیس)
admin_user_id BIGINT FK → users NOT NULL Organization's admin account
is_verified BIT NOT NULL DEFAULT 0 Platform-verified
verified_at DATETIME2 NULL
is_active BIT NOT NULL DEFAULT 1
created_at DATETIME2 NOT NULL
updated_at DATETIME2 NOT NULL

organization_nurses

Membership link between a nurse and an organization. A nurse can belong to at most one active organization at a time (enforced by checking left_at IS NULL).

Field Type Notes
id BIGINT PK IDENTITY
organization_id BIGINT FK → organizations NOT NULL
nurse_id BIGINT FK → nurse_profiles NOT NULL
joined_at DATETIME2 NOT NULL
left_at DATETIME2 NULL NULL = current member
status NVARCHAR(20) NOT NULL active / suspended / left

fraud_flags

Signals from a future fraud detection service. Modeled now so the schema is ready to receive data when the detection layer is built, requiring no migration. The confidence_score field anticipates an ML model output.

Field Type Notes
id BIGINT PK IDENTITY
entity_type NVARCHAR(50) NOT NULL user / payment_transaction / booking
entity_id BIGINT NOT NULL
flag_type NVARCHAR(100) NOT NULL e.g., identity_mismatch, unusual_payment_velocity, address_anomaly, kyc_response_inconsistency
severity NVARCHAR(20) NOT NULL low / medium / high
confidence_score DECIMAL(5,4) NULL 0.00001.0000 from detection model
details_json NVARCHAR(MAX) NULL Evidence from the detection model
status NVARCHAR(30) NOT NULL DEFAULT 'open' open / investigating / confirmed_fraud / false_positive / resolved
assigned_to_admin_id BIGINT FK → users NULL
created_at DATETIME2 NOT NULL
resolved_at DATETIME2 NULL

recurring_booking_schedules

Future feature: allows families to establish a repeating care pattern (e.g., every Monday and Thursday morning with the same nurse). At launch, all bookings are one-off. The rrule field uses the RFC 5545 recurrence rule format, which is widely supported by scheduling libraries.

Field Type Notes
id BIGINT PK IDENTITY
customer_id BIGINT FK → customer_profiles NOT NULL
nurse_id BIGINT FK → nurse_profiles NOT NULL
variant_id BIGINT FK → nurse_service_variants NOT NULL
patient_id BIGINT FK → patients NOT NULL
customer_address_id BIGINT FK → customer_addresses NOT NULL
rrule NVARCHAR(500) NOT NULL RFC 5545 recurrence rule, e.g., FREQ=WEEKLY;BYDAY=MO,TH
scheduled_time_start TIME NOT NULL
scheduled_time_end TIME NOT NULL
effective_from DATE NOT NULL
effective_until DATE NULL NULL = indefinite
is_active BIT NOT NULL DEFAULT 1
created_at DATETIME2 NOT NULL

Relationship Summary

Relationship Type Notes
usersnurse_profiles 1:1 role = nurse
userscustomer_profiles 1:1 role = customer
customer_profilespatients 1:N One family, many patients
customer_profilescustomer_addresses 1:N
nurse_profilesnurse_bank_accounts 1:N One primary at a time
nurse_profilesnurse_service_variants 1:N Each variant is one bookable offering
nurse_service_variantsnurse_service_variant_options 1:N Defines the option combination
service_option_groupsservice_option_values 1:N
nurse_profilesnurse_service_areas 1:N Many city/district pairs
nurse_profilesnurse_verifications 1:1
nurse_verificationsverification_steps 1:N One per step type
verification_stepsverification_documents 1:N
booking_requestsbookings 1:1 Created on nurse accept + payment
bookingsbooking_care_instructions 1:1
bookingsvisit_verifications 1:1 EVV record
bookingspayment_transactions 1:N Multiple attempts allowed
payment_transactionsrefunds 1:1 One refund per transaction
payment_transactionsinstallment_plans 1:1 Only if is_installment = true
installment_plansinstallment_entries 1:N One per installment
nurse_payout_batchesnurse_payouts 1:N One per nurse per batch
nurse_payoutsnurse_payout_booking_links 1:N Detailed booking breakdown
nurse_payout_booking_linksbookings N:1 Each booking in exactly one payout
bookingsreviews 1:1 One review per booking
reviewsreview_tag_links 1:N
patientspatient_care_records 1:N Grows with every visit
ticketsticket_participants 1:N
ticketsticket_messages 1:N
ticketsrefunds 1:1 (optional) Refund conversation
Sensitive entities → audit_logs *:N Append-only change log

Key Design Decisions

Why booking_requests and bookings are separate tables A booking request may be rejected, expire, or have its payment deadline lapse without a booking ever being created. Merging these would require many nullable fields and complex status logic. Keeping them separate makes each table's invariants clear: a bookings row always has a successful payment.

Why variant_snapshot_json exists on bookings If a nurse later edits or deletes a variant, historical bookings must still display what the customer paid for. The FK to nurse_service_variants is kept for joins but variant_snapshot_json is the authoritative record for display and dispute.

Why monetary amounts are stored as BIGINT (IRR) Rials are the smallest unit; there are no decimal subdivisions in practice. Storing as BIGINT eliminates floating-point precision issues. All division (for fee calculation) is done in application code with explicit rounding rules.

Why platform_fee_rate is stored per booking The platform's fee schedule will change over time. Deriving the split from platform_configs would corrupt historical reporting. Every booking stores the rate that was in effect at the moment of confirmation.

Why the verification pipeline is data-driven verification_step_types rows define what steps exist and whether they're automated. Adding a new regulatory requirement (e.g., professional liability insurance in year 2) requires one INSERT, not a schema migration and code deploy.

Why there is no direct nurse ↔ customer messaging Platform safety design: all communication is ticket-based and admin-readable. This protects vulnerable patients, provides dispute evidence, and prevents disintermediation. Post-booking coordination (nurse asking about the patient's room location, customer asking about arrival time) happens through a ticket of type booking_coordination.

Why patient_care_records are nurse-authored and patient-scoped Clinical notes build a longitudinal record for the patient, not the booking. When a different nurse takes over care, they read the history before accepting the booking request, making care continuity possible without the family having to repeat everything.

Advices

Several things stand out after sitting with the full model:

  1. The nurse search query will be expensive from day one

Finding nurses requires joining nurse_profiles (verified + accepting) → nurse_service_variants (category + price range) → nurse_service_variant_options → nurse_service_areas (city/district). That's already 4 joins before any sorting by rating or availability. At even modest scale this becomes slow.

Advice: build a denormalized nurse_search_index table that is updated on writes (when a nurse edits a variant, changes their area, or gets a new review). This table stores one row per variant with all search-relevant fields flat. Read path hits one table; write path maintains it. Much cheaper than adding Elasticsearch to the stack at your current stage.

  1. The two-stage clinical disclosure is implicit — make it explicit

Right now booking_requests.customer_notes (1000 chars, unencrypted) is what the nurse sees when deciding to accept. The full booking_care_instructions (encrypted, medications, conditions, etc.) only exists after the booking is confirmed. This boundary is correct and security-smart, but it's nowhere stated in the model.

If you don't document this explicitly in the API contract, a developer will one day accidentally expose the full clinical record to a nurse at the request stage — before any commitment. Write this rule into the model comments and enforce it at the application layer, not just by convention.

  1. Dispute window vs payout timing can overlap

A booking can be disputed after the weekly payout batch already ran and the nurse was paid. The current model has no clawback mechanism — nurse_payouts has no way to record that money needs to come back. Consider adding clawback_amount and clawback_status to nurse_payouts, or simply enforce a policy that payout only runs for bookings where the dispute window has closed (e.g., 72 hours post-completion). Either way, document the rule.

  1. The ticket system has an urgent on-site gap

The ticket system is async. If a nurse arrives and the patient doesn't answer the door, or there's a medical emergency, there is no real-time channel in the model. The only outlet is the emergency contact phone number buried in booking_care_instructions. This is probably acceptable for v1 — but be intentional about it. The operational playbook for nurses should explicitly say "in an emergency, call the emergency contact number from the app, then open a ticket." Otherwise nurses will try to find the family's phone number by other means, which breaks the platform's communication control.

  1. Three tables will eat your disk quietly

audit_logs, system_events, and notifications grow without any natural bound. Before you hit production, decide:

audit_logs — partition by month, archive after 23 years to cold storage system_events — same, or pipe directly to a data warehouse and don't keep them in SQL Server at all notifications — add a job that hard-deletes read notifications older than 90 days If you don't plan this before launch, you'll be doing emergency archiving under pressure later.

  1. is_verified on nurse_profiles is a derived value — protect it

This flag is true when all required verification steps pass. But it lives as a column that could theoretically be set independently. If a bug or a rogue admin sets is_verified = 1 without the steps passing, an unvetted nurse becomes bookable. Enforce this transactionally: the only code path that sets is_verified = true should be the one that also checks all required verification_steps.status = 'passed', inside the same transaction. Never expose a direct update API for this field.