CluPilotCloud/tests/Feature/Billing/StripeCatalogueModeTest.php

131 lines
5.1 KiB
PHP

<?php
use App\Models\PlanPrice;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use App\Support\OperatingMode;
use App\Support\Settings;
use App\Support\StripeCatalogueMode;
use Illuminate\Contracts\Console\Kernel;
/**
* Der Abgleich sagt, in wessen Konto er gearbeitet hat — und weigert sich, in
* einen Katalog hineinzuarbeiten, der einem anderen gehört.
*
* Eine Stripe-Price-ID gehört dem Konto, das sie ausgestellt hat. Der Befehl
* überspringt jede Zeile, die schon eine ID trägt (PlanPrices::inStep()), also
* repariert ein zweiter Lauf nach dem Umschalten gar nichts — er meldete
* „already in step" und ließe eine Zeile zurück, die behauptet, der Katalog
* gehöre jetzt zum Livekonto. Genau diese Lüge macht die Weigerung unnötig.
*/
beforeEach(function () {
$this->stripe = new FakeStripeClient;
app()->instance(StripeClient::class, $this->stripe);
});
it('records the mode its objects were created in', function () {
OperatingMode::set(OperatingMode::Test);
$this->artisan('stripe:sync-catalogue')->assertSuccessful();
expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test)
->and(StripeCatalogueMode::hasStoredObjects())->toBeTrue();
});
it('records nothing on a dry run, which creates nothing', function () {
OperatingMode::set(OperatingMode::Test);
$this->artisan('stripe:sync-catalogue', ['--dry-run' => true])->assertSuccessful();
expect(StripeCatalogueMode::recorded())->toBeNull();
});
it('refuses to work into a catalogue that belongs to the other account', function () {
OperatingMode::set(OperatingMode::Test);
$this->artisan('stripe:sync-catalogue')->assertSuccessful();
OperatingMode::set(OperatingMode::Live);
$this->artisan('stripe:sync-catalogue')->assertFailed();
// Und die Zeile lügt danach nicht: der Katalog gehört weiterhin dem
// Testkonto, weil dieser Lauf nichts angelegt hat.
expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test);
});
/**
* „Herkunft nie festgehalten" ist NICHT „fremdes Konto".
*
* Der Zustand entsteht ohne jeden Fehler: `CheckoutController` → `PlanPrices::
* ensure()` und `BookAddon` → `SyncStripeAddonItems` → `AddonPrices::ensure()`
* legen fehlende Preise selbst an, und keiner der beiden ruft `record()`. Er
* entsteht auch nach einem Lauf, der mittendrin abgebrochen ist.
*
* Die Objekte stammen dann aus genau dem geltenden Konto. Diesen Zustand in die
* Konto-Weigerung zu schicken, hieße einem Betreiber das Leeren eines Katalogs
* aufzutragen, an dem laufende Verträge abgerechnet werden — für ein
* Wiederaufsetzen, für das die Idempotenzschlüssel dieses Befehls ausdrücklich
* gebaut sind.
*/
it('takes up a catalogue whose origin was never written down', function () {
OperatingMode::set(OperatingMode::Test);
$this->artisan('stripe:sync-catalogue')->assertSuccessful();
Settings::forget(StripeCatalogueMode::SETTING);
$this->artisan('stripe:sync-catalogue')->assertSuccessful();
expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test);
});
it('leaves no half-built catalogue that contradicts itself when a run dies', function () {
OperatingMode::set(OperatingMode::Test);
// Stripe geht mitten im Lauf weg, nachdem das erste Produkt schon angelegt
// und seine ID gespeichert ist. `createProduct()` und `ensure()` werfen
// ungefangen — stünde record() am ENDE von handle(), bliebe genau der
// Zustand zurück, den der Test darüber beschreibt: IDs da, Herkunft leer.
app()->instance(StripeClient::class, new class extends FakeStripeClient
{
public function createPrice(
string $productId,
int $amountCents,
string $currency,
string $interval,
array $metadata = [],
?string $idempotencyKey = null,
): string {
throw new RuntimeException('Stripe went away mid-run');
}
});
try {
app(Kernel::class)->call('stripe:sync-catalogue');
} catch (RuntimeException) {
// Erwartet — der Abbruch IST der Testfall.
}
expect(StripeCatalogueMode::hasStoredObjects())->toBeTrue()
->and(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test);
});
it('syncs into the new account once the stale ids are cleared', function () {
OperatingMode::set(OperatingMode::Test);
$this->artisan('stripe:sync-catalogue')->assertSuccessful();
OperatingMode::set(OperatingMode::Live);
// Was der `breaks`-Satz und die Fehlermeldung dem Betreiber auftragen —
// Zeiger UND Register, weil PlanPrices::inStep() für den Netto-Preis eines
// Reverse-Charge-Kunden allein das Register fragt.
DB::table('plan_families')->update(['stripe_product_id' => null]);
DB::table('plan_prices')->update(['stripe_price_id' => null]);
DB::table('stripe_plan_prices')->delete();
DB::table('stripe_addon_prices')->delete();
$this->artisan('stripe:sync-catalogue')->assertSuccessful();
expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Live)
->and(PlanPrice::query()->whereNotNull('stripe_price_id')->exists())->toBeTrue();
});