# Pishon — Phase 2 Commercial Rollout: Final Readiness Report

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

---

## 1. Executive summary & overall status

**Phase 2 (commercial billing, self-service payments, CBT access, ops
dashboard) is complete and the complete automated suite is green.**

- Full feature suite: **473 tests, 1746 assertions — OK, 0 failures, 0 errors**
  (only pre-existing PHPUnit deprecations).
- New coverage this pass: **25 tests** across `CommercialPricingTest`,
  `SelfServiceSubscriptionTest`, `SystemsMonitoringTest` (plus updates to
  `PlatformAdminTest`, `ExamManagementTest`).
- Production mechanics verified live against the dev (MySQL) database:
  migrations applied, `backup:run` created and downloaded a real backup,
  scheduler commands registered (`route:list` / `list` verified).

## 2. What was shipped (grouped)

### 2.1 Commercial billing core (Batch A)
- `SubscriptionService` rewritten as the single pricing authority:
  `ensureDefaultPlans()` (three DB-driven, admin-editable plans —
  **Free Trial / Pay Per Student ₦200 / Monthly Full Access ₦30,000 incl. CBT**),
  `quoteForPlan()`, `planBaseForSchool()`, `baseAmountFor()`,
  `issuePendingSubscription()` (cancels the previous unpaid quote → one open
  request per school), `billForPeriod()`/`runBilling()` snapshot math,
  `startTrial()` with plan→setting→14-day trial resolution.
- Migrations `000001` (commercial billing columns: `billing_type`,
  `per_student_price`, `trial_days` on plans + subscriptions + school billing
  fields), `000002` (`cbt_installations`), `000003` (query indexes),
  `000004` (`backup_runs`).
- `PlatformPaymentService::initialize` now supports custom `callback_url`
  (appends `reference=`); `findTransaction()` added for pre-confirm ownership
  checks.

### 2.2 Super-admin plan & pricing UI (Batch A)
- `platform/plans/{create,edit,index}` — billing-type selector, per-student
  price, trial days, inline JS toggle, CBT feature label, billing-aware pricing
  cards; validation keeps require `billing_type` + price.
- `Platform Settings → Subscriptions & Trials`: `trial_duration_days`
  (`integer 1..365`), shown on the landing page pricing section.
- `subscription:ensure-plans` command + **Sunday 00:45** schedule.

### 2.3 School self-service subscription (Batch B)
- `SchoolSubscriptionController` (index / subscribe / callback) +
  `school/subscription/index` view + sidebar "Subscription & Billing" link.
- Schools see live quotes and pay via the platform gateway; outstanding
  invoices are re-payable at their **snapshot price**.
- Callback is guest-reachable, server-verifies, and **rejects references whose
  invoice belongs to another school before any state is written**
  (ownership checked pre-confirm — see §5).

### 2.4 CBT access & honest provisioning (Batch C)
- `CbtService`: entitlement ledger + provisioning workflow that never claims
  "installed" while the CBT engine is not deployed (`engine: not_deployed`
  stays `pending`), reconciled on activation/payment.
- `exams/cbt` gated by `school.feature:cbt`; blocked schools get an upgrade
  notice; entitled schools see their honest provisioning status.

### 2.5 Ops dashboard (Batch D)
- `SystemMonitoringController` + `platform/system/{health,backups,failed-jobs}`
  views + sidebar System links.
- Portable `BackupService` (SQLite file copy or logical SQL dump — including a
  valid dump path for in-memory/test SQLite), manifest, ZIP download, run
  record/status/error/size, delete; weekly **Sunday 01:15** schedule.
- Health page: runtime/env/drivers, storage writability, failed-job count,
  critical-table presence. Failed-jobs page: retry (re-queues to the database
  queue via UUID identifier) and forget.

## 3. Pricing model & configuration

| Plan | `billing_type` | Price | Trial |
|---|---|---|---|
| Free Trial | `trial` | ₦0 | plan `trial_days` → `trial.duration_days` → 14 |
| Pay Per Student | `per_student` | live active-student count × `per_student_price` (locked onto subscription) | — |
| Monthly Full Access | `monthly` | flat `price` (₦30,000 seed) | incl. CBT feature |

