Raytail Promotion Interchange Format (RAYPIF)

PropertyValue
Document Version1.0
Revision Date31st December 2025
StatusFirst Draft
Underlying FormatJSON
AuthorIhusaan Ahmed

1. Overview

1.1 Purpose

This document defines the Raytail Promotion Interchange Format (RAYPIF), a standardized JSON-based format for communicating promotion rules, conditions, and effects between systems in the Raytail Ecosystem. RAYPIF enables consistent promotion definition, validation, and execution across Point of Sale (POS), Cloud, e-commerce platforms, and other integrated business systems.

1.2 Scope

This specification covers:

  • Core promotion metadata structure
  • Rule definition syntax and semantics
  • Effect specification formats
  • Data binding and reference mechanisms
  • Supported operations and comparison types
  • Property definitions and data types

However, this does not aim to and WILL NOT define the promotion code structure, engine specifications or any other system specific implementation details (such as the possibility to apply multiple promotions per item). It just defines the communication format and definitions.

That being said it WILL give opinions and validations on what generally should happen and what validations are necessary where applicable.

1.3 Audience

This document is intended for:

  • System architects designing the components relevant to the promotion engine
  • Developers implementing the promotion logic or creation logic
  • Integration engineers connecting disparate systems
  • QA engineers validating the promotion behaviour
  • Business analysts defining the promotion requirements

This document contains a lot of technical terms and as such is not very suitable for non-technical personnel, though they may be given access depending on the relevance an requirements.

1.4 Change log

AuthorDateVersionChange
Ihusaan Ahmed29th December 20251.0Initial Proposal

2. Data Types

The format relies on JSON as the backing format and as such will support all the data types supported by the JSON specification. This section defines and constraints we put over the JSON data types.

2.1 Datetime

All date time should be provided in the ISO8601 specification format with timezone explicitly provided. You can use Z to indicate Zulu (UTC) time but timezone should always be provided.

2.2 Decimal types

All values provided as decimal values will be interpreted to have the following properties:

FeatureValueDescription
Precision12The number of significant digits in the value (including both integer and decimal parts). This is an absolute count of all digits.
Eg: 123456789.123, 123456789123, 123456.789123 are all considered precision of 12
Scale3The maximum number of digits after the decimal point that is supported. Any more digits will be auto rounded (normal round off, 5 and above rounded up and below rounded down) on the consumer side.

Effective Range

  • Minimum: -999,999,999.999
  • Maximum: 999,999,999.999
  • Or equivalently: -999,999,999,999 to 999,999,999,999 (when all 12 digits are in the integer part)

Validation Examples:

ValueValid?Reason
123.456Within precision (6) and scale (3)
999,999,999.999Maximum valid value (12 precision, 3 scale)
999,999,999.9999Will be rounded to 999999999.000 (scale exceeds 3, and rounding will exceed precision)
9,999,999,999,999Exceeds precision of 12
-0.001Minimum meaningful decimal value
Note

The comma's (thousand separators) are to make it easier to read. Actual JSON MUST NOT contain the commas

Important Notes:

  • Values exceeding precision of 12 should be rejected at validation time
  • Leading zeros do not count toward precision (e.g., 000123.456 = precision of 6)
  • Trailing zeros after the decimal point are significant (e.g., 100.000 uses scale of 3)

2.3 Integer types

All integer types must follow the following properties:

FeatureValue
Precision10
Range-2,147,483,648 to 2,147,483,647 (32-bit signed)

2.4 String types

The following applies to string types:

Field TypeMax LengthNotes
code50Alphanumeric + hyphen recommended
name200UTF-8 supported
description2000
conditionCode20
resource identifiers500To accommodate complex lookups
all others3000

2.5 Collection Constraints

CollectionMin ElementsMax ElementsMax Nesting Depth
data array010,000N/A
rules children110015 levels
effects children15010 levels
sourceQuantitySelector array15010 levels

3. Root / Top-Level Structure

Every promotion will be defined at the top level as a JSON object with the following properties:

FieldData typeIs RequiredDescription
codestringYesUnique Identifier for the promotion
namestringYesHuman readable promotion name. Will also appear on promotional materials and receipts
descriptionstringNoDetailed description of the promotion. Will appear to the staff members.
customerDescriptionstringNoDetailed description of the promotion for the customer, relevant for customer facing components like e-commerce and self-checkout
imagesImageObjectNoAn object specifying the various images involved with the promotion
isEnabledbooleanYesA quick flag to show the enabled status. This is independent of validity so to check for application check both this and validity fields.
validFromISO 8601YesA date indicating the promotion start date and time
validToISO 8601YesA date indicating the promotion end date and time
lastUpdatedISO 8601YesTimestamp of the last modification (can be used as an idempotency)
priorityintegerYesExecution order of promotions (higher = earlier execution). If we have a priority tie, lower lastUpdated date will be taken, then they are evaluated in lexicographic order by code.
rulesRuleNodeYesTree structure defining the rules. See below for details
effectsEffectNodeYesTree structure defining the effects. See below for details
dataDataArrayNoArray of data objects. This is used for applying the same ruleset for different data types.

4. Image Object

This object defines the urls for the various files that can be used by the consuming applications.

FieldData typeIs RequiredDescription
thumbnailUrlstringNoAn image that will be used when just a small thumbnail like image is needed. Expects an image in the ratio 1:1, preferably with max dimension of 250px
coverImageUrlstringNoAn image that will be used as a cover image for the promotion. Expects an image in the ratio 16:9, preferably with dimensions 1920x1080
marketingImagesstring arrayNoA set of images that will be used for promotional materials. Expects images in the ratio 16:9 or 4:3. The system can decide which ratios to use. Eg: different second screen configs could show different ratios
Note

None of these are required but if the original images object in Root is not null, one of these MUST be provided.


5. Rule Node

There is no singular structure for a rule node. Instead there are a variety of them that could each have its own child/children. Look at the individual nodes for details on what could act as a root node and what could have multiple children.

Regardless of the node, each will have a property called type that will uniquely identify the node.

Look at the header area for a quote like the following for the configuration:

Type: the value to expect in the type property ChildType: the type of child nodes it could have (single, multiple, none or both) Root: true or false indicating if this could act as the root of the rules object TerminalNode: true or false indicating if this is a terminator / leaf node

5.1 Rule Logic Node

Combine various child nodes with logical operations.

Type: logic ChildType: multiple Root: true TerminalNode: false

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways logic
subTypestringYesLogical operation. See below for all supported types
childrenarray of RuleNodeYesAt least one child to evaluate logical operations on

5.1.1 Supported Rule Logic Nodes

Below are all the supported logic nodes for RuleNodes. These are different to Effect logic nodes, try not to confuse them.

Logical OperationSub Type valueDescription
ANDandEvaluates if all children results in true
ORorEvaluates if one of the children results in true
XORxorEvaluates that one and only one child results in true
NANDnandEvaluates that one or more of the children results in false
NORnorEvaluates that none of the children results in true
XNORxnorEvaluates that either none of the children result in true or all of them results in true

