Raytail Promotion Interchange Format (RAYPIF)
| Property | Value |
|---|---|
| Document Version | 1.0 |
| Revision Date | 31st December 2025 |
| Status | First Draft |
| Underlying Format | JSON |
| Author | Ihusaan 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
| Author | Date | Version | Change |
|---|---|---|---|
| Ihusaan Ahmed | 29th December 2025 | 1.0 | Initial 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:
| Feature | Value | Description |
|---|---|---|
| Precision | 12 | The 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 |
| Scale | 3 | The 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,999to999,999,999,999(when all 12 digits are in the integer part)
Validation Examples:
| Value | Valid? | Reason |
|---|---|---|
123.456 | ✓ | Within precision (6) and scale (3) |
999,999,999.999 | ✓ | Maximum valid value (12 precision, 3 scale) |
999,999,999.9999 | ✓ | Will be rounded to 999999999.000 (scale exceeds 3, and rounding will exceed precision) |
9,999,999,999,999 | ✗ | Exceeds precision of 12 |
-0.001 | ✓ | Minimum meaningful decimal value |
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.000uses scale of 3)
2.3 Integer types
All integer types must follow the following properties:
| Feature | Value |
|---|---|
| Precision | 10 |
| Range | -2,147,483,648 to 2,147,483,647 (32-bit signed) |
2.4 String types
The following applies to string types:
| Field Type | Max Length | Notes |
|---|---|---|
| code | 50 | Alphanumeric + hyphen recommended |
| name | 200 | UTF-8 supported |
| description | 2000 | |
| conditionCode | 20 | |
| resource identifiers | 500 | To accommodate complex lookups |
| all others | 3000 |
2.5 Collection Constraints
| Collection | Min Elements | Max Elements | Max Nesting Depth |
|---|---|---|---|
| data array | 0 | 10,000 | N/A |
| rules children | 1 | 100 | 15 levels |
| effects children | 1 | 50 | 10 levels |
| sourceQuantitySelector array | 1 | 50 | 10 levels |
3. Root / Top-Level Structure
Every promotion will be defined at the top level as a JSON object with the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
code | string | Yes | Unique Identifier for the promotion |
name | string | Yes | Human readable promotion name. Will also appear on promotional materials and receipts |
description | string | No | Detailed description of the promotion. Will appear to the staff members. |
customerDescription | string | No | Detailed description of the promotion for the customer, relevant for customer facing components like e-commerce and self-checkout |
images | ImageObject | No | An object specifying the various images involved with the promotion |
isEnabled | boolean | Yes | A quick flag to show the enabled status. This is independent of validity so to check for application check both this and validity fields. |
validFrom | ISO 8601 | Yes | A date indicating the promotion start date and time |
validTo | ISO 8601 | Yes | A date indicating the promotion end date and time |
lastUpdated | ISO 8601 | Yes | Timestamp of the last modification (can be used as an idempotency) |
priority | integer | Yes | Execution 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. |
rules | RuleNode | Yes | Tree structure defining the rules. See below for details |
effects | EffectNode | Yes | Tree structure defining the effects. See below for details |
data | DataArray | No | Array 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.
| Field | Data type | Is Required | Description |
|---|---|---|---|
thumbnailUrl | string | No | An 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 |
coverImageUrl | string | No | An image that will be used as a cover image for the promotion. Expects an image in the ratio 16:9, preferably with dimensions 1920x1080 |
marketingImages | string array | No | A 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 |
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
typeproperty ChildType: the type of child nodes it could have (single,multiple,noneorboth) Root:trueorfalseindicating if this could act as the root of the rules object TerminalNode:trueorfalseindicating if this is a terminator / leaf node
5.1 Rule Logic Node
Combine various child nodes with logical operations.
Type:
logicChildType:multipleRoot:trueTerminalNode:false
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always logic |
subType | string | Yes | Logical operation. See below for all supported types |
children | array of RuleNode | Yes | At 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 Operation | Sub Type value | Description |
|---|---|---|
| AND | and | Evaluates if all children results in true |
| OR | or | Evaluates if one of the children results in true |
| XOR | xor | Evaluates that one and only one child results in true |
| NAND | nand | Evaluates that one or more of the children results in false |
| NOR | nor | Evaluates that none of the children results in true |
| XNOR | xnor | Evaluates 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:
resourceChildType:singleRoot:trueTerminalNode:false
It has no logic by itself and will evaluate to whatever the child evaluates to.
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always resource |
subType | string | Yes | See Resources for a list of all supported types |
resource | string or Reference | Yes | The resource identifier. See Resources for details. |
groupChildren | boolean | Yes | Indicates if multiple resources match within its subtree, if they should be grouped or treated as individual items |
child | RuleNode | Yes | The continuation of the logic with this resource context |
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:
comparisonChildType:multipleRoot:trueTerminalNode:false
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always comparison |
subType | string | Yes | See below for a list of all supported comparisons |
children | array of RuleNode | Yes | Two to three nodes depending on the rule being evaluated |
5.3.1 Supported Comparison Nodes
Below are all the supported nodes for ComparisonNodes.
| Comparison Operation | Sub Type value | Description |
|---|---|---|
| >= | gte | Evaluates if the first node's value is greater or equal to second node |
| > | gt | Evaluates if the first node's value is greater than second node |
| == | eq | Evaluates if the first node's value is equal to second node |
| != | neq | Evaluates if the first node's value is not equal to second node |
| < | lt | Evaluates if the first node's value is less than second node |
| <= | lte | Evaluates if the first node's value is less than or equal to second node |
| < x < | lt_gt | Evaluates if the value in second node is less than the third but greater than first |
| <= x < | lte_gt | Evaluates if the value in second node is less than the third but greater than or equal to first |
| < x <= | lt_gte | Evaluates if the value in second node is less than or equal to the third but greater than first |
| <= x <= | lte_gte | Evaluates 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.
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:
propertyChildType:noneRoot:falseTerminalNode:true
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always property |
propertyName | string or Reference | Yes | The property of the resource to get or compare against |
convertEquivalent | boolean | No | false 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:
literalChildType:noneRoot:trueTerminalNode:true
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always literal |
subType | string | Yes | See below for a list of all supported literal types |
value | string or Reference | Yes | The value encoded as a string |
5.5.1 Supported Literal Nodes
Below are all the supported logic nodes for LiteralNodes.
| Data Type | Sub Type value | Description |
|---|---|---|
| string | string | A text value |
| integer | int | An integer value converted to string |
| decimal | decimal | A decimal (double or float) value with max constraints as defined in Decimal Types |
| boolean | bool | A binary value in string format. true or false (lowercase) |
| datetime | datetime | A date time represented in ISO8601 Date time format |
| time | time | A 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:
funcChildType:multipleornoneRoot:falseTerminalNode:true(if no arguments) orfalse
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always func |
function | string | Yes | The function to be called |
children | array of RuleNode | No | The 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.
| Function | Sub Type value | Description |
|---|---|---|
| Current Timestamp | current_timestamp | The date time at the current instance in ISO 8601 format |
| Current Time | current_time | The time at the current instance in HH:mm:ss format |
| Terminal No | terminal_number | The terminal number of the current system |
| Sale transaction count | sale_txn_count | The 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 Add | add | Adds the output of all child nodes |
| Mathematical Subtract | subtract | Subtracts the values with first value considered primary |
| Mathematical Multiply | multiply | Multiples the output of all child nodes |
| Mathematical Divide | divide | Divides the output of all child nodes, with first value considered primary |
| Mathematical Modulus | mod | Modulus (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:
transformChildType:singleRoot:falseTerminalNode:false
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always transform |
transformations | array of TransformObject | Yes | The list of transformations to apply. These will be applied in order it is provided |
child | RuleNode | Yes | The 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:
| Field | Data type | Is Required | Description |
|---|---|---|---|
transformation | string | Yes | One of the supported transformations in Transformations |
params | array of strings or References | Yes | A 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 |
onError | string | Yes | Behaviour on a fault. See the note below for possible values. |
default | string | Yes* | Only required if returnDefault or forwardDefault is selected on onError |
saveLVar | string | No | If provided, the result will be saved to the local variable with key as this parameter value |
code | string | No | If provided, this step will be uniquely identified as the given code |
valueFrom | string | No | If 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 isforwardInput: forwards the input as is to the next functionreturnDefault: returns the object in defaultforwardDefault: forwards the object in defaultstopExecution: 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.
| Function | Sub Type value | Params | Description |
|---|---|---|---|
| Index of | index_of | - value: string | Finds the first index at which the provided value starts |
| Substring | substring | - start: int- length: int | Extracts a substring starting at the given start index up to count |
| Regex Extact | regex | - pattern: string- group: int | Extracts matched group (at the group index) from regex pattern |
| To Uppercase | to_uppercase | - | Converts a string to uppercase |
| To Lowercase | to_lowercase | - | Converts a string to lowercase |
| Trim | trim | - | Removes leading/trailing whitespace |
| LTrim | ltrim | - | Removes leading whitespace |
| RTrim | rtrim | - | Removes trailing whitespace |
| String Replace | replace | - search: string- replace: string- single: bool | Searches for the matching search string and replaces with replace string.Matches once if single is true |
| Regex Replace | regex_replace | - search: string - replace: string - single: bool | Searches for the matching search string and replaces with replace string as a regexMatches once if single is true |
| Round | round | - decimals: int | Rounds a number to the given decimals places |
| Absolute value | abs | - | Returns the absolute value of the number |
| Date Add | date_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 String | to_string | - | Converts input to its string format |
| To Int | to_int | - | Parse the value as an integer |
| To Datetime | to_datetime | - | Parse the value as an ISO 8601 date |
| To Bool | to_bool | - | Parse the value as a boolean |
| To Decimal | to_decimal | - | Parse the value as a decimal |
| Extract Key Value | extract_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 Index | split_index | - delimiter: string - index: int | Splits string by delimiter and returns element at index |
| Date Format | date_format | - format: string | Converts the input to the format. Uses yyyy-MM-ddTHH:mm:ss.fffz format |
| Floor | floor | - | Rounds down to nearest integer |
| Ceiling | ceil | - | Rounds up to nearest integer |
| Modulo | modulo | - divisor: decimal | Returns remainder of division with divisor |
| Contains | contains | - substring: string | Returns "true" if string contains substring, "false" otherwise. For case sensitivity use given to_uppercase or to_lowercase before this transformation |
| Starts with | starts_with | - substring: string | Returns "true" if string starts with substring, "false" otherwise. For case sensitivity use given to_uppercase or to_lowercase before this transformation |
| Ends with | ends_with | - substring: string | Returns "true" if string ends with substring, "false" otherwise. For case sensitivity use given to_uppercase or to_lowercase before this transformation |
| Is Null | is_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
typeproperty ChildType: the type of child nodes it could have (single,multiple,noneorboth) Root:trueorfalseindicating if this could act as the root of the effects object TerminalNode:trueorfalseindicating if this is a terminator / leaf node
6.1 Effect Logic Node
Combine various child nodes with logical operations.
Type:
logicChildType:multipleRoot:trueTerminalNode:false
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always logic |
subType | string | Yes | Logical operation. See below for all supported types |
children | array of EffectNode | Yes | At 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 Operation | Sub Type value | Description |
|---|---|---|
| AND | and | Provides all children as effects |
| OR | or | Provides any combination of the child, up to and including all |
| XOR | xor | Provides 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:
discountChildType:noneRoot:trueTerminalNode:true
It has the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always discount |
subType | string | Yes | What to apply the discount to. Accepts: - header: applies to the whole transaction- lineItem: applies to one or more line items |
resource | string or Reference | No | The resource to apply the discount to, only required for allMatching effect (set in applyMechanism) |
conditionCode | string | Yes | Condition code that is to be sent to backing system to represent the discount |
value | decimal or Reference | Yes | The amount of discount to give |
isPercentage | boolean | Yes | Indicates whether value is provided in percentage or monetary value |
applyMechanism | string | Yes* | Required if lineItem is selected in subTypeIndicates 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 |
applicationType | string | Yes | Possible 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 |
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.
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
groupChildrenis false then each matching item is assumed to be an independent context, applying discounts separately - If
groupChildrenis 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 contextallMatching: Expands beyond context to all items matchingresourcefilter
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
resourceandapplyMechanismare ignored- Based on
applicationType:- If
single: it is applied once - If
stacking: it is applied once per context (up to<count>)
- If
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 Mechanism | Application Type | Behavior |
|---|---|---|
triggerOnly | single | Apply discount once to each line in trigger context. Before applying, check if this promotion code already exists on the line. |
triggerOnly | stacking | Apply discount to each line in trigger context. Multiple contexts can stack discounts on same line up to <count> |
allMatching | single | Apply discount once to all lines matching resource. Check if promotion already applied before adding. |
allMatching | stacking | Apply 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:
freeItemChildType:noneRoot:trueTerminalNode:true
It has the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always freeItem |
article | string | Yes | The article to be given as the free item. Only code_uom and ean are allowed as selectors. |
conditionCode | string | Yes | Condition code that is to be sent to backing system to represent the discount |
quantity | decimal or Reference | Yes | The quantity of the free item to give |
scalesWithRequirements | boolean | Yes | A value indicating if the free item should scale with triggers |
sourceQuantitySelector | array of SourceSelectorNode | Yes* | Only required if scalesWithRequirements is true, this defines the rows that should be added up to find quantity. |
triggerQuantity | decimal or Reference | Yes* | Only required if scalesWithRequirements is true, this defines the quantity to trigger against. |
Scaling Behavior:
- Each selector node in
sourceQuantitySelectorarray is evaluated independently - All resulting quantities are summed together to get the effective quantity
triggerQuantityprovides 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.
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:
headerRoot:trueTerminalNode:true
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always header |
property | string | Yes or Reference | The 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:
lineItemRoot:trueTerminalNode:trueorfalse(in case of filter being given)
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always lineItem |
property | string or Reference | Yes | The property to get. It should point to a decimal or numeric value |
lookup | string or Reference | Yes | One of the ways to lookup line items in Line Items, with a special additional type of all available for all |
filter | LogicSelectorNode or ComparisonSelectorNode | No | Use this to further refine the lookup if needed. |
6.3.1.3 Customer Selector Node
Selects a property from customer.
Type:
customerRoot:trueTerminalNode:trueorfalse(in case of filter being given)
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always customer |
property | string or Reference | Yes | The property to get. It should point to a decimal or numeric value |
lookup | string or Reference | Yes | One of the ways to lookup line items in Customers, with a special additional type of all available for all |
filter | LogicSelectorNode or ComparisonSelectorNode | No | Use this to further refine the lookup if needed. |
6.3.1.4 Tender Selector Node
Selects a property from tender.
Type:
tenderRoot:trueTerminalNode:trueorfalse(in case of filter being given)
It can have the following properties:
| Field | Data type | Is Required | Description |
|---|---|---|---|
type | string | Yes | Always tender |
property | string or Reference | Yes | The property to get. It should point to a decimal or numeric value |
lookup | string or Reference | Yes | One of the ways to lookup line items in Tenders, with a special additional type of all available for all |
filter | LogicSelectorNode or ComparisonSelectorNode | No | Use 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.
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:
| Field | Data type | Nullable | Description |
|---|---|---|---|
storeCode | string | No | The code of the store |
sequenceNumber | string | No | The sequence number expected for this transaction |
businessDay | ISO 8601 | No | Current Business day |
beginTimeStamp | ISO 8601 | No | Date and time transaction was started on |
loggedInEmployeeId | string | Yes | EmployeeId of the current session. Could be null in e-commerce or self-checkout |
loggedInEmployeeName | string | Yes | Employee name of the current session. Could be null in e-commerce or self-checkout |
taxTotal | decimal | No | The total tax of the current transaction at this time |
discountTotal | decimal | No | The total amount of discount given to the current transaction |
subTotal | decimal | No | netTotal - taxTotal |
netTotal | decimal | No | Final 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 matchuom: 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.
| Field | Data type | Nullable | Description |
|---|---|---|---|
code | string | No | The code of the article |
name | string | No | The name of the article |
description | string | Yes | The description of the article |
brand | string | Yes | The brand of the article |
merchandisingCategory | string | Yes | The merchandising category of the article |
quantity | decimal | No | The current quantity of the line |
basePrice | decimal | No | The base price of the article before any discounts |
baseUom | string | No | The base unit of measure |
uom | string | No | The unit of measure currently selected |
numerator | int | No | The numerator of the conversion |
denominator | int | No | The denominator of the conversion |
currentPrice | decimal | No | The current price per unit with discounts applied |
discountPercentage | decimal | No | The currently applied discount percentage relative to basePrice |
discountAmount | decimal | No | The currently applied discount amount in real value |
isDiscountPercent | boolean | No | If the currently applied discount was applied as percentage or as absolute monetary value |
isBatchItem | boolean | No | If the currently selected item is a batch item |
batch | string | Yes | The currently selected batch. If the item is a batch item this would have a value. |
batchExpiry | ISO 8601 | Yes | The expiry date of the selected batch. If the item is a batch item this would have a value. |
isWarrantyApplicable | boolean | No | If the selected item is a warranty applicable article. |
subTotal | decimal | No | The subtotal of the line. Subtotal here is defined as item's discounted price without any taxes multiplied by quantity |
taxTotal | decimal | No | The total amount of tax applied to this line |
discountTotal | decimal | No | The total amount of discount applied to this line |
lineTotal | decimal | No | The final payment total of the discount applied to this line |
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
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 matchid_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 matchparticipation_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.
| Field | Data type | Nullable | Description |
|---|---|---|---|
code | string | Yes | The code of the customer |
typeCode | string | No | The type code of the customer |
typeDescription | string | No | The type name of the customer |
idType | string | No | The code of the identifier |
idName | string | No | The name of the identifier |
idNumber | string | No | The identifier number of the customer |
name | string | No | The name of the customer |
name2 | string | Yes | The secondary name of the customer |
dateOfBirth | ISO 8601 | Yes | The date of birth of the customer. Could also double as founding date for companies and institutes |
gender | string | Yes | For individual customers, their gender |
addressLine1 | string | Yes | The first line of the address |
addressLine2 | string | Yes | The second line of the address |
addressLine3 | string | Yes | The third line of the address |
city | string | Yes | The city name of the address |
state | string | Yes | The state name of the address |
country | string | Yes | The country name of the address |
postalCode | string | Yes | The postal code of the address |
email | string | Yes | The email of the customer |
telephone | string | No | The contact number of the customer |
tin | string | Yes | The tax identification number for the customer |
customerGroups | string | Yes | The 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.
| Field | Data type | Nullable | Description |
|---|---|---|---|
groupCode | string | No | The group code of the tender line |
groupDesc | string | No | The group name of the tender line |
tenderCode | string | No | The postback code of the tender |
tenderNumber | string | No | The number of the tender (unique identifier of tender) |
tenderDesc | string | No | The description of the tender |
tenderLongDesc | string | Yes | The long description of the tender |
currency | decimal | No | The currency of this tender line |
exchangeRate | decimal | No | The exchange rate of the tender line. Its expected that multiplying with this value will give the home currency amount |
tenderedAmount | decimal | No | Tendered amount in currency |
tenderedHomeAmount | decimal | No | Tendered amount in home currency |
smallestDenomination | decimal | No | The 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
codemust be unique across all promotionsvalidFrommust be beforevalidToprioritymust be non-negative- If
imagesis not set to null, then one child field of the object must be not null
9.1.2 Rules Structure Validation
- All node
typevalues 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
valueFromreferences must point to a validcodevalue that comes previously in the list or__input__- Local variable references (
lvar::) cannot reference variable not yet saved defaultmust be provided when usingreturnDefaultorforwardDefault- Cannot create circular dependencies in
valueFromreferences
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:
articlemust use onlycode_uom::orean::lookup formats (notbrand::ormc::)- If
scalesWithRequirementsistrue:sourceQuantitySelectoris required and must be a non-empty arraysourceQuantitySelectorarray must contain at least 1 selector nodesourceQuantitySelectorarray cannot exceed 50 selector nodestriggerQuantityis requiredtriggerQuantitymust be a positive number greater than 0
- If
scalesWithRequirementsisfalse:sourceQuantitySelectormust not be presenttriggerQuantitymust not be present
Source Selector Node Validation:
- All selector node
typevalues must be from supported list:header,lineItem,customer,tender propertyfield must reference a numeric or decimal property from the corresponding resource typelookupfield must follow resource lookup format specified in Section 8 for that resource type- Special case:
lookup: "all"is valid forlineItem,customer, andtendertypes
- Special case:
- 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
- For transformations they are handled by
- Date parsing may fail
- For transformations they are handled by
onError - For others they fail the unique execution
- For transformations they are handled by
- Boolean parsing may fail
- For transformations they are handled by
onError - For others they fail the unique execution
- For transformations they are handled by
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
- If this is handled (eg: transformation
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
applicationTypemust match one of these formats:"single""stacking:<count>"where<count>is a positive integer greater than 0
- The
stackingkeyword without<count>(e.g., just"stacking") is invalid <count>must be between 1 and 100 (inclusive)- If
subTypeislineItem:applyMechanismis required- If
applyMechanismistriggerOnly, rules must contain at least oneresourcenode withsubType: "article" - If
applyMechanismisallMatching, theresourcefield 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.
sourceQuantitySelectormatches 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 inquantity) for every 2 from thesourceQuantitySelector- 2 gives 1 free
- 3 still gives 1 free
- 4 gives 2 free
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.
sourceQuantitySelectormatches 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 inquantity) for every 2 from thesourceQuantitySelector- 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.
netTotalis matched to be greater than or equal to thestartAmount- Since its an
andthe following is only matched if the previous statement succeeds - Then the second condition is checked as follows:
- The data item
endAmountis transformed viais_nullwhich will produce a valuetrueorfalse - if
true(end value is null) then the second condition auto succeeds due to theorlogic node - if
falsethenetTotalis rematched tonetTotalthis time checking for less than only
- The data item
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.
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
LOYALTYexists 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
NONEis returned - This value is then compared with
GOLD
- An item with merchandising category
electronicsexists
On the effect side it applies a discount of 20% to all the items with merchandising category electronics, once per item.