File: /home/xedaptot/ai.naniguide.com/database/seeders/DemoSeeder.php
<?php
namespace Database\Seeders;
use Database\Seeders\Demo\DemoConfig;
use Database\Seeders\Demo\DemoWiper;
use Database\Seeders\Demo\Stage01_AdminsSeeder;
use Database\Seeders\Demo\Stage03_CustomersSeeder;
use Database\Seeders\Demo\Stage04_SubscriptionsSeeder;
use Database\Seeders\Demo\Stage05_AdminInfraSeeder;
use Database\Seeders\Demo\Stage06_CustomerInfraSeeder;
use Database\Seeders\Demo\Stage07_MailListsSeeder;
use Database\Seeders\Demo\Stage08_SubscribersSeeder;
use Database\Seeders\Demo\Stage09_TemplatesSeeder;
use Database\Seeders\Demo\Stage10_CampaignsSeeder;
use Database\Seeders\Demo\Stage11_TrackingLogsSeeder;
use Database\Seeders\Demo\Stage12_FormsSeeder;
use Database\Seeders\Demo\Stage14_AutomationsSeeder;
use Database\Seeders\Demo\Stage15_AbTestsSeeder;
use Database\Seeders\Demo\Stage16_FunnelsSeeder;
use Database\Seeders\Demo\Stage17_WebsitesSeeder;
use Database\Seeders\Demo\Stage18_NotificationsSeeder;
use Database\Seeders\Demo\Stage19_SettingsSeeder;
use Database\Seeders\Demo\Stage20_ManifestWriter;
use Database\Seeders\Demo\Stage21_CacheWarmer;
use Database\Seeders\Demo\Stage22_AdsSeeder;
use Database\Seeders\Demo\Stage23_CustomerAssetsSeeder;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
/**
* DemoSeeder — full reproducible demo dataset for the entire Acelle app.
*
* Usage:
* php artisan db:seed --class=DemoSeeder # full wipe + rebuild
* php artisan db:seed --class=DemoSeeder --no-wipe # additive (rare)
*
* Run order: wipe → DatabaseInit (languages, countries, admin_groups, plans,
* sending server) → extras (roles, templates) → Stage01..22.
*
* Each stage class lives in database/seeders/Demo/Stage{NN}_*.php and
* is responsible for one slice of the data. Sub-seeders share state via
* static `$created` arrays so later stages can look up earlier results.
*
* Configuration: see Database\Seeders\Demo\DemoConfig.
* Output: docs/demo/DEMO_ACCOUNTS.json (manifest written by Stage20).
* Documentation: docs/demo/DEMO.md (bot skill) + docs/demo/DEMO_USAGE.md (full guide)
*/
class DemoSeeder extends Seeder
{
public function run(): void
{
// Fail fast if faker (dev dependency) is missing
if (!class_exists(\Faker\Factory::class)) {
echo "\n";
echo "================================================================\n";
echo " ERROR: fakerphp/faker is not installed!\n";
echo " DemoSeeder requires dev dependencies.\n";
echo " Run: composer install\n";
echo "================================================================\n\n";
throw new \RuntimeException('fakerphp/faker is not installed. Run: composer install');
}
$startedAt = microtime(true);
// Symfony console rejects unknown flags — use ENV vars instead of CLI flags.
// Set on the artisan call: `DEMO_NO_WIPE=1 php artisan db:seed ...` etc.
$noWipe = !empty(getenv('DEMO_NO_WIPE')) || in_array('--no-wipe', $_SERVER['argv'] ?? [], true);
$keepCache = !empty(getenv('DEMO_KEEP_CACHE')) || in_array('--keep-cache', $_SERVER['argv'] ?? [], true);
echo "\n";
echo "================================================================\n";
echo " DEMO SEEDER — full app dataset\n";
echo "================================================================\n";
// ─── 1. Wipe ────────────────────────────────────────────────────────
if (!$noWipe) {
DemoWiper::wipe();
// Optionally flush the application cache so stale HasCache entries
// from a previous demo dataset don't bleed through. Skipped under
// --keep-cache (used by demo-reset.sh blue/green: target DB seeds
// run with new UIDs while webapp on the OTHER DB still serves
// requests — flushing would needlessly cold-cache the live DB
// during the seed window. Stale entries are orphan UIDs anyway,
// never queried by the new dataset.)
if (!$keepCache) {
try {
\Illuminate\Support\Facades\Cache::flush();
echo "[demo:wipe] flushed application cache\n";
} catch (\Throwable $e) {
echo "[demo:wipe] cache flush failed (non-fatal): " . $e->getMessage() . "\n";
}
} else {
echo "[demo:wipe] --keep-cache: skipping Cache::flush() (orphan entries by random UID will not collide)\n";
}
} else {
echo "[demo] --no-wipe: skipping destructive wipe\n";
}
// ─── 2. Bootstrap system reference data via DatabaseInit ──────────
// DatabaseInit seeds: languages, countries, admin_groups, plans,
// and a default sending server. It is idempotent (uses firstOrCreate
// patterns), so calling it after a wipe ensures all system reference
// data exists before the demo stages run.
$this->call(DatabaseInit::class);
$this->bootstrapExtrasIfNeeded();
// ─── 3. Demo stages (in dependency order) ──────────────────────────
$stages = [
Stage01_AdminsSeeder::class,
Stage03_CustomersSeeder::class,
Stage04_SubscriptionsSeeder::class,
Stage05_AdminInfraSeeder::class,
Stage06_CustomerInfraSeeder::class,
Stage07_MailListsSeeder::class,
Stage08_SubscribersSeeder::class,
Stage09_TemplatesSeeder::class,
Stage10_CampaignsSeeder::class,
Stage11_TrackingLogsSeeder::class,
Stage12_FormsSeeder::class,
Stage14_AutomationsSeeder::class, // "My Sales Pipeline" automation under Emma + 500 trigger rows
Stage15_AbTestsSeeder::class,
Stage16_FunnelsSeeder::class,
Stage17_WebsitesSeeder::class,
Stage18_NotificationsSeeder::class,
Stage19_SettingsSeeder::class,
Stage22_AdsSeeder::class, // MUST run after customers/lists exist
Stage23_CustomerAssetsSeeder::class, // dynamic upload from database/seeders/Demo/Photos
Stage21_CacheWarmer::class, // MUST run before manifest so totals reflect warm caches
Stage20_ManifestWriter::class,
];
foreach ($stages as $stage) {
if (!class_exists($stage)) {
echo "[demo] ⚠ {$stage} not yet implemented — skipping\n";
continue;
}
$this->call($stage);
}
$elapsed = round(microtime(true) - $startedAt, 1);
echo "================================================================\n";
echo " DEMO SEEDER DONE — elapsed {$elapsed}s\n";
echo " Manifest: " . DemoConfig::manifestPath() . "\n";
echo "================================================================\n\n";
// ─── Print login credentials ───────────────────────────────────
$this->printLoginCredentials();
}
/**
* Bootstrap extras that DatabaseInit does NOT seed: roles, templates,
* and enforce English as active default language.
*
* DatabaseInit already handles: languages, countries, admin_groups,
* plans, sending server, and USD currency.
*/
protected function bootstrapExtrasIfNeeded(): void
{
echo "[demo] checking extras beyond DatabaseInit...\n";
// ─── 1. Force English as active default ───────────────────────────
$en = \App\Model\Language::where('code', 'en')->first();
if ($en) {
$changed = false;
if ($en->status !== \App\Model\Language::STATUS_ACTIVE) {
$en->status = \App\Model\Language::STATUS_ACTIVE;
$changed = true;
}
if (!$en->is_default) {
\App\Model\Language::where('id', '!=', $en->id)->update(['is_default' => 0]);
$en->is_default = true;
$changed = true;
}
if ($changed) {
$en->save();
echo "[demo] → languages: forced English as active default\n";
}
}
// ─── 2. Roles ─────────────────────────────────────────────────────
if (\App\Model\Role::count() === 0) {
$this->bootstrapDefaultRoles();
echo "[demo] → roles: " . \App\Model\Role::count() . " rows\n";
}
// ─── 3. Templates — seed canonical system email/form templates ────
if (\App\Model\SystemEmailTemplate::count() === 0) {
(new \Database\Seeders\TemplateSeeder())->run();
echo "[demo] → templates: " . \App\Model\SystemEmailTemplate::count() . " system email templates\n";
}
echo "[demo] extras OK\n";
}
/**
* Print the primary login credentials after seeding completes.
*/
protected function printLoginCredentials(): void
{
$admin = DemoConfig::admins()[0] ?? null;
$primary = null;
foreach (DemoConfig::customers() as $c) {
if ($c['key'] === 'primary') { $primary = $c; break; }
}
echo "┌─────────────────────────────────────────────────────────────┐\n";
echo "│ LOGIN CREDENTIALS │\n";
echo "├──────────┬──────────────────────────────────┬──────────────┤\n";
echo "│ Role │ Email │ Password │\n";
echo "├──────────┼──────────────────────────────────┼──────────────┤\n";
if ($admin) {
printf("│ Admin │ %-32s│ %-12s│\n", $admin['email'], $admin['password']);
}
if ($primary) {
printf("│ Customer│ %-32s│ %-12s│\n", $primary['email'], $primary['password']);
}
echo "└──────────┴──────────────────────────────────┴──────────────┘\n";
echo "\n";
if ($admin) {
echo " Admin panel: /admin\n";
}
if ($primary) {
echo " Customer app: /dashboard\n";
}
echo "\n";
}
/**
* Recreate the default global roles (`organization_admin`, `organization_readonly`).
* Mirrors the AddDefaultRoles migration so we don't depend on it having
* been re-run. The customer/admin creation flow looks up the admin role
* via `Role::whereCode('organization_admin')->first()`, so the code MUST
* be exactly that string.
*/
protected function bootstrapDefaultRoles(): void
{
if (\App\Model\Role::count() > 0) return;
$adminRole = \App\Model\Role::newGlobalRole();
$adminRole->code = 'organization_admin';
$adminRole->name = 'Full Access';
$adminRole->description = 'The default role for new users who have full access to the customer account.';
$adminRole->is_global = true;
$adminRole->readonly = true;
$adminRole->save();
try { $adminRole->assignDefaultAdminPermissions(); } catch (\Throwable $e) {}
$readonlyRole = \App\Model\Role::newGlobalRole();
$readonlyRole->code = 'organization_readonly';
$readonlyRole->name = 'Read Only';
$readonlyRole->description = 'The default role for new users who can view all customer account data without the ability to edit or change it.';
$readonlyRole->is_global = true;
$readonlyRole->readonly = true;
$readonlyRole->save();
try { $readonlyRole->assignDefaultReadonlyPermissions(); } catch (\Throwable $e) {}
}
}