lomi.
Payments

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 = new
  • scope_type = organization_wide
  • discount_type = percentage
  • discount_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 = returning
  • usage_frequency_limit = per_month
  • usage_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_products
  • product_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 1 or 2: 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 = fixed
  • discount_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:

  1. SAVE20 (20%)
  2. FLAT1000 (fixed 1,000)

Sequential calculation:

  • After SAVE20: discount 2,000, running amount 8,000
  • After FLAT1000: discount 1,000, running amount 7,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:

  1. Coupon is applied to checkout session.
  2. A pending coupon_usage reservation is recorded for (coupon_id, checkout_session_id).
  3. Payment provider callback creates/finalizes transaction.
  4. 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:

  1. FLAT1000 first: discount 1,000, running amount 9,000
  2. SAVE20 second: 20% of 9,000 = 1,800, running amount 7,200
  3. Total discount: 2,800 (not 3,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_month to 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/returning restricted 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).

On this page