Files
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

146 lines
6.2 KiB
Markdown

# دیتابیس — تسک ۰۷
## `resource_occupancy` — مهم‌ترین جدول سیستم
| ستون | نوع | توضیح |
|---|---|---|
| `id` | BIGINT PK AI | BIGINT چون پرحجم‌ترین جدول می‌شود |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `resource_id` | INT NOT NULL | FK → `clinic_resources.id` ON DELETE RESTRICT |
| `appointment_id` | INT NULL | FK → `appointments.id` ON DELETE CASCADE؛ NULL = مسدودسازی دستی |
| `appointment_segment_id` | INT NULL | FK ON DELETE CASCADE |
| `start_at` | INT NOT NULL | **شامل `setup_minutes` منبع** |
| `end_at` | INT NOT NULL | **شامل `cleanup_minutes` منبع** |
| `units` | SMALLINT NOT NULL DEFAULT 1 | |
| `occupancy_kind` | VARCHAR(10) NOT NULL | `exclusive`\|`shared`\|`passive` |
| `status` | VARCHAR(10) NOT NULL | `hold`\|`booked`\|`released` |
| `expires_at` | INT NULL | فقط برای `hold` |
| `created_at` | INT NOT NULL | |
```sql
KEY idx_occ_resource_range (resource_id, start_at, end_at, status) -- کوئری داغ تسک ۰۶
KEY idx_occ_tenant_range (entity_type, entity_id, start_at) -- لیست‌های پنل
KEY idx_occ_appointment (appointment_id)
KEY idx_occ_expiry (status, expires_at) -- cron انقضا
```
## `resource_occupancy_slot` — تضمین یکتایی
```sql
CREATE TABLE resource_occupancy_slot (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
resource_id INT NOT NULL,
bucket INT NOT NULL, -- floor(timestamp / 300)
unit_index SMALLINT NOT NULL, -- 0 .. capacity-1
occupancy_id BIGINT NOT NULL,
UNIQUE KEY uniq_occ_slot (resource_id, bucket, unit_index), -- ← کل تضمین اینجاست
KEY idx_occ_slot_occupancy (occupancy_id),
CONSTRAINT fk_occ_slot_occupancy FOREIGN KEY (occupancy_id)
REFERENCES resource_occupancy(id) ON DELETE CASCADE
) ENGINE=InnoDB;
```
`BUCKET_SECONDS = 300` در `OccupancyWriter::BUCKET_SECONDS` ثابت است. تغییرش بعد از
تولید داده، migration کامل می‌خواهد — در کد کامنت هشدار بگذار.
سطل‌های یک بازه:
```php
// [start, end) نیم‌باز → سطل آخر شامل نمی‌شود اگر دقیقاً روی مرز باشد
$first = intdiv($start, self::BUCKET_SECONDS);
$last = intdiv($end - 1, self::BUCKET_SECONDS);
```
بدون `-1` نوبت ۱۰:۰۰-۱۰:۳۰ و نوبت ۱۰:۳۰-۱۱:۰۰ سطل مشترک می‌گیرند و دومی بی‌دلیل رد می‌شود.
## `appointment_segments`
| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `appointment_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `sequence` | SMALLINT NOT NULL | |
| `name` | VARCHAR(150) NOT NULL | snapshot نام بخش در لحظهٔ ثبت |
| `segment_type` | VARCHAR(40) NOT NULL | |
| `start_at` | INT NOT NULL | مطلق |
| `end_at` | INT NOT NULL | |
| `patient_present` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at` | INT NOT NULL | |
```sql
KEY idx_appt_seg_appointment (appointment_id, sequence)
KEY idx_appt_seg_tenant (entity_type, entity_id, start_at)
```
`name` و `segment_type` عمداً کپی می‌شوند نه FK: قانون پنجم مستند — «هر چیزی که ثبت شد
باید همان‌طور بماند». اگر کلینیک فردا الگو را عوض کند، نوبت دیروز نباید تغییر معنا دهد.
## تغییر `appointments`
```sql
ALTER TABLE appointments
ADD COLUMN branch_id INT NULL,
ADD COLUMN plan_total_minutes SMALLINT NULL,
ADD COLUMN patient_facing_minutes SMALLINT NULL,
ADD CONSTRAINT fk_appointments_branch FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE SET NULL,
ADD KEY idx_appointments_branch (branch_id, slot_start);
```
`slot_start` / `slot_end` **می‌مانند** و در حالت `resource` برابر شروع اولین بخش و پایان
آخرین بخش‌اند. دلیل: `AppointmentRepository`، لیست‌های پنل، سایت عمومی و اپ دسکتاپ همه
روی این دو ستون کوئری می‌زنند. برداشتنشان یعنی بازنویسی همه‌جا.
وضعیت جدید:
```php
public const STATUS_RESCHEDULED = 'rescheduled';
// ALLOWED_TRANSITIONS: confirmed → rescheduled ; pending → rescheduled
```
## Migration
```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
```
⚠️ **دو ایندکس را دستی در migration بنویس**`doctrine:migrations:diff` ترتیب ستون‌های
ایندکس ترکیبی را گاهی متفاوت تولید می‌کند و ترتیب اینجا حیاتی است
(`resource_id` اول در `idx_occ_resource_range`).
## backfill نوبت‌های موجود
```bash
ddev exec php bin/console app:occupancy:backfill --force
```
برای هر نوبت `pending`/`confirmed` آینده در حالت `slot`/`service`:
- یک `appointment_segment` واحد بساز (کل بازه)
- یک `resource_occupancy` روی منبع `type=doctor` همان پزشک
- ردیف‌های `resource_occupancy_slot` متناظر
اگر منبع `type=doctor` وجود ندارد (backfill تسک ۰۲ اجرا نشده)، آن نوبت رد شود و در
خروجی گزارش شود — نه خطا.
بدون این backfill، اولین رزرو در حالت جدید ممکن است روی نوبت قدیمی بنشیند.
## نگهداشت
```bash
ddev exec php bin/console app:occupancy:prune --older-than=90d --force
```
ردیف‌های `resource_occupancy_slot` مربوط به بازه‌های گذشته را حذف می‌کند.
`resource_occupancy` می‌ماند (آدیت و گزارش بهره‌وری تسک ۱۴).
## طبقه‌بندی tenant
| جدول | وضعیت |
|---|---|
| `resource_occupancy` | جفت tenant |
| `appointment_segments` | جفت tenant (uuid ممکن است از request بیاید) |
| `resource_occupancy_slot` | `AGGREGATE_CHILDREN` → ریشه `ResourceOccupancy`**هرگز مستقیم کوئری نشود** |