# Payin test cases

> The payments to run before you go live with Ozow, what each one delivers, and a handler that survives all of them.

Source: https://hub.ozow.com/integration-methods/testing/payin-test-cases-one-api/

These test cases are recommended but not mandatory for go-live. Run at least the core ones before
you accept real payments. The optional ones apply only if you have built the feature they test.

Building on the Payments API instead? Use [Payin test cases: Payments
API](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/payin-test-cases-payments-api.md); the statuses and the field
names are different.

> ℹ️ **Testing environment**: Payin integrations can be tested directly in production. Any test
> transactions settle into your configured bank account. To test without moving real money, staging
> credentials are available on request: contact your account manager or
> [support@ozow.com](mailto:support@ozow.com).

## What arrives, and what it says

A One API payment tells you its outcome on a
[`transaction.complete`](https://hub.ozow.com/api-reference/one-api/webhooks/transaction-complete.md) webhook, and also on the
standard Ozow notification when you set `notifyUrl` on the payment request. Configure both and both
fire, which is what Test 5 exists for.

The webhook carries [one envelope](https://hub.ozow.com/api-reference/one-api/schemas/webhook-envelope.md) whatever happened, and
`data.status` is one of four values:

| `data.status` | The transaction was |
|---|---|
| `Successful` | completed |
| `Incomplete` | created and not taken further |
| `Pending` | pending, or under investigation |
| `Error` | anything else, including cancelled, abandoned and voided |

**There is no `Cancelled` to test for.** A cancellation arrives as `Error`, and `data.reason` says
which kind of failure it was. A handler that switches on `Cancelled` never runs that branch.

## A handler that passes every test below

The tests are all the same handler seen from different angles: verify, then read the status, then
update the order once. This is that handler, written against `svix` 2.x.

```javascript
import { Webhook } from "svix";

// From Get Webhook Secret. Keep it out of source control.
const webhook = new Webhook(process.env.OZOW_WEBHOOK_SECRET);

app.post(
  "/ozow/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      // Verify against the raw body. Parsing first and re-serialising changes
      // the bytes and the signature will not match. Test 4.
      webhook.verify(req.body, {
        "svix-id": req.headers["svix-id"],
        "svix-timestamp": req.headers["svix-timestamp"],
        "svix-signature": req.headers["svix-signature"],
      });
    } catch {
      // Rejected, logged, and never acted on.
      return res.status(400).send("invalid signature");
    }

    // `verify` returns nothing on svix 2.x: it throws on a bad signature and
    // that is the whole result. Parse after it, never before. On svix 1.x it
    // returned the parsed body, so a handler carried over from that version
    // reads `undefined` here and fails after it has already acknowledged.
    const event = JSON.parse(req.body);

    // Acknowledge first. Ozow retries anything that is not a 2xx, and a slow
    // database is not a reason to be sent the same event again.
    res.sendStatus(200);

    const { type, data } = event;
    if (type !== "transaction.complete") return;

    // The same event can arrive more than once, and a payment can also notify
    // twice when `notifyUrl` is set alongside the webhook. Test 5.
    if (!claimOnce(data.id)) return;

    switch (data.status) {
      case "Successful":
        fulfilOrder(data.id);
        break;
      case "Pending":
        // Not an outcome. Leave the order alone and wait for the next event.
        break;
      case "Incomplete":
      case "Error":
        // Everything that is not a payment: cancelled, abandoned, voided, failed.
        failOrder(data.id, data.reason);
        break;
      default:
        // A value this code has never seen. Do not guess what it means.
        alertOps(`unknown status ${data.status} on ${data.id}`);
    }
  },
);
```

`claimOnce` is whatever makes the update happen once in your system: a unique constraint on the
transaction id, a row lock, or a conditional write. Deciding by "have I seen this id" in memory does
not survive two instances.

> ⚠️ **Important**: Update the order from this handler, never from the browser returning to your
> success URL. A customer who closes the tab still has to end up with the right order state.

---

## Core test cases

### Test 1: Successful payment

Verify that your integration handles a completed payment end to end.

**Steps**

1. Create a payment with [`POST /payments`](https://hub.ozow.com/api-reference/one-api/post-payments.md)
2. Complete the payment on the Ozow payment page with a valid payment method
3. Verify that your endpoint receives the webhook
4. Verify that the Svix signature validates
5. Verify that the order is updated
6. Verify that the customer reaches your success URL

**Expected outcomes**

- `type` is `transaction.complete` and `data.status` is `Successful`
- Order credited and fulfilled once
- Customer redirected to the success URL

---

### Test 2: Cancelled payment

Verify that a cancellation does not credit the order.

**Steps**

1. Create a payment
2. Cancel it on the Ozow payment page
3. Verify that your endpoint receives the webhook
4. Verify that the order is not credited
5. Verify that the customer reaches your cancel URL

**Expected outcomes**

- `data.status` is **`Error`**, not `Cancelled`, with the detail in `data.reason`
- Order not credited
- Customer redirected to the cancel URL

---

### Test 3: Failed payment

Verify that a failure does not credit the order.

**Steps**

1. Create a payment
2. Attempt a payment that fails
3. Verify that your endpoint receives the webhook
4. Verify that the order is not credited
5. Verify that the customer reaches your error URL

**Expected outcomes**

- `data.status` is `Error`, with the reason in `data.reason`
- Order not credited
- Customer redirected to the error URL

---

### Test 4: Signature verification

Verify that verification actually rejects something.

**Steps**

1. Complete a successful test payment and keep the delivery
2. Verify it with your implementation and confirm it passes
3. Change one byte of the body, or one character of `svix-signature`, and replay it
4. Confirm the tampered delivery fails verification and is not processed

**Expected outcomes**

- The genuine delivery passes
- The tampered delivery is rejected, logged, and updates nothing
- Verification runs against the raw body, not a re-serialised object

---

### Test 5: The same outcome twice

Verify that one payment updates one order once.

> ℹ️ **Note**: Ozow retries a delivery your endpoint did not acknowledge, and a payment with both a
> webhook and `notifyUrl` reports twice by design. Both look like a duplicate to your handler.

**Steps**

1. Complete a successful test payment
2. Deliver the same event to your endpoint a second time
3. If you have set `notifyUrl` as well, confirm what that notification does to the same order

**Expected outcomes**

- The second delivery is recognised and changes nothing
- The order is credited once
- Nothing throws

---

### Test 6: Transaction status check

Verify that you can ask, rather than wait.

**Steps**

1. Complete a test payment and note the payment id
2. Call [`GET /payments/{id}/transactions`](https://hub.ozow.com/api-reference/one-api/get-payments-id-transactions.md)
3. Compare the status with the outcome you were sent

**Expected outcomes**

- The call returns the transaction
- Its status matches the payment's outcome

> ℹ️ **Note**: This endpoint returns the transaction's own status, which is the full set on the
> [statuses page](https://hub.ozow.com/integration-methods/statuses.md), not the four the webhook maps to. A payment the webhook reports
> as `Complete` reads `Successful` here. Compare without case.

---

## Optional test cases

Run these only if you have built the feature.

### Test 7: Standalone button

Verify that a button opens the payment method it names.

**Steps**

1. Build the standalone button with the correct `institutionId`
2. Create a payment with that field set
3. Verify that the Ozow payment page opens on that payment method

**Expected outcomes**

- The named payment method is shown
- The customer is routed to it without choosing again

---

### Test 8: Customer Identity Verification

Run this only if your business is in a high-risk industry and Customer Identity Verification is
required.

**Steps**

1. Create a payment with a verified customer identity in `payer.identity`
2. Verify that the page offers only payment methods linked to that identity
3. Verify that unlinked payment methods are hidden
4. Create a payment with no identity and verify which methods are offered

**Expected outcomes**

- Only linked payment methods are offered when an identity is passed
- The behaviour without an identity is what you expect for your account