Commit Graph

3 Commits (823eeaf413c44cbc8b259bee8dbcd8df52303d19)

Author SHA1 Message Date
nexxo 823eeaf413 Refuse a key that does not belong to the slot it sits in
put('stripe.secret', 'sk_live_REALMONEY') in test mode was accepted,
get() handed it out, billing.stripe_secret reported satisfied, and the
badge above it said "Testbetrieb". The strict rule closed the automatic
route into that state (no fallback); the typed one stayed open.

The prefix decides it without touching the network, and that rule now
lives in ONE place — OperatingMode::ofStripeKey() — called by the three
that were answering it separately: the slot migration (unchanged verdict,
`?? Live` for a value it cannot place), StripeCheck's `live` flag
(unchanged verdict, null stays false), and the readiness check, which
never asked at all. A key it cannot place is not reported as a
contradiction: this check only says what it can prove.

The two directions get their own `breaks` sentence, because the
consequences are opposite — real money moving while the console says
test, versus no money moving while the order looks paid.

The "Prüfen" button no longer contradicts the check either: the page
rendered only ok/reason, so a live key in the test slot answered
"Geprüft: in Ordnung". It now names the account the key belongs to.

Red first:

  ⨯ it refuses a live key sitting in the test slot
  ⨯ it refuses a test key sitting in the live slot
  ⨯ it says what the wrong key does, not that a field is empty

Guard tests (ConfirmInModal, ModalHeight, IconLayout, DisplayTimezone)
run with the blade change: green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:05:58 +02:00
nexxo b1e5bf8023 Fix round: match the actual selection IssueInvoice and sync use
Two of the five billing checks measured something close to, but not
the same as, what actually gates the flow they report on:

- billing.invoice_series counted distinct kinds without filtering on
  `active`, but IssueInvoice draws its series with
  where('kind', ...)->where('active', true) (IssueInvoice.php:373,
  :444) — a deactivated row is invisible to it exactly like a missing
  one. An installation with a switched-off 'invoice' series was
  reported ready while no invoice could draw a number: false-green.

- billing.catalogue_synced counted every PlanPrice with a null
  stripe_price_id, but stripe:sync-catalogue only ever mirrors
  PUBLISHED versions (SyncStripeCatalogue's own header comment). A
  draft's unsynced price is an ordinary state, not a gap; the old
  query kept this check permanently unsatisfied on any installation
  that had ever drafted a next version — an alarm always on and
  therefore never read.

Both queries now mirror the condition the real gate uses instead of a
similar-looking one of their own. Two tests added that were red before
the fix: an inactive required series that must not read as ready, and
an unpublished draft's unsynced price that must not raise an alarm.

Also strengthens the weakest breaks sentence: invoice_series_breaks
named the mechanism ("a document cannot draw a number") rather than
the consequence, unlike its four siblings. It now says what the
customer and operator actually experience — a running cloud with no
invoice, the failure only logged — matching company_details_breaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:42:09 +02:00
nexxo d9c0ad0e4a Say what breaks, not just which field is empty
Task 7: the readiness collector (App\Support\Readiness) and its first
group, billing (App\Support\Readiness\BillingChecks). Reports missing
prerequisites — the active mode's Stripe key, its webhook signing
secret, complete company details, an invoice series per document kind,
a fully Stripe-synced catalogue — without enforcing anything itself;
the real locks (IssueInvoice, the checkout's Stripe-key guard) stay
where they are.

The webhook check uses App\Support\StripeWebhookSecret::current()
rather than querying config() by mode a second time, and the company
check calls CompanyProfile::missingForInvoicing() rather than
duplicating its list — two sources for one question is how they drift
apart.

Added lang/de/readiness.php and lang/en/readiness.php with every
`readiness.*` key this group uses, each `_breaks` sentence naming the
actual downstream failure rather than repeating the field name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:25:27 +02:00