Shape the routing with the DOMS Toolkit

The DOMS Toolkit allows you to create customized routing rules by providing you with tools to evaluate the content of the orders and the network of affiliates, and make decisions accordingly.

How to define a toolkit fence

In the following we will explain in detail what each field does and how to use it. Almost all of these steps will apply to building a toolkit rating so if you're interested in how a toolkit rating works, please don't skip this section!

ToolkitFence fields in detail

Click to see what each field does

entity1

This describes the entity type for the left-hand side. In case of conditional rules this is the entity that gets evaluated first.

entity2

This describes the entity type for the right-hand side. In case of conditional rules this is the entity that gets evaluated second (or never), but more to that later..

active

Whether this fence is active or not

name

The name, has to be unique

nameLocalized

An object containing the localized names for different languages

description - [optional]

An optional description to say what the fence has to do. Although optional, it is strongly recommended that you do give it a meaningful description.

descriptionLocalized - [optional]

An object containing the localized descriptions for different languages

order

A number determining whether the fence should be executed before or after different fences. Lower numbers have precedence over higher numbers.


Defining the actual rule

To define a rule you have to pick exactly one of the following.

rule

The setup for a conditional rule. Incompatible with using the comparisonRule field. Use this if you want to compare each side against a fixed value (e.g. order line item quantity > 3)

comparisonRule

The setup for a comparison rule. Incompatible with using the rule field. Use this if you want to compare the left side directly with the right side. (e.g. order brand == facility brand)


How to define a conditional rule

Conditional rules test a certain property of an entity (Order, Facility) against a pre-defined static value.

Show how to do it!

operator

What the positive evaluation of the left hand side means for the execution of the fence as a whole. For now, there is only the equality-operator.

leftPart - ToolkitRulePart

The first part that is evaluated. Its evaluation result determines whether the right part gets evaluated.

rightPart - ToolkitRulePart

The second part that is evaluated if the first evaluation passes. Its evaluation result determines the final outcome of the fence.

How to define a ToolkitRulePart

A ToolkitRulePart consists of an array of predicates and a connector that determines how they are connected together.

predicateConnector

OR will make the rule part true if any of the predicates evaluate to true. AND will only make the expression true if all predicates evaluate to true.

predicates

An array of one or more ToolkitPredicates. A predicate consists of the following fields:

  • propertyPath

  • entityOperator

  • expectedValue

  • transformation (optional)

  • transformationArgs (optional, depends on transformation used)

How to define a ToolkitPredicate

You have to define at least one predicate. In the following we'll explain what each field is used for.

propertyPath

A JSONPath defining the property to look at.

entityOperator

The operators determine how a property of an entity should be compared against a given value. They can be divided up into two groups: Single value entity operators and array entity operators.

Single value entity operators

These operators can only be used for single values, not for arrays, such as VALUE_EQUALS

  • VALUE_EQUALS -> actual value equals expected value (e.g. 2 == 2)

  • VALUE_NOT_EQUALS -> actual value unequals expected value (e.g. 2 != 3)

  • VALUE_CONTAINS -> actual value contains expected value (e.g. "HELLO WORLD" contains "HELLO")

  • VALUE_NOT_CONTAINS -> actual value does not contain expected value (e.g. "HELLO WORLD" does not contain "HI")

  • LESS_THAN -> actual value less than expected value (e.g. 2 < 3)

  • LESS_EQUALS -> actual value less than or equals expected value (e.g. 2 <= 3, 3 <= 3)

  • GREATER_THAN -> actual value greater than expected value (e.g. 3 > 2)

  • GREATER_EQUALS -> actual value greater than or equals expected value (e.g. 3 >= 2, 2 >= 2)

Array entity operators

These operators can only be used for arrays, not for single values. They can be grouped into three categories: ANY, EVERY, NONE. Within each category, either any, every or no value has meet the criteria. The condition may be anything of the aforementioned single value entity operators. Here are some examples on how this may look:

  • ANY_VALUE_EQUALS

  • EVERY_VALUE_GREATER_EQUALS (which is equivalent to NO_VALUE_LESS_THAN)

  • NO_VALUE_CONTAINS (which is equivalent to EVERY_VALUE_NOT_CONTAINS)

ANY, EVERY and NONE..

ANY

Requires that at least one element of the array satisfies the following condition. It is not required that more than one element match but if the array is empty this condition will NOT be fulfilled.

EVERY

Requires that every element of the array satisfies the following condition. If the array is empty, this condition will be true because there is no element that does not fulfill the condition.

NONE

