Commit Graph

3 Commits (006ce3568b57733b347bf3c4e3e45e15830273ae)

Author SHA1 Message Date
nexxo 26cf1a72a0 Remember which Stripe account the catalogue was built in
The mode switches the credentials. It does not switch what was made from
them: a Price id and a Product id belong to the account that issued them,
and test and live are two accounts. Every credential got two slots on this
branch; the ids derived from them sit in single-valued columns.

So the planned sequence of this installation ended in the exact false
green this page exists to rule out — sync in test, store the live key,
switch, and billing.catalogue_synced went on reporting satisfied because
it only asked whether the column was filled. "Bereit für Livebetrieb",
and the first real order got "No such price".

Detection, not repair. Stripe does not put the account in the id —
prod_… and price_… look the same in both, only KEYS carry _test_/_live_
— so the origin cannot be read back out of a stored id, and asking
Stripe is out: this page reaches nothing over the network on a page load.
What is left is to write it down at sync time, which is what
App\Support\StripeCatalogueMode does.

One setting for the whole catalogue is only honest because
stripe:sync-catalogue now REFUSES a run into a catalogue that belongs to
the other account. Without that, the run would skip every row that already
carries an id, answer "already in step", and record an account it never
touched — the same lie one level down. The registers count as stored
objects too: inStep() takes a registered row as proof on its own for the
reverse-charge half.

The `breaks` sentence says what happens (checkout fails, no order) and
what actually helps, including the awkward half: re-running the sync is
not enough, the pointers and both registers have to be cleared first.

The slot migration backfills the one case it can prove: whatever is at
Stripe was made with the one key this installation has ever stored, so it
belongs to the account that key opens. Otherwise a long-synced catalogue
would read as "origin unknown" and the page would demand a re-sync nobody
needs.

Red first:

  ⨯ it does not call the catalogue synced when its ids belong to the other account
  ⨯ it says the sale is refused and a fresh sync is needed, not that a column is empty
  ⨯ it records the mode its objects were created in
  ⨯ it refuses to work into a catalogue that belongs to the other account
  ⨯ it syncs into the new account once the stale ids are cleared

Full suite: 1814 passed, 6357 assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:12:56 +02:00
nexxo ac44717d1b Cover the unreadable branch and make rollback pick live deterministically
The only test that reached SecretCipher::decrypt() failing was an accident of
a fixture bug fixed in the previous commit: it encrypted with the wrong key,
so decryption threw and the migration's catch(Throwable) branch ran without
anyone meaning to test it. Fixing that fixture silently removed the only
coverage of a branch that decides a payment key's mode, so it gets its own
test with a deliberately unreadable row.

down() picked whichever of the :live/:test rows an unordered get() happened to
return first to restore to the bare key, and deleted the other — so which
credential survived a rollback depended on row order, not a decision. It now
processes :live rows first unconditionally, then :test rows only where the
bare key is still free, so the live credential always wins when both exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:32:58 +02:00
nexxo ecf948f30f Let the stored key say which mode it belongs to
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:16:15 +02:00