# Cycle 1: Event & Listener Infrastructure

## Goal
Create all 35+ domain events and their listeners so side effects (notifications, counter updates, audit entries) fire reliably after successful commits.

---

## Architecture

```
app/Domain/{Module}/Events/       — Event classes (data carriers)
app/Domain/{Module}/Listeners/    — Listener classes (one side effect each)
app/Providers/EventServiceProvider.php — Mapping
```

### Every Event MUST:
1. Implement `ShouldDispatchAfterCommit`
2. Use `Dispatchable`, `SerializesModels` traits
3. Accept the entity AND the `User $actor` in constructor
4. Be dispatched from the service layer inside `DB::transaction`

### Every Listener MUST:
1. Implement `ShouldQueue` (async — side effects never block the request)
2. Have a `handle(EventClass $event)` method
3. Do exactly ONE thing (send notification, update counter, log, etc.)
4. Never throw — catch + log failures

---

## Events to Create

### Financial Domain (app/Domain/Financial/Events/)

| Event Class | Dispatched From | Carries |
|-------------|-----------------|---------|
| `InvoiceSent` | InvoiceService::send() | Invoice, User |
| `InvoiceOverdue` | MarkOverdueInvoices command | Invoice |
| `InvoiceCancelled` | InvoiceService::cancel() | Invoice, User |
| `RefundRequested` | RefundService::request() | Refund, User |
| `RefundApproved` | RefundService::approve() | Refund, User |
| `RefundRejected` | RefundService::reject() | Refund, User |
| `InstallmentOverdue` | CheckInstallments command | Installment |
| `PaymentPlanDefaulted` | CheckPaymentPlanDefaults command | PaymentPlan |
| `WalletDeposited` | WalletService::deposit() | Wallet, amount, User |
| `WalletFrozen` | WalletService::freeze() | Wallet, User |
| `CashSessionClosed` | CashSessionService::close() | CashSession, User |
| `DailyClosingReady` | GenerateDailyClosing command | DailyClosing |

### Training Domain (app/Domain/Training/Events/)

| Event Class | Dispatched From | Carries |
|-------------|-----------------|---------|
| `ParticipantRegistered` | ParticipantService::register() | Participant, User |
| `ParticipantStatusChanged` | ParticipantService::changeStatus() | Participant, oldStatus, newStatus, User |
| `EnrollmentCreated` | EnrollmentService::enroll() | Enrollment, User |
| `EnrollmentCancelled` | EnrollmentService::cancel() | Enrollment, reason, User |
| `EnrollmentCompleted` | EnrollmentService::complete() | Enrollment, User |
| `WaitlistSpotAvailable` | EnrollmentService::cancel() [after decrement] | TrainingGroup, Waitlist |
| `SessionCreated` | SessionGeneratorService::generate() | TrainingSession |
| `SessionCancelled` | SessionGeneratorService::cancelSession() | TrainingSession, reason, User |
| `SessionCompleted` | SessionCompleteCommand | TrainingSession |
| `EvaluationShared` | EvaluationService::share() | Evaluation, User |

### Participant Domain (app/Domain/Participant/Events/)

| Event Class | Dispatched From | Carries |
|-------------|-----------------|---------|
| `ParticipantSuspended` | ParticipantService::suspend() | Participant, reason, User |
| `ParticipantFrozen` | ParticipantService::freeze() | Participant, reason, User |

### Attendance Domain (app/Domain/Attendance/Events/)

| Event Class | Dispatched From | Carries |
|-------------|-----------------|---------|
| `AttendanceMarked` | AttendanceMarkingService::markStatus() | AttendanceRecord, User |
| `ParticipantAbsent` | MarkAutoAbsent command | AttendanceRecord |
| `TrainerAbsent` | MarkAutoAbsent command | AttendanceRecord |
| `AttendanceThresholdBreached` | EnforceThresholds command | Participant, rate, threshold |

