# رویدادهای دامنه سیستم وقتی چیزی اتفاق می‌افتد یک **رویداد** ثبت می‌کند تا بقیه (پیامک، حسابداری، گزارش) واکنش نشان بدهند — بدون اینکه دامنهٔ نوبت‌دهی از وجودشان خبر داشته باشد. مرجع: بند ۱۶ مستند طراحی. اندپوینت عیب‌یابی: [../api/reports.md](../api/reports.md) --- ## سه قاعدهٔ غیرقابل‌مذاکره ۱. **payload فقط uuid و اسکالر است.** هیچ entity ای در رویداد نیست؛ مصرف‌کننده خودش واکشی می‌کند. entity در پیام async یعنی سریال‌سازی، detach شدن، و دادهٔ کهنه. `DomainEventLog` مقادیر غیراسکالر را **حذف** می‌کند، نه اینکه سریال‌شان کند. ۲. **انتشار بعد از commit.** ردیف رویداد در همان تراکنشی نوشته می‌شود که تغییر را انجام می‌دهد؛ انتشار جداست. ۳. **هر رویداد محیط دارد.** بدون `entity_type`/`entity_id`، پیامک کلینیک الف به شمارهٔ کلینیک ب می‌رود. --- ## چرا صندوق خروجی (outbox) بدون آن دو حالت شکست ممکن است: | حالت | نتیجه | |---|---| | انتشار پیش از commit، بعد rollback | پیامک رفته، نوبتی وجود ندارد | | commit موفق، انتشار شکست خورد (Redis down) | نوبت هست، هیچ‌کس مطلع نشد | با outbox ردیف رویداد **در همان تراکنش** ثبت می‌شود و یک worker بعداً منتشرش می‌کند: ```bash ddev exec php bin/console app:events:publish --limit=100 ``` حداکثر **تأخیر** داریم، هرگز گم‌شدن. `DomainEventPublisher::record()` عمداً flush نمی‌کند — همان چیزی که تضمین می‌کند رویداد با تراکنشِ برگشته از بین برود. جایی که فراخوان تراکنش باز ندارد، `recordAndFlush()` هست. ### ردیف مرده بعد از پنج تلاش ناموفق، ردیف با `last_error` **باقی می‌ماند** و دیگر برداشته نمی‌شود. حذف خاموش یعنی رویداد گم‌شدهٔ بی‌رد؛ ادمین باید بتواند ببیند چه چیزی منتشر نشد و چرا. --- ## `AppointmentEvent` یا `DomainEventLog`؟ هر دو ماندند و کارشان یکی نیست: | | `AppointmentEvent` | `DomainEventLog` | |---|---|---| | چیست | تاریخچهٔ تغییر وضعیت **یک نوبت** | اعلان تغییر به بیرونِ دامنه | | مخاطب | خودِ صفحهٔ نوبت | پیامک، حسابداری، گزارش | | دامنه | فقط نوبت | همهٔ دامنه‌ها | | مصرف | خوانده می‌شود | منتشر می‌شود | ادغامشان یعنی تاریخچهٔ نوبت به صف پیام تبدیل شود، یا صف پیام پر از جزئیاتی که فقط یک صفحه لازم دارد. --- ## فهرست رویدادها ``` HoldCreated AppointmentBooked AppointmentCancelled AppointmentRescheduled PatientNoShow AppointmentCompleted ResourceBlocked ResourceReleased CourseStarted CourseSessionCompleted CourseCompleted PackagePurchased CreditConsumed CreditRefunded ``` فهرست **بسته** است (`DomainEvents::ALL`) و نام ناشناخته استثنا می‌دهد: مصرف‌کننده روی رشته شرط می‌گذارد، و تایپوی یک حرفی یعنی رویدادی که هیچ‌کس نمی‌شنود و هیچ خطایی هم نمی‌دهد. ### وضعیت فعلی انتشار | رویداد | کجا ثبت می‌شود | |---|---| | `HoldCreated` | `HoldService::hold()` — بعد از گرفتن همهٔ منابع | | `AppointmentBooked` | `BookingService::confirm()` | | `AppointmentCancelled` | `BookingService::cancel()` | | `PatientNoShow` | `NoShowService::record()` | | `CourseStarted` | `CourseStarter::start()` | | `CourseSessionCompleted` · `CourseCompleted` | `CourseSessionLinker::complete()` | | `PackagePurchased` | `PackageSalesService::sell()` | | `CreditConsumed` · `CreditRefunded` | `CreditLedgerService` | `AppointmentRescheduled`، `AppointmentCompleted`، `ResourceBlocked` و `ResourceReleased` هنوز نقطهٔ ثبت ندارند: مسیرهایشان (جابه‌جایی نوبت، تکمیل دستی، بلوک منبع) از تسک‌های قبلی‌اند و دست‌زدن به آن‌ها بیرون از دامنهٔ این تسک بود. --- ## مصرف‌کنندهٔ تازه `DomainEventHandler` فقط لاگ می‌کند و **نباید بیشتر بکند**؛ درزِ اتصال است. مصرف‌کنندهٔ تازه کنارش ثبت می‌شود و باید **idempotent** باشد: messenger ممکن است پیام را دوباره تحویل بدهد، و `DomainEventMessage::$uuid` همان شناسه‌ای است که با آن تکراری را می‌شناسد. تکرار مسئلهٔ مصرف‌کننده است، نه رویداد: تضمین «دقیقاً یک بار» در صف توزیع‌شده وجود ندارد.