Break the follow-ups into six testable steps

Six tasks, each ending on a green billing folder and its own commit: move the
money gate into the class that promises it, four fixes that make the fake behave
like Stripe, ask Stripe for its products, recognise an orphaned one, count
adopted apart from created, and hand the operator a sweep for the orphans nobody
can adopt.

Self-review dropped one item the spec had carried over from the last review: the
zero guard in PlanPrices::ensure(). Its only purpose was to keep a tiered price
unreachable, and the gate move rejects those directly — so it would be a
behaviour change with no remaining benefit, and it would make a free plan
unsellable. Recorded as deliberately not built, with the reason, rather than
quietly dropped.

Also corrected: a test that counted every product where the run mints one per
family, and a step that named the test it replaces only by description.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/host-bootstrap
nexxo 2026-07-30 16:56:08 +02:00
parent 3ceec8fe5e
commit 7d66a01a40
2 changed files with 1702 additions and 6 deletions

File diff suppressed because it is too large Load Diff

View File

@ -103,10 +103,23 @@ einen vierteljährlichen oder mengenteilenden Preis überhaupt hinstellen kann.
wiederkehrende Preise **mit** ihren preisbestimmenden Eigenschaften; was davon
zulässig ist, entscheidet der Aufrufer.
**Verwandt, hier mit erledigt:** `PlanPrices::ensure()` bekommt die
`<= 0`-Schranke, die `AddonPrices::ensure()` schon hat. Sie ist das Einzige, was
einen `billing_scheme: tiered`-Preis (bei dem Stripe `unit_amount: null`
liefert, hier als 0 gelesen) auf der Paketseite überhaupt erreichbar macht.
### Die Null-Schranke in `PlanPrices` wird **nicht** gebaut
Der Abschluss-Review hatte sie vorgeschlagen, und §14 des Vorgängers hat sie
notiert: `AddonPrices::ensure()` hat eine `<= 0`-Schranke, `PlanPrices::ensure()`
nicht, und das sei das Einzige, was einen `billing_scheme: tiered`-Preis (bei dem
Stripe `unit_amount: null` liefert, hier als 0 gelesen) auf der Paketseite
erreichbar macht.
Das stimmte — **bis zu diesem Umzug**. Sobald `AdoptStripePrice` selbst
`billing_scheme !== 'per_unit'` ablehnt, ist ein gestufter Preis unabhängig vom
Betrag des Aufrufers ausgeschlossen. Die Schranke hätte dann keinen Zweck mehr,
wäre aber eine Verhaltensänderung: ein Paket zu null — ein kostenloser Tarif,
heute nicht im Katalog, aber jederzeit anlegbar — bekäme keinen Stripe-Preis
mehr und wäre nicht verkäuflich. Eine Schranke ohne Nutzen, die einen künftigen
Fall bricht, wird nicht eingebaut.
Der §14-Eintrag wird entsprechend abgehakt: nicht gebaut, und warum.
## 4. `stripe:sweep-orphan-prices` — das Aufräum-Kommando
@ -171,7 +184,9 @@ Ohne Entwurfsentscheidung — bauen, Test, weiter (R22.5):
(`SyncStripeCatalogue` holt `PlanPrices` pro Zeile neu), ein Zähler auf der
Instanz käme also nie über eins hinaus. Sie hat sonst keinen Zustand, und
der Zähler ist das, was den Bericht wahr macht.
6. `PlanPrices::ensure()` bekommt die `<= 0`-Schranke — siehe §3.
6. ~~`PlanPrices::ensure()` bekommt die `<= 0`-Schranke~~**entfällt**, siehe
§3: der Umzug des Geld-Tors nimmt ihr den Zweck, und sie würde einen
kostenlosen Tarif unverkäuflich machen.
## 7. Was nicht dazugehört
@ -234,7 +249,8 @@ Und an `StripeIdempotencyKeyTest.php`: Blättern **wirft** bei fehlender `id`
Warnung drosseln
- `app/Services/Billing/AddonPrices.php` — Produkt-Wiedererkennung in
`product()`
- `app/Services/Billing/PlanPrices.php``<= 0`-Schranke
- `app/Providers/AppServiceProvider.php` — beide Adoptions-Klassen als
Singleton, damit ihr Lauf-Protokoll (`$duplicates`, `$adoptions`) trägt
- `app/Console/Commands/SyncStripeCatalogue.php` — Produkt-Wiedererkennung,
getrennte Zählung, Kommentar berichtigt
- `tests/Feature/Billing/StripePriceAdoptionTest.php`,