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

4.9 KiB

دیتابیس — تسک ۱۴

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
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 است:

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

کوئری گزارش بهره‌وری

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) تسک ۰۷ این را پوشش می‌دهد.

کوئری دقت برنامه

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. اگر آن هم نیست، این گزارش به یک تسک جدا موکول شود و فقط گزارش بهره‌وری در این تسک بماند

تصمیم را بگیر و بنویس — نه یک گزارش با داده حدسی.

نگهداشت

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

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:

# هر ۱۰ ثانیه
App\Shared\Event\Message\FlushOutboxMessage: { frequency: 10 }

⚠️ طبق حافظهٔ عملیاتی پروژه، worker های Coolify باید loop-wrap شوند تا کانتینر خارج نشود. دستور جدید را با همان الگو اضافه کن.

طبقه‌بندی tenant

جدول وضعیت
domain_events جفت tenant