# دیتابیس — تسک ۱۳ ## `cancellation_policies` | ستون | نوع | توضیح | |---|---|---| | `id` | INT PK AI | | | `uuid` | VARCHAR(36) UNIQUE | | | `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | | | `service_item_id` | INT NULL | NULL = پیش‌فرض محیط — FK ON DELETE CASCADE | | `free_window_hours` | SMALLINT NOT NULL DEFAULT 24 | | | `penalty_mode` | VARCHAR(10) NOT NULL DEFAULT 'none' | `none`\|`percent`\|`fixed` | | `penalty_value` | INT NOT NULL DEFAULT 0 | درصد ۰..۱۰۰ یا ریال | | `deposit_refundable` | TINYINT(1) NOT NULL DEFAULT 0 | پس از پنجرهٔ رایگان | | `credit_refundable` | TINYINT(1) NOT NULL DEFAULT 1 | اعتبار پکیج | | `no_show_threshold` | SMALLINT NOT NULL DEFAULT 3 | | | `risk_tag_uuid` | VARCHAR(36) NULL | ارجاع به `tenant_tags.uuid` — بدون FK، الگوی موجود پروژه | | `active` | TINYINT(1) NOT NULL DEFAULT 1 | | | `created_at`/`updated_at` | INT NOT NULL | | ```sql UNIQUE KEY uniq_cancel_policy_scope (entity_type, entity_id, service_item_id) KEY idx_cancel_policies_tenant (entity_type, entity_id, active) ``` `risk_tag_uuid` بدون FK — همان الگوی `DiscountRule.target_tag_uuid` موجود. ## `no_show_records` ```sql CREATE TABLE no_show_records ( id INT PRIMARY KEY AUTO_INCREMENT, uuid VARCHAR(36) NOT NULL UNIQUE, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, patient_record_id INT NOT NULL, appointment_id INT NOT NULL, recorded_at INT NOT NULL, recorded_by INT NULL, UNIQUE KEY uniq_no_show_appointment (appointment_id), -- یک بار per نوبت KEY idx_no_show_patient (patient_record_id, recorded_at), -- کوئری شمارش ۱۲ ماه KEY idx_no_show_tenant (entity_type, entity_id, recorded_at), CONSTRAINT fk_ns_patient FOREIGN KEY (patient_record_id) REFERENCES patient_records(id) ON DELETE CASCADE, CONSTRAINT fk_ns_appt FOREIGN KEY (appointment_id) REFERENCES appointments(id) ON DELETE CASCADE ); ``` جدول جدا و نه یک ستون شمارنده روی بیمار — همان استدلال دفتر اعتبار تسک ۱۱: شمارنده، «چه زمانی و کدام نوبت» را از دست می‌دهد و پنجرهٔ ۱۲ ماهه غیرقابل محاسبه می‌شود. `uniq_no_show_appointment`: تغییر وضعیت به `no_show` ممکن است دوبار اتفاق بیفتد (idempotency)؛ رکورد دوم ثبت نشود. ## `waitlist_entries` | ستون | نوع | توضیح | |---|---|---| | `id` | INT PK AI | | | `uuid` | VARCHAR(36) UNIQUE | | | `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | | | `patient_record_id` | INT NOT NULL | FK ON DELETE CASCADE | | `service_item_id` | INT NOT NULL | FK ON DELETE CASCADE | | `branch_id` | INT NULL | FK ON DELETE CASCADE | | `desired_from` | INT NOT NULL | | | `desired_to` | INT NOT NULL | | | `preferred_day_parts` | JSON NULL | `["morning","evening"]` | | `priority` | SMALLINT NOT NULL DEFAULT 0 | | | `status` | VARCHAR(12) NOT NULL DEFAULT 'waiting' | `waiting`\|`notified`\|`converted`\|`expired` | | `notified_at` | INT NULL | آخرین اطلاع | | `notify_count` | SMALLINT NOT NULL DEFAULT 0 | سقف برای جلوگیری از اسپم | | `converted_appointment_id` | INT NULL | FK ON DELETE SET NULL | | `created_at`/`updated_at` | INT NOT NULL | | ```sql KEY idx_waitlist_match (service_item_id, branch_id, status, desired_from, desired_to) KEY idx_waitlist_tenant (entity_type, entity_id, status, created_at) KEY idx_waitlist_patient (patient_record_id, status) ``` `idx_waitlist_match` کوئری داغ است: «چه کسانی منتظر این سرویس در این بازه‌اند؟» ```sql SELECT * FROM waitlist_entries WHERE service_item_id = ? AND (branch_id = ? OR branch_id IS NULL) AND status = 'waiting' AND desired_from <= :freedEnd AND desired_to >= :freedStart ORDER BY priority DESC, created_at ASC LIMIT 10 ``` `preferred_day_parts` در PHP فیلتر می‌شود (JSON قابل ایندکس مطمئن نیست و نتیجه ≤ ۱۰ ردیف است). ## هیچ تغییری در `appointments` وضعیت‌های `cancelled_by_user`, `cancelled_by_doctor`, `no_show` از قبل هستند. `deposit_amount_rials` هم. ## جریمه در دفتر مالی موجود جدول جدید ندارد. `WalletTransaction` موجود استفاده می‌شود: ```php new WalletTransaction( user: $appt->getUser(), amountRials: -$penalty, kind: 'cancellation_penalty', // ← مقدار جدید در enum موجود reference: $appt->getUuid(), ); $tx->setRecordedEntity($appt->getEntityType(), $appt->getEntityId()); // per-محیط، طبق tenancy.md ``` `setRecordedEntity` اجباری است، وگرنه جریمه در دفتر همهٔ محیط‌ها دیده می‌شود (`docs/architecture/tenancy.md`، بخش کیف پول). ## 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:cancellation:seed-default-policy --force ``` `seed-default-policy` برای هر محیط یک سیاست پیش‌فرض محافظه‌کار می‌سازد: `free_window_hours=24, penalty_mode=none, deposit_refundable=true, credit_refundable=true`. **پیش‌فرض بدون جریمه** عمدی است: فعال شدن ناگهانی جریمه روی بیماران موجود، شکایت است. کلینیک خودش باید فعالش کند. ## پاکسازی ```bash ddev exec php bin/console app:waitlist:expire # روزانه ``` ورودی‌هایی که `desired_to` گذشته → `status='expired'`. ## طبقه‌بندی tenant | جدول | وضعیت | |---|---| | `cancellation_policies`, `no_show_records`, `waitlist_entries` | جفت tenant |