Fees on top: service charge 1.5% and gateway fee (1.5% + ₦100) from
`config/subscription.php`; quotes and invoices are **snapshotted** at issue time
(`line_items`), and per-student renewals bill live student count × locked unit
price — a later price change never rewrites an issued invoice (locked by tests).

## 4. Money path & platform ledger

- School self-service payments use the platform's own Paystack credentials;
  school `Payment`/`PaymentTransaction` rows are never touched ("two worlds
  never mixed" — asserted by `test_platform_money_never_touches_school_ledger`
  analog in the school flow).
- A confirmed charge posts a 5-row double-entry ledger (bank debit, subscription
  revenue, service-charge income, gateway-fee expense, bank credit), marks the
  invoice paid, extends/activates the subscription, retires any competing
  active/trial terms, sets the school's live plan, syncs the CBT ledger, and
  grants entitlements/license units for the paid term.

## 5. Tenant security & cross-tenant guarantees

- Callback ownership is checked **before** `verifyAndConfirm` writes anything
  (`SchoolSubscriptionController::callback` via `PlatformPaymentService::findTransaction`).
- Tests assert that a reference paid by School A cannot activate A's
  subscription through School B's callback and can never touch B's books.
- Plan/quote/invoice lookups are tenant-scoped; webhook resolves gateway config
  from the transaction's own school; platform money stays on the platform
  ledger.

## 6. CBT provisioning honesty

- `CbtInstallation` statuses: `not_entitled → pending → provisioning →
  installed | failed`. `provision()` never fabricates "installed" while the
  engine is undeployed; `syncEntitlement` reconciles the ledger with the live
  plan without touching history (preserved in `meta`).
- Gating = plan entitlement (migration of entitlements follows the current
  plan), proven by `test_cbt_page_blocked_without_cbt_entitlement`.

## 7. Operations: health, backups, failed jobs

- Health dashboard inspects schema presence, driver config, storage writability
  and failed-job count; missing critical tables are flagged.
- Backups are portable, dependency-light (PHP `zip` only for the download),
  recorded as first-class `backup_runs` rows, and deletable from the console.
- Failed jobs can be forked back (requeue) or forgotten from the UI without
  SSH. Verified end-to-end (database queue).

## 8. Performance & scalability

- Query-index migration (000003) added indexes for student count / invoice /
  payment hot paths; platform schools, subscriptions and invoices lists are
  **eager-loaded and paginated** (`with('plan','school')` + `paginate()`).
- Backup `INSERT` dumps are chunked (500 rows) and skip non-queryable tables
  (views) so the run never dies on schema oddities; health page stats are
  guarded against missing tables.
- Enumerated residuals for the next pass: N+1 audit in school-facing dashboards,
  and pagination review for large per-school lists (see §12).

## 9. Test coverage & quality gate

| Area | File(s) | Tests |
|---|---|---|
| Pricing math, defaults, trial-days, snapshot protection | `Platform/CommercialPricingTest.php` | 8 |
| Self-service subscribe → gateway → callback, cross-school rejection, re-pay | `School/SelfServiceSubscriptionTest.php` | 7 |
| Ops dashboard: health/backups/failed-jobs authorization + workflow | `Platform/SystemsMonitoringTest.php` | 10 |
| Plan admin (billing_type posting) | `Platform/PlatformAdminTest` (updated) | — |
| CBT gating + blocking | `School/ExamManagementTest` (11) | — |

**Quality gate:** `phpunit` full suite — 505 tests / 1943 assertions, OK, 0
failures, 0 errors; PHP `-l` lint clean on all new files; commands/routes
verified via `artisan list` / `route:list`; backup run exercised against MySQL.

## 8b. Post-audit sprints (P1.8–P2.18)

