BuyupAutoGrant
\Benecaster\Membership\BuyupAutoGrant
Gives a buy-up to subscribers when it is priced at 0 on their billing period. A $0 price row is the “upgrade to annual and get transcripts” offer: nobody clicks anything, so nothing in the purchase path could ever create the grant. This class does it instead.
There are four ways a free buy-up reaches someone, and this class covers two of them directly. The backfill (backfill()) runs when a price row becomes free, and grants the buy-up to every active subscriber already on that exact cadence and currency except anyone who has declined it (BuyupDeclineRepository). It grants only rows held by FreeBuyupBackfillQueue, so saving the buy-up again for any other reason grants nothing. The sign-up grant (grant_at_signup()) runs when someone joins on that period. The other two, a billing-period switch into the period and out of it, are handled by Benecaster’s plan-change handling, which subscribers reach from their account page. The three that give the buy-up (backfill, sign-up, and a switch onto the period) fire benecaster_buyup_auto_granted, never benecaster_buyup_purchased, because nobody bought anything. So does a subscriber adding a free buy-up back from their account page, which is handled by the account endpoint rather than this class. A switch off the period drops the buy-up, and Benecaster ends the grant when Stripe confirms it, firing benecaster_buyup_revoked.
Every grant goes through the same per-buy-up lock a purchase takes. That is not to stop two backfills racing each other. It is because a backfill and a concurrent purchase both count and then insert. The backfill takes the lock once for the whole sweep, since the sweep is the unit whose count must stay true. A cap and a free price work together: “the first 50 to go annual get transcripts free” stops granting at fifty, and everyone already holding it keeps it.
A subscriber is matched on the tier price row their subscription points at, never on billing_interval, which stores only the unit. A quarterly subscriber reads month there and would be matched to a monthly offer. A buy-up outside its sale window is skipped, so a free buy-up obeys “this month only” too. One subscriber’s Stripe failure is logged and skipped and does not abandon the rest of the sweep. Nothing here revokes. When a price moves away from 0, existing holders stay where they are under the Keep default.
Constructor Dependencies
| Type | Description |
|---|---|
\Benecaster\Membership\BuyupRepository |
Reads the buy-up, its sale window and its active status. |
\Benecaster\Membership\BuyupPriceRepository |
Finds the buy-up's $0 price rows. |
\Benecaster\Membership\SubscriberBuyupRepository |
Skips subscribers who already hold the buy-up, counts holders against the cap, and writes the grant. |
\Benecaster\Membership\SubscriptionRepository |
Lists the show's active subscriptions to match against the free row. |
\Benecaster\Membership\MembershipPriceRepository |
Reads each subscription's tier price row for its cadence and currency. |
\Benecaster\Membership\BuyupCapLock |
The per-buy-up advisory lock shared with purchases. |
\Benecaster\Payment\StripeSubscriptionManager |
Adds the $0 item to each subscriber's Stripe subscription. |
\Benecaster\Payment\StripeClient |
Resolves the keyset for the show. |
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
register(): void |
Public | — | Adds backfill() on benecaster_buyup_prices_provisioned, not on the definition hooks, because a grant needs a Stripe Price to name and the definition hooks fire before the provisioner has minted one. |
backfill( int $buyup_id, string $mode ): void |
Public | — | For each $0 price row on the buy-up, finds the show's active subscribers on that exact cadence and currency in the given keyset, adds the buy-up's $0 Stripe Price to their subscription and writes the grant. Skips anyone who already holds the buy-up, so a re-save grants nobody twice. Stops at the subscriber cap. Does nothing for an inactive buy-up or one outside its sale window. |
grant_at_signup( int $user_id, int $show_id, string $tier_slug, array $tier_price, string $stripe_subscription_id, array $declined, array $period ): void |
Public | — | Called by the sign-up endpoint once the new subscription exists. Grants each free buy-up on the price row the person signed up on, except those in $declined, the ones they unticked on the form. The form shows free buy-ups already ticked, so a caller that cannot collect that answer must not call this. Uses the same cap lock, free-rows-only rule and action as the backfill. |
free_buyups_for( int $show_id, string $tier_slug, array $tier_price ): array |
Public | — | The show's buy-ups that cost nothing on a given tier price row, each as { buyup_id, name, stripe_price_id, subscriber_cap }. Free rows only: adding a paid item nobody asked for would be charging them for signing up. |
match_free_buyups( array $buyups, array $tier_price, string $col ): array |
Public | — | Static. The matching rule with nothing fetched. The sign-up form uses it to decide which buy-ups to show as included, and the sign-up grant uses it to decide which to give. It is one function with two callers so the two answers can never disagree. Skips a buy-up outside its sale window. |
Hooks Fired
Notes
To react to a free grant, hook benecaster_buyup_auto_granted, not benecaster_buyup_purchased. A welcome email thanking a buyer, or anything counting revenue, should not run for someone who was given the buy-up. A step that provisions access probably should run. See the recipe React to a free buy-up grant.