Add DemoDataSeeder with 300 customers and realistic data
Generates 300 customers with profiles, 500 subscriptions, 400 services, 800 invoices, 600 transactions, 150 orders, 200 tickets with ~700 replies, 50 coupons, and 100 audit log entries. Data spread across 12 months with realistic plan linkage and varied statuses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ class DatabaseSeeder extends Seeder
|
||||
RoleAndPermissionSeeder::class,
|
||||
PlanSeeder::class,
|
||||
AdminUserSeeder::class,
|
||||
DemoDataSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
864
website/database/seeders/DemoDataSeeder.php
Normal file
864
website/database/seeders/DemoDataSeeder.php
Normal file
@@ -0,0 +1,864 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DemoDataSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Marker email domain used for idempotency checks.
|
||||
*/
|
||||
private const DEMO_DOMAIN = 'demo.ezscale.test';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// Idempotency: skip if demo data already exists
|
||||
if (User::query()->where('email', 'like', '%@'.self::DEMO_DOMAIN)->exists()) {
|
||||
$this->command->info('Demo data already exists. Skipping DemoDataSeeder.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->command->info('Seeding demo data...');
|
||||
|
||||
// Load real plans from PlanSeeder
|
||||
$plans = Plan::query()->where('status', 'active')->get();
|
||||
|
||||
if ($plans->isEmpty()) {
|
||||
$this->command->error('No plans found. Run PlanSeeder first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab admin user for audit logs / staff replies
|
||||
$adminUser = User::role('admin')->first();
|
||||
$adminId = $adminUser?->id ?? 1;
|
||||
|
||||
// ─── 1. Create ~300 Customers ─────────────────────────────────
|
||||
$this->command->info('Creating customers...');
|
||||
$customers = $this->createCustomers();
|
||||
|
||||
// ─── 2. Create ~500 Subscriptions ─────────────────────────────
|
||||
$this->command->info('Creating subscriptions...');
|
||||
$subscriptionMap = $this->createSubscriptions($customers, $plans);
|
||||
|
||||
// ─── 3. Create ~400 Services ──────────────────────────────────
|
||||
$this->command->info('Creating services...');
|
||||
$this->createServices($customers, $plans, $subscriptionMap);
|
||||
|
||||
// ─── 4. Create ~800 Invoices with Items ───────────────────────
|
||||
$this->command->info('Creating invoices...');
|
||||
$invoiceIds = $this->createInvoices($customers, $plans, $subscriptionMap);
|
||||
|
||||
// ─── 5. Create ~600 Payment Transactions ──────────────────────
|
||||
$this->command->info('Creating payment transactions...');
|
||||
$this->createPaymentTransactions($customers, $invoiceIds);
|
||||
|
||||
// ─── 6. Create ~150 Orders ────────────────────────────────────
|
||||
$this->command->info('Creating orders...');
|
||||
$this->createOrders($customers, $plans);
|
||||
|
||||
// ─── 7. Create ~200 Support Tickets with Replies ──────────────
|
||||
$this->command->info('Creating support tickets...');
|
||||
$this->createSupportTickets($customers, $adminId);
|
||||
|
||||
// ─── 8. Create ~50 Coupons ────────────────────────────────────
|
||||
$this->command->info('Creating coupons...');
|
||||
$this->createCoupons();
|
||||
|
||||
// ─── 9. Create ~100 Audit Logs ────────────────────────────────
|
||||
$this->command->info('Creating audit logs...');
|
||||
$this->createAuditLogs($customers, $adminId);
|
||||
|
||||
$this->command->info('Demo data seeded successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~300 customers with profiles.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, User>
|
||||
*/
|
||||
private function createCustomers(): \Illuminate\Support\Collection
|
||||
{
|
||||
$customers = collect();
|
||||
$statuses = array_merge(
|
||||
array_fill(0, 270, 'active'),
|
||||
array_fill(0, 15, 'suspended'),
|
||||
array_fill(0, 10, 'banned'),
|
||||
array_fill(0, 5, 'pending'),
|
||||
);
|
||||
shuffle($statuses);
|
||||
|
||||
$faker = fake();
|
||||
$batchSize = 50;
|
||||
$totalCustomers = 300;
|
||||
|
||||
for ($i = 0; $i < $totalCustomers; $i += $batchSize) {
|
||||
$batchCount = min($batchSize, $totalCustomers - $i);
|
||||
|
||||
for ($j = 0; $j < $batchCount; $j++) {
|
||||
$index = $i + $j;
|
||||
$createdAt = now()->subDays(rand(1, 365));
|
||||
|
||||
$user = User::factory()->create([
|
||||
'name' => $faker->name(),
|
||||
'email' => "demo_{$index}@".self::DEMO_DOMAIN,
|
||||
'status' => $statuses[$index] ?? 'active',
|
||||
'phone' => $faker->optional(0.7)->phoneNumber(),
|
||||
'company' => $faker->optional(0.4)->company(),
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
]);
|
||||
|
||||
$user->assignRole('customer');
|
||||
|
||||
$user->profile()->create([
|
||||
'billing_address_line1' => $faker->streetAddress(),
|
||||
'billing_city' => $faker->city(),
|
||||
'billing_state' => $faker->stateAbbr(),
|
||||
'billing_zip' => $faker->postcode(),
|
||||
'billing_country' => $faker->randomElement(['US', 'CA', 'GB', 'DE', 'FR', 'AU', 'NL', 'SE']),
|
||||
'tax_exempt' => $faker->boolean(5),
|
||||
'company_name' => $user->company,
|
||||
]);
|
||||
|
||||
$customers->push($user);
|
||||
}
|
||||
}
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~500 subscriptions linked to real plans.
|
||||
*
|
||||
* Returns a map of subscription_id => [user_id, plan_id] for use by other seeders.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, User> $customers
|
||||
* @param \Illuminate\Support\Collection<int, Plan> $plans
|
||||
* @return array<int, array{user_id: int, plan_id: int}>
|
||||
*/
|
||||
private function createSubscriptions(\Illuminate\Support\Collection $customers, \Illuminate\Support\Collection $plans): array
|
||||
{
|
||||
$subscriptionMap = [];
|
||||
$statuses = ['active', 'active', 'active', 'active', 'active', 'active', 'active', 'canceled', 'past_due', 'trialing'];
|
||||
$total = 500;
|
||||
$rows = [];
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$customer = $customers->random();
|
||||
$plan = $plans->random();
|
||||
$status = $statuses[array_rand($statuses)];
|
||||
$createdAt = $customer->created_at->copy()->addDays(rand(0, 60));
|
||||
|
||||
if ($createdAt->isFuture()) {
|
||||
$createdAt = now()->subDays(rand(1, 30));
|
||||
}
|
||||
|
||||
$periodStart = $createdAt->copy();
|
||||
$periodEnd = match ($plan->billing_cycle) {
|
||||
'quarterly' => $periodStart->copy()->addMonths(3),
|
||||
'annual' => $periodStart->copy()->addYear(),
|
||||
default => $periodStart->copy()->addMonth(),
|
||||
};
|
||||
|
||||
$endsAt = null;
|
||||
$cancelledAt = null;
|
||||
if ($status === 'canceled') {
|
||||
$cancelledAt = $createdAt->copy()->addDays(rand(30, 180));
|
||||
$endsAt = $cancelledAt->copy()->addDays(rand(0, 30));
|
||||
}
|
||||
|
||||
$trialEndsAt = null;
|
||||
if ($status === 'trialing') {
|
||||
$trialEndsAt = now()->addDays(rand(1, 14));
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'user_id' => $customer->id,
|
||||
'type' => 'default',
|
||||
'stripe_id' => 'sub_demo_'.Str::random(14).'_'.$i,
|
||||
'stripe_status' => $status,
|
||||
'stripe_price' => 'price_demo_'.Str::random(10),
|
||||
'quantity' => 1,
|
||||
'trial_ends_at' => $trialEndsAt,
|
||||
'ends_at' => $endsAt,
|
||||
'plan_id' => $plan->id,
|
||||
'gateway' => fake()->randomElement(['stripe', 'stripe', 'stripe', 'paypal']),
|
||||
'gateway_subscription_id' => 'sub_demo_'.Str::random(14),
|
||||
'gateway_customer_id' => 'cus_demo_'.Str::random(14),
|
||||
'gateway_price_id' => 'price_demo_'.Str::random(14),
|
||||
'current_period_start' => $periodStart,
|
||||
'current_period_end' => $periodEnd,
|
||||
'cancelled_at' => $cancelledAt,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
// Bulk insert in chunks
|
||||
foreach (array_chunk($rows, 100) as $chunk) {
|
||||
DB::table('subscriptions')->insert($chunk);
|
||||
}
|
||||
|
||||
// Build the subscription map (query back IDs)
|
||||
$allSubs = DB::table('subscriptions')
|
||||
->where('stripe_id', 'like', 'sub_demo_%')
|
||||
->select('id', 'user_id', 'plan_id')
|
||||
->get();
|
||||
|
||||
foreach ($allSubs as $sub) {
|
||||
$subscriptionMap[$sub->id] = [
|
||||
'user_id' => $sub->user_id,
|
||||
'plan_id' => $sub->plan_id,
|
||||
];
|
||||
}
|
||||
|
||||
return $subscriptionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~400 services.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, User> $customers
|
||||
* @param \Illuminate\Support\Collection<int, Plan> $plans
|
||||
* @param array<int, array{user_id: int, plan_id: int}> $subscriptionMap
|
||||
*/
|
||||
private function createServices(\Illuminate\Support\Collection $customers, \Illuminate\Support\Collection $plans, array $subscriptionMap): void
|
||||
{
|
||||
$platformMap = [
|
||||
'vps' => 'virtfusion',
|
||||
'dedicated' => 'synergycp',
|
||||
'hosting' => 'enhance',
|
||||
'game_server' => 'pterodactyl',
|
||||
'mysql' => 'enhance',
|
||||
];
|
||||
|
||||
$serviceStatuses = array_merge(
|
||||
array_fill(0, 300, 'active'),
|
||||
array_fill(0, 40, 'suspended'),
|
||||
array_fill(0, 30, 'pending'),
|
||||
array_fill(0, 30, 'terminated'),
|
||||
);
|
||||
shuffle($serviceStatuses);
|
||||
|
||||
$subIds = array_keys($subscriptionMap);
|
||||
$rows = [];
|
||||
$faker = fake();
|
||||
$total = 400;
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$customer = $customers->random();
|
||||
$plan = $plans->random();
|
||||
$status = $serviceStatuses[$i] ?? 'active';
|
||||
$serviceType = $plan->service_type;
|
||||
$platform = $platformMap[$serviceType] ?? 'virtfusion';
|
||||
|
||||
// Try to link to a subscription for this user
|
||||
$subId = null;
|
||||
foreach ($subIds as $sid) {
|
||||
if ($subscriptionMap[$sid]['user_id'] === $customer->id) {
|
||||
$subId = $sid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$createdAt = $customer->created_at->copy()->addDays(rand(1, 90));
|
||||
if ($createdAt->isFuture()) {
|
||||
$createdAt = now()->subDays(rand(1, 30));
|
||||
}
|
||||
|
||||
$provisionedAt = $status !== 'pending' ? $createdAt->copy()->addHours(rand(1, 48)) : null;
|
||||
$suspendedAt = $status === 'suspended' ? $createdAt->copy()->addDays(rand(30, 120)) : null;
|
||||
$terminatedAt = $status === 'terminated' ? $createdAt->copy()->addDays(rand(60, 180)) : null;
|
||||
|
||||
$hostParts = [$faker->domainWord(), $faker->randomElement(['us-east', 'us-west', 'eu-west', 'ap-south'])];
|
||||
|
||||
$rows[] = [
|
||||
'user_id' => $customer->id,
|
||||
'subscription_id' => $subId,
|
||||
'plan_id' => $plan->id,
|
||||
'service_type' => $serviceType,
|
||||
'platform' => $platform,
|
||||
'platform_service_id' => (string) $faker->numberBetween(10000, 99999),
|
||||
'status' => $status,
|
||||
'ipv4_address' => $faker->ipv4(),
|
||||
'ipv6_address' => $faker->optional(0.3)->ipv6(),
|
||||
'hostname' => implode('-', $hostParts).'.ezscale.cloud',
|
||||
'domain' => $faker->optional(0.5)->domainName(),
|
||||
'credentials' => null,
|
||||
'provisioned_at' => $provisionedAt,
|
||||
'suspended_at' => $suspendedAt,
|
||||
'terminated_at' => $terminatedAt,
|
||||
'auto_renew' => $faker->boolean(85),
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($rows, 100) as $chunk) {
|
||||
DB::table('services')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~800 invoices with line items.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, User> $customers
|
||||
* @param \Illuminate\Support\Collection<int, Plan> $plans
|
||||
* @param array<int, array{user_id: int, plan_id: int}> $subscriptionMap
|
||||
* @return array<int, array{id: int, user_id: int, total: float, status: string}>
|
||||
*/
|
||||
private function createInvoices(\Illuminate\Support\Collection $customers, \Illuminate\Support\Collection $plans, array $subscriptionMap): array
|
||||
{
|
||||
$invoiceStatuses = array_merge(
|
||||
array_fill(0, 500, 'paid'),
|
||||
array_fill(0, 150, 'pending'),
|
||||
array_fill(0, 100, 'overdue'),
|
||||
array_fill(0, 50, 'void'),
|
||||
);
|
||||
shuffle($invoiceStatuses);
|
||||
|
||||
$subIds = array_keys($subscriptionMap);
|
||||
$invoiceRows = [];
|
||||
$invoiceTracker = [];
|
||||
$faker = fake();
|
||||
$total = 800;
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$customer = $customers->random();
|
||||
$plan = $plans->random();
|
||||
$status = $invoiceStatuses[$i] ?? 'paid';
|
||||
|
||||
// Link to a subscription if possible
|
||||
$subId = null;
|
||||
foreach ($subIds as $sid) {
|
||||
if ($subscriptionMap[$sid]['user_id'] === $customer->id) {
|
||||
$subId = $sid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$price = (float) $plan->price;
|
||||
$tax = round($price * fake()->randomFloat(2, 0.05, 0.12), 2);
|
||||
$invoiceTotal = round($price + $tax, 2);
|
||||
|
||||
$createdAt = now()->subDays(rand(1, 365));
|
||||
$dueDate = $createdAt->copy()->addDays(rand(14, 30));
|
||||
$paidAt = null;
|
||||
|
||||
if ($status === 'paid') {
|
||||
$paidAt = $createdAt->copy()->addDays(rand(0, 14));
|
||||
}
|
||||
|
||||
if ($status === 'overdue') {
|
||||
$dueDate = now()->subDays(rand(1, 60));
|
||||
}
|
||||
|
||||
$gateway = $faker->randomElement(['stripe', 'stripe', 'stripe', 'paypal']);
|
||||
|
||||
$invoiceRows[] = [
|
||||
'user_id' => $customer->id,
|
||||
'subscription_id' => $subId,
|
||||
'gateway' => $gateway,
|
||||
'gateway_invoice_id' => 'in_demo_'.Str::random(14).'_'.$i,
|
||||
'number' => 'INV-'.str_pad((string) (100000 + $i), 6, '0', STR_PAD_LEFT),
|
||||
'total' => $invoiceTotal,
|
||||
'tax' => $tax,
|
||||
'currency' => 'USD',
|
||||
'status' => $status,
|
||||
'invoice_pdf' => null,
|
||||
'due_date' => $dueDate,
|
||||
'paid_at' => $paidAt,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
|
||||
$invoiceTracker[] = [
|
||||
'user_id' => $customer->id,
|
||||
'total' => $invoiceTotal,
|
||||
'status' => $status,
|
||||
'plan_name' => $plan->name,
|
||||
'plan_price' => $price,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($invoiceRows, 100) as $chunk) {
|
||||
DB::table('invoices')->insert($chunk);
|
||||
}
|
||||
|
||||
// Get the IDs back for invoice items and payment transactions
|
||||
$invoiceIds = DB::table('invoices')
|
||||
->where('gateway_invoice_id', 'like', 'in_demo_%')
|
||||
->orderBy('id')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Create invoice items for each invoice
|
||||
$itemRows = [];
|
||||
foreach ($invoiceIds as $idx => $invoiceId) {
|
||||
$tracker = $invoiceTracker[$idx] ?? null;
|
||||
if (! $tracker) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemRows[] = [
|
||||
'invoice_id' => $invoiceId,
|
||||
'description' => ($tracker['plan_name'] ?? 'Service').' - Monthly Subscription',
|
||||
'amount' => $tracker['plan_price'] ?? 10.00,
|
||||
'quantity' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($itemRows, 200) as $chunk) {
|
||||
DB::table('invoice_items')->insert($chunk);
|
||||
}
|
||||
|
||||
// Build return data
|
||||
$result = [];
|
||||
foreach ($invoiceIds as $idx => $invoiceId) {
|
||||
$tracker = $invoiceTracker[$idx] ?? null;
|
||||
$result[] = [
|
||||
'id' => $invoiceId,
|
||||
'user_id' => $tracker['user_id'] ?? 0,
|
||||
'total' => $tracker['total'] ?? 0,
|
||||
'status' => $tracker['status'] ?? 'paid',
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~600 payment transactions.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, User> $customers
|
||||
* @param array<int, array{id: int, user_id: int, total: float, status: string}> $invoiceData
|
||||
*/
|
||||
private function createPaymentTransactions(\Illuminate\Support\Collection $customers, array $invoiceData): void
|
||||
{
|
||||
$rows = [];
|
||||
$faker = fake();
|
||||
$total = 600;
|
||||
|
||||
// Use paid/pending invoices for payment transactions
|
||||
$paidInvoices = array_filter($invoiceData, fn ($inv) => $inv['status'] === 'paid');
|
||||
$paidInvoices = array_values($paidInvoices);
|
||||
|
||||
$transactionStatuses = array_merge(
|
||||
array_fill(0, 480, 'succeeded'),
|
||||
array_fill(0, 60, 'failed'),
|
||||
array_fill(0, 40, 'refunded'),
|
||||
array_fill(0, 20, 'pending'),
|
||||
);
|
||||
shuffle($transactionStatuses);
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$status = $transactionStatuses[$i] ?? 'succeeded';
|
||||
|
||||
// Link to an invoice if available
|
||||
$invoiceId = null;
|
||||
$userId = $customers->random()->id;
|
||||
$amount = round($faker->randomFloat(2, 4, 200), 2);
|
||||
|
||||
if (! empty($paidInvoices) && $i < count($paidInvoices)) {
|
||||
$inv = $paidInvoices[$i];
|
||||
$invoiceId = $inv['id'];
|
||||
$userId = $inv['user_id'];
|
||||
$amount = (float) $inv['total'];
|
||||
}
|
||||
|
||||
$gateway = $faker->randomElement(['stripe', 'stripe', 'stripe', 'paypal']);
|
||||
$paymentMethod = $gateway === 'stripe'
|
||||
? $faker->randomElement(['card', 'card', 'card', 'bank_transfer'])
|
||||
: 'paypal';
|
||||
|
||||
$createdAt = now()->subDays(rand(1, 365));
|
||||
|
||||
$rows[] = [
|
||||
'user_id' => $userId,
|
||||
'subscription_id' => null,
|
||||
'invoice_id' => $invoiceId,
|
||||
'gateway' => $gateway,
|
||||
'gateway_transaction_id' => ($gateway === 'stripe' ? 'pi_demo_' : 'PAY-DEMO-').Str::random(14).'_'.$i,
|
||||
'amount' => $amount,
|
||||
'currency' => 'USD',
|
||||
'status' => $status,
|
||||
'payment_method' => $paymentMethod,
|
||||
'description' => match ($status) {
|
||||
'succeeded' => 'Payment for hosting service',
|
||||
'failed' => 'Payment declined - '.fake()->randomElement(['insufficient funds', 'card expired', 'bank declined']),
|
||||
'refunded' => 'Refund processed',
|
||||
'pending' => 'Payment processing',
|
||||
default => 'Payment',
|
||||
},
|
||||
'metadata' => json_encode(['demo' => true]),
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($rows, 100) as $chunk) {
|
||||
DB::table('payment_transactions')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~150 orders.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, User> $customers
|
||||
* @param \Illuminate\Support\Collection<int, Plan> $plans
|
||||
*/
|
||||
private function createOrders(\Illuminate\Support\Collection $customers, \Illuminate\Support\Collection $plans): void
|
||||
{
|
||||
$orderStatuses = array_merge(
|
||||
array_fill(0, 80, 'completed'),
|
||||
array_fill(0, 30, 'pending'),
|
||||
array_fill(0, 25, 'processing'),
|
||||
array_fill(0, 15, 'cancelled'),
|
||||
);
|
||||
shuffle($orderStatuses);
|
||||
|
||||
$rows = [];
|
||||
$faker = fake();
|
||||
$total = 150;
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$customer = $customers->random();
|
||||
$plan = $plans->random();
|
||||
$status = $orderStatuses[$i] ?? 'completed';
|
||||
$createdAt = now()->subDays(rand(1, 365));
|
||||
|
||||
$completedAt = null;
|
||||
$cancelledAt = null;
|
||||
|
||||
if ($status === 'completed') {
|
||||
$completedAt = $createdAt->copy()->addDays(rand(0, 3));
|
||||
}
|
||||
|
||||
if ($status === 'cancelled') {
|
||||
$cancelledAt = $createdAt->copy()->addDays(rand(0, 7));
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'user_id' => $customer->id,
|
||||
'plan_id' => $plan->id,
|
||||
'invoice_id' => null,
|
||||
'service_id' => null,
|
||||
'order_number' => 'ORD-'.strtoupper(Str::random(8)).$i,
|
||||
'status' => $status,
|
||||
'total' => (float) $plan->price,
|
||||
'currency' => 'USD',
|
||||
'payment_gateway' => $faker->randomElement(['stripe', 'paypal']),
|
||||
'configuration' => json_encode([
|
||||
'hostname' => $faker->domainWord().'.ezscale.cloud',
|
||||
'location' => $faker->randomElement(['us-east-1', 'us-west-1', 'eu-west-1', 'ap-south-1']),
|
||||
'os' => $faker->randomElement(['Ubuntu 24.04', 'Debian 12', 'Rocky Linux 9', 'AlmaLinux 9', 'Windows Server 2022']),
|
||||
]),
|
||||
'admin_notes' => $faker->optional(0.2)->sentence(),
|
||||
'completed_at' => $completedAt,
|
||||
'cancelled_at' => $cancelledAt,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($rows, 100) as $chunk) {
|
||||
DB::table('orders')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~200 support tickets with 2-5 replies each.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, User> $customers
|
||||
*/
|
||||
private function createSupportTickets(\Illuminate\Support\Collection $customers, int $adminId): void
|
||||
{
|
||||
$faker = fake();
|
||||
$ticketSubjects = [
|
||||
'billing' => [
|
||||
'Invoice not received', 'Payment failed', 'Request refund', 'Billing cycle question',
|
||||
'Upgrade pricing inquiry', 'Coupon not working', 'Double charged', 'Payment method update help',
|
||||
'Tax exemption request', 'Downgrade subscription',
|
||||
],
|
||||
'technical' => [
|
||||
'Server not responding', 'High CPU usage', 'Cannot SSH into server', 'DNS not resolving',
|
||||
'SSL certificate issue', 'Database connection error', 'Slow website performance',
|
||||
'Firewall blocking port', 'Disk space full', 'Container restart loop',
|
||||
],
|
||||
'sales' => [
|
||||
'Custom server configuration', 'Volume discount inquiry', 'Enterprise plan availability',
|
||||
'Reseller program info', 'Data center locations', 'SLA details', 'Migration assistance',
|
||||
'Bulk order pricing', 'Partner program', 'Referral program',
|
||||
],
|
||||
'general' => [
|
||||
'Account access issue', 'Two-factor auth locked out', 'Email not verified',
|
||||
'Profile update help', 'API documentation', 'Feature request', 'Service status inquiry',
|
||||
'Password reset not working', 'Terms of service question', 'Account closure request',
|
||||
],
|
||||
];
|
||||
|
||||
$ticketStatuses = array_merge(
|
||||
array_fill(0, 60, 'open'),
|
||||
array_fill(0, 50, 'in_progress'),
|
||||
array_fill(0, 40, 'waiting'),
|
||||
array_fill(0, 50, 'closed'),
|
||||
);
|
||||
shuffle($ticketStatuses);
|
||||
|
||||
$priorities = ['low', 'low', 'medium', 'medium', 'medium', 'high', 'high', 'urgent'];
|
||||
$total = 200;
|
||||
|
||||
$ticketRows = [];
|
||||
$ticketMeta = [];
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$customer = $customers->random();
|
||||
$status = $ticketStatuses[$i] ?? 'open';
|
||||
$department = $faker->randomElement(['billing', 'technical', 'sales', 'general']);
|
||||
$priority = $priorities[array_rand($priorities)];
|
||||
$subjects = $ticketSubjects[$department];
|
||||
$subject = $subjects[array_rand($subjects)];
|
||||
|
||||
$createdAt = now()->subDays(rand(1, 180));
|
||||
$lastReplyAt = $status !== 'open' ? $createdAt->copy()->addDays(rand(0, 14)) : null;
|
||||
|
||||
$ticketRows[] = [
|
||||
'user_id' => $customer->id,
|
||||
'subject' => $subject,
|
||||
'status' => $status,
|
||||
'priority' => $priority,
|
||||
'department' => $department,
|
||||
'last_reply_at' => $lastReplyAt,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
|
||||
$ticketMeta[] = [
|
||||
'user_id' => $customer->id,
|
||||
'department' => $department,
|
||||
'created_at' => $createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($ticketRows, 100) as $chunk) {
|
||||
DB::table('support_tickets')->insert($chunk);
|
||||
}
|
||||
|
||||
// Get ticket IDs back
|
||||
$ticketIds = DB::table('support_tickets')
|
||||
->whereIn('user_id', $customers->pluck('id')->toArray())
|
||||
->orderBy('id')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Limit to the tickets we just created (in case there were pre-existing ones)
|
||||
$ticketIds = array_slice($ticketIds, -$total);
|
||||
|
||||
// Create 2-5 replies per ticket
|
||||
$replyTemplates = [
|
||||
'customer' => [
|
||||
'I need help with this issue urgently. Please assist.',
|
||||
'This is causing problems for my business. When can I expect a resolution?',
|
||||
'Thank you for looking into this. Any update on the status?',
|
||||
'I have tried the steps you suggested but the issue persists.',
|
||||
'Could you please provide more details on how to resolve this?',
|
||||
'The issue seems to have gotten worse since my last update.',
|
||||
'I appreciate your help. Let me know if you need more information.',
|
||||
'This has been ongoing for several days now. Please escalate.',
|
||||
],
|
||||
'staff' => [
|
||||
'Thank you for reaching out. I am looking into this for you now.',
|
||||
'I have identified the issue and am working on a fix. I will update you shortly.',
|
||||
'Could you please provide your server IP and any error messages you are seeing?',
|
||||
'I have escalated this to our senior team. You should hear back within 24 hours.',
|
||||
'The issue has been resolved. Please confirm that everything is working correctly.',
|
||||
'I have applied a credit to your account for the inconvenience. Is there anything else?',
|
||||
'I have updated the firewall rules. Please try connecting again.',
|
||||
'This appears to be resolved on our end. Please let us know if you experience further issues.',
|
||||
],
|
||||
];
|
||||
|
||||
$replyRows = [];
|
||||
|
||||
foreach ($ticketIds as $idx => $ticketId) {
|
||||
$meta = $ticketMeta[$idx] ?? null;
|
||||
if (! $meta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$replyCount = rand(2, 5);
|
||||
$replyDate = $meta['created_at']->copy();
|
||||
|
||||
for ($r = 0; $r < $replyCount; $r++) {
|
||||
$isStaff = $r % 2 !== 0; // alternate customer/staff
|
||||
$replyDate = $replyDate->copy()->addHours(rand(1, 48));
|
||||
|
||||
if ($replyDate->isFuture()) {
|
||||
$replyDate = now()->subMinutes(rand(1, 60));
|
||||
}
|
||||
|
||||
$templates = $isStaff ? $replyTemplates['staff'] : $replyTemplates['customer'];
|
||||
|
||||
$replyRows[] = [
|
||||
'ticket_id' => $ticketId,
|
||||
'user_id' => $isStaff ? $adminId : $meta['user_id'],
|
||||
'body' => $templates[array_rand($templates)],
|
||||
'is_staff_reply' => $isStaff,
|
||||
'created_at' => $replyDate,
|
||||
'updated_at' => $replyDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_chunk($replyRows, 200) as $chunk) {
|
||||
DB::table('ticket_replies')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~50 coupons.
|
||||
*/
|
||||
private function createCoupons(): void
|
||||
{
|
||||
$rows = [];
|
||||
$faker = fake();
|
||||
|
||||
$couponNames = [
|
||||
'WELCOME', 'SUMMER', 'WINTER', 'SPRING', 'FALL', 'HOLIDAY', 'NEWYEAR',
|
||||
'BLACKFRI', 'CYBERMON', 'LAUNCH', 'EARLYBIRD', 'LOYALTY', 'REFER',
|
||||
'VPS10', 'VPS20', 'VPS30', 'HOST15', 'HOST25', 'DEDI10', 'DEDI20',
|
||||
'SAVE10', 'SAVE15', 'SAVE20', 'SAVE25', 'SAVE30', 'SAVE40', 'SAVE50',
|
||||
'FIRST50', 'ANNUAL20', 'MIGRATE', 'TRANSFER', 'STARTUP', 'STUDENT',
|
||||
'NONPROFIT', 'DEVOPS', 'CLOUD10', 'CLOUD20', 'CLOUD30', 'BF2025',
|
||||
'CM2025', 'NY2026', 'SPECIAL', 'FLASH', 'LIMITED', 'EXCLUSIVE',
|
||||
'PARTNER', 'AGENCY', 'RESELLER', 'BULK', 'ENTERPRISE',
|
||||
];
|
||||
|
||||
$total = 50;
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$type = $faker->randomElement(['percentage', 'percentage', 'fixed_amount']);
|
||||
$value = $type === 'percentage'
|
||||
? $faker->randomElement([5, 10, 15, 20, 25, 30, 40, 50])
|
||||
: $faker->randomFloat(2, 2, 25);
|
||||
|
||||
$isActive = $faker->boolean(70);
|
||||
$expiresAt = $faker->optional(0.6)->dateTimeBetween('-3 months', '+6 months');
|
||||
$maxUses = $faker->optional(0.5)->numberBetween(10, 500);
|
||||
$timesUsed = $maxUses ? $faker->numberBetween(0, (int) min($maxUses, 100)) : $faker->numberBetween(0, 50);
|
||||
|
||||
$createdAt = now()->subDays(rand(1, 300));
|
||||
|
||||
$rows[] = [
|
||||
'code' => $couponNames[$i] ?? strtoupper(Str::random(6)),
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
'currency' => $type === 'fixed_amount' ? 'USD' : null,
|
||||
'applies_to' => $faker->optional(0.3)->randomElement([
|
||||
json_encode(['service_type' => 'vps']),
|
||||
json_encode(['service_type' => 'dedicated']),
|
||||
json_encode(['service_type' => 'hosting']),
|
||||
]),
|
||||
'max_uses' => $maxUses,
|
||||
'times_used' => $timesUsed,
|
||||
'active' => $isActive,
|
||||
'expires_at' => $expiresAt,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($rows, 50) as $chunk) {
|
||||
DB::table('coupons')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ~100 audit log entries.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection<int, User> $customers
|
||||
*/
|
||||
private function createAuditLogs(\Illuminate\Support\Collection $customers, int $adminId): void
|
||||
{
|
||||
$actions = [
|
||||
['action' => 'user.created', 'resource_type' => 'User'],
|
||||
['action' => 'user.updated', 'resource_type' => 'User'],
|
||||
['action' => 'user.suspended', 'resource_type' => 'User'],
|
||||
['action' => 'user.banned', 'resource_type' => 'User'],
|
||||
['action' => 'user.reactivated', 'resource_type' => 'User'],
|
||||
['action' => 'service.provisioned', 'resource_type' => 'Service'],
|
||||
['action' => 'service.suspended', 'resource_type' => 'Service'],
|
||||
['action' => 'service.terminated', 'resource_type' => 'Service'],
|
||||
['action' => 'invoice.created', 'resource_type' => 'Invoice'],
|
||||
['action' => 'invoice.paid', 'resource_type' => 'Invoice'],
|
||||
['action' => 'invoice.voided', 'resource_type' => 'Invoice'],
|
||||
['action' => 'subscription.created', 'resource_type' => 'Subscription'],
|
||||
['action' => 'subscription.cancelled', 'resource_type' => 'Subscription'],
|
||||
['action' => 'coupon.created', 'resource_type' => 'Coupon'],
|
||||
['action' => 'coupon.deactivated', 'resource_type' => 'Coupon'],
|
||||
['action' => 'plan.updated', 'resource_type' => 'Plan'],
|
||||
['action' => 'order.completed', 'resource_type' => 'Order'],
|
||||
['action' => 'order.cancelled', 'resource_type' => 'Order'],
|
||||
['action' => 'impersonation.started', 'resource_type' => 'User'],
|
||||
['action' => 'impersonation.stopped', 'resource_type' => 'User'],
|
||||
['action' => 'settings.updated', 'resource_type' => 'Setting'],
|
||||
['action' => 'login', 'resource_type' => null],
|
||||
['action' => 'logout', 'resource_type' => null],
|
||||
];
|
||||
|
||||
$faker = fake();
|
||||
$rows = [];
|
||||
$total = 100;
|
||||
|
||||
$userAgents = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15',
|
||||
];
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$actionDef = $actions[array_rand($actions)];
|
||||
$customer = $customers->random();
|
||||
$createdAt = now()->subDays(rand(1, 180));
|
||||
|
||||
$changes = null;
|
||||
if (str_contains($actionDef['action'], 'updated') || str_contains($actionDef['action'], 'suspended') || str_contains($actionDef['action'], 'banned')) {
|
||||
$changes = json_encode([
|
||||
'old' => ['status' => 'active'],
|
||||
'new' => ['status' => $faker->randomElement(['suspended', 'banned', 'active'])],
|
||||
]);
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'user_id' => $customer->id,
|
||||
'admin_id' => $adminId,
|
||||
'action' => $actionDef['action'],
|
||||
'resource_type' => $actionDef['resource_type'],
|
||||
'resource_id' => $actionDef['resource_type'] ? $faker->numberBetween(1, 500) : null,
|
||||
'ip_address' => $faker->ipv4(),
|
||||
'user_agent' => $userAgents[array_rand($userAgents)],
|
||||
'changes' => $changes,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_chunk($rows, 100) as $chunk) {
|
||||
DB::table('audit_logs')->insert($chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user