185 lines
9.0 KiB
PHP
185 lines
9.0 KiB
PHP
<?php
|
|
|
|
use App\Models\PlanFamily;
|
|
use App\Models\Subscription;
|
|
use App\Services\Billing\PlanCatalogue;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Die Leiter, wie sie verkauft wird.
|
|
*
|
|
* Zugeschnitten auf die Maschine, die dasteht: 388 GB vergebbar, ~58 GB RAM für
|
|
* Gäste. Neun Startkunden à 40 GB Platte lasten Speicher und RAM gleichzeitig
|
|
* aus — die alte Leiter band bei drei Kunden und ließ sechs RAM-Plätze
|
|
* verfallen.
|
|
*/
|
|
it('verkauft drei Pakete', function () {
|
|
expect(array_keys(app(PlanCatalogue::class)->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);
|
|
});
|