@open-mercato/core 0.7.1-develop.7113.1.9d83dca10c → 0.7.1-develop.7121.1.734c263bda
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/helpers/integration/crmFixtures.js.map +1 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +9 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/outbound-recipient.js +5 -1
- package/dist/modules/communication_channels/lib/outbound-recipient.js.map +2 -2
- package/dist/modules/communication_channels/lib/test-seed.js +5 -0
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +1 -1
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +1 -1
- package/dist/modules/customers/backend/customers/deals/page.js +4 -3
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/cli.js +5 -5
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/components/detail/pipelineStageUtils.js +1 -0
- package/dist/modules/customers/components/detail/pipelineStageUtils.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/dealAdapter.js +2 -2
- package/dist/modules/customers/components/linking/adapters/dealAdapter.js.map +2 -2
- package/dist/modules/customers/lib/closureStage.js.map +2 -2
- package/dist/modules/customers/lib/dealStatus.js +6 -4
- package/dist/modules/customers/lib/dealStatus.js.map +2 -2
- package/dist/modules/customers/lib/dealsSummaryQueries.js +3 -3
- package/dist/modules/customers/lib/dealsSummaryQueries.js.map +1 -1
- package/dist/modules/customers/migrations/Migration20260824180000_deal_status_lost.js +30 -0
- package/dist/modules/customers/migrations/Migration20260824180000_deal_status_lost.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +1 -1
- package/dist/modules/eudr/notifications.js +5 -0
- package/dist/modules/eudr/notifications.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +59 -28
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +146 -99
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +1 -30
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +1 -4
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +2 -1
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
- package/dist/modules/sales/data/validators.js +3 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +14 -2
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/json.js +8 -0
- package/dist/modules/sales/lib/json.js.map +7 -0
- package/dist/modules/sales/lib/lineSnapshots.js +66 -0
- package/dist/modules/sales/lib/lineSnapshots.js.map +7 -0
- package/dist/modules/warranty_claims/notifications.js +5 -0
- package/dist/modules/warranty_claims/notifications.js.map +2 -2
- package/dist/modules/wms/notifications.js +2 -0
- package/dist/modules/wms/notifications.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/crmFixtures.ts +1 -1
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +21 -9
- package/src/modules/communication_channels/lib/adapter.ts +11 -0
- package/src/modules/communication_channels/lib/outbound-recipient.ts +14 -1
- package/src/modules/communication_channels/lib/test-seed.ts +19 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +1 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +4 -3
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +5 -5
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +3 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2 -2
- package/src/modules/customers/cli.ts +5 -5
- package/src/modules/customers/commands/deals.ts +4 -3
- package/src/modules/customers/components/detail/pipelineStageUtils.ts +1 -0
- package/src/modules/customers/components/linking/adapters/dealAdapter.tsx +2 -2
- package/src/modules/customers/i18n/de.json +1 -1
- package/src/modules/customers/i18n/en.json +1 -1
- package/src/modules/customers/i18n/es.json +1 -1
- package/src/modules/customers/i18n/ko.json +1 -1
- package/src/modules/customers/i18n/pl.json +1 -1
- package/src/modules/customers/lib/closureStage.ts +2 -1
- package/src/modules/customers/lib/dealStatus.ts +23 -12
- package/src/modules/customers/lib/dealsSummaryQueries.ts +3 -3
- package/src/modules/customers/migrations/Migration20260824180000_deal_status_lost.ts +55 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +1 -1
- package/src/modules/eudr/notifications.ts +5 -0
- package/src/modules/query_index/lib/search-tokens.ts +100 -43
- package/src/modules/sales/commands/documents.ts +185 -105
- package/src/modules/sales/commands/returns.ts +1 -31
- package/src/modules/sales/commands/shared.ts +1 -4
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +12 -1
- package/src/modules/sales/data/validators.ts +3 -0
- package/src/modules/sales/lib/calculations.ts +33 -7
- package/src/modules/sales/lib/json.ts +4 -0
- package/src/modules/sales/lib/lineSnapshots.ts +98 -0
- package/src/modules/sales/lib/types.ts +20 -0
- package/src/modules/warranty_claims/notifications.ts +5 -0
- package/src/modules/wms/notifications.ts +2 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
6
7
|
import type { CommandHandler } from "@open-mercato/shared/lib/commands";
|
|
@@ -129,6 +130,11 @@ import {
|
|
|
129
130
|
type SalesLineCalculationResult,
|
|
130
131
|
type SalesDocumentCalculationResult,
|
|
131
132
|
} from "../lib/types";
|
|
133
|
+
import {
|
|
134
|
+
mapOrderLineEntityToSnapshot,
|
|
135
|
+
mapQuoteLineEntityToSnapshot,
|
|
136
|
+
resolveUpsertDiscountFields,
|
|
137
|
+
} from "../lib/lineSnapshots";
|
|
132
138
|
import { loadShippedQuantityByLine } from "../lib/shipments/snapshots";
|
|
133
139
|
import { resolveDictionaryEntryValue, resolveCachedDictionaryEntryValue } from "../lib/dictionaries";
|
|
134
140
|
import type { CacheStrategy } from "@open-mercato/cache";
|
|
@@ -2969,68 +2975,6 @@ function buildCalculationContext(params: {
|
|
|
2969
2975
|
};
|
|
2970
2976
|
}
|
|
2971
2977
|
|
|
2972
|
-
function mapOrderLineEntityToSnapshot(line: SalesOrderLine): SalesLineSnapshot {
|
|
2973
|
-
return {
|
|
2974
|
-
id: line.id,
|
|
2975
|
-
lineNumber: line.lineNumber,
|
|
2976
|
-
kind: line.kind,
|
|
2977
|
-
productId: line.productId ?? null,
|
|
2978
|
-
productVariantId: line.productVariantId ?? null,
|
|
2979
|
-
name: line.name ?? null,
|
|
2980
|
-
description: line.description ?? null,
|
|
2981
|
-
comment: line.comment ?? null,
|
|
2982
|
-
quantity: toNumeric(line.quantity),
|
|
2983
|
-
quantityUnit: line.quantityUnit ?? null,
|
|
2984
|
-
normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
|
|
2985
|
-
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
2986
|
-
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
2987
|
-
currencyCode: line.currencyCode,
|
|
2988
|
-
unitPriceNet: toNumeric(line.unitPriceNet),
|
|
2989
|
-
unitPriceGross: toNumeric(line.unitPriceGross),
|
|
2990
|
-
discountAmount: toNumeric(line.discountAmount),
|
|
2991
|
-
discountPercent: toNumeric(line.discountPercent),
|
|
2992
|
-
taxRate: toNumeric(line.taxRate),
|
|
2993
|
-
taxAmount: toNumeric(line.taxAmount),
|
|
2994
|
-
totalNetAmount: toNumeric(line.totalNetAmount),
|
|
2995
|
-
totalGrossAmount: toNumeric(line.totalGrossAmount),
|
|
2996
|
-
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
2997
|
-
promotionCode: line.promotionCode ?? null,
|
|
2998
|
-
metadata: line.metadata ? cloneJson(line.metadata) : null,
|
|
2999
|
-
customFieldSetId: line.customFieldSetId ?? null,
|
|
3000
|
-
};
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
function mapQuoteLineEntityToSnapshot(line: SalesQuoteLine): SalesLineSnapshot {
|
|
3004
|
-
return {
|
|
3005
|
-
id: line.id,
|
|
3006
|
-
lineNumber: line.lineNumber,
|
|
3007
|
-
kind: line.kind,
|
|
3008
|
-
productId: line.productId ?? null,
|
|
3009
|
-
productVariantId: line.productVariantId ?? null,
|
|
3010
|
-
name: line.name ?? null,
|
|
3011
|
-
description: line.description ?? null,
|
|
3012
|
-
comment: line.comment ?? null,
|
|
3013
|
-
quantity: toNumeric(line.quantity),
|
|
3014
|
-
quantityUnit: line.quantityUnit ?? null,
|
|
3015
|
-
normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
|
|
3016
|
-
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
3017
|
-
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
3018
|
-
currencyCode: line.currencyCode,
|
|
3019
|
-
unitPriceNet: toNumeric(line.unitPriceNet),
|
|
3020
|
-
unitPriceGross: toNumeric(line.unitPriceGross),
|
|
3021
|
-
discountAmount: toNumeric(line.discountAmount),
|
|
3022
|
-
discountPercent: toNumeric(line.discountPercent),
|
|
3023
|
-
taxRate: toNumeric(line.taxRate),
|
|
3024
|
-
taxAmount: toNumeric(line.taxAmount),
|
|
3025
|
-
totalNetAmount: toNumeric(line.totalNetAmount),
|
|
3026
|
-
totalGrossAmount: toNumeric(line.totalGrossAmount),
|
|
3027
|
-
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
3028
|
-
promotionCode: line.promotionCode ?? null,
|
|
3029
|
-
metadata: line.metadata ? cloneJson(line.metadata) : null,
|
|
3030
|
-
customFieldSetId: line.customFieldSetId ?? null,
|
|
3031
|
-
};
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
2978
|
function mapOrderAdjustmentToDraft(
|
|
3035
2979
|
adjustment: SalesOrderAdjustment,
|
|
3036
2980
|
): SalesAdjustmentDraft {
|
|
@@ -3087,6 +3031,13 @@ async function emitTotalsCalculated(
|
|
|
3087
3031
|
await eventBus.emitEvent("sales.document.totals.calculated", payload);
|
|
3088
3032
|
}
|
|
3089
3033
|
|
|
3034
|
+
function isStoredRowSourcedLine(line: DocumentLineCreateInput): boolean {
|
|
3035
|
+
return (
|
|
3036
|
+
(line as Pick<SalesLineSnapshot, "discountAmountFromStoredRow">)
|
|
3037
|
+
.discountAmountFromStoredRow === true
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3090
3041
|
function createLineSnapshotFromInput(
|
|
3091
3042
|
line: DocumentLineCreateInput,
|
|
3092
3043
|
lineNumber: number,
|
|
@@ -3118,6 +3069,16 @@ function createLineSnapshotFromInput(
|
|
|
3118
3069
|
unitPriceNet: line.unitPriceNet ?? null,
|
|
3119
3070
|
unitPriceGross: line.unitPriceGross ?? null,
|
|
3120
3071
|
discountAmount: line.discountAmount ?? null,
|
|
3072
|
+
// Several callers re-run an already-mapped snapshot through here — the line
|
|
3073
|
+
// upsert and delete paths rebuild every line of the document, not just the
|
|
3074
|
+
// one being edited. Those inputs already carry a line total from a stored
|
|
3075
|
+
// row, so their origin must survive rather than be overwritten with the
|
|
3076
|
+
// caller default, or the untouched lines get re-inflated by quantity on
|
|
3077
|
+
// every write. Exactly one of the two origin fields is ever set, which is
|
|
3078
|
+
// the invariant the calculation engine relies on.
|
|
3079
|
+
...(isStoredRowSourcedLine(line)
|
|
3080
|
+
? { discountAmountFromStoredRow: true }
|
|
3081
|
+
: { discountAmountBasis: line.discountAmountBasis ?? "unit" }),
|
|
3121
3082
|
discountPercent: line.discountPercent ?? null,
|
|
3122
3083
|
taxRate: line.taxRate ?? null,
|
|
3123
3084
|
taxAmount: line.taxAmount ?? null,
|
|
@@ -7266,8 +7227,11 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7266
7227
|
order.currencyCode,
|
|
7267
7228
|
unitPriceNet: unitPriceNet ?? 0,
|
|
7268
7229
|
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
7269
|
-
|
|
7270
|
-
parsed.discountAmount
|
|
7230
|
+
...resolveUpsertDiscountFields(
|
|
7231
|
+
parsed.discountAmount,
|
|
7232
|
+
parsed.discountAmountBasis,
|
|
7233
|
+
existingSnapshot,
|
|
7234
|
+
),
|
|
7271
7235
|
discountPercent:
|
|
7272
7236
|
parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
|
|
7273
7237
|
taxRate: taxRate ?? 0,
|
|
@@ -7760,8 +7724,11 @@ const quoteLineUpsertCommand: CommandHandler<
|
|
|
7760
7724
|
quote.currencyCode,
|
|
7761
7725
|
unitPriceNet: unitPriceNet ?? 0,
|
|
7762
7726
|
unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
|
|
7763
|
-
|
|
7764
|
-
parsed.discountAmount
|
|
7727
|
+
...resolveUpsertDiscountFields(
|
|
7728
|
+
parsed.discountAmount,
|
|
7729
|
+
parsed.discountAmountBasis,
|
|
7730
|
+
existingSnapshot,
|
|
7731
|
+
),
|
|
7765
7732
|
discountPercent:
|
|
7766
7733
|
parsed.discountPercent ?? existingSnapshot?.discountPercent ?? 0,
|
|
7767
7734
|
taxRate: taxRate ?? 0,
|
|
@@ -9197,6 +9164,88 @@ const createInvoiceCommand: CommandHandler<
|
|
|
9197
9164
|
},
|
|
9198
9165
|
};
|
|
9199
9166
|
|
|
9167
|
+
const invoiceUpdateChangeKeys = [
|
|
9168
|
+
"invoiceNumber",
|
|
9169
|
+
"statusEntryId",
|
|
9170
|
+
"status",
|
|
9171
|
+
"issueDate",
|
|
9172
|
+
"dueDate",
|
|
9173
|
+
"currencyCode",
|
|
9174
|
+
"subtotalNetAmount",
|
|
9175
|
+
"subtotalGrossAmount",
|
|
9176
|
+
"discountTotalAmount",
|
|
9177
|
+
"taxTotalAmount",
|
|
9178
|
+
"grandTotalNetAmount",
|
|
9179
|
+
"grandTotalGrossAmount",
|
|
9180
|
+
"paidTotalAmount",
|
|
9181
|
+
"outstandingAmount",
|
|
9182
|
+
"metadata",
|
|
9183
|
+
] as const;
|
|
9184
|
+
|
|
9185
|
+
function toAuditISOString(value: unknown): string | null {
|
|
9186
|
+
if (value instanceof Date) {
|
|
9187
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
9188
|
+
}
|
|
9189
|
+
if (typeof value === "string") {
|
|
9190
|
+
const parsed = new Date(value);
|
|
9191
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
9192
|
+
}
|
|
9193
|
+
return null;
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9196
|
+
function auditValuesEqual(left: unknown, right: unknown): boolean {
|
|
9197
|
+
if (Object.is(left, right)) return true;
|
|
9198
|
+
if (left instanceof Date || right instanceof Date) {
|
|
9199
|
+
const leftIso = toAuditISOString(left);
|
|
9200
|
+
const rightIso = toAuditISOString(right);
|
|
9201
|
+
return leftIso !== null && rightIso !== null && leftIso === rightIso;
|
|
9202
|
+
}
|
|
9203
|
+
return isDeepStrictEqual(left, right);
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9206
|
+
function buildAuditChanges(
|
|
9207
|
+
before: Record<string, unknown>,
|
|
9208
|
+
after: Record<string, unknown>,
|
|
9209
|
+
keys: readonly string[],
|
|
9210
|
+
): Record<string, { from: unknown; to: unknown }> {
|
|
9211
|
+
const changedKeys = keys.filter((key) => !auditValuesEqual(before[key], after[key]));
|
|
9212
|
+
return buildChanges(before, after, changedKeys);
|
|
9213
|
+
}
|
|
9214
|
+
|
|
9215
|
+
function selectInvoiceUpdateChangeKeys(
|
|
9216
|
+
input: z.infer<typeof invoiceUpdateSchema>,
|
|
9217
|
+
): Array<(typeof invoiceUpdateChangeKeys)[number]> {
|
|
9218
|
+
return invoiceUpdateChangeKeys.filter((key) =>
|
|
9219
|
+
key === "status"
|
|
9220
|
+
? input.statusEntryId !== undefined
|
|
9221
|
+
: input[key as keyof z.infer<typeof invoiceUpdateSchema>] !== undefined,
|
|
9222
|
+
);
|
|
9223
|
+
}
|
|
9224
|
+
|
|
9225
|
+
function applyInvoiceHeaderUpdate(
|
|
9226
|
+
invoice: SalesInvoice,
|
|
9227
|
+
input: z.infer<typeof invoiceUpdateSchema>,
|
|
9228
|
+
resolvedStatus: string | null | undefined,
|
|
9229
|
+
): void {
|
|
9230
|
+
if (input.invoiceNumber !== undefined) invoice.invoiceNumber = input.invoiceNumber;
|
|
9231
|
+
if (input.statusEntryId !== undefined) {
|
|
9232
|
+
invoice.statusEntryId = input.statusEntryId;
|
|
9233
|
+
invoice.status = resolvedStatus ?? null;
|
|
9234
|
+
}
|
|
9235
|
+
if (input.issueDate !== undefined) invoice.issueDate = input.issueDate;
|
|
9236
|
+
if (input.dueDate !== undefined) invoice.dueDate = input.dueDate;
|
|
9237
|
+
if (input.currencyCode !== undefined) invoice.currencyCode = input.currencyCode;
|
|
9238
|
+
if (input.subtotalNetAmount !== undefined) invoice.subtotalNetAmount = toNumericString(input.subtotalNetAmount);
|
|
9239
|
+
if (input.subtotalGrossAmount !== undefined) invoice.subtotalGrossAmount = toNumericString(input.subtotalGrossAmount);
|
|
9240
|
+
if (input.discountTotalAmount !== undefined) invoice.discountTotalAmount = toNumericString(input.discountTotalAmount);
|
|
9241
|
+
if (input.taxTotalAmount !== undefined) invoice.taxTotalAmount = toNumericString(input.taxTotalAmount);
|
|
9242
|
+
if (input.grandTotalNetAmount !== undefined) invoice.grandTotalNetAmount = toNumericString(input.grandTotalNetAmount);
|
|
9243
|
+
if (input.grandTotalGrossAmount !== undefined) invoice.grandTotalGrossAmount = toNumericString(input.grandTotalGrossAmount);
|
|
9244
|
+
if (input.paidTotalAmount !== undefined) invoice.paidTotalAmount = toNumericString(input.paidTotalAmount);
|
|
9245
|
+
if (input.outstandingAmount !== undefined) invoice.outstandingAmount = toNumericString(input.outstandingAmount);
|
|
9246
|
+
if (input.metadata !== undefined) invoice.metadata = input.metadata;
|
|
9247
|
+
}
|
|
9248
|
+
|
|
9200
9249
|
const updateInvoiceCommand: CommandHandler<
|
|
9201
9250
|
z.infer<typeof invoiceUpdateSchema>,
|
|
9202
9251
|
{ invoiceId: string }
|
|
@@ -9225,29 +9274,12 @@ const updateInvoiceCommand: CommandHandler<
|
|
|
9225
9274
|
deletedAt: null,
|
|
9226
9275
|
});
|
|
9227
9276
|
|
|
9228
|
-
|
|
9229
|
-
"invoiceNumber",
|
|
9230
|
-
"statusEntryId",
|
|
9231
|
-
"status",
|
|
9232
|
-
"issueDate",
|
|
9233
|
-
"dueDate",
|
|
9234
|
-
"currencyCode",
|
|
9235
|
-
"subtotalNetAmount",
|
|
9236
|
-
"subtotalGrossAmount",
|
|
9237
|
-
"discountTotalAmount",
|
|
9238
|
-
"taxTotalAmount",
|
|
9239
|
-
"grandTotalNetAmount",
|
|
9240
|
-
"grandTotalGrossAmount",
|
|
9241
|
-
"paidTotalAmount",
|
|
9242
|
-
"outstandingAmount",
|
|
9243
|
-
"metadata",
|
|
9244
|
-
]);
|
|
9245
|
-
|
|
9277
|
+
let resolvedStatus: string | null | undefined;
|
|
9246
9278
|
if (parsed.statusEntryId !== undefined) {
|
|
9247
|
-
|
|
9279
|
+
resolvedStatus = await resolveDictionaryEntryValue(em, parsed.statusEntryId ?? null, { tenantId: invoice.tenantId });
|
|
9248
9280
|
}
|
|
9249
9281
|
|
|
9250
|
-
|
|
9282
|
+
applyInvoiceHeaderUpdate(invoice, parsed, resolvedStatus);
|
|
9251
9283
|
invoice.updatedAt = new Date();
|
|
9252
9284
|
await em.flush();
|
|
9253
9285
|
|
|
@@ -9283,11 +9315,18 @@ const updateInvoiceCommand: CommandHandler<
|
|
|
9283
9315
|
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
9284
9316
|
return loadInvoiceSnapshot(em, result.invoiceId);
|
|
9285
9317
|
},
|
|
9286
|
-
buildLog: async ({ result, snapshots }) => {
|
|
9318
|
+
buildLog: async ({ input, result, snapshots }) => {
|
|
9287
9319
|
const before = snapshots.before as InvoiceGraphSnapshot | undefined;
|
|
9288
9320
|
const after = snapshots.after as InvoiceGraphSnapshot | undefined;
|
|
9289
9321
|
if (!after) return null;
|
|
9290
9322
|
const { translate } = await resolveTranslations();
|
|
9323
|
+
const changes = before
|
|
9324
|
+
? buildAuditChanges(
|
|
9325
|
+
before.invoice as unknown as Record<string, unknown>,
|
|
9326
|
+
after.invoice as unknown as Record<string, unknown>,
|
|
9327
|
+
selectInvoiceUpdateChangeKeys(input),
|
|
9328
|
+
)
|
|
9329
|
+
: {};
|
|
9291
9330
|
return {
|
|
9292
9331
|
actionLabel: translate("sales.audit.invoices.update", "Update invoice"),
|
|
9293
9332
|
resourceKind: "sales.invoice",
|
|
@@ -9296,6 +9335,7 @@ const updateInvoiceCommand: CommandHandler<
|
|
|
9296
9335
|
organizationId: after.invoice.organizationId,
|
|
9297
9336
|
snapshotBefore: before,
|
|
9298
9337
|
snapshotAfter: after,
|
|
9338
|
+
changes: Object.keys(changes).length ? changes : null,
|
|
9299
9339
|
payload: {
|
|
9300
9340
|
undo: { before, after } satisfies InvoiceUndoPayload,
|
|
9301
9341
|
},
|
|
@@ -9704,6 +9744,52 @@ const createCreditMemoCommand: CommandHandler<
|
|
|
9704
9744
|
},
|
|
9705
9745
|
};
|
|
9706
9746
|
|
|
9747
|
+
const creditMemoUpdateChangeKeys = [
|
|
9748
|
+
"creditMemoNumber",
|
|
9749
|
+
"statusEntryId",
|
|
9750
|
+
"status",
|
|
9751
|
+
"reason",
|
|
9752
|
+
"issueDate",
|
|
9753
|
+
"currencyCode",
|
|
9754
|
+
"subtotalNetAmount",
|
|
9755
|
+
"subtotalGrossAmount",
|
|
9756
|
+
"taxTotalAmount",
|
|
9757
|
+
"grandTotalNetAmount",
|
|
9758
|
+
"grandTotalGrossAmount",
|
|
9759
|
+
"metadata",
|
|
9760
|
+
] as const;
|
|
9761
|
+
|
|
9762
|
+
function selectCreditMemoUpdateChangeKeys(
|
|
9763
|
+
input: z.infer<typeof creditMemoUpdateSchema>,
|
|
9764
|
+
): Array<(typeof creditMemoUpdateChangeKeys)[number]> {
|
|
9765
|
+
return creditMemoUpdateChangeKeys.filter((key) =>
|
|
9766
|
+
key === "status"
|
|
9767
|
+
? input.statusEntryId !== undefined
|
|
9768
|
+
: input[key as keyof z.infer<typeof creditMemoUpdateSchema>] !== undefined,
|
|
9769
|
+
);
|
|
9770
|
+
}
|
|
9771
|
+
|
|
9772
|
+
function applyCreditMemoHeaderUpdate(
|
|
9773
|
+
creditMemo: SalesCreditMemo,
|
|
9774
|
+
input: z.infer<typeof creditMemoUpdateSchema>,
|
|
9775
|
+
resolvedStatus: string | null | undefined,
|
|
9776
|
+
): void {
|
|
9777
|
+
if (input.creditMemoNumber !== undefined) creditMemo.creditMemoNumber = input.creditMemoNumber;
|
|
9778
|
+
if (input.statusEntryId !== undefined) {
|
|
9779
|
+
creditMemo.statusEntryId = input.statusEntryId;
|
|
9780
|
+
creditMemo.status = resolvedStatus ?? null;
|
|
9781
|
+
}
|
|
9782
|
+
if (input.reason !== undefined) creditMemo.reason = input.reason;
|
|
9783
|
+
if (input.issueDate !== undefined) creditMemo.issueDate = input.issueDate;
|
|
9784
|
+
if (input.currencyCode !== undefined) creditMemo.currencyCode = input.currencyCode;
|
|
9785
|
+
if (input.subtotalNetAmount !== undefined) creditMemo.subtotalNetAmount = toNumericString(input.subtotalNetAmount);
|
|
9786
|
+
if (input.subtotalGrossAmount !== undefined) creditMemo.subtotalGrossAmount = toNumericString(input.subtotalGrossAmount);
|
|
9787
|
+
if (input.taxTotalAmount !== undefined) creditMemo.taxTotalAmount = toNumericString(input.taxTotalAmount);
|
|
9788
|
+
if (input.grandTotalNetAmount !== undefined) creditMemo.grandTotalNetAmount = toNumericString(input.grandTotalNetAmount);
|
|
9789
|
+
if (input.grandTotalGrossAmount !== undefined) creditMemo.grandTotalGrossAmount = toNumericString(input.grandTotalGrossAmount);
|
|
9790
|
+
if (input.metadata !== undefined) creditMemo.metadata = input.metadata;
|
|
9791
|
+
}
|
|
9792
|
+
|
|
9707
9793
|
const updateCreditMemoCommand: CommandHandler<
|
|
9708
9794
|
z.infer<typeof creditMemoUpdateSchema>,
|
|
9709
9795
|
{ creditMemoId: string }
|
|
@@ -9732,26 +9818,12 @@ const updateCreditMemoCommand: CommandHandler<
|
|
|
9732
9818
|
deletedAt: null,
|
|
9733
9819
|
});
|
|
9734
9820
|
|
|
9735
|
-
|
|
9736
|
-
"creditMemoNumber",
|
|
9737
|
-
"statusEntryId",
|
|
9738
|
-
"status",
|
|
9739
|
-
"reason",
|
|
9740
|
-
"issueDate",
|
|
9741
|
-
"currencyCode",
|
|
9742
|
-
"subtotalNetAmount",
|
|
9743
|
-
"subtotalGrossAmount",
|
|
9744
|
-
"taxTotalAmount",
|
|
9745
|
-
"grandTotalNetAmount",
|
|
9746
|
-
"grandTotalGrossAmount",
|
|
9747
|
-
"metadata",
|
|
9748
|
-
]);
|
|
9749
|
-
|
|
9821
|
+
let resolvedStatus: string | null | undefined;
|
|
9750
9822
|
if (parsed.statusEntryId !== undefined) {
|
|
9751
|
-
|
|
9823
|
+
resolvedStatus = await resolveDictionaryEntryValue(em, parsed.statusEntryId ?? null, { tenantId: creditMemo.tenantId });
|
|
9752
9824
|
}
|
|
9753
9825
|
|
|
9754
|
-
|
|
9826
|
+
applyCreditMemoHeaderUpdate(creditMemo, parsed, resolvedStatus);
|
|
9755
9827
|
creditMemo.updatedAt = new Date();
|
|
9756
9828
|
await em.flush();
|
|
9757
9829
|
|
|
@@ -9787,11 +9859,18 @@ const updateCreditMemoCommand: CommandHandler<
|
|
|
9787
9859
|
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
9788
9860
|
return loadCreditMemoSnapshot(em, result.creditMemoId);
|
|
9789
9861
|
},
|
|
9790
|
-
buildLog: async ({ result, snapshots }) => {
|
|
9862
|
+
buildLog: async ({ input, result, snapshots }) => {
|
|
9791
9863
|
const before = snapshots.before as CreditMemoGraphSnapshot | undefined;
|
|
9792
9864
|
const after = snapshots.after as CreditMemoGraphSnapshot | undefined;
|
|
9793
9865
|
if (!after) return null;
|
|
9794
9866
|
const { translate } = await resolveTranslations();
|
|
9867
|
+
const changes = before
|
|
9868
|
+
? buildAuditChanges(
|
|
9869
|
+
before.creditMemo as unknown as Record<string, unknown>,
|
|
9870
|
+
after.creditMemo as unknown as Record<string, unknown>,
|
|
9871
|
+
selectCreditMemoUpdateChangeKeys(input),
|
|
9872
|
+
)
|
|
9873
|
+
: {};
|
|
9795
9874
|
return {
|
|
9796
9875
|
actionLabel: translate("sales.audit.credit_memos.update", "Update credit memo"),
|
|
9797
9876
|
resourceKind: "sales.credit_memo",
|
|
@@ -9800,6 +9879,7 @@ const updateCreditMemoCommand: CommandHandler<
|
|
|
9800
9879
|
organizationId: after.creditMemo.organizationId,
|
|
9801
9880
|
snapshotBefore: before,
|
|
9802
9881
|
snapshotAfter: after,
|
|
9882
|
+
changes: Object.keys(changes).length ? changes : null,
|
|
9803
9883
|
payload: {
|
|
9804
9884
|
undo: { before, after } satisfies CreditMemoUndoPayload,
|
|
9805
9885
|
},
|
|
@@ -16,6 +16,7 @@ import type { SalesAdjustmentDraft, SalesLineSnapshot, SalesDocumentCalculationR
|
|
|
16
16
|
import { cloneJson, deriveLineNetFromGross, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from './shared'
|
|
17
17
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
18
18
|
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from '../data/entities'
|
|
19
|
+
import { mapOrderLineEntityToSnapshot } from '../lib/lineSnapshots'
|
|
19
20
|
import { loadShippedQuantityByLine } from '../lib/shipments/snapshots'
|
|
20
21
|
import { computeAvailableReturnQuantity } from '../lib/returnQuantity'
|
|
21
22
|
import {
|
|
@@ -134,37 +135,6 @@ function applyOrderTotals(order: SalesOrder, totals: SalesDocumentCalculationRes
|
|
|
134
135
|
order.lineItemCount = lineCount
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
function mapOrderLineEntityToSnapshot(line: SalesOrderLine): SalesLineSnapshot {
|
|
138
|
-
return {
|
|
139
|
-
id: line.id,
|
|
140
|
-
lineNumber: line.lineNumber,
|
|
141
|
-
kind: line.kind,
|
|
142
|
-
productId: line.productId ?? null,
|
|
143
|
-
productVariantId: line.productVariantId ?? null,
|
|
144
|
-
name: line.name ?? null,
|
|
145
|
-
description: line.description ?? null,
|
|
146
|
-
comment: line.comment ?? null,
|
|
147
|
-
quantity: toNumeric(line.quantity),
|
|
148
|
-
quantityUnit: line.quantityUnit ?? null,
|
|
149
|
-
normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
|
|
150
|
-
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
151
|
-
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
152
|
-
currencyCode: line.currencyCode,
|
|
153
|
-
unitPriceNet: toNumeric(line.unitPriceNet),
|
|
154
|
-
unitPriceGross: toNumeric(line.unitPriceGross),
|
|
155
|
-
discountAmount: toNumeric(line.discountAmount),
|
|
156
|
-
discountPercent: toNumeric(line.discountPercent),
|
|
157
|
-
taxRate: toNumeric(line.taxRate),
|
|
158
|
-
taxAmount: toNumeric(line.taxAmount),
|
|
159
|
-
totalNetAmount: toNumeric(line.totalNetAmount),
|
|
160
|
-
totalGrossAmount: toNumeric(line.totalGrossAmount),
|
|
161
|
-
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
162
|
-
promotionCode: line.promotionCode ?? null,
|
|
163
|
-
metadata: line.metadata ? cloneJson(line.metadata) : null,
|
|
164
|
-
customFieldSetId: line.customFieldSetId ?? null,
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
138
|
function mapOrderAdjustmentToDraft(adjustment: SalesOrderAdjustment): SalesAdjustmentDraft {
|
|
169
139
|
return {
|
|
170
140
|
id: adjustment.id,
|
|
@@ -49,10 +49,7 @@ export async function enforceSalesDocumentOptimisticLock(
|
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export
|
|
53
|
-
if (value === null || value === undefined) return value
|
|
54
|
-
return JSON.parse(JSON.stringify(value)) as T
|
|
55
|
-
}
|
|
52
|
+
export { cloneJson } from '../lib/json'
|
|
56
53
|
|
|
57
54
|
export function toNumericString(value: number | null | undefined): string | null {
|
|
58
55
|
if (value === undefined || value === null) return null
|
|
@@ -49,6 +49,17 @@ export function createSalesOrderLineDraft(
|
|
|
49
49
|
const catalogSnapshot = payload.catalogSnapshot && typeof payload.catalogSnapshot === 'object'
|
|
50
50
|
? payload.catalogSnapshot as Record<string, unknown>
|
|
51
51
|
: null
|
|
52
|
+
// `payload` is forwarded verbatim to the create API, where a supplied
|
|
53
|
+
// `discountAmount` is read per unit unless the caller says otherwise, while
|
|
54
|
+
// `record` below drives the grid, which shows the discount for the whole
|
|
55
|
+
// line. The multiplication is therefore the conversion between the two, not a
|
|
56
|
+
// second copy of the per-unit misreading the calculation engine used to hold
|
|
57
|
+
// — but it has to stop when the caller has explicitly said the amount is
|
|
58
|
+
// already a line total, or the grid would show the discount quantity times
|
|
59
|
+
// over while the persisted order shows it once.
|
|
60
|
+
const draftDiscountAmount = payload.discountAmountBasis === 'line'
|
|
61
|
+
? normalizeNumber(payload.discountAmount, 0)
|
|
62
|
+
: normalizeNumber(payload.discountAmount, 0) * quantity
|
|
52
63
|
|
|
53
64
|
return {
|
|
54
65
|
id,
|
|
@@ -65,7 +76,7 @@ export function createSalesOrderLineDraft(
|
|
|
65
76
|
currencyCode: typeof payload.currencyCode === 'string' ? payload.currencyCode : null,
|
|
66
77
|
unitPriceNet,
|
|
67
78
|
unitPriceGross,
|
|
68
|
-
discountAmount:
|
|
79
|
+
discountAmount: draftDiscountAmount,
|
|
69
80
|
discountPercent: normalizeNumber(payload.discountPercent, 0),
|
|
70
81
|
taxRate,
|
|
71
82
|
totalNet,
|
|
@@ -340,6 +340,9 @@ const linePricingSchema = z.object({
|
|
|
340
340
|
priceMode: z.enum(['net', 'gross']).optional(),
|
|
341
341
|
taxRateId: uuid().optional(),
|
|
342
342
|
discountAmount: decimal({ min: 0 }).optional(),
|
|
343
|
+
// How to read a supplied `discountAmount`. Omitting it reproduces the meaning
|
|
344
|
+
// the API has always documented — per unit — so no existing caller changes.
|
|
345
|
+
discountAmountBasis: z.enum(['unit', 'line']).optional(),
|
|
343
346
|
discountPercent: percentage().optional(),
|
|
344
347
|
taxRate: percentage().optional(),
|
|
345
348
|
taxAmount: decimal({ min: 0 }).optional(),
|
|
@@ -77,6 +77,35 @@ function resolveAdjustmentAmounts(
|
|
|
77
77
|
})
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// `discount_amount` stores the discount for the WHOLE line, while
|
|
81
|
+
// `discount_percent` records the operator's intent. The percentage therefore
|
|
82
|
+
// wins whenever it is set: a stored amount is only ever its cached result, and
|
|
83
|
+
// because the column is NOT NULL DEFAULT '0' a stored 0 cannot be told apart
|
|
84
|
+
// from "no discount supplied" — so it counts as absent rather than as a
|
|
85
|
+
// suppressing value. That is what makes recalculation idempotent, and what lets
|
|
86
|
+
// a row whose amount the old engine dropped or re-inflated heal itself on the
|
|
87
|
+
// next pass. Spec: .ai/specs/2026-08-07-sales-line-discount-amount-contract.md.
|
|
88
|
+
function resolveLineDiscountTotal(
|
|
89
|
+
line: SalesLineSnapshot,
|
|
90
|
+
netSubtotalBeforeDiscount: number,
|
|
91
|
+
quantity: number,
|
|
92
|
+
): number {
|
|
93
|
+
const percent = toNumber(line.discountPercent, 0)
|
|
94
|
+
if (line.discountPercent !== null && line.discountPercent !== undefined && percent !== 0) {
|
|
95
|
+
return (percent / 100) * netSubtotalBeforeDiscount
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const amount = toNumber(line.discountAmount, 0)
|
|
99
|
+
if (line.discountAmount === null || line.discountAmount === undefined || amount === 0) return 0
|
|
100
|
+
|
|
101
|
+
// A snapshot rebuilt from a persisted row already holds a line total, so it
|
|
102
|
+
// is never multiplied out again. Anything else came from a caller and keeps
|
|
103
|
+
// the per-unit meaning the API has always documented unless the caller says
|
|
104
|
+
// otherwise.
|
|
105
|
+
if (line.discountAmountFromStoredRow === true) return amount
|
|
106
|
+
return line.discountAmountBasis === 'line' ? amount : amount * quantity
|
|
107
|
+
}
|
|
108
|
+
|
|
80
109
|
function buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResult {
|
|
81
110
|
const quantity = Math.max(toNumber(line.quantity, 0), 0)
|
|
82
111
|
const taxRate = toNumber(line.taxRate, 0) / 100
|
|
@@ -85,14 +114,11 @@ function buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResul
|
|
|
85
114
|
(line.unitPriceGross !== null && line.unitPriceGross !== undefined
|
|
86
115
|
? toNumber(line.unitPriceGross) / (1 + taxRate)
|
|
87
116
|
: 0)
|
|
88
|
-
const discountPerUnit =
|
|
89
|
-
line.discountAmount ??
|
|
90
|
-
(line.discountPercent !== null && line.discountPercent !== undefined
|
|
91
|
-
? toNumber(line.discountPercent, 0) / 100 * toNumber(unitNet, 0)
|
|
92
|
-
: 0)
|
|
93
|
-
|
|
94
117
|
const netSubtotalBeforeDiscount = toNumber(unitNet, 0) * quantity
|
|
95
|
-
const discountTotal = Math.min(
|
|
118
|
+
const discountTotal = Math.min(
|
|
119
|
+
Math.max(resolveLineDiscountTotal(line, netSubtotalBeforeDiscount, quantity), 0),
|
|
120
|
+
netSubtotalBeforeDiscount,
|
|
121
|
+
)
|
|
96
122
|
const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0)
|
|
97
123
|
const explicitTaxAmount = line.taxAmount !== null && line.taxAmount !== undefined
|
|
98
124
|
let taxAmount = explicitTaxAmount
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { SalesOrderLine, SalesQuoteLine } from '../data/entities'
|
|
2
|
+
import { cloneJson } from './json'
|
|
3
|
+
import type { SalesLineDiscountBasis, SalesLineSnapshot } from './types'
|
|
4
|
+
|
|
5
|
+
function toNumeric(value: unknown): number {
|
|
6
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
7
|
+
if (typeof value === 'string' && value.trim().length) {
|
|
8
|
+
const parsed = Number(value)
|
|
9
|
+
if (Number.isFinite(parsed)) return parsed
|
|
10
|
+
}
|
|
11
|
+
return 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mapPersistedLine(line: SalesOrderLine | SalesQuoteLine): SalesLineSnapshot {
|
|
15
|
+
return {
|
|
16
|
+
id: line.id,
|
|
17
|
+
lineNumber: line.lineNumber,
|
|
18
|
+
kind: line.kind,
|
|
19
|
+
productId: line.productId ?? null,
|
|
20
|
+
productVariantId: line.productVariantId ?? null,
|
|
21
|
+
name: line.name ?? null,
|
|
22
|
+
description: line.description ?? null,
|
|
23
|
+
comment: line.comment ?? null,
|
|
24
|
+
quantity: toNumeric(line.quantity),
|
|
25
|
+
quantityUnit: line.quantityUnit ?? null,
|
|
26
|
+
normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
|
|
27
|
+
normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
|
|
28
|
+
uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
|
|
29
|
+
currencyCode: line.currencyCode,
|
|
30
|
+
unitPriceNet: toNumeric(line.unitPriceNet),
|
|
31
|
+
unitPriceGross: toNumeric(line.unitPriceGross),
|
|
32
|
+
discountAmount: toNumeric(line.discountAmount),
|
|
33
|
+
// The persisted column holds the discount for the whole line, so the
|
|
34
|
+
// calculation engine must not multiply it out by quantity again. This flag
|
|
35
|
+
// is the only thing standing between a round trip and a compounding
|
|
36
|
+
// re-inflation, and it is why mappers must never set `discountAmountBasis`
|
|
37
|
+
// instead — that one means "a caller asserted this".
|
|
38
|
+
discountAmountFromStoredRow: true,
|
|
39
|
+
discountPercent: toNumeric(line.discountPercent),
|
|
40
|
+
taxRate: toNumeric(line.taxRate),
|
|
41
|
+
taxAmount: toNumeric(line.taxAmount),
|
|
42
|
+
totalNetAmount: toNumeric(line.totalNetAmount),
|
|
43
|
+
totalGrossAmount: toNumeric(line.totalGrossAmount),
|
|
44
|
+
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
45
|
+
promotionCode: line.promotionCode ?? null,
|
|
46
|
+
metadata: line.metadata ? cloneJson(line.metadata) : null,
|
|
47
|
+
customFieldSetId: line.customFieldSetId ?? null,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Rebuild a calculation snapshot from a persisted order line.
|
|
53
|
+
*
|
|
54
|
+
* Shared by `commands/documents.ts` and `commands/returns.ts` on purpose: the
|
|
55
|
+
* two files used to carry byte-identical copies, and that duplication is why
|
|
56
|
+
* the return flows kept the discount defect after the order flows were fixed.
|
|
57
|
+
*/
|
|
58
|
+
export function mapOrderLineEntityToSnapshot(line: SalesOrderLine): SalesLineSnapshot {
|
|
59
|
+
return mapPersistedLine(line)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function mapQuoteLineEntityToSnapshot(line: SalesQuoteLine): SalesLineSnapshot {
|
|
63
|
+
return mapPersistedLine(line)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type UpsertDiscountFields = Pick<
|
|
67
|
+
SalesLineSnapshot,
|
|
68
|
+
'discountAmount' | 'discountAmountBasis' | 'discountAmountFromStoredRow'
|
|
69
|
+
>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decide the discount fields of an upsert payload one operand at a time.
|
|
73
|
+
*
|
|
74
|
+
* A line upsert can source its amount from two places with opposite meanings: a
|
|
75
|
+
* caller value, which is per unit unless the caller says otherwise, and the
|
|
76
|
+
* stored row, which is always a line total. Collapsing them into a single
|
|
77
|
+
* `caller ?? stored` expression means whichever origin the merged result is
|
|
78
|
+
* tagged with is wrong half the time — and the half that breaks is the
|
|
79
|
+
* upsert-existing path, which then re-inflates by a further factor of quantity
|
|
80
|
+
* while looking perfectly correct.
|
|
81
|
+
*
|
|
82
|
+
* Keeping `null` rather than `0` for "nothing supplied" is deliberate too: the
|
|
83
|
+
* column cannot represent that distinction, but the payload can, and the
|
|
84
|
+
* calculation engine needs it to tell a suppressing zero from an absent value.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveUpsertDiscountFields(
|
|
87
|
+
callerAmount: number | null | undefined,
|
|
88
|
+
callerBasis: SalesLineDiscountBasis | null | undefined,
|
|
89
|
+
existingSnapshot: Pick<SalesLineSnapshot, 'discountAmount'> | null | undefined,
|
|
90
|
+
): UpsertDiscountFields {
|
|
91
|
+
if (callerAmount !== null && callerAmount !== undefined) {
|
|
92
|
+
return { discountAmount: callerAmount, discountAmountBasis: callerBasis ?? 'unit' }
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
discountAmount: existingSnapshot?.discountAmount ?? null,
|
|
96
|
+
discountAmountFromStoredRow: existingSnapshot != null,
|
|
97
|
+
}
|
|
98
|
+
}
|