# Testing report

Date of execution: 2026-11-04 · PHP 8.3.6 · Laravel 11 · **SQLite in-memory** · HTTP to providers/sites **faked** (`Http::fake`) · WordPress/WooCommerce **stubbed**.

## Commands that were actually run
| Command | Result |
|---|---|
| `php artisan test` (SaaS) | **52 passed, 0 failed (335 assertions)** |
| `php plugin/tests/run.php` (plugin logic against WP/WC stubs; copy in `docs/plugin-harness/`) | **41 passed, 0 failed** |
| `php -l` on every plugin file | no syntax errors |
| Fresh extract of `voiceorder-saas.zip` → `key:generate`, `migrate --force`, `db:seed`, `config:cache`, `route:cache`, `view:cache`, `schedule:list`, `voiceorder:process-queue`, `php -S` + curl | all succeeded; `/`, `/login`, `/register`, `/up` = 200; `/api/v1/wallet` = 401; `/.env`, `/composer.json` = 404; bad-signature webhook = 401; API registration = 201 |
| Browser run (Playwright/Chromium) over landing, client dashboard, admin dashboard, provider editor, mobile menu | no JS errors; two layout defects found and fixed (provider-form placeholder text, copy-button width) |

## Acceptance matrix (spec §23)
✔ = executed and passing · ◐ = partially verified (see note) · ✘ = **not executed**

| # | Item | Status | Evidence / note |
|---|---|---|---|
| 1 | New client registration | ✔ | `CreditsAndWalletTest::registration_via_api…`, web register not browser-submitted |
| 2 | Email verification | ◐ | signed verification link + `Verified` event tested; **actual email delivery (SMTP) not tested** |
| 3 | Exactly 5 free credits, once | ✔ | verify twice + direct re-award = still 5, one `free_call_credits` row |
| 4 | Free credits before paid | ✔ | `free_credits_consumed_before_wallet…` (calls 1-5 free, 6th wallet) |
| 5 | Paid wallet deduction | ✔ | balance/total_spent/reserved asserted |
| 6 | Insufficient balance | ✔ | 402, no call row, no change |
| 7 | First website auto-approval | ✔ | after ownership proof; HTTP fetch of the site is faked |
| 8 | Second website needs approval | ✔ | 403 `approval_required` → request → approve → connect |
| 9 | Plugin reinstall doesn't reset credits | ◐ | server side proven (reconnect/URL variants reuse row, credits unchanged). **Plugin reinstall itself not run in WordPress** |
| 10 | API regeneration doesn't bypass limits | ✔ | |
| 11 | Domain normalisation | ✔ | www/non-www, http/https, case, port, trailing dot, path; plugin copy checked for parity |
| 12 | WooCommerce order detection | ◐ | hook handlers tested against a **stubbed** `WC_Order`; not on real WooCommerce |
| 13 | Duplicate hook protection | ◐ | 3 hook firings → 1 scheduled send (stub); server-side idempotency tested for real |
| 14 | Call dispatch | ◐ | request fields/headers asserted against a fake; **not the live TZSMM API** |
| 15 | Provider rejection | ✔ | reservation released (free and wallet variants), nothing charged |
| 16 | Provider timeout reconciliation | ✔ | ambiguous → no redial → resolved by callback; also temp-failure retry path |
| 17 | HMAC webhook verification | ✔ | wrong secret → 401 and nothing applied |
| 18 | Replay protection | ✔ | exact replay, stale/forged timestamp, signed-timestamp mode, plugin nonce replay |
| 19 | Digit 1 → confirm | ◐ | SaaS state + signed push tested; plugin sets `confirmed` on stub order. **Real DTMF callbacks untested (schema unknown)** |
| 20 | Digit 2 → cancel | ◐ | same as 19 |
| 21 | No answer | ✔ | retry then final `no_answer`; order untouched; one billable call, provider cost per attempt |
| 22 | COD stays unpaid | ◐ | stub order: no `payment_complete`, no `date_paid`; not on real WooCommerce |
| 23 | Duplicate webhook | ✔ | |
| 24 | Historical calls keep provider | ✔ | |
| 25 | Switching affects future calls only | ✔ | and old call's callback only accepted via its original provider/secret |
| 26 | Provider credential encryption | ✔ | ciphertext in DB, hidden in `toArray`, never rendered in the edit page |
| 27 | Client data isolation | ✔ | web + API cross-tenant 404s |
| 28 | Plugin activation/deactivation | ✘ | not run (no WordPress available in the build sandbox) |
| 29 | HPOS compatibility | ✘ | declared + CRUD-only code; **not run on a HPOS store** |
| 30 | Shared-hosting deployment | ◐ | install/boot verified from the extracted ZIP on PHP 8.3 + SQLite. **Not run on cPanel, not on MySQL/MariaDB** |

Additional automated coverage: role matrix (support/finance/admin/super admin), client-staff restrictions, voice-setting limits, provider create/update/activate/override/safe-delete flows, manual recharge flow (credited once, never via browser), plugin release upload validation + signed download + tamper/unsigned/disabled refusal, suspended vs blocked clients, cancelled Woo order drops queued call & releases credit, signed SaaS→plugin push content, ledger immutability, admin adjustment reason/overdraw rules, IP-based free-credit cap.

## Manual checklist (NOT executed – run on staging before launch)
- [ ] MySQL/MariaDB: `php artisan migrate` on a clean DB; run `php artisan test` against a MySQL test database.
- [ ] cPanel: follow `CPANEL_DEPLOYMENT.md` on a clean account; confirm `/.env`, `/storage/logs/laravel.log`, `/vendor/` are not reachable.
- [ ] Cron runs every minute and a queued call is dispatched.
- [ ] Real SMTP: registration email arrives, link verifies, 5 credits appear.
- [ ] WordPress + WooCommerce (HPOS on and off; classic and block checkout): activate, connect with a pairing code, ownership verification succeeds, place COD and prepaid orders → exactly one call each.
- [ ] Order with invalid phone / cancelled before dispatch / refunded → no call.
- [ ] TZSMM sandbox/live: test connection, Bangla & English test calls, keypad 1/2 arrive at the webhook, `Webhook map` built from real payloads, order → Confirmed/Cancelled; no-answer retry; provider timeout simulated.
- [ ] Plugin deactivate/reactivate, delete & reinstall → no new credits, same website slot; update flow from a published release.
- [ ] Disconnect then connect a different domain → blocked until approved.
- [ ] Concurrency: two simultaneous requests for the last free credit / last wallet unit → only one succeeds (row locking is DB-engine dependent: verify on InnoDB).
- [ ] Load: 1,000 queued calls processed by cron within expected time.
- [ ] External security review / `composer audit`.
