'campaign', 'meta_query' => [ [ 'key' => 'campaign_tracking_id', 'value' => $tracking_id, 'compare' => '=', ], ], 'posts_per_page' => 1, 'fields' => 'ids' ]); if (!$query->have_posts()) { // The ID is unique, save it log_to_file("generate_campaign_tracking_id - Campaign: {$campaign_id} ID: {$tracking_id}"); update_post_meta($campaign_id, 'campaign_tracking_id', $tracking_id); return true; } $attempts++; } // If maximum attempts exceeded, throw an exception log_to_file("generate_campaign_tracking_id - Unable to generate a unique campaign tracking ID!"); throw new Exception(__('Unable to generate a unique campaign tracking ID.', 'rl-mailwarmer')); } /** * Calculate the campaign timeline with semi-randomized daily email goals. * * @param int $campaign_id The campaign post ID. * @return array Weekly email schedule with randomized daily goals. * @throws Exception If the campaign parameters are invalid. */ public static function calculate_campaign_timeline($campaign_id) { action_log("calculate_campaign_timeline - Creating timeline for campaign {$campaign_id}"); // Fetch campaign details $warmup_period = (int) get_post_meta($campaign_id, 'warmup_period', true); // Weeks $target_volume = (int) get_post_meta($campaign_id, 'target_volume', true); // Daily emails to ramp up to $weekend_reduction_factor = get_field('weekend_reduction_factor', $campaign_id) ? (int) get_field('weekend_reduction_factor', $campaign_id) : 25; // Make it so the volume is REDUCED by this percentage, not capped at it $weekend_reduction_factor = 100 - $weekend_reduction_factor; $start_date = get_post_meta($campaign_id, 'start_date', true); // Campaign start date // log_to_file("calculate_campaign_timeline - Target weekend reduction factor: {$weekend_reduction_factor}"); if (!$warmup_period || !$target_volume || !$start_date) { throw new Exception(__('Invalid campaign parameters.', 'rl-mailwarmer')); } // Fetch ACF options $holidays_raw = get_field('calendar_holidays', 'option'); // Holidays in 12/25/25 format $holidays = array_map( fn($date) => DateTime::createFromFormat('m/d/y', trim($date))->format('Y-m-d'), explode("\n", $holidays_raw) ); $min_starting_volume = (int) get_field('min_starting_email_volume', 'option') ?: 5; $max_daily_volume = (int) get_field('max_campaign_daily_email_volume', 'option') ?: 1000; // Calculate starting daily volume (2.5% of target volume) $starting_daily_volume = max(ceil($target_volume * 0.025), $min_starting_volume); // Initialize variables $timeline = []; $total_days = $warmup_period * 7; // Total days in the campaign $start_date = new DateTime($start_date); // Calculate daily ramp-up rate $base_daily_increase = ($target_volume - $starting_daily_volume) / ($total_days * 0.75); log_to_file("calculate_campaign_timeline - Ramping up from $min_starting_volume to $target_volume over $total_days days, increasing by $base_daily_increase (+/- 25%) each day with no more than $max_daily_volume emails in a day"); // Generate timeline for ($day = 0; $day < $total_days; $day++) { $current_date = clone $start_date; $current_date->modify("+{$day} days"); $date_formatted = $current_date->format('Y-m-d'); // Adjust for holidays and weekends $is_weekend = in_array($current_date->format('N'), [6, 7]); $is_holiday = in_array($date_formatted, $holidays); $reduction_factor = ($is_weekend || $is_holiday) ? mt_rand($weekend_reduction_factor - 5, $weekend_reduction_factor + 5) / 100 : 1; // log_to_file("calculate_campaign_timeline - Using daily reduction factor: {$reduction_factor}"); // Calculate daily volume $percentage_variation = mt_rand(-25, 25); $daily_increase = round($base_daily_increase + ($base_daily_increase * $percentage_variation / 100), 2); $daily_volume = min( ceil(($starting_daily_volume + ($daily_increase * $day)) * $reduction_factor), ceil($target_volume + ($target_volume * mt_rand(5, 20)) / 100) ); log_to_file("calculate_campaign_timeline - Daily increase {$daily_increase}"); if ($daily_volume > $max_daily_volume) { // log_to_file("calculate_campaign_timeline - Max Daily Volume hit for campaign $campaign_id! Capping number of emails"); $daily_volume = $max_daily_volume; } // Calculate percent daily change $percent_daily_change = $day > 0 ? (($daily_volume - $last_daily_volume) / $last_daily_volume) * 100 : 0; // Round to 2 decimal places $percent_daily_change = round($percent_daily_change, 2); // Add a "+" sign if positive $formatted_percent_change = $percent_daily_change > 0 ? '+' . $percent_daily_change : (string)$percent_daily_change; $last_daily_volume = $daily_volume; $timeline[$date_formatted] = [ 'target_volume' => $daily_volume, 'current_volume' => 0, 'items_sent' => 0, 'percent_daily_change' => $formatted_percent_change, 'daily_increase' => $daily_increase, ]; // log_to_file("calculate_campaign_timeline - $day: $daily_volume"); } // Save the updated campaign timeline update_post_meta($campaign_id, 'campaign_timeline', json_encode($timeline, JSON_PRETTY_PRINT)); update_post_meta($campaign_id, 'campaign_timeline_json', $timeline); if( self::fill_campaign_timeline($campaign_id, $timeline) ) { // Check the number of saved messages per date $message_counts = RL_MailWarmer_DB_Helper::get_message_counts_by_date($campaign_id); update_post_meta($campaign_id, 'message_counts', json_encode($message_counts, JSON_PRETTY_PRINT)); action_log("calculate_campaign_timeline - Finished creating timeline for campaign {$campaign_id}"); return true; } else { log_to_file("calculate_campaign_timeline - Error filling campaign timeline!"); throw new Exception(__("Error filling campaign timeline for campaign ID: {$campaign_id}", 'rl-mailwarmer')); } } /** * Generate conversation placeholders of varying lengths and allocate them until all dates in the campaign meet the targets set by RL_MailWarmer_Campaign_Helper::calculate_campaign_timeline() * * @param int $campaign_id The campaign post ID. * @return array Weekly email schedule with randomized daily goals. * @throws Exception If the campaign parameters are invalid. */ public static function fill_campaign_timeline(int $campaign_id, $campaign_timeline = null) { $filled_dates = []; $weekly_volumes = []; $current_week = ''; $weekly_total = 0; $total_conversation_count = 0; $total_message_count = 0; $campaign_limited = get_post_meta($campaign_id, 'campaign_limited', true); if (!$campaign_timeline) { try { $campaign_timeline = get_field('campaign_timeline', $campaign_id); } catch (Exception $e) { log_to_file("fill_campaign_timeline - No campaign timeline found for campaign ID: {$campaign_id}"); throw new Exception(__("No campaign timeline found for campaign ID: {$campaign_id}", 'rl-mailwarmer')); } } if (!$campaign_limited ) { $ratios = [ 'extra-long' => [ 'percent_of_volume_lower' => 1, 'percent_of_volume_upper' => 3, 'num_participants_lower' => 1, 'num_participants_upper' => 8, 'num_responses_lower' => 5, 'num_responses_upper' => 10 ], 'long' => [ 'percent_of_volume_lower' => 2, 'percent_of_volume_upper' => 4, 'num_participants_lower' => 1, 'num_participants_upper' => 6, 'num_responses_lower' => 4, 'num_responses_upper' => 6 ], 'medium' => [ 'percent_of_volume_lower' => 2, 'percent_of_volume_upper' => 6, 'num_participants_lower' => 1, 'num_participants_upper' => 5, 'num_responses_lower' => 3, 'num_responses_upper' => 5 ], 'short' => [ 'percent_of_volume_lower' => 15, 'percent_of_volume_upper' => 20, 'num_participants_lower' => 1, 'num_participants_upper' => 4, 'num_responses_lower' => 2, 'num_responses_upper' => 4 ], ]; } else { log_to_file("fill_campaign_timeline - Creating a limited campaign"); $ratios = [ 'short' => [ 'percent_of_volume_lower' => 100, 'percent_of_volume_upper' => 100, 'num_participants_lower' => 1, 'num_participants_upper' => 5, 'num_responses_lower' => 1, 'num_responses_upper' => 1 ], ]; } $warmup_period = (int) get_post_meta($campaign_id, 'warmup_period', true); // Weeks $start_date = get_post_meta($campaign_id, 'start_date', true); // Campaign start date $total_days = $warmup_period * 7; // Total days in the campaign // $total_days = $warmup_period; // Total days in the campaign $start_date = date('Y-m-d H:i:s', strtotime($start_date)); $end_date = date('Y-m-d 23:59:59', strtotime($start_date . " + {$total_days} days")); // $end_date = date('Y-m-d H:i', strtotime($start_date . " +{$total_days} days")); log_to_file("fill_campaign_timeline - Start: $start_date End: $end_date"); // log_to_file("fill_campaign_timeline - Campaign timeline $campaign_timeline"); // Calculate weekly volumes foreach ($campaign_timeline as $date => $data) { // log_to_file("fill_campaign_timeline - Campaign timeline date: {$date}", $data); $timestamp = strtotime($date); $year = date('o', $timestamp); // ISO year $week = date('W', $timestamp); // ISO week $week_key = sprintf('%d-%02d', $year, $week); if (!isset($weekly_volumes[$week_key])) { $weekly_volumes[$week_key] = 0; } // log_to_file("fill_campaign_timeline - Weekly Volume: ", $weekly_volumes); $weekly_volumes[$week_key] += $data['target_volume']; } action_log("fill_campaign_timeline - Creating multi-step conversations for campaign {$campaign_id}. Message count: {$total_message_count}"); // Process each week foreach ($weekly_volumes as $week => $volume) { log_to_file("fill_campaign_timeline - Week $week goal: $volume"); $weekly_scheduled_messages = 0; // Process each conversation length (short, medium, long, etc.) foreach ($ratios as $length => $ratio) { // Calculate number of conversations for this length $percent = mt_rand($ratio['percent_of_volume_lower'], $ratio['percent_of_volume_upper']) / 100; $num_conversations = ceil($volume * $percent); log_to_file("fill_campaign_timeline - Generating $num_conversations $length conversations"); // Skip if no conversations to generate if ($num_conversations === 0) continue; // Generate conversations for ($i = 0; $i < $num_conversations; $i++) { // Check if weekly volume reached if ($weekly_scheduled_messages >= $volume) { break 2; // Break both loops } // Generate random number of responses for this conversation $num_particpants = mt_rand($ratio['num_participants_lower'], $ratio['num_participants_upper']); // Generate random number of responses for this conversation $num_responses = mt_rand($ratio['num_responses_lower'], $ratio['num_responses_upper']); // Get week start date list($year, $weekNum) = explode('-', $week); $week_start = date('Y-m-d', strtotime($year . 'W' . $weekNum)); if ($week_start < $start_date) { $week_start = $start_date; } // Generate conversation blueprint $conversation_steps = RL_MailWarmer_Conversation_Handler::generate_conversation_blueprint( $campaign_id, $num_particpants, $num_responses, $week_start, $end_date, $filled_dates ); // Update timeline volumes foreach ($conversation_steps as $step) { $step_date = date('Y-m-d', strtotime($step['scheduled_for'])); if (isset($campaign_timeline[$step_date])) { $campaign_timeline[$step_date]['current_volume']++; $weekly_scheduled_messages++; $total_message_count++; // Check if date is now filled if ($campaign_timeline[$step_date]['current_volume'] >= $campaign_timeline[$step_date]['target_volume']) { $filled_dates[] = $step_date; } } } $total_conversation_count++; } } } action_log("fill_campaign_timeline - Creating single-step conversations for campaign {$campaign_id}. Message count: {$total_message_count}"); // log_to_file("fill_campaign_timeline - Campaign Timeline without single-step conversations: ", $campaign_timeline); // Fill remaining capacity with single-message conversations log_to_file("fill_campaign_timeline - Filling remaining days with single conversations"); foreach ($campaign_timeline as $date => $data) { $remaining = $data['target_volume'] - $data['current_volume']; // log_to_file("fill_campaign_timeline - $date remaining: $remaining"); while ($remaining > 0) { $conversation_steps = RL_MailWarmer_Conversation_Handler::generate_conversation_blueprint( $campaign_id, 1, 1, $date, $date ); foreach ($conversation_steps as $step) { $step_date = date('Y-m-d', strtotime($step['scheduled_for'])); if (isset($campaign_timeline[$step_date])) { $campaign_timeline[$step_date]['current_volume']++; $total_message_count++; $remaining--; if ($campaign_timeline[$step_date]['current_volume'] >= $campaign_timeline[$step_date]['target_volume']) { $filled_dates[] = $step_date; } } } $total_conversation_count++; } } action_log("fill_campaign_timeline - Created {$total_conversation_count} conversations with {$total_message_count} messages for campaign {$campaign_id}"); // return $campaign_timeline; return true; } /** * Parse the AI response into structured steps for message creation. * * @param string $ai_response The JSON string returned by ChatGPT. * @return array An array of parsed conversation steps. */ // public static function parse_ai_response($ai_response) { // $steps = json_decode($ai_response, true); // if (json_last_error() !== JSON_ERROR_NONE) { // return new WP_Error('invalid_response', __('Failed to parse AI response.', 'rl-mailwarmer')); // } // return $steps; // } } /** * Add a meta box for generating the campaign timeline. */ add_action('add_meta_boxes', function () { add_meta_box( 'show_campaign_tracking_id', __('Tracking ID', 'rl-mailwarmer'), 'rl_mailwarmer_render_tracking_id_box', 'campaign', 'side', 'default' ); add_meta_box( 'generate_campaign_timeline', __('Generate Timeline', 'rl-mailwarmer'), 'rl_mailwarmer_render_generate_timeline_box', 'campaign', 'side', 'default' ); }); /** * Render the "Tracking ID" meta box. * * @param WP_Post $post The current post object. */ function rl_mailwarmer_render_tracking_id_box($post) { $campaign_tracking_id = get_field('campaign_tracking_id', $post->ID); ?>
post_type === 'campaign') { wp_enqueue_script( 'rl-mailwarmer-generate-timeline-js', RL_MAILWARMER_URL . 'js/generate-timeline.js', // Adjust path as needed ['jquery'], null, true ); wp_localize_script('rl-mailwarmer-generate-timeline-js', 'rlMailWarmerGenerateTimeline', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('generate_timeline_nonce'), ]); } }); add_action('wp_ajax_rl_mailwarmer_generate_timeline', function () { // Verify nonce if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'generate_timeline_nonce')) { wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer')); } // Validate and sanitize input $post_id = intval($_POST['post_id']); if (!$post_id || get_post_type($post_id) !== 'campaign') { wp_send_json_error(__('Invalid campaign ID.', 'rl-mailwarmer')); } // Generate the timeline try { RL_MailWarmer_Campaign_Helper::calculate_campaign_timeline($post_id); // log_to_file("wp_ajax_rl_mailwarmer_generate_timeline - Generated Timeline: ", $timeline); // update_post_meta($post_id, "campaign_timeline", $timeline); wp_send_json_success("Timeline generated"); } catch (Exception $e) { wp_send_json_error($e->getMessage()); } }); /* * Generate Conversation Metabox * */ add_action('add_meta_boxes', function () { add_meta_box( 'generate_conversation_box', // Unique ID for the meta box __('Generate Conversation', 'rl-mailwarmer'), // Title of the meta box 'rl_mailwarmer_render_conversation_box', // Callback to display the box content 'campaign', // Post type 'side', // Context: side, normal, or advanced 'default' // Priority ); }); /** * Render the Generate Conversation meta box. * * @param WP_Post $post The current post object. */ function rl_mailwarmer_render_conversation_box($post) { // Add a nonce for security wp_nonce_field('rl_generate_conversation_nonce', 'rl_generate_conversation_nonce_field'); // Meta box form ?>' . esc_html__('No timeline available for this campaign.', 'rl-mailwarmer') . '
'; return; } $campaign_timeline = $campaign_timeline; // Organize timeline by weeks for grid structure $weeks = []; $max_volume = 0; foreach ($campaign_timeline as $date => $day) { $max_volume = max($max_volume, $day['target_volume']); $week_number = date('W', strtotime($date)); if (!isset($weeks[$week_number])) { $weeks[$week_number] = []; } $weeks[$week_number][$date] = $day; } // log_to_file("edit_form_after_title - Weeks array: ", $weeks); // Add padding days and flatten $grid_columns = []; foreach ($weeks as $week_number => $week) { // Get the first date of this week $first_date = array_key_first($week); $first_day_num = date('N', strtotime($first_date)); // 1 (Monday) through 7 (Sunday) // Add padding days before if needed if ($first_day_num > 1) { $padding_days = $first_day_num - 1; $monday_date = date('Y-m-d', strtotime("-{$padding_days} days", strtotime($first_date))); for ($i = 0; $i < $padding_days; $i++) { $pad_date = date('Y-m-d', strtotime("+{$i} days", strtotime($monday_date))); $grid_columns[$pad_date] = [ 'target_volume' => 0, 'is_padding' => true ]; } } // Add actual days foreach ($week as $date => $day) { $day['is_padding'] = false; $grid_columns[$date] = $day; } } // log_to_file("rl_mailwarmer_display_campaign_timeline - Grid Columns: ", $grid_columns); $dates = array_keys($grid_columns); $volumes = array_map(function($day) { return (isset($day['is_padding']) && $day['is_padding'] == true ) ? 0 : $day; }, $grid_columns); // log_to_file("rl_mailwarmer_display_campaign_timeline - Grid volumes: ", $volumes); $chart_data = array_map(function($date, $volume) { return [ 'date' => date('M d', strtotime($date)), 'target' => isset($volume['target_volume']) ? $volume['target_volume'] : null , 'sent' => isset($volume['items_sent']) ? $volume['items_sent'] : null ]; }, $dates, $volumes); // log_to_file("Chart Data: ", $chart_data); // Gradient colors array // $gradients = ["#f12711", "#f24913", "#f36b15", "#f48d17", "#f5af19", "#f8c353", "#fad78c", "#fdebc6", "#ffffff"]; $gradients = ["#ffffff", "#fdebc6", "#fad78c", "#f8c353", "#f5af19", "#f48d17", "#f36b15", "#f24913", "#f12711"]; // Generate the grid ?>