Streamline shipping with a proximity-based service that uses Google Maps.

Tutorials

Streamline shipping with a proximity-based service that uses Commerce Layer and Google Maps.

September 10, 2024 Fabrizio Picca

Efficient shipping is a cornerstone of ecommerce success. Traditional methods relying on a single fulfillment center often lead to delays and higher costs. In this blog post, we'll explore a solution to this problem: a proximity-based shipping service using Commerce Layer and Google Maps. This approach ensures faster deliveries and optimized costs by dynamically selecting the nearest stock location to the customer's address. This example is designed to run on Deno, a modern runtime for JavaScript and TypeScript that offers enhanced security and performance features. Let's dive in.

The challenge: Optimal shipping routes.

Shipping products quickly and cost-effectively is a significant challenge for online retailers. Fulfillment from a single warehouse often results in inefficient shipping routes, longer delivery times, and increased expenses. Our proximity-based shipping service addresses this issue by selecting the closest stock location to the customer's address, ensuring quicker deliveries and lower costs.

Getting started: What you need.

To implement this solution, you will need:

  1. Commerce Layer Account: Sign up and set up your organization.
  2. Google Maps API Key: Register for the Google Maps API and obtain an API key.
  3. Deno Environment: Set up a Deno environment to run the code.
  4. Dependencies: Use import statements to include necessary modules.
import {
  Address,
  CommerceLayer,
  Order,
} from "npm:@commercelayer/sdk";
import { authentication } from "npm:@commercelayer/js-auth";
import { Client as GoogleMapsClient } from "npm:@googlemaps/google-maps-services-js";
import { oakCors } from "oakCors";
import { Application } from "Application";
import { Router } from "Router";

Architecture of the POC.

The architecture is quite straight forward. We are going to trigger a server function containing the proximity shipping logic using the commerce layer weboohks.

Proximity-based shipping architecture

Setting up Commerce Layer.

In the Webhooks app, create a new webhook for the orders.start_fulfilling event.

The Commerce Layer Webhooks App

Below is a screenshot of the configuration of the webhook. Notice the topic we are going to use and the includes that we need, in this case the shipping address of the order and, of course, the line items. Learn more about Commerce Layer real-time webhooks here. The Callback URL setting should contain your endpoint. In this POC, it is my Deno-deployed function.

Webhook configuration in the Commerce Layer web hook app.

Deep dive into the code

Here’s a detailed breakdown of the code that implements the proximity-based shipping service:

  • Importing dependencies.

We start by importing the essential modules from Commerce Layer SDK, Google Maps API, and other supporting libraries.

import {
  Address,
  CommerceLayer,
  Order,
} from "npm:@commercelayer/sdk";
import { authentication } from "npm:@commercelayer/js-auth";
import { Client as GoogleMapsClient } from "npm:@googlemaps/google-maps-services-js";
import { oakCors } from "oakCors";
import { Application } from "Application";
import { Router } from "Router";
  • Setting up constants and clients.

We define constants for the API endpoints, Commerce Layer credentials, and Google Maps API key. Then, we initialize the clients for Commerce Layer and Google Maps.

const ENDPOINT = "/api/v1/proximityShippingService";
const CL_CLIENT_ID = "YOUR_CL_CLIENT_ID";
const CL_CLIENT_SECRET = "YOUR_CL_CLIENT_SECRET";
const CL_ORG_SLUG = "proximity-shipping-demo";
const GOOGLE_MAPS_KEY = "YOUR_GOOGLE_MAPS_KEY";

const gmClient = new GoogleMapsClient({});
  • Authentication and initialization.

We authenticate with Commerce Layer using client credentials and initialize the Commerce Layer client with the obtained access token.

const auth = await authenticate('client_credentials', {
  clientId: CL_CLIENT_ID,
  clientSecret: CL_CLIENT_SECRET,
})

const cl = CommerceLayer({
  organization: CL_ORG_SLUG,
  accessToken: token.accessToken,
});
  • Setting up the router.

We set up a router using Oak to handle HTTP requests and enable CORS for the defined endpoint.

const router = new Router();
router.options(ENDPOINT, oakCors());
  • Fetching the stock locations.

We fetch the stock locations from Commerce Layer, including their addresses.

const stock_locations = await cl.stock_locations.list({
  include: ["address"],
});
  • Handling webhook requests and calculating distances.

We handle POST requests to the defined endpoint, process the order data, and calculate the nearest stock location using the Google Maps Distance Matrix API.

