# Phase 18-20 — CMS, Notifications, Documents & Audit

## Goal

Editable template system for all outbound communications and documents. Multi-channel notifications. Complete audit trail for compliance.

---

# PART A: TEMPLATE ENGINE (CMS)

## Entity: `templates`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | Nullable — null = system-wide default |
| name | string | Human-friendly name |
| name_ar | string | |
| slug | string | Unique per academy + type |
| type | string(20) | CHECK: invoice, receipt, sms, email, whatsapp, contract, certificate, report, custom |
| channel | string(20) | CHECK: print, email, sms, whatsapp, push, internal |
| subject | string | Nullable — for email/push |
| subject_ar | string | Nullable |
| body | text | Template content (HTML for print/email, plain for SMS) |
| body_ar | text | Nullable — Arabic version |
| variables | jsonb | Default '[]' — available variables with descriptions |
| css | text | Nullable — custom CSS for print templates |
| header_html | text | Nullable — reusable header for print |
| footer_html | text | Nullable — reusable footer for print |
| paper_size | string(10) | Nullable — CHECK: a4, a5, letter, receipt_80mm |
| orientation | string(10) | Nullable — CHECK: portrait, landscape |
| is_default | boolean | Default false — one default per academy+type |
| is_system | boolean | Default false — system templates can't be deleted |
| is_active | boolean | Default true |
| version | integer | Default 1 — increment on edit (version history) |
| preview_data | jsonb | Default '{}' — sample data for live preview |
| created_by | FK users | Nullable |
| updated_by | FK users | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | |

### Template Variables System

Each template type has registered variables that the rendering engine injects:

**Invoice/Receipt templates:**
```
{{academy_name}}        — Academy name
{{academy_name_ar}}     — Academy name (Arabic)
{{academy_logo}}        — Logo URL
{{academy_address}}     — Academy address
{{academy_phone}}       — Academy phone
{{invoice_number}}      — INV-202601-000001
{{invoice_date}}        — Issue date
{{due_date}}            — Payment due date
{{contact_name}}        — Bill-to name
{{contact_phone}}       — Bill-to phone
{{contact_address}}     — Bill-to address
{{items}}               — Loop: array of line items [{description, qty, unit_price, total}]
{{subtotal}}            — Before discount/tax
{{discount_label}}      — "خصم 15%"
{{discount_amount}}     — Discount value
{{tax_label}}           — "ضريبة 14%"
{{tax_amount}}          — Tax value
{{total_amount}}        — Final total
{{paid_amount}}         — Already paid
{{due_amount}}          — Remaining
{{payment_method}}      — How they paid
{{currency}}            — EGP
{{qr_code}}             — QR for verification (future)
{{notes}}               — Invoice notes
{{terms}}               — Payment terms
```

**SMS templates:**
```
{{participant_name}}    — First name
{{guardian_name}}       — Guardian name
{{amount_due}}          — Amount in EGP
{{due_date}}            — Due date
{{program_name}}        — Program enrolled in
{{session_date}}        — Next session date
{{session_time}}        — Next session time
{{academy_name}}        — Academy name
{{link}}                — Short link (payment, portal)
```

**Email templates:**
Same as SMS plus full HTML support, images, tables, styled buttons.

**Certificate templates:**
```
{{participant_name}}    — Full name
{{participant_name_ar}} — Arabic name
{{program_name}}        — Program completed
{{activity_name}}       — Sport/activity
{{completion_date}}     — Date of completion
{{trainer_name}}        — Head trainer
{{grade}}               — Final grade/score
{{certificate_number}}  — Unique cert number
{{academy_name}}        — Academy
{{academy_logo}}        — Logo
{{qr_verification}}     — QR code for cert verification
```

**Contract templates:**
```
{{guardian_name}}        — Legal guardian name
{{guardian_national_id}} — Guardian ID
{{participant_name}}     — Child name
{{program_name}}         — Program
{{start_date}}           — Start date
{{end_date}}             — End date
{{total_amount}}         — Total fee
{{payment_terms}}        — Payment schedule description
{{cancellation_terms}}   — Cancellation policy
{{date}}                 — Today's date
{{signature_box}}        — Placeholder for signature
```

### Template Rendering Service

```php
TemplateService::render(Template $template, array $data, string $locale = 'ar'): string
```

