@shipengine/connect-native-rating-api
TypeScript icon, indicating that this package has built-in type declarations

3.1.0 • Public • Published

Connect Native Rating

This package allows carrier implementers to rate natively through Connect instead of using an API that lives outside of the Auctane platform. To do this, an implementer will need to build rating logic and publish that to Connect. Once the code is published, rating data can be managed through Native Rating directly.

What is Native Rating?

Native Rating is the engine that runs carrier rating logic and provides key/value access to rating data through a hierarchy of Rate Cards, Rate Sets, and Rate Tables. This data access is abstracted from the carrier implementation so that development can focus on the rules of rating. This document will focus on building the logic for rating via Native Rating.

Requirements

The simplest Native Rating carrier implementation looks like this:

module.exports = {
  rateShipments: (context, ratingRequest) => [{id: "empty-rates", rates: []}]
};

This isn't very useful because for every request, an empty list of rates will be returned. But it shows some of the requirements of an implementation:

  1. The code must have a single default export using CommonJS
  2. The code must have NO external dependencies. This means no built-in or third party modules can be loaded.
  3. The exported object must contain a rateShipments function that takes a context and a rating request object. It must return an array of rate results correlated with a request id.
  4. The code must be plain Javascript.

Because these requirements make it tough to develop anything but the simplest rating logic, it is recommended to build the implementation using modern Javascript or Typescript patterns and then use something like webpack to transpile and bundle your code.

A full configuration of webpack is outside the scope of this document because the options are nearly endless. However, including the following options in your webpack.config.js will help webpack produce code that Native Rating needs.

module.exports = {
  ...,
  target: "node",
  output: {
    libraryTarget: "commonjs2"
  },
  ...
}

Rating logic

There are many ways to implement rating logic, but for a Native Rating implementation, we've found that it's best to approach it in three steps:

  1. Generate necessary data keys
  2. Fetch required data
  3. Build the rates from data

Let's break the steps down and talk about why it makes sense to think of an implementation in this way.

Generate necessary data keys

The purpose of this step is to end up with the smallest list of data keys necessary that will need to be retrieved from the data store. You'll want to do as much validation as possible in this step and figure out what can be excluded from data retrieval. For example, if your carrier does not offer overnight service to Alaska and the shipment being rated has a destination of Alaska, you can exclude any keys that are associated with overnight rates. If your carrier only supports domestic shipments, you can immediately return an empty list of rates if the shipment has a different origin and destination country.

The difficult part about this step is trying to figure out the minimum amount of data you'll need during step 3.

Fetch required data

After the list of keys needed is generated, the second step is to request the data from the Native Rating context that is passed in to your rateShipments function as the first argument. You should request all the data at once, if possible, as this will reduce the amount of time your logic is waiting for results. Calls to context.get* should be treated as expensive. Instead of doing this:

// This gets data one at a time, which will wait for each request to finish before starting the next request
const rates = await context.getRates(["rate-key-1", "rate-key-2"]);
const metadata = await context.getMetadata(["meta-key-1", "meta-key-2"]);
const zones = await context.getZones(["zone-key-1", "zone-key-2"]);

You should do something like this:

// This gets data all at once and will generally be faster
const ratePromise = context.getRates(["rate-key-1", "rate-key-2"]);
const metaPromise = context.getMetadata(["meta-key-1", "meta-key-2"]);
const zonePromise = context.getZones(["zone-key-1", "zone-key-2"]);

const [rates, metadata, zones] = await Promise.all([ratePromise, metaPromise, zonePromise]);

You should also try to avoid requesting data based on the results of a previous data request. The most common example of this is zone based rating. It's most straight-forward to get the zone for the shipment then based on the zone, generate they rating keys necessary. Unfortunately, this will result in two sequential calls made to the data store. Instead, see if it makes sense to generate keys for all zones and then request them at the same time that you're getting the zone. When you have both sets of data, you can get the rate that applies.

/**
 * This is a simple, but slower approach to zone-based rates
 */
const zoneKey = "1-9"; // Get the zone for a shipment going from a postal code that starts
                       // with 1 to a postal code that starts with 9. NOTE: this is an
                       // arbitrary key format and can be whatever you want
const zones = await context.getZone([zoneKey]);

// Now build up the keys based on the zone we got back
const zone = zones[zoneKey];
const rateKeys = [`ground-${zone}`, `air-${zone}`];
const rates = await context.getRates(rateKeys);

/**
 * This is a more efficient approach, but is more complex to reason about
 */
