'POST', 'callback' => [$this, 'handlePaymentProcessing'], 'permission_callback' => '__return_true' // Adjust based on your auth ]); register_rest_route('jvb/v1/square', '/saved-cards', [ 'methods' => 'GET', 'callback' => [$this, 'getSavedCards'], 'permission_callback' => 'is_user_logged_in' ]); register_rest_route('jvb/v1/square', '/order-history', [ 'methods' => 'GET', 'callback' => [$this, 'getOrderHistory'], 'permission_callback' => 'is_user_logged_in' ]); register_rest_route('jvb/v1/square', '/order-status/(?P[a-zA-Z0-9_-]+)', [ 'methods' => 'GET', 'callback' => [$this, 'getOrderStatus'], 'permission_callback' => '__return_true' // Allow guests with order ID ]); } public function handlePaymentProcessing($request): array { $data = $request->get_json_params(); // Generate idempotency key from cart_id + timestamp // This ensures retries use SAME key $cart_id = $data['cart_id'] ?? ''; if (!$cart_id) { return ['success' => false, 'message' => 'Missing cart ID']; } // Check if we already processed this cart $existing_order = get_transient(BASE . 'cart_order_' . $cart_id); if ($existing_order) { // Return cached result - prevents double charge return $existing_order; } // Generate idempotency key tied to this specific cart $idempotency_key = 'cart_' . $cart_id . '_' . time(); // Store key to prevent reprocessing set_transient(BASE . 'cart_idempotency_' . $cart_id, $idempotency_key, HOUR_IN_SECONDS); // Validate required fields $required = ['source_id', 'amount', 'items', 'customer']; foreach ($required as $field) { if (empty($data[$field])) { return [ 'success' => false, 'message' => "Missing required field: {$field}" ]; } } try { $square = JVB()->connect('square'); // Step 1: Get or create Square customer $customer_id = $square->getOrCreateSquareCustomer($data['customer']); // Step 2: Create Order in Square $order_response = $square->createSquareOrder($data['items'], $customer_id, $data); if (is_wp_error($order_response)) { throw new Exception($order_response->get_error_message()); } $order_id = $order_response['order']['id'] ?? null; if (!$order_id) { throw new Exception('Failed to create Square order'); } // Step 3: Create Payment in Square $payment_response = $square->createSquarePayment( $data['source_id'], $data['idempotency_key'], $data['amount'], $order_id, $customer_id ); if (is_wp_error($payment_response)) { throw new Exception($payment_response->get_error_message()); } $payment = $payment_response['payment'] ?? null; if (!$payment) { throw new Exception('Failed to create Square payment'); } // Step 4: Save order reference in WordPress $wp_order_id = $square->saveOrderToWordPress([ 'square_order_id' => $order_id, 'square_payment_id' => $payment['id'], 'customer' => $data['customer'], 'items' => $data['items'], 'amount' => $data['amount'], 'status' => $payment['status'] ]); $result = [ 'success' => true, 'order_id' => $order_id, 'payment_id' => $payment['id'], 'wp_order_id' => $wp_order_id, 'status' => $payment['status'], 'receipt_url' => $payment['receipt_url'] ?? null ]; set_transient(BASE . 'cart_order_' . $cart_id, $result, HOUR_IN_SECONDS); return $result; } catch (Exception $e) { $this->logError('Payment processing failed', [ 'error' => $e->getMessage(), 'idempotency_key' => $data['idempotency_key'] ]); return [ 'success' => false, 'message' => $e->getMessage() ]; } } public function getSavedCards($request): array { $user_id = get_current_user_id(); if (!$user_id) { return ['success' => false, 'message' => 'Not logged in']; } $square = JVB()->connect('square'); // Get Square customer ID for this user $square_customer_id = get_user_meta($user_id, BASE . '_square_customer_id', true); if (!$square_customer_id) { return ['success' => true, 'cards' => []]; } // Fetch cards from Square (2025-compliant - separate endpoint) $cards_response = $square->getRequest('cards?customer_id=' . $square_customer_id); if (is_wp_error($cards_response)) { return ['success' => false, 'message' => 'Failed to fetch cards']; } return [ 'success' => true, 'cards' => $cards_response['cards'] ?? [] ]; } public function getOrderHistory($request): array { $user_id = get_current_user_id(); if (!$user_id) { return ['success' => false, 'message' => 'Not logged in']; } // Get orders from custom post type $orders = get_posts([ 'post_type' => BASE . '_sq_orders', 'author' => $user_id, 'posts_per_page' => 50, 'orderby' => 'date', 'order' => 'DESC' ]); $order_data = []; foreach ($orders as $order) { $meta = new \JVBase\meta\MetaManager($order->ID, 'post'); $order_data[] = [ 'wp_order_id' => $order->ID, 'square_order_id' => $meta->getValue('square_order_id'), 'status' => $meta->getValue('status'), 'amount' => $meta->getValue('amount'), 'items' => $meta->getValue('items'), 'created_at' => $meta->getValue('created_at'), 'pickup_time' => $meta->getValue('pickup_time') ]; } return [ 'success' => true, 'orders' => $order_data ]; } public function getOrderStatus($request): array { $order_id = $request->get_param('order_id'); // Find WP post by Square order ID $wp_order_id = get_option(BASE . 'square_order_map_' . $order_id); if (!$wp_order_id) { return ['success' => false, 'message' => 'Order not found']; } $meta = new \JVBase\meta\MetaManager($wp_order_id, 'post'); return [ 'success' => true, 'order' => [ 'status' => $meta->getValue('status'), 'fulfillment_status' => $meta->getValue('fulfillment_status'), 'pickup_time' => $meta->getValue('pickup_time'), 'items' => $meta->getValue('items') ] ]; } }