fix(payments): compare global coupon redemptions against max_redemptions not max_redemptions_per_user
All checks were successful
build-and-release / build (employees) (push) Successful in 11s
build-and-release / build (cron) (push) Successful in 14s
build-and-release / build (customers) (push) Successful in 16s
build-and-release / build (catering-services) (push) Successful in 19s
build-and-release / build (companies) (push) Successful in 21s
build-and-release / build (developers) (push) Successful in 20s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (gateway) (push) Successful in 9s
build-and-release / build (graphic-designers) (push) Successful in 8s
build-and-release / build (job-seekers) (push) Successful in 8s
build-and-release / build (jobs) (push) Successful in 10s
build-and-release / build (social-media-managers) (push) Successful in 8s
build-and-release / build (makeup-artists) (push) Successful in 12s
build-and-release / build (photographers) (push) Successful in 8s
build-and-release / build (tutors) (push) Successful in 7s
build-and-release / build (users) (push) Successful in 6s
backend-integration-tests / ai-credits (push) Successful in 9s
build-and-release / build (ugc-content-creators) (push) Successful in 11s
build-and-release / build (video-editors) (push) Successful in 11s
build-and-release / build (payments) (push) Successful in 1m0s

validate_coupon was checking:
  total_redemptions (= redemptions_used, global count) >= max_per_user (per-user limit)

which means a coupon with max_redemptions=1000 and max_redemptions_per_user=1
would be marked exhausted after the first person ever used it.

Fix: also SELECT max_redemptions and compare global count against it.
The per-user check on line 192 (user_redemptions >= max_per_user) was
already correct and is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tracewebstudio Dev 2026-08-14 11:19:00 +02:00
parent 86e1fd47d2
commit 4e0e6af3e1

View file

@ -151,13 +151,14 @@ async fn validate_coupon(
original_price: i32,
) -> Result<Option<(i32, String, i32)>, String> {
// Get coupon details
let coupon: Option<(Uuid, String, i32, Option<i32>, i32, i32, Vec<Uuid>)> = sqlx::query_as(
let coupon: Option<(Uuid, String, i32, Option<i32>, i32, i32, Vec<Uuid>, i32)> = sqlx::query_as(
r#"
SELECT
id, discount_type,
CAST(discount_value * 100 AS INTEGER) as discount_value,
CAST(max_discount_amount * 100 AS INTEGER) as max_discount_amount,
max_redemptions_per_user, redemptions_used, applicable_package_ids
max_redemptions_per_user, redemptions_used, applicable_package_ids,
max_redemptions
FROM ai_coupons
WHERE code = $1
AND is_active = TRUE
@ -170,12 +171,12 @@ async fn validate_coupon(
.await
.map_err(|e| format!("DB error: {e}"))?;
let Some((coupon_id, discount_type, discount_value, max_discount_amount, max_per_user, total_redemptions, applicable_packages)) = coupon else {
let Some((coupon_id, discount_type, discount_value, max_discount_amount, max_per_user, total_redemptions, applicable_packages, global_limit)) = coupon else {
return Ok(None); // Coupon not found or invalid
};
// Check if coupon has remaining redemptions
if total_redemptions >= max_per_user {
// Check if coupon has remaining global redemptions
if total_redemptions >= global_limit {
return Err("Coupon redemption limit reached".to_string());
}