> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payments & Refunds Quickstart

> Create local payment and refund records, verify replay identity, and test refundable-balance guards with the embedded Node.js engine.

This walkthrough uses **`@stateset/embedded` 1.35.1** to create a 25.00 USD payment record and a
12.50 USD refund record. It verifies retry behavior and two rejected refund requests in a fresh
in-memory database.

<Note>
  These calls update local commerce records. They do not charge a card, contact a payment
  provider, or transfer a refund. The completion step below simulates a successful provider
  result so you can explore the lifecycle without credentials or money movement.
</Note>

## Prerequisites

Use Node.js **20.20.0+** and npm **10+**. In a new directory:

```bash theme={null}
mkdir stateset-payments-example
cd stateset-payments-example
npm init -y
npm install @stateset/embedded@1.35.1
```

See [SDK installation](/guides/sdk-installation) if the native module cannot load.

## Run the complete example

Save this as `payments-demo.mjs`:

```javascript theme={null}
import assert from 'node:assert/strict';
import { Commerce } from '@stateset/embedded';

const commerce = new Commerce(':memory:');
const customer = await commerce.customers.create({
  email: 'ada@example.com', firstName: 'Ada', lastName: 'Lovelace',
});
const order = await commerce.orders.createExact({
  customerId: customer.id,
  currency: 'USD',
  items: [{ sku: 'MUG-001', name: 'Ceramic mug', quantity: 2, unitPrice: '12.50' }],
});

const paymentInput = {
  orderId: order.id,
  customerId: customer.id,
  amount: order.totalAmountExact,
  currency: 'USD',
  paymentMethod: 'card',
  idempotencyKey: `payment-${order.id}-1`,
};
const payment = await commerce.payments.createExact(paymentInput);
assert.equal(payment.status, 'pending');
assert.equal(payment.amountExact, '25.00');
const paymentReplay = await commerce.payments.createExact(paymentInput);
assert.equal(paymentReplay.id, payment.id);
assert.equal(await commerce.payments.count(), 1);
console.log(`Payment: ${payment.status}, ${payment.amountExact} ${payment.currency}`);
console.log('Payment replay returned the same ID; payment count is 1');

await assert.rejects(
  () => commerce.payments.createRefundExact({
    paymentId: payment.id, amount: '12.50', idempotencyKey: `early-${payment.id}`,
  }),
  { code: 'VALIDATION', message: /Cannot refund a payment in status 'pending'/ },
);
console.log('Refund before payment completion rejected');

// Local simulation: in an integration, first verify the provider's successful result.
const completed = await commerce.payments.markCompleted(payment.id);
assert.equal(completed.status, 'completed');
console.log(`Payment after simulated success: ${completed.status}`);

const refundInput = {
  paymentId: payment.id,
  amount: '12.50',
  reason: 'One mug returned',
  idempotencyKey: `refund-${payment.id}-1`,
};
const refund = await commerce.payments.createRefundExact(refundInput);
assert.equal(refund.status, 'pending');
assert.equal(refund.amountExact, '12.50');
const refundReplay = await commerce.payments.createRefundExact(refundInput);
assert.equal(refundReplay.id, refund.id);
console.log(`Refund: ${refund.status}, ${refund.amountExact}`);
console.log('Refund replay returned the same ID');

await assert.rejects(
  () => commerce.payments.createRefundExact({
    ...refundInput, amount: '20.00', idempotencyKey: `too-large-${payment.id}`,
  }),
  { code: 'VALIDATION', message: /exceeds the refundable balance/ },
);
console.log('Additional 20.00 refund rejected; pending refund uses the balance');

const storedPayment = await commerce.payments.get(payment.id);
const storedOrder = await commerce.orders.get(order.id);
assert.equal(storedPayment.status, 'completed');
assert.equal(storedOrder.paymentStatus, 'pending');
console.log(`Order payment status: ${storedOrder.paymentStatus}`);
```

```bash theme={null}
node payments-demo.mjs
```

**Success:** all assertions pass and you see:

```text theme={null}
Payment: pending, 25.00 USD
Payment replay returned the same ID; payment count is 1
Refund before payment completion rejected
Payment after simulated success: completed
Refund: pending, 12.50
Refund replay returned the same ID
Additional 20.00 refund rejected; pending refund uses the balance
Order payment status: pending
```

## Read the results correctly

| Observation                                       | What it establishes                                                                            |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Payment replay returns the same ID                | Repeating this request with its original key reuses the local payment record                   |
| Refund against the pending payment fails          | A newly created payment is not yet refundable                                                  |
| `markCompleted` returns `completed`               | The local payment's state changed; no provider call occurred                                   |
| Refund replay returns the same ID                 | Repeating this refund request with its original key reuses the local refund record             |
| Another 20.00 USD refund fails                    | The pending 12.50 USD refund already counts against the 25.00 USD payment's refundable balance |
| Linked order still has `paymentStatus: 'pending'` | Completing the payment does not automatically update that order field in this example          |

Use decimal strings with `createExact` and `createRefundExact`, and read `amountExact`.
The binding also exposes numeric money fields; do not convert exact values to JavaScript
floating-point numbers for financial calculations.

## Integrate with a provider

Keep the local payment ID, provider transaction ID, and your operation's stable retry key in
your integration's persisted records. A local `idempotencyKey` does not configure the provider's
own retry behavior. Use that provider's contract when sending or reconciling a charge or refund.

If a provider request times out, its result is unknown. Reconcile the original operation before
creating another payment or refund. For local retries, preserve both the original request and
its key; generating a new key creates a different operation identity.

Only mark a local payment completed after verifying the provider result in a real integration.
Update and reconcile order, payment, refund, and inventory state explicitly rather than treating
one successful method call as completion of the whole workflow.

<Warning>
  The published Node 1.35.1 `Payments` class exposes refund creation but no refund-completion
  method. This walkthrough ends with a **pending** refund record. A production refund flow
  needs a supported interface for recording the final outcome as well as the provider call;
  do not treat this example as an end-to-end refund integration.
</Warning>

## Troubleshooting

| Symptom                                  | Check or next step                                                       |
| ---------------------------------------- | ------------------------------------------------------------------------ |
| Refund rejected while payment is pending | Verify the provider outcome before recording payment completion          |
| Refund exceeds the balance               | Include pending refunds when evaluating the remaining amount             |
| Multiple records after a retry           | Check that the original key and payload were reused in the same database |
| Order still says payment pending         | Payment and order records need explicit reconciliation                   |
| No provider transaction exists           | Expected here: these are local engine calls                              |
| Records disappear after exit             | `:memory:` is temporary; use a persistent database for integration work  |

## Next steps

* [Orders quickstart](/guides/orders-quickstart): verify reservation and cancellation behavior.
* [Returns quickstart](/guides/returns-quickstart): create and receive a return using an actual order item ID.
* [Durable workflows](/guides/temporal-first-durable-workflow): explore orchestration across service boundaries.
* [Finance and accounting](/stateset-icommerce/stateset-icommerce-finance): find the broader accounting domains.