const zoneKey = "1-9"; // Get the zone for a shipment going from a postal code that starts
                       // with 1 to a postal code that starts with 9. NOTE: this is an
                       // arbitrary key format and can be whatever you want
const zonePromise = context.getZone([zoneKey]);

// Build keys for all possible zones, which in our example is 1 through 5
const rateKeys = [1, 2, 3, 4, 5].flatMap(zone => [`ground-${zone}`, `air-${zone}`]);
const [possibleRates, zones] = await Promise.all([context.getRates(rateKeys), zonePromise]);

// Now that we have all the data, get the applicable rates
const zone = zones[zoneKey];
const applicableRateKeys = [`ground-${zone}`, `air-${zone}`];
const rates = possibleRates.filter(rate => applicableRateKeys.includes(rate.key));

There is a point where this approach no longer makes sense and it is based on the total number of keys requested per function. There is not a hard number, but we've found that between 100 and 200 keys is when the performance characteristics change. So in the example above, if you had 10 zones, 10 services, and 10 different weight groups, you would need to request 1000 keys from the data store. If you requested the zone first, that would cut the number of keys down to 100.

Build the rates from data

Once you have all the necessary data, you can build the final list of rates. This is where you would apply the data that you got back from the context and compile it into a valid rate set. In the examples above, you could just map the rates returned into the required RateResult format. In practice, it will usually be more complicated than that. For example, you may have requested metadata that dictates the maximum size of a flat rate box and based on that, you can now decide whether to return a flat rate for the shipment, or some scaled rate based on other metadata.

Connect module

When you've got your single file Javascript implementation written, you can build the Connect module. You'll need to add a few Connect packages to your project, which can be done using the following commands:

npm install --save @shipengine/connect-native-rating-api
npm install --save @shipengine/connect-runtime

The following code is a basic implementation of the module and in practice, this code won't need to change much.

// src/index.js
import { start } from '@shipengine/connect-runtime';
import { NativeRatingApp } from "@shipengine/connect-native-rating-api";

const app = new NativeRatingApp({
  // Replace this path with either the location of your single javascript file
  // or the output of your bundler
  ImplementationPath: "/path/to/your/single/file/implementation.js",
  RatingAndZoneData: __dirname + "/data.json",
  Metadata: {
    ApiCode: "demo_carrier",
    Name: "Demo Carrier",
  }
});

start(app);

The implementation path should point to your single file Javascript rating implementation that meets the requirements listed above. If you're writing the file directly, you can just use the path to that file. If you use Webpack or some other bundler, you'll want to use the output path of the tool for that property. The data.json file should store whatever data you want available when testing your rating logic. In the following example, there are two keys available: ground and air. You can add variable and zone information here if your logic needs it.

// src/data.json
{
  "rate_card_id": "default-rate-card",
  "currencyCode": "USD",
  "rates": [
    { "key": "ground", "value": "10" },
    { "key": "air", "value": "20" },
  ],
}

Testing

Once the module is set up and your logic is ready for testing, you can execute the script above. This will start a development server using your rating logic and sample data. If you make a request to http://localhost/3005/rates with shipment information, you will see output similar to the following:

example server output

The server will show you which rate, metadata, and zone keys were requested and which were found in the sample data. The server will also expose a docs endpoint that will serve the OpenAPI spec of the Native Rating server. This will provide the shape of the input and output required by the rates and zones endpoints.

Readme

Keywords

Package Sidebar

Install

npm i @shipengine/connect-native-rating-api

Weekly Downloads

5

Version

3.1.0

License

Apache-2.0

Unpacked Size

266 kB

Total Files

280

Last publish

Collaborators

  • arjun.modi
  • christian.casado
  • lzhang
  • lukasz.parala
  • deeepawesome
  • akowalczyk
  • ddygas
  • pspringerauct
  • dangnguyen91
  • arapicki
  • prasadjoshi29
  • zjaholkowska
  • maciej_sabik_auctane
  • kdobrzynskiactn
  • bmusielak
  • mmilowska
  • bartoszzurawski
  • mspiaczka-auctane
  • maciej_adamek
  • marcin_karwat_auctane
  • krzysztof.malcher
  • auctome
  • sushithegreat
  • agustin.martin.auctane
  • brock.bouchard.auctane
  • auctane.joshua.semar
  • romofel
  • chunter-auctane
  • luxehahn
  • jeffrysparrow
  • uiuxdeveloper
  • ckroutterauctane
  • shipengine_it
  • kaseycantu-se
  • joshuaflanagan
  • anthonyshull
  • binkard-auctane
  • dlblom
  • rickyr
  • dbernazal
  • auc-rhibbeler
  • harris.butler