Skip to main content

Advanced Shipping Options

In our Basic Flow Tutorial we examined only the case where we have a single shipping rule that offers free shipping to all supported delivery addresses.

Most commonly, shipping options are much more complex than this, offerring multiple shipping options or conditional shipping costs based on the cart contents and requuested delivery address.

In this tutorial we'll be expanding our Quote endpoint logic to handle the two cases, one for multiple shipping options, and another more advanced example where we will delegate the whole shipping calculation to our platform.

Delegate Shipping to the Platform

It is safe to assume that your eCommerce platform provides a way to calculate the shipping options available for a cart and a delivery address.

We will expand our stand-in eCommerce platform with a new Service interface that handles this calculation, but your platform might handle this as part of the cart or even a single function that handles this calculation, so adjust accordingly.

interface ShippingService {
getShippingOptions($cart, $address): []ShippingOption
}

We can now make use of this interface in our updated Quote logic :

src/Controller/Simpler/QuoteController.php
class SimplerQuoteController extends AbstractController {
public function postData(Request $request): Response {
$requestData = json_decode($request->getContent(), true);

$response = [
'request_id' => $requestData['request_id'],
'quotes' => [
[
'items' => []
]
]
];

$cart = CartService::createCart();
try {
$this->addQuotationItemsToCart($request['quotation']['items'], $cart);
$this->addLineItemsToResponse($cart->getLineItems(), $response);

$shippingAddress = $request['quotation']['address'];

$options = ShippingService::getShippingOptions($cart, $shippingAddress);

// if we no options were resolved, return the UNSHIPPABLE_LOCATION error
if (!count($options)) {
return new JsonResponse([
'code' => 'UNSHIPPABLE_LOCATION',
'message' => 'Shipping is available only for the UK'
], 400);
}

$response['quotes'][0]['shipping_option'] = [
// we'll be considering multiple options in the next example
'id' => $options[0]->getId(),
'name' => $options[0]->getLabelText(),
'total_cents' => $options[0]->getCost(),
'type' => 'DELIVERY'
];

return $this->json($response);
} finally {
$cart->delete();
}
}
}

Multiple Shipping Options

In our example so far we have skipped over the fact that the Simpler platform expects an array of quotes as a response, directly assigning a single quote in our array.

We hardcode the quote index to 0, and consider only the first shipping option returned by the Shipping Service. In order to support multiple shipping options we will have to return multiple quotes, one for each shipping option.

Frequently Asked Question

Why not return a single quote with multiple shipping options?

The answer lies in the flexibility provided by multiple quotes. A lot of the advanced use cases that you can suport via Simpler Checkout rely on the multiple quote mechanism and the separate pricing for each.

Examples include payment methods that are conditional on shipping selection or discounts available for specific shipping options. In that case each quote changes not only the shipping option, but more properties related to the cart such as the total pricing or the discounts applied.

In light of this, this is how we would expand our quote controller to handle all the options offered.

src/Controller/Simpler/QuoteController.php
class SimplerQuoteController extends AbstractController {
public function postData(Request $request): Response {
$requestData = json_decode($request->getContent(), true);

$response = [
'request_id' => $requestData['request_id'],
'quotes' => []
];

$cart = CartService::createCart();
try {
$this->addQuotationItemsToCart($request['quotation']['items'], $cart);

$shippingAddress = $request['quotation']['address'];

$options = ShippingService::getShippingOptions($cart, $shippingAddress);

// if we no options were resolved, return the UNSHIPPABLE_LOCATION error
if (!count($options)) {
return new JsonResponse([
'code' => 'UNSHIPPABLE_LOCATION',
'message' => 'Shipping is available only for the UK'
], 400);
}

// iterate through all the returned shipping options
foreach ($options as $option) {
$response['quotes'][] = $this->buildQuote($cart, $option);
}

return $this->json($response);
} finally {
$cart->delete();
}
}

private function buildQuote($cart, $option) {
$quote = [];

// we're moving the `addLineItemsToResponse` method logic here
foreach ($cart->getLineItems() as $item) {
$quote['items'][] = [
'id' => $item->getProduct()->getSKU(),
'quantity' => $item->getQuantity(),
'subtotal_cents' => $item->getTotalCost(),
]
}

// we're moving the option serialization logic here
$quote['shipping_option'] = [
'id' => $option->getId(),
'name' => $option->getLabelText(),
'total_cents' => $option->getCost(),
'type' => 'DELIVERY'
]

// the total_cents property in the root quote represents the total payable including shipping and discounts
$quote['total_cents'] = $cart->getGrandTotal() + $option->getCost();
}
}