Skip to content

Fix the four regressions this campaign's own fixes introduced - #8

Merged
sylvesterdamgaard merged 1 commit into
mainfrom
fix/rereview-regressions
Aug 3, 2026
Merged

Fix the four regressions this campaign's own fixes introduced#8
sylvesterdamgaard merged 1 commit into
mainfrom
fix/rereview-regressions

Conversation

@sylvesterdamgaard

Copy link
Copy Markdown
Contributor

Found by the re-review pass. Each is the same shape: a fix added a call that can fail, into a path whose surrounding design assumes it cannot.

Regression From
P1 A gateway outage on renewal killed the job instead of opening dunning the off-session fix
P2 GET /portal/{token} minted a gateway customer, and resolved up to 3× the gateway-customer fix
P2 The org-ref middleware hydrated a full model to return its own argument, on the 600/min enforcement path the ULID fix
P2 One garbled currency cell aborted an entire import run the ×100 money fix

The P1 is the one that matters. PaymentService::pay() gained two gateway calls, and charge() catches exceptions precisely so dunning opens — the new calls sat upstream of the only protection. A brief Stripe incident during the monthly run killed the job after the period had advanced and the invoice was issued: invoice left Open, dunning never opened, subscription still serving, nobody notified. Strictly worse than the decline it replaced, because a decline is visible.

#4 is a nice illustration of an exception-type assumption. MinorUnits::parse() catches MathException; Currency::of() throws UnknownCurrencyException, which is a plain RuntimeException. Unrelated hierarchies, so the catch never fired.

Gate

pint · PHPStan level max (0 errors) · 1034 tests · composer audit — green.

Found by the re-review, which is what it is for. Each is a case where a fix added a
call that can fail into a path whose surrounding design assumes it cannot.

1. A GATEWAY OUTAGE ON RENEWAL KILLED THE JOB INSTEAD OF OPENING DUNNING (P1).
   The off-session fix added `customers->resolve()` and `gateway->paymentMethods()`
   to PaymentService::pay(). Both talk to the gateway and both throw; nothing further
   up catches — not chargeRenewal(), not RenewSubscriptionJob. charge() itself
   catches precisely so dunning opens, so the new calls sat UPSTREAM of the only
   protection. A brief incident during the monthly run killed the job after the
   period had advanced and the invoice was issued: invoice left Open, dunning never
   opened, subscription still serving, nobody notified. Both now degrade to a FAILED
   result, which is the recoverable outcome.

2. GET /billing/portal/{token} MINTED A GATEWAY CUSTOMER (P2). gatewayAccountFor()
   resolves through a seam that CREATES on miss, so merely loading the portal page
   wrote a Stripe customer and a gateway_customers row — a non-idempotent side effect
   on a GET — and a gateway outage 500'd the whole page including the invoice history
   and cancel flow, which need no gateway at all. Now memoised per request (it was
   re-resolving up to three times in removeMethod()) and the page renders an empty
   saved-cards panel rather than failing.

3. THE ORG-REF MIDDLEWARE HYDRATED A MODEL TO RETURN ITS OWN ARGUMENT (P2). It ran
   `find()` — a full Eloquent model, every column — to return the string it was
   handed, on every authenticated request including /reserve and /usage at 600/min
   per token, and resolved twice when `org` arrived in both path and body. Now one
   non-hydrating `value('id')` with an orWhere, memoised per request.

4. ONE GARBLED CURRENCY CELL ABORTED AN ENTIRE IMPORT RUN (P2). MinorUnits::parse()
   catches MathException, but Currency::of() throws UnknownCurrencyException — an
   unrelated class — so the throw escaped through parse() with no handler and lost a
   10 000-record dry-run to one bad cell. The importer's whole design is per-record
   outcomes; the adapter now returns null and BillingImporter lands the record as the
   Conflict it already knows how to report.

Regression tests cover the two that are observable end to end: a gateway that throws
on listing methods still yields Failed, and an export carrying an unknown currency
still parses.
@sylvesterdamgaard
sylvesterdamgaard merged commit bf37958 into main Aug 3, 2026
2 checks passed
@sylvesterdamgaard
sylvesterdamgaard deleted the fix/rereview-regressions branch August 3, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant