Skip to main content

Context

Today the engine’s effects credit points, issue coupons, fire webhooks and trigger campaigns. All of them happen after the fact. apply_discount is different in kind: it modifies a price in flight, which means it sits on the tenant’s checkout critical path. That is the territory Talon.One occupies, and the convergence of payments and incentives — Adyen acquiring them in July 2026 — is a signal that this is where the category is going, not a niche. DEC-H4 ratified it for F2. The cost in F1 was zero precisely because rule_effect_types was left open: adding this is a row plus an implementation, never a migration. The new problem this introduces is a cart model. Every prior effect operated on a contact; this one operates on a basket of items that exists only for the duration of a checkout. That, plus real budget control at request time, is why this is its own feature and its own phase.

Scope (normative)

  • A cart/session model: items, quantities, prices, currency — accepted, never persisted as an order.
  • POST /v1/loyalty/qualifications: given a cart and a member, return applicable effects.
  • The apply_discount effect type: amount or percentage, scoped to cart or line item.
  • Budget checks at request time, with atomic reservation.
  • Margin guardrails: a maximum discount per cart and per line.
  • Stacking evaluation (FS-LOY-0008) applied to the candidate discounts.
  • loyalty.qualification_sessions for audit and reconciliation against what was actually charged.

Non-scope (normative)

  • Executing a payment or persisting an order. The tenant’s checkout owns both. We answer a question; we do not become the commerce system.
  • Tax. Discounts are computed on the values the caller supplies, and how tax interacts with them is the tenant’s jurisdiction and their problem.
  • Inventory. A discount does not reserve stock.

Behaviour (normative)

  1. The endpoint is read-mostly: it returns effects and reserves budget, but never writes a ledger transaction. Points are only credited when the tenant confirms the sale through a separate call.
  2. Budget reservation has a TTL. An abandoned checkout releases its reservation automatically; without that, one abandoned cart per second silently exhausts a campaign.
  3. Discounts are computed in minor units against the cart currency (DEC-B5). Percentage discounts round half-down in the member’s favour, and the rounding rule is documented in the public API reference rather than left to be discovered.
  4. Margin guardrails are hard caps. A rule that would exceed one is clamped and the clamp is reported, never applied silently — the tenant must be able to see that their rule wanted more than their guardrail allowed.
  5. Stacking is evaluated before the response. The response contains only the combination that will actually be honoured.
  6. Sessions are retained for reconciliation and are subject to the standard retention policy. They are not orders and carry no payment data.
  7. Budget: p95 <300 ms. This is on a checkout path — exceeding it costs the tenant conversions.
  8. FORBIDDEN: returning an effect the tenant cannot honour, or one whose budget was not reserved.

Data (normative)

API (normative)

Events (normative)

loyalty.qualification.confirmed on confirmation, which is what triggers point accrual through the normal effect path. The qualification request itself emits nothing — a browsing customer is not a domain event.

Acceptance criteria (normative)

  1. A cart of 3 items at 45 000 with a Gold 10% rule returns a 4 500 discount and reserves budget.
  2. An unconfirmed session releases its reservation at TTL, and the campaign budget returns to its prior value.
  3. Confirmation credits points once; replaying it with the same idempotency key credits nothing further.
  4. A rule that would exceed the margin guardrail is clamped, and the response reports both the requested and the applied amount.
  5. Stacking is honoured: a non-combinable pair returns only the higher-precedence discount.
  6. p95 <300 ms under the k6 checkout profile.
  7. Negative: the qualification call writes no ledger transaction — proven by a row count before and after.

Execution

Synchronous command. Endpoint in backend/api; budget reservation uses the same Upstash primitives as rate limiting; TTL release is a scheduled sweep.

Open questions

None outstanding. Every question this spec carried was answered in the consolidated register (../../../design/open-questions-v1.md, v1.1) and folded into the normative sections above.

Changelog

Delivery record

Not implemented yet.