mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-07-27 16:52:36 -05:00
Add Square payment processor, gated behind PAYMENT_PROCESSOR flag (default: stripe)
Consolidates payment processing onto the same Square account already used by tomtomgames.com and parkerslingshotrentals.com. Collapses the two prior parallel Stripe flows (hosted Checkout + embedded Elements) into a single Square Web Payments SDK flow, since Payments API is synchronous and removes the original reason for two paths. - includes/square.php: squareApi() cURL helper (mirrors the pattern already used on parkerslingshotrentals.com), markSquarePaymentResult() as the single source of truth for order completion shared by the sync response, webhook, and reconciliation poll - fixes a pre-existing bug where loyalty points were only ever awarded from the polling endpoint, never from the webhook. - api/create-square-payment.php replaces api/create-payment-intent.php; api/create-checkout-session.php deleted (no Square equivalent - single flow). - api/webhook.php rewritten for Squares signature scheme and event types. - api/payment-status.php repurposed to reconciliation-only fallback. - payment.php branches on PAYMENT_PROCESSOR so Stripe and Square code coexist deployed while dormant - flipping one config constant is the cutover/rollback. - admin/payments.php: added a Square settings card alongside the existing (now legacy-labeled) Stripe card. - db/schema.sql + live DB: added square_payment_id/square_order_id columns, stripe_* columns kept for historical orders. Not yet cut over - PAYMENT_PROCESSOR still defaults to stripe in config-secrets.php (outside this repo). Sandbox testing still needed before flipping to square/production.
This commit is contained in:
+188
-111
@@ -1,13 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Payment Page (Stripe)
|
||||
* Supports both PaymentIntent (card element) and Checkout Session (redirect) flows
|
||||
* 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';
|
||||
require_once __DIR__ . '/includes/stripe.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']);
|
||||
@@ -16,7 +23,6 @@ if (empty($orderId)) {
|
||||
redirect('/cart.php');
|
||||
}
|
||||
|
||||
// Get order
|
||||
$order = db()->fetch(
|
||||
"SELECT * FROM orders WHERE order_id = :id",
|
||||
['id' => $orderId]
|
||||
@@ -26,16 +32,20 @@ if (!$order) {
|
||||
redirect('/cart.php');
|
||||
}
|
||||
|
||||
// If already paid, redirect to confirmation
|
||||
if ($order['payment_status'] === 'paid') {
|
||||
clearCart();
|
||||
redirect('/order-confirmation.php?order=' . $orderId);
|
||||
}
|
||||
|
||||
$stripePublishableKey = STRIPE_PUBLISHABLE_KEY;
|
||||
$stripeConfigured = isStripeConfigured();
|
||||
$total = $order['total'];
|
||||
|
||||
if ($processor === 'square') {
|
||||
$squareConfigured = isSquareConfigured();
|
||||
} else {
|
||||
$stripePublishableKey = STRIPE_PUBLISHABLE_KEY;
|
||||
$stripeConfigured = isStripeConfigured();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
|
||||
@@ -51,7 +61,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
@@ -61,47 +71,76 @@ require_once __DIR__ . '/includes/header.php';
|
||||
<?= htmlspecialchars($order['customer_email']) ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if (!$stripeConfigured): ?>
|
||||
<!-- Demo Mode - No Stripe Keys -->
|
||||
<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: ?>
|
||||
<!-- Stripe Payment Options -->
|
||||
<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>
|
||||
|
||||
<?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>
|
||||
|
||||
<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="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: ?>
|
||||
<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 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>
|
||||
@@ -110,9 +149,12 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</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 stripeConfigured = <?= $stripeConfigured ? 'true' : 'false' ?>;
|
||||
const messageEl = document.getElementById('payment-message');
|
||||
|
||||
function showMessage(message, type = 'info') {
|
||||
@@ -121,88 +163,132 @@ function showMessage(message, type = 'info') {
|
||||
messageEl.innerHTML = `<i class="fas fa-${type === 'error' ? 'exclamation-circle' : 'check-circle'}"></i> ${message}`;
|
||||
}
|
||||
|
||||
<?php if (!$stripeConfigured): ?>
|
||||
// Demo mode payment
|
||||
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('/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) {
|
||||
showMessage('Payment simulated successfully! Redirecting...', 'success');
|
||||
setTimeout(() => window.location.href = data.redirect, 1000);
|
||||
} else if (data.error) {
|
||||
showMessage(data.error, 'error');
|
||||
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;
|
||||
(async function initSquare() {
|
||||
const payments = window.Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>');
|
||||
squareCard = await payments.card();
|
||||
await squareCard.attach('#card-element');
|
||||
})();
|
||||
|
||||
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');
|
||||
btn.disabled = false;
|
||||
text.style.display = 'inline';
|
||||
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: ?>
|
||||
// Stripe initialized
|
||||
const stripe = Stripe('<?= $stripePublishableKey ?>');
|
||||
const elements = stripe.elements();
|
||||
const cardElement = elements.create('card', {
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#1B1B1B',
|
||||
'::placeholder': { color: '#9CA3AF' }
|
||||
}
|
||||
}
|
||||
style: { base: { fontSize: '16px', color: '#1B1B1B', '::placeholder': { color: '#9CA3AF' } } }
|
||||
});
|
||||
|
||||
cardElement.mount('#card-element');
|
||||
|
||||
// Handle validation errors
|
||||
cardElement.on('change', function(event) {
|
||||
const displayError = document.getElementById('card-errors');
|
||||
if (event.error) {
|
||||
displayError.textContent = event.error.message;
|
||||
} else {
|
||||
displayError.textContent = '';
|
||||
}
|
||||
displayError.textContent = event.error ? event.error.message : '';
|
||||
});
|
||||
|
||||
// Stripe Checkout button (redirect to hosted page)
|
||||
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
|
||||
})
|
||||
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) {
|
||||
@@ -219,7 +305,6 @@ document.getElementById('checkout-btn').addEventListener('click', async function
|
||||
}
|
||||
});
|
||||
|
||||
// PaymentIntent form (inline card element)
|
||||
const form = document.getElementById('payment-form');
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
const buttonText = document.getElementById('button-text');
|
||||
@@ -227,25 +312,20 @@ 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;
|
||||
@@ -253,8 +333,6 @@ form.addEventListener('submit', async function(e) {
|
||||
spinner.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm payment with Stripe
|
||||
const { error, paymentIntent } = await stripe.confirmCardPayment(data.client_secret, {
|
||||
payment_method: {
|
||||
card: cardElement,
|
||||
@@ -264,14 +342,12 @@ form.addEventListener('submit', async function(e) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -282,6 +358,7 @@ form.addEventListener('submit', async function(e) {
|
||||
}
|
||||
});
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user