' . __('Sorry, there was an issue creating your site. Please contact support for assistance.', 'iwp-wp-integration') . '
'; + $msg = class_exists('IWP_Site_Manager') + ? IWP_Site_Manager::resolve_failure_message($site['api_response'] ?? null) + : null; + if ($msg) { + echo '' . esc_html($msg) . '
'; + } else { + echo '' . __('Sorry, there was an issue creating your site. Please contact support for assistance.', 'iwp-wp-integration') . '
'; + } echo '' . __('Your site is being created. You will receive another email when it\'s ready.', 'iwp-wp-integration') . '
'; } elseif ($status === 'failed') { - echo '' . __('There was an issue creating your site. Please contact support.', 'iwp-wp-integration') . '
'; + $msg = class_exists('IWP_Site_Manager') + ? IWP_Site_Manager::resolve_failure_message($site['api_response'] ?? null) + : null; + $err_text = $msg + ? esc_html($msg) + : __('There was an issue creating your site. Please contact support.', 'iwp-wp-integration'); + echo '' . $err_text . '
'; } echo ''; } @@ -1056,20 +1071,30 @@ public function display_customer_sites() { return; } - // Check for orders with deferred items (pending setup) - $deferred_orders = wc_get_orders(array( + // Check for orders with deferred items (pending setup). + // Routed through IWP_Woo_HPOS::get_orders() so the meta_query is + // evaluated correctly on both CPT and HPOS data stores and picks up + // legacy values that may still live in wp_postmeta. + $deferred_orders = IWP_Woo_HPOS::get_orders(array( 'customer_id' => $customer_id, 'limit' => 5, 'status' => array('completed', 'processing'), - 'meta_key' => '_iwp_deferred_items', 'orderby' => 'date', 'order' => 'DESC', + 'meta_query' => array( + array( + 'key' => '_iwp_deferred_items', + 'compare' => 'EXISTS', + ), + ), )); // Show pending setup notice with link to order if (!empty($deferred_orders)) { foreach ($deferred_orders as $def_order) { - $deferred_items = get_post_meta($def_order->get_id(), '_iwp_deferred_items', true); + // Read via the HPOS-safe helper: covers both wp_wc_orders_meta + // and wp_postmeta, with forward-migration for legacy values. + $deferred_items = IWP_Woo_HPOS::get_order_meta($def_order, '_iwp_deferred_items'); if (!empty($deferred_items) && is_array($deferred_items)) { $order_url = $def_order->get_view_order_url(); $count = count($deferred_items); @@ -1090,17 +1115,19 @@ public function display_customer_sites() { } } - // Get customer's orders with sites - $orders = wc_get_orders(array( + // Get customer's orders with created sites. + // Same compat helper as above — keeps the standard meta_query shape at + // the call site while handling the CPT vs HPOS split and legacy data. + $orders = IWP_Woo_HPOS::get_orders(array( 'customer_id' => $customer_id, - 'limit' => -1, - 'status' => array('completed', 'processing'), - 'meta_query' => array( + 'limit' => -1, + 'status' => array('completed', 'processing'), + 'meta_query' => array( array( - 'key' => '_iwp_created_sites', - 'compare' => 'EXISTS' - ) - ) + 'key' => '_iwp_created_sites', + 'compare' => 'EXISTS', + ), + ), )); if (empty($orders) && empty($deferred_orders)) { diff --git a/includes/frontend/class-iwp-onboarding.php b/includes/frontend/class-iwp-onboarding.php index 6046b01..2b632e1 100644 --- a/includes/frontend/class-iwp-onboarding.php +++ b/includes/frontend/class-iwp-onboarding.php @@ -53,9 +53,11 @@ public function render_shortcode($atts) { ''; } - $order_id = $order->get_id(); - $deferred_items = get_post_meta($order_id, '_iwp_deferred_items', true); - $sites_created = get_post_meta($order_id, '_iwp_sites_created', true); + $order_id = $order->get_id(); + // HPOS-safe reads so the onboarding page resolves correctly under both + // data stores and picks up any legacy postmeta-only values. + $deferred_items = IWP_Woo_HPOS::get_order_meta($order_id, '_iwp_deferred_items'); + $sites_created = IWP_Woo_HPOS::get_order_meta($order_id, '_iwp_sites_created'); $redirect_url = !empty($atts['redirect']) ? esc_url($atts['redirect']) : ''; ob_start(); @@ -211,8 +213,8 @@ public function handle_create_site() { wp_send_json_error(array('message' => __('You do not have permission to access this order.', 'iwp-wp-integration'))); } - // Get deferred items - $deferred_items = get_post_meta($order_id, '_iwp_deferred_items', true); + // Get deferred items — HPOS-safe read covers both data stores. + $deferred_items = IWP_Woo_HPOS::get_order_meta($order_id, '_iwp_deferred_items'); if (empty($deferred_items) || !isset($deferred_items[$item_index])) { wp_send_json_error(array('message' => __('No pending site setup found for this item.', 'iwp-wp-integration'))); } @@ -318,11 +320,11 @@ public function handle_create_site() { 'order_id' => $order_id, 'error' => $result->get_error_message(), )); - wp_send_json_error(array('message' => $result->get_error_message())); + wp_send_json_error(array('message' => IWP_API_Client::humanize_error($result))); } - // Store result in _iwp_sites_created - $sites_created = get_post_meta($order_id, '_iwp_sites_created', true); + // Store result in _iwp_sites_created — HPOS-safe read/write. + $sites_created = IWP_Woo_HPOS::get_order_meta($order_id, '_iwp_sites_created'); if (!is_array($sites_created)) { $sites_created = array(); } @@ -333,15 +335,16 @@ public function handle_create_site() { 'action' => 'created', 'source' => 'onboarding', ); - update_post_meta($order_id, '_iwp_sites_created', $sites_created); + IWP_Woo_HPOS::update_order_meta($order_id, '_iwp_sites_created', $sites_created); - // Remove this item from deferred list + // Remove this item from deferred list. Use delete_order_meta when the + // list is emptied so we don't leave an orphan empty-array row. unset($deferred_items[$item_index]); $deferred_items = array_values($deferred_items); // Re-index if (empty($deferred_items)) { - delete_post_meta($order_id, '_iwp_deferred_items'); + IWP_Woo_HPOS::delete_order_meta($order_id, '_iwp_deferred_items'); } else { - update_post_meta($order_id, '_iwp_deferred_items', $deferred_items); + IWP_Woo_HPOS::update_order_meta($order_id, '_iwp_deferred_items', $deferred_items); } // Add order note @@ -402,7 +405,8 @@ public function maybe_redirect_to_onboarding() { return; } - $deferred_items = get_post_meta($order_id, '_iwp_deferred_items', true); + // HPOS-safe read of the deferred-items list on the order. + $deferred_items = IWP_Woo_HPOS::get_order_meta($order_id, '_iwp_deferred_items'); if (empty($deferred_items) || !is_array($deferred_items)) { return; } diff --git a/includes/integrations/woocommerce/class-iwp-woo-hpos.php b/includes/integrations/woocommerce/class-iwp-woo-hpos.php index fe0f817..39d3730 100644 --- a/includes/integrations/woocommerce/class-iwp-woo-hpos.php +++ b/includes/integrations/woocommerce/class-iwp-woo-hpos.php @@ -16,6 +16,55 @@ */ class IWP_Woo_HPOS { + /** + * Resolve the first argument of the meta helpers to a loaded WC_Abstract_Order. + * + * Accepts either an order ID (int or numeric string) or an already-loaded + * WC_Abstract_Order (which covers WC_Order, WC_Subscription, + * WC_Order_Refund). Callers in loops should pass the object directly to + * avoid a redundant wc_get_order() lookup per iteration. + * + * Strict input contract: + * - WC_Abstract_Order instance → returned as-is. + * - Positive numeric (int or "123") → loaded; returns false (debug + * log) if no order exists. + * - Anything else (empty, 0, negative, + * non-numeric, array, null, object) → warn-log the bad type, false. + * + * Assumes WooCommerce is loaded — the class file is only required by + * iwp-wp-integration.php inside class_exists('WooCommerce'). + * + * @param int|numeric-string|WC_Abstract_Order $order_or_id + * @return WC_Abstract_Order|false + */ + private static function resolve_order_arg($order_or_id) { + if ($order_or_id instanceof WC_Abstract_Order) { + return $order_or_id; + } + + if (is_numeric($order_or_id) && ((int) $order_or_id) > 0) { + $order = self::get_order((int) $order_or_id); + if ($order instanceof WC_Abstract_Order) { + return $order; + } + if (class_exists('IWP_Logger')) { + IWP_Logger::debug('HPOS helper: order not found for ID', 'hpos-compat', array( + 'order_id' => (int) $order_or_id, + )); + } + return false; + } + + // Anything else is a caller bug — log loud so it surfaces in dev. + if (class_exists('IWP_Logger')) { + IWP_Logger::warning('HPOS helper: invalid order argument', 'hpos-compat', array( + 'type' => is_object($order_or_id) ? get_class($order_or_id) : gettype($order_or_id), + 'value' => is_scalar($order_or_id) ? (string) $order_or_id : null, + )); + } + return false; + } + /** * Constructor */ @@ -64,27 +113,246 @@ public static function get_order($order_id) { } /** - * Get orders (HPOS compatible) + * Get orders — transparent HPOS/CPT compat wrapper around wc_get_orders(). * - * @param array $args Query arguments - * @return WC_Order[] + * Callers pass a normal wc_get_orders() arg array, INCLUDING `meta_query` + * in its standard WP_Query shape. This wrapper handles the data-store and + * legacy-data quirks so callers don't have to branch on is_hpos_enabled(). + * + * Why we don't pass `meta_query` through to wc_get_orders() on either store: + * + * - CPT data store: the base `WC_Data_Store_WP::get_wp_query_args()` + * explicitly strips `meta_query` from $query_vars + * (class-wc-data-store-wp.php:284 — `'meta_query' === $key` → continue). + * WC 9.2+ additionally emits _doing_it_wrong when it sees the arg + * (class-wc-order-data-store-cpt.php:1076-1100). So `meta_query` + * never filters anything on CPT via the top-level arg. + * + * - HPOS authoritative: `OrdersTableQuery` DOES support `meta_query` + * natively (OrdersTableQuery.php:791-793 → OrdersTableMetaQuery), + * but it only joins wp_wc_orders_meta. Orders whose IWP meta was + * written by pre-HPOS-fix plugin builds (via update_post_meta()) have + * their values in wp_postmeta and get silently skipped by the native + * HPOS query. + * + * Unified strategy (no branching on data store): + * + * 1. Extract `meta_query` from $args before calling wc_get_orders(). + * 2. Fetch orders using only first-class args (customer_id, status, limit, …). + * These are supported identically by both data stores with no notices. + * 3. Evaluate the meta filter in PHP via self::get_order_meta(). That + * reader already handles the active-store / legacy-postmeta fallback + * and opportunistically migrates legacy values forward on first read, + * so a legacy order becomes visible once and repairs its own storage + * on the way through. + * + * Supported meta_query shape: an array of clauses each of the form + * array('key' => '