@open-mercato/core 0.10.1-develop.7723.1.3b2ea82ea7 → 0.10.1-develop.7724.1.95e4e7d38e

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.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/sales_order/index.js +2 -0
  3. package/dist/generated/entities/sales_order/index.js.map +2 -2
  4. package/dist/generated/entities/sales_order_line/index.js +2 -0
  5. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/sales/api/documents/factory.js +3 -0
  9. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  10. package/dist/modules/sales/api/order-lines/route.js +2 -1
  11. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  12. package/dist/modules/sales/backend/sales/documents/[id]/page.js +83 -6
  13. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  14. package/dist/modules/sales/commands/documents.js +141 -18
  15. package/dist/modules/sales/commands/documents.js.map +2 -2
  16. package/dist/modules/sales/commands/returns.js +24 -7
  17. package/dist/modules/sales/commands/returns.js.map +2 -2
  18. package/dist/modules/sales/components/documents/AdjustmentsSection.js +17 -11
  19. package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
  20. package/dist/modules/sales/components/documents/DocumentTotals.js +21 -3
  21. package/dist/modules/sales/components/documents/DocumentTotals.js.map +2 -2
  22. package/dist/modules/sales/components/documents/ItemsSection.js +23 -12
  23. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  24. package/dist/modules/sales/data/entities.js +10 -0
  25. package/dist/modules/sales/data/entities.js.map +2 -2
  26. package/dist/modules/sales/data/validators.js +9 -1
  27. package/dist/modules/sales/data/validators.js.map +2 -2
  28. package/dist/modules/sales/lib/calculations.js +85 -6
  29. package/dist/modules/sales/lib/calculations.js.map +2 -2
  30. package/dist/modules/sales/lib/externalAmounts.js +143 -0
  31. package/dist/modules/sales/lib/externalAmounts.js.map +7 -0
  32. package/dist/modules/sales/lib/lineSnapshots.js +6 -1
  33. package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
  34. package/dist/modules/sales/lib/makeSalesLineRoute.js +8 -2
  35. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  36. package/dist/modules/sales/lib/providers/totals.js +3 -2
  37. package/dist/modules/sales/lib/providers/totals.js.map +2 -2
  38. package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js +19 -0
  39. package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js.map +7 -0
  40. package/generated/entities/sales_order/index.ts +1 -0
  41. package/generated/entities/sales_order_line/index.ts +1 -0
  42. package/generated/entity-fields-registry.ts +2 -0
  43. package/package.json +7 -7
  44. package/src/modules/sales/api/documents/factory.ts +3 -0
  45. package/src/modules/sales/api/order-lines/route.ts +2 -1
  46. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +96 -0
  47. package/src/modules/sales/commands/documents.ts +219 -44
  48. package/src/modules/sales/commands/returns.ts +27 -3
  49. package/src/modules/sales/components/documents/AdjustmentsSection.tsx +34 -25
  50. package/src/modules/sales/components/documents/DocumentTotals.tsx +26 -2
  51. package/src/modules/sales/components/documents/ItemsSection.tsx +27 -12
  52. package/src/modules/sales/data/entities.ts +14 -0
  53. package/src/modules/sales/data/validators.ts +10 -0
  54. package/src/modules/sales/i18n/de.json +12 -0
  55. package/src/modules/sales/i18n/en.json +12 -0
  56. package/src/modules/sales/i18n/es.json +12 -0
  57. package/src/modules/sales/i18n/ko.json +12 -0
  58. package/src/modules/sales/i18n/pl.json +12 -0
  59. package/src/modules/sales/lib/calculations.ts +99 -4
  60. package/src/modules/sales/lib/externalAmounts.ts +174 -0
  61. package/src/modules/sales/lib/lineSnapshots.ts +6 -1
  62. package/src/modules/sales/lib/makeSalesLineRoute.ts +8 -0
  63. package/src/modules/sales/lib/providers/totals.ts +5 -2
  64. package/src/modules/sales/lib/types.ts +19 -2
  65. package/src/modules/sales/migrations/.snapshot-open-mercato.json +33 -1
  66. package/src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts +25 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/lib/calculations.ts"],
