2321854 removed it. That commit's own work — discarding an unconfirmed
registration and sweeping abandoned ones — is untouched and correct; what went
with it was every file of the Stripe price adoption merged an hour earlier:
AdoptStripePrice, IdempotencyKey, the unique-index migration, both test files,
the spec, the plan, and the edits in six more.
Nothing was lost. The commits stayed in history and the files stayed on disk,
untracked, which is what a staged deletion from a stale tree leaves behind.
Restored from 4eb90c8, the reviewed head, by explicit path — the fourteen paths
2321854 damaged and not one more, so the ten files that commit legitimately
added or changed keep exactly what it gave them.
This is the failure the repo's own rule exists to prevent: stage by path, never
`git add -A`, because a second session's index does not know what a first one
merged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Abmelden und neu registrieren" was advice that could not work. Signing out
frees nothing: the address is still held by the unique index, so registering
again with the address somebody meant fails — and every abandoned attempt
stayed in `users` for ever. Reported exactly that way, and it was right.
The button discards the account instead. Only an UNCONFIRMED one, and only
one with nothing behind it: the webhook creates a customer's login from a
paid checkout, so a user can sit on this page unconfirmed while already
having a contract, and deleting that is data loss dressed up as a
convenience. It refuses and says where to go. A confirmed account is closed,
not discarded — that is ConfirmCloseAccount, with an invoice history behind
it.
R23: confirmed in a modal. The modal mutates nothing; it dispatches the event
the page listens for, so the checks stay in the one place they already are
rather than being duplicated where they could drift.
And nothing has to be discarded by hand. clupilot:prune-unverified removes
registrations nobody confirmed after five days — long enough for somebody who
signed up on a Friday and found the mail in a spam folder on Monday, short
enough that a typo does not hold the correct address for a month. It skips a
confirmed account whatever its age, and any account with a customer record
behind it, matched on user_id AND on the address, because either link means
somebody is a customer. Every removal is logged with the address: a line that
disappears is what somebody asks about later, and a count answers nothing.
The deadline is said on the page as well as in the mail. Somebody who never
received the mail is looking at the page, and the deadline is the reason they
do not have to do anything about an attempt they abandon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ten follow-ups, none of them blocking and none of them done. The one that
matters: activePricesFor() recognises an orphaned Price and nothing recognises an
orphaned PRODUCT — and a second Product is worse than a duplicate Price, because
every activePricesFor() then asks about the wrong Product and recognition goes
blind for that whole family. The branch disables its own guard through the gap it
did not close.
Second: AdoptStripePrice promises in its own docblock that no adoption can move
money, but can only compare amount, currency and interval. The four properties
that also decide what a Price charges are filtered in HttpStripeClient, which the
fake cannot express — so no test of the class can reach the promise it makes.
Also recorded: two pre-existing test failures this branch surfaced without
causing, with the diagnosis, so the next person does not derive them again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six tasks, each ending on a green billing folder and its own commit: the key
fingerprint (and a fake that finally reproduces Stripe's refusal), the two
listing calls, the adoption step itself, then each price service in turn, then
one row per Stripe price.
The spec now names both test files. What goes on the wire is checked through
Http::fake against the real client — asserting against the fake would only prove
the fake agrees with itself, which is the mistake that let the incident through.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A run on 2026-07-29 23:11 created price_1TygdEC7u8NpJ8pOt3nsoyYw at Stripe and
died before the row was written. 9da1358 then added the tax_treatment metadata
field, so the next sweep sent the same idempotency key with different parameters
and Stripe refused it — for twenty-four hours, and not only for the sweep:
AddonPrices::ensure() also runs inside a customer's module booking.
Two defects, one spec: nothing asks Stripe what it already has, and the key
covers less than the call sends. The spec settles what identity means here
(product, amount, currency, interval — never metadata, which Stripe lets you
edit) and where the key's fingerprint may go (the catalogue calls, never a
refund, where a duplicate costs the customer their money twice).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>