diff --git a/admin/payments.php b/admin/payments.php index 2c96e61..81d77e1 100644 --- a/admin/payments.php +++ b/admin/payments.php @@ -9,7 +9,7 @@ require_once __DIR__ . '/includes/header.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $section = $_POST['section'] ?? ''; - + if ($section === 'stripe') { setSetting('payment_stripe', [ 'enabled' => isset($_POST['stripe_enabled']), @@ -20,7 +20,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ]); 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') { setSetting('payment_methods', [ 'card' => isset($_POST['method_card']), @@ -30,7 +42,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ]); setFlash('success', 'Payment methods updated'); } - + header('Location: /admin/payments.php'); exit; } @@ -43,6 +55,15 @@ $stripe = getSetting('payment_stripe', [ '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', [ 'card' => true, 'cash' => true, @@ -70,14 +91,70 @@ $methods = getSetting('payment_methods', [ - +
- + +
+ +
+
+

Square

+
+
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + From the Square Developer Dashboard's webhook subscription for this site +
+ + +
+
+
+ +
-

Stripe

+

Stripe (legacy)

@@ -86,41 +163,41 @@ $methods = getSetting('payment_methods', [ Enable Stripe payments
- +
- +
-
- +
-
- +
- Get this from your Stripe webhook settings
- +
- +
@@ -130,35 +207,35 @@ $methods = getSetting('payment_methods', [

Select which payment methods are available in the Point of Sale system.

- +
- +
- +
- +
- +
diff --git a/api/create-checkout-session.php b/api/create-checkout-session.php deleted file mode 100644 index 61f2117..0000000 --- a/api/create-checkout-session.php +++ /dev/null @@ -1,129 +0,0 @@ - '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); -} diff --git a/api/create-payment-intent.php b/api/create-payment-intent.php deleted file mode 100644 index ae8ca5a..0000000 --- a/api/create-payment-intent.php +++ /dev/null @@ -1,87 +0,0 @@ - '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); -} diff --git a/api/create-square-payment.php b/api/create-square-payment.php new file mode 100644 index 0000000..a32e1dc --- /dev/null +++ b/api/create-square-payment.php @@ -0,0 +1,94 @@ + '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); +} diff --git a/api/payment-status.php b/api/payment-status.php index 6bec0c3..9da23e0 100644 --- a/api/payment-status.php +++ b/api/payment-status.php @@ -1,157 +1,93 @@ 'Method not allowed'], 405); } $orderId = $_GET['order_id'] ?? ''; -$sessionId = $_GET['session_id'] ?? ''; -if (empty($orderId) && empty($sessionId)) { - jsonResponse(['error' => 'Order ID or Session ID required'], 400); +if (empty($orderId)) { + jsonResponse(['error' => 'Order ID required'], 400); } -// Get order by ID or session -if (!empty($orderId)) { - $order = db()->fetch( - "SELECT * FROM orders WHERE order_id = :id", - ['id' => $orderId] - ); -} else { - $order = db()->fetch( - "SELECT * FROM orders WHERE stripe_session_id = :session OR stripe_payment_intent = :session", - ['session' => $sessionId] - ); -} +$order = db()->fetch( + "SELECT * FROM orders WHERE order_id = :id", + ['id' => $orderId] +); if (!$order) { jsonResponse(['error' => 'Order not found'], 404); } -// If already marked as paid, return success if ($order['payment_status'] === 'paid') { jsonResponse([ - 'status' => 'complete', + 'status' => 'complete', 'payment_status' => 'paid', - 'order_id' => $order['order_id'], - 'order_number' => $order['order_number'], - 'redirect' => '/order-confirmation.php?order=' . $order['order_id'] + 'order_id' => $order['order_id'], + 'order_number' => $order['order_number'], + 'redirect' => '/order-confirmation.php?order=' . $order['order_id'] ]); } -// Check if Stripe is configured -if (!isStripeConfigured()) { +if (!isSquareConfigured()) { jsonResponse([ - 'status' => 'demo_mode', + 'status' => 'demo_mode', 'payment_status' => $order['payment_status'], - 'message' => 'Stripe not configured - running in demo mode' + 'message' => 'Square not configured - running in demo mode' + ]); +} + +if (empty($order['square_payment_id'])) { + jsonResponse([ + 'status' => 'pending', + 'payment_status' => $order['payment_status'] ]); } try { - // 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']] - ); + $result = squareGetPayment($order['square_payment_id']); + $payment = $result['payment'] ?? []; + $status = $payment['status'] ?? ''; - // Award loyalty points - if (!empty($order['customer_id'])) { - loyalty()->awardPoints( - $order['customer_id'], - (float) $order['total'], - 'Order #' . $order['order_number'], - $order['order_id'] - ); - } + markSquarePaymentResult($order['square_payment_id'], $order['order_id'], $status); - jsonResponse([ - 'status' => 'complete', - 'payment_status' => 'paid', - 'order_id' => $order['order_id'], - 'order_number' => $order['order_number'], - 'redirect' => '/order-confirmation.php?order=' . $order['order_id'] - ]); - } - + $updated = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $order['order_id']]); + + if ($updated['payment_status'] === 'paid') { 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' + 'status' => 'complete', + 'payment_status' => 'paid', + 'order_id' => $updated['order_id'], + 'order_number' => $updated['order_number'], + 'redirect' => '/order-confirmation.php?order=' . $updated['order_id'] ]); } - - // No Stripe reference found + jsonResponse([ - 'status' => 'pending', - 'payment_status' => $order['payment_status'] + 'status' => $status, + 'payment_status' => $updated['payment_status'] ]); - + } catch (Exception $e) { - error_log('Payment status check error: ' . $e->getMessage()); + error_log('Square payment status check error: ' . $e->getMessage()); jsonResponse([ - 'status' => 'error', + 'status' => 'error', 'payment_status' => $order['payment_status'], - 'error' => 'Failed to check payment status' + 'error' => 'Failed to check payment status' ]); } diff --git a/api/webhook.php b/api/webhook.php index beb9113..02701ee 100644 --- a/api/webhook.php +++ b/api/webhook.php @@ -1,107 +1,65 @@ 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); - exit(); - } -} else { - $event = json_decode($payload, true); - if (!$event) { - http_response_code(400); - exit(); - } +if (empty(SQUARE_WEBHOOK_SIGNATURE_KEY) || SQUARE_WEBHOOK_SIGNATURE_KEY === 'REPLACE_ME') { + error_log('Square webhook signature key not configured - rejecting webhook'); + http_response_code(400); + exit(); +} + +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); +if (!$event) { + http_response_code(400); + exit(); } $eventType = $event['type'] ?? ''; -$data = $event['data']['object'] ?? []; switch ($eventType) { - case 'checkout.session.completed': - // Stripe Checkout (hosted page) — metadata is on the session - $orderId = $data['metadata']['order_id'] ?? ''; - $paymentIntentId = $data['payment_intent'] ?? ''; - if ($orderId && ($data['payment_status'] ?? '') === 'paid') { - 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); - } + case 'payment.updated': + case 'payment.created': + $payment = $event['data']['object']['payment'] ?? []; + $paymentId = $payment['id'] ?? ''; + $status = $payment['status'] ?? ''; + $orderId = $payment['reference_id'] ?? null; + if ($paymentId && $status) { + markSquarePaymentResult($paymentId, $orderId, $status); } break; - case 'payment_intent.succeeded': - // Payment Intent flow (embedded/direct) - skip if already confirmed by checkout.session.completed - $paymentIntentId = $data['id'] ?? ''; - $orderId = $data['metadata']['order_id'] ?? ''; - if ($orderId && ($data['status'] ?? '') === 'succeeded') { - $order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]); - 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) { + case 'refund.updated': + case 'refund.created': + $refund = $event['data']['object']['refund'] ?? []; + $paymentId = $refund['payment_id'] ?? ''; + $status = $refund['status'] ?? ''; + if ($status === 'COMPLETED' && $paymentId) { db()->update('orders', [ 'payment_status' => 'refunded', 'order_status' => 'refunded', ], - 'stripe_payment_intent = :pi', - ['pi' => $paymentIntentId] + 'square_payment_id = :pid', + ['pid' => $paymentId] ); } break; @@ -109,4 +67,3 @@ switch ($eventType) { http_response_code(200); echo json_encode(['received' => true]); - diff --git a/db/schema.sql b/db/schema.sql index a07852f..46b4dd8 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -326,6 +326,8 @@ CREATE TABLE `orders` ( `order_status` enum('pending','confirmed','processing','shipped','delivered','cancelled','refunded') DEFAULT 'pending', `stripe_session_id` 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_url` varchar(500) DEFAULT NULL, `notes` text DEFAULT NULL, diff --git a/includes/square.php b/includes/square.php new file mode 100644 index 0000000..6260cec --- /dev/null +++ b/includes/square.php @@ -0,0 +1,143 @@ + 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']] + ); + } +} diff --git a/payment.php b/payment.php index 36ed4be..d90f2b1 100644 --- a/payment.php +++ b/payment.php @@ -1,13 +1,20 @@ 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'; Payment was cancelled. Please try again. - +
Order # @@ -61,47 +71,76 @@ require_once __DIR__ . '/includes/header.php';

- - - -
- Demo Mode: Stripe is not configured. Click below to simulate a successful payment. -
- - - - - -
- -

or enter card details below

-
- -
-
- -
-
+ + + +
+ Demo Mode: Square is not configured. Click below to simulate a successful payment.
- - - +
+ +
+ +
+
+ +
+
+
+ + +
+ + + +
+ Demo Mode: Stripe is not configured. Click below to simulate a successful payment. +
+
+ +
+ +
+ +

or enter card details below

+
+ +
+
+ +
+
+
+ + +
+ - + - +

Your payment is secure and encrypted

@@ -110,9 +149,12 @@ require_once __DIR__ . '/includes/header.php';
+ + + +