Skip to main content

Product Pricing

Product pricing adapters calculate prices when products are queried or added to cart. Use them to implement taxes, discounts, rounding, and currency conversion.

For conceptual overview, see Pricing System.

Creating an Adapter

Extend ProductPricingAdapter and register it with ProductPricingDirector:

import {
ProductPricingAdapter,
ProductPricingDirector,
} from '@unchainedshop/core-pricing';

class MyProductPricing extends ProductPricingAdapter {
static key = 'my-shop.pricing.custom';
static version = '1.0.0';
static label = 'Custom Product Pricing';
static orderIndex = 0;

static isActivatedFor({ product, currencyCode }) {
return true; // Activate for all products
}

async calculate() {
const { product, quantity, currencyCode } = this.context;

this.result.addItem({
amount: 1000, // 10.00 in cents
isTaxable: true,
isNetPrice: true,
category: 'BASE',
meta: { adapter: this.constructor.key },
});

return super.calculate();
}
}

ProductPricingDirector.registerAdapter(MyProductPricing);

Examples

Tax Calculation

class SwissTaxAdapter extends ProductPricingAdapter {
static key = 'my-shop.pricing.swiss-tax';
static orderIndex = 20; // After base price and discounts

static isActivatedFor({ country }) {
return country === 'CH';
}

async calculate() {
const taxRate = 0.081; // 8.1% Swiss VAT
const taxableAmount = this.calculation.sum({ isTaxable: true });

if (taxableAmount > 0) {
this.result.addItem({
amount: Math.round(taxableAmount * taxRate),
isTaxable: false,
isNetPrice: false,
category: 'TAX',
meta: { rate: taxRate, adapter: this.constructor.key },
});
}

return super.calculate();
}
}

Bulk Discount

class BulkDiscountAdapter extends ProductPricingAdapter {
static key = 'my-shop.pricing.bulk-discount';
static orderIndex = 10; // After base price, before tax

async calculate() {
const { quantity } = this.context;

if (quantity >= 10) {
const baseTotal = this.calculation.sum({ category: 'BASE' });
const discountRate = 0.1; // 10% off

this.result.addItem({
amount: -Math.round(baseTotal * discountRate),
isTaxable: true,
isNetPrice: true,
category: 'DISCOUNT',
meta: { type: 'bulk', rate: discountRate },
});
}

return super.calculate();
}
}

Price Rounding

class PriceRoundingAdapter extends ProductPricingAdapter {
static key = 'my-shop.pricing.rounding';
static orderIndex = 30; // Run last

async calculate() {
const { calculation = [] } = this;

if (calculation.length) {
const [basePrice] = calculation;
const rounded = this.roundToNext(basePrice.amount, 50);

this.resetCalculation();
this.result.addItem({
amount: rounded,
isTaxable: basePrice.isTaxable,
isNetPrice: basePrice.isNetPrice,
meta: { adapter: this.constructor.key },
});
}

return super.calculate();
}

roundToNext(value: number, precision: number) {
const remainder = value % precision;
return remainder === 0 ? value : value + (precision - remainder);
}
}

Currency Conversion

class CurrencyConversionAdapter extends ProductPricingAdapter {
static key = 'my-shop.pricing.currency';
static orderIndex = 1;

async calculate() {
const { currencyCode, baseCurrencyCode } = this.context;

if (currencyCode !== baseCurrencyCode) {
const rate = await this.getExchangeRate(baseCurrencyCode, currencyCode);

for (const item of this.calculation) {
item.amount = Math.round(item.amount * rate);
}
}

return super.calculate();
}

async getExchangeRate(from: string, to: string) {
// Fetch from your exchange rate service
return 1.1;
}
}

Adapter Properties

PropertyTypeDescription
keystringUnique identifier
versionstringVersion for tracking
labelstringHuman-readable name
orderIndexnumberExecution order (lower = earlier)

Context Properties

Available in this.context:

PropertyDescription
productThe product being priced
quantityQuantity requested
currencyCodeTarget currency
countryCountry code