@open-mercato/core 0.10.1-develop.7723.1.3b2ea82ea7 → 0.10.1-develop.7726.1.a447cc5525

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/sales_order/index.js +2 -0
  3. package/dist/generated/entities/sales_order/index.js.map +2 -2
  4. package/dist/generated/entities/sales_order_line/index.js +2 -0
  5. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/planner/api/availability-date-specific.js +2 -7
  9. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  10. package/dist/modules/planner/commands/availability-date-specific.js +14 -31
  11. package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
  12. package/dist/modules/planner/commands/availability-weekly.js +8 -22
  13. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  14. package/dist/modules/planner/lib/availabilityMerge.js +10 -18
  15. package/dist/modules/planner/lib/availabilityMerge.js.map +2 -2
  16. package/dist/modules/planner/lib/availabilityTimezone.js +161 -0
  17. package/dist/modules/planner/lib/availabilityTimezone.js.map +7 -0
  18. package/dist/modules/sales/api/documents/factory.js +3 -0
  19. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  20. package/dist/modules/sales/api/order-lines/route.js +2 -1
  21. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  22. package/dist/modules/sales/backend/sales/documents/[id]/page.js +83 -6
  23. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  24. package/dist/modules/sales/commands/documents.js +141 -18
  25. package/dist/modules/sales/commands/documents.js.map +2 -2
  26. package/dist/modules/sales/commands/returns.js +24 -7
  27. package/dist/modules/sales/commands/returns.js.map +2 -2
  28. package/dist/modules/sales/components/documents/AdjustmentsSection.js +17 -11
  29. package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
  30. package/dist/modules/sales/components/documents/DocumentTotals.js +21 -3
  31. package/dist/modules/sales/components/documents/DocumentTotals.js.map +2 -2
  32. package/dist/modules/sales/components/documents/ItemsSection.js +23 -12
  33. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  34. package/dist/modules/sales/data/entities.js +10 -0
  35. package/dist/modules/sales/data/entities.js.map +2 -2
  36. package/dist/modules/sales/data/validators.js +9 -1
  37. package/dist/modules/sales/data/validators.js.map +2 -2
  38. package/dist/modules/sales/lib/calculations.js +85 -6
  39. package/dist/modules/sales/lib/calculations.js.map +2 -2
  40. package/dist/modules/sales/lib/externalAmounts.js +143 -0
  41. package/dist/modules/sales/lib/externalAmounts.js.map +7 -0
  42. package/dist/modules/sales/lib/lineSnapshots.js +6 -1
  43. package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
  44. package/dist/modules/sales/lib/makeSalesLineRoute.js +8 -2
  45. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  46. package/dist/modules/sales/lib/providers/totals.js +3 -2
  47. package/dist/modules/sales/lib/providers/totals.js.map +2 -2
  48. package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js +19 -0
  49. package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js.map +7 -0
  50. package/generated/entities/sales_order/index.ts +1 -0
  51. package/generated/entities/sales_order_line/index.ts +1 -0
  52. package/generated/entity-fields-registry.ts +2 -0
  53. package/package.json +8 -7
  54. package/src/modules/planner/api/availability-date-specific.ts +5 -10
  55. package/src/modules/planner/commands/availability-date-specific.ts +14 -33
  56. package/src/modules/planner/commands/availability-weekly.ts +15 -24
  57. package/src/modules/planner/lib/availabilityMerge.ts +13 -19
  58. package/src/modules/planner/lib/availabilityTimezone.ts +242 -0
  59. package/src/modules/sales/api/documents/factory.ts +3 -0
  60. package/src/modules/sales/api/order-lines/route.ts +2 -1
  61. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +96 -0
  62. package/src/modules/sales/commands/documents.ts +219 -44
  63. package/src/modules/sales/commands/returns.ts +27 -3
  64. package/src/modules/sales/components/documents/AdjustmentsSection.tsx +34 -25
  65. package/src/modules/sales/components/documents/DocumentTotals.tsx +26 -2
  66. package/src/modules/sales/components/documents/ItemsSection.tsx +27 -12
  67. package/src/modules/sales/data/entities.ts +14 -0
  68. package/src/modules/sales/data/validators.ts +10 -0
  69. package/src/modules/sales/i18n/de.json +12 -0
  70. package/src/modules/sales/i18n/en.json +12 -0
  71. package/src/modules/sales/i18n/es.json +12 -0
  72. package/src/modules/sales/i18n/ko.json +12 -0
  73. package/src/modules/sales/i18n/pl.json +12 -0
  74. package/src/modules/sales/lib/calculations.ts +99 -4
  75. package/src/modules/sales/lib/externalAmounts.ts +174 -0
  76. package/src/modules/sales/lib/lineSnapshots.ts +6 -1
  77. package/src/modules/sales/lib/makeSalesLineRoute.ts +8 -0
  78. package/src/modules/sales/lib/providers/totals.ts +5 -2
  79. package/src/modules/sales/lib/types.ts +19 -2
  80. package/src/modules/sales/migrations/.snapshot-open-mercato.json +33 -1
  81. package/src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts +25 -0
