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

144 lines
6.1 KiB
Markdown

# دیتابیس — تسک ۰۸
## `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 | |
```sql
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`
```sql
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 | |
```sql
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`
```sql
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`
```sql
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
```bash
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` |