# RULE FILE 10 — Notifications, CMS Templates & Audit Rules

---

## Notification System Architecture

```
Event occurs → NotificationRouter → Channel Selection → Delivery → Log

1. Event dispatched (e.g., PaymentReceived)
2. Listener determines: who should be notified? (audience resolution)
3. For each recipient: check their notification preferences
4. For each enabled channel: queue the notification
5. Channel handler delivers (email, SMS, in-app)
6. Log the delivery (success/failure/pending)
```

---

## Notification Events — Complete Registry

| Event | Audience | Channels | Priority |
|-------|----------|----------|----------|
| `InvoiceCreated` | Guardian (primary) | in-app, email | normal |
| `PaymentReceived` | Guardian (primary) | in-app, SMS | normal |
| `PaymentOverdue` | Guardian (primary) | in-app, email, SMS | high |
| `InstallmentDue` | Guardian (primary) | in-app, SMS | high |
| `ParticipantAbsent` | Guardian (primary) | in-app, SMS | normal |
| `ConsecutiveAbsences` | Guardian + Admin | in-app, email, SMS | high |
| `ParticipantSuspended` | Guardian (primary) | in-app, email, SMS | high |
| `EnrollmentConfirmed` | Guardian (primary) | in-app, email | normal |
| `SessionCancelled` | All enrolled guardians + trainers | in-app, SMS | high |
| `WaitlistNotification` | Guardian (primary) | in-app, SMS, email | high |
| `EvaluationPublished` | Guardian (primary) | in-app, email | normal |
| `TrainerAbsent` | Admin + Branch Manager | in-app | high |
| `LowStockAlert` | Admin | in-app | normal |
| `RefundRequested` | Admin + Accountant | in-app | normal |
| `DailyClosingReminder` | Receptionist | in-app | normal |
| `FreezeExpiring` | Guardian (primary) | in-app, SMS | normal |

### Notification Priority Rules

- `high`: sent immediately (no batching)
- `normal`: can be batched (digest mode if user prefers)
- `low`: in-app only, no push

---

## Channel Handlers

### In-App (Always Available)

```php
// notifications table (Laravel default)
id, type, notifiable_type, notifiable_id, data (jsonb), read_at, created_at

// data structure
{
    "title": "تم استلام دفعة",
    "title_ar": "تم استلام دفعة",
    "body": "تم استلام مبلغ 500 ج.م للفاتورة #INV-2026-0142",
    "icon": "payment",
    "action_url": "/invoices/{uuid}",
    "priority": "normal"
}
```

### Email (via Poste.io)

Mail config:
```
MAIL_HOST=srv-captain--posteio
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_VERIFY_PEER=false
MAIL_FROM_ADDRESS=noreply@al-arcade.com
```

Rules:
1. All emails use Blade templates from `notification_templates` table
2. Email subjects are in Arabic (match audience language)
3. Emails include academy logo + name in header
4. Unsubscribe link in footer (toggles email preference for that notification type)
5. Max 1 email per recipient per notification event (no duplicates)

### SMS (Interface Only — Gateway TBD)

```php
interface SmsGateway
{
    public function send(string $phone, string $message): SmsResult;
    public function getBalance(): int;
    public function getDeliveryStatus(string $messageId): string;
}

// Development: LogSmsGateway (writes to storage/logs/sms.log)
// Production: VodafoneGateway or similar (when contracted)
```

Rules:
1. SMS messages max 160 chars (Arabic = 70 chars per segment)
2. SMS includes academy name at start: "أكاديمية الكابتن: {message}"
3. SMS sending is rate-limited: max 100/hour per academy
4. Phone number formatting: strip spaces, ensure +20 prefix for Egypt
5. Log every SMS attempt (success or failure) in `notification_logs`

---

## Notification Preferences

### Per-User Preferences Table

```
notification_preferences: id, user_id, academy_id,
notification_type (string — matches event name),
channel_email (boolean default true),
channel_sms (boolean default true),
channel_push (boolean default true),
digest_mode (boolean default false — batch into daily summary),
created_at, updated_at
```

### Resolution Rules

1. If no preference record exists for a user+type → use DEFAULTS (all channels enabled)
2. In-app notifications CANNOT be disabled (always delivered)
3. SMS can be disabled by user (expensive)
4. Email can be disabled by user
5. `digest_mode = true`: collect normal-priority notifications, send one daily email at 8AM
6. High-priority notifications IGNORE digest mode (always immediate)

---

## CMS Template System

### Templates Table

```
notification_templates: id, academy_id (nullable — null=platform default),
type, channel, name, subject (nullable — for email),
body, variables (jsonb — list of available variables),
is_active, is_default,
created_by, created_at, updated_at
```

### Template Types

```
CHECK (type IN ('invoice', 'receipt', 'payment_reminder', 'absence_notification',
    'enrollment_confirmation', 'session_cancelled', 'evaluation_published',
    'suspension_notice', 'waitlist_notification', 'freeze_notice',
    'welcome', 'password_reset', 'custom'))
```

