0 ) { Followly_Scheduler::schedule( $automation_id, $context, $delay ); } else { self::run( $automation_id, $context, false ); } } } /** * Run an automation's actions now. * * @param int $automation_id * @param Followly_Context $context * @param bool $recheck_conditions Re-evaluate conditions first (used for delayed/manual runs, * since state may have changed since the automation matched). */ public static function run( $automation_id, Followly_Context $context, $recheck_conditions = true ) { if ( 'publish' !== get_post_status( $automation_id ) ) { Followly_Log::insert( $automation_id, $context, 'skipped_inactive', __( 'Automation is not active.', 'followly' ) ); return; } // 'ignore_conditions' is the one shared "skip the normal safety nets" // flag — set by a manual run (per the automation's own "ignore filters // when run manually" setting) or by a retroactive bulk run's "force // resend" checkbox. It must bypass the once-per-order dedupe too, not // just the conditions recheck below: a forced resend is explicitly // asking to override "already sent", so this check can't be // unconditional or "force" silently does nothing once the action is // actually processed (which happens later, via the scheduled job — // long after the bulk-run screen itself decided to include the order). $ignore_safety_checks = ! empty( $context->get_arg( 'ignore_conditions' ) ); // Orders only, as the setting says: for a product trigger it would mean // one low-stock alert per product ever, even after restocking. if ( ! $ignore_safety_checks && 'order' === $context->type && self::is_once_per_order( $automation_id ) && Followly_Log::has_sent( $automation_id, $context->get_object_id() ) ) { Followly_Log::insert( $automation_id, $context, 'skipped_already_sent', __( 'Already sent for this order before ("only once" is enabled).', 'followly' ) ); return; } if ( $recheck_conditions && ! $ignore_safety_checks ) { $conditions = self::get_meta_json( $automation_id, '_followly_conditions' ); if ( ! Followly_Conditions::evaluate( $conditions, $context ) ) { Followly_Log::insert( $automation_id, $context, 'skipped_recheck', __( 'Conditions no longer met at run time.', 'followly' ) ); return; } } $actions = self::get_meta_json( $automation_id, '_followly_actions' ); if ( empty( $actions ) ) { Followly_Log::insert( $automation_id, $context, 'skipped_no_actions', __( 'No actions configured.', 'followly' ) ); return; } $ran_any = false; foreach ( $actions as $action ) { $handler = Followly_Actions::get( $action['type'] ?? 'email' ); if ( $handler && $handler->run( $context, $action ) ) { $ran_any = true; } } if ( $ran_any ) { $count = (int) get_post_meta( $automation_id, '_followly_run_count', true ); update_post_meta( $automation_id, '_followly_run_count', $count + 1 ); update_post_meta( $automation_id, '_followly_last_run', current_time( 'mysql' ) ); Followly_Log::insert( $automation_id, $context, 'sent' ); } else { Followly_Log::insert( $automation_id, $context, 'failed', __( 'Action(s) did not run (e.g. no valid recipient).', 'followly' ) ); } } /** * @return int[] Published automation post IDs configured for this trigger. */ private static function get_automations_for_trigger( $trigger_key ) { $query = new WP_Query( array( 'post_type' => 'followly_automation', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Only scans the few posts of the plugin's own post type. array( 'key' => '_followly_trigger', 'value' => $trigger_key, ), ), ) ); return $query->posts; } /** * Defaults to true (unset meta = never explicitly saved yet = "once"). */ public static function is_once_per_order( $automation_id ) { $value = get_post_meta( $automation_id, '_followly_once_per_order', true ); return '' === $value ? true : (bool) $value; } public static function get_meta_json( $post_id, $meta_key ) { $raw = get_post_meta( $post_id, $meta_key, true ); if ( ! $raw ) { return array(); } $decoded = json_decode( $raw, true ); return is_array( $decoded ) ? $decoded : array(); } }