mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-07-27 16:52:36 -05:00
Wire up wallet balance and gift cards at checkout; fix Square gift card option; fix account/rewards.php CSS; fix real PDO/schema bugs found along the way
Feature: checkout.php now lets logged-in customers apply existing wallet balance or redeem a gift card code (which tops up wallet first, then applies) toward their order total. Deduction is deferred to payment success (markSquarePaymentResult in includes/square.php), never at order creation, so an abandoned checkout never loses real wallet money - mirrors how loyalty points already work here, unlike stock which is decremented eagerly. payment.php gains a second Square Gift Card tab (payments.giftCard() SDK method) alongside the card form, both hitting the same create-square-payment.php endpoint since Square treats both source types identically. New api/apply-wallet-credit.php validates/quotes an amount without writing anything - actual spend happens only via markSquarePaymentResult(). The gift-card-to-wallet transaction logic was extracted out of api/redeem-gift-card.php into a shared loyalty()->redeemGiftCardToWallet() so the Wallet page and checkout both call the same code. Also fixed three unrelated pre-existing bugs surfaced while testing this: - loyalty.php awardPoints() reused the same named PDO parameter (:points) twice in one UPDATE - fails under real prepared statements, meaning loyalty points (and the email sent right after them) were silently never awarded for any order tied to a logged-in customer. - redeemGiftCardToWallet (formerly inline in redeem-gift-card.php) referenced a gift_cards.updated_at column that does not exist in the schema, and used invalid enum values (gift_card_transactions.type=redeem, wallet_transactions.type=gift_card) that do not match the actual enum definitions - gift card redemption has likely never worked at all. - account/rewards.php was missing the line that loads account.css, unlike every other account/*.php page, so its sidebar/layout rendered unstyled.
This commit is contained in:
@@ -37,6 +37,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['redeem_points'])) {
|
||||
redirect('/account/rewards.php');
|
||||
}
|
||||
|
||||
$extraHead = '<link rel="stylesheet" href="/assets/css/account.css?v='. filemtime(__DIR__ . '/../assets/css/account.css') .'">';
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
require_once __DIR__ . '/includes/sidebar.php';
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Apply Wallet Credit / Gift Card at Checkout
|
||||
*
|
||||
* Validates and quotes an amount to apply toward the current order total.
|
||||
* Does NOT deduct wallet balance here - that only happens once the Square
|
||||
* payment actually succeeds (see markSquarePaymentResult() in
|
||||
* includes/square.php), so an abandoned checkout never loses a customer's
|
||||
* real wallet money. A gift card code, if given, is redeemed into wallet
|
||||
* balance immediately (same as the Wallet page does) since that step is
|
||||
* safe/reversible-in-spirit - only the *spending* of wallet balance at
|
||||
* checkout is deferred to payment success.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/loyalty.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['error' => '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)
|
||||
]);
|
||||
+10
-73
@@ -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!'
|
||||
]);
|
||||
|
||||
+162
-37
@@ -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';
|
||||
<section class="section" style="padding-top: 2rem;">
|
||||
<div class="container">
|
||||
<h1 style="margin-bottom: 2rem;">Checkout</h1>
|
||||
|
||||
|
||||
<form method="POST" action="" id="checkout-form">
|
||||
<input type="hidden" name="wallet_amount_used" id="wallet_amount_used" value="0">
|
||||
<div class="checkout-layout">
|
||||
|
||||
|
||||
<!-- Customer & Shipping Info -->
|
||||
<div>
|
||||
<!-- Contact Information -->
|
||||
@@ -184,76 +197,76 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<?php else: ?>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email Address *</label>
|
||||
<input type="email" name="email" class="form-input"
|
||||
<input type="email" name="email" class="form-input"
|
||||
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" required>
|
||||
<?php if (isset($errors['email'])): ?>
|
||||
<span class="form-error"><?= $errors['email'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Full Name *</label>
|
||||
<input type="text" name="name" class="form-input"
|
||||
<input type="text" name="name" class="form-input"
|
||||
value="<?= htmlspecialchars($_POST['name'] ?? '') ?>" required>
|
||||
<?php if (isset($errors['name'])): ?>
|
||||
<span class="form-error"><?= $errors['name'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Phone (Optional)</label>
|
||||
<input type="tel" name="phone" class="form-input"
|
||||
<input type="tel" name="phone" class="form-input"
|
||||
value="<?= htmlspecialchars($_POST['phone'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
|
||||
<p class="text-muted mt-1" style="font-size: 0.875rem;">
|
||||
Already have an account? <a href="/login.php">Sign in</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Shipping Address -->
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">
|
||||
<h3 style="margin: 0;">Shipping Address</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php
|
||||
<?php
|
||||
$savedAddress = $customer ? json_decode($customer['shipping_address'] ?? '{}', true) : [];
|
||||
?>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Street Address *</label>
|
||||
<input type="text" name="address" class="form-input"
|
||||
<input type="text" name="address" class="form-input"
|
||||
value="<?= htmlspecialchars($_POST['address'] ?? $savedAddress['address'] ?? '') ?>" required>
|
||||
<?php if (isset($errors['address'])): ?>
|
||||
<span class="form-error"><?= $errors['address'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="address-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">City *</label>
|
||||
<input type="text" name="city" class="form-input"
|
||||
<input type="text" name="city" class="form-input"
|
||||
value="<?= htmlspecialchars($_POST['city'] ?? $savedAddress['city'] ?? '') ?>" required>
|
||||
<?php if (isset($errors['city'])): ?>
|
||||
<span class="form-error"><?= $errors['city'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">State *</label>
|
||||
<input type="text" name="state" class="form-input"
|
||||
<input type="text" name="state" class="form-input"
|
||||
value="<?= htmlspecialchars($_POST['state'] ?? $savedAddress['state'] ?? '') ?>" required>
|
||||
<?php if (isset($errors['state'])): ?>
|
||||
<span class="form-error"><?= $errors['state'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">ZIP *</label>
|
||||
<input type="text" name="zip" class="form-input"
|
||||
<input type="text" name="zip" class="form-input"
|
||||
value="<?= htmlspecialchars($_POST['zip'] ?? $savedAddress['zip'] ?? '') ?>" required>
|
||||
<?php if (isset($errors['zip'])): ?>
|
||||
<span class="form-error"><?= $errors['zip'] ?></span>
|
||||
@@ -262,7 +275,39 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php if ($customer): ?>
|
||||
<!-- Wallet / Gift Card -->
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">
|
||||
<h3 style="margin: 0;">Wallet & Gift Cards</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 1rem;">
|
||||
Wallet balance: <strong id="wallet-balance-display"><?= formatCurrency($customer['wallet_balance'] ?? 0) ?></strong>
|
||||
</p>
|
||||
<?php if (($customer['wallet_balance'] ?? 0) > 0): ?>
|
||||
<button type="button" id="apply-wallet-btn" class="btn btn-secondary mb-1">
|
||||
<i class="fas fa-wallet"></i> Apply Wallet Balance
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<div class="form-group" style="display: flex; gap: 0.5rem; align-items: flex-end;">
|
||||
<div style="flex: 1;">
|
||||
<label class="form-label">Gift Card Code</label>
|
||||
<input type="text" id="gift-card-code" class="form-input"
|
||||
placeholder="XXXX-XXXX-XXXX" style="text-transform: uppercase;">
|
||||
</div>
|
||||
<button type="button" id="apply-gift-card-btn" class="btn btn-secondary">Apply</button>
|
||||
</div>
|
||||
<div id="wallet-message" style="margin-top: 0.5rem;"></div>
|
||||
<div id="wallet-applied-row" style="display: none; margin-top: 0.5rem; font-weight: 600;">
|
||||
Applied: <span id="wallet-applied-amount"></span>
|
||||
<button type="button" id="remove-wallet-btn" class="btn btn-secondary" style="margin-left: 0.5rem; padding: 0.15rem 0.6rem; font-size: 0.8rem;">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Order Notes -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -274,7 +319,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Order Summary -->
|
||||
<div class="card checkout-summary">
|
||||
<div class="card-header">
|
||||
@@ -313,12 +358,16 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="checkout-summary-row" id="wallet-discount-row" style="display: none;">
|
||||
<span>Wallet / Gift Card</span>
|
||||
<span id="wallet-discount-amount" class="text-success"></span>
|
||||
</div>
|
||||
|
||||
<hr style="margin: 1rem 0;">
|
||||
|
||||
<div class="checkout-summary-total">
|
||||
<span>Total</span>
|
||||
<span><?= formatCurrency($total) ?></span>
|
||||
<span id="checkout-total-display"><?= formatCurrency($preDiscountTotal) ?></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block mt-2">
|
||||
@@ -330,7 +379,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</a>
|
||||
|
||||
<p class="secure-badge">
|
||||
<i class="fas fa-lock"></i> Secure checkout powered by Stripe
|
||||
<i class="fas fa-lock"></i> Secure checkout powered by <?= $processor === 'square' ? 'Square' : 'Stripe' ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,4 +388,80 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($customer): ?>
|
||||
<script>
|
||||
const ORDER_TOTAL_PRE_DISCOUNT = <?= json_encode($preDiscountTotal) ?>;
|
||||
const CURRENCY_SYMBOL = <?= json_encode(defined('TJJ_CURRENCY_SYMBOL') ? TJJ_CURRENCY_SYMBOL : '$') ?>;
|
||||
|
||||
function fmt(amount) {
|
||||
return CURRENCY_SYMBOL + Number(amount).toFixed(2);
|
||||
}
|
||||
|
||||
function showWalletMessage(msg, isError) {
|
||||
const el = document.getElementById('wallet-message');
|
||||
el.textContent = msg;
|
||||
el.style.color = isError ? 'var(--color-error)' : 'var(--color-success)';
|
||||
}
|
||||
|
||||
function applyWalletResult(data) {
|
||||
document.getElementById('wallet_amount_used').value = data.apply_amount;
|
||||
document.getElementById('checkout-total-display').textContent = fmt(data.new_total);
|
||||
document.getElementById('wallet-balance-display').textContent = fmt(data.wallet_balance);
|
||||
|
||||
if (data.apply_amount > 0) {
|
||||
document.getElementById('wallet-discount-row').style.display = '';
|
||||
document.getElementById('wallet-discount-amount').textContent = '-' + fmt(data.apply_amount);
|
||||
document.getElementById('wallet-applied-row').style.display = '';
|
||||
document.getElementById('wallet-applied-amount').textContent = fmt(data.apply_amount);
|
||||
} else {
|
||||
document.getElementById('wallet-discount-row').style.display = 'none';
|
||||
document.getElementById('wallet-applied-row').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function applyCredit(body) {
|
||||
try {
|
||||
const response = await fetch('/api/apply-wallet-credit.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(Object.assign({ order_total: ORDER_TOTAL_PRE_DISCOUNT }, body))
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
showWalletMessage(data.error, true);
|
||||
return;
|
||||
}
|
||||
showWalletMessage('Applied ' + fmt(data.apply_amount) + ' to your order.', false);
|
||||
applyWalletResult(data);
|
||||
} catch (err) {
|
||||
showWalletMessage('Failed to apply credit. Please try again.', true);
|
||||
}
|
||||
}
|
||||
|
||||
const applyWalletBtn = document.getElementById('apply-wallet-btn');
|
||||
if (applyWalletBtn) {
|
||||
applyWalletBtn.addEventListener('click', function() {
|
||||
applyCredit({});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('apply-gift-card-btn').addEventListener('click', function() {
|
||||
const code = document.getElementById('gift-card-code').value.trim();
|
||||
if (!code) {
|
||||
showWalletMessage('Enter a gift card code first.', true);
|
||||
return;
|
||||
}
|
||||
applyCredit({ gift_card_code: code });
|
||||
});
|
||||
|
||||
document.getElementById('remove-wallet-btn').addEventListener('click', function() {
|
||||
document.getElementById('wallet_amount_used').value = 0;
|
||||
document.getElementById('checkout-total-display').textContent = fmt(ORDER_TOTAL_PRE_DISCOUNT);
|
||||
document.getElementById('wallet-discount-row').style.display = 'none';
|
||||
document.getElementById('wallet-applied-row').style.display = 'none';
|
||||
showWalletMessage('', false);
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
+82
-2
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
+100
-3
@@ -86,6 +86,15 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="payment-options mb-2" style="display: flex; gap: 0.5rem;">
|
||||
<button type="button" id="pay-tab-card" class="btn btn-primary" style="flex: 1;">
|
||||
<i class="fas fa-credit-card"></i> Card
|
||||
</button>
|
||||
<button type="button" id="pay-tab-giftcard" class="btn btn-secondary" style="flex: 1;">
|
||||
<i class="fas fa-gift"></i> Gift Card
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="payment-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Card Details</label>
|
||||
@@ -100,6 +109,21 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form id="gift-card-payment-form" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Square Gift Card</label>
|
||||
<div id="gift-card-element" style="padding: 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius-md);"></div>
|
||||
<div id="gift-card-errors" class="form-error" style="margin-top: 0.5rem;"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="gift-card-submit-button" class="btn btn-secondary btn-lg btn-block">
|
||||
<span id="gift-card-button-text">Pay <?= formatCurrency($total) ?></span>
|
||||
<span id="gift-card-spinner" style="display: none;">
|
||||
<span class="loading"></span> Processing...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<?php if (!$stripeConfigured): ?>
|
||||
@@ -203,13 +227,86 @@ function wireDemoForm(endpoint) {
|
||||
wireDemoForm('/api/create-square-payment.php');
|
||||
<?php else: ?>
|
||||
// Square Web Payments SDK
|
||||
let squareCard;
|
||||
let squareCard, squareGiftCard;
|
||||
const squarePayments = window.Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>');
|
||||
(async function initSquare() {
|
||||
const payments = window.Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>');
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user