@open-mercato/core 0.6.7-develop.6552.1.5208a19369 → 0.6.7-develop.6553.1.2ec7246e04

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -140,6 +140,12 @@ function buildBaseDocumentResult(params) {
140
140
  }
141
141
  break;
142
142
  default:
143
+ if (adj.kind === "return") break;
144
+ subtotalNet += net;
145
+ subtotalGross += gross;
146
+ if (taxPortion) {
147
+ taxTotal += taxPortion;
148
+ }
143
149
  break;
144
150
  }
145
151
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/lib/calculations.ts"],
4
- "sourcesContent": ["import {\n type SalesAdjustmentDraft,\n type SalesCalculationContext,\n type CalculateDocumentOptions,\n type CalculateLineOptions,\n type SalesDocumentCalculationResult,\n type SalesDocumentKind,\n type SalesLineCalculationHook,\n type SalesLineCalculationResult,\n type SalesLineSnapshot,\n type SalesTotalsCalculationHook,\n} from './types'\n\nfunction toNumber(value: unknown, fallback = 0): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {\n return Number(value)\n }\n return fallback\n}\n\nfunction round(value: number): number {\n return Math.round((value + Number.EPSILON) * 1e4) / 1e4\n}\n\nfunction extractAdjustmentTaxRate(adjustment: SalesAdjustmentDraft): number | null {\n const metadata = (adjustment.metadata ?? {}) as Record<string, unknown>\n const candidate =\n metadata.taxRate ??\n (metadata as any)?.tax_rate ??\n (metadata as any)?.taxRateValue ??\n (metadata as any)?.tax_rate_value ??\n null\n const parsed = toNumber(candidate, NaN)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction resolveAdjustmentAmounts(\n adjustments: SalesAdjustmentDraft[],\n baseNet: number,\n baseGross: number\n): SalesAdjustmentDraft[] {\n return adjustments.map((adj) => {\n const rate = toNumber(adj.rate, NaN)\n const taxRate = extractAdjustmentTaxRate(adj)\n const hasAmountNet = Number.isFinite(toNumber(adj.amountNet, NaN))\n const hasAmountGross = Number.isFinite(toNumber(adj.amountGross, NaN))\n const hasRate = Number.isFinite(rate) && !hasAmountNet && !hasAmountGross\n const hasTaxRate = taxRate !== null\n let amountNet = toNumber(adj.amountNet, NaN)\n let amountGross = toNumber(adj.amountGross, NaN)\n\n if (hasRate) {\n const multiplier = (rate as number) / 100\n amountNet = round(Math.max(baseNet, 0) * multiplier)\n if (adj.kind === 'tax') {\n amountGross = amountNet\n } else if (hasTaxRate) {\n amountGross = round(amountNet * (1 + (taxRate as number) / 100))\n } else {\n amountGross = round(Math.max(baseGross, 0) * multiplier)\n }\n } else {\n if (!Number.isFinite(amountNet) && Number.isFinite(amountGross) && hasTaxRate) {\n amountNet = round((amountGross as number) / (1 + (taxRate as number) / 100))\n }\n if (!Number.isFinite(amountGross) && Number.isFinite(amountNet) && hasTaxRate) {\n amountGross = round((amountNet as number) * (1 + (taxRate as number) / 100))\n }\n }\n\n return {\n ...adj,\n amountNet: Number.isFinite(amountNet) ? amountNet : adj.amountNet,\n amountGross: Number.isFinite(amountGross) ? amountGross : adj.amountGross,\n }\n })\n}\n\nfunction buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResult {\n const quantity = Math.max(toNumber(line.quantity, 0), 0)\n const taxRate = toNumber(line.taxRate, 0) / 100\n const unitNet =\n line.unitPriceNet ??\n (line.unitPriceGross !== null && line.unitPriceGross !== undefined\n ? toNumber(line.unitPriceGross) / (1 + taxRate)\n : 0)\n const discountPerUnit =\n line.discountAmount ??\n (line.discountPercent !== null && line.discountPercent !== undefined\n ? toNumber(line.discountPercent, 0) / 100 * toNumber(unitNet, 0)\n : 0)\n\n const netSubtotalBeforeDiscount = toNumber(unitNet, 0) * quantity\n const discountTotal = Math.min(Math.max(discountPerUnit * quantity, 0), netSubtotalBeforeDiscount)\n const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0)\n const explicitTaxAmount = line.taxAmount !== null && line.taxAmount !== undefined\n let taxAmount = explicitTaxAmount\n ? toNumber(line.taxAmount, 0)\n : round(netSubtotal * Math.max(taxRate, 0))\n const grossSubtotal =\n line.totalGrossAmount !== null && line.totalGrossAmount !== undefined\n ? toNumber(line.totalGrossAmount, 0)\n : round(netSubtotal + taxAmount)\n // When tax was not supplied explicitly and the rate-derived tax is zero but\n // the gross total already embeds tax (gross > net) \u2014 e.g. a tax-class-priced\n // line whose resolved rate was not persisted \u2014 derive the tax from the\n // net/gross delta so the document-level tax total is not silently zeroed\n // while per-line net/gross stay correct (#2457).\n if (!explicitTaxAmount && taxAmount <= 0) {\n const grossNetDelta = round(grossSubtotal - netSubtotal)\n if (grossNetDelta > 0) taxAmount = grossNetDelta\n }\n\n return {\n line,\n netAmount: round(netSubtotal),\n grossAmount: round(grossSubtotal),\n taxAmount: round(taxAmount),\n discountAmount: round(discountTotal),\n adjustments: [],\n }\n}\n\nfunction buildBaseDocumentResult(params: {\n documentKind: SalesDocumentKind\n lines: SalesLineCalculationResult[]\n adjustments: SalesAdjustmentDraft[]\n currencyCode: string\n existingTotals?: { paidTotalAmount?: number | null; refundedTotalAmount?: number | null }\n}): SalesDocumentCalculationResult {\n const { documentKind, lines, adjustments, currencyCode } = params\n const orderedAdjustments = [...(adjustments ?? [])].sort(\n (a, b) => (a.position ?? 0) - (b.position ?? 0)\n )\n let baseSubtotalNet = 0\n let baseSubtotalGross = 0\n let subtotalNet = 0\n let subtotalGross = 0\n let discountTotal = 0\n let taxTotal = 0\n let shippingNet = 0\n let shippingGross = 0\n let surchargeTotal = 0\n\n for (const line of lines) {\n const net = toNumber(line.netAmount, 0)\n const gross = toNumber(line.grossAmount, 0)\n subtotalNet += net\n subtotalGross += gross\n baseSubtotalNet += net\n baseSubtotalGross += gross\n discountTotal += toNumber(line.discountAmount, 0)\n taxTotal += toNumber(line.taxAmount, 0)\n }\n\n const resolvedAdjustments = resolveAdjustmentAmounts(orderedAdjustments, baseSubtotalNet, baseSubtotalGross)\n const scopedAdjustments = resolvedAdjustments.filter(\n (adj) => !adj.scope || adj.scope === 'order'\n )\n\n for (const adj of scopedAdjustments) {\n const rawNet = toNumber(adj.amountNet, toNumber(adj.amountGross))\n const rawGross = toNumber(adj.amountGross, rawNet)\n // Each adjustment kind has an intrinsic sign convention. The API edge\n // (enforceAdjustmentSign) rejects values that would invert the kind's\n // semantic effect, but the calculation engine normalizes defensively so\n // direct DB writes or seeded data can't inflate the grand total either.\n // See #1905 (mirrors the existing return normalization a few lines below\n // introduced for #1705).\n const isNonNegativeKind =\n adj.kind === 'discount' ||\n adj.kind === 'surcharge' ||\n adj.kind === 'shipping' ||\n adj.kind === 'tax'\n const net = isNonNegativeKind ? Math.abs(rawNet) : rawNet\n const gross = isNonNegativeKind ? Math.abs(rawGross) : rawGross\n const taxRate = extractAdjustmentTaxRate(adj)\n const taxPortion = taxRate !== null ? round(gross - net) : 0\n switch (adj.kind) {\n case 'discount':\n discountTotal += net\n subtotalNet = Math.max(subtotalNet - net, 0)\n subtotalGross = Math.max(subtotalGross - gross, 0)\n if (taxPortion) {\n taxTotal = round(taxTotal - taxPortion)\n }\n break\n case 'tax':\n taxTotal += gross || net\n subtotalGross += gross || net\n break\n case 'shipping':\n shippingNet += net\n shippingGross += gross\n subtotalNet += net\n subtotalGross += gross\n if (taxPortion) {\n taxTotal += taxPortion\n }\n break\n case 'surcharge':\n surchargeTotal += net || gross\n subtotalNet += net || gross\n subtotalGross += gross || net\n if (taxPortion) {\n taxTotal += taxPortion\n }\n break\n default:\n break\n }\n }\n\n // Line-scoped and any other return (credit) adjustments reduce grand total.\n // Sign is normalized to negative regardless of the stored sign so a positive\n // amountNet / amountGross can never inflate totals (issue #1705).\n for (const adj of resolvedAdjustments) {\n if (adj.kind !== 'return') continue\n const net = toNumber(adj.amountNet, toNumber(adj.amountGross))\n const gross = toNumber(adj.amountGross, net)\n const netDelta = -Math.abs(net)\n const grossDelta = -Math.abs(gross)\n subtotalNet = Math.max(subtotalNet + netDelta, 0)\n subtotalGross = Math.max(subtotalGross + grossDelta, 0)\n }\n\n const grandTotalNet = round(subtotalNet)\n const grandTotalGross = round(subtotalGross)\n const paidTotalAmount = Math.max(toNumber(params.existingTotals?.paidTotalAmount, 0), 0)\n const refundedTotalAmount = Math.max(toNumber(params.existingTotals?.refundedTotalAmount, 0), 0)\n const outstandingAmount = Math.max(grandTotalGross - paidTotalAmount + refundedTotalAmount, 0)\n\n return {\n kind: documentKind,\n currencyCode,\n lines,\n adjustments: resolvedAdjustments,\n metadata: {},\n totals: {\n subtotalNetAmount: round(subtotalNet),\n subtotalGrossAmount: round(subtotalGross),\n discountTotalAmount: round(discountTotal),\n taxTotalAmount: round(taxTotal),\n shippingNetAmount: round(shippingNet),\n shippingGrossAmount: round(shippingGross),\n surchargeTotalAmount: round(surchargeTotal),\n grandTotalNetAmount: grandTotalNet,\n grandTotalGrossAmount: grandTotalGross,\n paidTotalAmount,\n refundedTotalAmount,\n outstandingAmount,\n },\n }\n}\n\nclass SalesCalculationRegistry {\n private lineCalculators: SalesLineCalculationHook[] = []\n private totalsCalculators: SalesTotalsCalculationHook[] = []\n\n registerLineCalculator(hook: SalesLineCalculationHook, opts?: { prepend?: boolean }): () => void {\n if (opts?.prepend) this.lineCalculators.unshift(hook)\n else this.lineCalculators.push(hook)\n return () => {\n this.lineCalculators = this.lineCalculators.filter((item) => item !== hook)\n }\n }\n\n registerTotalsCalculator(hook: SalesTotalsCalculationHook, opts?: { prepend?: boolean }): () => void {\n if (opts?.prepend) this.totalsCalculators.unshift(hook)\n else this.totalsCalculators.push(hook)\n return () => {\n this.totalsCalculators = this.totalsCalculators.filter((item) => item !== hook)\n }\n }\n\n async calculateLine(opts: CalculateLineOptions): Promise<SalesLineCalculationResult> {\n const { documentKind, line, context, eventBus } = opts\n let current = buildBaseLineResult(line)\n\n if (eventBus) {\n await eventBus.emitEvent('sales.line.calculate.before', {\n documentKind,\n line,\n context,\n result: current,\n setResult(next: SalesLineCalculationResult) {\n current = next\n },\n })\n }\n\n for (const hook of this.lineCalculators) {\n const next = await hook({ documentKind, line, context, current })\n if (next) current = next\n }\n\n if (eventBus) {\n await eventBus.emitEvent('sales.line.calculate.after', {\n documentKind,\n line,\n context,\n result: current,\n setResult(next: SalesLineCalculationResult) {\n current = next\n },\n })\n }\n\n return current\n }\n\n async calculateDocument(opts: CalculateDocumentOptions): Promise<SalesDocumentCalculationResult> {\n const { documentKind, lines, adjustments = [], context, eventBus, existingTotals } = opts\n const resolvedLines: SalesLineCalculationResult[] = []\n\n for (const line of lines) {\n const result = await this.calculateLine({ documentKind, line, context, eventBus })\n resolvedLines.push(result)\n }\n\n let current = buildBaseDocumentResult({\n documentKind,\n lines: resolvedLines,\n adjustments,\n currencyCode: context.currencyCode,\n existingTotals,\n })\n\n if (eventBus) {\n await eventBus.emitEvent('sales.document.calculate.before', {\n documentKind,\n lines: resolvedLines,\n context,\n adjustments,\n result: current,\n setResult(next: SalesDocumentCalculationResult) {\n current = next\n },\n })\n }\n\n for (const hook of this.totalsCalculators) {\n const next = await hook({\n documentKind,\n lines: resolvedLines,\n existingAdjustments: adjustments,\n context,\n current,\n eventBus,\n })\n if (next) current = next\n }\n\n if (eventBus) {\n await eventBus.emitEvent('sales.document.calculate.after', {\n documentKind,\n lines: resolvedLines,\n context,\n adjustments,\n result: current,\n setResult(next: SalesDocumentCalculationResult) {\n current = next\n },\n })\n }\n\n // Payment totals (paid/refunded) are authoritative inputs, not derived from\n // lines or adjustments. Totals calculators rebuild the document result from\n // lines+adjustments and would otherwise reset paid/refunded to 0 (and\n // outstanding back to the full grand total), producing a stale paid/\n // outstanding display after a payment. Re-apply the input totals last and\n // recompute outstanding against the post-calculation grand total.\n if (existingTotals) {\n const paidTotalAmount = Math.max(toNumber(existingTotals.paidTotalAmount, 0), 0)\n const refundedTotalAmount = Math.max(toNumber(existingTotals.refundedTotalAmount, 0), 0)\n current.totals = {\n ...current.totals,\n paidTotalAmount,\n refundedTotalAmount,\n outstandingAmount: round(\n Math.max(current.totals.grandTotalGrossAmount - paidTotalAmount + refundedTotalAmount, 0)\n ),\n }\n }\n\n return current\n }\n}\n\nexport function createSalesCalculationRegistry(): SalesCalculationRegistry {\n return new SalesCalculationRegistry()\n}\n\nexport const salesCalculations = createSalesCalculationRegistry()\n\nexport async function calculateLine(\n opts: CalculateLineOptions\n): Promise<SalesLineCalculationResult> {\n return salesCalculations.calculateLine(opts)\n}\n\nexport async function calculateDocumentTotals(\n opts: CalculateDocumentOptions\n): Promise<SalesDocumentCalculationResult> {\n return salesCalculations.calculateDocument(opts)\n}\n\nexport function registerSalesLineCalculator(\n hook: SalesLineCalculationHook,\n opts?: { prepend?: boolean }\n): () => void {\n return salesCalculations.registerLineCalculator(hook, opts)\n}\n\nexport function registerSalesTotalsCalculator(\n hook: SalesTotalsCalculationHook,\n opts?: { prepend?: boolean }\n): () => void {\n return salesCalculations.registerTotalsCalculator(hook, opts)\n}\n\nexport function rebuildDocumentResult(params: {\n documentKind: SalesDocumentKind\n currencyCode: string\n lines: SalesLineCalculationResult[]\n adjustments: SalesAdjustmentDraft[]\n metadata?: Record<string, unknown>\n}): SalesDocumentCalculationResult {\n const result = buildBaseDocumentResult({\n documentKind: params.documentKind,\n lines: params.lines,\n adjustments: params.adjustments,\n currencyCode: params.currencyCode,\n })\n result.metadata = params.metadata ?? {}\n return result\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n type SalesAdjustmentDraft,\n type SalesCalculationContext,\n type CalculateDocumentOptions,\n type CalculateLineOptions,\n type SalesDocumentCalculationResult,\n type SalesDocumentKind,\n type SalesLineCalculationHook,\n type SalesLineCalculationResult,\n type SalesLineSnapshot,\n type SalesTotalsCalculationHook,\n} from './types'\n\nfunction toNumber(value: unknown, fallback = 0): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {\n return Number(value)\n }\n return fallback\n}\n\nfunction round(value: number): number {\n return Math.round((value + Number.EPSILON) * 1e4) / 1e4\n}\n\nfunction extractAdjustmentTaxRate(adjustment: SalesAdjustmentDraft): number | null {\n const metadata = (adjustment.metadata ?? {}) as Record<string, unknown>\n const candidate =\n metadata.taxRate ??\n (metadata as any)?.tax_rate ??\n (metadata as any)?.taxRateValue ??\n (metadata as any)?.tax_rate_value ??\n null\n const parsed = toNumber(candidate, NaN)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction resolveAdjustmentAmounts(\n adjustments: SalesAdjustmentDraft[],\n baseNet: number,\n baseGross: number\n): SalesAdjustmentDraft[] {\n return adjustments.map((adj) => {\n const rate = toNumber(adj.rate, NaN)\n const taxRate = extractAdjustmentTaxRate(adj)\n const hasAmountNet = Number.isFinite(toNumber(adj.amountNet, NaN))\n const hasAmountGross = Number.isFinite(toNumber(adj.amountGross, NaN))\n const hasRate = Number.isFinite(rate) && !hasAmountNet && !hasAmountGross\n const hasTaxRate = taxRate !== null\n let amountNet = toNumber(adj.amountNet, NaN)\n let amountGross = toNumber(adj.amountGross, NaN)\n\n if (hasRate) {\n const multiplier = (rate as number) / 100\n amountNet = round(Math.max(baseNet, 0) * multiplier)\n if (adj.kind === 'tax') {\n amountGross = amountNet\n } else if (hasTaxRate) {\n amountGross = round(amountNet * (1 + (taxRate as number) / 100))\n } else {\n amountGross = round(Math.max(baseGross, 0) * multiplier)\n }\n } else {\n if (!Number.isFinite(amountNet) && Number.isFinite(amountGross) && hasTaxRate) {\n amountNet = round((amountGross as number) / (1 + (taxRate as number) / 100))\n }\n if (!Number.isFinite(amountGross) && Number.isFinite(amountNet) && hasTaxRate) {\n amountGross = round((amountNet as number) * (1 + (taxRate as number) / 100))\n }\n }\n\n return {\n ...adj,\n amountNet: Number.isFinite(amountNet) ? amountNet : adj.amountNet,\n amountGross: Number.isFinite(amountGross) ? amountGross : adj.amountGross,\n }\n })\n}\n\nfunction buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResult {\n const quantity = Math.max(toNumber(line.quantity, 0), 0)\n const taxRate = toNumber(line.taxRate, 0) / 100\n const unitNet =\n line.unitPriceNet ??\n (line.unitPriceGross !== null && line.unitPriceGross !== undefined\n ? toNumber(line.unitPriceGross) / (1 + taxRate)\n : 0)\n const discountPerUnit =\n line.discountAmount ??\n (line.discountPercent !== null && line.discountPercent !== undefined\n ? toNumber(line.discountPercent, 0) / 100 * toNumber(unitNet, 0)\n : 0)\n\n const netSubtotalBeforeDiscount = toNumber(unitNet, 0) * quantity\n const discountTotal = Math.min(Math.max(discountPerUnit * quantity, 0), netSubtotalBeforeDiscount)\n const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0)\n const explicitTaxAmount = line.taxAmount !== null && line.taxAmount !== undefined\n let taxAmount = explicitTaxAmount\n ? toNumber(line.taxAmount, 0)\n : round(netSubtotal * Math.max(taxRate, 0))\n const grossSubtotal =\n line.totalGrossAmount !== null && line.totalGrossAmount !== undefined\n ? toNumber(line.totalGrossAmount, 0)\n : round(netSubtotal + taxAmount)\n // When tax was not supplied explicitly and the rate-derived tax is zero but\n // the gross total already embeds tax (gross > net) \u2014 e.g. a tax-class-priced\n // line whose resolved rate was not persisted \u2014 derive the tax from the\n // net/gross delta so the document-level tax total is not silently zeroed\n // while per-line net/gross stay correct (#2457).\n if (!explicitTaxAmount && taxAmount <= 0) {\n const grossNetDelta = round(grossSubtotal - netSubtotal)\n if (grossNetDelta > 0) taxAmount = grossNetDelta\n }\n\n return {\n line,\n netAmount: round(netSubtotal),\n grossAmount: round(grossSubtotal),\n taxAmount: round(taxAmount),\n discountAmount: round(discountTotal),\n adjustments: [],\n }\n}\n\nfunction buildBaseDocumentResult(params: {\n documentKind: SalesDocumentKind\n lines: SalesLineCalculationResult[]\n adjustments: SalesAdjustmentDraft[]\n currencyCode: string\n existingTotals?: { paidTotalAmount?: number | null; refundedTotalAmount?: number | null }\n}): SalesDocumentCalculationResult {\n const { documentKind, lines, adjustments, currencyCode } = params\n const orderedAdjustments = [...(adjustments ?? [])].sort(\n (a, b) => (a.position ?? 0) - (b.position ?? 0)\n )\n let baseSubtotalNet = 0\n let baseSubtotalGross = 0\n let subtotalNet = 0\n let subtotalGross = 0\n let discountTotal = 0\n let taxTotal = 0\n let shippingNet = 0\n let shippingGross = 0\n let surchargeTotal = 0\n\n for (const line of lines) {\n const net = toNumber(line.netAmount, 0)\n const gross = toNumber(line.grossAmount, 0)\n subtotalNet += net\n subtotalGross += gross\n baseSubtotalNet += net\n baseSubtotalGross += gross\n discountTotal += toNumber(line.discountAmount, 0)\n taxTotal += toNumber(line.taxAmount, 0)\n }\n\n const resolvedAdjustments = resolveAdjustmentAmounts(orderedAdjustments, baseSubtotalNet, baseSubtotalGross)\n const scopedAdjustments = resolvedAdjustments.filter(\n (adj) => !adj.scope || adj.scope === 'order'\n )\n\n for (const adj of scopedAdjustments) {\n const rawNet = toNumber(adj.amountNet, toNumber(adj.amountGross))\n const rawGross = toNumber(adj.amountGross, rawNet)\n // Each adjustment kind has an intrinsic sign convention. The API edge\n // (enforceAdjustmentSign) rejects values that would invert the kind's\n // semantic effect, but the calculation engine normalizes defensively so\n // direct DB writes or seeded data can't inflate the grand total either.\n // See #1905 (mirrors the existing return normalization a few lines below\n // introduced for #1705).\n const isNonNegativeKind =\n adj.kind === 'discount' ||\n adj.kind === 'surcharge' ||\n adj.kind === 'shipping' ||\n adj.kind === 'tax'\n const net = isNonNegativeKind ? Math.abs(rawNet) : rawNet\n const gross = isNonNegativeKind ? Math.abs(rawGross) : rawGross\n const taxRate = extractAdjustmentTaxRate(adj)\n const taxPortion = taxRate !== null ? round(gross - net) : 0\n switch (adj.kind) {\n case 'discount':\n discountTotal += net\n subtotalNet = Math.max(subtotalNet - net, 0)\n subtotalGross = Math.max(subtotalGross - gross, 0)\n if (taxPortion) {\n taxTotal = round(taxTotal - taxPortion)\n }\n break\n case 'tax':\n taxTotal += gross || net\n subtotalGross += gross || net\n break\n case 'shipping':\n shippingNet += net\n shippingGross += gross\n subtotalNet += net\n subtotalGross += gross\n if (taxPortion) {\n taxTotal += taxPortion\n }\n break\n case 'surcharge':\n surchargeTotal += net || gross\n subtotalNet += net || gross\n subtotalGross += gross || net\n if (taxPortion) {\n taxTotal += taxPortion\n }\n break\n default:\n // `return` (credit) adjustments are handled by the dedicated loop below;\n // skip them here so an order-scoped return is not counted twice.\n if (adj.kind === 'return') break\n // Custom / operator-defined kinds carry an operator-controlled sign\n // (positive adds, negative credits). Fold the raw signed amount into the\n // grand total so a persisted adjustment can never be silently dropped\n // from the headline total while still appearing in the itemized\n // breakdown (#4052). No abs()/clamp here: unlike the sign-constrained\n // kinds above, custom kinds are intentionally unconstrained\n // (see enforceAdjustmentSign).\n subtotalNet += net\n subtotalGross += gross\n if (taxPortion) {\n taxTotal += taxPortion\n }\n break\n }\n }\n\n // Line-scoped and any other return (credit) adjustments reduce grand total.\n // Sign is normalized to negative regardless of the stored sign so a positive\n // amountNet / amountGross can never inflate totals (issue #1705).\n for (const adj of resolvedAdjustments) {\n if (adj.kind !== 'return') continue\n const net = toNumber(adj.amountNet, toNumber(adj.amountGross))\n const gross = toNumber(adj.amountGross, net)\n const netDelta = -Math.abs(net)\n const grossDelta = -Math.abs(gross)\n subtotalNet = Math.max(subtotalNet + netDelta, 0)\n subtotalGross = Math.max(subtotalGross + grossDelta, 0)\n }\n\n const grandTotalNet = round(subtotalNet)\n const grandTotalGross = round(subtotalGross)\n const paidTotalAmount = Math.max(toNumber(params.existingTotals?.paidTotalAmount, 0), 0)\n const refundedTotalAmount = Math.max(toNumber(params.existingTotals?.refundedTotalAmount, 0), 0)\n const outstandingAmount = Math.max(grandTotalGross - paidTotalAmount + refundedTotalAmount, 0)\n\n return {\n kind: documentKind,\n currencyCode,\n lines,\n adjustments: resolvedAdjustments,\n metadata: {},\n totals: {\n subtotalNetAmount: round(subtotalNet),\n subtotalGrossAmount: round(subtotalGross),\n discountTotalAmount: round(discountTotal),\n taxTotalAmount: round(taxTotal),\n shippingNetAmount: round(shippingNet),\n shippingGrossAmount: round(shippingGross),\n surchargeTotalAmount: round(surchargeTotal),\n grandTotalNetAmount: grandTotalNet,\n grandTotalGrossAmount: grandTotalGross,\n paidTotalAmount,\n refundedTotalAmount,\n outstandingAmount,\n },\n }\n}\n\nclass SalesCalculationRegistry {\n private lineCalculators: SalesLineCalculationHook[] = []\n private totalsCalculators: SalesTotalsCalculationHook[] = []\n\n registerLineCalculator(hook: SalesLineCalculationHook, opts?: { prepend?: boolean }): () => void {\n if (opts?.prepend) this.lineCalculators.unshift(hook)\n else this.lineCalculators.push(hook)\n return () => {\n this.lineCalculators = this.lineCalculators.filter((item) => item !== hook)\n }\n }\n\n registerTotalsCalculator(hook: SalesTotalsCalculationHook, opts?: { prepend?: boolean }): () => void {\n if (opts?.prepend) this.totalsCalculators.unshift(hook)\n else this.totalsCalculators.push(hook)\n return () => {\n this.totalsCalculators = this.totalsCalculators.filter((item) => item !== hook)\n }\n }\n\n async calculateLine(opts: CalculateLineOptions): Promise<SalesLineCalculationResult> {\n const { documentKind, line, context, eventBus } = opts\n let current = buildBaseLineResult(line)\n\n if (eventBus) {\n await eventBus.emitEvent('sales.line.calculate.before', {\n documentKind,\n line,\n context,\n result: current,\n setResult(next: SalesLineCalculationResult) {\n current = next\n },\n })\n }\n\n for (const hook of this.lineCalculators) {\n const next = await hook({ documentKind, line, context, current })\n if (next) current = next\n }\n\n if (eventBus) {\n await eventBus.emitEvent('sales.line.calculate.after', {\n documentKind,\n line,\n context,\n result: current,\n setResult(next: SalesLineCalculationResult) {\n current = next\n },\n })\n }\n\n return current\n }\n\n async calculateDocument(opts: CalculateDocumentOptions): Promise<SalesDocumentCalculationResult> {\n const { documentKind, lines, adjustments = [], context, eventBus, existingTotals } = opts\n const resolvedLines: SalesLineCalculationResult[] = []\n\n for (const line of lines) {\n const result = await this.calculateLine({ documentKind, line, context, eventBus })\n resolvedLines.push(result)\n }\n\n let current = buildBaseDocumentResult({\n documentKind,\n lines: resolvedLines,\n adjustments,\n currencyCode: context.currencyCode,\n existingTotals,\n })\n\n if (eventBus) {\n await eventBus.emitEvent('sales.document.calculate.before', {\n documentKind,\n lines: resolvedLines,\n context,\n adjustments,\n result: current,\n setResult(next: SalesDocumentCalculationResult) {\n current = next\n },\n })\n }\n\n for (const hook of this.totalsCalculators) {\n const next = await hook({\n documentKind,\n lines: resolvedLines,\n existingAdjustments: adjustments,\n context,\n current,\n eventBus,\n })\n if (next) current = next\n }\n\n if (eventBus) {\n await eventBus.emitEvent('sales.document.calculate.after', {\n documentKind,\n lines: resolvedLines,\n context,\n adjustments,\n result: current,\n setResult(next: SalesDocumentCalculationResult) {\n current = next\n },\n })\n }\n\n // Payment totals (paid/refunded) are authoritative inputs, not derived from\n // lines or adjustments. Totals calculators rebuild the document result from\n // lines+adjustments and would otherwise reset paid/refunded to 0 (and\n // outstanding back to the full grand total), producing a stale paid/\n // outstanding display after a payment. Re-apply the input totals last and\n // recompute outstanding against the post-calculation grand total.\n if (existingTotals) {\n const paidTotalAmount = Math.max(toNumber(existingTotals.paidTotalAmount, 0), 0)\n const refundedTotalAmount = Math.max(toNumber(existingTotals.refundedTotalAmount, 0), 0)\n current.totals = {\n ...current.totals,\n paidTotalAmount,\n refundedTotalAmount,\n outstandingAmount: round(\n Math.max(current.totals.grandTotalGrossAmount - paidTotalAmount + refundedTotalAmount, 0)\n ),\n }\n }\n\n return current\n }\n}\n\nexport function createSalesCalculationRegistry(): SalesCalculationRegistry {\n return new SalesCalculationRegistry()\n}\n\nexport const salesCalculations = createSalesCalculationRegistry()\n\nexport async function calculateLine(\n opts: CalculateLineOptions\n): Promise<SalesLineCalculationResult> {\n return salesCalculations.calculateLine(opts)\n}\n\nexport async function calculateDocumentTotals(\n opts: CalculateDocumentOptions\n): Promise<SalesDocumentCalculationResult> {\n return salesCalculations.calculateDocument(opts)\n}\n\nexport function registerSalesLineCalculator(\n hook: SalesLineCalculationHook,\n opts?: { prepend?: boolean }\n): () => void {\n return salesCalculations.registerLineCalculator(hook, opts)\n}\n\nexport function registerSalesTotalsCalculator(\n hook: SalesTotalsCalculationHook,\n opts?: { prepend?: boolean }\n): () => void {\n return salesCalculations.registerTotalsCalculator(hook, opts)\n}\n\nexport function rebuildDocumentResult(params: {\n documentKind: SalesDocumentKind\n currencyCode: string\n lines: SalesLineCalculationResult[]\n adjustments: SalesAdjustmentDraft[]\n metadata?: Record<string, unknown>\n}): SalesDocumentCalculationResult {\n const result = buildBaseDocumentResult({\n documentKind: params.documentKind,\n lines: params.lines,\n adjustments: params.adjustments,\n currencyCode: params.currencyCode,\n })\n result.metadata = params.metadata ?? {}\n return result\n}\n"],
5
+ "mappings": "AAaA,SAAS,SAAS,OAAgB,WAAW,GAAW;AACtD,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG,QAAO;AAChE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,MAAM,CAAC,OAAO,MAAM,OAAO,KAAK,CAAC,GAAG;AACpF,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAEA,SAAS,MAAM,OAAuB;AACpC,SAAO,KAAK,OAAO,QAAQ,OAAO,WAAW,GAAG,IAAI;AACtD;AAEA,SAAS,yBAAyB,YAAiD;AACjF,QAAM,WAAY,WAAW,YAAY,CAAC;AAC1C,QAAM,YACJ,SAAS,WACR,UAAkB,YAClB,UAAkB,gBAClB,UAAkB,kBACnB;AACF,QAAM,SAAS,SAAS,WAAW,GAAG;AACtC,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AAEA,SAAS,yBACP,aACA,SACA,WACwB;AACxB,SAAO,YAAY,IAAI,CAAC,QAAQ;AAC9B,UAAM,OAAO,SAAS,IAAI,MAAM,GAAG;AACnC,UAAM,UAAU,yBAAyB,GAAG;AAC5C,UAAM,eAAe,OAAO,SAAS,SAAS,IAAI,WAAW,GAAG,CAAC;AACjE,UAAM,iBAAiB,OAAO,SAAS,SAAS,IAAI,aAAa,GAAG,CAAC;AACrE,UAAM,UAAU,OAAO,SAAS,IAAI,KAAK,CAAC,gBAAgB,CAAC;AAC3D,UAAM,aAAa,YAAY;AAC/B,QAAI,YAAY,SAAS,IAAI,WAAW,GAAG;AAC3C,QAAI,cAAc,SAAS,IAAI,aAAa,GAAG;AAE/C,QAAI,SAAS;AACX,YAAM,aAAc,OAAkB;AACtC,kBAAY,MAAM,KAAK,IAAI,SAAS,CAAC,IAAI,UAAU;AACnD,UAAI,IAAI,SAAS,OAAO;AACtB,sBAAc;AAAA,MAChB,WAAW,YAAY;AACrB,sBAAc,MAAM,aAAa,IAAK,UAAqB,IAAI;AAAA,MACjE,OAAO;AACL,sBAAc,MAAM,KAAK,IAAI,WAAW,CAAC,IAAI,UAAU;AAAA,MACzD;AAAA,IACF,OAAO;AACL,UAAI,CAAC,OAAO,SAAS,SAAS,KAAK,OAAO,SAAS,WAAW,KAAK,YAAY;AAC7E,oBAAY,MAAO,eAA0B,IAAK,UAAqB,IAAI;AAAA,MAC7E;AACA,UAAI,CAAC,OAAO,SAAS,WAAW,KAAK,OAAO,SAAS,SAAS,KAAK,YAAY;AAC7E,sBAAc,MAAO,aAAwB,IAAK,UAAqB,IAAI;AAAA,MAC7E;AAAA,IACF;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,WAAW,OAAO,SAAS,SAAS,IAAI,YAAY,IAAI;AAAA,MACxD,aAAa,OAAO,SAAS,WAAW,IAAI,cAAc,IAAI;AAAA,IAChE;AAAA,EACF,CAAC;AACH;AAEA,SAAS,oBAAoB,MAAqD;AAChF,QAAM,WAAW,KAAK,IAAI,SAAS,KAAK,UAAU,CAAC,GAAG,CAAC;AACvD,QAAM,UAAU,SAAS,KAAK,SAAS,CAAC,IAAI;AAC5C,QAAM,UACJ,KAAK,iBACJ,KAAK,mBAAmB,QAAQ,KAAK,mBAAmB,SACrD,SAAS,KAAK,cAAc,KAAK,IAAI,WACrC;AACN,QAAM,kBACJ,KAAK,mBACJ,KAAK,oBAAoB,QAAQ,KAAK,oBAAoB,SACvD,SAAS,KAAK,iBAAiB,CAAC,IAAI,MAAM,SAAS,SAAS,CAAC,IAC7D;AAEN,QAAM,4BAA4B,SAAS,SAAS,CAAC,IAAI;AACzD,QAAM,gBAAgB,KAAK,IAAI,KAAK,IAAI,kBAAkB,UAAU,CAAC,GAAG,yBAAyB;AACjG,QAAM,cAAc,KAAK,IAAI,4BAA4B,eAAe,CAAC;AACzE,QAAM,oBAAoB,KAAK,cAAc,QAAQ,KAAK,cAAc;AACxE,MAAI,YAAY,oBACZ,SAAS,KAAK,WAAW,CAAC,IAC1B,MAAM,cAAc,KAAK,IAAI,SAAS,CAAC,CAAC;AAC5C,QAAM,gBACJ,KAAK,qBAAqB,QAAQ,KAAK,qBAAqB,SACxD,SAAS,KAAK,kBAAkB,CAAC,IACjC,MAAM,cAAc,SAAS;AAMnC,MAAI,CAAC,qBAAqB,aAAa,GAAG;AACxC,UAAM,gBAAgB,MAAM,gBAAgB,WAAW;AACvD,QAAI,gBAAgB,EAAG,aAAY;AAAA,EACrC;AAEA,SAAO;AAAA,IACL;AAAA,IACA,WAAW,MAAM,WAAW;AAAA,IAC5B,aAAa,MAAM,aAAa;AAAA,IAChC,WAAW,MAAM,SAAS;AAAA,IAC1B,gBAAgB,MAAM,aAAa;AAAA,IACnC,aAAa,CAAC;AAAA,EAChB;AACF;AAEA,SAAS,wBAAwB,QAME;AACjC,QAAM,EAAE,cAAc,OAAO,aAAa,aAAa,IAAI;AAC3D,QAAM,qBAAqB,CAAC,GAAI,eAAe,CAAC,CAAE,EAAE;AAAA,IAClD,CAAC,GAAG,OAAO,EAAE,YAAY,MAAM,EAAE,YAAY;AAAA,EAC/C;AACA,MAAI,kBAAkB;AACtB,MAAI,oBAAoB;AACxB,MAAI,cAAc;AAClB,MAAI,gBAAgB;AACpB,MAAI,gBAAgB;AACpB,MAAI,WAAW;AACf,MAAI,cAAc;AAClB,MAAI,gBAAgB;AACpB,MAAI,iBAAiB;AAErB,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,SAAS,KAAK,WAAW,CAAC;AACtC,UAAM,QAAQ,SAAS,KAAK,aAAa,CAAC;AAC1C,mBAAe;AACf,qBAAiB;AACjB,uBAAmB;AACnB,yBAAqB;AACrB,qBAAiB,SAAS,KAAK,gBAAgB,CAAC;AAChD,gBAAY,SAAS,KAAK,WAAW,CAAC;AAAA,EACxC;AAEA,QAAM,sBAAsB,yBAAyB,oBAAoB,iBAAiB,iBAAiB;AAC3G,QAAM,oBAAoB,oBAAoB;AAAA,IAC5C,CAAC,QAAQ,CAAC,IAAI,SAAS,IAAI,UAAU;AAAA,EACvC;AAEA,aAAW,OAAO,mBAAmB;AACnC,UAAM,SAAS,SAAS,IAAI,WAAW,SAAS,IAAI,WAAW,CAAC;AAChE,UAAM,WAAW,SAAS,IAAI,aAAa,MAAM;AAOjD,UAAM,oBACJ,IAAI,SAAS,cACb,IAAI,SAAS,eACb,IAAI,SAAS,cACb,IAAI,SAAS;AACf,UAAM,MAAM,oBAAoB,KAAK,IAAI,MAAM,IAAI;AACnD,UAAM,QAAQ,oBAAoB,KAAK,IAAI,QAAQ,IAAI;AACvD,UAAM,UAAU,yBAAyB,GAAG;AAC5C,UAAM,aAAa,YAAY,OAAO,MAAM,QAAQ,GAAG,IAAI;AAC3D,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK;AACH,yBAAiB;AACjB,sBAAc,KAAK,IAAI,cAAc,KAAK,CAAC;AAC3C,wBAAgB,KAAK,IAAI,gBAAgB,OAAO,CAAC;AACjD,YAAI,YAAY;AACd,qBAAW,MAAM,WAAW,UAAU;AAAA,QACxC;AACA;AAAA,MACF,KAAK;AACH,oBAAY,SAAS;AACrB,yBAAiB,SAAS;AAC1B;AAAA,MACF,KAAK;AACH,uBAAe;AACf,yBAAiB;AACjB,uBAAe;AACf,yBAAiB;AACjB,YAAI,YAAY;AACd,sBAAY;AAAA,QACd;AACA;AAAA,MACF,KAAK;AACH,0BAAkB,OAAO;AACzB,uBAAe,OAAO;AACtB,yBAAiB,SAAS;AAC1B,YAAI,YAAY;AACd,sBAAY;AAAA,QACd;AACA;AAAA,MACF;AAGE,YAAI,IAAI,SAAS,SAAU;AAQ3B,uBAAe;AACf,yBAAiB;AACjB,YAAI,YAAY;AACd,sBAAY;AAAA,QACd;AACA;AAAA,IACJ;AAAA,EACF;AAKA,aAAW,OAAO,qBAAqB;AACrC,QAAI,IAAI,SAAS,SAAU;AAC3B,UAAM,MAAM,SAAS,IAAI,WAAW,SAAS,IAAI,WAAW,CAAC;AAC7D,UAAM,QAAQ,SAAS,IAAI,aAAa,GAAG;AAC3C,UAAM,WAAW,CAAC,KAAK,IAAI,GAAG;AAC9B,UAAM,aAAa,CAAC,KAAK,IAAI,KAAK;AAClC,kBAAc,KAAK,IAAI,cAAc,UAAU,CAAC;AAChD,oBAAgB,KAAK,IAAI,gBAAgB,YAAY,CAAC;AAAA,EACxD;AAEA,QAAM,gBAAgB,MAAM,WAAW;AACvC,QAAM,kBAAkB,MAAM,aAAa;AAC3C,QAAM,kBAAkB,KAAK,IAAI,SAAS,OAAO,gBAAgB,iBAAiB,CAAC,GAAG,CAAC;AACvF,QAAM,sBAAsB,KAAK,IAAI,SAAS,OAAO,gBAAgB,qBAAqB,CAAC,GAAG,CAAC;AAC/F,QAAM,oBAAoB,KAAK,IAAI,kBAAkB,kBAAkB,qBAAqB,CAAC;AAE7F,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,UAAU,CAAC;AAAA,IACX,QAAQ;AAAA,MACN,mBAAmB,MAAM,WAAW;AAAA,MACpC,qBAAqB,MAAM,aAAa;AAAA,MACxC,qBAAqB,MAAM,aAAa;AAAA,MACxC,gBAAgB,MAAM,QAAQ;AAAA,MAC9B,mBAAmB,MAAM,WAAW;AAAA,MACpC,qBAAqB,MAAM,aAAa;AAAA,MACxC,sBAAsB,MAAM,cAAc;AAAA,MAC1C,qBAAqB;AAAA,MACrB,uBAAuB;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,yBAAyB;AAAA,EAA/B;AACE,SAAQ,kBAA8C,CAAC;AACvD,SAAQ,oBAAkD,CAAC;AAAA;AAAA,EAE3D,uBAAuB,MAAgC,MAA0C;AAC/F,QAAI,MAAM,QAAS,MAAK,gBAAgB,QAAQ,IAAI;AAAA,QAC/C,MAAK,gBAAgB,KAAK,IAAI;AACnC,WAAO,MAAM;AACX,WAAK,kBAAkB,KAAK,gBAAgB,OAAO,CAAC,SAAS,SAAS,IAAI;AAAA,IAC5E;AAAA,EACF;AAAA,EAEA,yBAAyB,MAAkC,MAA0C;AACnG,QAAI,MAAM,QAAS,MAAK,kBAAkB,QAAQ,IAAI;AAAA,QACjD,MAAK,kBAAkB,KAAK,IAAI;AACrC,WAAO,MAAM;AACX,WAAK,oBAAoB,KAAK,kBAAkB,OAAO,CAAC,SAAS,SAAS,IAAI;AAAA,IAChF;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,MAAiE;AACnF,UAAM,EAAE,cAAc,MAAM,SAAS,SAAS,IAAI;AAClD,QAAI,UAAU,oBAAoB,IAAI;AAEtC,QAAI,UAAU;AACZ,YAAM,SAAS,UAAU,+BAA+B;AAAA,QACtD;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,UAAU,MAAkC;AAC1C,oBAAU;AAAA,QACZ;AAAA,MACF,CAAC;AAAA,IACH;AAEA,eAAW,QAAQ,KAAK,iBAAiB;AACvC,YAAM,OAAO,MAAM,KAAK,EAAE,cAAc,MAAM,SAAS,QAAQ,CAAC;AAChE,UAAI,KAAM,WAAU;AAAA,IACtB;AAEA,QAAI,UAAU;AACZ,YAAM,SAAS,UAAU,8BAA8B;AAAA,QACrD;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,UAAU,MAAkC;AAC1C,oBAAU;AAAA,QACZ;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,kBAAkB,MAAyE;AAC/F,UAAM,EAAE,cAAc,OAAO,cAAc,CAAC,GAAG,SAAS,UAAU,eAAe,IAAI;AACrF,UAAM,gBAA8C,CAAC;AAErD,eAAW,QAAQ,OAAO;AACxB,YAAM,SAAS,MAAM,KAAK,cAAc,EAAE,cAAc,MAAM,SAAS,SAAS,CAAC;AACjF,oBAAc,KAAK,MAAM;AAAA,IAC3B;AAEA,QAAI,UAAU,wBAAwB;AAAA,MACpC;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA,cAAc,QAAQ;AAAA,MACtB;AAAA,IACF,CAAC;AAED,QAAI,UAAU;AACZ,YAAM,SAAS,UAAU,mCAAmC;AAAA,QAC1D;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,UAAU,MAAsC;AAC9C,oBAAU;AAAA,QACZ;AAAA,MACF,CAAC;AAAA,IACH;AAEA,eAAW,QAAQ,KAAK,mBAAmB;AACzC,YAAM,OAAO,MAAM,KAAK;AAAA,QACtB;AAAA,QACA,OAAO;AAAA,QACP,qBAAqB;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,UAAI,KAAM,WAAU;AAAA,IACtB;AAEA,QAAI,UAAU;AACZ,YAAM,SAAS,UAAU,kCAAkC;AAAA,QACzD;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,UAAU,MAAsC;AAC9C,oBAAU;AAAA,QACZ;AAAA,MACF,CAAC;AAAA,IACH;AAQA,QAAI,gBAAgB;AAClB,YAAM,kBAAkB,KAAK,IAAI,SAAS,eAAe,iBAAiB,CAAC,GAAG,CAAC;AAC/E,YAAM,sBAAsB,KAAK,IAAI,SAAS,eAAe,qBAAqB,CAAC,GAAG,CAAC;AACvF,cAAQ,SAAS;AAAA,QACf,GAAG,QAAQ;AAAA,QACX;AAAA,QACA;AAAA,QACA,mBAAmB;AAAA,UACjB,KAAK,IAAI,QAAQ,OAAO,wBAAwB,kBAAkB,qBAAqB,CAAC;AAAA,QAC1F;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,iCAA2D;AACzE,SAAO,IAAI,yBAAyB;AACtC;AAEO,MAAM,oBAAoB,+BAA+B;AAEhE,eAAsB,cACpB,MACqC;AACrC,SAAO,kBAAkB,cAAc,IAAI;AAC7C;AAEA,eAAsB,wBACpB,MACyC;AACzC,SAAO,kBAAkB,kBAAkB,IAAI;AACjD;AAEO,SAAS,4BACd,MACA,MACY;AACZ,SAAO,kBAAkB,uBAAuB,MAAM,IAAI;AAC5D;AAEO,SAAS,8BACd,MACA,MACY;AACZ,SAAO,kBAAkB,yBAAyB,MAAM,IAAI;AAC9D;AAEO,SAAS,sBAAsB,QAMH;AACjC,QAAM,SAAS,wBAAwB;AAAA,IACrC,cAAc,OAAO;AAAA,IACrB,OAAO,OAAO;AAAA,IACd,aAAa,OAAO;AAAA,IACpB,cAAc,OAAO;AAAA,EACvB,CAAC;AACD,SAAO,WAAW,OAAO,YAAY,CAAC;AACtC,SAAO;AACT;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.7-develop.6552.1.5208a19369",
3
+ "version": "0.6.7-develop.6553.1.2ec7246e04",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -253,16 +253,16 @@
253
253
  "zod": "^4.4.3"
