Refund

Return a completed spend, reversing the posting that recorded it.

Source src/operations/refund.ts#L54 · refundsrc/operations/registry.ts#L45 · refund

Refund

refund reverses a completed spend. It returns the buyer the full price they paid, then unwinds the sale account by account.

You name the order, not the buyer or the amount — the handler reads those off the recorded sale. Pass the orderId from the original spend. A reason for the audit trail is optional:

const outcome = await economy.submit({
  kind: "refund",
  idempotencyKey: "idem_refund_1",
  actor: { kind: "system", service: "support" },
  orderId: "ord_1",
  reason: "changed mind",
});
// → outcome.status === "committed"

The buyer gets the full price back. Each seller is debited only up to the balance they still hold. Any uncollectable remainder is booked as a debt to the platform.

Parameters

The payload is one refund variant of Operation, tagged by kind. The fields:

FieldTypeDefaultDescription
kind"refund"Selects this operation.
idempotencyKeystringMakes a retried request run at most once. A repeat with the same key returns the earlier result.
actorPrincipalWho is asking. Must be system or operator — see Authorization.
orderIdstringThe order to reverse, from the original spend. A blank or whitespace-only value throws.
reasonstring— (omitted)Optional note recorded in the reversing transaction's metadata.

Returns

refund returns an Outcome:

  • committed — the reversal posted; transaction is the reversing posting.
  • duplicate — the order was already reversed (by an earlier refund or a clawback); transaction is the recorded reversal.
  • rejected — no sale was found for the order. The only RejectionCode here is UNKNOWN_ORDER.

Postings

The reversal rebuilds the sale's double-entry posting in mirror image. It applies the opposite of each change the sale made.

To do that, it splits the original sale's legs into three groups:

  • Raise in full — the buyer's spendable and promo accounts the sale debited, plus any platform account the sale drew down. Raising an account never pushes a balance below zero, so these apply with no cap.
  • Claw back, capped — each seller's earned balance the sale credited, and REVENUE if it took a fee. The clawback is limited to what that account still holds. If a seller already spent or paid out their cut, only the part still there comes back.
  • Receivable — the total that couldn't be clawed back is credited to SYSTEM.RECEIVABLE, the platform's IOU, so debits and credits still cancel.

The shortfall is always denominated in CREDIT. A sale only moves CREDIT, so the receivable that stands in for an uncollectable part matches.

Authorization

refund is platform-initiated only. An end user (actor.kind === "user") may never run it: a self-serve refund debits a seller's earned balance, which is a fraud vector. Only a system service or a human operator may call it — see actors and authorization.

A user actor is declined with an UNAUTHORIZED fault before any work begins.

Reason codes

refund returns one RejectionCode:

CodeWhen
UNKNOWN_ORDERNo sale was recorded for orderId.

A blank or whitespace-only orderId is not a rejection — it throws a MALFORMED_OPERATION fault. That way a malformed request surfaces as a client error instead of degrading to a silent UNKNOWN_ORDER.

Preconditions and invariants

A refund and an order-tied clawback both reverse the same sale, so only one may run. Before posting, refund claims a second, order-scoped key, reversed:<orderId>. That claim makes the two paths mutually exclusive per order. Whoever claims first reverses; the other path returns the recorded transaction as duplicate.

The order-scoped claim is recorded in the same database transaction as the reversal. If the refund rolls back, the claim rolls back too, so the order can still be reversed later.

After the reversal commits, refund revokes the buyer's entitlement to the SKU — for a gift, the recipient's. That revocation runs in the same database transaction, and it is ownership a UI checks through read.entitled. It is a no-op if that user was never granted the SKU; for a sale predating ownership-at-purchase, nothing is revoked.

The reversing posting balances to zero across each currency, exactly as the sale did, so conservation holds.

See also