Skip to main content

Squillify development is paused indefinitely. Read why, and discover what this experiment achieved. Read the story

MicroSkills Reference

Cart Discount Calculator

commerce.discount_calculate

ProductionBounded custom (Class B)Deterministicstrictfastv1.0.0
No network

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

Note
Apply an enumerated set of discount rules to a supplied cart in one currency and return pre-tax totals with a full audit of what applied and what did not.

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

IdempotentRetry safeStreaming supportedFile input supportedRequires external service

Capabilities

math

Cost & billing

Flat rate per invocation

MetercallsBillable onsuccessCost per call1 credit

File handling

Accepts file_refProduces file_refMax input1 MB

How we handle your data

Sensitive output possible
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.

Read the Microskill Data Handling Policy

Input schema

FieldTypeRequiredDescription
cartobjectYesClosed 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.
discountRulesarrayYesBounded ordered array of discount rule declarations. The item shape is defined by the nested schema and the array contains from 0 through 10000 items.
optionsobjectNoClosed 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

FieldTypeRequiredDescription
totalsobjectYesClosed 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.
linesarrayYesBounded 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.
appliedarrayYesBounded 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.
skippedarrayYesBounded 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/execute
curl
curl -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"}]}}'
Integrations
TypeScript SDK
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

CodeHTTPRetryableDescription
VALIDATION_ERROR400NoThe request payload failed schema validation.
UNAUTHORIZED401NoMissing or invalid API key.
SKILL_NOT_ACCESSIBLE403NoThe API key is not granted access to this skill.
SKILL_NOT_AVAILABLE_FOR_PLAN403NoThis skill is not included in your current plan.
PLAN_ELIGIBILITY_UNAVAILABLE503YesPlan eligibility could not be decided. Retry after backoff.
SKILL_NOT_FOUND404NoThe requested skill or version does not exist.
TENANT_RATE_LIMITED429YesRequest rate limit exceeded. Retry after backoff.
QUOTA_EXCEEDED429YesPlan quota exhausted. Retryable once the quota period resets.
EXECUTION_TIMEOUT504YesExecution exceeded the allowed time budget.
CART_INVALID400NoThe request payload failed this skill's input validation.
CART_CURRENCY_MIXED400NoThe request payload failed this skill's input validation.
CART_LINE_LIMIT_EXCEEDED429NoA size, quota, rate, or time limit was exceeded.
DISCOUNT_RULE_INVALID400NoThe request payload failed this skill's input validation.
DISCOUNT_RULE_ID_DUPLICATE400NoThe request payload failed this skill's input validation.
DISCOUNT_RULE_LIMIT_EXCEEDED429NoA size, quota, rate, or time limit was exceeded.
DISCOUNT_TYPE_UNSUPPORTED400NoThe skill does not support the requested variant.
CURRENCY_UNKNOWN400NoThe request payload failed this skill's input validation.

Other limits

LimitValue
Recommended inline bytes32 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)

Request bodyjson
{
  "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

Inputjson
{
  "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"
  }
}
Expected outputjson
{
  "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

Inputjson
{
  "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"
    }
  ]
}
Expected outputjson
{
  "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

Inputjson
{
  "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"
  }
}
Expected outputjson
{
  "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"
    }
  ]
}
  • 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

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.

Tags

commercediscountmathcurrencypricing