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 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:58:37 +00:00
parent b6d0319be7
commit f89362528a
3 changed files with 95 additions and 73 deletions
+10
View File
@@ -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');
@@ -53,6 +54,15 @@ if (!isStripeConfigured()) {
['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)',
+21
View File
@@ -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');
@@ -76,6 +77,16 @@ try {
['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',
@@ -105,6 +116,16 @@ try {
['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',
+56 -65
View File
@@ -7,63 +7,36 @@
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,13 +126,24 @@ 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
@@ -172,14 +156,21 @@ class LoyaltyProgram {
['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