# Production readiness report

Written honestly. This distinguishes what is **built and tested**, what is
**built but unverified against the real world**, and what is **not built**.
Read the "Before you take real money" section before launching.

---

## 1. Automated test results

Run with `php tests/run.php`, `php tests/e2e.php` and `php tests/cron.php`.
These execute the real service classes against an in-memory SQLite database
built from the production MySQL schema. Results as of the last run:

| Suite | Assertions | Passed | Failed |
|---|---|---|---|
| `tests/run.php` — unit and integration | 114 | 114 | 0 |
| `tests/e2e.php` — §49 journey + §50 negatives | 39 | 39 | 0 |
| `tests/admin.php` — back office and RBAC | 50 | 50 | 0 |
| `tests/cron.php` — scheduled tasks | 4 | 4 | 0 |
| **Total** | **207** | **207** | **0** |

### What the tests actually prove

- **Eligibility** — an unknown state fails closed to manual review; a state with
  no active rule falls back to manual review; a state switched off is reported
  as disabled and instantly unsellable; a court-pending challan is forced to
  `PHYSICAL_COURT_PRESENCE_REQUIRED` even when the rule would permit a sale; the
  rule id and version are recorded on every decision.
- **Pricing** — an unverified discount is priced at zero; a discount cannot make
  the government amount negative; a coupon can never reduce the government
  pass-through; coupon allocation across items sums exactly, including a
  one-paisa coupon; changing the service fee does not reprice an existing order.
- **Payments** — a forged checkout signature is rejected and leaves the order
  unpaid; a replayed webhook is ignored; an unsigned webhook returns 400; a
  webhook claiming the wrong amount does not mark the order paid.
- **Documents** — files are ciphertext on disk; a PHP file renamed to `.png` is
  rejected on magic bytes; a PNG with PHP appended is rejected; the owner and
  the assigned advocate can read, nobody else can, and every access including
  every denial is logged.
- **Isolation** — a stranger cannot load another account's order, vehicle or
  document; an unassigned advocate cannot open a case or its documents.
- **Secrets** — credentials are encrypted at rest, never shown in full, and the
  audit log redacts passwords and one-time codes.
- **Role separation** — a support account cannot reach provider credentials,
  settings or staff management; a content account cannot see orders or open
  customer documents. Tested per permission, not assumed from the seeder.
- **Support privacy** — an internal staff note never appears in the
  customer-visible thread.
- **CMS safety** — script tags, inline event handlers and `javascript:` URLs are
  stripped from staff-authored HTML before it reaches the database; draft and
  future-dated posts stay out of public listings.
- **Credentials** — an advocate's bar council certificate is encrypted at rest,
  readable only by that advocate or a staff member holding `lawyers.manage`,
  replaced rather than duplicated on re-upload, and audited on every access.
- **Analytics** — the page view table holds no raw IP, only a per-day salted
  hash; re-running the daily rollup updates rather than double-counting.

### Four real defects the tests found, now fixed

0. **The CMS rendered the wrong column.** Page and blog templates read `body`
   while the schema stores `body_html`, so every page and post would have
   rendered empty in production. Found while building the editing screens.
1. **The court-presence override was incomplete.** Challans already before a
   court were blocked from `ONLINE_CLOSURE_ALLOWED` but not from
   `LAWYER_ASSISTED_ALLOWED`, so the single highest-risk category in the
   specification was still sellable through the second path. Now blocked for
   every purchasable action.
2. **State-scoped coupons used `array_intersect`.** A Delhi-only coupon applied
   to a mixed Delhi + Haryana cart. Now every item must be in an allowed state.
3. **The mock challan provider had no failure hook**, so provider-failure
   handling could not be exercised at all.

---

## 2. Built and tested

Core (config, database, crypto, session, CSRF, rate limiting, auth, ACL, audit,
validation, routing, views, settings) · eligibility engine · pricing · coupons ·
OTP · challan fetch and caching · orders · payments and webhooks · documents ·
lawyer routing · notifications · analytics · 43-table schema · seeder · all
customer, lawyer, support and admin controllers and views · installer · cron
runner · hardened `.htaccess` at four levels.

## 3. Back office

Added after the first pass, with the tests above: support ticket queue with
internal notes, expert-request workflow (including converting one into a visible
customer ticket), coupon management with usage history, page and blog editing
with SEO fields, email template editing with variable checking, advocate
credential upload and staff review, analytics with funnel drop-off, and an
outbound notification queue.

Note on report URLs: the admin views and routes disagreed on
`/admin/report/...` versus `/admin/reports/...`. Now aligned on the latter.

## 4. Built but NOT verified against the real world

These are the honest gaps. Each is isolated so it can be fixed in one file.

- **API Sathi request and response shape is an assumption.** I have no access to
  their documentation. The request body, field names and status codes in
  `ApiSathiChallanProvider::buildRequest()` and `mapChallan()` are a documented
  guess, overridable at runtime through `provider.challan.field_map` in Admin.
  **Expect to correct this on first contact with the live API.**
- **2Factor, MSG91 and Razorpay adapters** are written to each vendor's published
  API but have never been run against a live account. Signature verification for
  Razorpay follows their documented HMAC-SHA256 scheme and is tested against a
  mock using the identical scheme — which proves the logic, not the integration.
- **Eligibility rules are seeded conservatively, not legally researched.** Delhi
  and Haryana default to lawyer-assisted; drunk and dangerous driving are marked
  court-presence; Uttar Pradesh is visible but not sellable. These are my
  defaults, not legal advice. **Have a lawyer confirm each rule before enabling
  a state.**
- **Privacy policy, terms and refund policy are placeholders.** They must be
  replaced with text reviewed by an Indian lawyer, and the refund policy in
  particular needs to match what you will actually do when a challan cannot be
  closed after payment.
- **No load testing.** Performance under concurrency is unmeasured.
- **Email deliverability** (SPF, DKIM, DMARC on the sending domain) is
  infrastructure work outside the application.

## 5. Not built

- Refund processing through the gateway. Refunds are recorded as a status only;
  the actual money movement is manual through the Razorpay dashboard.
- Staff account creation through the interface. The installer creates the first
  administrator; further staff accounts need a row in `support_users` with a
  bcrypt hash. The permission model behind them is complete and tested.
- Lawyer payouts. Costs are recorded per item; settlement is out of band.
- SMS other than OTP.
- Multi-language. Every string is English and hard-coded in the templates.
- A WhatsApp integration beyond a `wa.me` link.
- Automated backups. See `docs/BACKUP_RESTORE.md` for the manual procedure.

## 6. Before you take real money

1. Every provider ships set to `mock`. Nothing real happens until you enter live
   credentials in Admin → Providers.
2. Correct the API Sathi field mapping against their real responses.
3. Have a lawyer review every eligibility rule, then enable states one at a time.
4. Replace the three legal pages.
5. Set `pricing.tax_rate_bp` after confirming your GST position with an
   accountant. It ships at 0.
6. Back up `config.php` offline. It holds the key that decrypts customer
   documents. **If you lose it those documents are unrecoverable by anyone.**
7. Delete `public_html/install/` after installation.
8. Install the cron entries from `docs/CRON.md`.
9. Work through `docs/SECURITY_CHECKLIST.md`.
10. Do one live end-to-end transaction with a real card for a small amount and
    confirm the money reaches your settlement account before announcing launch.
