Skip to main content
Proposed (STA-922, batch stackra-32; STA-691). It carries out owner decision R3-SO-08 (merging GRO-182 b, GRO-186 and the gift-card and promotion exclusions) and answers the Commerce STA-782 review: @stackra/payments had no restricted-tender kind and TerminalRequest had no PIN step. It reads with 0357, 0377 and 0378.

Context

Grocery takes SNAP at the till and online at launch in us (GRO-182) and meal vouchers in eu-fr and eu-be (GRO-186). Both are one mechanism: a tender that only certain lines may pay, the basket split by eligibility, no tax on the part the tender pays where the law says so, and the rest on a second tender. Online SNAP adds one rule the card flow lacks: the EBT amount is taken at delivery or pickup against the picked basket, never at checkout, and weight changes and substitutions re-split it. EBT needs a PIN, entered on the terminal or on the processor’s hosted PIN pad.

Decision

  1. A tender kind. TenderKind (card, restricted) travels on TerminalRequest and CheckoutRequest with the program code (program). A restricted request asks the provider for PaymentCapability.RestrictedTender, so only a processor that takes the program serves it.
  2. Programs are pack data. RestrictedTenderProgram (code, ruleSet, taxExempt, captureAtFulfilment, pinEntry) is read through the RestrictedTenderPrograms port; the market-pack driver reads the pack key payments.restricted-tenders (live entries only, so a program waiting on its processor is listed planned). No program, Market or category is written in code.
  3. Eligibility is a rule set. TenderEligibility.eligible(market, ruleSet, lines) returns one flag per line. The host binds it over @stackra/rules (rule-set:grocery:tender-eligibility, seeded per pack: fees, tips and delivery are never eligible). The memory driver serves tests.
  4. The split is one pure function. splitTender gives the restricted part (eligible lines, without their tax when the program is tax-exempt, capped by the tender’s balance), the waived tax (in proportion when the balance covers only part, rounded down) and the remainder; the three add up to the basket. The remainder is the product’s to take on an ordinary tender under its own reference.
  5. Capture at fulfilment. RestrictedTenderService authorises only when the program says captureAtFulfilment; captureAtFulfilment re-splits the picked basket and captures the restricted part, never more than was authorised, and voids when nothing eligible is left. Everything goes through PaymentService, so the unknown state and one payment per reference hold.
  6. The PIN step. TerminalRequest.pinEntry asks the terminal for a PIN (PaymentCapability.TerminalPin); the PIN never reaches the platform. A restricted program’s pinEntry sets it.

Consequences

  • OS integrations writes the EBT processor connector (Forage or equivalent, named in market-pack:us) and the meal-voucher connectors (Edenred, Swile; eu-fr) against @stackra/payments/contracts. They wait on the owner’s processor choice (owner:ebt-processor).
  • The host binds TenderEligibility over the rules engine; until it does, the host’s build fails on the missing port.
  • Terminal drivers that support PIN entry or a restricted program list the new capabilities.

Open

  • The EBT and meal-voucher processor drivers (owner decision on the processor).
  • Whether a program’s balance inquiry becomes its own gateway call; today the product passes the balance it read.