Requires that no element satisfies the following condition, meaning that every element does not satisfy the condition. This condition will always be fulfilled, unless there is an element that satisfies the condition, including for empty arrays.

Single value operators vs array operators

If your actual value is an array, you have to use an array operator. When is that the case? - If you use a wildcard operator (* or ?()) you get an array of elements (such as $.orderLineItems[*]). However, if you do use a transformer (such as COUNT to get the number of elements or SUM to add the quantities of each OrderLineItem), then you will get a single value and must use a single value operator.

transformation - [optional]

A transformation

transformationArgs -[optional]

The args for the transformation, if required.

expectedValue

The expected value the property should match Examples:

  • 2 - a number

  • "WAREHOUSE" - a string

  • "2024-02-19T16:16:38.107Z" - a date-string


How to define a comparison rule

Comparison rules are very different to conditional rules in that a certain property of the left-hand side (Order) can be compared to a certain property of the right-hand side (Facility).

An example of this could be an order with articles of a certain brand demanding a facility tagged with the same brand.

Show how to do it!

predicateConnector

OR will make the rule part true if any of the predicates evaluate to true. AND will only make the expression true if all predicates evaluate to true.

predicates

An array of one or more ToolkitComparisonPredicates.

How to define a ToolkitComparisonPredicate?

rightPropertyPath

A string containing a JSONPath for the right side entity.

leftPropertyPath

A string containing a JSONPath for the right side entity.

entityOperator

One of the following:

  • LEFT_CONTAINS_RIGHT - the left hand side should contain all values from the right hand side

  • RIGHT_CONTAINS_LEFT - the right hand side should contain all values from the left hand side

  • ALL_MATCHES - the arrays of values should be exactly equal

leftTransformation - [optional]

The transformation for the left hand side.

leftTransformationArgs - [optional]

The transformation arguments for the left transformation, if required.

rightTransformation - [optional]

The transformation for the right hand side.

rightTransformationArgs - [optional]

The transformation arguments for the right transformation, if required.


How to define a toolkit rating

This setup works largely the same as the setup of a toolkit fence with a few key differences.

Differences to toolkit fences

  • order -> since all ratings get evaluated either way the order field does not exist for ratings

  • maxPenalty -> the penalty which determines the weight of the rating in comparison to other ratings

  • That aside toolkit ratings follow the exact same syntax as toolkit fences


Intro to JSONPath

It always begins with $ referring to the entity, followed by a path to the nested property. (e.g. $.orderLineItems[*].quantity) Used in:

See in detail how this works:
  • $.tenantOrderId -> $ refers to the order entity while tenantOrderId refers to the tenantOrderId. The . indicates that tenantOrderId is a nested property of Order

  • $.orderLineItems[*].article -> Gets the articles from all orderLineItems, resulting in an array

  • $.orderLineItems[?(@.quantity > 3)].article -> Same as last time but only gets the articles from OrderLineItems with a quantity greater than 3

To sum up we can say the following: nested properties can be accessed using the . while nested array properties such as orderLineItems, tags or stickers can be accessed using the array brackets [ and ] with an asterisk * to indicate a wildcard.

If you want to be more specific, instead of a wildcard * you can use the ?() syntax. Inside the parentheses (, ) you can then define an arbitrary expression using the @ symbol to refer to the entity being tested.

If you are for instance on an OrderLineItem such as in our example, the @ refers to the OrderLineItem that is just being evaluated. You can then access all of its nested attributes and test for an arbitrary condition. This could be the quantity as in our previous example but it could also be more complex such as the following expression:

$.orderLineItems[?(@.tags.find(tag => tag.id === 'color' && tag.value === 'red'))].article

This gets us the articles of all OrderLineItems that have at least one tag of type color with the value red.

Note: It is recommended though to keep your expressions as simple as they can possibly be without compromising on their functionality to avoid mistakes leading to misbehaviour of the fence/rating.


Intro to transformers

Transformers allow to modify the input of a ToolkitPredicate before it gets evaluated. This can serve multiple purposes, maybe you only want to look at the number of elements, get the total price of all OrderLineItems or just want to consider the beginning of the tenantOrderId.

These are the transformations currently available in the platform.

  • SUM : sums the number values of each element - requires an array of elements

  • COUNT : counts the number of elements - requires an array of elements

  • SUBSTRING : gets a substring of a value, requires start and end parameters

  • LAST: gets the last n chars of a string - required length parameter

Count Transformation

{
"propertyPath": "$.orderLineItems[*]",
"transformation": "COUNT",
"entityOperator": "GREATER_THAN",
"expectedValue": 10
}

