Skip to main content

Pricing System

Unchained Engine uses a chain-of-responsibility pattern for pricing calculations. Multiple pricing adapters execute in sequence, each adding, modifying, or discounting prices.

Overview

Prices are calculated at multiple levels:

DirectorPurpose
ProductPricingDirectorBase product price, taxes, product-level discounts
DeliveryPricingDirectorShipping and handling fees
PaymentPricingDirectorPayment processing fees
OrderPricingDirectorCombines all pricing, applies order-level discounts

Pricing Chain

Adapters execute in order of their orderIndex (ascending). Lower numbers run first.

Each adapter:

  1. Receives the current calculation state
  2. Can add items to the calculation
  3. Passes control to the next adapter via super.calculate()

Order Index Guidelines

RangePurposeExamples
0-9Base price calculationCatalog price, ERP integration
10-19DiscountsMember discounts, bulk pricing
20-29Tax calculationVAT, sales tax
30+Final adjustmentsRounding, currency conversion

Pricing Categories

CategoryDescriptionTypical Use
BASEBase product/service priceInitial price calculation
DISCOUNTPrice reduction (negative amount)Coupons, promotions
TAXTax amountVAT, sales tax
DELIVERYShipping feesDelivery pricing
PAYMENTPayment processing feesCard fees, invoice fees

Price Item Properties

When adding items to the calculation, each item has:

PropertyTypeDescription
amountnumberPrice in smallest currency unit (cents)
isTaxablebooleanShould tax be calculated on this amount?
isNetPricebooleanIs this a net price (excluding tax)?
categorystringPrice category (BASE, TAX, DISCOUNT, etc.)
metaobjectAdditional metadata

Pricing Sheet

Access calculated prices via the pricing sheet:

const pricingSheet = await modules.orders.pricingSheet(order);

// Get totals
const total = pricingSheet.total(); // { amount, currency }
const gross = pricingSheet.gross(); // Before discounts
const net = pricingSheet.net(); // After discounts, before tax
const taxes = pricingSheet.taxes(); // Tax breakdown

// Get items by category
const discounts = pricingSheet.discounts();
const delivery = pricingSheet.delivery();
const payment = pricingSheet.payment();

// Sum specific items
const taxableAmount = pricingSheet.sum({ isTaxable: true });
const baseAmount = pricingSheet.sum({ category: 'BASE' });

GraphQL Price Fields

Query product prices:

query ProductPrice($productId: ID!) {
product(productId: $productId) {
... on SimpleProduct {
simulatedPrice(currencyCode: "CHF", quantity: 1) {
amount
currencyCode
isTaxable
isNetPrice
}
}
}
}

Query cart pricing:

query CartPricing {
me {
cart {
total {
amount
currencyCode
}
items {
total {
amount
currencyCode
}
}
delivery {
fee {
amount
currencyCode
}
}
payment {
fee {
amount
currencyCode
}
}
discounts {
total {
amount
}
code
}
}
}
}

Best Practices

1. Always Call super.calculate()

async calculate() {
// Your logic here

// IMPORTANT: Continue the chain
return super.calculate();
}

Returning without calling super.calculate() stops the pricing chain.

2. Handle Currency Properly

Always work in smallest currency units (cents) to avoid floating-point errors:

// Good
const amount = 1999; // $19.99 in cents

// Bad
const amount = 19.99; // Floating point issues

3. Include Metadata

Add metadata for debugging and reporting:

this.result.addItem({
amount: 100,
category: 'TAX',
meta: {
adapter: this.constructor.key,
rate: 0.081,
},
});