@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/providers/totals.ts"],
4
- "sourcesContent": ["import { registerSalesTotalsCalculator, rebuildDocumentResult } from '../calculations'\nimport type { SalesAdjustmentDraft, SalesDocumentCalculationResult } from '../types'\nimport {\n getPaymentProvider,\n getShippingProvider,\n normalizeProviderSettings,\n} from './registry'\nimport type {\n PaymentMethodContext,\n ProviderAdjustment,\n ProviderAdjustmentResult,\n ShippingMethodContext,\n ShippingMetrics,\n} from './types'\n\nconst SHIPPING_PREFIX = 'shipping-provider:'\nconst PAYMENT_PREFIX = 'payment-provider:'\n\nlet totalsRegistered = false\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 isProviderAdjustment(adjustment: SalesAdjustmentDraft): boolean {\n const key = adjustment.calculatorKey ?? ''\n return typeof key === 'string' && (key.startsWith(SHIPPING_PREFIX) || key.startsWith(PAYMENT_PREFIX))\n}\n\nfunction isManualOverride(adjustment: SalesAdjustmentDraft): boolean {\n const metadata = adjustment.metadata\n if (!metadata || typeof metadata !== 'object') return false\n const manual =\n (metadata as Record<string, unknown>).manualOverride ??\n (metadata as Record<string, unknown>).manual_override\n return manual === true\n}\n\nfunction withoutPrefix(adjustments: SalesAdjustmentDraft[], prefix: string) {\n return adjustments.filter((adj) => {\n if (!(adj.calculatorKey ?? '').startsWith(prefix)) return true\n return isManualOverride(adj)\n })\n}\n\nfunction extractProviderSettings(\n method: ShippingMethodContext | PaymentMethodContext | null | undefined\n): Record<string, unknown> | null {\n if (!method) return null\n if (method.providerSettings && typeof method.providerSettings === 'object') {\n return method.providerSettings as Record<string, unknown>\n }\n if (method.metadata && typeof method.metadata === 'object') {\n const meta = method.metadata as Record<string, unknown>\n if (meta.providerSettings && typeof meta.providerSettings === 'object') {\n return meta.providerSettings as Record<string, unknown>\n }\n }\n return null\n}\n\nfunction computeMetrics(lines: SalesDocumentCalculationResult['lines']): ShippingMetrics {\n let itemCount = 0\n let totalWeight = 0\n let totalVolume = 0\n let subtotalNet = 0\n let subtotalGross = 0\n\n for (const entry of lines) {\n const { line } = entry\n const qty = toNumber(line.quantity, 0)\n itemCount += qty\n subtotalNet += toNumber(entry.netAmount, 0)\n subtotalGross += toNumber(entry.grossAmount, 0)\n\n const meta = (line.metadata ?? {}) as Record<string, unknown>\n const configuration = (line.configuration ?? {}) as Record<string, unknown>\n const weight =\n toNumber((meta.weight as number | string | undefined) ?? (configuration.weight as number | string | undefined), 0) *\n qty\n const volume =\n toNumber(\n (meta.volume as number | string | undefined) ??\n (configuration.volume as number | string | undefined),\n 0\n ) * qty\n\n totalWeight += weight\n totalVolume += volume\n }\n\n return {\n itemCount,\n totalWeight,\n totalVolume,\n subtotalNet,\n subtotalGross,\n }\n}\n\nfunction normalizeAdjustments(params: {\n providerKey: string\n calculatorKey: string\n adjustments: ProviderAdjustment[]\n currencyCode: string\n defaultKind: SalesAdjustmentDraft['kind']\n}) {\n return params.adjustments.map<SalesAdjustmentDraft>((adj, index) => ({\n scope: 'order',\n kind: adj.kind ?? params.defaultKind,\n code: adj.code ?? params.providerKey,\n label: adj.label ?? params.providerKey,\n calculatorKey: params.calculatorKey,\n promotionId: null,\n rate: null,\n amountNet: toNumber(adj.amountNet, 0),\n amountGross:\n adj.amountGross === undefined || adj.amountGross === null\n ? toNumber(adj.amountNet, 0)\n : toNumber(adj.amountGross, 0),\n currencyCode: (adj.currencyCode || params.currencyCode || '').toUpperCase() || params.currencyCode,\n metadata: adj.metadata ?? null,\n position: 10_000 + index,\n }))\n}\n\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n key: string,\n value: unknown\n): Record<string, unknown> {\n const next = { ...(base ?? {}) }\n next[key] = value\n return next\n}\n\nfunction applyProviderResult(\n current: SalesDocumentCalculationResult,\n adjustments: SalesAdjustmentDraft[],\n providerKey: string,\n calculatorKey: string,\n result: ProviderAdjustmentResult | null | undefined,\n defaultKind: SalesAdjustmentDraft['kind']\n): SalesDocumentCalculationResult {\n if (!result?.adjustments?.length) {\n return rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments,\n metadata: current.metadata,\n })\n }\n const normalized = normalizeAdjustments({\n providerKey,\n calculatorKey,\n adjustments: result.adjustments,\n currencyCode: current.currencyCode,\n defaultKind,\n })\n const nextAdjustments = [...adjustments, ...normalized]\n const next = rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments: nextAdjustments,\n metadata: current.metadata,\n })\n if (result.metadata) {\n next.metadata = mergeMetadata(next.metadata, calculatorKey, result.metadata)\n }\n return next\n}\n\nexport function ensureProviderTotalsCalculator() {\n if (totalsRegistered) return\n totalsRegistered = true\n\n registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus }) => {\n const metadata = (context.metadata ?? {}) as Record<string, unknown>\n const shippingMethod = (metadata.shippingMethod ?? null) as ShippingMethodContext | null\n const paymentMethod = (metadata.paymentMethod ?? null) as PaymentMethodContext | null\n\n let runningAdjustments = (current.adjustments ?? []).filter(\n (adj) => !isProviderAdjustment(adj) || isManualOverride(adj)\n )\n let working = rebuildDocumentResult({\n documentKind,\n currencyCode: current.currencyCode,\n lines,\n adjustments: runningAdjustments,\n metadata: current.metadata,\n })\n\n if (shippingMethod?.providerKey) {\n const provider = getShippingProvider(shippingMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${SHIPPING_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n // Respect user overrides: keep existing provider-tagged adjustments and skip regeneration.\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n const rawSettings = extractProviderSettings(shippingMethod)\n const settings =\n normalizeProviderSettings('shipping', provider.key, rawSettings) ?? rawSettings ?? {}\n const metrics = computeMetrics(working.lines)\n let result = await provider.calculate({\n method: shippingMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n metrics,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.shipping.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(working, runningAdjustments, provider.key, calculatorKey, result, 'shipping')\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.shipping.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n if (paymentMethod?.providerKey) {\n const provider = getPaymentProvider(paymentMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${PAYMENT_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n runningAdjustments = withoutPrefix(runningAdjustments, PAYMENT_PREFIX)\n const rawSettings = extractProviderSettings(paymentMethod)\n const settings =\n normalizeProviderSettings('payment', provider.key, rawSettings) ?? rawSettings ?? {}\n let result = await provider.calculate({\n method: paymentMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.payment.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(\n working,\n runningAdjustments,\n provider.key,\n calculatorKey,\n result,\n 'surcharge'\n )\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.payment.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n return working\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { isExternalAmountsMode, registerSalesTotalsCalculator, rebuildDocumentResult } from '../calculations'\nimport type { SalesAdjustmentDraft, SalesDocumentCalculationResult } from '../types'\nimport {\n getPaymentProvider,\n getShippingProvider,\n normalizeProviderSettings,\n} from './registry'\nimport type {\n PaymentMethodContext,\n ProviderAdjustment,\n ProviderAdjustmentResult,\n ShippingMethodContext,\n ShippingMetrics,\n} from './types'\n\nconst SHIPPING_PREFIX = 'shipping-provider:'\nconst PAYMENT_PREFIX = 'payment-provider:'\n\nlet totalsRegistered = false\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 isProviderAdjustment(adjustment: SalesAdjustmentDraft): boolean {\n const key = adjustment.calculatorKey ?? ''\n return typeof key === 'string' && (key.startsWith(SHIPPING_PREFIX) || key.startsWith(PAYMENT_PREFIX))\n}\n\nfunction isManualOverride(adjustment: SalesAdjustmentDraft): boolean {\n const metadata = adjustment.metadata\n if (!metadata || typeof metadata !== 'object') return false\n const manual =\n (metadata as Record<string, unknown>).manualOverride ??\n (metadata as Record<string, unknown>).manual_override\n return manual === true\n}\n\nfunction withoutPrefix(adjustments: SalesAdjustmentDraft[], prefix: string) {\n return adjustments.filter((adj) => {\n if (!(adj.calculatorKey ?? '').startsWith(prefix)) return true\n return isManualOverride(adj)\n })\n}\n\nfunction extractProviderSettings(\n method: ShippingMethodContext | PaymentMethodContext | null | undefined\n): Record<string, unknown> | null {\n if (!method) return null\n if (method.providerSettings && typeof method.providerSettings === 'object') {\n return method.providerSettings as Record<string, unknown>\n }\n if (method.metadata && typeof method.metadata === 'object') {\n const meta = method.metadata as Record<string, unknown>\n if (meta.providerSettings && typeof meta.providerSettings === 'object') {\n return meta.providerSettings as Record<string, unknown>\n }\n }\n return null\n}\n\nfunction computeMetrics(lines: SalesDocumentCalculationResult['lines']): ShippingMetrics {\n let itemCount = 0\n let totalWeight = 0\n let totalVolume = 0\n let subtotalNet = 0\n let subtotalGross = 0\n\n for (const entry of lines) {\n const { line } = entry\n const qty = toNumber(line.quantity, 0)\n itemCount += qty\n subtotalNet += toNumber(entry.netAmount, 0)\n subtotalGross += toNumber(entry.grossAmount, 0)\n\n const meta = (line.metadata ?? {}) as Record<string, unknown>\n const configuration = (line.configuration ?? {}) as Record<string, unknown>\n const weight =\n toNumber((meta.weight as number | string | undefined) ?? (configuration.weight as number | string | undefined), 0) *\n qty\n const volume =\n toNumber(\n (meta.volume as number | string | undefined) ??\n (configuration.volume as number | string | undefined),\n 0\n ) * qty\n\n totalWeight += weight\n totalVolume += volume\n }\n\n return {\n itemCount,\n totalWeight,\n totalVolume,\n subtotalNet,\n subtotalGross,\n }\n}\n\nfunction normalizeAdjustments(params: {\n providerKey: string\n calculatorKey: string\n adjustments: ProviderAdjustment[]\n currencyCode: string\n defaultKind: SalesAdjustmentDraft['kind']\n}) {\n return params.adjustments.map<SalesAdjustmentDraft>((adj, index) => ({\n scope: 'order',\n kind: adj.kind ?? params.defaultKind,\n code: adj.code ?? params.providerKey,\n label: adj.label ?? params.providerKey,\n calculatorKey: params.calculatorKey,\n promotionId: null,\n rate: null,\n amountNet: toNumber(adj.amountNet, 0),\n amountGross:\n adj.amountGross === undefined || adj.amountGross === null\n ? toNumber(adj.amountNet, 0)\n : toNumber(adj.amountGross, 0),\n currencyCode: (adj.currencyCode || params.currencyCode || '').toUpperCase() || params.currencyCode,\n metadata: adj.metadata ?? null,\n position: 10_000 + index,\n }))\n}\n\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n key: string,\n value: unknown\n): Record<string, unknown> {\n const next = { ...(base ?? {}) }\n next[key] = value\n return next\n}\n\nfunction applyProviderResult(\n current: SalesDocumentCalculationResult,\n adjustments: SalesAdjustmentDraft[],\n providerKey: string,\n calculatorKey: string,\n result: ProviderAdjustmentResult | null | undefined,\n defaultKind: SalesAdjustmentDraft['kind']\n): SalesDocumentCalculationResult {\n if (!result?.adjustments?.length) {\n return rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments,\n metadata: current.metadata,\n })\n }\n const normalized = normalizeAdjustments({\n providerKey,\n calculatorKey,\n adjustments: result.adjustments,\n currencyCode: current.currencyCode,\n defaultKind,\n })\n const nextAdjustments = [...adjustments, ...normalized]\n const next = rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments: nextAdjustments,\n metadata: current.metadata,\n })\n if (result.metadata) {\n next.metadata = mergeMetadata(next.metadata, calculatorKey, result.metadata)\n }\n return next\n}\n\nexport function ensureProviderTotalsCalculator() {\n if (totalsRegistered) return\n totalsRegistered = true\n\n registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus, totalsMode }) => {\n // A provider charge here could not move the caller's header, so it would show\n // in the breakdown and be absent from the total.\n if (isExternalAmountsMode(totalsMode)) return current\n const metadata = (context.metadata ?? {}) as Record<string, unknown>\n const shippingMethod = (metadata.shippingMethod ?? null) as ShippingMethodContext | null\n const paymentMethod = (metadata.paymentMethod ?? null) as PaymentMethodContext | null\n\n let runningAdjustments = (current.adjustments ?? []).filter(\n (adj) => !isProviderAdjustment(adj) || isManualOverride(adj)\n )\n let working = rebuildDocumentResult({\n documentKind,\n currencyCode: current.currencyCode,\n lines,\n adjustments: runningAdjustments,\n metadata: current.metadata,\n })\n\n if (shippingMethod?.providerKey) {\n const provider = getShippingProvider(shippingMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${SHIPPING_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n // Respect user overrides: keep existing provider-tagged adjustments and skip regeneration.\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n const rawSettings = extractProviderSettings(shippingMethod)\n const settings =\n normalizeProviderSettings('shipping', provider.key, rawSettings) ?? rawSettings ?? {}\n const metrics = computeMetrics(working.lines)\n let result = await provider.calculate({\n method: shippingMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n metrics,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.shipping.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(working, runningAdjustments, provider.key, calculatorKey, result, 'shipping')\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.shipping.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n if (paymentMethod?.providerKey) {\n const provider = getPaymentProvider(paymentMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${PAYMENT_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n runningAdjustments = withoutPrefix(runningAdjustments, PAYMENT_PREFIX)\n const rawSettings = extractProviderSettings(paymentMethod)\n const settings =\n normalizeProviderSettings('payment', provider.key, rawSettings) ?? rawSettings ?? {}\n let result = await provider.calculate({\n method: paymentMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.payment.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(\n working,\n runningAdjustments,\n provider.key,\n calculatorKey,\n result,\n 'surcharge'\n )\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.payment.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n return working\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,19 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260910120000_sales_external_amounts_mode extends Migration {
3
+ async up() {
4
+ this.addSql(
5
+ `alter table "sales_orders" add column if not exists "totals_mode" text not null default 'computed';`
6
+ );
7
+ this.addSql(
8
+ `alter table "sales_order_lines" add column if not exists "amounts_mode" text not null default 'computed';`
9
+ );
10
+ }
11
+ async down() {
12
+ this.addSql(`alter table "sales_order_lines" drop column if exists "amounts_mode";`);
13
+ this.addSql(`alter table "sales_orders" drop column if exists "totals_mode";`);
14
+ }
15
+ }
16
+ export {
17
+ Migration20260910120000_sales_external_amounts_mode
18
+ };
19
+ //# sourceMappingURL=Migration20260910120000_sales_external_amounts_mode.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n// Opt-in `external` amounts mode for sales orders: the row-level statement that\n// the stored amounts are a caller's assertion mirrored from an external book of\n// record, not something core derived. Two defaulted columns, no backfill \u2014 every\n// existing row takes 'computed', which is exactly today's behaviour, so the\n// change is observably inert until a caller sets the mode.\n//\n// PostgreSQL has not rewritten a table for a defaulted column add since 11, so\n// both statements are metadata-only regardless of table size.\nexport class Migration20260910120000_sales_external_amounts_mode extends Migration {\n override async up(): Promise<void> {\n this.addSql(\n `alter table \"sales_orders\" add column if not exists \"totals_mode\" text not null default 'computed';`,\n );\n this.addSql(\n `alter table \"sales_order_lines\" add column if not exists \"amounts_mode\" text not null default 'computed';`,\n );\n }\n\n override async down(): Promise<void> {\n this.addSql(`alter table \"sales_order_lines\" drop column if exists \"amounts_mode\";`);\n this.addSql(`alter table \"sales_orders\" drop column if exists \"totals_mode\";`);\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAUnB,MAAM,4DAA4D,UAAU;AAAA,EACjF,MAAe,KAAoB;AACjC,SAAK;AAAA,MACH;AAAA,IACF;AACA,SAAK;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,uEAAuE;AACnF,SAAK,OAAO,iEAAiE;AAAA,EAC/E;AACF;",
6
+ "names": []
7
+ }
@@ -43,6 +43,7 @@ export const totals_snapshot = "totals_snapshot";
43
43
  export const paid_total_amount = "paid_total_amount";
