Coupon logic examples
Practical examples for customer eligibility, stacking, and retry-safe coupon usage.
This page complements the Discount Coupons reference with concrete scenarios based on production coupon logic.
Example 1: New-customer-only coupon
Coupon configuration:
customer_type = newscope_type = organization_widediscount_type = percentagediscount_percentage = 20
Behavior:
- Customer with no completed payment/installment transactions in the organization: coupon is valid.
- Customer with at least one completed payment/installment transaction: coupon is rejected as not eligible.
- If customer context is missing, eligibility checks for restricted customer types may fail.
Example 2: Returning-customer-only coupon
Coupon configuration:
customer_type = returningusage_frequency_limit = per_monthusage_limit_value = 1
Behavior:
- Returning customer can redeem once per month.
- Second redemption in the same month is rejected.
- Redemptions in a new month are allowed again (subject to other limits).
Example 3: Product-scoped coupon
Coupon configuration:
scope_type = specific_productsproduct_ids = [Product A, Product B]
Behavior:
- Checkout for Product A or B: can pass scope checks.
- Checkout for Product C: rejected (
coupon is not applicable to this product). - Organization-wide coupons skip product-link checks.
Example 4: Quantity-capped coupon
Coupon configuration:
max_quantity_per_use = 2
Behavior:
- Quantity
1or2: valid. - Quantity
3+: rejected with a quantity-limit message.
Example 5: Fixed discount clamped to base amount
Checkout:
- Base amount:
3,000 - Fees amount:
500 - Eligible base price:
2,500
Coupon:
discount_type = fixeddiscount_fixed_amount = 5,000
Result:
- Discount is clamped to
2,500(cannot exceed eligible base). - Final amount is
(2,500 - 2,500) + 500 = 500.
Example 6: Sequential stacking (two coupons)
Checkout amount: 10,000
Coupons in order:
SAVE20(20%)FLAT1000(fixed 1,000)
Sequential calculation:
- After
SAVE20: discount2,000, running amount8,000 - After
FLAT1000: discount1,000, running amount7,000 - Total discount:
3,000
Important: second coupon is applied to the reduced running amount, not the original amount.
Example 7: Pending reservation + transaction linking
Flow:
- Coupon is applied to checkout session.
- A pending
coupon_usagereservation is recorded for(coupon_id, checkout_session_id). - Payment provider callback creates/finalizes transaction.
- Reservation is linked to the transaction.
Why this matters:
- Prevents duplicate coupon usage rows during retries/webhook races.
- Keeps reporting accurate when providers retry callbacks.
Example 8: Safe retry behavior
If the same provider event is retried:
- existing pending reservation is updated/linked when possible,
- duplicate inserts are prevented by uniqueness and conflict handling,
- usage counting remains consistent when transaction completion logic runs.
Example 9: Stacking order changes the total discount
Same two coupons as Example 6, but reverse the order:
FLAT1000first: discount1,000, running amount9,000SAVE20second: 20% of9,000=1,800, running amount7,200- Total discount:
2,800(not3,000)
Always show the applied order in UI and persist the same order server-side.
Example 10: Usage frequency — sliding window vs calendar
Depending on which validation path runs, usage limits may be evaluated differently:
- One path counts coupon usage rows in a sliding window (for example last 24 hours for
per_day). - Another path ties
per_day/per_week/per_monthto calendar boundaries.
For a coupon limited to one use per day, a customer who redeems at 23:59 may or may not redeem again at 00:01 the next day, depending on which helper your checkout stack calls. Treat limits as “at most N redemptions in the configured period” and test in sandbox.
Example 11: Re-applying a coupon on the same checkout session
When a coupon is applied to a session that already has a pending coupon_usage row, the system may upsert that row (update discount and timestamps) instead of inserting duplicates — as long as the transaction is not finalized yet.
Implications:
- Changing the coupon code or recomputing the cart should replace the pending reservation, not stack duplicate rows.
- After payment completes, usage is keyed off the transaction; do not rely on session-only rows for accounting.
Example 12: Free or fully discounted checkouts
When the discount brings the payable amount to zero (for example 100% off campaigns), the platform may record a free completion path using dedicated provider/method codes so ledger and webhooks remain consistent.
Merchant-facing reporting should still show list price, discount, and net.
Example 13: Client-side retries
Your app retries POST /checkout-sessions or apply coupon due to a flaky network:
- Use the same idempotency key or session id so you do not create parallel sessions.
- On the server, duplicate completion webhooks should not double-increment usage if transaction completion is idempotent — still avoid duplicate client-initiated applies when possible.
Integration checklist
- Always validate before final apply.
- Send customer context when using
new/returningrestricted coupons. - Keep coupon application idempotent in your own client retries.
- For multi-coupon UX, preserve intended coupon order.
- Document for your team whether percentage or fixed coupons should be applied first (product decision, affects totals).