*/ public static function all(): array { $mode = OperatingMode::current(); return [ new Check( key: 'billing.stripe_secret', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.billing.stripe_secret', ['mode' => __('readiness.mode.'.$mode->value)]), breaks: __('readiness.billing.stripe_secret_breaks'), tab: 'services', // get() löst den aktiven Modus selbst auf UND ist für Stripe // strikt — ein Live-Schlüssel zählt im Testbetrieb nicht als // Nachweis, sonst widerspräche diese Seite der Kassensperre. satisfied: filled(app(SecretVault::class)->get('stripe.secret')), ), 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(), ), ]; } }