5.1.2 Examples

{
	"type": "logic",
	"subType": "and",
	"children": [
		RuleNode,
		...
	]
}

5.2 Resource Node

Specify the context for any subsequent node chain. For all supported resources and their fields check the Resources section.

Type: resource ChildType: single Root: true TerminalNode: false

It has no logic by itself and will evaluate to whatever the child evaluates to.

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways resource
subTypestringYesSee Resources for a list of all supported types
resourcestring or ReferenceYesThe resource identifier. See Resources for details.
groupChildrenbooleanYesIndicates if multiple resources match within its subtree, if they should be grouped or treated as individual items
childRuleNodeYesThe continuation of the logic with this resource context
Note

There CANNOT be nested resource nodes. A context can have only one resource and child resources are not currently supported.

This means in any subtree of a resource node cannot have another resource node even if its of the same type.

Examples:

  • ❌ Invalid: resource(article) → resource(customer) - nested resources
  • ❌ Invalid: resource(article) → resource(article) - nested same type
  • ✅ Valid: resource(article) → comparison → property

5.2.1 Examples

5.2.1.1 Resource with actual identifier
{
	"type": "resource",
	"subType": "lineItem",
	"resource": "code_uom::10002326|EA",
	"groupChildren": true,
	"child": RuleNode,
}
5.2.1.2 Resource with reference
{
	"type": "resource",
	"subType": "lineItem",
	"resource": "ref::sourceArticle",
	"groupChildren": false,
	"child": RuleNode,
}

5.3 Comparison Node

A node that has two to three children and evaluates them based on mathematical or logical operators.

Type: comparison ChildType: multiple Root: true TerminalNode: false

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways comparison
subTypestringYesSee below for a list of all supported comparisons
childrenarray of RuleNodeYesTwo to three nodes depending on the rule being evaluated

5.3.1 Supported Comparison Nodes

Below are all the supported nodes for ComparisonNodes.

Comparison OperationSub Type valueDescription
>=gteEvaluates if the first node's value is greater or equal to second node
>gtEvaluates if the first node's value is greater than second node
==eqEvaluates if the first node's value is equal to second node
!=neqEvaluates if the first node's value is not equal to second node
<ltEvaluates if the first node's value is less than second node
<=lteEvaluates if the first node's value is less than or equal to second node
< x <lt_gtEvaluates if the value in second node is less than the third but greater than first
<= x <lte_gtEvaluates if the value in second node is less than the third but greater than or equal to first
< x <=lt_gteEvaluates if the value in second node is less than or equal to the third but greater than first
<= x <=lte_gteEvaluates if the value in second node is less than or equal to the third but greater than or equal to first

5.3.2 Examples

{
	"type": "comparison",
	"subType": "neq",
	"children": [
		RuleNode,
		RuleNode
	]
}

5.4 Property Node

Extracts a property from the current resource context.

Important

This node extracts a value from a resource context. As such it should come in a subtree of a ResourceNode. The potential properties you can give per resource is detailed in Resources section below.

Type: property ChildType: none Root: false TerminalNode: true

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways property
propertyNamestring or ReferenceYesThe property of the resource to get or compare against
convertEquivalentbooleanNofalse if null, the value provided should be summed after it is converted to the base form (eg: UOM of articles may convert to base unit or tenders may convert to home currency)

5.4.1 Examples

5.4.1.1 With actual property name and child
{
	"type": "property",
	"propertyName": "lineTotal"
}
5.4.1.2 With reference property and values
{
	"type": "property",
	"propertyName": "ref::propertyName",
	"convertEquivalent": false
}

5.5 Literal Node

Define constant values that can be used for comparison or for something like always true promotion.

Type: literal ChildType: none Root: true TerminalNode: true

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways literal
subTypestringYesSee below for a list of all supported literal types
valuestring or ReferenceYesThe value encoded as a string

5.5.1 Supported Literal Nodes

Below are all the supported logic nodes for LiteralNodes.

Data TypeSub Type valueDescription
stringstringA text value
integerintAn integer value converted to string
decimaldecimalA decimal (double or float) value with max constraints as defined in Decimal Types
booleanboolA binary value in string format. true or false (lowercase)
datetimedatetimeA date time represented in ISO8601 Date time format
timetimeA time represented in HH:mm:ss format

5.5.2 Examples

{
	"type": "literal",
	"subType": "int",
	"value": "123"
}

5.6 Function Node

Some built in functions that allow you to tap into running state and other values.

Type: func ChildType: multiple or none Root: false TerminalNode: true (if no arguments) or false

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways func
functionstringYesThe function to be called
childrenarray of RuleNodeNoThe list of arguments to be passed to the function

5.6.1 Supported Function Nodes

Below are all the supported sub types for Function Nodes.

FunctionSub Type valueDescription
Current Timestampcurrent_timestampThe date time at the current instance in ISO 8601 format
Current Timecurrent_timeThe time at the current instance in HH:mm:ss format
Terminal Noterminal_numberThe terminal number of the current system
Sale transaction countsale_txn_countThe number of sale transactions since a timestamp.

Required arguments:
- timestamp in ISO8601 format
- boolean indicating if the count should be local terminal or false for store level
Mathematical AddaddAdds the output of all child nodes
Mathematical SubtractsubtractSubtracts the values with first value considered primary
Mathematical MultiplymultiplyMultiples the output of all child nodes
Mathematical DividedivideDivides the output of all child nodes, with first value considered primary
Mathematical ModulusmodModulus (division remainder) of dividing the first child with second child

5.6.2 Examples

5.6.2.1 Get current date time
{
	"type": "func",
	"function": "current_timestamp"
}
5.6.2.2 Trigger on every 200th transactions after 30th December 00:00 MDV time
{
	"type": "comparison",
	"subType": "eq",
	"children": [
		{
			"type": "func",
			"function": "mod",
			"children": [
				{
					"type": "func",
					"function": "sale_txn_count",
					"children": [
						{
							"type": "literal",
							"subType": "dateTime",
							"value": "2025-12-29T19:00:00.000+05:00"
						}
					]
				},
				{
					"type": "literal",
					"subType": "int",
					"value": "200"
				}
			]
		},
		{
			"type": "literal",
			"subType": "int",
			"value": "0"
		}
	]
}

5.7 Transformation Node

Transformation nodes change the output of a node (usually a PropertyNode) and modifies it so that we can have a workable version to be compared against.

Type: transform ChildType: single Root: false TerminalNode: false

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways transform
transformationsarray of TransformObjectYesThe list of transformations to apply. These will be applied in order it is provided
childRuleNodeYesThe first input to the transformation and the value tagged __input__. Typically a PropertyNode or LiteralNode.

5.7.1 Transformation Object

Each transformation object defines the behaviour of the transform to apply.

They can have these properties:

