Name the recurring filter, and stop the spec contradicting itself

Three sentences left over from the wave before, all of the kind this branch
exists to end.

- "Every active Price" was one word short. activePricesFor() sends
  `type: recurring` as well as `active: true`, so a one-time Price is never
  listed — which made the hand-built-payment-link sentence false for a one-time
  link: neither listed nor archived. Both places say `recurring` now, and the
  docblock says why the filter is not a gap: createPrice() always sends
  `recurring[interval]` and is the only call that mints a Price at one of our
  Products, so the filter can only ever hide somebody else's one-time Price.
  A filter left out of a sentence is a gap the next reader goes looking for.
- The sweep test file still opened "The orphans nobody can adopt" — the exact
  narrowing the command's own docblock had to be corrected away from. It happens
  to describe the fixtures, which are all metadata-less, so it now says that
  instead of claiming it of the command.
- The design doc called --archive "harmlos" seven lines above the correction
  establishing that it is not, for an adoptable orphan. A document that
  contradicts itself seven lines apart teaches nobody anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/host-bootstrap
nexxo 2026-07-30 19:24:18 +02:00
parent a43be4186a
commit 79654333a8
4 changed files with 42 additions and 23 deletions

View File

@ -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 * was the right call and it left an operator with a log line and no way to act
* on it. * on it.
* *
* **What this lists is wider than that log line.** Every active Price at one of * **What this lists is wider than that log line.** Every active RECURRING Price
* our Products that no row of ours knows adoptable or not. The only question * at one of our Products that no row of ours knows adoptable or not. The only
* asked below is whether a register row holds the id; nothing here re-asks * question asked below is whether a register row holds the id; nothing here
* AdoptStripePrice whether it would have taken the Price over. * 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 * **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 * 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 * 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 * 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 * 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 * further than this codebase. A Price an operator wired up by hand a
* link built in Stripe's own dashboard against one of our Products is exactly * SUBSCRIPTION payment link built in Stripe's own dashboard against one of our
* the Price carrying none of our metadata described above, and --archive * Products is exactly the Price carrying none of our metadata described above,
* withdraws it too. * 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 class SweepOrphanStripePrices extends Command
{ {

View File

@ -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 `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 **zwei** Preise je Paket an; bis er lief, kann ein geprüfter EU-Firmenkunde nicht
bestellen — das ist Absicht. bestellen — das ist Absicht.
- `stripe:sweep-orphan-prices` listet jeden **aktiven** Stripe-Preis an unseren - `stripe:sweep-orphan-prices` listet jeden **aktiven wiederkehrenden**
Produkten, den **keine** Zeile in `stripe_addon_prices` oder Stripe-Preis an unseren Produkten, den **keine** Zeile in
`stripe_plan_prices` kennt — mit ID, Betrag, Währung, Intervall und Metadaten. `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 Ohne Argument wird nichts angefasst; mit `--archive` werden die aufgelisteten
Preise bei Stripe stillgelegt, also **nicht mehr verkauft** (laufende Abos Preise bei Stripe stillgelegt, also **nicht mehr verkauft** (laufende Abos
darauf verrechnet Stripe weiter). **Zuerst `stripe:sync-catalogue` laufen darauf verrechnet Stripe weiter). **Zuerst `stripe:sync-catalogue` laufen

View File

@ -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. 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 - **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, `stripe_addon_prices` oder `stripe_plan_prices` kennt — mit ID, Betrag,
Währung, Intervall und Metadaten. Nichts wird angefasst. Währung, Intervall und Metadaten. Nichts wird angefasst.
- **Mit `--archive`:** dieselbe Liste wird stillgelegt. Das ist harmlos, und - **Mit `--archive`:** dieselbe Liste wird stillgelegt. Für alles, was **dieses
zwar aus demselben Grund wie überall sonst in diesem Projekt: ein Projekt** verkauft, ist das harmlos, und zwar aus demselben Grund wie überall
archivierter Preis verrechnet laufende Abos weiter, er wird nur nicht mehr sonst hier: ein archivierter Preis verrechnet laufende Abos weiter, er wird
verkauft. 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 - **`--dry-run`** wie bei `stripe:sync-catalogue`, damit die beiden Kommandos
sich gleich anfühlen. sich gleich anfühlen.

View File

@ -8,14 +8,19 @@ use App\Services\Stripe\StripeClient;
use Illuminate\Contracts\Console\Kernel; 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 * The command lists EVERY active recurring Price at one of our Products that no
* refuses because nothing in its metadata proves it is ours, or because it * row knows adoptable or not; the only question it asks is whether a register
* charges in a way we never mint. The recognition step names it in the log and * row holds the id. That is wider than the log line AdoptStripePrice writes, and
* leaves it, deliberately: adopting a stranger's Price is worse than minting a * it is why stripe:sync-catalogue has to run first: see the command's own
* second one. This is the other half of that decision, the one an operator can * docblock, which had to be corrected away from the narrower claim.
* act with. *
* 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 () { beforeEach(function () {
$this->stripe = new FakeStripeClient; $this->stripe = new FakeStripeClient;