Skip to content

Author a tiered plan

Author a tiered plan

Plans, prices, tiers, entitlements, and credit grants are ordinary models. The CatalogSeeder is the worked reference — model your own plan on it. This recipe shows the shape.

The models

  • Plan{key, product_id, name, interval, active}.
  • PlanPrice — one per currency: {plan_id, currency, price_minor, pricing_model, package_size?}.
  • PlanPriceTier — the per-tier schedule for tiered models: {plan_price_id, up_to, unit_minor, flat_minor?, sort_order}.
  • PlanEntitlement — one per meter: {plan_id, meter_id, enabled, allowance, multiplier, unlimited, overage}.
  • PlanCreditGrant — recurring included credits: {plan_id, pool, kind, cadence, amount, denomination}.

A graduated plan (seeder-style)

$plan = Plan::query()->create([
    'product_id' => $product->id, 'key' => 'growth',
    'name' => 'Growth', 'interval' => 'month', 'active' => true,
]);

// List price per currency.
foreach (['DKK' => 149_000, 'EUR' => 19_900, 'USD' => 21_900] as $ccy => $minor) {
    PlanPrice::query()->create([
        'plan_id' => $plan->id, 'currency' => $ccy,
        'price_minor' => $minor, 'pricing_model' => 'graduated',
    ]);
}

// Recurring included credits (the enforceable allowance pool).
PlanCreditGrant::query()->create([
    'plan_id' => $plan->id, 'pool' => Pools::INCLUDED,
    'kind' => GrantKind::Base, 'cadence' => GrantCadence::Monthly,
    'amount' => 300_000, 'denomination' => 'credit',
]);

The tier schedules

Set pricing_model on the PlanPrice, then add PlanPriceTier rows. The base price_minor stays the list amount MRR sums; the tiers are the per-seat schedule it scales by. up_to is the inclusive bound (null = ∞).

Model Tier field used Meaning
graduated unit_minor Each seat slice at its own tier's rate.
volume unit_minor Every seat at the single tier the total lands in.
package flat_minor + package_size A block price per pack.
stairstep flat_minor One flat price for the whole bracket.
foreach ([
    ['up_to' => 10,   'unit_minor' => 0],
    ['up_to' => 50,   'unit_minor' => 1_300],
    ['up_to' => null, 'unit_minor' => 1_050],
] as $order => $tier) {
    PlanPriceTier::query()->create([
        'plan_price_id' => $price->id, 'sort_order' => $order,
    ] + $tier);
}

Per-meter entitlements

PlanEntitlement::query()->create([
    'plan_id' => $plan->id, 'meter_id' => $meters['api.requests']->id,
    'enabled' => true, 'allowance' => 2_000_000,
    'multiplier' => 0.0003, 'unlimited' => false,
    'overage' => OverageBehaviour::Bill,   // or Block for a hard limit
]);

overage = Bill charges past the allowance; Block hard-stops. Set unlimited = true for no ceiling. See Catalog & pricing.

Verify in the console

Catalog → Plans & pricing renders the plan, its per-currency prices, and the tier table.