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.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# دیتابیس — تسک ۱۳
|
||||
|
||||
## `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 |
|
||||
Reference in New Issue
Block a user