# Pishon — Production Deployment & Release Runbook

Covers: preparing a clean release, environment variables, migrations, cache,
scheduler, queue worker, webhook wiring, and the hardening features shipped in
the production-readiness pass.

## 0. Tenant subdomain (wildcard) architecture

Every school gets its own clean URL; the file path of the install never leaks
into browser URLs.

| Served at                | What it is                                              |
|--------------------------|---------------------------------------------------------|
| `https://pishonedu.com.ng`            | Platform (landing, account, platform admin) |
| `https://{slug}.pishonedu.com.ng`     | That school's public website (clean short paths) |
| `https://{slug}.pishonedu.com.ng/school/{id}/…` | That school's admin/dashboard (kept path-based on purpose) |

Routing notes:

- `routes/tenant.php` registers a `Route::domain('{schoolSub}.{base_domain}')`
  group — the base domain is inserted **literally** from `APP_BASE_DOMAIN`
  (Laravel domain parameters cannot contain dots). It is `require`d at the top
  of `routes/web.php` so a subdomain request is matched *before* the apex `/`
  landing route.
- The apex, `localhost`/`127.0.0.1`, reserved subdomains (see
  `app/Services/Tenant/TenantResolver.php`) and unknown subdomains fall
  through; unknown subdomains get a branded `School not found` 404 from the
  `website.tenant` middleware.
- The legacy path-based URLs `/school/{schoolSlug}/…` still work and reuse the
  exact same controllers ("Apply", admissions centre, news, gallery, …).
- `tenant_url('public.*', …)` is the view-side helper: on a school's own
  subdomain/custom domain it strips the `/school/{slug}` prefix so links are
  clean short paths; everywhere else it returns the normal path-based URL.

Deployment requirements for subdomains:

1. **DNS wildcard**: an `A`/`AAAA` (or CNAME) record `*.pishonedu.com.ng`
   pointing to the hosting server, plus the apex record. In cPanel create a
   *Wildcard Subdomain* (`*.`) attached to the account.
