Skip to content

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

  1. The core ships one model: fixed (flat rate).
  2. Your module implements PricingModelContract and registers it in boot().
  3. Admin selects the model on a pricing tier in the Product Manager.
  4. Your JS handler is injected on the product page to update the UI.
  5. 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'); // bool

List all registered models:

php
app(PricingModelRegistry::class)->all(); // Collection of PricingModelContract

Released under the Commercial License.