File: /home/xedaptot/ai.naniguide.com/database/seeders/Demo/Stage10_CampaignsSeeder.php
<?php
namespace Database\Seeders\Demo;
use App\Model\Campaign;
use App\Model\CampaignsListsSegment;
use App\Model\Email;
use App\Model\MailList;
use App\Model\Sender;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Stage 10 — campaigns + their email rows + list/segment associations.
*
* Per customer:
* - Creates `campaigns` count from DemoConfig with a realistic mix of statuses:
* 60% done — already sent
* 15% new — draft
* 10% sending — currently sending
* 10% scheduled
* 5% error — archive_status=error
* - Each campaign points to one of the customer's lists (random)
* - Each campaign creates a sibling Email row with subject/from/html/etc.
*
* Stores results in self::$created[customer_key] = [Campaign, ...] so the
* tracking-logs stage can build open/click/bounce rows for each "done" campaign.
*/
class Stage10_CampaignsSeeder extends AbstractDemoSeeder
{
protected int $fakerOffset = 10;
/** customer_key → Campaign[] */
public static array $created = [];
public function run(): void
{
$f = $this->faker();
$customers = Stage03_CustomersSeeder::$created;
$total = 0;
foreach (DemoConfig::customers() as $cfg) {
$customer = $customers[$cfg['key']] ?? null;
$lists = Stage07_MailListsSeeder::$created[$cfg['key']] ?? [];
if (!$customer || empty($lists)) continue;
self::$created[$cfg['key']] = [];
$verifiedSenders = Sender::where('customer_id', $customer->id)->verified()->get();
$defaultSender = $verifiedSenders->first();
for ($i = 0; $i < ($cfg['campaigns'] ?? 0); $i++) {
$list = $lists[array_rand($lists)];
$status = $this->pickStatus($f);
$isError = $status === '__error__';
$realStatus = $isError ? Campaign::STATUS_DONE : $status;
// Email row first
$email = new Email([
'type' => Email::TYPE_REGULAR,
'subject' => DemoFaker::campaignSubject(),
'from_email' => $defaultSender ? $defaultSender->email : $list->from_email,
'from_name' => $defaultSender ? $defaultSender->name : $list->from_name,
'reply_to' => $list->from_email,
'html' => $this->randomHtml($f),
'plain' => $this->randomPlain($f),
'preheader' => $f->sentence(8),
'sign_dkim' => true,
'track_open' => true,
'track_click' => true,
'customer_id' => $customer->id,
]);
$email->skip_failed_message = false;
$email->fillDeliveryStatuses($email->getDefaultDeliveryStatuses());
$email->save();
// Assign a real template so reports / variant thumbnails /
// preview routes render actual content (mirrors what the
// campaign wizard does via templateChoose → setTemplate).
$this->assignRandomTemplate($email);
// Campaign row
$campaign = new Campaign();
$campaign->email_id = $email->id;
$campaign->customer_id = $customer->id;
$campaign->default_mail_list_id = $list->id;
$campaign->name = $f->randomElement(['Spring', 'Summer', 'Black Friday', 'Welcome', 'Promo', 'Holiday', 'Newsletter']) . ' ' . $f->randomElement(['#', 'Edition ', 'Wave ']) . $f->numberBetween(1, 50);
$campaign->status = $realStatus;
$campaign->is_error = $isError ? 1 : 0;
$campaign->is_paused = $f->boolean(5);
$campaign->last_error = $isError ? $f->randomElement([
'SMTP authentication failed',
'No verified sender found',
'Sending server quota exceeded',
'Connection timeout to SMTP server',
]) : null;
if (in_array($realStatus, [Campaign::STATUS_DONE, Campaign::STATUS_SENDING], true)) {
// Realistic time distribution that ALSO keeps the
// "Engagement over time" chart populated on the default
// 7-day window. We bias 50% of done campaigns into the
// last 7 days, 30% into 8-30 days, 20% historical.
$bucket = $f->numberBetween(1, 100);
if ($bucket <= 50) {
// last 7 days — gives the 7d chart real curves
$campaign->run_at = now()->subHours($f->numberBetween(2, 168));
} elseif ($bucket <= 80) {
// 8-30 days — fills the 30d chart
$campaign->run_at = now()->subDays($f->numberBetween(8, 30));
} else {
// historical 31-180 days — for "all time" reports
$campaign->run_at = now()->subDays($f->numberBetween(31, 180));
}
$campaign->delivery_at = $campaign->run_at;
}
if ($realStatus === Campaign::STATUS_SCHEDULED) {
$campaign->run_at = now()->addDays($f->numberBetween(1, 30));
}
if ($realStatus === Campaign::STATUS_DONE) {
$campaign->archive_status = $isError ? Campaign::ARCHIVE_STATUS_ERROR : Campaign::ARCHIVE_STATUS_YES;
}
// Spread create dates across last 30 days. For sent / sending
// campaigns clamp ≤ run_at so created_at always precedes the
// send (run_at can be deeper than 30 days for the historical
// bucket — there created_at falls right before run_at).
$campaignCreatedAt = now()->subDays(mt_rand(0, 30))->subSeconds(mt_rand(0, 86399));
if ($campaign->run_at && $campaign->run_at->lt($campaignCreatedAt)) {
$campaignCreatedAt = $campaign->run_at->copy()->subMinutes(mt_rand(5, 1440));
}
$campaign->created_at = $campaignCreatedAt;
$campaign->updated_at = $campaignCreatedAt;
$campaign->save();
// Pivot: campaigns_lists_segments
CampaignsListsSegment::create([
'campaign_id' => $campaign->id,
'mail_list_id' => $list->id,
'segment_id' => null,
'customer_id' => $customer->id,
]);
self::$created[$cfg['key']][] = $campaign;
$total++;
}
$this->log("customer {$cfg['key']}: {$cfg['campaigns']} campaigns");
}
$this->log("seeded {$total} campaigns");
}
protected function pickStatus($f): string
{
$r = $f->numberBetween(1, 100);
if ($r <= 60) return Campaign::STATUS_DONE;
if ($r <= 75) return Campaign::STATUS_NEW; // draft
if ($r <= 85) return Campaign::STATUS_SENDING;
if ($r <= 95) return Campaign::STATUS_SCHEDULED;
return '__error__'; // → done + is_error=1
}
protected function randomHtml($f): string
{
$title = $f->sentence(4);
$body = '<p>' . implode('</p><p>', $f->paragraphs(3)) . '</p>';
return "<!DOCTYPE html><html><body><h1>{$title}</h1>{$body}<p><a href=\"https://acelle-demo.com/cta\">Read more</a></p></body></html>";
}
protected function randomPlain($f): string
{
return $f->paragraph(8);
}
}