BuyupBillingCompatibility
\Benecaster\Membership\BuyupBillingCompatibility
Answers whether a subscriber can buy a buy-up right now and, if so, which of its price rows they buy. The class name describes only half of what it does. It still answers billing compatibility, and it also answers the buy-up’s sale window. The two were not split into separate classes because the listing and the purchase both already ask this one class, and two predicates is how an offer and a refusal come to disagree.
A buy-up is billed as an item on the subscriber’s existing tier subscription, and Stripe (under its default billing mode, which the plugin uses) requires every item on a subscription to share one interval and one currency. So the question is a row lookup: does this buy-up sell a price at this subscriber’s billing period, in their currency? The subscriber’s period and currency come from the tier price row their subscription points at, not from billing_interval, which stores only the unit. billing_interval is a fallback for older rows with no price row, and there anything but month is refused and month is matched without a currency check.
The checks run in a fixed order, and the order decides which reason a subscriber sees. A pending billing-period switch comes first. The sale window comes next, ahead of any price matching: telling someone their billing period is wrong when the real answer is “this ended last week” sends them to change something that will not help. Then the billing period, then the currency, then whether the matched row has a Stripe Price in the subscription’s keyset.
Every caller that needs the answer shares this class: the subscriber-facing list and purchase routes, the account-page render, the billing-period switch and the free-buy-up matching. Do not re-derive the rule elsewhere.
Constructor Dependencies
| Type | Description |
|---|---|
\Benecaster\Membership\MembershipPriceRepository |
Reads the subscription's tier price row (interval unit, interval count, currency). |
\Benecaster\Membership\BuyupPriceRepository |
Reads the buy-up's price rows. |
?\Benecaster\Membership\BuyupRepository |
Optional. Reads the buy-up's sale window. Without it the window check fails open, the same way a missing price row is treated elsewhere. |
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
incompatibility( array $subscription, int $buyup_id ): ?string |
Public | — | Why the subscriber cannot buy the buy-up now, as one of the REASON_* constants, or null when they can. $subscription is a benecaster_subscriptions row as hydrated by SubscriptionRepository. |
matching_price( array $subscription, int $buyup_id ): ?array |
Public | — | The price row the subscriber would buy, or null when incompatibility() returns a reason. The purchase must use this row for both the amount the subscriber consents to and the Stripe Price the item is created against. Falling back to the buy-up's default row is how a subscriber would agree to one figure and be charged another. |
window_reason( array $buyup, ?int $now = null ): ?string |
Public | — | Static. REASON_NOT_YET_OPEN before available_from, REASON_WINDOW_CLOSED at or after available_until, or null. The window is half-open, so the closing instant is outside it. $now defaults to the current time. |
message( string $reason ): string |
Public | — | Static. The subscriber-facing, translated explanation for a REASON_* code. The account template and the purchase endpoint's refusal share it, so the two never say different things. |
row_currency( array $row ): string |
Public | — | Static. The currency a price row is billed in: the row's own currency, uppercase ISO-4217 (USD when the row has none). That is the currency the row's Stripe Price is created in. Matching, display and purchase all ask this, so a match and a displayed card never read the row two different ways. |
buyup_currency( array $buyup ): string |
Public | — | Static. A buy-up's currency when no price row matches the subscriber, uppercase ISO-4217: the currency of its default price row, else USD. $buyup is a BuyupRepository row. Used so a listing can still name a currency when the subscriber has no subscription or no matching row. |
Constants
| Name | Value | Description |
|---|---|---|
REASON_SWITCH_PENDING |
'switch_pending' |
The subscriber has a billing-period switch pending; a new item would be dropped when the switch lands. Checked first. |
REASON_NOT_YET_OPEN |
'not_yet_open' |
Before the buy-up's available_from. Message: "This extra is not on sale yet. Check back soon." |
REASON_WINDOW_CLOSED |
'window_closed' |
At or after the buy-up's available_until. Message: "This extra is no longer available." Checked before the billing period, so it applies even to a subscriber whose period was never sold the buy-up. |
REASON_INTERVAL |
'interval' |
The buy-up has no price row at the subscriber's billing period. Often a deliberate offer (an annual-only buy-up) rather than a fault, so the message is an upsell: "This extra is not offered on your billing period. Change your billing period to add it." |
REASON_CURRENCY |
'currency' |
The buy-up sells the subscriber's period, but not in their currency. |
REASON_NOT_PROVISIONED |
'not_provisioned' |
The matched row has no Stripe Price in the subscription's keyset yet. A subscription with no recorded Stripe mode (the billing-period switch passes one) skips this check rather than failing it. |
Notes
The BUYUP_INTERVAL_UNIT and BUYUP_INTERVAL_COUNT constants are gone: a buy-up is no longer minted at a single interval. REASON_INTERVAL changed meaning with them, from "this buy-up is billed monthly" to "not offered on your billing period". See 15-internal-membership.md § Subscriber Self-Service and Buy-ups Subscriber REST API for the fields and error code this class drives.