diff --git a/account/rewards.php b/account/rewards.php index cb7f900..e22fd91 100644 --- a/account/rewards.php +++ b/account/rewards.php @@ -37,6 +37,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['redeem_points'])) { redirect('/account/rewards.php'); } +$extraHead = ''; require_once __DIR__ . '/../includes/header.php'; require_once __DIR__ . '/includes/sidebar.php'; ?> diff --git a/api/apply-wallet-credit.php b/api/apply-wallet-credit.php new file mode 100644 index 0000000..aa1073e --- /dev/null +++ b/api/apply-wallet-credit.php @@ -0,0 +1,58 @@ + 'Method not allowed'], 405); +} + +if (!CustomerAuth::isLoggedIn()) { + jsonResponse(['error' => 'Please log in to use wallet balance or a gift card'], 401); +} + +$customer = CustomerAuth::getFullUser(); +$input = json_decode(file_get_contents('php://input'), true); + +$orderTotal = (float) ($input['order_total'] ?? 0); +if ($orderTotal <= 0) { + jsonResponse(['error' => 'Invalid order total'], 400); +} + +$giftCardCode = trim($input['gift_card_code'] ?? ''); + +if (!empty($giftCardCode)) { + $redeemResult = loyalty()->redeemGiftCardToWallet($customer['customer_id'], $giftCardCode); + if (!$redeemResult['success']) { + jsonResponse(['error' => $redeemResult['error']], 400); + } + $walletBalance = $redeemResult['new_balance']; +} else { + $fresh = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $customer['customer_id']]); + $walletBalance = (float) ($fresh['wallet_balance'] ?? 0); +} + +$requested = isset($input['amount']) ? (float) $input['amount'] : $walletBalance; +$applyAmount = max(0, round(min($requested, $walletBalance, $orderTotal), 2)); + +jsonResponse([ + 'success' => true, + 'apply_amount' => $applyAmount, + 'wallet_balance' => $walletBalance, + 'new_total' => round($orderTotal - $applyAmount, 2) +]); diff --git a/api/redeem-gift-card.php b/api/redeem-gift-card.php index c3e6854..0e37830 100644 --- a/api/redeem-gift-card.php +++ b/api/redeem-gift-card.php @@ -7,6 +7,7 @@ header('Content-Type: application/json'); require_once __DIR__ . '/../includes/functions.php'; require_once __DIR__ . '/../includes/auth.php'; +require_once __DIR__ . '/../includes/loyalty.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['error' => 'Method not allowed'], 405); @@ -19,79 +20,15 @@ if (!CustomerAuth::isLoggedIn()) { $customer = CustomerAuth::getFullUser(); $input = json_decode(file_get_contents('php://input'), true); -$code = strtoupper(str_replace(['-', ' '], '', trim($input['code'] ?? ''))); +$result = loyalty()->redeemGiftCardToWallet($customer['customer_id'], $input['code'] ?? ''); -if (empty($code) || strlen($code) < 8) { - jsonResponse(['error' => 'Invalid gift card code'], 400); +if (!$result['success']) { + jsonResponse(['error' => $result['error']], 400); } -// Find gift card -$giftCard = db()->fetch( - "SELECT * FROM gift_cards WHERE code = :code AND is_active = 1", - ['code' => $code] -); - -if (!$giftCard) { - jsonResponse(['error' => 'Gift card not found or already used'], 404); -} - -if ($giftCard['current_balance'] <= 0) { - jsonResponse(['error' => 'This gift card has no remaining balance'], 400); -} - -if ($giftCard['expires_at'] && strtotime($giftCard['expires_at']) < time()) { - jsonResponse(['error' => 'This gift card has expired'], 400); -} - -$amount = $giftCard['current_balance']; - -try { - // Start transaction - db()->query("START TRANSACTION"); - - // Update gift card balance to 0 - db()->query( - "UPDATE gift_cards SET current_balance = 0, is_active = 0, updated_at = NOW() WHERE gift_card_id = :id", - ['id' => $giftCard['gift_card_id']] - ); - - // Log gift card transaction - db()->insert('gift_card_transactions', [ - 'gift_card_id' => $giftCard['gift_card_id'], - 'amount' => -$amount, - 'balance_after' => 0, - 'type' => 'redeem', - 'description' => 'Redeemed by customer: ' . $customer['email'] - ]); - - // Add to customer wallet - $newWalletBalance = ($customer['wallet_balance'] ?? 0) + $amount; - - db()->query( - "UPDATE customers SET wallet_balance = :balance, updated_at = NOW() WHERE customer_id = :id", - ['balance' => $newWalletBalance, 'id' => $customer['customer_id']] - ); - - // Log wallet transaction - db()->insert('wallet_transactions', [ - 'transaction_id' => generateId('wt_'), - 'customer_id' => $customer['customer_id'], - 'amount' => $amount, - 'balance_after' => $newWalletBalance, - 'type' => 'gift_card', - 'description' => 'Gift card redeemed: ' . $code - ]); - - db()->query("COMMIT"); - - jsonResponse([ - 'success' => true, - 'amount' => $amount, - 'new_balance' => $newWalletBalance, - 'message' => formatCurrency($amount) . ' has been added to your wallet!' - ]); - -} catch (Exception $e) { - db()->query("ROLLBACK"); - jsonResponse(['error' => 'Failed to redeem gift card. Please try again.'], 500); -} +jsonResponse([ + 'success' => true, + 'amount' => $result['amount'], + 'new_balance' => $result['new_balance'], + 'message' => formatCurrency($result['amount']) . ' has been added to your wallet!' +]); diff --git a/checkout.php b/checkout.php index 5673ed0..e10f0b6 100644 --- a/checkout.php +++ b/checkout.php @@ -6,6 +6,7 @@ $pageTitle = "Checkout - Tom's Java Jive"; require_once __DIR__ . '/includes/functions.php'; require_once __DIR__ . '/includes/auth.php'; +require_once __DIR__ . '/includes/loyalty.php'; $cart = getCart(); if (empty($cart)) { @@ -22,7 +23,7 @@ foreach ($cart as $productId => $quantity) { "SELECT product_id, name, price, sale_price, stock, images FROM products WHERE product_id = :id AND is_active = 1", ['id' => $productId] ); - + if ($product) { $images = json_decode($product['images'] ?? '[]', true); $product['image'] = !empty($images) ? $images[0] : '/assets/images/placeholder-product.svg'; @@ -50,10 +51,8 @@ if ($shippingSettings['flat_rate_enabled'] ?? true) { } } -$total = $subtotal + $shippingCost; - -// Get Stripe publishable key -$stripeKey = STRIPE_PUBLISHABLE_KEY; +$preDiscountTotal = $subtotal + $shippingCost; +$processor = defined('PAYMENT_PROCESSOR') ? PAYMENT_PROCESSOR : 'stripe'; $errors = []; @@ -67,19 +66,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $city = trim($_POST['city'] ?? ''); $state = trim($_POST['state'] ?? ''); $zip = trim($_POST['zip'] ?? ''); - + if (empty($email)) $errors['email'] = 'Email is required'; if (empty($name)) $errors['name'] = 'Name is required'; if (empty($address)) $errors['address'] = 'Address is required'; if (empty($city)) $errors['city'] = 'City is required'; if (empty($state)) $errors['state'] = 'State is required'; if (empty($zip)) $errors['zip'] = 'ZIP code is required'; - + + // Re-validate any requested wallet credit server-side against the + // customer's current balance - never trust the client-submitted amount. + $walletAmountUsed = 0; + if ($customer && !empty($_POST['wallet_amount_used'])) { + $requestedWallet = (float) $_POST['wallet_amount_used']; + $freshCustomer = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $customer['customer_id']]); + $currentBalance = (float) ($freshCustomer['wallet_balance'] ?? 0); + $walletAmountUsed = max(0, round(min($requestedWallet, $currentBalance, $preDiscountTotal), 2)); + } + + $total = round($preDiscountTotal - $walletAmountUsed, 2); + if (empty($errors)) { // Create order $orderId = generateId('ord_'); $orderNumber = generateOrderNumber(); - + // Get or create customer $customerId = null; if ($customer) { @@ -87,7 +98,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } else { $customerId = CustomerAuth::createGuest($email, $name, $phone); } - + // Prepare order items $orderItems = []; foreach ($cartItems as $item) { @@ -99,7 +110,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 'total' => $item['total'] ]; } - + // Insert order db()->insert('orders', [ 'order_id' => $orderId, @@ -111,6 +122,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 'items' => json_encode($orderItems), 'subtotal' => $subtotal, 'shipping_cost' => $shippingCost, + 'wallet_amount_used' => $walletAmountUsed, 'total' => $total, 'shipping_address' => json_encode([ 'address' => $address, @@ -120,11 +132,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 'country' => 'USA' ]), 'shipping_method' => 'standard', - 'payment_method' => 'stripe', + 'payment_method' => $processor, 'payment_status' => 'pending', 'order_status' => 'pending' ]); - + // Insert order items for reporting foreach ($orderItems as $item) { db()->insert('order_items', [ @@ -136,7 +148,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 'total' => $item['total'] ]); } - + // Reduce stock foreach ($cartItems as $item) { db()->query( @@ -144,10 +156,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ['qty' => $item['quantity'], 'id' => $item['product_id']] ); } - + // Store order ID for payment $_SESSION['pending_order_id'] = $orderId; - + // Redirect to payment page redirect('/payment.php?order=' . $orderId); } @@ -165,10 +177,11 @@ require_once __DIR__ . '/includes/header.php';

