Skip to main content

Tutorial : Basic Flow

Introduction

This tutorial is designed to guide you through the full integration of a mock eCommerce platform with Simpler, from the simplest possible complete flow, to advanced optional cases that you can implement based on your needs.

Basic Flow Tutorial Diagram

We'll be covering both the instrumentation of the SDK as well as the relevant route implementations, using a stand-in eCommerce platform expressed through interface methods. The code examples in these tutorials are presented in PHP 8 using the Symfony framework.

The eCommerce Platform

Throughout this tutorial we will be using an eCommerce platform stand-in that follows a layered architecture where domain objects and operations are exposed through interfaces.

If your eCommerce platform does not follow a layered architecture, it is still worthwhile following this tutorial through to the end, as it aims to provide a baseline for all the requirements of the Platform Interface.

We'll be using three services from our simplified eCommerce platform :

interface CatalogService {
public function getProductBySKU(): Product;
}

interface CartService {
public function createCart(): Cart;
}

interface CheckoutService {
public function createOrder($cart): Order;
}

Step 1 : User visits the storefront

When the user visits your storefront and navigates to your product page, we must update the frontend template in order to render the Simpler Quick Buy Button.

The first step is to include the SDK, by adding the link to the Simpler SDK in our document's head :

_document.html
<head>
<script src="https://checkout.simpler.so/sdk/simpler-checkout.js"></script>
</head>

We can then update the product page template to run another script that makes use of the Simpler singleton to instantiate the Quick Buy Button

_product.php
<body>
<div>
<h1><?= $product.title ?></h1>
<p><?= $product.description ?></p>
</div>
<!-- we create a container for our button here to control styling and layout -->
<div id="simpler-checkout-container"></div>

<script type="text/javascript">
// make sure the DOM has loaded before trying to mutate it
window.addEventListener('DOMContentLoaded', () => {
Simpler.checkout({
appId: 'YOUR_SIMPLER_APP_ID',
currency: 'EUR',
items: [{
id: '<?= $product.sku ?>',
quantity: 1
}],
// we retrieve the DOM element we created and use it as the target of the checkout call
target: document.getElementById('simpler-checkout-container')
});
})
</script>
</body>

Step 2 : User clicks Quick Buy

When the user clicks on the Quick Buy button everything will be handled automatically for you by the Simpler web component. The checkout window will be opened as a new tab on mobile or a popup centered on the user's screen on desktop.

Step 3 : Fetch product details

Once the checkout window is opened the Simpler platform will read the items that are requested, and perform a request to fetch the Details for these products. We'll set up a controller connected to the simpler/v1/products route that will handle the Simpler requests, fetch the product and build the JSON response :

src/Controller/Simpler/ProductsController.php
class SimplerProductsController extends AbstractController {

public function postData(Request $request): Response {
// Retrieve the JSON request body
$requestData = json_decode($request->getContent(), true);

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

// we only have a single product in this example, but let's futureproof by handling multiple products at once
foreach ($request['items'] as $item) {
try {
// we want to retrieve the product from our catalog and serialize the details in the Platform Interface API format
$product = CatalogService::getProductBySKU($item['id']);
$response['items'][] = [
'id' => $product->getSKU(),
'title' => $product->getTitle(),
'description' => $product->getDescription(),
'image_url' => $product->getImageURL(),
'shippable' => $product->requiresShipping()
];
} catch (ProductNotFoundException $e) {
// if the product is not found return a meaningful error
return new NotFoundHttpException('The product was not found');
}
}

return $this->json($response);
}

}

Step 4 : Initial quote

At this point Simpler is aware of the product details but it still cannot proceed as it does not have information on the pricing of the cart. In order to validate the cart and retrieve pricing information, Simpler will issue a Quote request before rendering the form to retrieve pricing information :

Let's set up a controller to handle the quote request, and use our CartService to try to create a cart with the supplied information. We'll use the created cart to read the totals and build the response and the discard the cart as it will not be needed anymore.

src/Controllers/Simpler/QuoteController.php
class SimplerQuoteController extends AbstractController {

public function postData(Request $request): Response {
// Retrieve the JSON request body
$requestData = json_decode($request->getContent(), true);

// we'll start by creating an empty cart
$cart = CartService::createCart();

try {
$this->addQuotationItemsToCart($request['quotation']['items'], $cart);
} finally {
// make sure that the cart is deleted even if an error occurs
$cart->delete();
}
}

private function addQuotationItemsToCart($items, $cart) {
// iterate through the items requested in the quotation and build add them to the cart we've created
foreach ($request['quotation']['items'] as $item) {
$product = CatalogService::getProductBySKU($item['sku']);
$cart->addProduct($product, $item['quantity']);
}

// run all calculations on the cart we've created - if applicable to your platform
$cart->calculateTotals();
}

}