FieldData typeIs RequiredDescription
transformationstringYesOne of the supported transformations in Transformations
paramsarray of strings or ReferencesYesA list of secondary parameters to the transformation function. See Transformation Process for details on the process. All values given should be in string format which will be parsed by the transformation function
onErrorstringYesBehaviour on a fault. See the note below for possible values.
defaultstringYes*Only required if returnDefault or forwardDefault is selected on onError
saveLVarstringNoIf provided, the result will be saved to the local variable with key as this parameter value
codestringNoIf provided, this step will be uniquely identified as the given code
valueFromstringNoIf provided, the output of code step will be used as the input here. __input__ for the original input to the transformation process.
Possible On Error Actions

The following are the possible values for on error.

  • returnInput: returns input as is
  • forwardInput: forwards the input as is to the next function
  • returnDefault: returns the object in default
  • forwardDefault: forwards the object in default
  • stopExecution: this path and consequently this unique execution will be stopped. This does not mean the whole promotion is invalid, just the record being processed. For example: if we have 3 line items matching and the second one fails, 1 and 3 will still continue and can result in success.

5.7.2 Supported Transformations

Below are all the supported logic nodes for Function Nodes.

FunctionSub Type valueParamsDescription
Index ofindex_of- value: stringFinds the first index at which the provided value starts
Substringsubstring- start: int
- length: int
Extracts a substring starting at the given start index up to count
Regex Extactregex- pattern: string
- group: int
Extracts matched group (at the group index) from regex pattern
To Uppercaseto_uppercase-Converts a string to uppercase
To Lowercaseto_lowercase-Converts a string to lowercase
Trimtrim-Removes leading/trailing whitespace
LTrimltrim-Removes leading whitespace
RTrimrtrim-Removes trailing whitespace
String Replacereplace- search: string
- replace: string
- single: bool
Searches for the matching search string and replaces with replace string.
Matches once if single is true
Regex Replaceregex_replace- search: string
- replace: string
- single: bool
Searches for the matching search string and replaces with replace string as a regex
Matches once if single is true
Roundround- decimals: intRounds a number to the given decimals places
Absolute valueabs-Returns the absolute value of the number
Date Adddate_add- amount: int
- unit: string
Adds the given amount of units to a date.

Valid units are
- year for years
- mon for months
- day for days
- hour for hours
- min for minutes
- sec fro seconds
To Stringto_string-Converts input to its string format
To Intto_int-Parse the value as an integer
To Datetimeto_datetime-Parse the value as an ISO 8601 date
To Boolto_bool-Parse the value as a boolean
To Decimalto_decimal-Parse the value as a decimal
Extract Key Valueextract_kv- delimiter: string (default ::)
- separator: string (default ,)
- key: string
Parses a delimited (using delimiter) key-value concatenated string (using seperator) and extracts value for specified key
Split and Indexsplit_index- delimiter: string
- index: int
Splits string by delimiter and returns element at index
Date Formatdate_format- format: stringConverts the input to the format. Uses yyyy-MM-ddTHH:mm:ss.fffz format
Floorfloor-Rounds down to nearest integer
Ceilingceil-Rounds up to nearest integer
Modulomodulo- divisor: decimalReturns remainder of division with divisor
Containscontains- substring: stringReturns "true" if string contains substring, "false" otherwise. For case sensitivity use given to_uppercase or to_lowercase before this transformation
Starts withstarts_with- substring: stringReturns "true" if string starts with substring, "false" otherwise. For case sensitivity use given to_uppercase or to_lowercase before this transformation
Ends withends_with- substring: stringReturns "true" if string ends with substring, "false" otherwise. For case sensitivity use given to_uppercase or to_lowercase before this transformation
Is Nullis_null-Asserts if the input is null

5.7.3 Local Variables

If any of the transformations provided to this node has saveLVar set, the value will be saved as a local variable specific to this transformation node. Meaning all subsequent transformations can access this value. They access this by providing lvar::<variable_name> to a parameter. The value will be attempted to be auto converted to the correct parameter type.

Example:

{
	"type": "comparison",
	"subType": "eq",
	"children": [
		{
			"type": "transform",
			"transformations": [
				{
					"transformation": "index_of",
					"params": ["fun"],
					"saveLVar": "index",
					"onError": "returnInput"
				},
				{
					"transformation": "substring",
					"params": ["lvar::index", "999"],
					"onError": "returnInput",
					"valueFrom": "__input__"
				}
			],
			"child": {
				"type": "literal",
				"subType": "string",
				"value": "transformations are fun"
			}
		},
		{
			"type": "literal",
			"subType": "string",
			"value": "fun"
		}
	]
}

will find the index of fun and take the remainder starting from there.

5.7.4 Transformation Process

The transformation is a powerful tool that follows the following process:


6. Effect Node

There is no singular structure for a effect node. Instead there are a variety of them that could each have its own child/children. Look at the individual nodes for details on what could act as a root node and what could have multiple children.

Regardless of the node, each will have a property called type that will uniquely identify the node.

Look at the header area for a quote like the following for the configuration:

Type: the value to expect in the type property ChildType: the type of child nodes it could have (single, multiple, none or both) Root: true or false indicating if this could act as the root of the effects object TerminalNode: true or false indicating if this is a terminator / leaf node

6.1 Effect Logic Node

Combine various child nodes with logical operations.

Type: logic ChildType: multiple Root: true TerminalNode: false

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways logic
subTypestringYesLogical operation. See below for all supported types
childrenarray of EffectNodeYesAt least one child to evaluate logical operations on

6.1.1 Supported Effect Logic Nodes

Below are all the supported logic nodes for EffectNodes. These are different to Rule logic nodes, try not to confuse them.

Logical OperationSub Type valueDescription
ANDandProvides all children as effects
ORorProvides any combination of the child, up to and including all
XORxorProvides one and only one child effect. The cashier / user selects one

6.2 Effect Discount Node

Provides a discount on items in the transaction.

Type: discount ChildType: none Root: true TerminalNode: true

It has the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways discount
subTypestringYesWhat to apply the discount to. Accepts:

- header: applies to the whole transaction
- lineItem: applies to one or more line items
resourcestring or ReferenceNoThe resource to apply the discount to, only required for allMatching effect (set in applyMechanism)
conditionCodestringYesCondition code that is to be sent to backing system to represent the discount
valuedecimal or ReferenceYesThe amount of discount to give
isPercentagebooleanYesIndicates whether value is provided in percentage or monetary value
applyMechanismstringYes*Required if lineItem is selected in subType

Indicates the way to implement it. Possible values:
- triggerOnly: only apply it to the item that triggered it (should also be filtered by resource)
- allMatching: apply the discount to all matching items
applicationTypestringYesPossible values:
- single: only apply once per line
- stacking:<count>: if there are multiple triggers targeting this line, it will apply as many times as <count> in stacking
Trigger only validation

The triggerOnly only works when the rules are applied to lineItem, if there is no resource node for lineItem in the rules section, this should fail.

Stacking validation

