# Milestone 1: Authentication & Placeholder Portal Home

## Implementation Checklist

> **Branch:** `feature/main-20260603`  
> **Goal:** Mobile app authenticates via Firebase → Laravel → redirects to portal home in WebView  
> **Status:** ✅ Tasks 1-9 + 11 Complete (Task 10 pending mobile app update)

---

## Firebase & Project Configuration (USE EXISTING — DO NOT CREATE NEW)

These are the **existing Firebase project credentials** currently in production on the mobile app.  
Use these exact values in the Laravel `.env` file.

| Config Item | Value | Source |
|---|---|---|
| Firebase Project ID | `restaurant-loyalty-app-cdd0a` | `firebase_options.dart` |
| Firebase Web API Key (Android) | `AIzaSyBzQ7A-lyse6PQ4yFwFo7QWFeRrdzUgOSI` | `firebase_options.dart` |
| Firebase iOS API Key | `AIzaSyAsvhA28v4Fff8mHNlwspPC8pi31wFA0cY` | `firebase_options.dart` |
| Firebase Messaging Sender ID | `65516779738` | `firebase_options.dart` |
| Firebase Android App ID | `1:65516779738:android:220306064b5a4b68630de3` | `firebase_options.dart` |
| Firebase iOS App ID | `1:65516779738:ios:2f8bd4199922bb05630de3` | `firebase_options.dart` |
| Storage Bucket | `restaurant-loyalty-app-cdd0a.firebasestorage.app` | `firebase_options.dart` |
| iOS Bundle ID | `com.CutPay.FriezAndBurgz` | `app_config.dart` |
| Android Package Name | `com.CutPay.FriezAndBurgz` | `app_config.dart` |
| Store App ID | `friezandburgz` | `app_config.dart` |
| iOS Client ID | `65516779738-4kl5rar1gio2d4p5e8j0ou9jm6ogp1ec.apps.googleusercontent.com` | `firebase_options.dart` |

### Token Validation Method (Same as Legacy)

The legacy `mobile-login.php` validates Firebase ID tokens using Google Identity Toolkit:

```
POST https://identitytoolkit.googleapis.com/v1/accounts:lookup?key={FIREBASE_WEB_API_KEY}
Body: {"idToken": "<TOKEN_FROM_MOBILE_APP>"}
Response: { "users": [{ "localId": "...", "email": "...", "displayName": "...", "photoUrl": "..." }] }
```

**We use the Web API Key** (`AIzaSyBzQ7A-lyse6PQ4yFwFo7QWFeRrdzUgOSI`) for this server-side validation.  
This is NOT a secret — it's a browser/public key restricted by Firebase project settings.

---

## .env Variables to Add

```env
# Firebase Configuration (same project as mobile app)
FIREBASE_PROJECT_ID=restaurant-loyalty-app-cdd0a
FIREBASE_WEB_API_KEY=AIzaSyBzQ7A-lyse6PQ4yFwFo7QWFeRrdzUgOSI
FIREBASE_MESSAGING_SENDER_ID=65516779738

# Mobile Portal
MOBILE_PORTAL_DEFAULT_STORE=friezandburgz
```

---

## Task Checklist

### Task 1: Customer Model + Migration
- [x] Create migration `create_customers_table`
- [x] Create migration `create_customer_devices_table`
- [x] Create `app/Models/Customer.php`
- [x] Create `app/Models/CustomerDevice.php`
- [x] Run migrations and verify tables exist
- [x] Add relationship: Customer hasMany CustomerDevices
- [x] Add relationship: Customer belongsTo Brand (nullable)

**Schema — `customers`:**
```
id, brand_id (FK nullable), firebase_uid (varchar 128), store_app_id (varchar 100),
name, email, phone, address, postcode, city, county, country,
provider (google/apple/facebook/email), photo_url, loyalty_points (default 0),
created_at, updated_at
UNIQUE INDEX (firebase_uid, store_app_id)
```

**Schema — `customer_devices`:**
```
id, customer_id (FK), fcm_token (text), device_type (varchar 10),
is_active (boolean default true), created_at, updated_at
INDEX (customer_id)
```

---

### Task 2: Firebase Auth Validation Service
- [x] Create `app/Services/FirebaseAuthService.php`
- [x] Method: `validateIdToken(string $idToken): ?array`
- [x] Uses `Http::post()` to Google Identity Toolkit endpoint
- [x] Returns user array `[localId, email, displayName, photoUrl]` or null
- [x] Reads `FIREBASE_WEB_API_KEY` from config/env
- [x] Add `config/firebase.php` config file
- [x] Handle HTTP errors gracefully (timeout, 4xx, 5xx)
- [x] Write unit test with mocked HTTP response