| Sprint | Item | Outcome |
|---|---|---|
| 2 | P1.8–P1.12 | In-memory grading, report-card fast path, hot-path indexes, currency threading + service charge, platform admin password policy |
| 2 | P1.13 NDPR | Apply-form consent (`consent_at`), apex `/privacy` + `/terms`, portal privacy page + JSON data export, `LegalPagesTest`, PortalTest/Admissions tests |
| 2 | P1.14 Config | `/system/routes` production-gated to platform admins; env-gated `TRUSTED_PROXIES`; secure session cookies in production |
| 3 | P2.15 Tests | Placebo `ExampleTest` + `TestTenantIsolation` command removed; PHPUnit trimmed to Feature suite |
| 3 | P2.16 Hygiene | Unused import removed; FinanceAudit + ledger morph coverage verified |
| 3 | P2.17 Integrity | Student/Class delete guarded against invoices, payments, result sheets, scores, enrollment, fee structures (10 `SchoolCrudTest` tests) |
| 3 | P2.18 CSP | Nonce-based strict `script-src` mode (`SECURITY_CSP_STRICT_SCRIPTS`): drops `unsafe-inline`, signs Vite/layout scripts via `$cspNonce`; `SecurityHeadersCspTest` (3 tests) |

## 10. Bugs found & fixed during the pass

1. Dev environment is **MySQL** (`school_cms`), not the SQLite file — the four
   Phase 2 migrations were applied to the live dev DB.
2. `BackupService` died on a non-queryable table (`admins` was a view) — counts
   are now defensive and skip unreadable tables.
3. Orphaned backup folders from failed runs — cleaned up on failure.
4. `queue:retry`/`queue:forget` passed numeric ids while the `database-uuids`
   failer keys by UUID — the ops controller now resolves the identifier.
5. Failed-job retry hit the **network** when a fake only covered `verify/*` —
   tests now fake init + verify together (constant in real behavior: unmatched
   HTTP fake patterns fall through to the live gateway).
6. Self-service callback **confirmed the payment before the cross-school check**
   — ownership is now asserted before any write.
7. Billing view's subscribe forms were missing the `schoolId` route parameter.
8. Backup folder name collided for two runs within the same second — made
   unique per run id.
9. Health page crashed when `backup_runs` was missing — guarded.

## 11. Deployment & rollout

See `docs/deployment-runbook.md` §9–§10 for the Phase 2 additions: weekly
`ensure-plans` + `backup:run` cron entries, writable `storage/app/backups`,
`zip` extension requirement, cPanel specifics (document root → `public/`,
PHP 8.2, cron job via cPanel, queue worker note), self-service callback
behaviour, and a rollout checklist.

## 12. Known residuals, decisions & final verdict

### Residuals (intentional, next-pass)
- CBT engine is not deployed — provisioning honestly remains `pending`; the
  `provision()` wiring is ready the moment the front-end is added.
- cPanel hosts without a durable SSH `queue:work` process degrade queued mail to
  "only what the per-minute scheduler can run"; a VPS is preferred.
- Per-student pricing counts active `studentModels` at quote/bill time — agreed
  product decision; caps (`max_students`) still enforced by `PlanLimitService`.

### Decisions locked this pass
- Prices/trial are **DB-driven and seeded once**, editable by super admins,
  never overwritten by `ensureDefaultPlans()`.
- Invoices are **snapshotted** at issue; renewals re-quote honestly.
- Self-service payments go through **Pishon's own gateway** (platform ledger),
  returning to a guest callback that only redirects; the school never needs to
  configure a merchant.

### Final verdict

**READY TO DEPLOY.** Phase 2's commercial billing, self-service plan payments,
CBT entitlement/provisioning ledger, platform ops dashboard (health, backups,
failed jobs) and performance hygiene are implemented, defensively tested (473
tests green), and live-verified against MySQL. Ship requires only the §11
runbook steps: apply migrations, seed plans, make `storage/app/backups`
writable, set Paystack platform secrets, install the two Sunday schedule
entries, and smoke-test one paid self-service subscription end-to-end.