router.post(ENDPOINT, oakCors(), async (conn) => {
  if (!conn.request.hasBody) {
    conn.throw(415);
  }

  const body = await conn.request.body().value;

  const order: Order = { ...body.data.attributes, id: body.data.id };

  const inclusions: Array<any> = body.included;

  const shippingAddress: Address = inclusions.find(
    (inclusion) =>
      inclusion.id === body.data.relationships.shipping_address.data.id
  ).attributes;

  if (shippingAddress) {
    const shippingAddressAsString = shippingAddress.full_address;

    if (shippingAddressAsString) {
      try{
        const result = await gmClient.distancematrix({
          params: {
            destinations: [shippingAddressAsString],
            origins: stock_locations.map(
              (stock_location) =>
                `${stock_location.address?.line_1},
              ${stock_location.address?.zip_code} ${stock_location.address?.city},
              ${stock_location.address?.country_code}`
            ),
            key: GOOGLE_MAPS_KEY,
          },
        });

        const closestStockLocation = result.data.rows
          .map((row, index) => {
            return {
              ...stock_locations[index],
              distance:
                row.elements[0].status === "ZERO_RESULTS"
                  ? -1
                  : row.elements[0].distance.value,
            };
          })
          .filter((result) => result.distance != -1)
          .sort((a, b) => a.distance - b.distance)[0];

        //make sure we have a stock location
        //TODO if closest == current do nothing
        if (closestStockLocation) { 
          const line_items = inclusions
            .filter((inclusion) => inclusion.type === 'line_items')
            .filter((line_item) => line_item.attributes.item_type === 'skus')

          //get line items from shipment
          const shipments = await cl.shipments.list({
            filters: {
              order_id_eq: order.id,
            },
            include: ["stock_line_items", "available_shipping_methods"],
          });

          //delete current shipments

          console.log(
            `order ${order.id} has ${shipments.length} shipment(s) automatically generated`
          );
          shipments.forEach((shipment) => {
            console.log(`deleting shipment ${shipment.id}`);
            if (shipment.stock_line_items) {
              shipment.stock_line_items.forEach((stock_line_item) => {
                if (stock_line_item.line_item) {
                  //storing line items from stock_line_items
                  console.log("storing line item ");
                  //line_items.push(stock_line_item.line_item);
                }
              });
            }
            cl.shipments.delete(shipment.id);
          });
          console.log(`all shipments deleted for order ${order.id}`);
          /*TODO considering that stock has been released we should also update
         stock data in the original stock location*/

          //create a new shipment from the closest stock location
          console.log(`creating new shipment using proximity shipping`);

          const newInventoryStockLocation = (
            await cl.inventory_stock_locations.list({
              filters: { stock_location_id_eq: closestStockLocation.id },
            })
          ).first();

          if (
            newInventoryStockLocation &&
            shipments?.[0].available_shipping_methods?.[0].id != null
          ) {
            const newShipment = await cl.shipments
              .create({
                order: cl.orders.relationship(order.id),
                inventory_stock_location:
                  cl.inventory_stock_locations.relationship(
                    newInventoryStockLocation.id
                  ),
                shipping_method: cl.shipping_methods.relationship(
                  shipments[0].available_shipping_methods[0].id
                ),
              })
              .catch((e) => {
                console.error(e);
                throw e;
              });

            await cl.shipments.update({
              id: newShipment.id,
              _upcoming: true,
            });

            await cl.shipments.update({
              id: newShipment.id,
              _picking: true,
            });

            console.log(
              `shipment ${newShipment.id} created and attached to order ${order.id} with stock location in ${closestStockLocation.address?.city}`
            );

            //create stock line items and link them to the shipment
            line_items.forEach(async (line_item) => {
              if (line_item.sku_code) {
                console.log("gettting stock item");
                const newStockItem = (
                  await cl.stock_items.list({
                    filters: {
                      sku_code_eq: line_item.sku_code,
                      stock_location_id_eq: closestStockLocation.id,
                    },
                  })
                ).first();

                if (newStockItem) {
                  console.log("creating new stock line item ");
                  await cl.stock_line_items
                    .create({
                      quantity: line_item.quantity,
                      sku_code: line_item.sku_code,
                      shipment: cl.shipments.relationship(newShipment.id),
                      line_item: cl.line_items.relationship(line_item.id),
                      stock_item: cl.stock_items.relationship(newStockItem.id),
                    })
                    .catch((e) => {
                      console.error(e);
                      throw e;
                    });
                }
              }
            });

            await cl.attachments.create({
              attachable: {id: order.id, type: 'orders'},
              name: 'System',
              description: `proximity shipping: closest shipping location to destination is ${closestStockLocation.id} ${closestStockLocation.address?.full_address}`,
              reference_origin: 'app-shipments--note'
            })

            //return 200
            conn.response.type = "json";
            conn.response.body = {
              success: true
            };
          }
        }
      } catch (err){
        console.log(err)
      }  
    }
  } else {
    conn.response.body = {
      success: false,
      error: {
        code: 404,
        message: `no shipping address in order ${body.data.attributes.id}`,
      },
    };
  }
});

This is the core part of our POC, where we do the actual calculation of the distances and define the “closest” shipping location to the shipping address. Code here is not optimal for sure, but it’s just to give a sense of how this could work.

This piece of code performs the following:

  1. Get shipping and stock locations address.
  2. Find the closest stock location using Google Distance Matrix API.
  3. If found, delete all current shipments.
  4. Recreate shipments from the closest stock location.
  5. Return ok.

You can find more information about the Google Distance Matrix API here.

  • Starting the server.

Finally, we create an Oak application, register the router, and start the server on port 8000.

const server = new Application();

server.use(router.routes());
server.use(router.allowedMethods());

await server.listen({ port: 8000 });

The result.

Here is screenshot of the output from our proximity shipping custom logic:

Shipping based on proximity shown in the timeline in Commerce Layer's dashboard.

In this order the original shipping location was different. Our custom strategy adjusted the shipment with the new stock location, closer to the order shipping address.

Conclusion: Elevate your shipping strategy.

Implementing a proximity-based shipping service can significantly enhance your ecommerce operations. By dynamically selecting the nearest stock location, you ensure faster delivery times and reduced shipping costs. This approach not only improves customer satisfaction, but also optimizes logistics, making your business more efficient and competitive. Leverage Commerce Layer's robust APIs and Google's distance calculation services today to transform your shipping process into a strategic advantage.