feat: create PlanSeeder with 4 tiered plans

This commit is contained in:
Paulo Castellano 2026-04-14 17:56:13 -03:00
parent d235e702cf
commit 5bc1548423
4 changed files with 154 additions and 18 deletions

View file

@ -14,6 +14,8 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
//
$this->call([
PlanSeeder::class,
]);
}
}

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Enums\Plan\Slug;
use App\Models\Plan;
use Illuminate\Database\Seeder;
class PlanSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$plans = [
[
'slug' => 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,
);
}
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
use App\Enums\Plan\Slug;
use App\Models\Plan;
use Database\Seeders\PlanSeeder;
test('seeder creates 4 plans', function () {
expect(Plan::count())->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);
});

View file

@ -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()