Skip to main content

Plan Items and Add-ons

PlanItem is the structured way to attach priced, quantified add-ons to a plan. Use it whenever a feature has a price, an included quantity, or an overage behaviour — not just a boolean flag.

When to use PlanItem vs features JSON

Use caseUse
Boolean feature flag (exports: true)Plan.features JSON column
Simple integer limit (projects: 10)PlanItem — migrate from features JSON for entitlement resolution via included_quantity
Priced add-on with overage billingPlanItem with unit_price and cap_behavior
Metered usage charged at period endPlanItem with pricing_rule = 'metered'
Booleans stay in features. Anything with a price or quantity goes in PlanItem.

Creating plan items

use Pixelworxio\Subscribd\Models\Plan;
use Pixelworxio\Subscribd\Models\PlanItem;
use Pixelworxio\Subscribd\Enums\CapBehavior;

$pro = Plan::where('key', 'pro')->firstOrFail();

// 3 projects included; $10/mo per additional project; hard cap at 50
PlanItem::create([
    'plan_id'           => $pro->id,
    'key'               => 'projects',
    'name'              => 'Projects',
    'included_quantity' => 3,
    'unit_price'        => 1000,        // $10.00 in minor units (cents)
    'cap_behavior'      => CapBehavior::ChargeUntilCeiling,
    'ceiling'           => 50,
    'sort_order'        => 0,
    'active'            => true,
]);

// 5 team seats included; no overage allowed
PlanItem::create([
    'plan_id'           => $pro->id,
    'key'               => 'team_seats',
    'name'              => 'Team Seats',
    'included_quantity' => 5,
    'unit_price'        => 0,
    'cap_behavior'      => CapBehavior::Block,
    'sort_order'        => 1,
    'active'            => true,
]);

CapBehavior enum

ValueBehaviour
BlockAccess denied when quantity exceeds included_quantity. No billing.
ChargeBill at unit_price per unit beyond included_quantity, with no ceiling.
ChargeUntilCeilingBill at unit_price per unit up to ceiling. Hard block beyond ceiling.

SubscriptionItem — per-subscriber state

When a subscriber has a plan with PlanItems, a SubscriptionItem record is created per item at subscription time. SubscriptionItem tracks:
  • quantity — how many units the subscriber is currently using
  • price_override / price_override_expires_at — admin-set per-subscriber price
  • gateway_item_id — the gateway’s item ID for native gateways (Stripe)
// Get a subscriber's current project count
$subscription = $user->subscription();
$item = $subscription->items()->where('key', 'projects')->firstOrFail();
$item->quantity;          // int — current usage
$item->price_override;    // int|null — cents, null if no override

Updating quantities

use Pixelworxio\Subscribd\Actions\UpdateQuantity;

$item = $user->subscription()->items()->where('key', 'projects')->firstOrFail();
app(UpdateQuantity::class)->execute($user->subscription(), quantity: 7, planItemKey: 'projects');
For gateways that manage items natively (Stripe), this also updates the subscription item at the gateway.

Entitlement checks

Entitlements resolves plan items automatically:
use Pixelworxio\Subscribd\Facades\Entitlements;

// How many projects can this user have?
$limit = Entitlements::for($user)->limit('projects');   // int|null (null = unlimited)

// Can they create another project?
$canCreate = $limit === null || ($user->projects()->count() < $limit);
Resolution order:
  1. Check Plan.features[$key] — if a value is set here, use it.
  2. Check PlanItem by key — use included_quantity as the limit.

Price overrides

Apply a per-subscriber price override to any SubscriptionItem:
use Pixelworxio\Subscribd\Actions\SetPriceOverride;
use Carbon\CarbonImmutable;

$item = $user->subscription()->items()->where('key', 'projects')->firstOrFail();

// Permanent override
app(SetPriceOverride::class)->execute(
    subscription: $user->subscription(),
    item: $item,
    price: 500,        // $5.00 per additional project (was $10.00)
    expiresAt: null,
);

// Time-limited override (reverts automatically at renewal)
app(SetPriceOverride::class)->execute(
    subscription: $user->subscription(),
    item: $item,
    price: 0,
    expiresAt: CarbonImmutable::parse('2026-12-31'),
);
When an override’s expires_at passes at renewal, RenewSubscription fires SubscriptionPriceOverrideReverted and reverts to price_snapshot.

Overage billing at renewal

At renewal, RenewSubscription automatically charges overages for items with CapBehavior::Charge or CapBehavior::ChargeUntilCeiling:
overage_units  = max(0, subscription_item.quantity - plan_item.included_quantity)
overage_amount = overage_units × effective_unit_price
effective_unit_price is SubscriptionItem.price_override when active, otherwise PlanItem.unit_price. For gateways with native subscription item support (Stripe), overage is reported via the gateway’s items API and included in the gateway’s renewal invoice. For other gateways, subscribd issues a separate charge() call.

Next steps