create(['plan' => $plan, 'datacenter' => 'fsn', 'status' => 'paid']); return app(OpenSubscription::class)($order); } /** The signed-in user behind a contract's customer, for the Livewire tests below. */ function limitUser(Subscription $subscription): User { return User::factory()->create(['email' => $subscription->customer->email]); } it('lehnt den vierten Block auf einmal ab', function () { $subscription = limitContract(); expect(fn () => app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 4)) ->toThrow(RuntimeException::class); expect($subscription->addons()->count())->toBe(0); }); it('zählt drei einzelne Buchungen als drei', function () { $subscription = limitContract(); foreach (range(1, 3) as $ignored) { app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, Order::factory()->create([ 'customer_id' => $subscription->customer_id, 'type' => 'addon', 'addon_key' => 'storage', 'status' => 'paid', ])); } expect(fn () => app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, Order::factory()->create([ 'customer_id' => $subscription->customer_id, 'type' => 'addon', 'addon_key' => 'storage', 'status' => 'paid', ])))->toThrow(RuntimeException::class); expect((int) $subscription->addons()->active()->sum('quantity'))->toBe(3); }); it('zählt eine gekündigte Buchung nicht mit', function () { $subscription = limitContract(); $addon = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 3); app(BookAddon::class)->cancel($addon); expect(app(AddonCatalogue::class)->bookableQuantity($subscription->refresh(), AddonCatalogue::STORAGE))->toBe(3); }); it('sagt, wie viele noch gehen', function () { $subscription = limitContract(); app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 2); expect(app(AddonCatalogue::class)->bookableQuantity($subscription->refresh(), AddonCatalogue::STORAGE))->toBe(1); }); /** * Nachtrag aus der Durchsicht: der Deckel hielt in BookAddon, aber der * Warenkorb kannte ihn nur halb. * * `purchase()` klemmte $lines mit `max(1, min(..., bookableQuantity, ...))` — * bei erreichtem Deckel (bookableQuantity = 0) floort das `max(1, 0)` auf * genau eine Bestellzeile statt auf keine. Der Knopf, der `purchase('storage')` * ohne Menge aufruft, ist in der Ansicht bedingungslos gerendert, also * entstand am Deckel weiterhin ein bezahlbarer Auftrag, den BookAddon nach * der Zahlung nur noch ablehnen konnte — während OrderObserver den * Fehlschlag abfängt und der Auftrag `paid` stehen bleibt. Genau das Szenario, * dessentwegen diese Aufgabe existiert, nur auf den Fall "genau am Deckel" * verengt statt beseitigt. */ it('legt am erreichten Deckel keine Bestellzeile mehr an', function () { $subscription = limitContract(); app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 3); withStripeSecret(); Livewire::actingAs(limitUser($subscription)) ->test(Billing::class) ->call('purchase', 'storage'); expect(Order::where('customer_id', $subscription->customer_id)->where('type', 'storage')->count())->toBe(0); }); /** * Nachtrag aus der Durchsicht: das Bestätigungs-Modal klemmte an der * absoluten Grenze (3), nicht an der Restmenge. Die Karte, die das Modal * öffnet, kennt nur die Zahl, die einen Downgrade-Blocker aufheben würde — * nicht, dass der Vertrag schon einen Block hält. Ohne diese Klemmung hätte * der Dialog "3 Pakete" versprochen, während purchase() danach nur die * tatsächlich noch buchbaren 2 in den Warenkorb gelegt hätte: Dialogtext und * wirkliche Bestellmenge liefen auseinander. */ it('klemmt das Bestätigungs-Modal an der Restmenge, nicht an der absoluten Grenze', function () { $subscription = limitContract(); app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1); Livewire::actingAs(limitUser($subscription)) ->test(ConfirmBookStorage::class, ['packs' => 3]) ->assertSet('packs', 2) ->assertSee(__('billing.storage_confirm_title', ['count' => 2])) ->call('proceed') ->assertDispatched('storage-packs-confirmed', packs: 2); });