**Validation URL:**
```
POST https://identitytoolkit.googleapis.com/v1/accounts:lookup?key={FIREBASE_WEB_API_KEY}
```

**Request Body:**
```json
{"idToken": "eyJhbGciOi..."}
```

**Expected Response:**
```json
{
  "users": [{
    "localId": "AbCdEf123...",
    "email": "user@example.com",
    "displayName": "John Doe",
    "photoUrl": "https://...",
    "emailVerified": true,
    "providerUserInfo": [{"providerId": "google.com", ...}]
  }]
}
```

---

### Task 3: FCM Device Registration Endpoint
- [x] Create `app/Http/Controllers/Mobile/MobileApiController.php`
- [x] Method: `registerDevice(Request $request)`
- [x] Route: `POST /mobile/api/save_fcm_token.php` (no auth middleware)
- [x] Validate: `firebase_uid` is required
- [x] Upsert customer by (`firebase_uid` + `store_app_id`)
- [x] Upsert device by `fcm_token` (avoid duplicates)
- [x] Deactivate other customers' devices with same token
- [x] Return JSON matching legacy contract exactly
- [x] Write feature test

**Request (JSON):**
```json
{
  "firebase_uid": "AbCdEf123...",
  "email": "user@example.com",
  "display_name": "John Doe",
  "phone_number": "07700123456",
  "provider": "google",
  "fcm_token": "dXXXX...long-token...",
  "device_type": "android",
  "store_app_id": "friezandburgz",
  "diag": "diagnostic string (optional)"
}
```

**Response (JSON — MUST match legacy):**
```json
{
  "success": true,
  "message": "User and token saved/updated",
  "token_received_prefix": "dXXXX...",
  "token_saved_prefix": "dXXXX...",
  "token_saved_updated_at": "2026-06-03 12:00:00"
}
```

**Error Response:**
```json
{
  "success": false,
  "message": "Missing required field: firebase_uid"
}
```

---

### Task 4: Mobile Login Bridge (Auth Endpoint)
- [x] Create `app/Http/Controllers/Mobile/MobileAuthController.php`
- [x] Method: `loginBridge(Request $request)`
- [x] Route: `GET /mobile/mobile-login.php` (no auth middleware)
- [x] Read query params: `?token=X&store_app_id=Y`
- [x] Validate `store_app_id` format (alphanumeric + hyphens/underscores, 1-64 chars)
- [x] Call `FirebaseAuthService::validateIdToken($token)`
- [x] If invalid → return 401 with plain text error
- [x] If valid → find or create Customer (by `firebase_uid` + `store_app_id`)
- [x] Store in session: `customer_id`, `firebase_uid`, `store_app_id`, `customer_name`
- [x] Redirect to `/portal`
- [x] Write feature test (valid token, invalid token, missing params)

**URL pattern (called by mobile app):**
```
GET https://icard.orderbuddy.tech/mobile/mobile-login.php?token=eyJhbGciOi...&store_app_id=friezandburgz
```

**Success:** HTTP 302 → `/portal`  
**Failure:** HTTP 401 plain text

---

### Task 5: Customer Auth Middleware
- [x] Create `app/Http/Middleware/CustomerAuth.php`
- [x] Check session has `customer_id`
- [x] If not → return 401 or redirect to error page
- [x] If yes → resolve Customer model, bind to request
- [x] Register middleware alias `customer.auth` in bootstrap/app.php
- [x] Write test (authenticated vs unauthenticated)

---

### Task 6: Portal Home Page (Placeholder)
- [x] Create `app/Http/Controllers/Mobile/PortalHomeController.php`
- [x] Method: `index(Request $request)` — returns Blade view
- [x] Route: `GET /portal` with `customer.auth` middleware
- [x] Create layout: `resources/views/mobile/layouts/app.blade.php`
  - [x] Mobile-first (viewport meta, no desktop sidebar)
  - [x] Brand-colored header
  - [x] Minimal CSS (Tailwind via CDN or Vite — keep simple)
- [x] Create view: `resources/views/mobile/home.blade.php`
  - [x] "Welcome, {customer_name}!" greeting
  - [x] Loyalty points card (showing 0)
  - [x] Navigation placeholders (Menu, Orders, Loyalty, Profile — greyed out)
  - [x] "Features coming soon" message
  - [x] Sign out button
- [x] Create sign-out route: `POST /portal/logout`
  - [x] Clears customer session
  - [x] Returns "Signed out" page (no redirect back to login — app handles re-login)

