Products and prices
A Product is a catalog entry. A Price is a versioned way to charge for it. A product can have many prices, different currencies, different purposes (recurring, setup, renewal), different points in time.
Products
use Meteric\Models\Product;
use Meteric\Enums\PricingModel;
$product = Product::create([
'type' => 'vps', // your category, free-form
'slug' => 'vps-xl',
'name' => 'VPS XL',
'pricing_model' => PricingModel::Fixed,
'is_proratable' => true,
'config' => ['downgrade' => 'defer'], // optional per-product downgrade policy
]);pricing_model is one of fixed, per_unit, tiered, volume, metered, hourly, one_off. metered and hourly are usage-based, isMetered() returns true for those.
Product config
The config array holds product-level settings. Two keys are read by the package:
config['downgrade']sets the default downgrade policy; it falls back todefer. Read it withdowngradePolicy().config['cancel_notice_days']is the notice required before a contract ends, in days; it falls back to0. Read it withcancelNoticeDays(). See cancellation.
Both keys are validated on write. config['downgrade'] must be a valid DowngradePolicy value (defer, discard, credit, refund) and config['cancel_notice_days'] a non-negative integer, or the assignment throws InvalidArgumentException. Any other key, a provisioner name or another host setting of your own, passes through untouched.
$product->config = ['downgrade' => 'nope']; // throws InvalidArgumentException
$product->config = ['provisioner' => 'virtfusion', 'cancel_notice_days' => 30]; // finePrices
use Meteric\Models\Price;
use Meteric\Enums\{PricingModel, Interval, BillingMode, PricePurpose};
$price = Price::create([
'product_id' => $product->id,
'currency' => 'EUR',
'amount_minor' => 1000, // €10.00
'purpose' => PricePurpose::Recurring,
'pricing_model' => PricingModel::Fixed,
'interval' => Interval::Month,
'interval_count' => 1,
'billing_mode' => BillingMode::InAdvance,
'setup_fee_minor' => 0,
]);amount is a Money accessor over amount_minor + currency. Read it back as money rather than touching the integer:
$price->amount; // Money €10.00
$price->setupFee(); // Money (0 if no setup fee)
$price->isRecurring(); // bool, false for one-off prices
$price->hasSetupFee(); // boolBilling mode
billing_mode is in_advance (prepaid, charged at period start) or in_arrears (postpaid, charged at period end). Usage and hourly prices bill in arrears regardless. An item can override the price's mode; otherwise the price's mode wins, falling back to in_advance.
Price purposes
purpose lets one product carry separate prices for different events: recurring, setup, register, renew, transfer, addon, option. Domain billing uses this, a register price and a renew price on the same product.
use Meteric\Enums\PricePurpose;
// The current recurring price for a currency.
$price = $product->priceFor('EUR');
// A different purpose.
$renew = $product->priceFor('EUR', PricePurpose::Renew);priceFor() returns the latest price with no valid_to for that currency and purpose, so superseding a price is a matter of inserting a new row and closing the old one with valid_to.
Per-unit and sub-cent rates
For per-unit, metered, and hourly pricing, set unit_rate instead of (or alongside) amount_minor. It is a high-precision numeric string, so you can price below a cent per unit without float drift.
$price = Price::create([
'product_id' => $product->id,
'currency' => 'EUR',
'unit_rate' => '0.00004200', // €0.000042 per unit
'purpose' => \Meteric\Enums\PricePurpose::Recurring,
'pricing_model' => \Meteric\Enums\PricingModel::PerUnit,
]);
$price->amountFor(100000); // Money, round(qty × unit_rate)amountFor($qty) multiplies by unit_rate when set, otherwise by the flat amount. Usage caps and allowances live on the meter dimension.
A price also carries the usage-style knobs included_qty (free allowance), block_size (bill per started block of N units), cap_minor, and min_charge_minor. amountForQuantity($qty) applies those on top of amountFor; options and addons bill through it.
Quantity discounts (tiers)
To make a quantity cheaper as it grows, set the tiers table and a tiered pricing model. A tier is { up_to, unit_minor }, ordered low to high, where up_to: null is the last, unbounded tier.
$price = Price::create([
'product_id' => $product->id,
'currency' => 'EUR',
'pricing_model' => PricingModel::Volume, // or Tiered
'tiers' => [
['up_to' => 10, 'unit_minor' => 500], // 1 to 10 at €5
['up_to' => 50, 'unit_minor' => 400], // 11 to 50 at €4
['up_to' => null, 'unit_minor' => 300], // 51+ at €3
],
]);Two models, picked by pricing_model:
Volume: the whole quantity is priced at the tier it lands in. 60 units bills60 × €3 = €180. This is the usual "the more you buy, the cheaper" deal.Tiered: each slice is priced at its own tier, then summed. 60 units bills10 × €5 + 40 × €4 + 10 × €3 = €240.
This runs through amountFor(), so it applies anywhere a quantity is priced: base items, configurable options (slots, extra IPs), and addons.
See also: Build a web hosting company's billing for a full catalog (plans, setup fees, domains, addons, volume-priced IPs).