diff --git a/app/Console/Commands/SweepOrphanStripePrices.php b/app/Console/Commands/SweepOrphanStripePrices.php index 2dbdeee..8e2c75e 100644 --- a/app/Console/Commands/SweepOrphanStripePrices.php +++ b/app/Console/Commands/SweepOrphanStripePrices.php @@ -18,10 +18,18 @@ use Illuminate\Console\Command; * was the right call and it left an operator with a log line and no way to act * on it. * - * **What this lists is wider than that log line.** Every active Price at one of - * our Products that no row of ours knows — adoptable or not. The only question - * asked below is whether a register row holds the id; nothing here re-asks - * AdoptStripePrice whether it would have taken the Price over. + * **What this lists is wider than that log line.** Every active RECURRING Price + * at one of our Products that no row of ours knows — adoptable or not. The only + * question asked below is whether a register row holds the id; nothing here + * re-asks AdoptStripePrice whether it would have taken the Price over. + * + * Recurring, because activePricesFor() sends `type: recurring` as well as + * `active: true`. That filter can hide somebody else's one-time Price from the + * list; it can never hide one of ours. createPrice() always sends + * `recurring[interval]`, and it is the only call that mints a Price at one of + * our Products at all — the setup fee is priced inline at the checkout, under an + * ad-hoc Product of its own. So the filter errs toward listing too little, never + * toward missing an orphan of ours. * * **So stripe:sync-catalogue runs first.** Until it has, an orphan listed here * may be one the next sync would simply have adopted, and --archive on that one @@ -41,10 +49,12 @@ use Illuminate\Console\Command; * the reason it always is here: Stripe goes on billing every subscription * already on an archived Price, it only stops being SOLD — and nothing this * codebase sells is sold on a Price no row knows. That reasoning reaches no - * further than this codebase. A Price an operator wired up by hand — a payment - * link built in Stripe's own dashboard against one of our Products — is exactly - * the Price carrying none of our metadata described above, and --archive - * withdraws it too. + * further than this codebase. A Price an operator wired up by hand — a + * SUBSCRIPTION payment link built in Stripe's own dashboard against one of our + * Products — is exactly the Price carrying none of our metadata described above, + * and --archive withdraws it too. A one-time link is the other side of the + * recurring filter: neither listed nor archived, and so neither protected nor + * disturbed by this command. */ class SweepOrphanStripePrices extends Command { diff --git a/docs/handoffs/2026-07-30-real-run-handoff.md b/docs/handoffs/2026-07-30-real-run-handoff.md index ab88736..c341fc9 100644 --- a/docs/handoffs/2026-07-30-real-run-handoff.md +++ b/docs/handoffs/2026-07-30-real-run-handoff.md @@ -223,9 +223,12 @@ Kette an einer anderen Stelle, und mehrere brechen sie **nach** der Zahlung. `stripe:reprice-subscriptions` (je zuerst `--dry-run`). Der Abgleich legt jetzt **zwei** Preise je Paket an; bis er lief, kann ein geprüfter EU-Firmenkunde nicht bestellen — das ist Absicht. -- `stripe:sweep-orphan-prices` listet jeden **aktiven** Stripe-Preis an unseren - Produkten, den **keine** Zeile in `stripe_addon_prices` oder - `stripe_plan_prices` kennt — mit ID, Betrag, Währung, Intervall und Metadaten. +- `stripe:sweep-orphan-prices` listet jeden **aktiven wiederkehrenden** + Stripe-Preis an unseren Produkten, den **keine** Zeile in + `stripe_addon_prices` oder `stripe_plan_prices` kennt — mit ID, Betrag, + Währung, Intervall und Metadaten. Einmalige Preise sieht es nicht; von uns + angelegt ist nie einer davon, `createPrice()` schickt immer + `recurring[interval]`. Ohne Argument wird nichts angefasst; mit `--archive` werden die aufgelisteten Preise bei Stripe stillgelegt, also **nicht mehr verkauft** (laufende Abos darauf verrechnet Stripe weiter). **Zuerst `stripe:sync-catalogue` laufen diff --git a/docs/superpowers/specs/2026-07-30-stripe-adoption-followups-design.md b/docs/superpowers/specs/2026-07-30-stripe-adoption-followups-design.md index 1e235c6..d8b3bd1 100644 --- a/docs/superpowers/specs/2026-07-30-stripe-adoption-followups-design.md +++ b/docs/superpowers/specs/2026-07-30-stripe-adoption-followups-design.md @@ -128,13 +128,14 @@ begründet. Die Log-Zeile allein reicht nicht: sie nennt die Waise, aber niemand kann etwas mit ihr tun, ohne von Hand in Stripes Oberfläche zu gehen. - **Ohne Argument:** ein Bericht. Für jedes unserer Produkte werden die aktiven - Preise geholt und die aufgelistet, die **keine Zeile** in + **wiederkehrenden** Preise geholt und die aufgelistet, die **keine Zeile** in `stripe_addon_prices` oder `stripe_plan_prices` kennt — mit ID, Betrag, Währung, Intervall und Metadaten. Nichts wird angefasst. -- **Mit `--archive`:** dieselbe Liste wird stillgelegt. Das ist harmlos, und - zwar aus demselben Grund wie überall sonst in diesem Projekt: ein - archivierter Preis verrechnet laufende Abos weiter, er wird nur nicht mehr - verkauft. +- **Mit `--archive`:** dieselbe Liste wird stillgelegt. Für alles, was **dieses + Projekt** verkauft, ist das harmlos, und zwar aus demselben Grund wie überall + sonst hier: ein archivierter Preis verrechnet laufende Abos weiter, er wird + nur nicht mehr verkauft. Für eine **übernehmbare** Waise gilt das ausdrücklich + **nicht** — siehe die Berichtigung unten. - **`--dry-run`** wie bei `stripe:sync-catalogue`, damit die beiden Kommandos sich gleich anfühlen. diff --git a/tests/Feature/Billing/SweepOrphanPricesTest.php b/tests/Feature/Billing/SweepOrphanPricesTest.php index 4d2937d..2b4a9fd 100644 --- a/tests/Feature/Billing/SweepOrphanPricesTest.php +++ b/tests/Feature/Billing/SweepOrphanPricesTest.php @@ -8,14 +8,19 @@ use App\Services\Stripe\StripeClient; use Illuminate\Contracts\Console\Kernel; /** - * The orphans nobody can adopt. + * The orphans an operator can finally act on. * - * A Price at one of our Products that no row knows, and that AdoptStripePrice - * refuses — because nothing in its metadata proves it is ours, or because it - * charges in a way we never mint. The recognition step names it in the log and - * leaves it, deliberately: adopting a stranger's Price is worse than minting a - * second one. This is the other half of that decision, the one an operator can - * act with. + * The command lists EVERY active recurring Price at one of our Products that no + * row knows — adoptable or not; the only question it asks is whether a register + * row holds the id. That is wider than the log line AdoptStripePrice writes, and + * it is why stripe:sync-catalogue has to run first: see the command's own + * docblock, which had to be corrected away from the narrower claim. + * + * The fixtures below are all at the narrow end anyway — planted with no metadata + * at all, so nothing could have adopted them, which is what a person clicking + * through Stripe's dashboard leaves behind. That keeps each test about the + * sweep's own question (does a row know this id?) rather than about what the + * recognition step would have done with it. */ beforeEach(function () { $this->stripe = new FakeStripeClient;