Skip to content

Powered by Grav

Coming soon — KahunaCart is in final testing. Join the list and be first to know. Join the list

Coupons

This page shows a store administrator how to create discount codes, scope them to products, and limit how often they are used. A coupon is a code the customer types on the cart page.

Codes are stored normalized, lowercased and trimmed, so lookups are case-insensitive on every database engine. SAVE20, save20 and Save20 are one coupon.

Coupon types

Type Admin label What it does
percent Percentage off A percentage off the items subtotal, or off just the scoped lines when the coupon names products. One order-level adjustment either way.
fixed_cart Fixed amount off the cart A flat amount off the cart, never more than the cart is worth. One order-level adjustment.
fixed_product Fixed amount off matching products A flat amount off each matching line, capped at that line's own total. One adjustment per line, carrying the item id so a receipt can show it against the item.

The fixed_product cap matters: a $10-off coupon takes $6 off a $6 line and moves on, rather than turning it into a $4 refund. Every discount is also capped at the order's current headroom, which is items plus whatever adjustments have already landed, so a discount never drives a total negative.

Create a coupon

  1. In the admin sidebar, click KahunaCart, then Coupons.
  2. Click New coupon.
  3. Under Discount, enter a Code and choose a Type.
  4. Enter Percent off or Amount off, depending on the type.
  5. Leave Enabled on.
  6. Optionally set Starts and Expires under Validity window.
  7. Optionally set Minimum cart total, Maximum cart total, Total uses and Uses per customer under Limits.
  8. Optionally tick products under Restrict to products. Leave it empty for the whole catalog.
  9. Click Create coupon.

The code works on the cart page immediately.

Important

A coupon that is misconfigured for its type is refused with "This coupon is not configured correctly" rather than silently discounting nothing. That covers a percent with no percentage, and a fixed type with no amount or a zero amount.

Scoping

product_ids limits a coupon to a set of products. An empty or absent list means the whole catalog.

One rule applies across all three types: a scoped coupon needs at least one of its products in the cart, or it is refused with "This coupon does not apply to anything in your cart." For fixed_cart, the discount still comes off the whole cart, but the cart has to qualify.

Limits and windows

Field Admin label Effect
enabled Enabled Off means "This coupon is no longer available".
starts_at Starts Before it, "This coupon is not active yet".
expires_at Expires At or after it, "This coupon has expired". The boundary is exclusive: a coupon expires at its timestamp, not after it.
min_total_minor Minimum cart total Measured against items_total_minor, the items subtotal, not the total.
max_total_minor Maximum cart total Measured against items_total_minor as well.
usage_limit Total uses Total redemptions across all customers. Blank means unlimited.
usage_limit_per_user Uses per customer Redemptions by one email address. Blank means unlimited.
individual_use Cannot combine with other coupons Stored, but see below.

Usage counting

usage_count on the coupon row is a denormalized counter kept in step with a kahunacart_coupon_usages log. It is what the usage_limit check reads, and it is incremented with an atomic UPDATE rather than a read-modify-write.

Redemption is recorded inside the order-completion transaction, so an order that rolls back takes its usage row with it.

A full refund releases the redemption: the counter is decremented with a floor guard, the log rows are deleted, and the code becomes available again to everyone. A partial refund does not release it, because the order still stands and so does its discount. See Orders and the admin.

Note

The per-user limit needs an email address to enforce. Until checkout knows one, that check is skipped, so a coupon with a per-user limit of 1 can look valid on the cart page and be refused at checkout once the email is known.

Individual use

The individual_use flag is carried on the coupon row but has nothing to arbitrate yet. An order holds exactly one code, in data_json.coupon_code, so there is no stacking to prevent and setting the flag changes nothing today.

When stacking arrives, individual_use = 1 will mean "this coupon runs alone": applying it drops every other code, and it cannot be added to a cart that already carries one.

Apply and remove a coupon on the storefront

The coupon form posts to {route}/cart with action=coupon and a code field. An empty code removes whatever is applied, and the "Remove" button is the same form with a submit carrying name="code" value="".

The code is stored on the cart and the cart is recalculated. Whether it was honored is the calculator's answer, not the form's, so an invalid code is reported by the recalculation and dropped there.

When a coupon lapses mid-checkout

A code that was valid when it was typed can stop being valid before the order is placed. It expires, it runs out because someone else redeemed the last one, or the cart moves outside the coupon's minimum or maximum.

KahunaCart's answer in every case is the same: drop the code and say so. What that means depends on where the customer is.

On the cart page. The code is dropped, the cart is recalculated without it, and the reason is flashed as an error.

At checkout submit. The order is not placed. Addresses have just changed, so shipping and tax are redone before anything is charged. If the coupon has stopped applying, the customer is sent back to checkout with the reason rather than on to payment.

At order completion. One last re-check runs inside the completion transaction, covering only the two conditions a third party can change: the coupon being switched off, and it being used up by someone else. If either happened, completion throws Coupon is no longer valid, the whole transaction rolls back including stock and the order number, and the order stays a cart.

Error messages

These are the exact strings shown to customers, so you know what to expect in logs and flash messages.

Condition Message
Code not found This coupon code is not valid
Disabled This coupon is no longer available
Before starts_at This coupon is not active yet
At or after expires_at This coupon has expired
Below min_total_minor Your order does not reach this coupon's minimum total
Above max_total_minor Your order is above this coupon's maximum total
usage_limit reached This coupon has reached its usage limit
usage_limit_per_user reached You have already used this coupon
Scoped, nothing matching in cart This coupon does not apply to anything in your cart
Bad configuration This coupon is not configured correctly

Receipts

Discounts appear as adjustment rows with a negative amount_minor and a label of Coupon plus the uppercased code. The adjustment's source records the coupon id, code, type, the base the discount was computed on, and for fixed_product the item it applies to.

API reference

All coupon endpoints require the kahunacart.settings permission.

TXT
GET    /kahunacart/coupons
POST   /kahunacart/coupons
PATCH  /kahunacart/coupons/{id}
DELETE /kahunacart/coupons/{id}

starts_at, expires_at, usage_limit and usage_limit_per_user are nullable. Clearing the field in the admin clears the column rather than writing a zero.