- 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.
139 lines
5.3 KiB
Markdown
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` |
|