At this point we are building the cart, adding the products from the quote request and deleting it immediately. The next step is to retrieve the pricing information from the cart we've created, build a response and respond to the quote request. Since the user has not yet supplied an address, we will not be returning a shipping cost yet.

Handling Money

If your platform uses floating point numbers to represent money you will have to convert these numbers to their cents equivalent when interacting with Simpler.

This is to ensure that all number representations are precise and additions or subtractions using these numbers will not result in floating point errors.

Whenever you see the _cents suffix in the Platform Interface API, make sure you are transforming your values to their integer cents equivalent (i.e. 3.30 == 330).


class SimplerQuoteController extends AbstractController {
public function postData(Request $request): Response {
$requestData = json_decode($request->getContent(), true);

// let's initialize our response
$response = [
'request_id' => $requestData['request_id'],
// we'll be returning a single quote for this example
'quotes' => [
[
'items' => []
]
]
];

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

// now that we've have a cart with the requested items, retrieve the resolved line items to build the response
$this->addLineItemsToResponse($cart->getLineItems(), $response);

// build our response json, and we're done
return $this->json($response);
} finally {
$cart->delete();
}
}

private function addLineItemsToResponse($lineItems, $response) {
foreach ($cart->getLineItems() as $item) {
$response['quotes'][0]['items'][] = [
'id' => $item->getProduct()->getSKU(),
'quantity' => $item->getQuantity(),
'subtotal_cents' => $item->getTotalCost(),
]
}

$response['total_cents'] = $cart->getGrandTotal();
}
}

Steps 5 & 6 : Subsequent quotes

Once the first quote request is succesful, the checkout form will be presented to the user, who will be able to interact with it and start providing details for their checkout. While the user is changing their information on the form, the Simpler Platform may issue new Quote requests to update the costs associated with the cart, including any relevant information in the requests to your interface.

When the user supplies an address we will have to use it to resolve the shipping options and costs for the cart. Let's consider the case where our eCommerce store provides free shipping for UK addresses and no shipping options for all other destinations :

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'];

// return the UNSHIPPABLE_LOCATION error code if shipping address is not supported
if ($shippingAddress['country'] != 'GB') {
return new JsonResponse([
'code' => 'UNSHIPPABLE_LOCATION',
'message' => 'Shipping is available only for the UK'
], 400);
}

$response['quotes'][0]['shipping_option'] = [
// the shipping option id will be returned as the selected option in the submit call
'id' => 'free_shipping',
'name' => 'Free Shipping',
'total_cents' => 0,
'type' => 'DELIVERY'
];

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

We're now returning a shipping option for the carts created by Simpler. The checkout session now has all the required information and the user can proceed with their payment and complete the order.

The simplistic shipping cost we have implemented for this example might not be powerful, but it presents the basic concept of including the shipping option in the quote we create. We will be revisiting quotes and shipping options in the Handling Shipping Options in Quotes tutorial.

Steps 7 & 8 : Order Submission

At this point the user has all the information they need and they can proceed with checking out their cart. All payment operations are handled by the Simpler Checkout system, the order gets stored alongside the payment authorization, and a Submit request is issued to the Platform Interface.

The Submit request body contains the cart, order & shopper information that you can use to create an order on your system. Let's set up the last controller and handle the order creation :

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

// we will create a cart once again - you can extract this to a service if you want
$cart = CartService::createCart();

foreach ($request['order']['items'] as $item) {
$product = CatalogService::getProductBySKU($item['sku']);
$cart->addProduct($product, $item['quantity']);
}

// the shipping_method_id property contains the shipping option ID we returned in our quote
$cart->setSelectedShippingOptionID($request['order']['shipping_method_id']);

// delegate the order creation to the platform
$order = CheckoutService::createOrder($cart);

// respond with the order ID
return $this->json([
'request_id' => $requestData['request_id'],
'order_id' => $order->getID()
]);
}
}

This should cover the order creation for now; We're getting all the information from the request, creating an order and returning the ID for future reference to Simpler.

Step 9 : Handling Success

The checkout has been completed and the user is presented with the checkout success screen. The final step to conclude the flow is to navigate to the storefront order success page, to provide the user with their receipt as well as to run any analytics handlers from your storefront.

Going back to the SDK from Step 1, we can provide an onSuccess handler to the checkout call arguments that will be invoked when the checkout flow has been successful. The onSuccess handler accepts a single argument, the order_id that we returned in our Submit response, that we can use to navigate the user to the relevant order success page :

  Simpler.checkout({
appId: 'YOUR_SIMPLER_APP_ID',
currency: 'EUR',
items: [{
id: '<?= $product.sku ?>',
quantity: 1
}],
target: document.getElementById('simpler-checkout-container')
onSuccess: (id) => {
window.location.href = window.location.origin + `/order-success?order_id=${id}`;
}
});