feat: add plan_prices table, PlanPrice model, and Plan relationship

- Create plan_prices migration with unique(plan_id, billing_cycle)
- PlanPrice model with factory
- Plan::prices() relationship and priceForCycle() helper
- Tests for model relationships, uniqueness, cascade delete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-03-14 23:31:12 -04:00
parent b2fd5abc7e
commit c034be820e
5 changed files with 232 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
use App\Models\Plan;
use App\Models\PlanPrice;
it('has prices relationship', function () {
$plan = Plan::factory()->create();
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'price' => 5.00,
]);
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'annual',
'price' => 51.00,
]);
expect($plan->prices)->toHaveCount(2);
expect($plan->prices->first())->toBeInstanceOf(PlanPrice::class);
});
it('resolves price for cycle', function () {
$plan = Plan::factory()->create();
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'price' => 5.00,
]);
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'quarterly',
'price' => 14.25,
]);
$monthly = $plan->priceForCycle('monthly');
expect($monthly)->not->toBeNull();
expect((float) $monthly->price)->toBe(5.00);
$quarterly = $plan->priceForCycle('quarterly');
expect((float) $quarterly->price)->toBe(14.25);
$invalid = $plan->priceForCycle('weekly');
expect($invalid)->toBeNull();
});
it('enforces unique plan_id and billing_cycle', function () {
$plan = Plan::factory()->create();
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'price' => 5.00,
]);
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'price' => 6.00,
]);
})->throws(\Illuminate\Database\QueryException::class);
it('cascade deletes plan prices when plan is deleted', function () {
$plan = Plan::factory()->create();
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'monthly',
'price' => 5.00,
]);
PlanPrice::create([
'plan_id' => $plan->id,
'billing_cycle' => 'annual',
'price' => 51.00,
]);
expect(PlanPrice::where('plan_id', $plan->id)->count())->toBe(2);
$plan->delete();
expect(PlanPrice::where('plan_id', $plan->id)->count())->toBe(0);
});
it('seeds all VPS plans with 4 price cycles each', function () {
$this->seed(\Database\Seeders\PlanSeeder::class);
$vpsSlugs = ['vps-1', 'vps-2', 'vps-4', 'vps-8', 'vps-16', 'vps-32', 'stor-500', 'stor-1tb'];
foreach ($vpsSlugs as $slug) {
$plan = Plan::where('slug', $slug)->first();
expect($plan)->not->toBeNull("Plan {$slug} should exist");
expect($plan->status)->toBe('active');
expect($plan->prices)->toHaveCount(4, "Plan {$slug} should have 4 price cycles");
$cycles = $plan->prices->pluck('billing_cycle')->sort()->values()->toArray();
expect($cycles)->toBe(['annual', 'monthly', 'quarterly', 'semi_annual']);
}
});
it('archives old VPS plan slugs', function () {
Plan::factory()->create(['slug' => 'vps-nano', 'service_type' => 'vps', 'status' => 'active']);
$this->seed(\Database\Seeders\PlanSeeder::class);
$oldPlan = Plan::where('slug', 'vps-nano')->first();
expect($oldPlan->status)->toBe('archived');
});
it('sets correct monthly base prices', function () {
$this->seed(\Database\Seeders\PlanSeeder::class);
$expectedPrices = [
'vps-1' => 5.00,
'vps-2' => 8.00,
'vps-4' => 15.00,
'vps-8' => 30.00,
'vps-16' => 55.00,
'vps-32' => 99.00,
'stor-500' => 18.00,
'stor-1tb' => 28.00,
];
foreach ($expectedPrices as $slug => $expectedPrice) {
$plan = Plan::where('slug', $slug)->first();
expect((float) $plan->price)->toBe($expectedPrice, "Plan {$slug} monthly price mismatch");
expect((float) $plan->priceForCycle('monthly')->price)->toBe($expectedPrice);
}
});