# Pishon — Production Readiness Report (Phase 1 Completion)

Date: 2026-09-14
Scope: `school-platform/` — Laravel 12 multi-tenant school SaaS.
Working tree: 422 files changed vs commit `9753252` (NOT committed).

---

## 1. Overall status

**All hardening passes are complete and the full automated suite is green.**

- Feature suite: **446 tests, 1604 assertions — OK** (the only notices are
  pre-existing PHPUnit deprecations, no failures, no errors).
- New coverage added in this pass: 23 tests across 4 files.
- All consulted areas (SQLite dev DB, caches, migrations, scheduler, commands)
  verified live.

## 2. What was shipped (grouped)

### Configuration, packaging & deployment
- `.env.example` rebuilt as a **production template** (locked to production,
  secure-session, SMTP, queue-on, Paystack/Flutterwave + platform-billing keys,
  security-header toggles — every key cross-checked against its config file).
- `.gitignore` cleaned of dev artifacts; `.gitattributes` export-ignores
  `.env*`, composer.phar, cookie/log dumps, stray HTML, tests, node_modules so
  `git archive` yields a clean release.
- Junk removed from repo root: `composer.phar`, `cookies.txt`, `dash.html`,
  `edit1.html`, `login.html`, `pub.html`, `pub2.html`, `pub3.html`,
  `rc.html`, `rc3.html`, `results.html`, `routes_list.txt`.
- Secret scan of `app/` + `config/` — no credentials found; only demo seeder
  passwords (never run in production).
- `docs/deployment-runbook.md`: production install, cron, queue worker,
  webhooks, rollout order.

### Transport & client security
- Global `SecurityHeaders` middleware (CSP, `X-Content-Type-Options`,
  `Referrer-Policy`, `Permissions-Policy`; HSTS on HTTPS). Config in
  `config/security.php`, **enabled only in production** so local Vite HMR is
  unaffected. Payment/embed hosts allow-listed in CSP.
- `storage/app/public/.htaccess` denies script execution + directory listing
  on all uploads.

### Authentication
- **Super-Admin 2FA (TOTP, RFC 6238)** — encrypted secret + hashed recovery
  codes at rest; enforce-on-login via challenge middleware; enable/verify/
  disable/regenerate under `My Profile → Security`.
- **Login throttling** — 5 attempts / minute / email+IP per guard (platform,
  school), success clears the counter.
- **Password policy** — single source (`PasswordPolicy`), min 8 + letters +
  numbers, applied to platform registration, both reset flows, and every
  change-password surface; sessions regenerated after password changes.
- Route-level throttling on registration, forgot-password, reset-password,
  onboarding steps, and reference/slug lookups.

### Tenancy & authorization defects
- `User.php` missing `HasOne` import fixed (tenant ownership binding).
- `CommunicationController` — message read / notification-mark now scope-bound
  to the signed-in user.
- `ActivityLoggable` resolves the audit actor across `school`/`platform`/
  `web` guards.
- `EmailSettingsController` retry path used an undefined `$log`.

### Upload security
- `FileUploadService` content-based rejection of dangerous MIME types
  (PHP/shell/executables/SVG) before persistence.
- SVG upload sinks closed on website logo + result-sheet emblem; generated
  favicon SVGs (internally safe) kept.

### Plan enforcement & lifecycle
- `PlanLimitService` blocks creating students, teachers, classes, and
  **admission approvals** beyond the active plan; schools with no plan or a
  0/null bound are unlimited; import path enforced per row.
- Trial lifecycle automation: `subscription:check-trials` warns owners one day
  before expiry (emails + `trial_warning_sent_at`) and expires overdue trials
  (school → INACTIVE, subscription → EXPIRED); scheduled 08:00 daily.
- `SubscriptionService`: trial plan guaranteed via `firstOrCreate`
  (plan_id NOT NULL), `payment_amount` always plan price, expired trials
  handled, class capacity counter corrected.

### Payments & webhooks
- **Webhook school config resolves from the transaction's own school** (never
  the first tenant's) — fixes wrong-secret validation; missing import repaired.
- Flutterwave email fallback mirrors Paystack (user → invoice student).
- Payment/resolve paths use school-scope-exempt lookups where required.
- Idempotency confirmed by existing unique `reference` columns (the extra
  provider-reference unique index was trialled then withdrawn because it
  invalidly blocked legitimate re-initialization).

### Route cleanup
- Newsletter subscription is now POST-only (no GET side-effect for crawlers).

## 3. Test delta

| Area | File(s) | Tests |
|---|---|---|
| 2FA enable/verify/block/disable + recovery consumption | `Security/TwoFactorAuthenticationTest.php` | 7 |
| Platform & school login lockout, success-reset, register throttle | `Security/LoginThrottlingTest.php` | 4 |
| Plan limits: students/teachers/classes, no-plan, unlimited | `Platform/PlanLimitEnforcementTest.php` | 6 |
| Trial warn/expire/once-only/not-yet/active-safe | `Platform/TrialLifecycleTest.php` | 5 |

## 4. Bugs found & fixed during the pass

1. `PlatformLoginController` 2FA branch — session marker flow verified end-to-end.
2. Recovery-code hashing mismatched dashed/plain forms → canonical normalize.
3. `TwoFactorController` redirected to a non-existent route
   (`platform.two-factor.setup` → `platform.security.show`).
4. `PaymentWebhookController` referenced `PaymentTransaction` without the
   import — every school webhook was 500ing; the earlier suite run masked it.
5. Idempotency migration over-restricted legitimate re-initialization — removed.

## 5. Deployment checklist (see `docs/deployment-runbook.md`)

- [ ] `composer install --no-dev --optimize-autoloader`
- [ ] `.env` per template; `APP_DEBUG=false`, secure cookies, HSTS/security on
- [ ] `php artisan migrate --force`
- [ ] `config:cache` + `route:cache` + `view:cache` (verified clean)
- [ ] cron: `php artisan schedule:run` every minute
- [ ] queue worker running (all mail is queued)
- [ ] webhook URLs + secrets for Paystack/Flutterwave (school AND platform)
- [ ] super-admin 2FA enabled, payment gateways configured, smoke test a tenant

## 6. Known residuals (intentional, documented)

- Seeder demo passwords (`Hash::make('password')`) left intact — seeders are
  dev-only by design.
- `unsafe-inline` is the **default** in CSP `script-src` for legacy blades,
  but a nonce-based strict mode now exists: set
  `SECURITY_CSP_STRICT_SCRIPTS=true` (+ `SECURITY_CSP_REPORT_ONLY=true` first)
  and the middleware drops `unsafe-inline`, nonces first-party module/Vite
  scripts automatically, and signs layout scripts via `nonce="{{ $cspNonce }}"`.
  Remaining per-component inline scripts (chart widgets etc.) need nonce
  attributes before a full switch; verified by
  `tests/Feature/Security/SecurityHeadersCspTest`.
- Two-factor challenge applies to the **platform** guard; school guards already
  have per-tenant auth and are not in scope for 2FA.
- Route caches cleared after verification so local dev stays flexible; deploy
  should re-run them.

## 7. Verdict

Ready to deploy to production **once** the `.env` production values, cron, and
queue worker are configured per the runbook. The application passes the full
automated suite and the hardening requirements of the 23-section Phase 1 spec.