Discounts can be scoped to specific products via `applies_to`
Context
Merchants asked for two related capabilities: a code that is only valid when the cart contains a certain product, and a code that discounts only that product rather than the whole cart. Until now a discount always applied to the entire order and the code carried no product information. This is the same shape as the Shopify "applies to: entire order / specific products" model the checkout already mirrors. An order line already records the product it was for, so both the eligibility and the scoped amount can be computed from data the checkout already has.
Decision
Add two attributes to a discount code: applies_to (whole order — the default — or specific products) and product_ids (the eligible products). The product id used is the same one the order line records, so no join table or parallel product list is introduced.
- Eligibility: a product-scoped code is only valid when the cart contains an eligible product. Order-create rejects it otherwise; the quote reports the same when it is given the cart's line items. A scoped code with no eligible-product list fails closed (invalid), never whole-cart.
- Scoped amount: the discount is computed against only the eligible products' total — a percentage takes that percent of those products, a fixed amount is clamped to their total — then flows through the same "discount" fee group (ADR 0002) as a resolved amount, so the cart total drops by exactly that. With no eligible product present it collapses to zero. Order-wide codes are unchanged.
- Quote contract: to quote a product-scoped code, checkout must send the cart's line items (
products: [{id, cost}]) to the quote endpoint; order-wide codes need nothing extra. Untrusted quote payloads are coerced (string ids/costs) so a malformed request cannot crash or skew pricing; order-create remains the authority.
Consequences
- Positive: product eligibility and product-scoped amounts, with no new table — the order line is the source of truth for which products a code touched.
- Positive: composes with the other discount features — minimum spend, expiry, and the usage/per-customer caps (ADR 0003) all apply on top of a scoped code.
- Neutral / to revisit: checkout must adopt the quote contract (send line items) before a scoped code can be validated in the UI; order-wide codes are unaffected, and no scoped codes exist until a merchant creates one.
- Out of scope: this is coded-discount scoping (the buyer enters a code). Automatic/codeless promotions ("10% off any cart over X, no code") remain a separate, larger decision — a rule engine over the same condition primitives — not taken here.
Alternatives considered
- A join table of code → products, or per-line frozen discounts, as the enforcement mechanism: rejected — the order line already carries the product id, so eligibility and scoping read it directly; a join adds a table and a lookup for no gain at this scale.
- Applying a scoped percentage through the fee engine's whole-cart base: rejected — the fee-group percentage runs on the whole subtotal, so a scoped percentage is resolved to a flat amount against the eligible products first, then applied.