The stacking in applicationType must always be accompanied by the <count> parameter. If it is not present, the effect is considered invalid and by extension the whole promotion will be disregarded during execution.

6.2.1 Discount Context

Application of discounts assumes that the engine is aware of all the resources that contributed to the success of the ruleset. As such the triggering lineItems are expected to be in the context.

  • If groupChildren is false then each matching item is assumed to be an independent context, applying discounts separately
  • If groupChildren is true then the items that contributed to the grouping is considered part of the same context. This makes it very unlikely one line could participate in multiple contexts, but it is not impossible

Effects use this implicit context information:

  • triggerOnly: Applies to items in the current context
  • allMatching: Expands beyond context to all items matching resource filter

6.2.2 Discount Application Logic

Discount application can be broadly split into two ways defined by the subType parameter:

6.2.2.1 Header level (subType header)

When subType is header:

  • Discount applies to entire transaction total
  • resource and applyMechanism are ignored
  • Based on applicationType:
    • If single: it is applied once
    • If stacking: it is applied once per context (up to <count>)

Example: VIP customers get 5% off entire transaction

{ 
	"type": "discount",
	"subType": "header",
	"conditionCode": "VIP_DISC",
	"value": 5.0,
	"isPercentage": true,
	"applicationType": "single"
}

Example: 50 MVR off per qualifying product group

{
	"type": "discount",
	"subType": "header",
	"conditionCode": "BULK_DISC",
	"value": 50.0,
	"isPercentage": false,
	"applicationType": "stacking:3"
}

If 3 product groups trigger independently, transaction gets 150 MVR off total

6.2.2.2 Line Item Level (subType lineItem)

When subType is lineItem, the application depends on applyMechanism and applicationType as the table below shows:

Apply MechanismApplication TypeBehavior
triggerOnlysingleApply discount once to each line in trigger context. Before applying, check if this promotion code already exists on the line.
triggerOnlystackingApply discount to each line in trigger context. Multiple contexts can stack discounts on same line up to <count>
allMatchingsingleApply discount once to all lines matching resource. Check if promotion already applied before adding.
allMatchingstackingApply discount N times to all lines matching resource, where N = number of contexts that triggered up to <count>

6.3 Free Item Node

Provides a free item as an effect.

Type: freeItem ChildType: none Root: true TerminalNode: true

It has the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways freeItem
articlestringYesThe article to be given as the free item. Only code_uom and ean are allowed as selectors.
conditionCodestringYesCondition code that is to be sent to backing system to represent the discount
quantitydecimal or ReferenceYesThe quantity of the free item to give
scalesWithRequirementsbooleanYesA value indicating if the free item should scale with triggers
sourceQuantitySelectorarray of SourceSelectorNodeYes*Only required if scalesWithRequirements is true, this defines the rows that should be added up to find quantity.
triggerQuantitydecimal or ReferenceYes*Only required if scalesWithRequirements is true, this defines the quantity to trigger against.

Scaling Behavior:

  • Each selector node in sourceQuantitySelector array is evaluated independently
  • All resulting quantities are summed together to get the effective quantity
  • triggerQuantity provides the baseline to match against
  • Free item quantity = quantity × (total_from_all_selectors / triggerQuantity)
  • Example: Buy 2 get 1 free. If customer buys 4, they get 2 free items.

Example with multiple selectors:

"sourceQuantitySelector": [
  {
    "type": "lineItem",
    "property": "quantity",
    "lookup": "code_uom::APPLE_JUICE|EA"
  },
  {
    "type": "lineItem",
    "property": "quantity",
    "lookup": "code_uom::ORANGE_JUICE|EA"
  }
]

If customer buys 3 apple juice + 2 orange juice = 5 total quantity With triggerQuantity: 2.0 and quantity: 1.0 Free items given = 1 × (5 / 2) = 2.5 → 2 free items (rounded down)

6.3.1 Source Selector Node

This is another family of nodes dedicated for selection of items to extract quantity from.

Rule nodes vs Source selector nodes

Due to the nature of both being basically logic builders, they have quite a bit of overlap. However these differ subtly to allow it to perform better as a selector than a rule.

6.3.1.1 Header Selector Node

Selects a property from header.

Type: header Root: true TerminalNode: true

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways header
propertystringYes or ReferenceThe property to get. It should point to a decimal or numeric value
6.3.1.2 Line Item Selector Node

Selects a property from line item.

Type: lineItem Root: true TerminalNode: true or false (in case of filter being given)

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways lineItem
propertystring or ReferenceYesThe property to get. It should point to a decimal or numeric value
lookupstring or ReferenceYesOne of the ways to lookup line items in Line Items, with a special additional type of all available for all
filterLogicSelectorNode or ComparisonSelectorNodeNoUse this to further refine the lookup if needed.
6.3.1.3 Customer Selector Node

Selects a property from customer.

Type: customer Root: true TerminalNode: true or false (in case of filter being given)

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways customer
propertystring or ReferenceYesThe property to get. It should point to a decimal or numeric value
lookupstring or ReferenceYesOne of the ways to lookup line items in Customers, with a special additional type of all available for all
filterLogicSelectorNode or ComparisonSelectorNodeNoUse this to further refine the lookup if needed.
6.3.1.4 Tender Selector Node

Selects a property from tender.

Type: tender Root: true TerminalNode: true or false (in case of filter being given)

It can have the following properties:

FieldData typeIs RequiredDescription
typestringYesAlways tender
propertystring or ReferenceYesThe property to get. It should point to a decimal or numeric value
lookupstring or ReferenceYesOne of the ways to lookup line items in Tenders, with a special additional type of all available for all
filterLogicSelectorNode or ComparisonSelectorNodeNoUse this to further refine the lookup if needed.
6.3.1.5 Logic Selector Node

This is mostly the same as Rule Logic Node, with the change being that the children could only be Selector Nodes.

6.3.1.6 Comparison Selector Node

This is exactly the same as Rule Comparison Node, with the change being that the children could only be Selector Nodes.

6.3.1.7 Literal Selector Node

This is exactly the same as Rule Literal Node.

6.3.1.8 Property Selector Node

This is exactly the same as Rule Property Node.

Resource

This node extracts a value from a resource context. However, since it will always be inside one of the 4 root selector nodes therefore, context is always present.

6.3.1.9 Function Selector Node

This is exactly the same as Rule Function Node, with the change being that the children could only be Selector Nodes.

6.3.1.10 Transformation Selector Node

This is exactly the same as Rule Transformation Node, with the change being that the children could only be Selector Nodes.


7. Data Array

There might arise certain cases where you want to apply the same logic across multiple combinations of data. Usually this happens when you want to run something like a buy one get one free promotion across multiple articles all giving different free items for but with the same validity, promotion name and the such. Data array is here to help you with that. Each item in the data array is a unique application of the same rules.

7.1 Ref format

Most of the resource / value specific fields in both RuleNodes and EffectNodes support giving the value as ref::<fieldName> format. This indicates that the value should be taken from the data array with column name <fieldName>.

