diff --git a/admin/order.php b/admin/order.php index 75dfd0f..049b685 100644 --- a/admin/order.php +++ b/admin/order.php @@ -5,6 +5,7 @@ ob_start(); $pageTitle = 'Order Details'; require_once __DIR__ . '/includes/header.php'; +require_once __DIR__ . '/../includes/square.php'; $orderId = $_GET['id'] ?? ''; @@ -24,41 +25,116 @@ if (!$order) { // Handle status update if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; - + if ($action === 'update_status') { $status = $_POST['status'] ?? ''; $trackingNumber = $_POST['tracking_number'] ?? ''; - + $updateData = ['order_status' => $status]; if ($trackingNumber) { $updateData['tracking_number'] = $trackingNumber; } - + db()->update('orders', $updateData, 'order_id = :id', ['id' => $orderId]); setFlash('success', 'Order status updated'); header('Location: /admin/order.php?id=' . $orderId); exit; } - + if ($action === 'add_note') { $note = trim($_POST['note'] ?? ''); if ($note) { $existingNotes = $order['notes'] ?? ''; $newNote = '[' . date('M j, Y g:i A') . '] ' . $note; $allNotes = $existingNotes ? $existingNotes . "\n" . $newNote : $newNote; - + db()->update('orders', ['notes' => $allNotes], 'order_id = :id', ['id' => $orderId]); setFlash('success', 'Note added'); header('Location: /admin/order.php?id=' . $orderId); exit; } } + + if ($action === 'process_refund') { + $refundAmount = (float) ($_POST['refund_amount'] ?? 0); + $reason = trim($_POST['refund_reason'] ?? ''); + + if ($order['payment_method'] !== 'square' || empty($order['square_payment_id'])) { + setFlash('error', 'This order has no Square payment to refund.'); + header('Location: /admin/order.php?id=' . $orderId); + exit; + } + if ($order['payment_status'] !== 'paid' && $order['payment_status'] !== 'partially_refunded') { + setFlash('error', 'Only paid orders can be refunded.'); + header('Location: /admin/order.php?id=' . $orderId); + exit; + } + if ($refundAmount <= 0 || $refundAmount > (float) $order['total']) { + setFlash('error', 'Invalid refund amount.'); + header('Location: /admin/order.php?id=' . $orderId); + exit; + } + + try { + $result = squareRefundPayment($order['square_payment_id'], $refundAmount, $reason ?: ('Order #' . $order['order_number'])); + $refund = $result['refund'] ?? []; + $refundStatus = $refund['status'] ?? ''; + + if (in_array($refundStatus, ['PENDING', 'COMPLETED'], true)) { + $isFullRefund = $refundAmount >= (float) $order['total']; + $newPaymentStatus = $isFullRefund ? 'refunded' : 'partially_refunded'; + + db()->update('orders', + ['payment_status' => $newPaymentStatus, 'order_status' => $isFullRefund ? 'refunded' : $order['order_status']], + 'order_id = :id', + ['id' => $orderId] + ); + + // Restore any wallet credit that was applied, proportionally on a full refund + if ($isFullRefund && !empty($order['wallet_amount_used']) && (float) $order['wallet_amount_used'] > 0 && !empty($order['customer_id'])) { + $walletAmount = (float) $order['wallet_amount_used']; + $cust = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $order['customer_id']]); + if ($cust) { + $newBalance = (float) $cust['wallet_balance'] + $walletAmount; + db()->update('customers', ['wallet_balance' => $newBalance], 'customer_id = :id', ['id' => $order['customer_id']]); + db()->insert('wallet_transactions', [ + 'transaction_id' => generateId('wt_'), + 'customer_id' => $order['customer_id'], + 'amount' => $walletAmount, + 'balance_after' => $newBalance, + 'type' => 'refund', + 'description' => 'Refund for Order #' . $order['order_number'], + ]); + } + } + + $existingNotes = $order['notes'] ?? ''; + $newNote = '[' . date('M j, Y g:i A') . '] Refunded ' . formatCurrency($refundAmount) + . ' via Square (refund ID: ' . ($refund['id'] ?? 'unknown') . ')' + . ($reason ? ' — ' . $reason : ''); + $allNotes = $existingNotes ? $existingNotes . "\n" . $newNote : $newNote; + db()->update('orders', ['notes' => $allNotes], 'order_id = :id', ['id' => $orderId]); + + setFlash('success', formatCurrency($refundAmount) . ' refunded successfully.'); + } else { + setFlash('error', 'Refund did not complete (status: ' . $refundStatus . ').'); + } + } catch (Exception $e) { + setFlash('error', 'Refund failed: ' . $e->getMessage()); + } + + header('Location: /admin/order.php?id=' . $orderId); + exit; + } } $items = json_decode($order['items'], true) ?? []; $shippingAddress = json_decode($order['shipping_address'], true) ?? []; $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded']; +$canRefund = $order['payment_method'] === 'square' + && !empty($order['square_payment_id']) + && in_array($order['payment_status'], ['paid', 'partially_refunded'], true); ?> - +
@@ -161,7 +246,7 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc

No notes yet.

- +
@@ -173,8 +258,34 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
+ + + +
+
+

Process Refund

+
+
+
+ +
+ + + Up to (full order total). Enter a lower amount for a partial refund. +
+
+ + +
+ +
+
+
+
- +
@@ -185,7 +296,7 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
- +
- +
- +
- +
@@ -218,7 +329,7 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc

- + View Customer @@ -226,7 +337,7 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
- +
@@ -236,14 +347,14 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc


- , - + , +

- +
@@ -260,13 +371,18 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc $paymentClass = match($order['payment_status']) { 'paid' => 'success', 'failed' => 'error', - 'refunded' => 'warning', + 'refunded', 'partially_refunded' => 'warning', default => 'primary' }; ?> - +
- + +
+ Square Payment ID + +
+
Stripe ID ... @@ -274,7 +390,7 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
- +
diff --git a/includes/square.php b/includes/square.php index ec4b330..ae14e76 100644 --- a/includes/square.php +++ b/includes/square.php @@ -73,6 +73,21 @@ function squareGetPayment(string $paymentId): array { return squareApi('GET', '/payments/' . $paymentId); } +function squareRefundPayment(string $paymentId, float $amount, string $reason = ''): array { + $body = [ + 'idempotency_key' => 'refund_' . $paymentId . '_' . bin2hex(random_bytes(6)), + 'amount_money' => [ + 'amount' => (int) round($amount * 100), + 'currency' => 'USD', + ], + 'payment_id' => $paymentId, + ]; + if ($reason) { + $body['reason'] = substr($reason, 0, 192); + } + return squareApi('POST', '/refunds', $body); +} + 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)