1. Load template body (or body_ar based on locale)
2. Parse variables: find all `{{var_name}}` patterns
3. For loop variables (like `{{items}}`): expand into HTML rows using a sub-template
4. Replace all variables with provided data
5. Wrap in header/footer if print template
6. Apply CSS
7. Return rendered HTML

For PDF generation: rendered HTML → DomPDF → PDF binary

### Template Editor (UI)

- Left panel: available variables as clickable chips
- Center: textarea/code editor with template body
- Right panel: live preview with sample data
- Toolbar: bold, italic, insert table, insert image, switch locale
- Save: increments version, preserves old version for rollback
- Test: render with sample data and show/download

---

# PART B: NOTIFICATION SYSTEM

## Architecture

```
Business Event → NotificationService → Channel Router → Channel Handler → Delivery + Log

Example flow:
InvoiceOverdue event
  → NotificationService finds applicable template + recipient preferences
  → Routes to: SMS (guardian phone) + Email (guardian email) + Internal (academy dashboard)
  → Each channel handler renders template with invoice data
  → Delivers + logs result
```

## Entity: `notification_preferences`

Per-user preferences for what they want to receive and how.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| user_id | FK users | |
| person_id | FK people | Nullable — for non-user people (guardians without login) |
| event_type | string(50) | e.g., "invoice_overdue", "session_reminder", "attendance_absent" |
| channel_email | boolean | Default true |
| channel_sms | boolean | Default true |
| channel_whatsapp | boolean | Default false |
| channel_push | boolean | Default true |
| channel_internal | boolean | Default true |
| is_enabled | boolean | Default true — master switch |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `notification_logs`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| channel | string(20) | CHECK: sms, email, whatsapp, push, internal |
| event_type | string(50) | What triggered this |
| template_id | FK templates | Nullable — which template was used |
| recipient_type | string | Polymorphic — User, Person |
| recipient_id | bigint | |
| recipient_address | string | Phone number, email, or device token |
| subject | string | Nullable — for email |
| body | text | Rendered content that was sent |
| status | string(20) | CHECK: queued, sending, sent, delivered, failed, bounced, rejected |
| sent_at | timestamp | Nullable |
| delivered_at | timestamp | Nullable |
| failed_at | timestamp | Nullable |
| failure_reason | text | Nullable |
| retry_count | integer | Default 0 |
| max_retries | integer | Default 3 |
| next_retry_at | timestamp | Nullable |
| gateway_message_id | string | Nullable — external ID from SMS/email provider |
| gateway_response | jsonb | Default '{}' — full provider response |
| cost | bigint | Nullable — piasters (SMS cost tracking) |
| metadata | jsonb | Default '{}' |
| created_at | timestamp | |
| updated_at | timestamp | |

### Channel Handlers

**Email (poste.io):**
```env
MAIL_MAILER=smtp
MAIL_HOST=srv-captain--posteio
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_VERIFY_PEER=false          # Self-signed cert inside Docker
MAIL_USERNAME=noreply@al-arcade.com
MAIL_FROM_ADDRESS=noreply@al-arcade.com
MAIL_FROM_NAME="${APP_NAME}"
```
- Laravel's built-in Mail with Mailable classes
- Queued (never send synchronously — blocks request)
- Retry on failure: 3 attempts with exponential backoff
- Track bounces via webhook (future)

**SMS:**
- Interface: `SmsGatewayInterface::send(string $phone, string $message): SmsResult`
- Implementation TBD (Twilio, Vonage, local Egyptian gateway)
- For now: `LogSmsGateway` that logs to notification_logs without sending
- Configuration stored in `system_settings`:
  - `sms_gateway`: twilio, vonage, local, log
  - `sms_api_key`: encrypted
  - `sms_sender_id`: "ElCaptain"
  - `sms_country_code`: "+20"
- Character limit: 160 per segment (count in Arabic = ~70 chars per segment due to UCS-2)
- Cost tracking: log cost per message for reporting

**WhatsApp (future):**
- Business API integration
- Template messages only (WhatsApp policy)
- Placeholder for now

**Push Notifications (future):**
- Firebase Cloud Messaging
- Placeholder: `push_token` on users table (nullable)

