`, so an * order the live hook captures at the same moment is never sent twice. * * @package OrderPing */ defined( 'ABSPATH' ) || exit; // The plugin owns its custom outbox table: direct queries are the only way to use it. The interpolated name is an // internal, prefixed function result and never user input; every value goes through $wpdb->prepare(). // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter /** Only orders created within this many seconds are re-captured. */ const ORDERPING_RECAPTURE_WINDOW = DAY_IN_SECONDS; /** Missed orders captured per sweep run. */ const ORDERPING_RECAPTURE_BATCH = 50; /** Orders examined per query page, and the most pages one run reads. */ const ORDERPING_RECAPTURE_PAGE = 50; const ORDERPING_RECAPTURE_MAX_PAGES = 10; /** Outbox rows read for the `exclude` list: window margin (seconds) and row cap. */ const ORDERPING_RECAPTURE_MARGIN = HOUR_IN_SECONDS; const ORDERPING_RECAPTURE_EXCLUDE_MAX = 2000; /** * Connect time of the current pairing. Pairings made before it was recorded are * stamped on first sight, so nothing placed before the upgrade is back-filled. * * @return int Unix time, or 0 when not connected. */ function orderping_recapture_connected_at() { $conn = get_option( ORDERPING_OPTION_CONNECTION ); if ( ! is_array( $conn ) ) { return 0; } if ( empty( $conn['connected_at'] ) ) { $conn['connected_at'] = time(); update_option( ORDERPING_OPTION_CONNECTION, $conn, false ); } return (int) $conn['connected_at']; } /** * Whether an outbox row already exists for this event key (in any status). * * @param string $key Event key. * @return bool */ function orderping_outbox_event_exists( $key ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return null !== $wpdb->get_var( $wpdb->prepare( 'SELECT 1 FROM ' . orderping_outbox_table() . ' WHERE event_key = %s', $key ) ); } /** * Order ids that already have a `placed-` outbox row created within the window * (plus a margin), newest first. Passed to wc_get_orders as `exclude` so the * scan only returns orders without a row and captured orders cannot hide a * missed one. Bounded; the per-order check in the sweep is the safety net. * * @param int $since Window start (Unix time). * @return int[] */ function orderping_recent_captured_order_ids( $since ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $ids = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT order_id FROM ' . orderping_outbox_table() . ' WHERE event_key LIKE %s AND created_at >= %d ORDER BY created_at DESC LIMIT %d', 'placed-%', $since - ORDERPING_RECAPTURE_MARGIN, ORDERPING_RECAPTURE_EXCLUDE_MAX ) ); return is_array( $ids ) ? array_map( 'intval', $ids ) : array(); } /** * Whether the sweep should hand this order to capture. Cheap checks only; runs * before the per-run budget is spent. * * @param WC_Order $order Order. * @return bool */ function orderping_recapture_candidate( $order ) { return $order instanceof WC_Order && in_array( $order->get_status(), ORDERPING_PLACED_STATUSES, true ) && in_array( $order->get_created_via(), ORDERPING_CHECKOUT_SOURCES, true ) && orderping_order_in_scope( $order ) && ! orderping_outbox_event_exists( 'placed-' . (int) $order->get_id() ); } /** * Re-captures recent placed checkout orders with no outbox row. Bounded, never * throws, never makes an HTTP request. Only orders handed to capture count * against the per-run budget. */ function orderping_sweep_missed_orders() { try { if ( null === orderping_connection() ) { return; } $now = time(); $since = max( $now - ORDERPING_RECAPTURE_WINDOW, orderping_recapture_connected_at() ); $exclude = orderping_recent_captured_order_ids( $since ); $done = 0; for ( $page = 0; $page < ORDERPING_RECAPTURE_MAX_PAGES && $done < ORDERPING_RECAPTURE_BATCH; $page++ ) { $args = array( 'status' => ORDERPING_PLACED_STATUSES, 'created_via' => ORDERPING_CHECKOUT_SOURCES, 'date_created' => $since . '...' . $now, 'limit' => ORDERPING_RECAPTURE_PAGE, 'offset' => $page * ORDERPING_RECAPTURE_PAGE, 'orderby' => 'date', 'order' => 'ASC', ); if ( array() !== $exclude ) { // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- bounded id list (ORDERPING_RECAPTURE_EXCLUDE_MAX); keeps captured orders from hiding a missed one. $args['exclude'] = $exclude; } $orders = wc_get_orders( $args ); if ( ! is_array( $orders ) || array() === $orders ) { break; } foreach ( $orders as $order ) { if ( $done >= ORDERPING_RECAPTURE_BATCH ) { break; } try { if ( ! orderping_recapture_candidate( $order ) ) { continue; } $done++; orderping_mark_capture_eligible( $order ); orderping_capture_order( $order ); } catch ( \Throwable $e ) { continue; // One bad order must not stop the rest. } } if ( count( $orders ) < ORDERPING_RECAPTURE_PAGE ) { break; } } } catch ( \Throwable $e ) { return; } }