05-Specifications / 05.06.Promo-Codes.spec

05.06.Promo Codes.spec

05.06. Promo Codes.spec

This document specifies the requirements for improving the promo code (voucher) feature by adding a consumption mechanism using Redis.

1. Problem Statement

Currently, promo codes defined in vouchers.json can be used an infinite number of times as long as they are marked as active. There is no mechanism to limit the number of times a code can be used or to mark it as "consumed".

2. Proposed Solution

We will implement a usage tracking system using Redis. This will allow us to define a max_uses property for each promo code and ensure that once this limit is reached, the code can no longer be applied.

3. Data Model Updates

3.1 Voucher Configuration (vouchers.json)

The voucher definition will be extended with optional max_uses and allowed_packages fields.

{
    "PROMO2026": {
        "type": "percentage",
        "value": 100,
        "label": "Limited Pilot - 100% off",
        "active": true,
        "max_uses": 50,
        "allowed_packages": ["basic", "pro"]
    }
}

3.2 Redis Storage Strategy

Usage state and audit history will be stored in Redis.

4. Logical Workflow

4.1 Validation Phase

During the pricing/quote and pricing/validate-voucher calls (now asynchronous):

  1. Load the voucher definition from vouchers.json.
  2. Check if active is true.
  3. Package Check: If allowed_packages is defined, verify the current package is in the list.
  4. Usage Check: If max_uses is defined and > 0, fetch the current usage count from Redis (GET cpt:vouchers:usage:{CODE}).
  5. If current_usage >= max_uses, the voucher is considered consumed.

4.2 Consumption Phase

A voucher should only be marked as consumed upon successful payment or successful report generation (for 100% discounts).

  1. When a payment is verified as SUCCEEDED (in PaymentService.verify_payment) or when a 100% discount is applied (in PaymentService.create_payment_intent):
  2. Retrieve the quote_voucher_code from the associated OrderSession.
  3. If a voucher was used, increment its usage in Redis: INCR cpt:vouchers:usage:{CODE}.
  4. Log the session to the audit history: SADD cpt:vouchers:history:{CODE} "{session_id}:{iso_timestamp}".

4.3 Atomic Operations & Race Conditions

To prevent over-consumption in high-concurrency scenarios: * The system will use a simple INCR after success for this iteration. * Refinement: If strict limits are critical, a Lua script will be implemented to perform a "check-and-increment" atomically during the validation phase.

5. Bootstrapping & Sync

5.1 Startup Check (Authoritative)

On API startup, the system should: 1. Read all vouchers from vouchers.json. 2. For each voucher with max_uses > 0, check if the key cpt:vouchers:usage:{CODE} exists in Redis. 3. If it does not exist, initialize it to 0. 4. Do not overwrite existing counters on restart to preserve state as long as Redis remains alive.

5.2 Manual Sync Action

A shell action ./run -a do_sync_vouchers_to_redis should be created to manually reload definitions or reset usage state if needed.

6. Implementation Notes & Review Findings

7. Future Considerations (PostgreSQL Migration)

In two weeks, the Redis usage counters and history sets will be migrated to: * vouchers_usage table: For persistent counters. * vouchers_audit table: For detailed usage history (linked to users and orders).