Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 242
0.00% covered (danger)
0.00%
0 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
WooCommerce
0.00% covered (danger)
0.00%
0 / 240
0.00% covered (danger)
0.00%
0 / 39
12656
0.00% covered (danger)
0.00%
0 / 1
 table_name
n/a
0 / 0
n/a
0 / 0
1
 table
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 id_field
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 full_sync_action_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 init_listeners
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
2
 init_full_sync_listeners
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_full_sync_actions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 init_before_send
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 filter_order_item
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_order_total_to_new_order
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
90
 add_order_total_to_status_changed
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
132
 maybe_append_order_total
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 claim_order_total_emission
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 build_order_total_payload
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 is_paid_order_status
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 filter_customer_updated_meta
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
156
 maybe_sync_customer_meta_update
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 action_delete_user
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 action_customer_meta_updates
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 get_customer_detail_props
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 build_minimal_customer_user_object
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 filter_meta
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 is_whitelisted_order_item_meta
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 action_woocommerce_remove_order_items
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 expand_order_item_ids
n/a
0 / 0
n/a
0 / 0
1
 build_order_item
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_full_sync_actions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 estimate_full_sync_actions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 get_where_sql
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_woocommerce_options_whitelist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_woocommerce_constants_whitelist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_woocommerce_post_meta_whitelist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_woocommerce_comment_meta_whitelist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_review_comment_types
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 filter_action_scheduler_comments
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 get_objects_by_id
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_order_item_by_ids
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 build_full_sync_action_array
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_next_chunk
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * WooCommerce sync module.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync\Modules;
9
10use WC_Order;
11use WP_Error;
12
13if ( ! defined( 'ABSPATH' ) ) {
14    exit( 0 );
15}
16
17/**
18 * Class to handle sync for WooCommerce.
19 */
20class WooCommerce extends Module {
21    /**
22     * Whitelist for order item meta we are interested to sync.
23     *
24     * @access private
25     *
26     * @var array
27     */
28    public static $order_item_meta_whitelist = array(
29        // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-product-store.php#L20 .
30        '_product_id',
31        '_variation_id',
32        '_qty',
33        // Tax ones also included in below class
34        // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-fee-data-store.php#L20 .
35        '_tax_class',
36        '_tax_status',
37        '_line_subtotal',
38        '_line_subtotal_tax',
39        '_line_total',
40        '_line_tax',
41        '_line_tax_data',
42        // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-shipping-data-store.php#L20 .
43        'method_id',
44        'cost',
45        'total_tax',
46        'taxes',
47        // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-tax-data-store.php#L20 .
48        'rate_id',
49        'label',
50        'compound',
51        'tax_amount',
52        'shipping_tax_amount',
53        // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-coupon-data-store.php .
54        'discount_amount',
55        'discount_amount_tax',
56    );
57
58    /**
59     * Mapping between WooCommerce customer detail user meta keys and customer prop names.
60     *
61     * @access private
62     *
63     * @var array
64     */
65    private static $customer_detail_meta_key_to_prop = array(
66        'paying_customer'     => 'is_paying_customer',
67        'billing_first_name'  => 'billing_first_name',
68        'billing_last_name'   => 'billing_last_name',
69        'billing_company'     => 'billing_company',
70        'billing_address_1'   => 'billing_address_1',
71        'billing_address_2'   => 'billing_address_2',
72        'billing_city'        => 'billing_city',
73        'billing_state'       => 'billing_state',
74        'billing_postcode'    => 'billing_postcode',
75        'billing_country'     => 'billing_country',
76        'billing_email'       => 'billing_email',
77        'billing_phone'       => 'billing_phone',
78        'shipping_first_name' => 'shipping_first_name',
79        'shipping_last_name'  => 'shipping_last_name',
80        'shipping_company'    => 'shipping_company',
81        'shipping_address_1'  => 'shipping_address_1',
82        'shipping_address_2'  => 'shipping_address_2',
83        'shipping_city'       => 'shipping_city',
84        'shipping_state'      => 'shipping_state',
85        'shipping_postcode'   => 'shipping_postcode',
86        'shipping_country'    => 'shipping_country',
87        'shipping_phone'      => 'shipping_phone',
88    );
89
90    /**
91     * Name of the order item database table.
92     *
93     * @access private
94     *
95     * @var string
96     */
97    private $order_item_table_name;
98
99    /**
100     * Customer detail meta changes to sync at the end of the request.
101     *
102     * @var array
103     */
104    private $customer_meta_updates = array();
105
106    /**
107     * User IDs deleted during the current request.
108     *
109     * @var array
110     */
111    private $deleted_user_ids = array();
112
113    /**
114     * Order IDs whose total we've already emitted this request, so an order's total is emitted once
115     * even if both woocommerce_new_order and woocommerce_order_status_changed observe it.
116     *
117     * @var array
118     */
119    private $synced_order_total_keys = array();
120
121    /**
122     * Cached list of order statuses WooCommerce considers paid, memoized per request to avoid
123     * re-running wc_get_is_paid_statuses() (and its filters) on every order this request observes.
124     *
125     * @var array|null
126     */
127    private $paid_order_statuses = null;
128
129    /**
130     * The table name.
131     *
132     * @access public
133     *
134     * @return string
135     * @deprecated since 3.11.0 Use table() instead.
136     */
137    public function table_name() {
138        _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\WooCommerce->table' );
139        return $this->order_item_table_name;
140    }
141
142    /**
143     * The table in the database with the prefix.
144     *
145     * @access public
146     *
147     * @return string|bool
148     */
149    public function table() {
150        global $wpdb;
151        return $wpdb->prefix . 'woocommerce_order_items';
152    }
153
154    /**
155     * The id field in the database.
156     *
157     * @access public
158     *
159     * @return string
160     */
161    public function id_field() {
162        return 'order_item_id';
163    }
164
165    /**
166     * The full sync action name for this module.
167     *
168     * @access public
169     *
170     * @return string
171     */
172    public function full_sync_action_name() {
173        return 'jetpack_full_sync_woocommerce_order_items';
174    }
175
176    /**
177     * Constructor.
178     *
179     * @global $wpdb
180     *
181     * @todo Should we refactor this to use $this->set_defaults() instead?
182     */
183    public function __construct() {
184        global $wpdb;
185        $this->order_item_table_name = $wpdb->prefix . 'woocommerce_order_items';
186
187        // Options, constants and post meta whitelists.
188        add_filter( 'jetpack_sync_options_whitelist', array( $this, 'add_woocommerce_options_whitelist' ), 10 );
189        add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_woocommerce_constants_whitelist' ), 10 );
190        add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 );
191        add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 );
192
193        add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) );
194        add_filter( 'jetpack_sync_before_enqueue_jetpack_updated_woo_customer_meta', array( $this, 'filter_customer_updated_meta' ) );
195
196        // Append an order's total to these actions when it reaches a paid status.
197        add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order', array( $this, 'add_order_total_to_new_order' ) );
198        add_filter( 'jetpack_sync_before_enqueue_woocommerce_order_status_changed', array( $this, 'add_order_total_to_status_changed' ) );
199        add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) );
200
201        // Blacklist Action Scheduler comment types.
202        add_filter( 'jetpack_sync_prevent_sending_comment_data', array( $this, 'filter_action_scheduler_comments' ), 10, 2 );
203
204        // Preprocess action to be sent by Jetpack sync.
205        add_action( 'woocommerce_remove_order_items', array( $this, 'action_woocommerce_remove_order_items' ), 10, 2 );
206    }
207
208    /**
209     * Sync module name.
210     *
211     * @access public
212     *
213     * @return string
214     */
215    public function name() {
216        return 'woocommerce';
217    }
218
219    /**
220     * Initialize WooCommerce action listeners.
221     *
222     * @access public
223     *
224     * @param callable $callable Action handler callable.
225     */
226    public function init_listeners( $callable ) {
227        // Attributes.
228        add_action( 'woocommerce_attribute_added', $callable, 10, 2 );
229        add_action( 'woocommerce_attribute_updated', $callable, 10, 3 );
230        add_action( 'woocommerce_attribute_deleted', $callable, 10, 3 );
231
232        // Orders. When an order reaches a paid status we append its total to these actions (via the
233        // jetpack_sync_before_enqueue_* filters in the constructor) so the Activity Log can aggregate
234        // revenue without a dedicated action. We register the extra accepted args so those filters
235        // receive the order object WooCommerce already passes (2nd arg here, 4th for the status change)
236        // and can avoid reloading it on this hot path; the filters strip the object back out before the
237        // action is enqueued, so it is never serialized or sent to WPcom.
238        add_action( 'woocommerce_new_order', $callable, 10, 2 );
239        add_action( 'woocommerce_order_status_changed', $callable, 10, 4 );
240        add_action( 'woocommerce_payment_complete', $callable, 10, 1 );
241
242        // Order items.
243        add_action( 'woocommerce_new_order_item', $callable, 10, 4 );
244        add_action( 'woocommerce_delete_order_item', $callable, 10, 1 );
245        add_action( 'woocommerce_remove_order_item_ids', $callable, 10, 1 );
246        $this->init_listeners_for_meta_type( 'order_item', $callable );
247        $this->init_meta_whitelist_handler( 'order_item', array( $this, 'filter_meta' ) );
248
249        // Payment tokens.
250        add_action( 'woocommerce_new_payment_token', $callable, 10, 1 );
251        add_action( 'woocommerce_payment_token_deleted', $callable, 10, 2 );
252        add_action( 'woocommerce_payment_token_updated', $callable, 10, 1 );
253        $this->init_listeners_for_meta_type( 'payment_token', $callable );
254
255        // Product downloads.
256        add_action( 'woocommerce_downloadable_product_download_log_insert', $callable, 10, 1 );
257        add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 );
258
259        // Tax rates.
260        // These are ignored on WP.com: tax items are derived from order data via wc_order_tax_lookup, which isn’t present there.
261        add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 );
262        add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 );
263        add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 );
264
265        // Webhooks.
266        add_action( 'woocommerce_new_webhook', $callable, 10, 1 );
267        add_action( 'woocommerce_webhook_deleted', $callable, 10, 2 );
268        add_action( 'woocommerce_webhook_updated', $callable, 10, 1 );
269
270        // Customers.
271        add_action( 'added_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
272        add_action( 'updated_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
273        add_action( 'deleted_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
274        add_action( 'delete_user', array( $this, 'action_delete_user' ), 10, 1 );
275        add_action( 'wpmu_delete_user', array( $this, 'action_delete_user' ), 10, 1 );
276        add_action( 'shutdown', array( $this, 'action_customer_meta_updates' ) );
277        add_action( 'jetpack_updated_woo_customer_meta', $callable, 10, 2 );
278    }
279
280    /**
281     * Initialize WooCommerce action listeners for full sync.
282     *
283     * @access public
284     *
285     * @param callable $callable Action handler callable.
286     */
287    public function init_full_sync_listeners( $callable ) {
288        add_action( 'jetpack_full_sync_woocommerce_order_items', $callable ); // Also sends post meta.
289    }
290
291    /**
292     * Retrieve the actions that will be sent for this module during a full sync.
293     *
294     * @access public
295     *
296     * @return array Full sync actions of this module.
297     */
298    public function get_full_sync_actions() {
299        return array( 'jetpack_full_sync_woocommerce_order_items' );
300    }
301
302    /**
303     * Initialize the module in the sender.
304     *
305     * @access public
306     */
307    public function init_before_send() {
308        // Full sync.
309        add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'build_full_sync_action_array' ) );
310    }
311
312    /**
313     * Expand the order items properly.
314     *
315     * @access public
316     *
317     * @param array $args The hook arguments.
318     * @return array $args The hook arguments.
319     */
320    public function filter_order_item( $args ) {
321        // Make sure we always have all the data - prior to WooCommerce 3.0 we only have the user supplied data in the second argument and not the full details.
322        $args[1] = $this->build_order_item( $args[0] );
323        return $args;
324    }
325
326    /**
327     * Append an order's total to the synced woocommerce_new_order args when it is paid.
328     *
329     * A brand new order can be created already in a paid status, in which case no status transition
330     * fires and only woocommerce_new_order observes the payment. When the order is paid we append a
331     * trailing order-total payload (total, currency) that the Activity Log aggregates into
332     * revenue; otherwise only the order ID is synced (the action still syncs for other purposes).
333     *
334     * @since 4.44.0 Appends a trailing [ 'total', 'currency' ] payload when the new order is paid.
335     *
336     * @param array $args Hook args: [ order_id, WC_Order ]. The order object is WooCommerce's 2nd arg.
337     * @return array|false The args ( [ order_id ] ), with a trailing order-total payload appended when paid, or false when invalid.
338     */
339    public function add_order_total_to_new_order( $args ) {
340        if ( ! is_array( $args ) || count( $args ) < 1 || ! is_numeric( $args[0] ) || (int) $args[0] <= 0 ) {
341            return false;
342        }
343
344        $order_id = (int) $args[0];
345
346        // Only use the order object WooCommerce passes as the 2nd arg; avoid wc_get_order on this hot path.
347        $order = ( isset( $args[1] ) && $args[1] instanceof WC_Order ) ? $args[1] : null;
348
349        // Rebuild the scalar arg shape WPcom expects. This also drops the WC_Order object the listener now
350        // receives so it is never enqueued or serialized into the sync queue.
351        $args = array( $order_id );
352        if ( $order && $this->is_paid_order_status( $order->get_status() ) ) {
353            $args = $this->maybe_append_order_total( $args, $order );
354        }
355
356        return $args;
357    }
358
359    /**
360     * Append an order's total to the synced woocommerce_order_status_changed args on payment.
361     *
362     * We emit on the transition *into* a paid status from a non-paid one â€” the payment moment â€” and
363     * skip paid -> paid steps (e.g. processing -> completed) so a fulfillment doesn't re-emit. When
364     * emitted we append a trailing order-total payload (total, currency) the Activity Log
365     * reads; otherwise only [ order_id, status_from, status_to ] is synced (the action still syncs for
366     * other purposes).
367     *
368     * @since 4.44.0 Appends a trailing [ 'total', 'currency' ] payload on the paid transition.
369     *
370     * @param array $args Hook args: [ order_id, status_from, status_to, WC_Order ]. The order is the 4th arg.
371     * @return array|false The args ( [ order_id, status_from, status_to ] ), with a trailing payload on the paid transition, or false when invalid.
372     */
373    public function add_order_total_to_status_changed( $args ) {
374        if ( ! is_array( $args ) || count( $args ) < 3 || ! is_numeric( $args[0] ) || (int) $args[0] <= 0 ) {
375            return false;
376        }
377
378        $order_id = (int) $args[0];
379
380        $status_from = $args[1];
381        $status_to   = $args[2];
382        if ( ! is_string( $status_from ) || ! is_string( $status_to ) ) {
383            return false;
384        }
385
386        // Only use the order object WooCommerce passes as the 4th arg; avoid wc_get_order on this hot path.
387        $order = ( isset( $args[3] ) && $args[3] instanceof WC_Order ) ? $args[3] : null;
388
389        // Rebuild the scalar arg shape WPcom expects. This also drops the WC_Order object the listener now
390        // receives so it is never enqueued or serialized into the sync queue.
391        $args = array( $order_id, $status_from, $status_to );
392
393        if ( $this->is_paid_order_status( $status_to ) && ! $this->is_paid_order_status( $status_from ) ) {
394            $args = $this->maybe_append_order_total( $args, $order );
395        }
396
397        return $args;
398    }
399
400    /**
401     * Append the order-total payload to the given args when this is the order's paid moment.
402     *
403     * @param array         $args  The scalar args built so far for the action.
404     * @param WC_Order|null $order Order object, or null when WooCommerce did not pass one.
405     * @return array The args, with a trailing order-total payload appended when emitted.
406     */
407    private function maybe_append_order_total( $args, $order ) {
408        if ( $order && $this->claim_order_total_emission( $order ) ) {
409            $payload = $this->build_order_total_payload( $order );
410
411            if ( $payload !== null ) {
412                $args[] = $payload;
413            }
414        }
415
416        return $args;
417    }
418
419    /**
420     * Claim the once-per-request emission slot for an order's total.
421     *
422     * Test-and-set: returns true (and records the claim) the first time it's called for an order this
423     * request, false thereafter â€” so the woocommerce_new_order and woocommerce_order_status_changed
424     * hooks don't both emit a freshly created paid order. Callers must confirm the order is paid first.
425     *
426     * @param WC_Order $order Order object.
427     * @return bool True when the caller obtained the claim and should emit.
428     */
429    private function claim_order_total_emission( $order ) {
430        $key = $order->get_id();
431        if ( isset( $this->synced_order_total_keys[ $key ] ) ) {
432            return false;
433        }
434        $this->synced_order_total_keys[ $key ] = true;
435
436        return true;
437    }
438
439    /**
440     * Build the trailing order-total payload appended to a paid order's synced action args.
441     *
442     * Intentionally minimal and scalar-only so it is safe to store and index on WPcom (Activity Log,
443     * Elasticsearch, MCP integrations). We read with the 'edit' context to get the raw stored values and
444     * skip the woocommerce_order_get_total / _currency view filters (e.g. multi-currency display
445     * conversion), then still normalize the total to a numeric string and cast the currency rather than
446     * trust whatever WooCommerce returns.
447     *
448     * @param WC_Order $order Order object.
449     * @return null|array {
450     *     @type string   $total       Order total as a numeric string.
451     *     @type string   $currency    Order currency code (e.g. 'USD').
452     * }
453     */
454    private function build_order_total_payload( $order ) {
455        $total = $order->get_total( 'edit' );
456
457        if ( $total <= 0 ) {
458            return null;
459        }
460
461        return array(
462            'total'    => function_exists( 'wc_format_decimal' ) ? wc_format_decimal( $total ) : (string) $total,
463            'currency' => (string) $order->get_currency( 'edit' ),
464        );
465    }
466
467    /**
468     * Whether an order status is one WooCommerce considers paid (and whose total we therefore sync).
469     *
470     * Uses WooCommerce's canonical, filterable list (wc_get_is_paid_statuses(), default 'processing'
471     * and 'completed', un-prefixed) so stores that register custom paid statuses are covered.
472     *
473     * @param string $status Order status without the `wc-` prefix (e.g. 'processing').
474     * @return bool True when WooCommerce treats the status as paid.
475     */
476    private function is_paid_order_status( $status ) {
477        // Fail fast on empty/invalid input (e.g. a missing status arg) before the WooCommerce lookup.
478        if ( ! is_string( $status ) || '' === $status || ! function_exists( 'wc_get_is_paid_statuses' ) ) {
479            return false;
480        }
481
482        if ( null === $this->paid_order_statuses ) {
483            $this->paid_order_statuses = wc_get_is_paid_statuses();
484        }
485
486        return in_array( $status, $this->paid_order_statuses, true );
487    }
488
489    /**
490     * Validate the minimal customer meta update payload before enqueueing.
491     *
492     * @param array $args Hook arguments.
493     * @return array|false Minimal user object and changed prop names, or false when invalid.
494     */
495    public function filter_customer_updated_meta( $args ) {
496        if (
497            ! is_array( $args )
498            || ! isset( $args[0] )
499            || ! isset( $args[1] )
500            || ! is_object( $args[0] )
501            || ! isset( $args[0]->data )
502            || ! is_object( $args[0]->data )
503            || ! isset( $args[0]->data->ID )
504            || ! is_numeric( $args[0]->data->ID )
505            || ! is_array( $args[1] )
506        ) {
507            return false;
508        }
509
510        $customer_id = (int) $args[0]->data->ID;
511        if ( $customer_id <= 0 ) {
512            return false;
513        }
514
515        $updated_props = $this->get_customer_detail_props( $args[1] );
516        if ( empty( $updated_props ) ) {
517            return false;
518        }
519
520        return array( $this->build_minimal_customer_user_object( $customer_id ), $updated_props );
521    }
522
523    /**
524     * Track updated WooCommerce customer meta props for syncing.
525     *
526     * @param int|array $meta_id  ID of the meta object, or IDs for deleted meta.
527     * @param int       $user_id  User ID.
528     * @param string    $meta_key Meta key.
529     * @param mixed     $value    Meta value.
530     */
531    public function maybe_sync_customer_meta_update( $meta_id, $user_id, $meta_key, $value ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
532        $customer_id = (int) $user_id;
533        if ( $customer_id <= 0 ) {
534            return;
535        }
536
537        if ( 'deleted_user_meta' === current_action() && isset( $this->deleted_user_ids[ $customer_id ] ) ) {
538            return;
539        }
540
541        if ( ! is_string( $meta_key ) && ! is_numeric( $meta_key ) ) {
542            return;
543        }
544
545        $meta_key = sanitize_key( (string) $meta_key );
546        if ( ! isset( self::$customer_detail_meta_key_to_prop[ $meta_key ] ) ) {
547            return;
548        }
549
550        $updated_prop = self::$customer_detail_meta_key_to_prop[ $meta_key ];
551
552        if ( ! isset( $this->customer_meta_updates[ $customer_id ] ) ) {
553            $this->customer_meta_updates[ $customer_id ] = array();
554        }
555
556        $this->customer_meta_updates[ $customer_id ][ $updated_prop ] = true;
557    }
558
559    /**
560     * Mark a deleted user so customer meta cleanup does not sync as profile changes.
561     *
562     * @param int $user_id User ID.
563     */
564    public function action_delete_user( $user_id ) {
565        $customer_id = (int) $user_id;
566        if ( $customer_id <= 0 ) {
567            return;
568        }
569
570        $this->deleted_user_ids[ $customer_id ] = true;
571        unset( $this->customer_meta_updates[ $customer_id ] );
572    }
573
574    /**
575     * Send batched WooCommerce customer meta updates.
576     */
577    public function action_customer_meta_updates() {
578        if ( empty( $this->customer_meta_updates ) ) {
579            return;
580        }
581
582        $customer_meta_updates       = $this->customer_meta_updates;
583        $this->customer_meta_updates = array();
584
585        foreach ( $customer_meta_updates as $customer_id => $updated_props ) {
586            if ( isset( $this->deleted_user_ids[ (int) $customer_id ] ) ) {
587                continue;
588            }
589
590            /**
591             * Fires when WooCommerce customer details stored in user meta are updated.
592             *
593             * @param object $customer      Minimal WP_User-shaped customer object.
594             * @param array  $updated_props Updated customer detail prop names.
595             */
596            do_action(
597                'jetpack_updated_woo_customer_meta',
598                $this->build_minimal_customer_user_object( (int) $customer_id ),
599                array_keys( $updated_props )
600            );
601        }
602    }
603
604    /**
605     * Retrieve whitelisted WooCommerce customer detail props.
606     *
607     * @param array $props Customer detail meta keys or prop names.
608     * @return array Customer detail prop names.
609     */
610    private function get_customer_detail_props( $props ) {
611        $updated_props = array();
612        foreach ( $props as $prop ) {
613            if ( ! is_string( $prop ) && ! is_numeric( $prop ) ) {
614                continue;
615            }
616
617            $prop = sanitize_key( (string) $prop );
618            if ( isset( self::$customer_detail_meta_key_to_prop[ $prop ] ) ) {
619                $updated_props[] = self::$customer_detail_meta_key_to_prop[ $prop ];
620                continue;
621            }
622
623            if ( in_array( $prop, self::$customer_detail_meta_key_to_prop, true ) ) {
624                $updated_props[] = $prop;
625            }
626        }
627
628        return array_values( array_unique( $updated_props ) );
629    }
630
631    /**
632     * Build a minimal WP_User-shaped object for Activity Log.
633     *
634     * @param int $customer_id Customer user ID.
635     * @return object Minimal user object.
636     */
637    private function build_minimal_customer_user_object( $customer_id ) {
638        $user_data = (object) array(
639            'ID'           => $customer_id,
640            'display_name' => '',
641            'user_login'   => '',
642            'user_email'   => '',
643        );
644
645        $user = get_userdata( $customer_id );
646        if ( $user ) {
647            $user_data->display_name = (string) $user->display_name;
648            $user_data->user_login   = (string) $user->user_login;
649            $user_data->user_email   = (string) $user->user_email;
650        }
651
652        return (object) array(
653            'ID'   => $customer_id,
654            'data' => $user_data,
655        );
656    }
657
658    /**
659     * Handler for filtering out non-whitelisted order item meta.
660     *
661     * @since 4.22.3
662     *
663     * @param array $args Hook arguments.
664     * @return array|false False if not whitelisted, the original hook args otherwise.
665     */
666    public function filter_meta( $args ) {
667        if (
668            ! empty( $args[2] ) && $this->is_whitelisted_order_item_meta( $args[2] )
669        ) {
670            return $args;
671        }
672
673        return false;
674    }
675
676    /**
677     * Whether an order item meta key is whitelisted for sync.
678     *
679     * @access public
680     *
681     * @since 4.22.3
682     *
683     * @param string $meta_key Order item meta key.
684     * @return bool True if whitelisted.
685     */
686    public function is_whitelisted_order_item_meta( $meta_key ) {
687        return is_string( $meta_key ) && in_array( $meta_key, self::$order_item_meta_whitelist, true );
688    }
689
690    /**
691     * Retrieve the order item ids to be removed and send them as one action
692     *
693     * @param WC_Order $order The order argument.
694     * @param string   $type Order item type.
695     */
696    public function action_woocommerce_remove_order_items( WC_Order $order, $type ) {
697        if ( $type ) {
698            $order_items = $order->get_items( $type );
699        } else {
700            $order_items = $order->get_items();
701        }
702        $order_item_ids = array_keys( $order_items );
703
704        if ( $order_item_ids ) {
705            do_action( 'woocommerce_remove_order_item_ids', $order_item_ids );
706        }
707    }
708
709    /**
710     * Expand order item IDs to order items and their meta.
711     *
712     * @access public
713     *
714     * @todo Refactor table name to use a $wpdb->prepare placeholder.
715     *
716     * @param array $args The hook arguments.
717     * @return array $args Expanded order items with meta.
718     * @deprecated since 4.7.0
719     */
720    public function expand_order_item_ids( $args ) {
721        _deprecated_function( __METHOD__, '4.7.0' );
722        $order_item_ids = $args[0];
723
724        global $wpdb;
725
726        $order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) );
727
728        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
729        $order_items = $wpdb->get_results(
730            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
731            "SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )"
732        );
733
734        return array(
735            $order_items,
736            $this->get_metadata( $order_item_ids, 'order_item', static::$order_item_meta_whitelist ),
737        );
738    }
739    /**
740     * Extract the full order item from the database by its ID.
741     *
742     * @access public
743     *
744     * @param int $order_item_id Order item ID.
745     * @return object Order item.
746     */
747    public function build_order_item( $order_item_id ) {
748        global $wpdb;
749        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct database access is intentional; caching is not required for this query.
750        return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE order_item_id = %d', $this->order_item_table_name, $order_item_id ) );
751    }
752
753    /**
754     * Enqueue the WooCommerce actions for full sync.
755     *
756     * @access public
757     *
758     * @param array   $config               Full sync configuration for this sync module.
759     * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
760     * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
761     * @return array Number of actions enqueued, and next module state.
762     */
763    public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
764        return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_woocommerce_order_items', $this->order_item_table_name, 'order_item_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
765    }
766
767    /**
768     * Retrieve an estimated number of actions that will be enqueued.
769     *
770     * @access public
771     *
772     * @todo Refactor the SQL query to use $wpdb->prepare().
773     *
774     * @param array $config Full sync configuration for this sync module.
775     * @return int Number of items yet to be enqueued.
776     */
777    public function estimate_full_sync_actions( $config ) {
778        global $wpdb;
779
780        $query = "SELECT count(*) FROM $this->order_item_table_name WHERE " . $this->get_where_sql( $config );
781        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
782        $count = (int) $wpdb->get_var( $query );
783
784        return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
785    }
786
787    /**
788     * Retrieve the WHERE SQL clause based on the module config.
789     *
790     * @access private
791     *
792     * @param array $config Full sync configuration for this sync module.
793     * @return string WHERE SQL clause.
794     */
795    public function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
796        return '1=1';
797    }
798
799    /**
800     * Add WooCommerce options to the options whitelist.
801     *
802     * @param array $list Existing options whitelist.
803     * @return array Updated options whitelist.
804     */
805    public function add_woocommerce_options_whitelist( $list ) {
806        return array_merge( $list, self::$wc_options_whitelist );
807    }
808
809    /**
810     * Add WooCommerce constants to the constants whitelist.
811     *
812     * @param array $list Existing constants whitelist.
813     * @return array Updated constants whitelist.
814     */
815    public function add_woocommerce_constants_whitelist( $list ) {
816        return array_merge( $list, self::$wc_constants_whitelist );
817    }
818
819    /**
820     * Add WooCommerce post meta to the post meta whitelist.
821     *
822     * @param array $list Existing post meta whitelist.
823     * @return array Updated post meta whitelist.
824     */
825    public function add_woocommerce_post_meta_whitelist( $list ) {
826        return array_merge( $list, self::$wc_post_meta_whitelist );
827    }
828
829    /**
830     * Add WooCommerce comment meta to the comment meta whitelist.
831     *
832     * @param array $list Existing comment meta whitelist.
833     * @return array Updated comment meta whitelist.
834     */
835    public function add_woocommerce_comment_meta_whitelist( $list ) {
836        return array_merge( $list, self::$wc_comment_meta_whitelist );
837    }
838
839    /**
840     * Adds 'revew' to the list of comment types so Sync will listen for status changes on 'reviews'.
841     *
842     * @access public
843     *
844     * @param array $comment_types The list of comment types prior to this filter.
845     * return array                The list of comment types with 'review' added.
846     */
847    public function add_review_comment_types( $comment_types ) {
848        if ( is_array( $comment_types ) ) {
849            $comment_types[] = 'review';
850        }
851        return $comment_types;
852    }
853
854    /**
855     * Stop comments from the Action Scheduler from being synced.
856     * https://github.com/woocommerce/woocommerce/tree/e7762627c37ec1f7590e6cac4218ba0c6a20024d/includes/libraries/action-scheduler
857     *
858     * @since 1.6.3
859     * @since-jetpack 7.7.0
860     *
861     * @param boolean $can_sync Should we prevent comment data from bing synced to WordPress.com.
862     * @param mixed   $comment  WP_COMMENT object.
863     *
864     * @return bool
865     */
866    public function filter_action_scheduler_comments( $can_sync, $comment ) {
867        if ( isset( $comment->comment_agent ) && 'ActionScheduler' === $comment->comment_agent ) {
868            return true;
869        }
870        return $can_sync;
871    }
872
873    /**
874     * Whitelist for options we are interested to sync.
875     *
876     * @access private
877     * @static
878     *
879     * @var array
880     */
881    private static $wc_options_whitelist = array(
882        'woocommerce_currency',
883        'woocommerce_db_version',
884        'woocommerce_weight_unit',
885        'woocommerce_version',
886        'woocommerce_unforce_ssl_checkout',
887        'woocommerce_tax_total_display',
888        'woocommerce_tax_round_at_subtotal',
889        'woocommerce_tax_display_shop',
890        'woocommerce_tax_display_cart',
891        'woocommerce_prices_include_tax',
892        'woocommerce_price_thousand_sep',
893        'woocommerce_price_num_decimals',
894        'woocommerce_price_decimal_sep',
895        'woocommerce_notify_low_stock',
896        'woocommerce_notify_low_stock_amount',
897        'woocommerce_notify_no_stock',
898        'woocommerce_notify_no_stock_amount',
899        'woocommerce_manage_stock',
900        'woocommerce_force_ssl_checkout',
901        'woocommerce_hide_out_of_stock_items',
902        'woocommerce_file_download_method',
903        'woocommerce_enable_signup_and_login_from_checkout',
904        'woocommerce_enable_shipping_calc',
905        'woocommerce_enable_review_rating',
906        'woocommerce_enable_guest_checkout',
907        'woocommerce_enable_coupons',
908        'woocommerce_enable_checkout_login_reminder',
909        'woocommerce_enable_ajax_add_to_cart',
910        'woocommerce_dimension_unit',
911        'woocommerce_default_country',
912        'woocommerce_default_customer_address',
913        'woocommerce_currency_pos',
914        'woocommerce_api_enabled',
915        'woocommerce_allow_tracking',
916        'woocommerce_task_list_hidden',
917        'woocommerce_cod_settings',
918        'woocommerce_store_address',
919        'woocommerce_store_address_2',
920        'woocommerce_store_city',
921        'woocommerce_store_postcode',
922        'woocommerce_admin_install_timestamp',
923        'woocommerce_enable_signup_from_checkout_for_subscriptions', // This and the below options relate to the WooCommerce Accounts and Privacy settings page. Required for the Activity Log.
924        'woocommerce_enable_myaccount_registration',
925        'woocommerce_registration_generate_password',
926        'woocommerce_erasure_request_removes_order_data',
927        'woocommerce_erasure_request_removes_subscription_data',
928        'woocommerce_erasure_request_removes_download_data',
929        'woocommerce_allow_bulk_remove_personal_data',
930        'woocommerce_registration_privacy_policy_text',
931        'woocommerce_checkout_privacy_policy_text',
932        'woocommerce_delete_inactive_accounts',
933        'woocommerce_trash_pending_orders',
934        'woocommerce_trash_failed_orders',
935        'woocommerce_trash_cancelled_orders',
936        'woocommerce_anonymize_refunded_orders',
937        'woocommerce_anonymize_completed_orders',
938        'woocommerce_anonymize_ended_subscriptions',
939        'woocommerce_enable_delayed_account_creation',
940        'woocommerce_gateway_stripe_retention',
941        'wc_downloads_approved_directories_mode', // This and the below options relate to the WooCommerce Products settings page. Required for the Activity Log.
942        'woocommerce_attribute_lookup_direct_updates',
943        'woocommerce_attribute_lookup_enabled',
944        'woocommerce_attribute_lookup_optimized_updates',
945        'woocommerce_cart_redirect_after_add',
946        'woocommerce_downloads_add_hash_to_filename',
947        'woocommerce_downloads_count_partial',
948        'woocommerce_downloads_deliver_inline',
949        'woocommerce_downloads_grant_access_after_payment',
950        'woocommerce_downloads_redirect_fallback_allowed',
951        'woocommerce_downloads_require_login',
952        'woocommerce_enable_reviews',
953        'woocommerce_hold_stock_minutes',
954        'woocommerce_review_rating_required',
955        'woocommerce_review_rating_verification_label',
956        'woocommerce_review_rating_verification_required',
957        'woocommerce_shop_page_id',
958        'woocommerce_stock_email_recipient',
959        'woocommerce_stock_format',
960        'woocommerce_allowed_countries',  // This and the below options relate to the WooCommerce General settings page. Required for the Activity Log.
961        'woocommerce_specific_allowed_countries',
962        'woocommerce_ship_to_countries',
963        'woocommerce_specific_ship_to_countries',
964        'woocommerce_all_except_countries',
965        'woocommerce_calc_taxes',
966        'woocommerce_calc_discounts_sequentially',
967        'woocommerce_analytics_enabled', // This and the below options relate to the WooCommerce Advanced settings page. Required for the Activity Log.
968        'woocommerce_cart_page_id',
969        'woocommerce_checkout_order_received_endpoint',
970        'woocommerce_checkout_page_id',
971        'woocommerce_checkout_pay_endpoint',
972        'woocommerce_custom_orders_table_data_sync_enabled',
973        'woocommerce_custom_orders_table_enabled',
974        'woocommerce_feature_block_email_editor_enabled',
975        'woocommerce_feature_blueprint_enabled',
976        'woocommerce_feature_cost_of_goods_sold_enabled',
977        'woocommerce_feature_customer_review_request_enabled',
978        'woocommerce_feature_deferred_transactional_emails_enabled',
979        'woocommerce_feature_destroy-empty-sessions_enabled',
980        'woocommerce_feature_email_improvements_enabled',
981        'woocommerce_feature_mcp_integration_enabled',
982        'woocommerce_feature_order_attribution_enabled',
983        'woocommerce_feature_point_of_sale_enabled',
984        'woocommerce_feature_product_instance_caching_enabled',
985        'woocommerce_feature_rate_limit_checkout_enabled',
986        'woocommerce_feature_remote_logging_enabled',
987        'woocommerce_feature_rest_api_caching_enabled',
988        'woocommerce_feature_site_visibility_badge_enabled',
989        'woocommerce_hpos_datastore_caching_enabled',
990        'woocommerce_hpos_fts_index_enabled',
991        'woocommerce_logout_endpoint',
992        'woocommerce_myaccount_add_payment_method_endpoint',
993        'woocommerce_myaccount_delete_payment_method_endpoint',
994        'woocommerce_myaccount_downloads_endpoint',
995        'woocommerce_myaccount_edit_account_endpoint',
996        'woocommerce_myaccount_edit_address_endpoint',
997        'woocommerce_myaccount_lost_password_endpoint',
998        'woocommerce_myaccount_orders_endpoint',
999        'woocommerce_myaccount_page_id',
1000        'woocommerce_myaccount_payment_methods_endpoint',
1001        'woocommerce_myaccount_set_default_payment_method_endpoint',
1002        'woocommerce_myaccount_subscription_payment_method_endpoint',
1003        'woocommerce_myaccount_subscriptions_endpoint',
1004        'woocommerce_myaccount_view_order_endpoint',
1005        'woocommerce_myaccount_view_subscription_endpoint',
1006        'woocommerce_show_marketplace_suggestions',
1007        'woocommerce_terms_page_id',
1008        'woocommerce_pickup_location_settings',  // This and the below options relate to the WooCommerce Shipping settings page. Required for the Activity Log.
1009        'pickup_location_pickup_locations',
1010        'woocommerce_ship_to_destination',
1011        'woocommerce_shipping_cost_requires_address',
1012        'woocommerce_shipping_debug_mode',
1013        'woocommerce_shipping_hide_rates_when_free',
1014        'woocommerce-ppcp-data-payment', // This and the below options relate to the Pay with PayPal payments settings page. Required for the Activity Log.
1015        'woocommerce-ppcp-data-settings',
1016        'woocommerce_ppcp-applepay_settings',
1017        'woocommerce_ppcp-axo-gateway_settings',
1018        'woocommerce_ppcp-bancontact_settings',
1019        'woocommerce_ppcp-blik_settings',
1020        'woocommerce_ppcp-card-button-gateway_settings',
1021        'woocommerce_ppcp-credit-card-gateway_settings',
1022        'woocommerce_ppcp-eps_settings',
1023        'woocommerce-ppcp-data-common',
1024        'woocommerce-ppcp-data-onboarding',
1025        'woocommerce_ppcp-googlepay_settings',
1026        'woocommerce_ppcp-ideal_settings',
1027        'woocommerce_ppcp-multibanco_settings',
1028        'woocommerce_ppcp-mybank_settings',
1029        'woocommerce_ppcp-oxxo-gateway_settings',
1030        'woocommerce_ppcp-p24_settings',
1031        'woocommerce_ppcp-pay-upon-invoice-gateway_settings',
1032        'woocommerce_ppcp-pwc_settings',
1033        'woocommerce_ppcp-trustly_settings',
1034        '_wcpay_feature_customer_multi_currency', // This and the below options relate to WooPayments.
1035        'current_protection_level',
1036        'woocommerce_woocommerce_payments_apple_pay_settings',
1037        'woocommerce_woocommerce_payments_google_pay_settings',
1038        'woocommerce_woocommerce_payments_settings',
1039        'wc_stripe_agentic_commerce_webhook_secret',  // This and the below options relate to additional payment types.
1040        'wc_square_settings',
1041        'woocommerce_amazon_payments_advanced_settings',
1042        'woocommerce_gift_cards_pay_settings',
1043        'woocommerce_square_cash_app_pay_settings',
1044        'woocommerce_square_credit_card_settings',
1045        'woocommerce_stripe_settings',
1046        'woocommerce_bacs_accounts', // This and the below options relate to offline payments.
1047        'woocommerce_bacs_settings',
1048        'woocommerce_cheque_settings',
1049        'woocommerce_ppcp-recaptcha_settings', // This and the below options relate to the WooCommerce Integrations settings page. Required for the Activity Log.
1050        'woocommerce_maxmind_geolocation_settings',
1051        'woocommerce_store_pages_only', // This and the below options relate to the WooCommerce Site Visibility settings page. Required for the Activity Log.
1052        'woocommerce_private_link',
1053        'woocommerce_coming_soon',
1054        'wcpay_multi_currency_enabled_currencies',  // This and the below option relate to the WooCommerce Multi-Currency settings page. Required for the Activity Log.
1055        'wcpay_multi_currency_enable_auto_currency',
1056        'woocommerce_pos_store_name', // This and the below options relate to the WooCommerce Point of Sale settings page. Required for the Activity Log.
1057        'woocommerce_pos_store_address',
1058        'woocommerce_pos_store_phone',
1059        'woocommerce_pos_store_email',
1060        'woocommerce_pos_refund_returns_policy',
1061        'wcs_notification_settings_update_time', // This and the below options relate to the WooCommerce Subscriptions settings page. Required for the Activity Log.
1062        'wcsatt_add_cart_to_subscription',
1063        'wcsatt_add_product_to_subscription',
1064        'woocommerce_subscriptions_accept_manual_renewals',
1065        'woocommerce_subscriptions_allow_switching',
1066        'woocommerce_subscriptions_allow_switching_product_plans',
1067        'woocommerce_subscriptions_apportion_length',
1068        'woocommerce_subscriptions_apportion_recurring_price',
1069        'woocommerce_subscriptions_apportion_sign_up_fee',
1070        'woocommerce_subscriptions_cancelled_role',
1071        'woocommerce_subscriptions_customer_notifications_enabled',
1072        'woocommerce_subscriptions_customer_notifications_offset',
1073        'woocommerce_subscriptions_downloads_add_line_items',
1074        'woocommerce_subscriptions_drip_downloadable_content_on_renewal',
1075        'woocommerce_subscriptions_enable_auto_renewal_toggle',
1076        'woocommerce_subscriptions_enable_downloadable_file_linking',
1077        'woocommerce_subscriptions_enable_early_renewal',
1078        'woocommerce_subscriptions_enable_retry',
1079        'woocommerce_subscriptions_enable_simple_subscription',
1080        'woocommerce_subscriptions_enable_variable_subscription',
1081        'woocommerce_subscriptions_first_billing_behavior',
1082        'woocommerce_subscriptions_gifting_default_option',
1083        'woocommerce_subscriptions_gifting_downloadable_products',
1084        'woocommerce_subscriptions_gifting_enable_gifting',
1085        'woocommerce_subscriptions_max_customer_suspensions',
1086        'woocommerce_subscriptions_multiple_purchase',
1087        'woocommerce_subscriptions_prorate_physical',
1088        'woocommerce_subscriptions_subscriber_role',
1089        'woocommerce_subscriptions_turn_off_automatic_payments',
1090        'woocommerce_subscriptions_zero_initial_payment_requires_payment',
1091        'woocommerce_email_from_address', // This and the below options relate to the WooCommerce Emails settings page. Required for the Activity Log.
1092        'woocommerce_email_from_name',
1093        'woocommerce_email_reply_to_address',
1094        'woocommerce_email_reply_to_enabled',
1095        'woocommerce_email_reply_to_name',
1096    );
1097
1098    /**
1099     * Whitelist for constants we are interested to sync.
1100     *
1101     * @access private
1102     * @static
1103     *
1104     * @var array
1105     */
1106    private static $wc_constants_whitelist = array(
1107        // WooCommerce constants.
1108        'WC_PLUGIN_FILE',
1109        'WC_ABSPATH',
1110        'WC_PLUGIN_BASENAME',
1111        'WC_VERSION',
1112        'WOOCOMMERCE_VERSION',
1113        'WC_ROUNDING_PRECISION',
1114        'WC_DISCOUNT_ROUNDING_MODE',
1115        'WC_TAX_ROUNDING_MODE',
1116        'WC_DELIMITER',
1117        'WC_LOG_DIR',
1118        'WC_SESSION_CACHE_GROUP',
1119        'WC_TEMPLATE_DEBUG_MODE',
1120    );
1121
1122    /**
1123     * Whitelist for post meta we are interested to sync.
1124     *
1125     * @access private
1126     * @static
1127     *
1128     * @var array
1129     */
1130    public static $wc_post_meta_whitelist = array(
1131        // WooCommerce products.
1132        // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-product-data-store-cpt.php#L21 .
1133        '_visibility',
1134        '_sku',
1135        '_price',
1136        '_regular_price',
1137        '_sale_price',
1138        '_sale_price_dates_from',
1139        '_sale_price_dates_to',
1140        'total_sales',
1141        '_tax_status',
1142        '_tax_class',
1143        '_manage_stock',
1144        '_backorders',
1145        '_sold_individually',
1146        '_weight',
1147        '_length',
1148        '_width',
1149        '_height',
1150        '_upsell_ids',
1151        '_crosssell_ids',
1152        '_purchase_note',
1153        '_default_attributes',
1154        '_product_attributes',
1155        '_virtual',
1156        '_downloadable',
1157        '_download_limit',
1158        '_download_expiry',
1159        '_featured',
1160        '_downloadable_files',
1161        '_wc_rating_count',
1162        '_wc_average_rating',
1163        '_wc_review_count',
1164        '_variation_description',
1165        '_thumbnail_id',
1166        '_file_paths',
1167        '_product_image_gallery',
1168        '_product_version',
1169        '_wp_old_slug',
1170
1171        // Woocommerce orders.
1172        // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L27 .
1173        '_order_key',
1174        '_order_currency',
1175        // '_billing_first_name', do not sync these as they contain personal data
1176        // '_billing_last_name',
1177        // '_billing_company',
1178        // '_billing_address_1',
1179        // '_billing_address_2',
1180        '_billing_city',
1181        '_billing_state',
1182        '_billing_postcode',
1183        '_billing_country',
1184        // '_billing_email', do not sync these as they contain personal data.
1185        // '_billing_phone',
1186        // '_shipping_first_name',
1187        // '_shipping_last_name',
1188        // '_shipping_company',
1189        // '_shipping_address_1',
1190        // '_shipping_address_2',
1191        '_shipping_city',
1192        '_shipping_state',
1193        '_shipping_postcode',
1194        '_shipping_country',
1195        '_completed_date',
1196        '_paid_date',
1197        '_cart_discount',
1198        '_cart_discount_tax',
1199        '_order_shipping',
1200        '_order_shipping_tax',
1201        '_order_tax',
1202        '_order_total',
1203        '_payment_method',
1204        '_payment_method_title',
1205        // '_transaction_id', do not sync these as they contain personal data.
1206        // '_customer_ip_address',
1207        // '_customer_user_agent',
1208        '_created_via',
1209        '_order_version',
1210        '_prices_include_tax',
1211        '_date_completed',
1212        '_date_paid',
1213        '_payment_tokens',
1214        // '_billing_address_index', do not sync these as they contain personal data.
1215        // '_shipping_address_index',
1216        '_recorded_sales',
1217        '_recorded_coupon_usage_counts',
1218        // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539 .
1219        '_download_permissions_granted',
1220        // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L594 .
1221        '_order_stock_reduced',
1222        '_cart_hash',
1223
1224        // Woocommerce order refunds.
1225        // See https://github.com/woocommerce/woocommerce/blob/b8a2815ae546c836467008739e7ff5150cb08e93/includes/data-stores/class-wc-order-refund-data-store-cpt.php#L20 .
1226        '_order_currency',
1227        '_refund_amount',
1228        '_refunded_by',
1229        '_refund_reason',
1230        '_order_shipping',
1231        '_order_shipping_tax',
1232        '_order_tax',
1233        '_order_total',
1234        '_order_version',
1235        '_prices_include_tax',
1236        '_payment_tokens',
1237    );
1238
1239    /**
1240     * Whitelist for comment meta we are interested to sync.
1241     *
1242     * @access private
1243     * @static
1244     *
1245     * @var array
1246     */
1247    private static $wc_comment_meta_whitelist = array(
1248        'rating',
1249    );
1250
1251    /**
1252     * Return a list of objects by their type and IDs
1253     *
1254     * @param string $object_type Object type.
1255     * @param array  $ids IDs of objects to return.
1256     *
1257     * @access public
1258     *
1259     * @return array|object|WP_Error|null
1260     */
1261    public function get_objects_by_id( $object_type, $ids ) {
1262        switch ( $object_type ) {
1263            case 'order_item':
1264                return $this->get_order_item_by_ids( $ids );
1265        }
1266
1267        return new WP_Error( 'unsupported_object_type', 'Unsupported object type' );
1268    }
1269
1270    /**
1271     * Returns a list of order_item objects by their IDs.
1272     *
1273     * @param array  $ids List of order_item IDs to fetch.
1274     * @param string $order Either 'ASC' or 'DESC'.
1275     *
1276     * @access public
1277     *
1278     * @return array|object|null
1279     */
1280    public function get_order_item_by_ids( $ids, $order = '' ) {
1281        global $wpdb;
1282
1283        if ( ! is_array( $ids ) ) {
1284            return array();
1285        }
1286
1287        // Make sure the IDs are numeric and are non-zero.
1288        $ids = array_filter( array_map( 'intval', $ids ) );
1289
1290        if ( empty( $ids ) ) {
1291            return array();
1292        }
1293
1294        // Prepare the placeholders for the prepared query below.
1295        $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
1296
1297        $query = "SELECT * FROM {$this->order_item_table_name} WHERE order_item_id IN ( $placeholders )";
1298        if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
1299            $query .= " ORDER BY order_item_id $order";
1300        }
1301
1302        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1303        return $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
1304    }
1305
1306    /**
1307     * Build the full sync action object for WooCommerce order items.
1308     *
1309     * @access public
1310     *
1311     * @param array $args An array with the order items and the previous end.
1312     *
1313     * @return array An array with the order items, order item meta and the previous end.
1314     */
1315    public function build_full_sync_action_array( $args ) {
1316        list( $filtered_order_items, $previous_end ) = $args;
1317        return array(
1318            'order_items'     => $filtered_order_items['objects'],
1319            'order_item_meta' => $filtered_order_items['meta'],
1320            'previous_end'    => $previous_end,
1321        );
1322    }
1323
1324    /**
1325     * Given the Module Configuration and Status return the next chunk of items to send.
1326     * This function also expands the posts and metadata and filters them based on the maximum size constraints.
1327     *
1328     * @param array $config This module Full Sync configuration.
1329     * @param array $status This module Full Sync status.
1330     * @param int   $chunk_size Chunk size.
1331     *
1332     * @return array
1333     */
1334    public function get_next_chunk( $config, $status, $chunk_size ) {
1335
1336        $order_item_ids = parent::get_next_chunk( $config, $status, $chunk_size );
1337
1338        if ( empty( $order_item_ids ) ) {
1339            return array();
1340        }
1341        // Fetch the order items in DESC order for the next chunk logic to work.
1342        $order_items = $this->get_order_item_by_ids( $order_item_ids, 'DESC' );
1343
1344        // If no orders were fetched, make sure to return the expected structure so that status is updated correctly.
1345        if ( empty( $order_items ) ) {
1346            return array(
1347                'object_ids' => $order_item_ids,
1348                'objects'    => array(),
1349            );
1350        }
1351
1352        // Get the order IDs from the orders that were fetched.
1353        $fetched_order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
1354        $metadata               = $this->get_metadata( $fetched_order_item_ids, 'order_item', static::$order_item_meta_whitelist );
1355
1356        // Filter the orders and metadata based on the maximum size constraints.
1357        list( $filtered_order_item_ids, $filtered_order_items, $filtered_order_items_metadata ) = $this->filter_objects_and_metadata_by_size(
1358            'order_item',
1359            $order_items,
1360            $metadata,
1361            self::MAX_META_LENGTH,
1362            self::MAX_SIZE_FULL_SYNC
1363        );
1364
1365        return array(
1366            'object_ids' => $filtered_order_item_ids,
1367            'objects'    => $filtered_order_items,
1368            'meta'       => $filtered_order_items_metadata,
1369        );
1370    }
1371}