Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 (if original_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):

  1. Coupon exists (by code lookup)
  2. Current time is after start_time (if set)
  3. Current time is before end_time (if set)
  4. Global usage hasn’t exceeded time_limit_global (if set)
  5. 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 Amount discounts: Production price must meet min_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:

  1. Coupon lookup: Fetch coupon by ID
  2. Applicability check: Validate using coupon_applicable()
  3. Per-account limit check: Query database for user’s usage count
  4. Price calculation: Apply discount to production price
  5. Order creation: Store order with coupon_used field
  6. 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_used field (coupon ID)
  • used_count on the coupon is derived by counting orders that reference it
  • Per-user usage is counted via CountCouponUsageByUser query

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_time and end_time are 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 serde deserialization
  • 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:

  1. Code Entry: Call VerifyCoupon as user types/submits coupon code
  2. Visual Feedback: Display discount type and amount from returned coupon
  3. Price Preview: Calculate and show discounted price before order creation
  4. Order Creation: Pass coupon_id (not code) in CreateOrderRequest
  5. Error Handling: Handle COUPON_INVALID result 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.