'success', 'campaign_id' => $id, 'next' => 'launch_on_platform']); break; case 'launch_meta': $campaign_id = pg_escape_string($db, $_POST['campaign_id'] ?? ''); $account_id_internal = (int)($_POST['account_id'] ?? 0); $camp = pg_fetch_assoc(pg_query($db, "SELECT * FROM xc_campaigns WHERE id='$campaign_id'")); $acc = pg_fetch_assoc(pg_query($db, "SELECT * FROM ads_accounts WHERE id=$account_id_internal AND platform='meta' AND is_active=true")); if (!$camp || !$acc) { echo json_encode(['error' => 'Campaign or account not found']); break; } $token = $acc['access_token_encrypted']; // Decrypt in prod $fb_account = $acc['account_id']; // Create campaign on Meta $meta_url = "https://graph.facebook.com/v18.0/act_$fb_account/campaigns"; $ch = curl_init($meta_url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ 'name' => $camp['campaign_name'], 'objective' => 'OUTCOME_' . strtoupper($camp['objective']), 'status' => 'PAUSED', 'special_ad_categories' => '[]', 'access_token' => $token ]) ]); $resp = json_decode(curl_exec($ch), true); curl_close($ch); if (isset($resp['id'])) { pg_query($db, "UPDATE xc_campaigns SET platform_campaign_id='{$resp['id']}', status='pending', start_date=NOW() WHERE id='$campaign_id'"); echo json_encode(['status' => 'success', 'meta_campaign_id' => $resp['id']]); } else { echo json_encode(['status' => 'error', 'meta_error' => $resp['error'] ?? $resp]); } break; case 'launch_tiktok': $campaign_id = pg_escape_string($db, $_POST['campaign_id'] ?? ''); $account_id_internal = (int)($_POST['account_id'] ?? 0); $camp = pg_fetch_assoc(pg_query($db, "SELECT * FROM xc_campaigns WHERE id='$campaign_id'")); $acc = pg_fetch_assoc(pg_query($db, "SELECT * FROM ads_accounts WHERE id=$account_id_internal AND platform='tiktok' AND is_active=true")); if (!$camp || !$acc) { echo json_encode(['error' => 'Not found']); break; } $token = $acc['access_token_encrypted']; $tt_url = "https://business-api.tiktok.com/open_api/v1.3/campaign/create/"; $ch = curl_init($tt_url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Access-Token: $token", 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'advertiser_id' => $acc['account_id'], 'campaign_name' => $camp['campaign_name'], 'objective_type' => 'CONVERSIONS', 'budget_mode' => 'BUDGET_MODE_DAY', 'budget' => $camp['budget_daily'] ]) ]); $resp = json_decode(curl_exec($ch), true); curl_close($ch); if (($resp['code'] ?? -1) == 0) { $tt_id = $resp['data']['campaign_id']; pg_query($db, "UPDATE xc_campaigns SET platform_campaign_id='$tt_id', status='pending', start_date=NOW() WHERE id='$campaign_id'"); echo json_encode(['status' => 'success', 'tiktok_campaign_id' => $tt_id]); } else { echo json_encode(['status' => 'error', 'tiktok_error' => $resp]); } break; case 'roas_check': // Check ROAS for all active campaigns + make decisions $active = pg_fetch_all(pg_query($db, "SELECT * FROM xc_campaigns WHERE status IN ('active','boosted') ORDER BY last_roas_check ASC")); if (!$active) { echo json_encode(['status' => 'no_active_campaigns']); break; } $decisions = []; foreach ($active as $camp) { // Calculate ROAS $spent = (float)$camp['budget_spent']; $revenue = (float)$camp['revenue_generated']; $roas = $spent > 0 ? round($revenue / $spent, 2) : 0; // Update ROAS pg_query($db, "UPDATE xc_campaigns SET actual_roas=$roas, last_roas_check=NOW() WHERE id='{$camp['id']}'"); // Decisions only after min spend if ($spent < $MIN_SPEND_DECISION) continue; $decision = null; if ($roas < $ROAS_KILL) { // KILL: pause campaign pg_query($db, "UPDATE xc_campaigns SET status='killed', paused_reason='low_roas' WHERE id='{$camp['id']}'"); $decision = ['type' => 'kill', 'reason' => "ROAS $roas < $ROAS_KILL"]; // TODO: Call platform API to pause } elseif ($roas > $ROAS_BOOST && $camp['status'] != 'boosted') { // BOOST: increase budget 50% $new_budget = round($camp['budget_daily'] * 1.5, 2); pg_query($db, "UPDATE xc_campaigns SET status='boosted', budget_daily=$new_budget WHERE id='{$camp['id']}'"); $decision = ['type' => 'boost', 'reason' => "ROAS $roas > $ROAS_BOOST", 'new_budget' => $new_budget]; // TODO: Call platform API to increase budget } if ($decision) { pg_query($db, "INSERT INTO ads_arbitrage_log(campaign_id, decision_type, decision_reason, roas_at_decision, budget_before, budget_after, previous_status, new_status) VALUES('{$camp['id']}', '{$decision['type']}', '".pg_escape_string($db, $decision['reason'])."', $roas, {$camp['budget_daily']}, ".($decision['new_budget'] ?? $camp['budget_daily']).", '{$camp['status']}', '".($decision['type'] == 'kill' ? 'killed' : 'boosted')."')"); $decisions[] = ['campaign' => $camp['campaign_name'], 'decision' => $decision]; } } echo json_encode(['status' => 'success', 'campaigns_checked' => count($active), 'decisions' => $decisions]); break; case 'record_conversion': // Record a conversion from tracking pixel/postback $campaign_id = pg_escape_string($db, $_POST['campaign_id'] ?? $_GET['cid'] ?? ''); $revenue = (float)($_POST['revenue'] ?? $_GET['rev'] ?? 0); if (!$campaign_id) { echo json_encode(['error' => 'Need campaign_id']); break; } pg_query($db, "UPDATE xc_campaigns SET conversions=conversions+1, revenue_generated=revenue_generated+$revenue WHERE id='$campaign_id'"); echo json_encode(['status' => 'recorded', 'campaign_id' => $campaign_id, 'revenue' => $revenue]); break; case 'record_spend': // Update spend from platform webhook or sync $campaign_id = pg_escape_string($db, $_POST['campaign_id'] ?? ''); $spent = (float)($_POST['spent'] ?? 0); $impressions = (int)($_POST['impressions'] ?? 0); $clicks = (int)($_POST['clicks'] ?? 0); if (!$campaign_id) { echo json_encode(['error' => 'Need campaign_id']); break; } pg_query($db, "UPDATE xc_campaigns SET budget_spent=budget_spent+$spent, impressions=impressions+$impressions, clicks=clicks+$clicks, ctr=CASE WHEN impressions+$impressions>0 THEN round((clicks+$clicks)::numeric/(impressions+$impressions)*100,2) ELSE 0 END, cpc=CASE WHEN clicks+$clicks>0 THEN round((budget_spent+$spent)::numeric/(clicks+$clicks),2) ELSE 0 END WHERE id='$campaign_id'"); echo json_encode(['status' => 'updated']); break; case 'create_creative': $titles = $_POST['titles'] ?? ['Default Ad Title']; $descriptions = $_POST['descriptions'] ?? ['Default description']; $media_urls = $_POST['media_urls'] ?? []; $media_type = pg_escape_string($db, $_POST['media_type'] ?? 'video'); $cta = pg_escape_string($db, $_POST['cta'] ?? 'Learn More'); $audience = pg_escape_string($db, json_encode($_POST['audience'] ?? new stdClass())); $yt_job_id = pg_escape_string($db, $_POST['youtube_job_id'] ?? ''); $id = pg_fetch_result(pg_query($db, "SELECT gen_random_uuid()"), 0); $titles_pg = "'{\"" . implode('","', array_map(function($t) use ($db) { return pg_escape_string($db, $t); }, (array)$titles)) . "\"}'"; $descs_pg = "'{\"" . implode('","', array_map(function($d) use ($db) { return pg_escape_string($db, $d); }, (array)$descriptions)) . "\"}'"; $media_pg = "'{\"" . implode('","', array_map(function($m) use ($db) { return pg_escape_string($db, $m); }, (array)$media_urls)) . "\"}'"; pg_query($db, "INSERT INTO ads_creatives(id, title_variations, description_variations, media_urls, media_type, call_to_action, target_audience, source_type) VALUES('$id', $titles_pg, $descs_pg, $media_pg, '$media_type', '$cta', '$audience', '".($yt_job_id ? 'youtube_auto' : 'manual')."')"); echo json_encode(['status' => 'success', 'creative_id' => $id]); break; case 'launch_multichannel': // Launch a full multi-channel campaign: Ads + Email + SMS + YouTube $offer_link = $_POST['offer_link'] ?? ''; $name = $_POST['name'] ?? 'MultiCH-'.date('Ymd'); $budget = (float)($_POST['budget'] ?? 50); $platforms = $_POST['platforms'] ?? 'all'; $enable_email = ($_POST['email'] ?? '0') == '1'; $enable_sms = ($_POST['sms'] ?? '0') == '1'; $enable_youtube = ($_POST['youtube'] ?? '0') == '1'; // 1. Create ads campaign $camp_id = pg_fetch_result(pg_query($db, "SELECT gen_random_uuid()"), 0); pg_query($db, "INSERT INTO xc_campaigns(id, campaign_name, platform_target, offer_link, budget_daily, status) VALUES('$camp_id', '".pg_escape_string($db, $name)."', '".pg_escape_string($db, $platforms)."', '".pg_escape_string($db, $offer_link)."', $budget, 'draft')"); $results = ['campaign_id' => $camp_id, 'channels' => []]; // 2. YouTube video generation (async) if ($enable_youtube) { $yt_resp = json_decode(@file_get_contents("http://127.0.0.1:5821/api/youtube-factory-api.php?action=auto_pipeline"), true); $results['channels']['youtube'] = $yt_resp; if (isset($yt_resp['job_id'])) { pg_query($db, "INSERT INTO xc_content_sync(ads_campaign_id, yt_job_id, content_angle, sync_status) VALUES('$camp_id', '{$yt_resp['job_id']}', '".pg_escape_string($db, $yt_resp['trend'] ?? '')."', 'synced')"); } } // 3. Email campaign trigger if ($enable_email) { $results['channels']['email'] = ['status' => 'queued', 'note' => 'Use unified-send with campaign_id=' . $camp_id]; } // 4. SMS trigger if ($enable_sms) { $results['channels']['sms'] = ['status' => 'queued', 'note' => 'Link SMS batch to campaign_id=' . $camp_id]; } echo json_encode(['status' => 'success', 'multichannel' => $results]); break; case 'roas_live': // Live ROAS dashboard data $campaigns = pg_fetch_all(pg_query($db, " SELECT id, campaign_name, platform_target, status, actual_roas, budget_daily, budget_spent, revenue_generated, impressions, clicks, conversions, ctr, cpc, cpa, EXTRACT(EPOCH FROM (NOW()-start_date))/3600 as hours_running FROM xc_campaigns WHERE status IN ('active','boosted','draft','pending') ORDER BY actual_roas DESC NULLS LAST ")); $totals = pg_fetch_assoc(pg_query($db, " SELECT COALESCE(SUM(budget_spent),0) as total_spent, COALESCE(SUM(revenue_generated),0) as total_revenue, COALESCE(SUM(conversions),0) as total_conversions, CASE WHEN SUM(budget_spent)>0 THEN round(SUM(revenue_generated)/SUM(budget_spent),2) ELSE 0 END as overall_roas, COUNT(*) FILTER (WHERE status='active') as active_count, COUNT(*) FILTER (WHERE status='killed') as killed_count, COUNT(*) FILTER (WHERE status='boosted') as boosted_count FROM xc_campaigns ")); echo json_encode(['status' => 'success', 'totals' => $totals, 'campaigns' => $campaigns ?: []]); break; case 'arbitrage_history': $logs = pg_fetch_all(pg_query($db, " SELECT al.*, xc.campaign_name FROM ads_arbitrage_log al LEFT JOIN xc_campaigns xc ON xc.id=al.campaign_id ORDER BY al.executed_at DESC LIMIT 50 ")); echo json_encode(['status' => 'success', 'decisions' => $logs ?: []]); break; case 'status': $s = pg_fetch_assoc(pg_query($db, " SELECT (SELECT COUNT(*) FROM ads_accounts WHERE is_active=true) as active_ad_accounts, (SELECT COUNT(*) FROM ads_creatives WHERE is_active=true) as active_creatives, (SELECT COUNT(*) FROM xc_campaigns) as total_campaigns, (SELECT COUNT(*) FROM xc_campaigns WHERE status='active') as active_campaigns, (SELECT COUNT(*) FROM xc_campaigns WHERE status='killed') as killed_campaigns, (SELECT COUNT(*) FROM xc_campaigns WHERE status='boosted') as boosted_campaigns, (SELECT COALESCE(SUM(budget_spent),0) FROM xc_campaigns) as total_spent, (SELECT COALESCE(SUM(revenue_generated),0) FROM xc_campaigns) as total_revenue, (SELECT COUNT(*) FROM ads_arbitrage_log) as total_decisions ")); echo json_encode(['status' => 'success', 'ads_commander' => $s]); break; } ?>