---

### Task 7: Debug Log Endpoint
- [x] Add method to `MobileApiController`: `debugLog(Request $request)`
- [x] Route: `POST /mobile/api/debug_log.php` (no auth)
- [x] Accept JSON: `{"store_app_id": "X", "logs": [...]}`
- [x] Write to `storage/logs/mobile-debug-{Y-m-d}.log`
- [x] Return: `{"success": true, "stored": N}`
- [x] Write basic test

---

### Task 8: Route Registration
- [x] Create route group file or add to `routes/web.php`:
  ```php
  // Mobile API endpoints (match legacy URL structure)
  Route::prefix('mobile')->group(function () {
      Route::post('/api/save_fcm_token.php', [MobileApiController::class, 'registerDevice']);
      Route::post('/api/debug_log.php', [MobileApiController::class, 'debugLog']);
      Route::get('/mobile-login.php', [MobileAuthController::class, 'loginBridge']);
  });

  // Customer portal (WebView)
  Route::prefix('portal')->middleware('customer.auth')->group(function () {
      Route::get('/', [PortalHomeController::class, 'index'])->name('portal.home');
      Route::post('/logout', [PortalHomeController::class, 'logout'])->name('portal.logout');
  });
  ```

---

### Task 9: Config File
- [x] Create `config/firebase.php`:
  ```php
  return [
      'project_id' => env('FIREBASE_PROJECT_ID'),
      'web_api_key' => env('FIREBASE_WEB_API_KEY'),
      'identity_toolkit_url' => env('FIREBASE_IDENTITY_TOOLKIT_URL', 'https://identitytoolkit.googleapis.com/v1/accounts:lookup'),
      'http_timeout_seconds' => (int) env('FIREBASE_HTTP_TIMEOUT_SECONDS', 10),
  ];
  ```
- [x] Update `.env.example` with Firebase variables

---

### Task 10: Mobile App Config Change (LAST — after Laravel is deployed)
- [ ] Change `backendUrl` in `lib/config/app_config.dart`:
  ```dart
  static const String backendUrl = 'https://icard.orderbuddy.tech/mobile';
  ```
- [ ] Test on device: sign in → WebView loads portal home

---

