# RC-009 — Theme Engine Foundation

## Status: ✅ IMPLEMENTATION COMPLETE — Awaiting Human Approval

---

## 1. Files Created

| File | Role |
|---|---|
| [`app/modules/Theme/ThemeManifest.php`](file:///c:/xampp/htdocs/inveetaire/app/modules/Theme/ThemeManifest.php) | Parses and validates `theme.json` only |
| [`app/modules/Theme/ThemeRegistry.php`](file:///c:/xampp/htdocs/inveetaire/app/modules/Theme/ThemeRegistry.php) | Scans `/themes` and returns all valid themes |
| [`app/modules/Theme/ThemeLoader.php`](file:///c:/xampp/htdocs/inveetaire/app/modules/Theme/ThemeLoader.php) | Loads theme by code, validates, resolves entry, fallback |
| [`app/modules/Theme/ThemeRenderer.php`](file:///c:/xampp/htdocs/inveetaire/app/modules/Theme/ThemeRenderer.php) | Renders theme entry file with invitation data |
| [`app/modules/Theme/ThemeService.php`](file:///c:/xampp/htdocs/inveetaire/app/modules/Theme/ThemeService.php) | Coordinates Registry + Loader + Renderer. Controller entry point |
| [`themes/theme-001/theme.json`](file:///c:/xampp/htdocs/inveetaire/themes/theme-001/theme.json) | Theme manifest (code, name, version, author, preview, entry) |
| [`themes/theme-001/index.php`](file:///c:/xampp/htdocs/inveetaire/themes/theme-001/index.php) | Theme entry point (delegates to existing identity.php) |
| `themes/theme-001/preview.webp` | Placeholder preview image |
| `themes/theme-001/assets/css/` | Asset directory (empty, ready for RC-010) |
| `themes/theme-001/assets/js/` | Asset directory (empty, ready for RC-010) |
| `themes/theme-001/assets/images/` | Asset directory (empty, ready for RC-010) |

## 2. Files Modified

| File | Change |
|---|---|
| [`app/modules/PublicInvitation/PublicInvitationController.php`](file:///c:/xampp/htdocs/inveetaire/app/modules/PublicInvitation/PublicInvitationController.php) | Now delegates rendering to `ThemeService::renderInvitation()`. Controller has zero knowledge of theme paths. |

---

## 3. Theme Engine Architecture

```
ThemeService          ← Controller's only contact point
├── ThemeRegistry     → Scans /themes, locates theme.json files
├── ThemeLoader       → Loads manifest, validates, resolves entry, fallback
│   └── ThemeManifest → Parses theme.json (single responsibility)
└── ThemeRenderer     → Renders entry file with data extracted into scope
```

**Namespace**: `App\Modules\Theme\` → `app/modules/Theme/` (auto-resolved by existing Autoloader, no changes needed)

---

## 4. Theme Loading Workflow

```
ThemeService::renderInvitation($payload)
    ↓
    Read theme_code from $payload['workspace']['theme_code']
    Fallback to 'theme-001' if empty/absent
    ↓
ThemeLoader::load($themeCode)
    ↓
    tryLoad($themeCode):
        1. Validate theme folder exists (ROOT_PATH/themes/{code}/)
        2. ThemeManifest::parse(theme.json)
           → Validate all 6 required fields (code, name, version, author, preview, entry)
        3. Resolve entry file path
        4. Validate entry file exists
    ↓
    If tryLoad() returns null → tryLoad('theme-001')
    If fallback also null    → emergencyStub() (never crashes)
    ↓
    Return: { manifest, path, entry }
```

---

## 5. Theme Rendering Workflow

```
ThemeService::renderInvitation($payload)
    ↓
    $resolved = ThemeLoader::load($themeCode)
    $payload['themeManifest'] = $resolved['manifest']
    ↓
ThemeRenderer::render($resolved['entry'], $payload)
    ↓
    Validate entry file exists → 503 if missing (never fatal crash)
    ↓
    Add $themePath = dirname($entryFile) to $payload
    ↓
    Isolated closure: extract($payload) → require($entryFile)
    ↓
    themes/theme-001/index.php executes
    ↓
    require ROOT_PATH . '/app/views/public/identity.php'
    ↓ (existing behavior preserved, unchanged)
    Render full invitation HTML
```

> [!IMPORTANT]
> The controller calls `$this->themeService->renderInvitation($payload)` and has **zero knowledge** of theme paths, entry files, or the `/themes/` directory. All theme-path logic is encapsulated in the Theme Engine.

---

## 6. Fallback Workflow

```
Request: /workspace-slug/i/guest-token
    ↓
workspace.theme_code = 'theme-aurora'  (hypothetical, not installed)
    ↓
ThemeLoader::load('theme-aurora')
    tryLoad('theme-aurora') → folder missing → null
    ↓
    themeCode !== FALLBACK_THEME → tryLoad('theme-001')
    tryLoad('theme-001') → SUCCESS
    ↓
    Return theme-001 data
    ↓
Invitation renders normally using theme-001
```

**Edge case**: If even theme-001 is missing → `emergencyStub()` returns a safe stub that prevents crashes. `ThemeRenderer::render()` then checks if the entry file exists and outputs a user-safe 503 message instead of a PHP fatal error.

---

## 7. Verification

All checks passed via `scratch/verify_rc009.php` CLI execution:

| Check | Result |
|---|---|
| Registry discovers theme-001 | ✅ PASS |
| Manifest parsed correctly | ✅ PASS — `{"code":"theme-001","name":"Klasik Putih","version":"1.0.0","author":"Inveetaire Team","preview":"preview.webp","entry":"index.php"}` |
| Loader resolves theme.json | ✅ PASS |
| Renderer resolves index.php entry | ✅ PASS |
| Invalid theme falls back to theme-001 | ✅ PASS — `'theme-nonexistent-xyz'` → `theme-001` |
| `themeExists('theme-001')` = true | ✅ PASS |
| `themeExists('theme-999')` = false | ✅ PASS |
| Landing page HTTP 200 | ✅ PASS (no regression) |
| Admin portal HTTP 200 | ✅ PASS (no regression) |

---

## 8. Acceptance Checklist

| Criterion | Status |
|---|---|
| Theme Engine exists (`app/modules/Theme/`) | ✅ |
| Single theme registered (`theme-001`) | ✅ |
| JSON manifest loaded (6 required fields validated) | ✅ |
| Renderer separated from controller | ✅ — Controller calls `ThemeService`, not file paths |
| Fallback implemented | ✅ — Any invalid code → theme-001 → emergencyStub |
| Architecture ready for RC-010 | ✅ — New theme = new folder + `theme.json` + `index.php`, zero controller changes |
| Business logic unchanged | ✅ — `PublicInvitationService`, model, RSVP, Wish, QR all untouched |
| Authentication unchanged | ✅ |
| Routing unchanged | ✅ |
| Dashboard unchanged | ✅ |
| Landing page unchanged | ✅ |
| No new DB table | ✅ |

---

## 9. PHP Syntax Results

```
[PASS] app/modules/Theme/ThemeManifest.php
[PASS] app/modules/Theme/ThemeRegistry.php
[PASS] app/modules/Theme/ThemeLoader.php
[PASS] app/modules/Theme/ThemeRenderer.php
[PASS] app/modules/Theme/ThemeService.php
[PASS] app/modules/PublicInvitation/PublicInvitationController.php
[PASS] themes/theme-001/index.php

All syntax checks PASSED.
```

---

> [!NOTE]
> **RC-010 Readiness**: Adding a new theme requires only: create `themes/theme-XXX/` folder, add `theme.json`, and add `index.php`. Zero changes to controllers, services, or routing. The workspace's `theme_code` field selects the theme at runtime.
