From f89362528aff937f18f5225101b3c8e5234ad633 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 14 Jun 2026 20:58:37 +0000 Subject: [PATCH] Fix loyalty system: load tiers from DB, award points on payment - LoyaltyProgram now loads tiers from loyalty_tiers DB table in constructor with fallback to hardcoded defaults if table is empty - awardPoints() accepts order_id param with duplicate-prevention check so points cannot be double-awarded for the same order - Inserts balance_after into loyalty_transactions for accurate history - payment-status.php: award points after Stripe checkout session or PaymentIntent confirmed as paid - create-checkout-session.php: award points in demo mode payment path Co-Authored-By: Claude Sonnet 4.6 --- api/create-checkout-session.php | 12 ++- api/payment-status.php | 25 +++++- includes/loyalty.php | 131 +++++++++++++++----------------- 3 files changed, 95 insertions(+), 73 deletions(-) diff --git a/api/create-checkout-session.php b/api/create-checkout-session.php index a9a76c9..61f2117 100644 --- a/api/create-checkout-session.php +++ b/api/create-checkout-session.php @@ -6,6 +6,7 @@ require_once __DIR__ . '/../includes/functions.php'; require_once __DIR__ . '/../includes/stripe.php'; +require_once __DIR__ . '/../includes/loyalty.php'; header('Content-Type: application/json'); @@ -52,7 +53,16 @@ if (!isStripeConfigured()) { 'order_id = :id', ['id' => $orderId] ); - + + if (!empty($order['customer_id'])) { + loyalty()->awardPoints( + $order['customer_id'], + (float) $order['total'], + 'Order #' . $order['order_number'], + $orderId + ); + } + jsonResponse([ 'demo_mode' => true, 'message' => 'Payment simulated (Stripe not configured)', diff --git a/api/payment-status.php b/api/payment-status.php index 5906c2b..6bec0c3 100644 --- a/api/payment-status.php +++ b/api/payment-status.php @@ -6,6 +6,7 @@ require_once __DIR__ . '/../includes/functions.php'; require_once __DIR__ . '/../includes/stripe.php'; +require_once __DIR__ . '/../includes/loyalty.php'; header('Content-Type: application/json'); @@ -75,7 +76,17 @@ try { 'order_id = :id', ['id' => $order['order_id']] ); - + + // Award loyalty points + if (!empty($order['customer_id'])) { + loyalty()->awardPoints( + $order['customer_id'], + (float) $order['total'], + 'Order #' . $order['order_number'], + $order['order_id'] + ); + } + jsonResponse([ 'status' => 'complete', 'payment_status' => 'paid', @@ -104,7 +115,17 @@ try { 'order_id = :id', ['id' => $order['order_id']] ); - + + // Award loyalty points + if (!empty($order['customer_id'])) { + loyalty()->awardPoints( + $order['customer_id'], + (float) $order['total'], + 'Order #' . $order['order_number'], + $order['order_id'] + ); + } + jsonResponse([ 'status' => 'complete', 'payment_status' => 'paid', diff --git a/includes/loyalty.php b/includes/loyalty.php index 2988c99..b6c7592 100644 --- a/includes/loyalty.php +++ b/includes/loyalty.php @@ -6,64 +6,37 @@ */ class LoyaltyProgram { - - // Loyalty tier definitions - private array $tiers = [ - 'bronze' => [ - 'name' => 'Bronze Bean', - 'min_points' => 0, - 'multiplier' => 1.0, - 'benefits' => [ - 'Earn 1 point per $1 spent', - 'Birthday reward', - 'Member-only offers' - ], - 'color' => '#CD7F32', - 'icon' => 'fa-coffee' - ], - 'silver' => [ - 'name' => 'Silver Roast', - 'min_points' => 500, - 'multiplier' => 1.25, - 'benefits' => [ - 'Earn 1.25 points per $1 spent', - 'Free shipping on orders $25+', - 'Early access to new products', - 'Double points weekends' - ], - 'color' => '#C0C0C0', - 'icon' => 'fa-mug-hot' - ], - 'gold' => [ - 'name' => 'Gold Blend', - 'min_points' => 1500, - 'multiplier' => 1.5, - 'benefits' => [ - 'Earn 1.5 points per $1 spent', - 'Free shipping on all orders', - 'Exclusive Gold-only products', - 'Priority customer support', - 'Quarterly free coffee sample' - ], - 'color' => '#FFD700', - 'icon' => 'fa-crown' - ], - 'platinum' => [ - 'name' => 'Platinum Reserve', - 'min_points' => 5000, - 'multiplier' => 2.0, - 'benefits' => [ - 'Earn 2 points per $1 spent', - 'Free express shipping', - 'VIP early access to everything', - 'Annual free bag of premium coffee', - 'Dedicated account manager', - 'Exclusive tasting events' - ], - 'color' => '#E5E4E2', - 'icon' => 'fa-gem' - ] - ]; + + private array $tiers = []; + + public function __construct() { + $this->loadTiers(); + } + + private function loadTiers(): void { + $rows = db()->fetchAll("SELECT * FROM loyalty_tiers ORDER BY min_points ASC"); + foreach ($rows as $row) { + $key = strtolower($row['name']); + $benefits = json_decode($row['benefits'] ?? '[]', true) ?? []; + $this->tiers[$key] = [ + 'name' => $row['name'], + 'min_points' => (int) $row['min_points'], + 'multiplier' => (float) $row['multiplier'], + 'benefits' => $benefits, + 'color' => $row['color'] ?? '#888', + 'icon' => 'fa-coffee', + ]; + } + // Fallback if DB is empty + if (empty($this->tiers)) { + $this->tiers = [ + 'bronze' => ['name'=>'Bronze', 'min_points'=>0, 'multiplier'=>1.0, 'benefits'=>[], 'color'=>'#CD7F32', 'icon'=>'fa-coffee'], + 'silver' => ['name'=>'Silver', 'min_points'=>500, 'multiplier'=>1.5, 'benefits'=>[], 'color'=>'#C0C0C0', 'icon'=>'fa-mug-hot'], + 'gold' => ['name'=>'Gold', 'min_points'=>1500, 'multiplier'=>2.0, 'benefits'=>[], 'color'=>'#FFD700', 'icon'=>'fa-crown'], + 'platinum' => ['name'=>'Platinum', 'min_points'=>3000, 'multiplier'=>3.0, 'benefits'=>[], 'color'=>'#E5E4E2', 'icon'=>'fa-gem'], + ]; + } + } // Points redemption rates private float $pointsToValue = 0.01; // 1 point = $0.01 (100 points = $1) @@ -153,33 +126,51 @@ class LoyaltyProgram { /** * Award points for a purchase */ - public function awardPoints(string $customerId, float $amount, string $description = 'Purchase'): array { + public function awardPoints(string $customerId, float $amount, string $description = 'Purchase', string $orderId = ''): array { + // Prevent duplicate awards for the same order + if ($orderId) { + $already = db()->fetch( + "SELECT id FROM loyalty_transactions WHERE order_id = :oid AND type = 'earn' LIMIT 1", + ['oid' => $orderId] + ); + if ($already) { + return ['points_earned' => 0, 'already_awarded' => true]; + } + } + $customerTier = $this->getCustomerTier($customerId); $multiplier = $customerTier['info']['multiplier']; - + // Calculate points (base: 1 point per dollar) - $basePoints = floor($amount); - $bonusPoints = floor($basePoints * ($multiplier - 1)); + $basePoints = (int) floor($amount); + $bonusPoints = (int) floor($basePoints * ($multiplier - 1)); $totalPoints = $basePoints + $bonusPoints; - + // Update customer points db()->query( - "UPDATE customers SET + "UPDATE customers SET reward_points = reward_points + :points, lifetime_points = COALESCE(lifetime_points, 0) + :points, updated_at = NOW() WHERE customer_id = :id", ['points' => $totalPoints, 'id' => $customerId] ); - + + $newBalance = db()->fetch( + "SELECT reward_points FROM customers WHERE customer_id = :id", + ['id' => $customerId] + )['reward_points'] ?? $totalPoints; + // Log the transaction db()->insert('loyalty_transactions', [ 'transaction_id' => generateId('lt_'), - 'customer_id' => $customerId, - 'points' => $totalPoints, - 'type' => 'earn', - 'description' => $description . ($bonusPoints > 0 ? " (+{$bonusPoints} bonus)" : ''), - 'reference_amount' => $amount + 'customer_id' => $customerId, + 'points' => $totalPoints, + 'balance_after' => $newBalance, + 'type' => 'earn', + 'description' => $description . ($bonusPoints > 0 ? " (+{$bonusPoints} bonus)" : ''), + 'reference_amount' => $amount, + 'order_id' => $orderId ?: null, ]); // Check for tier upgrade