OverLimitFlagManager
\Benecaster\License\OverLimitFlagManager
Evaluates over-limit state on every license validation cycle and maintains the is_over_limit flag on subscriber tokens. Two independent limits are checked: the paying-subscriber limit and the total token cap. The union of both flagged sets is written in one sync; flags clear themselves on the next cycle once counts fall back inside both limits, with no subscriber action required.
This class is the only consumer of the two enforcement preferences, and that is a deliberate contract rather than an accident of where the code happened to go. Two surfaces read it back downstream, both through one shared resolver rather than the option directly — FeedController::should_lock_over_cap_free_token() and SubscribersController — and the notes below explain why that is legitimate for them and nowhere else.
Constructor Dependencies
| Type | Description |
|---|---|
\Benecaster\License\CapChecker |
Supplies the current plan's subscriber limit and token cap. |
\Benecaster\Token\TokenRepository |
Reads active tokens and writes the is_over_limit flag. |
\Benecaster\License\LicenseManager |
Supplies the enforcement preferences. It is the third constructor parameter, so anything constructing this class directly must pass it. |
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
register(): void |
Public | — | Hooks on_license_validated() to the benecaster_license_validated action. |
on_license_validated( array $response, string $show_uuid = '' ): void |
Public | — | Recomputes and syncs flags for the current counts of the validated show's license, then fires benecaster_over_limit_flags_evaluated with that show's UUID. Runs once per connected show on each daily cycle. On a plan unlimited on both axes, clears all flags and returns. |
flag_all_active_tokens( ?array $show_ids = null ): void |
Public | — | Flags every active token site-wide, ignoring both enforcement preferences — see the notes. It has no caller in the plugin. Grace expiry at Day 30 bulk-revokes instead (revoke_all_active_with_reason( 'grace_timeout' )), so grace expiry produces no flagged tokens. The method stays public and callable by add-ons, but nothing in Benecaster reaches it. |
Hooks Fired
Notes
The two axes answer to their preferences differently, and the asymmetry is intended.
The paying-subscriber limit flags under both settings. Demotion to the public feed is the correct interim outcome either way, because the license server's rolling 24-hour window takes a day to mature and the site is over its limit throughout. Under auto-upgrade the plan is raised at the end of that window and the flags clear on the next cycle; under cap they persist.
The token cap flags only under cap. Under auto-upgrade the podcaster has asked for their bill to move rather than their listeners' access, so nothing is flagged locally and the server's cap window raises the plan instead.
Who gives up access first is a contract, not an implementation detail. Free-tier tokens are exhausted completely before a single paying token is selected, and within each group the newest go first — free status is the primary sort key, not a tiebreak between identical timestamps. A token whose tier cannot be resolved is treated as paying and sorts last, so it is never selected while a known free token remains. The ordering itself lives in TokenRepository::get_newest_token_ids(); this class is what consumes it.
The lock is what made that ordering load-bearing. Under demotion, selecting a paying subscriber ahead of a free one was merely unfair — they still received a working feed. Under locking, the same mistake means a paying customer gets a 410 having paid you. Anything that reorders this is a billing-fairness change, not a query optimisation, and it should be reviewed as one.
Do not move either preference check downstream — into FeedController or an add-on. is_over_limit is a single boolean with no record of which axis set it, so a consumer downstream of this class cannot consult one preference without getting the other axis wrong: reading the token-cap preference in the feed would wrongly release subscriber-axis flags, and the reverse. Gating at the flagging site keeps the decision where the axis is still known, and the feed inherits correct behaviour without a second read.
The one sanctioned exception, and why it does not break the rule above. FeedController::should_lock_over_cap_free_token() re-reads the token-cap preference — legitimate only because it has already narrowed the token to a free one, and the subscriber axis never flags free tokens. A flagged free token can therefore only have come from the cap axis, which makes the axis knowable again and the read safe. It is belt-and-braces rather than load-bearing: under auto-upgrade the cap axis flags nothing for it to act on. Do not treat it as license for a second reader — it works because of a narrowing that a general consumer does not have.
flag_all_active_tokens() ignores both preferences, deliberately. It is a payment-failure consequence rather than a limit consequence, and no plan the license server could upgrade to would resolve an unpaid license. It is also the one path that flags free tokens without the cap axis being involved — anything downstream inferring "a flagged free token means the cap axis" must exclude it.
Billing is never touched by an over-limit condition. Over-limit enforcement behaves identically across every bridge, including the Stripe-backed built-in membership: the token is flagged and the feed is demoted or locked. Benecaster must never pause, cancel or refund a subscription in response to an over-limit condition, even where it owns the Stripe path directly — the podcaster is over a limit they can resolve, and their subscribers are paying for access they still substantially have.
Do not read that as "Benecaster never pauses billing". HardCutoffBillingPauser stops billing on built-in-membership subscriptions when the podcaster's own license reaches the hard cutoff described in Payment failure — a different trigger, a different fault, and one where the feed has stopped entirely. Nothing in this class participates in it.