'woo_order_new',
'label' => 'New order is placed',
'description' => 'Trigger notification when a new order is placed.',
'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
'action' => array(
'hook' => 'woocommerce_new_order',
'callback' => function ( $order_id ){
if (!self::maybe_send_notification($order_id)) {
return;
}
$args = array (
'object_type' => 'order',
'object_id' => $order_id
);
Notifier_Notification_Triggers::send_trigger_request('woo_order_new', $args);
}
)
),
array(
'id' => 'woo_order_new_cod',
'label' => 'New order is placed with COD payment method',
'description' => 'Trigger notification when a new order is placed with Cash on Delivery payment method selected.',
'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
'action' => array(
'hook' => 'woocommerce_new_order',
'callback' => function ( $order_id ){
if (!self::maybe_send_notification($order_id)) {
return;
}
$args = array (
'object_type' => 'order',
'object_id' => $order_id
);
$order = new WC_Order( $order_id );
$method = $order->get_payment_method();
if('cod' != $method){
return;
}
Notifier_Notification_Triggers::send_trigger_request('woo_order_new_cod', $args);
}
)
),
array(
'id' => 'woocommerce_cart_abandonment',
'label' => 'Cart is abandoned',
'description' => 'Trigger notification when cart is abandoned as per your setup in the Settings > WooCommerce tab.',
'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags(array('WooCommerce', 'WooCommerce Cart Abandonment')),
'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields(array('WooCommerce Cart Abandonment')),
'action' => array(
'hook' => 'notifier_process_abandoned_cart',
'callback' => function ( $checkout_details ) {
$checkout_details = (array) $checkout_details;
Notifier_Notification_Triggers::send_trigger_request( 'woocommerce_cart_abandonment', $checkout_details );
}
)
),
);
$statuses = wc_get_order_statuses();
foreach($statuses as $key => $status){
$status_slug = str_replace('wc-','', $key);
if(in_array($status_slug, array('checkout-draft'))){
continue;
}
$trigger_id = 'woo_order_' . $status_slug;
$triggers[] = array(
'id' => $trigger_id,
'label' => 'Order status changes to ' . $status,
'description' => 'Trigger notification when status of an order changes to ' . $status . '.',
'merge_tags' => Notifier_Notification_Merge_Tags::get_merge_tags($merge_tag_types),
'recipient_fields' => Notifier_Notification_Merge_Tags::get_recipient_fields($recipient_types),
'action' => array(
'hook' => 'woocommerce_order_status_' . $status_slug,
'callback' => function ( $order_id ) use ($trigger_id) {
if (!self::maybe_send_notification($order_id)) {
return;
}
$args = array (
'object_type' => 'order',
'object_id' => $order_id
);
Notifier_Notification_Triggers::send_trigger_request($trigger_id, $args);
}
)
);
}
return $triggers;
}
/**
* Add notification trigger Woocommerce
*/
public static function add_notification_triggers ($triggers) {
$triggers['WooCommerce'] = self::get_woo_notification_triggers();
return $triggers;
}
/**
* Get Woocommerce merge tags
*/
public static function woocommerce_merge_tags($merge_tags) {
$merge_tags['WooCommerce'] = array(
array(
'id' => 'woo_store_url',
'label' => 'Shop page url',
'preview_value' => 'https://example.com/shop/',
'return_type' => 'text',
'source' => 'static',
'value' => function ( $bulk_data ) {
return (string) get_permalink( wc_get_page_id( 'shop' ) );
}
),
array(
'id' => 'woo_my_account_url',
'label' => 'My Account page url',
'preview_value' => 'https://example.com/my-account/',
'return_type' => 'text',
'source' => 'static',
'value' => function ( $bulk_data ) {
return (string) get_permalink( wc_get_page_id( 'myaccount' ) );
}
),
);
// WooCommerce Order fields — source/path maps directly to get_base_data() keys.
// Fields with custom formatting use a 'value' closure receiving $bulk_data.
$woo_fields = array();
$woo_fields['WooCommerce Order'] = array(
'id' => array(
'preview' => '123',
'label' => 'Order ID',
'source' => 'base',
'path' => 'id',
),
'subtotal_to_display' => array(
'preview' => '$50.00',
'label' => 'Order subtotal',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return html_entity_decode( $bulk_data['order']->get_subtotal_to_display() );
},
),
'coupon_codes' => array(
'preview' => 'OFF50',
'label' => 'Order coupon codes',
'source' => 'coupons',
'value' => function ( $bulk_data ) {
$codes = array();
foreach ( $bulk_data['coupons'] as $item ) {
$codes[] = $item->get_code();
}
return implode( ', ', $codes );
},
),
'discount_to_display' => array(
'preview' => '$5.00',
'label' => 'Order discount amount',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return html_entity_decode( $bulk_data['order']->get_discount_to_display() );
},
),
'shipping_method' => array(
'preview' => 'Flat rate',
'label' => 'Order shipping method',
'source' => 'shipping',
'value' => function ( $bulk_data ) {
$methods = array();
foreach ( $bulk_data['shipping'] as $item ) {
$methods[] = $item->get_method_title();
}
return implode( ', ', $methods );
},
),
'shipping_total' => array(
'preview' => '$2.50',
'label' => 'Order shipping amount',
'source' => 'base',
'path' => 'shipping_total',
'value' => function ( $bulk_data ) {
$currency = $bulk_data['base']['currency'];
return html_entity_decode( wc_price( $bulk_data['base']['shipping_total'], array( 'currency' => $currency ) ) );
},
),
'total_tax' => array(
'preview' => '$5.00',
'label' => 'Order tax amount',
'source' => 'base',
'path' => 'total_tax',
'value' => function ( $bulk_data ) {
$currency = $bulk_data['base']['currency'];
return html_entity_decode( wc_price( $bulk_data['base']['total_tax'], array( 'currency' => $currency ) ) );
},
),
'formatted_order_total' => array(
'preview' => '$50.00',
'label' => 'Order total amount',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return html_entity_decode( $bulk_data['order']->get_formatted_order_total() );
},
),
'payment_method_title' => array(
'preview' => 'Cash on delivery',
'label' => 'Order payment method',
'source' => 'base',
'path' => 'payment_method_title',
),
'checkout_payment_url' => array(
'preview' => 'https://example.com/checkout/order-received/123/?key=wc_order_abcdef',
'label' => 'Order checkout payment URL',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return $bulk_data['order']->get_checkout_payment_url();
},
),
'checkout_order_received_url' => array(
'preview' => 'https://example.com/checkout/order-received/123/?key=wc_order_abcdef',
'label' => 'Order thank you page URL',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return $bulk_data['order']->get_checkout_order_received_url();
},
),
'view_order_url' => array(
'preview' => 'https://example.com/my-account/view-order/123/',
'label' => 'Order view URL',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return $bulk_data['order']->get_view_order_url();
},
),
'status' => array(
'preview' => 'Processing',
'label' => 'Order status',
'source' => 'base',
'path' => 'status',
),
'first_product_image' => array(
'preview' => '',
'label' => 'Order first item featured image',
'return_type' => 'image',
'source' => 'items',
'value' => function ( $bulk_data ) {
foreach ( $bulk_data['items'] as $item ) {
$product = $item->get_product();
if ( $product ) {
$image_id = $product->get_image_id();
return $image_id ? wp_get_attachment_url( $image_id ) : wc_placeholder_img_src();
}
break;
}
return wc_placeholder_img_src();
},
),
'products_list' => array(
'preview' => '',
'label' => 'Order products list',
'return_type' => 'text',
'source' => 'items',
'value' => function ( $bulk_data ) {
$parts = array();
$currency = $bulk_data['base']['currency'];
foreach ( $bulk_data['items'] as $item ) {
$parts[] = sanitize_textarea_field(
$item->get_name() . ' x ' . $item->get_quantity() . ' (' . wc_price( $item->get_total(), array( 'currency' => $currency ) ) . ')'
);
}
return implode( ', ', $parts );
},
),
'order_first_item_name' => array(
'preview' => '',
'label' => 'Order first item name',
'return_type' => 'text',
'source' => 'items',
'value' => function ( $bulk_data ) {
foreach ( $bulk_data['items'] as $item ) {
return $item->get_name();
}
return '';
},
),
'order_first_item_price' => array(
'preview' => '',
'label' => 'Order first item price',
'return_type' => 'text',
'source' => 'items',
'value' => function ( $bulk_data ) {
$currency = $bulk_data['base']['currency'];
foreach ( $bulk_data['items'] as $item ) {
$product = $item->get_product();
if ( $product ) {
return html_entity_decode( wc_price( $product->get_price(), array( 'currency' => $currency ) ) );
}
break;
}
return '';
},
),
'order_first_item_total' => array(
'preview' => '',
'label' => 'Order first item total',
'return_type' => 'text',
'source' => 'items',
'value' => function ( $bulk_data ) {
$currency = $bulk_data['base']['currency'];
foreach ( $bulk_data['items'] as $item ) {
return html_entity_decode( wc_price( $item->get_total(), array( 'currency' => $currency ) ) );
}
return '';
},
),
);
$woo_fields['WooCommerce Customer'] = array(
'billing_first_name' => array(
'preview' => 'John',
'source' => 'base',
'path' => 'billing.first_name',
),
'billing_last_name' => array(
'preview' => 'Doe',
'source' => 'base',
'path' => 'billing.last_name',
),
'billing_company' => array(
'preview' => 'ABC Inc',
'source' => 'base',
'path' => 'billing.company',
),
'billing_address_1' => array(
'preview' => '123, XYZ Street',
'source' => 'base',
'path' => 'billing.address_1',
),
'billing_address_2' => array(
'preview' => 'XYZ Avenue',
'source' => 'base',
'path' => 'billing.address_2',
),
'billing_city' => array(
'preview' => 'New York',
'source' => 'base',
'path' => 'billing.city',
),
'billing_postcode' => array(
'preview' => '12345',
'source' => 'base',
'path' => 'billing.postcode',
),
'billing_state' => array(
'preview' => 'NY',
'source' => 'base',
'path' => 'billing.state',
'value' => function ( $bulk_data ) {
$country = $bulk_data['base']['billing']['country'];
$state = $bulk_data['base']['billing']['state'];
return WC()->countries->states[ $country ][ $state ] ?? $state;
},
),
'billing_country' => array(
'preview' => 'US',
'source' => 'base',
'path' => 'billing.country',
'value' => function ( $bulk_data ) {
$code = $bulk_data['base']['billing']['country'];
return WC()->countries->countries[ $code ] ?? $code;
},
),
'billing_email' => array(
'preview' => 'john@example.com',
'source' => 'base',
'path' => 'billing.email',
),
'billing_phone' => array(
'preview' => '987 654 321',
'source' => 'base',
'path' => 'billing.phone',
'value' => function ( $bulk_data ) {
$phone = $bulk_data['base']['billing']['phone'] ?? '';
$country = $bulk_data['base']['billing']['country'] ?? '';
return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
},
),
'formatted_billing_address' => array(
'preview' => '',
'label' => 'Complete billing address',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return str_replace( '
', ', ', $bulk_data['order']->get_formatted_billing_address() );
},
),
'shipping_first_name' => array(
'preview' => 'John',
'source' => 'base',
'path' => 'shipping.first_name',
),
'shipping_last_name' => array(
'preview' => 'Doe',
'source' => 'base',
'path' => 'shipping.last_name',
),
'shipping_company' => array(
'preview' => 'ABC Inc',
'source' => 'base',
'path' => 'shipping.company',
),
'shipping_address_1' => array(
'preview' => '123, XYZ Street',
'source' => 'base',
'path' => 'shipping.address_1',
),
'shipping_address_2' => array(
'preview' => 'XYZ Avenue',
'source' => 'base',
'path' => 'shipping.address_2',
),
'shipping_city' => array(
'preview' => 'New York',
'source' => 'base',
'path' => 'shipping.city',
),
'shipping_postcode' => array(
'preview' => '12345',
'source' => 'base',
'path' => 'shipping.postcode',
),
'shipping_state' => array(
'preview' => 'NY',
'source' => 'base',
'path' => 'shipping.state',
'value' => function ( $bulk_data ) {
$country = $bulk_data['base']['shipping']['country'];
$state = $bulk_data['base']['shipping']['state'];
return WC()->countries->states[ $country ][ $state ] ?? $state;
},
),
'shipping_country' => array(
'preview' => 'US',
'source' => 'base',
'path' => 'shipping.country',
'value' => function ( $bulk_data ) {
$code = $bulk_data['base']['shipping']['country'];
return WC()->countries->countries[ $code ] ?? $code;
},
),
'shipping_phone' => array(
'preview' => '987 654 321',
'source' => 'base',
'path' => 'shipping.phone',
'value' => function ( $bulk_data ) {
$phone = $bulk_data['base']['shipping']['phone'] ?? '';
$country = $bulk_data['base']['shipping']['country'] ?? '';
return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
},
),
'formatted_shipping_address' => array(
'preview' => '',
'label' => 'Complete shipping address',
'source' => 'computed',
'value' => function ( $bulk_data ) {
return str_replace( '
', ', ', $bulk_data['order']->get_formatted_shipping_address() );
},
),
);
foreach ( $woo_fields as $woo_field_key => $woo_field ) {
foreach ( $woo_field as $field => $field_data ) {
$label = isset( $field_data['label'] ) ? $field_data['label'] : ucfirst( str_replace( '_', ' ', $field ) );
$merge_tags[ $woo_field_key ][] = array(
'id' => 'woo_order_' . $field,
'label' => $label,
'preview_value' => isset( $field_data['preview'] ) ? $field_data['preview'] : '',
'return_type' => isset( $field_data['return_type'] ) ? $field_data['return_type'] : 'text',
'source' => isset( $field_data['source'] ) ? $field_data['source'] : 'base',
'path' => isset( $field_data['path'] ) ? $field_data['path'] : null,
'value' => isset( $field_data['value'] ) ? $field_data['value'] : null,
);
}
}
// Order meta keys — read from persistent option (populated on connect/sync).
$order_meta_keys = Notifier_Notification_Merge_Tags::get_custom_meta_keys( 'shop_order' );
if ( ! empty( $order_meta_keys ) ) {
foreach ( $order_meta_keys as $order_meta_key ) {
$merge_tags['WooCommerce Order Custom Meta'][] = array(
'id' => 'woo_order_meta_' . $order_meta_key,
'label' => $order_meta_key,
'preview_value' => '123',
'return_type' => 'all',
'source' => 'meta',
'path' => $order_meta_key,
);
}
}
// WooCommerce customer user meta — read from persistent option.
$user_meta_keys = Notifier_Notification_Merge_Tags::get_user_meta_keys();
if ( ! empty( $user_meta_keys ) ) {
foreach ( $user_meta_keys as $user_meta_key ) {
$merge_tags['WooCommerce Customer User Meta'][] = array(
'id' => 'woo_customer_meta_' . $user_meta_key,
'label' => $user_meta_key,
'preview_value' => '123',
'return_type' => 'all',
'source' => 'user_meta',
'path' => $user_meta_key,
);
}
}
$merge_tags['WooCommerce Cart Abandonment'] = array(
array(
'id' => 'wca_cart_products_list',
'label' => 'Abandoned cart products list',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
return $cart_details ? self::get_comma_separated_products(maybe_unserialize($cart_details['cart_data'])) : '';
}
),
array(
'id' => 'wca_cart_total',
'label' => 'Abandoned cart total',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
return $cart_details['cart_total'] ?? '';
}
),
array(
'id' => 'wca_cart_datetime',
'label' => 'Abandoned cart datetime',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
return $cart_details ? $cart_details['created_time'] : '';
}
),
array(
'id' => 'wca_checkout_recovery_url',
'label' => 'Abandoned cart checkout recovery URL',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
if ($cart_details && isset($cart_details['session_key'])) {
$session_key = sanitize_key($cart_details['session_key']);
$checkout_url = wc_get_checkout_url();
$recovery_url = add_query_arg(['wano_session' => $session_key], $checkout_url);
return esc_url($recovery_url);
}
return wc_get_checkout_url();
}
),
array(
'id' => 'wca_cart_recovery_url',
'label' => 'Abandoned cart recovery URL',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
if ($cart_details && isset($cart_details['session_key'])) {
$session_key = sanitize_key($cart_details['session_key']);
$cart_url = wc_get_cart_url();
$recovery_url = add_query_arg(['wano_session' => $session_key], $cart_url);
return esc_url($recovery_url);
}
return wc_get_cart_url();
}
),
array(
'id' => 'wca_cart_session_key',
'label' => 'Abandoned cart recovery key',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
if ($cart_details && isset($cart_details['session_key'])) {
$session_key = sanitize_key($cart_details['session_key']);
return $session_key;
}
return '';
}
),
array(
'id' => 'wca_cart_customer_email',
'label' => 'Billing email',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_content']) : [];
return $customer_data['billing_email'] ?? '';
}
),
array(
'id' => 'wca_cart_applied_coupon',
'label' => 'Coupon details',
'preview_value' => '',
'return_type' => 'text',
'value' => function ($cart_details) {
$coupon_data = $cart_details ? maybe_unserialize($cart_details['coupon_content'] ?? '') : [];
return $coupon_data ? implode(', ', $coupon_data) : '';
}
),
);
$wca_other_fields = array(
'first_name' => array(
'preview' => 'John',
'label' => 'Billing first name',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_first_name'] ?? '';
}
),
'last_name' => array(
'preview' => 'Doe',
'label' => 'Billing last name',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_last_name'] ?? '';
}
),
'billing_company' => array(
'preview' => 'ABC Inc',
'label' => 'Billing company',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_company'] ?? '';
}
),
'billing_address_1' => array(
'preview' => '123, XYZ Street',
'label' => 'Billing address line 1',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_address_1'] ?? '';
}
),
'billing_address_2' => array(
'preview' => 'XYZ Avenue',
'label' => 'Billing address line 2',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_address_2'] ?? '';
}
),
'billing_city' => array(
'preview' => 'New York',
'label' => 'Billing city',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_city'] ?? '';
}
),
'billing_postcode' => array(
'preview' => '12345',
'label' => 'Billing postcode',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_postcode'] ?? '';
}
),
'billing_state' => array(
'preview' => 'NY',
'label' => 'Billing state',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
$state_code = $customer_data['billing_state'] ?? '';
$country_code = $customer_data['billing_country'] ?? '';
return $state_code && $country_code
? WC()->countries->states[$country_code][$state_code] ?? ''
: '';
}
),
'billing_country' => array(
'preview' => 'US',
'label' => 'Billing country',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
$country_code = $customer_data['billing_country'] ?? '';
return $country_code
? WC()->countries->countries[$country_code] ?? ''
: '';
}
),
'billing_phone' => array(
'preview' => '987 654 321',
'label' => 'Billing phone number',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_phone'] ?? '';
}
),
'billing_email' => array(
'preview' => 'john@example.com',
'label' => 'Billing email',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['billing_email'] ?? '';
}
),
'shipping_first_name' => array(
'preview' => 'John',
'label' => 'Shipping first name',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_first_name'] ?? '';
}
),
'shipping_last_name' => array(
'preview' => 'Doe',
'label' => 'Shipping last name',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_last_name'] ?? '';
}
),
'shipping_company' => array(
'preview' => 'ABC Inc',
'label' => 'Shipping company',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_company'] ?? '';
}
),
'shipping_address_1' => array(
'preview' => '123, XYZ Street',
'label' => 'Shipping address line 1',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_address_1'] ?? '';
}
),
'shipping_address_2' => array(
'preview' => 'XYZ Avenue',
'label' => 'Shipping address line 2',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_address_2'] ?? '';
}
),
'shipping_city' => array(
'preview' => 'New York',
'label' => 'Shipping city',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_city'] ?? '';
}
),
'shipping_postcode' => array(
'preview' => '12345',
'label' => 'Shipping postcode',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_postcode'] ?? '';
}
),
'shipping_state' => array(
'preview' => 'NY',
'label' => 'Shipping state',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
$state_code = $customer_data['shipping_state'] ?? '';
$country_code = $customer_data['shipping_country'] ?? '';
return $state_code && $country_code
? WC()->countries->states[$country_code][$state_code] ?? ''
: '';
}
),
'shipping_country' => array(
'preview' => 'US',
'label' => 'Shipping country',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
$country_code = $customer_data['shipping_country'] ?? '';
return $country_code
? WC()->countries->countries[$country_code] ?? ''
: '';
}
),
'shipping_phone' => array(
'preview' => '987 654 321',
'label' => 'Shipping phone number',
'value' => function ($cart_details) {
$customer_data = $cart_details ? maybe_unserialize($cart_details['customer_data']) : [];
return $customer_data['shipping_phone'] ?? '';
}
),
);
foreach ($wca_other_fields as $field => $field_data) {
$merge_tags['WooCommerce Cart Abandonment'][] = array(
'id' => 'wca_cart_customer_' . $field,
'label' => $field_data['label'],
'preview_value' => $field_data['preview'],
'return_type' => 'text',
'value' => $field_data['value']
);
}
return $merge_tags;
}
/*
* Add recipient fields for WooCommerce
*/
public static function woocommerce_recipient_fields( $recipient_fields ) {
$recipient_fields['WooCommerce'] = array(
array(
'id' => 'woo_order_billing_phone',
'label' => 'Billing phone number',
'source' => 'base',
'path' => 'billing.phone',
'value' => function ( $bulk_data ) {
$phone = $bulk_data['base']['billing']['phone'];
$country = $bulk_data['base']['billing']['country'];
return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
},
),
array(
'id' => 'woo_order_shipping_phone',
'label' => 'Shipping phone number',
'source' => 'base',
'path' => 'shipping.phone',
'value' => function ( $bulk_data ) {
$phone = $bulk_data['base']['shipping']['phone'];
$country = $bulk_data['base']['shipping']['country'];
return html_entity_decode( sanitize_text_field( self::get_formatted_phone_number( $phone, $country ) ) );
},
),
);
// Order meta keys — read from persistent option.
$order_meta_keys = Notifier_Notification_Merge_Tags::get_custom_meta_keys( 'shop_order' );
if ( ! empty( $order_meta_keys ) ) {
foreach ( $order_meta_keys as $order_meta_key ) {
$recipient_fields['WooCommerce Order Custom Meta'][] = array(
'id' => 'woo_order_meta_' . $order_meta_key,
'label' => $order_meta_key,
'preview_value' => '123',
'return_type' => 'text',
'source' => 'meta',
'path' => $order_meta_key,
);
}
}
// WooCommerce customer user meta — read from persistent option.
$user_meta_keys = Notifier_Notification_Merge_Tags::get_user_meta_keys();
if ( ! empty( $user_meta_keys ) ) {
foreach ( $user_meta_keys as $user_meta_key ) {
$recipient_fields['WooCommerce Customer User Meta'][] = array(
'id' => 'woo_customer_meta_' . $user_meta_key,
'label' => $user_meta_key,
'preview_value' => '123',
'return_type' => 'text',
'source' => 'user_meta',
'path' => $user_meta_key,
);
}
}
$recipient_fields['WooCommerce Cart Abandonment'] = array(
array(
'id' => 'wca_cart_customer_billing_phone',
'label' => 'Customer billing phone number',
'value' => function ($cart_details) {
if(empty($cart_details['phone_number'])){
return '';
}
$customer_data = !empty($cart_details['customer_data']) ? maybe_unserialize($cart_details['customer_data']) : [];
$country_code = $customer_data['billing_country'] ?? '';
$phone_number = self::get_formatted_phone_number($cart_details['phone_number'], $country_code);
return html_entity_decode(sanitize_text_field($phone_number));
}
)
);
return $recipient_fields;
}
/**
* Get phone number with country extension code
*/
public static function get_formatted_phone_number( $phone_number, $country_code ) {
// If already starts with + and matches international format, return as-is
if ( strpos($phone_number, '+') === 0 && notifier_validate_phone_number($phone_number) ) {
return $phone_number;
}
$phone_number = notifier_sanitize_phone_number( $phone_number );
$phone_number = ltrim($phone_number, '0');
if ( in_array( $country_code, array( 'US', 'CA' ), true ) ) {
$calling_code = '+1';
$phone_number = ltrim( $phone_number, '+1' );
} else {
$calling_code = WC()->countries->get_country_calling_code( $country_code );
$calling_code = is_array( $calling_code ) ? $calling_code[0] : $calling_code;
if ( $calling_code ) {
$phone_number = str_replace( $calling_code, '', preg_replace( '/^0/', '', $phone_number ) );
}
}
$phone_number = ltrim($phone_number, '0');
return $calling_code . $phone_number;
}
/**
* Add checkbox field on checkout page
*/
public static function add_checkout_optin_fields() {
if ('yes' === get_option( NOTIFIER_PREFIX . 'enable_opt_in_checkbox_checkout')) {
$checkbox_text = get_option( NOTIFIER_PREFIX . 'checkout_opt_in_checkbox_text');
if (empty($checkbox_text)) {
$checkbox_text = 'Receive updates on WhatsApp';
}
woocommerce_form_field( NOTIFIER_PREFIX . 'whatsapp_opt_in', array(
'type' => 'checkbox',
'class' => array('form-row-wide'),
'label' => $checkbox_text,
),'');
}
}
/**
* Hook to save the custom field data
*/
public static function notifier_save_checkout_field($order_id) {
$opt_in = isset( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ? sanitize_text_field( wp_unslash( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ) : '';
if ( !empty($opt_in) ) {
$order = wc_get_order( $order_id );
$order->update_meta_data( NOTIFIER_PREFIX . 'whatsapp_opt_in', $opt_in );
$order->save();
}
}
/**
* Render opt-in checkbox after billing address on order edit page.
* Hook passes a WC_Order object (works with both HPOS and legacy).
*/
public static function render_order_optin_field( $order ) {
if ( 'yes' !== get_option( NOTIFIER_PREFIX . 'enable_opt_in_checkbox_checkout' ) ) {
return;
}
if ( ! is_a( $order, 'WC_Order' ) ) {
$order = wc_get_order( $order->ID );
}
if ( ! $order ) {
return;
}
$checkbox_text = get_option( NOTIFIER_PREFIX . 'checkout_opt_in_checkbox_text' );
if ( empty( $checkbox_text ) ) {
$checkbox_text = 'Receive updates on WhatsApp';
}
$opt_in = $order->get_meta( NOTIFIER_PREFIX . 'whatsapp_opt_in' );
woocommerce_wp_checkbox( array(
'id' => NOTIFIER_PREFIX . 'whatsapp_opt_in',
'label' => '' . __( 'WANotifier', 'notifier' ) . '',
'description' => esc_html( $checkbox_text ),
'value' => $opt_in,
'cbvalue' => '1',
'wrapper_class' => 'form-field-wide',
) );
}
/**
* Hook to save the custom field data on order page
*/
public static function save_admin_order_meta( $post_id, $post ) {
if ( ! isset( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) ), 'woocommerce_save_data' ) ) {
return;
}
if ( ! current_user_can( 'edit_shop_orders' ) ) {
return;
}
$order = wc_get_order( $post_id );
if ( ! $order ) {
return;
}
$opt_in = isset( $_POST[ NOTIFIER_PREFIX . 'whatsapp_opt_in' ] ) ? '1' : '';
$order->update_meta_data( NOTIFIER_PREFIX . 'whatsapp_opt_in', $opt_in );
$order->save();
}
/**
* Check if the notification should be sent based on global and user opt-in settings.
*/
public static function maybe_send_notification( $order_id ) {
if ('yes' !== get_option( NOTIFIER_PREFIX . 'enable_opt_in_checkbox_checkout' )) {
return true;
}
$order = wc_get_order( $order_id );
$opt_in = $order->get_meta( NOTIFIER_PREFIX . 'whatsapp_opt_in' );
return '1' === $opt_in || true === $opt_in;
}
/**
* Handle checkout data updates.
*/
public static function notifier_save_cart_abandonment_data() {
check_ajax_referer('notifier_save_cart_abandonment_data', 'security');
// Check if cart abandonment tracking is enabled
if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
wp_send_json(array('error' => true, 'message' => 'Tracking not enabled.'));
}
global $wpdb;
$table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
// Ensure WooCommerce session is available
if (!WC()->session || !WC()->session->has_session()) {
wp_send_json(array('error' => true, 'message' => 'WooCommerce session not initialized.'));
}
$cart_data = maybe_serialize(WC()->session->get('cart')); // Serialized cart data.
$cart_total = floatval(preg_replace('/[^\d.]/', '', WC()->cart->get_total('edit'))); // Cart total amount.
$applied_coupons = WC()->cart->get_applied_coupons();// Fetch applied coupon details
$serialized_coupons = maybe_serialize($applied_coupons);
$session_key = WC()->session->get('notifier_session_key');
$customer_data = array(
'billing_first_name' => sanitize_text_field($_POST['billing_first_name'] ?? WC()->checkout()->get_value('billing_first_name')),
'billing_last_name' => sanitize_text_field($_POST['billing_last_name'] ?? WC()->checkout()->get_value('billing_last_name')),
'billing_company' => sanitize_text_field($_POST['billing_company'] ?? WC()->checkout()->get_value('billing_company')),
'billing_address_1' => sanitize_textarea_field($_POST['billing_address_1'] ?? WC()->checkout()->get_value('billing_address_1')),
'billing_address_2' => sanitize_textarea_field($_POST['billing_address_2'] ?? WC()->checkout()->get_value('billing_address_2')),
'billing_city' => sanitize_text_field($_POST['billing_city'] ?? WC()->checkout()->get_value('billing_city')),
'billing_postcode' => sanitize_text_field($_POST['billing_postcode'] ?? WC()->checkout()->get_value('billing_postcode')),
'billing_state' => sanitize_text_field($_POST['billing_state'] ?? WC()->checkout()->get_value('billing_state')),
'billing_country' => sanitize_text_field($_POST['billing_country'] ?? WC()->checkout()->get_value('billing_country')),
'billing_phone' => sanitize_text_field($_POST['billing_phone'] ?? WC()->checkout()->get_value('billing_phone')),
'billing_email' => sanitize_email($_POST['billing_email'] ?? WC()->checkout()->get_value('billing_email')),
'shipping_first_name' => sanitize_text_field($_POST['shipping_first_name'] ?? WC()->checkout()->get_value('shipping_first_name')),
'shipping_last_name' => sanitize_text_field($_POST['shipping_last_name'] ?? WC()->checkout()->get_value('shipping_last_name')),
'shipping_company' => sanitize_text_field($_POST['shipping_company'] ?? WC()->checkout()->get_value('shipping_company')),
'shipping_address_1' => sanitize_textarea_field($_POST['shipping_address_1'] ?? WC()->checkout()->get_value('shipping_address_1')),
'shipping_address_2' => sanitize_textarea_field($_POST['shipping_address_2'] ?? WC()->checkout()->get_value('shipping_address_2')),
'shipping_city' => sanitize_text_field($_POST['shipping_city'] ?? WC()->checkout()->get_value('shipping_city')),
'shipping_postcode' => sanitize_text_field($_POST['shipping_postcode'] ?? WC()->checkout()->get_value('shipping_postcode')),
'shipping_state' => sanitize_text_field($_POST['shipping_state'] ?? WC()->checkout()->get_value('shipping_state')),
'shipping_country' => sanitize_text_field($_POST['shipping_country'] ?? WC()->checkout()->get_value('shipping_country')),
'shipping_phone' => sanitize_text_field($_POST['shipping_phone'] ?? WC()->checkout()->get_value('shipping_phone')),
);
$serialized_customer_data = maybe_serialize($customer_data);
$phone_number = sanitize_text_field($customer_data['billing_phone']);
// Handle case where phone number is missing
if (empty($phone_number)) {
if($session_key){
as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
$wpdb->delete($table_name, array('session_key' => sanitize_key($session_key)), array('%s'));
WC()->session->set( 'notifier_session_key', null );
}
wp_send_json(array('error' => true, 'message' => 'phone number is empty.'));
}
if (WC()->cart->is_empty()) {
if($session_key){
as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
$wpdb->delete($table_name, array('session_key' => sanitize_key($session_key)), array('%s'));
WC()->session->set( 'notifier_session_key', null );
}
wp_send_json(array('error' => true, 'message' => 'cart is empty.'));
}
// Handle session ID
if (!$session_key) {
$session_key = notifier_generate_random_key(16); // Generate a unique session ID.
WC()->session->set('notifier_session_key', $session_key);
}
// Check if an existing entry exists
$existing_entry = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE session_key = %s",
$session_key
));
// If entry exists and is triggered, do not schedule action
if ($existing_entry && $existing_entry->triggered) {
wp_send_json(array('error' => true, 'message' => 'Cart abandonment already processed.' ));
}
// Fetch all active session_keys for this phone number
$existing_session_keys = $wpdb->get_col(
$wpdb->prepare(
"SELECT session_key FROM $table_name WHERE phone_number = %s AND triggered = 0 AND order_id = 0",
$phone_number
)
);
// Delete all other sessions except the current one
if (!empty($existing_session_keys)) {
foreach ($existing_session_keys as $old_session_key) {
if ($session_key !== $old_session_key) {
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $table_name WHERE session_key = %s",
$old_session_key
)
);
}
}
}
// Update or insert cart and customer data into the table
if ($existing_entry) {
// Update the existing entry
$wpdb->update(
$table_name,
array(
'phone_number' => $phone_number,
'cart_data' => $cart_data,
'cart_total' => $cart_total,
'coupon_data' => $serialized_coupons,
'customer_data' => $serialized_customer_data,
'updated_time' => current_time('mysql'),
),
array('session_key' => sanitize_key($session_key)),
array('%s', '%s', '%f', '%s', '%s', '%s'),
array('%s')
);
} else {
// Insert a new entry
$wpdb->insert(
$table_name,
array(
'session_key' => sanitize_key($session_key),
'phone_number' => $phone_number,
'cart_data' => $cart_data,
'cart_total' => $cart_total,
'coupon_data' => $serialized_coupons,
'customer_data' => $serialized_customer_data,
'created_time' => current_time('mysql'),
'updated_time' => current_time('mysql'),
),
array('%s', '%s', '%s', '%f', '%s', '%s', '%s', '%s')
);
}
// Unschedule previous actions for this session
as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
// Schedule a new action if billing phone is available
if ( !as_next_scheduled_action('notifier_check_cart_abandonment', array($session_key))) {
$run_time_minutes = (int) get_option('notifier_abandonment_cart_cron_run_time', 20); // Default to 20 minutes if not set
$run_time_seconds = $run_time_minutes * MINUTE_IN_SECONDS;
as_schedule_single_action(time() + $run_time_seconds, 'notifier_check_cart_abandonment', array($session_key));
}
wp_send_json(array('error' => false, 'message' => 'Cart abandonment request has been successfully processed. ' ));
}
/**
* Handle cart updates data updates.
*/
public static function notifier_update_cart_data_on_update() {
// Check if cart abandonment tracking is enabled
if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
return;
}
// Prevent execution on the Thank You and order recieved page
if (is_checkout() || is_wc_endpoint_url('order-received')) {
return;
}
if (!WC()->session || !WC()->session->has_session()) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
$cart_data = maybe_serialize(WC()->session->get('cart')); // Serialized cart data.
$cart_total = floatval(preg_replace('/[^\d.]/', '', WC()->cart->get_total('edit'))); // Cart total amount.
$session_key = WC()->session->get('notifier_session_key');
//Check if an order is already linked to this session record
$existing_order_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT order_id FROM $table_name WHERE session_key = %s",
sanitize_key($session_key)
)
);
// If an order exists, do NOT delete the record
if (!empty($existing_order_id)) {
as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
WC()->session->set( 'notifier_session_key', null );
return;
}
if (WC()->cart->is_empty()) {
if($session_key){
as_unschedule_all_actions('notifier_check_cart_abandonment', array($session_key));
WC()->session->set( 'notifier_session_key', null );
}
return;
}
// If session_key is not set, create and save a new one
if (!$session_key) {
$session_key = notifier_generate_random_key(16); // Generate a 16-character session key
WC()->session->set('notifier_session_key', $session_key);
}
// Check if an existing entry exists for the session_key
$existing_entry = $wpdb->get_row(
$wpdb->prepare(
"SELECT session_id FROM $table_name WHERE session_key = %s",
$session_key
)
);
if ($existing_entry) {
// Update the existing entry with the new cart data
$wpdb->update(
$table_name,
array(
'cart_data' => $cart_data,
'cart_total' => $cart_total,
'updated_time'=> current_time('mysql'),
),
array('session_key' => $session_key),
array('%s', '%f', '%s'),
array('%s')
);
} else {
// Insert a new entry for the session
$wpdb->insert(
$table_name,
array(
'session_key' => $session_key,
'cart_data' => $cart_data,
'cart_total' => $cart_total,
'created_time'=> current_time('mysql'),
'updated_time'=> current_time('mysql'),
),
array('%s', '%s', '%f', '%s', '%s')
);
}
}
/**
* Fire when cart data get updated
*/
public static function notifier_process_cart_abandonment($session_key) {
// Check if cart abandonment tracking is enabled
if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
// Check if the session data still exists in the DB
$abandoned_cart_entry = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM $table_name WHERE session_key = %s AND triggered = 0 AND order_id = 0",
$session_key
),
ARRAY_A
);
if (!empty($abandoned_cart_entry)) {
// Fire the notifier process action
do_action('notifier_process_abandoned_cart', $abandoned_cart_entry);
// Mark the triggered to true
$wpdb->update(
$table_name,
array('triggered' => 1),
array('session_key' => $session_key),
array('%d'),
array('%s')
);
as_unschedule_all_actions('notifier_check_cart_abandonment', array(sanitize_key($session_key)));
}
}
/**
* Fetch comma separated products from cart
*/
public static function get_comma_separated_products( $cart_data ) {
$product_names = [];
foreach ($cart_data as $item) {
if (!isset($item['product_id'])) {
continue; // Skip if product_id is missing
}
$product = wc_get_product($item['product_id']);
if ($product) {
$product_names[] = $product->get_name();
}
}
// Return comma-separated product names
return implode(', ', $product_names);
}
/**
* retrive cart data and set from url param
*/
public static function retrive_cart_data_from_url(){
if(!is_checkout() && !is_cart()){
return;
}
if (isset($_GET['wano_session'])) {
global $wpdb;
$table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
$session_key = sanitize_key($_GET['wano_session']);
if (empty($session_key)) {
return;
}
$cart_entry = $wpdb->get_row(
$wpdb->prepare("SELECT cart_data, customer_data FROM $table_name WHERE session_key = %s", $session_key),
ARRAY_A
);
if (!empty($cart_entry['cart_data'])) {
WC()->cart->empty_cart();
$cart_contents = maybe_unserialize($cart_entry['cart_data']);
foreach ($cart_contents as $item_key => $item) {
WC()->cart->add_to_cart($item['product_id'], $item['quantity'], $item['variation_id'], $item['variation']);
}
}
}
}
// ========================================
// v3 PAYLOAD BUILDER
// ========================================
/**
* Build the full WooCommerce order payload for v3 trigger dispatch.
*
* Fetches all order data in bulk (one call per data type), then maps
* each defined merge tag and recipient field using source/path or value closure.
* Meta keys discovered here that are new are persisted and trigger an async schema push.
*
* @param int $order_id WooCommerce order ID.
* @param string $trigger_id Trigger ID (e.g. 'woo_order_new').
* @return array Payload with merge_tags_data and recipient_fields.
*/
public static function build_wc_order_payload( $order_id, $trigger_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return array( 'merge_tags_data' => array(), 'recipient_fields' => array() );
}
$customer_id = $order->get_customer_id();
// Single bulk fetch — all data in ~6 calls total.
$bulk_data = array(
'order' => $order,
'base' => $order->get_base_data(),
'items' => $order->get_items( 'line_item' ),
'shipping' => $order->get_items( 'shipping' ),
'coupons' => $order->get_items( 'coupon' ),
'fees' => $order->get_items( 'fee' ),
'meta' => $order->get_meta_data(),
'user_meta' => $customer_id ? get_user_meta( $customer_id ) : array(),
);
// Index meta by key for fast lookups.
$meta_by_key = array();
foreach ( $bulk_data['meta'] as $meta_item ) {
if ( ! empty( $meta_item->key ) ) {
$meta_by_key[ $meta_item->key ] = $meta_item->value;
}
}
// Index user meta (get_user_meta returns arrays of values per key).
$user_meta_by_key = array();
foreach ( $bulk_data['user_meta'] as $key => $values ) {
$user_meta_by_key[ $key ] = is_array( $values ) ? $values[0] : $values;
}
$merge_tags_data = array();
$recipient_fields = array();
$all_merge_tags = Notifier_Notification_Merge_Tags::get_merge_tags( self::get_order_merge_tag_types() );
foreach ( $all_merge_tags as $group => $tags ) {
foreach ( $tags as $tag ) {
if ( empty( $tag['id'] ) ) {
continue;
}
if ( 0 === strpos( $tag['id'], 'woo_order_meta_' ) || 0 === strpos( $tag['id'], 'woo_customer_meta_' ) ) {
continue;
}
$value = self::resolve_field_from_bulk( $tag, $bulk_data, $meta_by_key, $user_meta_by_key );
if ( null !== $value ) {
$merge_tags_data[ $tag['id'] ] = html_entity_decode( sanitize_text_field( (string) $value ) );
}
}
}
$all_recipient_fields = Notifier_Notification_Merge_Tags::get_recipient_fields( self::get_order_recipient_types() );
foreach ( $all_recipient_fields as $group => $fields ) {
foreach ( $fields as $field ) {
if ( empty( $field['id'] ) ) {
continue;
}
if ( 0 === strpos( $field['id'], 'woo_order_meta_' ) || 0 === strpos( $field['id'], 'woo_customer_meta_' ) ) {
continue;
}
$value = self::resolve_field_from_bulk( $field, $bulk_data, $meta_by_key, $user_meta_by_key );
if ( null !== $value ) {
$recipient_fields[ $field['id'] ] = html_entity_decode( sanitize_text_field( (string) $value ) );
}
}
}
foreach ( $meta_by_key as $key => $value ) {
$tag_id = 'woo_order_meta_' . $key;
$merge_tags_data[ $tag_id ] = is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
$recipient_fields[ $tag_id ] = $merge_tags_data[ $tag_id ];
}
foreach ( $user_meta_by_key as $key => $value ) {
$tag_id = 'woo_customer_meta_' . $key;
$merge_tags_data[ $tag_id ] = is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
$recipient_fields[ $tag_id ] = $merge_tags_data[ $tag_id ];
}
// Live diff: detect new meta keys and persist + schedule async schema push.
self::maybe_update_known_meta_keys( 'shop_order', array_keys( $meta_by_key ) );
if ( $customer_id ) {
self::maybe_update_known_meta_keys( '_user', array_keys( $user_meta_by_key ) );
}
return array(
'merge_tags_data' => $merge_tags_data,
'recipient_fields' => $recipient_fields,
);
}
/**
* Resolve a single merge tag or recipient field value from pre-fetched bulk data.
*
* @param array $field_def Field definition with source, path, and/or value closure.
* @param array $bulk_data Pre-fetched bulk data array.
* @param array $meta_by_key Order meta indexed by key.
* @param array $user_meta_by_key User meta indexed by key.
* @return string|null Resolved value, or null if not applicable.
*/
private static function resolve_field_from_bulk( $field_def, $bulk_data, $meta_by_key, $user_meta_by_key ) {
if ( isset( $field_def['value'] ) && is_callable( $field_def['value'] ) ) {
return call_user_func( $field_def['value'], $bulk_data );
}
$source = isset( $field_def['source'] ) ? $field_def['source'] : null;
$path = isset( $field_def['path'] ) ? $field_def['path'] : null;
if ( ! $source || ! $path ) {
return null;
}
if ( 'base' === $source ) {
$parts = explode( '.', $path );
$data = $bulk_data['base'];
foreach ( $parts as $part ) {
if ( ! isset( $data[ $part ] ) ) {
return '';
}
$data = $data[ $part ];
}
return (string) $data;
}
if ( 'meta' === $source ) {
$value = $meta_by_key[ $path ] ?? '';
return is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
}
if ( 'user_meta' === $source ) {
$value = $user_meta_by_key[ $path ] ?? '';
return is_array( $value ) || is_object( $value ) ? json_encode( $value ) : (string) $value;
}
return null;
}
/**
* Compare discovered meta keys against stored known keys.
* If new keys found, update the persistent option and schedule an async schema push.
*
* @param string $type Post type key (e.g. 'shop_order') or '_user' for user meta.
* @param array $live_keys Keys found in the current object's meta.
*/
private static function maybe_update_known_meta_keys( $type, $live_keys ) {
if ( '_user' === $type ) {
$known = get_option( 'notifier_user_meta_keys', array() );
$new_keys = array_diff( $live_keys, $known );
if ( ! empty( $new_keys ) ) {
update_option( 'notifier_user_meta_keys', array_unique( array_merge( $known, $new_keys ) ), false );
as_enqueue_async_action( 'notifier_push_updated_schema', array( 'slug' => 'woocommerce' ), 'notifier' );
}
return;
}
$all_known = get_option( 'notifier_custom_meta_keys', array() );
$known = isset( $all_known[ $type ] ) ? $all_known[ $type ] : array();
$new_keys = array_diff( $live_keys, $known );
if ( ! empty( $new_keys ) ) {
$all_known[ $type ] = array_unique( array_merge( $known, $new_keys ) );
update_option( 'notifier_custom_meta_keys', $all_known, false );
as_enqueue_async_action( 'notifier_push_updated_schema', array( 'slug' => 'woocommerce' ), 'notifier' );
}
}
/**
* Action Scheduler handler: push updated trigger schema to SaaS after new meta keys discovered.
*
* @param string $slug Integration slug.
*/
public static function handle_push_updated_schema( $slug ) {
if ( ! Notifier_Connection::is_connected( $slug ) ) {
return;
}
$trigger_schema = Notifier_Connection::build_trigger_schema( $slug );
Notifier_Connection::send_saas_request( $slug, 'sync-triggers', array( 'triggers' => $trigger_schema ) );
}
// ========================================
// v3 CONTACT SYNC
// ========================================
/**
* Sync a WP user as a contact to SaaS when they register.
*
* @param int $user_id
*/
public static function on_user_register( $user_id ) {
self::sync_customer_contact( $user_id, 'woo_customer_created' );
}
/**
* Sync a WP user as a contact to SaaS when their profile is updated.
*
* @param int $user_id
* @param WP_User $old_user_data
*/
public static function on_user_update( $user_id, $old_user_data ) {
self::sync_customer_contact( $user_id, 'woo_customer_updated' );
}
/**
* Sync contact from checkout billing data (covers guest checkouts).
*
* Only runs when continuous sync is enabled in the connection config.
*
* @param int $order_id
*/
public static function on_checkout_contact_sync( $order_id ) {
$conn = Notifier_Connection::get_connection( 'woocommerce' );
if ( empty( $conn ) || empty( $conn['continuous_sync'] ) ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$phone = $order->get_billing_phone();
if ( empty( $phone ) ) {
return;
}
$contact = array(
'billing' => array(
'phone' => $phone,
'email' => $order->get_billing_email(),
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'company' => $order->get_billing_company(),
'address_1' => $order->get_billing_address_1(),
'address_2' => $order->get_billing_address_2(),
'city' => $order->get_billing_city(),
'state' => $order->get_billing_state(),
'postcode' => $order->get_billing_postcode(),
'country' => $order->get_billing_country(),
),
);
self::push_contacts_to_saas( array( $contact ), array(), 'woo_customer_created' );
}
/**
* Sync a WordPress/WooCommerce user as a contact to the SaaS.
*
* Only runs when continuous sync is enabled in the connection config.
*
* @param int $user_id
* @param string $event_key Trigger slug: 'woo_customer_created' or 'woo_customer_updated'.
*/
private static function sync_customer_contact( $user_id, $event_key ) {
$conn = Notifier_Connection::get_connection( 'woocommerce' );
if ( empty( $conn ) ) {
return;
}
// Respect the continuous sync toggle.
if ( empty( $conn['continuous_sync'] ) ) {
return;
}
$phone = get_user_meta( $user_id, 'billing_phone', true );
if ( empty( $phone ) ) {
return; // Can't sync without a phone number.
}
$contact = self::build_customer_contact_data( $user_id );
self::push_contacts_to_saas( array( $contact ), array(), $event_key );
}
/**
* Push contacts to the SaaS via the unified /{webhook_key}/event endpoint.
*
* @param array $contacts Array of contact data arrays in WC customer format.
* @param array $meta Optional metadata (total_contacts, remaining_contacts).
* @param string $event_key Trigger slug identifying the event: 'woo_customer_created',
* 'woo_customer_updated', or 'woo_customer_batch_sync'.
*/
private static function push_contacts_to_saas( $contacts, $meta = array(), $event_key = 'woo_customer_created' ) {
$conn = Notifier_Connection::get_connection( 'woocommerce' );
if ( empty( $conn ) || empty( $conn['webhook_key'] ) || empty( $conn['saas_url'] ) ) {
return;
}
$url = trailingslashit( $conn['saas_url'] ) . NOTIFIER_APP_API_PATH . '/integrations/woocommerce/' . $conn['webhook_key'] . '/event';
$body = array_merge( array(
'event_key' => $event_key,
'contacts' => $contacts,
), $meta );
$response = wp_remote_post( $url, array(
'headers' => array(
'Content-Type' => 'application/json',
'X-Auth-Key' => $conn['auth_key'] ?? '',
),
'body' => wp_json_encode( $body ),
'timeout' => 30,
'sslverify' => true,
) );
if ( is_wp_error( $response ) ) {
error_log( 'WANotifier: push_contacts_to_saas failed: ' . $response->get_error_message() );
}
}
/**
* REST handler: SaaS pushes config updates (e.g. continuous_sync toggle).
*
* Stores the config values in the plugin's WooCommerce connection option so
* the real-time sync hooks can read them without calling the SaaS.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
*/
public static function handle_rest_update_config( $request ) {
$data = $request->get_json_params();
$conn = Notifier_Connection::get_connection( 'woocommerce' );
if ( empty( $conn ) ) {
return new WP_REST_Response( array( 'error' => true, 'message' => 'Not connected.' ), 400 );
}
if ( array_key_exists( 'continuous_sync', $data ) ) {
$conn['continuous_sync'] = ! empty( $data['continuous_sync'] );
Notifier_Connection::save_connection( 'woocommerce', $conn );
}
return new WP_REST_Response( array( 'error' => false, 'message' => 'Config updated.' ), 200 );
}
/**
* REST handler: SaaS requests the plugin to stop a running batch customer sync.
*
* Cancels all pending Action Scheduler jobs for the batch sync and sets a
* transient flag so any currently-running page handler exits early.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
*/
public static function handle_rest_batch_sync_stop( $request ) {
// Cancel all queued (not yet started) batch sync pages.
as_unschedule_all_actions( 'notifier_wc_batch_contact_sync', array(), 'notifier' );
// Set a short-lived flag so any in-progress page handler exits before pushing to SaaS.
set_transient( 'notifier_wc_batch_sync_stopped', 1, 5 * MINUTE_IN_SECONDS );
return new WP_REST_Response( array(
'error' => false,
'message' => 'Batch sync stopped.',
), 200 );
}
/**
* REST handler: SaaS requests the plugin to start a batch customer sync.
*
* Schedules an Action Scheduler job on the plugin side that pages through
* all WooCommerce customers and pushes them to the SaaS /{webhook_key}/event endpoint.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
*/
public static function handle_rest_batch_sync_start( $request ) {
// Cancel any existing pending batch sync jobs to avoid duplicates.
as_unschedule_all_actions( 'notifier_wc_batch_contact_sync', array(), 'notifier' );
// Clear any lingering stop flag from a previous stop request.
delete_transient( 'notifier_wc_batch_sync_stopped' );
// Count total customers with billing phone.
$total_query = new WP_User_Query( array(
'role__in' => array( 'customer', 'subscriber' ),
'meta_key' => 'billing_phone',
'meta_compare' => '!=',
'meta_value' => '',
'number' => -1,
'fields' => 'ID',
) );
$total = count( $total_query->get_results() );
// Schedule first page.
as_enqueue_async_action(
'notifier_wc_batch_contact_sync',
array( 'page' => 1, 'per_page' => 50, 'total' => $total ),
'notifier'
);
return new WP_REST_Response( array(
'error' => false,
'message' => 'Batch sync started.',
'total' => $total,
), 200 );
}
/**
* Action Scheduler handler: process one page of the batch customer sync.
*
* Runs in the plugin. Fetches a page of WC customers, builds contact data,
* and pushes the batch to the SaaS /{webhook_key}/event endpoint. Schedules the next
* page if more customers remain.
*
* @param int $page Current page number.
* @param int $per_page Number of customers per page.
* @param int $total Total customers to sync.
*/
public static function process_batch_contact_sync_page( $page, $per_page, $total ) {
// Abort immediately if a stop was requested while this job was queued.
if ( get_transient( 'notifier_wc_batch_sync_stopped' ) ) {
return;
}
$page = (int) $page ?: 1;
$per_page = (int) $per_page ?: 50;
$total = (int) $total ?: 0;
$user_query = new WP_User_Query( array(
'role__in' => array( 'customer', 'subscriber' ),
'meta_key' => 'billing_phone',
'meta_compare' => '!=',
'meta_value' => '',
'number' => $per_page,
'paged' => $page,
'orderby' => 'ID',
'order' => 'ASC',
) );
$users = $user_query->get_results();
if ( empty( $users ) ) {
return;
}
$contacts = array();
foreach ( $users as $user ) {
$contacts[] = self::build_customer_contact_data( $user->ID );
}
$processed_so_far = $page * $per_page;
$remaining = max( 0, $total - $processed_so_far );
self::push_contacts_to_saas( $contacts, array(
'total_contacts' => $total,
'remaining_contacts' => $remaining,
), 'woo_customer_batch_sync' );
if ( $remaining > 0 ) {
as_enqueue_async_action(
'notifier_wc_batch_contact_sync',
array( 'page' => $page + 1, 'per_page' => $per_page, 'total' => $total ),
'notifier'
);
}
}
/**
* Build contact data array for a WP user in WC customer format.
*
* @param int $user_id
* @return array
*/
private static function build_customer_contact_data( $user_id ) {
$user = get_userdata( $user_id );
$billing_phone = get_user_meta( $user_id, 'billing_phone', true );
$billing_country = get_user_meta( $user_id, 'billing_country', true );
return array(
'id' => $user_id,
'email' => $user ? $user->user_email : '',
'first_name' => get_user_meta( $user_id, 'first_name', true ) ?: get_user_meta( $user_id, 'billing_first_name', true ),
'last_name' => get_user_meta( $user_id, 'last_name', true ) ?: get_user_meta( $user_id, 'billing_last_name', true ),
'username' => $user ? $user->user_login : '',
'billing' => array(
'phone' => self::get_formatted_phone_number( $billing_phone, $billing_country ),
'email' => get_user_meta( $user_id, 'billing_email', true ),
'first_name' => get_user_meta( $user_id, 'billing_first_name', true ),
'last_name' => get_user_meta( $user_id, 'billing_last_name', true ),
'company' => get_user_meta( $user_id, 'billing_company', true ),
'address_1' => get_user_meta( $user_id, 'billing_address_1', true ),
'address_2' => get_user_meta( $user_id, 'billing_address_2', true ),
'city' => get_user_meta( $user_id, 'billing_city', true ),
'state' => get_user_meta( $user_id, 'billing_state', true ),
'postcode' => get_user_meta( $user_id, 'billing_postcode', true ),
'country' => $billing_country,
),
);
}
/**
* store session id during order creation
*/
public static function notifier_save_session_on_new_order($order_id) {
if ('yes' !== get_option('notifier_enable_abandonment_cart_tracking', 'no')) {
return;
}
if (!WC()->session || !WC()->session->has_session()) {
return;
}
// Fetch specific session key
$session_key = WC()->session->get('notifier_session_key');
if (!empty($session_key)) {
global $wpdb;
$table_name = $wpdb->prefix . NOTIFIER_CART_ABANDONMENT_TABLE;
$wpdb->update(
$table_name,
array('order_id' => $order_id), // Update the order_id
array('session_key' => sanitize_key($session_key)), // Where condition
array('%d'), // Data type for order_id
array('%s') // Data type for session_key
);
as_unschedule_all_actions('notifier_check_cart_abandonment', array(sanitize_key($session_key)));
}
}
}