Sales calculations
The sales calculation service powers quote and order totals. It runs per-line calculators, document-level calculators, and emits events so you can override numbers without forking the sales module.
Per-line calculators
Use registerSalesLineCalculator to adjust every line before document totals are rebuilt:
// src/modules/your-module/index.ts
import { registerSalesLineCalculator } from '@open-mercato/core/modules/sales/lib/calculations'
registerSalesLineCalculator(({ line, current }) => {
if (line.kind !== 'product') return current
const ecoFee = 1.25 * Number(line.quantity ?? 1)
return {
...current,
netAmount: current.netAmount + ecoFee,
grossAmount: current.grossAmount + ecoFee,
adjustments: [
...current.adjustments,
{
scope: 'line',
kind: 'surcharge',
label: 'Environmental fee',
amountNet: ecoFee,
amountGross: ecoFee,
currencyCode: line.currencyCode,
},
],
}
})
Document-level calculators
Add or override order-level adjustments with registerSalesTotalsCalculator (executed after all line calculators):
// src/modules/your-module/index.ts
import { registerSalesTotalsCalculator } from '@open-mercato/core/modules/sales/lib/calculations'
registerSalesTotalsCalculator(({ current, documentKind }) => {
if (documentKind !== 'order') return current
const handling = 4
const adjustments = [
...current.adjustments,
{
scope: 'order',
kind: 'surcharge',
label: 'Handling',
amountNet: handling,
amountGross: handling,
currencyCode: current.currencyCode,
},
]
return {
...current,
adjustments,
totals: {
...current.totals,
surchargeTotalAmount: (current.totals.surchargeTotalAmount ?? 0) + handling,
subtotalNetAmount: current.totals.subtotalNetAmount + handling,
subtotalGrossAmount: current.totals.subtotalGrossAmount + handling,
grandTotalNetAmount: current.totals.grandTotalNetAmount + handling,
grandTotalGrossAmount: current.totals.grandTotalGrossAmount + handling,
outstandingAmount: current.totals.outstandingAmount + handling,
},
}
})
Event hooks
When the calculation service has an eventBus (default in DI), it emits events you can intercept:
sales.line.calculate.before|afterto override a single line result.sales.document.calculate.before|afterto replace the document totals.sales.shipping.adjustments.apply.before|afterto mutate provider-generated shipping adjustments while they are being added.sales.payment.adjustments.apply.before|afterto mutate provider-generated payment adjustments.
Example subscriber that tweaks provider adjustments and forces a minimum fee:
// src/modules/your-module/di.ts
import type { AwilixContainer } from 'awilix'
import type { EventBus } from '@open-mercato/events'
export function register(container: AwilixContainer) {
const bus = container.resolve<EventBus>('eventBus')
bus.on('sales.shipping.adjustments.apply.before', ({ result, setResult }) => {
if (!result) return
const adjusted = result.adjustments.map((adj) => ({
...adj,
amountNet: (adj.amountNet ?? 0) * 1.05,
amountGross: (adj.amountGross ?? adj.amountNet ?? 0) * 1.05,
}))
setResult({ ...result, adjustments: adjusted })
})
bus.on('sales.payment.adjustments.apply.after', ({ adjustments, setAdjustments }) => {
if (!adjustments?.length) return
const next = adjustments.map((adj) => ({
...adj,
amountNet: Math.max(adj.amountNet ?? 0, 1),
amountGross: Math.max(adj.amountGross ?? adj.amountNet ?? 0, 1),
}))
setAdjustments(next)
})
}
Use these hooks to implement per-item tweaks (line events) or whole-document overrides (document events) without changing the built-in sales commands.