# ONINA — Deployment & Operations Guide

Luxury Gulf abaya store. Laravel 12 + Filament 4 + Livewire 3 + MySQL.

---

## 🚀 Quick deploy — Namecheap Shared (cPanel)

For shared-hosting plans. Use the **deployment tarball** (`onina-deploy.tar.gz`) we ship; no Composer or Node required on the server.

### 0. Server requirements

In **cPanel → Select PHP Version**:
- PHP **8.3+** (Filament 4 / Laravel 12 minimum).
- Enable extensions: `gd`, `intl`, `bcmath`, `exif`, `pdo_mysql`, `mbstring`, `openssl`, `tokenizer`, `xml`, `ctype`, `fileinfo`, `curl`, `zip`.

### 1. Create the database

cPanel → **MySQL Databases**:
- Create database `cpaneluser_oninastore`.
- Create user + strong password.
- Grant **ALL PRIVILEGES** to the user on the database.

### 2. Upload the project

Upload `onina-deploy.tar.gz` to your home directory (NOT `public_html`). In **File Manager → Extract**:

```
/home/<cpaneluser>/oninastore/        ← Laravel project root
/home/<cpaneluser>/public_html/       ← what the web sees
```

### 3. Point `public_html` at Laravel's `public/`

Two ways, pick one:

**A) Copy `public/` contents into `public_html/` (safest, no SSH needed):**
1. Move everything in `oninastore/public/*` into `public_html/`.
2. Edit `public_html/index.php` and change the two require lines:

```php
require __DIR__.'/../oninastore/vendor/autoload.php';
$app = require_once __DIR__.'/../oninastore/bootstrap/app.php';
```

**B) Symlink (if SSH/Terminal is available):**
```bash
cd ~/public_html && ln -s ../oninastore/public ./oninastore-public  # then change webroot in cPanel
```

### 4. Configure `.env`

In the project root (`oninastore/`):
```bash
cp .env.production.example .env
```
Edit `.env` and fill: `APP_URL`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`.

### 5. Run the installer (cPanel → Terminal, or SSH)

```bash
cd ~/oninastore
bash install-on-server.sh
```

This runs: `key:generate`, `storage:link`, `migrate --force`, baseline seeders, `optimize`, `filament:cache-components`, and creates the first admin user.

### 6. Verify

- Open `https://YOUR-DOMAIN/` → storefront.
- Open `https://YOUR-DOMAIN/login` → admin + customer login (single page).
- Sign in as the seeded admin (default `admin@onina.om` / `CHANGE-ME-NOW`) and **change the password immediately** from Profile.

### Important constraints on shared hosting

- ⚠️ **No `ffmpeg`/`cwebp` available** → do NOT run `php artisan onina:import-assets` on the server. Run it **locally** (we already did), and the optimised `storage/app/public/media/` is included in the deployment tarball.
- ⚠️ **No long-running queue worker** → `.env` ships with `QUEUE_CONNECTION=database`. If you later need async work, add a cron that runs `php artisan queue:work --stop-when-empty` every minute via cPanel **Cron Jobs**.
- ⚠️ **Sessions in DB** → no Redis on shared. The `SESSION_DRIVER=database` + `CACHE_STORE=file` defaults are tuned for this.

---

## VPS / Forge deployment (production-grade)

## Recommended hosting

| Layer | Recommendation |
|---|---|
| Server | VPS (Hetzner CPX / DigitalOcean) provisioned with **Laravel Forge** |
| PHP | 8.3+ with `gd`, `intl`, `bcmath`, `exif`, OPcache enabled |
| Web | Nginx + PHP-FPM |
| DB | MySQL 8 (managed or on-box with daily backups) |
| Cache/Queue/Session | **Redis** |
| Media/CDN | Object storage (S3 / Cloudflare R2) behind **Cloudflare CDN** |
| Video transcoding | `ffmpeg` + `cwebp` installed on the box (used by `onina:import-assets`) |
| TLS | Let's Encrypt (auto via Forge) — HSTS header is emitted on HTTPS |

## First deploy

```bash
git clone … && cd ONINASTORE
composer install --no-dev --optimize-autoloader
npm ci && npm run build
cp .env.example .env   # then fill values (see below)
php artisan key:generate
php artisan migrate --force
php artisan db:seed --class=CurrencySeeder --force
php artisan db:seed --class=SettingsSeeder --force
php artisan storage:link
php artisan optimize          # config + route + view + event cache
# create the first admin:
php artisan tinker --execute="App\Models\User::create(['name'=>'Admin','email'=>'you@onina.om','password'=>bcrypt('CHANGE_ME')]);"
# import & optimise brand media (point --source at the ONINA assets folder):
php artisan onina:import-assets --source=/path/to/ONINA
```

