Hello community,
I’m trying to figure out what would be the best way to partially cancel an order programmatically, while correctly voiding authorised funds from a partially captured transaction. The store is set up to capture funds on fulfilment. Here is an example scenario:
- Customer purchases 2 items
- 1 of the items is fulfilled
- 1 of the items can’t be fulfilled and should be cancelled
There are two different variations of the scenario:
- The partial cancellation occurs on an item with the same line item ID
- The partial cancellation occurs on items with different line item IDs
I’m currently using the following process as a base for both variations:
- Calculate a refund for the cancelled item
- Create a refund with no transaction associated
The following happens in Shopify when this is followed:
Partial cancellation occurs on an item with the same line item ID
- Shopify always selects the already fulfilled item when the refund is created
- The unfulfilled item that should be cancelled stays in the “Unfulfilled” state
- The fulfilled item can no longer be returned
- The payment section still requires capturing the remaining funds
Partial cancellation occurs on items with different line item IDs
- The cancelled item is marked as “Removed” after the $0 refund is submitted
- The payment section still requires capturing the remaining funds
I have tried the following without any success:
- Void the partially captured auth transaction (transactionVoid)-> Shopify returns an error `AUTH_NOT_VOIDABLE`.
- Create a void transaction via the REST API → Shopify refunds the captured funds and doesn’t actually create a void transaction
- Execute the `orderEditBegin`, `orderEditSetQuantity`, `orderEditCommit` → Although this solves the issue of the 1st variation scenario and moves the “Unfulfilled” product as “Removed”, Shopify now request that a refund needs to be issued to the customer. It always tries to refund the already captured portion and doesn’t allow you to void the remaining auth funds
- Cancel the fulfilment order → It simply creates a new one. Nothing really happens
At this point, I’m a bit clueless about what else I can try out, and I hope that someone that had faced this issue in the past can shed some light on what the best practice is for handling it.
TL;DR: there are two things that I want to achieve when a partial cancellation occurs on an order:
- Mark any cancelled “Unfulfilled” items as “Removed” via the Shopify API
- Void the remaining partially captured authorised funds via the Shopify API
Thanks in advance!
Hi @andres03, I had a look into this and on the funds, you’ve hit expected behavior. Once any amount has been captured against an authorization, that authorization can no longer be voided by any means, full or partial. There’s no partial void in the API, so transactionVoid only works while nothing has been captured, and the REST “void” you tried falls back to a refund for the same reason. This is called out in the post-payment guide (“After an authorization has been partially captured, it can no longer be voided by any means. It must either be captured, or left to expire after the 7 to 30 day authorization period”).
So the remaining authorized funds aren’t something you void but instead they must be released. How depends on whether your gateway supports multiple captures. On a single capture gateway the first capture takes the whole order and the uncaptured remainder is released automatically. I reproduced this on a test order: after capturing $50 of a $100 authorization the order sat at PARTIALLY_PAID with totalCapturableSet and the authorization’s unsettled amount both at 0, nothing left to void or capture. On Shopify Plus with Shopify Payments multi-capture you instead pass finalCapture: true on your orderCapture call, which captures the fulfilled amount and releases the rest in one step. The capture per fulfillment behavior covers the single capture case where payment for the whole order is taken at the first fulfillment.
For the line items, a refund acts on a line item’s refundable (fulfilled, restockable) quantity, so when the cancelled unit shares a line item ID with the fulfilled one it picks the fulfilled unit, blocks the return on it, and leaves the unfulfilled unit stuck. Order editing is the supported way to take an unfulfilled item off the order: orderEditBegin, orderEditSetQuantity to 0 on the unfulfilled item, orderEditCommit moves it to Removed. That works for both the same line item ID and different line item ID cases, and it’s why cancelling the fulfillment order didn’t help (that just returns the items to a new unfulfilled fulfillment order, it doesn’t remove anything).
On a non multi-capture gateway the full order was already captured at the first fulfillment, so the cancelled item’s amount is sitting in captured funds and a refund is the only way back. If you capture only the fulfilled amount first (single capture releases the rest, or finalCapture: true on Plus) and then edit out the unfulfilled item, the captured total already matches what’s left and there’s nothing extra to refund.
Let me know if this helps or if you have follow up questions and I’ll be happy to have another look!
Hi @Donal-Shopify,
thank you for the extensive answer! The shop on which I’m trying this is using `capture per fulfillment` behaviour with Shopify Payments. The capture is executed by Shopify when an item is marked as fulfilled, and I’m currently not calling the orderCapture mutation.
So if I understand correctly, in my case, the remaining auth funds can’t be released on the cancellation (manually or programmatically) and just have to wait for them to expire?
Hey @andres03! Capture per fulfillment is a Shopify Plus feature, and on Plus with Shopify Payments it runs on a multi-capturable authorization, so you should be able to release the remainder yourself.
The automatic capture fires with finalCapture defaulting to false, which is why the auth stays open. Instead of letting it fire, call orderCapture yourself on the kept item’s amount with finalCapture: true. That captures what you’re keeping and voids the cancelled item’s remaining hold in one call. It only overrides the automation for that one order, so no store-wide setting changes. transactionVoid stays blocked once anything is captured (the AUTH_NOT_VOIDABLE you hit), so this is the supported way forward.
The one case where you’re stuck waiting for expiry is if the kept item was already auto-captured and the only thing left uncaptured is the cancelled item’s amount. There’s no positive amount to attach finalCapture to (it can’t be zero) and nothing over-captured to refund, so that leftover hold just releases at expiry. Owning the capture going forward avoids that.
I haven’t been able to test this end to end on a test store just yet, so its certainly worth a dry run or two to confirm it works how you need it to!