Checkout

- +
+
- +
@@ -184,76 +197,76 @@ require_once __DIR__ . '/includes/header.php';
-
- +
-
- +
-
- +

Already have an account? Sign in

- +

Shipping Address

- - +
-
- +
-
- +
-
- +
- @@ -262,7 +275,39 @@ require_once __DIR__ . '/includes/header.php';
- + + + +
+
+

Wallet & Gift Cards

+
+
+

+ Wallet balance: +

+ 0): ?> + + +
+
+ + +
+ +
+
+ +
+
+ +
@@ -274,7 +319,7 @@ require_once __DIR__ . '/includes/header.php';
- +
@@ -313,12 +358,16 @@ require_once __DIR__ . '/includes/header.php';
+
Total - +
@@ -339,4 +388,80 @@ require_once __DIR__ . '/includes/header.php';
+ + + + diff --git a/includes/loyalty.php b/includes/loyalty.php index b6c7592..925d43f 100644 --- a/includes/loyalty.php +++ b/includes/loyalty.php @@ -150,10 +150,10 @@ class LoyaltyProgram { db()->query( "UPDATE customers SET reward_points = reward_points + :points, - lifetime_points = COALESCE(lifetime_points, 0) + :points, + lifetime_points = COALESCE(lifetime_points, 0) + :points2, updated_at = NOW() WHERE customer_id = :id", - ['points' => $totalPoints, 'id' => $customerId] + ['points' => $totalPoints, 'points2' => $totalPoints, 'id' => $customerId] ); $newBalance = db()->fetch( @@ -417,6 +417,86 @@ class LoyaltyProgram { 'new_customer_bonus' => $newCustomerBonus ]; } + + /** + * Redeem a gift card code into wallet balance. Shared by the Wallet page + * and checkout so both go through the same transaction logic instead of + * duplicating it. + */ + public function redeemGiftCardToWallet(string $customerId, string $rawCode): array { + $code = strtoupper(str_replace(['-', ' '], '', trim($rawCode))); + + if (empty($code) || strlen($code) < 8) { + return ['success' => false, 'error' => 'Invalid gift card code']; + } + + $giftCard = db()->fetch( + "SELECT * FROM gift_cards WHERE code = :code AND is_active = 1", + ['code' => $code] + ); + + if (!$giftCard) { + return ['success' => false, 'error' => 'Gift card not found or already used']; + } + + if ($giftCard['current_balance'] <= 0) { + return ['success' => false, 'error' => 'This gift card has no remaining balance']; + } + + if ($giftCard['expires_at'] && strtotime($giftCard['expires_at']) < time()) { + return ['success' => false, 'error' => 'This gift card has expired']; + } + + $customer = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $customerId]); + if (!$customer) { + return ['success' => false, 'error' => 'Customer not found']; + } + + $amount = $giftCard['current_balance']; + + try { + db()->query("START TRANSACTION"); + + db()->query( + "UPDATE gift_cards SET current_balance = 0, is_active = 0 WHERE gift_card_id = :id", + ['id' => $giftCard['gift_card_id']] + ); + + db()->insert('gift_card_transactions', [ + 'gift_card_id' => $giftCard['gift_card_id'], + 'amount' => -$amount, + 'balance_after' => 0, + 'type' => 'redemption', + ]); + + $newWalletBalance = (float) $customer['wallet_balance'] + $amount; + + db()->query( + "UPDATE customers SET wallet_balance = :balance, updated_at = NOW() WHERE customer_id = :id", + ['balance' => $newWalletBalance, 'id' => $customerId] + ); + + db()->insert('wallet_transactions', [ + 'transaction_id' => generateId('wt_'), + 'customer_id' => $customerId, + 'amount' => $amount, + 'balance_after' => $newWalletBalance, + 'type' => 'deposit', + 'description' => 'Gift card redeemed: ' . $code + ]); + + db()->query("COMMIT"); + + return [ + 'success' => true, + 'amount' => $amount, + 'new_balance' => $newWalletBalance + ]; + } catch (Exception $e) { + db()->query("ROLLBACK"); + return ['success' => false, 'error' => 'Failed to redeem gift card. Please try again.']; + } + } } // Helper function diff --git a/includes/square.php b/includes/square.php index 6260cec..ec4b330 100644 --- a/includes/square.php +++ b/includes/square.php @@ -128,6 +128,23 @@ function markSquarePaymentResult(string $paymentId, ?string $orderId, string $st ); } + if (!empty($order['wallet_amount_used']) && (float) $order['wallet_amount_used'] > 0 && !empty($order['customer_id'])) { + $walletAmount = (float) $order['wallet_amount_used']; + $cust = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $order['customer_id']]); + if ($cust) { + $newBalance = max(0, (float) $cust['wallet_balance'] - $walletAmount); + db()->update('customers', ['wallet_balance' => $newBalance], 'customer_id = :id', ['id' => $order['customer_id']]); + db()->insert('wallet_transactions', [ + 'transaction_id' => generateId('wt_'), + 'customer_id' => $order['customer_id'], + 'amount' => -$walletAmount, + 'balance_after' => $newBalance, + 'type' => 'purchase', + 'description' => 'Applied to Order #' . $order['order_number'], + ]); + } + } + $updated = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $order['order_id']]); if ($updated) { emailService()->sendOrderConfirmation($updated); diff --git a/payment.php b/payment.php index d90f2b1..981fe54 100644 --- a/payment.php +++ b/payment.php @@ -86,6 +86,15 @@ require_once __DIR__ . '/includes/header.php'; +
+ + +
+
@@ -100,6 +109,21 @@ require_once __DIR__ . '/includes/header.php'; + + @@ -203,13 +227,86 @@ function wireDemoForm(endpoint) { wireDemoForm('/api/create-square-payment.php'); // Square Web Payments SDK -let squareCard; +let squareCard, squareGiftCard; +const squarePayments = window.Square.payments('', ''); (async function initSquare() { - const payments = window.Square.payments('', ''); - squareCard = await payments.card(); + squareCard = await squarePayments.card(); await squareCard.attach('#card-element'); })(); +const cardTab = document.getElementById('pay-tab-card'); +const giftCardTab = document.getElementById('pay-tab-giftcard'); +const cardForm = document.getElementById('payment-form'); +const giftCardForm = document.getElementById('gift-card-payment-form'); +let giftCardInitialized = false; + +cardTab.addEventListener('click', function() { + cardTab.className = 'btn btn-primary'; + giftCardTab.className = 'btn btn-secondary'; + cardForm.style.display = ''; + giftCardForm.style.display = 'none'; +}); + +giftCardTab.addEventListener('click', async function() { + cardTab.className = 'btn btn-secondary'; + giftCardTab.className = 'btn btn-primary'; + cardForm.style.display = 'none'; + giftCardForm.style.display = ''; + if (!giftCardInitialized) { + giftCardInitialized = true; + squareGiftCard = await squarePayments.giftCard(); + await squareGiftCard.attach('#gift-card-element'); + } +}); + +giftCardForm.addEventListener('submit', async function(e) { + e.preventDefault(); + const submitBtn = document.getElementById('gift-card-submit-button'); + const buttonText = document.getElementById('gift-card-button-text'); + const spinner = document.getElementById('gift-card-spinner'); + submitBtn.disabled = true; + buttonText.style.display = 'none'; + spinner.style.display = 'inline'; + document.getElementById('gift-card-errors').textContent = ''; + + try { + const tokenResult = await squareGiftCard.tokenize(); + if (tokenResult.status !== 'OK') { + const msg = (tokenResult.errors && tokenResult.errors[0] && tokenResult.errors[0].message) || 'Gift card validation failed'; + document.getElementById('gift-card-errors').textContent = msg; + submitBtn.disabled = false; + buttonText.style.display = 'inline'; + spinner.style.display = 'none'; + return; + } + + const response = await fetch('/api/create-square-payment.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order_id: orderId, source_id: tokenResult.token }) + }); + const data = await response.json(); + + if (data.error) { + showMessage(data.error, 'error'); + submitBtn.disabled = false; + buttonText.style.display = 'inline'; + spinner.style.display = 'none'; + return; + } + if (data.success && data.redirect) { + showMessage('Payment successful! Redirecting...', 'success'); + setTimeout(() => window.location.href = data.redirect, 1000); + } + } catch (err) { + showMessage('Payment failed. Please try again.', 'error'); + console.error(err); + submitBtn.disabled = false; + buttonText.style.display = 'inline'; + spinner.style.display = 'none'; + } +}); + const form = document.getElementById('payment-form'); const submitButton = document.getElementById('submit-button'); const buttonText = document.getElementById('button-text');