# Test mode

The test twin of an organization is a separate organization with its own data, numbers and keys, served by a SIMULATED provider. Nothing depends on the clock or on the amounts: the same scenario always behaves the same.

## Scenarios of one invoice

Send the header `Test-Scenario: <name>` with `POST /v1/invoices`. Outside a test organization the header is REJECTED ([`test_scenario_not_allowed`](https://api.invox.page/v1/errors/test_scenario_not_allowed)), never ignored. `GET /v1/test/scenarios` (permission `invoices:read`) serves this same list.

| Scenario | What the provider answers | How the invoice ends |
| --- | --- | --- |
| `accepted` | SUNAT accepts the invoice at once. | The invoice becomes `accepted`. |
| `rejected` | SUNAT rejects the content of the invoice (fiscal code 2800). | The invoice becomes `rejected`. Its number was not used before SUNAT. |
| `pending` | The provider receives the invoice and SUNAT does not answer. | The invoice stays `processing` (nothing resolves it by itself, however long you wait) until POST /v1/test/invoices/{id}/resolve decides. |
| `already_exists` | The provider received the invoice but its answer was lost; the retry is told the number already exists. | The service looks the number up, sees the document is its own, and the invoice becomes `accepted`. Exercises recovery without a duplicate. |
| `sunat_1033` | SUNAT answers 1033: the number was registered before with other data. | Its number is NEVER changed automatically. The invoice stays `processing` while SUNAT is asked again (three times, ten minutes apart: skip each wait with POST /v1/test/invoices/{id}/advance); then it ends `failed` with `status_reason: number_already_registered`. That reason says how it is corrected to the next number. |
| `accepted_with_observations` | SUNAT accepts the invoice and flags an observation on it (code 4252). | The invoice becomes `accepted`. The observation travels in its CDR; nothing has to be done for the invoice to be valid. |
| `already_exists_other` | The number of the invoice is already held at the provider by a document that is not this one. | The service looks the number up, sees the document is NOT its own, and the invoice takes the next number in the same row (`attempt` 2) and becomes `accepted`. |
| `timeout_processed` | The answer to the first send is lost, but the provider DID receive the invoice. | The retry is told the number already exists; the service checks the document is its own and the invoice becomes `accepted` with the same number. No duplicate. |
| `timeout_not_received` | The answer to the first send is lost and the provider received nothing. | The invoice stays `processing`; the retry sends the same number again and it becomes `accepted`. |
| `transient_failures` | The provider fails twice (as a 5xx would) and accepts the third send. | The invoice stays `processing` through both failures, never `failed`, and becomes `accepted` on the third send, with the same number. |
| `unrecognized_answer` | The provider answers every send with a code the service does not know (simulated code 9999), and asking it does not settle it. | The invoice stays `processing`: the provider is asked first, the same number is sent again twice (1 min, then 5 min), and then it is HELD: `held` says since when and why, `next_step.action` is `held_for_review`, the event `invoice.held` is written and nothing is sent again. A read-only recheck runs at 1 h and at 24 h (skip the wait with POST /v1/test/invoices/{id}/advance). It never ends `failed` or `rejected` by itself. |

A scenario can take parameters after a semicolon: `unrecognized_answer; then=accepted`: once the invoice has been held, the provider answers in a way the service understands: the recheck (or the first turn after a release) finds the invoice and it becomes `accepted`. `then=unrecognized` (the default): it keeps answering the same way, so a released invoice is held again.

## Conditions of the provider ACCOUNT

A condition is a state of the account, not of one invoice: it lasts until it is lifted and wins over the header. While it lasts, invoices are still taken (`202`) and stay `processing` (never `failed`) and go out by themselves when it lifts. Set it with `PUT /v1/test/provider` (permission `issuers:write`); `condition: "none"` lifts it.

| Condition | What it simulates | Code the account shows |
| --- | --- | --- |
| `account_suspended` | The provider suspended the account: it refuses every operation until the account is active again. | `provider_account_suspended` |
| `no_balance` | The provider blocks the account for lack of payment: it refuses every operation until it is paid. | `provider_account_unpaid` |
| `credential_invalid` | The provider rejects the credential of the account: no operation is even looked at. | `provider_auth_failed` |
| `config_invalid` | The connection to the provider is misconfigured: its calls cannot reach the account. | `provider_config_invalid` |
| `outage` | The provider is down: every call fails without saying anything about the document. | - |

`GET /v1/provider` (permission `issuers:read`) shows the state the service recorded for the account.

## Every test tool

| Route | Needs | Retry | What it does |
| --- | --- | --- | --- |
| `GET /v1/test/provider` | `issuers:read` | read | The condition the simulated provider account is in |
| `PUT /v1/test/provider` | `issuers:write` | idempotent | Put the simulated provider account in a condition, or lift it |
| `POST /v1/test/provider/connect_sandbox` | `issuers:write` | safe as it is | Connect this test organization to the provider's sandbox (staff of the service only) |
| `POST /v1/test/invoices/{id}/advance` | `invoices:emit` | safe as it is | Run now the next scheduled step of a test invoice that is waiting |
| `GET /v1/test/scenarios` | `invoices:read` | read | What the simulated provider can be told to answer |
| `POST /v1/test/invoices/{id}/resolve` | `invoices:emit` | safe as it is | Decide the outcome of a test invoice that waits for one |
| `GET /v1/test/void_scenarios` | `invoices:read` | read | What the simulated provider can be told to answer to a void |
| `POST /v1/test/invoices/{id}/void/resolve` | `invoices:void` | safe as it is | Decide the outcome of a test void that waits for one |
| `POST /v1/test/invoices/{id}/void/advance` | `invoices:void` | safe as it is | Run now the next scheduled step of a test void that is waiting |
| `POST /v1/test/events` | `webhooks:write` | `Idempotency-Key` | Fabricate an event about a test invoice, to exercise a receiver |

## Reproducing an error

Every entry of the error catalog (`GET /v1/errors`) says how to provoke it in test mode in `reproduce`, or why it cannot be reproduced.
