Files
clinicpro/docs/new_feture/taskes/task-12-treatment-course/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

139 lines
5.3 KiB
Markdown

# دیتابیس — تسک ۱۲
## `course_protocols`
| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `service_item_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `session_count` | SMALLINT NOT NULL | |
| `min_days` | SMALLINT NOT NULL | |
| `ideal_days` | SMALLINT NOT NULL | |
| `max_days` | SMALLINT NOT NULL | |
| `prefer_same_resource` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at`/`updated_at` | INT NOT NULL | |
```sql
UNIQUE KEY uniq_protocol_service (service_item_id) -- یک پروتکل فعال per سرویس
KEY idx_protocols_tenant (entity_type, entity_id, active)
```
قید اپلیکیشنی: `min_days <= ideal_days <= max_days` و `session_count >= 2`
(دورهٔ یک‌جلسه‌ای همان نوبت تکی است).
## `course_protocol_steps`
```sql
CREATE TABLE course_protocol_steps (
id INT PRIMARY KEY AUTO_INCREMENT,
protocol_id INT NOT NULL,
session_number SMALLINT NOT NULL,
params JSON NULL, -- {"energy": 12} — اسکالر
override_duration_minutes SMALLINT NULL,
UNIQUE KEY uniq_step (protocol_id, session_number),
CONSTRAINT fk_step_protocol FOREIGN KEY (protocol_id) REFERENCES course_protocols(id) ON DELETE CASCADE
);
```
فرزند aggregate با ریشهٔ `CourseProtocol`.
## `treatment_courses`
| ستون | نوع | توضیح |
|---|---|---|
| `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 RESTRICT |
| `service_item_id` | INT NOT NULL | FK ON DELETE RESTRICT |
| `protocol_id` | INT NOT NULL | FK ON DELETE RESTRICT |
| `session_count` | SMALLINT NOT NULL | **snapshot** |
| `min_days` | SMALLINT NOT NULL | **snapshot** |
| `ideal_days` | SMALLINT NOT NULL | **snapshot** |
| `max_days` | SMALLINT NOT NULL | **snapshot** |
| `patient_package_id` | INT NULL | FK → `patient_packages.id` ON DELETE SET NULL |
| `preferred_resource_id` | INT NULL | FK → `clinic_resources.id` ON DELETE SET NULL |
| `status` | VARCHAR(12) NOT NULL DEFAULT 'active' | `active`\|`completed`\|`abandoned` |
| `abandon_reason` | VARCHAR(255) NULL | |
| `started_at` | INT NOT NULL | |
| `completed_at` | INT NULL | |
| `created_at`/`updated_at` | INT NOT NULL | |
```sql
KEY idx_courses_tenant (entity_type, entity_id, status, started_at)
KEY idx_courses_patient (patient_record_id, status)
UNIQUE KEY uniq_active_course (patient_record_id, service_item_id, status)
```
⚠️ `uniq_active_course` با MariaDB روی مقدار `status` کار نمی‌کند به شکلی که فقط
`active` را یکتا کند (چند ردیف `completed` مجازند). راه درست: **قید اپلیکیشنی** در
`CourseStarter` + کلید یکتای جزئی که MariaDB ندارد.
جایگزین: یک ستون `active_course_key VARCHAR(64) NULL UNIQUE` با همان الگوی
`Appointment.active_slot_key`:
```php
$this->activeCourseKey = $this->status === self::STATUS_ACTIVE
? sprintf('%d:%d', $this->patient->getId(), $this->service->getId())
: null;
```
الگوی اثبات‌شدهٔ همین کدبیس — استفاده‌اش کن، دوباره اختراع نکن.
## `course_sessions`
| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `course_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `session_number` | SMALLINT NOT NULL | |
| `params` | JSON NULL | **snapshot** از `course_protocol_steps` |
| `appointment_id` | INT NULL UNIQUE | FK ON DELETE SET NULL |
| `status` | VARCHAR(12) NOT NULL DEFAULT 'planned' | `planned`\|`booked`\|`completed`\|`skipped` |
| `completed_at` | INT NULL | |
| `created_at`/`updated_at` | INT NOT NULL | |
```sql
UNIQUE KEY uniq_course_session (course_id, session_number)
UNIQUE KEY uniq_session_appointment (appointment_id)
KEY idx_sessions_tenant (entity_type, entity_id, status)
KEY idx_sessions_course (course_id, session_number)
```
`uniq_session_appointment`: یک نوبت به بیش از یک جلسهٔ دوره وصل نمی‌شود.
## تغییر `appointments`
```sql
ALTER TABLE appointments
ADD COLUMN course_session_id INT NULL,
ADD CONSTRAINT fk_appointments_course_session
FOREIGN KEY (course_session_id) REFERENCES course_sessions(id) ON DELETE SET NULL,
ADD KEY idx_appointments_course_session (course_session_id);
```
دو طرفه است (`course_sessions.appointment_id` هم وجود دارد) — عمدی: لیست نوبت‌های پنل
باید بدون JOIN بفهمد نوبت جزو دوره است، و صفحهٔ دوره باید بدون JOIN نوبت را پیدا کند.
هر دو در `CourseSessionLinker` **هم‌زمان** ست می‌شوند؛ هیچ جای دیگری ننویسد.
## Migration
```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
```
بدون backfill.
## طبقه‌بندی tenant
| جدول | وضعیت |
|---|---|
| `course_protocols`, `treatment_courses`, `course_sessions` | جفت tenant |
| `course_protocol_steps` | `AGGREGATE_CHILDREN` → ریشه `CourseProtocol` |