# RULE FILE 08 — Service Layer & Integration Contract

How services are written. How they talk to each other. How events propagate. No ambiguity.

---

## Service Architecture — THE PATTERN

Every service follows this exact structure:

```php
namespace App\Domain\{Module}\Services;

use Illuminate\Support\Facades\DB;

class {Entity}Service
{
    // Constructor injection for dependencies (other services)
    public function __construct(
        private readonly WalletService $walletService,
        private readonly NotificationService $notificationService,
    ) {}

    // Public methods: business operations
    // Private methods: internal helpers
    
    public function create(array $data, User $actor): Model
    {
        return DB::transaction(function () use ($data, $actor) {
            // 1. Validate business rules (not form validation — that's FormRequest)
            // 2. Create the record
            // 3. Create related records (if atomic)
            // 4. Dispatch event (AFTER commit via afterCommit on event)
            
            $entity = Entity::create([...]);
            
            // Event dispatched AFTER transaction commits
            EntityCreated::dispatch($entity);
            
            return $entity;
        });
    }
}
```

### Service Rules — INVIOLABLE

1. **ALL writes wrapped in `DB::transaction()`** — no partial state
2. **Services receive explicit params** — never use `auth()`, `request()`, or `session()` inside services
3. **Services return the entity** — controllers/components decide what to do with it
4. **Services throw exceptions for business rule violations** — controllers catch and handle
5. **Services dispatch events** — never send emails, notifications, or trigger side effects directly
6. **One service per entity** — `InvoiceService` handles invoices, `PaymentService` handles payments
7. **Services CAN call other services** — via constructor injection. This is the ONLY way modules interact.
8. **Services NEVER call controllers or Livewire components**

---

## Cross-Module Service Dependencies — THE EXACT MAP

```
InvoiceService
  → uses: PaymentService (to check paid amount)
  → uses: PricingService (to calculate item prices)
  → dispatches: InvoiceCreated, InvoicePaid, InvoiceCancelled

PaymentService
  → uses: WalletService (for wallet payments)
  → uses: InvoiceService (to update invoice paid_amount)
  → uses: TransactionService (to create double-entry)
  → dispatches: PaymentReceived, PaymentRefunded

WalletService
  → standalone (no service dependencies)
  → dispatches: WalletDeposited, WalletWithdrawn

PricingService
  → uses: ParticipantService (to get participant context: age, siblings, classification)
  → uses: EnrollmentService (to count active enrollments for volume rules)
  → standalone otherwise (pure calculation, no side effects)
  → dispatches: nothing (it's a query, not a command)

ParticipantService
  → uses: EnrollmentService (to check active enrollments on status change)
  → dispatches: ParticipantStatusChanged, ParticipantRegistered

EnrollmentService
  → uses: InvoiceService (to create enrollment invoice)
  → uses: PricingService (to calculate fee)
  → uses: WaitlistService (to notify next when spot opens)
  → uses: AttendanceGenerationService (to create/remove expectations)
  → dispatches: EnrollmentCreated, EnrollmentCancelled

AttendanceService
  → uses: ParticipantService (for auto-suspension on threshold breach)
  → uses: NotificationService (for guardian alerts)
  → dispatches: AttendanceMarked, ParticipantAbsent

SessionService
  → uses: AttendanceGenerationService (generate records when session created)
  → uses: SpaceReservationService (reserve space for session)
  → dispatches: SessionCreated, SessionCancelled

SpaceReservationService
  → uses: SpaceCollisionService (check for conflicts)
  → dispatches: ReservationCreated, ReservationCancelled

InventoryService
  → standalone (manages movements)
  → dispatches: ProductLowStock, StockCountCompleted

POSService
  → uses: InvoiceService (create invoice for transaction)
  → uses: PaymentService (record payment)
  → uses: InventoryService (deduct stock)
  → uses: EnrollmentService (if program purchased)
  → uses: PricingService (calculate item prices)
  → uses: WalletService (if wallet payment)
  → uses: KitService (if kit sold)
  → dispatches: POSTransactionCompleted
```

### CIRCULAR DEPENDENCY PREVENTION