Applies to orders with 10 OrderLineItems or more.

Sum Transformation

{
"propertyPath": "$.orderLineItems[*].quantity",
"transformation": "SUM",
"entityOperator": "GREATER_THAN",
"expectedValue": 100
}

Applies to orders where all OrderLineItems in sum have a total quantity of 100.

Substring Transformation

{
"propertyPath": "$.orderLineItems[*].article.tenantArticleId",
"transformation": "SUBSTRING",
"entityOperator": "ANY_VALUE_EQUALS",
"expectedValue": "Coca",
"transformationArgs": [0, 4]
}

Applies to orders where any OrderLineItem has a tenantArticleId that begins with "Coca".

Last Chars Transformation

{
"propertyPath": "$.orderLineItems[*].article.tenantArticleId",
"transformation": "LAST",
"entityOperator": "ANY_VALUE_EQUALS",
"expectedValue": "Christmas Special"
}

Applies to orders where any OrderLineItem has a tenantArticleId that ends with "Christmas Special"


Examples:

Scenario 1, routing orders to a facility located in the same area as the customer.

In this scenario we received the task of routing orders based on the user's location. Our customer has a very specific use case, where the order from customers located in the zip codes 50xxx and 51xxx should be routed to facilities located in the same areas (both areas are valid regardless of which zip is used).

The first step would be to create a Fence in our system by using the API. We use the API endpoint /api/configurations/routing/toolkit/fences like in the following example:

curl -sSL -X POST 'https://your.api.fulfillmenttools.com/api/configurations/routing/toolkit/fences' \
  --header 'Authorization: Bearer <TOKEN>' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "entity1": "ORDER",
    "entity2": "FACILITY",
    "active": true,
    "name": "Fence assign orders based on user ZIP",
    "nameLocalized": {
        "de_DE": "Fence ordnet Aufträge anhand der Postleitzahl des Kunden"
    },
    "description": "",
    "order":0,
    "rule": {
        "operator": "EQUALS",
        "leftPart": {
            "predicateConnector": "OR",
            "predicates": [
                {
                    "entityOperator": "VALUE_EQUALS",
                    "expectedValue": "50",
                    "propertyPath": "$.consumer.addresses[0].postalCode",
                    "transformation": "SUBSTRING",
                    "transformationArgs": [0,2]
                },
          {
                    "entityOperator": "VALUE_EQUALS",
                    "expectedValue": "51",
                    "propertyPath": "$.consumer.addresses[0].postalCode",
                    "transformation": "SUBSTRING",
                    "transformationArgs": [0,2]
                },
            ]
        },
        "rightPart": {
            "predicateConnector": "OR",
            "predicates": [
                {
                    "entityOperator": "VALUE_EQUALS",
                    "expectedValue": "50",
                    "propertyPath": "$.address.postalCode",
                    "transformation": "SUBSTRING",
                    "transformationArgs": [0,2]
                },
                 {
                    "entityOperator": "VALUE_EQUALS",
                    "expectedValue": "51",
                    "propertyPath": "$.address.postalCode",
                    "transformation": "SUBSTRING",
                    "transformationArgs": [0,2]
                },
            ]
        }
    }
}'

The interpretation of this Fence would be:

Orders from zip code areas beginning with 50 or 51 must be routed to facilities in zip code areas 50 or 51.

Some details in the example:

  • The Fence rule consists of two parts, the left part processes the information contained in the order (entity1), and the right part processes the information contained in the facility (entity2). Left part and right part are connected with the operator EQUALS, which means that if the result of evaluating the left part is true then the result of evaluating the right part has to evaluate to true, and analogously when the left side evaluates to false

  • The left part consists of two predicates connected with the OR predicate evaluator. The predicates are similar, as they take the value of the postalCode property of the consumer's address.

  • Both predicates have a transformation, which means that after getting the value of the postalCode, and before evaluating the predicate, this postal code is transformed by using the SUBSTRING transformation. In this case, the first two digits are extracted.

  • Then, for this extracted values, they are compared by using the entity operator, in this case VALUE_EQUALS. This would be interpreted as, "if the extracted value equals the expected value, then this predicate is evalued to true".

  • After evaluating the two left-side predicates individually, they are evaluated as a whole using the predicate operator OR.

  • Analogously, the right-hand side of the rule is evaluated. The result of evaluating the right side and the left side are evaluated again by using the rule operator EQUALS.

Suppose we have an incoming order for an user that is located in an area with postal code 51379, and the fence is evaluating a Facility located in an area with postal code 51355. When evaluating this combination from order and facility with the fence specified above would be:

