stripe:sweep-orphan-prices described itself as listing "what AdoptStripePrice
will not take over". It computes nothing of the sort: it lists every active
Price at one of our Products that no register row knows, adoptable or not — the
only question it asks is whether a row holds the id. And then it invited the
destructive path, with no word anywhere about running stripe:sync-catalogue
first.
Following that advice can make a plan unsellable. Archive an orphan the next
sync would have adopted and the sync can no longer see it: activePricesFor()
asks Stripe for active prices only, so adoption returns null and createPrice()
fires under the same key the crashed run used. For twenty-four hours Stripe
answers a repeated key by replaying the stored response — the archived Price's
id, in the body it had while it was live. The register writes it down as live,
and the checkout is refused. This is the same class of defect the branch exists
against: a comment that was eloquent and wrong.
- The docblock now says what the command lists, names the twenty-four-hour
replay so nobody has to rediscover it, and the ordering requirement is in
$description and printed on every path that found something, --archive
included. The reassurance that archiving is harmless is now scoped to what
THIS codebase sells; a payment link an operator built by hand against one of
our Products is withdrawn all the same, and says so.
- stripe:sync-catalogue --dry-run says "created or adopted" again. A dry run
counts intents without calling ensure(), so it cannot know which Stripe
already holds — and a dry run is the first thing an operator runs after an
interrupted one. The live line keeps both figures, where they are knowable.
- The sweep's plan side gets a test: an orphan at a family Product is reported
and a known plan Price never is. Deleting either half of the lookup turns it
red; the five tests it joins all stayed green for the products() half.
- The throttle test pinned "once" and neither "per price" nor "per day". A key
mutated to a constant would have silenced every orphan after the first and
kept it green — the silent no-op the branch is built against. It now plants a
second orphan, and travels a day to hear the first one again.
- Two of my own documents corrected. The spec sentence claiming an adoptable
orphan never appears in this list is what the implementation faithfully built;
it holds only if a sync has run since the orphan appeared, which nothing
enforced. And the two adoption classes do not carry the same run log:
$adoptions on the Price side, $duplicates on the Product side.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>