CluPilotCloud/app/Support/StripeCatalogueMode.php

94 lines
4.2 KiB
PHP

<?php
namespace App\Support;
use App\Models\PlanFamily;
use App\Models\PlanPrice;
use App\Models\StripeAddonPrice;
use App\Models\StripePlanPrice;
/**
* In welchem Stripe-KONTO die gespeicherten Objekt-IDs entstanden sind.
*
* Der Betriebsmodus schaltet die Zugangsdaten um. Er schaltet nicht um, was aus
* ihnen entstanden ist: eine Stripe-Price-ID und eine Product-ID gehören dem
* Konto, das sie ausgestellt hat, und ein Testkonto und ein Livekonto sind zwei
* Konten. Jedes Zugangsdatum hat auf dieser Installation zwei Plätze; die
* daraus abgeleiteten IDs stehen in einwertigen Spalten
* (`plan_families.stripe_product_id`, `plan_prices.stripe_price_id`) und in den
* zwei Registern (`stripe_plan_prices`, `stripe_addon_prices`).
*
* Ohne diese Zeile lief der geplante Ablauf dieser Installation ins Leere:
* abgleichen im Testbetrieb, Live-Schlüssel hinterlegen, umschalten — und
* `billing.catalogue_synced` meldete weiter „erfüllt", weil es nur prüfte, ob
* die Spalte gefüllt ist. Die Seite sagte „Bereit für Livebetrieb", und die
* erste echte Bestellung schickte eine Test-Preis-ID an die Live-API: *No such
* price*, kein Auftrag. Genau die falsche Grünmeldung, gegen die diese Seite
* gebaut ist, ausgelöst durch genau den Schalter, den dieses Vorhaben einführt.
*
* **Warum eine Einstellungszeile und nicht die Objekte selbst.** Stripe schreibt
* die Kontoart nicht in die ID: `prod_…` und `price_…` sehen im Test- und im
* Livekonto gleich aus (nur SCHLÜSSEL tragen `_test_`/`_live_` im Präfix — das
* ist OperatingMode::ofStripeKey()). Die Herkunft ist aus einer gespeicherten ID
* also nicht ableitbar, und nachfragen dürfte die Bereitschaftsseite nicht: sie
* erreicht beim Seitenaufruf nichts über das Netz. Bleibt: beim Abgleich
* festhalten, in welchem Modus die IDs entstanden sind.
*
* **Warum EINE Zeile für den ganzen Katalog.** Ein Lauf von
* stripe:sync-catalogue benutzt genau einen Schlüssel, also genau ein Konto. Und
* der Befehl weigert sich, in einen Katalog hineinzuarbeiten, der zu einem
* anderen Konto gehört (siehe dort) — ohne diese Weigerung könnte ein Lauf
* einen halb gemischten Zustand hinterlassen, über den eine einzelne Zeile dann
* lügen würde.
*/
final class StripeCatalogueMode
{
public const SETTING = 'billing.catalogue_mode';
/** Der Modus, in dem der Katalog zuletzt angelegt wurde — null: noch nie. */
public static function recorded(): ?OperatingMode
{
$stored = Settings::get(self::SETTING);
return $stored === null ? null : OperatingMode::tryFrom((string) $stored);
}
/** Festhalten, wessen Konto die gerade angelegten IDs gehören. */
public static function record(?OperatingMode $mode = null): void
{
Settings::set(self::SETTING, ($mode ?? OperatingMode::current())->value);
}
/**
* Gehört der gespeicherte Katalog zum Konto des aktiven Modus?
*
* „Noch nie abgeglichen" (null) ist hier bewusst KEINE Übereinstimmung: eine
* Installation, in der IDs liegen, über deren Herkunft nichts festgehalten
* ist, kann nicht behaupten, sie gehörten zum aktiven Konto. Die Migration,
* die die Plätze einführt, trägt für den einen Fall nach, den sie belegen
* kann (der vorhandene Schlüssel ist der einzige, mit dem je etwas angelegt
* worden sein kann).
*/
public static function matchesActiveMode(): bool
{
return self::recorded() === OperatingMode::current();
}
/**
* Liegt überhaupt eine Stripe-Objekt-ID in der Datenbank?
*
* Alle vier Orte, an denen der Abgleich etwas hinterlässt — auch die zwei
* Register, weil PlanPrices::inStep() für den Netto-Preis eines
* Reverse-Charge-Kunden ALLEIN das Register fragt: eine geleerte Spalte in
* `plan_prices` ohne gelöschte Registerzeile ließe genau diese Hälfte des
* Katalogs still auf das alte Konto zeigen.
*/
public static function hasStoredObjects(): bool
{
return PlanFamily::query()->whereNotNull('stripe_product_id')->exists()
|| PlanPrice::query()->whereNotNull('stripe_price_id')->exists()
|| StripePlanPrice::query()->exists()
|| StripeAddonPrice::query()->exists();
}
}