44
44
  export const refunded_total_amount = "refunded_total_amount";
45
45
  export const outstanding_amount = "outstanding_amount";
46
+ export const totals_mode = "totals_mode";
46
47
  export const line_item_count = "line_item_count";
47
48
  export const metadata = "metadata";
48
49
  export const custom_field_set_id = "custom_field_set_id";
@@ -30,6 +30,7 @@ export const tax_rate = "tax_rate";
30
30
  export const tax_amount = "tax_amount";
31
31
  export const total_net_amount = "total_net_amount";
32
32
  export const total_gross_amount = "total_gross_amount";
33
+ export const amounts_mode = "amounts_mode";
33
34
  export const configuration = "configuration";
34
35
  export const promotion_code = "promotion_code";
35
36
  export const promotion_snapshot = "promotion_snapshot";
@@ -3054,6 +3054,7 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
3054
3054
  "paid_total_amount": "paid_total_amount",
3055
3055
  "refunded_total_amount": "refunded_total_amount",
3056
3056
  "outstanding_amount": "outstanding_amount",
3057
+ "totals_mode": "totals_mode",
3057
3058
  "line_item_count": "line_item_count",
3058
3059
  "metadata": "metadata",
3059
3060
  "custom_field_set_id": "custom_field_set_id",
@@ -3136,6 +3137,7 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
3136
3137
  "tax_amount": "tax_amount",
