set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32))); withStripeSecret(); Http::preventStrayRequests(); }); it('lists only the invoices the customer still owes', function () { Http::fake(['api.stripe.com/v1/invoices*' => Http::response(['data' => [ ['id' => 'in_1', 'number' => 'R-1', 'amount_due' => 21480, 'currency' => 'eur', 'created' => 1750000000], ]])]); $open = app(HttpStripeClient::class)->openInvoices('cus_42'); expect($open)->toHaveCount(1) ->and($open[0]['amount_due_cents'])->toBe(21480) ->and($open[0]['currency'])->toBe('EUR'); // `open` ist Stripes Status für finalisiert und unbezahlt. Ohne den Filter // käme die ganze Rechnungshistorie zurück, und der Kunde bekäme einen // Bezahlknopf neben Rechnungen, die er längst beglichen hat. Http::assertSent(fn ($request) => $request['customer'] === 'cus_42' && $request['status'] === 'open'); }); it('reports a refused charge instead of throwing', function () { // Der häufigste Fall überhaupt: die Karte lehnt ab. Das ist kein // Serverfehler, das ist die Antwort — und die Seite muss sie zeigen // können, statt in eine 500 zu laufen. Http::fake(['api.stripe.com/v1/invoices/in_1/pay' => Http::response([ 'error' => ['code' => 'card_declined', 'message' => 'Your card was declined.'], ], 402)]); $result = app(HttpStripeClient::class)->payInvoice('in_1'); expect($result['paid'])->toBeFalse() ->and($result['failure'])->toContain('declined'); }); it('still throws on an answer that says nothing about the card', function () { // 401 oder 500 sind Aussagen über UNS, nicht über das Zahlungsmittel des // Kunden. Sie als „Ihre Karte wurde abgelehnt" auszugeben wäre eine // Behauptung über sein Konto, hergeleitet aus unserem Fehler. Http::fake(['api.stripe.com/v1/invoices/in_1/pay' => Http::response([], 500)]); // Konkrete Klasse, nicht `Throwable::class`: Pest behandelt eine // Schnittstelle als zu enthaltende MELDUNG, nicht als Typ — der Test wäre // sonst grün, ohne irgendetwas zu prüfen. expect(fn () => app(HttpStripeClient::class)->payInvoice('in_1')) ->toThrow(RequestException::class); }); it('reports a successful charge', function () { Http::fake(['api.stripe.com/v1/invoices/in_1/pay' => Http::response([ 'id' => 'in_1', 'status' => 'paid', 'paid' => true, ])]); expect(app(HttpStripeClient::class)->payInvoice('in_1')['paid'])->toBeTrue(); }); it('pages to the end instead of stopping at the first hundred', function () { // Dieselbe Falle, die diesem Projekt schon einmal teuer kam: der alte // Hetzner-Client las nur die erste Seite und legte deshalb einen zweiten // A-Eintrag an. `invoiceLines()` direkt daneben blättert längst bis zum // Ende. Hier gilt es dem Geld: eine ältere offene Rechnung, die auf Seite // zwei liegt, wäre für den Kunden schlicht nicht bezahlbar. $seite = fn (int $von, bool $weiter) => Http::response([ 'has_more' => $weiter, 'data' => collect(range($von, $von + 99)) ->map(fn (int $i) => [ 'id' => 'in_'.$i, 'number' => 'R-'.$i, 'amount_due' => 100, 'currency' => 'eur', 'created' => 1750000000, ])->all(), ]); // Nach der Seite gefragt wird über `starting_after`, nicht über eine // Seitenzahl — daran wird hier auch unterschieden, sonst beantwortete die // Attrappe beide Aufrufe gleich und der Test liefe gegen den Fehler grün. Http::fake(function ($request) use ($seite) { parse_str((string) parse_url($request->url(), PHP_URL_QUERY), $query); return isset($query['starting_after']) ? $seite(101, false) : $seite(1, true); }); expect(app(HttpStripeClient::class)->openInvoices('cus_42'))->toHaveCount(200); });