Seed live events, registrations, and Pro subscriptions without billing APIs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user