3137
3138
  "total_net_amount": "total_net_amount",
3138
3139
  "total_gross_amount": "total_gross_amount",
3140
+ "amounts_mode": "amounts_mode",
3139
3141
  "configuration": "configuration",
3140
3142
  "promotion_code": "promotion_code",
3141
3143
  "promotion_snapshot": "promotion_snapshot",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7723.1.3b2ea82ea7",
3
+ "version": "0.10.1-develop.7724.1.95e4e7d38e",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -265,16 +265,16 @@
265
265
  "zod": "^4.4.3"
266
266
  },
267
267
  "peerDependencies": {
268
- "@open-mercato/ai-assistant": "0.10.1-develop.7723.1.3b2ea82ea7",
269
- "@open-mercato/shared": "0.10.1-develop.7723.1.3b2ea82ea7",
270
- "@open-mercato/ui": "0.10.1-develop.7723.1.3b2ea82ea7",
268
+ "@open-mercato/ai-assistant": "0.10.1-develop.7724.1.95e4e7d38e",
269
+ "@open-mercato/shared": "0.10.1-develop.7724.1.95e4e7d38e",
270
+ "@open-mercato/ui": "0.10.1-develop.7724.1.95e4e7d38e",
271
271
  "react": "^19.0.0",
272
272
  "react-dom": "^19.0.0"
273
273
  },
