create(['plan' => 'start', 'datacenter' => $host->datacenter, 'status' => 'paid']); $subscription = app(App\Actions\OpenSubscription::class)($order); $instance = Instance::factory()->create([ 'customer_id' => $subscription->customer_id, 'host_id' => $host->id, 'status' => 'active', 'quota_gb' => 30, 'disk_gb' => 40, ]); $subscription->update(['instance_id' => $instance->id]); return $subscription->refresh(); } it('lehnt einen Block ab, für den der Host keinen Platz hat', function () { config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]); // 60 GB gesamt, davon 40 durch die Instanz gebunden: für 22 weitere ist // kein Platz. `active()` setzt total_gb/RAM/Kerne — ohne diesen Zustand // hat ein Host gar keine Größe. $host = Host::factory()->active()->create(['datacenter' => 'fsn', 'total_gb' => 60]); $subscription = packedInstanceOn($host); expect(fn () => app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1)) ->toThrow(RuntimeException::class); expect($subscription->addons()->count())->toBe(0); }); it('lässt einen Block durch, für den der Host Platz hat', function () { config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]); $host = Host::factory()->active()->create(['datacenter' => 'fsn', 'total_gb' => 500]); $subscription = packedInstanceOn($host); app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1); expect((int) $subscription->addons()->active()->sum('quantity'))->toBe(1); }); it('fragt nicht, solange es keine Maschine gibt', function () { config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]); $order = Order::factory()->create(['plan' => 'start', 'datacenter' => 'fsn', 'status' => 'paid']); $subscription = app(App\Actions\OpenSubscription::class)($order); app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1); expect((int) $subscription->addons()->active()->sum('quantity'))->toBe(1); }); /** * Nachtrag aus der Durchsicht: die Kapazitätsprüfung darf einen wiederholt * zugestellten Webhook nicht treffen. * * Stripe liefert bei einem 5xx oder Timeout denselben Webhook ein zweites Mal * aus — BookAddon::__invoke() wird mit demselben `$order` erneut aufgerufen, * und book()'s eigener Kurzschluss ("Idempotent against a retried webhook") * soll dafür nur die bestehende Buchung zurückgeben, ohne irgendetwas neu zu * entscheiden. Läge die Kapazitätsprüfung VOR diesem Kurzschluss, würde die * Wiederholung erneut fragen "passt NOCH ein Block drauf" — obwohl gar keiner * hinzukommt — und einen längst bezahlten, längst gebuchten Vorgang mit einem * Fehler quittieren, sobald der Host in der Zwischenzeit enger geworden ist. */ it('gibt bei einem wiederholten Webhook die vorhandene Buchung zurück, auch wenn der Host inzwischen eng geworden ist', function () { config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]); // 100 GB gesamt → 85 GB frei (reserve_pct 15). Die gepackte Instanz bindet // 40, für den einen Block (22) bleiben 45 - 22 = 23 übrig: reichlich für // den ERSTEN Aufruf. $host = Host::factory()->active()->create(['datacenter' => 'fsn', 'total_gb' => 100]); $subscription = packedInstanceOn($host); $order = Order::factory()->create([ 'customer_id' => $subscription->customer_id, 'type' => 'addon', 'addon_key' => 'storage', 'status' => 'paid', ]); $first = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, $order); // Der Host wird zwischen den beiden Zustellversuchen enger — hier durch // eine zweite Instanz, die den Rest praktisch aufbraucht (40 + 22 + 30 = // 92 committed, nur noch 85 - 92 < 0 → 0 frei). Die Ursache ist beliebig // (ein zweiter Kunde, ein weiterer Block); die Frage ist nur, ob die // WIEDERHOLUNG sie stellen darf. Instance::factory()->create([ 'host_id' => $host->id, 'status' => 'active', 'disk_gb' => 30, ]); $second = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, $order); expect($second->id)->toBe($first->id) ->and((int) $subscription->addons()->active()->sum('quantity'))->toBe(1); });