### Template Channels

```
CHECK (channel IN ('email', 'sms', 'whatsapp', 'print'))
```

### Template Variables — Per Type

| Template Type | Available Variables |
|--------------|-------------------|
| `invoice` | `{{participant_name}}`, `{{invoice_number}}`, `{{total_amount}}`, `{{due_date}}`, `{{items_list}}`, `{{academy_name}}`, `{{balance_due}}` |
| `receipt` | `{{participant_name}}`, `{{payment_amount}}`, `{{payment_method}}`, `{{payment_date}}`, `{{invoice_number}}`, `{{remaining_balance}}`, `{{receipt_number}}` |
| `payment_reminder` | `{{participant_name}}`, `{{guardian_name}}`, `{{amount_due}}`, `{{due_date}}`, `{{days_overdue}}`, `{{invoice_number}}` |
| `absence_notification` | `{{participant_name}}`, `{{session_date}}`, `{{group_name}}`, `{{consecutive_count}}`, `{{attendance_rate}}` |
| `enrollment_confirmation` | `{{participant_name}}`, `{{program_name}}`, `{{group_name}}`, `{{start_date}}`, `{{schedule}}`, `{{fee_amount}}` |
| `session_cancelled` | `{{group_name}}`, `{{session_date}}`, `{{session_time}}`, `{{reason}}`, `{{trainer_name}}` |
| `evaluation_published` | `{{participant_name}}`, `{{program_name}}`, `{{evaluation_date}}`, `{{overall_score}}`, `{{link}}` |
| `suspension_notice` | `{{participant_name}}`, `{{reason}}`, `{{suspension_date}}`, `{{contact_info}}` |

### Template Rendering Service

```php
TemplateService::render(string $type, string $channel, array $variables, ?int $academyId = null): string

// Resolution order:
// 1. Academy-specific template for this type+channel (if exists)
// 2. Platform default template for this type+channel
// 3. Throw TemplateNotFoundException

// Variable replacement:
// str_replace(['{{var1}}', '{{var2}}'], [$val1, $val2], $template->body)

// Validation: ALL variables in template.variables JSON must be provided
// Missing variable → throw MissingTemplateVariableException (don't render with {{raw_brackets}})
```

### Template Rules

1. Academy can override platform defaults (their template takes priority)
2. Platform defaults are seeded at system setup (not per-academy)
3. Templates support Arabic text (stored as UTF-8)
4. SMS templates have max length indicator in editor (70 Arabic chars / 160 Latin)
5. Email templates support basic HTML (no scripts, no external resources)
6. Print templates include CSS inline (for DomPDF rendering)
7. Template editing: admin only. Preview before save. Version history (optional).

---

## Document Generation (PDF)

### Invoice PDF

Generated by: `DocumentService::generateInvoicePdf(Invoice $invoice): string`

Returns: file path to generated PDF in `storage/app/documents/invoices/`

Structure:
```
┌────────────────────────────────────────┐
│  [Academy Logo]  اسم الأكاديمية       │
│  العنوان │ الهاتف │ البريد الإلكتروني │
├────────────────────────────────────────┤
│  فاتورة رقم: INV-2026-0142            │
│  التاريخ: 29/06/2026                  │
│  إلى: أحمد محمد علي (ولي الأمر)       │
├────────────────────────────────────────┤
│  البند        │ الكمية │ السعر │ الإجمالي │
│  ────────────────────────────────────  │
│  اشتراك كرة قدم │ 1 │ 800 │ 800 ج.م │
│  طقم رياضي     │ 1 │ 700 │ 700 ج.م │
├────────────────────────────────────────┤
│  المجموع: 1,500 ج.م                   │
│  الخصم: -150 ج.م (خصم عائلي)         │
│  الإجمالي: 1,350 ج.م                  │
│  المدفوع: 800 ج.م                     │
│  المتبقي: 550 ج.م                     │
├────────────────────────────────────────┤
│  طريقة الدفع: نقدي                    │
│  ملاحظات: ...                          │
└────────────────────────────────────────┘
```

### PDF Rules

1. DomPDF with Arabic font embedded (Cairo or Noto Sans Arabic)
2. A4 size, portrait, 15mm margins
3. Academy branding loaded from academy settings (logo_path, colors)
4. All amounts formatted with `format_money()` helper
5. Generated PDFs stored in: `storage/app/documents/{type}/{year}/{month}/{filename}.pdf`
6. Filename: `{type}-{number}-{timestamp}.pdf` → `invoice-INV-2026-0142-20260629.pdf`
7. PDFs are generated ON DEMAND (not pre-cached) — cached for 1 hour after first generation

---

## Audit Log — THE IMMUTABLE RECORD

### Audit Log Table

