order-process-engine
v2.0.1
Published
A lightweight engine for e-commerce order processing.
Maintainers
Readme
order-process-engine
A lightweight TypeScript/JavaScript engine for calculating e-commerce order totals.
It helps you compute:
- Subtotal
- Marketplace fee
- Shipping charges (with free-shipping threshold)
- Tax
- Final total
The package can be used in two ways:
- As a class (
OrderEngine) with configurable defaults - As standalone utility functions (
calcMarketplaceFee,calcShipping,calcTax,roundToTwo)
Table of Contents
- Why this package
- Features
- Installation
- Using in other projects
- Quick start
- How calculations work
- API reference
- Configuration
- Examples
- Project scripts
- Project structure
- Development notes
- Troubleshooting
- License
Why this package
Order totals can get repetitive and error-prone when fee/tax/shipping logic is spread across multiple places.
This package centralizes that logic in a tiny, typed module so you can:
- Keep calculations consistent
- Reuse logic across backend/frontend services
- Get IDE autocomplete and compile-time checks
Features
- TypeScript-first API with exported types
- Configurable fee, shipping, and tax values
- Free-shipping threshold logic
- Utility exports if you do not need a class
- Builds for both CommonJS and ESM with type declarations
- Dynamic pricing rules for promotions, shipping, fee, and tax
- Context-aware pricing using region/channel/customer metadata
- Optional async rule and tax providers for DB/API-backed pricing
- Explainable outputs with applied rules and pricing breakdown
Installation
npm install order-process-engineIf you are developing this repository locally:
npm install
npm run buildUsing in other projects
Use OrderEngine and choose one of these patterns:
1) Default configuration (18% tax + default fees/shipping)
import { OrderEngine, type OrderItem } from "order-process-engine";
const items: OrderItem[] = [
{ price: 40, quantity: 2 },
{ price: 20, quantity: 1 },
];
const engine = new OrderEngine();
const result = engine.calculate(items);
console.log(result);
/*
{
subtotal: 100,
marketplaceFee: 10.5,
shippingCharges: 0,
tax: 18,
total: 118
}
*/2) Custom configuration (your own tax/fees/shipping)
import { OrderEngine, type OrderItem } from "order-process-engine";
const items: OrderItem[] = [
{ price: 1200, quantity: 1 },
{ price: 250, quantity: 2 },
];
const engine = new OrderEngine({
tax: 0.12, // 12%
mktFeePercent: 0.08, // 8%
mktFeeFixed: 2.5,
shippingBase: 40,
shippingThreshold: 2000,
});
const result = engine.calculate(items);
console.log(result);JavaScript (CommonJS) usage is the same concept:
const { OrderEngine } = require("order-process-engine");
const engine = new OrderEngine({
tax: 0.07,
mktFeePercent: 0.1,
mktFeeFixed: 1,
});
const result = engine.calculate([{ price: 300, quantity: 2 }]);
console.log(result);3) Dynamic pricing intelligence (rules + context)
import {
OrderEngine,
type OrderItem,
type PricingContext,
type PricingRuleSet,
} from "order-process-engine";
const items: OrderItem[] = [
{ price: 1200, quantity: 1, category: "electronics", weight: 1.2 },
{ price: 250, quantity: 2, category: "books", weight: 0.4 },
];
const ruleSet: PricingRuleSet = {
promotions: [
{
id: "vip-10",
name: "VIP 10% Off",
type: "promotion",
scope: "order",
discountMode: "percent",
discountValue: 0.1,
priority: 10,
when: [{ field: "context.customerTier", op: "eq", value: "vip" }],
},
],
shipping: [
{
id: "express-app-flat",
name: "App Channel Shipping",
type: "shipping",
mode: "flat",
flatAmount: 25,
priority: 10,
when: [{ field: "context.channel", op: "eq", value: "app" }],
},
],
tax: [
{
id: "in-default-tax",
name: "India GST",
type: "tax",
rate: 0.18,
priority: 10,
when: [{ field: "context.region", op: "eq", value: "IN" }],
},
],
};
const context: PricingContext = {
tenantId: "store-42",
region: "IN",
channel: "app",
customerTier: "vip",
couponCodes: ["SUMMER10"],
};
const engine = new OrderEngine({ ruleSet });
const result = engine.calculate(items, context);
console.log(result.appliedRules);
console.log(result.breakdown);Quick start
Class-based usage
import { OrderEngine, type OrderItem } from "order-process-engine";
const items: OrderItem[] = [
{ price: 29.99, quantity: 2 },
{ price: 15.5, quantity: 1 },
];
const engine = new OrderEngine();
const result = engine.calculate(items);
console.log(result);
/*
{
subtotal: 100,
marketplaceFee: 10.5,
shippingCharges: 0,
tax: 18,
total: 118
}
*/Utility-function usage
import {
calcMarketplaceFee,
calcShipping,
calcTax,
roundToTwo,
} from "order-process-engine";
const subtotal = 75.48;
const fee = calcMarketplaceFee(subtotal, 0.1, 0.5);
const shipping = calcShipping(subtotal, 10, 100);
const tax = calcTax(subtotal, 0.18);
const total = roundToTwo(subtotal + shipping + tax);How calculations work
Given items:
[{ price, quantity }, ...]The engine computes:
subtotal = sum(price * quantity)marketplaceFee = subtotal * feePercent + feeFixedshippingCharges = subtotal >= freeShippingThreshold ? 0 : shippingBasetax = roundToTwo(subtotal * taxRate)total = subtotal + shippingCharges + tax
With pricing intelligence enabled, the engine uses this staged flow:
subtotal = sum(price * quantity)- Apply promotions (order/item scope + conditions + priority)
taxableSubtotal = subtotal - discountTotal- Resolve shipping from matching shipping rule (or fallback)
- Resolve marketplace fee from matching fee rule (or fallback)
- Resolve tax from tax rule or
taxProvider total = taxableSubtotal + shippingCharges + tax
API reference
OrderEngine
Constructor
new OrderEngine(customConfig?)customConfig is optional and can override any of:
v1 static config:
mktFeePercent(number)mktFeeFixed(number)shippingBase(number)shippingThreshold(number)tax(number)
v2 pricing intelligence config:
ruleSet(PricingRuleSet)ruleSetProvider((context) => PricingRuleSet | Promise<PricingRuleSet>)taxProvider((taxableSubtotal, context) => number | Promise<number>)cacheTtlMs(reserved for provider-layer cache strategies)
Method
calculate(items: OrderItem[]): CalculationResult
calculate(items: OrderItem[], context: PricingContext): CalculationResult
calculateAsync(
items: OrderItem[],
context?: PricingContext,
): Promise<CalculationResult>Notes:
calculate(...)supports static rules and synchronous providers.- If your provider returns a
Promise, usecalculateAsync(...).
OrderItem
interface OrderItem {
price: number;
quantity: number;
weight?: number;
category?: string;
}Notes:
weightandcategoryare currently optional metadata fields and are not used in core calculations.
CalculationResult
interface CalculationResult {
subtotal: number;
marketplaceFee: number;
shippingCharges: number;
tax: number;
total: number;
discountTotal?: number;
appliedRules?: AppliedRule[];
breakdown?: {
discountTotal: number;
taxableSubtotal: number;
};
}PricingContext
interface PricingContext {
tenantId?: string;
region?: string;
postalCode?: string;
channel?: "web" | "app" | "marketplace";
currency?: string;
customerId?: string;
customerTier?: "new" | "regular" | "vip";
couponCodes?: string[];
now?: Date | string;
}PricingRuleSet
interface PricingRuleSet {
promotions?: PromotionRule[];
shipping?: ShippingRule[];
marketplaceFee?: MarketplaceFeeRule[];
tax?: TaxRule[];
}calcMarketplaceFee(price, percent, fixed)
Returns:
price * percent + fixed;calcShipping(total, base, threshold)
Returns:
0iftotal >= thresholdbaseotherwise
calcTax(subtotal, taxRate)
Returns tax rounded to 2 decimals:
roundToTwo(subtotal * taxRate);roundToTwo(num)
Rounding helper:
Math.round((num + Number.EPSILON) * 100) / 100;Configuration
Default runtime configuration used by OrderEngine:
| Key | Default | Meaning |
| ------------------- | ------- | ------------------------------------- |
| mktFeePercent | 0.1 | 10% variable marketplace fee |
| mktFeeFixed | 0.5 | Fixed fee per order |
| shippingBase | 10 | Flat shipping under threshold |
| shippingThreshold | 100 | Free shipping starts at this subtotal |
| tax | 0.18 | 18% tax |
You can override any of these:
import { OrderEngine } from "order-process-engine";
const engine = new OrderEngine({
mktFeePercent: 0.12,
mktFeeFixed: 1.0,
shippingBase: 7.5,
shippingThreshold: 80,
tax: 0.07,
});Examples
Example 1: Free shipping case
import { OrderEngine } from "order-process-engine";
const engine = new OrderEngine();
const result = engine.calculate([
{ price: 40, quantity: 2 },
{ price: 30, quantity: 1 },
]);
// subtotal = 110
// shippingCharges = 0 (because subtotal >= 100)
console.log(result);Example 2: Custom tax and shipping policy
import { OrderEngine } from "order-process-engine";
const engine = new OrderEngine({
shippingBase: 5,
shippingThreshold: 50,
tax: 0.0825,
});
const result = engine.calculate([{ price: 20, quantity: 2 }]);
console.log(result);Example 3: Utility-only integration
import {
calcMarketplaceFee,
calcShipping,
calcTax,
roundToTwo,
} from "order-process-engine";
function computeOrder(subtotal: number) {
const fee = calcMarketplaceFee(subtotal, 0.1, 0.5);
const shipping = calcShipping(subtotal, 10, 100);
const tax = calcTax(subtotal, 0.18);
return {
subtotal,
fee,
shipping,
tax,
total: roundToTwo(subtotal + shipping + tax),
};
}Example 4: Dynamic rules from a provider (async)
import { OrderEngine } from "order-process-engine";
const engine = new OrderEngine({
async ruleSetProvider(context) {
// Example: fetch from DB/remote config per tenant-region
const ruleSet = await fetch(
`https://pricing.example.com/rules?tenant=${context.tenantId}®ion=${context.region}`,
).then((res) => res.json());
return ruleSet;
},
});
const result = await engine.calculateAsync(
[{ price: 999, quantity: 1, category: "electronics", weight: 1.4 }],
{
tenantId: "store-42",
region: "IN",
channel: "web",
customerTier: "regular",
},
);
console.log(result.total);
console.log(result.appliedRules);Example 5: External tax provider
import { OrderEngine } from "order-process-engine";
const engine = new OrderEngine({
taxProvider: async (taxableSubtotal, context) => {
const response = await fetch("https://tax.example.com/quote", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ taxableSubtotal, region: context.region }),
}).then((res) => res.json());
return response.taxAmount;
},
});
const result = await engine.calculateAsync([{ price: 100, quantity: 2 }], {
region: "US-CA",
});
console.log(result.tax);Example 6: v1 to v2 migration (minimal changes)
If you already use static config in v1, you only need to add a ruleSet, pass a context, and keep the rest of your flow the same.
Before (v1 static config):
import { OrderEngine, type OrderItem } from "order-process-engine";
const items: OrderItem[] = [
{ price: 1200, quantity: 1 },
{ price: 250, quantity: 2 },
];
const engine = new OrderEngine({
tax: 0.12,
mktFeePercent: 0.08,
mktFeeFixed: 2.5,
shippingBase: 40,
shippingThreshold: 2000,
});
const result = engine.calculate(items);
console.log(result.total);After (v2 context + rules):
import {
OrderEngine,
type OrderItem,
type PricingContext,
type PricingRuleSet,
} from "order-process-engine";
const items: OrderItem[] = [
{ price: 1200, quantity: 1, category: "electronics", weight: 1.2 },
{ price: 250, quantity: 2, category: "books", weight: 0.4 },
];
const ruleSet: PricingRuleSet = {
promotions: [
{
id: "vip-10",
name: "VIP 10% Off",
type: "promotion",
scope: "order",
discountMode: "percent",
discountValue: 0.1,
when: [{ field: "context.customerTier", op: "eq", value: "vip" }],
},
],
shipping: [
{
id: "in-flat-shipping",
name: "IN Flat Shipping",
type: "shipping",
mode: "flat",
flatAmount: 25,
when: [{ field: "context.region", op: "eq", value: "IN" }],
},
],
tax: [
{
id: "in-tax",
name: "IN Tax",
type: "tax",
rate: 0.18,
when: [{ field: "context.region", op: "eq", value: "IN" }],
},
],
};
const context: PricingContext = {
tenantId: "store-42",
region: "IN",
customerTier: "vip",
};
const engine = new OrderEngine({ ruleSet });
const result = engine.calculate(items, context);
console.log(result.total);
console.log(result.appliedRules);Minimal migration checklist:
- Keep
OrderEngineandcalculateas your entry points. - Add
ruleSetto constructor config. - Pass
contextas the second argument incalculate(items, context). - Optionally read
result.appliedRulesandresult.breakdownfor explainable pricing. - If your rules come from APIs/DB, switch to
calculateAsync(...).
License
MIT
