131 lines
5.1 KiB
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();
|
|
});
|