# Deployment Guide — Alva Requor Store POS

Target stack: PHP 8.1+ (developed/tested on 8.2), MySQL 8+ / MariaDB 10.4+, Nginx or Apache, Linux server. Laravel 10.

---

## 1. Server requirements

- PHP 8.1+ with extensions: `pdo_mysql`, `mbstring`, `bcmath` (used throughout for all money/quantity math — do not disable), `openssl`, `tokenizer`, `xml`, `ctype`, `json`, `fileinfo`, `gd` or `imagick` (product image uploads).
- MySQL 8+ or MariaDB 10.4+ (InnoDB; the schema relies on foreign keys and row-level locking via `SELECT ... FOR UPDATE`, which MyISAM does not support — do not switch the storage engine).
- Composer 2+, Node.js 18+/npm (build-time only — Node is not needed on the production server itself if you build assets in CI/CD and deploy the `public/build` output).
- A process supervisor (systemd or supervisord) if you ever add a queued job (see §5 — not required today).
- `mysqldump` available on the server (see §7 — required for the backup command).

## 2. Installing the codebase

```bash
git clone <repo> smart-pos && cd smart-pos
composer install --no-dev --optimize-autoloader
npm install && npm run build
cp .env.example .env
php artisan key:generate
```

`--no-dev` excludes `phpunit`, `laravel/pint`, `laravel/sail`, `spatie/laravel-ignition` (the dev-only detailed-error-page package) from production. `npm run build` compiles `resources/js`/`resources/css` into `public/build` — `npm` itself is not needed after this step.

## 3. Configuring `.env`

Copy every value from `.env.example` and set at minimum:

- `APP_ENV=production`
- `APP_DEBUG=false` — **must** be false. `true` renders full stack traces, file paths, and raw SQL exception text to visitors (see `config/logging.php`/SECURITY_AUDIT.md). This is the single most important production setting in this file.
- `APP_URL` — the real public URL, no trailing slash, no `/public` suffix (point your web server's document root at the `public/` folder instead — see §4).
- `APP_TIMEZONE` — defaults to `Africa/Dar_es_Salaam`; every date-range report, shift, and expiry check runs off this. Change only if deploying outside East Africa.
- `DB_*` — production database credentials. Use a dedicated MySQL user scoped to this one database, not `root`.
- `SESSION_SECURE_COOKIE=true` — required once served over HTTPS (which production must be). Leaving this `false`/blank lets the session cookie travel over plain HTTP.
- `LOG_LEVEL=error` (or `warning`) — `debug` is appropriate for local dev only; at `debug` in production the daily log fills with noise that makes real errors harder to find.
- `MYSQLDUMP_PATH` — only needed if `mysqldump` isn't already on the server's `PATH`.
- `BCRYPT_ROUNDS=12` — leave as-is unless your server is unusually slow; never lower it.
- Mail (`MAIL_*`) — only relevant if a future change adds email notifications; today the app sends none, so `MAIL_MAILER=log` is harmless to leave as-is.

## 4. Web server document root

Point the web server's document root at `public/`, **not** the project root. Never expose `app/`, `config/`, `database/`, `.env`, or `storage/` directly — the `public/` folder is the only directory meant to be web-reachable, and `public/.htaccess`/the Nginx config below are what route every request through `public/index.php`.

**Nginx** (typical):
```nginx
root /var/www/smart-pos/public;
index index.php;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.(?!well-known).* { deny all; }
```

**Apache**: point the vhost's `DocumentRoot` at `public/` and ensure `AllowOverride All` so `public/.htaccess` (Laravel's default rewrite rules) takes effect.

If you cannot change the document root (e.g. shared hosting serving from the project root, matching the current local XAMPP dev setup with `APP_URL=http://localhost/SMART_POS/public`), the app still works, but every other file in the repo becomes web-reachable unless your host separately restricts access — avoid this in a real production deployment.

## 5. First deploy: migrations, seeding, permissions

```bash
php artisan migrate --force
php artisan db:seed --class=RolesAndPermissionsSeeder --force
```

