- 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.
127 lines
4.9 KiB
Markdown
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 |
|