254
254
  },
255
255
  "peerDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.7-develop.6552.1.5208a19369",
257
- "@open-mercato/shared": "0.6.7-develop.6552.1.5208a19369",
258
- "@open-mercato/ui": "0.6.7-develop.6552.1.5208a19369",
256
+ "@open-mercato/ai-assistant": "0.6.7-develop.6553.1.2ec7246e04",
257
+ "@open-mercato/shared": "0.6.7-develop.6553.1.2ec7246e04",
258
+ "@open-mercato/ui": "0.6.7-develop.6553.1.2ec7246e04",
259
259
  "react": "^19.0.0",
260
260
  "react-dom": "^19.0.0"
261
261
  },
262
262
  "devDependencies": {
263
- "@open-mercato/ai-assistant": "0.6.7-develop.6552.1.5208a19369",
264
- "@open-mercato/shared": "0.6.7-develop.6552.1.5208a19369",
265
- "@open-mercato/ui": "0.6.7-develop.6552.1.5208a19369",
263
+ "@open-mercato/ai-assistant": "0.6.7-develop.6553.1.2ec7246e04",
264
+ "@open-mercato/shared": "0.6.7-develop.6553.1.2ec7246e04",
265
+ "@open-mercato/ui": "0.6.7-develop.6553.1.2ec7246e04",
266
266
  "@testing-library/dom": "^10.4.1",
267
267
  "@testing-library/jest-dom": "^6.9.1",
268
268
  "@testing-library/react": "^16.3.1",
@@ -208,6 +208,21 @@ function buildBaseDocumentResult(params: {
208
208
  }
209
209
  break
210
210
  default:
211
+ // `return` (credit) adjustments are handled by the dedicated loop below;
212
+ // skip them here so an order-scoped return is not counted twice.
213
+ if (adj.kind === 'return') break
214
+ // Custom / operator-defined kinds carry an operator-controlled sign
215
+ // (positive adds, negative credits). Fold the raw signed amount into the
216
+ // grand total so a persisted adjustment can never be silently dropped
217
+ // from the headline total while still appearing in the itemized
218
+ // breakdown (#4052). No abs()/clamp here: unlike the sign-constrained
219
+ // kinds above, custom kinds are intentionally unconstrained
220
+ // (see enforceAdjustmentSign).
221
+ subtotalNet += net
222
+ subtotalGross += gross
223
+ if (taxPortion) {
224
+ taxTotal += taxPortion
225
+ }
211
226
  break
212
227
  }
213
228
  }