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
.
You may have seen we start referencing Page
as a Model
. While in the API we still use Page
name for backwards compatibility, we will be renaming it to Model
in the future.
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
- Variables
mutation TransactionRequestAction($id: ID, $actionType: TransactionActionEnum!, $amount: PositiveDecimal) {
transactionRequestAction(id: $id, actionType: $actionType, amount: $amount) {
transaction {
events {
id
message
pspReference
amount {
amount
}
type
}
}
}
}
{
"id": "VHJhbnNhY3Rpb25JdGVtOjljY2NkYTYyLTllMjktNGE0OC05NzIyLWRlYzAwOTRmZmY5Yg==",
"actionType": "REFUND",
"amount": "10",
"refundReason": "Customer requested a refund",
"refundReasonReference": "UGFnZTox" # ID of a Page/Model representing the reason for the refund
}
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
.refundReason
- Optional plain text reason for the refund. Will be populated to theTransactionEvent.message
field.refundReasonReference
- The ID of aPage
representing the reason for the refund. This is useful for tracking and reporting purposes, like aggregating refunds by specific type. See configuration to learn more.
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 TransactionEvent
s 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
TransactionItem
with achargedAmount
of 100 USD, thetotalBalance
would be 0, aschargedAmount
-order.total
results in 0(1). - Adding a granted refund with an amount of 10 USD would result in a
totalBalance
of 10, aschargedAmount
- (order.total
-grantedRefund.amount
) gives 10. ThechargeStatus
will 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
. ThetotalBalance
will be 0, aschargedAmount
(90USD) - (order.total
-grantedRefund.amount
) gives 0. ThechargeStatus
will 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
- Variables
mutation OrderGrantRefundCreate($id: ID!, $input: OrderGrantRefundCreateInput!) {
orderGrantRefundCreate(id: $id, input: $input) {
grantedRefund {
id
}
order {
id
}
errors {
field
code
message
lines {
lineId
field
message
code
}
}
}
}
{
"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",
"reasonReference": "UGFnZTox" # ID of a Page
}
}
The mutation accepts below arguments:
id
- ID of theOrder
to 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.reasonReference
- ID of thePage
representing the reason for the refund. See configuration to learn more.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 providedlines
andgrantRefundForShipping
.transactionId
- ID ofTransactionItem
that will be used to process the refund. Ifamount
is provided in the input, thetransaction.chargedAmount
needs to be equal to or greater than the providedamount
. Ifamount
is 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
- Variables
mutation OrderGrantRefundUpdate($id: ID!, $input: OrderGrantRefundUpdateInput!) {
orderGrantRefundUpdate(id: $id, input: $input) {
grantedRefund {
id
}
errors {
addLines {
field
message
code
lineId
}
field
code
}
}
}
{
"id": "T3JkZXJHcmFudGVkUmVmdW5kOjE=",
"input": {
"addLines": [
{
"id": "T3JkZXJMaW5lOjgzYmZmZWI3LTVkNjEtNDMzZS1iOGFkLTFhMTE1NWI2ZTgwNg==",
"quantity": 3
}
],
"removeLines": [],
"transactionId": "VHJhbnNhY3Rpb25JdGVtOmUzOTVjNzdmLWFmNjQtNDRmZC05NmRiLThkZmNkMDYwNmZmOA==",
"amount": 10,
"grantRefundForShipping": false,
"reason": "New reason",
"reasonReference": "xXyzaXdaxbX" # ID of a Page
}
}
The mutation accepts below arguments:
id
- ID of theOrderGrantedRefund
which should be updatedinput
:addLines
: Lines that should be added toOrderGrantedRefund
id
- ID of theOrderLine
.quantity
- The quantity of the specific lines planned to refund.reason
- Reason for the planned refund related to the specific line.reasonReference
- ID of thePage
representing the reason for the refund. See configuration to learn more.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 providedlines
andgrantRefundForShipping
.transactionId
- ID ofTransactionItem
that will be used to process the refund. Ifamount
is provided in the input, thetransaction.chargedAmount
needs to be equal to or greater than the providedamount
. Ifamount
is 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
- Variables
mutation TransactionRequestRefundForGrantedRefund($grantedRefundId: ID!) {
transactionRequestRefundForGrantedRefund(grantedRefundId: $grantedRefundId) {
transaction {
id
}
errors {
field
message
code
}
}
}
{
"grantedRefundId": "T3JkZXJHcmFudGVkUmVmdW5kOjE="
}
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 TransactionEvent
s 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
-
alreadyGrantedRefund
is the difference between already refunded and overcharged amountsalreadyGrantedRefund = max((totalRefunded - overchargedAmount), 0)
-
totalGrantedRefund
is a sum of allamount
from allOrderGrantedRefund
. The maximum value isorder.total
, when the value is above, it's replaced with theorder.total
.totalGrantedRefund = max(sum(orderTotalGranted.amount), order.total)
-
totalRefunded
is a sum ofamount
from all transactions with refunded or pending refunds amounttotalRefunded = sum(amountRefunded + amountRefundPending)
-
overchargedAmount
defines theamount
that has been charged over theorder.total
value. 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
TransactionItem
s: one with achargedAmount
of 100 USD and the second withchargedAmount
of 60 USD. ThetotalBalance
would be 60, aschargedAmount
-order.total
results in 60, and we do not have any granted refunds. ThechargeStatus
isOVERCHARGED
. -
Adding a granted refund of 10 USD increases the
totalBalance
to 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) = 70
The
totalRemainingGrant
becomes 10, as thetotalRefundedAmount
is 0. -
The second transaction is refunded for an amount of 50 USD. After this operation, the
totalBalance
is 20, and the order remainsOVERCHARGED
. ThetotalChargedAmount
decreases, and thetotalRefundedAmount
increases. ThetotalRemainingGrant
remains 10 because theoverchargedAmount
still 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.total
value, reducing thetotalRemainingGrant
. -
Finally, the last refund of 5 USD is processed, resulting in a
totalBalance
of 0. ThechargeStatus
isFULL
, and thetotalRemainingGrant
is 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
totalChargedAmount
includesamountChargePending
, andtotalRefundedAmount
includesamountRefundPending
. - Authorized amounts, including
amountAuthorized
andamountAuthorizePending
, are also considered processed. These amounts will contribute to the overall processed totals and impact theoverchargedAmount
.
Refund reasons configuration​
Available from 3.22
Staff users with MANAGE_SETTINGS
permission can access additional refunds configuration available in Dashboard and via API.
Configuration is global and reflected on every channel.
Currently, RefundSettings
type controls a single behavior: whether the reasonReference
is required when the refund is created or updated.
Setting Refund Reason Type Requirement​
You can decide you want a strict policy to require a reason reference for every refund. Such reference will be a relation to a Page
type. To enable that, you need first to create a Page Type dedicated for such refunds. You can do that in the Dashboard or by pageTypeCreate
mutation.
Once you have a PageType
ID, you can set it in the refundSettingsUpdate
mutation.
- Mutation
- Variables
mutation SetRefundTypeRequired($pageTypeId:ID!){
refundSettingsUpdate(input:{
refundReasonReferenceType:$pageTypeId
}){
errors{
message
}
}
}
{
"pageTypeId": "your-id"
}
Now, every time a staff user tries to create a refund via transactionActionRequest
or orderGrantRefundCreate
/orderGrantRefundUpdate
mutations (or via Dashboard), they will be required to provide a reason reference. The existing "reason" field will still be available, but it will be optional.
Note that permitted App
will be able to omit this setting. For the staff user, the ValidationError will be raised if the reason reference is not provided. App can omit this field for more powerful workflows.
Apart from that, the refundReason
provided in refund mutations must be a valid Page
ID of the type set in the refundReasonReferenceType
setting. If not, a ValidationError will be raised as well.
Disabling refund reason type requirement​
To opt-out from this strict policy, staff user can disable it in the Dashboard or by running the refundReasonReferenceClear
mutation:
mutation DisableRefundTypeRequired {
refundReasonReferenceClear {
errors {
message
}
}
}