### Facility Domain (app/Domain/Facility/Events/)

| Event Class | Dispatched From | Carries |
|-------------|-----------------|---------|
| `SpaceReservationCreated` | ReservationService::reserve() | SpaceReservation, User |
| `SpaceCollisionDetected` | SpaceCollisionService::check() | Facility, conflictDetails |
| `FacilityStatusChanged` | FacilityService::changeStatus() | Facility, oldStatus, newStatus, User |

### Scheduling Domain (app/Domain/Scheduling/Events/)

| Event Class | Dispatched From | Carries |
|-------------|-----------------|---------|
| `AssignmentCreated` | AssignmentService::assign() | Assignment, User |
| `AssignmentEnded` | AssignmentService::changeStatus('completed') | Assignment, User |

### Inventory Domain (app/Domain/Inventory/Events/)

| Event Class | Dispatched From | Carries |
|-------------|-----------------|---------|
| `ProductLowStock` | InventoryService::createMovement() [after check] | Product, InventoryLevel, Warehouse |
| `PurchaseOrderReceived` | PurchaseOrderService::receive() | PurchaseOrder, User |
| `StockCountCompleted` | StockCountService::completeCount() | StockCount, User |
| `KitAssembled` | KitService::assemble() | Kit, quantity, User |

---

## Listeners to Create

### Notification Listeners

| Listener | Handles | Action |
|----------|---------|--------|
| `SendInvoiceNotification` | InvoiceCreated, InvoiceSent | Notify billable via NotificationService |
| `SendOverdueReminder` | InvoiceOverdue | Notify billable + admin |
| `SendPaymentConfirmation` | PaymentReceived | Notify payer |
| `NotifyGuardianOfAbsence` | ParticipantAbsent | Notify primary guardian |
| `NotifyAdminLowStock` | ProductLowStock | Notify inventory managers |
| `NotifyWaitlistSpot` | WaitlistSpotAvailable | Notify first in queue |
| `NotifySessionCancellation` | SessionCancelled | Notify all enrolled participants |
| `NotifyEvaluationShared` | EvaluationShared | Notify participant + guardian |
| `NotifyRefundStatus` | RefundApproved, RefundRejected | Notify requester |
| `NotifyThresholdBreach` | AttendanceThresholdBreached | Notify guardian + admin |
| `NotifyInstallmentDue` | InstallmentOverdue | Notify plan holder |

### Counter/Denormalization Listeners

| Listener | Handles | Action |
|----------|---------|--------|
| `UpdateGroupCount` | EnrollmentCreated, EnrollmentCancelled | Increment/decrement training_groups.current_count |
| `UpdateInvoicePaidAmount` | PaymentReceived | Recalculate invoice.paid_amount, balance_due, status |
| `UpdateCashSessionTotals` | PaymentReceived (cash) | Update cash_sessions.total_cash_in, transactions_count |
| `CheckLowStockThreshold` | InventoryMovement (via event) | Compare level vs min_stock, dispatch ProductLowStock |

### Process Listeners

| Listener | Handles | Action |
|----------|---------|--------|
| `ProcessWaitlist` | EnrollmentCancelled | Check waitlist, auto-promote first |
| `GenerateAttendanceRecords` | EnrollmentCreated | Create expected records for future sessions |
| `RemoveAttendanceRecords` | EnrollmentCancelled | Delete expected records for future sessions |
| `CancelLinkedReservation` | SessionCancelled | Set reservation status to cancelled |
| `CreateAutoReservation` | SessionCreated | Auto-create space reservation if group has assigned space |
| `ActivateEnrollmentOnPayment` | InvoicePaid | If pending enrollment invoice is fully paid, activate enrollment |
| `SuspendOnThreshold` | AttendanceThresholdBreached | Auto-suspend participant if consecutive absences >= 5 |

---

## Event Template

