*/ public static function all(): array { $mode = OperatingMode::current(); // get() löst den aktiven Modus selbst auf UND ist für Stripe strikt — // ein Live-Schlüssel im LIVE-Platz zählt im Testbetrieb nicht als // Nachweis, sonst widerspräche diese Seite der Kassensperre. $secret = app(SecretVault::class)->get('stripe.secret'); // Was im Platz des aktiven Modus liegt, kann trotzdem der Schlüssel des // anderen Kontos sein — eingetippt, nicht zurückgefallen. Am Präfix // ohne Netzverkehr entscheidbar; `null` heißt „nicht entscheidbar" und // wird hier bewusst NICHT als Widerspruch gemeldet: diese Prüfung sagt // nur, was sie beweisen kann. $keyMode = OperatingMode::ofStripeKey($secret); $wrongSlot = $keyMode !== null && $keyMode !== $mode; return [ new Check( key: 'billing.stripe_secret', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.billing.stripe_secret', ['mode' => __('readiness.mode.'.$mode->value)]), // Zwei Ausfälle, zwei Folgen — und „ohne ihn nimmt die Kasse // keine Bestellung an" wäre über einen Schlüssel, der DA ist, // schlicht falsch. breaks: $wrongSlot ? __('readiness.billing.stripe_secret_'.$mode->value.'_slot_breaks') : __('readiness.billing.stripe_secret_breaks'), tab: 'services', satisfied: filled($secret) && ! $wrongSlot, ), new Check( key: 'billing.webhook_secret', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.billing.webhook_secret'), breaks: __('readiness.billing.webhook_secret_breaks'), tab: 'env', // Die Auswahl nach Modus liegt schon in StripeWebhookSecret — // hier noch einmal per config() nach Modus zu unterscheiden // wäre eine zweite Stelle, die dieselbe Entscheidung trifft. satisfied: filled(StripeWebhookSecret::current()), ), new Check( key: 'billing.company_details', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.billing.company_details'), breaks: __('readiness.billing.company_details_breaks'), tab: 'company', // Die vorhandene Liste wird gerufen, nicht verdoppelt: zwei // Quellen für eine Frage ist der Weg, auf dem sie auseinanderlaufen. satisfied: CompanyProfile::missingForInvoicing() === [], ), new Check( key: 'billing.invoice_series', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.billing.invoice_series'), breaks: __('readiness.billing.invoice_series_breaks'), tab: 'company', // IssueInvoice draws a series with `where('kind', $kind) // ->where('active', true)` (IssueInvoice.php:373, :444) — a // deactivated row is invisible to it exactly like a missing // one, so the same `active` filter belongs here too. Without // it, an installation with a switched-off 'invoice' series // still counted as ready, the false-green this page exists to // rule out. satisfied: InvoiceSeries::query() ->whereIn('kind', ['invoice', 'credit_note', 'cancellation']) ->where('active', true) ->distinct() ->count('kind') === 3, ), new Check( key: 'billing.catalogue_synced', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.billing.catalogue_synced'), breaks: __('readiness.billing.catalogue_synced_breaks'), tab: 'services', // stripe:sync-catalogue (SyncStripeCatalogue::handle()) only // ever touches PUBLISHED versions — "a draft has promised // nothing, and a Product for it would be a price list entry // for something that may never exist" (its own header // comment). A draft's own unsynced price is therefore not a // gap; counting it anyway turns this check into an alarm that // never clears on any installation that has ever drafted a // next version, which stops it being read at all. satisfied: PlanPrice::query() ->whereHas('version', fn ($query) => $query->whereNotNull('published_at')) ->whereNull('stripe_price_id') ->doesntExist(), ), ]; } }