Custom Pricing Models
v1.1
ChargePanda's pricing model system lets your module add new billing strategies without touching core code. Every pricing model has two sides: a PHP handler (server logic) and a JS handler (product page UI).
How it works
- The core ships one model:
fixed(flat rate). - Your module implements
PricingModelContractand registers it inboot(). - Admin selects the model on a pricing tier in the Product Manager.
- Your JS handler is injected on the product page to update the UI.
- At checkout, your PHP handler computes the price and cart metadata.
Step 1 — Implement PricingModelContract
php
<?php
namespace Modules\MyPlugin\PricingModels;
use App\Contracts\PricingModelContract;
use App\Models\PricingTier;
class SubscriptionBoxPricingModel implements PricingModelContract
{
// Unique string key stored in pricing_tiers.pricing_model
public function key(): string { return 'subscription_box'; }
// Used in log messages and internal references
public function label(): string { return 'Subscription Box'; }
// -----------------------------------------------------------------
// Price calculation
// -----------------------------------------------------------------
// Total price for a given quantity at checkout
public function calculatePrice(PricingTier $tier, int $quantity): float
{
return (float) $tier->price * $quantity;
}
// -----------------------------------------------------------------
// Cart integration
// -----------------------------------------------------------------
// How many units to add to the cart
public function cartQty(PricingTier $tier, int $requestedQty): int
{
return max(1, $requestedQty);
}
// Unit price stored on the cart item
public function cartUnitPrice(PricingTier $tier, bool $trialAvailable): float
{
return $trialAvailable ? 0.0 : (float) $tier->price;
}
// Price label shown in the cart
public function cartPriceLabel(PricingTier $tier, bool $trialAvailable, ?string $billingCycleLabel): string
{
return ch_format_price((float) $tier->price) . ' / box / ' . $billingCycleLabel;
}
// Extra options merged into the cart item (returned as key-value array)
public function extraCartOptions(PricingTier $tier, int $qty): array
{
return [];
}
// -----------------------------------------------------------------
// Admin Product Manager UI metadata
// -----------------------------------------------------------------
// Label shown in the pricing model dropdown
public function adminLabel(): string { return 'Subscription Box — flat rate per box'; }
// Badge shown on the plan row in the product manager
public function badgeLabel(): string { return 'Box'; }
// Whether to show the per-unit amount field in admin
public function usesPerUnitAmount(): bool { return false; }
// Label for the per-unit amount field (if usesPerUnitAmount = true)
public function perUnitLabel(): string { return ''; }
// Suffix shown next to the per-unit amount field
public function perUnitSuffix(): string { return ''; }
// Help text under the per-unit amount field
public function perUnitHelp(): string { return ''; }
// -----------------------------------------------------------------
// Checkout page display
// -----------------------------------------------------------------
// Whether to display $0.00 at checkout (for usage-based models)
public function checkoutDisplayZero(): bool { return false; }
// Suffix appended to the price on the checkout page
public function checkoutSuffix(float $perUnitAmount, ?string $billingCycleLabel): string
{
return '/ box';
}
}Step 2 — Register in boot()
php
use Modules\MyPlugin\PricingModels\SubscriptionBoxPricingModel;
public function boot(): void
{
$this->registerPricingModel(new SubscriptionBoxPricingModel());
}That's it for the PHP side. The model now appears in the admin pricing model dropdown and the checkout engine uses your handler automatically.
Step 3 — JS handler
The product page uses a JS SDK (window.CP) to update the UI when the user interacts with pricing options. Register a handler for your model's key:
js
// public/modules/my-plugin/js/pricing-models.js
CP.registerPricingModel('subscription_box', {
// Called when this model's tier is selected.
// Return the display price for the product page.
getDisplayPrice: function (tier) {
var price = parseFloat(tier.price || 0);
return price === 0 ? 'Free' : CP.formatPrice(price);
},
// Return the price label shown below the main price.
getPriceLabel: function (tier) {
return '/ box / ' + (tier.billing_cycle_label || '');
},
// Return extra HTML to inject below the price (e.g. seat input).
// Return empty string if nothing extra is needed.
getExtraHtml: function (tier) {
return '';
},
// Return the quantity to send to the server when adding to cart.
getCartQty: function (tier) {
return 1;
},
});Register the JS file in boot():
php
$this->registerFrontendScript(asset('modules/my-plugin/js/pricing-models.js'));The script is injected on the product page after the core SDK, so window.CP is available.
Resolving models at runtime
If your module code needs to call a pricing model handler directly:
php
use App\Support\PricingModelRegistry;
$handler = app(PricingModelRegistry::class)->resolve('subscription_box');
$price = $handler->calculatePrice($tier, $qty);Check if a model is registered:
php
app(PricingModelRegistry::class)->has('subscription_box'); // boolList all registered models:
php
app(PricingModelRegistry::class)->all(); // Collection of PricingModelContract