Refunds
There are two ways to process refunds:
- Manual refund: This method involves directly refunding the payment through the payment app.
- Refund based on
OrderGrantedRefund: This method triggers the refund request based on the details stored inOrderGrantedRefund.
Manual refund
The manual refund can be triggered by calling transactionRequestAction. If the refund is successful, the transaction.chargedAmount will be reduced. The order's authorizeStatus, chargeStatus, and totalBalance will also be recalculated based on the new values of chargedAmount and refundedAmount. This method is useful when handling overcharged orders.
The below example shows how to trigger the manual refund:
mutation {
transactionRequestAction(
id: "VHJhbnNhY3Rpb25JdGVtOjljY2NkYTYyLTllMjktNGE0OC05NzIyLWRlYzAwOTRmZmY5Yg=="
actionType: REFUND
amount: "10"
) {
transaction {
events {
id
message
pspReference
amount {
amount
}
type
}
}
}
}
The mutation accepts below arguments:
id- ID of the transaction which will be used to trigger the refund action.actionType- The type of action to be performed on the requestedTransactionItem. For a refund action, useREFUND.amount- The amount of the action. The amount is rounded based on the given currency precision. If not provided Saleor will useTransactionItem.chargedAmount.
Grant refunds
A granted refund refers to the process where a customer receives their money back after a refund request has been approved. Once a refund is granted, the customer is reimbursed for the purchase.
When processing a refund based on OrderGrantedRefund, there are two steps involved. Firstly, an order must be granted a refund, which defines what should be refunded. This step requires the MANAGE_ORDERS permission. Secondly, a refund needs to be requested based on the created OrderGrantedRefund. This step requires the HANDLE_PAYMENTS permission.
A granted refund contains all the details related to the refund, such as the list of refunded lines, the amount, and the included shipping costs. This is useful when handling refunds based on the products returned by the customer.
The current status of the OrderGrantedRefund is represented by the status field. The possible statuses are:
NONE: No refund request has been triggered for the granted refund.PENDING: The refund request has been triggered, but the payment app has not provided the final result.SUCCESS: The refund has been successfully processed.FAILURE: The last refund request failed.
The status is calculated based on the latest refund TransactionEvents assigned to the OrderGrantedRefund. The events can be accessed via the OrderGrantedRefund.transactionEvents field. The assigned TransactionItem can be accessed via the OrderGrantedRefund.transaction field.
The OrderGrantedRefund has an impact on the authorizeStatus, chargeStatus, and totalBalance, as it reduces the total value used to calculate the totalBalance.
The maximum amount of OrderGrantedRefund is the order.total.
For example:
- if an order has a total of 100 USD and a single
TransactionItemwith achargedAmountof 100 USD, thetotalBalancewould be 0, aschargedAmount-order.totalresults in 0(1). - Adding a granted refund with an amount of 10 USD would result in a
totalBalanceof 10, aschargedAmount- (order.total-grantedRefund.amount) gives 10. ThechargeStatuswill beOVERCHARGED(2). - If a refund request is made based on the defined granted refund and is successfully processed by the payment app, it will reduce the
chargedAmount. ThetotalBalancewill be 0, aschargedAmount(90USD) - (order.total-grantedRefund.amount) gives 0. ThechargeStatuswill be changed toFULL(3).
| Step Nr | total | totalBalance | authorizeStatus | chargeStatus | tr.chargedAmount | orderGrantedRefund.amount |
|---|---|---|---|---|---|---|
| 1 | 100 | 0 | FULL | FULL | 100 | 0 |
| 2 | 100 | 10 | FULL | OVERCHARGED | 100 | 10 |
| 3 | 100 | 0 | FULL | FULL | 90 | 10 |
The following example shows how to create the granted refund with the assigned TransactionItem for the order.
mutation {
orderGrantRefundCreate(
id: "T3JkZXI6NWZlOTE5NzItYjg3OC00Y2QyLTkyN2UtZTQwZDJjZDRjMmEz"
input: {
transactionId: "VHJhbnNhY3Rpb25JdGVtOmUzOTVjNzdmLWFmNjQtNDRmZC05NmRiLThkZmNkMDYwNmZmOA=="
lines: [
{
id: "T3JkZXJMaW5lOjdlNzg5NzY0LTUyZWMtNDU3Mi05NWNkLTM5ZjQ0OTJmZDk4ZA=="
quantity: 5
reason: "Line reason"
}
{
id: "T3JkZXJMaW5lOjdlNzg5NzY0LTUyZWMtNDU3Mi05NWNkLTM5ZjQ0OTJmZDk4Z1=="
quantity: 5
}
]
grantRefundForShipping: true
amount: 10
reason: "Returned by customer"
}
) {
grantedRefund {
id
}
order {
id
}
errors {
field
code
message
lines {
lineId
field
message
code
}
}
}
}
The mutation accepts below arguments:
id- ID of theOrderto which granted refund should be assigned.input:lines- List of lines related to the planned refund action:id- ID of theOrderLine.quantity- The quantity of the specific lines planned to refund.reason- Reason of the refund related to the specific line.grantRefundForShipping- Determines if the shipping costs will be also included in the refund.amount- Amount of the granted refund. If not provided, the amount will be calculated automatically based on providedlinesandgrantRefundForShipping.transactionId- ID ofTransactionItemthat will be used to process the refund. Ifamountis provided in the input, thetransaction.chargedAmountneeds to be equal to or greater than the providedamount. Ifamountis not provided in the input and calculated automatically by Saleor, themin(calculatedAmount, transaction.chargedAmount)will be used. This field was added in Saleor 3.20, and it will be a mandatory input field starting from Saleor 3.21.
The below example shows how to update the existing granted refund
mutation {
orderGrantRefundUpdate(
id: "T3JkZXJHcmFudGVkUmVmdW5kOjE="
input: {
addLines: [
{
id: "T3JkZXJMaW5lOjgzYmZmZWI3LTVkNjEtNDMzZS1iOGFkLTFhMTE1NWI2ZTgwNg=="
quantity: 3
}
]
removeLines: []
transactionId: "VHJhbnNhY3Rpb25JdGVtOmUzOTVjNzdmLWFmNjQtNDRmZC05NmRiLThkZmNkMDYwNmZmOA=="
amount: 10
grantRefundForShipping: false
reason: "New reason"
}
) {
grantedRefund {
id
}
errors {
addLines {
field
message
code
lineId
}
field
code
}
}
}
The mutation accepts below arguments:
id- ID of theOrderGrantedRefundwhich should be updatedinput:addLines: Lines that should be added toOrderGrantedRefundid- ID of theOrderLine.quantity- The quantity of the specific lines planned to refund.reason- Reason for the planned refund related to the specific line.removeLines- List ofOrderGrantedRefundLine's IDs that should be removed fromOrderGrantedRefund.grantRefundForShipping- Determines if the shipping costs will be also included in the refund.amount- Amount of the granted refund. If not provided, the amount will be calculated automatically based on providedlinesandgrantRefundForShipping.transactionId- ID ofTransactionItemthat will be used to process the refund. Ifamountis provided in the input, thetransaction.chargedAmountneeds to be equal to or greater than the providedamount. Ifamountis not provided in the input and calculated automatically by Saleor, themin(calculatedAmount, transaction.chargedAmount)will be used. This field was added in Saleor 3.20, and it will be a mandatory input field starting from Saleor 3.21.
When OrderGrantedRefund.status is SUCCESS or PENDING, only reason can be updated.
Below example shows how to trigger the refund based on OrderGrantedRefund:
mutation {
transactionRequestRefundForGrantedRefund(
grantedRefundId: "T3JkZXJHcmFudGVkUmVmdW5kOjE="
) {
transaction {
id
}
errors {
field
message
code
}
}
}
To request a refund, you need to provide the grantedRefundId field. The mandatory refund details are stored as OrderGrantedRefund, so the refund request will be created based on this data.
The OrderGrantedRefund.status field will be updated based on the result of the refund action. The TransactionEvents related to the requested action will be accessible via OrderGrantedRefund.transactionEvents field.
Total remaining grant calculations
The amount specified in the OrderGrantedRefund determines the portion of the order.total that should be refunded to the customer.
The order.totalRemainingGrant specifies the amount that still needs to be refunded
to achieve a state where the total charged amount equals the order.total minus the granted refund amount.
The order.totalRemainingGrant amount is calculated based on the following formula:
totalRemainingGrant = totalGrantedRefund - alreadyGrantedRefund
where
-
alreadyGrantedRefundis the difference between already refunded and overcharged amountsalreadyGrantedRefund = max((totalRefunded - overchargedAmount), 0) -
totalGrantedRefundis a sum of allamountfrom allOrderGrantedRefund. The maximum value isorder.total, when the value is above, it's replaced with theorder.total.totalGrantedRefund = max(sum(orderTotalGranted.amount), order.total) -
totalRefundedis a sum ofamountfrom all transactions with refunded or pending refunds amounttotalRefunded = sum(amountRefunded + amountRefundPending) -
overchargedAmountdefines theamountthat has been charged over theorder.totalvalue. It's calculated as a sum of all processed amounts (excluding cancel amounts) minus theorder.total.processedAmount = sum(amountCharged + amountRefunded + amountAuthorized + amountChargePending + amountRefundPending + amountAuthorizePending)
overchargedAmount = processedAmount - order.total
Below is an example order with multiple transactions and totalRemainingGrant at each stage:
-
An order has a total of 100 USD and two
TransactionItems: one with achargedAmountof 100 USD and the second withchargedAmountof 60 USD. ThetotalBalancewould be 60, aschargedAmount-order.totalresults in 60, and we do not have any granted refunds. ThechargeStatusisOVERCHARGED. -
Adding a granted refund of 10 USD increases the
totalBalanceto 70, as it widens the difference between the total charged amount and the new expected order total. Here's the calculation:totalBalance = totalCharged - (order.total - totalGrantedRefunds) = 160 - (100 - 10) = 70The
totalRemainingGrantbecomes 10, as thetotalRefundedAmountis 0. -
The second transaction is refunded for an amount of 50 USD. After this operation, the
totalBalanceis 20, and the order remainsOVERCHARGED. ThetotalChargedAmountdecreases, and thetotalRefundedAmountincreases. ThetotalRemainingGrantremains 10 because theoverchargedAmountstill exceeds the original order.total.overchargedAmount = totalChargedAmount + totalRefundedAmount - order.total = 110 + 50 - 100 = 60
totalRefunded = max(totalGrantedRefund - (totalRefunded - overchargedAmount), 0) = max(10 - max(50 - 60, 0)) = 10 - 0 = 10 -
In the next step the first transaction was refunded for 15 USD. This brings the total below the
order.totalvalue, reducing thetotalRemainingGrant. -
Finally, the last refund of 5 USD is processed, resulting in a
totalBalanceof 0. ThechargeStatusisFULL, and thetotalRemainingGrantis 0.
| Step Nr | total | totalBalance | authorizeStatus | chargeStatus | totalChargedAmount | totalRefundedAmount | orderGrantedRefund.amount | totalRemainingGrant.amount |
|---|---|---|---|---|---|---|---|---|
| 1 | 100 | 60 | FULL | OVERCHARGED | 160 | 0 | 0 | 0 |
| 2 | 100 | 70 | FULL | OVERCHARGED | 160 | 0 | 10 | 10 |
| 3 | 100 | 20 | FULL | OVERCHARGED | 110 | 50 | 10 | 10 |
| 4 | 100 | 5 | FULL | OVERCHARGED | 95 | 65 | 10 | 5 |
| 5 | 100 | 0 | FULL | FULL | 90 | 70 | 10 | 0 |
Note:
totalChargedAmount- is the sum of charged amounts from all transactionstotalRefundedAmount- is the sum of all refunded or pending refunds amount from all transactions
In case of calculating total remaining grant:
- Pending amounts are treated as they were processed, so are included in all processed amounts.
It also means that
totalChargedAmountincludesamountChargePending, andtotalRefundedAmountincludesamountRefundPending. - Authorized amounts, including
amountAuthorizedandamountAuthorizePending, are also considered processed. These amounts will contribute to the overall processed totals and impact theoverchargedAmount.