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:
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¶
- Create a return record (an RMA) with the order, the lines and quantities, the reason, and the requested outcome: refund, exchange, or store credit.
- 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.
- Approve or reject with a reason. Approval issues return instructions or a label.
- When the parcel arrives, inspect each line and record the condition: resellable, damaged, or missing.
- Refund from the inspection result, not from the request. Use the refund calculation in the notes.
- Restock only resellable units, as a
returnstock movement. Damaged units go to a separate location or are written off. - 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()orPOST /wp-json/wc/v3/orders/{id}/refunds. Setapi_refundtotrueonly 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_paidis 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.