# retail.coupon-validate
Decides whether a coupon code can be used on a basket, and says why not in
reason codes a checkout can translate, rather than a single yes or no.
## The checks, in the order the reasons are listed
| reason | fails when |
|---|---|
| `not-yet-valid` | `onDate` is before `validFrom` |
| `expired` | `onDate` is after `validTo` (the last day is inclusive) |
| `usage-limit-reached` | `timesUsed` is `maxUses` or more |
| `customer-limit-reached` | `customerUses` is `maxUsesPerCustomer` or more |
| `no-eligible-items` | no line passes the product rules |
| `minimum-spend-not-met` | the spend is below `minimumSpend` |
Every failing reason is returned, not just the first, so a checkout can say
"this code expired, and it needs a £20 spend" in one go. `valid` is true
exactly when `reasons` is empty. A null limit is no limit.
## Which lines are eligible
A line is eligible when its SKU is in `skus` or its category is in
`categories` (either list; both empty means every line), and neither its SKU is
in `excludedSkus` nor its category in `excludedCategories`. Exclusions always
win: that is how "20% off homeware, excluding sale items" is written.
## Minimum spend
`minimumSpendOn` says what counts towards it: `basket` sums every line,
`eligible` only the eligible ones. Retailers do both, and the difference is a
common source of complaints, so the coupon has to say. Line totals should be
after line promotions and before this coupon's own discount. The minimum spend
and the lines must share a currency.
This function only decides eligibility. Working out the discount and spreading
it across the eligible lines is `money.apply-rate` and `money.allocate`, and
recording the use (incrementing `timesUsed`) is the caller's job, once the
order is placed, not when the code is checked.
1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.