### Task 11: Push Notification System (Admin → Customer Devices)
- [x] Create migration `create_notifications_table`
- [x] Create migration `create_notification_recipients_table`
- [x] Create `app/Models/Notification.php`
- [x] Create `app/Models/NotificationRecipient.php`
- [x] Create `app/Services/FCMNotificationService.php` (FCM V1 API + OAuth2 service account auth)
- [x] Create `app/Http/Controllers/NotificationController.php` (full CRUD + send)
- [x] Route: `GET /notifications` (index — list with status filter)
- [x] Route: `GET /notifications/create` (create form — auto-loads user's store_app_ids)
- [x] Route: `POST /notifications` (store — save draft or scheduled)
- [x] Route: `GET /notifications/{id}` (show — with recipient delivery status)
- [x] Route: `GET /notifications/{id}/edit` (edit draft)
- [x] Route: `PUT /notifications/{id}` (update)
- [x] Route: `DELETE /notifications/{id}` (destroy draft only)
- [x] Route: `POST /notifications/{id}/send` (trigger FCM send)
- [x] Create Blade views: `notifications/index`, `create`, `edit`, `show`
- [x] Add sidebar navigation link (bell icon)
- [x] Add `customers()` relationship to Brand model
- [x] Update `config/firebase.php` with `service_account_path` and `ios_bundle_id`
- [x] Update `.env.example` with FCM env vars
- [x] Write 14 feature tests (all passing)

**Key Behaviors:**
- Store dropdown auto-populated from user's brands → customers → store_app_ids (no manual entry)
- Notifications belong to authenticated user (owner-only access)
- Auto-deactivates stale FCM tokens on 404 responses from Google
- Platform-specific payloads: Android (high priority, channel_id) + iOS (APNs priority 10, badge)
- Service account JSON auth via JWT assertion → OAuth2 token exchange

---

## Testing Checklist

| # | Test | Method | Pass Criteria | Status |
|---|------|--------|---------------|--------|
| 1 | Device registration (valid) | POST from Postman | 200 + JSON with `success: true`, customer row created | ✅ |
| 2 | Device registration (missing firebase_uid) | POST from Postman | 400/200 + `success: false` | ✅ |
| 3 | Auth bridge (valid Firebase token) | GET in browser | 302 redirect to /portal, session set | ✅ |
| 4 | Auth bridge (invalid/expired token) | GET with garbage | 401 returned, no session | ✅ |
| 5 | Auth bridge (missing params) | GET without token | 401 or 400 | ✅ |
| 6 | Portal home (authenticated) | Visit /portal with session | Shows welcome + customer name | ✅ |
| 7 | Portal home (unauthenticated) | Visit /portal without session | Blocked by middleware (401) | ✅ |
| 8 | Portal logout | POST /portal/logout | Session cleared | ✅ |
| 9 | Debug log | POST from Postman | 200, log file written | ✅ |
| 10 | Full E2E (mobile app) | Sign in → WebView loads | Portal home visible in app | 🔲 |

---

## File Inventory (to be created)

| File | Purpose |
|------|---------|
| `database/migrations/xxxx_create_customers_table.php` | Customer table |
| `database/migrations/xxxx_create_customer_devices_table.php` | Device tokens |
| `app/Models/Customer.php` | Customer Eloquent model |
| `app/Models/CustomerDevice.php` | Device Eloquent model |
| `app/Services/FirebaseAuthService.php` | Token validation |
| `config/firebase.php` | Firebase config |
| `app/Http/Controllers/Mobile/MobileApiController.php` | FCM + debug endpoints |
| `app/Http/Controllers/Mobile/MobileAuthController.php` | Login bridge |
| `app/Http/Middleware/CustomerAuth.php` | Portal auth guard |
| `app/Http/Controllers/Mobile/PortalHomeController.php` | Portal home |
| `resources/views/mobile/layouts/app.blade.php` | Mobile portal layout |
| `resources/views/mobile/home.blade.php` | Placeholder home |
| `resources/views/mobile/signed-out.blade.php` | Post-logout page |

---

## Build Order (Recommended Sequence)

```
1. config/firebase.php + .env.example update
2. Customer + CustomerDevice models & migrations → run migrate
3. FirebaseAuthService (token validation)
4. MobileApiController (registerDevice + debugLog)
5. MobileAuthController (loginBridge)
6. CustomerAuth middleware → register in app
7. PortalHomeController + Blade views
8. Route registration (all routes)
9. Run tests
10. Deploy → test with real Firebase token
11. Update mobile app backendUrl → test on device
```

---

## Risks & Notes

| Risk | Mitigation |
|------|-----------|
| Firebase token expiry during testing | Tokens expire in 1 hour — use fresh token from mobile app or Firebase Auth REST API |
| Session cookies not working in WebView | Ensure `SESSION_DOMAIN` and `SameSite` are set correctly for the domain |
| CORS issues on POST endpoints | Mobile app uses direct POST (not browser) — CORS shouldn't apply, but add headers if needed |
| `.php` extension in Laravel routes | Laravel handles this fine — it's just a URL path, not actual PHP file serving |

---

## Progress Log

| Date | Task | Status | Notes |
|------|------|--------|-------|
| 2026-06-03 | Checklist created | ✅ | Ready to start implementation |
| 2026-06-04 | Task 1 implemented | ✅ | Added customers/customer_devices migrations + models, and passed isolated migration/test validation |
| 2026-06-04 | Task 2 implemented | ✅ | Added FirebaseAuthService + config/firebase.php + mocked HTTP tests |
| 2026-06-04 | Task 3 implemented | ✅ | Added MobileApiController registerDevice + route + feature tests (upsert/token reassignment) |
| 2026-06-04 | Task 4 implemented | ✅ | Added MobileAuthController loginBridge + route + feature tests (valid/invalid/missing params) |
| 2026-06-04 | Task 5 implemented | ✅ | Added CustomerAuth middleware + alias registration + feature tests (auth/unauth/stale session) |
| 2026-06-05 | Task 6 implemented | ✅ | Added PortalHomeController + `/portal` and `/portal/logout` routes + placeholder Blade views + feature tests |
| 2026-06-05 | Task 7 implemented | ✅ | Added MobileApiController debugLog + route + feature tests (validation/file write/append) |
| 2026-06-05 | Task 8 implemented | ✅ | Finalized grouped route registration in `routes/web.php` for mobile APIs and customer portal |
| 2026-06-05 | Task 9 implemented | ✅ | Confirmed `config/firebase.php` and `.env.example` Firebase variables are present and aligned |
| 2026-06-12 | Task 11 implemented | ✅ | FCM Notification System — NotificationController (CRUD + send), FCMNotificationService (V1 API + OAuth2), Notification/NotificationRecipient models, migrations, Blade views (index/create/edit/show), sidebar nav link, 14 feature tests passing |
| | | | |
