mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-07-28 09:12: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');
|
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/header.php';
|
||||||
require_once __DIR__ . '/includes/sidebar.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)
|
||||||
|
]);
|
||||||
@@ -7,6 +7,7 @@ header('Content-Type: application/json');
|
|||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
require_once __DIR__ . '/../includes/auth.php';
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
|
require_once __DIR__ . '/../includes/loyalty.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||||
@@ -19,79 +20,15 @@ if (!CustomerAuth::isLoggedIn()) {
|
|||||||
$customer = CustomerAuth::getFullUser();
|
$customer = CustomerAuth::getFullUser();
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$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) {
|
if (!$result['success']) {
|
||||||
jsonResponse(['error' => 'Invalid gift card code'], 400);
|
jsonResponse(['error' => $result['error']], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find gift card
|
jsonResponse([
|
||||||
$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,
|
'success' => true,
|
||||||
'amount' => $amount,
|
'amount' => $result['amount'],
|
||||||
'new_balance' => $newWalletBalance,
|
'new_balance' => $result['new_balance'],
|
||||||
'message' => formatCurrency($amount) . ' has been added to your wallet!'
|
'message' => formatCurrency($result['amount']) . ' has been added to your wallet!'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
db()->query("ROLLBACK");
|
|
||||||
jsonResponse(['error' => 'Failed to redeem gift card. Please try again.'], 500);
|
|
||||||
}
|
|
||||||
|
|||||||
+132
-7
@@ -6,6 +6,7 @@
|
|||||||
$pageTitle = "Checkout - Tom's Java Jive";
|
$pageTitle = "Checkout - Tom's Java Jive";
|
||||||
require_once __DIR__ . '/includes/functions.php';
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
require_once __DIR__ . '/includes/auth.php';
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/loyalty.php';
|
||||||
|
|
||||||
$cart = getCart();
|
$cart = getCart();
|
||||||
if (empty($cart)) {
|
if (empty($cart)) {
|
||||||
@@ -50,10 +51,8 @@ if ($shippingSettings['flat_rate_enabled'] ?? true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = $subtotal + $shippingCost;
|
$preDiscountTotal = $subtotal + $shippingCost;
|
||||||
|
$processor = defined('PAYMENT_PROCESSOR') ? PAYMENT_PROCESSOR : 'stripe';
|
||||||
// Get Stripe publishable key
|
|
||||||
$stripeKey = STRIPE_PUBLISHABLE_KEY;
|
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
@@ -75,6 +74,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (empty($state)) $errors['state'] = 'State is required';
|
if (empty($state)) $errors['state'] = 'State is required';
|
||||||
if (empty($zip)) $errors['zip'] = 'ZIP code 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)) {
|
if (empty($errors)) {
|
||||||
// Create order
|
// Create order
|
||||||
$orderId = generateId('ord_');
|
$orderId = generateId('ord_');
|
||||||
@@ -111,6 +122,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
'items' => json_encode($orderItems),
|
'items' => json_encode($orderItems),
|
||||||
'subtotal' => $subtotal,
|
'subtotal' => $subtotal,
|
||||||
'shipping_cost' => $shippingCost,
|
'shipping_cost' => $shippingCost,
|
||||||
|
'wallet_amount_used' => $walletAmountUsed,
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
'shipping_address' => json_encode([
|
'shipping_address' => json_encode([
|
||||||
'address' => $address,
|
'address' => $address,
|
||||||
@@ -120,7 +132,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
'country' => 'USA'
|
'country' => 'USA'
|
||||||
]),
|
]),
|
||||||
'shipping_method' => 'standard',
|
'shipping_method' => 'standard',
|
||||||
'payment_method' => 'stripe',
|
'payment_method' => $processor,
|
||||||
'payment_status' => 'pending',
|
'payment_status' => 'pending',
|
||||||
'order_status' => 'pending'
|
'order_status' => 'pending'
|
||||||
]);
|
]);
|
||||||
@@ -167,6 +179,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<h1 style="margin-bottom: 2rem;">Checkout</h1>
|
<h1 style="margin-bottom: 2rem;">Checkout</h1>
|
||||||
|
|
||||||
<form method="POST" action="" id="checkout-form">
|
<form method="POST" action="" id="checkout-form">
|
||||||
|
<input type="hidden" name="wallet_amount_used" id="wallet_amount_used" value="0">
|
||||||
<div class="checkout-layout">
|
<div class="checkout-layout">
|
||||||
|
|
||||||
<!-- Customer & Shipping Info -->
|
<!-- Customer & Shipping Info -->
|
||||||
@@ -263,6 +276,38 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</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 -->
|
<!-- Order Notes -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -313,12 +358,16 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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;">
|
<hr style="margin: 1rem 0;">
|
||||||
|
|
||||||
<div class="checkout-summary-total">
|
<div class="checkout-summary-total">
|
||||||
<span>Total</span>
|
<span>Total</span>
|
||||||
<span><?= formatCurrency($total) ?></span>
|
<span id="checkout-total-display"><?= formatCurrency($preDiscountTotal) ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg btn-block mt-2">
|
<button type="submit" class="btn btn-primary btn-lg btn-block mt-2">
|
||||||
@@ -330,7 +379,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p class="secure-badge">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,4 +388,80 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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'; ?>
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|||||||
+82
-2
@@ -150,10 +150,10 @@ class LoyaltyProgram {
|
|||||||
db()->query(
|
db()->query(
|
||||||
"UPDATE customers SET
|
"UPDATE customers SET
|
||||||
reward_points = reward_points + :points,
|
reward_points = reward_points + :points,
|
||||||
lifetime_points = COALESCE(lifetime_points, 0) + :points,
|
lifetime_points = COALESCE(lifetime_points, 0) + :points2,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE customer_id = :id",
|
WHERE customer_id = :id",
|
||||||
['points' => $totalPoints, 'id' => $customerId]
|
['points' => $totalPoints, 'points2' => $totalPoints, 'id' => $customerId]
|
||||||
);
|
);
|
||||||
|
|
||||||
$newBalance = db()->fetch(
|
$newBalance = db()->fetch(
|
||||||
@@ -417,6 +417,86 @@ class LoyaltyProgram {
|
|||||||
'new_customer_bonus' => $newCustomerBonus
|
'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
|
// 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']]);
|
$updated = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $order['order_id']]);
|
||||||
if ($updated) {
|
if ($updated) {
|
||||||
emailService()->sendOrderConfirmation($updated);
|
emailService()->sendOrderConfirmation($updated);
|
||||||
|
|||||||
+100
-3
@@ -86,6 +86,15 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?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">
|
<form id="payment-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Card Details</label>
|
<label class="form-label">Card Details</label>
|
||||||
@@ -100,6 +109,21 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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 endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php if (!$stripeConfigured): ?>
|
<?php if (!$stripeConfigured): ?>
|
||||||
@@ -203,13 +227,86 @@ function wireDemoForm(endpoint) {
|
|||||||
wireDemoForm('/api/create-square-payment.php');
|
wireDemoForm('/api/create-square-payment.php');
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
// Square Web Payments SDK
|
// Square Web Payments SDK
|
||||||
let squareCard;
|
let squareCard, squareGiftCard;
|
||||||
|
const squarePayments = window.Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>');
|
||||||
(async function initSquare() {
|
(async function initSquare() {
|
||||||
const payments = window.Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>');
|
squareCard = await squarePayments.card();
|
||||||
squareCard = await payments.card();
|
|
||||||
await squareCard.attach('#card-element');
|
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 form = document.getElementById('payment-form');
|
||||||
const submitButton = document.getElementById('submit-button');
|
const submitButton = document.getElementById('submit-button');
|
||||||
const buttonText = document.getElementById('button-text');
|
const buttonText = document.getElementById('button-text');
|
||||||
|
|||||||
Reference in New Issue
Block a user