Files
clinicpro/docs/new_feture/taskes/task-14-events-utilization/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

127 lines
4.9 KiB
Markdown

# دیتابیس — تسک ۱۴
## `domain_events` — outbox
| ستون | نوع | توضیح |
|---|---|---|
| `id` | BIGINT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | شناسهٔ idempotency برای مصرف‌کننده |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | محیط رویداد |
| `name` | VARCHAR(60) NOT NULL | `AppointmentBooked` |
| `payload` | JSON NOT NULL | فقط uuid و اسکالر |
| `occurred_at` | INT NOT NULL | زمان وقوع (نه انتشار) |
| `published_at` | INT NULL | NULL = منتشر نشده |
| `attempts` | SMALLINT NOT NULL DEFAULT 0 | |
| `last_error` | VARCHAR(255) NULL | |
```sql
KEY idx_de_pending (published_at, occurred_at) -- worker: WHERE published_at IS NULL
KEY idx_de_tenant (entity_type, entity_id, occurred_at)
KEY idx_de_name (name, occurred_at)
```
`idx_de_pending` کوئری worker است:
```sql
SELECT * FROM domain_events
WHERE published_at IS NULL AND attempts < 5
ORDER BY occurred_at ASC
LIMIT 100
```
`attempts < 5` سقف تلاش. ردیف مرده با `last_error` باقی می‌ماند تا ادمین ببیند —
حذف خاموش یعنی رویداد گم‌شدهٔ بی‌رد.
## تغییر جدول موجود
هیچ. `resource_occupancy` (تسک ۰۷) ستون‌های لازم برای گزارش بهره‌وری را دارد:
`start_at`, `end_at`, `occupancy_kind`, `status`, `resource_id`.
`appointments.plan_total_minutes` (تسک ۰۷) پیش‌بینی را دارد.
`AppointmentEvent` موجود دست‌نخورده می‌ماند — تاریخچهٔ وضعیت نوبت است، رویداد دامنه نیست.
تفاوتشان را در `docs/architecture/domain-events.md` بنویس:
| | `AppointmentEvent` | `DomainEventLog` |
|---|---|---|
| دامنه | فقط نوبت | همهٔ دامنه‌ها |
| مصرف‌کننده | UI تاریخچه | سیستم‌های دیگر (پیامک، حسابداری) |
| انتشار | ندارد | messenger |
## کوئری گزارش بهره‌وری
```sql
SELECT ro.resource_id,
SUM(ro.end_at - ro.start_at) AS occupied_seconds,
SUM(CASE WHEN ro.occupancy_kind <> 'passive'
THEN ro.end_at - ro.start_at ELSE 0 END) AS active_seconds,
COUNT(DISTINCT ro.appointment_id) AS appointment_count
FROM resource_occupancy ro
WHERE ro.entity_type = :type AND ro.entity_id = :id
AND ro.status = 'booked'
AND ro.start_at >= :from AND ro.start_at < :to
GROUP BY ro.resource_id
```
`ro.start_at < :to` (نه `end_at <= :to`) — نوبتی که در بازه شروع شده ولی بیرون تمام شده،
باید شمرده شود. جزئی است ولی روی گزارش هفتگی چند درصد اختلاف می‌سازد.
ایندکس `idx_occ_tenant_range (entity_type, entity_id, start_at)` تسک ۰۷ این را پوشش می‌دهد.
## کوئری دقت برنامه
```sql
SELECT a.service_item_id,
AVG(a.plan_total_minutes) AS planned_avg,
AVG((ps.ended_at - ps.started_at) / 60) AS actual_avg,
COUNT(*) AS sample
FROM appointments a
JOIN patient_sessions ps ON ps.appointment_id = a.id
WHERE a.entity_type = :type AND a.entity_id = :id
AND a.status = 'completed'
AND a.slot_start >= :from AND a.slot_start < :to
AND a.plan_total_minutes IS NOT NULL
AND ps.ended_at IS NOT NULL
GROUP BY a.service_item_id
HAVING sample >= 10
```
⚠️ **بررسی لازم پیش از پیاده‌سازی:** `patient_sessions` باید `appointment_id` و
`started_at`/`ended_at` داشته باشد. اگر ندارد، دو گزینه:
1. از `appointment_events` استفاده کن: فاصلهٔ بین انتقال به `salon` و انتقال به `completed`
2. اگر آن هم نیست، این گزارش به یک تسک جدا موکول شود و فقط گزارش بهره‌وری در این تسک بماند
**تصمیم را بگیر و بنویس** — نه یک گزارش با داده حدسی.
## نگهداشت
```bash
ddev exec php bin/console app:events:prune --older-than=180d --force
```
ردیف‌های `published_at IS NOT NULL` قدیمی‌تر از ۶ ماه. ردیف‌های شکست‌خورده
(`published_at IS NULL AND attempts >= 5`) **هرگز** حذف نمی‌شوند.
## Migration
```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
```
worker انتشار در `config/packages/messenger.yaml` و `scheduler`:
```yaml
# هر ۱۰ ثانیه
App\Shared\Event\Message\FlushOutboxMessage: { frequency: 10 }
```
⚠️ طبق حافظهٔ عملیاتی پروژه، worker های Coolify باید loop-wrap شوند تا کانتینر خارج نشود.
دستور جدید را با همان الگو اضافه کن.
## طبقه‌بندی tenant
| جدول | وضعیت |
|---|---|
| `domain_events` | جفت tenant |