Coupon
Coupon is the discount system that allows users to reduce their order total when purchasing productions. The system supports flexible discount strategies with comprehensive validation rules and usage limits.
Core Concept
A Coupon defines:
- Discount strategy: How the discount is calculated (rate or amount)
- Validity period: When the coupon can be used
- Usage limits: How many times the coupon can be used globally and per user
- Activation status: Whether the coupon is currently active
Data Model
pub struct Coupon {
pub id: i32,
pub code: String, // Unique code users enter
pub is_active: bool, // Administrative on/off switch
pub discount: Json<Discount>, // Discount strategy
pub start_time: Option<PrimitiveDateTime>, // When coupon becomes valid
pub end_time: Option<PrimitiveDateTime>, // When coupon expires
pub time_limit_per_account: Option<i32>, // Max uses per user
pub time_limit_global: Option<i32>, // Max total uses
pub used_count: i32, // Current usage count
}
Discount Types
The system supports two discount strategies through the Discount enum:
1. Rate Discount
Applies a percentage discount to the order total, regardless of the order amount.
Discount::Rate(RateDiscount {
rate: Decimal // e.g., 0.20 for 20% off
})
- Calculation:
final_price = original_price × (1 - rate) - Use case: General promotions (e.g., “20% off any purchase”)
- No minimum order requirement
2. Amount Discount
Subtracts a fixed amount from the order total, with a minimum order requirement.
Discount::Amount(AmountDiscount {
min_amount: Decimal, // Minimum order required
discount: Decimal // Amount to subtract
})
- Calculation:
final_price = original_price - discount(iforiginal_price >= min_amount) - Use case: Threshold promotions (e.g., “$10 off orders over $50”)
- Validation: Coupon is invalid if order doesn’t meet
min_amount
The discount data is stored as JSON in the database, allowing flexible extension of discount strategies in the future.
Validation Rules
Coupon validation occurs in two places:
1. Pre-Purchase Verification (VerifyCoupon)
Allows users to check if a coupon is valid before creating an order. This provides immediate feedback in the UI.
Validation checks (in order):
- Coupon exists (by code lookup)
- Current time is after
start_time(if set) - Current time is before
end_time(if set) - Global usage hasn’t exceeded
time_limit_global(if set) - User’s usage count hasn’t exceeded
time_limit_per_account(if set)
Returns Option<Coupon> - None if any validation fails.
2. Order Creation Validation
When a user creates an order with a coupon, the system performs additional validation through coupon_applicable():
Additional checks:
- All time-based validations (as above)
- For
Amountdiscounts: Production price must meetmin_amount - Per-account usage limit is re-checked at the database level (race condition protection)
If validation fails during order creation, returns CreateOrderResult::CouponInvalid.
Integration with Orders
Order Creation Flow
When a user creates an order with a coupon:
- Coupon lookup: Fetch coupon by ID
- Applicability check: Validate using
coupon_applicable() - Per-account limit check: Query database for user’s usage count
- Price calculation: Apply discount to production price
- Order creation: Store order with
coupon_usedfield - Usage tracking: The order record links to the coupon for usage counting
Price Calculation
let mut amount = prod.price;
if let Some(coupon) = coupon {
amount = match *coupon.discount {
Discount::Rate(r) => amount * (Decimal::ONE - r.rate),
Discount::Amount(a) => {
if amount >= a.min_amount {
amount - a.discount
} else {
amount // Discount not applied if below minimum
}
}
};
}
Usage Counting
The system tracks coupon usage through the orders table:
- Orders store the
coupon_usedfield (coupon ID) used_counton the coupon is derived by counting orders that reference it- Per-user usage is counted via
CountCouponUsageByUserquery
Important: Usage counting is based on order creation, not order payment status. An unpaid order still counts toward usage limits.
Active Status and Code Uniqueness
Active Status (is_active)
The is_active flag allows administrators to enable/disable coupons without deletion:
- Active: Coupon can be found and used
- Inactive: Coupon is invisible to users but preserved in database
This is useful for:
- Temporarily pausing a promotion
- Testing coupons before public release
- Historical record keeping
Code Uniqueness
The database enforces unique active codes through a partial index:
CREATE UNIQUE INDEX "idx_coupon_code"
ON "shop"."coupon" ("code")
WHERE is_active = TRUE;
Implications:
- Multiple inactive coupons can share the same code
- Only one active coupon can have a specific code at any time
- This allows code reuse across different promotion periods
Management Operations
The system provides admin APIs for coupon lifecycle management:
CRUD Operations
- Create: Generate new coupons with all configuration options
- Update: Modify existing coupons (code, discount, limits, times)
- List: Retrieve all coupons (no pagination - suitable for admin dashboard)
- Get: Fetch individual coupon by ID or code
- Delete: Permanently remove coupon from database
Note: Deleting a coupon does not cascade to orders. Orders retain the coupon_used ID even if the coupon is deleted.
Time Management
All timestamps use Unix epoch format in the API but are stored as TIMESTAMP WITHOUT TIME ZONE in the database:
- API layer converts between Unix timestamps and
PrimitiveDateTime - All time comparisons use UTC
start_timeandend_timeare optional - omitting them means no time restriction
Design Decisions
Why JSON for Discount?
Storing discount as JSON enables:
- Easy addition of new discount strategies without schema changes
- Type-safe handling through Rust’s
serdedeserialization - Database-level storage of complex discount rules
Why Count Orders, Not Payments?
Usage limits count order creation, not successful payments, because:
- Prevents abuse through repeated unpaid orders
- Simplifies usage tracking (no need to track order status changes)
- Protects limited-use coupons from reservation attacks
Why Separate Verification API?
The VerifyCoupon endpoint exists separately from order creation to:
- Provide immediate UI feedback without creating an order
- Allow frontend to show applicable discounts before purchase
- Reduce unnecessary order creation for invalid coupons
Frontend Integration Points
When implementing the coupon UI:
- Code Entry: Call
VerifyCouponas user types/submits coupon code - Visual Feedback: Display discount type and amount from returned coupon
- Price Preview: Calculate and show discounted price before order creation
- Order Creation: Pass
coupon_id(not code) inCreateOrderRequest - Error Handling: Handle
COUPON_INVALIDresult with user-friendly message
Key point: The verification step returns a Coupon object with an id field. Use this ID when creating the order, not the code string.