recordRefund
Record a refund against an order. recordRefund never calls a payment provider: it writes down a refund that has already happened. It is the one place that saves refund transactions and emits the order_refunded event, so every payment method gets the same bookkeeping.
refundOrder calls it after an admin refund. Call it yourself from a webhook to record a refund that was made outside EverShop, such as one issued from the provider's dashboard.
Import
import { recordRefund } from "@evershop/evershop/oms/services";
Syntax
recordRefund(params: RecordRefundParams, conn?: PoolClient): Promise<RecordRefundResult>
interface RecordRefundParams {
order: OrderRow;
transactionId: string;
amount: number;
currency?: string;
offline?: boolean;
raw?: unknown;
}
interface RecordRefundResult {
status: string;
isFullRefund: boolean;
alreadyRecorded: boolean;
}
Parameters
| Parameter | Type | Description |
|---|---|---|
order | OrderRow | The order row. Needs order_id, uuid, payment_method, payment_status and currency. |
transactionId | string | The provider's id for this refund. It is the idempotency key: a refund with an id that is already recorded for the order is skipped. |
amount | number | This refund's amount, in major currency units. |
currency | string (optional) | Defaults to the order's currency. |
offline | boolean (optional) | Records the transaction as offline instead of online (Cash On Delivery). |
raw | unknown (optional) | The provider's response. Stored as JSON on the transaction for audit. |
conn (optional) — a database connection. Pass one to run inside a transaction you already hold, such as a webhook. recordRefund then neither commits nor rolls back: that is up to the caller. Without it, recordRefund opens and commits its own transaction.
Return Value
Returns Promise<RecordRefundResult>:
status— the new payment status:<payment_method>_refundedor<payment_method>_partial_refunded.isFullRefund—truewhen the refunds recorded so far, including this one, reach the captured amount, compared in the currency's smallest unit.alreadyRecorded—truewhen a transaction with thistransactionIdalready exists. In that case nothing is written or emitted,statusis the order's current payment status andisFullRefundisfalse.
Behavior
- Reads the order's payment transactions.
- If one already has this
transactionId, returns withalreadyRecorded: true. - Finds the captured transaction (the newest one that is not a refund) and adds this amount to the refunds already recorded.
- Saves the refund as a
payment_transactionwithpayment_action: 'refund'and the capture as itsparent_transaction_id. - Sets the payment status and re-derives the order status.
- Adds the activity
Refunded <amount> <currency>. Refund ID: <id>. - Emits
order_refundedon the same connection, so subscribers see it only after the transaction commits.
Errors
| Message | Cause |
|---|---|
Order <uuid> has no payment method to refund | order.payment_method is empty. |
Cannot find the captured transaction to refund for order <uuid> | The order has no non-refund payment_transaction row. |
Invalid status | The method did not register <method>_refunded or <method>_partial_refunded. |
Notes
recordRefundtrusts its input. It does not check theisRefundableflag or that the amount fits what remains captured: a refund the provider already made has to be recorded.- Two simultaneous calls with the same
transactionIdare protected only by the database's unique constraint on the order and transaction id. In a webhook, lock the order row first (SELECT ... FOR UPDATE), as the Stripe module does. - It is not wrapped in
hookable. - The event payload is
{ orderId, amount, currency, isFullRefund, transactionId, paymentMethod }. It also triggers the refund email to the customer, which can be switched off withsystem.notification_emails.order_refunded.enabled.
Examples
Record a refund that was made from the Stripe dashboard, inside the webhook's transaction:
import { recordRefund } from "@evershop/evershop/oms/services";
await recordRefund(
{
order,
transactionId: refund.id,
amount: parseFloat(display(refund.amount, charge.currency)),
raw: charge
},
connection
);
See Also
- refundOrder — Refund through the payment method's handler
- addPaymentTransaction — Record any other payment transaction
- Events and Subscribers — React to
order_refunded