```
audit_logs: id, academy_id,
user_id (FK users), user_name (denormalized — in case user deleted),
action (string), module (string),
auditable_type, auditable_id (polymorphic — what was affected),
old_values (jsonb, nullable), new_values (jsonb, nullable),
ip_address (string), user_agent (string),
metadata (jsonb),
created_at
```

**NO `updated_at`, NO `deleted_at`** — audit logs are APPEND-ONLY. NEVER modified.

### What Gets Logged — COMPLETE LIST

| Action | Module | When |
|--------|--------|------|
| `login` | auth | Successful login |
| `login_failed` | auth | Failed login attempt |
| `logout` | auth | User logs out |
| `impersonate_start` | auth | Admin starts impersonating |
| `impersonate_end` | auth | Admin stops impersonating |
| `created` | * | Any entity created |
| `updated` | * | Any entity updated (with old/new values) |
| `deleted` | * | Any entity soft-deleted |
| `status_changed` | participants | Status transition |
| `payment_received` | financial | Payment recorded |
| `payment_refunded` | financial | Refund processed |
| `invoice_cancelled` | financial | Invoice cancelled |
| `wallet_adjustment` | financial | Manual wallet change |
| `cash_session_closed` | financial | Session closed (with variance) |
| `permission_changed` | users | Role or permissions modified |
| `setting_changed` | settings | System setting modified |
| `export` | * | Data exported (what, who, when) |

### Audit Rules

1. EVERY write operation (create/update/delete) logs automatically via model observer
2. `old_values` stores the previous state, `new_values` stores the new state
3. Sensitive fields are EXCLUDED from logs: `password`, `remember_token`, `api_key`
4. Financial logs store the EXACT amounts (before and after)
5. Audit logs are queryable by: user, date range, module, action, entity
6. Retention: FOREVER for financial. 1 year for non-financial (configurable).
7. Admin can VIEW audit logs. NOBODY can edit or delete them.
8. SuperAdmin can view cross-tenant audit logs.

### Automatic Audit via Model Observer

```php
// Applied to all auditable models via trait
trait Auditable
{
    public static function bootAuditable(): void
    {
        static::created(fn ($model) => AuditService::log('created', $model));
        static::updated(fn ($model) => AuditService::log('updated', $model, $model->getOriginal()));
        static::deleted(fn ($model) => AuditService::log('deleted', $model));
    }
}
```

---

## System Settings

### Settings Table

```
system_settings: id, academy_id (nullable — null=platform level),
group (string), key (string), value (text),
type (string — string/integer/boolean/json/encrypted),
description, description_ar,
is_public (boolean — shown to non-admin users?),
created_at, updated_at,
UNIQUE(academy_id, key)
```

### Setting Groups & Keys

| Group | Key | Type | Default | Description |
|-------|-----|------|---------|-------------|
| `general` | `timezone` | string | `Africa/Cairo` | Academy timezone |
| `general` | `date_format` | string | `d/m/Y` | Display date format |
| `general` | `currency_symbol` | string | `ج.م` | Currency display |
| `general` | `language` | string | `ar` | Primary language |
| `attendance` | `grace_period_minutes` | integer | `15` | Late threshold |
| `attendance` | `trainer_grace_period_minutes` | integer | `10` | Trainer late threshold |
| `attendance` | `auto_absent_hours` | integer | `2` | Hours after session to auto-mark |
| `attendance` | `consecutive_absence_threshold` | integer | `5` | Before auto-suspend |
| `attendance` | `min_attendance_percent` | integer | `75` | Minimum for good standing |
| `financial` | `max_total_discount_percent` | integer | `50` | Global discount cap |
| `financial` | `refund_approval_threshold` | integer | `50000` | Piasters above which needs approval |
| `financial` | `invoice_due_days` | integer | `7` | Days until invoice overdue |
| `financial` | `late_fee_amount` | integer | `5000` | Late fee in piasters |
| `financial` | `late_fee_grace_days` | integer | `3` | Days after due before late fee |
| `enrollment` | `waitlist_response_hours` | integer | `48` | Hours to respond to offer |
| `enrollment` | `max_freezes_per_year` | integer | `2` | Freeze limit |
| `enrollment` | `max_freeze_days` | integer | `30` | Maximum freeze duration |
| `notification` | `sms_enabled` | boolean | `false` | SMS gateway configured? |
| `notification` | `email_enabled` | boolean | `true` | Email sending enabled? |
| `notification` | `digest_time` | string | `08:00` | When to send daily digest |
| `academy` | `logo_path` | string | null | Academy logo file |
| `academy` | `primary_color` | string | `#1d4ed8` | Brand color |
| `academy` | `receipt_footer` | string | null | Text at bottom of receipts |

### Settings Access

```php
// Reading (with caching)
$value = Setting::get('attendance.grace_period_minutes', 15);
// Uses: academy-scoped first, falls back to platform default, then hardcoded default

// Writing
Setting::set('attendance.grace_period_minutes', 20);
// Updates if exists, creates if not. Audit logged.
```
