mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-07-28 09:12: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:
+79
-2
@@ -21,6 +21,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
setFlash('success', 'Stripe settings updated');
|
setFlash('success', 'Stripe settings updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($section === 'square') {
|
||||||
|
setSetting('payment_square', [
|
||||||
|
'enabled' => isset($_POST['square_enabled']),
|
||||||
|
'sandbox' => isset($_POST['square_sandbox']),
|
||||||
|
'app_id' => trim($_POST['square_app_id'] ?? ''),
|
||||||
|
'location_id' => trim($_POST['square_location_id'] ?? ''),
|
||||||
|
'access_token' => trim($_POST['square_access_token'] ?? ''),
|
||||||
|
'webhook_signature_key' => trim($_POST['square_webhook_signature_key'] ?? '')
|
||||||
|
]);
|
||||||
|
setFlash('success', 'Square settings updated');
|
||||||
|
}
|
||||||
|
|
||||||
if ($section === 'methods') {
|
if ($section === 'methods') {
|
||||||
setSetting('payment_methods', [
|
setSetting('payment_methods', [
|
||||||
'card' => isset($_POST['method_card']),
|
'card' => isset($_POST['method_card']),
|
||||||
@@ -43,6 +55,15 @@ $stripe = getSetting('payment_stripe', [
|
|||||||
'webhook_secret' => ''
|
'webhook_secret' => ''
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$square = getSetting('payment_square', [
|
||||||
|
'enabled' => defined('PAYMENT_PROCESSOR') && PAYMENT_PROCESSOR === 'square',
|
||||||
|
'sandbox' => defined('SQUARE_ENV') && SQUARE_ENV === 'sandbox',
|
||||||
|
'app_id' => defined('SQUARE_APP_ID') ? SQUARE_APP_ID : '',
|
||||||
|
'location_id' => defined('SQUARE_LOCATION_ID') ? SQUARE_LOCATION_ID : '',
|
||||||
|
'access_token' => defined('SQUARE_ACCESS_TOKEN') ? SQUARE_ACCESS_TOKEN : '',
|
||||||
|
'webhook_signature_key' => defined('SQUARE_WEBHOOK_SIGNATURE_KEY') ? SQUARE_WEBHOOK_SIGNATURE_KEY : ''
|
||||||
|
]);
|
||||||
|
|
||||||
$methods = getSetting('payment_methods', [
|
$methods = getSetting('payment_methods', [
|
||||||
'card' => true,
|
'card' => true,
|
||||||
'cash' => true,
|
'cash' => true,
|
||||||
@@ -72,12 +93,68 @@ $methods = getSetting('payment_methods', [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- Stripe Settings -->
|
<!-- Square Settings -->
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="section" value="square">
|
||||||
|
<div class="admin-card">
|
||||||
|
<div class="admin-card-header">
|
||||||
|
<h3 class="admin-card-title"><i class="fas fa-square" style="color: #006AFF;"></i> Square</h3>
|
||||||
|
</div>
|
||||||
|
<div class="admin-card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox" name="square_enabled" <?= $square['enabled'] ? 'checked' : '' ?>>
|
||||||
|
Enable Square payments
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox" name="square_sandbox" <?= $square['sandbox'] ? 'checked' : '' ?>>
|
||||||
|
Sandbox mode (use sandbox app/location/token)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Application ID</label>
|
||||||
|
<input type="text" name="square_app_id" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['app_id']) ?>"
|
||||||
|
placeholder="sq0idp-...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Location ID</label>
|
||||||
|
<input type="text" name="square_location_id" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['location_id']) ?>"
|
||||||
|
placeholder="L...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Access Token</label>
|
||||||
|
<input type="password" name="square_access_token" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['access_token']) ?>"
|
||||||
|
placeholder="EAAA...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<label class="form-label">Webhook Signature Key</label>
|
||||||
|
<input type="password" name="square_webhook_signature_key" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['webhook_signature_key']) ?>"
|
||||||
|
placeholder="...">
|
||||||
|
<small class="text-muted">From the Square Developer Dashboard's webhook subscription for this site</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary mt-2">Save Square Settings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Stripe Settings (legacy) -->
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="section" value="stripe">
|
<input type="hidden" name="section" value="stripe">
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
<div class="admin-card-header">
|
<div class="admin-card-header">
|
||||||
<h3 class="admin-card-title"><i class="fab fa-stripe" style="color: #635BFF;"></i> Stripe</h3>
|
<h3 class="admin-card-title"><i class="fab fa-stripe" style="color: #635BFF;"></i> Stripe <small class="text-muted">(legacy)</small></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-card-body">
|
<div class="admin-card-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Tom's Java Jive - Create Stripe Checkout Session API
|
|
||||||
* Uses hosted checkout page (redirects to Stripe)
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
|
||||||
require_once __DIR__ . '/../includes/loyalty.php';
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
// Only accept POST
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$orderId = $input['order_id'] ?? '';
|
|
||||||
$originUrl = $input['origin_url'] ?? '';
|
|
||||||
|
|
||||||
if (empty($orderId)) {
|
|
||||||
jsonResponse(['error' => 'Order ID required'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($originUrl)) {
|
|
||||||
$originUrl = SITE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get order
|
|
||||||
$order = db()->fetch(
|
|
||||||
"SELECT * FROM orders WHERE order_id = :id",
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$order) {
|
|
||||||
jsonResponse(['error' => 'Order not found'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($order['payment_status'] === 'paid') {
|
|
||||||
jsonResponse(['error' => 'Order already paid'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Stripe is configured
|
|
||||||
if (!isStripeConfigured()) {
|
|
||||||
// Demo mode - simulate successful payment
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => 'demo_' . bin2hex(random_bytes(8))
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!empty($order['customer_id'])) {
|
|
||||||
loyalty()->awardPoints(
|
|
||||||
$order['customer_id'],
|
|
||||||
(float) $order['total'],
|
|
||||||
'Order #' . $order['order_number'],
|
|
||||||
$orderId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'demo_mode' => true,
|
|
||||||
'message' => 'Payment simulated (Stripe not configured)',
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $orderId
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build line items from order
|
|
||||||
$items = json_decode($order['items'], true) ?? [];
|
|
||||||
$lineItems = [];
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$lineItems[] = [
|
|
||||||
'name' => $item['name'],
|
|
||||||
'price' => floatval($item['price']),
|
|
||||||
'quantity' => intval($item['quantity']),
|
|
||||||
'currency' => 'usd'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add shipping if applicable
|
|
||||||
if ($order['shipping_cost'] > 0) {
|
|
||||||
$lineItems[] = [
|
|
||||||
'name' => 'Shipping',
|
|
||||||
'price' => floatval($order['shipping_cost']),
|
|
||||||
'quantity' => 1,
|
|
||||||
'currency' => 'usd'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build success/cancel URLs
|
|
||||||
$successUrl = rtrim($originUrl, '/') . '/order-confirmation.php?order=' . $orderId . '&session_id={CHECKOUT_SESSION_ID}';
|
|
||||||
$cancelUrl = rtrim($originUrl, '/') . '/payment.php?order=' . $orderId . '&cancelled=1';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$session = stripe()->createCheckoutSession(
|
|
||||||
$lineItems,
|
|
||||||
$successUrl,
|
|
||||||
$cancelUrl,
|
|
||||||
[
|
|
||||||
'customer_email' => $order['customer_email'],
|
|
||||||
'metadata' => [
|
|
||||||
'order_id' => $orderId,
|
|
||||||
'order_number' => $order['order_number']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store checkout session ID
|
|
||||||
db()->update('orders',
|
|
||||||
['stripe_session_id' => $session['id']],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'url' => $session['url'],
|
|
||||||
'session_id' => $session['id']
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('Stripe Checkout error: ' . $e->getMessage());
|
|
||||||
jsonResponse(['error' => 'Failed to create checkout session: ' . $e->getMessage()], 500);
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Tom's Java Jive - Create Stripe Payment Intent API
|
|
||||||
* Uses cURL-based Stripe integration (no Composer required)
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
// Only accept POST
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$orderId = $input['order_id'] ?? '';
|
|
||||||
|
|
||||||
if (empty($orderId)) {
|
|
||||||
jsonResponse(['error' => 'Order ID required'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get order
|
|
||||||
$order = db()->fetch(
|
|
||||||
"SELECT * FROM orders WHERE order_id = :id",
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$order) {
|
|
||||||
jsonResponse(['error' => 'Order not found'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($order['payment_status'] === 'paid') {
|
|
||||||
jsonResponse(['error' => 'Order already paid'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Stripe is configured
|
|
||||||
if (!isStripeConfigured()) {
|
|
||||||
// Demo mode - simulate successful payment
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => 'demo_' . bin2hex(random_bytes(8))
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'demo_mode' => true,
|
|
||||||
'message' => 'Payment simulated (Stripe not configured)',
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $orderId
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Stripe Payment Intent using cURL-based API
|
|
||||||
try {
|
|
||||||
$paymentIntent = stripe()->createPaymentIntent(
|
|
||||||
$order['total'],
|
|
||||||
'usd',
|
|
||||||
[
|
|
||||||
'metadata' => [
|
|
||||||
'order_id' => $orderId,
|
|
||||||
'order_number' => $order['order_number']
|
|
||||||
],
|
|
||||||
'receipt_email' => $order['customer_email'],
|
|
||||||
'description' => 'Order #' . $order['order_number']
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store payment intent ID
|
|
||||||
db()->update('orders',
|
|
||||||
['stripe_payment_intent' => $paymentIntent['id']],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'client_secret' => $paymentIntent['client_secret']
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('Stripe error: ' . $e->getMessage());
|
|
||||||
jsonResponse(['error' => 'Payment initialization failed: ' . $e->getMessage()], 500);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Tom's Java Jive - Create Square Payment API
|
||||||
|
* Accepts a Web Payments SDK card nonce (source_id) and charges it synchronously.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
|
require_once __DIR__ . '/../includes/square.php';
|
||||||
|
require_once __DIR__ . '/../includes/loyalty.php';
|
||||||
|
require_once __DIR__ . '/../includes/email.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$orderId = $input['order_id'] ?? '';
|
||||||
|
$sourceId = $input['source_id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($orderId)) {
|
||||||
|
jsonResponse(['error' => 'Order ID required'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = db()->fetch(
|
||||||
|
"SELECT * FROM orders WHERE order_id = :id",
|
||||||
|
['id' => $orderId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
jsonResponse(['error' => 'Order not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($order['payment_status'] === 'paid') {
|
||||||
|
jsonResponse(['error' => 'Order already paid'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo mode - Square not configured
|
||||||
|
if (!isSquareConfigured()) {
|
||||||
|
db()->update('orders',
|
||||||
|
[
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
'order_status' => 'confirmed',
|
||||||
|
'square_payment_id' => 'demo_' . bin2hex(random_bytes(8)),
|
||||||
|
'payment_method' => 'square',
|
||||||
|
],
|
||||||
|
'order_id = :id',
|
||||||
|
['id' => $orderId]
|
||||||
|
);
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'demo_mode' => true,
|
||||||
|
'message' => 'Payment simulated (Square not configured)',
|
||||||
|
'redirect' => '/order-confirmation.php?order=' . $orderId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($sourceId)) {
|
||||||
|
jsonResponse(['error' => 'Card details required'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = squareCreatePayment(
|
||||||
|
$sourceId,
|
||||||
|
(float) $order['total'],
|
||||||
|
$orderId,
|
||||||
|
['note' => 'Order #' . $order['order_number']]
|
||||||
|
);
|
||||||
|
$payment = $result['payment'] ?? [];
|
||||||
|
$status = $payment['status'] ?? '';
|
||||||
|
|
||||||
|
if ($status === 'COMPLETED') {
|
||||||
|
markSquarePaymentResult($payment['id'], $orderId, 'COMPLETED');
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'redirect' => '/order-confirmation.php?order=' . $orderId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the payment id even if not yet completed, so the reconciliation
|
||||||
|
// poll and webhook can find this order by square_payment_id.
|
||||||
|
db()->update('orders',
|
||||||
|
['square_payment_id' => $payment['id'] ?? null],
|
||||||
|
'order_id = :id',
|
||||||
|
['id' => $orderId]
|
||||||
|
);
|
||||||
|
|
||||||
|
jsonResponse(['error' => 'Payment was not completed (status: ' . $status . ')'], 400);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Square payment error: ' . $e->getMessage());
|
||||||
|
jsonResponse(['error' => 'Payment failed: ' . $e->getMessage()], 500);
|
||||||
|
}
|
||||||
+38
-102
@@ -1,45 +1,39 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tom's Java Jive - Check Payment Status API
|
* Tom's Java Jive - Check Payment Status API
|
||||||
* Polls Stripe for payment/checkout session status
|
* Reconciliation-only fallback: Square payments complete synchronously in
|
||||||
|
* create-square-payment.php, so this endpoint only matters if the browser
|
||||||
|
* lost that response after Square had already accepted the charge. It shares
|
||||||
|
* markSquarePaymentResult() with the webhook and the synchronous path so
|
||||||
|
* loyalty points/emails are never duplicated no matter which path resolves first.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
require_once __DIR__ . '/../includes/square.php';
|
||||||
require_once __DIR__ . '/../includes/loyalty.php';
|
require_once __DIR__ . '/../includes/loyalty.php';
|
||||||
|
require_once __DIR__ . '/../includes/email.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Only accept GET
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderId = $_GET['order_id'] ?? '';
|
$orderId = $_GET['order_id'] ?? '';
|
||||||
$sessionId = $_GET['session_id'] ?? '';
|
|
||||||
|
|
||||||
if (empty($orderId) && empty($sessionId)) {
|
if (empty($orderId)) {
|
||||||
jsonResponse(['error' => 'Order ID or Session ID required'], 400);
|
jsonResponse(['error' => 'Order ID required'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get order by ID or session
|
|
||||||
if (!empty($orderId)) {
|
|
||||||
$order = db()->fetch(
|
$order = db()->fetch(
|
||||||
"SELECT * FROM orders WHERE order_id = :id",
|
"SELECT * FROM orders WHERE order_id = :id",
|
||||||
['id' => $orderId]
|
['id' => $orderId]
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
$order = db()->fetch(
|
|
||||||
"SELECT * FROM orders WHERE stripe_session_id = :session OR stripe_payment_intent = :session",
|
|
||||||
['session' => $sessionId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
jsonResponse(['error' => 'Order not found'], 404);
|
jsonResponse(['error' => 'Order not found'], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already marked as paid, return success
|
|
||||||
if ($order['payment_status'] === 'paid') {
|
if ($order['payment_status'] === 'paid') {
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'complete',
|
'status' => 'complete',
|
||||||
@@ -50,105 +44,47 @@ if ($order['payment_status'] === 'paid') {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Stripe is configured
|
if (!isSquareConfigured()) {
|
||||||
if (!isStripeConfigured()) {
|
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'demo_mode',
|
'status' => 'demo_mode',
|
||||||
'payment_status' => $order['payment_status'],
|
'payment_status' => $order['payment_status'],
|
||||||
'message' => 'Stripe not configured - running in demo mode'
|
'message' => 'Square not configured - running in demo mode'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (empty($order['square_payment_id'])) {
|
||||||
// Check with Stripe
|
|
||||||
if (!empty($order['stripe_session_id'])) {
|
|
||||||
// Check checkout session status
|
|
||||||
$session = stripe()->getCheckoutSession($order['stripe_session_id']);
|
|
||||||
|
|
||||||
if ($session['payment_status'] === 'paid') {
|
|
||||||
// Update order
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => $session['payment_intent'] ?? null
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $order['order_id']]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Award loyalty points
|
|
||||||
if (!empty($order['customer_id'])) {
|
|
||||||
loyalty()->awardPoints(
|
|
||||||
$order['customer_id'],
|
|
||||||
(float) $order['total'],
|
|
||||||
'Order #' . $order['order_number'],
|
|
||||||
$order['order_id']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => 'complete',
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_id' => $order['order_id'],
|
|
||||||
'order_number' => $order['order_number'],
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $order['order_id']
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => $session['status'],
|
|
||||||
'payment_status' => $session['payment_status']
|
|
||||||
]);
|
|
||||||
|
|
||||||
} elseif (!empty($order['stripe_payment_intent'])) {
|
|
||||||
// Check payment intent status
|
|
||||||
$paymentIntent = stripe()->getPaymentIntent($order['stripe_payment_intent']);
|
|
||||||
|
|
||||||
if ($paymentIntent['status'] === 'succeeded') {
|
|
||||||
// Update order
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed'
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $order['order_id']]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Award loyalty points
|
|
||||||
if (!empty($order['customer_id'])) {
|
|
||||||
loyalty()->awardPoints(
|
|
||||||
$order['customer_id'],
|
|
||||||
(float) $order['total'],
|
|
||||||
'Order #' . $order['order_number'],
|
|
||||||
$order['order_id']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => 'complete',
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_id' => $order['order_id'],
|
|
||||||
'order_number' => $order['order_number'],
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $order['order_id']
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => $paymentIntent['status'],
|
|
||||||
'payment_status' => 'pending'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No Stripe reference found
|
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'pending',
|
'status' => 'pending',
|
||||||
'payment_status' => $order['payment_status']
|
'payment_status' => $order['payment_status']
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = squareGetPayment($order['square_payment_id']);
|
||||||
|
$payment = $result['payment'] ?? [];
|
||||||
|
$status = $payment['status'] ?? '';
|
||||||
|
|
||||||
|
markSquarePaymentResult($order['square_payment_id'], $order['order_id'], $status);
|
||||||
|
|
||||||
|
$updated = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $order['order_id']]);
|
||||||
|
|
||||||
|
if ($updated['payment_status'] === 'paid') {
|
||||||
|
jsonResponse([
|
||||||
|
'status' => 'complete',
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
'order_id' => $updated['order_id'],
|
||||||
|
'order_number' => $updated['order_number'],
|
||||||
|
'redirect' => '/order-confirmation.php?order=' . $updated['order_id']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'status' => $status,
|
||||||
|
'payment_status' => $updated['payment_status']
|
||||||
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('Payment status check error: ' . $e->getMessage());
|
error_log('Square payment status check error: ' . $e->getMessage());
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'payment_status' => $order['payment_status'],
|
'payment_status' => $order['payment_status'],
|
||||||
|
|||||||
+30
-73
@@ -1,107 +1,65 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tom's Java Jive - Stripe Webhook Handler
|
* Tom's Java Jive - Square Webhook Handler
|
||||||
* Uses cURL-based Stripe integration (no Composer required)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
require_once __DIR__ . '/../includes/square.php';
|
||||||
|
require_once __DIR__ . '/../includes/loyalty.php';
|
||||||
require_once __DIR__ . '/../includes/email.php';
|
require_once __DIR__ . '/../includes/email.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$payload = file_get_contents('php://input');
|
$payload = file_get_contents('php://input');
|
||||||
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
|
$sigHeader = $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ?? '';
|
||||||
|
$notificationUrl = SITE_URL . '/api/webhook.php';
|
||||||
|
|
||||||
// Verify webhook signature (if secret is configured)
|
if (empty(SQUARE_WEBHOOK_SIGNATURE_KEY) || SQUARE_WEBHOOK_SIGNATURE_KEY === 'REPLACE_ME') {
|
||||||
if (!empty(STRIPE_WEBHOOK_SECRET) && STRIPE_WEBHOOK_SECRET !== 'whsec_your_webhook_secret') {
|
error_log('Square webhook signature key not configured - rejecting webhook');
|
||||||
try {
|
|
||||||
stripe()->verifyWebhookSignature($payload, $sigHeader, STRIPE_WEBHOOK_SECRET);
|
|
||||||
$event = json_decode($payload, true);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('Stripe webhook signature verification failed: ' . $e->getMessage());
|
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
if (!verifySquareWebhookSignature($payload, $sigHeader, $notificationUrl, SQUARE_WEBHOOK_SIGNATURE_KEY)) {
|
||||||
|
error_log('Square webhook signature verification failed');
|
||||||
|
http_response_code(400);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
$event = json_decode($payload, true);
|
$event = json_decode($payload, true);
|
||||||
if (!$event) {
|
if (!$event) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$eventType = $event['type'] ?? '';
|
$eventType = $event['type'] ?? '';
|
||||||
$data = $event['data']['object'] ?? [];
|
|
||||||
|
|
||||||
switch ($eventType) {
|
switch ($eventType) {
|
||||||
|
|
||||||
case 'checkout.session.completed':
|
case 'payment.updated':
|
||||||
// Stripe Checkout (hosted page) — metadata is on the session
|
case 'payment.created':
|
||||||
$orderId = $data['metadata']['order_id'] ?? '';
|
$payment = $event['data']['object']['payment'] ?? [];
|
||||||
$paymentIntentId = $data['payment_intent'] ?? '';
|
$paymentId = $payment['id'] ?? '';
|
||||||
if ($orderId && ($data['payment_status'] ?? '') === 'paid') {
|
$status = $payment['status'] ?? '';
|
||||||
db()->update('orders',
|
$orderId = $payment['reference_id'] ?? null;
|
||||||
[
|
if ($paymentId && $status) {
|
||||||
'payment_status' => 'paid',
|
markSquarePaymentResult($paymentId, $orderId, $status);
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => $paymentIntentId,
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
|
|
||||||
if ($order) {
|
|
||||||
emailService()->sendOrderConfirmation($order);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'payment_intent.succeeded':
|
case 'refund.updated':
|
||||||
// Payment Intent flow (embedded/direct) - skip if already confirmed by checkout.session.completed
|
case 'refund.created':
|
||||||
$paymentIntentId = $data['id'] ?? '';
|
$refund = $event['data']['object']['refund'] ?? [];
|
||||||
$orderId = $data['metadata']['order_id'] ?? '';
|
$paymentId = $refund['payment_id'] ?? '';
|
||||||
if ($orderId && ($data['status'] ?? '') === 'succeeded') {
|
$status = $refund['status'] ?? '';
|
||||||
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
|
if ($status === 'COMPLETED' && $paymentId) {
|
||||||
if ($order && $order['order_status'] !== 'confirmed') {
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => $paymentIntentId,
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
|
|
||||||
if ($order) {
|
|
||||||
emailService()->sendOrderConfirmation($order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'payment_intent.payment_failed':
|
|
||||||
$orderId = $data['metadata']['order_id'] ?? '';
|
|
||||||
if ($orderId) {
|
|
||||||
db()->update('orders',
|
|
||||||
['payment_status' => 'failed'],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'charge.refunded':
|
|
||||||
$paymentIntentId = $data['payment_intent'] ?? '';
|
|
||||||
if ($paymentIntentId) {
|
|
||||||
db()->update('orders',
|
db()->update('orders',
|
||||||
[
|
[
|
||||||
'payment_status' => 'refunded',
|
'payment_status' => 'refunded',
|
||||||
'order_status' => 'refunded',
|
'order_status' => 'refunded',
|
||||||
],
|
],
|
||||||
'stripe_payment_intent = :pi',
|
'square_payment_id = :pid',
|
||||||
['pi' => $paymentIntentId]
|
['pid' => $paymentId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -109,4 +67,3 @@ switch ($eventType) {
|
|||||||
|
|
||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
echo json_encode(['received' => true]);
|
echo json_encode(['received' => true]);
|
||||||
|
|
||||||
|
|||||||
@@ -326,6 +326,8 @@ CREATE TABLE `orders` (
|
|||||||
`order_status` enum('pending','confirmed','processing','shipped','delivered','cancelled','refunded') DEFAULT 'pending',
|
`order_status` enum('pending','confirmed','processing','shipped','delivered','cancelled','refunded') DEFAULT 'pending',
|
||||||
`stripe_session_id` varchar(255) DEFAULT NULL,
|
`stripe_session_id` varchar(255) DEFAULT NULL,
|
||||||
`stripe_payment_intent` varchar(255) DEFAULT NULL,
|
`stripe_payment_intent` varchar(255) DEFAULT NULL,
|
||||||
|
`square_payment_id` varchar(255) DEFAULT NULL,
|
||||||
|
`square_order_id` varchar(255) DEFAULT NULL,
|
||||||
`tracking_number` varchar(100) DEFAULT NULL,
|
`tracking_number` varchar(100) DEFAULT NULL,
|
||||||
`tracking_url` varchar(500) DEFAULT NULL,
|
`tracking_url` varchar(500) DEFAULT NULL,
|
||||||
`notes` text DEFAULT NULL,
|
`notes` text DEFAULT NULL,
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Tom's Java Jive - Square Integration (cURL-based, no SDK required)
|
||||||
|
*/
|
||||||
|
|
||||||
|
function squareApi(string $method, string $path, array $body = []): array {
|
||||||
|
$base = (defined('SQUARE_ENV') && SQUARE_ENV === 'sandbox')
|
||||||
|
? 'https://connect.squareupsandbox.com/v2'
|
||||||
|
: 'https://connect.squareup.com/v2';
|
||||||
|
|
||||||
|
$ch = curl_init($base . $path);
|
||||||
|
$opts = [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
|
||||||
|
'Square-Version: 2024-01-18',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if ($method !== 'GET') {
|
||||||
|
$opts[CURLOPT_CUSTOMREQUEST] = $method;
|
||||||
|
$opts[CURLOPT_POSTFIELDS] = json_encode($body ?: new stdClass());
|
||||||
|
}
|
||||||
|
curl_setopt_array($ch, $opts);
|
||||||
|
|
||||||
|
$resp = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err) {
|
||||||
|
throw new Exception('Square API connection error: ' . $err);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($resp ?: '{}', true);
|
||||||
|
|
||||||
|
if (!empty($decoded['errors'])) {
|
||||||
|
$msg = $decoded['errors'][0]['detail'] ?? ($decoded['errors'][0]['code'] ?? 'Unknown Square error');
|
||||||
|
throw new Exception($msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode >= 400) {
|
||||||
|
throw new Exception('Square API error (HTTP ' . $httpCode . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a payment. $sourceId is the nonce from the Web Payments SDK's tokenize() call.
|
||||||
|
*/
|
||||||
|
function squareCreatePayment(string $sourceId, float $amount, string $referenceId, array $options = []): array {
|
||||||
|
$body = [
|
||||||
|
'source_id' => $sourceId,
|
||||||
|
'idempotency_key' => $referenceId . '_' . bin2hex(random_bytes(8)),
|
||||||
|
'amount_money' => [
|
||||||
|
'amount' => (int) round($amount * 100),
|
||||||
|
'currency' => 'USD',
|
||||||
|
],
|
||||||
|
'location_id' => SQUARE_LOCATION_ID,
|
||||||
|
'autocomplete' => true,
|
||||||
|
'reference_id' => $referenceId,
|
||||||
|
];
|
||||||
|
if (!empty($options['note'])) {
|
||||||
|
$body['note'] = $options['note'];
|
||||||
|
}
|
||||||
|
return squareApi('POST', '/payments', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function squareGetPayment(string $paymentId): array {
|
||||||
|
return squareApi('GET', '/payments/' . $paymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSquareConfigured(): bool {
|
||||||
|
return defined('SQUARE_ACCESS_TOKEN') && defined('SQUARE_APP_ID') && defined('SQUARE_LOCATION_ID')
|
||||||
|
&& !empty(SQUARE_ACCESS_TOKEN) && !empty(SQUARE_APP_ID) && !empty(SQUARE_LOCATION_ID)
|
||||||
|
&& SQUARE_ACCESS_TOKEN !== 'REPLACE_ME';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Square's webhook signature: base64(HMAC-SHA256(notification_url . raw_body, signature_key)).
|
||||||
|
* The notification URL must exactly match what's configured in the Square Dashboard subscription.
|
||||||
|
*/
|
||||||
|
function verifySquareWebhookSignature(string $payload, string $signature, string $notificationUrl, string $signatureKey): bool {
|
||||||
|
if (empty($signatureKey) || empty($signature)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$expected = base64_encode(hash_hmac('sha256', $notificationUrl . $payload, $signatureKey, true));
|
||||||
|
return hash_equals($expected, $signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for "an order's Square payment resolved" — called from the
|
||||||
|
* synchronous create-payment response, the webhook, and the reconciliation poll, so
|
||||||
|
* loyalty points/emails are never awarded or sent more than once regardless of which
|
||||||
|
* path resolves the order first.
|
||||||
|
*/
|
||||||
|
function markSquarePaymentResult(string $paymentId, ?string $orderId, string $status): void {
|
||||||
|
$order = $orderId
|
||||||
|
? db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId])
|
||||||
|
: db()->fetch("SELECT * FROM orders WHERE square_payment_id = :pid", ['pid' => $paymentId]);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === 'COMPLETED') {
|
||||||
|
if ($order['order_status'] !== 'confirmed') {
|
||||||
|
db()->update('orders',
|
||||||
|
[
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
'order_status' => 'confirmed',
|
||||||
|
'square_payment_id' => $paymentId,
|
||||||
|
'payment_method' => 'square',
|
||||||
|
],
|
||||||
|
'order_id = :id',
|
||||||
|
['id' => $order['order_id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($order['customer_id'])) {
|
||||||
|
loyalty()->awardPoints(
|
||||||
|
$order['customer_id'],
|
||||||
|
(float) $order['total'],
|
||||||
|
'Order #' . $order['order_number'],
|
||||||
|
$order['order_id']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $order['order_id']]);
|
||||||
|
if ($updated) {
|
||||||
|
emailService()->sendOrderConfirmation($updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (in_array($status, ['FAILED', 'CANCELED'], true)) {
|
||||||
|
db()->update('orders',
|
||||||
|
['payment_status' => 'failed'],
|
||||||
|
'order_id = :id',
|
||||||
|
['id' => $order['order_id']]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
-50
@@ -1,13 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tom's Java Jive - Payment Page (Stripe)
|
* Tom's Java Jive - Payment Page
|
||||||
* Supports both PaymentIntent (card element) and Checkout Session (redirect) flows
|
* Supports Stripe (legacy) or Square, gated by the PAYMENT_PROCESSOR constant.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$pageTitle = "Payment - Tom's Java Jive";
|
$pageTitle = "Payment - 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';
|
||||||
|
|
||||||
|
$processor = defined('PAYMENT_PROCESSOR') ? PAYMENT_PROCESSOR : 'stripe';
|
||||||
|
|
||||||
|
if ($processor === 'square') {
|
||||||
|
require_once __DIR__ . '/includes/square.php';
|
||||||
|
} else {
|
||||||
require_once __DIR__ . '/includes/stripe.php';
|
require_once __DIR__ . '/includes/stripe.php';
|
||||||
|
}
|
||||||
|
|
||||||
$orderId = $_GET['order'] ?? $_SESSION['pending_order_id'] ?? '';
|
$orderId = $_GET['order'] ?? $_SESSION['pending_order_id'] ?? '';
|
||||||
$cancelled = isset($_GET['cancelled']);
|
$cancelled = isset($_GET['cancelled']);
|
||||||
@@ -16,7 +23,6 @@ if (empty($orderId)) {
|
|||||||
redirect('/cart.php');
|
redirect('/cart.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get order
|
|
||||||
$order = db()->fetch(
|
$order = db()->fetch(
|
||||||
"SELECT * FROM orders WHERE order_id = :id",
|
"SELECT * FROM orders WHERE order_id = :id",
|
||||||
['id' => $orderId]
|
['id' => $orderId]
|
||||||
@@ -26,15 +32,19 @@ if (!$order) {
|
|||||||
redirect('/cart.php');
|
redirect('/cart.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already paid, redirect to confirmation
|
|
||||||
if ($order['payment_status'] === 'paid') {
|
if ($order['payment_status'] === 'paid') {
|
||||||
clearCart();
|
clearCart();
|
||||||
redirect('/order-confirmation.php?order=' . $orderId);
|
redirect('/order-confirmation.php?order=' . $orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$total = $order['total'];
|
||||||
|
|
||||||
|
if ($processor === 'square') {
|
||||||
|
$squareConfigured = isSquareConfigured();
|
||||||
|
} else {
|
||||||
$stripePublishableKey = STRIPE_PUBLISHABLE_KEY;
|
$stripePublishableKey = STRIPE_PUBLISHABLE_KEY;
|
||||||
$stripeConfigured = isStripeConfigured();
|
$stripeConfigured = isStripeConfigured();
|
||||||
$total = $order['total'];
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/header.php';
|
require_once __DIR__ . '/includes/header.php';
|
||||||
?>
|
?>
|
||||||
@@ -62,8 +72,37 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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: ?>
|
||||||
|
<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): ?>
|
<?php if (!$stripeConfigured): ?>
|
||||||
<!-- Demo Mode - No Stripe Keys -->
|
|
||||||
<div class="alert alert-info mb-2">
|
<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.
|
<i class="fas fa-info-circle"></i> <strong>Demo Mode:</strong> Stripe is not configured. Click below to simulate a successful payment.
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +115,6 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<!-- Stripe Payment Options -->
|
|
||||||
<div class="payment-options mb-2">
|
<div class="payment-options mb-2">
|
||||||
<button type="button" id="checkout-btn" class="btn btn-primary btn-lg btn-block mb-1">
|
<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
|
<i class="fas fa-credit-card"></i> Pay with Stripe Checkout
|
||||||
@@ -99,6 +137,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div id="payment-message" style="display: none; margin-top: 1rem;"></div>
|
<div id="payment-message" style="display: none; margin-top: 1rem;"></div>
|
||||||
|
|
||||||
@@ -110,9 +149,12 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<script>
|
||||||
const orderId = '<?= $orderId ?>';
|
const orderId = '<?= $orderId ?>';
|
||||||
const stripeConfigured = <?= $stripeConfigured ? 'true' : 'false' ?>;
|
|
||||||
const messageEl = document.getElementById('payment-message');
|
const messageEl = document.getElementById('payment-message');
|
||||||
|
|
||||||
function showMessage(message, type = 'info') {
|
function showMessage(message, type = 'info') {
|
||||||
@@ -121,29 +163,23 @@ function showMessage(message, type = 'info') {
|
|||||||
messageEl.innerHTML = `<i class="fas fa-${type === 'error' ? 'exclamation-circle' : 'check-circle'}"></i> ${message}`;
|
messageEl.innerHTML = `<i class="fas fa-${type === 'error' ? 'exclamation-circle' : 'check-circle'}"></i> ${message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
<?php if (!$stripeConfigured): ?>
|
function wireDemoForm(endpoint) {
|
||||||
// Demo mode payment
|
|
||||||
const demoForm = document.getElementById('demo-payment-form');
|
const demoForm = document.getElementById('demo-payment-form');
|
||||||
demoForm.addEventListener('submit', async function(e) {
|
demoForm.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const btn = document.getElementById('demo-submit');
|
const btn = document.getElementById('demo-submit');
|
||||||
const text = document.getElementById('demo-text');
|
const text = document.getElementById('demo-text');
|
||||||
const spinner = document.getElementById('demo-spinner');
|
const spinner = document.getElementById('demo-spinner');
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
text.style.display = 'none';
|
text.style.display = 'none';
|
||||||
spinner.style.display = 'inline';
|
spinner.style.display = 'inline';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/create-payment-intent.php', {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ order_id: orderId })
|
body: JSON.stringify({ order_id: orderId })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.demo_mode && data.redirect) {
|
if (data.demo_mode && data.redirect) {
|
||||||
showMessage('Payment simulated successfully! Redirecting...', 'success');
|
showMessage('Payment simulated successfully! Redirecting...', 'success');
|
||||||
setTimeout(() => window.location.href = data.redirect, 1000);
|
setTimeout(() => window.location.href = data.redirect, 1000);
|
||||||
@@ -160,49 +196,99 @@ demoForm.addEventListener('submit', async function(e) {
|
|||||||
spinner.style.display = 'none';
|
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');
|
||||||
|
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: ?>
|
<?php else: ?>
|
||||||
// Stripe initialized
|
|
||||||
const stripe = Stripe('<?= $stripePublishableKey ?>');
|
const stripe = Stripe('<?= $stripePublishableKey ?>');
|
||||||
const elements = stripe.elements();
|
const elements = stripe.elements();
|
||||||
const cardElement = elements.create('card', {
|
const cardElement = elements.create('card', {
|
||||||
style: {
|
style: { base: { fontSize: '16px', color: '#1B1B1B', '::placeholder': { color: '#9CA3AF' } } }
|
||||||
base: {
|
|
||||||
fontSize: '16px',
|
|
||||||
color: '#1B1B1B',
|
|
||||||
'::placeholder': { color: '#9CA3AF' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cardElement.mount('#card-element');
|
cardElement.mount('#card-element');
|
||||||
|
|
||||||
// Handle validation errors
|
|
||||||
cardElement.on('change', function(event) {
|
cardElement.on('change', function(event) {
|
||||||
const displayError = document.getElementById('card-errors');
|
const displayError = document.getElementById('card-errors');
|
||||||
if (event.error) {
|
displayError.textContent = event.error ? event.error.message : '';
|
||||||
displayError.textContent = event.error.message;
|
|
||||||
} else {
|
|
||||||
displayError.textContent = '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stripe Checkout button (redirect to hosted page)
|
|
||||||
document.getElementById('checkout-btn').addEventListener('click', async function() {
|
document.getElementById('checkout-btn').addEventListener('click', async function() {
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
this.innerHTML = '<span class="loading"></span> Redirecting to Stripe...';
|
this.innerHTML = '<span class="loading"></span> Redirecting to Stripe...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/create-checkout-session.php', {
|
const response = await fetch('/api/create-checkout-session.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ order_id: orderId, origin_url: window.location.origin })
|
||||||
order_id: orderId,
|
|
||||||
origin_url: window.location.origin
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.demo_mode && data.redirect) {
|
if (data.demo_mode && data.redirect) {
|
||||||
window.location.href = data.redirect;
|
window.location.href = data.redirect;
|
||||||
} else if (data.url) {
|
} 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 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');
|
||||||
@@ -227,25 +312,20 @@ const spinner = document.getElementById('spinner');
|
|||||||
|
|
||||||
form.addEventListener('submit', async function(e) {
|
form.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
buttonText.style.display = 'none';
|
buttonText.style.display = 'none';
|
||||||
spinner.style.display = 'inline';
|
spinner.style.display = 'inline';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/create-payment-intent.php', {
|
const response = await fetch('/api/create-payment-intent.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ order_id: orderId })
|
body: JSON.stringify({ order_id: orderId })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.demo_mode && data.redirect) {
|
if (data.demo_mode && data.redirect) {
|
||||||
window.location.href = data.redirect;
|
window.location.href = data.redirect;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
showMessage(data.error, 'error');
|
showMessage(data.error, 'error');
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
@@ -253,8 +333,6 @@ form.addEventListener('submit', async function(e) {
|
|||||||
spinner.style.display = 'none';
|
spinner.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm payment with Stripe
|
|
||||||
const { error, paymentIntent } = await stripe.confirmCardPayment(data.client_secret, {
|
const { error, paymentIntent } = await stripe.confirmCardPayment(data.client_secret, {
|
||||||
payment_method: {
|
payment_method: {
|
||||||
card: cardElement,
|
card: cardElement,
|
||||||
@@ -264,14 +342,12 @@ form.addEventListener('submit', async function(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
showMessage(error.message, 'error');
|
showMessage(error.message, 'error');
|
||||||
} else if (paymentIntent.status === 'succeeded') {
|
} else if (paymentIntent.status === 'succeeded') {
|
||||||
showMessage('Payment successful! Redirecting...', 'success');
|
showMessage('Payment successful! Redirecting...', 'success');
|
||||||
setTimeout(() => window.location.href = '/order-confirmation.php?order=' + orderId, 1000);
|
setTimeout(() => window.location.href = '/order-confirmation.php?order=' + orderId, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showMessage('Payment failed. Please try again.', 'error');
|
showMessage('Payment failed. Please try again.', 'error');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -282,6 +358,7 @@ form.addEventListener('submit', async function(e) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user