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 Intern und Enterprise aus dem Laden, lässt aber beide verschenkbar', function () { // Beide verlassen sellable() über dasselbe Feld — `internal` —, nicht über // den Verkaufsschalter: der bleibt an, sonst verböte currentVersion() auch // das Verschenken (Aufgabe 6/InternalPlanTest). expect(PlanFamily::query()->where('key', 'test')->value('internal'))->toBeTruthy() ->and(PlanFamily::query()->where('key', 'test')->value('name'))->toBe('Intern') ->and(PlanFamily::query()->where('key', 'test')->value('sales_enabled'))->toBeTruthy() ->and(PlanFamily::query()->where('key', 'enterprise')->value('internal'))->toBeTruthy() ->and(PlanFamily::query()->where('key', 'enterprise')->value('sales_enabled'))->toBeTruthy(); }); it('hält Intern nicht für erledigt, nur weil Kontingent und Platte schon stimmten', function () { // Der Defekt: die Prüfung verglich nur quota_gb und disk_gb. Genau der // Bestand, den der Betreiber früher von Hand über die Konsole angelegt // hatte — Kontingent und Platte schon richtig (5 GB / 20 GB), RAM, Kerne // und Plätze noch auf den alten Werten (1024 MB, 1 Kern, 5 Plätze). $family = PlanFamily::query()->where('key', 'test')->sole(); DB::table('plan_versions')->where('plan_family_id', $family->id) ->update(['available_until' => now(), 'updated_at' => now()]); DB::table('plan_versions')->insert([ 'uuid' => (string) Str::uuid(), 'plan_family_id' => $family->id, 'version' => 99, 'quota_gb' => 5, 'disk_gb' => 20, // richtig, wie die Vorgabe 'ram_mb' => 1024, 'cores' => 1, 'seats' => 5, // die alten, falschen Werte 'traffic_gb' => 1000, 'performance' => 'standard', 'free_months' => 2, 'template_vmid' => 9000, 'features' => '[]', 'available_from' => now()->subYear(), 'available_until' => null, 'published_at' => now()->subYear(), 'created_at' => now(), 'updated_at' => now(), ]); // Der Preis muss ebenfalls stimmen, sonst würde die Prüfung ihn für sich // allein schon als Unterschied werten, ohne dass RAM/Kerne/Plätze geprüft // wurden — hier soll ausdrücklich der Rest der Vorgabe geprüft werden. DB::table('plan_prices')->insert([ 'uuid' => (string) Str::uuid(), 'plan_version_id' => DB::table('plan_versions')->where('plan_family_id', $family->id)->where('version', 99)->value('id'), 'term' => 'monthly', 'amount_cents' => 100, 'currency' => 'EUR', 'created_at' => now(), 'updated_at' => now(), ]); (require database_path('migrations/2026_08_01_000004_switch_to_new_plan_ladder.php'))->up(); $current = app(PlanCatalogue::class)->currentVersion('test'); expect($current->ram_mb)->toBe(4096) ->and($current->cores)->toBe(2) ->and($current->seats)->toBe(3); }); it('hebt Intern nachträglich auf seine neuen Größen, für einen Bestand, auf dem die erste Migration schon lief', function () { // Nachgebaut statt echt reproduziert: die erste Migration ist inzwischen // korrigiert und erzeugt diesen kaputten Bestand selbst nicht mehr. Genau // das war aber der gemessene Zustand auf der Entwicklungsdatenbank, bevor // dieser Nachtrag existierte — Kontingent und Platte schon richtig, RAM, // Kerne und Plätze noch auf den alten Werten, nur eine einzige Version. $family = PlanFamily::query()->where('key', 'test')->sole(); $liveVersionId = DB::table('plan_versions') ->where('plan_family_id', $family->id)->whereNull('available_until')->value('id'); // Dieselbe Zeile, auf ihre alten Werte zurückgesetzt — die frische // Installation legt Version 1 schon richtig an, und dieser Test spielt // genau die Zeile durch, die vor diesem Nachtrag falsch stehen blieb. DB::table('plan_versions')->where('id', $liveVersionId) ->update(['ram_mb' => 1024, 'cores' => 1, 'seats' => 5, 'updated_at' => now()]); // Vorher: der gemessene Bestand aus dem Aufgabenblatt. $before = app(PlanCatalogue::class)->currentVersion('test'); expect($before->ram_mb)->toBe(1024) ->and($before->cores)->toBe(1) ->and($before->seats)->toBe(5); (require database_path('migrations/2026_08_01_000005_finish_the_plan_ladder_switch.php'))->up(); // Nachher: 4096 MB, 2 Kerne, 3 Plätze — Kontingent und Platte unverändert. $after = app(PlanCatalogue::class)->currentVersion('test'); expect($after->ram_mb)->toBe(4096) ->and($after->cores)->toBe(2) ->and($after->seats)->toBe(3) ->and($after->quota_gb)->toBe(5) ->and($after->disk_gb)->toBe(20); }); it('tut nichts an Intern, wenn es seine neuen Größen bereits trägt', function () { // Der Fall einer Neuinstallation: die korrigierte erste Migration hat // Intern schon richtig angelegt, und dieser Nachtrag darf keine dritte // Version nachschieben. $before = App\Models\PlanVersion::query()->where( 'plan_family_id', PlanFamily::query()->where('key', 'test')->value('id') )->count(); (require database_path('migrations/2026_08_01_000005_finish_the_plan_ladder_switch.php'))->up(); expect(App\Models\PlanVersion::query()->where( 'plan_family_id', PlanFamily::query()->where('key', 'test')->value('id') )->count())->toBe($before) ->and(app(PlanCatalogue::class)->currentVersion('test')->ram_mb)->toBe(4096); }); it('macht Enterprise verschenkbar statt käuflich', function () { (require database_path('migrations/2026_08_01_000005_finish_the_plan_ladder_switch.php'))->up(); expect(array_keys(app(PlanCatalogue::class)->grantable()))->toContain('enterprise') ->and(array_keys(app(PlanCatalogue::class)->sellable()))->not->toContain('enterprise'); }); 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); });