PluginProbe
WANotifier for Forms and Actions / 3.1.1
WANotifier for Forms and Actions v3.1.1
3.1.1 3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 1.0.2 All 67 releases
notifier / includes / classes / integrations / class-notifier-woocommerce.php

class-notifier-woocommerce.php in WANotifier for Forms and Actions 3.1.1, at includes/classes/integrations/class-notifier-woocommerce.php

1,959 lines 71.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6 /**
7 * Woocommerce notifications
8 *
9 * @package Wa_Notifier
10 */
11 class Notifier_Woocommerce {
12
13 /**
14 * Init.
15 */
16 public static function init() {
17 add_filter( 'notifier_notification_triggers', array( __CLASS__, 'add_notification_triggers'), 10 );
18 add_filter( 'notifier_notification_merge_tags', array( __CLASS__, 'woocommerce_merge_tags') );
19 add_filter( 'notifier_notification_recipient_fields', array( __CLASS__, 'woocommerce_recipient_fields') );
20 add_action( 'woocommerce_review_order_before_submit', array( __CLASS__, 'add_checkout_optin_fields') );
21 add_action( 'woocommerce_checkout_update_order_meta', array( __CLASS__, 'notifier_save_checkout_field'));
22 add_action( 'woocommerce_admin_order_data_after_billing_address', array( __CLASS__, 'render_order_optin_field' ) );
23 add_action( 'woocommerce_process_shop_order_meta', array( __CLASS__, 'save_admin_order_meta' ), 10, 2 );
24
25 add_action( 'wp_ajax_notifier_save_cart_abandonment_data', array(__CLASS__, 'notifier_save_cart_abandonment_data'));
26 add_action( 'wp_ajax_nopriv_notifier_save_cart_abandonment_data', array(__CLASS__, 'notifier_save_cart_abandonment_data'));
27 add_action( 'woocommerce_cart_updated', array( __CLASS__, 'notifier_update_cart_data_on_update'), 999);
28 add_action( 'notifier_check_cart_abandonment', array( __CLASS__ , 'notifier_process_cart_abandonment' ), 10, 1);
29
30 add_action( 'woocommerce_new_order', array( __CLASS__ , 'notifier_save_session_on_new_order' ), 10, 1);
31
32 add_action( 'wp', array(__CLASS__, 'retrive_cart_data_from_url'));
33
34 // v3: Action Scheduler handler for async schema push when new meta keys are discovered.
35 add_action( 'notifier_push_updated_schema', array( __CLASS__, 'handle_push_updated_schema' ), 10, 1 );
36
37 // v3: contact sync hooks — priority 100 so WooCommerce and other plugins have
38 // already written all billing/user meta before we read it.
39 add_action( 'user_register', array( __CLASS__, 'on_user_register' ), 100 );
40 add_action( 'profile_update', array( __CLASS__, 'on_user_update' ), 100, 2 );
41 add_action( 'woocommerce_checkout_update_order_meta', array( __CLASS__, 'on_checkout_contact_sync' ), 100 );
42
43 // v3: Action Scheduler handler for batch contact sync.
44 add_action( 'notifier_wc_batch_contact_sync', array( __CLASS__, 'process_batch_contact_sync_page' ), 10, 3 );
45 }
46
47 /**
48 * Merge tag group types relevant to a WooCommerce order context.
49 * Used by both get_woo_notification_triggers() and build_wc_order_payload().
50 */
51 public static function get_order_merge_tag_types() {
52 return array( 'WordPress', 'WooCommerce', 'WooCommerce Order', 'WooCommerce Customer', 'WooCommerce Order Custom Meta', 'WooCommerce Customer User Meta' );
53 }
54
55 /**
56 * Recipient field group types relevant to a WooCommerce order context.
57 * Used by both get_woo_notification_triggers() and build_wc_order_payload().
58 */
59 public static function get_order_recipient_types() {
60 return array( 'WooCommerce', 'WooCommerce Order Custom Meta', 'WooCommerce Customer User Meta' );
61 }
62
63 /**
64 * Add Woocommerce notification triggers
65 */
66 public static function get_woo_notification_triggers() {
67 $merge_tag_types = self::get_order_merge_tag_types();
68 $recipient_types = self::get_order_recipient_types();
69 $triggers = array (
70 array(
71 'id' => 'woo_order_new',
72 'label' => 'New order is placed',
73 'description' => 'Trigger notification when a new order is placed.',
74 'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
75 'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
76 'action' => array(
77 'hook' => 'woocommerce_new_order',
78 'callback' => function ( $order_id ){
79 if (!self::maybe_send_notification($order_id)) {
80 return;
81 }
82
83 $args = array (
84 'object_type' => 'order',
85 'object_id' => $order_id
86 );
87 Notifier_Notification_Triggers::send_trigger_request('woo_order_new', $args);
88 }
89 )
90 ),
91 array(
92 'id' => 'woo_order_new_cod',
93 'label' => 'New order is placed with COD payment method',
94 'description' => 'Trigger notification when a new order is placed with Cash on Delivery payment method selected.',
95 'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
96 'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
97 'action' => array(
98 'hook' => 'woocommerce_new_order',
99 'callback' => function ( $order_id ){
100 if (!self::maybe_send_notification($order_id)) {
101 return;
102 }
103
104 $args = array (
105 'object_type' => 'order',
106 'object_id' => $order_id
107 );
108 $order = new WC_Order( $order_id );
109 $method = $order->get_payment_method();
110 if('cod' != $method){
111 return;
112 }
113 Notifier_Notification_Triggers::send_trigger_request('woo_order_new_cod', $args);
114 }
115 )
116 ),
117 array(
118 'id' => 'woocommerce_cart_abandonment',
119 'label' => 'Cart is abandoned',
120 'description' => 'Trigger notification when cart is abandoned as per your setup in the Settings > WooCommerce tab.',
121 'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags(array('WooCommerce', 'WooCommerce Cart Abandonment')),
122 'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields(array('WooCommerce Cart Abandonment')),
123 'action' => array(
124 'hook' => 'notifier_process_abandoned_cart',
125 'callback' => function ( $checkout_details ) {
126 $checkout_details = (array) $checkout_details;
127 Notifier_Notification_Triggers::send_trigger_request( 'woocommerce_cart_abandonment', $checkout_details );
128 }
129 )
130 ),
131 );
132
133 $statuses = wc_get_order_statuses();
134 foreach($statuses as $key => $status){
135 $status_slug = str_replace('wc-','', $key);
136 if(in_array($status_slug, array('checkout-draft'))){
137 continue;
138 }
139 $trigger_id = 'woo_order_' . $status_slug;
140 $triggers[] = array(
141 'id' => $trigger_id,
142 'label' => 'Order status changes to ' . $status,
143 'description' => 'Trigger notification when status of an order changes to ' . $status . '.',
144 'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
145 'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
146 'action' => array(
147 'hook' => 'woocommerce_order_status_' . $status_slug,
148 'callback' => function ( $order_id ) use ($trigger_id) {
149 if (!self::maybe_send_notification($order_id)) {
150 return;
151 }
152
153 $args = array (
154 'object_type' => 'order',
155 'object_id' => $order_id
156 );
157 Notifier_Notification_Triggers::send_trigger_request($trigger_id, $args);
158 }
159 )
160 );
161 }
162
163 return $triggers;
164 }
165
166 /**
167 * Add notification trigger Woocommerce
168 */
169 public static function add_notification_triggers ($triggers) {
170 $triggers['WooCommerce'] = self::get_woo_notification_triggers();
171 return $triggers;
172 }
173
174 /**
175 * Get Woocommerce merge tags
176 */
177 public static function woocommerce_merge_tags($merge_tags) {
178 $merge_tags['WooCommerce'] = array(
179 array(
180 'id' => 'woo_store_url',
181 'label' => 'Shop page url',
182 'preview_value' => 'https://example.com/shop/',
183 'return_type' => 'text',
184 'source' => 'static',
185 'value' => function ( $bulk_data ) {
186 return (string) get_permalink( wc_get_page_id( 'shop' ) );
187 }
188 ),
189 array(
190 'id' => 'woo_my_account_url',
191 'label' => 'My Account page url',
192 'preview_value' => 'https://example.com/my-account/',
193 'return_type' => 'text',
194 'source' => 'static',
195 'value' => function ( $bulk_data ) {
196 return (string) get_permalink( wc_get_page_id( 'myaccount' ) );
197 }
198 ),
199 );
200
201 // WooCommerce Order fields — source/path maps directly to get_base_data() keys.
202 // Fields with custom formatting use a 'value' closure receiving $bulk_data.
203 $woo_fields = array();
204 $woo_fields['WooCommerce Order'] = array(
205 'id' => array(
206 'preview' => '123',
207 'label' => 'Order ID',
208 'source' => 'base',
209 'path' => 'id',
210 ),
211 'subtotal_to_display' => array(
212 'preview' => '$50.00',
213 'label' => 'Order subtotal',
214 'source' => 'computed',
215 'value' => function ( $bulk_data ) {
216 return html_entity_decode( $bulk_data['order']->get_subtotal_to_display() );
217 },
218 ),
219 'coupon_codes' => array(
220 'preview' => 'OFF50',
221 'label' => 'Order coupon codes',
222 'source' => 'coupons',
223 'value' => function ( $bulk_data ) {
224 $codes = array();
225 foreach ( $bulk_data['coupons'] as $item ) {
226 $codes[] = $item->get_code();
227 }
228 return implode( ', ', $codes );
229 },
230 ),
231 'discount_to_display' => array(
232 'preview' => '$5.00',
233 'label' => 'Order discount amount',
234 'source' => 'computed',
235 'value' => function ( $bulk_data ) {
236 return html_entity_decode( $bulk_data['order']->get_discount_to_display() );
237 },
238 ),
239 'shipping_method' => array(
240 'preview' => 'Flat rate',
241 'label' => 'Order shipping method',
242 'source' => 'shipping',
243 'value' => function ( $bulk_data ) {
244 $methods = array();
245 foreach ( $bulk_data['shipping'] as $item ) {
246 $methods[] = $item->get_method_title();
247 }
248 return implode( ', ', $methods );
249 },
250 ),
251 'shipping_total' => array(
252 'preview' => '$2.50',
253 'label' => 'Order shipping amount',
254 'source' => 'base',
255 'path' => 'shipping_total',
256 'value' => function ( $bulk_data ) {
257 $currency = $bulk_data['base']['currency'];
258 return html_entity_decode( wc_price( $bulk_data['base']['shipping_total'], array( 'currency' => $currency ) ) );
259 },
260 ),
261 'total_tax' => array(
262 'preview' => '$5.00',
263 'label' => 'Order tax amount',
264 'source' => 'base',
265 'path' => 'total_tax',
266 'value' => function ( $bulk_data ) {
267 $currency = $bulk_data['base']['currency'];
268 return html_entity_decode( wc_price( $bulk_data['base']['total_tax'], array( 'currency' => $currency ) ) );
269 },
270 ),
271 'formatted_order_total' => array(
272 'preview' => '$50.00',
273 'label' => 'Order total amount',
274 'source' => 'computed',
275 'value' => function ( $bulk_data ) {
276 return html_entity_decode( $bulk_data['order']->get_formatted_order_total() );
277 },
278 ),
279 'payment_method_title' => array(
280 'preview' => 'Cash on delivery',
281 'label' => 'Order payment method',
282 'source' => 'base',
283 'path' => 'payment_method_title',
284 ),
285 'checkout_payment_url' => array(
286 'preview' => 'https://example.com/checkout/order-received/123/?key=wc_order_abcdef',
287 'label' => 'Order checkout payment URL',
288 'source' => 'computed',
289 'value' => function ( $bulk_data ) {
290 return $bulk_data['order']->get_checkout_payment_url();
291 },
292 ),
293 'checkout_order_received_url' => array(
294 'preview' => 'https://example.com/checkout/order-received/123/?key=wc_order_abcdef',
295 'label' => 'Order thank you page URL',
296 'source' => 'computed',
297 'value' => function ( $bulk_data ) {
298 return $bulk_data['order']->get_checkout_order_received_url();
299 },
300 ),
301 'view_order_url' => array(
302 'preview' => 'https://example.com/my-account/view-order/123/',
303 'label' => 'Order view URL',
304 'source' => 'computed',
305 'value' => function ( $bulk_data ) {
306 return $bulk_data['order']->get_view_order_url();
307 },
308 ),
309 'status' => array(
310 'preview' => 'Processing',
311 'label' => 'Order status',
312 'source' => 'base',
313 'path' => 'status',
314 ),
315 'first_product_image' => array(
316 'preview' => '',
317 'label' => 'Order first item featured image',
318 'return_type' => 'image',
319 'source' => 'items',
320 'value' => function ( $bulk_data ) {
321 foreach ( $bulk_data['items'] as $item ) {
322 $product = $item->get_product();
323 if ( $product ) {
324 $image_id = $product->get_image_id();
325 return $image_id ? wp_get_attachment_url( $image_id ) : wc_placeholder_img_src();
326 }
327 break;
328 }
329 return wc_placeholder_img_src();
330 },
331 ),
332 'products_list' => array(
333 'preview' => '',
334 'label' => 'Order products list',
335 'return_type' => 'text',
336 'source' => 'items',
337 'value' => function ( $bulk_data ) {
338 $parts = array();
339 $currency = $bulk_data['base']['currency'];
340 foreach ( $bulk_data['items'] as $item ) {
341 $parts[] = sanitize_textarea_field(
342 $item->get_name() . ' x ' . $item->get_quantity() . ' (' . wc_price( $item->get_total(), array( 'currency' => $currency ) ) . ')'
343 );
344 }
345 return implode( ', ', $parts );
346 },
347 ),
348 'order_first_item_name' => array(
349 'preview' => '',
350 'label' => 'Order first item name',
351 'return_type' => 'text',
352 'source' => 'items',
353 'value' => function ( $bulk_data ) {
354 foreach ( $bulk_data['items'] as $item ) {
355 return $item->get_name();
356 }
357 return '';
358 },
359 ),
360 'order_first_item_price' => array(
361 'preview' => '',
362 'label' => 'Order first item price',
363 'return_type' => 'text',
364 'source' => 'items',
365 'value' => function ( $bulk_data ) {
366 $currency = $bulk_data['base']['currency'];
367 foreach ( $bulk_data['items'] as $item ) {
368 $product = $item->get_product();
369 if ( $product ) {
370 return html_entity_decode( wc_price( $product->get_price(), array( 'currency' => $currency ) ) );
371 }
372 break;
373 }
374 return '';
375 },
376 ),
377 'order_first_item_total' => array(
378 'preview' => '',
379 'label' => 'Order first item total',
380 'return_type' => 'text',
381 'source' => 'items',
382 'value' => function ( $bulk_data ) {
383 $currency = $bulk_data['base']['currency'];
384 foreach ( $bulk_data['items'] as $item ) {
385 return html_entity_decode( wc_price( $item->get_total(), array( 'currency' => $currency ) ) );
386 }
387 return '';
388 },
389 ),
390 );
391
392 $woo_fields['WooCommerce Customer'] = array(
393 'billing_first_name' => array(
394 'preview' => 'John',
395 'source' => 'base',
396 'path' => 'billing.first_name',
397 ),
398 'billing_last_name' => array(
399 'preview' => 'Doe',
400 'source' => 'base',
401 'path' => 'billing.last_name',
402 ),
403 'billing_company' => array(
404 'preview' => 'ABC Inc',
405 'source' => 'base',
406 'path' => 'billing.company',
407 ),
408 'billing_address_1' => array(
409 'preview' => '123, XYZ Street',
410 'source' => 'base',
411 'path' => 'billing.address_1',
412 ),
413 'billing_address_2' => array(
414 'preview' => 'XYZ Avenue',
415 'source' => 'base',
416 'path' => 'billing.address_2',
417 ),
418 'billing_city' => array(
419 'preview' => 'New York',
420 'source' => 'base',
421 'path' => 'billing.city',
422 ),
423 'billing_postcode' => array(
424 'preview' => '12345',
425 'source' => 'base',
426 'path' => 'billing.postcode',
427 ),
428 'billing_state' => array(
429 'preview' => 'NY',
430 'source' => 'base',
431 'path' => 'billing.state',
432 'value' => function ( $bulk_data ) {
433 $country = $bulk_data['base']['billing']['country'];
434 $state = $bulk_data['base']['billing']['state'];
435 return WC()->countries->states[ $country ][ $state ] ?? $state;
436 },
437 ),
438 'billing_country' => array(
439 'preview' => 'US',
440 'source' => 'base',
441 'path' => 'billing.country',
442 'value' => function ( $bulk_data ) {
443 $code = $bulk_data['base']['billing']['country'];
444 return WC()->countries->countries[ $code ] ?? $code;
445 },
446 ),
447 'billing_email' => array(
448 'preview' => 'john@example.com',
449 'source' => 'base',
450 'path' => 'billing.email',
451 ),
452 'billing_phone' => array(
453 'preview' => '987 654 321',
454 'source' => 'base',
455 'path' => 'billing.phone',
456 'value' => function ( $bulk_data ) {
457 $phone = $bulk_data['base']['billing']['phone'] ?? '';
458 $country = $bulk_data['base']['billing']['country'] ?? '';
459 return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
460 },
461 ),
462 'formatted_billing_address' => array(
463 'preview' => '',
464 'label' => 'Complete billing address',
465 'source' => 'computed',
466 'value' => function ( $bulk_data ) {
467 return str_replace( '<br/>', ', ', $bulk_data['order']->get_formatted_billing_address() );
468 },
469 ),
470 'shipping_first_name' => array(
471 'preview' => 'John',
472 'source' => 'base',
473 'path' => 'shipping.first_name',
474 ),
475 'shipping_last_name' => array(
476 'preview' => 'Doe',
477 'source' => 'base',
478 'path' => 'shipping.last_name',
479 ),
480 'shipping_company' => array(
481 'preview' => 'ABC Inc',
482 'source' => 'base',
483 'path' => 'shipping.company',
484 ),
485 'shipping_address_1' => array(
486 'preview' => '123, XYZ Street',
487 'source' => 'base',
488 'path' => 'shipping.address_1',
489 ),
490 'shipping_address_2' => array(
491 'preview' => 'XYZ Avenue',
492 'source' => 'base',
493 'path' => 'shipping.address_2',
494 ),
495 'shipping_city' => array(
496 'preview' => 'New York',
497 'source' => 'base',
498 'path' => 'shipping.city',
499 ),
500 'shipping_postcode' => array(
501 'preview' => '12345',
502 'source' => 'base',
503 'path' => 'shipping.postcode',
504 ),
505 'shipping_state' => array(
506 'preview' => 'NY',
507 'source' => 'base',
508 'path' => 'shipping.state',
509 'value' => function ( $bulk_data ) {
510 $country = $bulk_data['base']['shipping']['country'];
511 $state = $bulk_data['base']['shipping']['state'];
512 return WC()->countries->states[ $country ][ $state ] ?? $state;
513 },
514 ),
515 'shipping_country' => array(
516 'preview' => 'US',
517 'source' => 'base',
518 'path' => 'shipping.country',
519 'value' => function ( $bulk_data ) {
520 $code = $bulk_data['base']['shipping']['country'];
521 return WC()->countries->countries[ $code ] ?? $code;
522 },
523 ),
524 'shipping_phone' => array(
525 'preview' => '987 654 321',
526 'source' => 'base',
527 'path' => 'shipping.phone',
528 'value' => function ( $bulk_data ) {
529 $phone = $bulk_data['base']['shipping']['phone'] ?? '';
530 $country = $bulk_data['base']['shipping']['country'] ?? '';
531 return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
532 },
533 ),
534 'formatted_shipping_address' => array(
535 'preview' => '',
536 'label' => 'Complete shipping address',
537 'source' => 'computed',
538 'value' => function ( $bulk_data ) {
539 return str_replace( '<br/>', ', ', $bulk_data['order']->get_formatted_shipping_address() );
540 },
541 ),
542 );
543
544 foreach ( $woo_fields as $woo_field_key => $woo_field ) {
545 foreach ( $woo_field as $field => $field_data ) {
546 $label = isset( $field_data['label'] ) ? $field_data['label'] : ucfirst( str_replace( '_', ' ', $field ) );
547 $merge_tags[ $woo_field_key ][] = array(
548 'id' => 'woo_order_' . $field,
549 'label' => $label,
550 'preview_value' => isset( $field_data['preview'] ) ? $field_data['preview'] : '',
551 'return_type' => isset( $field_data['return_type'] ) ? $field_data['return_type'] : 'text',
552 'source' => isset( $field_data['source'] ) ? $field_data['source'] : 'base',
553 'path' => isset( $field_data['path'] ) ? $field_data['path'] : null,
554 'value' => isset( $field_data['value'] ) ? $field_data['value'] : null,
555 );
556 }
557 }
558
559 // Order meta keys — read from persistent option (populated on connect/sync).
560 $order_meta_keys = Notifier_Notification_Merge_Tags::get_custom_meta_keys( 'shop_order' );
561 if ( ! empty( $order_meta_keys ) ) {
562 foreach ( $order_meta_keys as $order_meta_key ) {
563 $merge_tags['WooCommerce Order Custom Meta'][] = array(
564 'id' => 'woo_order_meta_' . $order_meta_key,
565 'label' => $order_meta_key,
566 'preview_value' => '123',
567 'return_type' => 'all',
568 'source' => 'meta',
569 'path' => $order_meta_key,
570 );
571 }
572 }
573
574 // WooCommerce customer user meta — read from persistent option.
575 $user_meta_keys = Notifier_Notification_Merge_Tags::get_user_meta_keys();
576 if ( ! empty( $user_meta_keys ) ) {
577 foreach ( $user_meta_keys as $user_meta_key ) {
578 $merge_tags['WooCommerce Customer User Meta'][] = array(
579 'id' => 'woo_customer_meta_' . $user_meta_key,
580 'label' => $user_meta_key,
581 'preview_value' => '123',
582 'return_type' => 'all',
583 'source' => 'user_meta',
584 'path' => $user_meta_key,
585 );
586 }
587 }
588
589 $merge_tags['WooCommerce Cart Abandonment'] = array(
590 array(
591 'id' => 'wca_cart_products_list',
592 'label' => 'Abandoned cart products list',
593 'preview_value' => '',
594 'return_type' => 'text',
595 'value' => function ($cart_details) {
596 return $cart_details ? self::get_comma_separated_products(maybe_unserialize($cart_details['cart_data'])) : '';
597 }
598 ),
599 array(
600 'id' => 'wca_cart_total',
601 'label' => 'Abandoned cart total',
602 'preview_value' => '',
603 'return_type' => 'text',
604 'value' => function ($cart_details) {
605 return $cart_details['cart_total'] ?? '';
606 }
607 ),
608 array(
609 'id' => 'wca_cart_datetime',
610 'label' => 'Abandoned cart datetime',
611 'preview_value' => '',
612 'return_type' => 'text',
613 'value' => function ($cart_details) {
614 return $cart_details ? $cart_details['created_time'] : '';
615 }
616 ),
617 array(
618 'id' => 'wca_checkout_recovery_url',
619 'label' => 'Abandoned cart checkout recovery URL',
620 'preview_value' => '',
621 'return_type' => 'text',
622 'value' => function ($cart_details) {
623 if ($cart_details && isset($cart_details['session_key'])) {
624 $session_key = sanitize_key($cart_details['session_key']);
625 $checkout_url = wc_get_checkout_url();
626 $recovery_url = add_query_arg(['wano_session' => $session_key], $checkout_url);
627 return esc_url($recovery_url);
628 }
629 return wc_get_checkout_url();
630 }
631 ),
632 array(
633 'id' => 'wca_cart_recovery_url',
634 'label' => 'Abandoned cart recovery URL',
635 'preview_value' => '',
636 'return_type' => 'text',
637 'value' => function ($cart_details) {
638 if ($cart_details && isset($cart_details['session_key'])) {
639 $session_key = sanitize_key($cart_details['session_key']);
640 $cart_url = wc_get_cart_url();
641 $recovery_url = add_query_arg(['wano_session' => $session_key], $cart_url);
642 return esc_url($recovery_url);
643 }
644 return wc_get_cart_url();
645 }
646 ),
647 array(
648 'id' => 'wca_cart_session_key',
649 'label' => 'Abandoned cart recovery key',
650 'preview_value' => '',
651 'return_type' => 'text',
652 'value' => function ($cart_details) {
653 if ($cart_details && isset($cart_details['session_key'])) {
654 $session_key = sanitize_key($cart_details['session_key']);
655 return $session_key;
656 }
657 return '';
658 }
659 ),
660 array(
661 'id' => 'wca_cart_customer_email',
662 'label' => 'Billing email',
663 'preview_value' => '',
664 'return_type' => 'text',
665 'value' => function ($cart_details) {
666 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_content']) : [];
667 return $customer_data['billing_email'] ?? '';
668 }
669 ),
670 array(
671 'id' => 'wca_cart_applied_coupon',
672 'label' => 'Coupon details',
673 'preview_value' => '',
674 'return_type' => 'text',
675 'value' => function ($cart_details) {
676 $coupon_data = $cart_details ? maybe_unserialize($cart_details['coupon_content'] ?? '') : [];
677 return $coupon_data ? implode(', ', $coupon_data) : '';
678 }
679 ),
680 );
681
682 $wca_other_fields = array(
683 'first_name' => array(
684 'preview' => 'John',
685 'label' => 'Billing first name',
686 'value' => function ($cart_details) {
687 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
688 return $customer_data['billing_first_name'] ?? '';
689 }
690 ),
691 'last_name' => array(
692 'preview' => 'Doe',
693 'label' => 'Billing last name',
694 'value' => function ($cart_details) {
695 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
696 return $customer_data['billing_last_name'] ?? '';
697 }
698 ),
699 'billing_company' => array(
700 'preview' => 'ABC Inc',
701 'label' => 'Billing company',
702 'value' => function ($cart_details) {
703 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
704 return $customer_data['billing_company'] ?? '';
705 }
706 ),
707 'billing_address_1' => array(
708 'preview' => '123, XYZ Street',
709 'label' => 'Billing address line 1',
710 'value' => function ($cart_details) {
711 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
712 return $customer_data['billing_address_1'] ?? '';
713 }
714 ),
715 'billing_address_2' => array(
716 'preview' => 'XYZ Avenue',
717 'label' => 'Billing address line 2',
718 'value' => function ($cart_details) {
719 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
720 return $customer_data['billing_address_2'] ?? '';
721 }
722 ),
723 'billing_city' => array(
724 'preview' => 'New York',
725 'label' => 'Billing city',
726 'value' => function ($cart_details) {
727 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
728 return $customer_data['billing_city'] ?? '';
729 }
730 ),
731 'billing_postcode' => array(
732 'preview' => '12345',
733 'label' => 'Billing postcode',
734 'value' => function ($cart_details) {
735 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
736 return $customer_data['billing_postcode'] ?? '';
737 }
738 ),
739 'billing_state' => array(
740 'preview' => 'NY',
741 'label' => 'Billing state',
742 'value' => function ($cart_details) {
743 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
744 $state_code = $customer_data['billing_state'] ?? '';
745 $country_code = $customer_data['billing_country'] ?? '';
746 return $state_code && $country_code
747 ? WC()->countries->states[$country_code][$state_code] ?? ''
748 : '';
749 }
750 ),
751 'billing_country' => array(
752 'preview' => 'US',
753 'label' => 'Billing country',
754 'value' => function ($cart_details) {
755 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
756 $country_code = $customer_data['billing_country'] ?? '';
757 return $country_code
758 ? WC()->countries->countries[$country_code] ?? ''
759 : '';
760 }
761 ),
762 'billing_phone' => array(
763 'preview' => '987 654 321',
764 'label' => 'Billing phone number',
765 'value' => function ($cart_details) {
766 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
767 return $customer_data['billing_phone'] ?? '';
768 }
769 ),
770 'billing_email' => array(
771 'preview' => 'john@example.com',
772 'label' => 'Billing email',
773 'value' => function ($cart_details) {
774 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
775 return $customer_data['billing_email'] ?? '';
776 }
777 ),
778 'shipping_first_name' => array(
779 'preview' => 'John',
780 'label' => 'Shipping first name',
781 'value' => function ($cart_details) {
782 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
783 return $customer_data['shipping_first_name'] ?? '';
784 }
785 ),
786 'shipping_last_name' => array(
787 'preview' => 'Doe',
788 'label' => 'Shipping last name',
789 'value' => function ($cart_details) {
790 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
791 return $customer_data['shipping_last_name'] ?? '';
792 }
793 ),
794 'shipping_company' => array(
795 'preview' => 'ABC Inc',
796 'label' => 'Shipping company',
797 'value' => function ($cart_details) {
798 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
799 return $customer_data['shipping_company'] ?? '';
800 }
801 ),
802 'shipping_address_1' => array(
803 'preview' => '123, XYZ Street',
804 'label' => 'Shipping address line 1',
805 'value' => function ($cart_details) {
806 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
807 return $customer_data['shipping_address_1'] ?? '';
808 }
809 ),
810 'shipping_address_2' => array(
811 'preview' => 'XYZ Avenue',
812 'label' => 'Shipping address line 2',
813 'value' => function ($cart_details) {
814 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
815 return $customer_data['shipping_address_2'] ?? '';
816 }
817 ),
818 'shipping_city' => array(
819 'preview' => 'New York',
820 'label' => 'Shipping city',
821 'value' => function ($cart_details) {
822 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
823 return $customer_data['shipping_city'] ?? '';
824 }
825 ),
826 'shipping_postcode' => array(
827 'preview' => '12345',
828 'label' => 'Shipping postcode',
829 'value' => function ($cart_details) {
830 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
831 return $customer_data['shipping_postcode'] ?? '';
832 }
833 ),
834 'shipping_state' => array(
835 'preview' => 'NY',
836 'label' => 'Shipping state',
837 'value' => function ($cart_details) {
838 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
839 $state_code = $customer_data['shipping_state'] ?? '';
840 $country_code = $customer_data['shipping_country'] ?? '';
841 return $state_code && $country_code
842 ? WC()->countries->states[$country_code][$state_code] ?? ''
843 : '';
844 }
845 ),
846 'shipping_country' => array(
847 'preview' => 'US',
848 'label' => 'Shipping country',
849 'value' => function ($cart_details) {
850 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
851 $country_code = $customer_data['shipping_country'] ?? '';
852 return $country_code
853 ? WC()->countries->countries[$country_code] ?? ''
854 : '';
855 }
856 ),
857 'shipping_phone' => array(
858 'preview' => '987 654 321',
859 'label' => 'Shipping phone number',
860 'value' => function ($cart_details) {
861 $customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
862 return $customer_data['shipping_phone'] ?? '';
863 }
864 ),
865 );
866
867 foreach ($wca_other_fields as $field => $field_data) {
868 $merge_tags['WooCommerce Cart Abandonment'][] = array(
869 'id' => 'wca_cart_customer_' . $field,
870 'label' => $field_data['label'],
871 'preview_value' => $field_data['preview'],
872 'return_type' => 'text',
873 'value' => $field_data['value']
874 );
875 }
876
877 return $merge_tags;
878 }
879
880 /*
881 * Add recipient fields for WooCommerce
882 */
883 public static function woocommerce_recipient_fields( $recipient_fields ) {
884 $recipient_fields['WooCommerce'] = array(
885 array(
886 'id' => 'woo_order_billing_phone',
887 'label' => 'Billing phone number',
888 'source' => 'base',
889 'path' => 'billing.phone',
890 'value' => function ( $bulk_data ) {
891 $phone = $bulk_data['base']['billing']['phone'];
892 $country = $bulk_data['base']['billing']['country'];
893 return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
894 },
895 ),
896 array(
897 'id' => 'woo_order_shipping_phone',
898 'label' => 'Shipping phone number',
899 'source' => 'base',
900 'path' => 'shipping.phone',
901 'value' => function ( $bulk_data ) {
902 $phone = $bulk_data['base']['shipping']['phone'];
903 $country = $bulk_data['base']['shipping']['country'];
904 return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
905 },
906 ),
907 );
908
909 // Order meta keys — read from persistent option.
910 $order_meta_keys = Notifier_Notification_Merge_Tags::get_custom_meta_keys( 'shop_order' );
911 if ( ! empty( $order_meta_keys ) ) {
912 foreach ( $order_meta_keys as $order_meta_key ) {
913 $recipient_fields['WooCommerce Order Custom Meta'][] = array(
914 'id' => 'woo_order_meta_' . $order_meta_key,
915 'label' => $order_meta_key,
916 'preview_value' => '123',
917 'return_type' => 'text',
918 'source' => 'meta',
919 'path' => $order_meta_key,
920 );
921 }
922 }
923
924 // WooCommerce customer user meta — read from persistent option.
925 $user_meta_keys = Notifier_Notification_Merge_Tags::get_user_meta_keys();
926 if ( ! empty( $user_meta_keys ) ) {
927 foreach ( $user_meta_keys as $user_meta_key ) {
928 $recipient_fields['WooCommerce Customer User Meta'][] = array(
929 'id' => 'woo_customer_meta_' . $user_meta_key,
930 'label' => $user_meta_key,
931 'preview_value' => '123',
932 'return_type' => 'text',
933 'source' => 'user_meta',
934 'path' => $user_meta_key,
935 );
936 }
937 }
938
939 $recipient_fields['WooCommerce Cart Abandonment'] = array(
940 array(
941 'id' => 'wca_cart_customer_billing_phone',
942 'label' => 'Customer billing phone number',
943 'value' => function ($cart_details) {
944 if(empty($cart_details['phone_number'])){
945 return '';
946 }
947 $customer_data = !empty($cart_details['customer_data']) ? maybe_unserialize($cart_details['customer_data']) : [];
948 $country_code = $customer_data['billing_country'] ?? '';
949 $phone_number = self::get_formatted_phone_number($cart_details['phone_number'], $country_code);
950 return html_entity_decode(sanitize_text_field($phone_number));
951 }
952 )
953 );
954
955 return $recipient_fields;
956 }
957
958 /**
959 * Get phone number with country extension code
960 */
961 public static function get_formatted_phone_number( $phone_number, $country_code ) {
962 // If already starts with + and matches international format, return as-is
963 if ( strpos($phone_number, '+') === 0 && notifier_validate_phone_number($phone_number) ) {
964 return $phone_number;
965 }
966 $phone_number = notifier_sanitize_phone_number( $phone_number );
967 $phone_number = ltrim($phone_number, '0');
968 if ( in_array( $country_code, array( 'US', 'CA' ), true ) ) {
969 $calling_code = '+1';
970 $phone_number = ltrim( $phone_number, '+1' );
971 } else {
972 $calling_code = WC()->countries->get_country_calling_code( $country_code );
973 $calling_code = is_array( $calling_code ) ? $calling_code[0] : $calling_code;
974 if ( $calling_code ) {
975 $phone_number = str_replace( $calling_code, '', preg_replace( '/^0/', '', $phone_number ) );
976 }
977 }
978 $phone_number = ltrim($phone_number, '0');
979 return $calling_code . $phone_number;
980 }
981
982 /**
983 * Add checkbox field on checkout page
984 */
985 public static function add_checkout_optin_fields() {
986 if ('yes' === get_option( NOTIFIER_PREFIX . 'enable_opt_in_checkbox_checkout')) {
987 $checkbox_text = get_option( NOTIFIER_PREFIX . 'checkout_opt_in_checkbox_text');
988 if (empty($checkbox_text)) {
989 $checkbox_text = 'Receive updates on WhatsApp';
990 }
991
992 woocommerce_form_field( NOTIFIER_PREFIX . 'whatsapp_opt_in', array(
993 'type' => 'checkbox',
994 'class' => array('form-row-wide'),
995 'label' => $checkbox_text,
996 ),'');
997 }
998 }
999
1000 /**
1001 * Hook to save the custom field data
1002 */
1003 public static function notifier_save_checkout_field($order_id) {
1004 $opt_in = isset( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ? sanitize_text_field( wp_unslash( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ) : '';
1005 if ( !empty($opt_in) ) {
1006 $order = wc_get_order( $order_id );
1007 $order->update_meta_data( NOTIFIER_PREFIX . 'whatsapp_opt_in', $opt_in );
1008 $order->save();
1009 }
1010 }
1011
1012 /**
1013 * Render opt-in checkbox after billing address on order edit page.
1014 * Hook passes a WC_Order object (works with both HPOS and legacy).
1015 */
1016 public static function render_order_optin_field( $order ) {
1017 if ( 'yes' !== get_option( NOTIFIER_PREFIX . 'enable_opt_in_checkbox_checkout' ) ) {
1018 return;
1019 }
1020
1021 if ( ! is_a( $order, 'WC_Order' ) ) {
1022 $order = wc_get_order( $order->ID );
1023 }
1024 if ( ! $order ) {
1025 return;
1026 }
1027
1028 $checkbox_text = get_option( NOTIFIER_PREFIX . 'checkout_opt_in_checkbox_text' );
1029 if ( empty( $checkbox_text ) ) {
1030 $checkbox_text = 'Receive updates on WhatsApp';
1031 }
1032
1033 $opt_in = $order->get_meta( NOTIFIER_PREFIX . 'whatsapp_opt_in' );
1034
1035 woocommerce_wp_checkbox( array(
1036 'id' => NOTIFIER_PREFIX . 'whatsapp_opt_in',
1037 'label' => '<strong>' . __( 'WANotifier', 'notifier' ) . '</strong>',
1038 'description' => esc_html( $checkbox_text ),
1039 'value' => $opt_in,
1040 'cbvalue' => '1',
1041 'wrapper_class' => 'form-field-wide',
1042 ) );
1043 }
1044
1045 /**
1046 * Hook to save the custom field data on order page
1047 */
1048 public static function save_admin_order_meta( $post_id, $post ) {
1049 if ( ! isset( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) ), 'woocommerce_save_data' ) ) {
1050 return;
1051 }
1052
1053 if ( ! current_user_can( 'edit_shop_orders' ) ) {
1054 return;
1055 }
1056
1057 $order = wc_get_order( $post_id );
1058 if ( ! $order ) {
1059 return;
1060 }
1061
1062 $opt_in = isset( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ? '1' : '';
1063 $order->update_meta_data( NOTIFIER_PREFIX . 'whatsapp_opt_in', $opt_in );
1064 $order->save();
1065 }
1066
1067 /**
1068 * Check if the notification should be sent based on global and user opt-in settings.
1069 */
1070 public static function maybe_send_notification( $order_id ) {
1071 if ('yes' !== get_option( NOTIFIER_PREFIX . 'enable_opt_in_checkbox_checkout' )) {
1072 return true;
1073 }
1074
1075 $order = wc_get_order( $order_id );
1076 $opt_in = $order->get_meta( NOTIFIER_PREFIX . 'whatsapp_opt_in' );
1077 return '1' === $opt_in || true === $opt_in;
1078 }
1079
1080 /**
1081 * Handle checkout data updates.
1082 */
1083 public static function notifier_save_cart_abandonment_data() {
1084 check_ajax_referer('notifier_save_cart_abandonment_data', 'security');
1085
1086 // Check if cart abandonment tracking is enabled
1087 if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
1088 wp_send_json(array('error' => true, 'message' => 'Tracking not enabled.'));
1089 }
1090
1091 global $wpdb;
1092 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1093
1094 // Ensure WooCommerce session is available
1095 if (!WC()->session || !WC()->session->has_session()) {
1096 wp_send_json(array('error' => true, 'message' => 'WooCommerce session not initialized.'));
1097 }
1098
1099 $cart_data = maybe_serialize(WC()->session->get('cart')); // Serialized cart data.
1100 $cart_total = floatval(preg_replace('/[^\d.]/', '', WC()->cart->get_total('edit'))); // Cart total amount.
1101 $applied_coupons = WC()->cart->get_applied_coupons();// Fetch applied coupon details
1102 $serialized_coupons = maybe_serialize($applied_coupons);
1103
1104 $session_key = WC()->session->get('notifier_session_key');
1105
1106 $customer_data = array(
1107 'billing_first_name' => sanitize_text_field($_POST['billing_first_name'] ?? WC()->checkout()->get_value('billing_first_name')),
1108 'billing_last_name' => sanitize_text_field($_POST['billing_last_name'] ?? WC()->checkout()->get_value('billing_last_name')),
1109 'billing_company' => sanitize_text_field($_POST['billing_company'] ?? WC()->checkout()->get_value('billing_company')),
1110 'billing_address_1' => sanitize_textarea_field($_POST['billing_address_1'] ?? WC()->checkout()->get_value('billing_address_1')),
1111 'billing_address_2' => sanitize_textarea_field($_POST['billing_address_2'] ?? WC()->checkout()->get_value('billing_address_2')),
1112 'billing_city' => sanitize_text_field($_POST['billing_city'] ?? WC()->checkout()->get_value('billing_city')),
1113 'billing_postcode' => sanitize_text_field($_POST['billing_postcode'] ?? WC()->checkout()->get_value('billing_postcode')),
1114 'billing_state' => sanitize_text_field($_POST['billing_state'] ?? WC()->checkout()->get_value('billing_state')),
1115 'billing_country' => sanitize_text_field($_POST['billing_country'] ?? WC()->checkout()->get_value('billing_country')),
1116 'billing_phone' => sanitize_text_field($_POST['billing_phone'] ?? WC()->checkout()->get_value('billing_phone')),
1117 'billing_email' => sanitize_email($_POST['billing_email'] ?? WC()->checkout()->get_value('billing_email')),
1118 'shipping_first_name' => sanitize_text_field($_POST['shipping_first_name'] ?? WC()->checkout()->get_value('shipping_first_name')),
1119 'shipping_last_name' => sanitize_text_field($_POST['shipping_last_name'] ?? WC()->checkout()->get_value('shipping_last_name')),
1120 'shipping_company' => sanitize_text_field($_POST['shipping_company'] ?? WC()->checkout()->get_value('shipping_company')),
1121 'shipping_address_1' => sanitize_textarea_field($_POST['shipping_address_1'] ?? WC()->checkout()->get_value('shipping_address_1')),
1122 'shipping_address_2' => sanitize_textarea_field($_POST['shipping_address_2'] ?? WC()->checkout()->get_value('shipping_address_2')),
1123 'shipping_city' => sanitize_text_field($_POST['shipping_city'] ?? WC()->checkout()->get_value('shipping_city')),
1124 'shipping_postcode' => sanitize_text_field($_POST['shipping_postcode'] ?? WC()->checkout()->get_value('shipping_postcode')),
1125 'shipping_state' => sanitize_text_field($_POST['shipping_state'] ?? WC()->checkout()->get_value('shipping_state')),
1126 'shipping_country' => sanitize_text_field($_POST['shipping_country'] ?? WC()->checkout()->get_value('shipping_country')),
1127 'shipping_phone' => sanitize_text_field($_POST['shipping_phone'] ?? WC()->checkout()->get_value('shipping_phone')),
1128 );
1129
1130 $serialized_customer_data = maybe_serialize($customer_data);
1131 $phone_number = sanitize_text_field($customer_data['billing_phone']);
1132
1133 // Handle case where phone number is missing
1134 if (empty($phone_number)) {
1135 if($session_key){
1136 as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
1137 $wpdb->delete($table_name, array('session_key' => sanitize_key($session_key)), array('%s'));
1138 WC()->session->set( 'notifier_session_key', null );
1139 }
1140 wp_send_json(array('error' => true, 'message' => 'phone number is empty.'));
1141 }
1142
1143 if (WC()->cart->is_empty()) {
1144 if($session_key){
1145 as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
1146 $wpdb->delete($table_name, array('session_key' => sanitize_key($session_key)), array('%s'));
1147 WC()->session->set( 'notifier_session_key', null );
1148 }
1149 wp_send_json(array('error' => true, 'message' => 'cart is empty.'));
1150 }
1151
1152 // Handle session ID
1153 if (!$session_key) {
1154 $session_key = notifier_generate_random_key(16); // Generate a unique session ID.
1155 WC()->session->set('notifier_session_key', $session_key);
1156 }
1157
1158 // Check if an existing entry exists
1159 $existing_entry = $wpdb->get_row($wpdb->prepare(
1160 "SELECT * FROM $table_name WHERE session_key = %s",
1161 $session_key
1162 ));
1163
1164 // If entry exists and is triggered, do not schedule action
1165 if ($existing_entry && $existing_entry->triggered) {
1166 wp_send_json(array('error' => true, 'message' => 'Cart abandonment already processed.' ));
1167 }
1168
1169 // Fetch all active session_keys for this phone number
1170 $existing_session_keys = $wpdb->get_col(
1171 $wpdb->prepare(
1172 "SELECT session_key FROM $table_name WHERE phone_number = %s AND triggered = 0 AND order_id = 0",
1173 $phone_number
1174 )
1175 );
1176
1177 // Delete all other sessions except the current one
1178 if (!empty($existing_session_keys)) {
1179 foreach ($existing_session_keys as $old_session_key) {
1180 if ($session_key !== $old_session_key) {
1181 $wpdb->query(
1182 $wpdb->prepare(
1183 "DELETE FROM $table_name WHERE session_key = %s",
1184 $old_session_key
1185 )
1186 );
1187 }
1188 }
1189 }
1190
1191 // Update or insert cart and customer data into the table
1192 if ($existing_entry) {
1193 // Update the existing entry
1194 $wpdb->update(
1195 $table_name,
1196 array(
1197 'phone_number' => $phone_number,
1198 'cart_data' => $cart_data,
1199 'cart_total' => $cart_total,
1200 'coupon_data' => $serialized_coupons,
1201 'customer_data' => $serialized_customer_data,
1202 'updated_time' => current_time('mysql'),
1203 ),
1204 array('session_key' => sanitize_key($session_key)),
1205 array('%s', '%s', '%f', '%s', '%s', '%s'),
1206 array('%s')
1207 );
1208 } else {
1209 // Insert a new entry
1210 $wpdb->insert(
1211 $table_name,
1212 array(
1213 'session_key' => sanitize_key($session_key),
1214 'phone_number' => $phone_number,
1215 'cart_data' => $cart_data,
1216 'cart_total' => $cart_total,
1217 'coupon_data' => $serialized_coupons,
1218 'customer_data' => $serialized_customer_data,
1219 'created_time' => current_time('mysql'),
1220 'updated_time' => current_time('mysql'),
1221 ),
1222 array('%s', '%s', '%s', '%f', '%s', '%s', '%s', '%s')
1223 );
1224 }
1225
1226 // Unschedule previous actions for this session
1227 as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
1228
1229 // Schedule a new action if billing phone is available
1230 if ( !as_next_scheduled_action('notifier_check_cart_abandonment', array($session_key))) {
1231 $run_time_minutes = (int) get_option('notifier_abandonment_cart_cron_run_time', 20); // Default to 20 minutes if not set
1232 $run_time_seconds = $run_time_minutes * MINUTE_IN_SECONDS;
1233 as_schedule_single_action(time() + $run_time_seconds, 'notifier_check_cart_abandonment', array($session_key));
1234 }
1235
1236 wp_send_json(array('error' => false, 'message' => 'Cart abandonment request has been successfully processed. ' ));
1237 }
1238
1239 /**
1240 * Handle cart updates data updates.
1241 */
1242 public static function notifier_update_cart_data_on_update() {
1243 // Check if cart abandonment tracking is enabled
1244 if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
1245 return;
1246 }
1247
1248 // Prevent execution on the Thank You and order recieved page
1249 if (is_checkout() || is_wc_endpoint_url('order-received')) {
1250 return;
1251 }
1252
1253 if (!WC()->session || !WC()->session->has_session()) {
1254 return;
1255 }
1256
1257 global $wpdb;
1258 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1259 $cart_data = maybe_serialize(WC()->session->get('cart')); // Serialized cart data.
1260 $cart_total = floatval(preg_replace('/[^\d.]/', '', WC()->cart->get_total('edit'))); // Cart total amount.
1261
1262 $session_key = WC()->session->get('notifier_session_key');
1263
1264 //Check if an order is already linked to this session record
1265 $existing_order_id = $wpdb->get_var(
1266 $wpdb->prepare(
1267 "SELECT order_id FROM $table_name WHERE session_key = %s",
1268 sanitize_key($session_key)
1269 )
1270 );
1271
1272 // If an order exists, do NOT delete the record
1273 if (!empty($existing_order_id)) {
1274 as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
1275 WC()->session->set( 'notifier_session_key', null );
1276 return;
1277 }
1278
1279 if (WC()->cart->is_empty()) {
1280 if($session_key){
1281 as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
1282 WC()->session->set( 'notifier_session_key', null );
1283 }
1284 return;
1285 }
1286
1287 // If session_key is not set, create and save a new one
1288 if (!$session_key) {
1289 $session_key = notifier_generate_random_key(16); // Generate a 16-character session key
1290 WC()->session->set('notifier_session_key', $session_key);
1291 }
1292
1293 // Check if an existing entry exists for the session_key
1294 $existing_entry = $wpdb->get_row(
1295 $wpdb->prepare(
1296 "SELECT session_id FROM $table_name WHERE session_key = %s",
1297 $session_key
1298 )
1299 );
1300
1301 if ($existing_entry) {
1302 // Update the existing entry with the new cart data
1303 $wpdb->update(
1304 $table_name,
1305 array(
1306 'cart_data' => $cart_data,
1307 'cart_total' => $cart_total,
1308 'updated_time'=> current_time('mysql'),
1309 ),
1310 array('session_key' => $session_key),
1311 array('%s', '%f', '%s'),
1312 array('%s')
1313 );
1314 } else {
1315 // Insert a new entry for the session
1316 $wpdb->insert(
1317 $table_name,
1318 array(
1319 'session_key' => $session_key,
1320 'cart_data' => $cart_data,
1321 'cart_total' => $cart_total,
1322 'created_time'=> current_time('mysql'),
1323 'updated_time'=> current_time('mysql'),
1324 ),
1325 array('%s', '%s', '%f', '%s', '%s')
1326 );
1327 }
1328 }
1329
1330 /**
1331 * Fire when cart data get updated
1332 */
1333 public static function notifier_process_cart_abandonment($session_key) {
1334 // Check if cart abandonment tracking is enabled
1335 if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
1336 return;
1337 }
1338
1339 global $wpdb;
1340 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1341
1342 // Check if the session data still exists in the DB
1343 $abandoned_cart_entry = $wpdb->get_row(
1344 $wpdb->prepare(
1345 "SELECT * FROM $table_name WHERE session_key = %s AND triggered = 0 AND order_id = 0",
1346 $session_key
1347 ),
1348 ARRAY_A
1349 );
1350
1351 if (!empty($abandoned_cart_entry)) {
1352 // Fire the notifier process action
1353 do_action('notifier_process_abandoned_cart', $abandoned_cart_entry);
1354
1355 // Mark the triggered to true
1356 $wpdb->update(
1357 $table_name,
1358 array('triggered' => 1),
1359 array('session_key' => $session_key),
1360 array('%d'),
1361 array('%s')
1362 );
1363 as_unschedule_all_actions('notifier_check_cart_abandonment', array(sanitize_key($session_key)));
1364 }
1365 }
1366
1367 /**
1368 * Fetch comma separated products from cart
1369 */
1370 public static function get_comma_separated_products( $cart_data ) {
1371 $product_names = [];
1372
1373 foreach ($cart_data as $item) {
1374 if (!isset($item['product_id'])) {
1375 continue; // Skip if product_id is missing
1376 }
1377
1378 $product = wc_get_product($item['product_id']);
1379 if ($product) {
1380 $product_names[] = $product->get_name();
1381 }
1382 }
1383
1384 // Return comma-separated product names
1385 return implode(', ', $product_names);
1386 }
1387
1388 /**
1389 * retrive cart data and set from url param
1390 */
1391 public static function retrive_cart_data_from_url(){
1392 if(!is_checkout() && !is_cart()){
1393 return;
1394 }
1395
1396 if (isset($_GET['wano_session'])) {
1397 global $wpdb;
1398 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1399 $session_key = sanitize_key($_GET['wano_session']);
1400 if (empty($session_key)) {
1401 return;
1402 }
1403
1404 $cart_entry = $wpdb->get_row(
1405 $wpdb->prepare("SELECT cart_data, customer_data FROM $table_name WHERE session_key = %s", $session_key),
1406 ARRAY_A
1407 );
1408
1409 if (!empty($cart_entry['cart_data'])) {
1410 WC()->cart->empty_cart();
1411 $cart_contents = maybe_unserialize($cart_entry['cart_data']);
1412 foreach ($cart_contents as $item_key => $item) {
1413 WC()->cart->add_to_cart($item['product_id'], $item['quantity'], $item['variation_id'], $item['variation']);
1414 }
1415 }
1416 }
1417 }
1418
1419 // ========================================
1420 // v3 PAYLOAD BUILDER
1421 // ========================================
1422
1423 /**
1424 * Build the full WooCommerce order payload for v3 trigger dispatch.
1425 *
1426 * Fetches all order data in bulk (one call per data type), then maps
1427 * each defined merge tag and recipient field using source/path or value closure.
1428 * Meta keys discovered here that are new are persisted and trigger an async schema push.
1429 *
1430 * @param int $order_id WooCommerce order ID.
1431 * @param string $trigger_id Trigger ID (e.g. 'woo_order_new').
1432 * @return array Payload with merge_tags_data and recipient_fields.
1433 */
1434 public static function build_wc_order_payload( $order_id, $trigger_id ) {
1435 $order = wc_get_order( $order_id );
1436 if ( ! $order ) {
1437 return array( 'merge_tags_data' => array(), 'recipient_fields' => array() );
1438 }
1439
1440 $customer_id = $order->get_customer_id();
1441
1442 // Single bulk fetch — all data in ~6 calls total.
1443 $bulk_data = array(
1444 'order' => $order,
1445 'base' => $order->get_base_data(),
1446 'items' => $order->get_items( 'line_item' ),
1447 'shipping' => $order->get_items( 'shipping' ),
1448 'coupons' => $order->get_items( 'coupon' ),
1449 'fees' => $order->get_items( 'fee' ),
1450 'meta' => $order->get_meta_data(),
1451 'user_meta' => $customer_id ? get_user_meta( $customer_id ) : array(),
1452 );
1453
1454 // Index meta by key for fast lookups.
1455 $meta_by_key = array();
1456 foreach ( $bulk_data['meta'] as $meta_item ) {
1457 if ( ! empty( $meta_item->key ) ) {
1458 $meta_by_key[ $meta_item->key ] = $meta_item->value;
1459 }
1460 }
1461
1462 // Index user meta (get_user_meta returns arrays of values per key).
1463 $user_meta_by_key = array();
1464 foreach ( $bulk_data['user_meta'] as $key => $values ) {
1465 $user_meta_by_key[ $key ] = is_array( $values ) ? $values[0] : $values;
1466 }
1467
1468 $merge_tags_data = array();
1469 $recipient_fields = array();
1470
1471 $all_merge_tags = Notifier_Notification_Merge_Tags::get_merge_tags( self::get_order_merge_tag_types() );
1472 foreach ( $all_merge_tags as $group => $tags ) {
1473 foreach ( $tags as $tag ) {
1474 if ( empty( $tag['id'] ) ) {
1475 continue;
1476 }
1477 if ( 0 === strpos( $tag['id'], 'woo_order_meta_' ) || 0 === strpos( $tag['id'], 'woo_customer_meta_' ) ) {
1478 continue;
1479 }
1480 $value = self::resolve_field_from_bulk( $tag, $bulk_data, $meta_by_key, $user_meta_by_key );
1481 if ( null !== $value ) {
1482 $merge_tags_data[ $tag['id'] ] = html_entity_decode( sanitize_text_field( (string) $value ) );
1483 }
1484 }
1485 }
1486
1487 $all_recipient_fields = Notifier_Notification_Merge_Tags::get_recipient_fields( self::get_order_recipient_types() );
1488 foreach ( $all_recipient_fields as $group => $fields ) {
1489 foreach ( $fields as $field ) {
1490 if ( empty( $field['id'] ) ) {
1491 continue;
1492 }
1493 if ( 0 === strpos( $field['id'], 'woo_order_meta_' ) || 0 === strpos( $field['id'], 'woo_customer_meta_' ) ) {
1494 continue;
1495 }
1496 $value = self::resolve_field_from_bulk( $field, $bulk_data, $meta_by_key, $user_meta_by_key );
1497 if ( null !== $value ) {
1498 $recipient_fields[ $field['id'] ] = html_entity_decode( sanitize_text_field( (string) $value ) );
1499 }
1500 }
1501 }
1502
1503 foreach ( $meta_by_key as $key => $value ) {
1504 $tag_id = 'woo_order_meta_' . $key;
1505 $merge_tags_data[ $tag_id ] = is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
1506 $recipient_fields[ $tag_id ] = $merge_tags_data[ $tag_id ];
1507 }
1508
1509 foreach ( $user_meta_by_key as $key => $value ) {
1510 $tag_id = 'woo_customer_meta_' . $key;
1511 $merge_tags_data[ $tag_id ] = is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
1512 $recipient_fields[ $tag_id ] = $merge_tags_data[ $tag_id ];
1513 }
1514
1515 // Live diff: detect new meta keys and persist + schedule async schema push.
1516 self::maybe_update_known_meta_keys( 'shop_order', array_keys( $meta_by_key ) );
1517 if ( $customer_id ) {
1518 self::maybe_update_known_meta_keys( '_user', array_keys( $user_meta_by_key ) );
1519 }
1520
1521 return array(
1522 'merge_tags_data' => $merge_tags_data,
1523 'recipient_fields' => $recipient_fields,
1524 );
1525 }
1526
1527 /**
1528 * Resolve a single merge tag or recipient field value from pre-fetched bulk data.
1529 *
1530 * @param array $field_def Field definition with source, path, and/or value closure.
1531 * @param array $bulk_data Pre-fetched bulk data array.
1532 * @param array $meta_by_key Order meta indexed by key.
1533 * @param array $user_meta_by_key User meta indexed by key.
1534 * @return string|null Resolved value, or null if not applicable.
1535 */
1536 private static function resolve_field_from_bulk( $field_def, $bulk_data, $meta_by_key, $user_meta_by_key ) {
1537 if ( isset( $field_def['value'] ) && is_callable( $field_def['value'] ) ) {
1538 return call_user_func( $field_def['value'], $bulk_data );
1539 }
1540
1541 $source = isset( $field_def['source'] ) ? $field_def['source'] : null;
1542 $path = isset( $field_def['path'] ) ? $field_def['path'] : null;
1543
1544 if ( ! $source || ! $path ) {
1545 return null;
1546 }
1547
1548 if ( 'base' === $source ) {
1549 $parts = explode( '.', $path );
1550 $data = $bulk_data['base'];
1551 foreach ( $parts as $part ) {
1552 if ( ! isset( $data[ $part ] ) ) {
1553 return '';
1554 }
1555 $data = $data[ $part ];
1556 }
1557 return (string) $data;
1558 }
1559
1560 if ( 'meta' === $source ) {
1561 $value = $meta_by_key[ $path ] ?? '';
1562 return is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
1563 }
1564
1565 if ( 'user_meta' === $source ) {
1566 $value = $user_meta_by_key[ $path ] ?? '';
1567 return is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
1568 }
1569
1570 return null;
1571 }
1572
1573 /**
1574 * Compare discovered meta keys against stored known keys.
1575 * If new keys found, update the persistent option and schedule an async schema push.
1576 *
1577 * @param string $type Post type key (e.g. 'shop_order') or '_user' for user meta.
1578 * @param array $live_keys Keys found in the current object's meta.
1579 */
1580 private static function maybe_update_known_meta_keys( $type, $live_keys ) {
1581 if ( '_user' === $type ) {
1582 $known = get_option( 'notifier_user_meta_keys', array() );
1583 $new_keys = array_diff( $live_keys, $known );
1584 if ( ! empty( $new_keys ) ) {
1585 update_option( 'notifier_user_meta_keys', array_unique( array_merge( $known, $new_keys ) ), false );
1586 as_enqueue_async_action( 'notifier_push_updated_schema', array( 'slug' => 'woocommerce' ), 'notifier' );
1587 }
1588 return;
1589 }
1590
1591 $all_known = get_option( 'notifier_custom_meta_keys', array() );
1592 $known = isset( $all_known[ $type ] ) ? $all_known[ $type ] : array();
1593 $new_keys = array_diff( $live_keys, $known );
1594 if ( ! empty( $new_keys ) ) {
1595 $all_known[ $type ] = array_unique( array_merge( $known, $new_keys ) );
1596 update_option( 'notifier_custom_meta_keys', $all_known, false );
1597 as_enqueue_async_action( 'notifier_push_updated_schema', array( 'slug' => 'woocommerce' ), 'notifier' );
1598 }
1599 }
1600
1601 /**
1602 * Action Scheduler handler: push updated trigger schema to SaaS after new meta keys discovered.
1603 *
1604 * @param string $slug Integration slug.
1605 */
1606 public static function handle_push_updated_schema( $slug ) {
1607 if ( ! Notifier_Connection::is_connected( $slug ) ) {
1608 return;
1609 }
1610 $trigger_schema = Notifier_Connection::build_trigger_schema( $slug );
1611 Notifier_Connection::send_saas_request( $slug, 'sync-triggers', array( 'triggers' => $trigger_schema ) );
1612 }
1613
1614 // ========================================
1615 // v3 CONTACT SYNC
1616 // ========================================
1617
1618 /**
1619 * Sync a WP user as a contact to SaaS when they register.
1620 *
1621 * @param int $user_id
1622 */
1623 public static function on_user_register( $user_id ) {
1624 self::sync_customer_contact( $user_id, 'woo_customer_created' );
1625 }
1626
1627 /**
1628 * Sync a WP user as a contact to SaaS when their profile is updated.
1629 *
1630 * @param int $user_id
1631 * @param WP_User $old_user_data
1632 */
1633 public static function on_user_update( $user_id, $old_user_data ) {
1634 self::sync_customer_contact( $user_id, 'woo_customer_updated' );
1635 }
1636
1637 /**
1638 * Sync contact from checkout billing data (covers guest checkouts).
1639 *
1640 * Only runs when continuous sync is enabled in the connection config.
1641 *
1642 * @param int $order_id
1643 */
1644 public static function on_checkout_contact_sync( $order_id ) {
1645 $conn = Notifier_Connection::get_connection( 'woocommerce' );
1646 if ( empty( $conn ) || empty( $conn['continuous_sync'] ) ) {
1647 return;
1648 }
1649
1650 $order = wc_get_order( $order_id );
1651 if ( ! $order ) {
1652 return;
1653 }
1654
1655 $phone = $order->get_billing_phone();
1656 if ( empty( $phone ) ) {
1657 return;
1658 }
1659
1660 $contact = array(
1661 'billing' => array(
1662 'phone' => $phone,
1663 'email' => $order->get_billing_email(),
1664 'first_name' => $order->get_billing_first_name(),
1665 'last_name' => $order->get_billing_last_name(),
1666 'company' => $order->get_billing_company(),
1667 'address_1' => $order->get_billing_address_1(),
1668 'address_2' => $order->get_billing_address_2(),
1669 'city' => $order->get_billing_city(),
1670 'state' => $order->get_billing_state(),
1671 'postcode' => $order->get_billing_postcode(),
1672 'country' => $order->get_billing_country(),
1673 ),
1674 );
1675
1676 self::push_contacts_to_saas( array( $contact ), array(), 'woo_customer_created' );
1677 }
1678
1679 /**
1680 * Sync a WordPress/WooCommerce user as a contact to the SaaS.
1681 *
1682 * Only runs when continuous sync is enabled in the connection config.
1683 *
1684 * @param int $user_id
1685 * @param string $event_key Trigger slug: 'woo_customer_created' or 'woo_customer_updated'.
1686 */
1687 private static function sync_customer_contact( $user_id, $event_key ) {
1688 $conn = Notifier_Connection::get_connection( 'woocommerce' );
1689 if ( empty( $conn ) ) {
1690 return;
1691 }
1692
1693 // Respect the continuous sync toggle.
1694 if ( empty( $conn['continuous_sync'] ) ) {
1695 return;
1696 }
1697
1698 $phone = get_user_meta( $user_id, 'billing_phone', true );
1699 if ( empty( $phone ) ) {
1700 return; // Can't sync without a phone number.
1701 }
1702
1703 $contact = self::build_customer_contact_data( $user_id );
1704 self::push_contacts_to_saas( array( $contact ), array(), $event_key );
1705 }
1706
1707 /**
1708 * Push contacts to the SaaS via the unified /{webhook_key}/event endpoint.
1709 *
1710 * @param array $contacts Array of contact data arrays in WC customer format.
1711 * @param array $meta Optional metadata (total_contacts, remaining_contacts).
1712 * @param string $event_key Trigger slug identifying the event: 'woo_customer_created',
1713 * 'woo_customer_updated', or 'woo_customer_batch_sync'.
1714 */
1715 private static function push_contacts_to_saas( $contacts, $meta = array(), $event_key = 'woo_customer_created' ) {
1716 $conn = Notifier_Connection::get_connection( 'woocommerce' );
1717 if ( empty( $conn ) || empty( $conn['webhook_key'] ) || empty( $conn['saas_url'] ) ) {
1718 return;
1719 }
1720
1721 $url = trailingslashit( $conn['saas_url'] ) . NOTIFIER_APP_API_PATH . '/integrations/woocommerce/' . $conn['webhook_key'] . '/event';
1722 $body = array_merge( array(
1723 'event_key' => $event_key,
1724 'contacts' => $contacts,
1725 ), $meta );
1726
1727
1728 $response = wp_remote_post( $url, array(
1729 'headers' => array(
1730 'Content-Type' => 'application/json',
1731 'X-Auth-Key' => $conn['auth_key'] ?? '',
1732 ),
1733 'body' => wp_json_encode( $body ),
1734 'timeout' => 30,
1735 'sslverify' => true,
1736 ) );
1737
1738 if ( is_wp_error( $response ) ) {
1739 error_log( 'WANotifier: push_contacts_to_saas failed: ' . $response->get_error_message() );
1740 }
1741 }
1742
1743 /**
1744 * REST handler: SaaS pushes config updates (e.g. continuous_sync toggle).
1745 *
1746 * Stores the config values in the plugin's WooCommerce connection option so
1747 * the real-time sync hooks can read them without calling the SaaS.
1748 *
1749 * @param WP_REST_Request $request
1750 * @return WP_REST_Response
1751 */
1752 public static function handle_rest_update_config( $request ) {
1753 $data = $request->get_json_params();
1754
1755 $conn = Notifier_Connection::get_connection( 'woocommerce' );
1756 if ( empty( $conn ) ) {
1757 return new WP_REST_Response( array( 'error' => true, 'message' => 'Not connected.' ), 400 );
1758 }
1759
1760 if ( array_key_exists( 'continuous_sync', $data ) ) {
1761 $conn['continuous_sync'] = ! empty( $data['continuous_sync'] );
1762 Notifier_Connection::save_connection( 'woocommerce', $conn );
1763 }
1764
1765 return new WP_REST_Response( array( 'error' => false, 'message' => 'Config updated.' ), 200 );
1766 }
1767
1768 /**
1769 * REST handler: SaaS requests the plugin to stop a running batch customer sync.
1770 *
1771 * Cancels all pending Action Scheduler jobs for the batch sync and sets a
1772 * transient flag so any currently-running page handler exits early.
1773 *
1774 * @param WP_REST_Request $request
1775 * @return WP_REST_Response
1776 */
1777 public static function handle_rest_batch_sync_stop( $request ) {
1778 // Cancel all queued (not yet started) batch sync pages.
1779 as_unschedule_all_actions( 'notifier_wc_batch_contact_sync', array(), 'notifier' );
1780
1781 // Set a short-lived flag so any in-progress page handler exits before pushing to SaaS.
1782 set_transient( 'notifier_wc_batch_sync_stopped', 1, 5 * MINUTE_IN_SECONDS );
1783
1784 return new WP_REST_Response( array(
1785 'error' => false,
1786 'message' => 'Batch sync stopped.',
1787 ), 200 );
1788 }
1789
1790 /**
1791 * REST handler: SaaS requests the plugin to start a batch customer sync.
1792 *
1793 * Schedules an Action Scheduler job on the plugin side that pages through
1794 * all WooCommerce customers and pushes them to the SaaS /{webhook_key}/event endpoint.
1795 *
1796 * @param WP_REST_Request $request
1797 * @return WP_REST_Response
1798 */
1799 public static function handle_rest_batch_sync_start( $request ) {
1800
1801 // Cancel any existing pending batch sync jobs to avoid duplicates.
1802 as_unschedule_all_actions( 'notifier_wc_batch_contact_sync', array(), 'notifier' );
1803
1804 // Clear any lingering stop flag from a previous stop request.
1805 delete_transient( 'notifier_wc_batch_sync_stopped' );
1806
1807 // Count total customers with billing phone.
1808 $total_query = new WP_User_Query( array(
1809 'role__in' => array( 'customer', 'subscriber' ),
1810 'meta_key' => 'billing_phone',
1811 'meta_compare' => '!=',
1812 'meta_value' => '',
1813 'number' => -1,
1814 'fields' => 'ID',
1815 ) );
1816 $total = count( $total_query->get_results() );
1817
1818
1819 // Schedule first page.
1820 as_enqueue_async_action(
1821 'notifier_wc_batch_contact_sync',
1822 array( 'page' => 1, 'per_page' => 50, 'total' => $total ),
1823 'notifier'
1824 );
1825
1826
1827 return new WP_REST_Response( array(
1828 'error' => false,
1829 'message' => 'Batch sync started.',
1830 'total' => $total,
1831 ), 200 );
1832 }
1833
1834 /**
1835 * Action Scheduler handler: process one page of the batch customer sync.
1836 *
1837 * Runs in the plugin. Fetches a page of WC customers, builds contact data,
1838 * and pushes the batch to the SaaS /{webhook_key}/event endpoint. Schedules the next
1839 * page if more customers remain.
1840 *
1841 * @param int $page Current page number.
1842 * @param int $per_page Number of customers per page.
1843 * @param int $total Total customers to sync.
1844 */
1845 public static function process_batch_contact_sync_page( $page, $per_page, $total ) {
1846 // Abort immediately if a stop was requested while this job was queued.
1847 if ( get_transient( 'notifier_wc_batch_sync_stopped' ) ) {
1848 return;
1849 }
1850
1851 $page = (int) $page ?: 1;
1852 $per_page = (int) $per_page ?: 50;
1853 $total = (int) $total ?: 0;
1854
1855
1856 $user_query = new WP_User_Query( array(
1857 'role__in' => array( 'customer', 'subscriber' ),
1858 'meta_key' => 'billing_phone',
1859 'meta_compare' => '!=',
1860 'meta_value' => '',
1861 'number' => $per_page,
1862 'paged' => $page,
1863 'orderby' => 'ID',
1864 'order' => 'ASC',
1865 ) );
1866
1867 $users = $user_query->get_results();
1868
1869 if ( empty( $users ) ) {
1870 return;
1871 }
1872
1873
1874 $contacts = array();
1875 foreach ( $users as $user ) {
1876 $contacts[] = self::build_customer_contact_data( $user->ID );
1877 }
1878
1879 $processed_so_far = $page * $per_page;
1880 $remaining = max( 0, $total - $processed_so_far );
1881
1882
1883 self::push_contacts_to_saas( $contacts, array(
1884 'total_contacts' => $total,
1885 'remaining_contacts' => $remaining,
1886 ), 'woo_customer_batch_sync' );
1887
1888 if ( $remaining > 0 ) {
1889 as_enqueue_async_action(
1890 'notifier_wc_batch_contact_sync',
1891 array( 'page' => $page + 1, 'per_page' => $per_page, 'total' => $total ),
1892 'notifier'
1893 );
1894 }
1895 }
1896
1897 /**
1898 * Build contact data array for a WP user in WC customer format.
1899 *
1900 * @param int $user_id
1901 * @return array
1902 */
1903 private static function build_customer_contact_data( $user_id ) {
1904 $user = get_userdata( $user_id );
1905 $billing_phone = get_user_meta( $user_id, 'billing_phone', true );
1906 $billing_country = get_user_meta( $user_id, 'billing_country', true );
1907
1908 return array(
1909 'id' => $user_id,
1910 'email' => $user ? $user->user_email : '',
1911 'first_name' => get_user_meta( $user_id, 'first_name', true ) ?: get_user_meta( $user_id, 'billing_first_name', true ),
1912 'last_name' => get_user_meta( $user_id, 'last_name', true ) ?: get_user_meta( $user_id, 'billing_last_name', true ),
1913 'username' => $user ? $user->user_login : '',
1914 'billing' => array(
1915 'phone' => self::get_formatted_phone_number( $billing_phone, $billing_country ),
1916 'email' => get_user_meta( $user_id, 'billing_email', true ),
1917 'first_name' => get_user_meta( $user_id, 'billing_first_name', true ),
1918 'last_name' => get_user_meta( $user_id, 'billing_last_name', true ),
1919 'company' => get_user_meta( $user_id, 'billing_company', true ),
1920 'address_1' => get_user_meta( $user_id, 'billing_address_1', true ),
1921 'address_2' => get_user_meta( $user_id, 'billing_address_2', true ),
1922 'city' => get_user_meta( $user_id, 'billing_city', true ),
1923 'state' => get_user_meta( $user_id, 'billing_state', true ),
1924 'postcode' => get_user_meta( $user_id, 'billing_postcode', true ),
1925 'country' => $billing_country,
1926 ),
1927 );
1928 }
1929
1930 /**
1931 * store session id during order creation
1932 */
1933 public static function notifier_save_session_on_new_order($order_id) {
1934 if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
1935 return;
1936 }
1937
1938 if (!WC()->session || !WC()->session->has_session()) {
1939 return;
1940 }
1941
1942 // Fetch specific session key
1943 $session_key = WC()->session->get('notifier_session_key');
1944
1945 if (!empty($session_key)) {
1946 global $wpdb;
1947 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1948 $wpdb->update(
1949 $table_name,
1950 array('order_id' => $order_id), // Update the order_id
1951 array('session_key' => sanitize_key($session_key)), // Where condition
1952 array('%d'), // Data type for order_id
1953 array('%s') // Data type for session_key
1954 );
1955 as_unschedule_all_actions('notifier_check_cart_abandonment', array(sanitize_key($session_key)));
1956 }
1957 }
1958 }
1959