diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a19a78b1..05862e2e 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -14,6 +14,8 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // + $this->call([ + PlanSeeder::class, + ]); } } diff --git a/database/seeders/PlanSeeder.php b/database/seeders/PlanSeeder.php new file mode 100644 index 00000000..a0034352 --- /dev/null +++ b/database/seeders/PlanSeeder.php @@ -0,0 +1,88 @@ + Slug::Starter, + 'name' => 'Starter', + 'stripe_monthly_price_id' => env('STRIPE_STARTER_MONTHLY'), + 'stripe_yearly_price_id' => env('STRIPE_STARTER_YEARLY'), + 'monthly_price' => 1900, + 'yearly_price' => 19000, + 'social_account_limit' => 5, + 'member_limit' => 1, + 'brand_limit' => 0, + 'ai_images_limit' => 50, + 'ai_videos_limit' => 10, + 'data_retention_days' => 30, + 'sort' => 1, + ], + [ + 'slug' => Slug::Plus, + 'name' => 'Plus', + 'stripe_monthly_price_id' => env('STRIPE_PLUS_MONTHLY'), + 'stripe_yearly_price_id' => env('STRIPE_PLUS_YEARLY'), + 'monthly_price' => 2900, + 'yearly_price' => 29000, + 'social_account_limit' => 10, + 'member_limit' => 5, + 'brand_limit' => 5, + 'ai_images_limit' => 150, + 'ai_videos_limit' => 30, + 'data_retention_days' => 60, + 'sort' => 2, + ], + [ + 'slug' => Slug::Pro, + 'name' => 'Pro', + 'stripe_monthly_price_id' => env('STRIPE_PRO_MONTHLY'), + 'stripe_yearly_price_id' => env('STRIPE_PRO_YEARLY'), + 'monthly_price' => 4900, + 'yearly_price' => 49000, + 'social_account_limit' => 30, + 'member_limit' => 15, + 'brand_limit' => 15, + 'ai_images_limit' => 500, + 'ai_videos_limit' => 100, + 'data_retention_days' => 90, + 'sort' => 3, + ], + [ + 'slug' => Slug::Max, + 'name' => 'Max', + 'stripe_monthly_price_id' => env('STRIPE_MAX_MONTHLY'), + 'stripe_yearly_price_id' => env('STRIPE_MAX_YEARLY'), + 'monthly_price' => 9900, + 'yearly_price' => 99000, + 'social_account_limit' => 100, + 'member_limit' => 20, + 'brand_limit' => 50, + 'ai_images_limit' => 2000, + 'ai_videos_limit' => 500, + 'data_retention_days' => 730, + 'sort' => 4, + ], + ]; + + foreach ($plans as $plan) { + Plan::updateOrCreate( + ['slug' => $plan['slug']], + $plan, + ); + } + } +} diff --git a/tests/Feature/PlanSeederTest.php b/tests/Feature/PlanSeederTest.php new file mode 100644 index 00000000..e8c8a08b --- /dev/null +++ b/tests/Feature/PlanSeederTest.php @@ -0,0 +1,53 @@ +toBe(4); +}); + +test('seeder creates plans with correct limits', function () { + $starter = Plan::where('slug', Slug::Starter)->first(); + + expect($starter->name)->toBe('Starter') + ->and($starter->monthly_price)->toBe(1900) + ->and($starter->yearly_price)->toBe(19000) + ->and($starter->social_account_limit)->toBe(5) + ->and($starter->member_limit)->toBe(1) + ->and($starter->brand_limit)->toBe(0) + ->and($starter->ai_images_limit)->toBe(50) + ->and($starter->ai_videos_limit)->toBe(10) + ->and($starter->data_retention_days)->toBe(30) + ->and($starter->sort)->toBe(1); + + $max = Plan::where('slug', Slug::Max)->first(); + + expect($max->name)->toBe('Max') + ->and($max->monthly_price)->toBe(9900) + ->and($max->yearly_price)->toBe(99000) + ->and($max->social_account_limit)->toBe(100) + ->and($max->member_limit)->toBe(20) + ->and($max->brand_limit)->toBe(50) + ->and($max->ai_images_limit)->toBe(2000) + ->and($max->ai_videos_limit)->toBe(500) + ->and($max->data_retention_days)->toBe(730) + ->and($max->sort)->toBe(4); +}); + +test('seeder is idempotent', function () { + $this->seed(PlanSeeder::class); + + expect(Plan::count())->toBe(4); +}); + +test('active scope excludes archived plans', function () { + $plan = Plan::where('slug', Slug::Starter)->first(); + $plan->update(['is_archived' => true]); + + expect(Plan::active()->count())->toBe(3) + ->and(Plan::count())->toBe(4); +}); diff --git a/tests/Feature/PlanTest.php b/tests/Feature/PlanTest.php index b6a1ddf5..bbcf59cd 100644 --- a/tests/Feature/PlanTest.php +++ b/tests/Feature/PlanTest.php @@ -6,6 +6,8 @@ use App\Models\Plan; test('plan can be created with factory', function () { + Plan::where('slug', Slug::Pro)->delete(); + $plan = Plan::factory()->create([ 'slug' => Slug::Pro, 'name' => 'Pro', @@ -22,7 +24,7 @@ }); test('plan slug is cast to enum', function () { - $plan = Plan::factory()->create(['slug' => Slug::Starter]); + $plan = Plan::where('slug', Slug::Starter)->first(); expect($plan->slug)->toBeInstanceOf(Slug::class) ->and($plan->slug)->toBe(Slug::Starter) @@ -30,37 +32,28 @@ }); test('active scope excludes archived plans', function () { - Plan::factory()->create(['slug' => Slug::Starter, 'is_archived' => false]); - Plan::factory()->create(['slug' => Slug::Plus, 'is_archived' => false]); - Plan::factory()->archived()->create(['slug' => Slug::Pro]); + $activeBefore = Plan::active()->count(); - $activePlans = Plan::active()->get(); + $plan = Plan::where('slug', Slug::Pro)->first(); + $plan->update(['is_archived' => true]); - expect($activePlans)->toHaveCount(2); + expect(Plan::active()->count())->toBe($activeBefore - 1); }); test('formatted monthly price returns dollar format', function () { - $plan = Plan::factory()->create(['monthly_price' => 1900]); + $plan = Plan::where('slug', Slug::Starter)->first(); expect($plan->formattedMonthlyPrice())->toBe('$19'); }); test('formatted yearly price returns dollar format', function () { - $plan = Plan::factory()->create(['yearly_price' => 19000]); + $plan = Plan::where('slug', Slug::Starter)->first(); expect($plan->formattedYearlyPrice())->toBe('$190'); }); test('integer fields are cast correctly', function () { - $plan = Plan::factory()->create([ - 'social_account_limit' => 10, - 'member_limit' => 3, - 'brand_limit' => 5, - 'ai_images_limit' => 50, - 'ai_videos_limit' => 25, - 'data_retention_days' => 365, - 'sort' => 2, - ]); + $plan = Plan::where('slug', Slug::Starter)->first(); expect($plan->social_account_limit)->toBeInt() ->and($plan->member_limit)->toBeInt()