Add plan-capped Events demo:seed command.
Deploy Ladill Events / deploy (push) Successful in 2m20s

Seed live events, registrations, and Pro subscriptions without billing APIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 10:03:03 +00:00
co-authored by Cursor
parent a7231ab16f
commit 66e6e117de
2 changed files with 492 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
<?php
namespace App\Console\Commands;
use App\Models\Events\ProSubscription;
use App\Models\PaymentGatewaySetting;
use App\Models\QrCode;
use App\Models\QrEventRegistration;
use App\Models\QrTeamMember;
use App\Models\User;
use App\Services\Events\SubscriptionService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Seed plan-aware Events demo data for a suite demo identity.
*/
class DemoSeedCommand extends Command
{
protected $signature = 'demo:seed
{identity : Email or public_id of the local mirrored user}
{--plan=free : free|pro|enterprise}
{--reset : Remove prior demo data for this user before seeding}
{--public-id= : Platform public_id when creating a local mirror}
{--email= : Email when creating a local mirror}
{--name= : Display name when creating a local mirror}';
protected $description = 'Seed Events demo data for a suite demo account';
public function handle(SubscriptionService $subscriptions): int
{
$plan = strtolower((string) $this->option('plan'));
if (! in_array($plan, ['free', 'pro', 'enterprise'], true)) {
$this->error('Plan must be free, pro, or enterprise.');
return self::FAILURE;
}
$user = $this->resolveUser();
if (! $user) {
return self::FAILURE;
}
if ($this->option('reset')) {
$this->resetDemoData($user);
$this->info('Reset demo data for '.$user->email);
}
$this->assignPlan($user, $plan, $subscriptions);
if ($this->hasDemoMarker($user) && ! $this->option('reset')) {
$this->info('Demo data already present; plan updated. Use --reset to reseed.');
$this->printSummary($user, $plan, $subscriptions);
return self::SUCCESS;
}
$this->seedEvents($user, $plan);
$this->printSummary($user, $plan, $subscriptions);
return self::SUCCESS;
}
private function resolveUser(): ?User
{
$identity = trim((string) $this->argument('identity'));
$publicIdOpt = trim((string) ($this->option('public-id') ?: ''));
$emailOpt = trim((string) ($this->option('email') ?: ''));
$nameOpt = trim((string) ($this->option('name') ?: ''));
$user = User::query()
->where('email', $identity)
->orWhere('public_id', $identity)
->first();
if ($user) {
return $user;
}
$publicId = $publicIdOpt !== '' ? $publicIdOpt : (Str::isUuid($identity) ? $identity : '');
$email = $emailOpt !== '' ? $emailOpt : (filter_var($identity, FILTER_VALIDATE_EMAIL) ? $identity : '');
if ($publicId === '' || $email === '') {
$this->error('Local user not found for "'.$identity.'". Pass --public-id and --email to create a mirror.');
return null;
}
return User::query()->updateOrCreate(
['public_id' => $publicId],
[
'email' => $email,
'name' => $nameOpt !== '' ? $nameOpt : Str::before($email, '@'),
'email_verified_at' => now(),
],
);
}
private function assignPlan(User $user, string $plan, SubscriptionService $subscriptions): void
{
if ($plan === 'free') {
ProSubscription::query()->where('user_id', $user->id)->delete();
return;
}
$planKey = $plan === 'enterprise'
? ProSubscription::PLAN_ENTERPRISE
: ProSubscription::PLAN_PRO;
ProSubscription::query()->updateOrCreate(
['user_id' => $user->id],
[
'plan' => $planKey,
'status' => ProSubscription::STATUS_ACTIVE,
'price_minor' => $planKey === ProSubscription::PLAN_ENTERPRISE
? $subscriptions->enterprisePriceMinor()
: $subscriptions->priceMinor(),
'currency' => (string) config('events.pro.currency', 'GHS'),
'auto_renew' => false,
'started_at' => now(),
'current_period_end' => now()->addYear(),
'last_charged_at' => null,
'canceled_at' => null,
'last_reference' => 'demo-seed-'.$planKey,
'last_error' => null,
],
);
}
private function hasDemoMarker(User $user): bool
{
return QrCode::query()
->where('user_id', $user->id)
->where('type', QrCode::TYPE_EVENT)
->where('short_code', 'like', $this->codePrefix($user).'%')
->exists();
}
private function codePrefix(User $user): string
{
return 'dm'.substr(md5((string) $user->public_id), 0, 8);
}
private function resetDemoData(User $user): void
{
DB::transaction(function () use ($user) {
PaymentGatewaySetting::query()->where('owner_ref', $user->public_id)->delete();
QrTeamMember::query()
->where('account_id', $user->id)
->orWhere('user_id', $user->id)
->delete();
$codes = QrCode::query()->where('user_id', $user->id)->pluck('id');
if ($codes->isNotEmpty()) {
QrEventRegistration::query()->whereIn('qr_code_id', $codes)->delete();
QrCode::query()->whereIn('id', $codes)->delete();
}
QrEventRegistration::query()->where('user_id', $user->id)->delete();
ProSubscription::query()->where('user_id', $user->id)->delete();
});
}
private function seedEvents(User $user, string $plan): void
{
$prefix = $this->codePrefix($user);
$paid = $plan !== 'free';
$eventCount = $paid ? ($plan === 'enterprise' ? 5 : 4) : 2;
$regsPerEvent = $paid ? ($plan === 'enterprise' ? 40 : 25) : 35;
$programmeId = null;
if ($paid) {
$programme = QrCode::query()->updateOrCreate(
['short_code' => $prefix.'-itn'],
[
'user_id' => $user->id,
'type' => QrCode::TYPE_ITINERARY,
'label' => 'Demo Summit Programme',
'payload' => [
'content' => [
'title' => 'Accra Tech Summit Programme',
'subtitle' => 'Two-day agenda',
'description' => 'Demo programme for sales walkthroughs.',
'event_date' => now()->addDays(14)->toDateString(),
'location' => 'Kempinski Accra',
'brand_color' => '#0F766E',
'days' => [[
'label' => 'Day 1',
'date' => now()->addDays(14)->toDateString(),
'items' => [
[
'ref' => 'day0-item0',
'time' => '09:00',
'title' => 'Opening keynote',
'description' => 'Welcome and product roadmap',
'location' => 'Main hall',
'host' => 'Ama Mensah',
'host_speaker_email' => 'ama.demo@ladill.com',
],
[
'ref' => 'day0-item1',
'time' => '11:00',
'title' => 'Breakout: ticketing',
'description' => 'Paid tiers and badges',
'location' => 'Room B',
'host' => 'Kojo Asante',
'host_speaker_email' => 'kojo.demo@ladill.com',
],
],
]],
],
'style' => [],
],
'is_active' => true,
],
);
$programmeId = $programme->id;
}
for ($i = 1; $i <= $eventCount; $i++) {
$mode = $paid ? 'ticketing' : 'free';
$tiers = $paid
? [
['name' => 'General', 'price' => 150, 'capacity' => 200],
['name' => 'VIP', 'price' => 450, 'capacity' => 40],
]
: [['name' => 'Registration', 'price' => 0, 'capacity' => 100]];
$event = QrCode::query()->updateOrCreate(
['short_code' => $prefix.'-e'.$i],
[
'user_id' => $user->id,
'type' => QrCode::TYPE_EVENT,
'label' => $paid ? 'Demo Paid Event '.$i : 'Demo Free Event '.$i,
'payload' => [
'content' => [
'name' => $paid ? 'Accra Business Forum '.$i : 'Community Meetup '.$i,
'tagline' => $paid ? 'Ticketed demo event' : 'Free RSVP demo event',
'description' => 'Seeded by demo:seed for sales walkthroughs.',
'location' => 'Accra, Ghana',
'starts_at' => now()->addDays(7 + $i)->toIso8601String(),
'ends_at' => now()->addDays(7 + $i)->addHours(6)->toIso8601String(),
'organizer' => $user->name ?: 'Ladill Demo',
'currency' => 'GHS',
'mode' => $mode,
'format' => $i === 1 ? 'in_person' : 'hybrid',
'tiers' => $tiers,
'badge_fields' => ['company', 'role'],
'badge_size' => 'standard',
'accepts_payment' => $paid,
'registration_open' => true,
'programme_qr_id' => $programmeId,
'speakers' => $paid ? [
[
'name' => 'Ama Mensah',
'email' => 'ama.demo@ladill.com',
'role' => 'Keynote',
],
] : [],
],
'style' => [],
],
'is_active' => true,
],
);
$this->seedRegistrations($user, $event, $regsPerEvent, $paid, $prefix, $i);
}
if ($paid) {
PaymentGatewaySetting::query()->updateOrCreate(
['owner_ref' => $user->public_id],
[
'provider' => PaymentGatewaySetting::PROVIDER_PAYSTACK,
'public_key' => 'pk_test_demo_events',
'secret_key' => 'sk_test_demo_events',
'is_active' => true,
'metadata' => ['demo' => true],
],
);
}
}
private function seedRegistrations(
User $user,
QrCode $event,
int $count,
bool $paid,
string $prefix,
int $eventIndex,
): void {
$existing = QrEventRegistration::query()
->where('qr_code_id', $event->id)
->where('reference', 'like', $prefix.'%')
->count();
if ($existing >= $count) {
return;
}
for ($n = $existing + 1; $n <= $count; $n++) {
$tier = $paid ? ($n % 5 === 0 ? 'VIP' : 'General') : 'Registration';
$amount = $paid ? ($tier === 'VIP' ? 45000 : 15000) : 0;
$ref = sprintf('%s-r%02d-%03d', $prefix, $eventIndex, $n);
$badge = strtoupper(substr($prefix, 2, 4).sprintf('%02d%03d', $eventIndex, $n));
QrEventRegistration::query()->updateOrCreate(
['reference' => $ref],
[
'qr_code_id' => $event->id,
'user_id' => $user->id,
'badge_code' => substr($badge, 0, 16),
'tier_name' => $tier,
'amount_minor' => $amount,
'currency' => 'GHS',
'attendee_name' => 'Demo Attendee '.$n,
'attendee_email' => 'attendee'.$n.'.e'.$eventIndex.'@demo.ladill.com',
'attendee_phone' => '+23320'.str_pad((string) (($eventIndex * 1000) + $n), 7, '0', STR_PAD_LEFT),
'badge_fields' => [
'company' => 'Demo Co '.$n,
'role' => $tier === 'VIP' ? 'Executive' : 'Guest',
],
'status' => QrEventRegistration::STATUS_CONFIRMED,
'payment_reference' => $paid ? 'demo-pay-'.$ref : null,
'paid_at' => $paid ? now()->subDays(max(1, 30 - $n)) : null,
'checked_in_at' => $n % 4 === 0 ? now()->subHours($n) : null,
'metadata' => ['demo' => true],
],
);
}
}
private function printSummary(User $user, string $plan, SubscriptionService $subscriptions): void
{
$events = QrCode::query()
->where('user_id', $user->id)
->where('type', QrCode::TYPE_EVENT)
->count();
$programmes = QrCode::query()
->where('user_id', $user->id)
->where('type', QrCode::TYPE_ITINERARY)
->count();
$regs = QrEventRegistration::query()->where('user_id', $user->id)->count();
$this->table(
['Field', 'Value'],
[
['email', $user->email],
['public_id', $user->public_id],
['requested_plan', $plan],
['effective_plan', $subscriptions->planKey($user)],
['live_events', (string) $events],
['programmes', (string) $programmes],
['registrations', (string) $regs],
],
);
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
namespace Tests\Feature;
use App\Models\Events\ProSubscription;
use App\Models\QrCode;
use App\Models\QrEventRegistration;
use App\Models\User;
use App\Services\Events\SubscriptionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
class DemoSeedCommandTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
config([
'billing.api_url' => 'https://ladill.com/api/billing',
'billing.api_key' => 'events-billing-key',
'events.pro.enabled' => true,
'events.pro.price_minor' => 4900,
'events.plans.pro.price_minor' => 4900,
'events.plans.enterprise.price_minor' => 14900,
'events.free.max_live_events' => 2,
'events.free.max_tickets_per_month' => 100,
]);
}
public function test_free_plan_respects_live_event_and_registration_caps(): void
{
$user = User::factory()->create([
'public_id' => (string) Str::uuid(),
'email' => 'demo-free@ladill.com',
]);
$this->artisan('demo:seed', [
'identity' => $user->email,
'--plan' => 'free',
])->assertSuccessful();
$this->assertSame('free', app(SubscriptionService::class)->planKey($user));
$this->assertNull(ProSubscription::query()->where('user_id', $user->id)->first());
$events = QrCode::query()
->where('user_id', $user->id)
->where('type', QrCode::TYPE_EVENT)
->count();
$regs = QrEventRegistration::query()->where('user_id', $user->id)->count();
$this->assertLessThanOrEqual(2, $events);
$this->assertGreaterThan(0, $events);
$this->assertLessThan(100, $regs);
$this->assertSame(0, QrCode::query()->where('user_id', $user->id)->where('type', QrCode::TYPE_ITINERARY)->count());
}
public function test_pro_plan_assigns_subscription_and_seeds_paid_payloads(): void
{
$user = User::factory()->create([
'public_id' => (string) Str::uuid(),
'email' => 'demo-pro@ladill.com',
]);
$this->artisan('demo:seed', [
'identity' => $user->public_id,
'--plan' => 'pro',
])->assertSuccessful();
$this->assertSame('pro', app(SubscriptionService::class)->planKey($user));
$sub = ProSubscription::query()->where('user_id', $user->id)->first();
$this->assertNotNull($sub);
$this->assertTrue($sub->current_period_end->isFuture());
$this->assertFalse($sub->auto_renew);
$events = QrCode::query()
->where('user_id', $user->id)
->where('type', QrCode::TYPE_EVENT)
->get();
$this->assertGreaterThan(2, $events->count());
$this->assertSame('ticketing', $events->first()->content()['mode'] ?? null);
$this->assertGreaterThan(0, QrCode::query()->where('user_id', $user->id)->where('type', QrCode::TYPE_ITINERARY)->count());
$this->assertGreaterThan(50, QrEventRegistration::query()->where('user_id', $user->id)->count());
}
public function test_command_is_idempotent_and_reset_reseeds(): void
{
$user = User::factory()->create([
'public_id' => (string) Str::uuid(),
'email' => 'demo-enterprise@ladill.com',
]);
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'enterprise'])->assertSuccessful();
$firstEvents = QrCode::query()->where('user_id', $user->id)->where('type', QrCode::TYPE_EVENT)->count();
$firstRegs = QrEventRegistration::query()->where('user_id', $user->id)->count();
$this->artisan('demo:seed', ['identity' => $user->email, '--plan' => 'enterprise'])->assertSuccessful();
$this->assertSame($firstEvents, QrCode::query()->where('user_id', $user->id)->where('type', QrCode::TYPE_EVENT)->count());
$this->assertSame($firstRegs, QrEventRegistration::query()->where('user_id', $user->id)->count());
$this->artisan('demo:seed', [
'identity' => $user->email,
'--plan' => 'free',
'--reset' => true,
])->assertSuccessful();
$this->assertSame('free', app(SubscriptionService::class)->planKey($user->fresh()));
$this->assertLessThanOrEqual(2, QrCode::query()->where('user_id', $user->id)->where('type', QrCode::TYPE_EVENT)->count());
$this->assertLessThan(100, QrEventRegistration::query()->where('user_id', $user->id)->count());
}
public function test_creates_mirror_when_enough_identity_options_given(): void
{
$publicId = (string) Str::uuid();
$this->artisan('demo:seed', [
'identity' => 'demo-new@ladill.com',
'--plan' => 'free',
'--public-id' => $publicId,
'--email' => 'demo-new@ladill.com',
'--name' => 'Demo New',
])->assertSuccessful();
$user = User::query()->where('public_id', $publicId)->first();
$this->assertNotNull($user);
$this->assertSame('demo-new@ladill.com', $user->email);
}
}