4
- "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\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\nconst logger = createLogger('sales')\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\n// The engine rounds to the 4 decimals the numeric columns carry, but callers\n// work in money at 2, so an exact comparison would report half a cent of\n// honest rounding as a mismatch. Half a minor unit is the widest divergence\n// that cannot be a real discrepancy and the narrowest that silences that noise.\nconst NET_RECONCILIATION_TOLERANCE = 0.005\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\n// `discount_amount` stores the discount for the WHOLE line, while\n// `discount_percent` records the operator's intent. The percentage therefore\n// wins whenever it is set: a stored amount is only ever its cached result, and\n// because the column is NOT NULL DEFAULT '0' a stored 0 cannot be told apart\n// from \"no discount supplied\" \u2014 so it counts as absent rather than as a\n// suppressing value. That is what makes recalculation idempotent, and what lets\n// a row whose amount the old engine dropped or re-inflated heal itself on the\n// next pass. Spec: .ai/specs/2026-08-07-sales-line-discount-amount-contract.md.\nfunction resolveLineDiscountTotal(\n line: SalesLineSnapshot,\n netSubtotalBeforeDiscount: number,\n quantity: number,\n): number {\n const percent = toNumber(line.discountPercent, 0)\n if (line.discountPercent !== null && line.discountPercent !== undefined && percent !== 0) {\n return (percent / 100) * netSubtotalBeforeDiscount\n }\n\n const amount = toNumber(line.discountAmount, 0)\n if (line.discountAmount === null || line.discountAmount === undefined || amount === 0) return 0\n\n // A snapshot rebuilt from a persisted row already holds a line total, so it\n // is never multiplied out again. Anything else came from a caller and keeps\n // the per-unit meaning the API has always documented unless the caller says\n // otherwise.\n if (line.discountAmountFromStoredRow === true) return amount\n return line.discountAmountBasis === 'line' ? amount : amount * quantity\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 netSubtotalBeforeDiscount = toNumber(unitNet, 0) * quantity\n const discountTotal = Math.min(\n Math.max(resolveLineDiscountTotal(line, netSubtotalBeforeDiscount, quantity), 0),\n netSubtotalBeforeDiscount,\n )\n const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0)\n // Unlike totalGrossAmount below, a supplied totalNetAmount is never honoured\n // verbatim \u2014 net always comes from unitPriceNet/discount so it stays\n // internally consistent with them. A caller-supplied value is still\n // reconciled against the computed one so a divergence (e.g. a mis-read\n // discount) surfaces instead of being silently discarded (#5644).\n //\n // Only a caller's value is reconciled: a snapshot rebuilt from a persisted\n // row (`totalsFromStoredRow`) carries the engine's own previous output, and\n // on a row the discount contract still has to heal that value is *supposed*\n // to differ from the recomputed net. Warning about it would drown the caller\n // signal this exists for in one line per line per recalculation.\n if (line.totalsFromStoredRow !== true && line.totalNetAmount !== null && line.totalNetAmount !== undefined) {\n const computedNetAmount = round(netSubtotal)\n const suppliedNetAmount = toNumber(line.totalNetAmount, NaN)\n if (!Number.isFinite(suppliedNetAmount)) {\n // Falling back to the computed value here would compare equal and log\n // nothing \u2014 the same silent discard #5644 exists to end.\n logger.warn('Sales line totalNetAmount is not a finite number; the computed value is used', {\n lineId: line.id ?? null,\n productId: line.productId ?? null,\n suppliedTotalNetAmount: line.totalNetAmount,\n computedNetAmount,\n })\n } else if (Math.abs(round(suppliedNetAmount) - computedNetAmount) > NET_RECONCILIATION_TOLERANCE) {\n logger.warn('Sales line totalNetAmount does not match the computed net amount; the computed value is used', {\n lineId: line.id ?? null,\n productId: line.productId ?? null,\n suppliedTotalNetAmount: round(suppliedNetAmount),\n computedNetAmount,\n })\n }\n }\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": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n type SalesAdjustmentDraft,\n type SalesAmountsMode,\n type SalesDocumentAmounts,\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\nconst logger = createLogger('sales')\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\n// The engine rounds to the 4 decimals the numeric columns carry, but callers\n// work in money at 2, so an exact comparison would report half a cent of\n// honest rounding as a mismatch. Half a minor unit is the widest divergence\n// that cannot be a real discrepancy and the narrowest that silences that noise.\nconst NET_RECONCILIATION_TOLERANCE = 0.005\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\n// `discount_amount` stores the discount for the WHOLE line, while\n// `discount_percent` records the operator's intent. The percentage therefore\n// wins whenever it is set: a stored amount is only ever its cached result, and\n// because the column is NOT NULL DEFAULT '0' a stored 0 cannot be told apart\n// from \"no discount supplied\" \u2014 so it counts as absent rather than as a\n// suppressing value. That is what makes recalculation idempotent, and what lets\n// a row whose amount the old engine dropped or re-inflated heal itself on the\n// next pass. Spec: .ai/specs/2026-08-07-sales-line-discount-amount-contract.md.\nfunction resolveLineDiscountTotal(\n line: SalesLineSnapshot,\n netSubtotalBeforeDiscount: number,\n quantity: number,\n): number {\n const percent = toNumber(line.discountPercent, 0)\n if (line.discountPercent !== null && line.discountPercent !== undefined && percent !== 0) {\n return (percent / 100) * netSubtotalBeforeDiscount\n }\n\n const amount = toNumber(line.discountAmount, 0)\n if (line.discountAmount === null || line.discountAmount === undefined || amount === 0) return 0\n\n // A snapshot rebuilt from a persisted row already holds a line total, so it\n // is never multiplied out again. Anything else came from a caller and keeps\n // the per-unit meaning the API has always documented unless the caller says\n // otherwise.\n if (line.discountAmountFromStoredRow === true) return amount\n return line.discountAmountBasis === 'line' ? amount : amount * quantity\n}\n\n/** The header fields a caller owns under `external`; the rest stay core-derived. */\nexport const EXTERNAL_HEADER_FIELDS = [\n 'subtotalNetAmount',\n 'subtotalGrossAmount',\n 'discountTotalAmount',\n 'taxTotalAmount',\n 'shippingNetAmount',\n 'shippingGrossAmount',\n 'surchargeTotalAmount',\n 'grandTotalNetAmount',\n 'grandTotalGrossAmount',\n] as const\n\nexport function isExternalAmountsMode(mode: SalesAmountsMode | null | undefined): boolean {\n return mode === 'external'\n}\n\nfunction resolveSuppliedTotals(\n supplied: Partial<SalesDocumentAmounts> | null | undefined\n): Pick<SalesDocumentAmounts, (typeof EXTERNAL_HEADER_FIELDS)[number]> {\n const resolved = {} as Record<(typeof EXTERNAL_HEADER_FIELDS)[number], number>\n for (const field of EXTERNAL_HEADER_FIELDS) {\n resolved[field] = round(toNumber(supplied?.[field], 0))\n }\n return resolved\n}\n\n// The derived discount is signed and unclamped: a markup is a negative discount.\nfunction buildExternalLineResult(line: SalesLineSnapshot): SalesLineCalculationResult {\n const quantity = Math.max(toNumber(line.quantity, 0), 0)\n const netAmount = round(toNumber(line.totalNetAmount, 0))\n return {\n line,\n netAmount,\n grossAmount: round(toNumber(line.totalGrossAmount, 0)),\n taxAmount: round(toNumber(line.taxAmount, 0)),\n discountAmount: round(toNumber(line.unitPriceNet, 0) * quantity - netAmount),\n adjustments: [],\n }\n}\n\nfunction buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResult {\n if (isExternalAmountsMode(line.amountsMode)) return buildExternalLineResult(line)\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 netSubtotalBeforeDiscount = toNumber(unitNet, 0) * quantity\n const discountTotal = Math.min(\n Math.max(resolveLineDiscountTotal(line, netSubtotalBeforeDiscount, quantity), 0),\n netSubtotalBeforeDiscount,\n )\n const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0)\n // Unlike totalGrossAmount below, a supplied totalNetAmount is never honoured\n // verbatim \u2014 net always comes from unitPriceNet/discount so it stays\n // internally consistent with them. A caller-supplied value is still\n // reconciled against the computed one so a divergence (e.g. a mis-read\n // discount) surfaces instead of being silently discarded (#5644).\n //\n // Only a caller's value is reconciled: a snapshot rebuilt from a persisted\n // row (`totalsFromStoredRow`) carries the engine's own previous output, and\n // on a row the discount contract still has to heal that value is *supposed*\n // to differ from the recomputed net. Warning about it would drown the caller\n // signal this exists for in one line per line per recalculation.\n if (line.totalsFromStoredRow !== true && line.totalNetAmount !== null && line.totalNetAmount !== undefined) {\n const computedNetAmount = round(netSubtotal)\n const suppliedNetAmount = toNumber(line.totalNetAmount, NaN)\n if (!Number.isFinite(suppliedNetAmount)) {\n // Falling back to the computed value here would compare equal and log\n // nothing \u2014 the same silent discard #5644 exists to end.\n logger.warn('Sales line totalNetAmount is not a finite number; the computed value is used', {\n lineId: line.id ?? null,\n productId: line.productId ?? null,\n suppliedTotalNetAmount: line.totalNetAmount,\n computedNetAmount,\n })\n } else if (Math.abs(round(suppliedNetAmount) - computedNetAmount) > NET_RECONCILIATION_TOLERANCE) {\n logger.warn('Sales line totalNetAmount does not match the computed net amount; the computed value is used', {\n lineId: line.id ?? null,\n productId: line.productId ?? null,\n suppliedTotalNetAmount: round(suppliedNetAmount),\n computedNetAmount,\n })\n }\n }\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 totalsMode?: SalesAmountsMode | null\n suppliedTotals?: Partial<SalesDocumentAmounts> | 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 if (isExternalAmountsMode(params.totalsMode)) {\n const suppliedTotals = resolveSuppliedTotals(params.suppliedTotals)\n return {\n kind: documentKind,\n currencyCode,\n lines,\n adjustments: resolvedAdjustments,\n metadata: {},\n totals: {\n ...suppliedTotals,\n paidTotalAmount,\n refundedTotalAmount,\n outstandingAmount: round(\n Math.max(suppliedTotals.grandTotalGrossAmount - paidTotalAmount + refundedTotalAmount, 0)\n ),\n },\n }\n }\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 // Re-applied after the registry and events, which may otherwise overwrite it.\n if (isExternalAmountsMode(line.amountsMode)) {\n const supplied = buildExternalLineResult(line)\n current = {\n ...current,\n netAmount: supplied.netAmount,\n grossAmount: supplied.grossAmount,\n taxAmount: supplied.taxAmount,\n discountAmount: supplied.discountAmount,\n }\n }\n\n return current\n }\n\n async calculateDocument(opts: CalculateDocumentOptions): Promise<SalesDocumentCalculationResult> {\n const {\n documentKind,\n lines,\n adjustments = [],\n context,\n eventBus,\n existingTotals,\n totalsMode,\n suppliedTotals,\n } = 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 totalsMode,\n suppliedTotals,\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 totalsMode,\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 // Totals calculators rebuild the header from the lines, and core registers one\n // itself by module side effect (lib/providers/index.ts) \u2014 so this always runs.\n if (isExternalAmountsMode(totalsMode)) {\n current.totals = { ...current.totals, ...resolveSuppliedTotals(suppliedTotals) }\n }\n\n if (existingTotals || isExternalAmountsMode(totalsMode)) {\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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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,143 @@
1
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
3
+ import { EXTERNAL_HEADER_FIELDS } from "./calculations.js";
4
+ const OPTIONAL_EXTERNAL_HEADER_FIELDS = /* @__PURE__ */ new Set([
5
+ "shippingNetAmount",
6
+ "shippingGrossAmount",
7
+ "surchargeTotalAmount"
8
+ ]);
9
+ const REQUIRED_EXTERNAL_HEADER_FIELDS = EXTERNAL_HEADER_FIELDS.filter((field) => !OPTIONAL_EXTERNAL_HEADER_FIELDS.has(field));
10
+ const REQUIRED_EXTERNAL_LINE_FIELDS = [
11
+ "unitPriceNet",
12
+ "totalNetAmount",
13
+ "totalGrossAmount",
14
+ "taxAmount"
15
+ ];
16
+ function toFiniteNumber(value) {
17
+ if (typeof value === "number" && Number.isFinite(value)) return value;
18
+ if (typeof value === "string" && value.trim() !== "") {
19
+ const parsed = Number(value);
20
+ return Number.isFinite(parsed) ? parsed : null;
21
+ }
22
+ return null;
23
+ }
24
+ function collectSuppliedHeaderTotals(source) {
25
+ const totals = {};
26
+ if (!source) return totals;
27
+ for (const field of EXTERNAL_HEADER_FIELDS) {
28
+ const value = toFiniteNumber(source[field]);
29
+ if (value !== null) totals[field] = value;
30
+ }
31
+ return totals;
32
+ }
33
+ function hasAnySuppliedHeaderTotal(source) {
34
+ if (!source) return false;
35
+ return EXTERNAL_HEADER_FIELDS.some((field) => toFiniteNumber(source[field]) !== null);
36
+ }
37
+ function missingHeaderFields(source) {
38
+ return REQUIRED_EXTERNAL_HEADER_FIELDS.filter(
39
+ (field) => toFiniteNumber(source?.[field]) === null
40
+ );
41
+ }
42
+ function missingLineFields(line) {
43
+ return REQUIRED_EXTERNAL_LINE_FIELDS.filter((field) => toFiniteNumber(line[field]) === null);
44
+ }
45
+ function buildExternalHeaderTotals(source) {
46
+ const supplied = collectSuppliedHeaderTotals(source);
47
+ for (const field of OPTIONAL_EXTERNAL_HEADER_FIELDS) {
48
+ supplied[field] ??= 0;
49
+ }
50
+ return supplied;
51
+ }
52
+ async function assertExternalHeaderComplete(source) {
53
+ const missing = missingHeaderFields(source);
54
+ if (!missing.length) return;
55
+ const { translate } = await resolveTranslations();
56
+ throw new CrudHttpError(400, {
57
+ error: translate(
58
+ "sales.errors.externalTotalsRequired",
59
+ "An order with external amounts must supply its own document totals: {{fields}}.",
60
+ { fields: missing.join(", ") }
61
+ )
62
+ });
63
+ }
64
+ async function assertExternalLineComplete(line, lineNumber) {
65
+ const missing = missingLineFields(line);
66
+ if (!missing.length) return;
67
+ const { translate } = await resolveTranslations();
68
+ throw new CrudHttpError(400, {
69
+ error: translate(
70
+ "sales.errors.externalAmountsIncomplete",
71
+ "Line {{line}} has external amounts but is missing: {{fields}}. Partial specification is not a mode.",
72
+ { line: String(lineNumber), fields: missing.join(", ") }
73
+ )
74
+ });
75
+ }
76
+ async function assertUniformAmountsMode(documentMode, lines) {
77
+ const conflicting = lines.some(
78
+ (line) => line.amountsMode != null && line.amountsMode !== documentMode
79
+ );
80
+ if (!conflicting) return;
81
+ const { translate } = await resolveTranslations();
82
+ throw new CrudHttpError(400, {
83
+ error: translate(
84
+ "sales.errors.externalModeMixed",
85
+ "An order and all of its lines must share the same amounts mode.",
86
+ { mode: documentMode }
87
+ )
88
+ });
89
+ }
90
+ async function refuseOnExternalOrder() {
91
+ const { translate } = await resolveTranslations();
92
+ throw new CrudHttpError(409, {
93
+ error: translate(
94
+ "sales.errors.externalAdjustmentRefused",
95
+ "This order carries amounts from an external system, so adjustments cannot be added or removed. Switch it to computed amounts first."
96
+ )
97
+ });
98
+ }
99
+ async function requireOrderTotalsForExternalWrite(source) {
100
+ if (!hasAnySuppliedHeaderTotal(source)) {
101
+ const { translate } = await resolveTranslations();
102
+ throw new CrudHttpError(400, {
103
+ error: translate(
104
+ "sales.errors.externalTotalsRequired",
105
+ "An order with external amounts must supply its own document totals: {{fields}}.",
106
+ { fields: REQUIRED_EXTERNAL_HEADER_FIELDS.join(", ") }
107
+ )
108
+ });
109
+ }
110
+ await assertExternalHeaderComplete(source);
111
+ }
112
+ function readPersistedHeaderTotals(order) {
113
+ const totals = {};
114
+ for (const field of EXTERNAL_HEADER_FIELDS) {
115
+ totals[field] = toFiniteNumber(order[field]) ?? 0;
116
+ }
117
+ return totals;
118
+ }
119
+ async function assertAmountsModeUnsupportedOnQuote(payloads) {
120
+ if (!payloads?.some((payload) => payload.amountsMode != null || payload.totalsMode != null)) return;
121
+ const { translate } = await resolveTranslations();
122
+ throw new CrudHttpError(400, {
123
+ error: translate(
124
+ "sales.errors.externalModeUnsupportedOnQuote",
125
+ "Quotes cannot carry an amounts mode; they always use computed amounts."
126
+ )
127
+ });
128
+ }
129
+ export {
130
+ REQUIRED_EXTERNAL_HEADER_FIELDS,
131
+ REQUIRED_EXTERNAL_LINE_FIELDS,
132
+ assertAmountsModeUnsupportedOnQuote,
133
+ assertExternalHeaderComplete,
134
+ assertExternalLineComplete,
135
+ assertUniformAmountsMode,
136
+ buildExternalHeaderTotals,
137
+ collectSuppliedHeaderTotals,
138
+ hasAnySuppliedHeaderTotal,
139
+ readPersistedHeaderTotals,
140
+ refuseOnExternalOrder,
141
+ requireOrderTotalsForExternalWrite
142
+ };
143
+ //# sourceMappingURL=externalAmounts.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sales/lib/externalAmounts.ts"],
4
+ "sourcesContent": ["import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { SalesAmountsMode } from '../data/entities'\nimport { EXTERNAL_HEADER_FIELDS } from './calculations'\nimport type { SalesDocumentAmounts } from './types'\n\nexport type ExternalHeaderField = (typeof EXTERNAL_HEADER_FIELDS)[number]\n\n// A mirrored document that charges no shipping or surcharge has nothing to say\n// about them, so they default to zero instead of being required.\nconst OPTIONAL_EXTERNAL_HEADER_FIELDS: ReadonlySet<ExternalHeaderField> = new Set([\n 'shippingNetAmount',\n 'shippingGrossAmount',\n 'surchargeTotalAmount',\n])\n\nexport const REQUIRED_EXTERNAL_HEADER_FIELDS: readonly ExternalHeaderField[] =\n EXTERNAL_HEADER_FIELDS.filter((field) => !OPTIONAL_EXTERNAL_HEADER_FIELDS.has(field))\n\n/**\n * `unitPriceNet` is required because the derived discount consumes it: without it\n * the line's whole net would be stored as a discount, indistinguishable from a markup.\n */\nexport const REQUIRED_EXTERNAL_LINE_FIELDS = [\n 'unitPriceNet',\n 'totalNetAmount',\n 'totalGrossAmount',\n 'taxAmount',\n] as const\n\nexport type ExternalLineField = (typeof REQUIRED_EXTERNAL_LINE_FIELDS)[number]\n\nfunction toFiniteNumber(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\ntype HeaderSource = Record<string, unknown> | null | undefined\n\nexport function collectSuppliedHeaderTotals(source: HeaderSource): Partial<SalesDocumentAmounts> {\n const totals: Partial<SalesDocumentAmounts> = {}\n if (!source) return totals\n for (const field of EXTERNAL_HEADER_FIELDS) {\n const value = toFiniteNumber(source[field])\n if (value !== null) totals[field] = value\n }\n return totals\n}\n\nexport function hasAnySuppliedHeaderTotal(source: HeaderSource): boolean {\n if (!source) return false\n return EXTERNAL_HEADER_FIELDS.some((field) => toFiniteNumber(source[field]) !== null)\n}\n\nfunction missingHeaderFields(source: HeaderSource): ExternalHeaderField[] {\n return REQUIRED_EXTERNAL_HEADER_FIELDS.filter(\n (field) => toFiniteNumber(source?.[field]) === null,\n )\n}\n\nfunction missingLineFields(line: Record<string, unknown>): ExternalLineField[] {\n return REQUIRED_EXTERNAL_LINE_FIELDS.filter((field) => toFiniteNumber(line[field]) === null)\n}\n\n/** The supplied header with its optional fields defaulted to zero. */\nexport function buildExternalHeaderTotals(source: HeaderSource): Partial<SalesDocumentAmounts> {\n const supplied = collectSuppliedHeaderTotals(source)\n for (const field of OPTIONAL_EXTERNAL_HEADER_FIELDS) {\n supplied[field] ??= 0\n }\n return supplied\n}\n\nexport async function assertExternalHeaderComplete(source: HeaderSource): Promise<void> {\n const missing = missingHeaderFields(source)\n if (!missing.length) return\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(400, {\n error: translate(\n 'sales.errors.externalTotalsRequired',\n 'An order with external amounts must supply its own document totals: {{fields}}.',\n { fields: missing.join(', ') },\n ),\n })\n}\n\nexport async function assertExternalLineComplete(\n line: Record<string, unknown>,\n lineNumber: number,\n): Promise<void> {\n const missing = missingLineFields(line)\n if (!missing.length) return\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(400, {\n error: translate(\n 'sales.errors.externalAmountsIncomplete',\n 'Line {{line}} has external amounts but is missing: {{fields}}. Partial specification is not a mode.',\n { line: String(lineNumber), fields: missing.join(', ') },\n ),\n })\n}\n\nexport async function assertUniformAmountsMode(\n documentMode: SalesAmountsMode,\n lines: Array<{ amountsMode?: SalesAmountsMode | null }>,\n): Promise<void> {\n const conflicting = lines.some(\n (line) => line.amountsMode != null && line.amountsMode !== documentMode,\n )\n if (!conflicting) return\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(400, {\n error: translate(\n 'sales.errors.externalModeMixed',\n 'An order and all of its lines must share the same amounts mode.',\n { mode: documentMode },\n ),\n })\n}\n\nexport async function refuseOnExternalOrder(): Promise<never> {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(409, {\n error: translate(\n 'sales.errors.externalAdjustmentRefused',\n 'This order carries amounts from an external system, so adjustments cannot be added or removed. Switch it to computed amounts first.',\n ),\n })\n}\n\nexport async function requireOrderTotalsForExternalWrite(source: HeaderSource): Promise<void> {\n if (!hasAnySuppliedHeaderTotal(source)) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(400, {\n error: translate(\n 'sales.errors.externalTotalsRequired',\n 'An order with external amounts must supply its own document totals: {{fields}}.',\n { fields: REQUIRED_EXTERNAL_HEADER_FIELDS.join(', ') },\n ),\n })\n }\n await assertExternalHeaderComplete(source)\n}\n\nexport function readPersistedHeaderTotals(\n order: Record<ExternalHeaderField, string | number | null | undefined>,\n): Partial<SalesDocumentAmounts> {\n const totals: Partial<SalesDocumentAmounts> = {}\n for (const field of EXTERNAL_HEADER_FIELDS) {\n totals[field] = toFiniteNumber(order[field]) ?? 0\n }\n return totals\n}\n\nexport async function assertAmountsModeUnsupportedOnQuote(\n payloads:\n | Array<{ amountsMode?: SalesAmountsMode | null; totalsMode?: SalesAmountsMode | null }>\n | null\n | undefined,\n): Promise<void> {\n // Both reach quote commands, through schemas shared with orders.\n if (!payloads?.some((payload) => payload.amountsMode != null || payload.totalsMode != null)) return\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(400, {\n error: translate(\n 'sales.errors.externalModeUnsupportedOnQuote',\n 'Quotes cannot carry an amounts mode; they always use computed amounts.',\n ),\n })\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -52,7 +52,12 @@ function mapPersistedLine(line) {
52
52
  };
