File: //home/xedaptot/ai.naniguide.com/database/seeders/Demo/Stage22_AdsSeeder.php
<?php
namespace Database\Seeders\Demo;
use App\Model\AdAbTest;
use App\Model\AdAbVariant;
use App\Model\AdAccount;
use App\Model\AdActivityLog;
use App\Model\AdAudience;
use App\Model\AdAutomationRule;
use App\Model\AdCampaign;
use App\Model\AdCampaignPlatform;
use App\Model\AdConversion;
use App\Model\AdCreative;
use App\Model\AdLeadFormConfig;
use App\Model\AdLead;
use App\Model\AdMetric;
use App\Model\AdPixel;
use App\Model\AdPlatform;
use App\Model\AdProduct;
use App\Model\AdProductCatalog;
use App\Model\AdPublishLog;
use App\Model\AdTemplate;
use App\Model\Customer;
use App\Model\Plan;
use App\Model\Setting;
use Illuminate\Support\Facades\DB;
/**
* Stage 22 — Ads module demo data.
*
* Seeds realistic data for ALL 13 ads phases (A–M):
* - Enables ads_enabled='yes' on all plans (so the sidebar nav appears)
* - Per customer: 4 ad_platforms (one per platform), 1-2 ad_accounts each
* - 3-5 ad_campaigns per customer with mixed statuses (draft/active/paused)
* - 2-3 ad_audiences from email lists
* - 1-2 ad_ab_tests per customer (with 2-3 variants each)
* - 1-2 ad_automation_rules per customer
* - 1-2 ad_pixels per customer
* - 1 ad_product_catalog with 5-10 products per customer
* - 1 ad_lead_form_config per customer with 3-5 leads
* - 14 days of ad_metrics for active campaigns
* - System ad_templates (6 starter templates, available to all customers)
* - Activity logs for all major actions
*
* **Critical:** This stage runs AFTER Stage20_ManifestWriter so all customers
* exist with subscriptions. Seeders skip silently if model classes don't exist.
*/
class Stage22_AdsSeeder extends AbstractDemoSeeder
{
protected int $fakerOffset = 22;
public function run(): void
{
if (!class_exists(AdPlatform::class)) {
$this->log('AdPlatform model missing — skipping');
return;
}
$f = $this->faker();
// 0. Enable ads on all plans — set MAX_ADS_* quotas to unlimited (null).
$this->time('enabling ads on all plans', function () {
$service = app(\App\Services\Plans\PlansService::class);
$plans = Plan::all();
foreach ($plans as $plan) {
$service->setQuotas($plan, array_merge(
// Preserve existing quotas — read current rows and merge
\App\Model\PlanQuota::where('plan_id', $plan->id)->pluck('limit_value', 'quota_key')->all(),
[
\App\Services\Plans\Quotas\QuotaKey::MAX_ADS_PLATFORMS->value => null,
\App\Services\Plans\Quotas\QuotaKey::MAX_ADS_CAMPAIGNS->value => null,
\App\Services\Plans\Quotas\QuotaKey::MAX_ADS_AUDIENCES->value => null,
]
));
}
$this->log('enabled ads (unlimited) on ' . $plans->count() . ' plans');
});
// 0.5 Phase N — Admin self-service credentials (seed settings so admin
// wizard shows pre-configured state instead of empty form). Mock mode is
// enabled so testAppCredentials() returns happy path for all 4 platforms.
$this->time('seeding Phase N admin credentials', function () {
if (!class_exists(Setting::class)) {
$this->log('Setting model missing — skipping Phase N');
return;
}
$creds = [
'ads.mock_mode' => 'yes',
'ads.meta.app_id' => '3847291085647203',
'ads.meta.app_secret' => 'demo_meta_secret_abcdef0123456789',
'ads.google.client_id' => '847291085647-abc123def456.apps.googleusercontent.com',
'ads.google.client_secret' => 'GOCSPX-demo_google_client_secret_xyz',
'ads.google.developer_token' => 'DemoDevTokenXXXXXXXXXX',
'ads.tiktok.app_id' => '7291085647203',
'ads.tiktok.app_secret' => 'demo_tiktok_secret_abcdef0123456789',
'ads.linkedin.client_id' => '78abc123def456ghi',
'ads.linkedin.client_secret' => 'demo_linkedin_secret_xyz123',
];
foreach ($creds as $key => $value) {
try {
Setting::set($key, $value);
} catch (\Throwable $e) {
// ignore if setting table schema differs
}
}
$this->log('seeded ' . count($creds) . ' Phase N admin credentials');
});
// 1. System templates — read sample JSON + SVG thumbnails FROM DISK.
// These files live in resources/themes/default/master/sample/ads/
// and are synced one-way from the builderjs repo via copy.sh.
// NEVER inline content arrays here — see docs/builderjs-integration/BUILDER_CONNECT.md
// "Sample Templates + Thumbnails — ONE-WAY RULE".
$this->time('seeding system ad templates from disk', function () {
$manifest = [
['name' => 'Blank', 'platform' => AdTemplate::PLATFORM_UNIVERSAL, 'format' => AdTemplate::FORMAT_IMAGE, 'featured' => false, 'file' => 'Blank'],
['name' => 'Facebook Feed', 'platform' => 'meta', 'format' => AdTemplate::FORMAT_FEED, 'featured' => true, 'file' => 'FacebookFeed'],
['name' => 'Instagram Feed', 'platform' => 'meta', 'format' => AdTemplate::FORMAT_IMAGE, 'featured' => true, 'file' => 'InstagramFeed'],
['name' => 'Instagram Story', 'platform' => 'meta', 'format' => AdTemplate::FORMAT_STORY, 'featured' => true, 'file' => 'InstagramStory'],
['name' => 'Meta Carousel', 'platform' => 'meta', 'format' => AdTemplate::FORMAT_CAROUSEL, 'featured' => false, 'file' => 'MetaCarousel'],
['name' => 'Google Responsive', 'platform' => 'google', 'format' => AdTemplate::FORMAT_IMAGE, 'featured' => true, 'file' => 'GoogleResponsive'],
['name' => 'TikTok Video', 'platform' => 'tiktok', 'format' => AdTemplate::FORMAT_VIDEO, 'featured' => false, 'file' => 'TikTokVideo'],
['name' => 'LinkedIn Sponsored', 'platform' => 'linkedin', 'format' => AdTemplate::FORMAT_FEED, 'featured' => false, 'file' => 'LinkedInSponsored'],
];
$base = resource_path('themes/default/master/sample/ads');
// Idempotent: wipe system templates and re-seed from the manifest so
// changes to the sample JSONs propagate cleanly on re-run.
AdTemplate::whereNull('customer_id')->delete();
$seeded = 0;
foreach ($manifest as $meta) {
$jsonPath = $base . '/' . $meta['file'] . '.json';
$svgPath = $base . '/' . $meta['file'] . '.svg';
if (!is_file($jsonPath)) {
$this->log('missing sample file ' . $jsonPath . ' — skipping');
continue;
}
$content = json_decode((string) file_get_contents($jsonPath), true);
if (!is_array($content)) {
$this->log('invalid JSON in ' . $jsonPath . ' — skipping');
continue;
}
$tpl = AdTemplate::create([
'uid' => \Illuminate\Support\Str::random(32),
'customer_id' => null,
'name' => $meta['name'],
'platform' => $meta['platform'],
'format' => $meta['format'],
'category' => null,
'is_featured' => $meta['featured'],
'use_count' => 0,
'content' => $content,
]);
if (is_file($svgPath)) {
$tpl->updateThumbnailFromPath($svgPath);
}
$seeded++;
}
$this->log('created ' . $seeded . ' system ad templates from disk');
});
// Get demo customers — focus on top customers for richest data
$customers = Customer::limit(8)->get();
if ($customers->isEmpty()) {
$this->log('no customers found — skipping per-customer ads data');
return;
}
$platformsCreated = 0;
$campaignsCreated = 0;
$audiencesCreated = 0;
$abTestsCreated = 0;
$automationsCreated = 0;
$pixelsCreated = 0;
$catalogsCreated = 0;
$leadFormsCreated = 0;
$metricsCreated = 0;
// Collect rows for bulk insert across all customers — these tables don't
// need IDs returned to drive subsequent inserts (leaf tables, no children),
// so a single bulk insert per table is dramatically faster than per-row
// Eloquent create(). Tables touched: ad_metrics, ad_conversions, ad_products,
// ad_leads, ad_publish_logs, ad_activity_logs.
$now = now();
$metricRows = [];
$conversionRows = [];
$productRows = [];
$leadRows = [];
$publishLogRows = [];
$activityLogRows = [];
$this->time('seeding per-customer ads data', function () use (
$customers, $f, $now,
&$platformsCreated, &$campaignsCreated, &$audiencesCreated, &$abTestsCreated,
&$automationsCreated, &$pixelsCreated, &$catalogsCreated, &$leadFormsCreated, &$metricsCreated,
&$metricRows, &$conversionRows, &$productRows, &$leadRows, &$publishLogRows, &$activityLogRows
) {
foreach ($customers as $customer) {
// 2. Platforms — one per platform type
$platforms = [];
foreach (AdPlatform::PLATFORMS as $platformType) {
if (AdPlatform::where('customer_id', $customer->id)->where('platform', $platformType)->exists()) {
$platforms[$platformType] = AdPlatform::where('customer_id', $customer->id)->where('platform', $platformType)->first();
continue;
}
$p = AdPlatform::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'platform' => $platformType,
'name' => AdPlatform::PLATFORM_LABELS[$platformType],
'access_token' => 'demo_access_' . $platformType . '_' . $customer->id,
'token_expires_at' => now()->addDays(60),
'platform_user_id' => (string) ($customer->id * 1000 + array_search($platformType, AdPlatform::PLATFORMS)),
'platform_user_name' => 'Customer ' . $customer->id,
'scopes' => ['ads_management', 'ads_read'],
'status' => AdPlatform::STATUS_ACTIVE,
'health_status' => AdPlatform::HEALTH_HEALTHY,
'last_health_check_at' => now()->subMinutes(mt_rand(5, 60)),
]);
$platforms[$platformType] = $p;
$platformsCreated++;
// Ad accounts — 1-2 per platform
for ($i = 0; $i < mt_rand(1, 2); $i++) {
AdAccount::create([
'uid' => uniqid(),
'ad_platform_id' => $p->id,
'remote_account_id' => 'act_' . $platformType . '_' . $customer->id . '_' . $i,
'name' => 'Account ' . ($i + 1) . ' for Customer ' . $customer->id,
'currency' => 'USD',
'timezone' => 'America/New_York',
'status' => 'active',
'is_default' => $i === 0,
]);
}
}
// 3. Campaigns — 3-5 with mixed statuses
$campaigns = [];
$campaignSpecs = [
['Summer Sale Promo', 'sales', 'active'],
['Brand Awareness Q1', 'awareness', 'active'],
['Newsletter Growth', 'leads', 'paused'],
['Holiday Campaign Draft', 'sales', 'draft'],
['Webinar Promotion', 'traffic', 'completed'],
];
foreach (array_slice($campaignSpecs, 0, mt_rand(3, 5)) as $spec) {
$campaign = AdCampaign::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'name' => $spec[0],
'objective' => $spec[1],
'status' => $spec[2],
'daily_budget' => mt_rand(20, 500),
'budget_currency' => 'USD',
'bid_strategy' => 'auto',
'utm_source' => 'acelle_ads',
'utm_medium' => 'paid_social',
'utm_campaign' => strtolower(str_replace(' ', '_', $spec[0])),
'schedule_start' => now()->subDays(mt_rand(7, 30)),
'published_at' => $spec[2] !== 'draft' ? now()->subDays(mt_rand(5, 25)) : null,
]);
$campaigns[] = $campaign;
$campaignsCreated++;
// Link to 1-2 platforms
$linkedPlatforms = array_rand($platforms, min(2, count($platforms)));
if (!is_array($linkedPlatforms)) $linkedPlatforms = [$linkedPlatforms];
foreach ($linkedPlatforms as $pt) {
AdCampaignPlatform::create([
'ad_campaign_id' => $campaign->id,
'ad_platform_id' => $platforms[$pt]->id,
'remote_campaign_id' => 'demo_' . $pt . '_' . $campaign->id,
'status' => $spec[2] === 'active' ? 'active' : 'pending',
'last_synced_at' => now()->subHours(mt_rand(1, 12)),
]);
}
// 4. Metrics for active/completed campaigns — 14 days with
// realistic trends: CTR improves over time (optimization),
// weekend dips, budget spent grows accordingly.
if (in_array($spec[2], ['active', 'completed'])) {
$linkedPlatformKey = is_array($linkedPlatforms) ? $linkedPlatforms[0] : $linkedPlatforms;
$platformObj = $platforms[$linkedPlatformKey];
for ($d = 0; $d < 14; $d++) {
$daysAgo = 13 - $d;
$date = now()->subDays($daysAgo);
$isWeekend = $date->dayOfWeek === 0 || $date->dayOfWeek === 6;
// CTR improves from ~1% → ~3% as campaign optimizes
$baseCtrBps = 100 + ($d * 15) + mt_rand(-10, 20); // basis points
$impressions = ($isWeekend ? mt_rand(500, 2500) : mt_rand(2000, 6000))
* max(1, (int) round(($campaign->daily_budget ?? 50) / 50));
$clicks = (int) round($impressions * $baseCtrBps / 10000);
$spend = round(min($campaign->daily_budget ?? 50, mt_rand(800, 5500) / 100), 2);
$conversions = (int) round($clicks * (mt_rand(20, 80) / 1000));
$metricRows[] = [
'ad_campaign_id' => $campaign->id,
'platform' => $platformObj->platform,
'date' => $date->format('Y-m-d'),
'impressions' => $impressions,
'clicks' => $clicks,
'spend' => $spend,
'conversions' => $conversions,
'ctr' => $impressions ? round(($clicks / $impressions) * 100, 4) : 0,
'cpc' => $clicks ? round($spend / $clicks, 2) : 0,
'cpm' => round($spend / ($impressions / 1000), 2),
'roas' => $spend > 0 ? round(($conversions * 65) / $spend, 2) : 0,
'created_at' => $now,
'updated_at' => $now,
];
$metricsCreated++;
}
}
// 4b. Creative for each campaign — seeded with real BuilderJS
// content from disk sample JSONs (same one-way rule as templates)
// + an unsplash hero image so the grid-view thumbnail renders.
$ctaMap = [
'sales' => 'shop_now',
'awareness' => 'learn_more',
'leads' => 'sign_up',
'traffic' => 'learn_more',
'engagement' => 'learn_more',
'app_installs' => 'download',
];
$headlineMap = [
'Summer Sale Promo' => 'Up to 40% Off — Ends Sunday',
'Brand Awareness Q1' => 'Meet the Team Behind Acelle',
'Newsletter Growth' => 'Join 50,000+ Marketers on Our Weekly Digest',
'Holiday Campaign Draft' => 'Your Holiday Gift Guide is Here',
'Webinar Promotion' => 'Free Masterclass: Email Marketing in 2026',
];
$sampleMap = [
'sales' => 'FacebookFeed',
'awareness' => 'InstagramFeed',
'leads' => 'GoogleResponsive',
'traffic' => 'FacebookFeed',
'engagement' => 'InstagramStory',
];
$heroImageMap = [
'Summer Sale Promo' => 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=1080&h=1080&fit=crop',
'Brand Awareness Q1' => 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=1080&h=1080&fit=crop',
'Newsletter Growth' => 'https://images.unsplash.com/photo-1607082348824-0a96f2a4b9da?w=1200&h=628&fit=crop',
'Holiday Campaign Draft' => 'https://images.unsplash.com/photo-1513885535751-8b9238bd345a?w=1080&h=1080&fit=crop',
'Webinar Promotion' => 'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=1080&h=1080&fit=crop',
];
$sampleBase = resource_path('themes/default/master/sample/ads');
$sampleKey = $sampleMap[$spec[1]] ?? 'FacebookFeed';
$samplePath = $sampleBase . '/' . $sampleKey . '.json';
$builderContent = null;
if (is_file($samplePath)) {
$decoded = json_decode((string) file_get_contents($samplePath), true);
if (is_array($decoded)) $builderContent = $decoded;
}
$creative = AdCreative::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'ad_campaign_id' => $campaign->id,
'name' => 'Primary creative — ' . $spec[0],
'type' => 'image',
'headline' => $headlineMap[$spec[0]] ?? ('Discover ' . $spec[0]),
'primary_text' => 'Limited time offer — ' . $spec[0] . '. Click to learn more.',
'description' => 'Acelle Mail ' . $spec[1] . ' campaign',
'call_to_action' => $ctaMap[$spec[1]] ?? 'learn_more',
'destination_url' => 'https://example.com/offer/' . strtolower(str_replace(' ', '-', $spec[0])),
'image_url' => $heroImageMap[$spec[0]] ?? null,
'builder_content' => $builderContent,
]);
$campaign->ad_creative_id = $creative->id;
$campaign->save();
// 4c. Publish logs (1 publish per platform link, mostly success + occasional error)
foreach ($linkedPlatforms as $pt) {
$isError = mt_rand(1, 100) <= 15; // 15% error rate
$publishLogRows[] = [
'ad_campaign_id' => $campaign->id,
'ad_platform_id' => $platforms[$pt]->id,
'platform' => $pt,
'action' => 'publish',
'status' => $isError ? 'error' : 'success',
'request_data' => json_encode(['campaign_name' => $spec[0], 'daily_budget' => $campaign->daily_budget]),
'response_data' => $isError ? null : json_encode(['remote_id' => 'demo_' . $pt . '_' . $campaign->id, 'status' => 'active']),
'error_message' => $isError ? 'Creative rejected: logo too small (must be ≥200px)' : null,
'created_at' => $campaign->published_at ?? now()->subDays(mt_rand(1, 10)),
'updated_at' => $now,
];
}
}
// 5. Audiences — custom (from mail lists) + 1 lookalike + 1 saved/retargeting
$mailLists = $customer->mailLists ?? collect();
if (method_exists($customer, 'mailLists')) {
$mailLists = $customer->mailLists()->limit(3)->get();
}
$firstCustomAudience = null;
foreach ($mailLists->take(2) as $idx => $list) {
$audience = AdAudience::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'name' => $list->name . ' — Subscribers',
'type' => 'custom',
'source_type' => 'mail_list',
'source_id' => $list->id,
'platform' => $idx === 0 ? 'meta' : 'google',
'platform_audience_id' => 'aud_custom_' . $list->id,
'size_estimate' => mt_rand(500, 50000),
'status' => 'synced',
'sync_status' => 'completed',
'last_synced_at' => now()->subHours(mt_rand(1, 24)),
]);
if ($firstCustomAudience === null) $firstCustomAudience = $audience;
$audiencesCreated++;
}
// Lookalike audience (based on first custom audience)
if ($firstCustomAudience) {
AdAudience::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'name' => 'Lookalike 1% — ' . $firstCustomAudience->name,
'type' => 'lookalike',
'source_type' => null,
'source_id' => $firstCustomAudience->id,
'platform' => 'meta',
'platform_audience_id' => 'aud_lal_' . $firstCustomAudience->id,
'size_estimate' => mt_rand(80000, 250000),
'status' => 'synced',
'sync_status' => 'completed',
'last_synced_at' => now()->subHours(mt_rand(1, 48)),
'settings' => ['similarity_percent' => 1, 'country' => 'US'],
]);
$audiencesCreated++;
}
// Saved / retargeting audience (website visitors, past purchasers)
AdAudience::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'name' => 'Website Visitors (Last 30 Days)',
'type' => 'saved',
'source_type' => null,
'platform' => 'meta',
'platform_audience_id' => 'aud_retarget_' . $customer->id,
'size_estimate' => mt_rand(5000, 45000),
'status' => 'synced',
'sync_status' => 'completed',
'last_synced_at' => now()->subHours(mt_rand(1, 6)),
]);
$audiencesCreated++;
// 6. A/B tests — 1-2 per customer if campaigns exist
if (count($campaigns) > 0) {
foreach (array_slice($campaigns, 0, mt_rand(1, 2)) as $campaign) {
$abTest = AdAbTest::create([
'uid' => uniqid(),
'ad_campaign_id' => $campaign->id,
'status' => mt_rand(0, 1) ? 'running' : 'completed',
'confidence' => mt_rand(80, 99),
'settings' => ['significance_threshold' => 95, 'split' => [50, 50]],
'started_at' => now()->subDays(mt_rand(3, 14)),
'completed_at' => mt_rand(0, 1) ? now()->subDays(mt_rand(1, 5)) : null,
]);
$abTestsCreated++;
// 2-3 variants with a VISIBLE winner — winner gets 2x CTR
$variantCount = mt_rand(2, 3);
$winnerIdx = 0; // variant A wins deterministically
for ($v = 0; $v < $variantCount; $v++) {
$isWinner = $v === $winnerIdx;
$impressions = mt_rand(3000, 8000);
// Winner gets 3-4% CTR, losers get 1-2% CTR
$ctrBps = $isWinner ? mt_rand(300, 420) : mt_rand(100, 200);
$clicks = (int) round($impressions * $ctrBps / 10000);
$spend = round($impressions / 1000 * mt_rand(300, 800) / 100, 2);
AdAbVariant::create([
'uid' => uniqid(),
'ad_ab_test_id' => $abTest->id,
'name' => 'Variant ' . chr(65 + $v) . ($isWinner ? ' — Short urgent headline' : ' — Long descriptive headline'),
'weight' => (int) (100 / $variantCount),
'impressions' => $impressions,
'clicks' => $clicks,
'spend' => $spend,
'conversions' => (int) round($clicks * ($isWinner ? 0.08 : 0.04)),
'ctr' => round(($clicks / $impressions) * 100, 4),
'is_winner' => $isWinner,
]);
}
}
}
// 7. Automation rules
$automationSpecs = [
['Open → Add to Audience', 'email_open', 'add_to_audience'],
['Click → Start Campaign', 'email_click', 'start_campaign'],
];
foreach (array_slice($automationSpecs, 0, mt_rand(1, 2)) as $spec) {
AdAutomationRule::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'name' => $spec[0],
'trigger_event' => $spec[1],
'action_type' => $spec[2],
'status' => 'active',
'executions_count' => mt_rand(10, 500),
'last_triggered_at' => now()->subHours(mt_rand(1, 48)),
]);
$automationsCreated++;
}
// 8. Pixels + sample conversions
$createdPixels = [];
foreach (['meta', 'google'] as $pp) {
$pixel = AdPixel::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'platform' => $pp,
'pixel_id' => $pp . '_pix_' . str_pad((string) $customer->id, 6, '0', STR_PAD_LEFT),
'status' => 'active',
'last_verified_at' => now()->subHours(mt_rand(1, 24)),
]);
$createdPixels[] = $pixel;
$pixelsCreated++;
}
// 8b. Sample conversions per pixel (showcase pixel tracking)
$eventNames = ['PageView', 'AddToCart', 'InitiateCheckout', 'Purchase', 'Lead'];
foreach ($createdPixels as $pixel) {
$conversionCount = mt_rand(8, 25);
for ($c = 0; $c < $conversionCount; $c++) {
$event = $eventNames[array_rand($eventNames)];
$value = in_array($event, ['Purchase', 'AddToCart', 'InitiateCheckout'])
? round(mt_rand(1500, 25000) / 100, 2)
: null;
$conversionRows[] = [
'ad_pixel_id' => $pixel->id,
'event_name' => $event,
'source_url' => 'https://example.com/' . strtolower($event) . '/product-' . mt_rand(1, 100),
'value' => $value,
'currency' => $value ? 'USD' : null,
'attribution' => json_encode(['source' => 'paid_social', 'platform' => $pixel->platform]),
'created_at' => now()->subHours(mt_rand(1, 168)),
'updated_at' => $now,
];
}
}
// 9. Product catalog (1 catalog with 5-10 products)
$catalog = AdProductCatalog::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'name' => 'Catalog #' . $customer->id,
'source_type' => 'manual',
'sync_status' => 'completed',
'last_synced_at' => now()->subDays(mt_rand(1, 7)),
'product_count' => 0,
]);
$catalogsCreated++;
$productCount = mt_rand(5, 10);
$sampleProducts = [
['Premium Widget Pro', 'widgets', 49.99],
['Deluxe Gadget Plus', 'gadgets', 99.99],
['Standard Tool Kit', 'tools', 29.99],
['Eco Bundle', 'eco', 79.99],
['Fast Charger', 'electronics', 19.99],
['Smart Lamp', 'electronics', 39.99],
['Coffee Subscription', 'food', 24.99],
['Yoga Mat Pro', 'fitness', 49.99],
['Travel Backpack', 'travel', 89.99],
['Wireless Headphones', 'electronics', 129.99],
];
for ($p = 0; $p < $productCount; $p++) {
$sample = $sampleProducts[$p % count($sampleProducts)];
$productRows[] = [
'ad_product_catalog_id' => $catalog->id,
'external_id' => 'sku_' . $customer->id . '_' . $p,
'title' => $sample[0],
'description' => 'High quality ' . $sample[0] . ' for everyday use.',
'price' => $sample[2],
'currency' => 'USD',
'availability' => 'in_stock',
'category' => $sample[1],
'created_at' => $now,
'updated_at' => $now,
];
}
$catalog->update(['product_count' => $productCount]);
// 10. Lead form configs — 1 Meta + 1 Google per customer
$defaultList = $mailLists->first();
$realisticLeadEmails = [
['[email protected]', 'Alice Chen'],
['[email protected]', 'Bob Martinez'],
['[email protected]', 'Carol Wright'],
['[email protected]', 'David Kim'],
['[email protected]', 'Emma Wilson'],
['[email protected]', 'Frank Ng'],
['[email protected]', 'Grace Lee'],
['[email protected]', 'Henry Okoro'],
['[email protected]', 'Iris Patel'],
['[email protected]', 'James Takeda'],
];
foreach (['meta', 'google'] as $lfPlatform) {
$leadForm = AdLeadFormConfig::create([
'uid' => uniqid(),
'customer_id' => $customer->id,
'platform' => $lfPlatform,
'form_id' => $lfPlatform . '_form_' . str_pad((string) $customer->id, 6, '0', STR_PAD_LEFT),
'mail_list_id' => $defaultList?->id,
'field_mapping' => [
'email' => 'email',
'name' => 'first_name',
'phone' => 'phone',
'company' => 'company',
],
'status' => 'active',
'leads_count' => mt_rand(3, 10),
'last_synced_at' => now()->subHours(mt_rand(1, 12)),
]);
$leadFormsCreated++;
// 3-5 sample leads with realistic emails
$leadCount = mt_rand(3, 5);
$pickedEmails = array_rand($realisticLeadEmails, $leadCount);
if (!is_array($pickedEmails)) $pickedEmails = [$pickedEmails];
foreach ($pickedEmails as $emailIdx) {
$leadInfo = $realisticLeadEmails[$emailIdx];
$leadRows[] = [
'ad_lead_form_config_id' => $leadForm->id,
'platform_lead_id' => 'lead_' . $customer->id . '_' . $lfPlatform . '_' . $emailIdx,
'data' => json_encode([
'email' => $leadInfo[0],
'name' => $leadInfo[1],
'phone' => '+1-555-' . mt_rand(1000, 9999),
'company' => explode('@', $leadInfo[0])[1],
]),
'synced_at' => now()->subHours(mt_rand(1, 48)),
'created_at' => $now,
'updated_at' => $now,
];
}
}
// 11. Activity logs — rich mix of success, warning, error with
// resolution hints (realistic demo shows both wins and failures).
$activities = [
// Successes
['platform.connected', 'success', 'Meta Business account connected', 'meta', null, null, null],
['platform.connected', 'success', 'Google Ads account connected', 'google', null, null, null],
['campaign.created', 'success', 'Created campaign "Summer Sale Promo"', null, null, null, null],
['campaign.published', 'success', 'Campaign published to Meta (ID: act_123)', 'meta', null, null, null],
['audience.synced', 'success', 'Audience synced to Facebook (12,450 users)', 'meta', null, null, null],
['ab_test.started', 'info', 'A/B test started — Variant A vs Variant B', null, null, null, null],
['pixel.verified', 'success', 'Meta Pixel verified (last 24h: 342 events)', 'meta', null, null, null],
['conversion.tracked', 'info', 'Conversion tracked: Purchase $49.99', 'meta', null, null, null],
// Warnings
['token.expiring_soon', 'warning', 'Meta access token expires in 5 days', 'meta', 'TOKEN_EXPIRING', 'Access token will expire on ' . now()->addDays(5)->format('M d'), 'Click "Reconnect" on the Platforms page to refresh your token before it expires.'],
['audience.size_warning', 'warning', 'Audience "VIP Customers" below Meta minimum (87 users)', 'meta', 'AUDIENCE_TOO_SMALL', 'Audience has 87 users — Meta requires at least 100 for targeting.', 'Grow your list by 13+ subscribers or combine with another audience.'],
// Errors
['campaign.publish_failed', 'error', 'Publish to Meta failed: Creative rejected', 'meta', 'CREATIVE_REJECTED', 'Meta rejected the primary image: logo size must be ≥200×200px.', 'Edit your creative and upload a larger logo. Then click "Retry Publish".'],
['audience.sync_failed', 'error', 'Audience sync to Google failed', 'google', 'GOOGLE_API_RATE_LIMIT', 'Google Ads API rate limit exceeded (10,000 req/day).', 'Sync will auto-retry in 2 hours. Upgrade your Google Ads API quota for higher limits.'],
];
$firstPlatform = reset($platforms);
foreach ($activities as $a) {
if (!$firstPlatform) continue;
$activityLogRows[] = [
'uid' => uniqid(),
'customer_id' => $customer->id,
'loggable_type' => AdPlatform::class,
'loggable_id' => $firstPlatform->id,
'action' => $a[0],
'status' => $a[1],
'title' => $a[2],
'platform' => $a[3],
'error_code' => $a[4] ?? null,
'error_message' => $a[5] ?? null,
'resolution_hint' => $a[6] ?? null,
'created_at' => now()->subHours(mt_rand(1, 168)), // last 7 days
];
}
}
});
// Flush collected rows in chunks (single INSERT per chunk → ~6 INSERTs
// total instead of ~900 per-row Eloquent creates).
$this->time('bulk-insert leaf ad tables', function () use (
$metricRows, $conversionRows, $productRows, $leadRows, $publishLogRows, $activityLogRows
) {
$bulk = [
'ad_metrics' => $metricRows,
'ad_conversions' => $conversionRows,
'ad_products' => $productRows,
'ad_leads' => $leadRows,
'ad_publish_logs' => $publishLogRows,
'ad_activity_logs' => $activityLogRows,
];
foreach ($bulk as $table => $rows) {
if (empty($rows)) continue;
foreach (array_chunk($rows, 500) as $chunk) {
DB::table($table)->insert($chunk);
}
$this->log("bulk inserted " . count($rows) . " rows into {$table}");
}
});
$this->log("seeded ads data: {$platformsCreated} platforms, {$campaignsCreated} campaigns, {$audiencesCreated} audiences, {$abTestsCreated} A/B tests, {$automationsCreated} automations, {$pixelsCreated} pixels, {$catalogsCreated} catalogs, {$leadFormsCreated} lead forms, {$metricsCreated} metrics");
}
}