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
* 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
{

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
**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

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.
- **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.

View File

@ -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;