Courier Selection
Courier Selection
Courier Selection Script
A courier selection script is the means by which a courier and courier service can be automatically associated with a shipment. This association typically takes place at the point at which the shipment is received, but can also be done at a later stage in the order processing workflow.
Courier selection may be based on a number of factors, including any or all of the factors below (often a combination of these) * whether the shipment is international or domestic * the shipment weight * the customer's choice * the value of the order * the type of products in the shipment * the customer's expected service delivery
All of this information can be determined programatically in a Groovy Script using the OrderFlow API.
A Simple Example
The example below performs a courier selection based primarily on the customer's choice, but also on the shipment weight.
def customerchoice = value.deliverySuggestion.code; def weight = value.weight; def courierOptions = ''; if (customerchoice == null) { throw new IllegalStateException("Unable to determine courier choice. Please ensure 'deliverySuggestionCode' shipment attribute is set"); } customerchoice = customerchoice.toLowerCase(); def courierReference; def serviceCode; if (customerchoice.contains("dpd")) { courierReference = "dpd"; serviceCode = "dpd_classic"; } if (customerchoice.contains("interlink")) { s if (weight <= 5) { courierReference = "dpd"; serviceCode = "interlink_nextday"; } else { courierReference = "dpd"; serviceCode = "interlink_nextday_parcel"; } } if (courierReference == null) { courierReference = "generic"; serviceCode = ""; } values.courierDeliveryInfo.courierReference=courierReference; values.courierDeliveryInfo.serviceCode=serviceCode; values.courierDeliveryInfo.courierOptions=courierOptions; return null;
A few points to note:
- the customer's choice is determined here using the Groovy expression
value.deliverySuggestion.code
. If none is set, the script will return an error. - the shipment weight is expected to be populated.
- if no valid courier selection is made, the courier selection is returned
Scripting Context
Scripting Variables
value
Holds an instance of the Shipment for which the courier selection is being performed.
values
Holds an instance of the context map for the courier selection script. Doesn't typically get used directly, but does hold additional referencable data as described next.
values.courierDeliveryInfo
The values.courierDeliveryInfo
holds an instance of rtd.courier.info.CourierDeliveryInfo
.
The fields in this class hold data which maps to the selected courier, service and options, as summarised in the table below.
Courier Delivery Info Fields
Name | Description | Maps To |
---|---|---|
courierReference | The externalReference value for the selected courier for the shipment |
shipment.courier.externalReference |
serviceCode | The selected service code from the courier | shipment.deliveryMethod.serviceCode |
courierOptions | A comma separated list of options | shipment.deliveryMethod.courierOptions |
mailFormat | Mail format set for this shipment | shipment.deliveryMethod.mailFormat |
lineHaulSiteReference | Only applies if line hauling is used. If shipment.lineHaulDestinationSite.externalReference |
Note the 'Maps To' column above denotes the Groovy expression that can be used to extract from the shipment the value populated using the courier selection script.
Returning to the initial courier selection script example, we can see how the values.courierDeliveryInfo
expression is populated.
values.courierDeliveryInfo.courierReference=courierReference; values.courierDeliveryInfo.serviceCode=serviceCode; values.courierDeliveryInfo.courierOptions=courierOptions; return null;
Note that the return statement in the courier selection script is recommended for OrderFlow 3.8.2 and below in order to ensure that the courier selection result does not get interpreted as JSON text (as per a legacy implementation of the courier selection script that is no longer in widespread use).
Further Examples
The example below selects between domestic and international couriers and services depending upon destination and weight.
def courierReference; def serviceCode; def weight = value.weight; if (weight == null) { throw new IllegalStateException("Unable to determine weight for shipment."); } def genericNonInternationalCountryCodes = ['UK','GB','IM','GG','JE','IE']; def countryCode = value.implicitAddress.countryCode; if (countryCode == null) { throw new IllegalStateException("Unable to determine country code for shipment."); } def international = !genericNonInternationalCountryCodes.contains(countryCode); if (international) { courierReference = "royalmail_international_netdespatch"; serviceCode = "IE1E" } else { if (weight <= 5) { courierReference = "royalmail_domestic_netdespatch"; serviceCode = "TPN01N"; } else { courierReference = "royalmail_domestic_netdespatch"; serviceCode = "TPH01N"; } } if (courierReference == null) { courierReference = "generic"; serviceCode = ""; } values.courierDeliveryInfo.courierReference=courierReference; values.courierDeliveryInfo.serviceCode=serviceCode; values.courierDeliveryInfo.courierOptions=''; return null;
Useful Expressions
The following Groovy expression are useful when writing courier selection scripts.
Customer Choice
The OrderFlow convention is to receive the delivery suggestion via the shipment delivery suggestion code, as shown in the script below.
def customerchoice = value.deliverySuggestion.code;
Note that the customer choice can sometimes be mapped to an explicit choice in courier. On other occasions, it may contain information on the service level, or expected time to delivery. It tends to be very environment specific.
Country Code
A reliable way to get the ISO two character country code for a shipment is using the script below:
def customerchoice = value.implicitAddress.countryCode;
Note the use of implicitAddress
; if a delivery address has been set at the shipment level, then this will be used.
Otherwise, the order delivery address will be used.
Post Codes
The post code can retrieved in a similar way to the country code:
def customerchoice = value.implicitAddress.postCode;
The post code is often useful to identify shipments coming from more remote destinations on the British Isles, including the Channel Islands (Jersey, Guernsey), Isle of Man, as well as regions such as Northern Ireland and the Scottish Highlands. For some carriers, different rules and restrictions need to be applied for some of these shipments compared to those in more heavily populated regions of Great Britain.
Order Value
The total price for the order paid by the customer can be found using the following expression:
def price = value.orderItem.totalPrice.gross;
As the total gross price, it includes goods and shipping, as well as all taxes.
Shipping Charges
The shipping charges are set against the order in a similar way:
def shippingCharge = value.orderItem.shippingPrice.gross;
The amount paid by the customer for shipping may certainly affect the courier choice in some environments.
Priority
The shipment priority field is often used to influence courier choice.
def shippingCharge = value.priority;
The priority value is set on a numerical scale, with a higher value used for higher priority shipments. The possible priority values and gradations are not set in stone, and will vary according to need. It is certainly common to at least define a distinction between normal and high priority shipments.
Expected Delivery Date
On occasion, the front end eCommerce system may capture or record an expected delivery date by which the customer is expecting to receive their order.
If set, it can be obtained using the following expression:
def requestedDeliveryDate = value.requestedDeliveryDate;
Country Group
As of OrderFlow 4.2.0.1 the following expression can be used to determine whether the destination country is in a particular country group. This can be useful for scripts that will apply different courier selection for EU countries as opposed to countries outside of the EU.
See below for an example:
def countryLookup = values.countryLookup; if (!countryLookup.isInCountryGroup('eu',value.implicitAddress.countryCode)) { options = 'international_only'; }
Unit Testing
A Note on Unit Testing
This section requires the that you have in place the OrderFlow integration and scripting environment. If you are interested in having this set up in your environment to enable you to write your own unit tests, please contact the Realtime Despatch support team.
For complex courier selection scripts, a accompanying unit test is highly recommended (and mandatory if implemented by Realtime Despatch Technical Staff).
Writing a unit test should generally be done at the same time or even before the courier selection script is written, in line with the principles of Test Driven Development.
Write Test
Unit testing of courier selection can be done using a unit test which extends BaseCourierSelectionScriptTest
.
This test which defines a run(packageName, scriptName, shipment)
method, which provides a convenient way of
setting up the scripting context required for the courier selection script.
The packageName parameter is the package name containing the script.
The scriptName is the name of the file containing the courier selection script.
The shipment is an instance of rtd.domain.Shipment
, which needs to be instantiated in code.
An example of a unit test is shown below:
/** * Courier selection script test for 'orderflow.courier.selection.script'. * * @author Phil Zoio */ public class OrderFlowCourierSelectionScriptTest extends BaseCourierSelectionScriptTest { private Shipment shipment; private OrderItem orderItem; @Override protected void setUp() throws Exception { super.setUp(); setupShipment(); } /** * Do basic shipment setup */ void setupShipment() { shipment = new Shipment(); orderItem = new OrderItem(); shipment.setOrderItem(orderItem); orderItem.getTotalPrice().setGross(100D); } void runSelection() { run("rtd.orderflow.courier", "orderflow.courier.selection.script", shipment); } public void testGBHighValue() throws Exception { shipment.getAddress().setCountryCode("GB"); String expectedServiceCode = null; String expectedOptions = null; expectCourier("dpd", expectedServiceCode, expectedOptions); } public void testGBLowValue() throws Exception { orderItem.getTotalPrice().setGross(10D); shipment.getAddress().setCountryCode("GB"); expectCourier("royalmail_tracked", "TPN01", "serviceOption=signature"); } public void testEU() throws Exception { shipment.getAddress().setCountryCode("DE"); String expectedServiceCode = null; String expectedOptions = null; expectCourier("ups", expectedServiceCode, expectedOptions); } public void testUS() throws Exception { shipment.getAddress().setCountryCode("US"); String expectedServiceCode = null; String expectedOptions = null; expectCourier("ups_international", expectedServiceCode, expectedOptions); } void expectCourier(String expectedCourier, String expectedServiceCode, String expectedOptions) { runSelection(); System.out.println(courierDeliveryInfo); assertEquals(expectedCourier, courierDeliveryInfo.getCourierReference()); assertEquals(expectedServiceCode, courierDeliveryInfo.getServiceCode()); assertEquals(expectedOptions, courierDeliveryInfo.getCourierOptions()); } }
Note that in writing the unit test, your responsibilities are:
- identify a scenario that needs to be tested. For this, define the outcome,
through a call to the
expectCourier(String expectedCourier, String expectedServiceCode, String expectedOptions)
method above. - set up the
shipment
instance so that it meets to preconditions for testing the outcome.
You will want to repeat this process for each of the outcomes to be tested, in order that all outcomes are adequately tested.
Write Script
You will then need to write the script in a way that satisfies the outcome. The example script tested using the code above:
def delivery=values.courierDeliveryItem def countrycode = value.implicitAddress.countryCode; def euCountries = ['BE','BG','CZ','DK','DE','EE','IE','EL','ES','FR','IT','CY', 'LV','LT','LU','HU','MT','NL','AT','PL','PT','RO','SI','SK','FI','SE'] def rmDomestic = ['GB','UK']; def courierReference = null; def serviceCode = null; def courierOptions = null; if (euCountries.contains(countrycode)) { courierReference = 'ups'; } else if (rmDomestic.contains(countrycode)) { if (value.orderItem.implicitTotalPrice.gross > 20) { courierReference = 'dpd'; } else { courierReference='royalmail_tracked'; serviceCode='TPN01'; courierOptions='serviceOption=signature'; } } // Test to capture all country codes not specifically listed in euCountries and rmDomestic else { courierReference = 'ups_international'; } values.courierDeliveryInfo.courierReference=courierReference; values.courierDeliveryInfo.serviceCode=serviceCode; values.courierDeliveryInfo.courierOptions=courierOptions;
IDE Setup
A screenshot showing these in an Eclipse IDE project environment is shown below.
Note the use of the test package naming convention, which follows the along the lines:
MetaPack Booking Code
Documentation to be added.
Hypaship Delivery Group
Documentation to be added.