Loading...
Loading...
Build a coupon system with percentage and fixed discounts, usage limits per customer, expiration dates, and bulk unique-code generation
npx skill4agent add finsilabs/awesome-ecommerce-skills coupon-management| Platform | Built-in Capability | When to Add an App/Plugin |
|---|---|---|
| Shopify | Shopify Discounts — supports percentage, fixed amount, free shipping, BOGO; usage limits, expiry, minimum purchase | When you need bulk unique codes (Shopify supports import), customer-group-specific coupons, or loyalty integration (Smile.io, LoyaltyLion) |
| WooCommerce | WooCommerce Coupons — built into core; percentage, fixed cart, fixed product, free shipping types | When you need bulk unique code generation: WooCommerce Smart Coupons plugin; advanced rules: YITH WooCommerce Dynamic Pricing & Discounts |
| BigCommerce | Coupon codes built in — percentage, fixed amount, free shipping, free product types | When you need B2B-specific codes or advanced restrictions; BigCommerce app marketplace has options like Coupon Manager Pro |
| Custom / Headless | Must build — see Custom section below | N/A — you are building the system |
SUMMER20POST /v2/coupons// Coupon validation at checkout
async function validateCoupon(
code: string,
customerId: string,
orderSubtotalCents: number
): Promise<{ valid: boolean; discountCents: number; error?: string }> {
const coupon = await db.coupons.findOne({ code: code.toUpperCase().trim(), is_active: true });
if (!coupon) return { valid: false, discountCents: 0, error: 'Code not found' };
const now = new Date();
if (coupon.expires_at && coupon.expires_at < now) return { valid: false, discountCents: 0, error: 'Code expired' };
if (coupon.usage_limit && coupon.usage_count >= coupon.usage_limit) return { valid: false, discountCents: 0, error: 'Code fully used' };
if (coupon.min_order_cents && orderSubtotalCents < coupon.min_order_cents) return { valid: false, discountCents: 0, error: `Minimum order $${coupon.min_order_cents / 100}` };
// Per-customer limit check
if (coupon.per_customer_limit) {
const uses = await db.couponRedemptions.count({ coupon_id: coupon.id, customer_id: customerId });
if (uses >= coupon.per_customer_limit) return { valid: false, discountCents: 0, error: 'Already used' };
}
const discountCents = coupon.type === 'percentage'
? Math.round(orderSubtotalCents * (coupon.value / 100))
: Math.min(coupon.value_cents, orderSubtotalCents);
return { valid: true, discountCents };
}
// Atomic redemption — use inside the order creation transaction
async function redeemCoupon(tx: Tx, couponId: string, customerId: string, orderId: string, discountCents: number) {
// Atomic increment with guard — prevents over-redemption under concurrency
const result = await tx.raw(
`UPDATE coupons SET usage_count = usage_count + 1
WHERE id = ? AND (usage_limit IS NULL OR usage_count < usage_limit)
RETURNING id`,
[couponId]
);
if (result.rowCount === 0) throw new Error('COUPON_EXHAUSTED');
await tx.couponRedemptions.insert({ coupon_id: couponId, customer_id: customerId, order_id: orderId, discount_cents: discountCents });
}import crypto from 'crypto';
function generateCode(prefix = 'PROMO', length = 8): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no ambiguous chars
return prefix + '-' + Array.from(crypto.randomBytes(length))
.map(b => chars[b % chars.length]).join('');
}
async function bulkGenerate(template: CouponTemplate, quantity: number): Promise<string[]> {
const codes: string[] = [];
while (codes.length < quantity) {
const batch = Array.from({ length: Math.min(500, quantity - codes.length) }, () => generateCode(template.prefix));
const inserted = await db.coupons.insertMany(
batch.map(code => ({ ...template, code, usage_limit: 1, per_customer_limit: 1 })),
{ onConflict: 'ignore' }
);
codes.push(...inserted.map(r => r.code));
}
return codes;
}| Problem | Solution |
|---|---|
| Two customers redeem the last use simultaneously | Use atomic |
| Coupon still valid after order cancellation | Decrement the usage count when an order is cancelled or refunded; Shopify does this automatically |
| Per-customer limit bypassed with multiple accounts | Supplement customer-ID checks with email checks; for high-value campaigns, require verified phone numbers |
| Bulk-generated codes collide with existing ones | Use |
| Free-shipping coupon stacks with a percentage discount unexpectedly | Define your stacking policy explicitly; on Shopify, use the "Can be combined with" settings on each discount |