sellable()))->toBe(['start', 'team', 'business']); }); it('verkauft sie zu den beschlossenen Zahlen', function () { $sellable = app(PlanCatalogue::class)->sellable(); expect($sellable['start']['quota_gb'])->toBe(30) ->and($sellable['start']['disk_gb'])->toBe(40) ->and($sellable['start']['ram_mb'])->toBe(6144) ->and($sellable['start']['seats'])->toBe(10) ->and($sellable['start']['price_cents'])->toBe(3900) ->and($sellable['start']['yearly_price_cents'])->toBe(39000) ->and($sellable['team']['quota_gb'])->toBe(85) ->and($sellable['team']['disk_gb'])->toBe(100) ->and($sellable['team']['price_cents'])->toBe(7900) ->and($sellable['business']['quota_gb'])->toBe(175) ->and($sellable['business']['disk_gb'])->toBe(200) ->and($sellable['business']['price_cents'])->toBe(13900); }); it('lässt neun Startkunden auf 388 GB', function () { $start = app(PlanCatalogue::class)->sellable()['start']; expect(intdiv(388, $start['disk_gb']))->toBe(9); }); it('nimmt das interne Paket und Enterprise aus dem Laden', function () { expect(PlanFamily::query()->where('key', 'test')->value('internal'))->toBeTruthy() ->and(PlanFamily::query()->where('key', 'test')->value('name'))->toBe('Intern') ->and(PlanFamily::query()->where('key', 'enterprise')->value('sales_enabled'))->toBeFalsy(); }); it('schaltet kein zweites Mal um, wenn sie noch einmal gefahren wird', function () { // Ein zweiter Deploy, ein wiederholtes `migrate` auf einem Bestand, der die // Umschaltung schon hinter sich hat: die Migration darf dann keine dritte // Fassung nachschieben und die laufende nicht beenden. Erkannt wird das an // dem, was die laufende Fassung TRÄGT, nicht an ihrer Nummer. $before = App\Models\PlanVersion::query()->count(); (require database_path('migrations/2026_08_01_000004_switch_to_new_plan_ladder.php'))->up(); expect(App\Models\PlanVersion::query()->count())->toBe($before) ->and(app(PlanCatalogue::class)->sellable()['team']['quota_gb'])->toBe(85) ->and(app(PlanCatalogue::class)->currentVersion('team')->available_until)->toBeNull(); }); it('lässt einen laufenden Vertrag auf seinen eigenen Zahlen', function () { // Ein Vertrag, wie ihn OpenSubscription vor der Umschaltung geschrieben // hat, mit der Maschine dazu. $subscription = Subscription::factory()->create([ 'plan' => 'team', 'quota_gb' => 500, 'disk_gb' => 540, 'price_cents' => 17900, ]); $instance = App\Models\Instance::factory()->create([ 'customer_id' => $subscription->customer_id, 'status' => 'active', 'quota_gb' => 500, 'disk_gb' => 540, ]); // Der Katalog sagt heute etwas anderes — und keine Leseseite schreibt es // dem Vertrag um. Die mittlere Zusicherung ist die eigentliche: sie geht // durch denselben Weg, über den eine Bereitstellung das Kontingent holt. expect(app(PlanCatalogue::class)->sellable()['team']['quota_gb'])->toBe(85) ->and(App\Services\Billing\StorageAllowance::for($instance)->planGb)->toBe(500) ->and($subscription->fresh()->price_cents)->toBe(17900); });