- 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.
146 lines
6.2 KiB
Markdown
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` — **هرگز مستقیم کوئری نشود** |
|