This document specifies the requirements for improving the promo code (voucher) feature by adding a consumption mechanism using Redis.
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".
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.
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"]
}
}
max_uses: (Integer, Optional) The maximum number of successful applications allowed for this code. If omitted or 0, it is treated as unlimited.allowed_packages: (List[String], Optional) List of package IDs (basic, pro, expert) the voucher is valid for. If omitted, valid for all.Usage state and audit history will be stored in Redis.
cpt:vouchers:usage:{CODE} (Integer).cpt:vouchers:history:{CODE} (Set or List) containing {session_id}:{timestamp} entries for a lightweight audit trail.During the pricing/quote and pricing/validate-voucher calls (now asynchronous):
vouchers.json.active is true.allowed_packages is defined, verify the current package is in the list.max_uses is defined and > 0, fetch the current usage count from Redis (GET cpt:vouchers:usage:{CODE}).current_usage >= max_uses, the voucher is considered consumed.A voucher should only be marked as consumed upon successful payment or successful report generation (for 100% discounts).
SUCCEEDED (in PaymentService.verify_payment) or when a 100% discount is applied (in PaymentService.create_payment_intent):quote_voucher_code from the associated OrderSession.INCR cpt:vouchers:usage:{CODE}.SADD cpt:vouchers:history:{CODE} "{session_id}:{iso_timestamp}".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.
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.
A shell action ./run -a do_sync_vouchers_to_redis should be created to manually reload definitions or reset usage state if needed.
app.services.cpq service must be refactored to be asynchronous because it now requires awaiting Redis I/O. This affects validate_voucher, create_quote, and the corresponding router endpoints.VoucherValidateResponse should include a reason field to differentiate between:NOT_FOUND: Code does not exist.INACTIVE: Code is disabled.CONSUMED: Usage limit reached.INELIGIBLE: Package not allowed.PaymentStep.vue needs to handle these specific error reasons to provide better user feedback.bnc-cpt-apiIn 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).