`--force` is required because `APP_ENV=production` otherwise prompts for confirmation (there's no interactive TTY in most deploy pipelines). Do **not** run `migrate:fresh` or the full `db:seed` (which also runs `BusinessSeeder`, creating the demo business and four fixed-password demo accounts) against a real production database — those seeders are meant for local/demo setup only. Create your real business/branch/admin user through the application itself (or a one-off tinker session) instead.

File permissions: `storage/` and `bootstrap/cache/` must be writable by the web server user (e.g. `www-data`):
```bash
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
```

Product images and expense receipts are served from `storage/app/public` — link it:
```bash
php artisan storage:link
```

## 6. Production optimization commands

Run after every deploy (after `composer install`, before serving traffic):

```bash
php artisan config:cache      # freezes all config/*.php + .env into one file
php artisan route:cache       # precompiles the route table
php artisan event:cache       # precompiles event/listener bindings
php artisan view:cache        # precompiles Blade templates
```

**Caveat that matters here specifically**: `config:cache` freezes `.env` values into the cache — if you change anything in `.env` after caching, it will **not** take effect until you re-run `php artisan config:cache` (or `config:clear`). This has bitten real deployments where an admin edited `.env` directly on the server and couldn't figure out why nothing changed.

To safely clear everything during troubleshooting: `php artisan optimize:clear` (clears all four caches above at once).

## 7. Backup strategy

A custom command, `php artisan app:backup-database`, dumps the database via `mysqldump`, gzip-compresses it, and writes it to `storage/app/backups/{database}_{timestamp}.sql.gz` (private — not web-reachable, and already excluded from git via `storage/app/.gitignore`). It also prunes backups older than `--keep-days` (default 14).

It is registered on the scheduler (`app/Console/Kernel.php`) to run daily at 02:00 (server timezone = `APP_TIMEZONE`). The scheduler itself needs exactly one cron entry on the server:

```
* * * * * cd /var/www/smart-pos && php artisan schedule:run >> /dev/null 2>&1
```

Laravel's scheduler then decides internally what's actually due each minute — you do not add a separate cron line per scheduled task.

**Checking backup status**: there is no dedicated UI page for this (deliberately — see the production-readiness checklist for why). Check via:
```bash
ls -la storage/app/backups/              # newest file's timestamp = last successful backup
tail -f storage/logs/laravel-*.log | grep -i backup   # "Database backup completed" / "Database backup failed"
```
A failed backup logs `Log::error('Database backup failed', [...])` with the `mysqldump` error output — monitor your log aggregator (if any) for that string, or `grep` the daily log file.

**Restoring** from a backup:
```bash
gunzip -c storage/app/backups/retail_db_2026-08-13_020000.sql.gz | mysql -u <user> -p <database>
```
Test this on a scratch database before you ever need it for real — an untested backup is not a backup.

**What this does not cover**: uploaded files (`storage/app/public` — product images, expense receipts). For a complete disaster-recovery strategy, also sync that directory off-server (e.g. `rsync`/`rclone` to off-site storage on the same or a similar schedule) — this is a documented recommendation, not implemented, since it requires choosing a destination (S3, another host, etc.) specific to your hosting setup.

## 8. Queue

`QUEUE_CONNECTION=sync` is the correct default — there are no queued jobs anywhere in the codebase today (verified: no class implements `ShouldQueue`), so `sync` (run inline, no worker process needed) has zero downside and one fewer moving part to operate. If a future change adds a queued job/notification, switch to `QUEUE_CONNECTION=database` and run a supervised worker:
```
php artisan queue:work --tries=3 --max-time=3600
```
under systemd/supervisord (auto-restart on crash), not as a bare background process.

## 9. Scheduler

Covered in §7 — one cron line (`schedule:run` every minute) drives everything Laravel schedules internally, currently just the daily backup.

## 10. Health check / smoke test after deploy

1. `php artisan about` — confirm `Debug Mode: OFF`, `Environment: production`, correct DB driver.
2. Visit `/login`, sign in, confirm the dashboard loads.
3. `php artisan app:backup-database` once manually — confirm it succeeds and a `.sql.gz` file appears in `storage/app/backups`.
4. Ring up one real POS sale end-to-end (barcode scan or manual add → payment → complete) against a test product, then void/cancel it, to confirm the inventory/finance write path is healthy on the production database before real cashiers start using it.

## 11. Rolling back a bad deploy

```bash
php artisan down                         # maintenance mode
git checkout <previous-tag>
composer install --no-dev --optimize-autoloader
php artisan migrate:rollback --step=<N>  # only if the bad deploy included migrations - see caution below
php artisan optimize:clear && php artisan config:cache route:cache view:cache
php artisan up
```

Only roll back migrations if you're certain no data written under the new schema needs to survive — `migrate:rollback` runs each migration's `down()`, which for this codebase drops the tables/columns/constraints those migrations added. Take a fresh backup (§7) before rolling back schema changes on a database with real transactions in it.