7.2 Structure

There is no defined structure for the data array object. Just the requirement that any ref:: field name should be present in every item within the array. If the fields ref::sourceItem and ref::targetItem are used in the promotion then the structure of each item should be:

{
	"sourceItem": "...",
	"targetItem": "..."
}

such that the object in root is:

{
	...
	"data": [
		{
			"sourceItem": "...",
			"targetItem": "..."
		},
		{
			"sourceItem": "...",
			"targetItem": "..."
		}
	]
	...
}

7.3 Error Handling

On creation side, validation should fail and it should not be possible to save without this field in the data array object. This is to prevent downstream from handling bad data.

However, downstream should also guard against this and log a warning about it and ignore the subbranch that has the problematic issue. If its part of the rules section then promotion should be discarded entirely as we may be providing benefits to transactions that does not meet the criteria.


8. Resources

The agreed upon resources that the specification will act upon are the following.

8.1 Resource Parameter

A subtree within the rules can have a ResourceNode with a resource parameter through which you can set the resource. The resource parameter needs a value to identify which resources match the pattern. For cases like line items, multiple rows could match it and will be evaluated one by one, separately.

The resource parameter will be in the format: <lookup_prefix>::<lookup_params> where the lookup params will be combined using pipe |. So for a 3 parameter lookup it will be <lookup_prefix>::<lookup_param1>|<lookup_param2>|<lookup_param3>.

8.1.1 Escape sequences

If any of the parameters have a pipe within its text, it should be escaped by adding a backslash \. If you need a backslash in the text it should likewise be escaped with a backslash. In the unlikely event that a backslash followed by a pipe is needed, the following is how it should look like: \\\|.

  • \| results in |
  • \\ results in \
  • \\| results in \ with pipe splitting the params
  • \\\| results in \|

8.1.2 Case sensitivity

When a parameter is evaluated, it should be checked ordinal with case ignored. So aBcD, ABCD, abcd, ... all should be considered the same.

8.2 Resource List

Below are the list of all resources available to a promotion.

8.2.1 Header

ResourceType: header

8.2.1.1 Resource lookups

There is only one header per transaction. So no matter what you give in the resource parameter, it will match.

8.2.1.2 Structure

Header is assumed to have the following fields:

FieldData typeNullableDescription
storeCodestringNoThe code of the store
sequenceNumberstringNoThe sequence number expected for this transaction
businessDayISO 8601NoCurrent Business day
beginTimeStampISO 8601NoDate and time transaction was started on
loggedInEmployeeIdstringYesEmployeeId of the current session. Could be null in e-commerce or self-checkout
loggedInEmployeeNamestringYesEmployee name of the current session. Could be null in e-commerce or self-checkout
taxTotaldecimalNoThe total tax of the current transaction at this time
discountTotaldecimalNoThe total amount of discount given to the current transaction
subTotaldecimalNonetTotal - taxTotal
netTotaldecimalNoFinal amount the customer pays

8.2.2 Line Items

ResourceType: lineItem

8.2.2.1 Resource lookups

The following are the supported ways to lookup this resource.

8.2.2.1.1 Code with Uom

The article is matched with article code and uom.

Format: code_uom::<code>|<uom> Params:

  • code: The article code. Should be an exact match
  • uom: The unit of measure. Should be an exact match
8.2.2.1.2 Ean

The article is matched with the ean (barcode)

Format: ean::<ean> Params:

  • ean: The article's barcode. Should be an exact match
8.2.2.1.3 Brand

The article is matched with the brand.

Format: brand::<brand> Params:

  • brand: The article's brand. Should be evaluated in lowercase contains (%...% in sql)
8.2.2.1.4 Merchandising Category

The article is matched with the article's merchandising category.

Format: mc::<mc> Params:

  • mc: The article's merchandising category. Should be evaluated in lowercase contains (%...% in sql)
8.2.2.2 Structure

Line items are expected to have the following fields.

FieldData typeNullableDescription
codestringNoThe code of the article
namestringNoThe name of the article
descriptionstringYesThe description of the article
brandstringYesThe brand of the article
merchandisingCategorystringYesThe merchandising category of the article
quantitydecimalNoThe current quantity of the line
basePricedecimalNoThe base price of the article before any discounts
baseUomstringNoThe base unit of measure
uomstringNoThe unit of measure currently selected
numeratorintNoThe numerator of the conversion
denominatorintNoThe denominator of the conversion
currentPricedecimalNoThe current price per unit with discounts applied
discountPercentagedecimalNoThe currently applied discount percentage relative to basePrice
discountAmountdecimalNoThe currently applied discount amount in real value
isDiscountPercentbooleanNoIf the currently applied discount was applied as percentage or as absolute monetary value
isBatchItembooleanNoIf the currently selected item is a batch item
batchstringYesThe currently selected batch. If the item is a batch item this would have a value.
batchExpiryISO 8601YesThe expiry date of the selected batch. If the item is a batch item this would have a value.
isWarrantyApplicablebooleanNoIf the selected item is a warranty applicable article.
subTotaldecimalNoThe subtotal of the line. Subtotal here is defined as item's discounted price without any taxes multiplied by quantity
taxTotaldecimalNoThe total amount of tax applied to this line
discountTotaldecimalNoThe total amount of discount applied to this line
lineTotaldecimalNoThe final payment total of the discount applied to this line
Grouping

If the resource is grouped the following fields will have these modifications:

  • All decimal values will be sum
  • String and boolean values will be expected to have same values (grouping occurs at code + uom + currentPrice level), so any and all values will take the first parameter
  • batch and batchExpiry will have the fastest expiring batch and batchExpiry

8.2.3 Customers

ResourceType: customer

8.2.3.1 Resource lookups

The following are the ways you can lookup a customer

8.2.3.1.1 Customer Code

The customer is matched with customer code.

Format: code::<code> Params:

  • code: The customer's code. Should be an exact match
Nullable

This field could be null. So its best not to use unless needed.

8.2.3.1.2 Customer Type

The customer is matched with customer code.

Format: type::<type> Params:

  • type: The customer's Type Code. Should be an exact match.
8.2.3.1.3 Customer Identifier Type and Number

The customer is matched with customer identifier type and number.

Format: id::<id_type>|<id_number> Params:

  • id_type: The customer's identifier type. Should be an exact match
  • id_number: The customer's identifier number. Should be an exact match
8.2.3.1.4 Participation Group

The customer is matched with participation group. If the customer is in the group, it should be matched.

Format: group::<group_code>|<participation_value> Params:

  • group_code: The group to be matched. Should be an exact match
  • participation_value: The participation value that should be matched. Could have the special value * that matches all participation values.
8.2.3.1.5 Present

Evaluates that the customer is present. Basically matches any customer. This is most commonly used when checking for other parameters when selecting customers.

Format: present Params: NA

8.2.3.2 Structure

Customers are expected to have the following fields.

