mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-07-27 16:52:36 -05:00
30daef5f74
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.
462 lines
19 KiB
PHP
462 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* Tom's Java Jive - Payment Page
|
|
* Supports Stripe (legacy) or Square, gated by the PAYMENT_PROCESSOR constant.
|
|
*/
|
|
|
|
$pageTitle = "Payment - Tom's Java Jive";
|
|
require_once __DIR__ . '/includes/functions.php';
|
|
require_once __DIR__ . '/includes/auth.php';
|
|
|
|
$processor = defined('PAYMENT_PROCESSOR') ? PAYMENT_PROCESSOR : 'stripe';
|
|
|
|
if ($processor === 'square') {
|
|
require_once __DIR__ . '/includes/square.php';
|
|
} else {
|
|
require_once __DIR__ . '/includes/stripe.php';
|
|
}
|
|
|
|
$orderId = $_GET['order'] ?? $_SESSION['pending_order_id'] ?? '';
|
|
$cancelled = isset($_GET['cancelled']);
|
|
|
|
if (empty($orderId)) {
|
|
redirect('/cart.php');
|
|
}
|
|
|
|
$order = db()->fetch(
|
|
"SELECT * FROM orders WHERE order_id = :id",
|
|
['id' => $orderId]
|
|
);
|
|
|
|
if (!$order) {
|
|
redirect('/cart.php');
|
|
}
|
|
|
|
if ($order['payment_status'] === 'paid') {
|
|
clearCart();
|
|
redirect('/order-confirmation.php?order=' . $orderId);
|
|
}
|
|
|
|
$total = $order['total'];
|
|
|
|
if ($processor === 'square') {
|
|
$squareConfigured = isSquareConfigured();
|
|
} else {
|
|
$stripePublishableKey = STRIPE_PUBLISHABLE_KEY;
|
|
$stripeConfigured = isStripeConfigured();
|
|
}
|
|
|
|
require_once __DIR__ . '/includes/header.php';
|
|
?>
|
|
|
|
<section class="section" style="padding-top: 2rem;">
|
|
<div class="container">
|
|
<div class="card" style="max-width: 500px; margin: 0 auto;">
|
|
<div class="card-header">
|
|
<h2 style="margin: 0;">Complete Payment</h2>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php if ($cancelled): ?>
|
|
<div class="alert alert-warning mb-2">
|
|
<i class="fas fa-exclamation-triangle"></i> Payment was cancelled. Please try again.
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div style="background: var(--color-background); padding: 1rem; border-radius: var(--radius-md); margin-bottom: 1.5rem;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
|
<span>Order #<?= htmlspecialchars($order['order_number']) ?></span>
|
|
<strong><?= formatCurrency($total) ?></strong>
|
|
</div>
|
|
<p class="text-muted mb-0" style="font-size: 0.875rem;">
|
|
<?= htmlspecialchars($order['customer_email']) ?>
|
|
</p>
|
|
</div>
|
|
|
|
<?php if ($processor === 'square'): ?>
|
|
<?php if (!$squareConfigured): ?>
|
|
<div class="alert alert-info mb-2">
|
|
<i class="fas fa-info-circle"></i> <strong>Demo Mode:</strong> Square is not configured. Click below to simulate a successful payment.
|
|
</div>
|
|
<form id="demo-payment-form">
|
|
<button type="submit" id="demo-submit" class="btn btn-primary btn-lg btn-block">
|
|
<span id="demo-text">Complete Demo Payment <?= formatCurrency($total) ?></span>
|
|
<span id="demo-spinner" style="display: none;">
|
|
<span class="loading"></span> Processing...
|
|
</span>
|
|
</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>
|
|
<div id="card-element" style="padding: 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius-md);"></div>
|
|
<div id="card-errors" class="form-error" style="margin-top: 0.5rem;"></div>
|
|
</div>
|
|
|
|
<button type="submit" id="submit-button" class="btn btn-secondary btn-lg btn-block">
|
|
<span id="button-text">Pay <?= formatCurrency($total) ?></span>
|
|
<span id="spinner" style="display: none;">
|
|
<span class="loading"></span> Processing...
|
|
</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): ?>
|
|
<div class="alert alert-info mb-2">
|
|
<i class="fas fa-info-circle"></i> <strong>Demo Mode:</strong> Stripe is not configured. Click below to simulate a successful payment.
|
|
</div>
|
|
<form id="demo-payment-form">
|
|
<button type="submit" id="demo-submit" class="btn btn-primary btn-lg btn-block">
|
|
<span id="demo-text">Complete Demo Payment <?= formatCurrency($total) ?></span>
|
|
<span id="demo-spinner" style="display: none;">
|
|
<span class="loading"></span> Processing...
|
|
</span>
|
|
</button>
|
|
</form>
|
|
<?php else: ?>
|
|
<div class="payment-options mb-2">
|
|
<button type="button" id="checkout-btn" class="btn btn-primary btn-lg btn-block mb-1">
|
|
<i class="fas fa-credit-card"></i> Pay with Stripe Checkout
|
|
</button>
|
|
<p class="text-muted text-center" style="font-size: 0.875rem;">or enter card details below</p>
|
|
</div>
|
|
|
|
<form id="payment-form">
|
|
<div class="form-group">
|
|
<label class="form-label">Card Details</label>
|
|
<div id="card-element" style="padding: 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius-md);"></div>
|
|
<div id="card-errors" class="form-error" style="margin-top: 0.5rem;"></div>
|
|
</div>
|
|
|
|
<button type="submit" id="submit-button" class="btn btn-secondary btn-lg btn-block">
|
|
<span id="button-text">Pay <?= formatCurrency($total) ?></span>
|
|
<span id="spinner" style="display: none;">
|
|
<span class="loading"></span> Processing...
|
|
</span>
|
|
</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
|
|
<div id="payment-message" style="display: none; margin-top: 1rem;"></div>
|
|
|
|
<p class="text-muted text-center mt-2" style="font-size: 0.75rem;">
|
|
<i class="fas fa-lock"></i> Your payment is secure and encrypted
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<?php if ($processor === 'square' && $squareConfigured): ?>
|
|
<script src="<?= (defined('SQUARE_ENV') && SQUARE_ENV === 'sandbox') ? 'https://sandbox.web.squarecdn.com/v1/square.js' : 'https://web.squarecdn.com/v1/square.js' ?>"></script>
|
|
<?php endif; ?>
|
|
|
|
<script>
|
|
const orderId = '<?= $orderId ?>';
|
|
const messageEl = document.getElementById('payment-message');
|
|
|
|
function showMessage(message, type = 'info') {
|
|
messageEl.style.display = 'block';
|
|
messageEl.className = `alert alert-${type === 'error' ? 'error' : 'success'}`;
|
|
messageEl.innerHTML = `<i class="fas fa-${type === 'error' ? 'exclamation-circle' : 'check-circle'}"></i> ${message}`;
|
|
}
|
|
|
|
function wireDemoForm(endpoint) {
|
|
const demoForm = document.getElementById('demo-payment-form');
|
|
demoForm.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('demo-submit');
|
|
const text = document.getElementById('demo-text');
|
|
const spinner = document.getElementById('demo-spinner');
|
|
btn.disabled = true;
|
|
text.style.display = 'none';
|
|
spinner.style.display = 'inline';
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ order_id: orderId })
|
|
});
|
|
const data = await response.json();
|
|
if (data.demo_mode && data.redirect) {
|
|
showMessage('Payment simulated successfully! Redirecting...', 'success');
|
|
setTimeout(() => window.location.href = data.redirect, 1000);
|
|
} else if (data.error) {
|
|
showMessage(data.error, 'error');
|
|
btn.disabled = false;
|
|
text.style.display = 'inline';
|
|
spinner.style.display = 'none';
|
|
}
|
|
} catch (err) {
|
|
showMessage('Payment failed. Please try again.', 'error');
|
|
btn.disabled = false;
|
|
text.style.display = 'inline';
|
|
spinner.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
<?php if ($processor === 'square'): ?>
|
|
<?php if (!$squareConfigured): ?>
|
|
wireDemoForm('/api/create-square-payment.php');
|
|
<?php else: ?>
|
|
// Square Web Payments SDK
|
|
let squareCard, squareGiftCard;
|
|
const squarePayments = window.Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>');
|
|
(async function initSquare() {
|
|
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');
|
|
const spinner = document.getElementById('spinner');
|
|
|
|
form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
submitButton.disabled = true;
|
|
buttonText.style.display = 'none';
|
|
spinner.style.display = 'inline';
|
|
document.getElementById('card-errors').textContent = '';
|
|
|
|
try {
|
|
const tokenResult = await squareCard.tokenize();
|
|
if (tokenResult.status !== 'OK') {
|
|
const msg = (tokenResult.errors && tokenResult.errors[0] && tokenResult.errors[0].message) || 'Card validation failed';
|
|
document.getElementById('card-errors').textContent = msg;
|
|
submitButton.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.demo_mode && data.redirect) {
|
|
window.location.href = data.redirect;
|
|
return;
|
|
}
|
|
if (data.error) {
|
|
showMessage(data.error, 'error');
|
|
submitButton.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);
|
|
submitButton.disabled = false;
|
|
buttonText.style.display = 'inline';
|
|
spinner.style.display = 'none';
|
|
}
|
|
});
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
<?php if (!$stripeConfigured): ?>
|
|
wireDemoForm('/api/create-payment-intent.php');
|
|
<?php else: ?>
|
|
const stripe = Stripe('<?= $stripePublishableKey ?>');
|
|
const elements = stripe.elements();
|
|
const cardElement = elements.create('card', {
|
|
style: { base: { fontSize: '16px', color: '#1B1B1B', '::placeholder': { color: '#9CA3AF' } } }
|
|
});
|
|
cardElement.mount('#card-element');
|
|
cardElement.on('change', function(event) {
|
|
const displayError = document.getElementById('card-errors');
|
|
displayError.textContent = event.error ? event.error.message : '';
|
|
});
|
|
|
|
document.getElementById('checkout-btn').addEventListener('click', async function() {
|
|
this.disabled = true;
|
|
this.innerHTML = '<span class="loading"></span> Redirecting to Stripe...';
|
|
try {
|
|
const response = await fetch('/api/create-checkout-session.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ order_id: orderId, origin_url: window.location.origin })
|
|
});
|
|
const data = await response.json();
|
|
if (data.demo_mode && data.redirect) {
|
|
window.location.href = data.redirect;
|
|
} else if (data.url) {
|
|
window.location.href = data.url;
|
|
} else if (data.error) {
|
|
showMessage(data.error, 'error');
|
|
this.disabled = false;
|
|
this.innerHTML = '<i class="fas fa-credit-card"></i> Pay with Stripe Checkout';
|
|
}
|
|
} catch (err) {
|
|
showMessage('Failed to create checkout session.', 'error');
|
|
this.disabled = false;
|
|
this.innerHTML = '<i class="fas fa-credit-card"></i> Pay with Stripe Checkout';
|
|
}
|
|
});
|
|
|
|
const form = document.getElementById('payment-form');
|
|
const submitButton = document.getElementById('submit-button');
|
|
const buttonText = document.getElementById('button-text');
|
|
const spinner = document.getElementById('spinner');
|
|
|
|
form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
submitButton.disabled = true;
|
|
buttonText.style.display = 'none';
|
|
spinner.style.display = 'inline';
|
|
try {
|
|
const response = await fetch('/api/create-payment-intent.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ order_id: orderId })
|
|
});
|
|
const data = await response.json();
|
|
if (data.demo_mode && data.redirect) {
|
|
window.location.href = data.redirect;
|
|
return;
|
|
}
|
|
if (data.error) {
|
|
showMessage(data.error, 'error');
|
|
submitButton.disabled = false;
|
|
buttonText.style.display = 'inline';
|
|
spinner.style.display = 'none';
|
|
return;
|
|
}
|
|
const { error, paymentIntent } = await stripe.confirmCardPayment(data.client_secret, {
|
|
payment_method: {
|
|
card: cardElement,
|
|
billing_details: {
|
|
name: '<?= addslashes($order['customer_name']) ?>',
|
|
email: '<?= addslashes($order['customer_email']) ?>'
|
|
}
|
|
}
|
|
});
|
|
if (error) {
|
|
showMessage(error.message, 'error');
|
|
} else if (paymentIntent.status === 'succeeded') {
|
|
showMessage('Payment successful! Redirecting...', 'success');
|
|
setTimeout(() => window.location.href = '/order-confirmation.php?order=' + orderId, 1000);
|
|
}
|
|
} catch (err) {
|
|
showMessage('Payment failed. Please try again.', 'error');
|
|
console.error(err);
|
|
} finally {
|
|
submitButton.disabled = false;
|
|
buttonText.style.display = 'inline';
|
|
spinner.style.display = 'none';
|
|
}
|
|
});
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</script>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|