The map above has NO cycles. If you find yourself needing A→B→A, you're doing it wrong. Solutions:
- Use events (A dispatches event, listener calls B which doesn't need A)
- Extract shared logic into a third service
- Pass data directly instead of calling back

---

## Event System — HOW SIDE EFFECTS HAPPEN

### Event Contract

```php
namespace App\Domain\{Module}\Events;

class PaymentReceived implements ShouldDispatchAfterCommit
{
    public function __construct(
        public readonly Payment $payment,
        public readonly Invoice $invoice,
        public readonly User $actor,
    ) {}
}
```

**CRITICAL**: All domain events implement `ShouldDispatchAfterCommit`. This ensures the event only fires if the transaction commits. If it rolls back → no event → no side effects.

### Listener Contract

```php
namespace App\Domain\{Module}\Listeners;

class SendPaymentNotification implements ShouldQueue
{
    public function handle(PaymentReceived $event): void
    {
        // ONE listener does ONE thing
        // This one sends the notification
        // Another listener updates reports
        // Another listener checks if invoice is now fully paid
    }
}
```

**CRITICAL**: Listeners implement `ShouldQueue`. Side effects happen asynchronously. The user doesn't wait for email sending.

### Event → Listener Registration (Complete)

| Event | Listeners |
|-------|-----------|
| `InvoiceCreated` | `NotifyGuardianOfInvoice`, `LogAuditEntry` |
| `InvoicePaid` | `NotifyGuardianOfPaymentComplete`, `UpdateParticipantPaymentStatus`, `LogAuditEntry` |
| `InvoiceCancelled` | `ReverseFinancialTransactions`, `NotifyGuardianOfCancellation`, `LogAuditEntry` |
| `PaymentReceived` | `UpdateInvoicePaidAmount`, `UpdateCashSessionTotals`, `NotifyGuardianOfPayment`, `LogAuditEntry` |
| `PaymentRefunded` | `UpdateInvoiceBalance`, `UpdatePaymentStatus`, `NotifyGuardianOfRefund`, `LogAuditEntry` |
| `WalletDeposited` | `NotifyOwnerOfDeposit`, `LogAuditEntry` |
| `WalletWithdrawn` | `LogAuditEntry` |
| `ParticipantStatusChanged` | `NotifyGuardianOfStatusChange`, `HandleEnrollmentOnStatusChange`, `LogAuditEntry` |
| `EnrollmentCreated` | `GenerateAttendanceExpectations`, `UpdateGroupCount`, `NotifyTrainerOfNewStudent`, `LogAuditEntry` |
| `EnrollmentCancelled` | `RemoveFutureAttendance`, `UpdateGroupCount`, `ProcessWaitlist`, `CalculateRefund`, `LogAuditEntry` |
| `AttendanceMarked` | `UpdateSessionStats`, `CheckConsecutiveAbsences`, `NotifyGuardianIfAbsent`, `LogAuditEntry` |
| `SessionCancelled` | `CancelAttendanceRecords`, `FreeSpaceReservation`, `NotifyAffectedPeople`, `LogAuditEntry` |
| `POSTransactionCompleted` | `UpdatePOSSessionTotals`, `CheckLowStock`, `LogAuditEntry` |
| `ProductLowStock` | `NotifyAdminOfLowStock` |

### Event Rules

1. Events carry ONLY the data needed by listeners (the entity + actor)
2. Events are NOT the place for business logic — listeners handle that
3. One event can have MANY listeners — each does one thing
4. If a listener fails: it retries (queue retry). Does NOT roll back the original action.
5. Listener failure is NOT a reason to fail the user's request — the core action already committed.
6. Log listener failures for admin review (failed_jobs table)

---

## Exception Hierarchy

```php
namespace App\Domain\Shared\Exceptions;

// Base
class DomainException extends \RuntimeException {}

// Financial
class InsufficientBalanceException extends DomainException {}
class InvoiceAlreadyPaidException extends DomainException {}
class PaymentExceedsBalanceException extends DomainException {}
class InvalidRefundAmountException extends DomainException {}

// Participant
class InvalidStatusTransitionException extends DomainException {}
class ParticipantSuspendedException extends DomainException {}
class DuplicateEnrollmentException extends DomainException {}
class GroupAtCapacityException extends DomainException {}

// Inventory
class InsufficientStockException extends DomainException {}
class KitUnavailableException extends DomainException {}

// Scheduling
class SpaceCollisionException extends DomainException {}
class OutsideOperatingHoursException extends DomainException {}
class FacilityUnavailableException extends DomainException {}

// Assignment
class AssignmentConflictException extends DomainException {}

// Auth
class UnauthorizedException extends DomainException {}
class AccountLockedException extends DomainException {}
```

### Exception Handling in Controllers/Components

```php
// Livewire component
public function recordPayment(): void
{
    try {
        $payment = app(PaymentService::class)->recordPayment(
            invoice: $this->invoice,
            amount: $this->amount,
            method: $this->method,
            actor: auth()->user(),
        );
        session()->flash('success', __('تم تسجيل الدفعة بنجاح'));
        $this->redirect(route('invoices.show', $this->invoice));
    } catch (PaymentExceedsBalanceException $e) {
        $this->addError('amount', __('المبلغ يتجاوز الرصيد المستحق'));
    } catch (InvoiceAlreadyPaidException $e) {
        session()->flash('error', __('هذه الفاتورة مدفوعة بالكامل'));
        $this->redirect(route('invoices.show', $this->invoice));
    } catch (InsufficientBalanceException $e) {
        $this->addError('amount', __('رصيد المحفظة غير كافٍ'));
    }
}
```

---

## Model Relationship Cheat Sheet — THE EXACT DEFINITIONS

```php
// Organization (Academy)
hasMany: branches, users, people, participants, programs, facilities, ...everything

// User
belongsTo: Organization (academy_id)
hasOne: Person (via people.user_id)
belongsTo: Role (role_id)

// Person
belongsTo: Organization (academy_id)
belongsTo: User (user_id, nullable)
hasOne: Participant
hasOne: Guardian
hasMany: Assignments
hasMany: AttendanceRecords

// Participant
belongsTo: Organization (academy_id)
belongsTo: Person (person_id)
belongsTo: Branch (branch_id)
hasMany: Enrollments
hasMany: AttendanceRecords (through person)
belongsToMany: Guardians (via guardian_participant pivot)
hasMany: Invoices (via billable morph)
hasOne: Wallet (via owner morph)

// Guardian
belongsTo: Organization (academy_id)
belongsTo: Person (person_id)
belongsToMany: Participants (via guardian_participant pivot)

// TrainingProgram
belongsTo: Organization (academy_id)
belongsTo: Activity (activity_id)
hasMany: TrainingGroups

// TrainingGroup
belongsTo: Organization (academy_id)
belongsTo: TrainingProgram (training_program_id)
belongsTo: Branch (branch_id)
hasMany: Enrollments
hasMany: TrainingSessions
hasMany: Assignments (via morph)
hasMany: GroupSchedules

// TrainingSession
belongsTo: TrainingGroup (training_group_id)
belongsTo: Facility (facility_id)
belongsTo: Person as trainer (trainer_id)
hasMany: AttendanceRecords (via morph)
hasOne: SpaceReservation

// Enrollment
belongsTo: Participant (participant_id)
belongsTo: TrainingGroup (training_group_id)
belongsTo: Invoice (invoice_id, nullable)

// Invoice
belongsTo: Organization (academy_id)
morphTo: billable (Participant, Guardian, Person)
hasMany: InvoiceItems
hasMany: Payments
hasOne: PaymentPlan

// Payment
belongsTo: Invoice (invoice_id)
hasMany: Transactions (via reference morph)

// Facility
belongsTo: Organization (academy_id)
belongsTo: Branch (branch_id)
hasMany: SpaceLayouts
hasMany: FacilityOperatingHours

// SpaceLayout
belongsTo: Facility (facility_id)
hasMany: SpaceSegments
hasMany: SpaceReservations

// Assignment
belongsTo: Person (person_id)
morphTo: assignable (TrainingGroup, TrainingSession, Facility, Branch, Program)

// AttendanceRecord
belongsTo: Person (person_id)
morphTo: attendable (TrainingSession, Event, Shift)
belongsTo: Assignment (assignment_id, nullable)

// Product
belongsTo: Organization (academy_id)
belongsTo: ProductCategory (category_id)
hasMany: InventoryLevels
hasMany: InventoryMovements

// Wallet
morphTo: owner (Person, Guardian)
hasMany: WalletTransactions
```

---

## Scheduled Jobs — Complete Registry

| Job | Schedule | Module | Action |
|-----|----------|--------|--------|
| `AutoMarkAbsent` | Hourly | Attendance | Mark old 'expected' as 'absent' |
| `CheckConsecutiveAbsences` | Daily 6AM | Attendance | Suspend participants at threshold |
| `SendAttendanceReminders` | 30min after session start | Attendance | Remind trainers who haven't taken attendance |
| `ProcessOverdueInvoices` | Daily midnight | Financial | Mark overdue, send reminders |
| `ProcessOverdueInstallments` | Daily midnight | Financial | Mark overdue, charge late fees |
| `DetectDefaultedPlans` | Daily | Financial | 3 consecutive overdue → default plan |
| `LowStockAlerts` | Daily 8AM | Inventory | Notify admin of low stock |
| `RecalculateInventoryLevels` | Weekly Sunday 3AM | Inventory | Integrity check |
| `ExpiredStockCheck` | Daily | Inventory | Alert on expired items |
| `GenerateUpcomingSessions` | Weekly Monday 2AM | Training | Generate sessions 4 weeks ahead |
| `ProcessWaitlistNotifications` | Every 6 hours | Enrollment | Expire unresponded notifications, notify next |
| `FreezeExpiryCheck` | Daily midnight | Participant | End expired freezes |
| `CleanupFailedJobs` | Weekly | System | Archive old failed jobs |
| `PruneNotifications` | Monthly | System | Remove read notifications older than 90 days |
| `DatabaseBackup` | Daily 2AM | System | pg_dump to backup location |