FieldData typeNullableDescription
codestringYesThe code of the customer
typeCodestringNoThe type code of the customer
typeDescriptionstringNoThe type name of the customer
idTypestringNoThe code of the identifier
idNamestringNoThe name of the identifier
idNumberstringNoThe identifier number of the customer
namestringNoThe name of the customer
name2stringYesThe secondary name of the customer
dateOfBirthISO 8601YesThe date of birth of the customer. Could also double as founding date for companies and institutes
genderstringYesFor individual customers, their gender
addressLine1stringYesThe first line of the address
addressLine2stringYesThe second line of the address
addressLine3stringYesThe third line of the address
citystringYesThe city name of the address
statestringYesThe state name of the address
countrystringYesThe country name of the address
postalCodestringYesThe postal code of the address
emailstringYesThe email of the customer
telephonestringNoThe contact number of the customer
tinstringYesThe tax identification number for the customer
customerGroupsstringYesThe customer groups they are participating in the format: <group1_code>::<group1_participation_value>,<group2_code>::<group2_participation_value>,...

The max length you can expect here is 1000

8.2.4 Tenders

ResourceType: tender

8.2.4.1 Resource lookups

The following are the supported ways to lookup this resource.

8.2.4.1.1 Tender Number

The tender is matched with tender number

Format: number::<number> Params:

  • number: The tender number. Should be an exact match
8.2.4.1.2 Tender Code

The tender is matched with tender postback code

Format: code::<code> Params:

  • code: The tender code. Should be an exact match
8.2.4.1.3 Tender Group Code

The tender is matched with tender group code

Format: group::<group_code> Params:

  • group_code: The tender's group code. Should be an exact match
8.2.4.2 Structure

Tenders are expected to have the following fields.

FieldData typeNullableDescription
groupCodestringNoThe group code of the tender line
groupDescstringNoThe group name of the tender line
tenderCodestringNoThe postback code of the tender
tenderNumberstringNoThe number of the tender (unique identifier of tender)
tenderDescstringNoThe description of the tender
tenderLongDescstringYesThe long description of the tender
currencydecimalNoThe currency of this tender line
exchangeRatedecimalNoThe exchange rate of the tender line. Its expected that multiplying with this value will give the home currency amount
tenderedAmountdecimalNoTendered amount in currency
tenderedHomeAmountdecimalNoTendered amount in home currency
smallestDenominationdecimalNoThe smallest supported value of the tender

9. Validation Rules

9.1 Create time validation

The following validations MUST be performed when a promotion is created or updated.

9.1.1 Root Structure Validation

  • code must be unique across all promotions
  • validFrom must be before validTo
  • priority must be non-negative
  • If images is not set to null, then one child field of the object must be not null

9.1.2 Rules Structure Validation

  • All node type values must be from the supported list
  • Logic nodes must have at least 1 child
  • Property nodes must be a descendant of Resource Nodes
  • Comparison nodes must have exactly 2 or 3 children (depending on operator)
  • Resource nodes cannot be nested
  • Function nodes must have correct parameter count for their function type
  • Maximum nesting depth cannot exceed limits defined in 2.5 Collection Constraints

9.1.3 Transformation Validation

  • All transformation names must be from the supported list in 5.7.2 Supported Transformations
  • Parameter count must match transformation requirements
  • valueFrom references must point to a valid code value that comes previously in the list or __input__
  • Local variable references (lvar::) cannot reference variable not yet saved
  • default must be provided when using returnDefault or forwardDefault
  • Cannot create circular dependencies in valueFrom references

9.1.4 Data Array Validation

  • All ref:: references in rules and effects must have corresponding fields in every data array object
  • Data array cannot exceed 10,000 items
  • Each data array object must have consistent field names

9.1.5 Resource Validation

  • Resource identifiers must follow format specified in 8.1 Resource Parameter
  • Escape sequences must be properly formed
  • Referenced properties must exist for the resource type

9.1.6 Effect Free Item Node Validation

Free Item Structure:

  • article must use only code_uom:: or ean:: lookup formats (not brand:: or mc::)
  • If scalesWithRequirements is true:
    • sourceQuantitySelector is required and must be a non-empty array
    • sourceQuantitySelector array must contain at least 1 selector node
    • sourceQuantitySelector array cannot exceed 50 selector nodes
    • triggerQuantity is required
    • triggerQuantity must be a positive number greater than 0
  • If scalesWithRequirements is false:
    • sourceQuantitySelector must not be present
    • triggerQuantity must not be present

Source Selector Node Validation:

  • All selector node type values must be from supported list: header, lineItem, customer, tender
  • property field must reference a numeric or decimal property from the corresponding resource type
  • lookup field must follow resource lookup format specified in Section 8 for that resource type
    • Special case: lookup: "all" is valid for lineItem, customer, and tender types
  • For selector nodes with filter:
    • Filter must be a valid Logic Selector Node or Comparison Selector Node
    • Filter children can only be other Selector Nodes (not Rule Nodes)
    • Property Selector Nodes in filters must reference properties from the parent selector's resource type
  • Logic Selector Nodes must have at least 1 child
  • Comparison Selector Nodes must have exactly 2 or 3 children (depending on comparison operator)
  • Function Selector Nodes must have correct parameter count for their function type
  • Transformation Selector Nodes follow same validation rules as Rule Transformation Nodes (Section 9.1.3)
  • Maximum nesting depth for selectors with filters cannot exceed 10 levels

9.2 Runtime Validation

The following conditions are checked during promotion execution:

9.2.1 Type Conversion Failures

  • String to number conversions may fail
    • For transformations they are handled by onError
    • For others they fail the unique execution
  • Date parsing may fail
    • For transformations they are handled by onError
    • For others they fail the unique execution
  • Boolean parsing may fail
    • For transformations they are handled by onError
    • For others they fail the unique execution

9.2.2 Transformation runtime errors

  • Regex patterns might be invalid (handled by onError)
  • Array index out of bounds in substring/split operations (handled by onError)
  • Null or empty input values (handled by onError)
  • Division by zero in modulo operations (handled by onError)

9.2.3 Resource Resolution Failures

  • Resource lookups may return no matches (promotion skipped for this context)
  • Property extraction may return null values
    • If this is handled (eg: transformation is_null) then it continues
    • Otherwise the current context fails
  • ref:: references may not resolve from data array (promotion execution stops with error)
    • This is fatal to the context

9.2.4Effect Discount Node Validation

  • applicationType must match one of these formats:
    • "single"
    • "stacking:<count>" where <count> is a positive integer greater than 0
  • The stacking keyword without <count> (e.g., just "stacking") is invalid
  • <count> must be between 1 and 100 (inclusive)
  • If subType is lineItem:
    • applyMechanism is required
    • If applyMechanism is triggerOnly, rules must contain at least one resource node with subType: "article"
    • If applyMechanism is allMatching, the resource field must be provided in the effect

10. Error handling

This has been mentioned within the spec multiple times, this section just brings everything together in a single location for convenience.