274
274
  "devDependencies": {
275
- "@open-mercato/ai-assistant": "0.10.1-develop.7723.1.3b2ea82ea7",
276
- "@open-mercato/shared": "0.10.1-develop.7723.1.3b2ea82ea7",
277
- "@open-mercato/ui": "0.10.1-develop.7723.1.3b2ea82ea7",
275
+ "@open-mercato/ai-assistant": "0.10.1-develop.7724.1.95e4e7d38e",
276
+ "@open-mercato/shared": "0.10.1-develop.7724.1.95e4e7d38e",
277
+ "@open-mercato/ui": "0.10.1-develop.7724.1.95e4e7d38e",
278
278
  "@testing-library/dom": "^10.4.1",
279
279
  "@testing-library/jest-dom": "^7.0.1",
280
280
  "@testing-library/react": "^16.3.3",
@@ -424,6 +424,7 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
424
424
 
425
425
  const orderOnlyFields = [
426
426
  'expected_delivery_at',
427
+ 'totals_mode',
427
428
  'shipping_net_amount',
428
429
  'shipping_gross_amount',
429
430
  'surcharge_total_amount',
@@ -535,6 +536,7 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
535
536
  paidTotalAmount: toNumber(item.paid_total_amount),
536
537
  refundedTotalAmount: toNumber(item.refunded_total_amount),
537
538
  outstandingAmount: toNumber(item.outstanding_amount),
539
+ ...(binding.kind === 'order' ? { totalsMode: item.totals_mode ?? 'computed' } : {}),
538
540
  customerSnapshot: normalizeJsonRecord(item.customer_snapshot),
539
541
  billingAddressSnapshot: normalizeJsonRecord(item.billing_address_snapshot),
540
542
  shippingAddressSnapshot: normalizeJsonRecord(item.shipping_address_snapshot),
@@ -665,6 +667,7 @@ export function buildDocumentOpenApi(binding: DocumentBinding) {
665
667
  paidTotalAmount: z.number().nullable().optional(),
666
668
  refundedTotalAmount: z.number().nullable().optional(),
667
669
  outstandingAmount: z.number().nullable().optional(),
670
+ totalsMode: z.enum(['computed', 'external']).optional(),
668
671
  createdAt: z.string(),
669
672
  updatedAt: z.string(),
670
673
  customFields: z.record(z.string(), z.unknown()).optional(),
@@ -1,5 +1,5 @@
1
1
  import { SalesOrderLine } from "../../data/entities";
2
- import { orderLineCreateSchema } from "../../data/validators";
2
+ import { orderHeaderTotalsSchema, orderLineCreateSchema } from "../../data/validators";
3
3
  import { E } from "#generated/entities.ids.generated";
4
4
  import * as F from "#generated/entities/sales_order_line";
5
5
  import { makeSalesLineRoute } from "../../lib/makeSalesLineRoute";
@@ -11,6 +11,7 @@ const route = makeSalesLineRoute({
11
11
  parentFkColumn: "order_id",
12
12
  parentFkParam: "orderId",
13
13
  createSchema: orderLineCreateSchema,
14
+ writeExtensionShape: { orderTotals: orderHeaderTotalsSchema.optional() },
14
15
  features: { view: "sales.orders.view", manage: "sales.orders.manage" },
15
16
  commandPrefix: "sales.orders.lines",
16
17
  openApi: {
@@ -77,6 +77,7 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
77
77
  import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
78
78
  import { useSalesChannelsEnabled } from '@open-mercato/core/modules/sales/components/useSalesChannelsEnabled'
79
79
  import { createLogger } from '@open-mercato/shared/lib/logger'
80
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
80
81
 
81
82
  const logger = createLogger('sales')
82
83
 
@@ -899,6 +900,7 @@ type DocumentRecord = {
899
900
  paidTotalAmount?: number | null
900
901
  refundedTotalAmount?: number | null
901
902
  outstandingAmount?: number | null
903
+ totalsMode?: 'computed' | 'external' | null
902
904
  createdAt?: string
903
905
  updatedAt?: string
904
906
  metadata?: Record<string, unknown> | null
@@ -1478,6 +1480,7 @@ function MethodInlineEditor({
1478
1480
  emptyResultsLabel,
1479
1481
  selectedHint,
1480
1482
  icon,
1483
+ note,
1481
1484
  allowClear = true,
1482
1485
  }: {
1483
1486
  label: string
@@ -1494,6 +1497,7 @@ function MethodInlineEditor({
1494
1497
  emptyResultsLabel: string
1495
1498
  selectedHint: (id: string) => string
1496
1499
  icon: React.ReactNode
1500
+ note?: string | null
1497
1501
  allowClear?: boolean
1498
1502
  }) {
1499
1503
  const t = useT()
@@ -1607,6 +1611,7 @@ function MethodInlineEditor({
1607
1611
  <div className="flex items-start justify-between gap-2">
1608
1612
  <div className="flex-1 min-w-0">
1609
1613
  <p className="text-xs uppercase tracking-wide text-muted-foreground">{label}</p>
1614
+ {note ? <p className="mt-1 text-xs text-muted-foreground">{note}</p> : null}
1610
1615
  {editing ? (
1611
1616
  <div
1612
1617
  className="mt-2 space-y-2"
@@ -1937,6 +1942,7 @@ export default function SalesDocumentDetailPage({
1937
1942
  const [generating, setGenerating] = React.useState(false)
1938
1943
  const [converting, setConverting] = React.useState(false)
1939
1944
  const [deleting, setDeleting] = React.useState(false)
1945
+ const [switchingToComputedAmounts, setSwitchingToComputedAmounts] = React.useState(false)
1940
1946
  const [sending, setSending] = React.useState(false)
1941
1947
  const [sendOpen, setSendOpen] = React.useState(false)
1942
1948
  const [validForDays, setValidForDays] = React.useState(DEFAULT_QUOTE_VALID_FOR_DAYS)
@@ -2844,6 +2850,8 @@ export default function SalesDocumentDetailPage({
2844
2850
  },
2845
2851
  [t]
2846
2852
  )
2853
+ const amountsAreExternal = kind === 'order' && record?.totalsMode === 'external'
2854
+
2847
2855
  const totalsItems = React.useMemo(() => {
2848
2856
  if (!record) return []
2849
2857
  const items: { key: string; label: string; amount: number | null | undefined; emphasize?: boolean }[] = [
@@ -3791,6 +3799,48 @@ export default function SalesDocumentDetailPage({
3791
3799
  }
3792
3800
  }, [fetchDocumentByKind, kind, record, runMutationWithContext, t, validForDays])
3793
3801
 
3802
+ const handleSwitchToComputedAmounts = React.useCallback(async () => {
3803
+ if (!record || kind !== 'order') return
3804
+ const ok = await confirm({
3805
+ title: t(
3806
+ 'sales.documents.amountsSwitchConfirm',
3807
+ 'Recompute this order\u2019s amounts from its lines? The totals supplied by the source system will be replaced and cannot be restored from here.',
3808
+ ),
3809
+ variant: 'default',
3810
+ })
3811
+ if (!ok) return
3812
+ setSwitchingToComputedAmounts(true)
3813
+ try {
3814
+ await runMutationWithContext(async () => {
3815
+ await withScopedApiRequestHeaders(buildOptimisticLockHeader(record.updatedAt), () =>
3816
+ apiCallOrThrow('/api/sales/orders', {
3817
+ method: 'PUT',
3818
+ headers: { 'Content-Type': 'application/json' },
3819
+ body: JSON.stringify({ id: record.id, totalsMode: 'computed' }),
3820
+ }),
3821
+ )
3822
+ }, { id: record.id, totalsMode: 'computed' })
3823
+ flash(t('sales.documents.amountsSwitchDone', 'Amounts are now computed from the lines.'), 'success')
3824
+ await refreshDocumentTotals()
3825
+ } catch (err) {
3826
+ logger.error('sales.documents.amounts.switchToComputed', { err })
3827
+ try {
3828
+ getTelemetryRuntime()?.reportError(err, {
3829
+ module: 'sales',
3830
+ code: 'sales.amounts_switch_to_computed_failed',
3831
+ attributes: { orderId: record.id },
3832
+ })
3833
+ } catch (reportFailure) {
3834
+ logger.warn('sales.documents.amounts.switchToComputed.reportFailed', { err: reportFailure })
3835
+ }
3836
+ if (!handleDocumentMutationError(err, t, () => setReloadKey((prev) => prev + 1))) {
3837
+ flash(t('sales.documents.amountsSwitchFailed', 'Could not switch to computed amounts.'), 'error')
3838
+ }
3839
+ } finally {
3840
+ setSwitchingToComputedAmounts(false)
3841
+ }
3842
+ }, [confirm, kind, record, refreshDocumentTotals, runMutationWithContext, t])
3843
+
3794
3844
  const handleDelete = React.useCallback(async () => {
3795
3845
  if (!record) return
3796
3846
  const ok = await confirm({
@@ -3851,6 +3901,14 @@ export default function SalesDocumentDetailPage({
3851
3901
  t('sales.documents.detail.shippingMethod.selected', 'Selected shipping method: {{id}}', { id })
3852
3902
  }
3853
3903
  icon={<Truck className="h-5 w-5 text-muted-foreground" />}
3904
+ note={
3905
+ amountsAreExternal
3906
+ ? t(
3907
+ 'sales.documents.amountsExternalMethodNote',
3908
+ 'The method is recorded, but no charge is added \u2014 this order\u2019s totals come from the source system.',
3909
+ )
3910
+ : null
3911
+ }
3854
3912
  />
3855
3913
  ),
3856
3914
  },
@@ -3877,6 +3935,14 @@ export default function SalesDocumentDetailPage({
3877
3935
  t('sales.documents.detail.paymentMethod.selected', 'Selected payment method: {{id}}', { id })
3878
3936
  }
3879
3937
  icon={<CreditCard className="h-5 w-5 text-muted-foreground" />}
3938
+ note={
3939
+ amountsAreExternal
3940
+ ? t(
3941
+ 'sales.documents.amountsExternalMethodNote',
3942
+ 'The method is recorded, but no charge is added \u2014 this order\u2019s totals come from the source system.',
3943
+ )
3944
+ : null
3945
+ }
3880
3946
  />
3881
3947
  ),
3882
3948
  },
@@ -3949,6 +4015,7 @@ export default function SalesDocumentDetailPage({
3949
4015
  )
3950
4016
  return fields
3951
4017
  }, [
4018
+ amountsAreExternal,
3952
4019
  handleUpdateComment,
3953
4020
  handleUpdateCustomerReference,
3954
4021
  handleUpdateExpectedDeliveryAt,
@@ -4266,6 +4333,9 @@ export default function SalesDocumentDetailPage({
4266
4333
  if (activeTab === 'items') {
4267
4334
  return (
4268
4335
  <SalesDocumentItemsSection
4336
+ // Remount on a mode flip: the switch rewrites every line server-side, and the
4337
+ // section loads its lines only once per `documentId`, which does not change.
4338
+ key={`items:${amountsAreExternal ? 'external' : 'computed'}`}
4269
4339
  documentId={record.id}
4270
4340
  kind={kind}
4271
4341
  currencyCode={record.currencyCode ?? null}
@@ -4274,6 +4344,7 @@ export default function SalesDocumentDetailPage({
4274
4344
  tenantId={(record as any)?.tenantId ?? (record as any)?.tenant_id ?? null}
4275
4345
  onActionChange={handleSectionActionChange}
4276
4346
  onItemsChange={(items) => setHasItems(items.length > 0)}
4347
+ amountsReadOnly={amountsAreExternal}
4277
4348
  />
4278
4349
  )
4279
4350
  }
@@ -4332,6 +4403,7 @@ export default function SalesDocumentDetailPage({
4332
4403
  tenantId={(record as any)?.tenantId ?? (record as any)?.tenant_id ?? null}
4333
4404
  onActionChange={handleSectionActionChange}
4334
4405
  onRowsChange={setAdjustmentRows}
4406
+ amountsReadOnly={amountsAreExternal}
4335
4407
  />
4336
4408
  )
4337
4409
  }
@@ -4914,6 +4986,30 @@ export default function SalesDocumentDetailPage({
4914
4986
  title={t('sales.documents.detail.totals.title', 'Totals')}
4915
4987
  currency={record.currencyCode ?? null}
4916
4988
  items={totalsItems}
4989
+ sourceBadge={
4990
+ amountsAreExternal
4991
+ ? {
4992
+ label: t('sales.documents.amountsExternal', 'Amounts from source'),
4993
+ hint: t(
4994
+ 'sales.documents.amountsExternalHint',
4995
+ 'Totals on this document come from an external system and are stored as supplied.',
4996
+ ),
4997
+ }
4998
+ : null
4999
+ }
5000
+ footerAction={
5001
+ amountsAreExternal ? (
5002
+ <Button
5003
+ type="button"
5004
+ variant="outline"
5005
+ size="sm"
5006
+ disabled={switchingToComputedAmounts}
5007
+ onClick={handleSwitchToComputedAmounts}
5008
+ >
5009
+ {t('sales.documents.amountsSwitchToComputed', 'Switch to computed amounts')}
5010
+ </Button>
5011
+ ) : null
5012
+ }
4917
5013
  />
4918
5014
 
4919
5015
  <div className="space-y-4" ref={detailSectionRef}>