Cart Discount Calculator
commerce.discount_calculate
Overview
You send a cart and your discount rules, and we return the pre-tax totals, the per-line breakdown, every rule that applied with the amount it took, and every rule that did not apply with the reason it was passed over. Rules run in a resolved order — priority, then line-scope before cart-scope, then id — never in the order they happened to arrive, so shuffling the request cannot change the answer. No line and no cart total can fall below zero. Each rule's money is rounded once at the currency's own decimals, and a cart-wide amount is split across lines so that the line discounts add up to the cart discount exactly. Tax is deliberately not here: whether tax comes before or after a discount is a jurisdictional decision, and this skill will not make it for you silently.
Discount stacking is where pricing quietly goes wrong: two teams apply the same coupons in different orders, the line discounts do not add up to the cart discount because each was rounded on its own, and nobody can say why a customer's code did nothing. You get a fixed order, each rule's money rounded once and split so the parts sum to the whole, and every rule that was passed over reported with a reason.
Reliability
Fully deterministic in three specific senses, each of which is asserted rather than asserted-to. Order: rules run by priority, then LINE scope before CART scope, then id, so a permuted request returns an identical answer. Floors: no line total and no cart total can go below zero, and a rule that would overshoot is trimmed to what is left and reported with CLAMPED_TO_ZERO — which is a note on a rule that did take something, so a rule whose every matched line was already at zero is skipped ZERO_EFFECT instead. Arithmetic: every amount is decimal, never floating point; each rule's money is rounded once at the currency's decimals, half up; a cart-wide amount is split across lines by largest remainder with ties going to the earlier line, so the line discounts sum to the cart discount exactly; and the one number that is not rounded half up is options.maxTotalDiscountRatio's budget, which is truncated toward zero so the cap cannot be exceeded. A property this skill does not implement is refused, not ignored: an unknown top-level property is a broken request and comes back as the platform's TOOL_INPUT_INVALID, while one inside options, a rule, a line or appliesTo is this skill's own typed code. Failures, and what to do about each. CART_INVALID — trigger: cart.lines is empty, a line is missing id, unitPrice or quantity, a line repeats an id, a unit price is negative or finer than 12 decimals, a quantity is outside 1 to 1000000, a line carries an unknown property, or a line total exceeds 1E18; retryable: no, the same request always fails; next: fix the line we name by index. CART_CURRENCY_MIXED — trigger: a line declares a currency different from cart.currency; retryable: no; next: split the cart by currency, because we will not sum two currencies and we do not convert. CART_LINE_LIMIT_EXCEEDED — trigger: more than 1000 lines in one call; retryable: no; next: aggregate identical lines by quantity, or split the cart. Two ceilings bound a cart and the smaller one wins: 1000 lines, and the 1 MiB request. Plain lines are about 45 bytes each, so 1001 of them reach the line ceiling comfortably inside the byte one; lines carrying the full 32 tags of 64 characters are about 2 KB each, so a cart of those meets the request ceiling at fewer than 500 lines and is refused for size, by the platform, before this code can fire. DISCOUNT_RULE_INVALID — trigger: a rule is missing a required property, carries one its type does not use, has a percentage outside 0 to 1, a negative amount, a non-positive maxDiscountAmount, a bad scope, or options.maxTotalDiscountRatio outside 0 to 1; also an unknown property in options, or an options.stacking value that is neither SEQUENTIAL nor ADDITIVE; retryable: no; next: fix the rule we name by index and remove properties that do not belong to its type. DISCOUNT_RULE_ID_DUPLICATE — trigger: two rules share an id; retryable: no; next: give each rule its own id, since ids are how applied and skipped identify them. We report the index rather than the id. DISCOUNT_RULE_LIMIT_EXCEEDED — trigger: more than 100 rules in one call; retryable: no; next: send fewer rules. DISCOUNT_TYPE_UNSUPPORTED — trigger: a type outside the five, or FIXED_PRICE, QUANTITY_BREAK or BUY_X_GET_Y with CART scope; retryable: no; next: use one of the five types, with LINE scope and appliesTo for the three that reason about a single line. An unknown type is always refused, never quietly skipped. CURRENCY_UNKNOWN — trigger: cart.currency is outside the twenty supported codes; retryable: no; next: pick a supported code.
Context footprint
The answer is proportional to your cart and rule count: one entry per line, one per applied rule, one per skipped rule. A 1000-line cart with 100 rules answers well inside the 1 MiB request ceiling. A successful call costs 1 execution credit.
When to use it
When you have a cart and a set of promotion rules and need the pre-tax result, reproducibly, with an audit trail you can show a customer.
When you need tax, shipping, coupon validity, entitlement or inventory — each of those is a separate decision this skill deliberately leaves to you.
Scope
Non-goals
- Does not compute tax — discount-before-tax versus tax-before-discount is jurisdictional, so we return pre-tax totals and you apply finance.calculate_vat to them
- Does not price shipping
- Does not check whether a coupon code exists, is still valid, or has been redeemed already — it applies the rules you send
- Does not read or spend a loyalty balance
- Does not check inventory or reserve anything
- Does not convert between currencies
- Does not evaluate a caller-supplied expression, formula or script
Properties
Capabilities
Cost & billing
Flat rate per invocation
File handling
How we handle your data
What this means
This skill may receive sensitive content and may return submitted, transformed, extracted, or reformatted content.
Squillify guarantees
- Runs inside Squillify
- This skill runs inside Squillify.
- Payloads are not sent to an external service by this skill.
- Squillify does not permanently store submitted payloads.
- Telemetry records metadata only, not submitted content.
- Submitted payloads are not used for training.
- Submitted payloads are not reviewed by humans.
- This skill does not target sensitive data.
- The response may contain submitted or transformed content.
Your responsibility
Squillify does not determine whether submitted content is sensitive. Your application is responsible for deciding what data it is allowed to submit. Your application is responsible for securing the response.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
| cart | object | Yes | Closed cart object with currency, bounded line items, and optional customer context. Its documented commercial properties are bounded to 256 entries — unconstrained RFC 8259 JSON items. |
| discountRules | array | Yes | Bounded ordered array of discount rule declarations. The item shape is defined by the nested schema and the array contains from 0 through 10000 items. |
| options | object | No | Closed calculation option object; omit it for deterministic documented defaults. Its documented commercial properties are bounded to 256 entries — unconstrained RFC 8259 JSON items. |
Output schema
| Field | Type | Required | Description |
|---|---|---|---|
| totals | object | Yes | Closed monetary totals receipt with subtotal, discount, tax basis, and final total. Its documented commercial properties are bounded to 256 entries — unconstrained RFC 8259 JSON items. |
| lines | array | Yes | Bounded array of calculated cart-line receipts in caller order. The item shape is defined by the nested schema and the array contains from 0 through 100000 items. |
| applied | array | Yes | Bounded array of applied discount receipts in deterministic rule order. The item shape is defined by the nested schema and the array contains from 0 through 100000 items. |
| skipped | array | Yes | Bounded array of skipped rule receipts with stable reason codes. The item shape is defined by the nested schema and the array contains from 0 through 100000 items. |
API contract
POST /v1/executecurl -sS -X POST "$BASE/v1/execute" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"skillKey":"commerce.discount_calculate","input":{"cart":{"currency":"EUR","lines":[{"id":"L1","unitPrice":"20.00","quantity":2}]},"discountRules":[{"id":"SPRING10","scope":"CART","type":"PERCENT","value":"0.10"}]}}'import { Squillify } from "@squillify/sdk";
const sq = new Squillify({ apiKey: process.env.SQUILLIFY_API_KEY });
const { output } = await sq.execute("commerce.discount_calculate", {"discountRules":[{"id":"SPRING10","priority":10,"scope":"CART","type":"PERCENT","value":"0.10"},{"id":"MEMBER05","priority":20,"scope":"CART","type":"PERCENT","value":"0.05"}],"cart":{"lines":[{"id":"L1","sku":"MUG-BLUE","unitPrice":"20.00","quantity":2,"category":"kitchen"},{"id":"L2","sku":"POT-RED","unitPrice":"15.00","quantity":1,"category":"kitchen"},{"id":"L3","sku":"CUP-GREEN","unitPrice":"9.99","quantity":3,"category":"kitchen"}],"currency":"EUR"}});Errors
| Code | HTTP | Retryable | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 | No | The request payload failed schema validation. |
| UNAUTHORIZED | 401 | No | Missing or invalid API key. |
| SKILL_NOT_ACCESSIBLE | 403 | No | The API key is not granted access to this skill. |
| SKILL_NOT_AVAILABLE_FOR_PLAN | 403 | No | This skill is not included in your current plan. |
| PLAN_ELIGIBILITY_UNAVAILABLE | 503 | Yes | Plan eligibility could not be decided. Retry after backoff. |
| SKILL_NOT_FOUND | 404 | No | The requested skill or version does not exist. |
| TENANT_RATE_LIMITED | 429 | Yes | Request rate limit exceeded. Retry after backoff. |
| QUOTA_EXCEEDED | 429 | Yes | Plan quota exhausted. Retryable once the quota period resets. |
| EXECUTION_TIMEOUT | 504 | Yes | Execution exceeded the allowed time budget. |
| CART_INVALID | 400 | No | The request payload failed this skill's input validation. |
| CART_CURRENCY_MIXED | 400 | No | The request payload failed this skill's input validation. |
| CART_LINE_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| DISCOUNT_RULE_INVALID | 400 | No | The request payload failed this skill's input validation. |
| DISCOUNT_RULE_ID_DUPLICATE | 400 | No | The request payload failed this skill's input validation. |
| DISCOUNT_RULE_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| DISCOUNT_TYPE_UNSUPPORTED | 400 | No | The skill does not support the requested variant. |
| CURRENCY_UNKNOWN | 400 | No | The request payload failed this skill's input validation. |
Other limits
| Limit | Value |
|---|---|
| Recommended inline bytes | 32 KiB |
Limitations
- Does not compute tax — discount-before-tax versus tax-before-discount is jurisdictional, so we return pre-tax totals and you apply finance.calculate_vat to them
- Does not price shipping
- Does not check whether a coupon code exists, is still valid, or has been redeemed already — it applies the rules you send
- Does not read or spend a loyalty balance
- Does not check inventory or reserve anything
- Does not convert between currencies
- Does not evaluate a caller-supplied expression, formula or script
- Producing a tax-inclusive total
- Validating or redeeming a coupon code
- Deciding which promotions a customer is entitled to
How to call
POST /v1/execute (requires authentication)
{
"skillKey": "commerce.discount_calculate",
"input": {
"discountRules": [
{
"id": "SPRING10",
"priority": 10,
"scope": "CART",
"type": "PERCENT",
"value": "0.10"
},
{
"id": "MEMBER05",
"priority": 20,
"scope": "CART",
"type": "PERCENT",
"value": "0.05"
}
],
"cart": {
"lines": [
{
"id": "L1",
"sku": "MUG-BLUE",
"unitPrice": "20.00",
"quantity": 2,
"category": "kitchen"
},
{
"id": "L2",
"sku": "POT-RED",
"unitPrice": "15.00",
"quantity": 1,
"category": "kitchen"
},
{
"id": "L3",
"sku": "CUP-GREEN",
"unitPrice": "9.99",
"quantity": 3,
"category": "kitchen"
}
],
"currency": "EUR"
}
}
}Examples
Two cart-wide percentages under the default sequential stacking
{
"discountRules": [
{
"id": "SPRING10",
"priority": 10,
"scope": "CART",
"type": "PERCENT",
"value": "0.10"
},
{
"id": "MEMBER05",
"priority": 20,
"scope": "CART",
"type": "PERCENT",
"value": "0.05"
}
],
"cart": {
"lines": [
{
"id": "L1",
"sku": "MUG-BLUE",
"unitPrice": "20.00",
"quantity": 2,
"category": "kitchen"
},
{
"id": "L2",
"sku": "POT-RED",
"unitPrice": "15.00",
"quantity": 1,
"category": "kitchen"
},
{
"id": "L3",
"sku": "CUP-GREEN",
"unitPrice": "9.99",
"quantity": 3,
"category": "kitchen"
}
],
"currency": "EUR"
}
}{
"applied": [
{
"ruleId": "SPRING10",
"scope": "CART",
"type": "PERCENT",
"discountAmount": "8.50",
"affectedLineIds": [
"L1",
"L2",
"L3"
],
"order": 0,
"notes": []
},
{
"ruleId": "MEMBER05",
"scope": "CART",
"type": "PERCENT",
"discountAmount": "3.82",
"affectedLineIds": [
"L1",
"L2",
"L3"
],
"order": 1,
"notes": []
}
],
"totals": {
"originalSubtotal": "84.97",
"totalDiscount": "12.32",
"subtotal": "72.65",
"lineCount": 3,
"currency": "EUR"
},
"lines": [
{
"id": "L1",
"originalTotal": "40.00",
"discountTotal": "5.80",
"total": "34.20",
"appliedRuleIds": [
"SPRING10",
"MEMBER05"
]
},
{
"id": "L2",
"originalTotal": "15.00",
"discountTotal": "2.17",
"total": "12.83",
"appliedRuleIds": [
"SPRING10",
"MEMBER05"
]
},
{
"id": "L3",
"originalTotal": "29.97",
"discountTotal": "4.35",
"total": "25.62",
"appliedRuleIds": [
"SPRING10",
"MEMBER05"
]
}
],
"skipped": []
}The same cart and the same two rules under additive stacking, which costs more
{
"cart": {
"lines": [
{
"id": "L1",
"sku": "MUG-BLUE",
"unitPrice": "20.00",
"quantity": 2,
"category": "kitchen"
},
{
"id": "L2",
"sku": "POT-RED",
"unitPrice": "15.00",
"quantity": 1,
"category": "kitchen"
},
{
"id": "L3",
"sku": "CUP-GREEN",
"unitPrice": "9.99",
"quantity": 3,
"category": "kitchen"
}
],
"currency": "EUR"
},
"options": {
"stacking": "ADDITIVE"
},
"discountRules": [
{
"id": "SPRING10",
"priority": 10,
"scope": "CART",
"type": "PERCENT",
"value": "0.10"
},
{
"id": "MEMBER05",
"priority": 20,
"scope": "CART",
"type": "PERCENT",
"value": "0.05"
}
]
}{
"applied": [
{
"ruleId": "SPRING10",
"scope": "CART",
"type": "PERCENT",
"discountAmount": "8.50",
"affectedLineIds": [
"L1",
"L2",
"L3"
],
"order": 0,
"notes": []
},
{
"ruleId": "MEMBER05",
"scope": "CART",
"type": "PERCENT",
"discountAmount": "4.25",
"affectedLineIds": [
"L1",
"L2",
"L3"
],
"order": 1,
"notes": []
}
],
"totals": {
"originalSubtotal": "84.97",
"totalDiscount": "12.75",
"subtotal": "72.22",
"lineCount": 3,
"currency": "EUR"
},
"lines": [
{
"id": "L1",
"originalTotal": "40.00",
"discountTotal": "6.00",
"total": "34.00",
"appliedRuleIds": [
"SPRING10",
"MEMBER05"
]
},
{
"id": "L2",
"originalTotal": "15.00",
"discountTotal": "2.25",
"total": "12.75",
"appliedRuleIds": [
"SPRING10",
"MEMBER05"
]
},
{
"id": "L3",
"originalTotal": "29.97",
"discountTotal": "4.50",
"total": "25.47",
"appliedRuleIds": [
"SPRING10",
"MEMBER05"
]
}
],
"skipped": []
}An exclusive clearance rule blocking a later one, and a coupon that matches nothing
{
"discountRules": [
{
"value": "0.30",
"id": "CLEARANCE30",
"type": "PERCENT",
"appliesTo": {
"tags": [
"clearance"
]
},
"scope": "LINE",
"priority": 10,
"exclusive": true
},
{
"id": "BUNDLE10",
"priority": 20,
"scope": "LINE",
"type": "PERCENT",
"value": "0.10"
},
{
"type": "PERCENT",
"id": "WINTERBOOTS",
"priority": 30,
"value": "0.20",
"appliesTo": {
"skus": [
"BOOT-WINTER"
]
},
"scope": "LINE"
}
],
"cart": {
"lines": [
{
"category": "camping",
"id": "L1",
"tags": [
"clearance"
],
"sku": "TENT-2P",
"quantity": 1,
"unitPrice": "180.00"
},
{
"id": "L2",
"sku": "LAMP-LED",
"unitPrice": "24.50",
"quantity": 2,
"category": "camping"
}
],
"currency": "EUR"
}
}{
"applied": [
{
"ruleId": "CLEARANCE30",
"scope": "LINE",
"type": "PERCENT",
"discountAmount": "54.00",
"affectedLineIds": [
"L1"
],
"order": 0,
"notes": []
},
{
"ruleId": "BUNDLE10",
"scope": "LINE",
"type": "PERCENT",
"discountAmount": "4.90",
"affectedLineIds": [
"L2"
],
"order": 1,
"notes": []
}
],
"totals": {
"originalSubtotal": "229.00",
"totalDiscount": "58.90",
"subtotal": "170.10",
"lineCount": 2,
"currency": "EUR"
},
"lines": [
{
"id": "L1",
"originalTotal": "180.00",
"discountTotal": "54.00",
"total": "126.00",
"appliedRuleIds": [
"CLEARANCE30"
]
},
{
"id": "L2",
"originalTotal": "49.00",
"discountTotal": "4.90",
"total": "44.10",
"appliedRuleIds": [
"BUNDLE10"
]
}
],
"skipped": [
{
"ruleId": "WINTERBOOTS",
"reasonCode": "NO_MATCHING_LINE"
}
]
}Recommended use cases
- Show a customer the cart total after every promotion, with a line-by-line breakdown that adds up
- Explain to support why a customer's coupon had no effect
- Compare what the same promotions cost you under sequential and additive stacking before you commit to a policy
Avoid for
- Producing a tax-inclusive total
- Validating or redeeming a coupon code
- Deciding which promotions a customer is entitled to
Related skills
Composable with
Safety & privacy
- Runs without calling any external service.
- Your payloads are never stored permanently.
- Telemetry captures metadata only — never your payload contents.
- We never train models on your payloads.
- No human reviews your payloads.