Order level pricing
Add extra line items to an order based on the entire cart - minimum fees, shipping, volume discounts, and post-processing minimums.
Order level pricing lets you add extra line items to an order based on what's in the entire cart.
Use it whenever prices of different parts relate to each other: minimum order fees, shipping costs, volume discounts, or minimum post-processing charges.
⚠️ Use the AI prompt in the code interface on the Phasio platform to get help with your custom requirements.
What you get
Your code runs once per order. Phasio gives you these variables:
| Variable | What it is |
|---|---|
parts | All parts in the order. Each has price, specification, requisition, revision. |
subtotal | Sum of all part prices (before order-level adjustments). |
addLineItem({ name, price }) | Adds a line to the order. Positive = additional cost. Negative = discount. |
createBands({...}) | Creates a lookup function for tiered pricing. |
round(value, precision) | Rounds to N decimal places. |
How parts works
Each entry in parts looks like this:
{
price: 42.50, // price of this line item
requisition: { quantity: 10 }, // how many units ordered
specification: {
volume: 12000, // mm³
area: 8500, // mm²
width: 50, height: 30, length: 80, // bounding box in mm
material: { name: 'PA12' },
postProcessing: [{ name: 'Black Dye' }],
color: 'black'
}
}You loop through parts, group or sum what you need, then call addLineItem().
How createBands works
createBands maps an input value to tiers. It returns the value of the highest threshold the input exceeds.
const getDiscount = createBands({
50: 0.02, // input >= 50 → 0.02
100: 0.05, // input >= 100 → 0.05
200: 0.10, // input >= 200 → 0.10
})
getDiscount(30) // → 0 (below all thresholds)
getDiscount(75) // → 0.02
getDiscount(150) // → 0.05
getDiscount(500) // → 0.10You can set a custom default (instead of 0) as the second argument:
const getFee = createBands({ 100: 5, 500: 3 }, 10)
getFee(50) // → 10 (below all thresholds, returns default)Example 1 - Minimum Order Value
Problem: Every order must be at least €100.
if (subtotal < 100) {
addLineItem({
name: 'Minimum Order Fee (€100)',
price: 100 - subtotal
})
}Customer orders €40 of parts → €60 fee is added.
Example 2 - Minimum Price per Material
Problem: Each material has a setup cost. Small orders of one material should still cover that cost.
const minPrices: Record<string, number> = {
'PA12': 48,
'PA11': 69,
'TPU': 69,
}
const totals: Record<string, number> = {}
parts.forEach(({ specification, price }) => {
const mat = specification.material.name
if (minPrices[mat]) {
totals[mat] = (totals[mat] || 0) + price
}
})
Object.keys(totals).forEach(mat => {
if (totals[mat] < minPrices[mat]) {
addLineItem({
name: `Min. order fee ${mat}`,
price: round(minPrices[mat] - totals[mat], 2)
})
}
})Example 3 - Minimum Post-Processing Cost
Problem: A dyeing process requires a cartridge. If only a small amount of one color is ordered, there's still a minimum cartridge cost.
const ppMinCosts: Record<string, number> = {
'Black Dye': 20,
'Blue Dye': 50, // rare color = higher minimum
}
const ppTotals: Record<string, number> = {}
parts.forEach(({ specification, price }) => {
if (!specification.postProcessing) return
specification.postProcessing.forEach(pp => {
if (ppMinCosts[pp.name]) {
ppTotals[pp.name] = (ppTotals[pp.name] || 0) + price
}
})
})
Object.keys(ppTotals).forEach(ppName => {
if (ppTotals[ppName] < ppMinCosts[ppName]) {
addLineItem({
name: `Min. charge ${ppName}`,
price: round(ppMinCosts[ppName] - ppTotals[ppName], 2)
})
}
})Grouping by color
If the minimum depends on the dye color (e.g. E-Coloring grouped per color), use specification.color as part of the grouping key:
const ppMinCosts: Record<string, number> = {
'Dyeing': 50,
'Vapor Smooth': 80,
}
// Group by post-process + color
const ppTotals: Record<string, { label: string; total: number; min: number }> = {}
parts.forEach(({ specification, price }) => {
if (!specification.postProcessing) return
specification.postProcessing.forEach(pp => {
if (!ppMinCosts[pp.name]) return
const groupKey = `${pp.name}_${specification.color || 'default'}`
if (!ppTotals[groupKey]) {
ppTotals[groupKey] = {
label: `${pp.name}${specification.color ? ` (${specification.color})` : ''}`,
total: 0,
min: ppMinCosts[pp.name],
}
}
ppTotals[groupKey].total += price
})
})
Object.keys(ppTotals).forEach(key => {
const { label, total, min } = ppTotals[key]
if (total > 0 && total < min) {
addLineItem({
name: `Min. charge ${label}`,
price: round(min - total, 2)
})
}
})This way, 5 black Dyeing parts are grouped together, but 1 blue Dyeing part gets its own minimum check.
Example 4 - Volume Discount per Material
Problem: Reward customers who order a lot of one material.
const getDiscount = createBands({
500: 0.02,
1000: 0.05,
2000: 0.08,
5000: 0.10,
})
const totals: Record<string, number> = {}
parts.forEach(({ specification, price }) => {
const mat = specification.material.name
totals[mat] = (totals[mat] || 0) + price
})
Object.keys(totals).forEach(mat => {
const pct = getDiscount(totals[mat])
if (pct > 0) {
addLineItem({
name: `Volume discount ${mat} (${(pct * 100).toFixed(1)}%)`,
price: -round(pct * totals[mat], 2)
})
}
})☝️ price is negative because it's a discount.
Example 5 - Shipping Cost (Bin Packing)
Problem: Estimate shipping by packing all parts into boxes.
This uses a "first fit decreasing" algorithm to pack parts into the fewest boxes.
Step 1 - Define box sizes
const PACKING_OFFSET_MM = 25
const BOXES = [
{ name: 'S', extX: 254, extY: 203, extZ: 152, weightKg: 5, cost: 11 },
{ name: 'M', extX: 305, extY: 254, extZ: 203, weightKg: 10, cost: 15 },
{ name: 'L', extX: 406, extY: 305, extZ: 254, weightKg: 18, cost: 22 },
{ name: 'XL', extX: 508, extY: 406, extZ: 305, weightKg: 27, cost: 31 },
]All dimensions in mm (external). PACKING_OFFSET_MM is subtracted from each side for padding.
Step 2 - Helpers
function fitsInChamber(
w: number, h: number, l: number,
cX: number, cY: number, cZ: number
): boolean {
const o: [number,number,number][] = [
[w,h,l],[w,l,h],[h,w,l],[h,l,w],[l,w,h],[l,h,w]
]
return o.some(([a,b,c]) => a<=cX && b<=cY && c<=cZ)
}
function chamberOf(box: typeof BOXES[number]) {
return {
x: box.extX - PACKING_OFFSET_MM,
y: box.extY - PACKING_OFFSET_MM,
z: box.extZ - PACKING_OFFSET_MM,
}
}Step 3 - Pack items
interface ShippingItem {
width: number; height: number; length: number; weight: number
}
function packItems(items: ShippingItem[]) {
if (items.length === 0) return { cost: 0, boxCount: 0, boxes: [] as {name:string,count:number}[] }
const sorted = [...items].sort(
(a, b) => b.width*b.height*b.length - a.width*a.height*a.length
)
interface Bin { box: typeof BOXES[number]; usedWeightG: number }
const bins: Bin[] = []
for (const item of sorted) {
let placed = false
for (const bin of bins) {
const c = chamberOf(bin.box)
if (bin.usedWeightG + item.weight > bin.box.weightKg * 1000) continue
if (!fitsInChamber(item.width, item.height, item.length, c.x, c.y, c.z)) continue
bin.usedWeightG += item.weight
placed = true
break
}
if (!placed) {
let chosen = BOXES[BOXES.length - 1]
for (let i = 0; i < BOXES.length; i++) {
const c = chamberOf(BOXES[i])
if (item.weight <= BOXES[i].weightKg * 1000 &&
fitsInChamber(item.width, item.height, item.length, c.x, c.y, c.z)) {
chosen = BOXES[i]
break
}
}
bins.push({ box: chosen, usedWeightG: item.weight })
}
}
const cost = bins.reduce((s, b) => s + b.box.cost, 0)
const summary = bins.reduce((acc, b) => {
const e = acc.find(x => x.name === b.box.name)
if (e) e.count++; else acc.push({ name: b.box.name, count: 1 })
return acc
}, [] as { name: string; count: number }[])
return { cost, boxCount: bins.length, boxes: summary }
}Step 4 - Collect items and add shipping
const allItems: ShippingItem[] = []
parts.forEach(({ specification, requisition }) => {
const { width, height, length, volume } = specification
const density = 1.1 // g/cm³, adjust per material
const weightG = (volume / 1000) * density
for (let i = 0; i < requisition.quantity; i++) {
allItems.push({ width, height, length, weight: weightG })
}
})
const shipping = packItems(allItems)
if (shipping.cost > 0) {
const label = shipping.boxes.map(b => `${b.count}× ${b.name}`).join(', ')
addLineItem({ name: `Shipping (${label})`, price: shipping.cost })
}Combining recipes
You can stack multiple recipes in one equation. Recommended order:
- Volume discounts
- Minimum material fees
- Minimum post-processing fees
- Shipping cost
- Minimum order value (always last)
Put minimum order value last so it accounts for all other adjustments.
Quick reference
| I want to… | Pattern |
|---|---|
| Charge a minimum order total | Compare subtotal, add the difference |
| Minimum per material | Sum by material.name, top up if below threshold |
| Minimum per post-process | Sum by postProcessing[].name (+ color), top up |
| Volume discount | Sum by material, use createBands, add negative price |
| Shipping estimate | Collect dimensions, run bin-packing, add box costs |
Tips
- Negative price = discount. Always use
price: -amountfor discounts. createBandsis your best friend for any tiered pricing. Define thresholds once, reuse everywhere.round(value, 2)avoids floating-point rounding issues.- Group by whatever matters: material, color, post-process, or any combination using a string key like
`${material}_${color}`. - Test edge cases: single part, empty cart, oversized part, mixed materials.
- Code runs in a sandbox - no network calls, no timers, just pure math.
Last updated on