### Key `.env` values
- `APP_ENV=production`, `APP_DEBUG=false`, `APP_URL=https://onina.om`
- `DB_*` → MySQL credentials
- `CACHE_STORE=redis`, `SESSION_DRIVER=redis`, `QUEUE_CONNECTION=redis`
- `ASSET_URL=https://cdn.onina.om` (or leave empty to serve `/storage` from the app)
- Amwal Pay: set `PAYMENT_AMWAL_ENABLED=true` + `AMWAL_MERCHANT_ID` + `AMWAL_SECRET_KEY` once credentials arrive (driver scaffold in `app/Services/Payment/Gateways/AmwalGateway.php`).

## Subsequent deploys (zero-downtime via Forge)

```bash
php artisan down
git pull && composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan optimize:clear && php artisan optimize
php artisan up
```

## Caching strategy

- **App caches:** `php artisan optimize` (config/route/view/event) on every deploy.
- **OPcache:** enable in production `php.ini` (`opcache.enable=1`, `opcache.validate_timestamps=0`).
- **Domain caches (already wired):**
  - `nav.categories` — header/footer categories (1h; auto-flushed on Category save/delete).
  - `currencies.active` — active currency list (1h).
  - `brand.logo` — logo asset path (forever; run `php artisan cache:clear` if the logo file is replaced).
  - `setting.*` — store settings (forever; auto-flushed on Setting save).
- **HTTP caching:** serve `/storage` (or the CDN origin) with long `Cache-Control: max-age` + immutable, since media filenames are content-hashed/width-suffixed.
- **Queue worker** (for future async media conversions / mail): `php artisan queue:work redis --tries=3` under Supervisor.

## Performance notes (implemented)

- Images: source assets converted to **responsive WebP** (400/800/1200/1600) + product gallery conversions (thumb/card/zoom). Served with `loading="lazy"` and `srcset`.
- Videos: source `.mov` transcoded to web **MP4** (H.264, faststart) + WebP poster; hero video is muted/autoplay/loop.
- Assets referenced via **relative `/storage`** (or `ASSET_URL` CDN) so they work on any host.
- Catalog/nav queries cached; eager-loading (`with`) on product/category queries to avoid N+1.

## Media storage structure

```
storage/app/public/media/
  brand/      onina-logo-{w}.webp, onina-icon-{w}.webp
  onina/
    images/   product-NNN-xxxx-{w}.webp        (responsive product photography)
    videos/   onina-NNN-xxxx.mp4               (web-optimised hero/marketing video)
    posters/  onina-NNN-xxxx.webp              (video poster frames)
  categories/ <uploaded>
  tailoring/  <measurement illustrations>
storage/app/public/invoices/  ONA-…png         (generated invoice images)
public/storage  →  symlink to storage/app/public
```
For production, mirror `storage/app/public` to S3/R2 and set `ASSET_URL` to the CDN. Keep the **original** ONINA source files (the un-optimised `img/` + `video/`) backed up off-server; only optimised derivatives are served.

## Security checklist (implemented)

- Security headers + Content-Security-Policy (`app/Http/Middleware/SecurityHeaders.php`).
- Rate limiting: `api` (60/min) and `tracking` (10/min, anti-enumeration) limiters.
- CSRF protection on web; Amwal callback explicitly excluded and signature-verified (on integration).
- Admin panel gated by `FilamentUser::canAccessPanel`; staff live in `users`, shoppers in `customers`.
- Form validation on all inputs; file uploads constrained to images via Filament/medialibrary.
- Set `APP_DEBUG=false` in production; rotate `APP_KEY` is **not** needed unless compromised.

## API (v1)

Public, rate-limited, JSON (bilingual fields, prices in OMR):
- `GET /api/v1/categories`
- `GET /api/v1/products` (`?category=slug`, `?featured=1`, `?per_page=`)
- `GET /api/v1/products/{slug}`
- `GET /api/v1/orders/{number}/track`
- `GET /api/v1/me` (Sanctum token)

`GET /sitemap.xml` and `/robots.txt` are generated dynamically.
