NonProductionChargeBlocked
\Benecaster\Payment\NonProductionChargeBlocked
The exception OutboundChargeGuard throws when a money or subscription-lifecycle call is refused because this install is a copy of the one that armed itself. This is the thing your add-on catches at its own entry points.
Every entry point that can reach a guarded call must catch it, and the reason is specific. An uncaught exception inside a webhook handler is a 500, and Stripe answers a 500 by retrying — so the one thing worse than the charge you blocked is blocking it in a way that makes Stripe try again all day. Catch it at the boundary and convert it to a settled refusal.
It is a distinct type rather than a bare runtime exception so that boundaries can catch this without swallowing genuine Stripe or transport errors, which must keep propagating and being retried.
It extends RuntimeException deliberately, and the reason belongs in your catch ordering: every boundary that was already defensive degrades to its old behaviour instead of letting the refusal escape through it. If you catch both, the specific arm must come first — a RuntimeException arm placed above it will swallow the refusal and, worse, commonly echoes the caught message to the caller, publishing both site URLs and the wp-config.php constant.
This is also why the plugin throws here at all, where the licence server’s equivalent rule says to return the method’s established failure value: the plugin’s money methods return a non-nullable subscription or payment intent and have no failure value to return, so their established failure mode already is an exception.
Notes
Carries three readonly properties — operation, captured_url and current_url — naming the refused call, the URL this install recorded when it armed itself, and the URL it is running at now.
These are for your logs, never for a response body. The assembled message names both URLs and the constant that disarms the guard; that is infrastructure detail a subscriber can do nothing with, on endpoints an anonymous visitor can reach. Core's own routes answer with a fixed generic string and send the detail to the debug log instead.