Files
myron f89362528a 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>
2026-06-14 20:58:37 +00:00

430 lines
15 KiB
PHP

<?php
/**
* Tom's Java Jive - Customer Loyalty Program
*
* Handles loyalty tiers, points, and rewards
*/
class LoyaltyProgram {
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)
/**
* Get all tier definitions
*/
public function getTiers(): array {
return $this->tiers;
}
/**
* Get a specific tier
*/
public function getTier(string $tierKey): ?array {
return $this->tiers[$tierKey] ?? null;
}
/**
* Determine customer's tier based on total points earned (lifetime)
*/
public function calculateTier(int $lifetimePoints): string {
$currentTier = 'bronze';
foreach ($this->tiers as $key => $tier) {
if ($lifetimePoints >= $tier['min_points']) {
$currentTier = $key;
}
}
return $currentTier;
}
/**
* Get customer's current tier info
*/
public function getCustomerTier(string $customerId): array {
$customer = db()->fetch(
"SELECT reward_points, lifetime_points, loyalty_tier FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer) {
return ['tier' => 'bronze', 'info' => $this->tiers['bronze'], 'points' => 0];
}
$lifetimePoints = $customer['lifetime_points'] ?? $customer['reward_points'] ?? 0;
$tierKey = $this->calculateTier($lifetimePoints);
$tier = $this->tiers[$tierKey];
// Calculate progress to next tier
$nextTierKey = $this->getNextTier($tierKey);
$nextTier = $nextTierKey ? $this->tiers[$nextTierKey] : null;
$progress = 100;
$pointsToNext = 0;
if ($nextTier) {
$currentMin = $tier['min_points'];
$nextMin = $nextTier['min_points'];
$pointsToNext = $nextMin - $lifetimePoints;
$progress = min(100, (($lifetimePoints - $currentMin) / ($nextMin - $currentMin)) * 100);
}
return [
'tier' => $tierKey,
'info' => $tier,
'points' => $customer['reward_points'] ?? 0,
'lifetime_points' => $lifetimePoints,
'next_tier' => $nextTierKey,
'next_tier_info' => $nextTier,
'points_to_next' => $pointsToNext,
'progress_percent' => round($progress, 1)
];
}
/**
* Get next tier key
*/
private function getNextTier(string $currentTier): ?string {
$keys = array_keys($this->tiers);
$index = array_search($currentTier, $keys);
return isset($keys[$index + 1]) ? $keys[$index + 1] : null;
}
/**
* Award points for a purchase
*/
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 = (int) floor($amount);
$bonusPoints = (int) floor($basePoints * ($multiplier - 1));
$totalPoints = $basePoints + $bonusPoints;
// Update customer points
db()->query(
"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,
'balance_after' => $newBalance,
'type' => 'earn',
'description' => $description . ($bonusPoints > 0 ? " (+{$bonusPoints} bonus)" : ''),
'reference_amount' => $amount,
'order_id' => $orderId ?: null,
]);
// Check for tier upgrade
$newTier = $this->checkTierUpgrade($customerId, $customerTier['tier']);
return [
'points_earned' => $totalPoints,
'base_points' => $basePoints,
'bonus_points' => $bonusPoints,
'multiplier' => $multiplier,
'tier_upgraded' => $newTier !== null,
'new_tier' => $newTier
];
}
/**
* Redeem points for credit
*/
public function redeemPoints(string $customerId, int $points): array {
$customer = db()->fetch(
"SELECT reward_points FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer || $customer['reward_points'] < $points) {
return ['success' => false, 'error' => 'Insufficient points'];
}
$creditValue = $points * $this->pointsToValue;
// Deduct points
db()->query(
"UPDATE customers SET reward_points = reward_points - :points, updated_at = NOW() WHERE customer_id = :id",
['points' => $points, 'id' => $customerId]
);
// Log the redemption
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $customerId,
'points' => -$points,
'type' => 'redeem',
'description' => "Redeemed for " . formatCurrency($creditValue) . " credit",
'reference_amount' => $creditValue
]);
// Add to wallet
$newBalance = db()->fetch(
"SELECT wallet_balance FROM customers WHERE customer_id = :id",
['id' => $customerId]
)['wallet_balance'] ?? 0;
$newBalance += $creditValue;
db()->query(
"UPDATE customers SET wallet_balance = :balance WHERE customer_id = :id",
['balance' => $newBalance, 'id' => $customerId]
);
// Log wallet transaction
db()->insert('wallet_transactions', [
'transaction_id' => generateId('wt_'),
'customer_id' => $customerId,
'amount' => $creditValue,
'balance_after' => $newBalance,
'type' => 'loyalty_redemption',
'description' => "Redeemed {$points} loyalty points"
]);
return [
'success' => true,
'points_redeemed' => $points,
'credit_value' => $creditValue,
'new_points_balance' => $customer['reward_points'] - $points,
'new_wallet_balance' => $newBalance
];
}
/**
* Check and process tier upgrade
*/
public function checkTierUpgrade(string $customerId, string $currentTier): ?string {
$customer = db()->fetch(
"SELECT lifetime_points, loyalty_tier FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer) {
return null;
}
$calculatedTier = $this->calculateTier($customer['lifetime_points'] ?? 0);
$storedTier = $customer['loyalty_tier'] ?? 'bronze';
// Compare tier levels
$tierOrder = ['bronze', 'silver', 'gold', 'platinum'];
$calculatedIndex = array_search($calculatedTier, $tierOrder);
$storedIndex = array_search($storedTier, $tierOrder);
if ($calculatedIndex > $storedIndex) {
// Upgrade!
db()->query(
"UPDATE customers SET loyalty_tier = :tier, updated_at = NOW() WHERE customer_id = :id",
['tier' => $calculatedTier, 'id' => $customerId]
);
// Log the upgrade
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $customerId,
'points' => 0,
'type' => 'tier_upgrade',
'description' => "Upgraded from {$this->tiers[$storedTier]['name']} to {$this->tiers[$calculatedTier]['name']}"
]);
// Send notifications
$this->sendTierUpgradeNotifications($customerId, $calculatedTier);
return $calculatedTier;
}
return null;
}
/**
* Send tier upgrade notifications
*/
private function sendTierUpgradeNotifications(string $customerId, string $newTier): void {
$customer = db()->fetch(
"SELECT email, phone, name FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer) return;
$tierInfo = $this->tiers[$newTier];
// Send email notification
if (!empty($customer['email'])) {
require_once __DIR__ . '/email.php';
// Custom email for tier upgrade would go here
}
// Send SMS notification
if (!empty($customer['phone'])) {
require_once __DIR__ . '/sms.php';
sendSMS()->sendTierUpgrade($customer['phone'], $tierInfo['name']);
}
// Send push notification
require_once __DIR__ . '/push.php';
pushNotify()->sendTierNotification($customerId, $tierInfo['name'], $tierInfo['benefits']);
}
/**
* Get points conversion info
*/
public function getConversionInfo(): array {
return [
'points_per_dollar' => 1,
'points_value' => $this->pointsToValue,
'points_for_one_dollar' => intval(1 / $this->pointsToValue),
'description' => 'Earn 1 point for every $1 spent. Redeem 100 points for $1 credit.'
];
}
/**
* Get customer's loyalty history
*/
public function getHistory(string $customerId, int $limit = 20): array {
return db()->fetchAll(
"SELECT * FROM loyalty_transactions WHERE customer_id = :id ORDER BY created_at DESC LIMIT :limit",
['id' => $customerId, 'limit' => $limit]
);
}
/**
* Award birthday bonus
*/
public function awardBirthdayBonus(string $customerId): array {
$customerTier = $this->getCustomerTier($customerId);
// Bonus points based on tier
$bonusPoints = match($customerTier['tier']) {
'platinum' => 500,
'gold' => 300,
'silver' => 200,
default => 100
};
db()->query(
"UPDATE customers SET reward_points = reward_points + :points WHERE customer_id = :id",
['points' => $bonusPoints, 'id' => $customerId]
);
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $customerId,
'points' => $bonusPoints,
'type' => 'birthday_bonus',
'description' => "Birthday reward - Happy Birthday!"
]);
return ['success' => true, 'points' => $bonusPoints];
}
/**
* Award referral bonus
*/
public function awardReferralBonus(string $referrerId, string $newCustomerId): array {
$referrerBonus = 100;
$newCustomerBonus = 50;
// Award to referrer
db()->query(
"UPDATE customers SET reward_points = reward_points + :points WHERE customer_id = :id",
['points' => $referrerBonus, 'id' => $referrerId]
);
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $referrerId,
'points' => $referrerBonus,
'type' => 'referral_bonus',
'description' => "Referral bonus - Thank you for spreading the word!"
]);
// Award to new customer
db()->query(
"UPDATE customers SET reward_points = reward_points + :points WHERE customer_id = :id",
['points' => $newCustomerBonus, 'id' => $newCustomerId]
);
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $newCustomerId,
'points' => $newCustomerBonus,
'type' => 'referral_welcome',
'description' => "Welcome bonus from referral"
]);
return [
'referrer_bonus' => $referrerBonus,
'new_customer_bonus' => $newCustomerBonus
];
}
}
// Helper function
function loyalty(): LoyaltyProgram {
static $instance = null;
if ($instance === null) {
$instance = new LoyaltyProgram();
}
return $instance;
}