10.1 During rules validation

When rules are loaded and the document is validated for evaluation, if an error occurs:

  • The entire promotion is considered invalid
  • No effects are applied
  • Other promotions continue to be accepted

10.2 During rules evaluation

After validation, when its evaluated during a transaction:

  • The current unique execution is failed
  • No effects applied even if the non-failed part matches correctly
  • Other contexts continue to be evaluated

10.3 When reading data from Data array

When reading from the data array, if there is an error due to any reason (data conversion, not found, etc):

  • The current unique execution is failed
  • No effects applied even if the non-failed part matches correctly
  • Other contexts continue to be evaluated

10.4 During Effects application

If during the effects application an error occurs:

  • Notify the person using the system
    • Customer case may require system specific conditions
  • This unique execution fails
  • Other contexts continue to be evaluated

10.5 During Transformation Node

Follow the guidelines provided by the transformation node on the error behaviours.


Appendix

Appendix contains examples for simple and complex promotions for reference.

Appendix 1

Scenario: We want to provide a 10% discount to all items branded with the company "CocaCola".

Specification:

{
	"code": "cocacola10dis2025",
	"name": "CocaCola 10% Discount",
	"description": "For every CocaCola braded item get 10% off",
	"customerDescription": "For every CocaCola braded item get 10% off",
	"images": null,
	"isEnabled": true,
	"validFrom": "2025-12-01T00:00:00.000Z",
	"validTo": "2025-12-31T23:59:59.999Z",
	"lastUpdated": "2025-11-14T17:53:12.129Z",
	"priority": 250,
	"rules": {
		"type": "resource",
		"subType": "lineItem",
		"resource": "brand::cocacola",
		"groupChildren": false,
		"child": {
			"type": "literal",
			"subType": "bool",
			"value": "true"
		}
	},
	"effects": {
		"type": "discount",
		"subType": "lineItem",
		"conditionCode": "DISC",
		"value": 10.0,
		"isPercentage": true,
		"applyMechanism": "triggerOnly",
		"applicationType": "single"
	}
}

How it works:

The rules matches all line items which has the brand cocacola case-insensitive, the child literal gives a true value basically matching all. This is required since the resource node by itself just passes what ever its child gives.

The effect side applies a discount of 10% discount to all matching items.

Appendix 2

Scenario: We want to provide a free apple ean: 11223344 when buying 2 apple juice packets code_uom: 121212, EA, auto scaling based on the amount of soap

Assumptions:

  • Apple juice is batch managed and as such can have multiple lines per transaction

Specification:

{
	"code": "bAPPLEPACgAPPLE21",
	"name": "Buy 2 apple juice packets and get 1 fresh apple free",
	"description": "Buy 2 apple juice packets and get 1 fresh apple free",
	"customerDescription": "Buy 2 apple juice packets and get 1 fresh apple free",
	"images": null,
	"isEnabled": true,
	"validFrom": "2025-12-01T00:00:00.000Z",
	"validTo": "2025-12-31T23:59:59.999Z",
	"lastUpdated": "2025-11-14T17:53:12.129Z",
	"priority": 260,
	"rules": {
		"type": "resource",
		"subType": "lineItem",
		"resource": "code_uom::121212|EA",
		"groupChildren": true,
		"child": {
			"type": "comparison",
			"subType": "gte",
			"children": [
				{
					"type": "property",
					"propertyName": "quantity"
				},
				{
					"type": "literal",
					"subType": "decimal",
					"value": "2.0"
				}
			]
		}
	},
	"effects": {
		"type": "freeItem",
		"article": "ean::11223344",
		"conditionCode": "FREE",
		"quantity": 1.0,
		"scalesWithRequirements": true,
		"sourceQuantitySelector": [
			{
				"type": "lineItem",
				"property": "quantity",
				"lookup": "code_uom::121212|EA"
			}
		],
		"triggerQuantity": 2.0
	}
}

How it works

The rules section checks for all line items which match the code and unit of measure provided. This could be multiple in case of multiple batches. Since the resource node is set to groupChildren, it would group all the lines together into one execution context. The lines are then further refined by comparing the quantity of the lines with the literal value 2.0.

On the effect side a free item with barcode 11223344 is provided. The settings work as follows.

  • sourceQuantitySelector matches all items with given code and unit of measure as rule side summing up all the quantity.
  • Since it is set to scaleWithRequirements, it will give 1 free item (set in quantity) for every 2 from the sourceQuantitySelector
    • 2 gives 1 free
    • 3 still gives 1 free
    • 4 gives 2 free
Alternatives

The following is also an acceptible way to do this and is perfectly valid! The above example establishes the resource context first and then does the comparison, but making the comparison root does not really make a difference. One is arguably more human readable but no impact on processing whatsoever.

{
	"type": "comparison",
	"subType": "gte",
	"children": [
		{
			"type": "resource",
			"subType": "lineItem",
			"resource": "code_uom::121212|EA",
			"groupChildren": true,
			"child": {
				"type": "property",
				"propertyName": "quantity"
			}
		},
		{
			"type": "literal",
			"subType": "decimal",
			"value": "2.0"
		}
	]
}

Appendix 3

Scenario: We want to provide the same ruleset (buy x get y) with same validity and name to a bunch of articles.

Specification:

{
	"code": "FRUITFESTIVAL2025",
	"name": "Fruit festival 2025",
	"description": "For each purchase of a 2 fruit juice packets, get a fresh fruit free",
	"customerDescription": "For each purchase of a 2 fruit juice packets, get a fresh fruit free",
	"images": null,
	"isEnabled": true,
	"validFrom": "2025-12-01T00:00:00.000Z",
	"validTo": "2025-12-31T23:59:59.999Z",
	"lastUpdated": "2025-11-14T17:53:12.129Z",
	"priority": 270,
	"rules": {
		"type": "resource",
		"subType": "lineItem",
		"resource": "ref::source",
		"groupChildren": true,
		"child": {
			"type": "comparison",
			"subType": "gte",
			"children": [
				{
					"type": "property",
					"propertyName": "quantity"
				},
				{
					"type": "literal",
					"subType": "decimal",
					"value": "2.0"
				}
			]
		}
	},
	"effects": {
		"type": "freeItem",
		"article": "ref::free",
		"conditionCode": "FREE",
		"quantity": 1.0,
		"scalesWithRequirements": true,
		"sourceQuantitySelector": [
			{
				"type": "lineItem",
				"property": "quantity",
				"lookup": "ref::source"
			}
		],
		"triggerQuantity": 2.0
	},
	"data": [
		{
			"source": "code_uom::112233|EA",
			"free": "ean::112211756"
		},
		{
			"source": "code_uom::112234|EA",
			"free": "ean::112211759"
		},
		{
			"source": "ean::112211721",
			"free": "code_uom::112235|EA"
		},
		{
			"source": "code_uom::112237|EA",
			"free": "ean::112211251"
		}
	]
}

How it works

