Recommendation Hint Catalog (POS v2)
Wire shape:
EvaluateResponseV2.recommendations[].params: many KeyValuePair.
The v2 evaluate response surfaces near-miss + bug-fix hints as structured
triples — { code, params, defaultMessage } — so POS systems can render
each hint in the cashier's locale without round-tripping English template
strings. POS teams ship one i18n bundle per locale keyed by code; the
engine is locale-agnostic.
This catalog is the versioned contract between engine and POS:
- Codes are permanent: once a code is shipped, it is never repurposed.
- New codes are additive and bump
EvaluateResponseV2.minorVersion. - A POS that does not recognise a code MUST fall back to
defaultMessage.
The 13 codes
code | Required params keys | English defaultMessage template |
|---|---|---|
MISSING_ARTICLE | articleNumber (or articleListId) | Add article {articleNumber} to qualify for this promotion. |
MISSING_QUANTITY_IN_GROUP | articleGroupId, missingQuantity, thresholdQuantity | Add {missingQuantity} more item(s) from group {articleGroupId} to reach the threshold of {thresholdQuantity}. |
BELOW_MIN_QUANTITY | articleNumber, currentQuantity, requiredQuantity | Add {requiredQuantity, plural, =1 {1 unit} other {# units}} of article {articleNumber} (currently {currentQuantity}). |
BELOW_RECEIPT_THRESHOLD | currentAmount, requiredAmount, currency | Spend {requiredAmount} {currency} (currently {currentAmount}) to qualify. |
CUSTOMER_GROUP_REQUIRED | customerGroup | This promotion is reserved for customer group {customerGroup}. |
CUSTOMER_REQUIRED | customerId | This promotion requires a customer to be identified at checkout. |
LOYALTY_TIER_REQUIRED | requiredTier, currentTier | Loyalty tier {requiredTier} required (current tier: {currentTier}). |
PAYMENT_MEANS_REQUIRED | requiredMeans | Pay with {requiredMeans} to qualify for this promotion. |
MANUFACTURER_REQUIRED | manufacturerId | Add an item from manufacturer {manufacturerId} to qualify. |
MANUAL_DISCOUNT_REQUIRED | (none — boolean) | A manual discount must be applied to qualify for this promotion. |
TIME_WINDOW_INVALID | validFrom, validUntil | This promotion is valid from {validFrom} to {validUntil}. |
CUSTOM_FIELD_REQUIRED | fieldKey, expectedValue | Field {fieldKey} must equal {expectedValue} to qualify. |
CHANNEL_REQUIRED | requiredChannel | This promotion is available on channel {requiredChannel}. |
The English templates use ICU MessageFormat-style placeholders. Locale bundles MAY use ICU directly or any other format library — the engine does not interpret them; it only ships them as fallback strings.
Wire encoding
On the wire, params is many KeyValuePair. Each entry is { key, value },
both strings. Numeric and boolean values arrive as their string
representation — POS coerces per the documented contract for each code.
Example wire payload:
{
"kind": "NEAR_MISS",
"code": "BELOW_RECEIPT_THRESHOLD",
"params": [
{ "key": "currentAmount", "value": "42.50" },
{ "key": "requiredAmount", "value": "50.00" },
{ "key": "currency", "value": "EUR" }
],
"defaultMessage": "Spend {requiredAmount} {currency} (currently {currentAmount}) to qualify."
}
POS-side rendering
function renderRecommendation(rec, i18n) {
// 1. Decode params back into a plain object.
const data = Object.fromEntries(
rec.params.map(p => [p.key, p.value])
);
// 2. Coerce per-code typed values. Per the contract for
// BELOW_RECEIPT_THRESHOLD, currentAmount and requiredAmount are decimals.
if (rec.code === 'BELOW_RECEIPT_THRESHOLD') {
data.currentAmount = parseFloat(data.currentAmount);
data.requiredAmount = parseFloat(data.requiredAmount);
}
// 3. Look up locale bundle, fall through to defaultMessage.
const tpl = i18n.t(`hint.${rec.code}`) || rec.defaultMessage;
return formatMessage(tpl, data);
}
Every POS adapter MUST implement the fallback to defaultMessage so an
unknown code (forward-compat) still surfaces a usable string.
Versioning rules
- Adding a new code is additive → bump
EvaluateResponseV2.minorVersion. - Removing a code is a breaking change → requires a major version bump AND a deprecation cycle (out of scope for the v2 release).
- Renaming a code is forbidden — codes are user-facing contract.
- Changing the required
paramskeys on an existing code is breaking — add a new code with the extended params instead. - The English
defaultMessagetemplate MAY be reworded for clarity in any release; POS bundles drive the user-visible copy.
See also
- POS Integration Service reference — the
/pos/v2/evaluateaction, request/response shapes, and whererecommendations[]appears inEvaluateResponseV2. - Evaluate / Confirm lifecycle — how recommendations are produced during a checkout evaluation.