# Mobile App Backend Migration — Laravel Implementation Strategy

> **Status:** Milestone 1 Complete — Auth + Portal + Push Notifications implemented  
> **Date:** 2026-06-03 (Updated: 2026-06-15)  
> **Branch:** `feature/main-20260603`  
> **Context:** No active customer base — no data migration required

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Legacy System Analysis](#2-legacy-system-analysis)
3. [Laravel Gap Analysis](#3-laravel-gap-analysis)
4. [API Compatibility Matrix](#4-api-compatibility-matrix)
5. [Phase 1 Implementation Plan](#5-phase-1-implementation-plan)
6. [Phase 2 Improvements](#6-phase-2-improvements)
7. [Phase 3 Native Roadmap](#7-phase-3-native-roadmap)
8. [Risk Assessment](#8-risk-assessment)

---

## 1. Executive Summary

### Approach
Replace the legacy PHP backend with Laravel API endpoints while keeping the mobile app largely unchanged. The mobile app currently functions as:

- **Native Flutter shell** → Login, push notifications, connectivity handling
- **WebView** → Loads the full customer loyalty portal (menu, ordering, loyalty, profile)

### Phase 1 Goal
Build a Laravel-hosted customer portal that the mobile app WebView can load, plus the 2 direct API endpoints the app calls natively. No mobile app redesign.

### Key Simplifications
- No active customers → no data migration
- No active loyalty balances → fresh start on loyalty engine
- No order history to preserve → clean implementation
- WebView architecture stays → we only need to serve web pages + APIs from Laravel

---

## 2. Legacy System Analysis

### 2.1 Mobile App Architecture

| Component | Type | What It Does |
|-----------|------|-------------|
| `SplashScreen` | Native | 2.5s brand splash, checks auth state |
| `LoginScreen` | Native | Firebase Auth (Google, Apple, Facebook, Email) |
| `HomeScreen` | Native shell + WebView | Loads loyalty portal via authenticated URL |
| `NotificationsScreen` | Native | In-app notification history (SharedPreferences) |
| `NotificationDetailScreen` | Native | Full notification view |
| Push Notifications | Native | FCM registration, foreground/background handling |

### 2.2 Direct API Calls (Flutter → Backend)

The mobile app makes exactly **2 direct HTTP calls** outside the WebView:

| # | Endpoint | Method | Purpose |
|---|----------|--------|---------|
| 1 | `api/save_fcm_token.php` | POST | Register user + FCM device token after login |
| 2 | `api/debug_log.php` | POST | Upload client diagnostic logs |

### 2.3 WebView Authentication Bridge

```
Flutter App
  │
  │ 1. User signs in via Firebase Auth
  │ 2. App gets Firebase ID token
  │ 3. App calls: POST api/save_fcm_token.php (register device)
  │ 4. App loads WebView URL:
  │       {backendUrl}/mobile-login.php?token={FIREBASE_ID_TOKEN}&store_app_id={STORE_APP_ID}
  │
  ▼
mobile-login.php (Legacy PHP)
  │
  │ 1. Validates Firebase ID token via Google Identity Toolkit API
  │ 2. Creates PHP session with user identity
  │ 3. Redirects to WEBAPP_HOME (/FNBDemo/menu1/loyalty_portal.php)
  │
  ▼
Loyalty Portal (PHP WebView Content)
  │
  │ Customer sees: menu, cart, orders, loyalty, profile, promotions
  │ All interactions are AJAX calls to snippet_func/*.php endpoints
  │
  ▼
```

### 2.4 Current Configuration

```dart
// Mobile app config (app_config.dart)
backendUrl = 'https://orderbuddy.tinypay.uk/v2/FNBCLS/menu1/mobile_app'
storeAppId = 'friezandburgz'

// Constructed URLs:
// API:     https://orderbuddy.tinypay.uk/v2/FNBCLS/menu1/mobile_app/api/save_fcm_token.php
// WebView: https://orderbuddy.tinypay.uk/v2/FNBCLS/menu1/mobile_app/mobile-login.php?token=...&store_app_id=friezandburgz
// Redirect: /FNBDemo/menu1/loyalty_portal.php
```

### 2.5 Customer Portal Features (WebView Content)

The legacy loyalty portal provides these features to customers:

| Feature | Description | Legacy Endpoint |
|---------|-------------|-----------------|
| **Login** | Cookie-based session after Firebase bridge | `_applogin.php` |
| **Registration** | Create customer account | `_register.php` |
| **Menu Browsing** | Categories + products with images, addons | `getallproducts.php` |
| **Product Details** | Individual product with addon groups | `getProductById.php` |
| **Favorites** | Save/retrieve favorite products | `getfav.php` |
| **Shopping Cart** | Add items, calculate totals, apply tax | `insertcartitem.php`, `getcart.php` |
| **Promo Codes** | Apply discount codes at checkout | `promocode.php` |
| **Order Placement** | Finalize order (delivery/collection/table) | `placeorder.php` |
| **Order History** | View past orders with status | `getorder.php` |
| **Order Status** | Track current order status | `orderstatus.php`, `getstatus.php` |
| **Loyalty Points** | View balance, earn on orders | `getUser.php` (cmd=loyaltypointsbycookie) |
| **Rewards** | Redeem loyalty points for discounts/products | `rewards.php` |
| **Customer Profile** | Update address, phone, email | `getUser.php`, `cusdash.php` |
| **Business Hours** | Check if restaurant is open | `cart_misc_fn.php` (cmd=isopen) |
| **Table Orders** | In-restaurant table ordering | `getorder.php` (cmd=bytable) |
| **Upsells** | Suggested items at checkout | `getorder.php` (cmd=allupsells) |
| **Shipping/Delivery** | Address validation, delivery zones | `postcode.php`, `addressio.php` |

### 2.6 Legacy API Response Format

**Critical:** The legacy API uses pipe-delimited plain text, NOT JSON:

```
Field delimiter:  |  (pipe)
Record delimiter: ¬  (not sign / double separator)
Sub-field:        ;  (semicolon)
Compression:      Base64 + GZIP for large payloads (cart/order)
Content-Type:     text/plain (not application/json)
```

**Example — Login Response:**
```
John Doe|123 High Street|07700123456|john@email.com|SW1A 1AA|England|UK|245
```

**Example — Products Response:**
```
cat_id|cat_name|cat_image¬prod_id|prod_name|price|description|image|vat_rate|tax_included|in_stock¬...
```

### 2.7 Authentication Model (Legacy)

- No API tokens — uses PHP session cookies
- Customer identified by cookie `ruf7secid` (customer ID)
- Restaurant identified by `menuidxkey` (session/cookie `rjfh7mu`)
- Firebase Auth only used for mobile app login bridge (not the web portal itself)

### 2.8 Database Schema (Customer-Facing)

| Table | Purpose | Key Fields |
|-------|---------|------------|
| `mobile_users` | Firebase-authenticated mobile users | firebase_uid, store_app_id, email, display_name, phone_number, loyalty_points |
| `user_fcm_tokens` | Device push tokens | firebase_uid, store_app_id, fcm_token, device_type, is_active |
| `tblCustomer` | Legacy customer records | id, name, email, address, phone, postcode, points |
| `tblOrder` | Orders | id, customer_id, total, tax, status, payment_type, date |
| `tblOrderDetail` | Order line items | order_id, product_id, qty, price, addons |
| `tblProducts` | Menu items | id, name, price, category_id, vat_rate, tax_included, in_stock |
| `tblCategory` | Product categories | id, name, image |
| `tblAddon` / `tblAddonGroup` | Product customizations | group_id, name, options, prices |
| `tbroyalty` | Loyalty program config | price_per_point, points_required, reward_by_discount/fixed/product |
| `tblPromoCode` | Discount codes | code, discount_percent, valid_from, valid_to |
| `notification_logs` | Sent notifications | store_app_id, title, message, total_sent |

### 2.9 Push Notification System

| Component | Technology |
|-----------|-----------|
| Sending service | Firebase Cloud Messaging V1 API |
| Auth method | Service Account JSON (OAuth2) |
| Admin panel | `admin/notifications.php` (PHP, session-based) |
| Token storage | `user_fcm_tokens` table |
| Targeting | All users or specific firebase_uid |
| Rich media | Image URL, icon URL support |

---

## 3. Laravel Gap Analysis

### 3.1 Already Available in Laravel Portal

| Feature | Status | Notes |
|---------|--------|-------|
| Multi-brand/branch management | ✅ | Brands → Branches with DB switching |
| Admin dashboard (sales, orders) | ✅ | Full analytics |
| Live orders monitoring | ✅ | Cross-branch, real-time |
| Reports (X/Z/Sales) | ✅ | Date range, shareable |
| Card machine payment API | ✅ | Full Pushy-based flow |
| Terminal CRUD | ✅ | Web UI + API |
| Staff management | ✅ | Branch assignment |
| Stock management | ✅ | Inventory view |
| Admin auth | ✅ | Laravel Breeze (session) |
| Multi-database connections | ✅ | BranchConnectionManager |
| i18n | ✅ | EN + RU |

### 3.2 Missing — Required for Mobile App

| Feature | Priority | Complexity | Notes |
|---------|----------|------------|-------|
| **Mobile auth bridge** (Firebase → session/token) | ✅ DONE | Low | Replaces `mobile-login.php` |
| **FCM token registration API** | ✅ DONE | Low | Replaces `save_fcm_token.php` |
| **FCM push sending** | ✅ DONE | Medium | Admin notification panel (FCM V1 API) |
| **Debug log endpoint** | ✅ DONE | Low | Replaces `debug_log.php` |
| **Customer loyalty portal** (WebView pages) | CRITICAL | High | Full web app served by Laravel |
| **Menu/products API** | HIGH | Medium | Read from branch DB |
| **Cart + checkout** | HIGH | High | Tax calculation, addons, promos |
| **Order placement** | HIGH | High | Multi-mode (delivery/collection/table) |
| **Order history** | HIGH | Low | Read from branch DB |
| **Loyalty points engine** | HIGH | Medium | Earn on orders, redeem at checkout |
| **Rewards/redemption** | MEDIUM | Medium | Configurable reward tiers |
| **Promotion codes** | MEDIUM | Low | Validate + apply discount |
| **Customer profile** | MEDIUM | Low | View/edit personal details |
| **Business hours check** | LOW | Low | Is-open validation |
| **Delivery zone validation** | LOW | Low | Postcode checking |

---

## 4. API Compatibility Matrix

### 4.1 Mobile App Direct Calls (Must Replicate)

These are the only endpoints the Flutter app calls directly (outside WebView):

| Legacy Endpoint | Laravel Equivalent | Request Format | Response Format | Changes Required |
|----------------|-------------------|----------------|-----------------|-----------------|
| `POST api/save_fcm_token.php` | `POST /api/mobile/register-device` | JSON body | JSON `{success, message}` | **None** — same contract |
| `POST api/debug_log.php` | `POST /api/mobile/debug-log` | JSON body | JSON `{success}` | **None** — same contract |
| `GET mobile-login.php?token=X&store_app_id=Y` | `GET /mobile/login?token=X&store_app_id=Y` | Query params | HTTP redirect to portal | **Minimal** — redirect to Laravel route |

### 4.2 Mobile App Config Change Required

The ONLY change in the Flutter app for Phase 1:

```dart
// BEFORE (app_config.dart)
static const String backendUrl = 'https://orderbuddy.tinypay.uk/v2/FNBCLS/menu1/mobile_app';

// AFTER
static const String backendUrl = 'https://icard.orderbuddy.tech/mobile';
```

All URL construction stays the same:
- `{backendUrl}/api/save_fcm_token.php` → `https://icard.orderbuddy.tech/mobile/api/save_fcm_token.php`
- `{backendUrl}/mobile-login.php?token=...` → `https://icard.orderbuddy.tech/mobile/mobile-login.php?token=...`

**Alternative (zero mobile changes):** Use URL path aliasing on Laravel to match the exact legacy paths.

### 4.3 WebView Content (Served by Laravel)

The WebView loads HTML/JS/CSS pages. Two approaches:

| Approach | Description | Pros | Cons |
|----------|-------------|------|------|
| **A: Blade-rendered portal** | Build customer portal as Laravel Blade pages | Clean, maintainable, modern | More development effort |
| **B: Legacy PHP ported** | Copy legacy PHP and serve from Laravel public | Fast parity | Tech debt, hard to maintain |

**Recommended: Approach A** — Build a new Blade/Livewire customer portal that provides the same features. Since there are no active customers, there's no requirement for pixel-perfect legacy compatibility.

### 4.4 WebView Internal APIs (Called by Portal JS)

These AJAX endpoints are called by the WebView content (JavaScript inside the portal). If we build a new Blade portal, we define our own API contract. If porting legacy PHP, these must be replicated:

| Legacy `snippet_func/` | New Laravel Route | Purpose |
|------------------------|-------------------|---------|
| `_applogin.php` | Session already established | Login handled by bridge |
| `getallproducts.php` | `GET /portal/api/menu` | Full menu with categories |
| `getProductById.php` | `GET /portal/api/products/{id}` | Single product + addons |
| `getfav.php` | `GET /portal/api/favorites` | Customer favorites |
| `insertcartitem.php` | `POST /portal/api/cart/init` | Start cart session |
| `getcart.php` | `POST /portal/api/cart/calculate` | Calculate totals |
| `placeorder.php` | `POST /portal/api/orders` | Submit order |
| `getorder.php` | `GET /portal/api/orders` | Order history |
| `orderstatus.php` | `GET /portal/api/orders/{id}/status` | Track order |
| `getUser.php` | `GET /portal/api/customer/loyalty` | Points balance |
| `rewards.php` | `GET /portal/api/rewards` | Available rewards |
| `promocode.php` | `POST /portal/api/promo/validate` | Check promo code |
| `cart_misc_fn.php` | `GET /portal/api/store/status` | Is-open check |
| `cusdash.php` | `PATCH /portal/api/customer/profile` | Update profile |

**Note:** Since we're building a new Blade portal, these become internal Laravel routes (controller methods), not legacy pipe-delimited APIs.

---

## 5. Phase 1 Implementation Plan

Phase 1 is split into incremental milestones. Each milestone is independently deployable and testable before the next begins.

---

### 5.1 Architecture Overview

```
┌───────────────────────────────────────────────────────────────────────────┐
│                        Laravel Application                                 │
│                                                                            │
│  ┌─────────────────────┐    ┌────────────────────────────────────────┐   │
│  │  Admin Portal        │    │  Customer Portal (Mobile WebView)      │   │
│  │  (Existing Blade)    │    │  (New Blade/Tailwind — built over time)│   │
│  │                      │    │                                        │   │
│  │  /dashboard          │    │  /portal          ← Placeholder first │   │
│  │  /live-orders        │    │  /portal/menu     ← Later             │   │
│  │  /reports            │    │  /portal/cart     ← Later             │   │
│  │  /cardmachine/*      │    │  /portal/orders   ← Later             │   │
│  │  /brands, /branches  │    │  /portal/loyalty  ← Later             │   │
│  │  /staff, /stock      │    │  /portal/profile  ← Later             │   │
│  └─────────────────────┘    └────────────────────────────────────────┘   │
│                                                                            │
│  ┌───────────────────────────────────────────────────────────────────┐   │
│  │  Mobile API (Direct Flutter calls — matches legacy contract)       │   │
│  │                                                                     │   │
│  │  POST /mobile/api/save_fcm_token.php  → register device            │   │
│  │  POST /mobile/api/debug_log.php       → upload logs                 │   │
│  │  GET  /mobile/mobile-login.php        → Firebase → session → portal│   │
│  └───────────────────────────────────────────────────────────────────┘   │
│                                                                            │
│  ┌───────────────────────────────────────────────────────────────────┐   │
│  │  Existing Card Machine API                                          │   │
│  │  POST /api/cardmachine/payment                                      │   │
│  └───────────────────────────────────────────────────────────────────┘   │
└───────────────────────────────────────────────────────────────────────────┘
```

---

### 5.2 Milestone 1: Authentication & Placeholder Home (IMMEDIATE PRIORITY)

> **Goal:** Mobile app can authenticate via Firebase, call Laravel, and see a portal home page in WebView.  
> **Outcome:** Full end-to-end auth flow working. Portal home is a placeholder.  
> **Mobile app change:** Update `backendUrl` only.

#### Flow After Milestone 1

```
Flutter App
  │
  │ 1. User signs in via Firebase Auth (existing — no changes)
  │ 2. App gets Firebase ID token (existing — no changes)
  │ 3. App calls: POST {backendUrl}/api/save_fcm_token.php
  │       → Laravel receives, upserts Customer + device token, returns JSON
  │ 4. App loads WebView URL:
  │       {backendUrl}/mobile-login.php?token={FIREBASE_ID_TOKEN}&store_app_id={ID}
  │       → Laravel validates token, creates session, redirects to /portal
  │
  ▼
Laravel Portal Home (/portal)
  │
  │ Placeholder page showing:
  │   - "Welcome, {customer_name}!"
  │   - Loyalty points: 0
  │   - "Features coming soon" message
  │   - Sign out button
  │
  ▼
```

#### Implementation Tasks — Milestone 1

| # | Task | Description |
|---|------|-------------|
| 1 | **Customer model + migration** | `customers` table + `customer_devices` table |
| 2 | **FCM device registration endpoint** | `POST /mobile/api/save_fcm_token.php` — match legacy JSON contract |
| 3 | **Firebase token validation service** | `App\Services\FirebaseAuthService` — validates via Google Identity Toolkit |
| 4 | **Mobile login bridge route** | `GET /mobile/mobile-login.php` — validate token → session → redirect |
| 5 | **Customer auth guard** | Middleware to protect `/portal/*` routes (session-based) |
| 6 | **Portal home page (placeholder)** | `GET /portal` — simple Blade page, mobile-optimized |
| 7 | **Debug log endpoint** | `POST /mobile/api/debug_log.php` — store logs |
| 8 | **Mobile app config update** | Change `backendUrl` in `app_config.dart` |

#### Detailed Specifications — Milestone 1

**Task 1: Customer Model + Migration**

```sql
-- Table: customers (central database)
customers
  - id (bigint, PK, auto)
  - brand_id (FK → brands, nullable)
  - firebase_uid (varchar 128, nullable, indexed)
  - store_app_id (varchar 100, indexed)
  - name (varchar 255)
  - email (varchar 255, nullable)
  - phone (varchar 32, nullable)
  - address (text, nullable)
  - postcode (varchar 20, nullable)
  - city (varchar 100, nullable)
  - county (varchar 100, nullable)
  - country (varchar 100, nullable)
  - provider (varchar 20 — google/apple/facebook/email)
  - photo_url (text, nullable)
  - loyalty_points (integer, default 0)
  - created_at (timestamp)
  - updated_at (timestamp)
  - UNIQUE INDEX (firebase_uid, store_app_id)

-- Table: customer_devices (central database)
customer_devices
  - id (bigint, PK, auto)
  - customer_id (FK → customers)
  - fcm_token (text)
  - device_type (varchar 10 — android/ios/unknown)
  - is_active (boolean, default true)
  - created_at (timestamp)
  - updated_at (timestamp)
  - INDEX (customer_id)
```

**Task 2: FCM Device Registration — Exact Contract**

```
Route:      POST /mobile/api/save_fcm_token.php
Middleware: none (public — identified by firebase_uid in payload)
Controller: App\Http\Controllers\Mobile\MobileApiController@registerDevice
```

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"
}
```

Logic:
1. Validate `firebase_uid` is present (return 400 if missing)
2. Upsert `customers` record (match on `firebase_uid` + `store_app_id`)
3. If `fcm_token` is present and non-empty:
   - Upsert `customer_devices` (match on `fcm_token` to avoid duplicates)
   - Deactivate any other devices with same `fcm_token` but different customer
4. Return success JSON with token prefix info

**Task 3: Firebase Auth Validation Service**

```
Class: App\Services\FirebaseAuthService
Method: validateIdToken(string $idToken): ?array
```

Logic (same as legacy `mobile-login.php`):
1. POST to `https://identitytoolkit.googleapis.com/v1/accounts:lookup?key={FIREBASE_WEB_API_KEY}`
2. Body: `{"idToken": "{token}"}`
3. If HTTP 200 and `users[0]` exists → return user array (localId, email, displayName, photoUrl)
4. Otherwise → return null

Config required: `FIREBASE_WEB_API_KEY` in `.env`

**Task 4: Mobile Login Bridge**

```
Route:      GET /mobile/mobile-login.php
Middleware: none (public — token in query param)
Controller: App\Http\Controllers\Mobile\MobileAuthController@loginBridge
```

Logic:
1. Read `?token=X&store_app_id=Y` from query string
2. Validate `store_app_id` format (alphanumeric + hyphens/underscores, 1-64 chars)
3. Call `FirebaseAuthService::validateIdToken($token)`
4. If invalid → return 401 plain text error
5. If valid → find or create Customer (by `firebase_uid` + `store_app_id`)
6. Store in session: `customer_id`, `firebase_uid`, `store_app_id`, `customer_name`
7. Redirect to `/portal`

**Task 5: Customer Auth Guard (Middleware)**

```
Class: App\Http\Middleware\CustomerAuth
```

Logic:
- Check session has `customer_id`
- If not → redirect to an error page or return 401
- If yes → bind Customer to request (available in controllers)

Applied to all `/portal/*` routes.

**Task 6: Portal Home (Placeholder)**

```
Route:      GET /portal
Middleware: customer.auth
Controller: App\Http\Controllers\Portal\PortalHomeController@index
View:       resources/views/portal/home.blade.php
Layout:     resources/views/portal/layouts/app.blade.php
```

Placeholder content:
- Mobile-optimized layout (full-width, no desktop sidebar)
- Brand header with logo + color from config or brand record
- "Welcome, {name}!" greeting
- Loyalty points card (showing 0 initially)
- Navigation placeholders (Menu, Orders, Loyalty, Profile — disabled/greyed)
- Sign out button (clears session, returns to a "signed out" page)

**Task 7: Debug Log Endpoint**

```
Route:      POST /mobile/api/debug_log.php
Middleware: none (public)
Controller: App\Http\Controllers\Mobile\MobileApiController@debugLog
```

Request: `{"store_app_id": "X", "logs": [{"tag": "...", "message": "...", "timestamp": "..."}]}`
Response: `{"success": true, "stored": N}`
Storage: Log to `storage/logs/mobile-debug-{date}.log` (file-based for simplicity)

**Task 8: Mobile App Change**

Single line change in `lib/config/app_config.dart`:
```dart
// Change from:
static const String backendUrl = 'https://orderbuddy.tinypay.uk/v2/FNBCLS/menu1/mobile_app';
// Change to:
static const String backendUrl = 'https://icard.orderbuddy.tech/mobile';
```

No other mobile app changes required.

#### Testing Checklist — Milestone 1

| # | Test | Method | Pass Criteria |
|---|------|--------|---------------|
| 1 | Device registration | POST from Postman/app | 200 + correct JSON response, customer + device rows created |
| 2 | Auth bridge (valid token) | GET in browser with real Firebase token | Session created, redirected to /portal |
| 3 | Auth bridge (invalid token) | GET with garbage token | 401 returned, no session |
| 4 | Portal home (authenticated) | Load /portal with valid session | Shows welcome + customer name |
| 5 | Portal home (unauthenticated) | Load /portal without session | Blocked by middleware |
| 6 | Debug log | POST from app | 200, logs written to file |
| 7 | Full flow (app) | Sign in on mobile app → WebView loads | Portal home visible in app |

---

### 5.3 Milestone 2: Customer Portal Features (WebView Content)

> **Goal:** Build the full customer-facing WebView portal with all business features.  
> **Starts after:** Milestone 1 is deployed and tested.  
> **Approach:** New Blade/Tailwind pages, mobile-first design, served within the same Laravel app.

These features will be built as WebView pages initially, then converted to native Flutter screens in Phase 3.

#### Feature Build Order

| # | Feature | Route | Reads From | Complexity | Depends On |
|---|---------|-------|-----------|------------|------------|
| 1 | Menu browsing | `/portal/menu` | Branch DB: `tblCategory`, `tblProducts` | Medium | BranchConnectionManager |
| 2 | Product detail + addons | `/portal/menu/{product}` | Branch DB: `tblAddonGroup`, `tblAddon` | Medium | Menu |
| 3 | Shopping cart | `/portal/cart` | Session | High | Menu + Product detail |
| 4 | Checkout + order placement | `/portal/checkout` | Branch DB: write to `tblOrder` + `tblOrderDetail` | High | Cart |
| 5 | Order history | `/portal/orders` | Branch DB: `tblOrder`, `tblOrderDetail` | Low | — |
| 6 | Customer profile | `/portal/profile` | Central DB: `customers` | Low | — |
| 7 | Loyalty points | `/portal/loyalty` | Central DB + Branch DB: `tbroyalty` | Medium | Orders (earn) |
| 8 | Rewards redemption | `/portal/loyalty/rewards` | Branch DB: `tbroyalty` | Medium | Loyalty |
| 9 | Promo codes | (part of checkout) | Branch DB: `tblPromoCode` | Low | Cart |
| 10 | Business hours | (validation on cart/checkout) | Branch DB: `tblSettings` | Low | — |
| 11 | ~~Push notification sending (admin)~~ | ~~Admin dashboard panel~~ | ~~Central DB: `customer_devices`~~ | ~~Medium~~ | ✅ **DONE** (implemented in Milestone 1, Task 11) |
| 12 | Delivery zone validation | (part of checkout) | Branch DB config | Low | Checkout |

#### Key Design Decisions

- **All portal pages** use a shared mobile-first Blade layout (no admin nav)
- **Customer session** persists across portal pages (set by Milestone 1 login bridge)
- **Branch connection** resolved per-customer: Customer → Brand → Branch → branch DB
- **Cart** stored in Laravel session (serialized array) — no DB cart table needed initially
- **Tax calculation** uses exact legacy formula (Appendix C)
- **Loyalty points** earned automatically after order placement

---

### 5.4 Mobile App Changes (Complete Phase 1)

| Change | File | Description |
|--------|------|-------------|
| Backend URL | `lib/config/app_config.dart` | Change `backendUrl` to Laravel URL |

**That's it.** No other mobile changes required for the entire Phase 1 (both milestones).

### 5.5 Deployment Plan

**Milestone 1 (auth + placeholder):**
1. Implement on `feature/main-20260603` branch
2. Deploy to production (`icard.orderbuddy.tech`)
3. Test auth bridge with real Firebase token in browser
4. Update mobile app `backendUrl` → test on device
5. Confirm full flow: app login → WebView → placeholder home

**Milestone 2 (portal features):**
1. Build features incrementally (menu first, then cart, then orders...)
2. Each feature is deployable independently — portal home links to completed features
3. No mobile app update needed — WebView automatically shows new pages as they're deployed
4. Final validation: all features working end-to-end

---

## 6. Phase 2 Improvements

After Phase 1 achieves feature parity:

### API Improvements
- Add proper REST API layer (`/api/v1/`) with Sanctum tokens for future native screens
- API versioning headers
- Standardized JSON error responses
- Request rate limiting
- API documentation (OpenAPI/Swagger)

### Authentication Improvements
- Replace Firebase session bridge with Sanctum token exchange
- Add biometric unlock support (app-side)
- Refresh token rotation
- Device authorization (trusted devices)

### Performance
- Redis caching for menu data
- CDN for product images
- Lazy-load portal pages
- Optimize DB queries (eager loading)

### Admin Features
- Customer management dashboard (view customers, loyalty, orders)
- Promotion campaign builder
- Push notification scheduler (queue-based — current implementation sends immediately)
- Customer analytics/insights

### Architecture
- Event-driven loyalty (earn/redeem events)
- Queue-based notification delivery
- Audit logging for sensitive operations

---

## 7. Phase 3 Native Roadmap

After Phase 2 APIs are stable, progressively replace WebView with native Flutter screens:

### Priority Order

| # | Screen | Replace WebView With | Benefit | Effort |
|---|--------|---------------------|---------|--------|
| 1 | Home/Loyalty | Native dashboard widget | Instant load, offline balance | Low |
| 2 | Order History | Native list + detail | Better UX, pull-to-refresh | Low |
| 3 | Profile | Native form | Secure, native validation | Low |
| 4 | Promotions | Native cards/carousel | Visual impact, engagement | Medium |
| 5 | Menu Browsing | Native grid/list | Performance, search, offline | High |
| 6 | Cart + Checkout | Native flow | Payment integration, UX | High |
| 7 | Order Tracking | Native real-time | Push-driven status updates | Medium |

### App Architecture (Phase 3)

```
lib/
├── core/
│   ├── api/         (Sanctum-authenticated API client)
│   ├── auth/        (Firebase + Sanctum token management)
│   ├── models/      (Customer, Order, Product, Loyalty)
│   └── cache/       (Local SQLite for offline)
├── features/
│   ├── auth/        (Login — keep existing)
│   ├── home/        (Native loyalty dashboard)
│   ├── menu/        (Native menu browsing)
│   ├── cart/        (Native cart + checkout)
│   ├── orders/      (Native order history + tracking)
│   ├── loyalty/     (Native points + rewards)
│   ├── profile/     (Native settings)
│   └── notifications/ (Keep existing)
└── config/
    └── app_config.dart
```

### WebView Removal Criteria
A WebView section is ready for removal when:
1. Native equivalent has feature parity
2. API endpoint is stable (no breaking changes planned)
3. Offline fallback works (cached data shown when offline)
4. User testing validates UX improvement

---

## 8. Risk Assessment

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Branch DB schema doesn't match expected tables | Medium | High | Verify table structure before building queries |
| Firebase token validation differs from legacy | Low | Medium | Test with real tokens early |
| WebView session/cookie issues on new domain | Medium | Medium | Test cross-domain cookies, use SameSite=None if needed |
| Tax calculation discrepancy | Medium | High | Port exact legacy formula, test with known values |
| Menu/product data structure varies per restaurant | Medium | Medium | Design flexible schema reading |
| App Store review delay for URL change | Low | Low | URL change is minor — no review concern |
| Portal UX doesn't match legacy (user confusion) | Low | Low | No active users to confuse |

### Assumptions
- Laravel portal will be served from `icard.orderbuddy.tech` (existing domain)
- Branch databases are accessible from the Laravel server
- Firebase project credentials are available for token validation
- The portal will initially support one brand/restaurant (Friez & Burgz) as proof of concept

---

## Appendix A: Legacy Endpoint Signatures

### `save_fcm_token.php` — Full Contract

**Request:**
```http
POST /api/save_fcm_token.php
Content-Type: application/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": "iOS: APNs=yes | FCM token obtained (180 chars)"
}
```

**Response (success):**
```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"
}
```

**Response (error):**
```json
{
  "success": false,
  "message": "Missing required field: firebase_uid"
}
```

### `mobile-login.php` — Full Contract

**Request:**
```http
GET /mobile-login.php?token=eyJhbGciOi...FIREBASE_ID_TOKEN&store_app_id=friezandburgz
```

**Success:** HTTP 302 redirect to customer portal home  
**Failure:** HTTP 401 with plain text error message

**Validation method:** POST to `https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=FIREBASE_WEB_API_KEY` with `{"idToken": "..."}` — returns user info.

### `debug_log.php` — Full Contract

**Request:**
```http
POST /api/debug_log.php
Content-Type: application/json

{
  "store_app_id": "friezandburgz",
  "logs": [
    {"tag": "FCM", "message": "Token obtained", "timestamp": "2026-06-03T12:00:00Z"},
    {"tag": "AUTH", "message": "Google sign-in success", "timestamp": "2026-06-03T12:00:01Z"}
  ]
}
```

**Response:**
```json
{"success": true, "stored": 2}
```

---

## Appendix B: Branch Database Tables (Customer-Facing)

These tables exist in each restaurant's branch database and must be read by the customer portal:

```sql
-- Products & Menu
tblCategory       (id, catName, catImage, catOrder, isVisible)
tblProducts       (id, pName, pPrice, pDesc, catID, pImage, vat_rate, tax_included, inStock, isVisible)
tblAddonGroup     (id, groupName, productID, isRequired, maxSelect)
tblAddon          (id, addonName, addonPrice, groupID)

-- Orders
tblOrder          (id, cusID, orderTotal, orderTax, orderStatus, paymentType, paymentStatus, 
                   shipType, orderDate, orderNote, shippingCharge, accountID)
tblOrderDetail    (id, orderID, productID, qty, lineTotal, addonString)

-- Customers
tblCustomer       (id, cusName, cusEmail, cusPhone, cusAddress, cusPostcode, cusCity, 
                   cusCounty, cusCountry, cusPassword, cusPoints, createdAt)

-- Loyalty & Promotions
tbroyalty         (id, bizName, pricePerPoint, pointsRequired, rewardByDiscount, 
                   rewardByFixedAmount, rewardByProduct, rewardProductURL)
tblPromoCode      (id, code, discountPercent, validFrom, validTo, isActive, usageLimit)

-- Business Info
tblSettings       (id, key, value)  -- contains business hours, delivery config, etc.
```

---

## Appendix C: Tax Calculation Logic (Must Match Legacy)

```php
// Legacy PHP tax calculation (must be replicated exactly)

// If tax_included = 1 (price already includes VAT):
$vat_amount = $price - ($price / (1 + ($vat_rate / 100)));
$net_price = $price - $vat_amount;
// Customer pays: $price (as listed)

// If tax_included = 0 (price is ex-VAT):
$vat_amount = $price * ($vat_rate / 100);
$final_price = $price + $vat_amount;
// Customer pays: $final_price
```

---

## Appendix D: Loyalty Points Logic

```php
// Legacy loyalty calculation
$loyalty_config = query("SELECT * FROM tbroyalty LIMIT 1");

// Earning points:
$points_earned = floor($order_total / $loyalty_config['pricePerPoint']);

// Redeeming:
if ($customer_points >= $loyalty_config['pointsRequired']) {
    // Apply reward (discount OR fixed amount OR free product)
    if ($loyalty_config['rewardByDiscount'] > 0) {
        $discount = $order_total * ($loyalty_config['rewardByDiscount'] / 100);
    } elseif ($loyalty_config['rewardByFixedAmount'] > 0) {
        $discount = $loyalty_config['rewardByFixedAmount'];
    }
    $customer_points -= $loyalty_config['pointsRequired'];
}
```
