@open-mercato/core 0.10.1-develop.7722.1.901c72b9e9 → 0.10.1-develop.7724.1.95e4e7d38e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/sales_order/index.js +2 -0
- package/dist/generated/entities/sales_order/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +2 -0
- package/dist/generated/entities/sales_order_line/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +3 -0
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +2 -1
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +83 -6
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +141 -18
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +24 -7
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +17 -11
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/DocumentTotals.js +21 -3
- package/dist/modules/sales/components/documents/DocumentTotals.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +23 -12
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +10 -0
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +9 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +85 -6
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/externalAmounts.js +143 -0
- package/dist/modules/sales/lib/externalAmounts.js.map +7 -0
- package/dist/modules/sales/lib/lineSnapshots.js +6 -1
- package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js +8 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/sales/lib/providers/totals.js +3 -2
- package/dist/modules/sales/lib/providers/totals.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js +19 -0
- package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js.map +7 -0
- package/generated/entities/sales_order/index.ts +1 -0
- package/generated/entities/sales_order_line/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/sales/api/documents/factory.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +2 -1
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +96 -0
- package/src/modules/sales/commands/documents.ts +219 -44
- package/src/modules/sales/commands/returns.ts +27 -3
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +34 -25
- package/src/modules/sales/components/documents/DocumentTotals.tsx +26 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +27 -12
- package/src/modules/sales/data/entities.ts +14 -0
- package/src/modules/sales/data/validators.ts +10 -0
- package/src/modules/sales/i18n/de.json +12 -0
- package/src/modules/sales/i18n/en.json +12 -0
- package/src/modules/sales/i18n/es.json +12 -0
- package/src/modules/sales/i18n/ko.json +12 -0
- package/src/modules/sales/i18n/pl.json +12 -0
- package/src/modules/sales/lib/calculations.ts +99 -4
- package/src/modules/sales/lib/externalAmounts.ts +174 -0
- package/src/modules/sales/lib/lineSnapshots.ts +6 -1
- package/src/modules/sales/lib/makeSalesLineRoute.ts +8 -0
- package/src/modules/sales/lib/providers/totals.ts +5 -2
- package/src/modules/sales/lib/types.ts +19 -2
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +33 -1
- package/src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts +25 -0
|
@@ -84,7 +84,9 @@ import {
|
|
|
84
84
|
orderCreateSchema,
|
|
85
85
|
ORDER_PAYMENT_LEDGER_WARNING_CODE,
|
|
86
86
|
resolveSuppliedOrderPaymentLedgerFields,
|
|
87
|
+
amountsModeSchema,
|
|
87
88
|
orderLineCreateSchema,
|
|
89
|
+
orderHeaderTotalsSchema,
|
|
88
90
|
orderAdjustmentCreateSchema,
|
|
89
91
|
invoiceCreateSchema,
|
|
90
92
|
invoiceUpdateSchema,
|
|
@@ -130,9 +132,11 @@ import type {
|
|
|
130
132
|
ShippingMethodContext,
|
|
131
133
|
} from "../lib/providers";
|
|
132
134
|
import {
|
|
135
|
+
type SalesAmountsMode,
|
|
133
136
|
type SalesLineSnapshot,
|
|
134
137
|
type SalesLineUomSnapshot,
|
|
135
138
|
type SalesAdjustmentDraft,
|
|
139
|
+
type SalesDocumentAmounts,
|
|
136
140
|
type SalesLineCalculationResult,
|
|
137
141
|
type SalesDocumentCalculationResult,
|
|
138
142
|
} from "../lib/types";
|
|
@@ -143,6 +147,18 @@ import {
|
|
|
143
147
|
resolveUpsertTotalsOrigin,
|
|
144
148
|
resolveUpsertCalculatedAmounts,
|
|
145
149
|
} from "../lib/lineSnapshots";
|
|
150
|
+
import {
|
|
151
|
+
assertAmountsModeUnsupportedOnQuote,
|
|
152
|
+
assertExternalHeaderComplete,
|
|
153
|
+
assertExternalLineComplete,
|
|
154
|
+
assertUniformAmountsMode,
|
|
155
|
+
buildExternalHeaderTotals,
|
|
156
|
+
hasAnySuppliedHeaderTotal,
|
|
157
|
+
readPersistedHeaderTotals,
|
|
158
|
+
refuseOnExternalOrder,
|
|
159
|
+
requireOrderTotalsForExternalWrite,
|
|
160
|
+
} from "../lib/externalAmounts";
|
|
161
|
+
import { isExternalAmountsMode } from "../lib/calculations";
|
|
146
162
|
import { loadShippedQuantityByLine } from "../lib/shipments/snapshots";
|
|
147
163
|
import { resolveDictionaryEntryValue, resolveCachedDictionaryEntryValue } from "../lib/dictionaries";
|
|
148
164
|
import { assertOrderCanBeCancelled, isCancelledOrderStatus } from "../lib/orderCancellationGuard";
|
|
@@ -390,6 +406,7 @@ type OrderGraphSnapshot = {
|
|
|
390
406
|
outstandingAmount: string;
|
|
391
407
|
totalsSnapshot: Record<string, unknown> | null;
|
|
392
408
|
lineItemCount: number;
|
|
409
|
+
totalsMode: SalesAmountsMode;
|
|
393
410
|
};
|
|
394
411
|
lines: OrderLineSnapshot[];
|
|
395
412
|
adjustments: OrderAdjustmentSnapshot[];
|
|
@@ -430,6 +447,7 @@ type OrderLineSnapshot = {
|
|
|
430
447
|
taxAmount: string;
|
|
431
448
|
totalNetAmount: string;
|
|
432
449
|
totalGrossAmount: string;
|
|
450
|
+
amountsMode: SalesAmountsMode;
|
|
433
451
|
configuration: Record<string, unknown> | null;
|
|
434
452
|
promotionCode: string | null;
|
|
435
453
|
promotionSnapshot: Record<string, unknown> | null;
|
|
@@ -604,7 +622,7 @@ const addressSnapshotSchema = z
|
|
|
604
622
|
// is 0 — and would turn a clear into a written zero.
|
|
605
623
|
const exchangeRateSchema = z.coerce.number().min(0);
|
|
606
624
|
|
|
607
|
-
|
|
625
|
+
const documentUpdateFieldsSchema = z
|
|
608
626
|
.object({
|
|
609
627
|
id: z.string().uuid(),
|
|
610
628
|
customerEntityId: z.string().uuid().nullable().optional(),
|
|
@@ -652,43 +670,63 @@ export const documentUpdateSchema = z
|
|
|
652
670
|
tags: z.array(z.string().uuid()).optional(),
|
|
653
671
|
customFields: z.record(z.string(), z.unknown()).optional(),
|
|
654
672
|
customFieldSetId: z.string().uuid().nullable().optional(),
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
673
|
+
totalsMode: amountsModeSchema.optional(),
|
|
674
|
+
...orderHeaderTotalsSchema.shape,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
type DocumentUpdateFields = z.infer<typeof documentUpdateFieldsSchema>;
|
|
678
|
+
|
|
679
|
+
function carriesDocumentEdit(input: DocumentUpdateFields): boolean {
|
|
680
|
+
return (
|
|
681
|
+
typeof input.currencyCode === "string" ||
|
|
682
|
+
input.placedAt !== undefined ||
|
|
683
|
+
input.expectedDeliveryAt !== undefined ||
|
|
684
|
+
input.channelId !== undefined ||
|
|
685
|
+
input.statusEntryId !== undefined ||
|
|
686
|
+
input.exchangeRate !== undefined ||
|
|
687
|
+
input.paymentStatusEntryId !== undefined ||
|
|
688
|
+
input.fulfillmentStatusEntryId !== undefined ||
|
|
689
|
+
input.shippingAddressId !== undefined ||
|
|
690
|
+
input.billingAddressId !== undefined ||
|
|
691
|
+
input.customerEntityId !== undefined ||
|
|
692
|
+
input.customerContactId !== undefined ||
|
|
693
|
+
input.customerSnapshot !== undefined ||
|
|
694
|
+
input.metadata !== undefined ||
|
|
695
|
+
input.customerReference !== undefined ||
|
|
696
|
+
input.externalReference !== undefined ||
|
|
697
|
+
input.comment !== undefined ||
|
|
698
|
+
input.comments !== undefined ||
|
|
699
|
+
input.internalNotes !== undefined ||
|
|
700
|
+
input.orderNumber !== undefined ||
|
|
701
|
+
input.quoteNumber !== undefined ||
|
|
702
|
+
input.shippingAddressSnapshot !== undefined ||
|
|
703
|
+
input.billingAddressSnapshot !== undefined ||
|
|
704
|
+
input.shippingMethodId !== undefined ||
|
|
705
|
+
input.shippingMethodCode !== undefined ||
|
|
706
|
+
input.shippingMethodSnapshot !== undefined ||
|
|
707
|
+
input.paymentMethodId !== undefined ||
|
|
708
|
+
input.paymentMethodCode !== undefined ||
|
|
709
|
+
input.paymentMethodSnapshot !== undefined ||
|
|
710
|
+
input.tags !== undefined ||
|
|
711
|
+
input.customFields !== undefined ||
|
|
712
|
+
input.customFieldSetId !== undefined
|
|
691
713
|
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// The route cannot see an order's persisted mode, so it lets header totals count
|
|
717
|
+
// as an edit; each command re-applies `documentEditSchema` once it can. An empty
|
|
718
|
+
// update must not reach a quote: execution reverts a `sent` quote to draft.
|
|
719
|
+
export const documentUpdateSchema = documentUpdateFieldsSchema.refine(
|
|
720
|
+
(input) =>
|
|
721
|
+
carriesDocumentEdit(input) ||
|
|
722
|
+
input.totalsMode !== undefined ||
|
|
723
|
+
hasAnySuppliedHeaderTotal(input),
|
|
724
|
+
{ message: "update_payload_empty" },
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const documentEditSchema = documentUpdateFieldsSchema.refine(carriesDocumentEdit, {
|
|
728
|
+
message: "update_payload_empty",
|
|
729
|
+
});
|
|
692
730
|
|
|
693
731
|
export type DocumentUpdateInput = z.infer<typeof documentUpdateSchema>;
|
|
694
732
|
|
|
@@ -2049,6 +2087,7 @@ async function loadOrderSnapshot(
|
|
|
2049
2087
|
? cloneJson(order.totalsSnapshot)
|
|
2050
2088
|
: null,
|
|
2051
2089
|
lineItemCount: order.lineItemCount,
|
|
2090
|
+
totalsMode: order.totalsMode ?? "computed",
|
|
2052
2091
|
},
|
|
2053
2092
|
lines: lines.map((line) => ({
|
|
2054
2093
|
id: line.id,
|
|
@@ -2082,6 +2121,7 @@ async function loadOrderSnapshot(
|
|
|
2082
2121
|
taxAmount: line.taxAmount,
|
|
2083
2122
|
totalNetAmount: line.totalNetAmount,
|
|
2084
2123
|
totalGrossAmount: line.totalGrossAmount,
|
|
2124
|
+
amountsMode: line.amountsMode ?? "computed",
|
|
2085
2125
|
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
2086
2126
|
promotionCode: line.promotionCode ?? null,
|
|
2087
2127
|
promotionSnapshot: line.promotionSnapshot
|
|
@@ -3124,6 +3164,7 @@ function createLineSnapshotFromInput(
|
|
|
3124
3164
|
"customFields" in line && line.customFields
|
|
3125
3165
|
? cloneJson((line as any).customFields)
|
|
3126
3166
|
: null,
|
|
3167
|
+
amountsMode: (line as { amountsMode?: SalesAmountsMode | null }).amountsMode ?? null,
|
|
3127
3168
|
};
|
|
3128
3169
|
}
|
|
3129
3170
|
|
|
@@ -3163,7 +3204,12 @@ function convertLineCalculationToEntityInput(
|
|
|
3163
3204
|
index: number,
|
|
3164
3205
|
) {
|
|
3165
3206
|
const line = lineResult.line;
|
|
3166
|
-
|
|
3207
|
+
// The gross-derived net repair heals core-owned rows; on an external line a
|
|
3208
|
+
// zero net is the caller's figure, not a defect to correct.
|
|
3209
|
+
const persistTotals = isExternalAmountsMode(line.amountsMode)
|
|
3210
|
+
? <T,>(payload: T): T => payload
|
|
3211
|
+
: reconcileLinePersistedTotals;
|
|
3212
|
+
return persistTotals({
|
|
3167
3213
|
lineNumber: line.lineNumber ?? index + 1,
|
|
3168
3214
|
kind: line.kind ?? "product",
|
|
3169
3215
|
statusEntryId: sourceLine.statusEntryId ?? null,
|
|
@@ -3201,6 +3247,8 @@ function convertLineCalculationToEntityInput(
|
|
|
3201
3247
|
taxAmount: toNumericString(lineResult.taxAmount) ?? "0",
|
|
3202
3248
|
totalNetAmount: toNumericString(lineResult.netAmount) ?? "0",
|
|
3203
3249
|
totalGrossAmount: toNumericString(lineResult.grossAmount) ?? "0",
|
|
3250
|
+
// Conditional: this converter also serves quote lines, which have no column.
|
|
3251
|
+
...(line.amountsMode ? { amountsMode: line.amountsMode } : {}),
|
|
3204
3252
|
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
3205
3253
|
promotionCode: line.promotionCode ?? null,
|
|
3206
3254
|
promotionSnapshot: sourceLine.promotionSnapshot
|
|
@@ -3695,6 +3743,25 @@ function applyOrderTotals(
|
|
|
3695
3743
|
order.lineItemCount = lineCount;
|
|
3696
3744
|
}
|
|
3697
3745
|
|
|
3746
|
+
function applyOrderLineAmountsFromCalculation(params: {
|
|
3747
|
+
em: EntityManager;
|
|
3748
|
+
lines: SalesOrderLine[];
|
|
3749
|
+
calculation: SalesDocumentCalculationResult;
|
|
3750
|
+
amountsMode: SalesAmountsMode;
|
|
3751
|
+
}): void {
|
|
3752
|
+
const { em, lines, calculation, amountsMode } = params;
|
|
3753
|
+
lines.forEach((line, index) => {
|
|
3754
|
+
const result = calculation.lines[index];
|
|
3755
|
+
if (!result) return;
|
|
3756
|
+
line.amountsMode = amountsMode;
|
|
3757
|
+
line.discountAmount = toNumericString(result.discountAmount) ?? "0";
|
|
3758
|
+
line.taxAmount = toNumericString(result.taxAmount) ?? "0";
|
|
3759
|
+
line.totalNetAmount = toNumericString(result.netAmount) ?? "0";
|
|
3760
|
+
line.totalGrossAmount = toNumericString(result.grossAmount) ?? "0";
|
|
3761
|
+
em.persist(line);
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3698
3765
|
function normalizePaymentTotal(value: unknown): number {
|
|
3699
3766
|
if (typeof value === "number" && Number.isFinite(value))
|
|
3700
3767
|
return Math.max(value, 0);
|
|
@@ -4069,6 +4136,9 @@ function applyOrderSnapshot(
|
|
|
4069
4136
|
? cloneJson(snapshot.totalsSnapshot)
|
|
4070
4137
|
: null;
|
|
4071
4138
|
order.lineItemCount = snapshot.lineItemCount;
|
|
4139
|
+
// Restored with the amounts, never separately: the lines come back with their
|
|
4140
|
+
// own mode, so omitting this leaves a mixed document the next write recomputes.
|
|
4141
|
+
order.totalsMode = snapshot.totalsMode ?? "computed";
|
|
4072
4142
|
}
|
|
4073
4143
|
|
|
4074
4144
|
async function assertQuoteGraphUndoCurrent(
|
|
@@ -4744,6 +4814,7 @@ async function restoreOrderGraph(
|
|
|
4744
4814
|
? cloneJson(snapshot.order.totalsSnapshot)
|
|
4745
4815
|
: null,
|
|
4746
4816
|
lineItemCount: snapshot.order.lineItemCount,
|
|
4817
|
+
totalsMode: snapshot.order.totalsMode ?? "computed",
|
|
4747
4818
|
createdAt: new Date(),
|
|
4748
4819
|
updatedAt: new Date(),
|
|
4749
4820
|
});
|
|
@@ -4854,10 +4925,14 @@ async function restoreOrderGraph(
|
|
|
4854
4925
|
discountPercent: line.discountPercent,
|
|
4855
4926
|
taxRate: line.taxRate,
|
|
4856
4927
|
taxAmount: line.taxAmount,
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4928
|
+
// Not repaired from gross: an external line's net is the caller's figure.
|
|
4929
|
+
totalNetAmount: isExternalAmountsMode(line.amountsMode)
|
|
4930
|
+
? line.totalNetAmount
|
|
4931
|
+
: toNumericString(
|
|
4932
|
+
deriveLineNetFromGross(line.totalNetAmount, line.totalGrossAmount, line.taxRate),
|
|
4933
|
+
),
|
|
4860
4934
|
totalGrossAmount: line.totalGrossAmount,
|
|
4935
|
+
amountsMode: line.amountsMode ?? "computed",
|
|
4861
4936
|
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
4862
4937
|
promotionCode: line.promotionCode ?? null,
|
|
4863
4938
|
promotionSnapshot: line.promotionSnapshot
|
|
@@ -5157,6 +5232,7 @@ const createQuoteCommand: CommandHandler<
|
|
|
5157
5232
|
updatedAt: new Date(),
|
|
5158
5233
|
});
|
|
5159
5234
|
|
|
5235
|
+
await assertAmountsModeUnsupportedOnQuote(parsed.lines ?? []);
|
|
5160
5236
|
const lineInputs = (parsed.lines ?? []).map((line, index) =>
|
|
5161
5237
|
quoteLineCreateSchema.parse({
|
|
5162
5238
|
...line,
|
|
@@ -5539,6 +5615,8 @@ const updateQuoteCommand: CommandHandler<
|
|
|
5539
5615
|
},
|
|
5540
5616
|
async execute(rawInput, ctx) {
|
|
5541
5617
|
const parsed = documentUpdateSchema.parse(rawInput ?? {});
|
|
5618
|
+
await assertAmountsModeUnsupportedOnQuote([parsed]);
|
|
5619
|
+
documentEditSchema.parse(rawInput ?? {});
|
|
5542
5620
|
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
5543
5621
|
const quote = await findOneWithDecryption(em, SalesQuote, {
|
|
5544
5622
|
id: parsed.id,
|
|
@@ -5836,6 +5914,17 @@ const updateOrderCommand: CommandHandler<
|
|
|
5836
5914
|
if (!order)
|
|
5837
5915
|
throw notFound("Sales order not found");
|
|
5838
5916
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
5917
|
+
const currentTotalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
5918
|
+
const nextTotalsMode: SalesAmountsMode = parsed.totalsMode ?? currentTotalsMode;
|
|
5919
|
+
const totalsModeChanged = nextTotalsMode !== currentTotalsMode;
|
|
5920
|
+
const externalHeaderSupplied =
|
|
5921
|
+
isExternalAmountsMode(nextTotalsMode) && hasAnySuppliedHeaderTotal(parsed);
|
|
5922
|
+
if (!totalsModeChanged && !externalHeaderSupplied) {
|
|
5923
|
+
documentEditSchema.parse(rawInput ?? {});
|
|
5924
|
+
}
|
|
5925
|
+
if (isExternalAmountsMode(nextTotalsMode) && (totalsModeChanged || externalHeaderSupplied)) {
|
|
5926
|
+
await requireOrderTotalsForExternalWrite(parsed);
|
|
5927
|
+
}
|
|
5839
5928
|
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
5840
5929
|
const previousStatus = normalizeStatusValue(order.status);
|
|
5841
5930
|
if (parsed.statusEntryId && !isCancelledOrderStatus(previousStatus)) {
|
|
@@ -5849,6 +5938,13 @@ const updateOrderCommand: CommandHandler<
|
|
|
5849
5938
|
}
|
|
5850
5939
|
}
|
|
5851
5940
|
let statusChangeNote: SalesNote | null = null;
|
|
5941
|
+
const suppliedTotals: Partial<SalesDocumentAmounts> | null = isExternalAmountsMode(
|
|
5942
|
+
nextTotalsMode,
|
|
5943
|
+
)
|
|
5944
|
+
? externalHeaderSupplied
|
|
5945
|
+
? buildExternalHeaderTotals(parsed)
|
|
5946
|
+
: readPersistedHeaderTotals(order)
|
|
5947
|
+
: null;
|
|
5852
5948
|
const shouldRecalculateTotals =
|
|
5853
5949
|
parsed.shippingMethodId !== undefined ||
|
|
5854
5950
|
parsed.shippingMethodSnapshot !== undefined ||
|
|
@@ -5856,7 +5952,9 @@ const updateOrderCommand: CommandHandler<
|
|
|
5856
5952
|
parsed.paymentMethodId !== undefined ||
|
|
5857
5953
|
parsed.paymentMethodSnapshot !== undefined ||
|
|
5858
5954
|
parsed.paymentMethodCode !== undefined ||
|
|
5859
|
-
parsed.currencyCode !== undefined
|
|
5955
|
+
parsed.currencyCode !== undefined ||
|
|
5956
|
+
totalsModeChanged ||
|
|
5957
|
+
externalHeaderSupplied;
|
|
5860
5958
|
// Apply the scalar update, totals recalc, and status-change note atomically.
|
|
5861
5959
|
// applyDocumentUpdate/replace*/setRecordCustomFields flush mid-build, so
|
|
5862
5960
|
// without a transaction a later failure would leave a half-updated order
|
|
@@ -5893,10 +5991,17 @@ const updateOrderCommand: CommandHandler<
|
|
|
5893
5991
|
),
|
|
5894
5992
|
]);
|
|
5895
5993
|
const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
|
|
5994
|
+
// Leaving external: the stored tax and gross are the source's figures,
|
|
5995
|
+
// and the computed path honours both verbatim while it re-derives net,
|
|
5996
|
+
// so they must be re-derived too or net + tax ≠ gross (#7925).
|
|
5997
|
+
const leavingExternal =
|
|
5998
|
+
totalsModeChanged && !isExternalAmountsMode(nextTotalsMode);
|
|
5896
5999
|
const calcLines = lineSnapshots.map((line, index) =>
|
|
5897
6000
|
createLineSnapshotFromInput(
|
|
5898
6001
|
{
|
|
5899
6002
|
...line,
|
|
6003
|
+
...(leavingExternal ? { taxAmount: null, totalGrossAmount: null } : {}),
|
|
6004
|
+
amountsMode: nextTotalsMode,
|
|
5900
6005
|
organizationId: order.organizationId,
|
|
5901
6006
|
tenantId: order.tenantId,
|
|
5902
6007
|
orderId: order.id,
|
|
@@ -5931,6 +6036,8 @@ const updateOrderCommand: CommandHandler<
|
|
|
5931
6036
|
adjustments: adjustmentDrafts,
|
|
5932
6037
|
context: calculationContext,
|
|
5933
6038
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
6039
|
+
totalsMode: nextTotalsMode,
|
|
6040
|
+
suppliedTotals,
|
|
5934
6041
|
});
|
|
5935
6042
|
const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
|
|
5936
6043
|
organizationId: order.organizationId,
|
|
@@ -5955,6 +6062,15 @@ const updateOrderCommand: CommandHandler<
|
|
|
5955
6062
|
calculation,
|
|
5956
6063
|
adjustmentInputs,
|
|
5957
6064
|
);
|
|
6065
|
+
if (totalsModeChanged) {
|
|
6066
|
+
applyOrderLineAmountsFromCalculation({
|
|
6067
|
+
em,
|
|
6068
|
+
lines: existingLines,
|
|
6069
|
+
calculation,
|
|
6070
|
+
amountsMode: nextTotalsMode,
|
|
6071
|
+
});
|
|
6072
|
+
order.totalsMode = nextTotalsMode;
|
|
6073
|
+
}
|
|
5958
6074
|
applyOrderTotals(order, calculation.totals, calculation.lines.length);
|
|
5959
6075
|
let eventBus: EventBus | null = null;
|
|
5960
6076
|
try {
|
|
@@ -6306,7 +6422,23 @@ const createOrderCommand: CommandHandler<
|
|
|
6306
6422
|
)
|
|
6307
6423
|
: null;
|
|
6308
6424
|
|
|
6309
|
-
const
|
|
6425
|
+
const totalsMode: SalesAmountsMode = parsed.totalsMode ?? "computed";
|
|
6426
|
+
await assertUniformAmountsMode(totalsMode, normalizedLineInputs);
|
|
6427
|
+
const modedLineInputs = normalizedLineInputs.map((line) => ({
|
|
6428
|
+
...line,
|
|
6429
|
+
amountsMode: totalsMode,
|
|
6430
|
+
}));
|
|
6431
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
6432
|
+
await assertExternalHeaderComplete(parsed);
|
|
6433
|
+
for (const [index, line] of modedLineInputs.entries()) {
|
|
6434
|
+
await assertExternalLineComplete(line, line.lineNumber ?? index + 1);
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
order.totalsMode = totalsMode;
|
|
6438
|
+
const suppliedTotals: Partial<SalesDocumentAmounts> | null = isExternalAmountsMode(totalsMode)
|
|
6439
|
+
? buildExternalHeaderTotals(parsed)
|
|
6440
|
+
: null;
|
|
6441
|
+
const lineSnapshots: SalesLineSnapshot[] = modedLineInputs.map(
|
|
6310
6442
|
(line, index) =>
|
|
6311
6443
|
createLineSnapshotFromInput(line, line.lineNumber ?? index + 1),
|
|
6312
6444
|
);
|
|
@@ -6333,6 +6465,8 @@ const createOrderCommand: CommandHandler<
|
|
|
6333
6465
|
adjustments: adjustmentDrafts,
|
|
6334
6466
|
context: calculationContext,
|
|
6335
6467
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
6468
|
+
totalsMode,
|
|
6469
|
+
suppliedTotals,
|
|
6336
6470
|
});
|
|
6337
6471
|
|
|
6338
6472
|
let eventBus: EventBus | null = null;
|
|
@@ -6349,7 +6483,7 @@ const createOrderCommand: CommandHandler<
|
|
|
6349
6483
|
[
|
|
6350
6484
|
async () => {
|
|
6351
6485
|
em.persist(order);
|
|
6352
|
-
await replaceOrderLines(em, order, calculation,
|
|
6486
|
+
await replaceOrderLines(em, order, calculation, modedLineInputs);
|
|
6353
6487
|
await replaceOrderAdjustments(
|
|
6354
6488
|
em,
|
|
6355
6489
|
order,
|
|
@@ -7188,13 +7322,17 @@ const convertQuoteToOrderCommand: CommandHandler<
|
|
|
7188
7322
|
},
|
|
7189
7323
|
};
|
|
7190
7324
|
|
|
7325
|
+
// Nested rather than spread flat: a line payload already has a `totalNetAmount`
|
|
7326
|
+
// that means the line's, not the document's.
|
|
7191
7327
|
const orderLineUpsertSchema = orderLineCreateSchema.extend({
|
|
7192
7328
|
id: z.string().uuid().optional(),
|
|
7329
|
+
orderTotals: orderHeaderTotalsSchema.optional(),
|
|
7193
7330
|
});
|
|
7194
7331
|
|
|
7195
7332
|
const orderLineDeleteSchema = z.object({
|
|
7196
7333
|
id: z.string().uuid(),
|
|
7197
7334
|
orderId: z.string().uuid(),
|
|
7335
|
+
orderTotals: orderHeaderTotalsSchema.optional(),
|
|
7198
7336
|
});
|
|
7199
7337
|
|
|
7200
7338
|
const quoteLineUpsertSchema = quoteLineCreateSchema.extend({
|
|
@@ -7511,6 +7649,14 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7511
7649
|
const existingSnapshot = parsed.id
|
|
7512
7650
|
? (lineSnapshots.find((line) => line.id === parsed.id) ?? null)
|
|
7513
7651
|
: null;
|
|
7652
|
+
const totalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
7653
|
+
await assertUniformAmountsMode(totalsMode, [parsed]);
|
|
7654
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
7655
|
+
await requireOrderTotalsForExternalWrite(parsed.orderTotals);
|
|
7656
|
+
}
|
|
7657
|
+
const suppliedTotals: Partial<SalesDocumentAmounts> | null = isExternalAmountsMode(totalsMode)
|
|
7658
|
+
? buildExternalHeaderTotals(parsed.orderTotals)
|
|
7659
|
+
: null;
|
|
7514
7660
|
await assertOrderAcceptsNewLine(order, existingSnapshot);
|
|
7515
7661
|
await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
|
|
7516
7662
|
const priceMode =
|
|
@@ -7524,6 +7670,7 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7524
7670
|
let unitPriceGross =
|
|
7525
7671
|
parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
|
|
7526
7672
|
let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
|
|
7673
|
+
const unitPriceSupplied = unitPriceNet !== null || unitPriceGross !== null;
|
|
7527
7674
|
if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
|
|
7528
7675
|
let taxService: TaxCalculationService | null = null;
|
|
7529
7676
|
try {
|
|
@@ -7659,11 +7806,25 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7659
7806
|
parsed.customFields && typeof parsed.customFields === "object"
|
|
7660
7807
|
? cloneJson(parsed.customFields)
|
|
7661
7808
|
: ((existingSnapshot as any)?.customFields ?? null),
|
|
7809
|
+
amountsMode: totalsMode,
|
|
7662
7810
|
};
|
|
7663
7811
|
Object.assign(
|
|
7664
7812
|
updatedSnapshot,
|
|
7665
7813
|
resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot),
|
|
7666
7814
|
);
|
|
7815
|
+
// After the line above, never before: tax and gross only exist once it runs,
|
|
7816
|
+
// and it nulls them when pricing changed, which an external line must restate.
|
|
7817
|
+
// The unit price is checked as resolved, not as defaulted to zero above,
|
|
7818
|
+
// and a price the tax service derived from no source price counts as absent.
|
|
7819
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
7820
|
+
await assertExternalLineComplete(
|
|
7821
|
+
{
|
|
7822
|
+
...(updatedSnapshot as Record<string, unknown>),
|
|
7823
|
+
unitPriceNet: unitPriceSupplied ? unitPriceNet : null,
|
|
7824
|
+
},
|
|
7825
|
+
updatedSnapshot.lineNumber ?? 1,
|
|
7826
|
+
);
|
|
7827
|
+
}
|
|
7667
7828
|
(updatedSnapshot as any).statusEntryId = statusEntryId;
|
|
7668
7829
|
(updatedSnapshot as any).catalogSnapshot =
|
|
7669
7830
|
parsed.catalogSnapshot ??
|
|
@@ -7716,6 +7877,8 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7716
7877
|
adjustments: adjustmentDrafts,
|
|
7717
7878
|
context: calculationContext,
|
|
7718
7879
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
7880
|
+
totalsMode,
|
|
7881
|
+
suppliedTotals,
|
|
7719
7882
|
});
|
|
7720
7883
|
let eventBus: EventBus | null = null;
|
|
7721
7884
|
try {
|
|
@@ -7858,6 +8021,13 @@ const orderLineDeleteCommand: CommandHandler<
|
|
|
7858
8021
|
),
|
|
7859
8022
|
});
|
|
7860
8023
|
}
|
|
8024
|
+
const totalsMode: SalesAmountsMode = order.totalsMode ?? "computed";
|
|
8025
|
+
if (isExternalAmountsMode(totalsMode)) {
|
|
8026
|
+
await requireOrderTotalsForExternalWrite(parsed.orderTotals);
|
|
8027
|
+
}
|
|
8028
|
+
const suppliedTotals: Partial<SalesDocumentAmounts> | null = isExternalAmountsMode(totalsMode)
|
|
8029
|
+
? buildExternalHeaderTotals(parsed.orderTotals)
|
|
8030
|
+
: null;
|
|
7861
8031
|
const sourceInputs = filtered.map((line, index) => ({
|
|
7862
8032
|
...mapOrderLineEntityToSnapshot(line),
|
|
7863
8033
|
statusEntryId: line.statusEntryId ?? null,
|
|
@@ -7895,6 +8065,8 @@ const orderLineDeleteCommand: CommandHandler<
|
|
|
7895
8065
|
adjustments: adjustmentDrafts,
|
|
7896
8066
|
context: calculationContext,
|
|
7897
8067
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
8068
|
+
totalsMode,
|
|
8069
|
+
suppliedTotals,
|
|
7898
8070
|
});
|
|
7899
8071
|
let eventBus: EventBus | null = null;
|
|
7900
8072
|
try {
|
|
@@ -7986,6 +8158,7 @@ const quoteLineUpsertCommand: CommandHandler<
|
|
|
7986
8158
|
async execute(input, ctx) {
|
|
7987
8159
|
const rawBody = (input?.body as Record<string, unknown> | undefined) ?? {};
|
|
7988
8160
|
const parsed = quoteLineUpsertSchema.parse(rawBody);
|
|
8161
|
+
await assertAmountsModeUnsupportedOnQuote([parsed]);
|
|
7989
8162
|
const em = (ctx.container.resolve("em") as EntityManager).fork();
|
|
7990
8163
|
const quote = await findOneWithDecryption(em, SalesQuote, {
|
|
7991
8164
|
id: parsed.quoteId,
|
|
@@ -8460,6 +8633,7 @@ const orderAdjustmentUpsertCommand: CommandHandler<
|
|
|
8460
8633
|
throw notFound("Sales order not found");
|
|
8461
8634
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
8462
8635
|
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
8636
|
+
if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
|
|
8463
8637
|
if (parsed.scope === "line") {
|
|
8464
8638
|
throw new CrudHttpError(400, {
|
|
8465
8639
|
error: "Line-scoped adjustments are not supported yet.",
|
|
@@ -8752,6 +8926,7 @@ const orderAdjustmentDeleteCommand: CommandHandler<
|
|
|
8752
8926
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
8753
8927
|
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
8754
8928
|
|
|
8929
|
+
if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
|
|
8755
8930
|
const [existingLines, adjustments] = await Promise.all([
|
|
8756
8931
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
8757
8932
|
em.find(
|
|
@@ -13,6 +13,8 @@ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/
|
|
|
13
13
|
import { SalesDocumentNumberGenerator } from '../services/salesDocumentNumberGenerator'
|
|
14
14
|
import type { SalesCalculationService } from '../services/salesCalculationService'
|
|
15
15
|
import type { SalesAdjustmentDraft, SalesLineSnapshot, SalesDocumentCalculationResult } from '../lib/types'
|
|
16
|
+
import { isExternalAmountsMode } from '../lib/calculations'
|
|
17
|
+
import { readPersistedHeaderTotals } from '../lib/externalAmounts'
|
|
16
18
|
import { cloneJson, deriveLineNetFromGross, ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload, toNumericString, enforceSalesDocumentOptimisticLock, SALES_RESOURCE_KIND_ORDER, SALES_RESOURCE_KIND_RETURN } from './shared'
|
|
17
19
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
18
20
|
import { SalesOrder, SalesOrderAdjustment, SalesOrderLine, SalesReturn, SalesReturnLine } from '../data/entities'
|
|
@@ -135,6 +137,24 @@ function applyOrderTotals(order: SalesOrder, totals: SalesDocumentCalculationRes
|
|
|
135
137
|
order.lineItemCount = lineCount
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
// Deliberate, not an omission: an external order's header belongs to the source
|
|
141
|
+
// system, which issues its own credit document. The return itself is still recorded.
|
|
142
|
+
function applyOrderTotalsUnlessExternal(
|
|
143
|
+
order: SalesOrder,
|
|
144
|
+
calculation: SalesDocumentCalculationResult,
|
|
145
|
+
): void {
|
|
146
|
+
if (isExternalAmountsMode(order.totalsMode ?? 'computed')) return
|
|
147
|
+
applyOrderTotals(order, calculation.totals, calculation.lines.length)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveOrderCalculationMode(order: SalesOrder) {
|
|
151
|
+
const totalsMode = order.totalsMode ?? 'computed'
|
|
152
|
+
return {
|
|
153
|
+
totalsMode,
|
|
154
|
+
suppliedTotals: isExternalAmountsMode(totalsMode) ? readPersistedHeaderTotals(order) : null,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
138
158
|
function mapOrderAdjustmentToDraft(adjustment: SalesOrderAdjustment): SalesAdjustmentDraft {
|
|
139
159
|
return {
|
|
140
160
|
id: adjustment.id,
|
|
@@ -206,6 +226,7 @@ export async function recalculateOrderTotalsForDisplay(
|
|
|
206
226
|
adjustments: adjustmentDrafts,
|
|
207
227
|
context: buildCalculationContext(order),
|
|
208
228
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
229
|
+
...resolveOrderCalculationMode(order),
|
|
209
230
|
})
|
|
210
231
|
return calculation.totals
|
|
211
232
|
}
|
|
@@ -391,8 +412,9 @@ async function reverseReturnEffects(
|
|
|
391
412
|
adjustments: adjustmentDrafts,
|
|
392
413
|
context: buildCalculationContext(order),
|
|
393
414
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
415
|
+
...resolveOrderCalculationMode(order),
|
|
394
416
|
})
|
|
395
|
-
|
|
417
|
+
applyOrderTotalsUnlessExternal(order, calculation)
|
|
396
418
|
order.updatedAt = new Date()
|
|
397
419
|
em.persist(order)
|
|
398
420
|
},
|
|
@@ -577,8 +599,9 @@ async function restoreReturnEffects(
|
|
|
577
599
|
adjustments: adjustmentDrafts,
|
|
578
600
|
context: buildCalculationContext(order),
|
|
579
601
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
602
|
+
...resolveOrderCalculationMode(order),
|
|
580
603
|
})
|
|
581
|
-
|
|
604
|
+
applyOrderTotalsUnlessExternal(order, calculation)
|
|
582
605
|
order.updatedAt = new Date()
|
|
583
606
|
em.persist(order)
|
|
584
607
|
},
|
|
@@ -763,8 +786,9 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
|
|
|
763
786
|
adjustments: adjustmentDrafts,
|
|
764
787
|
context: buildCalculationContext(order),
|
|
765
788
|
existingTotals: resolveExistingPaymentTotals(order),
|
|
789
|
+
...resolveOrderCalculationMode(order),
|
|
766
790
|
})
|
|
767
|
-
|
|
791
|
+
applyOrderTotalsUnlessExternal(order, calculation)
|
|
768
792
|
order.updatedAt = new Date()
|
|
769
793
|
tx.persist(order)
|
|
770
794
|
|