source_type = $source_slug; $this->optgroup_label = __( 'Cart', 'wp-marketing-automations' ); $this->event_name = __( 'Cart Abandoned', 'wp-marketing-automations' ); $this->event_desc = __( 'This automation would trigger when a user abandoned the cart.', 'wp-marketing-automations' ); $this->event_merge_tag_groups = array( 'wc_ab_cart', 'bwf_contact' ); $this->event_rule_groups = array( 'ab_cart', 'aerocheckout', 'bwf_contact_segments', 'bwf_contact', 'bwf_contact_fields', 'bwf_contact_user', 'bwf_contact_wc', 'bwf_contact_geo', 'bwf_engagement', 'bwf_broadcast' ); $this->support_lang = true; $this->priority = 5; $this->customer_email_tag = '{{cart_billing_email}}'; $this->v2 = true; $this->optgroup_priority = 5; $this->supported_blocks = [ 'cart' ]; $this->automation_add = true; } public function load_hooks() { } public static function get_instance( $source_slug ) { if ( null === self::$instance ) { self::$instance = new self( $source_slug ); } return self::$instance; } /** * Get all the abandoned rows from db table. It runs at every 2 minutes. */ public function get_eligible_abandoned_rows() { global $wpdb; $global_settings = BWFAN_Common::get_global_settings(); $abandoned_time_in_minutes = intval( $global_settings['bwfan_ab_init_wait_time'] ); /** Status 0: Pending, 4: Re-Scheduled */ $query = $wpdb->prepare( 'SELECT * FROM {table_name} WHERE TIMESTAMPDIFF(MINUTE,last_modified,UTC_TIMESTAMP) >= %d AND status IN (0,4)', $abandoned_time_in_minutes ); $active_abandoned_carts = BWFAN_Model_Abandonedcarts::get_results( $query ); if ( ! is_array( $active_abandoned_carts ) || count( $active_abandoned_carts ) === 0 ) { return; } $active_abandoned_carts = BWFAN_Abandoned_Cart::remove_duplicate_cart( $active_abandoned_carts ); $ids = array_column( $active_abandoned_carts, 'ID', 'ID' ); /** Status 1: In-Progress (Automations Found), 3: Pending (No Tasks Found) */ BWFAN_Core()->public->load_active_automations( $this->get_slug() ); BWFAN_Core()->public->load_active_v2_automations( $this->get_slug() ); if ( ( ! is_array( $this->automations_arr ) || count( $this->automations_arr ) === 0 ) && ( ! is_array( $this->automations_v2_arr ) || count( $this->automations_v2_arr ) === 0 ) ) { /** Status 3 - No automation found */ BWFAN_Common::update_abandoned_rows( $ids, 3 ); foreach ( $active_abandoned_carts as $active_abandoned_cart ) { BWFAN_Common::maybe_create_abandoned_contact( $active_abandoned_cart ); // create contact at the time of abandonment } return; } $days_to_check = isset( $global_settings['bwfan_disable_abandonment_days'] ) && intval( $global_settings['bwfan_disable_abandonment_days'] ) > 0 ? intval( $global_settings['bwfan_disable_abandonment_days'] ) : 0; if ( ! empty( $days_to_check ) ) { $after_date = date( 'Y-m-d H:i:s', strtotime( " -$days_to_check day" ) ); } foreach ( $active_abandoned_carts as $active_abandoned_cart ) { BWFAN_Common::maybe_create_abandoned_contact( $active_abandoned_cart );// create contact at the time of abandonment if ( empty( $days_to_check ) ) { $this->process( $active_abandoned_cart ); continue; } /** Cool Off period checking */ $query = "SELECT customers.l_order_date FROM {$wpdb->prefix}bwf_wc_customers AS customers WHERE EXISTS ( SELECT 1 FROM {$wpdb->prefix}bwf_contact AS contact WHERE contact.email = %s AND contact.id = customers.cid ) AND customers.l_order_date >= %s LIMIT 1"; $last_order = $wpdb->get_var( $wpdb->prepare( $query, $active_abandoned_cart['email'], $after_date ) ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( ! empty( $last_order ) ) { /** Order found. No need to process the cart */ /** Status 5 - Under Cool off period */ BWFAN_Common::update_abandoned_rows( [ $active_abandoned_cart['ID'] ], 5 ); continue; } $this->process( $active_abandoned_cart ); } } /** * Set up rules data * * @param $value */ public function pre_executable_actions( $value ) { BWFAN_Core()->rules->setRulesData( $this->abandoned_data, 'abandoned_data' ); } /** * Make the required data for the current event and send it asynchronously. * * @param $abandoned_cart * * @return array|bool|void */ public function process( $abandoned_cart ) { $this->abandoned_id = $abandoned_cart['ID']; $this->abandoned_data = BWFAN_Model_Abandonedcarts::get( $this->abandoned_id ); if ( ! is_array( $this->abandoned_data ) ) { BWFAN_Common::update_abandoned_rows( [ $abandoned_cart['ID'] ], 3 ); return; } $this->abandoned_email = $this->abandoned_data['email']; $this->token = $abandoned_cart['token']; $this->cart_item = $this->abandoned_data['items']; $this->user_id = $abandoned_cart['user_id']; $this->abandoned_phone = $this->get_abandoned_phone( $this->abandoned_data ); $this->contact_data_v2 = array( 'abandoned_id' => absint( $this->abandoned_id ), 'email' => $this->abandoned_email, 'user_id' => $this->user_id, 'cart_item' => $this->abandoned_data['items'], 'phone' => $this->abandoned_phone, 'event' => $this->get_slug(), 'version' => 2 ); return $this->run_automations(); } /** get abandoned phone number * * @param $abandoned_data * * @return mixed|string|void */ public function get_abandoned_phone( $abandoned_data ) { if ( empty( $abandoned_data ) ) { return ''; } $phone = ''; $checkout_data = json_decode( $abandoned_data['checkout_data'], true ); if ( ! isset( $checkout_data['fields'] ) || empty( $checkout_data['fields'] ) ) { return $phone; } $checkout_fields = $checkout_data['fields']; /** check if the billing phone available */ if ( isset( $checkout_fields['billing_phone'] ) && ! empty( $checkout_fields['billing_phone'] ) ) { $phone = $checkout_fields['billing_phone']; } /** check if the billing phone available */ if ( empty( $phone ) && isset( $checkout_fields['shipping_phone'] ) && ! empty( $checkout_fields['shipping_phone'] ) ) { $phone = $checkout_fields['shipping_phone']; } /** still empty then return */ if ( empty( $phone ) ) { return $phone; } $cart_phone = $phone; $cart_country = ''; /** check for billing country */ if ( isset( $checkout_fields['billing_country'] ) && ! empty( $checkout_fields['billing_country'] ) ) { $cart_country = $checkout_fields['billing_country']; } /** check for shipping country */ if ( empty( $cart_country ) && isset( $checkout_fields['shipping_country'] ) && ! empty( $checkout_fields['shipping_country'] ) ) { $cart_country = $checkout_fields['shipping_country']; } /** cart country not exists than return cart phone without country */ if ( empty( $cart_country ) ) { return $cart_phone; } $phone = BWFAN_Phone_Numbers::add_country_code( $cart_phone, $cart_country ); return $phone; } /** * Override method to change the state of Cart based on Automations found * * @return array|bool */ public function run_automations() { $any_automation_ran = BWFAN_Common::maybe_run_v2_automations( $this->get_slug(), $this->contact_data_v2 ); /** Run v1 automations */ $automation_actions = array(); foreach ( $this->automations_arr as $automation_id => $automation_data ) { if ( $this->get_slug() !== $automation_data['event'] || 0 !== intval( $automation_data['requires_update'] ) ) { continue; } $ran_actions = $this->handle_single_automation_run( $automation_data, $automation_id ); $automation_actions[ $automation_id ] = $ran_actions; } /** * We found no tasks to create. And no v2 automation contact row created * So, setting status 3 i.e. Pending (No Tasks Found) */ if ( 0 === array_sum( $automation_actions ) && ! $any_automation_ran ) { BWFAN_Common::update_abandoned_rows( array( $this->abandoned_id ), 3 ); return $automation_actions; } /** Updating carts to in-progress i.e. 1 state */ BWFAN_Common::update_abandoned_rows( array( $this->abandoned_id ), 1 ); return $automation_actions; } /** * Override method to change the state of Cart based on Tasks to be created * * @param $automation_data * @param $automation_id * * @return bool|int */ public function handle_single_automation_run( $automation_data, $automation_id ) { $this->event_automation_id = $automation_id; /** Setup the rules data */ $this->pre_executable_actions( $automation_data ); /** get all the actions which have passed the rules */ $actions = $this->get_executable_actions( $automation_data ); if ( ! isset( $actions['actions'] ) || ! is_array( $actions['actions'] ) || count( $actions['actions'] ) === 0 ) { return 0; } $event_data = $this->get_automation_event_data( $automation_data ); try { /** Register all those tasks which passed through rules or which are direct actions. The following function is present in every event class. */ $this->register_tasks( $automation_id, $actions['actions'], $event_data ); } catch ( Exception $exception ) { BWFAN_Core()->logger->log( 'Register task function not overrided by child class' . get_class( $this ), $this->log_type ); } return count( $actions['actions'] ); } /** * Registers the tasks for current event. * * @param $automation_id * @param $integration_data * @param $event_data */ public function register_tasks( $automation_id, $integration_data, $event_data ) { $data_to_send = $this->get_event_data(); add_action( 'bwfan_task_created_ab_cart_abandoned', [ $this, 'update_task_meta' ], 10, 2 ); $this->create_tasks( $automation_id, $integration_data, $event_data, $data_to_send ); } public function get_event_data() { $data_to_send = []; $data_to_send['global']['email'] = $this->abandoned_email; $data_to_send['global']['cart_abandoned_id'] = $this->abandoned_id; $data_to_send['global']['cart_details'] = $this->abandoned_data; $data_to_send['global']['phone'] = $this->abandoned_phone; $data_to_send['global']['user_id'] = $this->user_id; $checkout_data = isset( $this->abandoned_data['checkout_data'] ) ? json_decode( $this->abandoned_data['checkout_data'], true ) : []; $data_to_send['global']['language'] = isset( $checkout_data['lang'] ) ? $checkout_data['lang'] : ''; return $data_to_send; } /** * If any event has email and it does not contain order object, then following method must be overridden by child event class. * Return email * @return bool */ public function get_email_event() { return $this->abandoned_email; } /** * If any event has user id and it does not contain order object, then following method must be overridden by child event class. * Return user id * @return bool */ public function get_user_id_event() { return $this->user_id; } public function update_task_meta( $index, $task_id ) { BWFAN_Core()->tasks->insert_taskmeta( $task_id, 'c_a_id', $this->abandoned_id ); } /** * Make the view data for the current event which will be shown in task listing screen. * * @param $global_data * * @return false|string */ public function get_task_view( $global_data ) { ob_start(); ?>