53
53
  }
54
54
  function mapOrderLineEntityToSnapshot(line) {
55
- return mapPersistedLine(line);
55
+ return {
56
+ ...mapPersistedLine(line),
57
+ // Who owns the amounts, not where they came from — never conflate it with the
58
+ // origin flags above. Order-only: quote lines have no such column.
59
+ amountsMode: line.amountsMode ?? "computed"
60
+ };
56
61
  }
57
62
  function mapQuoteLineEntityToSnapshot(line) {
58
63
  return mapPersistedLine(line);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/lib/lineSnapshots.ts"],
4
- "sourcesContent": ["import type { SalesOrderLine, SalesQuoteLine } from '../data/entities'\nimport { cloneJson } from './json'\nimport type { SalesLineDiscountBasis, SalesLineSnapshot } from './types'\n\nfunction toNumeric(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim().length) {\n const parsed = Number(value)\n if (Number.isFinite(parsed)) return parsed\n }\n return 0\n}\n\n/**\n * A snapshot rebuilt from a persisted row, plus the columns the calculation\n * engine never reads but the write paths must carry back to the row.\n *\n * The line upsert, line delete and adjustment write paths all rebuild *every*\n * line of the document from these snapshots and then `Object.assign` the result\n * onto the existing entities. Anything the mapper drops is therefore not merely\n * absent from the calculation \u2014 it is written back as `null` over the stored\n * value of lines the caller never touched (#5911).\n */\nexport type SalesPersistedLineSnapshot = SalesLineSnapshot & {\n statusEntryId: string | null\n catalogSnapshot: Record<string, unknown> | null\n promotionSnapshot: Record<string, unknown> | null\n}\n\nfunction mapPersistedLine(line: SalesOrderLine | SalesQuoteLine): SalesPersistedLineSnapshot {\n return {\n id: line.id,\n statusEntryId: line.statusEntryId ?? null,\n catalogSnapshot: line.catalogSnapshot ? cloneJson(line.catalogSnapshot) : null,\n promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,\n lineNumber: line.lineNumber,\n kind: line.kind,\n productId: line.productId ?? null,\n productVariantId: line.productVariantId ?? null,\n name: line.name ?? null,\n description: line.description ?? null,\n comment: line.comment ?? null,\n quantity: toNumeric(line.quantity),\n quantityUnit: line.quantityUnit ?? null,\n normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),\n normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,\n uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,\n currencyCode: line.currencyCode,\n unitPriceNet: toNumeric(line.unitPriceNet),\n unitPriceGross: toNumeric(line.unitPriceGross),\n discountAmount: toNumeric(line.discountAmount),\n // The persisted column holds the discount for the whole line, so the\n // calculation engine must not multiply it out by quantity again. This flag\n // is the only thing standing between a round trip and a compounding\n // re-inflation, and it is why mappers must never set `discountAmountBasis`\n // instead \u2014 that one means \"a caller asserted this\".\n discountAmountFromStoredRow: true,\n discountPercent: toNumeric(line.discountPercent),\n taxRate: toNumeric(line.taxRate),\n taxAmount: toNumeric(line.taxAmount),\n // The totals below are the engine's own previous output read back off the\n // row, not something a caller asserted, so they are not reconciled against\n // the recomputed net (#5644) \u2014 on a legacy row that divergence is the\n // discount contract healing itself, not a caller mistake.\n totalsFromStoredRow: true,\n totalNetAmount: toNumeric(line.totalNetAmount),\n totalGrossAmount: toNumeric(line.totalGrossAmount),\n configuration: line.configuration ? cloneJson(line.configuration) : null,\n promotionCode: line.promotionCode ?? null,\n metadata: line.metadata ? cloneJson(line.metadata) : null,\n customFieldSetId: line.customFieldSetId ?? null,\n }\n}\n\n/**\n * Rebuild a calculation snapshot from a persisted order line.\n *\n * Shared by `commands/documents.ts` and `commands/returns.ts` on purpose: the\n * two files used to carry byte-identical copies, and that duplication is why\n * the return flows kept the discount defect after the order flows were fixed.\n */\nexport function mapOrderLineEntityToSnapshot(line: SalesOrderLine): SalesPersistedLineSnapshot {\n return mapPersistedLine(line)\n}\n\nexport function mapQuoteLineEntityToSnapshot(line: SalesQuoteLine): SalesPersistedLineSnapshot {\n return mapPersistedLine(line)\n}\n\ntype UpsertDiscountFields = Pick<\n SalesLineSnapshot,\n 'discountAmount' | 'discountAmountBasis' | 'discountAmountFromStoredRow'\n>\n\n/**\n * Decide the discount fields of an upsert payload one operand at a time.\n *\n * A line upsert can source its amount from two places with opposite meanings: a\n * caller value, which is per unit unless the caller says otherwise, and the\n * stored row, which is always a line total. Collapsing them into a single\n * `caller ?? stored` expression means whichever origin the merged result is\n * tagged with is wrong half the time \u2014 and the half that breaks is the\n * upsert-existing path, which then re-inflates by a further factor of quantity\n * while looking perfectly correct.\n *\n * Keeping `null` rather than `0` for \"nothing supplied\" is deliberate too: the\n * column cannot represent that distinction, but the payload can, and the\n * calculation engine needs it to tell a suppressing zero from an absent value.\n */\nexport function resolveUpsertDiscountFields(\n callerAmount: number | null | undefined,\n callerBasis: SalesLineDiscountBasis | null | undefined,\n existingSnapshot: Pick<SalesLineSnapshot, 'discountAmount'> | null | undefined,\n): UpsertDiscountFields {\n if (callerAmount !== null && callerAmount !== undefined) {\n return { discountAmount: callerAmount, discountAmountBasis: callerBasis ?? 'unit' }\n }\n return {\n discountAmount: existingSnapshot?.discountAmount ?? null,\n discountAmountFromStoredRow: existingSnapshot != null,\n }\n}\n\n/**\n * Decide whether the `totalNetAmount` an upsert payload ends up carrying is a\n * caller assertion or a value that came back off the stored row.\n *\n * The upsert merges caller input over the existing snapshot, so the merged\n * total has two possible origins and only the caller one is worth reconciling\n * against the recomputed net (#5644): a value read back off the row is what the\n * engine itself wrote last time, and on a legacy row it is exactly what\n * recalculation is supposed to heal.\n */\nexport function resolveUpsertTotalsOrigin(\n callerTotalNetAmount: number | null | undefined,\n existingSnapshot: Pick<SalesLineSnapshot, 'totalNetAmount'> | null | undefined,\n): Pick<SalesLineSnapshot, 'totalsFromStoredRow'> {\n if (callerTotalNetAmount !== null && callerTotalNetAmount !== undefined) return {}\n return existingSnapshot != null ? { totalsFromStoredRow: true } : {}\n}\n\n/** Reuse stored tax/gross only while the edited line's pricing inputs are unchanged. */\nexport function resolveUpsertCalculatedAmounts(\n caller: Pick<SalesLineSnapshot, 'taxAmount' | 'totalGrossAmount'>,\n nextSnapshot: SalesLineSnapshot,\n existingSnapshot: SalesLineSnapshot | null,\n): Pick<SalesLineSnapshot, 'taxAmount' | 'totalGrossAmount'> {\n const pricingFields = [\n 'quantity',\n 'unitPriceNet',\n 'unitPriceGross',\n 'taxRate',\n ] as const\n const nextDiscountAmount = nextSnapshot.discountAmount ?? 0\n const nextDiscountLineAmount = nextSnapshot.discountAmountFromStoredRow === true ||\n nextSnapshot.discountAmountBasis === 'line'\n ? nextDiscountAmount\n : nextDiscountAmount * nextSnapshot.quantity\n const discountChanged =\n (nextSnapshot.discountPercent ?? 0) !== (existingSnapshot?.discountPercent ?? 0) ||\n ((nextSnapshot.discountPercent ?? 0) === 0 &&\n Math.round((nextDiscountLineAmount + Number.EPSILON) * 1e4) / 1e4 !==\n (existingSnapshot?.discountAmount ?? 0))\n const pricingChanged = existingSnapshot === null || discountChanged ||\n pricingFields.some((field) => (nextSnapshot[field] ?? 0) !== (existingSnapshot[field] ?? 0))\n const grossChanged = caller.totalGrossAmount !== undefined && caller.totalGrossAmount !== null &&\n caller.totalGrossAmount !== existingSnapshot?.totalGrossAmount\n const taxChanged = caller.taxAmount !== undefined && caller.taxAmount !== null &&\n caller.taxAmount !== existingSnapshot?.taxAmount\n return {\n taxAmount: caller.taxAmount ??\n (pricingChanged || grossChanged ? null : existingSnapshot?.taxAmount ?? null),\n totalGrossAmount: caller.totalGrossAmount ??\n (pricingChanged || taxChanged ? null : existingSnapshot?.totalGrossAmount ?? null),\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { SalesOrderLine, SalesQuoteLine } from '../data/entities'\nimport { cloneJson } from './json'\nimport type { SalesLineDiscountBasis, SalesLineSnapshot } from './types'\n\nfunction toNumeric(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim().length) {\n const parsed = Number(value)\n if (Number.isFinite(parsed)) return parsed\n }\n return 0\n}\n\n/**\n * A snapshot rebuilt from a persisted row, plus the columns the calculation\n * engine never reads but the write paths must carry back to the row.\n *\n * The line upsert, line delete and adjustment write paths all rebuild *every*\n * line of the document from these snapshots and then `Object.assign` the result\n * onto the existing entities. Anything the mapper drops is therefore not merely\n * absent from the calculation \u2014 it is written back as `null` over the stored\n * value of lines the caller never touched (#5911).\n */\nexport type SalesPersistedLineSnapshot = SalesLineSnapshot & {\n statusEntryId: string | null\n catalogSnapshot: Record<string, unknown> | null\n promotionSnapshot: Record<string, unknown> | null\n}\n\nfunction mapPersistedLine(line: SalesOrderLine | SalesQuoteLine): SalesPersistedLineSnapshot {\n return {\n id: line.id,\n statusEntryId: line.statusEntryId ?? null,\n catalogSnapshot: line.catalogSnapshot ? cloneJson(line.catalogSnapshot) : null,\n promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,\n lineNumber: line.lineNumber,\n kind: line.kind,\n productId: line.productId ?? null,\n productVariantId: line.productVariantId ?? null,\n name: line.name ?? null,\n description: line.description ?? null,\n comment: line.comment ?? null,\n quantity: toNumeric(line.quantity),\n quantityUnit: line.quantityUnit ?? null,\n normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),\n normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,\n uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,\n currencyCode: line.currencyCode,\n unitPriceNet: toNumeric(line.unitPriceNet),\n unitPriceGross: toNumeric(line.unitPriceGross),\n discountAmount: toNumeric(line.discountAmount),\n // The persisted column holds the discount for the whole line, so the\n // calculation engine must not multiply it out by quantity again. This flag\n // is the only thing standing between a round trip and a compounding\n // re-inflation, and it is why mappers must never set `discountAmountBasis`\n // instead \u2014 that one means \"a caller asserted this\".\n discountAmountFromStoredRow: true,\n discountPercent: toNumeric(line.discountPercent),\n taxRate: toNumeric(line.taxRate),\n taxAmount: toNumeric(line.taxAmount),\n // The totals below are the engine's own previous output read back off the\n // row, not something a caller asserted, so they are not reconciled against\n // the recomputed net (#5644) \u2014 on a legacy row that divergence is the\n // discount contract healing itself, not a caller mistake.\n totalsFromStoredRow: true,\n totalNetAmount: toNumeric(line.totalNetAmount),\n totalGrossAmount: toNumeric(line.totalGrossAmount),\n configuration: line.configuration ? cloneJson(line.configuration) : null,\n promotionCode: line.promotionCode ?? null,\n metadata: line.metadata ? cloneJson(line.metadata) : null,\n customFieldSetId: line.customFieldSetId ?? null,\n }\n}\n\n/**\n * Rebuild a calculation snapshot from a persisted order line.\n *\n * Shared by `commands/documents.ts` and `commands/returns.ts` on purpose: the\n * two files used to carry byte-identical copies, and that duplication is why\n * the return flows kept the discount defect after the order flows were fixed.\n */\nexport function mapOrderLineEntityToSnapshot(line: SalesOrderLine): SalesPersistedLineSnapshot {\n return {\n ...mapPersistedLine(line),\n // Who owns the amounts, not where they came from \u2014 never conflate it with the\n // origin flags above. Order-only: quote lines have no such column.\n amountsMode: line.amountsMode ?? 'computed',\n }\n}\n\nexport function mapQuoteLineEntityToSnapshot(line: SalesQuoteLine): SalesPersistedLineSnapshot {\n return mapPersistedLine(line)\n}\n\ntype UpsertDiscountFields = Pick<\n SalesLineSnapshot,\n 'discountAmount' | 'discountAmountBasis' | 'discountAmountFromStoredRow'\n>\n\n/**\n * Decide the discount fields of an upsert payload one operand at a time.\n *\n * A line upsert can source its amount from two places with opposite meanings: a\n * caller value, which is per unit unless the caller says otherwise, and the\n * stored row, which is always a line total. Collapsing them into a single\n * `caller ?? stored` expression means whichever origin the merged result is\n * tagged with is wrong half the time \u2014 and the half that breaks is the\n * upsert-existing path, which then re-inflates by a further factor of quantity\n * while looking perfectly correct.\n *\n * Keeping `null` rather than `0` for \"nothing supplied\" is deliberate too: the\n * column cannot represent that distinction, but the payload can, and the\n * calculation engine needs it to tell a suppressing zero from an absent value.\n */\nexport function resolveUpsertDiscountFields(\n callerAmount: number | null | undefined,\n callerBasis: SalesLineDiscountBasis | null | undefined,\n existingSnapshot: Pick<SalesLineSnapshot, 'discountAmount'> | null | undefined,\n): UpsertDiscountFields {\n if (callerAmount !== null && callerAmount !== undefined) {\n return { discountAmount: callerAmount, discountAmountBasis: callerBasis ?? 'unit' }\n }\n return {\n discountAmount: existingSnapshot?.discountAmount ?? null,\n discountAmountFromStoredRow: existingSnapshot != null,\n }\n}\n\n/**\n * Decide whether the `totalNetAmount` an upsert payload ends up carrying is a\n * caller assertion or a value that came back off the stored row.\n *\n * The upsert merges caller input over the existing snapshot, so the merged\n * total has two possible origins and only the caller one is worth reconciling\n * against the recomputed net (#5644): a value read back off the row is what the\n * engine itself wrote last time, and on a legacy row it is exactly what\n * recalculation is supposed to heal.\n */\nexport function resolveUpsertTotalsOrigin(\n callerTotalNetAmount: number | null | undefined,\n existingSnapshot: Pick<SalesLineSnapshot, 'totalNetAmount'> | null | undefined,\n): Pick<SalesLineSnapshot, 'totalsFromStoredRow'> {\n if (callerTotalNetAmount !== null && callerTotalNetAmount !== undefined) return {}\n return existingSnapshot != null ? { totalsFromStoredRow: true } : {}\n}\n\n/** Reuse stored tax/gross only while the edited line's pricing inputs are unchanged. */\nexport function resolveUpsertCalculatedAmounts(\n caller: Pick<SalesLineSnapshot, 'taxAmount' | 'totalGrossAmount'>,\n nextSnapshot: SalesLineSnapshot,\n existingSnapshot: SalesLineSnapshot | null,\n): Pick<SalesLineSnapshot, 'taxAmount' | 'totalGrossAmount'> {\n const pricingFields = [\n 'quantity',\n 'unitPriceNet',\n 'unitPriceGross',\n 'taxRate',\n ] as const\n const nextDiscountAmount = nextSnapshot.discountAmount ?? 0\n const nextDiscountLineAmount = nextSnapshot.discountAmountFromStoredRow === true ||\n nextSnapshot.discountAmountBasis === 'line'\n ? nextDiscountAmount\n : nextDiscountAmount * nextSnapshot.quantity\n const discountChanged =\n (nextSnapshot.discountPercent ?? 0) !== (existingSnapshot?.discountPercent ?? 0) ||\n ((nextSnapshot.discountPercent ?? 0) === 0 &&\n Math.round((nextDiscountLineAmount + Number.EPSILON) * 1e4) / 1e4 !==\n (existingSnapshot?.discountAmount ?? 0))\n const pricingChanged = existingSnapshot === null || discountChanged ||\n pricingFields.some((field) => (nextSnapshot[field] ?? 0) !== (existingSnapshot[field] ?? 0))\n const grossChanged = caller.totalGrossAmount !== undefined && caller.totalGrossAmount !== null &&\n caller.totalGrossAmount !== existingSnapshot?.totalGrossAmount\n const taxChanged = caller.taxAmount !== undefined && caller.taxAmount !== null &&\n caller.taxAmount !== existingSnapshot?.taxAmount\n return {\n taxAmount: caller.taxAmount ??\n (pricingChanged || grossChanged ? null : existingSnapshot?.taxAmount ?? null),\n totalGrossAmount: caller.totalGrossAmount ??\n (pricingChanged || taxChanged ? null : existingSnapshot?.totalGrossAmount ?? null),\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -80,6 +80,7 @@ function makeSalesLineRoute(config) {
80
80
  parentFkColumn,
81
81
  parentFkParam,
82
82
  createSchema,
83
+ writeExtensionShape,
83
84
  features,
84
85
  commandPrefix
85
86
  } = config;
@@ -92,11 +93,13 @@ function makeSalesLineRoute(config) {
92
93
  sortDir: z.enum(["asc", "desc"]).optional()
93
94
  }).passthrough();
