@open-mercato/core 0.7.1-develop.7137.1.26575786c0 → 0.7.1-develop.7148.1.3076e5ccf7
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/backend/users/page.js +4 -4
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +17 -4
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +1 -1
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.js +16 -0
- package/dist/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.js.map +7 -0
- package/dist/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.js +16 -0
- package/dist/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.js.map +7 -0
- package/dist/modules/integrations/backend/integrations/[id]/page.js +16 -3
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.js +42 -0
- package/dist/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.js.map +7 -0
- package/dist/modules/messages/api/route.js +2 -1
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +11 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +22 -0
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/lineSnapshots.js +11 -1
- package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.js +16 -0
- package/dist/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/backend/users/page.tsx +4 -4
- package/src/modules/auth/i18n/de.json +4 -0
- package/src/modules/auth/i18n/en.json +4 -0
- package/src/modules/auth/i18n/es.json +4 -0
- package/src/modules/auth/i18n/ko.json +4 -0
- package/src/modules/auth/i18n/pl.json +4 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +17 -4
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +2 -1
- package/src/modules/customers/i18n/de.json +5 -0
- package/src/modules/customers/i18n/en.json +5 -0
- package/src/modules/customers/i18n/es.json +5 -0
- package/src/modules/customers/i18n/ko.json +5 -0
- package/src/modules/customers/i18n/pl.json +5 -0
- package/src/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.ts +22 -0
- package/src/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.ts +20 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +23 -2
- package/src/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.ts +50 -0
- package/src/modules/integrations/i18n/de.json +1 -0
- package/src/modules/integrations/i18n/en.json +1 -0
- package/src/modules/integrations/i18n/es.json +1 -0
- package/src/modules/integrations/i18n/ko.json +1 -0
- package/src/modules/integrations/i18n/pl.json +1 -0
- package/src/modules/messages/api/route.ts +2 -1
- package/src/modules/sales/commands/documents.ts +14 -0
- package/src/modules/sales/lib/calculations.ts +41 -0
- package/src/modules/sales/lib/lineSnapshots.ts +23 -0
- package/src/modules/sales/lib/types.ts +9 -0
- package/src/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.ts +21 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
const logger = createLogger("sales");
|
|
1
3
|
function toNumber(value, fallback = 0) {
|
|
2
4
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3
5
|
if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) {
|
|
@@ -8,6 +10,7 @@ function toNumber(value, fallback = 0) {
|
|
|
8
10
|
function round(value) {
|
|
9
11
|
return Math.round((value + Number.EPSILON) * 1e4) / 1e4;
|
|
10
12
|
}
|
|
13
|
+
const NET_RECONCILIATION_TOLERANCE = 5e-3;
|
|
11
14
|
function extractAdjustmentTaxRate(adjustment) {
|
|
12
15
|
const metadata = adjustment.metadata ?? {};
|
|
13
16
|
const candidate = metadata.taxRate ?? metadata?.tax_rate ?? metadata?.taxRateValue ?? metadata?.tax_rate_value ?? null;
|
|
@@ -69,6 +72,25 @@ function buildBaseLineResult(line) {
|
|
|
69
72
|
netSubtotalBeforeDiscount
|
|
70
73
|
);
|
|
71
74
|
const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0);
|
|
75
|
+
if (line.totalsFromStoredRow !== true && line.totalNetAmount !== null && line.totalNetAmount !== void 0) {
|
|
76
|
+
const computedNetAmount = round(netSubtotal);
|
|
77
|
+
const suppliedNetAmount = toNumber(line.totalNetAmount, NaN);
|
|
78
|
+
if (!Number.isFinite(suppliedNetAmount)) {
|
|
79
|
+
logger.warn("Sales line totalNetAmount is not a finite number; the computed value is used", {
|
|
80
|
+
lineId: line.id ?? null,
|
|
81
|
+
productId: line.productId ?? null,
|
|
82
|
+
suppliedTotalNetAmount: line.totalNetAmount,
|
|
83
|
+
computedNetAmount
|
|
84
|
+
});
|
|
85
|
+
} else if (Math.abs(round(suppliedNetAmount) - computedNetAmount) > NET_RECONCILIATION_TOLERANCE) {
|
|
86
|
+
logger.warn("Sales line totalNetAmount does not match the computed net amount; the computed value is used", {
|
|
87
|
+
lineId: line.id ?? null,
|
|
88
|
+
productId: line.productId ?? null,
|
|
89
|
+
suppliedTotalNetAmount: round(suppliedNetAmount),
|
|
90
|
+
computedNetAmount
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
72
94
|
const explicitTaxAmount = line.taxAmount !== null && line.taxAmount !== void 0;
|
|
73
95
|
let taxAmount = explicitTaxAmount ? toNumber(line.taxAmount, 0) : round(netSubtotal * Math.max(taxRate, 0));
|
|
74
96
|
const grossSubtotal = line.totalGrossAmount !== null && line.totalGrossAmount !== void 0 ? toNumber(line.totalGrossAmount, 0) : round(netSubtotal + taxAmount);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sales/lib/calculations.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type SalesAdjustmentDraft,\n type SalesCalculationContext,\n type CalculateDocumentOptions,\n type CalculateLineOptions,\n type SalesDocumentCalculationResult,\n type SalesDocumentKind,\n type SalesLineCalculationHook,\n type SalesLineCalculationResult,\n type SalesLineSnapshot,\n type SalesTotalsCalculationHook,\n} from './types'\n\nfunction toNumber(value: unknown, fallback = 0): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {\n return Number(value)\n }\n return fallback\n}\n\nfunction round(value: number): number {\n return Math.round((value + Number.EPSILON) * 1e4) / 1e4\n}\n\nfunction extractAdjustmentTaxRate(adjustment: SalesAdjustmentDraft): number | null {\n const metadata = (adjustment.metadata ?? {}) as Record<string, unknown>\n const candidate =\n metadata.taxRate ??\n (metadata as any)?.tax_rate ??\n (metadata as any)?.taxRateValue ??\n (metadata as any)?.tax_rate_value ??\n null\n const parsed = toNumber(candidate, NaN)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction resolveAdjustmentAmounts(\n adjustments: SalesAdjustmentDraft[],\n baseNet: number,\n baseGross: number\n): SalesAdjustmentDraft[] {\n return adjustments.map((adj) => {\n const rate = toNumber(adj.rate, NaN)\n const taxRate = extractAdjustmentTaxRate(adj)\n const hasAmountNet = Number.isFinite(toNumber(adj.amountNet, NaN))\n const hasAmountGross = Number.isFinite(toNumber(adj.amountGross, NaN))\n const hasRate = Number.isFinite(rate) && !hasAmountNet && !hasAmountGross\n const hasTaxRate = taxRate !== null\n let amountNet = toNumber(adj.amountNet, NaN)\n let amountGross = toNumber(adj.amountGross, NaN)\n\n if (hasRate) {\n const multiplier = (rate as number) / 100\n amountNet = round(Math.max(baseNet, 0) * multiplier)\n if (adj.kind === 'tax') {\n amountGross = amountNet\n } else if (hasTaxRate) {\n amountGross = round(amountNet * (1 + (taxRate as number) / 100))\n } else {\n amountGross = round(Math.max(baseGross, 0) * multiplier)\n }\n } else {\n if (!Number.isFinite(amountNet) && Number.isFinite(amountGross) && hasTaxRate) {\n amountNet = round((amountGross as number) / (1 + (taxRate as number) / 100))\n }\n if (!Number.isFinite(amountGross) && Number.isFinite(amountNet) && hasTaxRate) {\n amountGross = round((amountNet as number) * (1 + (taxRate as number) / 100))\n }\n }\n\n return {\n ...adj,\n amountNet: Number.isFinite(amountNet) ? amountNet : adj.amountNet,\n amountGross: Number.isFinite(amountGross) ? amountGross : adj.amountGross,\n }\n })\n}\n\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 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": "
|
|
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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -35,6 +35,11 @@ function mapPersistedLine(line) {
|
|
|
35
35
|
discountPercent: toNumeric(line.discountPercent),
|
|
36
36
|
taxRate: toNumeric(line.taxRate),
|
|
37
37
|
taxAmount: toNumeric(line.taxAmount),
|
|
38
|
+
// The totals below are the engine's own previous output read back off the
|
|
39
|
+
// row, not something a caller asserted, so they are not reconciled against
|
|
40
|
+
// the recomputed net (#5644) — on a legacy row that divergence is the
|
|
41
|
+
// discount contract healing itself, not a caller mistake.
|
|
42
|
+
totalsFromStoredRow: true,
|
|
38
43
|
totalNetAmount: toNumeric(line.totalNetAmount),
|
|
39
44
|
totalGrossAmount: toNumeric(line.totalGrossAmount),
|
|
40
45
|
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
@@ -58,9 +63,14 @@ function resolveUpsertDiscountFields(callerAmount, callerBasis, existingSnapshot
|
|
|
58
63
|
discountAmountFromStoredRow: existingSnapshot != null
|
|
59
64
|
};
|
|
60
65
|
}
|
|
66
|
+
function resolveUpsertTotalsOrigin(callerTotalNetAmount, existingSnapshot) {
|
|
67
|
+
if (callerTotalNetAmount !== null && callerTotalNetAmount !== void 0) return {};
|
|
68
|
+
return existingSnapshot != null ? { totalsFromStoredRow: true } : {};
|
|
69
|
+
}
|
|
61
70
|
export {
|
|
62
71
|
mapOrderLineEntityToSnapshot,
|
|
63
72
|
mapQuoteLineEntityToSnapshot,
|
|
64
|
-
resolveUpsertDiscountFields
|
|
73
|
+
resolveUpsertDiscountFields,
|
|
74
|
+
resolveUpsertTotalsOrigin
|
|
65
75
|
};
|
|
66
76
|
//# sourceMappingURL=lineSnapshots.js.map
|
|
@@ -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\nfunction mapPersistedLine(line: SalesOrderLine | SalesQuoteLine): SalesLineSnapshot {\n return {\n id: line.id,\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 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): SalesLineSnapshot {\n return mapPersistedLine(line)\n}\n\nexport function mapQuoteLineEntityToSnapshot(line: SalesQuoteLine): SalesLineSnapshot {\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"],
|
|
5
|
-
"mappings": "AACA,SAAS,iBAAiB;AAG1B,SAAS,UAAU,OAAwB;AACzC,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG,QAAO;AAChE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,QAAQ;AACpD,UAAM,SAAS,OAAO,KAAK;AAC3B,QAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,EACtC;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAA0D;AAClF,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT,YAAY,KAAK;AAAA,IACjB,MAAM,KAAK;AAAA,IACX,WAAW,KAAK,aAAa;AAAA,IAC7B,kBAAkB,KAAK,oBAAoB;AAAA,IAC3C,MAAM,KAAK,QAAQ;AAAA,IACnB,aAAa,KAAK,eAAe;AAAA,IACjC,SAAS,KAAK,WAAW;AAAA,IACzB,UAAU,UAAU,KAAK,QAAQ;AAAA,IACjC,cAAc,KAAK,gBAAgB;AAAA,IACnC,oBAAoB,UAAU,KAAK,sBAAsB,KAAK,QAAQ;AAAA,IACtE,gBAAgB,KAAK,kBAAkB,KAAK,gBAAgB;AAAA,IAC5D,aAAa,KAAK,cAAc,UAAU,KAAK,WAAW,IAAI;AAAA,IAC9D,cAAc,KAAK;AAAA,IACnB,cAAc,UAAU,KAAK,YAAY;AAAA,IACzC,gBAAgB,UAAU,KAAK,cAAc;AAAA,IAC7C,gBAAgB,UAAU,KAAK,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAM7C,6BAA6B;AAAA,IAC7B,iBAAiB,UAAU,KAAK,eAAe;AAAA,IAC/C,SAAS,UAAU,KAAK,OAAO;AAAA,IAC/B,WAAW,UAAU,KAAK,SAAS;AAAA,
|
|
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\nfunction mapPersistedLine(line: SalesOrderLine | SalesQuoteLine): SalesLineSnapshot {\n return {\n id: line.id,\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): SalesLineSnapshot {\n return mapPersistedLine(line)\n}\n\nexport function mapQuoteLineEntityToSnapshot(line: SalesQuoteLine): SalesLineSnapshot {\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"],
|
|
5
|
+
"mappings": "AACA,SAAS,iBAAiB;AAG1B,SAAS,UAAU,OAAwB;AACzC,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG,QAAO;AAChE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,QAAQ;AACpD,UAAM,SAAS,OAAO,KAAK;AAC3B,QAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,EACtC;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAA0D;AAClF,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT,YAAY,KAAK;AAAA,IACjB,MAAM,KAAK;AAAA,IACX,WAAW,KAAK,aAAa;AAAA,IAC7B,kBAAkB,KAAK,oBAAoB;AAAA,IAC3C,MAAM,KAAK,QAAQ;AAAA,IACnB,aAAa,KAAK,eAAe;AAAA,IACjC,SAAS,KAAK,WAAW;AAAA,IACzB,UAAU,UAAU,KAAK,QAAQ;AAAA,IACjC,cAAc,KAAK,gBAAgB;AAAA,IACnC,oBAAoB,UAAU,KAAK,sBAAsB,KAAK,QAAQ;AAAA,IACtE,gBAAgB,KAAK,kBAAkB,KAAK,gBAAgB;AAAA,IAC5D,aAAa,KAAK,cAAc,UAAU,KAAK,WAAW,IAAI;AAAA,IAC9D,cAAc,KAAK;AAAA,IACnB,cAAc,UAAU,KAAK,YAAY;AAAA,IACzC,gBAAgB,UAAU,KAAK,cAAc;AAAA,IAC7C,gBAAgB,UAAU,KAAK,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAM7C,6BAA6B;AAAA,IAC7B,iBAAiB,UAAU,KAAK,eAAe;AAAA,IAC/C,SAAS,UAAU,KAAK,OAAO;AAAA,IAC/B,WAAW,UAAU,KAAK,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,IAKnC,qBAAqB;AAAA,IACrB,gBAAgB,UAAU,KAAK,cAAc;AAAA,IAC7C,kBAAkB,UAAU,KAAK,gBAAgB;AAAA,IACjD,eAAe,KAAK,gBAAgB,UAAU,KAAK,aAAa,IAAI;AAAA,IACpE,eAAe,KAAK,iBAAiB;AAAA,IACrC,UAAU,KAAK,WAAW,UAAU,KAAK,QAAQ,IAAI;AAAA,IACrD,kBAAkB,KAAK,oBAAoB;AAAA,EAC7C;AACF;AASO,SAAS,6BAA6B,MAAyC;AACpF,SAAO,iBAAiB,IAAI;AAC9B;AAEO,SAAS,6BAA6B,MAAyC;AACpF,SAAO,iBAAiB,IAAI;AAC9B;AAsBO,SAAS,4BACd,cACA,aACA,kBACsB;AACtB,MAAI,iBAAiB,QAAQ,iBAAiB,QAAW;AACvD,WAAO,EAAE,gBAAgB,cAAc,qBAAqB,eAAe,OAAO;AAAA,EACpF;AACA,SAAO;AAAA,IACL,gBAAgB,kBAAkB,kBAAkB;AAAA,IACpD,6BAA6B,oBAAoB;AAAA,EACnD;AACF;AAYO,SAAS,0BACd,sBACA,kBACgD;AAChD,MAAI,yBAAyB,QAAQ,yBAAyB,OAAW,QAAO,CAAC;AACjF,SAAO,oBAAoB,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AACrE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
import { declareQueryIndexReindex } from "@open-mercato/shared/lib/query/migration-reindex";
|
|
3
|
+
const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
4
|
+
"workflows:workflow_definition"
|
|
5
|
+
]);
|
|
6
|
+
class Migration20260901120000_reindex_workflow_definitions extends Migration {
|
|
7
|
+
up() {
|
|
8
|
+
}
|
|
9
|
+
down() {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
Migration20260901120000_reindex_workflow_definitions,
|
|
14
|
+
queryIndexReindexEntityTypes
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=Migration20260901120000_reindex_workflow_definitions.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\nimport { declareQueryIndexReindex } from '@open-mercato/shared/lib/query/migration-reindex';\n\n/**\n * Catch-up reindex for `Migration20260428102318`, which renamed three seeded\n * `workflow_definitions.workflow_id` values in raw SQL and never notified the query index. On\n * every install that already applied it, `entity_indexes.doc` for `workflows:workflow_definition`\n * still resolves those records by the pre-rename identifier \u2014 `checkout_simple_v1` rather than\n * `workflows.checkout-demo`, and so on \u2014 which is exactly the identifier the rename removed.\n *\n * This migration executes no SQL \u2014 the declaration below is its entire payload.\n */\nexport const queryIndexReindexEntityTypes = declareQueryIndexReindex([\n 'workflows:workflow_definition',\n]);\n\nexport class Migration20260901120000_reindex_workflow_definitions extends Migration {\n override up(): void | Promise<void> {}\n\n override down(): void | Promise<void> {}\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAC1B,SAAS,gCAAgC;AAWlC,MAAM,+BAA+B,yBAAyB;AAAA,EACnE;AACF,CAAC;AAEM,MAAM,6DAA6D,UAAU;AAAA,EACzE,KAA2B;AAAA,EAAC;AAAA,EAE5B,OAA6B;AAAA,EAAC;AACzC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7148.1.3076e5ccf7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
256
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
257
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7148.1.3076e5ccf7",
|
|
256
|
+
"@open-mercato/shared": "0.7.1-develop.7148.1.3076e5ccf7",
|
|
257
|
+
"@open-mercato/ui": "0.7.1-develop.7148.1.3076e5ccf7",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
263
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
264
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7148.1.3076e5ccf7",
|
|
263
|
+
"@open-mercato/shared": "0.7.1-develop.7148.1.3076e5ccf7",
|
|
264
|
+
"@open-mercato/ui": "0.7.1-develop.7148.1.3076e5ccf7",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.0",
|
|
267
267
|
"@testing-library/react": "^16.3.1",
|
|
@@ -377,10 +377,10 @@ export default function UsersListPage() {
|
|
|
377
377
|
)
|
|
378
378
|
const columns = React.useMemo<ColumnDef<Row>[]>(() => {
|
|
379
379
|
const base: ColumnDef<Row>[] = [
|
|
380
|
-
{ accessorKey: 'email', header: 'Email' },
|
|
380
|
+
{ accessorKey: 'email', header: t('auth.users.list.columns.email', 'Email') },
|
|
381
381
|
{ accessorKey: 'name', header: t('auth.users.list.columns.name', 'Display name') },
|
|
382
|
-
{ accessorKey: 'organizationName', header: 'Organization' },
|
|
383
|
-
{ accessorKey: 'roles', header: 'Roles', cell: ({ row }) => (row.original.roles || []).join(', ') },
|
|
382
|
+
{ accessorKey: 'organizationName', header: t('auth.users.list.columns.organization', 'Organization') },
|
|
383
|
+
{ accessorKey: 'roles', header: t('auth.users.list.columns.roles', 'Roles'), cell: ({ row }) => (row.original.roles || []).join(', ') },
|
|
384
384
|
{
|
|
385
385
|
accessorKey: 'isConfirmed',
|
|
386
386
|
header: t('auth.users.list.columns.status', 'Status'),
|
|
@@ -392,7 +392,7 @@ export default function UsersListPage() {
|
|
|
392
392
|
},
|
|
393
393
|
]
|
|
394
394
|
if (showTenantColumn) {
|
|
395
|
-
base.splice(1, 0, { accessorKey: 'tenantName', header: 'Tenant' })
|
|
395
|
+
base.splice(1, 0, { accessorKey: 'tenantName', header: t('auth.users.list.columns.tenant', 'Tenant') })
|
|
396
396
|
}
|
|
397
397
|
return base
|
|
398
398
|
}, [showTenantColumn, t])
|
|
@@ -599,8 +599,12 @@
|
|
|
599
599
|
"auth.users.form.loading": "Benutzerdaten werden geladen...",
|
|
600
600
|
"auth.users.form.title.create": "Benutzer erstellen",
|
|
601
601
|
"auth.users.form.title.edit": "Benutzer bearbeiten",
|
|
602
|
+
"auth.users.list.columns.email": "E-Mail",
|
|
602
603
|
"auth.users.list.columns.name": "Anzeigename",
|
|
604
|
+
"auth.users.list.columns.organization": "Organisation",
|
|
605
|
+
"auth.users.list.columns.roles": "Rollen",
|
|
603
606
|
"auth.users.list.columns.status": "Status",
|
|
607
|
+
"auth.users.list.columns.tenant": "Mandant",
|
|
604
608
|
"auth.users.list.error.delete": "Benutzer konnte nicht gelöscht werden",
|
|
605
609
|
"auth.users.list.error.load": "Benutzer konnten nicht geladen werden",
|
|
606
610
|
"auth.users.list.filters.name": "Anzeigename",
|
|
@@ -599,8 +599,12 @@
|
|
|
599
599
|
"auth.users.form.loading": "Loading user data...",
|
|
600
600
|
"auth.users.form.title.create": "Create User",
|
|
601
601
|
"auth.users.form.title.edit": "Edit User",
|
|
602
|
+
"auth.users.list.columns.email": "Email",
|
|
602
603
|
"auth.users.list.columns.name": "Display name",
|
|
604
|
+
"auth.users.list.columns.organization": "Organization",
|
|
605
|
+
"auth.users.list.columns.roles": "Roles",
|
|
603
606
|
"auth.users.list.columns.status": "Status",
|
|
607
|
+
"auth.users.list.columns.tenant": "Tenant",
|
|
604
608
|
"auth.users.list.error.delete": "Failed to delete user",
|
|
605
609
|
"auth.users.list.error.load": "Failed to load users",
|
|
606
610
|
"auth.users.list.filters.name": "Display name",
|
|
@@ -599,8 +599,12 @@
|
|
|
599
599
|
"auth.users.form.loading": "Cargando datos del usuario...",
|
|
600
600
|
"auth.users.form.title.create": "Crear usuario",
|
|
601
601
|
"auth.users.form.title.edit": "Editar usuario",
|
|
602
|
+
"auth.users.list.columns.email": "Correo electrónico",
|
|
602
603
|
"auth.users.list.columns.name": "Nombre visible",
|
|
604
|
+
"auth.users.list.columns.organization": "Organización",
|
|
605
|
+
"auth.users.list.columns.roles": "Roles",
|
|
603
606
|
"auth.users.list.columns.status": "Estado",
|
|
607
|
+
"auth.users.list.columns.tenant": "Inquilino",
|
|
604
608
|
"auth.users.list.error.delete": "No se pudo eliminar el usuario",
|
|
605
609
|
"auth.users.list.error.load": "No se pudieron cargar los usuarios",
|
|
606
610
|
"auth.users.list.filters.name": "Nombre visible",
|
|
@@ -599,8 +599,12 @@
|
|
|
599
599
|
"auth.users.form.loading": "사용자 데이터를 불러오는 중...",
|
|
600
600
|
"auth.users.form.title.create": "사용자 생성",
|
|
601
601
|
"auth.users.form.title.edit": "사용자 편집",
|
|
602
|
+
"auth.users.list.columns.email": "이메일",
|
|
602
603
|
"auth.users.list.columns.name": "표시 이름",
|
|
604
|
+
"auth.users.list.columns.organization": "조직",
|
|
605
|
+
"auth.users.list.columns.roles": "역할",
|
|
603
606
|
"auth.users.list.columns.status": "상태",
|
|
607
|
+
"auth.users.list.columns.tenant": "테넌트",
|
|
604
608
|
"auth.users.list.error.delete": "사용자 삭제에 실패했습니다",
|
|
605
609
|
"auth.users.list.error.load": "사용자를 불러오지 못했습니다",
|
|
606
610
|
"auth.users.list.filters.name": "표시 이름",
|
|
@@ -599,8 +599,12 @@
|
|
|
599
599
|
"auth.users.form.loading": "Ładowanie danych użytkownika...",
|
|
600
600
|
"auth.users.form.title.create": "Utwórz użytkownika",
|
|
601
601
|
"auth.users.form.title.edit": "Edytuj użytkownika",
|
|
602
|
+
"auth.users.list.columns.email": "Email",
|
|
602
603
|
"auth.users.list.columns.name": "Nazwa wyświetlana",
|
|
604
|
+
"auth.users.list.columns.organization": "Organizacja",
|
|
605
|
+
"auth.users.list.columns.roles": "Role",
|
|
603
606
|
"auth.users.list.columns.status": "Status",
|
|
607
|
+
"auth.users.list.columns.tenant": "Najemca",
|
|
604
608
|
"auth.users.list.error.delete": "Nie udało się usunąć użytkownika",
|
|
605
609
|
"auth.users.list.error.load": "Nie udało się wczytać użytkowników",
|
|
606
610
|
"auth.users.list.filters.name": "Nazwa wyświetlana",
|
|
@@ -480,11 +480,24 @@ export function CompanyPeopleSection({
|
|
|
480
480
|
confirmButtonLabel: translate('customers.linking.person.confirmButton', 'Link person'),
|
|
481
481
|
showLinkSettings: true,
|
|
482
482
|
roleOptions: [
|
|
483
|
-
{
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
{
|
|
484
|
+
id: 'decision_maker',
|
|
485
|
+
label: translate('customers.linking.person.role.decisionMaker', 'Decision maker'),
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: 'budget_holder',
|
|
489
|
+
label: translate('customers.linking.person.role.budgetHolder', 'Budget holder'),
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: 'stakeholder',
|
|
493
|
+
label: translate('customers.linking.person.role.stakeholder', 'Stakeholder'),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: 'contact',
|
|
497
|
+
label: translate('customers.linking.person.role.contact', 'Contact'),
|
|
498
|
+
},
|
|
487
499
|
],
|
|
500
|
+
allFilterLabel: translate('customers.linking.person.role.all', 'All'),
|
|
488
501
|
excludeLinkedCompanyId: companyId,
|
|
489
502
|
addNew: {
|
|
490
503
|
title: translate('customers.linking.person.addNew', 'Add new contact'),
|
|
@@ -40,6 +40,7 @@ type PersonAdapterOptions = {
|
|
|
40
40
|
addNew?: LinkEntityAdapter<PersonDetails, PersonLinkSettings>['addNew']
|
|
41
41
|
showLinkSettings?: boolean
|
|
42
42
|
roleOptions?: Array<{ id: string; label: string }>
|
|
43
|
+
allFilterLabel?: string
|
|
43
44
|
headerIcon?: React.ReactNode
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -217,7 +218,7 @@ export function createPersonLinkAdapter(
|
|
|
217
218
|
roleOptions.length > 0
|
|
218
219
|
? {
|
|
219
220
|
options: [
|
|
220
|
-
{ id: 'all', label: 'All' },
|
|
221
|
+
{ id: 'all', label: options.allFilterLabel ?? 'All' },
|
|
221
222
|
...roleOptions.map((role) => ({ id: role.id, label: role.label })),
|
|
222
223
|
],
|
|
223
224
|
defaultId: 'all',
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Einen bestehenden Kontakt mit diesem Unternehmen verknüpfen",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Einen bestehenden Kontakt mit {{name}} verknüpfen",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Personen verknüpfen",
|
|
1718
|
+
"customers.linking.person.role.all": "Alle",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Budgetverantwortlicher",
|
|
1720
|
+
"customers.linking.person.role.contact": "Kontaktperson",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Entscheider",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Stakeholder",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "Keine passenden Personen gefunden.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Alle Personen durchsuchen…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "PASSENDE KONTAKTE",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Link an existing contact to this company",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Link an existing contact to {{name}}",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Link people",
|
|
1718
|
+
"customers.linking.person.role.all": "All",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Budget holder",
|
|
1720
|
+
"customers.linking.person.role.contact": "Contact",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Decision maker",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Stakeholder",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "No matching people found.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Search all people…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "MATCHING CONTACTS",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Vincula un contacto existente a esta empresa",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Vincula un contacto existente a {{name}}",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Vincular personas",
|
|
1718
|
+
"customers.linking.person.role.all": "Todos",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Responsable del presupuesto",
|
|
1720
|
+
"customers.linking.person.role.contact": "Contacto",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Responsable de la decisión",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Parte interesada",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "No se encontraron personas.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Buscar todas las personas…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "CONTACTOS COINCIDENTES",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "기존 연락처를 이 회사에 연결",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "기존 연락처를 {{name}}에 연결",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "사람 연결",
|
|
1718
|
+
"customers.linking.person.role.all": "전체",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "예산 담당자",
|
|
1720
|
+
"customers.linking.person.role.contact": "연락 담당자",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "의사결정권자",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "이해관계자",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "일치하는 사람을 찾을 수 없습니다.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "전체 사람 검색…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "일치하는 연락처",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Powiąż istniejący kontakt z tą firmą",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Powiąż istniejący kontakt z {{name}}",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Powiąż osoby",
|
|
1718
|
+
"customers.linking.person.role.all": "Wszystkie",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Dysponent budżetu",
|
|
1720
|
+
"customers.linking.person.role.contact": "Kontakt",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Decydent",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Interesariusz",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "Nie znaleziono pasujących osób.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Szukaj wszystkich osób…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "PASUJĄCE KONTAKTY",
|
package/src/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
import { declareQueryIndexReindex } from '@open-mercato/shared/lib/query/migration-reindex';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Catch-up reindex for `Migration20260519120000_pipeline_stage_color_tones`, which rewrote
|
|
6
|
+
* `customer_dictionary_entries.color` in raw SQL and never notified the query index. On every
|
|
7
|
+
* install that already applied it, `entity_indexes.doc` for `customers:customer_dictionary_entry`
|
|
8
|
+
* still holds the legacy hex chip (`#facc15`, `#a855f7`, …) instead of the semantic tone, and so
|
|
9
|
+
* do the `search_tokens` rows derived from that document. Both migrations are forward-only, so a
|
|
10
|
+
* follow-up migration is the only route to repair existing installs.
|
|
11
|
+
*
|
|
12
|
+
* This migration executes no SQL — the declaration below is its entire payload.
|
|
13
|
+
*/
|
|
14
|
+
export const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
15
|
+
'customers:customer_dictionary_entry',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export class Migration20260901120000_reindex_pipeline_stage_colors extends Migration {
|
|
19
|
+
override up(): void | Promise<void> {}
|
|
20
|
+
|
|
21
|
+
override down(): void | Promise<void> {}
|
|
22
|
+
}
|
package/src/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
import { declareQueryIndexReindex } from '@open-mercato/shared/lib/query/migration-reindex';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Catch-up reindex for `Migration20260410171544`, which added `position` and `is_default` to
|
|
6
|
+
* `dictionary_entries` and backfilled them in raw SQL. Documents written before that migration
|
|
7
|
+
* were never rebuilt, so `entity_indexes.doc` for `dictionaries:dictionary_entry` is missing both
|
|
8
|
+
* keys entirely rather than holding a stale value — the same absent emit, a different symptom.
|
|
9
|
+
*
|
|
10
|
+
* This migration executes no SQL — the declaration below is its entire payload.
|
|
11
|
+
*/
|
|
12
|
+
export const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
13
|
+
'dictionaries:dictionary_entry',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export class Migration20260901120000_reindex_dictionary_entries extends Migration {
|
|
17
|
+
override up(): void | Promise<void> {}
|
|
18
|
+
|
|
19
|
+
override down(): void | Promise<void> {}
|
|
20
|
+
}
|