CluPilotCloud/database/seeders/DemoCustomerSeeder.php

210 lines
7.6 KiB
PHP

<?php
namespace Database\Seeders;
use App\Models\Backup;
use App\Models\Customer;
use App\Models\Datacenter;
use App\Models\Host;
use App\Models\Instance;
use App\Models\InstanceMetric;
use App\Models\InstanceTraffic;
use App\Models\Order;
use App\Models\Seat;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* One complete customer, written as real rows.
*
* The panel used to be judged against fixtures baked into the views, and every
* round of that hid a different gap: a card with no data source, a figure that
* came from a translation string, a chart drawn from an array in a Blade file.
* A real customer with real rows makes the panel tell the truth about itself —
* whatever is missing shows up as missing.
*
* Everything hangs off one User, so removing the demo is one deletion:
*
* php artisan db:seed --class=DemoCustomerSeeder --force
* php artisan tinker --execute="App\Models\User::where('email','demo@clupilot.com')->first()?->delete()"
*
* Safe to run twice: every row is matched on a natural key first.
*/
class DemoCustomerSeeder extends Seeder
{
private const EMAIL = 'demo@clupilot.com';
public function run(): void
{
// The demo account is a real login on a panel that is reachable from the
// public internet, so it never gets a password written in a source file.
// DEMO_PASSWORD if the operator set one, otherwise a fresh random one
// printed once — a weak default would sit on production forever, and
// "it is only the demo" is how the first account gets taken.
$password = (string) (env('DEMO_PASSWORD') ?: Str::password(24));
$user = User::firstOrCreate(
['email' => self::EMAIL],
['name' => 'Kanzlei Muster', 'password' => Hash::make($password)],
);
if ($user->wasRecentlyCreated) {
$this->command?->warn('Demo password (shown once): '.$password);
}
$customer = Customer::firstOrCreate(
['user_id' => $user->id],
['name' => 'Kanzlei Muster', 'email' => self::EMAIL, 'contact_name' => 'Dr. M. Muster', 'status' => 'active'],
);
$datacenter = Datacenter::firstOrCreate(
['code' => 'fsn'],
['name' => 'Falkenstein', 'location' => 'DE', 'active' => true],
);
$host = Host::query()->where('datacenter', $datacenter->code)->first()
?? Host::factory()->create(['datacenter' => $datacenter->code, 'name' => 'pve-fsn-01', 'status' => 'active']);
$order = Order::firstOrCreate(
['customer_id' => $customer->id, 'type' => 'plan'],
[
'plan' => 'team',
'amount_cents' => 17900,
'currency' => 'EUR',
'datacenter' => $datacenter->code,
'status' => 'paid',
],
);
$instance = Instance::firstOrCreate(
['customer_id' => $customer->id],
[
'order_id' => $order->id,
'host_id' => $host->id,
'vmid' => 9001,
'plan' => 'team',
'quota_gb' => 500,
'disk_gb' => 500,
'ram_mb' => 8192,
'cores' => 4,
'traffic_addons' => 0,
'guest_ip' => '10.20.0.31',
'subdomain' => 'kanzlei-muster',
'status' => 'active',
'cert_ok' => true,
'route_written' => true,
],
);
Subscription::firstOrCreate(
['customer_id' => $customer->id],
[
'order_id' => $order->id,
'instance_id' => $instance->id,
'plan' => 'team',
'tier' => 2,
'term' => 'monthly',
'price_cents' => 17900,
'currency' => 'EUR',
'quota_gb' => 500,
'traffic_gb' => 3072,
'seats' => 25,
'ram_mb' => 8192,
'cores' => 4,
'disk_gb' => 500,
'performance' => 'enhanced',
'status' => 'active',
'started_at' => now()->subMonths(4),
'current_period_start' => now()->startOfMonth(),
'current_period_end' => now()->addMonth()->startOfMonth(),
],
);
$this->seats($customer);
$this->backups($instance);
$this->traffic($instance);
$this->metrics($instance);
$this->command?->info('Demo customer ready: '.self::EMAIL);
}
private function seats(Customer $customer): void
{
$people = [
['Dr. M. Muster', 'demo@clupilot.com', 'owner', 'active'],
['S. Berger', 'berger@kanzlei-muster.test', 'admin', 'active'],
['A. Wimmer', 'wimmer@kanzlei-muster.test', 'member', 'active'],
['T. Klein', 'klein@kanzlei-muster.test', 'member', 'active'],
['P. Hofer', 'hofer@kanzlei-muster.test', 'member', 'invited'],
['Steuerberatung Ost', 'extern@partner.test', 'readonly', 'active'],
];
foreach ($people as [$name, $email, $role, $status]) {
Seat::firstOrCreate(
['customer_id' => $customer->id, 'email' => $email],
['name' => $name, 'role' => $role, 'status' => $status, 'invited_at' => now()->subDays(30)],
);
}
}
private function backups(Instance $instance): void
{
Backup::firstOrCreate(
['instance_id' => $instance->id, 'schedule' => 'täglich 03:00'],
['external_job_id' => 'demo-backup', 'status' => 'ok', 'last_ok_at' => now()->setTime(3, 12)],
);
}
private function traffic(Instance $instance): void
{
InstanceTraffic::firstOrCreate(
['instance_id' => $instance->id, 'period' => InstanceTraffic::currentPeriod()],
[
'rx_bytes' => (int) (340 * 1024 ** 3),
'tx_bytes' => (int) (72 * 1024 ** 3),
'last_netin' => 0,
'last_netout' => 0,
'sampled_at' => now(),
],
);
}
/**
* Thirty days of samples: a disk that fills slowly, transfer that varies,
* and one day where two checks out of 288 failed.
*
* Deliberately not a smooth curve — a straight line is the first thing that
* gives a demo away, and a chart that never wobbles teaches nobody what a
* real one looks like.
*/
private function metrics(Instance $instance): void
{
$total = (int) (500 * 1024 ** 3);
$used = (int) (198 * 1024 ** 3);
$wobble = [1.0, 0.6, 1.4, 0.9, 1.7, 0.4, 0.2, 1.1, 1.5, 0.8];
foreach (range(29, 0) as $offset) {
$day = Carbon::today()->subDays($offset);
$i = 29 - $offset;
$used += (int) ($wobble[$i % 10] * 0.42 * 1024 ** 3);
InstanceMetric::updateOrCreate(
['instance_id' => $instance->id, 'day' => $day->toDateString()],
[
'disk_used_bytes' => $used,
'disk_total_bytes' => $total,
'rx_bytes' => (int) ((9 + $wobble[$i % 10] * 4) * 1024 ** 3),
'tx_bytes' => (int) ((2 + $wobble[($i + 3) % 10]) * 1024 ** 3),
// 288 checks a day is one every five minutes.
'checks_total' => 288,
'checks_ok' => $offset === 12 ? 286 : 288,
],
);
}
}
}