Skip to main content

BuyupAutoGrant

\Benecaster\Membership\BuyupAutoGrant

Class Premium

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.