@@ -74,7 +74,9 @@ import {
74
74
  orderCreateSchema,
75
75
  ORDER_PAYMENT_LEDGER_WARNING_CODE,
76
76
  resolveSuppliedOrderPaymentLedgerFields,
77
+ amountsModeSchema,
77
78
  orderLineCreateSchema,
79
+ orderHeaderTotalsSchema,
78
80
  orderAdjustmentCreateSchema,
79
81
  invoiceCreateSchema,
80
82
  invoiceUpdateSchema,
@@ -109,6 +111,18 @@ import {
109
111
  resolveUpsertTotalsOrigin,
110
112
  resolveUpsertCalculatedAmounts
111
113
  } from "../lib/lineSnapshots.js";
114
+ import {
115
+ assertAmountsModeUnsupportedOnQuote,
116
+ assertExternalHeaderComplete,
117
+ assertExternalLineComplete,
118
+ assertUniformAmountsMode,
119
+ buildExternalHeaderTotals,
120
+ hasAnySuppliedHeaderTotal,
121
+ readPersistedHeaderTotals,
122
+ refuseOnExternalOrder,
123
+ requireOrderTotalsForExternalWrite
124
+ } from "../lib/externalAmounts.js";
125
+ import { isExternalAmountsMode } from "../lib/calculations.js";
112
126
  import { loadShippedQuantityByLine } from "../lib/shipments/snapshots.js";
113
127
  import { resolveDictionaryEntryValue, resolveCachedDictionaryEntryValue } from "../lib/dictionaries.js";
114
128
  import { assertOrderCanBeCancelled, isCancelledOrderStatus } from "../lib/orderCancellationGuard.js";
@@ -151,7 +165,7 @@ const dateOnlySchema = z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/, { message:
151
165
  });
152
166
  const addressSnapshotSchema = z.record(z.string(), z.unknown()).nullable().optional();
153
167
  const exchangeRateSchema = z.coerce.number().min(0);