LEFT PARTRULE OPERATOR (EQUALS)RIGHT PART

LEFT PART PREDICATE 1 EXTRACTION

51379

51355

RIGHT PART PREDICATE 1 EXTRACTION

LEFT PART PREDICATE 1 TRANSFORMATION

51

51

RIGHT PART PREDICATE 1 TRANSFORMATION

LEFT PART PREDICATE 1 EXPECTED VALUE

50

50

RIGHT PART PREDICATE 1 EXPECTED VALUE

LEFT PART PREDICATE 1 EVALUATE EQUALS

FALSE

FALSE

RIGHT PART PREDICATE 1 EVALUATE EQUALS

LEFT PART PREDICATE 2 EXTRACTION

51379

51355

RIGHT PART PREDICATE 2 EXTRACTION

LEFT PART PREDICATE 2 TRANSFORMATION

51

51

RIGHT PART PREDICATE 2 TRANSFORMATION

LEFT PART PREDICATE 2 EXPECTED VALUE

51

51

RIGHT PART PREDICATE 2 EXPECTED VALUE

LEFT PART PREDICATE 2 EVALUATE EQUALS

TRUE

TRUE

RIGHT PART PREDICATE 2 EVALUATE EQUALS

EVALUATE BOTH PREDICATES WITH OR

TRUE OR FALSE -> TRUE

TRUE OR FALSE -> TRUE

EVALUATE BOTH PREDICATES WITH OR

TRUE or TRUE -> TRUE

Result of the Rule evaluation

TRUE

Result of the Rule evaluation

and the facility will be retained as candidate for the routing, because the fence evaluated to true.

Scenario 2, directly compare values of the entity on the left with the entity on the right.

In this scenario we will ignore the evaluation of the left side and the right side of the rule, to perform a direct comparison of values of the entity on the left with the right entity, to handle the following scenario:

Orders with order line items containing a tag with id "CATEGORY" should be routed to facilities that have the same category. Available categories are "DANGEROUS_GOODS" and "SAFE_GOODS".

We create the fence in our system by using the following request:

curl -sSL -X POST 'https://your.api.fulfillmenttools.com/api/configurations/routing/toolkit/fences' \
  --header 'Authorization: Bearer <TOKEN>' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "entity1": "ORDER",
  "entity2": "FACILITY",
  "active": true,
  "name": "Fence ensure respects the product category",
  "comparisonRule": {
    "predicateConnector": "AND",
    "predicates": [
      {
        "entityOperator": "RIGHT_CONTAINS_LEFT",
        "leftPropertyPath": "$.orderLineItems[*].tags[?(@.id === \"CATEGORY\")].value",
        "rightPropertyPath": "$.tags[?(@.id === \"CATEGORY\")].value"
      }
    ]
  },
  "order": 0
}'

Suppose we have an incoming order with an order line item that has the tag {id:"CATEGORY",value:"DANGEROUS_GOODS"}, and we have to facilities, (1) the first which has the tag SAFE_GOODS and (2) the second which has the tag DANGEROUS_GOODS

  • When evaluating the Fence for the (1) first facility, the following will happen:

    • the fence will consider the order and take the value of the orderLineItems.tags[*].value, in this case, the value "[DANGEROUS_GOODS]".

    • the fence will consider the facility take the value of the tag "tags[*].value", in this case for facility (1) it is "[SAFE_GOODS]".

    • then it will apply the entity operator "RIGHT_CONTAINS_LEFT". Because the value "[SAFE_GOODS]" does not contains "[DANGEROUS GOODS]", the fence will evaluate to false, and this facility will be discarded.

  • When evaluating the Fence for the (2) second facility, the following will happen:

    • the fence will consider the order and take the value of the orderLineItems.tags[*].value, in this case, the value "[DANGEROUS_GOODS]".

    • the fence will consider the facility take the value of the tag "tags[*].value", in this case for facility (1) it is "[DANGEROUS_GOODS]".

    • then it will apply the entity operator "RIGHT_CONTAINS_LEFT". Because the value "[DANGEROUS_GOODS]" is contained in "[DANGEROUS GOODS]", the fence will evaluate to true, and this facility will be retained as candidate.

Note: In case the order has many order line items containing many different tags, then all of them will be evaluated and compared with the values from the right part.


Reference

  • Find the full specification of the toolkit configuration endpoints in our fulfillmenttools API

  • You can find the JSONPath documentation here

  • A helpful tool for testing JSONPath expressions can be found here

Last updated