Skip to content

Returns and refunds

returns-and-refunds

Run returns as a tracked RMA from request to inspection, refund only what the policy and payment allow, and restock only inspected items. Use when building a returns portal, refund rules, exchanges, store credit, or restocking after a return.

Version 0.3.2  ·  MIT  ·  Source  ·  Rule

Install this skill

Copy the skill and its rule into your project:

npx @sadrmanesh/commerce-agent-skills add returns-and-refunds

The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.

A return is a small workflow with its own record, not a status on the order. Keep the return, the refund, and the stock movement separate, and link them. Statuses and the refund calculation are in the RMA notes.

Procedure

  1. Create a return record (an RMA) with the order, the lines and quantities, the reason, and the requested outcome: refund, exchange, or store credit.
  2. Check eligibility on the server: the return window from the delivery date, the product's returnable flag, quantities not already returned, and final-sale items.
  3. Approve or reject with a reason. Approval issues return instructions or a label.
  4. When the parcel arrives, inspect each line and record the condition: resellable, damaged, or missing.
  5. Refund from the inspection result, not from the request. Use the refund calculation in the notes.
  6. Restock only resellable units, as a return stock movement. Damaged units go to a separate location or are written off.
  7. Close the return. The order shows refunded amounts and return history but keeps its original lines.

Refunding

  • Refund through the original payment method, using the gateway's refund API and an idempotency key per refund. Store the provider refund id.
  • The total refunded on an order can never exceed what was captured. Check the sum of earlier refunds first.
  • Refund tax in proportion to the refunded lines. Refund shipping only when the policy says so, such as for a wrong or damaged item.
  • A partial refund leaves the order paid for the rest. Use the platform's refunded status only when everything is refunded.
  • Store credit is a liability. Record it in a ledger with an expiry if the law allows one, not as a negative order.
  • In WooCommerce, create refunds with wc_create_refund() or POST /wp-json/wc/v3/orders/{id}/refunds. Set api_refund to true only when the gateway should return the money; otherwise it is a manual refund record.

Exchanges

An exchange is a return plus a new order for the replacement, linked to the return. Reserve the replacement when the exchange is approved so it does not sell out while the parcel is in transit. Charge or refund the price difference on the new order.

Gotchas

  • Restocking at approval puts units that never come back, or come back broken, on sale.
  • Refunding at request time pays out for parcels that never arrive.
  • A refund that is created only in the payment provider's dashboard leaves the shop showing the order as fully paid. Import provider refunds by webhook.
  • Two staff members refunding the same line at once can exceed the captured amount. Lock the order or check the refund total inside the transaction.
  • Some laws give a minimum withdrawal period for distance sales, such as 14 days in the EU. The store's window cannot be shorter for those customers.

Verification

  • A return outside the window is refused with the reason shown.
  • A line cannot be returned twice beyond its purchased quantity.
  • A damaged unit is refunded per policy but not added to sellable stock.
  • The sum of refunds never exceeds the captured amount, even with two concurrent requests.
  • A partial refund leaves the order paid, with the refunded amount shown.

Evaluation

Worked examples and the checks they must satisfy are in evals/tasks.json.

References

RMA states and refund calculation

States

State Next Notes
requested approved, rejected Created by the shopper or staff
approved in_transit, canceled Instructions or label issued. Reserve exchange items here.
in_transit received Carrier tracking attached
received inspected Parcel arrived
inspected refunded, exchanged, credited, rejected Condition recorded per line
refunded, exchanged, credited, rejected, canceled none Final

Only move forward. Record who changed the state and when.

Refund calculation

For each inspected line:

line_refund = unit_price_paid * accepted_quantity
            - restocking_fee (if the policy has one and the item was not faulty)
tax_refund  = line_tax_paid * accepted_quantity / purchased_quantity
  • unit_price_paid is after discounts. An order-level coupon is spread across lines in proportion to their price, the same way the order calculated it.
  • Add shipping only for faulty or wrong items, or when the whole order is returned and the policy refunds shipping.
  • Round each line in minor units, then add. Cap the total at captured - already_refunded.

Records

Record Holds
Return order id, lines, quantities, reason, state, outcome
Inspection line, condition, notes, photos, inspector
Refund amount, tax, shipping, provider refund id, status
Stock movement return into a sellable or quarantine location

Evaluation

Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.

refund-after-inspection

A shopper returns two of three mugs. One arrives broken. Explain what is refunded, what is restocked, and when.

Must include: inspect, resellable, damaged, restock

Must not include: restock both at approval

Worked example

Approve the return for two mugs and wait for the parcel. Nothing is refunded or restocked at approval.

When it arrives, inspect each unit. The intact mug is resellable: refund its paid price and tax and restock it with a return movement. The broken mug is damaged: if it broke in transit or was faulty, refund it per policy, but move it to a damaged location instead of sellable stock. The third mug stays with the shopper and the order stays paid for it.

woocommerce-partial-refund

Refund one line of a WooCommerce order through the payment gateway and keep the order paid for the rest.

Must include: orders/{id}/refunds, api_refund, partial

Must not include: set the order status to refunded

Worked example

Send POST /wp-json/wc/v3/orders/{id}/refunds with the line_items entry for that line, its refund_total and refund_tax, the summed amount, and api_refund: true so the gateway returns the money.

This is a partial refund. WooCommerce keeps the order processing or completed and shows the refunded amount. Check that earlier refunds plus this one do not exceed the order total before you send it.