create(); $customer = Customer::factory()->create(['user_id' => $user->id]); $instance = Instance::factory()->create(['customer_id' => $customer->id, 'status' => 'active', 'quota_gb' => 500]); foreach (range(1, $seats) as $n) { Seat::create([ 'customer_id' => $customer->id, 'email' => "seat{$n}@example.test", 'role' => $n === 1 ? 'owner' : 'member', 'status' => 'active', ]); } if ($usedBytes !== null) { InstanceMetric::create([ 'instance_id' => $instance->id, 'day' => now()->toDateString(), 'disk_used_bytes' => $usedBytes, 'disk_total_bytes' => 500 * 1024 ** 3, ]); } // One test below calls Billing::purchase(), which since Task 5 refuses // outright without a Stripe key for the active mode. Without this, that // test would see the same "no order" result for the wrong reason — the // Stripe guard, not the seat block it names — and prove nothing. withStripeSecret(); return [$user, $customer, $instance]; } it('allows a downgrade when everything fits', function () { [, $customer, $instance] = downgradeCustomer(seats: 3, usedBytes: 40 * 1024 ** 3); $check = DowngradeCheck::for($customer, $instance, ['seats' => 10, 'quota_gb' => 200]); expect($check->allowed)->toBeTrue()->and($check->blockers)->toBe([]); }); it('names the seats that are in the way, with both numbers', function () { [, $customer, $instance] = downgradeCustomer(seats: 31); $check = DowngradeCheck::for($customer, $instance, ['seats' => 10, 'quota_gb' => 200]); expect($check->allowed)->toBeFalse() ->and($check->blockers[0]['key'])->toBe('seats') ->and($check->blockers[0]['current'])->toBe('31') ->and($check->blockers[0]['limit'])->toBe('10'); }); it('measures storage rather than trusting the allowance', function () { // 480 GB really in use. The contract says 500 GB, but what decides whether // 200 GB is enough is what is actually stored. [, $customer, $instance] = downgradeCustomer(seats: 2, usedBytes: 480 * 1024 ** 3); $check = DowngradeCheck::for($customer, $instance, ['seats' => 10, 'quota_gb' => 200]); expect($check->allowed)->toBeFalse() ->and(collect($check->blockers)->pluck('key')->all())->toBe(['storage']); }); it('does not refuse a customer who has simply never been measured', function () { // A new instance has no reading. Refusing on the contractual allowance // would block every downgrade for anyone who once bought a large plan. [, $customer, $instance] = downgradeCustomer(seats: 2, usedBytes: null); expect(DowngradeCheck::for($customer, $instance, ['seats' => 10, 'quota_gb' => 200])->allowed)->toBeTrue(); }); it('refuses a blocked downgrade even when the button is never rendered', function () { // The action is callable from a stale tab or a second window. A limit // enforced only in markup is not enforced. [$user, $customer, $instance] = downgradeCustomer(seats: 31); Livewire::actingAs($user) ->test(Billing::class) ->call('purchase', 'downgrade', 'start'); expect(Order::query()->where('customer_id', $customer->id)->where('type', 'downgrade')->count())->toBe(0); });