post([$this, 'handlePaymentProcessing']) ->auth('public') ->rateLimit(2) ->register(); Route::for('square/saved-cards') ->post([$this, 'getSavedCards']) ->auth('user') ->rateLimit(5) ->register(); Route::for('square/order-history') ->get([$this, 'getOrderHistory']) ->auth('user') ->rateLimit(5) ->register(); Route::for(Route::pattern('square/order-status/{order_id}')) ->get([$this, 'getOrderStatus']) ->auth('public') ->rateLimit(20) ->register(); } //TODO: Are we processing this through our server at all? Or is it in the javascript going straight to square? public function handlePaymentProcessing($request):WP_REST_Response { $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 $this->validationError(['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 $this->success($existing_order); } // Generate idempotency key tied to this specific cart $idempotency_key = 'cart_' . $cart_id . '_' . time(); // Store key to prevent reprocessing //TODO: Should we just use our Cache.php? 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 $this->validationError(['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 $this->success($result); } catch (Exception $e) { $this->logError('Payment processing failed', [ 'error' => $e->getMessage(), 'idempotency_key' => $data['idempotency_key'] ]); return $this->error($e->getMessage()); } } public function getSavedCards(WP_REST_Request $request):WP_REST_Response { $data = $request->get_params(); $user_id = absint($data['user']??0); if ($user_id === 0) { return $this->validationError(['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 $this->success(['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 $this->error('Failed to fetch cards'); } return $this->success(['cards' => $cards_response['cards']??[]]); } public function getOrderHistory(WP_REST_Request $request):WP_REST_Response { $data = $request->get_params(); $user_id = absint($data['user']??0); if ($user_id === 0) { return $this->validationError(['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 = Meta::forPost($order->ID); $fields = $meta->getAll(['square_order_id', 'status', 'amount', 'items', 'created_at', 'pickup_time']); $order_data[] = array_merge([ 'wp_order_id' => $order->ID, ], $fields); } return $this->success(['orders' => $order_data]); } public function getOrderStatus(WP_REST_Request $request):WP_REST_Response { $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 $this->error('Order not found'); } $meta = Meta::forPost($wp_order_id); $fields = $meta->getAll(['status', 'fulfillment_status', 'pickup_time', 'items']); return $this->success(['order' => $fields]); } }