2. **TLS**: a wildcard certificate covering `*.pishonedu.com.ng` (AutoSSL can
   issue a % wildcard subdomain cert; or a Let's Encrypt wildcard). Required
   because `SESSION_SECURE_COOKIE=true`.
3. **Document root**: two valid layouts, both remove any
   `/school-cms/school-platform/public/` prefix from URLs — the app generates
   root-relative URLs only:
   - **cPanel split layout (recommended, see `docs/cpanel-production-deployment.md`)**:
     app at `…/school-platform`; the fixed `public_html` document root contains
     Laravel's `public/` contents, and `public_html/index.php` bootstraps
     `../school-platform`. Package with `deployment/build-package.ps1`.
   - Alternate: point the domain's document root at `…/school-platform/public`
     (works when the host allows changing the main-domain docroot), with
     `storage/app/public` symlinked to `public/storage`.
4. **`.env`**: `APP_URL=https://pishonedu.com.ng`,
   `APP_BASE_DOMAIN=pishonedu.com.ng`, `SESSION_DOMAIN=null` (cookies are
   host-scoped per tenant). After **changing `APP_BASE_DOMAIN`**, rerun
   `php artisan route:cache` — the subdomain route literal is baked into the
   cache at build time.
5. **Custom domains** (schools that bring their own domain): stored in the
   school's `domain` field; requests for that host resolve to the school and
   the bare root redirects to the school's path-based home.

## 1. Build the release package

The repo root (`C:\xampp\htdocs\school-cms`) is the workspace. The releasable
artifact is the `school-platform/` folder. `.gitattributes` already excludes
dev/test/environment junk from exports.

From the workspace root, produce a clean archive with:

```
git archive --format=zip -o pishon-release.zip --prefix=school-platform/ 9e5c65e
```

Replace the commit hash with a release tag (e.g. `v1.0.0`). Untracked junk
(`composer.phar`, stray `*.html`, cookies/log dumps) was removed from the repo
root and is excluded via `.gitignore`/`.gitattributes`.

On the server:

```
cd /var/www
unzip pishon-release.zip
cd school-platform
composer install --no-dev --optimize-autoloader --classmap-authoritative
```

(`node_modules/`, `tests/`, `.env*` backups and local caches are excluded from
the archive; `vendor/` is never shipped — always run `composer install`.)

## 2. Environment (`.env.example` is the template)

`APP_ENV=production`, `APP_DEBUG=false`. Set `APP_BASE_DOMAIN` (used by
subdomain routing). Production flags worth enabling:

| Variable                     | Meaning                                                            |
|------------------------------|--------------------------------------------------------------------|
| `SESSION_SECURE_COOKIE=true` | Session cookie only over TLS                                      |
| `SESSION_SAME_SITE=lax`      | CSRF-friendly same-site policy                                    |
| `TRUSTED_PROXIES`            | Comma-separated proxy IP/CIDRs in front of the app (load balancer, Cloudflare). Empty = hosted directly (default; safest) |
| `SECURITY_HEADERS_ENABLED=true` | Adds CSP, HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy |
| `SECURITY_HSTS_ENABLED=true` | HSTS header on HTTPS requests (only with the above enabled)       |
| `SECURITY_CSP_REPORT_ONLY=false` | Roll the CSP out in report-only first (violations logged, nothing blocked) |
| `SECURITY_CSP_STRICT_SCRIPTS=false` | Drop `unsafe-inline` from `script-src`, replacing it with a per-request nonce. Only after migrating remaining inline components to `nonce="{{ $cspNonce }}"`; try under `SECURITY_CSP_REPORT_ONLY=true` first |
| `MAIL_MAILER=smtp`           | Real mail transport                                              |
| `MAIL_QUEUE_ENABLED=true`    | Route all mail through the queue worker                          |
| `PAYSTACK_SECRET_KEY/FLUTTERWAVE_SECRET_KEY` + webhook secrets | Gateway/school money (also the fallback for schools in "Pishon gateway" mode)       |
| `PLATFORM_BILLING_PAYSTACK_SECRET_KEY` etc. | Pishon's own billing             |

CSP is config-driven in `config/security.php`. The in-page checkout calls
Paystack/Flutterwave and the parent portal may embed Vimeo/YouTube — those
hosts are already allow-listed in the default CSP `connect-src`/`frame-src`.

## 3. Migrations & caches

```
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
```

Caches were verified to build cleanly. Never enable `config:cache` while
editing env locally; the deploy pipeline re-runs it after every release.

## 4. Scheduler (cron) — required

```cron
* * * * * cd /var/www/school-platform && php artisan schedule:run >> /dev/null 2>&1
```

The schedule (`routes/console.php`) currently runs every day:

- `00:30` finance overdue flags
- `00:45` plan seeding (`subscription:ensure-plans`) — Sundays; recreates the three
  default plans if missing (idempotent, never overwrites admin edits)
- `01:15` recurring expense generation
- `01:15` platform backup (`backup:run`) — Sundays; database snapshot + uploaded
  files + manifest (see §9)
- `02:30` subscription billing (`subscription:bill`)
- `08:00` trial lifecycle (`subscription:check-trials`) — warns owners one day
  before trial expiry and flips expired trials to `INACTIVE`/`EXPIRED`; emails
  the school owner at both points.
- `09:00` payment reminders (`finance:send-reminders`)
- every 5 min `queue:work --once` belt-and-braces keep-alive for the mail queue

For servers with no native cron (some Windows/CPanel), schedule via a task
scheduler that invokes `php artisan schedule:run` every minute.

## 5. Queue worker — required for mail

`MAIL_QUEUE_ENABLED=true` pushes every email through the queue. Run a durable
worker:

```
php artisan queue:work --sleep=3 --max-time=3600 --daemon
```

Supervisor / systemd should restart it on exit. Without a worker, mail stays
in the `jobs` table and notifications (admission, results, invoices, 2FA,
trial warnings) will not arrive.

## 6. Payment webhooks

Both money paths share one endpoint: `/api/webhooks/payments/{provider}`
(CSRF-exempt, signature-verified, idempotent by transaction `reference`).

- **School money**: configure Paystack/Flutterwave webhook URL to
  `https://YOUR-DOMAIN/api/webhooks/payments/paystack`
  (or `/flutterwave`), and store the `webhook_secret` in each school's gateway
  settings (School → Finance → Payment gateways).
- **Pishon money**: platform-level webhooks call the same URL with the
  platform secrets.
- The controller resolves school gateway config by the *transaction's own
  school* — never the first config across tenants — so school-specific secrets
  validate correctly.
- **Config pinning:** each new charge snapshots its `gateway_mode` and
  `gateway_config_id` into the transaction at init. Webhooks/verification
  re-resolve that **same** config, so a school switching gateway mode or
  deactivating keys mid-flight never breaks validation of in-flight charges.
- **Unknown references 404 immediately** — no callback probes other tenants'
  configs looking for a match; a forged signature on a *known* reference gets
  401.

## 7. Hardening features from this pass — operational notes

- **Super Admin 2FA (TOTP)**: Platform → My Profile → Security (or
  `/platform/security`). Enable with an authenticator app; recovery codes are
  hashed at rest and shown once. Admins with 2FA prompted at every new
  login session.
- **Login throttling**: 5 attempts/minute per email+IP per guard (platform,
  school). A lockout returns a friendly message; success clears the counter.
- **Password policy**: min 8 chars + letters + numbers enforced at platform
  registration, both password resets, and all change-password surfaces.
- **Plan enforcement**: `PlanLimitService` blocks creating students, teachers,
  classes and admission approvals beyond the active plan's
  `max_students/max_teachers/max_classes`. Schools with no plan, or a bound of
  `0`/null, are unlimited. Admins see an upgrade prompt, not an error.
- **Upload security**: `FileUploadService` rejects dangerous payloads
  (executables, PHP scripts, SVGs) by content; `storage/app/public/.htaccess`
  denies script execution in uploads.
- **Security headers**: CSP mitigates XSS/injection; only ships in production
  so local Vite HMR is unaffected.

## 8. Rollout order

1. Run migrations (section 3).
2. Start the queue worker (section 5).
3. Install cron (section 4).
4. Configure `.env` production flags + gateways (sections 2, 6).
5. Smoke test one tenant: log in, create a student, trigger an email, approve a
   payment webhook, enable 2FA on the super-admin account.

## 9. Phase 2 — commercial & operations

### 9.1 Self-service subscription payments

School admins pay for plans through Pishon's own Paystack credentials
(`services.paystack.secret_key` / `webhook_secret`). The `initialize` request
carries an HTTPS `callback_url` back to
`https://YOUR-DOMAIN/school/{schoolId}/billing/callback`; the callback only
redirects the admin afterwards — every confirmation is the **server-side
verify** (never the URL parameters), and the reference is checked against the
school's own invoice before any state is written. No Paystack dashboard
"callback URL" entry is required — it is purely a redirect target.

### 9.2 Backups (`backup:run`)

- Output: `storage/app/backups/<Ymd_His>-<id>/` with `db.sql(.sqlite)`
  (SQLite file copy, or a logical `CREATE`+`INSERT` dump for MySQL/PostgreSQL),
  a copy of `storage/app/public/uploads` and a `manifest.json`.
- **Keep `storage/app/backups` writable**; the weekly Sunday 01:15 run and the
  manual "Run backup now" button (Platform → System → Backups) write there.
- **Extract on a shared host**: `php artisan` requires the `zip` extension
  enabled for downloading backups (bundled by default with XAMPP and most
  cPanel PHP builds — verify via `php -m`).
- Restore is intentionally manual (docs-era, human-reviewed): drop the release's
  SQL file into a fresh DB (or replace the SQLite file), restart migrations if
  desired, and restore `uploads/` back under `storage/app/public`. Never
  automate restore blindly in production.

### 9.3 cPanel specifics

- PHP: select **8.2** (must have `pdo_mysql`, `mbstring`, `openssl`, `fileinfo`,
  `tokenizer`, `xml`, `ctype`, `json`, `zip`). Enable **CGI/CGI-Force** in
  MultiPHP INI Editor if the app 500s on shared hosts.
- Document root: point the addon/primary domain at `public/`.
- `storage/` and `bootstrap/cache/` must be owned/writable by the PHP user.
- Cron: create a cron job (`* * * * * /usr/local/bin/php /home/USER/school-platform/artisan schedule:run >> /dev/null 2>&1`);
  find the PHP binary with `which php` from the terminal.
- Queue worker: without two-way SSH, run `php artisan schedule:run` every
  minute and let `Schedule` + `withoutOverlapping` keep long-lived tasks alive,
  or prefer a VPS for the durable `queue:work` worker.

## 10. Phase 2 rollout checklist

- [ ] Migrations applied (3 commercial migrations + `backup_runs`)
- [ ] `subscription:ensure-plans` scheduled or run once so the three default
      plans (Free Trial / Pay Per Student / Monthly Full Access) exist
- [ ] `storage/app/backups` writable; first manual backup run + ZIP download
      verified
- [ ] Platform Settings → Subscriptions & Trials: `trial_duration_days` set
- [ ] Platform Settings → Billing & Trials: service-charge / gateway-fee rates
      set (or left blank to use the `.env` defaults)
- [ ] Platform Settings → Payments: platform Paystack/Flutterwave public keys,
      secret keys and webhook secrets stored (these power Pishon-mode school
      payments and Pishon subscription billing; blank fields fall back to `.env`)
- [ ] Paystack platform secret/webhook secret set; a school self-service payment
      smoke-tested end-to-end (pending invoice → gateway → callback → ACTIVE)
- [ ] Health dashboard shows all critical tables; failed-jobs page reachable