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
← All changes | includes/classes/integrations/class-notifier-woocommerce.php +1105 -353 2.7.23.1.1 View file →
@@ -1,5 +1,9 @@
1 1 <?php
2 +if ( ! defined( 'ABSPATH' ) ) {
3 + exit;
4 +}
5 +
2 6 /**
3 7 * Woocommerce notifications
4 8 *
5 9 * @package Wa_Notifier
@@ -14,8 +18,10 @@
14 18 add_filter( 'notifier_notification_merge_tags', array( __CLASS__, 'woocommerce_merge_tags') );
15 19 add_filter( 'notifier_notification_recipient_fields', array( __CLASS__, 'woocommerce_recipient_fields') );
16 20 add_action( 'woocommerce_review_order_before_submit', array( __CLASS__, 'add_checkout_optin_fields') );
17 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 );
18 24
19 25 add_action( 'wp_ajax_notifier_save_cart_abandonment_data', array(__CLASS__, 'notifier_save_cart_abandonment_data'));
20 26 add_action( 'wp_ajax_nopriv_notifier_save_cart_abandonment_data', array(__CLASS__, 'notifier_save_cart_abandonment_data'));
21 27 add_action( 'woocommerce_cart_updated', array( __CLASS__, 'notifier_update_cart_data_on_update'), 999);
@@ -21,20 +27,46 @@
21 27 add_action( 'woocommerce_cart_updated', array( __CLASS__, 'notifier_update_cart_data_on_update'), 999);
22 28 add_action( 'notifier_check_cart_abandonment', array( __CLASS__ , 'notifier_process_cart_abandonment' ), 10, 1);
23 29
24 30 add_action( 'woocommerce_new_order', array( __CLASS__ , 'notifier_save_session_on_new_order' ), 10, 1);
25 - add_action( 'woocommerce_thankyou', array(__CLASS__, 'update_cart_abandonment_order_data'));
26 - add_action( 'woocommerce_order_status_changed', array(__CLASS__, 'update_abandonment_cart_on_order_status'), 999, 3);
27 31
28 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 );
29 45 }
30 46
31 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 + /**
32 64 * Add Woocommerce notification triggers
33 65 */
34 66 public static function get_woo_notification_triggers() {
35 - $merge_tag_types = array('WooCommerce', 'WooCommerce Order', 'WooCommerce Customer', 'WooCommerce Order Custom Meta', 'WooCommerce Customer User Meta', 'WooCommerce Cart Abandonment');
36 - $recipient_types = array('WooCommerce', 'WooCommerce Order Custom Meta', 'WooCommerce Customer User Meta');
67 + $merge_tag_types = self::get_order_merge_tag_types();
68 + $recipient_types = self::get_order_recipient_types();
37 69 $triggers = array (
38 70 array(
39 71 'id' => 'woo_order_new',
40 72 'label' => 'New order is placed',
@@ -41,9 +73,9 @@
41 73 'description' => 'Trigger notification when a new order is placed.',
42 74 'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
43 75 'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
44 76 'action' => array(
45 - 'hook' => 'woocommerce_checkout_order_processed',
77 + 'hook' => 'woocommerce_new_order',
46 78 'callback' => function ( $order_id ){
47 79 if (!self::maybe_send_notification($order_id)) {
48 80 return;
49 81 }
@@ -62,9 +94,9 @@
62 94 'description' => 'Trigger notification when a new order is placed with Cash on Delivery payment method selected.',
63 95 'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
64 96 'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
65 97 'action' => array(
66 - 'hook' => 'woocommerce_checkout_order_processed',
98 + 'hook' => 'woocommerce_new_order',
67 99 'callback' => function ( $order_id ){
68 100 if (!self::maybe_send_notification($order_id)) {
69 101 return;
70 102 }
@@ -83,9 +115,9 @@
83 115 )
84 116 ),
85 117 array(
86 118 'id' => 'woocommerce_cart_abandonment',
87 - 'label' => 'Cart is abandoned (New)',
119 + 'label' => 'Cart is abandoned',
88 120 'description' => 'Trigger notification when cart is abandoned as per your setup in the Settings > WooCommerce tab.',
89 121 'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags(array('WooCommerce', 'WooCommerce Cart Abandonment')),
90 122 'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields(array('WooCommerce Cart Abandonment')),
91 123 'action' => array(
@@ -148,9 +180,10 @@
148 180 'id' => 'woo_store_url',
149 181 'label' => 'Shop page url',
150 182 'preview_value' => 'https://example.com/shop/',
151 183 'return_type' => 'text',
152 - 'value' => function ($args) {
184 + 'source' => 'static',
185 + 'value' => function ( $bulk_data ) {
153 186 return (string) get_permalink( wc_get_page_id( 'shop' ) );
154 187 }
155 188 ),
156 189 array(
@@ -157,253 +190,399 @@
157 190 'id' => 'woo_my_account_url',
158 191 'label' => 'My Account page url',
159 192 'preview_value' => 'https://example.com/my-account/',
160 193 'return_type' => 'text',
161 - 'value' => function ($args) {
162 - return get_permalink( wc_get_page_id( 'myaccount' ) );
194 + 'source' => 'static',
195 + 'value' => function ( $bulk_data ) {
196 + return (string) get_permalink( wc_get_page_id( 'myaccount' ) );
163 197 }
164 198 ),
165 199 );
166 200
167 - $woo_fields = array ();
168 - $woo_fields['WooCommerce Order'] = array ( // Woocommerce order fields
169 - 'id' => array(
170 - 'preview' => '123',
171 - 'label' => 'Order ID'
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',
172 210 ),
173 - 'subtotal_to_display' => array(
174 - 'preview' => '$50.00',
175 - 'label' => 'Order subtotal'
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 + },
176 218 ),
177 - 'coupon_codes' => array(
178 - 'preview' => 'OFF50',
179 - 'label' => 'Order coupon codes',
180 - 'value' => function ($order, $field_function) {
181 - return implode(', ', $order->$field_function());
182 - }
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 + },
183 230 ),
184 - 'discount_to_display' => array(
185 - 'preview' => '$5.00',
186 - 'label' => 'Order discount amount'
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 + },
187 238 ),
188 - 'shipping_method' => array(
189 - 'preview' => 'Flat rate',
190 - 'label' => 'Order shipping method'
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 + },
191 250 ),
192 - 'shipping_total' => array(
193 - 'preview' => '$2.50',
194 - 'label' => 'Order shipping amount',
195 - 'value' => function ($order, $field_function) {
196 - return wc_price( $order->$field_function(), array( 'currency' => $order->get_currency() ) );
197 - }
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 + },
198 260 ),
199 - 'total_tax' => array(
200 - 'preview' => '$5.00',
201 - 'label' => 'Order tax amount',
202 - 'value' => function ($order, $field_function) {
203 - return wc_price( $order->$field_function(), array( 'currency' => $order->get_currency() ) );
204 - }
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 + },
205 270 ),
206 271 'formatted_order_total' => array(
207 - 'preview' => '$50.00',
208 - 'label' => 'Order total amount'
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 + },
209 278 ),
210 - 'payment_method_title' => array(
211 - 'preview' => 'Cash on delivery',
212 - 'label' => 'Order payment method'
279 + 'payment_method_title' => array(
280 + 'preview' => 'Cash on delivery',
281 + 'label' => 'Order payment method',
282 + 'source' => 'base',
283 + 'path' => 'payment_method_title',
213 284 ),
214 - 'checkout_payment_url' => array(
215 - 'preview' => 'https://example.com/checkout/order-received/123/?key=wc_order_abcdef',
216 - 'label' => 'Order checkout payment URL'
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 + },
217 292 ),
218 - 'checkout_order_received_url' => array(
219 - 'preview' => 'https://example.com/checkout/order-received/123/?key=wc_order_abcdef',
220 - 'label' => 'Order thank you page URL'
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 + },
221 300 ),
222 - 'view_order_url' => array(
223 - 'preview' => 'https://example.com/my-account/view-order/123/',
224 - 'label' => 'Order view URL'
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 + },
225 308 ),
226 - 'status' => array(
227 - 'preview' => 'Processing',
228 - 'label' => 'Order status'
309 + 'status' => array(
310 + 'preview' => 'Processing',
311 + 'label' => 'Order status',
312 + 'source' => 'base',
313 + 'path' => 'status',
229 314 ),
230 315 'first_product_image' => array(
231 - 'preview' => '',
232 - 'label' => 'Order product image',
233 - 'return_type' => 'image',
234 - 'value' => function ($order, $field_function) {
235 - $image_id = false;
236 - $image_url = '';
237 - foreach($order->get_items() as $item){
238 - $first_product_id = $item->get_product_id();
239 - $product = wc_get_product( $first_product_id );
240 - $image_id = $product->get_image_id();
241 - if($image_id){
242 - break;
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();
243 326 }
327 + break;
244 328 }
245 -
246 - if($image_id){
247 - $image_url = wp_get_attachment_url( $product->get_image_id() );
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 + );
248 344 }
249 - else{
250 - $image_url = wc_placeholder_img_src();
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();
251 356 }
252 - return $image_url;
253 - }
357 + return '';
358 + },
254 359 ),
255 - 'products_list' => array(
256 - 'preview' => '',
257 - 'label' => 'Order products list',
258 - 'return_type' => 'text',
259 - 'value' => function ($order, $field_function) {
260 - $order_item_data = array();
261 - foreach ( $order->get_items() as $item ) {
262 - $order_item = $item->get_name().' x '.$item->get_quantity().' ('.wc_price( $item->get_total() ).')';
263 - $order_item_data[] = sanitize_textarea_field($order_item);
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;
264 373 }
265 -
266 - return implode(', ',$order_item_data);
267 - }
374 + return '';
375 + },
268 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 + ),
269 390 );
270 391
271 - $woo_fields['WooCommerce Customer'] = array (
272 - 'billing_first_name' => array('preview' => 'John'),
273 - 'billing_last_name' => array('preview' => 'Doe'),
274 - 'billing_company' => array('preview' => 'ABC Inc'),
275 - 'billing_address_1' => array('preview' => '123, XYZ Street'),
276 - 'billing_address_2' => array('preview' => 'XYZ Avenue'),
277 - 'billing_city' => array('preview' => 'New York'),
278 - 'billing_postcode' => array('preview' => '12345'),
279 - 'billing_state' => array(
280 - 'preview' => 'NY',
281 - 'value' => function ($order, $field_function) {
282 - $country_function = str_replace('state', 'country', $field_function);
283 - $state_code = $order->$field_function();
284 - $country_code = $order->$country_function();
285 - return WC()->countries->states[$country_code][$state_code];
286 - }
392 + $woo_fields['WooCommerce Customer'] = array(
393 + 'billing_first_name' => array(
394 + 'preview' => 'John',
395 + 'source' => 'base',
396 + 'path' => 'billing.first_name',
287 397 ),
288 - 'billing_country' => array(
289 - 'preview' => 'US',
290 - 'value' => function ($order, $field_function) {
291 - return WC()->countries->countries[$order->$field_function()];
292 - }
398 + 'billing_last_name' => array(
399 + 'preview' => 'Doe',
400 + 'source' => 'base',
401 + 'path' => 'billing.last_name',
293 402 ),
294 - 'billing_email' => array('preview' => 'john@example.com'),
295 - 'billing_phone' => array('preview' => '987 654 321'),
296 - 'formatted_billing_address' => array(
297 - 'preview' => '',
298 - 'label' => 'Complete billing address',
299 - 'value' => function ($order, $field_function) {
300 - return str_replace('<br/>', ', ', $order->$field_function());
301 - }
403 + 'billing_company' => array(
404 + 'preview' => 'ABC Inc',
405 + 'source' => 'base',
406 + 'path' => 'billing.company',
302 407 ),
303 - 'shipping_first_name' => array('preview' => 'John'),
304 - 'shipping_last_name' => array('preview' => 'Doe'),
305 - 'shipping_company' => array('preview' => 'ABC Inc'),
306 - 'shipping_address_1' => array('preview' => '123, XYZ Street'),
307 - 'shipping_address_2' => array('preview' => 'XYZ Avenue'),
308 - 'shipping_city' => array('preview' => 'New York'),
309 - 'shipping_postcode' => array('preview' => '12345'),
310 - 'shipping_state' => array(
311 - 'preview' => 'NY',
312 - 'value' => function ($order, $field_function) {
313 - $country_function = str_replace('state', 'country', $field_function);
314 - $state_code = $order->$field_function();
315 - $country_code = $order->$country_function();
316 - return WC()->countries->states[$country_code][$state_code];
317 - }
408 + 'billing_address_1' => array(
409 + 'preview' => '123, XYZ Street',
410 + 'source' => 'base',
411 + 'path' => 'billing.address_1',
318 412 ),
319 - 'shipping_country' => array(
320 - 'preview' => 'US',
321 - 'value' => '',
322 - 'value' => function ($order, $field_function) {
323 - return WC()->countries->countries[$order->$field_function()];
324 - }
413 + 'billing_address_2' => array(
414 + 'preview' => 'XYZ Avenue',
415 + 'source' => 'base',
416 + 'path' => 'billing.address_2',
325 417 ),
326 - 'shipping_phone' => array('preview' => '987 654 321'),
327 - 'formatted_shipping_address' => array(
328 - 'preview' => '',
329 - 'label' => 'Complete shipping address',
330 - 'value' => function ($order, $field_function) {
331 - return str_replace('<br/>', ', ', $order->$field_function());
332 - }
333 - )
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 + ),
334 542 );
335 543
336 - foreach ($woo_fields as $woo_field_key => $woo_field) {
337 - foreach ($woo_field as $field => $field_data) {
338 - if (isset($field_data['label'])) {
339 - $label = $field_data['label'];
340 - } else {
341 - $label = ucfirst( str_replace('_', ' ', $field) );
342 - }
343 -
344 - $merge_tags[$woo_field_key][] = array(
345 - 'id' => 'woo_order_' . $field,
346 - 'label' => $label,
347 - 'preview_value' => isset($field_data['preview']) ? $field_data['preview'] : '',
348 - 'return_type' => isset($field_data['return_type']) ? $field_data['return_type'] : 'text',
349 - 'value' => function ($args) use ($field, $field_data) {
350 - $order = wc_get_order( $args['object_id'] );
351 - $field_function = 'get_' . $field;
352 - if (isset($field_data['value'])) {
353 - $value = $field_data['value']($order, $field_function);
354 - } else {
355 - $value = $order->$field_function();
356 - }
357 - return html_entity_decode(sanitize_text_field($value));
358 - }
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,
359 555 );
360 556 }
361 557 }
362 558
363 - // Order meta keys
364 - $order_meta_keys = Notifier_Notification_Merge_Tags::get_custom_meta_keys('shop_order');
365 - if(!empty($order_meta_keys)){
366 - foreach($order_meta_keys as $order_meta_key){
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 ) {
367 563 $merge_tags['WooCommerce Order Custom Meta'][] = array(
368 - 'id' => 'woo_order_meta_' . $order_meta_key,
369 - 'label' => $order_meta_key,
564 + 'id' => 'woo_order_meta_' . $order_meta_key,
565 + 'label' => $order_meta_key,
370 566 'preview_value' => '123',
371 - 'return_type' => 'all',
372 - 'value' => function ($args) use ($order_meta_key) {
373 - $order = wc_get_order( $args['object_id'] );
374 - $value = $order->get_meta( $order_meta_key );
375 - if(is_array($value) || is_object($value)){
376 - $value = json_encode($value);
377 - }
378 - return (string) $value;
379 - }
567 + 'return_type' => 'all',
568 + 'source' => 'meta',
569 + 'path' => $order_meta_key,
380 570 );
381 571 }
382 572 }
383 573
384 - // WooCommerce customer user meta
574 + // WooCommerce customer user meta — read from persistent option.
385 575 $user_meta_keys = Notifier_Notification_Merge_Tags::get_user_meta_keys();
386 - if(!empty($user_meta_keys)){
387 - foreach($user_meta_keys as $user_meta_key){
576 + if ( ! empty( $user_meta_keys ) ) {
577 + foreach ( $user_meta_keys as $user_meta_key ) {
388 578 $merge_tags['WooCommerce Customer User Meta'][] = array(
389 - 'id' => 'woo_customer_meta_' . $user_meta_key,
390 - 'label' => $user_meta_key,
579 + 'id' => 'woo_customer_meta_' . $user_meta_key,
580 + 'label' => $user_meta_key,
391 581 'preview_value' => '123',
392 - 'return_type' => 'all',
393 - 'value' => function ($args) use ($user_meta_key) {
394 - $order = wc_get_order( $args['object_id'] );
395 - $user_id = $order->get_user_id();
396 - if(0 == $user_id){
397 - return '';
398 - }
399 -
400 - $value = get_user_meta($user_id, $user_meta_key, true);
401 - if(is_array($value) || is_object($value)){
402 - $value = json_encode($value);
403 - }
404 - return (string) $value;
405 - }
582 + 'return_type' => 'all',
583 + 'source' => 'user_meta',
584 + 'path' => $user_meta_key,
406 585 );
407 586 }
408 587 }
409 588
@@ -409,9 +588,9 @@
409 588
410 589 $merge_tags['WooCommerce Cart Abandonment'] = array(
411 590 array(
412 591 'id' => 'wca_cart_products_list',
413 - 'label' => 'Adandoned cart products list',
592 + 'label' => 'Abandoned cart products list',
414 593 'preview_value' => '',
415 594 'return_type' => 'text',
416 595 'value' => function ($cart_details) {
417 596 return $cart_details ? self::get_comma_separated_products(maybe_unserialize($cart_details['cart_data'])) : '';
@@ -416,20 +595,20 @@
416 595 'value' => function ($cart_details) {
417 596 return $cart_details ? self::get_comma_separated_products(maybe_unserialize($cart_details['cart_data'])) : '';
418 597 }
419 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 + ),
420 608 array(
421 - 'id' => 'wca_cart_total',
422 - 'label' => 'Adandoned cart total',
423 - 'preview_value' => '',
424 - 'return_type' => 'text',
425 - 'value' => function ($cart_details) {
426 - return $cart_entry['cart_total'] ?? '';
427 - }
428 - ),
429 - array(
430 609 'id' => 'wca_cart_datetime',
431 - 'label' => 'Adandoned cart datetime',
610 + 'label' => 'Abandoned cart datetime',
432 611 'preview_value' => '',
433 612 'return_type' => 'text',
434 613 'value' => function ($cart_details) {
435 614 return $cart_details ? $cart_details['created_time'] : '';
@@ -465,8 +644,21 @@
465 644 return wc_get_cart_url();
466 645 }
467 646 ),
468 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(
469 661 'id' => 'wca_cart_customer_email',
470 662 'label' => 'Billing email',
471 663 'preview_value' => '',
472 664 'return_type' => 'text',
@@ -480,11 +672,11 @@
480 672 'label' => 'Coupon details',
481 673 'preview_value' => '',
482 674 'return_type' => 'text',
483 675 'value' => function ($cart_details) {
484 - $coupon_data = $data ? maybe_unserialize($cart_details['coupon_content']) : [];
485 - return $coupon_data ? implode(', ', $coupon_data) : '';
486 - }
676 + $coupon_data = $cart_details ? maybe_unserialize($cart_details['coupon_content'] ?? '') : [];
677 + return $coupon_data ? implode(', ', $coupon_data) : '';
678 + }
487 679 ),
488 680 );
489 681
490 682 $wca_other_fields = array(
@@ -687,77 +879,60 @@
687 879
688 880 /*
689 881 * Add recipient fields for WooCommerce
690 882 */
691 - public static function woocommerce_recipient_fields($recipient_fields){
883 + public static function woocommerce_recipient_fields( $recipient_fields ) {
692 884 $recipient_fields['WooCommerce'] = array(
693 885 array(
694 - 'id' => 'woo_order_billing_phone',
695 - 'label' => 'Billing phone number',
696 - 'value' => function ($args) {
697 - $order = wc_get_order( $args['object_id'] );
698 - $phone_number = $order->get_billing_phone();
699 - $country_code = $order->get_billing_country();
700 - $phone_number = self::get_formatted_phone_number($phone_number, $country_code);
701 - return html_entity_decode(sanitize_text_field($phone_number));
702 - }
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 + },
703 895 ),
704 896 array(
705 - 'id' => 'woo_order_shipping_phone',
706 - 'label' => 'Shipping phone number',
707 - 'value' => function ($args) {
708 - $order = wc_get_order( $args['object_id'] );
709 - $phone_number = $order->get_shipping_phone();
710 - $country_code = $order->get_shipping_country();
711 - $phone_number = self::get_formatted_phone_number($phone_number, $country_code);
712 - return html_entity_decode(sanitize_text_field($phone_number));
713 - }
714 - )
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 + ),
715 907 );
716 908
717 - // Order meta keys
718 - $order_meta_keys = Notifier_Notification_Merge_Tags::get_custom_meta_keys('shop_order');
719 - if(!empty($order_meta_keys)){
720 - foreach($order_meta_keys as $order_meta_key){
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 ) {
721 913 $recipient_fields['WooCommerce Order Custom Meta'][] = array(
722 - 'id' => 'woo_order_meta_' . $order_meta_key,
723 - 'label' => $order_meta_key,
914 + 'id' => 'woo_order_meta_' . $order_meta_key,
915 + 'label' => $order_meta_key,
724 916 'preview_value' => '123',
725 - 'return_type' => 'text',
726 - 'value' => function ($args) use ($order_meta_key) {
727 - $order = wc_get_order( $args['object_id'] );
728 - $value = $order->get_meta( $order_meta_key );
729 - if(is_array($value) || is_object($value)){
730 - $value = json_encode($value);
731 - }
732 - return (string) $value;
733 - }
917 + 'return_type' => 'text',
918 + 'source' => 'meta',
919 + 'path' => $order_meta_key,
734 920 );
735 921 }
736 922 }
737 923
738 - // WooCommerce customer user meta
924 + // WooCommerce customer user meta — read from persistent option.
739 925 $user_meta_keys = Notifier_Notification_Merge_Tags::get_user_meta_keys();
740 - if(!empty($user_meta_keys)){
741 - foreach($user_meta_keys as $user_meta_key){
926 + if ( ! empty( $user_meta_keys ) ) {
927 + foreach ( $user_meta_keys as $user_meta_key ) {
742 928 $recipient_fields['WooCommerce Customer User Meta'][] = array(
743 - 'id' => 'woo_customer_meta_' . $user_meta_key,
744 - 'label' => $user_meta_key,
929 + 'id' => 'woo_customer_meta_' . $user_meta_key,
930 + 'label' => $user_meta_key,
745 931 'preview_value' => '123',
746 - 'return_type' => 'text',
747 - 'value' => function ($args) use ($user_meta_key) {
748 - $order = wc_get_order( $args['object_id'] );
749 - $user_id = $order->get_user_id();
750 - if(0 == $user_id){
751 - return '';
752 - }
753 -
754 - $value = get_user_meta($user_id, $user_meta_key, true);
755 - if(is_array($value) || is_object($value)){
756 - $value = json_encode($value);
757 - }
758 - return (string) $value;
759 - }
932 + 'return_type' => 'text',
933 + 'source' => 'user_meta',
934 + 'path' => $user_meta_key,
760 935 );
761 936 }
762 937 }
763 938
@@ -783,8 +958,12 @@
783 958 /**
784 959 * Get phone number with country extension code
785 960 */
786 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 + }
787 966 $phone_number = notifier_sanitize_phone_number( $phone_number );
788 967 $phone_number = ltrim($phone_number, '0');
789 968 if ( in_array( $country_code, array( 'US', 'CA' ), true ) ) {
790 969 $calling_code = '+1';
@@ -821,9 +1000,9 @@
821 1000 /**
822 1001 * Hook to save the custom field data
823 1002 */
824 1003 public static function notifier_save_checkout_field($order_id) {
825 - $opt_in = sanitize_text_field($_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ]);
1004 + $opt_in = isset( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ? sanitize_text_field( wp_unslash( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ) : '';
826 1005 if ( !empty($opt_in) ) {
827 1006 $order = wc_get_order( $order_id );
828 1007 $order->update_meta_data( NOTIFIER_PREFIX . 'whatsapp_opt_in', $opt_in );
829 1008 $order->save();
@@ -829,8 +1008,63 @@
829 1008 $order->save();
830 1009 }
831 1010 }
832 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 +
833 1067 /**
834 1068 * Check if the notification should be sent based on global and user opt-in settings.
835 1069 */
836 1070 public static function maybe_send_notification( $order_id ) {
@@ -850,44 +1084,50 @@
850 1084 check_ajax_referer('notifier_save_cart_abandonment_data', 'security');
851 1085
852 1086 // Check if cart abandonment tracking is enabled
853 1087 if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
854 - wp_send_json(array('error' => true));
1088 + wp_send_json(array('error' => true, 'message' => 'Tracking not enabled.'));
855 1089 }
856 1090
857 1091 global $wpdb;
858 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 +
859 1099 $cart_data = maybe_serialize(WC()->session->get('cart')); // Serialized cart data.
860 - $cart_total = WC()->cart->get_total('edit'); // Cart total amount.
861 - // Fetch applied coupon details
862 - $applied_coupons = WC()->cart->get_applied_coupons();
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
863 1102 $serialized_coupons = maybe_serialize($applied_coupons);
864 1103
865 1104 $session_key = WC()->session->get('notifier_session_key');
866 1105
867 1106 $customer_data = array(
868 - 'billing_first_name' => $_POST['billing_first_name'] ?? WC()->checkout()->get_value('billing_first_name'),
869 - 'billing_last_name' => $_POST['billing_last_name'] ?? WC()->checkout()->get_value('billing_last_name'),
870 - 'billing_company' => $_POST['billing_company'] ?? WC()->checkout()->get_value('billing_company'),
871 - 'billing_address_1' => $_POST['billing_address_1'] ?? WC()->checkout()->get_value('billing_address_1'),
872 - 'billing_address_2' => $_POST['billing_address_2'] ?? WC()->checkout()->get_value('billing_address_2'),
873 - 'billing_city' => $_POST['billing_city'] ?? WC()->checkout()->get_value('billing_city'),
874 - 'billing_postcode' => $_POST['billing_postcode'] ?? WC()->checkout()->get_value('billing_postcode'),
875 - 'billing_state' => $_POST['billing_state'] ?? WC()->checkout()->get_value('billing_state'),
876 - 'billing_country' => $_POST['billing_country'] ?? WC()->checkout()->get_value('billing_country'),
877 - 'billing_phone' => $_POST['billing_phone'] ?? WC()->checkout()->get_value('billing_phone'),
878 - 'billing_email' => $_POST['billing_email'] ?? WC()->checkout()->get_value('billing_email'),
879 - 'shipping_first_name' => $_POST['shipping_first_name'] ?? WC()->checkout()->get_value('shipping_first_name'),
880 - 'shipping_last_name' => $_POST['shipping_last_name'] ?? WC()->checkout()->get_value('shipping_last_name'),
881 - 'shipping_company' => $_POST['shipping_company'] ?? WC()->checkout()->get_value('shipping_company'),
882 - 'shipping_address_1' => $_POST['shipping_address_1'] ?? WC()->checkout()->get_value('shipping_address_1'),
883 - 'shipping_address_2' => $_POST['shipping_address_2'] ?? WC()->checkout()->get_value('shipping_address_2'),
884 - 'shipping_city' => $_POST['shipping_city'] ?? WC()->checkout()->get_value('shipping_city'),
885 - 'shipping_postcode' => $_POST['shipping_postcode'] ?? WC()->checkout()->get_value('shipping_postcode'),
886 - 'shipping_state' => $_POST['shipping_state'] ?? WC()->checkout()->get_value('shipping_state'),
887 - 'shipping_country' => $_POST['shipping_country'] ?? WC()->checkout()->get_value('shipping_country'),
888 - 'shipping_phone' => $_POST['shipping_phone'] ?? WC()->checkout()->get_value('shipping_phone'),
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')),
889 1128 );
1129 +
890 1130 $serialized_customer_data = maybe_serialize($customer_data);
891 1131 $phone_number = sanitize_text_field($customer_data['billing_phone']);
892 1132
893 1133 // Handle case where phone number is missing
@@ -925,11 +1165,30 @@
925 1165 if ($existing_entry && $existing_entry->triggered) {
926 1166 wp_send_json(array('error' => true, 'message' => 'Cart abandonment already processed.' ));
927 1167 }
928 1168
929 - // Determine created_time
930 - $created_time = $existing_entry ? $existing_entry->created_time : current_time('mysql');
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 + );
931 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 +
932 1191 // Update or insert cart and customer data into the table
933 1192 if ($existing_entry) {
934 1193 // Update the existing entry
935 1194 $wpdb->update(
@@ -936,9 +1195,9 @@
936 1195 $table_name,
937 1196 array(
938 1197 'phone_number' => $phone_number,
939 1198 'cart_data' => $cart_data,
940 - 'cart_total' => sanitize_text_field($cart_total),
1199 + 'cart_total' => $cart_total,
941 1200 'coupon_data' => $serialized_coupons,
942 1201 'customer_data' => $serialized_customer_data,
943 1202 'updated_time' => current_time('mysql'),
944 1203 ),
@@ -953,9 +1212,9 @@
953 1212 array(
954 1213 'session_key' => sanitize_key($session_key),
955 1214 'phone_number' => $phone_number,
956 1215 'cart_data' => $cart_data,
957 - 'cart_total' => sanitize_text_field($cart_total),
1216 + 'cart_total' => $cart_total,
958 1217 'coupon_data' => $serialized_coupons,
959 1218 'customer_data' => $serialized_customer_data,
960 1219 'created_time' => current_time('mysql'),
961 1220 'updated_time' => current_time('mysql'),
@@ -972,8 +1231,10 @@
972 1231 $run_time_minutes = (int) get_option('notifier_abandonment_cart_cron_run_time', 20); // Default to 20 minutes if not set
973 1232 $run_time_seconds = $run_time_minutes * MINUTE_IN_SECONDS;
974 1233 as_schedule_single_action(time() + $run_time_seconds, 'notifier_check_cart_abandonment', array($session_key));
975 1234 }
1235 +
1236 + wp_send_json(array('error' => false, 'message' => 'Cart abandonment request has been successfully processed. ' ));
976 1237 }
977 1238
978 1239 /**
979 1240 * Handle cart updates data updates.
@@ -983,18 +1244,21 @@
983 1244 if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
984 1245 return;
985 1246 }
986 1247
987 - $current_url = $_SERVER['REQUEST_URI'];
988 1248 // Prevent execution on the Thank You and order recieved page
989 - if (is_checkout() || strpos($current_url, '/order-received/') !== false) {
1249 + if (is_checkout() || is_wc_endpoint_url('order-received')) {
990 1250 return;
991 1251 }
992 1252
1253 + if (!WC()->session || !WC()->session->has_session()) {
1254 + return;
1255 + }
1256 +
993 1257 global $wpdb;
994 1258 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
995 1259 $cart_data = maybe_serialize(WC()->session->get('cart')); // Serialized cart data.
996 - $cart_total = WC()->cart->get_total('edit'); // Cart total amount.
1260 + $cart_total = floatval(preg_replace('/[^\d.]/', '', WC()->cart->get_total('edit'))); // Cart total amount.
997 1261
998 1262 $session_key = WC()->session->get('notifier_session_key');
999 1263
1000 1264 //Check if an order is already linked to this session record
@@ -1066,8 +1330,13 @@
1066 1330 /**
1067 1331 * Fire when cart data get updated
1068 1332 */
1069 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 +
1070 1339 global $wpdb;
1071 1340 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1072 1341
1073 1342 // Check if the session data still exists in the DB
@@ -1119,12 +1388,19 @@
1119 1388 /**
1120 1389 * retrive cart data and set from url param
1121 1390 */
1122 1391 public static function retrive_cart_data_from_url(){
1392 + if(!is_checkout() && !is_cart()){
1393 + return;
1394 + }
1395 +
1123 1396 if (isset($_GET['wano_session'])) {
1124 1397 global $wpdb;
1125 1398 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1126 1399 $session_key = sanitize_key($_GET['wano_session']);
1400 + if (empty($session_key)) {
1401 + return;
1402 + }
1127 1403
1128 1404 $cart_entry = $wpdb->get_row(
1129 1405 $wpdb->prepare("SELECT cart_data, customer_data FROM $table_name WHERE session_key = %s", $session_key),
1130 1406 ARRAY_A
@@ -1139,68 +1415,544 @@
1139 1415 }
1140 1416 }
1141 1417 }
1142 1418
1419 + // ========================================
1420 + // v3 PAYLOAD BUILDER
1421 + // ========================================
1422 +
1143 1423 /**
1144 - * store session id during order creation
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.
1145 1433 */
1146 - public static function notifier_save_session_on_new_order($order_id) {
1147 - // Fetch specific session key
1148 - $session_key = WC()->session->get('notifier_session_key');
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 + }
1149 1439
1150 - if (!empty($session_key)) {
1151 - // Store the session key in the order meta
1152 - update_post_meta($order_id, '_notifier_abandonment_session_key', sanitize_key($session_key));
1153 - }
1154 - }
1440 + $customer_id = $order->get_customer_id();
1155 1441
1156 - /**
1157 - * Update cart abandonment data when the order status changes.
1158 - */
1159 - public static function update_abandonment_cart_on_order_status($order_id, $old_status, $new_status) {
1160 - $acceptable_order_statuses = array('processing', 'completed', 'pending');
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 + );
1161 1453
1162 - if (in_array($new_status, $acceptable_order_statuses, true)) {
1163 - $order = wc_get_order($order_id);
1164 - $session_key = get_post_meta($order_id, '_notifier_abandonment_session_key', true);
1165 - if (!empty($session_key) && $order) {
1166 - global $wpdb;
1167 - $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
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 + }
1168 1461
1169 - // Mark the entry as triggered (completed order) in the abandonment cart table.
1170 - $wpdb->update(
1171 - $table_name,
1172 - array('order_id' => $order_id), // Update the order_id
1173 - array('session_key' => sanitize_key($session_key)), // Where condition
1174 - array('%d'), // Data type for order_id
1175 - array('%s') // Data type for session_key
1176 - );
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 + }
1177 1467
1178 - WC()->session->set( 'notifier_session_key', null );
1179 - as_unschedule_all_actions('notifier_check_cart_abandonment', array(sanitize_key($session_key)));
1180 - }
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;
1181 1936 }
1182 - }
1183 1937
1184 - /**
1185 - * Update the order ID in the abandonment cart table when an order is created.
1186 - */
1187 - public static function update_cart_abandonment_order_data($order_id) {
1188 - $order = wc_get_order($order_id);
1189 - $session_key = get_post_meta($order_id, '_notifier_abandonment_session_key', true);
1190 - if (!empty($session_key) && $order) {
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)) {
1191 1946 global $wpdb;
1192 1947 $table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
1193 -
1194 - // Update the order_id in the abandonment cart table.
1195 1948 $wpdb->update(
1196 1949 $table_name,
1197 - array('order_id' => $order_id),
1198 - array('session_key' => sanitize_key($session_key)),
1199 - array('%d'),
1200 - array('%s')
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
1201 1954 );
1202 - WC()->session->set( 'notifier_session_key', null );
1203 - as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
1955 + as_unschedule_all_actions('notifier_check_cart_abandonment', array(sanitize_key($session_key)));
1204 1956 }
1205 1957 }
1206 1958 }