```php
<?php

namespace App\Domain\{Module}\Events;

use App\Domain\{Module}\Models\{Entity};
use App\Models\User;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class {EventName} implements ShouldDispatchAfterCommit
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public {Entity} ${entity},
        public User $actor,
    ) {}
}
```

---

## Listener Template

```php
<?php

namespace App\Domain\{Module}\Listeners;

use App\Domain\{Module}\Events\{EventName};
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;

class {ListenerName} implements ShouldQueue
{
    public function handle({EventName} $event): void
    {
        try {
            // ONE side effect here
        } catch (\Throwable $e) {
            Log::error('{ListenerName} failed: ' . $e->getMessage(), [
                'event' => class_basename($event),
                'entity_id' => $event->{entity}->id,
            ]);
        }
    }

    public function failed({EventName} $event, \Throwable $exception): void
    {
        Log::critical('{ListenerName} PERMANENTLY FAILED', [
            'entity_id' => $event->{entity}->id,
            'error' => $exception->getMessage(),
        ]);
    }
}
```

---

## EventServiceProvider Registration

```php
protected $listen = [
    // Financial
    InvoiceCreated::class => [SendInvoiceNotification::class],
    InvoicePaid::class => [UpdateInvoicePaidAmount::class, ActivateEnrollmentOnPayment::class],
    PaymentReceived::class => [SendPaymentConfirmation::class, UpdateCashSessionTotals::class],
    InvoiceOverdue::class => [SendOverdueReminder::class],
    // Training
    EnrollmentCreated::class => [UpdateGroupCount::class, GenerateAttendanceRecords::class],
    EnrollmentCancelled::class => [UpdateGroupCount::class, RemoveAttendanceRecords::class, ProcessWaitlist::class],
    SessionCancelled::class => [NotifySessionCancellation::class, CancelLinkedReservation::class],
    SessionCreated::class => [CreateAutoReservation::class],
    // Attendance
    ParticipantAbsent::class => [NotifyGuardianOfAbsence::class],
    AttendanceThresholdBreached::class => [NotifyThresholdBreach::class, SuspendOnThreshold::class],
    // Inventory
    ProductLowStock::class => [NotifyAdminLowStock::class],
    // ... etc
];
```

---

## Services to Update (add event dispatches)

| Service | Method | Event to Dispatch |
|---------|--------|-------------------|
| ParticipantService | register() | ParticipantRegistered |
| ParticipantService | changeStatus() | ParticipantStatusChanged |
| ParticipantService | freeze() | ParticipantFrozen |
| ParticipantService | suspend() | ParticipantSuspended |
| EnrollmentService | enroll() | EnrollmentCreated |
| EnrollmentService | cancel() | EnrollmentCancelled, WaitlistSpotAvailable |
| EnrollmentService | complete() | EnrollmentCompleted |
| SessionGeneratorService | generate() | SessionCreated (per session) |
| SessionGeneratorService | cancelSession() | SessionCancelled |
| EvaluationService | share() | EvaluationShared |
| FacilityService | changeStatus() | FacilityStatusChanged |
| ReservationService | reserve() | SpaceReservationCreated |
| AssignmentService | assign() | AssignmentCreated |
| WalletService | deposit() | WalletDeposited |
| CashSessionService | close() | CashSessionClosed |
| InventoryService | createMovement() | [check threshold → ProductLowStock] |
| PurchaseOrderService | receive() | PurchaseOrderReceived |
| StockCountService | completeCount() | StockCountCompleted |
| KitService | assemble() | KitAssembled |

---

## Verification Checklist

- [ ] `grep -r "implements ShouldDispatchAfterCommit" app/Domain | wc -l` = 35+
- [ ] `grep -r "implements ShouldQueue" app/Domain | wc -l` = 25+
- [ ] `php artisan event:list` shows all mappings
- [ ] Queue worker processes test event successfully
- [ ] NotificationService receives events and creates notification_logs
- [ ] Group count updates when enrollment created/cancelled
- [ ] Attendance records generated when enrollment activated