94
95
  const upsertSchema = createSchema.extend({
95
- id: z.string().uuid().optional()
96
+ id: z.string().uuid().optional(),
97
+ ...writeExtensionShape ?? {}
96
98
  });
97
99
  const deleteSchema = z.object({
98
100
  id: z.string().uuid(),
99
- [parentFkParam]: z.string().uuid()
101
+ [parentFkParam]: z.string().uuid(),
102
+ ...writeExtensionShape ?? {}
100
103
  });
101
104
  const routeMetadata = {
102
105
  GET: { requireAuth: true, requireFeatures: [features.view] },
@@ -162,6 +165,8 @@ function makeSalesLineRoute(config) {
162
165
  ];
163
166
  const returnedQuantity = F["returned_quantity"];
164
167
  if (typeof returnedQuantity === "string") fields.push(returnedQuantity);
168
+ const amountsMode = F["amounts_mode"];
169
+ if (typeof amountsMode === "string") fields.push(amountsMode);
165
170
  return fields;
166
171
  })(),
167
172
  sortFieldMap: {
@@ -282,6 +287,7 @@ function makeSalesLineRoute(config) {
282
287
  promotion_snapshot: z.record(z.string(), z.unknown()).nullable().optional(),
283
288
  metadata: z.record(z.string(), z.unknown()).nullable().optional(),
284
289
  custom_field_set_id: z.string().uuid().nullable().optional(),
290
+ amounts_mode: z.enum(["computed", "external"]).optional(),
285
291
  created_at: z.string(),
286
292
  updated_at: z.string()
287
293
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sales/lib/makeSalesLineRoute.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n buildCustomFieldFiltersFromQuery,\n extractAllCustomFieldEntries,\n} from '@open-mercato/shared/lib/crud/custom-fields'\nimport {\n canonicalizeUnitCode,\n REFERENCE_UNIT_CODES,\n} from '@open-mercato/shared/lib/units/unitCodes'\nimport {\n createPagedListResponseSchema,\n createSalesCrudOpenApi,\n defaultOkResponseSchema,\n} from '../api/openapi'\nimport { withScopedPayload } from '../api/utils'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- MikroORM entity class constructor\ntype EntityClass = new (...args: any[]) => unknown\n\ninterface SalesLineRouteConfig {\n entity: EntityClass\n entityId: string\n fieldConstants: Record<string, string>\n parentFkColumn: string\n parentFkParam: string\n createSchema: z.ZodObject<z.ZodRawShape>\n features: { view: string; manage: string }\n commandPrefix: string\n openApi: {\n resourceName: string\n description: string\n }\n}\n\nconst rawBodySchema = z.object({}).passthrough()\n\nfunction resolveRawBody(raw: unknown): Record<string, unknown> {\n if (!raw || typeof raw !== 'object') return {}\n if ('body' in raw) {\n const payload = raw as { body?: unknown }\n if (payload.body && typeof payload.body === 'object') {\n return payload.body as Record<string, unknown>\n }\n }\n return raw as Record<string, unknown>\n}\n\nfunction transformItem(item: Record<string, unknown> | null | undefined) {\n if (!item) return item\n const normalized = { ...item }\n const cfEntries = extractAllCustomFieldEntries(item)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) delete normalized[key]\n }\n const quantityUnit = canonicalizeUnitCode(\n normalized['quantity_unit'] ?? normalized['quantityUnit'],\n )\n const normalizedUnit =\n canonicalizeUnitCode(\n normalized['normalized_unit'] ?? normalized['normalizedUnit'],\n ) ?? quantityUnit\n return {\n ...normalized,\n quantity_unit: quantityUnit,\n normalized_unit: normalizedUnit,\n ...cfEntries,\n }\n}\n\nconst uomSnapshotOpenApiSchema = z\n .object({\n version: z.literal(1),\n productId: z.string().nullable(),\n productVariantId: z.string().nullable(),\n baseUnitCode: z.string().nullable(),\n enteredUnitCode: z.string().nullable(),\n enteredQuantity: z.string(),\n toBaseFactor: z.string(),\n normalizedQuantity: z.string(),\n rounding: z.object({\n mode: z.enum(['half_up', 'down', 'up']),\n scale: z.number().int(),\n }),\n source: z.object({\n conversionId: z.string().nullable(),\n resolvedAt: z.string(),\n }),\n unitPriceReference: z\n .object({\n enabled: z.boolean(),\n referenceUnitCode: z.enum(REFERENCE_UNIT_CODES).nullable(),\n baseQuantity: z.string().nullable(),\n grossPerReference: z.string().nullable().optional(),\n netPerReference: z.string().nullable().optional(),\n })\n .optional(),\n })\n .nullable()\n .optional()\n\nexport function makeSalesLineRoute(config: SalesLineRouteConfig) {\n const {\n entity,\n entityId,\n fieldConstants: F,\n parentFkColumn,\n parentFkParam,\n createSchema,\n features,\n commandPrefix,\n } = config\n\n const listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n id: z.string().uuid().optional(),\n [parentFkParam]: z.string().uuid().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\n const upsertSchema = createSchema.extend({\n id: z.string().uuid().optional(),\n })\n\n const deleteSchema = z.object({\n id: z.string().uuid(),\n [parentFkParam]: z.string().uuid(),\n })\n\n const routeMetadata = {\n GET: { requireAuth: true, requireFeatures: [features.view] },\n POST: { requireAuth: true, requireFeatures: [features.manage] },\n PUT: { requireAuth: true, requireFeatures: [features.manage] },\n DELETE: { requireAuth: true, requireFeatures: [features.manage] },\n }\n\n const crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: {\n entityType: entityId,\n },\n enrichers: {\n entityId,\n },\n list: {\n schema: listSchema,\n entityId,\n fields: (() => {\n const fields: string[] = [\n F.id,\n parentFkColumn,\n F.line_number,\n F.kind,\n F.status_entry_id,\n F.status,\n F.product_id,\n F.product_variant_id,\n F.catalog_snapshot,\n F.name,\n F.description,\n F.comment,\n F.organization_id,\n F.tenant_id,\n F.quantity,\n F.quantity_unit,\n F.normalized_quantity,\n F.normalized_unit,\n F.uom_snapshot,\n F.currency_code,\n F.unit_price_net,\n F.unit_price_gross,\n F.discount_amount,\n F.discount_percent,\n F.tax_rate,\n F.tax_amount,\n F.total_net_amount,\n F.total_gross_amount,\n F.configuration,\n F.promotion_code,\n F.promotion_snapshot,\n F.metadata,\n F.custom_field_set_id,\n F.created_at,\n F.updated_at,\n ]\n const returnedQuantity = F['returned_quantity']\n if (typeof returnedQuantity === 'string') fields.push(returnedQuantity)\n return fields\n })(),\n sortFieldMap: {\n id: F.id,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n lineNumber: F.line_number,\n },\n // Line ids are random v4 UUIDs, so the factory's `id` default orders a\n // document's lines arbitrarily and reshuffles them whenever an integration\n // rewrites the lines (delete-and-reinsert is the normal upsert shape).\n // `line_number` is the document's real reading order; `id` only breaks ties\n // between legacy rows that all still sit at the column default of 0.\n defaultSort: { field: 'lineNumber', dir: 'asc' },\n tiebreakSortField: 'id',\n buildFilters: async (query: Record<string, unknown>, ctx: CrudCtx) => {\n const filters: Record<string, unknown> = {}\n if (query.id) filters.id = { $eq: query.id }\n if (query[parentFkParam]) filters[parentFkColumn] = { $eq: query[parentFkParam] }\n try {\n const em = ctx.container.resolve<EntityManager>('em')\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityId,\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch {\n // ignore\n }\n return filters\n },\n transformItem,\n },\n actions: {\n create: {\n commandId: `${commandPrefix}.upsert`,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const payload = upsertSchema.parse(\n withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),\n )\n return { body: payload }\n },\n response: ({ result }: { result: Record<string, unknown> | null }) => ({\n id: result?.lineId ?? null,\n [parentFkParam]: result?.[parentFkParam] ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: `${commandPrefix}.upsert`,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const payload = upsertSchema.parse(\n withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),\n )\n return { body: payload }\n },\n response: ({ result }: { result: Record<string, unknown> | null }) => ({\n id: result?.lineId ?? null,\n [parentFkParam]: result?.[parentFkParam] ?? null,\n }),\n },\n delete: {\n commandId: `${commandPrefix}.delete`,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const payload = deleteSchema.parse(\n withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),\n )\n if (!payload.id || !payload[parentFkParam]) {\n throw new CrudHttpError(400, {\n error: translate(\n 'sales.documents.detail.error',\n 'Document not found or inaccessible.',\n ),\n })\n }\n return { body: payload }\n },\n response: () => ({ ok: true }),\n },\n },\n })\n\n const lineItemSchema = z.object({\n id: z.string().uuid(),\n [parentFkColumn]: z.string().uuid(),\n line_number: z.number(),\n kind: z.string(),\n status_entry_id: z.string().uuid().nullable().optional(),\n status: z.string().nullable().optional(),\n product_id: z.string().uuid().nullable().optional(),\n product_variant_id: z.string().uuid().nullable().optional(),\n catalog_snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n name: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n comment: z.string().nullable().optional(),\n quantity: z.number(),\n quantity_unit: z.string().nullable().optional(),\n normalized_quantity: z.number(),\n normalized_unit: z.string().nullable().optional(),\n uom_snapshot: uomSnapshotOpenApiSchema,\n currency_code: z.string(),\n unit_price_net: z.number(),\n unit_price_gross: z.number(),\n discount_amount: z.number(),\n discount_percent: z.number(),\n tax_rate: z.number(),\n tax_amount: z.number(),\n total_net_amount: z.number(),\n total_gross_amount: z.number(),\n configuration: z.record(z.string(), z.unknown()).nullable().optional(),\n promotion_code: z.string().nullable().optional(),\n promotion_snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n metadata: z.record(z.string(), z.unknown()).nullable().optional(),\n custom_field_set_id: z.string().uuid().nullable().optional(),\n created_at: z.string(),\n updated_at: z.string(),\n })\n\n const upsertResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n [parentFkParam]: z.string().uuid().nullable(),\n })\n\n const openApi = createSalesCrudOpenApi({\n resourceName: config.openApi.resourceName,\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(lineItemSchema),\n create: {\n schema: upsertSchema,\n responseSchema: upsertResponseSchema,\n description: `Creates ${config.openApi.description}.`,\n },\n update: {\n schema: upsertSchema,\n responseSchema: upsertResponseSchema,\n description: `Updates ${config.openApi.description}.`,\n },\n del: {\n schema: deleteSchema,\n responseSchema: defaultOkResponseSchema,\n description: `Deletes ${config.openApi.description}.`,\n },\n })\n\n return {\n metadata: routeMetadata,\n openApi,\n GET: crud.GET,\n POST: crud.POST,\n PUT: crud.PUT,\n DELETE: crud.DELETE,\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,qBAAmC;AAC5C,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAoBlC,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,SAAS,eAAe,KAAuC;AAC7D,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,MAAI,UAAU,KAAK;AACjB,UAAM,UAAU;AAChB,QAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAS,UAAU;AACpD,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,cAAc,MAAkD;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,aAAa,EAAE,GAAG,KAAK;AAC7B,QAAM,YAAY,6BAA6B,IAAI;AACnD,aAAW,OAAO,OAAO,KAAK,UAAU,GAAG;AACzC,QAAI,IAAI,WAAW,KAAK,EAAG,QAAO,WAAW,GAAG;AAAA,EAClD;AACA,QAAM,eAAe;AAAA,IACnB,WAAW,eAAe,KAAK,WAAW,cAAc;AAAA,EAC1D;AACA,QAAM,iBACJ;AAAA,IACE,WAAW,iBAAiB,KAAK,WAAW,gBAAgB;AAAA,EAC9D,KAAK;AACP,SAAO;AAAA,IACL,GAAG;AAAA,IACH,eAAe;AAAA,IACf,iBAAiB;AAAA,IACjB,GAAG;AAAA,EACL;AACF;AAEA,MAAM,2BAA2B,EAC9B,OAAO;AAAA,EACN,SAAS,EAAE,QAAQ,CAAC;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,kBAAkB,EAAE,OAAO,EAAE,SAAS;AAAA,EACtC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,iBAAiB,EAAE,OAAO;AAAA,EAC1B,cAAc,EAAE,OAAO;AAAA,EACvB,oBAAoB,EAAE,OAAO;AAAA,EAC7B,UAAU,EAAE,OAAO;AAAA,IACjB,MAAM,EAAE,KAAK,CAAC,WAAW,QAAQ,IAAI,CAAC;AAAA,IACtC,OAAO,EAAE,OAAO,EAAE,IAAI;AAAA,EACxB,CAAC;AAAA,EACD,QAAQ,EAAE,OAAO;AAAA,IACf,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,IAClC,YAAY,EAAE,OAAO;AAAA,EACvB,CAAC;AAAA,EACD,oBAAoB,EACjB,OAAO;AAAA,IACN,SAAS,EAAE,QAAQ;AAAA,IACnB,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,SAAS;AAAA,IACzD,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,IAClC,mBAAmB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAClD,iBAAiB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAClD,CAAC,EACA,SAAS;AACd,CAAC,EACA,SAAS,EACT,SAAS;AAEL,SAAS,mBAAmB,QAA8B;AAC/D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,aAAa,EAChB,OAAO;AAAA,IACN,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,IACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,IACtD,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,IAC/B,CAAC,aAAa,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,IAC5C,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,IAC/B,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAAA,EAC5C,CAAC,EACA,YAAY;AAEf,QAAM,eAAe,aAAa,OAAO;AAAA,IACvC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACjC,CAAC;AAED,QAAM,eAAe,EAAE,OAAO;AAAA,IAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,IACpB,CAAC,aAAa,GAAG,EAAE,OAAO,EAAE,KAAK;AAAA,EACnC,CAAC;AAED,QAAM,gBAAgB;AAAA,IACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,SAAS,IAAI,EAAE;AAAA,IAC3D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,SAAS,MAAM,EAAE;AAAA,IAC9D,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,SAAS,MAAM,EAAE;AAAA,IAC7D,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,SAAS,MAAM,EAAE;AAAA,EAClE;AAEA,QAAM,OAAO,cAAc;AAAA,IACzB,UAAU;AAAA,IACV,KAAK;AAAA,MACH;AAAA,MACA,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa;AAAA,MACb,iBAAiB;AAAA,IACnB;AAAA,IACA,SAAS;AAAA,MACP,YAAY;AAAA,IACd;AAAA,IACA,WAAW;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,MAAM;AACb,cAAM,SAAmB;AAAA,UACvB,EAAE;AAAA,UACF;AAAA,UACA,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,UACF,EAAE;AAAA,QACJ;AACA,cAAM,mBAAmB,EAAE,mBAAmB;AAC9C,YAAI,OAAO,qBAAqB,SAAU,QAAO,KAAK,gBAAgB;AACtE,eAAO;AAAA,MACT,GAAG;AAAA,MACH,cAAc;AAAA,QACZ,IAAI,EAAE;AAAA,QACN,WAAW,EAAE;AAAA,QACb,WAAW,EAAE;AAAA,QACb,YAAY,EAAE;AAAA,MAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,aAAa,EAAE,OAAO,cAAc,KAAK,MAAM;AAAA,MAC/C,mBAAmB;AAAA,MACnB,cAAc,OAAO,OAAgC,QAAiB;AACpE,cAAM,UAAmC,CAAC;AAC1C,YAAI,MAAM,GAAI,SAAQ,KAAK,EAAE,KAAK,MAAM,GAAG;AAC3C,YAAI,MAAM,aAAa,EAAG,SAAQ,cAAc,IAAI,EAAE,KAAK,MAAM,aAAa,EAAE;AAChF,YAAI;AACF,gBAAM,KAAK,IAAI,UAAU,QAAuB,IAAI;AACpD,gBAAM,YAAY,MAAM,iCAAiC;AAAA,YACvD;AAAA,YACA;AAAA,YACA;AAAA,YACA,UAAU,IAAI,MAAM,YAAY;AAAA,UAClC,CAAC;AACD,iBAAO,OAAO,SAAS,SAAS;AAAA,QAClC,QAAQ;AAAA,QAER;AACA,eAAO;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP,QAAQ;AAAA,QACN,WAAW,GAAG,aAAa;AAAA,QAC3B,QAAQ;AAAA,QACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAsC;AAChE,gBAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,gBAAM,UAAU,aAAa;AAAA,YAC3B,kBAAkB,eAAe,GAAG,KAAK,CAAC,GAAG,KAAK,SAAS;AAAA,UAC7D;AACA,iBAAO,EAAE,MAAM,QAAQ;AAAA,QACzB;AAAA,QACA,UAAU,CAAC,EAAE,OAAO,OAAmD;AAAA,UACrE,IAAI,QAAQ,UAAU;AAAA,UACtB,CAAC,aAAa,GAAG,SAAS,aAAa,KAAK;AAAA,QAC9C;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,QACN,WAAW,GAAG,aAAa;AAAA,QAC3B,QAAQ;AAAA,QACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAsC;AAChE,gBAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,gBAAM,UAAU,aAAa;AAAA,YAC3B,kBAAkB,eAAe,GAAG,KAAK,CAAC,GAAG,KAAK,SAAS;AAAA,UAC7D;AACA,iBAAO,EAAE,MAAM,QAAQ;AAAA,QACzB;AAAA,QACA,UAAU,CAAC,EAAE,OAAO,OAAmD;AAAA,UACrE,IAAI,QAAQ,UAAU;AAAA,UACtB,CAAC,aAAa,GAAG,SAAS,aAAa,KAAK;AAAA,QAC9C;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,WAAW,GAAG,aAAa;AAAA,QAC3B,QAAQ;AAAA,QACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAsC;AAChE,gBAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,gBAAM,UAAU,aAAa;AAAA,YAC3B,kBAAkB,eAAe,GAAG,KAAK,CAAC,GAAG,KAAK,SAAS;AAAA,UAC7D;AACA,cAAI,CAAC,QAAQ,MAAM,CAAC,QAAQ,aAAa,GAAG;AAC1C,kBAAM,IAAI,cAAc,KAAK;AAAA,cAC3B,OAAO;AAAA,gBACL;AAAA,gBACA;AAAA,cACF;AAAA,YACF,CAAC;AAAA,UACH;AACA,iBAAO,EAAE,MAAM,QAAQ;AAAA,QACzB;AAAA,QACA,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,MAC9B;AAAA,IACF;AAAA,EACF,CAAC;AAED,QAAM,iBAAiB,EAAE,OAAO;AAAA,IAC9B,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,IACpB,CAAC,cAAc,GAAG,EAAE,OAAO,EAAE,KAAK;AAAA,IAClC,aAAa,EAAE,OAAO;AAAA,IACtB,MAAM,EAAE,OAAO;AAAA,IACf,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IACvD,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACvC,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IAClD,oBAAoB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IAC1D,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,IACxE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACrC,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAC5C,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACxC,UAAU,EAAE,OAAO;AAAA,IACnB,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAC9C,qBAAqB,EAAE,OAAO;AAAA,IAC9B,iBAAiB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAChD,cAAc;AAAA,IACd,eAAe,EAAE,OAAO;AAAA,IACxB,gBAAgB,EAAE,OAAO;AAAA,IACzB,kBAAkB,EAAE,OAAO;AAAA,IAC3B,iBAAiB,EAAE,OAAO;AAAA,IAC1B,kBAAkB,EAAE,OAAO;AAAA,IAC3B,UAAU,EAAE,OAAO;AAAA,IACnB,YAAY,EAAE,OAAO;AAAA,IACrB,kBAAkB,EAAE,OAAO;AAAA,IAC3B,oBAAoB,EAAE,OAAO;AAAA,IAC7B,eAAe,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,IACrE,gBAAgB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAC/C,oBAAoB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,IAC1E,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,IAChE,qBAAqB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IAC3D,YAAY,EAAE,OAAO;AAAA,IACrB,YAAY,EAAE,OAAO;AAAA,EACvB,CAAC;AAED,QAAM,uBAAuB,EAAE,OAAO;AAAA,IACpC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,IAC/B,CAAC,aAAa,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC9C,CAAC;AAED,QAAM,UAAU,uBAAuB;AAAA,IACrC,cAAc,OAAO,QAAQ;AAAA,IAC7B,aAAa;AAAA,IACb,oBAAoB,8BAA8B,cAAc;AAAA,IAChE,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,aAAa,WAAW,OAAO,QAAQ,WAAW;AAAA,IACpD;AAAA,IACA,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,aAAa,WAAW,OAAO,QAAQ,WAAW;AAAA,IACpD;AAAA,IACA,KAAK;AAAA,MACH,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,aAAa,WAAW,OAAO,QAAQ,WAAW;AAAA,IACpD;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA,KAAK,KAAK;AAAA,IACV,MAAM,KAAK;AAAA,IACX,KAAK,KAAK;AAAA,IACV,QAAQ,KAAK;AAAA,EACf;AACF;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n buildCustomFieldFiltersFromQuery,\n extractAllCustomFieldEntries,\n} from '@open-mercato/shared/lib/crud/custom-fields'\nimport {\n canonicalizeUnitCode,\n REFERENCE_UNIT_CODES,\n} from '@open-mercato/shared/lib/units/unitCodes'\nimport {\n createPagedListResponseSchema,\n createSalesCrudOpenApi,\n defaultOkResponseSchema,\n} from '../api/openapi'\nimport { withScopedPayload } from '../api/utils'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- MikroORM entity class constructor\ntype EntityClass = new (...args: any[]) => unknown\n\ninterface SalesLineRouteConfig {\n entity: EntityClass\n entityId: string\n fieldConstants: Record<string, string>\n parentFkColumn: string\n parentFkParam: string\n createSchema: z.ZodObject<z.ZodRawShape>\n /** Extra fields accepted by the write payloads on top of `createSchema`. */\n writeExtensionShape?: z.ZodRawShape\n features: { view: string; manage: string }\n commandPrefix: string\n openApi: {\n resourceName: string\n description: string\n }\n}\n\nconst rawBodySchema = z.object({}).passthrough()\n\nfunction resolveRawBody(raw: unknown): Record<string, unknown> {\n if (!raw || typeof raw !== 'object') return {}\n if ('body' in raw) {\n const payload = raw as { body?: unknown }\n if (payload.body && typeof payload.body === 'object') {\n return payload.body as Record<string, unknown>\n }\n }\n return raw as Record<string, unknown>\n}\n\nfunction transformItem(item: Record<string, unknown> | null | undefined) {\n if (!item) return item\n const normalized = { ...item }\n const cfEntries = extractAllCustomFieldEntries(item)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) delete normalized[key]\n }\n const quantityUnit = canonicalizeUnitCode(\n normalized['quantity_unit'] ?? normalized['quantityUnit'],\n )\n const normalizedUnit =\n canonicalizeUnitCode(\n normalized['normalized_unit'] ?? normalized['normalizedUnit'],\n ) ?? quantityUnit\n return {\n ...normalized,\n quantity_unit: quantityUnit,\n normalized_unit: normalizedUnit,\n ...cfEntries,\n }\n}\n\nconst uomSnapshotOpenApiSchema = z\n .object({\n version: z.literal(1),\n productId: z.string().nullable(),\n productVariantId: z.string().nullable(),\n baseUnitCode: z.string().nullable(),\n enteredUnitCode: z.string().nullable(),\n enteredQuantity: z.string(),\n toBaseFactor: z.string(),\n normalizedQuantity: z.string(),\n rounding: z.object({\n mode: z.enum(['half_up', 'down', 'up']),\n scale: z.number().int(),\n }),\n source: z.object({\n conversionId: z.string().nullable(),\n resolvedAt: z.string(),\n }),\n unitPriceReference: z\n .object({\n enabled: z.boolean(),\n referenceUnitCode: z.enum(REFERENCE_UNIT_CODES).nullable(),\n baseQuantity: z.string().nullable(),\n grossPerReference: z.string().nullable().optional(),\n netPerReference: z.string().nullable().optional(),\n })\n .optional(),\n })\n .nullable()\n .optional()\n\nexport function makeSalesLineRoute(config: SalesLineRouteConfig) {\n const {\n entity,\n entityId,\n fieldConstants: F,\n parentFkColumn,\n parentFkParam,\n createSchema,\n writeExtensionShape,\n features,\n commandPrefix,\n } = config\n\n const listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n id: z.string().uuid().optional(),\n [parentFkParam]: z.string().uuid().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\n const upsertSchema = createSchema.extend({\n id: z.string().uuid().optional(),\n ...(writeExtensionShape ?? {}),\n })\n\n const deleteSchema = z.object({\n id: z.string().uuid(),\n [parentFkParam]: z.string().uuid(),\n ...(writeExtensionShape ?? {}),\n })\n\n const routeMetadata = {\n GET: { requireAuth: true, requireFeatures: [features.view] },\n POST: { requireAuth: true, requireFeatures: [features.manage] },\n PUT: { requireAuth: true, requireFeatures: [features.manage] },\n DELETE: { requireAuth: true, requireFeatures: [features.manage] },\n }\n\n const crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: {\n entityType: entityId,\n },\n enrichers: {\n entityId,\n },\n list: {\n schema: listSchema,\n entityId,\n fields: (() => {\n const fields: string[] = [\n F.id,\n parentFkColumn,\n F.line_number,\n F.kind,\n F.status_entry_id,\n F.status,\n F.product_id,\n F.product_variant_id,\n F.catalog_snapshot,\n F.name,\n F.description,\n F.comment,\n F.organization_id,\n F.tenant_id,\n F.quantity,\n F.quantity_unit,\n F.normalized_quantity,\n F.normalized_unit,\n F.uom_snapshot,\n F.currency_code,\n F.unit_price_net,\n F.unit_price_gross,\n F.discount_amount,\n F.discount_percent,\n F.tax_rate,\n F.tax_amount,\n F.total_net_amount,\n F.total_gross_amount,\n F.configuration,\n F.promotion_code,\n F.promotion_snapshot,\n F.metadata,\n F.custom_field_set_id,\n F.created_at,\n F.updated_at,\n ]\n const returnedQuantity = F['returned_quantity']\n if (typeof returnedQuantity === 'string') fields.push(returnedQuantity)\n const amountsMode = F['amounts_mode']\n if (typeof amountsMode === 'string') fields.push(amountsMode)\n return fields\n })(),\n sortFieldMap: {\n id: F.id,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n lineNumber: F.line_number,\n },\n // Line ids are random v4 UUIDs, so the factory's `id` default orders a\n // document's lines arbitrarily and reshuffles them whenever an integration\n // rewrites the lines (delete-and-reinsert is the normal upsert shape).\n // `line_number` is the document's real reading order; `id` only breaks ties\n // between legacy rows that all still sit at the column default of 0.\n defaultSort: { field: 'lineNumber', dir: 'asc' },\n tiebreakSortField: 'id',\n buildFilters: async (query: Record<string, unknown>, ctx: CrudCtx) => {\n const filters: Record<string, unknown> = {}\n if (query.id) filters.id = { $eq: query.id }\n if (query[parentFkParam]) filters[parentFkColumn] = { $eq: query[parentFkParam] }\n try {\n const em = ctx.container.resolve<EntityManager>('em')\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityId,\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch {\n // ignore\n }\n return filters\n },\n transformItem,\n },\n actions: {\n create: {\n commandId: `${commandPrefix}.upsert`,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const payload = upsertSchema.parse(\n withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),\n )\n return { body: payload }\n },\n response: ({ result }: { result: Record<string, unknown> | null }) => ({\n id: result?.lineId ?? null,\n [parentFkParam]: result?.[parentFkParam] ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: `${commandPrefix}.upsert`,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const payload = upsertSchema.parse(\n withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),\n )\n return { body: payload }\n },\n response: ({ result }: { result: Record<string, unknown> | null }) => ({\n id: result?.lineId ?? null,\n [parentFkParam]: result?.[parentFkParam] ?? null,\n }),\n },\n delete: {\n commandId: `${commandPrefix}.delete`,\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }: { raw: unknown; ctx: CrudCtx }) => {\n const { translate } = await resolveTranslations()\n const payload = deleteSchema.parse(\n withScopedPayload(resolveRawBody(raw) ?? {}, ctx, translate),\n )\n if (!payload.id || !payload[parentFkParam]) {\n throw new CrudHttpError(400, {\n error: translate(\n 'sales.documents.detail.error',\n 'Document not found or inaccessible.',\n ),\n })\n }\n return { body: payload }\n },\n response: () => ({ ok: true }),\n },\n },\n })\n\n const lineItemSchema = z.object({\n id: z.string().uuid(),\n [parentFkColumn]: z.string().uuid(),\n line_number: z.number(),\n kind: z.string(),\n status_entry_id: z.string().uuid().nullable().optional(),\n status: z.string().nullable().optional(),\n product_id: z.string().uuid().nullable().optional(),\n product_variant_id: z.string().uuid().nullable().optional(),\n catalog_snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n name: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n comment: z.string().nullable().optional(),\n quantity: z.number(),\n quantity_unit: z.string().nullable().optional(),\n normalized_quantity: z.number(),\n normalized_unit: z.string().nullable().optional(),\n uom_snapshot: uomSnapshotOpenApiSchema,\n currency_code: z.string(),\n unit_price_net: z.number(),\n unit_price_gross: z.number(),\n discount_amount: z.number(),\n discount_percent: z.number(),\n tax_rate: z.number(),\n tax_amount: z.number(),\n total_net_amount: z.number(),\n total_gross_amount: z.number(),\n configuration: z.record(z.string(), z.unknown()).nullable().optional(),\n promotion_code: z.string().nullable().optional(),\n promotion_snapshot: z.record(z.string(), z.unknown()).nullable().optional(),\n metadata: z.record(z.string(), z.unknown()).nullable().optional(),\n custom_field_set_id: z.string().uuid().nullable().optional(),\n amounts_mode: z.enum(['computed', 'external']).optional(),\n created_at: z.string(),\n updated_at: z.string(),\n })\n\n const upsertResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n [parentFkParam]: z.string().uuid().nullable(),\n })\n\n const openApi = createSalesCrudOpenApi({\n resourceName: config.openApi.resourceName,\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(lineItemSchema),\n create: {\n schema: upsertSchema,\n responseSchema: upsertResponseSchema,\n description: `Creates ${config.openApi.description}.`,\n },\n update: {\n schema: upsertSchema,\n responseSchema: upsertResponseSchema,\n description: `Updates ${config.openApi.description}.`,\n },\n del: {\n schema: deleteSchema,\n responseSchema: defaultOkResponseSchema,\n description: `Deletes ${config.openApi.description}.`,\n },\n })\n\n return {\n metadata: routeMetadata,\n openApi,\n GET: crud.GET,\n POST: crud.POST,\n PUT: crud.PUT,\n DELETE: crud.DELETE,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import { registerSalesTotalsCalculator, rebuildDocumentResult } from "../calculations.js";
1
+ import { isExternalAmountsMode, registerSalesTotalsCalculator, rebuildDocumentResult } from "../calculations.js";
2
2
  import {
3
3
  getPaymentProvider,
4
4
  getShippingProvider,
@@ -127,7 +127,8 @@ function applyProviderResult(current, adjustments, providerKey, calculatorKey, r
127
127
  function ensureProviderTotalsCalculator() {
128
128
  if (totalsRegistered) return;
129
129
  totalsRegistered = true;
130
- registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus }) => {
130
+ registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus, totalsMode }) => {
131
+ if (isExternalAmountsMode(totalsMode)) return current;
131
132
  const metadata = context.metadata ?? {};
132
133
  const shippingMethod = metadata.shippingMethod ?? null;
133
134
  const paymentMethod = metadata.paymentMethod ?? null;