154
- const documentUpdateSchema = z.object({
168
+ const documentUpdateFieldsSchema = z.object({
155
169
  id: z.string().uuid(),
156
170
  customerEntityId: z.string().uuid().nullable().optional(),
157
171
  customerContactId: z.string().uuid().nullable().optional(),
@@ -191,11 +205,20 @@ const documentUpdateSchema = z.object({
191
205
  paymentMethodSnapshot: z.record(z.string(), z.unknown()).nullable().optional(),
192
206
  tags: z.array(z.string().uuid()).optional(),
193
207
  customFields: z.record(z.string(), z.unknown()).optional(),
194
- customFieldSetId: z.string().uuid().nullable().optional()
195
- }).refine(
196
- (input) => typeof input.currencyCode === "string" || input.placedAt !== void 0 || input.expectedDeliveryAt !== void 0 || input.channelId !== void 0 || input.statusEntryId !== void 0 || input.exchangeRate !== void 0 || input.paymentStatusEntryId !== void 0 || input.fulfillmentStatusEntryId !== void 0 || input.shippingAddressId !== void 0 || input.billingAddressId !== void 0 || input.customerEntityId !== void 0 || input.customerContactId !== void 0 || input.customerSnapshot !== void 0 || input.metadata !== void 0 || input.customerReference !== void 0 || input.externalReference !== void 0 || input.comment !== void 0 || input.comments !== void 0 || input.internalNotes !== void 0 || input.orderNumber !== void 0 || input.quoteNumber !== void 0 || input.shippingAddressSnapshot !== void 0 || input.billingAddressSnapshot !== void 0 || input.shippingMethodId !== void 0 || input.shippingMethodCode !== void 0 || input.shippingMethodSnapshot !== void 0 || input.paymentMethodId !== void 0 || input.paymentMethodCode !== void 0 || input.paymentMethodSnapshot !== void 0 || input.tags !== void 0 || input.customFields !== void 0 || input.customFieldSetId !== void 0,
208
+ customFieldSetId: z.string().uuid().nullable().optional(),
209
+ totalsMode: amountsModeSchema.optional(),
210
+ ...orderHeaderTotalsSchema.shape
211
+ });
212
+ function carriesDocumentEdit(input) {
213
+ return typeof input.currencyCode === "string" || input.placedAt !== void 0 || input.expectedDeliveryAt !== void 0 || input.channelId !== void 0 || input.statusEntryId !== void 0 || input.exchangeRate !== void 0 || input.paymentStatusEntryId !== void 0 || input.fulfillmentStatusEntryId !== void 0 || input.shippingAddressId !== void 0 || input.billingAddressId !== void 0 || input.customerEntityId !== void 0 || input.customerContactId !== void 0 || input.customerSnapshot !== void 0 || input.metadata !== void 0 || input.customerReference !== void 0 || input.externalReference !== void 0 || input.comment !== void 0 || input.comments !== void 0 || input.internalNotes !== void 0 || input.orderNumber !== void 0 || input.quoteNumber !== void 0 || input.shippingAddressSnapshot !== void 0 || input.billingAddressSnapshot !== void 0 || input.shippingMethodId !== void 0 || input.shippingMethodCode !== void 0 || input.shippingMethodSnapshot !== void 0 || input.paymentMethodId !== void 0 || input.paymentMethodCode !== void 0 || input.paymentMethodSnapshot !== void 0 || input.tags !== void 0 || input.customFields !== void 0 || input.customFieldSetId !== void 0;
214
+ }
215
+ const documentUpdateSchema = documentUpdateFieldsSchema.refine(
216
+ (input) => carriesDocumentEdit(input) || input.totalsMode !== void 0 || hasAnySuppliedHeaderTotal(input),
197
217
  { message: "update_payload_empty" }
198
218
  );
219
+ const documentEditSchema = documentUpdateFieldsSchema.refine(carriesDocumentEdit, {
220
+ message: "update_payload_empty"
221
+ });
199
222
  function cloneJson(value) {
200
223
  if (value === null || value === void 0) return value;
201
224
  return JSON.parse(JSON.stringify(value));
@@ -1230,7 +1253,8 @@ async function loadOrderSnapshot(em, id) {
1230
1253
  refundedTotalAmount: order.refundedTotalAmount,
1231
1254
  outstandingAmount: order.outstandingAmount,
1232
1255
  totalsSnapshot: order.totalsSnapshot ? cloneJson(order.totalsSnapshot) : null,
1233
- lineItemCount: order.lineItemCount
1256
+ lineItemCount: order.lineItemCount,
1257
+ totalsMode: order.totalsMode ?? "computed"
1234
1258
  },
1235
1259
  lines: lines.map((line) => ({
1236
1260
  id: line.id,
@@ -1262,6 +1286,7 @@ async function loadOrderSnapshot(em, id) {
1262
1286
  taxAmount: line.taxAmount,
1263
1287
  totalNetAmount: line.totalNetAmount,
1264
1288
  totalGrossAmount: line.totalGrossAmount,
1289
+ amountsMode: line.amountsMode ?? "computed",
1265
1290
  configuration: line.configuration ? cloneJson(line.configuration) : null,
1266
1291
  promotionCode: line.promotionCode ?? null,
1267
1292
  promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,
@@ -1991,7 +2016,8 @@ function createLineSnapshotFromInput(line, lineNumber) {
1991
2016
  promotionCode: line.promotionCode ?? null,
1992
2017
  metadata: line.metadata ? cloneJson(line.metadata) : null,
1993
2018
  customFieldSetId: "customFieldSetId" in line ? line.customFieldSetId ?? null : null,
1994
- customFields: "customFields" in line && line.customFields ? cloneJson(line.customFields) : null
2019
+ customFields: "customFields" in line && line.customFields ? cloneJson(line.customFields) : null,
2020
+ amountsMode: line.amountsMode ?? null
1995
2021
  };
1996
2022
  }
1997
2023
  function createAdjustmentDraftFromInput(adjustment) {
@@ -2019,7 +2045,8 @@ function createAdjustmentDraftFromInput(adjustment) {
2019
2045
  }
2020
2046
  function convertLineCalculationToEntityInput(lineResult, sourceLine, document, index) {
2021
2047
  const line = lineResult.line;
2022
- return reconcileLinePersistedTotals({
2048
+ const persistTotals = isExternalAmountsMode(line.amountsMode) ? (payload) => payload : reconcileLinePersistedTotals;
2049
+ return persistTotals({
2023
2050
  lineNumber: line.lineNumber ?? index + 1,
2024
2051
  kind: line.kind ?? "product",
2025
2052
  statusEntryId: sourceLine.statusEntryId ?? null,
@@ -2047,6 +2074,8 @@ function convertLineCalculationToEntityInput(lineResult, sourceLine, document, i
2047
2074
  taxAmount: toNumericString(lineResult.taxAmount) ?? "0",
2048
2075
  totalNetAmount: toNumericString(lineResult.netAmount) ?? "0",
2049
2076
  totalGrossAmount: toNumericString(lineResult.grossAmount) ?? "0",
2077
+ // Conditional: this converter also serves quote lines, which have no column.
2078
+ ...line.amountsMode ? { amountsMode: line.amountsMode } : {},
2050
2079
  configuration: line.configuration ? cloneJson(line.configuration) : null,
2051
2080
  promotionCode: line.promotionCode ?? null,
2052
2081
  promotionSnapshot: sourceLine.promotionSnapshot ? cloneJson(sourceLine.promotionSnapshot) : null,
@@ -2426,6 +2455,19 @@ function applyOrderTotals(order, totals, lineCount) {
2426
2455
  order.totalsSnapshot = cloneJson(totals);
2427
2456
  order.lineItemCount = lineCount;
2428
2457
  }
2458
+ function applyOrderLineAmountsFromCalculation(params) {
2459
+ const { em, lines, calculation, amountsMode } = params;
2460
+ lines.forEach((line, index) => {
2461
+ const result = calculation.lines[index];
2462
+ if (!result) return;
2463
+ line.amountsMode = amountsMode;
2464
+ line.discountAmount = toNumericString(result.discountAmount) ?? "0";
2465
+ line.taxAmount = toNumericString(result.taxAmount) ?? "0";
2466
+ line.totalNetAmount = toNumericString(result.netAmount) ?? "0";
2467
+ line.totalGrossAmount = toNumericString(result.grossAmount) ?? "0";
2468
+ em.persist(line);
2469
+ });
2470
+ }
2429
2471
  function normalizePaymentTotal(value) {
2430
2472
  if (typeof value === "number" && Number.isFinite(value))
2431
2473
  return Math.max(value, 0);
@@ -2679,6 +2721,7 @@ function applyOrderSnapshot(order, snapshot) {
2679
2721
  order.outstandingAmount = snapshot.outstandingAmount;
2680
2722
  order.totalsSnapshot = snapshot.totalsSnapshot ? cloneJson(snapshot.totalsSnapshot) : null;
2681
2723
  order.lineItemCount = snapshot.lineItemCount;
2724
+ order.totalsMode = snapshot.totalsMode ?? "computed";
2682
2725
  }
2683
2726
  async function assertQuoteGraphUndoCurrent(em, quoteId, expected) {
2684
2727
  const current = await loadQuoteSnapshot(em, quoteId);
@@ -3202,6 +3245,7 @@ async function restoreOrderGraph(em, snapshot, verified = null) {
3202
3245
  outstandingAmount: snapshot.order.outstandingAmount,
3203
3246
  totalsSnapshot: snapshot.order.totalsSnapshot ? cloneJson(snapshot.order.totalsSnapshot) : null,
3204
3247
  lineItemCount: snapshot.order.lineItemCount,
3248
+ totalsMode: snapshot.order.totalsMode ?? "computed",
3205
3249
  createdAt: /* @__PURE__ */ new Date(),
3206
3250
  updatedAt: /* @__PURE__ */ new Date()
3207
3251
  });
@@ -3302,10 +3346,12 @@ async function restoreOrderGraph(em, snapshot, verified = null) {
3302
3346
  discountPercent: line.discountPercent,
3303
3347
  taxRate: line.taxRate,
3304
3348
  taxAmount: line.taxAmount,
3305
- totalNetAmount: toNumericString(
3349
+ // Not repaired from gross: an external line's net is the caller's figure.
3350
+ totalNetAmount: isExternalAmountsMode(line.amountsMode) ? line.totalNetAmount : toNumericString(
3306
3351
  deriveLineNetFromGross(line.totalNetAmount, line.totalGrossAmount, line.taxRate)
3307
3352
  ),
3308
3353
  totalGrossAmount: line.totalGrossAmount,
3354
+ amountsMode: line.amountsMode ?? "computed",
3309
3355
  configuration: line.configuration ? cloneJson(line.configuration) : null,
3310
3356
  promotionCode: line.promotionCode ?? null,
3311
3357
  promotionSnapshot: line.promotionSnapshot ? cloneJson(line.promotionSnapshot) : null,
@@ -3549,6 +3595,7 @@ const createQuoteCommand = {
3549
3595
  createdAt: /* @__PURE__ */ new Date(),
3550
3596
  updatedAt: /* @__PURE__ */ new Date()
3551
3597
  });
3598
+ await assertAmountsModeUnsupportedOnQuote(parsed.lines ?? []);
3552
3599
  const lineInputs = (parsed.lines ?? []).map(
3553
3600
  (line, index) => quoteLineCreateSchema.parse({
3554
3601
  ...line,
@@ -3894,6 +3941,8 @@ const updateQuoteCommand = {
3894
3941
  },
3895
3942
  async execute(rawInput, ctx) {
3896
3943
  const parsed = documentUpdateSchema.parse(rawInput ?? {});
3944
+ await assertAmountsModeUnsupportedOnQuote([parsed]);
3945
+ documentEditSchema.parse(rawInput ?? {});
3897
3946
  const em = ctx.container.resolve("em").fork();
3898
3947
  const quote = await findOneWithDecryption(em, SalesQuote, {
3899
3948
  id: parsed.id,
@@ -4151,6 +4200,16 @@ const updateOrderCommand = {
4151
4200
  if (!order)
4152
4201
  throw notFound("Sales order not found");
4153
4202
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
4203
+ const currentTotalsMode = order.totalsMode ?? "computed";
4204
+ const nextTotalsMode = parsed.totalsMode ?? currentTotalsMode;
4205
+ const totalsModeChanged = nextTotalsMode !== currentTotalsMode;
4206
+ const externalHeaderSupplied = isExternalAmountsMode(nextTotalsMode) && hasAnySuppliedHeaderTotal(parsed);
4207
+ if (!totalsModeChanged && !externalHeaderSupplied) {
4208
+ documentEditSchema.parse(rawInput ?? {});
4209
+ }
4210
+ if (isExternalAmountsMode(nextTotalsMode) && (totalsModeChanged || externalHeaderSupplied)) {
4211
+ await requireOrderTotalsForExternalWrite(parsed);
4212
+ }
4154
4213
  await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
4155
4214
  const previousStatus = normalizeStatusValue(order.status);
4156
4215
  if (parsed.statusEntryId && !isCancelledOrderStatus(previousStatus)) {
@@ -4164,7 +4223,10 @@ const updateOrderCommand = {
4164
4223
  }
4165
4224
  }
4166
4225
  let statusChangeNote = null;
4167
- const shouldRecalculateTotals = parsed.shippingMethodId !== void 0 || parsed.shippingMethodSnapshot !== void 0 || parsed.shippingMethodCode !== void 0 || parsed.paymentMethodId !== void 0 || parsed.paymentMethodSnapshot !== void 0 || parsed.paymentMethodCode !== void 0 || parsed.currencyCode !== void 0;
4226
+ const suppliedTotals = isExternalAmountsMode(
4227
+ nextTotalsMode
4228
+ ) ? externalHeaderSupplied ? buildExternalHeaderTotals(parsed) : readPersistedHeaderTotals(order) : null;
4229
+ const shouldRecalculateTotals = parsed.shippingMethodId !== void 0 || parsed.shippingMethodSnapshot !== void 0 || parsed.shippingMethodCode !== void 0 || parsed.paymentMethodId !== void 0 || parsed.paymentMethodSnapshot !== void 0 || parsed.paymentMethodCode !== void 0 || parsed.currencyCode !== void 0 || totalsModeChanged || externalHeaderSupplied;
4168
4230
  await withAtomicFlush(
4169
4231
  em,
4170
4232
  [
@@ -4197,10 +4259,13 @@ const updateOrderCommand = {
4197
4259
  )
4198
4260
  ]);
4199
4261
  const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
4262
+ const leavingExternal = totalsModeChanged && !isExternalAmountsMode(nextTotalsMode);
4200
4263
  const calcLines = lineSnapshots.map(
4201
4264
  (line, index) => createLineSnapshotFromInput(
4202
4265
  {
4203
4266
  ...line,
4267
+ ...leavingExternal ? { taxAmount: null, totalGrossAmount: null } : {},
4268
+ amountsMode: nextTotalsMode,
4204
4269
  organizationId: order.organizationId,
4205
4270
  tenantId: order.tenantId,
4206
4271
  orderId: order.id,
@@ -4232,7 +4297,9 @@ const updateOrderCommand = {
4232
4297
  lines: calcLines,
4233
4298
  adjustments: adjustmentDrafts,
4234
4299
  context: calculationContext,
4235
- existingTotals: resolveExistingPaymentTotals(order)
4300
+ existingTotals: resolveExistingPaymentTotals(order),
4301
+ totalsMode: nextTotalsMode,
4302
+ suppliedTotals
4236
4303
  });
4237
4304
  const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
4238
4305
  organizationId: order.organizationId,
@@ -4257,6 +4324,15 @@ const updateOrderCommand = {
4257
4324
  calculation,
4258
4325
  adjustmentInputs
4259
4326
  );
4327
+ if (totalsModeChanged) {
4328
+ applyOrderLineAmountsFromCalculation({
4329
+ em,
4330
+ lines: existingLines,
4331
+ calculation,
4332
+ amountsMode: nextTotalsMode
4333
+ });
4334
+ order.totalsMode = nextTotalsMode;
4335
+ }
4260
4336
  applyOrderTotals(order, calculation.totals, calculation.lines.length);
4261
4337
  let eventBus = null;
4262
4338
  try {
@@ -4545,7 +4621,21 @@ const createOrderCommand = {
4545
4621
  orderId: order.id
4546
4622
  })
4547
4623
  ) : null;
4548
- const lineSnapshots = normalizedLineInputs.map(
4624
+ const totalsMode = parsed.totalsMode ?? "computed";
4625
+ await assertUniformAmountsMode(totalsMode, normalizedLineInputs);
4626
+ const modedLineInputs = normalizedLineInputs.map((line) => ({
4627
+ ...line,
4628
+ amountsMode: totalsMode
4629
+ }));
4630
+ if (isExternalAmountsMode(totalsMode)) {
4631
+ await assertExternalHeaderComplete(parsed);
4632
+ for (const [index, line] of modedLineInputs.entries()) {
4633
+ await assertExternalLineComplete(line, line.lineNumber ?? index + 1);
4634
+ }
4635
+ }
4636
+ order.totalsMode = totalsMode;
4637
+ const suppliedTotals = isExternalAmountsMode(totalsMode) ? buildExternalHeaderTotals(parsed) : null;
4638
+ const lineSnapshots = modedLineInputs.map(
4549
4639
  (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
4550
4640
  );
4551
4641
  const adjustmentDrafts = adjustmentInputs ? adjustmentInputs.map((adj) => createAdjustmentDraftFromInput(adj)) : [];
@@ -4566,7 +4656,9 @@ const createOrderCommand = {
4566
4656
  lines: lineSnapshots,
4567
4657
  adjustments: adjustmentDrafts,
4568
4658
  context: calculationContext,
4569
- existingTotals: resolveExistingPaymentTotals(order)
4659
+ existingTotals: resolveExistingPaymentTotals(order),
4660
+ totalsMode,
4661
+ suppliedTotals
4570
4662
  });
4571
4663
  let eventBus = null;
4572
4664
  try {
@@ -4579,7 +4671,7 @@ const createOrderCommand = {
4579
4671
  [
4580
4672
  async () => {
4581
4673
  em.persist(order);
4582
- await replaceOrderLines(em, order, calculation, normalizedLineInputs);
4674
+ await replaceOrderLines(em, order, calculation, modedLineInputs);
4583
4675
  await replaceOrderAdjustments(
4584
4676
  em,
4585
4677
  order,
@@ -5315,11 +5407,13 @@ const convertQuoteToOrderCommand = {
5315
5407
  }
5316
5408
  };
5317
5409
  const orderLineUpsertSchema = orderLineCreateSchema.extend({
5318
- id: z.string().uuid().optional()
5410
+ id: z.string().uuid().optional(),
5411
+ orderTotals: orderHeaderTotalsSchema.optional()
5319
5412
  });
5320
5413
  const orderLineDeleteSchema = z.object({
5321
5414
  id: z.string().uuid(),
5322
- orderId: z.string().uuid()
5415
+ orderId: z.string().uuid(),
5416
+ orderTotals: orderHeaderTotalsSchema.optional()
5323
5417
  });
5324
5418
  const quoteLineUpsertSchema = quoteLineCreateSchema.extend({
5325
5419
  id: z.string().uuid().optional()
@@ -5517,12 +5611,19 @@ const orderLineUpsertCommand = {
5517
5611
  ]);
5518
5612
  const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
5519
5613
  const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
5614
+ const totalsMode = order.totalsMode ?? "computed";
5615
+ await assertUniformAmountsMode(totalsMode, [parsed]);
5616
+ if (isExternalAmountsMode(totalsMode)) {
5617
+ await requireOrderTotalsForExternalWrite(parsed.orderTotals);
5618
+ }
5619
+ const suppliedTotals = isExternalAmountsMode(totalsMode) ? buildExternalHeaderTotals(parsed.orderTotals) : null;
5520
5620
  await assertOrderAcceptsNewLine(order, existingSnapshot);
5521
5621
  await assertShippedOrderLineEditable(em, order, existingSnapshot, parsed);
5522
5622
  const priceMode = parsed.priceMode === "gross" ? "gross" : parsed.priceMode === "net" ? "net" : null;
5523
5623
  let unitPriceNet = parsed.unitPriceNet ?? existingSnapshot?.unitPriceNet ?? null;
5524
5624
  let unitPriceGross = parsed.unitPriceGross ?? existingSnapshot?.unitPriceGross ?? null;
5525
5625
  let taxRate = parsed.taxRate ?? existingSnapshot?.taxRate ?? null;
5626
+ const unitPriceSupplied = unitPriceNet !== null || unitPriceGross !== null;
5526
5627
  if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
5527
5628
  let taxService = null;
5528
5629
  try {
@@ -5624,12 +5725,22 @@ const orderLineUpsertCommand = {
5624
5725
  promotionCode: parsed.promotionCode ?? existingSnapshot?.promotionCode ?? null,
5625
5726
  metadata,
5626
5727
  customFieldSetId: parsed.customFieldSetId ?? existingSnapshot?.customFieldSetId ?? null,
5627
- customFields: parsed.customFields && typeof parsed.customFields === "object" ? cloneJson(parsed.customFields) : existingSnapshot?.customFields ?? null
5728
+ customFields: parsed.customFields && typeof parsed.customFields === "object" ? cloneJson(parsed.customFields) : existingSnapshot?.customFields ?? null,
5729
+ amountsMode: totalsMode
5628
5730
  };
5629
5731
  Object.assign(
5630
5732
  updatedSnapshot,
5631
5733
  resolveUpsertCalculatedAmounts(parsed, updatedSnapshot, existingSnapshot)
5632
5734
  );
5735
+ if (isExternalAmountsMode(totalsMode)) {
5736
+ await assertExternalLineComplete(
5737
+ {
5738
+ ...updatedSnapshot,
5739
+ unitPriceNet: unitPriceSupplied ? unitPriceNet : null
5740
+ },
5741
+ updatedSnapshot.lineNumber ?? 1
5742
+ );
5743
+ }
5633
5744
  updatedSnapshot.statusEntryId = statusEntryId;
5634
5745
  updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
5635
5746
  updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
@@ -5668,7 +5779,9 @@ const orderLineUpsertCommand = {
5668
5779
  lines: calcLines,
5669
5780
  adjustments: adjustmentDrafts,
5670
5781
  context: calculationContext,
5671
- existingTotals: resolveExistingPaymentTotals(order)
5782
+ existingTotals: resolveExistingPaymentTotals(order),
5783
+ totalsMode,
5784
+ suppliedTotals
5672
5785
  });
5673
5786
  let eventBus = null;
5674
5787
  try {
@@ -5805,6 +5918,11 @@ const orderLineDeleteCommand = {
5805
5918
  )
5806
5919
  });
5807
5920
  }
5921
+ const totalsMode = order.totalsMode ?? "computed";
5922
+ if (isExternalAmountsMode(totalsMode)) {
5923
+ await requireOrderTotalsForExternalWrite(parsed.orderTotals);
5924
+ }
5925
+ const suppliedTotals = isExternalAmountsMode(totalsMode) ? buildExternalHeaderTotals(parsed.orderTotals) : null;
5808
5926
  const sourceInputs = filtered.map((line, index) => ({
5809
5927
  ...mapOrderLineEntityToSnapshot(line),
5810
5928
  statusEntryId: line.statusEntryId ?? null,
@@ -5836,7 +5954,9 @@ const orderLineDeleteCommand = {
5836
5954
  lines: calcLines,
5837
5955
  adjustments: adjustmentDrafts,
5838
5956
  context: calculationContext,
5839
- existingTotals: resolveExistingPaymentTotals(order)
5957
+ existingTotals: resolveExistingPaymentTotals(order),
5958
+ totalsMode,
5959
+ suppliedTotals
5840
5960
  });
5841
5961
  let eventBus = null;
5842
5962
  try {
@@ -5922,6 +6042,7 @@ const quoteLineUpsertCommand = {
5922
6042
  async execute(input, ctx) {
5923
6043
  const rawBody = input?.body ?? {};
5924
6044
  const parsed = quoteLineUpsertSchema.parse(rawBody);
6045
+ await assertAmountsModeUnsupportedOnQuote([parsed]);
5925
6046
  const em = ctx.container.resolve("em").fork();
5926
6047
  const quote = await findOneWithDecryption(em, SalesQuote, {
5927
6048
  id: parsed.quoteId,
@@ -6325,6 +6446,7 @@ const orderAdjustmentUpsertCommand = {
6325
6446
  throw notFound("Sales order not found");
6326
6447
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
6327
6448
  await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
6449
+ if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
6328
6450
  if (parsed.scope === "line") {
6329
6451
  throw new CrudHttpError(400, {
6330
6452
  error: "Line-scoped adjustments are not supported yet."
@@ -6575,6 +6697,7 @@ const orderAdjustmentDeleteCommand = {
6575
6697
  throw notFound("Sales order not found");
6576
6698
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
6577
6699
  await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
6700
+ if (isExternalAmountsMode(order.totalsMode ?? "computed")) await refuseOnExternalOrder();
6578
6701
  const [existingLines, adjustments] = await Promise.all([
6579
6702
  em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
6580
6703
  em.find(