On the rule side, for every data set (in this case 4) the ruleset is evaluated once.

On each evaluation, all line items which match the lookup value provided in source. This could be multiple in case of multiple batches. Since the resource node is set to groupChildren, it would group all the lines together into one execution context. The lines are then further refined by comparing the quantity of the lines with the literal value 2.0.

On the effect side a free item with given free lookup value is provided. The settings work as follows.

  • sourceQuantitySelector matches all items with given code and unit of measure as rule side summing up all the quantity.
  • Since it is set to scaleWithRequirements, it will give 1 free item (set in quantity) for every 2 from the sourceQuantitySelector
    • 2 gives 1 free
    • 3 still gives 1 free
    • 4 gives 2 free

As you can see on the data, not all data objects need to have the same selector type. Since they are treated as individual instances, they are not at all linked in anything other than the ruleset and effect definitions.

Appendix 4

Scenario: Progressive discounts on beverages based on spending:

  • Spend 500-999.999 MVR → 10% off beverages
  • Spend 1000-1999.999 MVR → 15% off beverages
  • Spend 2000+ MVR → 20% off beverages
{
	"code": "TIEREDSPEND2025",
	"name": "Tiered spending discount on beverages",
	"description": "Progressive discounts on beverages: 500+ get 10%, 1000+ get 15%, 2000+ get 20%",
	"customerDescription": "Spend more, save more on beverages! Up to 20% off",
	"images": null,
	"isEnabled": true,
	"validFrom": "2025-12-01T00:00:00.000Z",
	"validTo": "2025-12-31T23:59:59.999Z",
	"lastUpdated": "2025-11-14T17:53:12.129Z",
	"priority": 100,
	"rules": {
		"type": "resource",
		"subType": "header",
		"resource": "present",
		"groupChildren": true,
		"child": {
			"type": "logic",
			"subType": "and",
			"children": [
				{
					"type": "comparison",
					"subType": "gte",
					"children": [
						{
							"type": "property",
							"propertyName": "netTotal"
						},
						{
							"type": "literal",
							"subType": "decimal",
							"value": "ref::startAmount"
						}
					]
				},
				{
					"type": "logic",
					"subType": "or",
					"children": [
						{
							"type": "comparison",
							"subType": "eq",
							"children": [
								{
									"type": "transform",
									"transformations": [
										{
											"transformation": "is_null",
											"params": [],
											"onError": "returnInput"
										}
									],
									"child": {
										"type": "literal",
										"subType": "string",
										"value": "ref::endAmount"
									}
								},
								{
									"type": "literal",
									"subType": "bool",
									"value": "true"
								}
							]
						},
						{
							"type": "comparison",
							"subType": "lt",
							"children": [
								{
									"type": "property",
									"propertyName": "netTotal"
								},
								{
									"type": "literal",
									"subType": "decimal",
									"value": "ref::endAmount"
								}
							]
						}
					]
				}
			]
		}
	},
	"effects": {
		"type": "discount",
		"subType": "lineItem",
		"resource": "mc::beverages",
		"conditionCode": "ref::code",
		"value": "ref::discount",
		"isPercentage": true,
		"applyMechanism": "allMatching",
		"applicationType": "single"
	},
	"data": [
		{
			"startAmount": "2000.0",
			"endAmount": null,
			"discount": "20.0",
			"code": "BEV20"
		},
		{
			"startAmount": "1000.0",
			"endAmount": "2000.0",
			"discount": "15.0",
			"code": "BEV15"
		},
		{
			"startAmount": "500.0",
			"endAmount": "1000.0",
			"discount": "10.0",
			"code": "BEV10"
		}
	]
}

How it works

On the rule side, for every data set (in this case 3) the ruleset is evaluated once.

On each evaluation, the header is matched with the ruleset.

  • netTotal is matched to be greater than or equal to the startAmount
  • Since its an and the following is only matched if the previous statement succeeds
  • Then the second condition is checked as follows:
    • The data item endAmount is transformed via is_null which will produce a value true or false
    • if true (end value is null) then the second condition auto succeeds due to the or logic node
    • if false the netTotal is rematched to netTotal this time checking for less than only

On the effect side a discount of discount percent is given to all items with the merchandising category beverages (matched case-insensitive). Since applyMechanism is all matching, it gives it to all matching lines and each line will only receive it once since the applicationType is single.

Implementation Gotcha

Since this is a specification document, it does not give you implementation details but if the promotion is structured like this, then there is a possibility that BEV20 drops the net total down enough for it to not be viable anymore there by switching to BEV15 which inturn may bump it back above to BEV20 range. This could cause inconsistencies and is advised to guard against.

The problem is not the flip-flopping but that it can actually flip in the first place. That should not happen in a production system.

One simple fix could be that while evaluation, any discounts given by this and any other items behind it in promotion priority are removed (assumed not given), during evaluation.

Appendix 5

Scenario VIP customers (defined as GOLD) customers get 20% off on electronics

Assumptions

  • A customer group with code LOYALTY exists with participation value providing the tier of participation.
{
	"code": "VIP_ELEC_2025",
	"name": "VIP Electronics Priviledge",
	"description": "VIP customers ",
	"customerDescription": "Spend more, save more on beverages! Up to 20% off",
	"images": null,
	"isEnabled": true,
	"validFrom": "2025-12-01T00:00:00.000Z",
	"validTo": "2025-12-31T23:59:59.999Z",
	"lastUpdated": "2025-11-14T17:53:12.129Z",
	"priority": 100,
	"rules": {
		"type": "logic",
		"subType": "and",
		"children": [
			{
				"type": "resource",
				"subType": "customer",
				"resource": "present",
				"groupChildren": true,
				"child": {
					"type": "comparison",
					"subType": "eq",
					"children": [
						{
							"type": "transform",
							"transformations": [
								{
									"transformation": "extract_kv",
									"params": ["::", ",", "LOYALTY"],
									"onError": "returnDefault",
									"default": "NONE"
								}
							],
							"child": {
								"type": "property",
								"propertyName": "customerGroups"
							}
						},
						{
							"type": "literal",
							"subType": "string",
							"value": "GOLD"
						}
					]
				}
			},
			{
				"type": "resource",
				"subType": "lineItem",
				"resource": "mc::electronics",
				"groupChildren": false,
				"child": {
					"type": "literal",
					"subType": "bool",
					"value": "true"
				}
			}
		]
	},
	"effects": {
		"type": "discount",
		"subType": "lineItem",
		"resource": "mc::electronics",
		"conditionCode": "VIPELEC",
		"value": 20.0,
		"isPercentage": true,
		"applyMechanism": "allMatching",
		"applicationType": "single"
	}
}

How it works

On the rule side the root is an and which checks the following:

  • There is a customer present and they have the group LOYALTY
    • If the group is found, the participation value is returned
    • If not found then NONE is returned
    • This value is then compared with GOLD
  • An item with merchandising category electronics exists

On the effect side it applies a discount of 20% to all the items with merchandising category electronics, once per item.