From 4e0e6af3e128015b59b374ad0898a76ca32d9902 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Fri, 14 Aug 2026 11:19:00 +0200 Subject: [PATCH] fix(payments): compare global coupon redemptions against max_redemptions not max_redemptions_per_user 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 --- apps/payments/src/ai_credits.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/payments/src/ai_credits.rs b/apps/payments/src/ai_credits.rs index 78cb2e8..c267582 100644 --- a/apps/payments/src/ai_credits.rs +++ b/apps/payments/src/ai_credits.rs @@ -151,17 +151,18 @@ async fn validate_coupon( original_price: i32, ) -> Result, String> { // Get coupon details - let coupon: Option<(Uuid, String, i32, Option, i32, i32, Vec)> = sqlx::query_as( + let coupon: Option<(Uuid, String, i32, Option, i32, i32, Vec, i32)> = sqlx::query_as( r#" - SELECT - id, discount_type, + 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 - AND valid_from <= NOW() + WHERE code = $1 + AND is_active = TRUE + AND valid_from <= NOW() AND (valid_until IS NULL OR valid_until > NOW()) "# ) @@ -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()); }