Files
clinicpro/docs/new_feture/taskes/task-08-pricing-snapshot/database.md
T
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
2026-07-30 11:43:58 +03:30

6.1 KiB

دیتابیس — تسک ۰۸

price_lists

ستون نوع توضیح
id INT PK AI
uuid VARCHAR(36) UNIQUE
entity_type / entity_id VARCHAR(10) / INT NOT NULL
branch_id INT NULL NULL = همهٔ شعب محیط
name VARCHAR(150) NOT NULL «نیمهٔ دوم ۱۴۰۵»
valid_from INT NOT NULL نیمه‌شب روز شروع
valid_to INT NULL NULL = بی‌پایان
status VARCHAR(10) NOT NULL DEFAULT 'draft' draft|active|archived
created_at/updated_at INT NOT NULL
KEY idx_price_lists_tenant (entity_type, entity_id, status, valid_from)
KEY idx_price_lists_branch (branch_id, status, valid_from)

تداخل بازه در سطح اپلیکیشن بررسی می‌شود (activate)، نه DB — MariaDB محدودیت بازه‌ای ندارد و راه سطل زمانی تسک ۰۷ اینجا بی‌مورد است چون تعداد لیست‌ها کم و تغییرشان نادر است.

price_list_items

CREATE TABLE price_list_items (
  id INT PRIMARY KEY AUTO_INCREMENT,
  price_list_id INT NOT NULL,
  service_item_id INT NULL,        -- قیمت سرویس
  service_option_id INT NULL,      -- قیمت آیتم
  price_rials INT NOT NULL,
  UNIQUE KEY uniq_pli_service (price_list_id, service_item_id),
  UNIQUE KEY uniq_pli_option  (price_list_id, service_option_id),
  KEY idx_pli_list (price_list_id),
  CONSTRAINT fk_pli_list    FOREIGN KEY (price_list_id)     REFERENCES price_lists(id)     ON DELETE CASCADE,
  CONSTRAINT fk_pli_service FOREIGN KEY (service_item_id)   REFERENCES service_items(id)   ON DELETE CASCADE,
  CONSTRAINT fk_pli_option  FOREIGN KEY (service_option_id) REFERENCES service_options(id) ON DELETE CASCADE
);

دقیقاً یکی از service_item_id / service_option_id غیر-NULL (قید اپلیکیشنی). فرزند aggregate با ریشهٔ PriceList.

price_snapshots

ستون نوع توضیح
id INT PK AI
uuid VARCHAR(36) UNIQUE
entity_type / entity_id VARCHAR(10) / INT NOT NULL
appointment_id INT NOT NULL UNIQUE FK ON DELETE CASCADE — یک snapshot per نوبت
base_rials INT NOT NULL
options_rials INT NOT NULL DEFAULT 0
discount_rials INT NOT NULL DEFAULT 0
insurance_base_rials INT NOT NULL DEFAULT 0
insurance_supplementary_rials INT NOT NULL DEFAULT 0
tax_rials INT NOT NULL DEFAULT 0
final_rials INT NOT NULL
deposit_rials INT NOT NULL DEFAULT 0
applied_policy_ids JSON NULL [{id, version}]
price_list_id INT NULL FK SET NULL — کدام لیست مبنا بود
created_at INT NOT NULL
UNIQUE KEY uniq_snapshot_appointment (appointment_id)
KEY idx_snapshot_tenant (entity_type, entity_id, created_at)

UNIQUE روی appointment_id: یک نوبت یک فاکتور رزرو دارد. reschedule نوبت جدید می‌سازد (تسک ۰۷) پس snapshot جدید هم می‌گیرد و قدیمی سالم می‌ماند.

⚠️ همهٔ مبالغ INT ریال. DECIMAL یا FLOAT ننویس — بقیهٔ پروژه (price_rials, visit_price_rials, deposit_amount_rials) همه INT ریال‌اند و قاطی کردن دو نوع یعنی خطای گردکردن در جمع فاکتور.

price_snapshot_lines

CREATE TABLE price_snapshot_lines (
  id INT PRIMARY KEY AUTO_INCREMENT,
  snapshot_id INT NOT NULL,
  kind VARCHAR(15) NOT NULL,          -- base|option|discount|insurance|tax|package
  label VARCHAR(200) NOT NULL,        -- snapshot متنی — نام لحظهٔ ثبت
  amount_rials INT NOT NULL,          -- منفی برای تخفیف و سهم بیمه
  source_type VARCHAR(20) NULL,       -- service|option|discount_rule|policy|insurance
  source_id INT NULL,                 -- بدون FK — منبع ممکن است حذف شود
  sort_order SMALLINT NOT NULL DEFAULT 0,
  KEY idx_psl_snapshot (snapshot_id, sort_order),
  CONSTRAINT fk_psl_snapshot FOREIGN KEY (snapshot_id) REFERENCES price_snapshots(id) ON DELETE CASCADE
);

source_id بدون FK عمدی: قانون تخفیف ممکن است فردا حذف شود ولی فاکتور دیروز باید همان‌طور بماند. label هم به همین دلیل کپی متنی است، نه JOIN.

deposit_policies

CREATE TABLE deposit_policies (
  id INT PRIMARY KEY AUTO_INCREMENT,
  uuid VARCHAR(36) NOT NULL UNIQUE,
  entity_type VARCHAR(10) NOT NULL,
  entity_id INT NOT NULL,
  service_item_id INT NULL,            -- NULL = پیش‌فرض محیط
  mode VARCHAR(10) NOT NULL,           -- none|fixed|percent
  value INT NOT NULL DEFAULT 0,
  min_rials INT NULL,
  max_rials INT NULL,
  active TINYINT(1) NOT NULL DEFAULT 1,
  created_at INT NOT NULL,
  updated_at INT NOT NULL,
  UNIQUE KEY uniq_deposit_scope (entity_type, entity_id, service_item_id),
  KEY idx_deposit_tenant (entity_type, entity_id, active)
);

تغییر جدول موجود

هیچ. appointments.visit_price_rials, deposit_required, deposit_amount_rials, insurance_base_id, insurance_supplementary_id همه استفاده می‌شوند و کافی‌اند.

Tariff هم دست‌نخورده می‌ماند و در زنجیرهٔ PriceResolver سطر ۴ است.

Migration

ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console app:pricing:backfill-snapshots --force

app:pricing:backfill-snapshots برای نوبت‌های confirmed آینده که snapshot ندارند، یکی از visit_price_rials موجود می‌سازد (یک ردیف base). بدون آن، صفحهٔ فاکتور برای نوبت‌های موجود خالی است.

طبقه‌بندی tenant

جدول وضعیت
price_lists, price_snapshots, deposit_policies جفت tenant
price_list_items, price_snapshot_lines AGGREGATE_CHILDREN