<!--
  SINGLE-SOURCE SYNC (ADR-0021): canonical prose for /docs/testing, mirrored 1:1 by
  resources/js/pages/public/docs/testing.tsx — edit BOTH in the same change (engineering-rules P4).
  Sourced from docs/01-architecture/10-api-design.md §10 and docs/01-architecture/13-brokeret-crm-connector.md §7.
-->

# Testing

Sandbox is a first-class environment per merchant: same API, same Cashier, same webhooks, same state
machines, with `test_gateway` acting as the Provider. Point an `sk_test_` key at it — the prefix
selects sandbox; there is no environment URL or header.

## Magic amounts

The **cent fraction of the amount** steers the outcome:

| Amount ends in   | Behaviour                                                  | What it proves                               |
| ---------------- | ---------------------------------------------------------- | -------------------------------------------- |
| `.01`            | `createPayment` declines → `failed`                        | Your `payment.failed` handling               |
| `.02`            | Stays pending, then succeeds via async webhook             | Webhook-driven crediting (not redirect)      |
| `.03`            | Expires without completion → `expired`                     | Your expiry handling / no stuck rows         |
| `.04`            | Confirmation webhook delivered **twice** (stable event id) | Your dedupe on the `evt_` id                 |
| `.05`            | Succeeds; a later refund of it fails                       | Refunds are Phase 1-deferred (informational) |
| `.06`            | Underpayment — a smaller `received_amount`                 | Your under/over-paid crypto handling         |
| exactly `999.99` | Seeded Risk Rule → `under_review`                          | The Payment sits until an operator approves  |
| any other        | Succeeds promptly                                          | The happy path end-to-end                    |

Run at minimum: a happy path (any amount), `.02` (async webhook crediting), `.04` (dedupe), `.01`
(failure), and `999.99` (review) — and confirm each drives the correct record change on your side.

Notes:

- The `999.99` review trigger is a seeded Risk Rule, not gateway logic — review is a Risk engine
  decision by design. The Payment sits in `under_review` until an operator approves or rejects it in
  the Portal review queue.
- For crypto methods, `test_gateway` issues a fake deposit address and simulates the confirmation
  webhook after ~10 seconds.
- Sandbox outbound webhooks are real deliveries with real signatures against the endpoint's test-mode
  secret.

## Fire a webhook on demand

In sandbox you can force a signed inbound webhook at a Payment (PayXiro-side artisan command):

```
php artisan payxiro:test-gateway:fire-webhook pay_... --type=payment.succeeded
```

## Before you go live

- Sandbox cases above pass: happy path, async webhook, dedupe, failure, review.
- Signature verification rejects a tampered body and a stale timestamp.
- Crediting is idempotent on `payment.id`: a replayed webhook and a status-poll cannot double-credit.
- The webhook route is excluded from CSRF and responds 2xx well within the delivery timeout.
- Live `sk_live_` key and `whsec_` secret are set in your secret store (never committed).
- `success_url` / `cancel_url` are within the Platform's allowed redirect patterns; embed origins (if
  used) are on the allow-list.
