get_id() : (int) $order ); }, 20, 2 ); add_action( 'aieo_withdrawal_return_approved', static function ( $declaration_id, $order ) { unset( $declaration_id ); self::on_withdrawal_event( 'return_approved', is_object( $order ) && method_exists( $order, 'get_id' ) ? $order->get_id() : (int) $order ); }, 20, 2 ); add_action( 'aieo_customer_opted_in', array( __CLASS__, 'on_opted_in' ), 10, 1 ); add_action( 'aieo_customer_opted_out', array( __CLASS__, 'on_opted_out' ), 10, 1 ); add_action( 'newsletter_user_confirmed', array( __CLASS__, 'on_tnp_confirmed' ), 10, 1 ); add_action( 'newsletter_user_unsubscribed', array( __CLASS__, 'on_tnp_unsub' ), 10, 1 ); } public static function maybe_schedule() { if ( class_exists( 'AIEO_Schedule_Gate', false ) && ! AIEO_Schedule_Gate::due( __METHOD__ ) ) { return; } static $checked = false; if ( $checked ) { return; } $checked = true; if ( get_transient( 'aieo_workflow_sched_lock' ) ) { return; } set_transient( 'aieo_workflow_sched_lock', 1, 5 * MINUTE_IN_SECONDS ); if ( function_exists( 'as_next_scheduled_action' ) && function_exists( 'as_schedule_recurring_action' ) ) { if ( false === as_next_scheduled_action( self::HOOK_TICK, null, 'aieo' ) ) { as_schedule_recurring_action( time() + HOUR_IN_SECONDS, HOUR_IN_SECONDS, self::HOOK_TICK, array(), 'aieo' ); } } elseif ( ! wp_next_scheduled( self::HOOK_TICK ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::HOOK_TICK ); } } public static function dedup_pre_schedule( $pre, $timestamp, $interval, $hook, $args = array(), $group = '' ) { if ( self::HOOK_TICK !== $hook || 'aieo' !== $group ) { return $pre; } global $wpdb; $lk = AIEO_Automation_Test_Mode::lock_name( 'aieo_workflow_sched' ); if ( 1 !== (int) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 0)', $lk ) ) ) { return 1; } try { $existing = (int) $wpdb->get_var( $wpdb->prepare( "SELECT a.action_id FROM {$wpdb->prefix}actionscheduler_actions a LEFT JOIN {$wpdb->prefix}actionscheduler_groups g ON g.group_id = a.group_id WHERE a.hook = %s AND a.status IN ('pending','in-progress') AND g.slug = %s ORDER BY a.action_id ASC LIMIT 1", self::HOOK_TICK, 'aieo' ) ); return $existing > 0 ? $existing : $pre; } finally { $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lk ) ); } } public static function tick() { if ( ! self::ready() ) { return; } if ( function_exists( 'aieo_subjects_maybe_install' ) ) { aieo_subjects_maybe_install(); } if ( class_exists( 'AIEO_DMM_Module_Base' ) && ! AIEO_DMM_Module_Base::is_aieo_pro_user() ) { return; } global $wpdb; $lock = AIEO_Automation_Test_Mode::lock_name(); if ( 1 !== (int) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 0)', $lock ) ) ) { return; } try { foreach ( AIEO_Workflows::list_all() as $wf ) { if ( 'active' !== $wf['status'] ) { continue; } self::fire_trigger( $wf ); } $more = self::process_runs( self::BATCH ); if ( $more && function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( self::HOOK_TICK, array(), 'aieo' ); } } finally { $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) ); } } private static function fire_trigger( $wf ) { switch ( $wf['trigger_type'] ) { case 'scheduled': if ( self::channels_live( $wf ) ) { self::fire_scheduled( $wf ); } break; case 'abandoned_cart': if ( self::channels_live( $wf ) ) { self::fire_abandoned_cart( $wf ); } break; case 'birthday': if ( self::channels_live( $wf ) ) { self::fire_birthday( $wf ); } break; case 'welcome': if ( self::channels_live( $wf ) ) { self::fire_welcome( $wf ); } break; case 'back_in_stock': if ( self::channels_live( $wf ) ) { self::fire_back_in_stock( $wf ); } break; case 'win_back': if ( self::channels_live( $wf ) ) { self::fire_win_back( $wf ); } break; case 'order_completed': self::fire_order_completed( $wf ); break; case 'guest_created': self::fire_guest_created( $wf ); break; case 'abandoned_checkout': self::fire_abandoned_checkout( $wf ); break; } if ( class_exists( 'AIEO_Social_Broadcast' ) ) { try { AIEO_Social_Broadcast::maybe_launch( $wf ); } catch ( \Throwable $e ) { } } } public static function on_opted_in( $identity ) { self::fire_consent_event( 'opted_in', $identity ); } public static function on_opted_out( $identity ) { self::fire_consent_event( 'opted_out', $identity ); } public static function on_tnp_confirmed( $user ) { $e = self::tnp_email( $user ); if ( '' !== $e ) { self::fire_consent_event( 'opted_in', $e ); } } public static function on_tnp_unsub( $user ) { $e = self::tnp_email( $user ); if ( '' !== $e ) { self::fire_consent_event( 'opted_out', $e ); } } private static function tnp_email( $user ) { if ( is_object( $user ) && ! empty( $user->email ) ) { return (string) $user->email; } if ( is_array( $user ) && ! empty( $user['email'] ) ) { return (string) $user['email']; } return ''; } public static function on_survey_signal( $nps_class, $identity ) { $class = (string) $nps_class; if ( 'detractor' !== $class && 'promoter' !== $class ) { return; } self::fire_consent_event( 'survey_' . $class, $identity ); } public static function on_survey_answered( $entry_id, $blueprint, $answers, $ctx ) { unset( $answers ); if ( ! self::ready() || ! class_exists( 'AIEO_Workflows' ) ) { return; } $entry_id = (int) $entry_id; $identity = array( 'user_id' => (int) ( isset( $ctx['user_id'] ) ? $ctx['user_id'] : 0 ), 'email' => (string) ( isset( $ctx['email'] ) ? $ctx['email'] : '' ), ); $run_ctx = array( 'order_id' => (int) ( isset( $ctx['order_id'] ) ? $ctx['order_id'] : 0 ), ); foreach ( AIEO_Workflows::list_all() as $wf ) { if ( 'active' !== $wf['status'] || 'survey_answered' !== (string) $wf['trigger_type'] ) { continue; } $cfg = is_array( $wf['trigger_config'] ?? null ) ? $wf['trigger_config'] : array(); $want = isset( $cfg['survey'] ) ? (string) $cfg['survey'] : ''; if ( '' !== $want && $want !== (string) $blueprint ) { continue; } if ( ! self::survey_condition_met( $entry_id, $cfg ) ) { continue; } self::fire_one_consent_workflow( $wf, $identity, $run_ctx ); } } private static function survey_condition_met( $entry_id, array $cfg ) { $question = isset( $cfg['question'] ) ? sanitize_key( (string) $cfg['question'] ) : ''; $operator = isset( $cfg['operator'] ) ? (string) $cfg['operator'] : 'any'; if ( '' === $question || 'any' === $operator ) { return true; } global $wpdb; $values = AIEO_Form_CPT::entry_values_table(); $rows = (array) $wpdb->get_results( $wpdb->prepare( "SELECT choice_key, value_num FROM {$values} WHERE entry_id = %d AND field_key = %s AND disposition = 'answered'", (int) $entry_id, $question ), ARRAY_A ); if ( ! $rows ) { return false; } $want = isset( $cfg['value'] ) ? (string) $cfg['value'] : ''; foreach ( $rows as $row ) { $code = (string) $row['choice_key']; $num = ( null === $row['value_num'] ) ? null : (float) $row['value_num']; switch ( $operator ) { case 'is': if ( $code === $want || ( null !== $num && (float) $want === $num ) ) { return true; } break; case 'not': break; case 'lte': if ( null !== $num && $num <= (float) $want ) { return true; } break; case 'gte': if ( null !== $num && $num >= (float) $want ) { return true; } break; } } if ( 'not' === $operator ) { foreach ( $rows as $row ) { if ( (string) $row['choice_key'] === $want ) { return false; } } return true; } return false; } public static function on_withdrawal_event( $trigger_type, $order_id ) { if ( ! function_exists( 'wc_get_order' ) ) { return; } $order = wc_get_order( (int) $order_id ); if ( ! $order ) { return; } self::fire_consent_event( (string) $trigger_type, array( 'user_id' => (int) $order->get_customer_id(), 'email' => (string) $order->get_billing_email(), ) ); } private static function fire_one_consent_workflow( $wf, $identity, $context = array() ) { list( $uid, $email ) = self::resolve_identity( $identity ); if ( $uid <= 0 && '' === $email ) { return; } if ( $uid > 0 ) { $rule = self::audience_rule( $wf ); if ( $rule && class_exists( 'AIEO_Audience_Rules' ) && ! AIEO_Audience_Rules::matches_user( $rule, $uid ) ) { return; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, (array) $context ); return; } $crule = self::audience_rule( $wf ); if ( $crule && class_exists( 'AIEO_Audience_Rules' ) && ! AIEO_Audience_Rules::matches_email( $crule, $email ) ) { return; } self::enqueue_email_run( $wf, $email, (array) $context, 24 ); } public static function fire_consent_event( $trigger_type, $identity ) { if ( ! self::ready() || ! class_exists( 'AIEO_Workflows' ) ) { return; } if ( class_exists( 'AIEO_DMM_Module_Base' ) && ! AIEO_DMM_Module_Base::is_aieo_pro_user() ) { return; } list( $uid, $email ) = self::resolve_identity( $identity ); if ( $uid <= 0 && '' === $email ) { return; } foreach ( AIEO_Workflows::list_all() as $wf ) { if ( 'active' !== $wf['status'] || $trigger_type !== (string) $wf['trigger_type'] ) { continue; } if ( $uid > 0 ) { $rule = self::audience_rule( $wf ); if ( $rule && class_exists( 'AIEO_Audience_Rules' ) && ! AIEO_Audience_Rules::matches_user( $rule, $uid ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, array() ); } else { $crule = self::audience_rule( $wf ); if ( $crule && class_exists( 'AIEO_Audience_Rules' ) && ! AIEO_Audience_Rules::matches_email( $crule, $email ) ) { continue; } self::enqueue_email_run( $wf, $email, array(), 24 ); } } } private static function resolve_identity( $identity ) { $uid = 0; $email = ''; if ( is_int( $identity ) || ( is_string( $identity ) && ctype_digit( $identity ) ) ) { $uid = (int) $identity; } elseif ( is_string( $identity ) && is_email( $identity ) ) { $email = strtolower( sanitize_email( $identity ) ); } elseif ( $identity instanceof WP_User ) { $uid = (int) $identity->ID; } elseif ( is_object( $identity ) ) { if ( ! empty( $identity->user_id ) ) { $uid = (int) $identity->user_id; } if ( $uid <= 0 && ! empty( $identity->ID ) ) { $uid = (int) $identity->ID; } if ( ! empty( $identity->email ) ) { $email = strtolower( sanitize_email( (string) $identity->email ) ); } } elseif ( is_array( $identity ) ) { if ( ! empty( $identity['user_id'] ) ) { $uid = (int) $identity['user_id']; } if ( ! empty( $identity['email'] ) ) { $email = strtolower( sanitize_email( (string) $identity['email'] ) ); } } if ( $uid <= 0 && '' !== $email ) { $u = get_user_by( 'email', $email ); if ( $u instanceof WP_User ) { $uid = (int) $u->ID; } } if ( $uid > 0 && '' === $email ) { $u = get_user_by( 'id', $uid ); if ( $u instanceof WP_User ) { $email = strtolower( (string) $u->user_email ); } } return array( $uid, $email ); } private static function fire_guest_created( $wf ) { if ( ! function_exists( 'wc_get_orders' ) ) { return; } $since = time() - self::ORDER_LOOKBACK_HOURS * HOUR_IN_SECONDS; $page = 1; $guard = 0; do { $ids = wc_get_orders( array( 'status' => array( 'wc-processing', 'wc-completed', 'wc-on-hold' ), 'customer_id' => 0, 'limit' => self::BATCH, 'paged' => $page, 'orderby' => 'ID', 'order' => 'ASC', 'date_created' => '>=' . $since, 'return' => 'ids', ) ); $n = count( (array) $ids ); foreach ( (array) $ids as $oid ) { $order = wc_get_order( (int) $oid ); if ( ! $order || (int) $order->get_customer_id() > 0 ) { continue; } if ( apply_filters( 'aieo_suppress_order_customer_email', false, $order, 'workflow' ) ) { continue; } $email = (string) $order->get_billing_email(); if ( '' === $email ) { continue; } if ( get_user_by( 'email', $email ) ) { continue; } self::enqueue_email_run( $wf, $email, array( 'order_id' => (int) $oid, 'product_id' => self::first_product_id( $order ) ), -1 ); } $page++; $guard++; } while ( $n >= self::BATCH && $guard < 100 ); } private static function first_product_id( $order ) { foreach ( $order->get_items() as $item ) { $pid = (int) $item->get_product_id(); if ( $pid > 0 ) { return $pid; } } return 0; } private static function fire_abandoned_checkout( $wf ) { global $wpdb; if ( ! class_exists( 'AIEO_Carts_Store' ) ) { return; } $carts = AIEO_Carts_Store::carts_table(); if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $carts ) ) !== $carts ) { return; } $cfg = (array) ( $wf['trigger_config'] ?? array() ); $min_age = max( 0, (int) ( $cfg['min_age_hours'] ?? 1 ) ); $rules = ( isset( $cfg['order_rules'] ) && is_array( $cfg['order_rules'] ) ) ? $cfg['order_rules'] : array(); $crule = self::audience_rule( $wf ); $after = 0; $guard = 0; do { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, wc_order_id, user_id, customer_email, items, total, coupons FROM `{$carts}` WHERE kind = 'abandoned' AND outcome = 'open' AND customer_email <> '' AND wc_order_id IS NOT NULL AND id > %d AND created_at <= (UTC_TIMESTAMP() - INTERVAL %d HOUR) ORDER BY id ASC LIMIT %d", $after, $min_age, self::BATCH ), ARRAY_A ); $n = count( (array) $rows ); foreach ( (array) $rows as $r ) { $after = (int) $r['id']; $oid = (int) $r['wc_order_id']; if ( $oid <= 0 || self::has_run_for_order( (int) $wf['id'], $oid ) ) { continue; } if ( $rules && class_exists( 'AIEO_Order_Rules' ) ) { $items = json_decode( (string) $r['items'], true ); $extra = array( 'total' => (float) $r['total'], 'coupons' => json_decode( (string) $r['coupons'], true ) ?: array(), 'email' => (string) $r['customer_email'] ); if ( ! AIEO_Order_Rules::matches_cart( is_array( $items ) ? $items : array(), $rules, $extra ) ) { continue; } } $uid = (int) $r['user_id']; $email = (string) $r['customer_email']; $pid = self::first_cart_product_id( (string) $r['items'] ); $ctx = array( 'order_id' => $oid, 'cart_id' => (int) $r['id'], 'product_id' => $pid ); if ( $crule && class_exists( 'AIEO_Audience_Rules' ) ) { $in_audience = $uid > 0 ? AIEO_Audience_Rules::matches_user( $crule, $uid ) : AIEO_Audience_Rules::matches_email( $crule, $email ); if ( ! $in_audience ) { continue; } } if ( $uid > 0 ) { $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, $ctx, true ); } else { $ctx['guest_email'] = $email; self::enqueue_run( $wf, 0, 0, $ctx, true, md5( strtolower( trim( $email ) ) ) ); } } $guard++; } while ( $n >= self::BATCH && $guard < 100 ); } private static function first_cart_product_id( $items_json ) { $items = json_decode( (string) $items_json, true ); if ( is_array( $items ) ) { foreach ( $items as $it ) { $pid = (int) ( $it['product_id'] ?? 0 ); if ( $pid > 0 ) { return $pid; } } } return 0; } private static function cart_still_abandoned( $ctx ) { global $wpdb; $cart_id = (int) ( $ctx['cart_id'] ?? 0 ); if ( $cart_id <= 0 || ! class_exists( 'AIEO_Carts_Store' ) ) { return false; } $carts = AIEO_Carts_Store::carts_table(); $oid = (int) ( $ctx['order_id'] ?? 0 ); if ( $oid > 0 && function_exists( 'wc_get_order' ) ) { $order = wc_get_order( $oid ); if ( $order instanceof WC_Order && ! in_array( $order->get_status(), array( 'checkout-draft', 'auto-draft', 'pending', 'failed' ), true ) ) { return false; } } $outcome = (string) $wpdb->get_var( $wpdb->prepare( "SELECT outcome FROM `{$carts}` WHERE id = %d", $cart_id ) ); return 'open' === $outcome; } private static function channels_live( $wf ) { $sends = false; foreach ( (array) ( $wf['steps'] ?? array() ) as $s ) { if ( ! in_array( (string) ( $s['type'] ?? '' ), array( 'send', 'ask_survey' ), true ) ) { continue; } $sends = true; $ch = (string) ( $s['channel'] ?? 'cascade' ); $test = ( 'email' === $ch || 'push' === $ch ) ? AIEO_Automation_Test_Mode::test_on( $ch ) : AIEO_Automation_Test_Mode::any_test(); if ( self::channel_live( $ch ) || $test ) { return true; } } return ! $sends; } private static function fire_welcome( $wf ) { global $wpdb; $hours = max( 1, (int) ( $wf['trigger_config']['hours'] ?? 48 ) ); $since = gmdate( 'Y-m-d H:i:s', time() - $hours * HOUR_IN_SECONDS ); $runs = aieo_workflow_runs_table(); $no_test = ! AIEO_Automation_Test_Mode::any_test(); $args = array( (int) $wf['id'] ); if ( $no_test ) { $args[] = '%' . $wpdb->esc_like( '"_test":1' ) . '%'; } $args[] = $since; $args[] = self::BATCH; $uids = $wpdb->get_col( $wpdb->prepare( "SELECT u.ID FROM {$wpdb->users} u LEFT JOIN `{$runs}` r ON r.workflow_id = %d AND r.wp_user_id = u.ID AND NOT ( r.state = 'cancelled' AND r.step_index = 0 )" . ( $no_test ? ' AND r.context_json NOT LIKE %s' : '' ) . " WHERE u.user_registered >= %s AND r.id IS NULL ORDER BY u.ID ASC LIMIT %d", $args ) ); $rule = self::audience_rule( $wf ); foreach ( (array) $uids as $uid ) { $uid = (int) $uid; if ( $rule && ! AIEO_Audience_Rules::matches_user( $rule, $uid ) ) { continue; } if ( self::has_run_ever( (int) $wf['id'], $uid ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, array() ); } } private static function fire_back_in_stock( $wf ) { global $wpdb; $wl = $wpdb->prefix . 'aieo_waitlist'; if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wl ) ) !== $wl ) { return; } $after = 0; $guard = 0; do { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT w.id AS wl_id, u.ID AS wp_user_id, w.email, w.product_id, w.variation_id, COALESCE(NULLIF(w.variation_id,0), w.product_id) AS stock_pid FROM `{$wl}` w JOIN {$wpdb->postmeta} ss ON ss.post_id = COALESCE(NULLIF(w.variation_id,0), w.product_id) AND ss.meta_key = '_stock_status' AND ss.meta_value = 'instock' JOIN {$wpdb->posts} sp ON sp.ID = COALESCE(NULLIF(w.variation_id,0), w.product_id) AND sp.post_status = 'publish' JOIN {$wpdb->posts} pp ON pp.ID = w.product_id AND pp.post_status = 'publish' JOIN {$wpdb->users} u ON LOWER(u.user_email) COLLATE utf8mb4_general_ci = LOWER(w.email) COLLATE utf8mb4_general_ci WHERE w.status = 'subscribed' AND w.notified_at IS NULL AND w.product_id > 0 AND w.id > %d ORDER BY w.id ASC LIMIT %d", $after, self::BATCH ), ARRAY_A ); foreach ( (array) $rows as $r ) { $uid = (int) $r['wp_user_id']; $stock_pid = (int) $r['stock_pid']; $wl_id = (int) $r['wl_id']; if ( $wl_id > $after ) { $after = $wl_id; } if ( self::has_recent_run( (int) $wf['id'], $uid, 24 ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, array( 'product_id' => $stock_pid, 'wl_id' => $wl_id, 'wl_email' => (string) $r['email'] ) ); } $guard++; } while ( count( (array) $rows ) >= self::BATCH && $guard < 500 ); } private static function fire_win_back( $wf ) { global $wpdb; $rule = self::audience_rule( $wf ); if ( ! $rule || ! class_exists( 'AIEO_Audience_Rules' ) ) { return; } $ci = $wpdb->prefix . 'aieo_customer_intelligence'; if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $ci ) ) !== $ci ) { return; } $where = AIEO_Audience_Rules::compile( $rule ); $where = str_replace( '%', '%%', $where ); $a = AIEO_Audience_Rules::CI_ALIAS; $meta_key = 'aieo_winback_fired_cohort_' . (int) $wf['id']; $after = 0; $guard = 0; do { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT {$a}.wp_user_id, {$a}.dormancy_cohort FROM `{$ci}` {$a} WHERE ({$where}) AND {$a}.wp_user_id > %d ORDER BY {$a}.wp_user_id ASC LIMIT %d", $after, self::BATCH ), ARRAY_A ); foreach ( (array) $rows as $r ) { $uid = (int) $r['wp_user_id']; $coh = (string) $r['dormancy_cohort']; if ( $uid > $after ) { $after = $uid; } if ( '' === $coh ) { continue; } if ( (string) get_user_meta( $uid, $meta_key, true ) === $coh ) { continue; } if ( AIEO_Automation_Test_Mode::any_test() && (string) get_user_meta( $uid, $meta_key . AIEO_Automation_Test_Mode::SUFFIX, true ) === $coh ) { continue; } if ( self::has_recent_run( (int) $wf['id'], $uid, self::WINBACK_MIN_GAP_HOURS ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, array( 'cohort' => $coh, 'winback_meta_key' => $meta_key ) ); } $guard++; } while ( count( (array) $rows ) >= self::BATCH && $guard < 500 ); } private static function guard_test_sql() { global $wpdb; if ( AIEO_Automation_Test_Mode::any_test() ) { return ''; } return $wpdb->prepare( ' AND context_json NOT LIKE %s', '%' . $wpdb->esc_like( '"_test":1' ) . '%' ); } private static function has_run_ever( $wf_id, $uid ) { global $wpdb; $t = aieo_workflow_runs_table(); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE workflow_id = %d AND wp_user_id = %d AND NOT ( state = 'cancelled' AND step_index = 0 )", (int) $wf_id, (int) $uid ) . self::guard_test_sql() ) > 0; } private static function push_text( $html, $coupon = null ) { $t = preg_replace( '~|~i', "\n", (string) $html ); $t = html_entity_decode( wp_strip_all_tags( (string) $t ), ENT_QUOTES, 'UTF-8' ); $lines = array(); foreach ( preg_split( '~\R~u', $t ) ?: array() as $line ) { $line = trim( (string) preg_replace( '~[ \t\x{00A0}]+~u', ' ', (string) $line ) ); if ( '' !== $line ) { $lines[] = $line; } } if ( is_array( $coupon ) && ! empty( $coupon['code'] ) ) { $lines[] = (string) $coupon['label'] . ': ' . strtoupper( (string) $coupon['code'] ); } return implode( "\n", $lines ); } private static function service_address( $run, array $ctx ) { $uid = (int) ( $run['wp_user_id'] ?? 0 ); if ( $uid > 0 ) { $u = get_userdata( $uid ); return $u ? strtolower( (string) $u->user_email ) : ''; } return strtolower( (string) ( $ctx['guest_email'] ?? '' ) ); } private static function tester_addresses( $run, array $ctx ) { $uid = (int) ( $run['wp_user_id'] ?? 0 ); $u = ( $uid > 0 && AIEO_Automation_Test_Mode::is_tester( $uid ) ) ? get_userdata( $uid ) : null; if ( ! $u ) { return $ctx; } foreach ( array( 'bill_email', 'wl_email', 'guest_email' ) as $k ) { $a = (string) ( $ctx[ $k ] ?? '' ); if ( '' !== $a && ! AIEO_Automation_Test_Mode::is_tester( 0, $a ) ) { $ctx[ $k ] = strtolower( (string) $u->user_email ); } } return $ctx; } private static function dedup_args( $wf, $i ) { $week = 7 * DAY_IN_SECONDS; $secs = $week; if ( 'scheduled' === (string) ( $wf['trigger_type'] ?? '' ) ) { $every = array( 'daily' => DAY_IN_SECONDS, 'weekly' => $week, 'monthly' => 30 * DAY_IN_SECONDS ); $secs = (int) min( $week, max( HOUR_IN_SECONDS, ( $every[ (string) ( $wf['trigger_config']['interval'] ?? 'weekly' ) ] ?? $week ) - 4 * HOUR_IN_SECONDS ) ); } return array( 'dedup_step' => max( 0, (int) $i ), 'dedup_secs' => $secs ); } private static function fire_order_completed( $wf ) { if ( ! function_exists( 'wc_get_orders' ) ) { return; } $cfg = (array) ( $wf['trigger_config'] ?? array() ); $status = self::order_trigger_status( $wf ); $rules = ( isset( $cfg['order_rules'] ) && is_array( $cfg['order_rules'] ) ) ? $cfg['order_rules'] : array(); $crule = self::audience_rule( $wf ); $date_field = ( 'completed' === $status ) ? 'date_completed' : ( ( 'processing' === $status ) ? 'date_paid' : 'date_modified' ); $since = time() - self::ORDER_LOOKBACK_HOURS * HOUR_IN_SECONDS; $date_arg = '>=' . $since; if ( 'checkout-draft' === $status ) { if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '11.0', '>=' ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( '[AIEO] Workflow #%d triggers on order status checkout-draft, which WooCommerce %s no longer creates. Switch it to the "Abandoned checkout (cart recovery)" trigger.', isset( $wf['id'] ) ? (int) $wf['id'] : 0, WC_VERSION ) ); } return; } $upper = time() - self::CHECKOUT_DRAFT_GRACE_MIN * MINUTE_IN_SECONDS; $date_arg = $since . '...' . max( $since, $upper ); } $page = 1; $guard = 0; do { $ids = wc_get_orders( array( 'status' => 'wc-' . $status, 'limit' => self::BATCH, 'paged' => $page, 'orderby' => 'ID', 'order' => 'ASC', $date_field => $date_arg, 'return' => 'ids', ) ); $n = count( (array) $ids ); foreach ( (array) $ids as $oid ) { $oid = (int) $oid; if ( self::has_run_for_order( (int) $wf['id'], $oid ) ) { continue; } $order = wc_get_order( $oid ); if ( ! $order ) { continue; } if ( apply_filters( 'aieo_suppress_order_customer_email', false, $order, 'workflow' ) ) { continue; } if ( class_exists( 'AIEO_Order_Rules' ) && ! AIEO_Order_Rules::matches( $order, $rules ) ) { continue; } $uid = (int) $order->get_customer_id(); if ( $crule && class_exists( 'AIEO_Audience_Rules' ) ) { $bill_email = (string) $order->get_billing_email(); $in_audience = $uid > 0 ? AIEO_Audience_Rules::matches_user( $crule, $uid ) : AIEO_Audience_Rules::matches_email( $crule, $bill_email ); if ( ! $in_audience ) { continue; } } $email = (string) $order->get_billing_email(); if ( '' === $email ) { continue; } $ehash = md5( strtolower( trim( $email ) ) ); if ( ! empty( $cfg['once_per_customer'] ) && ( $uid > 0 ? self::has_run_ever( (int) $wf['id'], $uid ) : self::has_email_run( (int) $wf['id'], $ehash, -1 ) ) ) { continue; } $sid = ( $uid > 0 && class_exists( 'AIEO_Subject_Resolver' ) ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, array( 'order_id' => $oid, 'bill_email' => $email ), true, ( ! empty( $cfg['once_per_customer'] ) && $uid <= 0 ) ? $ehash : '' ); } $page++; $guard++; } while ( $n >= self::BATCH && $guard < 100 ); } private static function has_run_for_order( $wf_id, $order_id ) { global $wpdb; $t = aieo_workflow_runs_table(); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE workflow_id = %d AND order_id = %d AND NOT ( state = 'cancelled' AND step_index = 0 )", (int) $wf_id, (int) $order_id ) . self::guard_test_sql() ) > 0; } private static function order_trigger_statuses() { return array( 'completed', 'processing', 'on-hold', 'pending', 'cancelled', 'refunded', 'failed', 'checkout-draft' ); } private static function order_trigger_status( $wf ) { $s = (string) ( $wf['trigger_config']['status'] ?? 'completed' ); return in_array( $s, self::order_trigger_statuses(), true ) ? $s : 'completed'; } private static function order_still_qualifies( $ctx, $wf ) { $oid = (int) ( $ctx['order_id'] ?? 0 ); if ( $oid <= 0 || ! function_exists( 'wc_get_order' ) ) { return false; } $order = wc_get_order( $oid ); if ( ! $order instanceof WC_Order ) { return false; } $want = self::order_trigger_status( $wf ); $current = $order->get_status(); $still = ( $current === $want ) || ( 'processing' === $want && 'completed' === $current ); if ( ! $still ) { return false; } $rules = ( isset( $wf['trigger_config']['order_rules'] ) && is_array( $wf['trigger_config']['order_rules'] ) ) ? $wf['trigger_config']['order_rules'] : array(); return ! ( class_exists( 'AIEO_Order_Rules' ) && ! AIEO_Order_Rules::matches( $order, $rules ) ); } private static function fire_scheduled( $wf ) { $interval = (string) ( $wf['trigger_config']['interval'] ?? 'weekly' ); $secs = array( 'daily' => DAY_IN_SECONDS, 'weekly' => 7 * DAY_IN_SECONDS, 'monthly' => 30 * DAY_IN_SECONDS ); $gap = $secs[ $interval ] ?? ( 7 * DAY_IN_SECONDS ); if ( AIEO_Automation_Test_Mode::any_test() && ! self::reaches_customers( $wf ) ) { $key = 'aieo_wf_sched_' . (int) $wf['id'] . AIEO_Automation_Test_Mode::SUFFIX; if ( get_transient( $key ) ) { return; } set_transient( $key, time(), max( HOUR_IN_SECONDS, $gap - HOUR_IN_SECONDS ) ); self::enqueue_audience( $wf ); return; } $last = $wf['last_run_at'] ? strtotime( $wf['last_run_at'] . ' UTC' ) : 0; if ( $last && ( time() - $last ) < $gap ) { return; } self::enqueue_audience( $wf ); self::mark_run( $wf['id'] ); } public static function run_for_tester( $workflow_id, $wp_user_id, $email = '' ) { global $wpdb; if ( ! AIEO_Automation_Test_Mode::any_test() ) { return new WP_Error( 'aieo_not_test', __( 'Switch e-mail or push to Test first.', 'ai-eshop-optimizer' ) ); } $wf = AIEO_Workflows::get( (int) $workflow_id ); if ( ! $wf ) { return new WP_Error( 'aieo_no_workflow', __( 'Workflow not found.', 'ai-eshop-optimizer' ) ); } $uid = (int) $wp_user_id; $email = strtolower( sanitize_email( (string) $email ) ); if ( $uid <= 0 && '' !== $email ) { $u = get_user_by( 'email', $email ); $uid = $u ? (int) $u->ID : 0; } if ( ! AIEO_Automation_Test_Mode::is_tester( $uid, $email ) ) { return new WP_Error( 'aieo_not_tester', __( 'Only a tester can be run. Add them to the testers first.', 'ai-eshop-optimizer' ) ); } $ctx = self::tester_context( $wf, $uid, $email ); $sid = ( $uid > 0 && class_exists( 'AIEO_Subject_Resolver' ) ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; if ( $uid <= 0 ) { $ctx['guest_email'] = $email; } if ( 1 !== (int) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', AIEO_Automation_Test_Mode::lock_name(), 10 ) ) ) { return new WP_Error( 'aieo_busy', __( 'The automations are working through their queue right now. Try again in a minute.', 'ai-eshop-optimizer' ) ); } try { self::$last_run_id = 0; if ( ! self::enqueue_run( $wf, $uid, $sid, $ctx, true, $uid > 0 ? '' : md5( $email ) ) || self::$last_run_id <= 0 ) { return new WP_Error( 'aieo_no_run', __( 'The run could not be started.', 'ai-eshop-optimizer' ) ); } $t = aieo_workflow_runs_table(); $run = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$t}` WHERE id = %d", self::$last_run_id ), ARRAY_A ); if ( ! $run ) { return new WP_Error( 'aieo_no_run', __( 'The run could not be started.', 'ai-eshop-optimizer' ) ); } self::process_run( $run ); } finally { $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', AIEO_Automation_Test_Mode::lock_name() ) ); } $after = $wpdb->get_row( $wpdb->prepare( "SELECT state, context_json FROM `{$t}` WHERE id = %d", (int) $run['id'] ), ARRAY_A ); $after_ctx = json_decode( (string) ( $after['context_json'] ?? '' ), true ); return array( 'run_id' => (int) $run['id'], 'state' => (string) ( $after['state'] ?? '' ), 'delivered' => is_array( $after_ctx ) && ! empty( $after_ctx['_delivered'] ) && empty( $after_ctx['_holdout'] ), ); } private static function tester_context( $wf, $wp_user_id, $email ) { $trigger = (string) ( $wf['trigger_type'] ?? '' ); $ctx = array(); $addr = '' !== $email ? $email : ( $wp_user_id > 0 ? strtolower( (string) get_userdata( $wp_user_id )->user_email ) : '' ); if ( in_array( $trigger, array( 'order_completed', 'guest_created', 'withdrawal_recorded', 'return_approved', 'survey_answered', 'survey_detractor', 'survey_promoter' ), true ) && function_exists( 'wc_get_orders' ) ) { $q = array( 'type' => 'shop_order', 'limit' => 1, 'orderby' => 'date', 'order' => 'DESC', 'return' => 'ids' ); $oids = $wp_user_id > 0 ? wc_get_orders( $q + array( 'customer' => $wp_user_id ) ) : array(); if ( empty( $oids ) && '' !== $addr ) { $oids = wc_get_orders( $q + array( 'billing_email' => $addr ) ); } if ( ! empty( $oids ) ) { $ctx['order_id'] = (int) $oids[0]; $ctx['bill_email'] = $addr; $o = wc_get_order( (int) $oids[0] ); if ( $o ) { $ctx['product_id'] = self::first_product_id( $o ); $ctx['bill_email'] = (string) $o->get_billing_email(); } } } if ( in_array( $trigger, array( 'abandoned_checkout', 'abandoned_cart' ), true ) && class_exists( 'AIEO_Carts_Store' ) ) { global $wpdb; $t = AIEO_Carts_Store::carts_table(); $row = $wpdb->get_row( $wpdb->prepare( "SELECT id, wc_order_id FROM `{$t}` WHERE kind = 'abandoned' AND ( ( %d > 0 AND user_id = %d ) OR customer_email = %s ) ORDER BY id DESC LIMIT 1", (int) $wp_user_id, (int) $wp_user_id, $addr ), ARRAY_A ); if ( $row ) { $ctx['cart_id'] = (int) $row['id']; $ctx['order_id'] = (int) $row['wc_order_id']; } } if ( in_array( $trigger, array( 'back_in_stock', 'abandoned_cart' ), true ) && empty( $ctx['product_id'] ) && function_exists( 'wc_get_products' ) ) { $pids = wc_get_products( array( 'status' => 'publish', 'stock_status' => 'instock', 'limit' => 1, 'return' => 'ids' ) ); if ( ! empty( $pids ) ) { $ctx['product_id'] = (int) $pids[0]; } } if ( 'back_in_stock' === $trigger ) { $ctx['wl_email'] = $addr; } return $ctx; } public static function run_manual( $workflow_id ) { $wf = AIEO_Workflows::get( (int) $workflow_id ); if ( ! $wf ) { return 0; } if ( ! in_array( (string) ( $wf['trigger_type'] ?? '' ), array( 'scheduled', 'manual' ), true ) ) { return 0; } if ( 'active' !== (string) ( $wf['status'] ?? '' ) ) { return 0; } if ( ! self::channels_live( $wf ) ) { return 0; } $n = self::enqueue_audience( $wf ); if ( ! AIEO_Automation_Test_Mode::any_test() || self::reaches_customers( $wf ) ) { self::mark_run( $wf['id'] ); } return $n; } public static function manual_refusal( $workflow_id ) { $wf = AIEO_Workflows::get( (int) $workflow_id ); if ( ! $wf ) { return __( 'Workflow not found.', 'ai-eshop-optimizer' ); } if ( ! in_array( (string) ( $wf['trigger_type'] ?? '' ), array( 'scheduled', 'manual' ), true ) ) { return __( 'Only a broadcast can be run now: the other automations start on their own event.', 'ai-eshop-optimizer' ); } if ( 'active' !== (string) ( $wf['status'] ?? '' ) ) { return __( 'Activate the workflow first: a draft or paused workflow sends nothing.', 'ai-eshop-optimizer' ); } if ( ! self::audience_rule( $wf ) ) { return __( 'Choose an audience first: a broadcast never goes to every customer.', 'ai-eshop-optimizer' ); } if ( ! self::channels_live( $wf ) ) { return __( 'No channel can send it: switch e-mail or push to Live, or to Test for the testers.', 'ai-eshop-optimizer' ); } return __( 'No one in the audience can get it now: the audience is empty, or everyone in it already had it.', 'ai-eshop-optimizer' ); } private static function enqueue_audience( $wf ) { $rule = self::audience_rule( $wf ); if ( ! $rule ) { return 0; } $once = ! empty( $wf['trigger_config']['once_per_customer'] ); $n = 0; $after = 0; $guard = 0; $test = AIEO_Automation_Test_Mode::any_test(); $seen = array(); do { $uids = AIEO_Audience_Rules::member_user_ids( $rule, $after, self::BATCH ); foreach ( $uids as $uid ) { $uid = (int) $uid; if ( $uid > $after ) { $after = $uid; } if ( $once && self::has_run_ever( (int) $wf['id'], $uid ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; $ctx = array(); if ( $test && AIEO_Automation_Test_Mode::is_tester( $uid ) ) { $seen[ $uid ] = true; $ctx = array( '_test_audience' => 1 ); } if ( self::enqueue_run( $wf, $uid, $sid, $ctx ) ) { $n++; } } $guard++; } while ( count( $uids ) >= self::BATCH && $guard < 500 ); if ( $test ) { $t = AIEO_Automation_Test_Mode::testers(); foreach ( $t['users'] as $uid ) { if ( isset( $seen[ $uid ] ) || ( $once && self::has_run_ever( (int) $wf['id'], $uid ) ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; if ( self::enqueue_run( $wf, $uid, $sid, array( '_test_audience' => 0 ) ) ) { $n++; } } foreach ( $t['emails'] as $email ) { $u = get_user_by( 'email', $email ); if ( $u ) { if ( isset( $seen[ (int) $u->ID ] ) || in_array( (int) $u->ID, $t['users'], true ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( (int) $u->ID ) : 0; $ok = self::enqueue_run( $wf, (int) $u->ID, $sid, array( '_test_audience' => 0 ) ); } else { $ok = self::enqueue_run( $wf, 0, 0, array( 'guest_email' => $email, '_test_audience' => 0 ), true, md5( $email ) ); } if ( $ok ) { $n++; } } } return $n; } private static function fire_abandoned_cart( $wf ) { global $wpdb; $a = $wpdb->prefix . 'aieo_addcart_events'; if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $a ) ) !== $a ) { return; } $hours = max( 1, (int) ( $wf['trigger_config']['hours'] ?? 4 ) ); $subj = $wpdb->prefix . 'aieo_subjects'; $coll = self::subjects_collation(); $rule = self::audience_rule( $wf ); $after = 0; $guard = 0; do { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT s.wp_user_id, MIN(a.product_id) AS product_id, MIN(s.subject_id) AS subject_id FROM `{$a}` a JOIN `{$subj}` s ON s.visitor_key = a.session_id{$coll} WHERE a.order_id = 0 AND a.product_id > 0 AND s.wp_user_id > %d AND s.merged_into = 0 AND a.ts_added <= (UTC_TIMESTAMP() - INTERVAL %d HOUR) AND a.ts_added >= (UTC_TIMESTAMP() - INTERVAL 7 DAY) GROUP BY s.wp_user_id ORDER BY s.wp_user_id ASC LIMIT %d", $after, $hours, self::BATCH ), ARRAY_A ); foreach ( (array) $rows as $r ) { $uid = (int) $r['wp_user_id']; $pid = (int) $r['product_id']; if ( $uid > $after ) { $after = $uid; } if ( $rule && ! AIEO_Audience_Rules::matches_user( $rule, $uid ) ) { continue; } if ( self::has_recent_run( (int) $wf['id'], $uid, self::ABANDON_DEDUP_HOURS ) ) { continue; } self::enqueue_run( $wf, $uid, (int) $r['subject_id'], array( 'product_id' => $pid ) ); } $guard++; } while ( count( (array) $rows ) >= self::BATCH && $guard < 500 ); } private static function subjects_collation() { global $wpdb; static $c = null; if ( null === $c ) { $subj = $wpdb->prefix . 'aieo_subjects'; $col = $wpdb->get_var( $wpdb->prepare( "SELECT COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'visitor_key' LIMIT 1", $subj ) ); $c = ( $col && preg_match( '/^[a-z0-9_]+$/', (string) $col ) ) ? ' COLLATE ' . $col : ''; } return $c; } private static function has_recent_run( $wf_id, $uid, $hours ) { global $wpdb; $t = aieo_workflow_runs_table(); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE workflow_id = %d AND wp_user_id = %d AND created >= (UTC_TIMESTAMP() - INTERVAL %d HOUR) AND NOT ( state = 'cancelled' AND step_index = 0 )", (int) $wf_id, (int) $uid, max( 1, (int) $hours ) ) . self::guard_test_sql() ) > 0; } private static function fire_birthday( $wf ) { $day = 'aieo_wf_bday_' . $wf['id'] . ( AIEO_Automation_Test_Mode::restricts() ? AIEO_Automation_Test_Mode::SUFFIX : '' ); if ( get_transient( $day ) ) { return; } set_transient( $day, 1, DAY_IN_SECONDS ); $days = max( 0, (int) ( $wf['trigger_config']['days_before'] ?? 14 ) ); $target_ts = time() + $days * DAY_IN_SECONDS; $target = gmdate( 'm-d', $target_ts ); $targets = array( $target ); if ( '03-01' === $target && ! (bool) gmdate( 'L', $target_ts ) ) { $targets[] = '02-29'; } global $wpdb; $in = implode( ',', array_fill( 0, count( $targets ), '%s' ) ); $params = array_merge( $targets, $targets, array( self::BATCH ) ); $uids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT um.user_id FROM {$wpdb->usermeta} um WHERE ( um.meta_key = 'aieo_birthday_md' AND um.meta_value IN ({$in}) ) OR ( um.meta_key = '_automatewoo_birthday_md' AND um.meta_value IN ({$in}) AND NOT EXISTS ( SELECT 1 FROM {$wpdb->usermeta} um2 WHERE um2.user_id = um.user_id AND um2.meta_key = 'aieo_birthday_md' AND um2.meta_value <> '' ) ) LIMIT %d", $params ) ); $rule = self::audience_rule( $wf ); foreach ( (array) $uids as $uid ) { $uid = (int) $uid; if ( $rule && ! AIEO_Audience_Rules::matches_user( $rule, $uid ) ) { continue; } $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; self::enqueue_run( $wf, $uid, $sid, array() ); } } private static function enqueue_run( $wf, $wp_user_id, $subject_id, $context, $skip_user_dedup = false, $email_hash = '' ) { global $wpdb; $t = aieo_workflow_runs_table(); $now = gmdate( 'Y-m-d H:i:s' ); if ( AIEO_Automation_Test_Mode::any_test() ) { $who = (string) ( $context['guest_email'] ?? ( $context['bill_email'] ?? ( $context['wl_email'] ?? '' ) ) ); $tester = AIEO_Automation_Test_Mode::is_tester( (int) $wp_user_id, $who ); if ( $tester ) { $context['_test'] = 1; } elseif ( ! self::reaches_customers( $wf ) ) { return false; } } if ( ! $skip_user_dedup ) { $open = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE workflow_id = %d AND wp_user_id = %d AND state IN ('pending','running')", (int) $wf['id'], (int) $wp_user_id ) . self::guard_test_sql() ); if ( $open > 0 ) { return false; } } $ok = $wpdb->insert( $t, array( 'workflow_id' => (int) $wf['id'], 'subject_id' => (int) $subject_id, 'wp_user_id' => (int) $wp_user_id, 'order_id' => (int) ( $context['order_id'] ?? 0 ), 'email_hash' => (string) $email_hash, 'step_index' => 0, 'state' => 'pending', 'context_json' => wp_json_encode( (array) $context ), 'run_at' => $now, 'created' => $now, 'updated' => $now, ) ); if ( false === $ok ) { return false; } self::$last_run_id = (int) $wpdb->insert_id; if ( ! empty( $context['_test'] ) ) { return true; } $wpdb->query( $wpdb->prepare( 'UPDATE `' . aieo_workflows_table() . '` SET last_run_at = %s WHERE id = %d', $now, (int) $wf['id'] ) ); return true; } private static function enqueue_email_run( $wf, $email, $context, $dedup_hours = 0 ) { $email = strtolower( sanitize_email( (string) $email ) ); if ( '' === $email || ! is_email( $email ) ) { return false; } $hash = md5( $email ); if ( 0 !== (int) $dedup_hours && self::has_email_run( (int) $wf['id'], $hash, (int) $dedup_hours ) ) { return false; } $uid = 0; $sid = 0; $u = get_user_by( 'email', $email ); if ( $u instanceof WP_User ) { $uid = (int) $u->ID; $sid = class_exists( 'AIEO_Subject_Resolver' ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; } $context['guest_email'] = $email; return self::enqueue_run( $wf, $uid, $sid, $context, true, $hash ); } private static function has_email_run( $wf_id, $email_hash, $within_hours ) { global $wpdb; $t = aieo_workflow_runs_table(); if ( (int) $within_hours > 0 ) { return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE workflow_id = %d AND email_hash = %s AND ( created >= (UTC_TIMESTAMP() - INTERVAL %d HOUR) OR state IN ('pending','running') ) AND NOT ( state = 'cancelled' AND step_index = 0 )", (int) $wf_id, (string) $email_hash, (int) $within_hours ) . self::guard_test_sql() ) > 0; } return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE workflow_id = %d AND email_hash = %s AND NOT ( state = 'cancelled' AND step_index = 0 )", (int) $wf_id, (string) $email_hash ) . self::guard_test_sql() ) > 0; } private static function process_runs( $limit ) { global $wpdb; $t = aieo_workflow_runs_table(); $now = gmdate( 'Y-m-d H:i:s' ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$t}` WHERE state IN ('pending','running') AND run_at <= %s ORDER BY run_at ASC LIMIT %d", $now, (int) $limit ), ARRAY_A ); if ( empty( $rows ) ) { return false; } foreach ( $rows as $run ) { self::process_run( $run ); } return count( $rows ) >= $limit; } private static function process_run( $run ) { global $wpdb; $now = $wpdb->get_row( $wpdb->prepare( 'SELECT state, step_index, context_json FROM `' . aieo_workflow_runs_table() . '` WHERE id = %d', (int) $run['id'] ), ARRAY_A ); if ( ! $now || ! in_array( (string) $now['state'], array( 'pending', 'running' ), true ) ) { return; } $run = array_merge( $run, $now ); $ctx = json_decode( (string) $run['context_json'], true ) ?: array(); if ( ! empty( $ctx['_test'] ) ) { AIEO_Automation_Test_Mode::enter(); try { self::process_run_steps( $run, $ctx ); } finally { AIEO_Automation_Test_Mode::leave(); } return; } self::process_run_steps( $run, $ctx ); } private static function process_run_steps( $run, $ctx ) { global $wpdb; $t = aieo_workflow_runs_table(); $wf = AIEO_Workflows::get( (int) $run['workflow_id'] ); if ( ! $wf || ( 'active' !== $wf['status'] && empty( $ctx['_test'] ) ) ) { $si = ( empty( $ctx['_delivered'] ) && empty( $ctx['_acted'] ) ) ? 0 : (int) $run['step_index']; $wpdb->update( $t, array( 'state' => 'cancelled', 'step_index' => $si, 'updated' => gmdate( 'Y-m-d H:i:s' ) ), array( 'id' => (int) $run['id'] ) ); return; } $steps = (array) $wf['steps']; $i = (int) $run['step_index']; while ( $i < count( $steps ) ) { $step = $steps[ $i ]; $type = (string) ( $step['type'] ?? '' ); $asking = false; if ( 'ask_survey' === $type ) { if ( ! class_exists( 'AIEO_Survey_Ask' ) ) { $i++; continue; } $asked = AIEO_Survey_Ask::prepare( $step, $wf, $run, $ctx ); if ( ! $asked ) { $i++; continue; } $ctx['_aieo_survey'] = $asked['survey']; $step = $asked['step']; $type = 'send'; $asking = true; } if ( 'wait' === $type ) { $secs = ( 'hours' === ( $step['unit'] ?? 'days' ) ? HOUR_IN_SECONDS : DAY_IN_SECONDS ) * max( 0, (float) ( $step['value'] ?? 0 ) ); $run_at = gmdate( 'Y-m-d H:i:s', time() + (int) $secs ); $wpdb->update( $t, array( 'step_index' => $i + 1, 'run_at' => $run_at, 'state' => 'pending', 'context_json' => wp_json_encode( $ctx ), 'updated' => gmdate( 'Y-m-d H:i:s' ) ), array( 'id' => (int) $run['id'] ) ); return; } if ( 'send' === $type ) { $tt = (string) ( $wf['trigger_type'] ?? '' ); $recheck_on = ! empty( $wf['trigger_config']['recheck'] ) || ( 'order_completed' === $tt && 'checkout-draft' === self::order_trigger_status( $wf ) ) || 'abandoned_checkout' === $tt; $recheck_fail = $recheck_on && ( ( 'order_completed' === $tt && ! self::order_still_qualifies( $ctx, $wf ) ) || ( 'abandoned_checkout' === $tt && ! self::cart_still_abandoned( $ctx ) ) ); if ( $recheck_fail ) { $ctx['_recheck_failed'] = 1; $wpdb->update( $t, array( 'state' => 'cancelled', 'step_index' => 0, 'context_json' => wp_json_encode( $ctx ), 'updated' => gmdate( 'Y-m-d H:i:s' ) ), array( 'id' => (int) $run['id'] ) ); return; } if ( ! empty( $wf['trigger_config']['optimize_send_time'] ) && empty( $ctx['_std_done'] ) && class_exists( 'AIEO_Send_Time' ) ) { $std_ch = (string) ( $step['channel'] ?? 'cascade' ); $std_live = ( 'email' === $std_ch ) ? self::channel_live( 'email' ) : ( ( 'push' === $std_ch ) ? self::channel_live( 'push' ) : ( self::channel_live( 'email' ) || self::channel_live( 'push' ) ) ); if ( $std_live ) { $best = AIEO_Send_Time::best_hour( (int) $run['wp_user_id'], (string) ( $ctx['guest_email'] ?? '' ) ); if ( null !== $best ) { $ctx['_std_done'] = 1; $delay = (int) AIEO_Send_Time::delay_to_hour( $best ); if ( $delay > 0 ) { $wpdb->update( $t, array( 'state' => 'pending', 'step_index' => $i, 'run_at' => gmdate( 'Y-m-d H:i:s', time() + $delay ), 'context_json' => wp_json_encode( $ctx ), 'updated' => gmdate( 'Y-m-d H:i:s' ), ), array( 'id' => (int) $run['id'] ) ); return; } } } } if ( ! empty( $ctx['_awaits_consent'] ) && 'email' === (string) ( $step['channel'] ?? 'cascade' ) && class_exists( 'AIEO_Email_Channel' ) && '' === AIEO_Email_Channel::resolve_recipient( (int) $run['subject_id'], (int) $run['wp_user_id'] ) ) { $outcome = 'terminal'; self::$send_grant = 0; self::$send_reason = 'no_consented_email'; } else { $outcome = self::do_send( $step, $wf, $run, $ctx, $i ); } if ( 'terminal' === $outcome && in_array( self::$send_reason, array( 'no_consented_email', 'no_consent' ), true ) && 'welcome' === (string) ( $wf['trigger_type'] ?? '' ) && empty( $ctx['_delivered'] ) && (int) $run['wp_user_id'] > 0 ) { $u = get_userdata( (int) $run['wp_user_id'] ); $until = ( $u ? (int) strtotime( $u->user_registered . ' UTC' ) : 0 ) + max( 1, (int) ( $wf['trigger_config']['hours'] ?? 48 ) ) * HOUR_IN_SECONDS; $now = gmdate( 'Y-m-d H:i:s' ); self::withdraw_send_coupon(); if ( time() < $until ) { $ctx['_awaits_consent'] = 1; $wpdb->update( $t, array( 'state' => 'pending', 'step_index' => $i, 'run_at' => gmdate( 'Y-m-d H:i:s', min( $until, time() + HOUR_IN_SECONDS ) ), 'context_json' => wp_json_encode( $ctx ), 'updated' => $now ), array( 'id' => (int) $run['id'] ) ); return; } self::withdraw_send_coupon(); $wpdb->update( $t, array( 'state' => 'cancelled', 'step_index' => 0, 'context_json' => wp_json_encode( $ctx ), 'updated' => $now ), array( 'id' => (int) $run['id'] ) ); return; } unset( $ctx['_awaits_consent'] ); if ( $asking && 'sent' !== $outcome ) { unset( $ctx['_aieo_survey'] ); } self::note_send_outcome( $outcome, $wf, $ctx ); if ( 'paused' === $outcome ) { $ctx['_held'] = self::carrier( $step, $wf, $run, $ctx ); } else { unset( $ctx['_held'] ); } $guarded = self::is_guarded_trigger( $wf ); $reschedule = ( 'retry' === $outcome ) || ( 'paused' === $outcome && $guarded ); if ( 'paused' === $outcome && ! $guarded ) { self::withdraw_send_coupon(); $si = ( empty( $ctx['_delivered'] ) && empty( $ctx['_acted'] ) ) ? 0 : $i; $wpdb->update( $t, array( 'state' => 'cancelled', 'step_index' => $si, 'context_json' => wp_json_encode( $ctx ), 'updated' => gmdate( 'Y-m-d H:i:s' ) ), array( 'id' => (int) $run['id'] ) ); return; } if ( $reschedule ) { if ( 'retry' === $outcome ) { $key = '_send_retries_' . $i; $retries = (int) ( $ctx[ $key ] ?? 0 ) + 1; if ( $retries > self::MAX_SEND_RETRIES ) { $reschedule = false; } else { $ctx[ $key ] = $retries; } } if ( $reschedule ) { $wpdb->update( $t, array( 'state' => 'pending', 'step_index' => $i, 'run_at' => gmdate( 'Y-m-d H:i:s', time() + HOUR_IN_SECONDS ), 'context_json' => wp_json_encode( $ctx ), 'updated' => gmdate( 'Y-m-d H:i:s' ), ), array( 'id' => (int) $run['id'] ) ); return; } } if ( ! in_array( $outcome, array( 'sent', 'holdout' ), true ) ) { self::withdraw_send_coupon(); } self::stamp_guard_on_terminal( $wf, $run, $ctx ); } if ( ( 'custom_function' === $type || in_array( $type, self::MUTATION_STEPS, true ) ) && ! empty( $ctx['_test'] ) ) { $ctx['_test_skipped'] = (array) ( $ctx['_test_skipped'] ?? array() ); $ctx['_test_skipped'][] = self::would_do( $type, $step, $ctx, $i ); } elseif ( 'custom_function' === $type ) { self::do_custom_function( $step, $wf, $run, $ctx ); $ctx['_acted'] = 1; } elseif ( in_array( $type, self::MUTATION_STEPS, true ) ) { self::do_mutation( $type, $step, $wf, $run, $ctx ); $ctx['_acted'] = 1; } $i++; } $wpdb->update( $t, array( 'state' => 'done', 'step_index' => $i, 'context_json' => wp_json_encode( $ctx ), 'updated' => gmdate( 'Y-m-d H:i:s' ) ), array( 'id' => (int) $run['id'] ) ); } private static function note_send_outcome( $outcome, $wf, array &$ctx ) { if ( 'sent' === $outcome ) { $ctx['_delivered'] = 1; if ( empty( $ctx['_reached'] ) && empty( $ctx['_test'] ) ) { $ctx['_reached'] = 1; global $wpdb; $wpdb->query( $wpdb->prepare( 'UPDATE `' . aieo_workflows_table() . '` SET runs_count = runs_count + 1 WHERE id = %d', (int) $wf['id'] ) ); } } elseif ( 'holdout' === $outcome ) { $ctx['_delivered'] = 1; $ctx['_holdout'] = 1; } } private static function reaches_customers( $wf ) { if ( AIEO_Automation_Test_Mode::restricts() ) { return false; } $email = AIEO_Automation_Test_Mode::engine_live( 'email' ); $push = AIEO_Automation_Test_Mode::engine_live( 'push' ); $trigger = (string) ( $wf['trigger_type'] ?? '' ); $sends = false; foreach ( (array) ( $wf['steps'] ?? array() ) as $s ) { if ( ! in_array( (string) ( $s['type'] ?? '' ), array( 'send', 'ask_survey' ), true ) ) { continue; } $sends = true; $ch = in_array( $trigger, array_merge( array( 'order_completed', 'back_in_stock' ), self::SERVICE_TRIGGERS ), true ) ? 'email' : (string) ( $s['channel'] ?? 'cascade' ); if ( ( 'email' === $ch && $email ) || ( 'push' === $ch && $push ) || ( 'email' !== $ch && 'push' !== $ch && ( $email || $push ) ) ) { return true; } } return ! $sends; } private static function carrier( $step, $wf, $run, $ctx ) { $trigger = (string) ( $wf['trigger_type'] ?? '' ); $channel = (string) ( $step['channel'] ?? 'cascade' ); if ( 'order_completed' === $trigger || ( 'back_in_stock' === $trigger && (int) ( $ctx['wl_id'] ?? 0 ) > 0 ) || in_array( $trigger, self::SERVICE_TRIGGERS, true ) ) { return 'email'; } if ( (int) $run['wp_user_id'] <= 0 && '' !== (string) ( $ctx['guest_email'] ?? '' ) ) { return 'email' === $channel ? 'email' : 'cascade'; } if ( 'cascade' === $channel && (int) $run['subject_id'] <= 0 ) { return 'email'; } return in_array( $channel, array( 'email', 'push' ), true ) ? $channel : 'cascade'; } public static function mark_held_runs() { global $wpdb; if ( ! self::ready() ) { return -1; } $t = aieo_workflow_runs_table(); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$t}` WHERE state = 'pending' AND TIMESTAMPDIFF( SECOND, updated, run_at ) BETWEEN 3540 AND 3660 AND ( context_json IS NULL OR ( context_json NOT LIKE %s AND context_json NOT LIKE %s ) ) ORDER BY id ASC LIMIT 20000", '%' . $wpdb->esc_like( '"_test":1' ) . '%', '%' . $wpdb->esc_like( '"_held":' ) . '%' ), ARRAY_A ); $live = array( 'email' => self::channel_live( 'email' ), 'push' => self::channel_live( 'push' ), ); $n = 0; $wfs = array(); foreach ( (array) $rows as $run ) { $wid = (int) $run['workflow_id']; if ( ! array_key_exists( $wid, $wfs ) ) { $wfs[ $wid ] = AIEO_Workflows::get( $wid ); } $wf = $wfs[ $wid ]; $step = $wf ? ( $wf['steps'][ (int) $run['step_index'] ] ?? null ) : null; if ( ! is_array( $step ) || ! in_array( (string) ( $step['type'] ?? '' ), array( 'send', 'ask_survey' ), true ) ) { continue; } $ctx = json_decode( (string) $run['context_json'], true ) ?: array(); $carrier = self::carrier( $step, $wf, $run, $ctx ); $closed = ( 'cascade' === $carrier ) ? ( ! $live['email'] && ! $live['push'] ) : ! $live[ $carrier ]; if ( ! $closed ) { continue; } $ctx['_held'] = $carrier; $n += (int) $wpdb->update( $t, array( 'context_json' => wp_json_encode( $ctx ) ), array( 'id' => (int) $run['id'] ) ); } return $n; } private static function is_guarded_trigger( $wf ) { return in_array( (string) ( $wf['trigger_type'] ?? '' ), array( 'welcome', 'birthday', 'back_in_stock', 'win_back', 'abandoned_cart', 'order_completed', 'opted_in', 'opted_out', 'guest_created', 'abandoned_checkout', 'survey_detractor', 'survey_promoter', 'survey_answered', 'withdrawal_recorded', 'return_approved' ), true ); } private static function do_custom_function( $step, $wf, $run, $ctx ) { $fn = trim( (string) ( $step['function_name'] ?? '' ) ); if ( '' === $fn ) { return; } $payload = array( 'function' => $fn, 'workflow' => $wf, 'run' => $run, 'context' => $ctx, 'order_id' => (int) ( $ctx['order_id'] ?? 0 ), 'wp_user_id' => (int) ( $run['wp_user_id'] ?? 0 ), 'subject_id' => (int) ( $run['subject_id'] ?? 0 ), ); do_action( 'aieo_workflow_custom_function', $fn, $payload ); do_action( 'aieo_workflow_custom_function_' . $fn, $payload ); if ( function_exists( $fn ) && (bool) apply_filters( 'aieo_workflow_allow_custom_function', false, $fn, $payload ) ) { call_user_func( $fn, $payload ); } } private static function do_mutation( $type, $step, $wf, $run, $ctx ) { $oid = (int) ( $ctx['order_id'] ?? 0 ); $uid = (int) ( $run['wp_user_id'] ?? 0 ); $order = ( $oid > 0 && function_exists( 'wc_get_order' ) ) ? wc_get_order( $oid ) : null; if ( ! $order instanceof WC_Order ) { $order = null; } if ( $uid <= 0 && $order ) { $uid = (int) $order->get_customer_id(); } switch ( $type ) { case 'change_order_status': if ( ! $order ) { return; } $to = sanitize_key( (string) ( $step['order_status'] ?? '' ) ); $allowed = array( 'processing', 'completed', 'on-hold', 'cancelled', 'refunded', 'pending', 'failed' ); if ( ! in_array( $to, $allowed, true ) || $order->get_status() === $to ) { return; } $note = trim( (string) ( $step['note'] ?? '' ) ); $order->update_status( $to, ( '' !== $note ? $note . ' ' : '' ) . '[AIEO workflow]', false ); break; case 'order_add_note': if ( ! $order ) { return; } $note = trim( (string) ( $step['note'] ?? '' ) ); if ( '' === $note ) { return; } $order->add_order_note( $note, empty( $step['customer_note'] ) ? 0 : 1, false ); break; case 'customer_change_role': $role = sanitize_key( (string) ( $step['role'] ?? '' ) ); $mode = (string) ( $step['mode'] ?? 'replace' ); if ( $uid <= 0 || ! class_exists( 'AIEO_Safe_Roles' ) || ! AIEO_Safe_Roles::is_safe( $role ) || ! AIEO_Safe_Roles::is_customer_account( $uid ) ) { return; } if ( 'remove' === $mode ) { AIEO_Safe_Roles::revoke( $uid, $role, 'workflow' ); break; } AIEO_Safe_Roles::grant( $uid, $role, 'workflow' ); if ( 'replace' === $mode ) { $u = get_userdata( $uid ); foreach ( $u ? (array) $u->roles : array() as $other ) { if ( $other !== $role ) { AIEO_Safe_Roles::revoke( $uid, (string) $other, 'workflow' ); } } } break; case 'order_update_meta': if ( ! $order ) { return; } $k = self::clean_meta_key( $step['meta_key'] ?? '' ); if ( '' === $k ) { return; } $order->update_meta_data( $k, self::meta_step_value( $step, $wf, $run, $ctx ) ); $order->save(); break; case 'customer_update_meta': $k = self::clean_meta_key( $step['meta_key'] ?? '' ); if ( $uid <= 0 || '' === $k || self::is_blocked_user_meta( $k ) ) { return; } update_user_meta( $uid, $k, self::meta_step_value( $step, $wf, $run, $ctx ) ); break; } } private static function would_do( $type, $step, $ctx, $i ) { $what = ''; switch ( $type ) { case 'change_order_status': $what = sanitize_key( (string) ( $step['order_status'] ?? '' ) ); break; case 'customer_change_role': $what = sanitize_key( (string) ( $step['role'] ?? '' ) ); break; case 'order_update_meta': case 'customer_update_meta': $what = self::clean_meta_key( $step['meta_key'] ?? '' ); break; case 'custom_function': $what = sanitize_text_field( (string) ( $step['function_name'] ?? '' ) ); break; } return array( 'i' => (int) $i, 'type' => (string) $type, 'order' => (int) ( $ctx['order_id'] ?? 0 ), 'what' => $what, 'mode' => 'customer_change_role' === $type ? sanitize_key( (string) ( $step['mode'] ?? 'replace' ) ) : '', ); } private static function meta_step_value( $step, $wf, $run, $ctx ) { $v = (string) ( $step['meta_value'] ?? '' ); if ( class_exists( 'AIEO_Merge_Tags' ) && false !== strpos( $v, '{{' ) ) { $known = array_flip( AIEO_Merge_Tags::CATALOG ); $out = preg_replace_callback( '/\{\{\s*([a-z0-9_.]+)[^}]*\}\}/u', static function ( $m ) use ( $known, $run, $ctx, $wf, $step ) { return isset( $known[ $m[1] ] ) ? AIEO_Merge_Tags::resolve( $m[0], $run, $ctx, $wf, $step, null, array(), 'text' ) : $m[0]; }, $v ); $v = null === $out ? $v : (string) $out; } return sanitize_text_field( $v ); } private static function clean_meta_key( $k ) { $k = trim( (string) $k ); return ( '' !== $k && preg_match( '/^[A-Za-z0-9_\-]+$/', $k ) ) ? $k : ''; } private static function is_blocked_user_meta( $k ) { global $wpdb; $lk = strtolower( $k ); if ( preg_match( '/(^|_)capabilities$|(^|_)user_level$|session_tokens|user-settings/', $lk ) ) { return true; } foreach ( array( $wpdb->prefix . 'capabilities', $wpdb->prefix . 'user_level', 'wp_capabilities', 'wp_user_level' ) as $bad ) { if ( $lk === strtolower( $bad ) ) { return true; } } return function_exists( 'is_protected_meta' ) && is_protected_meta( $k, 'user' ); } private static function stamp_guard_on_terminal( $wf, $run, $ctx ) { if ( 'win_back' === (string) ( $wf['trigger_type'] ?? '' ) && '' !== (string) ( $ctx['winback_meta_key'] ?? '' ) ) { $key = (string) $ctx['winback_meta_key'] . ( empty( $ctx['_test'] ) ? '' : AIEO_Automation_Test_Mode::SUFFIX ); update_user_meta( (int) $run['wp_user_id'], $key, (string) ( $ctx['cohort'] ?? '' ) ); } } private static function do_send( $step, $wf, $run, $ctx, $i = 0 ) { self::$send_grant = 0; self::$send_reason = ''; if ( AIEO_Automation_Test_Mode::in_scope() ) { $ctx = self::tester_addresses( $run, $ctx ); } if ( ! empty( $ctx['order_id'] ) && function_exists( 'wc_get_order' ) ) { $ctx_order = wc_get_order( (int) $ctx['order_id'] ); if ( $ctx_order && apply_filters( 'aieo_suppress_order_customer_email', false, $ctx_order, 'workflow' ) ) { return 'terminal'; } } $subject_id = (int) $run['subject_id']; $trigger_id = AIEO_Automation_Test_Mode::trigger_id( (int) $wf['id'] ); $is_test = AIEO_Automation_Test_Mode::in_scope(); $subject = (string) ( $step['subject'] ?? '' ); $body = (string) ( $step['body'] ?? '' ); $product_id = 0; $products = array(); $src = (string) ( $step['product_source'] ?? 'none' ); if ( 'trigger_product' === $src && ! empty( $ctx['product_id'] ) ) { $product_id = (int) $ctx['product_id']; $products[] = $product_id; } elseif ( 'recommendations' === $src && $subject_id > 0 && class_exists( 'AIEO_Behavioural_Reco' ) ) { foreach ( AIEO_Behavioural_Reco::recommend( $subject_id, 4 ) as $r ) { $products[] = (int) $r['item_id']; } $product_id = $products[0] ?? 0; } $channel = (string) ( $step['channel'] ?? 'cascade' ); $trigger_type = (string) ( $wf['trigger_type'] ?? '' ); $is_order = ( 'order_completed' === $trigger_type ); $service = in_array( $trigger_type, self::SERVICE_TRIGGERS, true ); $service_to = $service ? self::service_address( $run, $ctx ) : ''; $guest_addr = ( (int) $run['wp_user_id'] <= 0 ) ? (string) ( $ctx['guest_email'] ?? '' ) : ''; $is_guest_mail = ( '' !== $guest_addr ); $carrier_live = ( $is_order || $service ) ? self::channel_live( 'email' ) : ( $is_guest_mail ? ( self::channel_live( 'email' ) || self::channel_live( 'push' ) ) : self::channel_live( $channel ) ); $holdout_pct = (int) ( $wf['trigger_config']['holdout_pct'] ?? 0 ); if ( $carrier_live && $holdout_pct > 0 && ! $is_test && ! $is_order && ! $service && 'back_in_stock' !== $trigger_type ) { $hkey = ( (int) $run['wp_user_id'] > 0 ) ? ( 'u' . (int) $run['wp_user_id'] ) : ( 'e' . strtolower( trim( (string) ( $ctx['guest_email'] ?? '' ) ) ) ); if ( 'e' !== $hkey && self::in_holdout( (int) $wf['id'], $hkey, $holdout_pct ) ) { self::log_holdout( $trigger_id, $run, $ctx ); return 'holdout'; } } $copy_subj_field = 'subject'; $copy_body_field = 'body'; if ( class_exists( 'AIEO_Workflow_AB' ) ) { $ab_variants = AIEO_Workflow_AB::variants( $step ); if ( count( $ab_variants ) > 1 ) { $vi = $is_test ? ( (int) ( $run['id'] ?? 0 ) % count( $ab_variants ) ) : AIEO_Workflow_AB::choose( (int) $wf['id'], (int) $run['step_index'], (int) $run['wp_user_id'], (string) ( $ctx['guest_email'] ?? '' ), count( $ab_variants ) ); $subject = (string) ( $ab_variants[ $vi ]['subject'] ?? $subject ); $body = (string) ( $ab_variants[ $vi ]['body'] ?? $body ); if ( 1 === (int) $vi ) { $copy_subj_field = 'variant_subject'; $copy_body_field = 'variant_body'; } } } if ( class_exists( 'AIEO_Campaign_I18n' ) && AIEO_Campaign_I18n::is_active() ) { $wpml_lang = AIEO_Campaign_I18n::recipient_language_code( (int) $run['wp_user_id'], $guest_addr, $ctx ); $subject = AIEO_Campaign_I18n::translate( (int) $wf['id'], (int) $i, $copy_subj_field, $subject, $wpml_lang ); $body = AIEO_Campaign_I18n::translate( (int) $wf['id'], (int) $i, $copy_body_field, $body, $wpml_lang ); } $coupon = null; if ( class_exists( 'AIEO_Coupon_Minter' ) && 'none' !== (string) ( $step['offer'] ?? 'none' ) && 'opted_out' !== $trigger_type && ( (int) $run['wp_user_id'] > 0 || $is_order || $is_guest_mail ) && $carrier_live ) { $addr = ( 'back_in_stock' === $trigger_type ) ? (string) ( $ctx['wl_email'] ?? '' ) : ( $is_order ? (string) ( $ctx['bill_email'] ?? '' ) : ( $is_guest_mail ? $guest_addr : ( class_exists( 'AIEO_Email_Channel' ) ? AIEO_Email_Channel::resolve_recipient( $subject_id, (int) $run['wp_user_id'] ) : '' ) ) ); if ( $service ) { $addr = $service_to; } $coupon_source = $is_test ? $trigger_id : ( ( $is_order && ! empty( $ctx['order_id'] ) ) ? $trigger_id . '_o' . (int) $ctx['order_id'] : ( $is_guest_mail ? $trigger_id . '_g' . md5( $guest_addr ) : $trigger_id ) ); $coupon = AIEO_Coupon_Minter::mint( array( 'offer_type' => (string) $step['offer'], 'offer_value' => (float) ( $step['offer_value'] ?? 0 ), 'ttl_days' => max( 1, (int) ( $step['offer_ttl_days'] ?? 14 ) ), 'wp_user_id' => (int) $run['wp_user_id'], 'subject_id' => $subject_id, 'email' => $addr, 'source' => $coupon_source, 'channel' => ( $is_order || $service ) ? 'email' : substr( $channel, 0, 10 ), ) ); if ( $coupon ) { self::$send_grant = (int) $coupon['grant_id']; $coupon['url'] = self::tag_url( AIEO_Coupon_Minter::offer_url( $coupon, (string) $addr ), 'email', $wf ); } } if ( class_exists( 'AIEO_Merge_Tags' ) ) { $subject = AIEO_Merge_Tags::resolve( $subject, $run, $ctx, $wf, $step, $coupon, $products, 'text' ); $body = AIEO_Merge_Tags::resolve( $body, $run, $ctx, $wf, $step, $coupon, $products, 'html' ); } $html = ( class_exists( 'AIEO_I18n' ) ) ? AIEO_I18n::with_locale( AIEO_I18n::recipient_locale( (int) $run['wp_user_id'], $guest_addr, $ctx ), function () use ( $subject, $body, $products, $step, $coupon, $wf ) { return self::render_email( $subject, $body, $products, $step, $coupon, $wf ); } ) : self::render_email( $subject, $body, $products, $step, $coupon, $wf ); $push = array( 'title' => $subject !== '' ? $subject : get_bloginfo( 'name' ), 'body' => self::push_text( $body, $coupon ), 'url' => self::tag_url( $product_id > 0 ? (string) get_permalink( $product_id ) : home_url( '/' ), 'push', $wf ), ); if ( ! class_exists( 'AIEO_Channel_Cascade' ) || ! class_exists( 'AIEO_Email_Channel' ) ) { return 'terminal'; } if ( 'back_in_stock' === $trigger_type && (int) ( $ctx['wl_id'] ?? 0 ) > 0 ) { return self::dispatch_back_in_stock( $ctx, $trigger_id, $subject, $html, $coupon ); } if ( $is_order ) { return self::dispatch_order_completed( $ctx, $trigger_id, $subject, $html, $product_id, $coupon ); } if ( $service ) { return self::dispatch_transactional_email( $service_to, $trigger_id, $subject, $html, $product_id, $coupon, false, 'opted_out' === $trigger_type && empty( $ctx['_delivered'] ) ); } $dedup = self::dedup_args( $wf, $i ); $guest_email = (string) ( $ctx['guest_email'] ?? '' ); if ( '' !== $guest_email && (int) $run['wp_user_id'] <= 0 ) { if ( 'email' !== $channel && class_exists( 'AIEO_Channel_Cascade' ) && method_exists( 'AIEO_Channel_Cascade', 'dispatch_guest' ) ) { $gp = AIEO_Channel_Cascade::dispatch_guest( array( 'trigger_id' => $trigger_id, 'guest_email' => $guest_email, 'target_product_id' => $product_id, 'push' => $push, ) + $dedup ); if ( 'push' === ( $gp['channel'] ?? '' ) ) { if ( $coupon ) { AIEO_Coupon_Minter::mark_sent( (int) $coupon['grant_id'], 'push' ); } return 'sent'; } if ( 'push' === $channel ) { return 'terminal'; } } return self::dispatch_transactional_email( $guest_email, $trigger_id, $subject, $html, $product_id, $coupon, true ); } $args = array( 'trigger_id' => $trigger_id, 'subject_id' => $subject_id, 'push' => $push, 'email' => ( 'push' === $channel ) ? array() : array( 'subject' => $subject, 'html' => $html ), 'target_product_id' => $product_id, ) + $dedup; $sent = false; $reason = ''; $ch = 'email'; if ( 'email' === $channel || ( 'cascade' === $channel && $subject_id <= 0 ) ) { $addr = AIEO_Email_Channel::resolve_recipient( $subject_id, (int) $run['wp_user_id'] ); if ( '' === $addr ) { $reason = 'no_consented_email'; } else { $res = AIEO_Email_Channel::dispatch( array( 'trigger_id' => $trigger_id, 'user_id' => (int) $run['wp_user_id'], 'subject_id' => $subject_id, 'email' => $addr, 'subject' => $subject, 'html' => $html, 'target_product_id' => $product_id, ) + $dedup ); $sent = ! empty( $res['sent'] ); $reason = (string) ( $res['reason'] ?? '' ); } } else { $res = AIEO_Channel_Cascade::dispatch( $args ); $ch = (string) ( $res['channel'] ?? 'email' ); $reason = (string) ( $res['reason'] ?? '' ); $sent = ( 'push' === $ch && 'push_sent_fallback_scheduled' === $reason ) || ( 'email' === $ch && 'sent' === $reason ); } if ( $sent && $coupon ) { AIEO_Coupon_Minter::mark_sent( (int) $coupon['grant_id'], ( 'push' === $ch ) ? 'push' : 'email' ); } return self::classify_outcome( $sent, $reason ); } private static function withdraw_send_coupon() { if ( self::$send_grant > 0 && class_exists( 'AIEO_Coupon_Minter' ) && method_exists( 'AIEO_Coupon_Minter', 'withdraw' ) ) { AIEO_Coupon_Minter::withdraw( self::$send_grant ); } self::$send_grant = 0; } private static function classify_outcome( $sent, $reason ) { self::$send_reason = $sent ? '' : (string) $reason; if ( $sent ) { return 'sent'; } if ( 'engine_paused' === $reason ) { return 'paused'; } if ( in_array( $reason, array( 'deduped', 'no_consented_email', 'no_consent', 'aw_unsub', 'no_subject', 'no_email_payload', 'not_tester' ), true ) ) { return 'terminal'; } return 'retry'; } private static function dispatch_back_in_stock( $ctx, $trigger_id, $subject, $html, $coupon ) { global $wpdb; $wl = $wpdb->prefix . 'aieo_waitlist'; $wl_id = (int) $ctx['wl_id']; $email = (string) ( $ctx['wl_email'] ?? '' ); $dedup_pid = (int) ( $ctx['product_id'] ?? 0 ); if ( '' === $email || ! is_email( $email ) ) { return 'terminal'; } if ( $dedup_pid > 0 && function_exists( 'wc_get_product' ) ) { $wp = wc_get_product( $dedup_pid ); if ( ! $wp || 'instock' !== $wp->get_stock_status() || 'publish' !== $wp->get_status() || ( $wp->get_parent_id() > 0 && 'publish' !== get_post_status( $wp->get_parent_id() ) ) ) { return 'terminal'; } } $claimed = (int) $wpdb->query( $wpdb->prepare( "UPDATE `{$wl}` SET status = 'notified', notified_at = UTC_TIMESTAMP() WHERE id = %d AND status = 'subscribed' AND notified_at IS NULL", $wl_id ) ); if ( $claimed < 1 ) { return 'terminal'; } $res = AIEO_Email_Channel::dispatch( array( 'trigger_id' => $trigger_id, 'user_id' => 0, 'subject_id' => 0, 'skip_dedup' => true, 'email' => $email, 'subject' => $subject, 'html' => $html, 'target_product_id' => $dedup_pid, ) ); $sent = ! empty( $res['sent'] ); $reason = (string) ( $res['reason'] ?? '' ); if ( $sent ) { if ( $coupon ) { AIEO_Coupon_Minter::mark_sent( (int) $coupon['grant_id'], 'email' ); } return 'sent'; } if ( 'aw_unsub' === $reason ) { $wpdb->query( $wpdb->prepare( "UPDATE `{$wl}` SET status = 'suppressed' WHERE id = %d", $wl_id ) ); return 'terminal'; } $wpdb->query( $wpdb->prepare( "UPDATE `{$wl}` SET status = 'subscribed', notified_at = NULL WHERE id = %d", $wl_id ) ); if ( 'engine_paused' === $reason ) { return 'paused'; } return 'retry'; } private static function dispatch_order_completed( $ctx, $trigger_id, $subject, $html, $product_id, $coupon ) { return self::dispatch_transactional_email( (string) ( $ctx['bill_email'] ?? '' ), $trigger_id, $subject, $html, $product_id, $coupon ); } private static function dispatch_transactional_email( $email, $trigger_id, $subject, $html, $product_id, $coupon, $marketing = false, $confirms_opt_out = false ) { $email = (string) $email; if ( '' === $email || ! is_email( $email ) ) { return 'terminal'; } if ( ! class_exists( 'AIEO_Email_Channel' ) ) { return 'terminal'; } $res = AIEO_Email_Channel::dispatch( array( 'trigger_id' => $trigger_id, 'user_id' => 0, 'subject_id' => 0, 'skip_dedup' => true, 'email' => $email, 'subject' => $subject, 'html' => $html, 'target_product_id' => (int) $product_id, 'marketing' => (bool) $marketing, ) + ( $confirms_opt_out ? array( 'confirms_opt_out' => true ) : array() ) ); $sent = ! empty( $res['sent'] ); $reason = (string) ( $res['reason'] ?? '' ); if ( $sent && $coupon ) { AIEO_Coupon_Minter::mark_sent( (int) $coupon['grant_id'], 'email' ); } return self::classify_outcome( $sent, $reason ); } private static function tag_url( $url, $medium, $wf ) { $url = (string) $url; if ( '' === $url ) { return $url; } $medium = ( 'push' === $medium ) ? 'push' : 'email'; $cfg = is_array( $wf['trigger_config'] ?? null ) ? $wf['trigger_config'] : array(); $ov = static function ( $key, $default ) use ( $cfg ) { $v = isset( $cfg[ $key ] ) ? trim( (string) $cfg[ $key ] ) : ''; return '' !== $v ? $v : $default; }; $name = trim( wp_strip_all_tags( (string) ( $wf['name'] ?? '' ) ) ); $name40 = ( '' !== $name ) ? ( function_exists( 'mb_substr' ) ? mb_substr( $name, 0, 40 ) : substr( $name, 0, 40 ) ) : ''; $args = array( 'utm_source' => $ov( 'utm_source', 'aieo' ), 'utm_medium' => $ov( 'utm_medium', $medium ), 'utm_campaign' => AIEO_Automation_Test_Mode::in_scope() ? AIEO_Automation_Test_Mode::trigger_id( (int) ( $wf['id'] ?? 0 ) ) : $ov( 'utm_campaign', 'wf_' . (int) ( $wf['id'] ?? 0 ) ), ); $content = $ov( 'utm_content', $name40 ); if ( '' !== $content ) { $args['utm_content'] = $content; } $term = $ov( 'utm_term', '' ); if ( '' !== $term ) { $args['utm_term'] = $term; } if ( 'push' === $medium ) { $args['aieo_src'] = 'push'; } return add_query_arg( $args, $url ); } private static function in_holdout( $wf_id, $key, $pct ) { $pct = max( 0, min( 90, (int) $pct ) ); if ( $pct <= 0 ) { return false; } $bucket = hexdec( substr( md5( 'aieo_holdout|' . (int) $wf_id . '|' . $key ), 0, 8 ) ) % 100; return $bucket < $pct; } private static function log_holdout( $trigger_id, $run, $ctx ) { global $wpdb; $t = $wpdb->prefix . 'aieo_push_dispatch'; if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) !== $t ) { return; } $now = gmdate( 'Y-m-d H:i:s' ); $since = gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ); $uid = (int) $run['wp_user_id']; $email = ( $uid > 0 ) ? '' : strtolower( trim( (string) ( $ctx['guest_email'] ?? '' ) ) ); $dup = ( $uid > 0 ) ? (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE channel='holdout' AND trigger_id=%s AND user_id=%d AND created_at >= %s", $trigger_id, $uid, $since ) ) : (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$t}` WHERE channel='holdout' AND trigger_id=%s AND recipient_email=%s AND created_at >= %s", $trigger_id, $email, $since ) ); if ( $dup > 0 ) { return; } $wpdb->insert( $t, array( 'channel' => 'holdout', 'subscription_id' => 0, 'subject_id' => (int) $run['subject_id'], 'recipient_email' => $email, 'user_id' => $uid, 'trigger_id' => (string) $trigger_id, 'template_id' => 0, 'target_product_id' => (int) ( $ctx['product_id'] ?? 0 ), 'state' => 'holdout', 'suppress_reason' => 'holdout', 'queued_at' => $now, 'created_at' => $now, ) ); } private static function render_email( $subject, $body, $products, $step, $coupon = null, $wf = array() ) { $site = esc_html( get_bloginfo( 'name' ) ); $cards = ''; foreach ( (array) $products as $pid ) { if ( ! function_exists( 'wc_get_product' ) ) { break; } $p = wc_get_product( (int) $pid ); if ( ! $p || 'publish' !== $p->get_status() ) { continue; } $img = $p->get_image_id(); $cards .= '' . ( $img ? '' : '' ) . ' ' . esc_html( $p->get_name() ) . ''; } $offer = ''; if ( $coupon && ! empty( $coupon['code'] ) ) { $fallback = function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : home_url( '/cart/' ); $apply = esc_url( ! empty( $coupon['url'] ) ? (string) $coupon['url'] : $fallback ); $offer = '

' . esc_html( (string) $coupon['label'] ) . '

' . '

' . esc_html__( 'Your code:', 'ai-eshop-optimizer' ) . ' ' . esc_html( strtoupper( (string) $coupon['code'] ) ) . '

' . '

' . esc_html__( 'Apply & shop', 'ai-eshop-optimizer' ) . '

'; } elseif ( 'none' !== ( $step['offer'] ?? 'none' ) ) { $val = (int) ( $step['offer_value'] ?? 0 ); $lbl = class_exists( 'AIEO_Coupon_Minter' ) ? AIEO_Coupon_Minter::label( (string) $step['offer'], $val ) : ( ( 'percent' === $step['offer'] ) ? $val . '% off' : ( function_exists( 'wc_price' ) ? html_entity_decode( wp_strip_all_tags( wc_price( $val, array( 'decimals' => 0 ) ) ), ENT_QUOTES, 'UTF-8' ) . ' off' : '€' . $val . ' off' ) ); $offer = '

' . esc_html( $lbl ) . '

'; } return '' . '' . '' . ( $subject !== '' ? '' : '' ) . '' . ( $offer ? '' : '' ) . ( $cards ? '' : '' ) . '
' . $site . '
' . esc_html( $subject ) . '
' . nl2br( wp_kses_post( $body ) ) . '
' . $offer . '
' . $cards . '
'; } public static function preview_send( $wf, $step, $coupon_email = '', $target_user_id = 0 ) { list( $run, $ctx, $products ) = self::preview_target( $wf, $step, (int) $target_user_id ); $coupon = self::preview_coupon( $wf, $step, $coupon_email ); $subject = (string) ( $step['subject'] ?? '' ); $body = (string) ( $step['body'] ?? '' ); if ( class_exists( 'AIEO_Merge_Tags' ) ) { $subject = AIEO_Merge_Tags::resolve( $subject, $run, $ctx, $wf, $step, $coupon, $products, 'text' ); $body = AIEO_Merge_Tags::resolve( $body, $run, $ctx, $wf, $step, $coupon, $products, 'html' ); } $html = class_exists( 'AIEO_I18n' ) ? AIEO_I18n::with_locale( AIEO_I18n::recipient_locale( (int) $run['wp_user_id'], (string) ( $ctx['guest_email'] ?? '' ), $ctx ), function () use ( $subject, $body, $products, $step, $coupon, $wf ) { return self::render_email( $subject, $body, $products, $step, $coupon, $wf ); } ) : self::render_email( $subject, $body, $products, $step, $coupon, $wf ); return array( 'subject' => $subject, 'html' => $html, 'target' => (string) ( $ctx['bill_email'] ?? '' ), 'coupon_code' => ( is_array( $coupon ) && ! empty( $coupon['code'] ) ) ? (string) $coupon['code'] : '', 'push' => array( 'title' => '' !== $subject ? $subject : get_bloginfo( 'name' ), 'body' => self::push_text( $body, $coupon ), ), ); } private static function preview_target( $wf, $step, $target_user_id = 0 ) { $trigger = (string) ( $wf['trigger_type'] ?? '' ); $uid = 0; $order_id = 0; $cart_id = 0; $cart_pid = 0; $email = ''; if ( $target_user_id > 0 && get_userdata( (int) $target_user_id ) ) { $uid = (int) $target_user_id; } if ( in_array( $trigger, array( 'abandoned_checkout', 'abandoned_cart' ), true ) && class_exists( 'AIEO_Carts_Store' ) ) { global $wpdb; $t = AIEO_Carts_Store::carts_table(); $row = $uid > 0 ? $wpdb->get_row( $wpdb->prepare( "SELECT id, wc_order_id, user_id, customer_email, items FROM `{$t}` WHERE kind = 'abandoned' AND user_id = %d ORDER BY id DESC LIMIT 1", $uid ), ARRAY_A ) : $wpdb->get_row( "SELECT id, wc_order_id, user_id, customer_email, items FROM `{$t}` WHERE kind = 'abandoned' AND customer_email <> '' ORDER BY id DESC LIMIT 1", ARRAY_A ); if ( $row ) { $cart_id = (int) $row['id']; $cart_pid = self::first_cart_product_id( (string) $row['items'] ); $order_id = (int) $row['wc_order_id']; $uid = (int) $row['user_id']; $email = (string) $row['customer_email']; } } if ( 0 === $uid && '' === $email && class_exists( 'AIEO_Audience_Rules' ) ) { $rule = self::audience_rule( $wf ); if ( $rule ) { $uids = AIEO_Audience_Rules::member_user_ids( $rule, 0, 1 ); if ( ! empty( $uids ) ) { $uid = (int) $uids[0]; } } } if ( 0 === $uid && '' === $email && function_exists( 'wc_get_orders' ) ) { $oids = wc_get_orders( array( 'type' => 'shop_order', 'limit' => 3, 'orderby' => 'date', 'order' => 'DESC', 'return' => 'ids' ) ); foreach ( (array) $oids as $oid ) { $o = wc_get_order( (int) $oid ); if ( $o && '' !== (string) $o->get_billing_email() ) { $order_id = (int) $o->get_id(); $uid = (int) $o->get_customer_id(); $email = (string) $o->get_billing_email(); break; } } } if ( $uid > 0 && 0 === $order_id && 0 === $cart_id && function_exists( 'wc_get_orders' ) ) { $oids = wc_get_orders( array( 'customer' => $uid, 'limit' => 1, 'orderby' => 'date', 'order' => 'DESC', 'return' => 'ids' ) ); if ( ! empty( $oids ) ) { $order_id = (int) $oids[0]; } } if ( '' === $email && $uid > 0 ) { $u = get_userdata( $uid ); if ( $u ) { $email = (string) $u->user_email; } } $sid = ( $uid > 0 && class_exists( 'AIEO_Subject_Resolver' ) ) ? (int) AIEO_Subject_Resolver::subject_for_user( $uid ) : 0; $run = array( 'wp_user_id' => $uid, 'subject_id' => $sid, 'step_index' => 0 ); $ctx = array( 'guest_email' => $email, 'bill_email' => $email, 'wl_email' => $email, 'order_id' => $order_id, 'cart_id' => $cart_id ); if ( $cart_pid > 0 ) { $ctx['product_id'] = $cart_pid; } $products = array(); $src = (string) ( $step['product_source'] ?? 'none' ); if ( 'recommendations' === $src && $sid > 0 && class_exists( 'AIEO_Behavioural_Reco' ) ) { foreach ( AIEO_Behavioural_Reco::recommend( $sid, 4 ) as $r ) { $products[] = (int) $r['item_id']; } } elseif ( 'trigger_product' === $src && $cart_pid > 0 ) { $products[] = $cart_pid; } elseif ( 'trigger_product' === $src && $order_id > 0 && function_exists( 'wc_get_order' ) ) { $o = wc_get_order( $order_id ); if ( $o ) { foreach ( $o->get_items() as $it ) { $products[] = (int) $it->get_product_id(); break; } } } if ( empty( $products ) && 'none' !== $src && function_exists( 'wc_get_products' ) ) { $products = array_map( 'intval', (array) wc_get_products( array( 'status' => 'publish', 'limit' => 3, 'return' => 'ids' ) ) ); } return array( $run, $ctx, $products ); } private static function preview_coupon( $wf, $step, $lock_email ) { $offer = (string) ( $step['offer'] ?? 'none' ); if ( 'none' === $offer ) { return null; } $val = round( (float) ( $step['offer_value'] ?? 0 ), 2 ); $label = class_exists( 'AIEO_Coupon_Minter' ) ? AIEO_Coupon_Minter::label( $offer, $val ) : ( ( 'percent' === $offer ) ? ( $val . '% off' ) : ( function_exists( 'wc_price' ) ? html_entity_decode( wp_strip_all_tags( wc_price( $val, array( 'decimals' => 0 ) ) ), ENT_QUOTES, 'UTF-8' ) . ' off' : '€' . $val . ' off' ) ); $lock_email = function_exists( 'sanitize_email' ) ? sanitize_email( (string) $lock_email ) : (string) $lock_email; if ( class_exists( 'AIEO_Coupon_Minter' ) && '' !== $lock_email && $val > 0 ) { $coupon = AIEO_Coupon_Minter::mint( array( 'offer_type' => $offer, 'offer_value' => (float) $val, 'ttl_days' => max( 1, (int) ( $step['offer_ttl_days'] ?? 14 ) ), 'wp_user_id' => 0, 'subject_id' => 0, 'email' => $lock_email, 'source' => 'wf_' . (int) ( $wf['id'] ?? 0 ) . '_preview', 'channel' => 'email', ) ); if ( $coupon ) { $coupon['url'] = self::tag_url( AIEO_Coupon_Minter::offer_url( $coupon, $lock_email ), 'email', $wf ); return $coupon; } } return array( 'code' => 'PREVIEW-' . strtoupper( substr( md5( (string) ( $wf['id'] ?? 0 ) . $offer . $val ), 0, 6 ) ), 'label' => $label, 'url' => function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : home_url( '/cart/' ), ); } private static function audience_rule( $wf ) { $aid = (int) ( $wf['audience_id'] ?? 0 ); if ( $aid <= 0 || ! class_exists( 'AIEO_Audience_Rules' ) ) { return null; } foreach ( AIEO_Audience_Rules::list_saved() as $a ) { if ( (int) $a['id'] === $aid ) { return $a['rule']; } } return null; } private static function mark_run( $id ) { global $wpdb; $t = aieo_workflows_table(); $wpdb->query( $wpdb->prepare( "UPDATE `{$t}` SET last_run_at = %s WHERE id = %d", gmdate( 'Y-m-d H:i:s' ), (int) $id ) ); } public static function repair_run_counts() { global $wpdb; if ( ! function_exists( 'aieo_workflows_table' ) || ! function_exists( 'aieo_workflow_runs_table' ) ) { return -1; } $wf = aieo_workflows_table(); $runs = aieo_workflow_runs_table(); foreach ( array( $wf, $runs ) as $table ) { if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) { return -1; } } $n = $wpdb->query( $wpdb->prepare( "UPDATE `{$wf}` w LEFT JOIN ( SELECT workflow_id, COUNT(*) AS c FROM `{$runs}` WHERE ( context_json LIKE %s OR ( context_json LIKE %s AND context_json NOT LIKE %s ) ) AND context_json NOT LIKE %s GROUP BY workflow_id ) r ON r.workflow_id = w.id SET w.runs_count = COALESCE(r.c, 0) WHERE w.runs_count <> COALESCE(r.c, 0)", '%' . $wpdb->esc_like( '"_reached":1' ) . '%', '%' . $wpdb->esc_like( '"_delivered":1' ) . '%', '%' . $wpdb->esc_like( '"_holdout":1' ) . '%', '%' . $wpdb->esc_like( '"_test":1' ) . '%' ) ); return false === $n ? 0 : (int) $n; } private static function ready() { return function_exists( 'aieo_workflows_table' ) && class_exists( 'AIEO_Workflows' ) && (string) $GLOBALS['wpdb']->get_var( $GLOBALS['wpdb']->prepare( 'SHOW TABLES LIKE %s', aieo_workflows_table() ) ) === aieo_workflows_table(); } private static function channel_live( $channel ) { $email = class_exists( 'AIEO_Email_Engine' ) ? (bool) AIEO_Email_Engine::is_live() : ( 'live' === get_option( 'aieo_email_engine_status', 'paused' ) ); $push = ( 'live' === get_option( 'aieo_push_engine_status', 'paused' ) ) && ( ( class_exists( 'AIEO_DMM_Web_Push' ) && method_exists( 'AIEO_DMM_Web_Push', 'is_engine_live' ) ) ? (bool) AIEO_DMM_Web_Push::is_engine_live() : true ); $email = $email || AIEO_Automation_Test_Mode::opens( 'email' ); $push = $push || AIEO_Automation_Test_Mode::opens( 'push' ); if ( 'email' === $channel ) { return $email; } if ( 'push' === $channel ) { return $push; } return $email || $push; } }