Returns
Return orders are a fixture in retail. To support the creation of return orders (and potential refunds as a consequence) EVA supports various methods. Here we'll dig deeper into how that is pushed and managed on EVA.
Single-step returns process
Using this strategy, 'completed' returns are simply pushed into the OMS, and all previous (sub)steps are not registered first by the OMS. This is used when a pre-printed return label is included in the original order, and the consumer can simply ship it back to the warehouse, no questions asked. Once booked into the 3rd party warehouse system, you can update EVA of the return using the PushReturnOrder service. This creates a return order with all specified data.
PushReturnOrder
Just like PushSalesOrder
we have a service that supports pushing externally created return orders into EVA. This service is - very surprisingly - called PushReturnOrder.
This service creates a return order with all specified data. In this service, it is required to identify the external system and the return order's ID in that external system by setting BackendSystemID and BackendID.
In EVA, returns are based on an original order. This order can be identified either through the order's ID in the external system by setting OriginalBackendID or through the order's EvaID by setting OriginalOrderID. At least one of these is required.
If the stock has already been received, set AlreadyReceived to true. This will immediately ship the order.
If the stock has already been received and the open amount should be refunded directly, set AutoRefund to true.
The order lines also have to be matched to the original line, this should be done by providing the OriginalBackendID or the OriginalOrderLineID. If those are not available, the product's CustomID can be set, but matching based on that is not advised.
{
"BackendSystemID": "Salesforce", //External system ID
"BackendID": 132465789, //Return order's ID in external system
"OriginalBackendID": 245608, //Original sales order's ID in external system
"OriginalOrderID": 245678, //Original sales order's EVA ID
"Remark": "too big", //Optional remark
"ReturnToOrganizationUnitBackendID": 14, //BackendID of the OU on which the return was booked
"StockOrganizationUnitID": 34, //ID of the OU to which the stock was returned
"AlreadyReceived": true, //Whether or not the stock was already received
"AutoRefund": true, //Whether or not the order should be automatically refunded
"Lines": [
{
"OriginalBackendID": 45674657, //Order line ID in the external system
"OriginalOrderLineID": 8646425, //Original sales order line ID
"CustomID": null, //Optional, not recommended
"ProductID": 43265,
"Quantity": 1,
"Remark": "Too big", //Optional remark
"ReturnReasonID": 1 //Return reason ID
}
]
}
{
}
Multistep returns process
One strategy to perform returns is via a multistep returns process, in which a return is first 'registered' by the customer, then shipped, and finally processed. This is used in combination with a returns/RMA portal, in which a customer can for example register returns, fill-out a reason for return and generate a return shipping label.
Returns in this process are created using CreateCustomerReturn. This creates a new order in EVA with a negative order amount and a reference to the original order. When the warehouse receives the return, it is marked as shipped. This in turn triggers the refund using the payment reference of the original order. When the refund is successful, the return order is marked completed.
EVA supports the connection to / creation of a return portal where users can look up their original orders using their order ID and their email address. The user can return one or more lines from their order, and receive the shipping return instructions using this tool.
Customer initiated returns
Customer initiated returns are returns that might be made via a customer return portal on your website.
To support these kinds of returns, we use the service CreateCustomerReturn.
Here is a sample:
{
"OrderID": 121, // ID of the original sales order.
"TargetOrderID": 123, // OrderID returned from CreateOrder
"Lines": [
{
"OrderLineID": 156,
"Quantity": 1,
"ReturnReasonID": 1,
"Remark": "testremark"
},
"CustomFieldsV2": [ // you can add custom fields for the sole purpose of the return order
{
"CustomFieldID": 123,
"Name": "ReturnPortalID",
"DisplayName": "ReturnPortalID",
"DataType": "String",
"DataTypeID": 0,
"Options": {
"IsArray": false,
"IsRequired": false
},
"BackendID": "ReturnPortalID",
"Value": "1234",
"IsEditableByUser": true,
"IsArray": false
}
],
],
"ReturnWithoutProducts": false, // Whether or not to return without products
"AmoutToRefund": 49.95,
"CreateInOriginalOrganizationUnit": true,
"StockOrganizationUnitID": 4,
"BackendID": "null",
"BackendSystemID": "null"
}
{
}
Authentication
Instead of regular authentication, we allow customers to retrieve temporary tokens for specific orders to support the return portals. The token is obtained through the OrderID in combination with the EmailAddress of the customer that is attached to the order.
{
"OrderID": 121,
"EmailAddress": "[email protected]"
}
{
}
Get the returnable status
Call GetReturnableStatusForOrder to check whether (a part of) an order can be returned or not. The response will specify which order line(s) are still returnable and its respective returnable amount.
Keep in mind that the results of your ReturnableStatus extension point is leading when it comes to the ReturnableStatus.
Here is a sample order for which order lines can still be returned:
{
"OrderID": 123
}
{
"Result":[
{
"OrderLineID":156726,
"ReturnableQuantity":1,
"ReasonCode":"NotShipped",
"Reason":"Not shipped",
"IsReturnable":true,
"OverrideFunctionality": {
"Scope": 0, }
"OrderLineType":0,
"ReturnableAmount":142.36000
},
{
"OrderLineID":156727,
"ReturnableQuantity":1,
"ReasonCode":"NotShipped",
"Reason":"Not shipped",
"IsReturnable":true,
"OverrideFunctionality": {
"Scope": 0, }
"OrderLineType":0,
"ReturnableAmount":175.20000
},
{
"OrderLineID":156725,
"ReturnableQuantity":0,
"ReasonCode":"NotShipped",
"Reason":"Not shipped",
"IsReturnable":false,
"OverrideFunctionality": {
"Scope": 0, }
"OrderLineType":0
}
],
"ReturnSets":[
],
"StockOrganizationUnits":[
{
"OrganizationUnitID":1,
"OrganizationUnitName":"Company",
"Reason":0
},
{
"OrganizationUnitID":70,
"OrganizationUnitName":"Store Beverly Hills",
"Reason":1
},
{
"OrganizationUnitID":86,
"OrganizationUnitName":"Warehouse US",
"Reason":2
}
]
}
In the scenario a return is not possible (sample above), the ReasonCode property will show why, by means of a static code. This static code is linked to a default translation and in turn this translation is shown on the front end. The benefit of this static code and its default translation, is that it can be translated to whatever language you like.
To translate the code, use the standard translation API: ReplaceEntityTranslation
. For examples (including for ReturnableStatusReason
) see Translations.
Even when a product is not returnable, due to its return period having expired for example, you might still be able to return it regardless if you (or your colleague via elevation) have the OverrideReturnablePeriod functionality. Whether this is possible for a product is shown in the above call as well, with the OverrideFunctionality property.
Managing return reasons
These are the services needed to create, update, delete and get return reasons. For each we show a sample of how the service works.
Create
To create a return reason, use CreateReturnReason.
{
"Name": "Wrong order",
"Description": "We didn't order this",
"BackendID": "wrong_order",
"OrganizationUnitSetID": "1",
"Type": "Customer"
}
{
"ID": 9
}
- OrganizationUnitSetID is added here to tag the created return reason to that set. This means that all organization units attached to that set would have that created return reason available for use. Other organization units would not. This allows you to have different return reasons set up for different OrganzationUnitSetID if needed.
- The BackendID lets you give a custom identifier to the reason, to harmonize it across environments and perhaps match it to reason in a third-party system.
List
This return reason along with all other return reasons created (if any) can now be fetched using ListReturnReasons.
{}
{
"Result": {
"PageConfig": {
"Filter": {
"Type": "0"
},
"Start": 0,
"Limit": 2147483647,
"SortDirection": 0
},
"Page": [
{
"ID": 3,
"Name": "AssortmentChange",
"Description": "The assortment was not correct",
"BackendID": "1",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
},
{
"ID": 4,
"Name": "CommercialAgreement",
"Description": "",
"BackendID": "2",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
},
{
"ID": 5,
"Name": "Damaged",
"Description": "The product was damaged.",
"BackendID": "3",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
},
{
"ID": 6,
"Name": "QualityIssue",
"Description": "The product does not meet the expected quality.",
"BackendID": "4",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
},
{
"ID": 7,
"Name": "WrongDelivery",
"Description": "Not the correct product",
"BackendID": "5",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
},
{
"ID": 8,
"Name": "Transport",
"Description": "The product was lost in transport.",
"BackendID": "6",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
},
{
"ID": 9,
"Name": "WrongPrice",
"Description": "The product was not delivered with the right price attached.",
"BackendID": "7",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
},
{
"ID": 10,
"Name": "Promotion",
"Description": "",
"BackendID": "8",
"OrganizationUnitSetID": 1,
"OrganizationUnitSetName": "New Black",
"Type": 0,
"Priority": 0
}
],
"Offset": 0,
"Limit": 2147483647,
"Total": 8,
"SortDirection": 0,
"Filters": {
"Type": "0"
},
"NumberOfPages": 1,
"CurrentPage": 1
}
}
Update
If you messed up or had a change of mind on any of the created return reasons, then you can take care of that using UpdateReturnReason to update it.
{
"ID": "9",
"Name": "Wrong order",
"Description": "We didn't order this",
"BackendID": "wrong_order",
"OrganizationUnitSetID": "12"
}
{
}
Delete
If you no longer need a return reason use DeleteReturnReason to have it removed.
{
"ID": "9"
}
{
}
Documents for return orders
In our default customer return order process, once you place the return order, we kick off a process that generates documents and emails them to the customer.
The following flows assume you are making use of Return order orchestration in combination with Unified orders. If you don't use unified orders, the setting UseOrderOrchestrationForReturnOrders
needs to be set to true in order for these flows to work.
There are a few settings and stencils EVA relies upon when generating return documents.
First off: when using the originating organization unit of the return order, the setting ShipmentDocumentGeneratorName
determines EVA's course of action for producing the return labels.
When this generator produces one or more labels, EVA attaches them to an email built by means of stencil ReturnMail, and additionally attaches the stencil ReturnForm (if configured).
For the ShipmentDocumentGeneratorName
, these are the current options;
- Default: default value, EVA itself will generate a single label for the return order, based on the Stencil template ReturnLabel
- NULL: will prevent generation of any document (and thus the sending of the email completely)
- Arvato: Arvato will take care of all aspects of the return label and shipping
- Consignor: EVA will request the return label from Consignor and consequently send the customer this label via the ReturnMail template
Any documents generated will be attached to the order: the labels as order blob type ReturnLabel (3) and the form (if applicable) as order blob type ReturnForm (4). This is important, because this is where the hook comes in!
Doing your own document generation with value Null
If you want to be in charge of your own document generation, set this generator to NULL and simply listen to the placed event export (see Webhook of the order target to act. You’ll recognize these orders from their Properties enum value containing 131072 (“has return lines”) from the GetOrder
service (replacing the HasReturns property on the order root, earlier).
Generate the documents in any way you want, upload them using StoreBlob
and attach them to the order using AttachBlobToOrder
. Be sure to use the same order blob types mentioned above.
Finally, call SendReturnMailTo
to have EVA gather and send these order blob types to the customer, using the stencil ReturnMail as above.