Coupon and Discount Service Development
Coupons and discounts aren't just "enter promo code" on checkout. This is rules system managing pricing: who, when, what, how much. Poorly designed system becomes loopholes and analytics chaos. Well designed — precision marketing tool.
Discount Types
Before coding, fix model:
Coupons (Promo Codes) — user enters code or link applies auto. Code unique or reusable, bound to discount rule.
Automatic Discounts — apply without code meeting conditions: "category X 15% off Friday", "500 rubles off 3000+ order".
Loyalty Programs — discount depends on purchase history (cashback, points, tiers).
Group Discounts — B2B bulk rates, employee discounts, partner pricing.
Database Model
discount_rules (
id, name, type, -- coupon | automatic | loyalty
discount_type, -- percentage | fixed_amount | free_shipping | bxgy
discount_value NUMERIC,
min_order_amount NUMERIC,
min_qty INT,
max_uses INT, -- NULL = unlimited
max_uses_per_user INT,
starts_at TIMESTAMPTZ,
ends_at TIMESTAMPTZ,
is_active BOOLEAN,
stackable BOOLEAN -- combine with other discounts
)
discount_conditions (
id, rule_id,
condition_type, -- product | category | tag | user_group | first_order
condition_operator, -- in | not_in | gte | lte
condition_value JSONB
)
coupons (
id, rule_id, code VARCHAR(32),
usage_count INT DEFAULT 0,
is_single_use BOOLEAN
)
coupon_uses (
id, coupon_id, order_id, user_id, used_at,
discount_amount NUMERIC -- recorded at use time
)
Splitting discount_rules and coupons allows one rule multiple codes (bulk email) or one code different limits.
Coupon Generation Batches
For email campaigns need unique codes — one per recipient:
function generateCouponBatch(int $ruleId, int $count): array {
$codes = [];
while (count($codes) < $count) {
$code = strtoupper(Str::random(8)); // A-Z0-9, 8 chars
if (!Coupon::where('code', $code)->exists()) {
$codes[] = ['rule_id' => $ruleId, 'code' => $code, 'is_single_use' => true];
}
}
Coupon::insert($codes);
return array_column($codes, 'code');
}
For large batches (100k+ codes) generate upfront with index uniqueness check, not SELECT EXISTS in loop.
Coupon Validation and Application
On checkout code entry check:
- Code exists and active
- Start/end date
- Usage limit not exceeded (
max_uses) - User didn't exhaust limit (
max_uses_per_user) - Cart sum >=
min_order_amount - Cart items match conditions (
discount_conditions)
Check atomically on apply — race condition possible, both requests apply last coupon simultaneously. Solution:
UPDATE coupons
SET usage_count = usage_count + 1
WHERE code = :code
AND usage_count < max_uses -- for single-use: usage_count < 1
RETURNING id;
-- if 0 rows — coupon exhausted
UPDATE ... RETURNING inside transaction eliminates race.
Cart Discount Calculation
Server calculates, never trust client. Algorithm:
1. Get applied rules (automatic + coupon)
2. For each rule determine eligible items (apply conditions)
3. Apply in priority order
4. If stackable=false — apply largest discount only
5. Return breakdown: which discount applied to each item
Breakdown important for display ("–500 rubles SAVE500 coupon") and analytics.
BxGy (Buy X Get Y) — "buy 3 get 4th free". Separate rule type: qty >= X add item Y zero price or reduce N-th unit price.
Automatic Discounts and Priorities
Multiple automatic rules can trigger. Need policy:
- First matched — apply highest priority rule
- Best discount — apply rule giving most benefit
-
All compatible — apply all with
stackable=true
Policy set per-store, may vary by rule type.
Analytics and Reporting
Without analytics marketing flies blind. Basic metrics:
| Metric | SQL |
|---|---|
| Coupon uses | SELECT COUNT(*) FROM coupon_uses WHERE coupon_id = ? |
| Average discount | SELECT AVG(discount_amount) FROM coupon_uses WHERE ... |
| Revenue with discount | SUM(order.total) vs SUM(order.total + discount_amount) |
| Conversion coupon vs no | Compare CR for sessions with applied_coupon and without |
For marketers — dashboard with period, discount type, channel (utm source) filters.
Abuse Prevention
- One coupon per order — standard, but if stacking allowed, explicit config
- Email verification before "new customer" discount — else create 100 accounts
- Rate limiting on coupon apply endpoint — protect from brute force
- Alerts on sharp usage spike — possible leak
Marketer Cabinet
Interface for discount management must allow:
- Create discount rules with visual condition builder
- Generate and export CSV coupon batches
- Real-time stats per campaign
- Deactivate campaign one-click (important for errors)
Timeline
- Basic coupon system (promo code, percent/amount, end date): 1–2 weeks
- Full system (category/product conditions, automatic, BxGy, analytics, marketer cabinet): 3–5 weeks
- Loyalty program with points and tiers adds 3–4 weeks