**Internal Notifications:**
- Database-stored notifications (Laravel's built-in Notification system)
- Shown in-app: bell icon with unread count
- Read/unread tracking

### Notification Event Registry

| Event | Default Channels | Recipients | Template |
|-------|-----------------|-----------|----------|
| `invoice_created` | email, internal | Billable person | invoice_created_email |
| `invoice_overdue` | sms, email, internal | Billable + guardian | invoice_overdue_sms |
| `payment_received` | sms, email | Payer | payment_confirmation_sms |
| `installment_reminder` | sms | Billable + guardian | installment_reminder_sms |
| `session_reminder` | sms, push | Participant + guardian | session_reminder_sms |
| `session_cancelled` | sms, email, push | All group participants + trainer | session_cancelled_sms |
| `attendance_absent` | sms | Guardian | attendance_absent_sms |
| `participant_suspended` | email, sms | Guardian | suspension_notification |
| `enrollment_confirmed` | email, sms | Participant + guardian | enrollment_confirmation |
| `evaluation_shared` | email, push | Guardian | evaluation_ready |
| `waitlist_spot` | sms, email, push | Waitlisted participant | waitlist_available |
| `medical_clearance_expiring` | email | Participant + guardian | medical_expiring |
| `trainer_session_reminder` | push, internal | Trainer | trainer_session_reminder |
| `refund_approved` | email, sms | Customer | refund_approved_notification |
| `low_stock_alert` | internal | Admin | low_stock_internal |
| `cash_session_discrepancy` | internal | Branch manager | cash_discrepancy_alert |

---

# PART C: DOCUMENT GENERATION

## Entity: `generated_documents`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| template_id | FK templates | Which template was used |
| document_type | string(20) | CHECK: invoice, receipt, contract, certificate, report, custom |
| documentable_type | string | Polymorphic — Invoice, Enrollment, Participant |
| documentable_id | bigint | |
| title | string | |
| file_path | string | Stored PDF path |
| file_size | integer | Bytes |
| format | string(10) | CHECK: pdf, html |
| locale | string(5) | ar or en |
| generated_by | FK users | |
| generated_at | timestamp | |
| downloaded_count | integer | Default 0 |
| emailed_at | timestamp | Nullable — if sent via email |
| printed_at | timestamp | Nullable — if printed |
| metadata | jsonb | Default '{}' — rendering context snapshot |
| created_at | timestamp | |

### Document Generation Service

```php
DocumentService::generate(Template $template, Model $entity, string $locale): GeneratedDocument
```

1. Resolve data from entity (invoice → items, totals, contact; enrollment → program, participant, etc.)
2. Render template with data (TemplateService::render)
3. Convert HTML to PDF (DomPDF)
4. Store PDF in filesystem (`storage/documents/{academy_uuid}/{type}/{uuid}.pdf`)
5. Create generated_documents record
6. Return document (for download or email attachment)

### Certificate Verification
- Each certificate gets a unique number
- QR code on certificate links to verification URL
- Public route: `GET /verify/{certificate_number}` → shows cert details without auth
- Prevents forged certificates

---

# PART D: AUDIT & COMPLIANCE

## Entity: `audit_logs`

General-purpose change tracking. Every significant business action.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| academy_id | FK organizations | Nullable — null for super admin actions |
| user_id | FK users | Who did it |
| action | string(30) | created, updated, deleted, restored, status_changed, exported, imported, login, logout |
| auditable_type | string | What was affected (model class) |
| auditable_id | bigint | Which record |
| description | string | Human-readable: "Created invoice INV-202601-000001" |
| old_values | jsonb | Nullable — previous state (for updates) |
| new_values | jsonb | Nullable — new state (for creates/updates) |
| changed_fields | jsonb | Default '[]' — which fields changed (for quick filtering) |
| ip_address | string(45) | |
| user_agent | text | Nullable |
| url | string | The route/URL that triggered this |
| method | string(10) | HTTP method: GET, POST, PUT, DELETE |
| metadata | jsonb | Default '{}' — extra context |
| created_at | timestamp | IMMUTABLE — never edited |

### What Gets Audited (Automatically via Trait)

Every model with `Auditable` trait gets:
- `created` → log with new_values (all fields)
- `updated` → log with old_values (changed fields only) + new_values
- `deleted` → log with old_values (full record)
- `restored` → log

### What Gets Audited (Manually via Service)

- Login success/failure (with IP + device)
- Permission changes (role assigned/removed)
- Impersonation start/end
- Export actions (who exported what data)
- Settings changes
- Template edits
- Approval/rejection actions

### Audit Retention Policy
- Configurable per academy (default: 2 years)
- Financial audit logs: 7 years (legal requirement in Egypt)
- Login history: 1 year
- Purge job: archives old records to cold storage (future)

### Audit Access Rules
- Only `academy_owner` and users with `audit.logs.view` permission can see audit logs
- Users can see their own login history
- SuperAdmin sees all audits across tenants
- Audit records can NEVER be edited or deleted (immutable by design)
- If someone tries to tamper with audit logs → separate integrity check (hash chain, future)

---

## Entity: `system_settings`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| academy_id | FK organizations | Nullable — null = global system setting |
| group | string(30) | Grouping: general, financial, notification, sms, email, security, localization |
| key | string(50) | Setting key |
| value | text | Setting value (JSON-encoded for complex values) |
| type | string(20) | CHECK: string, integer, boolean, json, encrypted |
| is_encrypted | boolean | Default false — for sensitive values (API keys, passwords) |
| description | string | Nullable — what this setting does |
| updated_by | FK users | Nullable |
| updated_at | timestamp | |

### System Settings Keys

**General:**
- `app_name`, `app_name_ar`, `app_logo`, `support_email`, `support_phone`

**Financial:**
- `default_currency`, `tax_rate`, `tax_enabled`, `invoice_prefix`, `receipt_prefix`
- `refund_approval_threshold`, `auto_overdue_days`, `late_fee_enabled`
- `installment_grace_period_days`, `installment_default_after_missed`

**SMS:**
- `sms_gateway` (twilio/vonage/local/log)
- `sms_api_key` (encrypted)
- `sms_api_secret` (encrypted)
- `sms_sender_id`
- `sms_country_code`
- `sms_enabled`

**Email:**
- `smtp_host`, `smtp_port`, `smtp_username`, `smtp_password` (encrypted)
- `smtp_encryption`, `smtp_verify_peer`
- `email_from_address`, `email_from_name`
- `email_enabled`

**Security:**
- `max_login_attempts`, `lockout_duration_minutes`
- `password_min_length`, `session_timeout_minutes`
- `require_2fa` (future)

**Localization:**
- `default_locale`, `available_locales`, `date_format`, `time_format`
- `calendar_type` (gregorian/hijri/both)
- `week_start_day`

---

## Services Layer

### `NotificationService`
- `send(event, recipient, data)` — routes to appropriate channels based on preferences
- `sendBulk(event, recipients, data)` — batch (for group notifications)
- `retry(notificationLog)` — retry failed notification
- `getPreferences(user)` — get notification preferences
- `updatePreferences(user, preferences)` — update preferences

### `TemplateService`
- `render(template, data, locale)` — render template with variables
- `getVariables(templateType)` — get available variables for a template type
- `preview(template, sampleData)` — render with sample data for preview
- `duplicate(template)` — clone template

### `DocumentService`
- `generate(template, entity, locale)` — render + create PDF + store + log
- `download(document)` — stream PDF to browser
- `email(document, recipient)` — attach PDF and send
- `print(document)` — trigger browser print dialog (returns HTML)

### `AuditService`
- `log(action, model, oldValues, newValues, metadata)` — create audit record
- `getHistory(model)` — get audit trail for any entity
- `getLoginHistory(user)` — get login attempts
- `search(filters)` — filtered audit log query

### `SystemSettingService`
- `get(key, academyId, default)` — get setting value (checks academy → falls back to global)
- `set(key, value, academyId, user)` — update setting (audit-logged)
- `getGroup(group, academyId)` — get all settings in a group
- `isEncrypted(key)` — check if value needs decryption

---

## Scheduled Jobs

| Job | Frequency | Action |
|-----|-----------|--------|
| `RetryFailedNotifications` | Every 5 min | Retry notifications with retry_count < max and next_retry_at <= now |
| `CleanupExpiredNotifications` | Daily | Remove logs older than retention period |
| `AuditLogArchive` | Monthly | Move logs older than retention to archive table |
| `DocumentCleanup` | Weekly | Remove orphaned generated docs (entity deleted) |
