@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-539cff4960

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 (184) hide show
  1. package/dist/generated/entities/catalog_product/index.js +16 -0
  2. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  3. package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
  4. package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
  5. package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
  6. package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
  7. package/dist/generated/entities/sales_invoice_line/index.js +7 -1
  8. package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
  9. package/dist/generated/entities/sales_order_line/index.js +6 -0
  10. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  11. package/dist/generated/entities/sales_quote_line/index.js +6 -0
  12. package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
  13. package/dist/generated/entities.ids.generated.js +1 -0
  14. package/dist/generated/entities.ids.generated.js.map +2 -2
  15. package/dist/generated/entity-fields-registry.js +2 -0
  16. package/dist/generated/entity-fields-registry.js.map +2 -2
  17. package/dist/modules/catalog/api/prices/route.js +123 -8
  18. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  19. package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
  20. package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
  21. package/dist/modules/catalog/api/products/route.js +351 -201
  22. package/dist/modules/catalog/api/products/route.js.map +2 -2
  23. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  25. package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
  26. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  27. package/dist/modules/catalog/commands/index.js +1 -0
  28. package/dist/modules/catalog/commands/index.js.map +2 -2
  29. package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
  30. package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
  31. package/dist/modules/catalog/commands/products.js +355 -73
  32. package/dist/modules/catalog/commands/products.js.map +2 -2
  33. package/dist/modules/catalog/commands/shared.js +18 -4
  34. package/dist/modules/catalog/commands/shared.js.map +2 -2
  35. package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
  36. package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
  37. package/dist/modules/catalog/components/products/productForm.js +66 -5
  38. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  39. package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
  40. package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
  41. package/dist/modules/catalog/data/entities.js +86 -0
  42. package/dist/modules/catalog/data/entities.js.map +2 -2
  43. package/dist/modules/catalog/data/validators.js +65 -3
  44. package/dist/modules/catalog/data/validators.js.map +2 -2
  45. package/dist/modules/catalog/events.js +3 -0
  46. package/dist/modules/catalog/events.js.map +2 -2
  47. package/dist/modules/catalog/lib/unitCodes.js +7 -0
  48. package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
  49. package/dist/modules/catalog/lib/unitResolution.js +53 -0
  50. package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
  51. package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
  52. package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
  53. package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
  54. package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
  55. package/dist/modules/catalog/search.js +69 -1
  56. package/dist/modules/catalog/search.js.map +2 -2
  57. package/dist/modules/catalog/seed/examples.js +91 -42
  58. package/dist/modules/catalog/seed/examples.js.map +2 -2
  59. package/dist/modules/dashboards/seed/analytics.js +3 -0
  60. package/dist/modules/dashboards/seed/analytics.js.map +2 -2
  61. package/dist/modules/sales/api/order-lines/route.js +98 -15
  62. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  63. package/dist/modules/sales/api/quote-lines/route.js +101 -14
  64. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  65. package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
  66. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  67. package/dist/modules/sales/commands/documents.js +1424 -260
  68. package/dist/modules/sales/commands/documents.js.map +3 -3
  69. package/dist/modules/sales/commands/shared.js +6 -2
  70. package/dist/modules/sales/commands/shared.js.map +2 -2
  71. package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
  72. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  73. package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
  74. package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
  75. package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
  76. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  77. package/dist/modules/sales/data/entities.js +59 -3
  78. package/dist/modules/sales/data/entities.js.map +2 -2
  79. package/dist/modules/sales/data/validators.js +35 -0
  80. package/dist/modules/sales/data/validators.js.map +2 -2
  81. package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
  82. package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
  83. package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
  84. package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
  85. package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
  86. package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
  87. package/dist/modules/sales/search.js +28 -0
  88. package/dist/modules/sales/search.js.map +2 -2
  89. package/dist/modules/sales/seed/examples.js +14 -1
  90. package/dist/modules/sales/seed/examples.js.map +2 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
  92. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  93. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
  94. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  95. package/dist/modules/staff/translations.js +9 -0
  96. package/dist/modules/staff/translations.js.map +7 -0
  97. package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
  98. package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
  99. package/dist/modules/translations/lib/extract-record-id.js +31 -2
  100. package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
  101. package/dist/modules/translations/lib/resolve-field-list.js +3 -0
  102. package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
  103. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
  104. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  105. package/dist/modules/translations/widgets/injection-table.js +18 -29
  106. package/dist/modules/translations/widgets/injection-table.js.map +2 -2
  107. package/generated/entities/catalog_product/index.ts +8 -0
  108. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  109. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  110. package/generated/entities/sales_invoice_line/index.ts +3 -0
  111. package/generated/entities/sales_order_line/index.ts +3 -0
  112. package/generated/entities/sales_quote_line/index.ts +3 -0
  113. package/generated/entities.ids.generated.ts +1 -0
  114. package/generated/entity-fields-registry.ts +2 -0
  115. package/package.json +2 -2
  116. package/src/modules/auth/i18n/de.json +1 -1
  117. package/src/modules/auth/i18n/en.json +1 -1
  118. package/src/modules/auth/i18n/es.json +1 -1
  119. package/src/modules/auth/i18n/pl.json +1 -1
  120. package/src/modules/catalog/api/prices/route.ts +213 -81
  121. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  122. package/src/modules/catalog/api/products/route.ts +638 -402
  123. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  124. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  125. package/src/modules/catalog/commands/index.ts +1 -0
  126. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  127. package/src/modules/catalog/commands/products.ts +1151 -693
  128. package/src/modules/catalog/commands/shared.ts +19 -5
  129. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  130. package/src/modules/catalog/components/products/productForm.ts +369 -256
  131. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  132. package/src/modules/catalog/data/entities.ts +82 -1
  133. package/src/modules/catalog/data/validators.ts +118 -34
  134. package/src/modules/catalog/events.ts +3 -0
  135. package/src/modules/catalog/i18n/de.json +56 -0
  136. package/src/modules/catalog/i18n/en.json +56 -0
  137. package/src/modules/catalog/i18n/es.json +56 -0
  138. package/src/modules/catalog/i18n/pl.json +56 -0
  139. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  140. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  141. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  142. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  143. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  144. package/src/modules/catalog/search.ts +73 -1
  145. package/src/modules/catalog/seed/examples.ts +552 -479
  146. package/src/modules/dashboards/i18n/de.json +1 -1
  147. package/src/modules/dashboards/i18n/en.json +1 -1
  148. package/src/modules/dashboards/i18n/es.json +1 -1
  149. package/src/modules/dashboards/i18n/pl.json +1 -1
  150. package/src/modules/dashboards/seed/analytics.ts +3 -0
  151. package/src/modules/sales/api/order-lines/route.ts +158 -68
  152. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  153. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  154. package/src/modules/sales/commands/documents.ts +4250 -2424
  155. package/src/modules/sales/commands/shared.ts +7 -2
  156. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  157. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  158. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  159. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  160. package/src/modules/sales/data/entities.ts +53 -0
  161. package/src/modules/sales/data/validators.ts +36 -0
  162. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  163. package/src/modules/sales/i18n/de.json +23 -3
  164. package/src/modules/sales/i18n/en.json +23 -3
  165. package/src/modules/sales/i18n/es.json +23 -3
  166. package/src/modules/sales/i18n/pl.json +23 -3
  167. package/src/modules/sales/lib/types.ts +30 -0
  168. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  169. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  170. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  171. package/src/modules/sales/search.ts +28 -0
  172. package/src/modules/sales/seed/examples.ts +20 -1
  173. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  174. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
  175. package/src/modules/staff/translations.ts +5 -0
  176. package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
  177. package/src/modules/translations/lib/extract-record-id.ts +47 -3
  178. package/src/modules/translations/lib/resolve-field-list.ts +4 -0
  179. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
  180. package/src/modules/translations/widgets/injection-table.ts +19 -33
  181. package/src/modules/workflows/i18n/de.json +4 -4
  182. package/src/modules/workflows/i18n/en.json +4 -4
  183. package/src/modules/workflows/i18n/es.json +4 -4
  184. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -1,9 +1,16 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { z } from "zod";
3
3
  import { registerCommand } from "@open-mercato/shared/lib/commands";
4
- import { buildChanges, emitCrudSideEffects, requireId } from "@open-mercato/shared/lib/commands/helpers";
4
+ import {
5
+ buildChanges,
6
+ emitCrudSideEffects,
7
+ requireId
8
+ } from "@open-mercato/shared/lib/commands/helpers";
5
9
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
- import { deriveResourceFromCommandId, invalidateCrudCache } from "@open-mercato/shared/lib/crud/cache";
10
+ import {
11
+ deriveResourceFromCommandId,
12
+ invalidateCrudCache
13
+ } from "@open-mercato/shared/lib/crud/cache";
7
14
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
8
15
  import { resolveNotificationService } from "../../notifications/lib/notificationService.js";
9
16
  import { buildFeatureNotificationFromType } from "../../notifications/lib/notificationBuilder.js";
@@ -11,7 +18,7 @@ import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/h
11
18
  import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
12
19
  import { normalizeCustomFieldValues } from "@open-mercato/shared/lib/custom-fields/normalize";
13
20
  import { E } from "../../../generated/entities.ids.generated.js";
14
- import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
21
+ import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
15
22
  import {
16
23
  SalesQuote,
17
24
  SalesQuoteLine,
@@ -32,6 +39,11 @@ import {
32
39
  SalesDocumentTag,
33
40
  SalesDocumentTagAssignment
34
41
  } from "../data/entities.js";
42
+ import {
43
+ CatalogProduct,
44
+ CatalogProductUnitConversion
45
+ } from "../../catalog/data/entities.js";
46
+ import { Dictionary, DictionaryEntry } from "../../dictionaries/data/entities.js";
35
47
  import { CustomFieldValue } from "@open-mercato/core/modules/entities/data/entities";
36
48
  import {
37
49
  CustomerAddress,
@@ -64,6 +76,11 @@ import { resolveDictionaryEntryValue } from "../lib/dictionaries.js";
64
76
  import { resolveStatusEntryIdByValue } from "../lib/statusHelpers.js";
65
77
  import { loadSalesSettings } from "./settings.js";
66
78
  import { notificationTypes } from "../notifications.js";
79
+ import {
80
+ REFERENCE_UNIT_CODES,
81
+ canonicalizeUnitCode,
82
+ toUnitLookupKey
83
+ } from "@open-mercato/shared/lib/units/unitCodes";
67
84
  const orderCrudEvents = {
68
85
  module: "sales",
69
86
  entity: "order",
@@ -85,7 +102,9 @@ const quoteCrudEvents = {
85
102
  })
86
103
  };
87
104
  const currencyCodeSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{3}$/, { message: "currency_code_invalid" });
88
- const dateOnlySchema = z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/, { message: "invalid_date" }).refine((value) => !Number.isNaN(new Date(value).getTime()), { message: "invalid_date" });
105
+ const dateOnlySchema = z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/, { message: "invalid_date" }).refine((value) => !Number.isNaN(new Date(value).getTime()), {
106
+ message: "invalid_date"
107
+ });
89
108
  const addressSnapshotSchema = z.record(z.string(), z.unknown()).nullable().optional();
90
109
  const documentUpdateSchema = z.object({
91
110
  id: z.string().uuid(),
@@ -259,13 +278,20 @@ function resolveNoteAuthorFromAuth(auth) {
259
278
  return uuidRegex.test(sub) ? sub : null;
260
279
  }
261
280
  function resolveStatusChangeActor(auth, translate) {
262
- const unknownLabel = translate("sales.orders.status_change.actor_unknown", "unknown user");
281
+ const unknownLabel = translate(
282
+ "sales.orders.status_change.actor_unknown",
283
+ "unknown user"
284
+ );
263
285
  if (!auth) return unknownLabel;
264
286
  if (auth.isApiKey) {
265
287
  const keyName = typeof auth.keyName === "string" ? auth.keyName.trim() : "";
266
288
  const keyId = typeof auth.keyId === "string" ? auth.keyId.trim() : "";
267
289
  const label = keyName || keyId || (typeof auth.sub === "string" ? auth.sub : "");
268
- return label ? translate("sales.orders.status_change.actor_api_key", "API key {name}", { name: label }) : unknownLabel;
290
+ return label ? translate(
291
+ "sales.orders.status_change.actor_api_key",
292
+ "API key {name}",
293
+ { name: label }
294
+ ) : unknownLabel;
269
295
  }
270
296
  const email = typeof auth.email === "string" ? auth.email.trim() : "";
271
297
  if (email) return email;
@@ -415,17 +441,29 @@ async function applyDocumentUpdate({
415
441
  deletedAt: null
416
442
  });
417
443
  if (!channel) {
418
- throw new CrudHttpError(400, { error: translate("sales.documents.detail.channelInvalid", "Selected channel could not be found.") });
444
+ throw new CrudHttpError(400, {
445
+ error: translate(
446
+ "sales.documents.detail.channelInvalid",
447
+ "Selected channel could not be found."
448
+ )
449
+ });
419
450
  }
420
451
  entity.channelId = channel.id;
421
452
  }
422
453
  }
423
454
  if (input.statusEntryId !== void 0) {
424
- const statusValue = await resolveDictionaryEntryValue(em, input.statusEntryId);
455
+ const statusValue = await resolveDictionaryEntryValue(
456
+ em,
457
+ input.statusEntryId
458
+ );
425
459
  if (input.statusEntryId && !statusValue) {
426
- throw new CrudHttpError(400, { error: translate("sales.documents.detail.statusInvalid", "Selected status could not be found.") });
460
+ throw new CrudHttpError(400, {
461
+ error: translate(
462
+ "sales.documents.detail.statusInvalid",
463
+ "Selected status could not be found."
464
+ )
465
+ });
427
466
  }
428
- ;
429
467
  entity.statusEntryId = input.statusEntryId ?? null;
430
468
  entity.status = statusValue;
431
469
  }
@@ -448,13 +486,23 @@ async function applyDocumentUpdate({
448
486
  if (input.shippingAddressId !== void 0) {
449
487
  entity.shippingAddressId = input.shippingAddressId ?? null;
450
488
  if (input.shippingAddressSnapshot === void 0) {
451
- entity.shippingAddressSnapshot = await resolveAddressSnapshot(em, organizationId, tenantId, input.shippingAddressId);
489
+ entity.shippingAddressSnapshot = await resolveAddressSnapshot(
490
+ em,
491
+ organizationId,
492
+ tenantId,
493
+ input.shippingAddressId
494
+ );
452
495
  }
453
496
  }
454
497
  if (input.billingAddressId !== void 0) {
455
498
  entity.billingAddressId = input.billingAddressId ?? null;
456
499
  if (input.billingAddressSnapshot === void 0) {
457
- entity.billingAddressSnapshot = await resolveAddressSnapshot(em, organizationId, tenantId, input.billingAddressId);
500
+ entity.billingAddressSnapshot = await resolveAddressSnapshot(
501
+ em,
502
+ organizationId,
503
+ tenantId,
504
+ input.billingAddressId
505
+ );
458
506
  }
459
507
  }
460
508
  if (input.shippingAddressSnapshot !== void 0) {
@@ -473,18 +521,20 @@ async function applyDocumentUpdate({
473
521
  deletedAt: null
474
522
  });
475
523
  if (!shippingMethod) {
476
- throw new CrudHttpError(400, { error: translate("sales.documents.detail.shippingMethodInvalid", "Selected shipping method could not be found.") });
524
+ throw new CrudHttpError(400, {
525
+ error: translate(
526
+ "sales.documents.detail.shippingMethodInvalid",
527
+ "Selected shipping method could not be found."
528
+ )
529
+ });
477
530
  }
478
531
  }
479
- ;
480
532
  entity.shippingMethodId = input.shippingMethodId ?? null;
481
533
  entity.shippingMethod = shippingMethod ?? null;
482
534
  entity.shippingMethodCode = input.shippingMethodCode ?? shippingMethod?.code ?? null;
483
535
  if (input.shippingMethodSnapshot !== void 0) {
484
- ;
485
536
  entity.shippingMethodSnapshot = input.shippingMethodSnapshot ?? null;
486
537
  } else {
487
- ;
488
538
  entity.shippingMethodSnapshot = shippingMethod ? {
489
539
  id: shippingMethod.id,
490
540
  code: shippingMethod.code,
@@ -514,18 +564,20 @@ async function applyDocumentUpdate({
514
564
  deletedAt: null
515
565
  });
516
566
  if (!paymentMethod) {
517
- throw new CrudHttpError(400, { error: translate("sales.documents.detail.paymentMethodInvalid", "Selected payment method could not be found.") });
567
+ throw new CrudHttpError(400, {
568
+ error: translate(
569
+ "sales.documents.detail.paymentMethodInvalid",
570
+ "Selected payment method could not be found."
571
+ )
572
+ });
518
573
  }
519
574
  }
520
- ;
521
575
  entity.paymentMethodId = input.paymentMethodId ?? null;
522
576
  entity.paymentMethod = paymentMethod ?? null;
523
577
  entity.paymentMethodCode = input.paymentMethodCode ?? paymentMethod?.code ?? null;
524
578
  if (input.paymentMethodSnapshot !== void 0) {
525
- ;
526
579
  entity.paymentMethodSnapshot = input.paymentMethodSnapshot ?? null;
527
580
  } else {
528
- ;
529
581
  entity.paymentMethodSnapshot = paymentMethod ? {
530
582
  id: paymentMethod.id,
531
583
  code: paymentMethod.code,
@@ -550,7 +602,6 @@ async function applyDocumentUpdate({
550
602
  });
551
603
  }
552
604
  if (input.customFieldSetId !== void 0) {
553
- ;
554
605
  entity.customFieldSetId = input.customFieldSetId ?? null;
555
606
  }
556
607
  if (input.customFields !== void 0) {
@@ -567,9 +618,24 @@ async function applyDocumentUpdate({
567
618
  async function loadQuoteSnapshot(em, id) {
568
619
  const quote = await em.findOne(SalesQuote, { id, deletedAt: null });
569
620
  if (!quote) return null;
570
- const lines = await em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } });
571
- const adjustments = await em.find(SalesQuoteAdjustment, { quote }, { orderBy: { position: "asc" } });
572
- const [addresses, notes, tags, quoteCustomFields, lineCustomFields, adjustmentCustomFields] = await Promise.all([
621
+ const lines = await em.find(
622
+ SalesQuoteLine,
623
+ { quote },
624
+ { orderBy: { lineNumber: "asc" } }
625
+ );
626
+ const adjustments = await em.find(
627
+ SalesQuoteAdjustment,
628
+ { quote },
629
+ { orderBy: { position: "asc" } }
630
+ );
631
+ const [
632
+ addresses,
633
+ notes,
634
+ tags,
635
+ quoteCustomFields,
636
+ lineCustomFields,
637
+ adjustmentCustomFields
638
+ ] = await Promise.all([
573
639
  em.find(SalesDocumentAddress, { documentId: id, documentKind: "quote" }),
574
640
  em.find(SalesNote, { contextType: "quote", contextId: id }),
575
641
  findWithDecryption(
@@ -590,38 +656,48 @@ async function loadQuoteSnapshot(em, id) {
590
656
  em,
591
657
  entityId: E.sales.sales_quote_line,
592
658
  recordIds: lines.map((line) => line.id),
593
- tenantIdByRecord: Object.fromEntries(lines.map((line) => [line.id, quote.tenantId])),
594
- organizationIdByRecord: Object.fromEntries(lines.map((line) => [line.id, quote.organizationId]))
659
+ tenantIdByRecord: Object.fromEntries(
660
+ lines.map((line) => [line.id, quote.tenantId])
661
+ ),
662
+ organizationIdByRecord: Object.fromEntries(
663
+ lines.map((line) => [line.id, quote.organizationId])
664
+ )
595
665
  }) : Promise.resolve({}),
596
666
  adjustments.length ? loadCustomFieldValues({
597
667
  em,
598
668
  entityId: E.sales.sales_quote_adjustment,
599
669
  recordIds: adjustments.map((adj) => adj.id),
600
- tenantIdByRecord: Object.fromEntries(adjustments.map((adj) => [adj.id, quote.tenantId])),
601
- organizationIdByRecord: Object.fromEntries(adjustments.map((adj) => [adj.id, quote.organizationId]))
670
+ tenantIdByRecord: Object.fromEntries(
671
+ adjustments.map((adj) => [adj.id, quote.tenantId])
672
+ ),
673
+ organizationIdByRecord: Object.fromEntries(
674
+ adjustments.map((adj) => [adj.id, quote.organizationId])
675
+ )
602
676
  }) : Promise.resolve({})
603
677
  ]);
604
- const addressSnapshots = addresses.map((entry) => ({
605
- id: entry.id,
606
- organizationId: entry.organizationId,
607
- tenantId: entry.tenantId,
608
- documentId: entry.documentId,
609
- documentKind: "quote",
610
- customerAddressId: entry.customerAddressId ?? null,
611
- name: entry.name ?? null,
612
- purpose: entry.purpose ?? null,
613
- companyName: entry.companyName ?? null,
614
- addressLine1: entry.addressLine1,
615
- addressLine2: entry.addressLine2 ?? null,
616
- city: entry.city ?? null,
617
- region: entry.region ?? null,
618
- postalCode: entry.postalCode ?? null,
619
- country: entry.country ?? null,
620
- buildingNumber: entry.buildingNumber ?? null,
621
- flatNumber: entry.flatNumber ?? null,
622
- latitude: entry.latitude ?? null,
623
- longitude: entry.longitude ?? null
624
- }));
678
+ const addressSnapshots = addresses.map(
679
+ (entry) => ({
680
+ id: entry.id,
681
+ organizationId: entry.organizationId,
682
+ tenantId: entry.tenantId,
683
+ documentId: entry.documentId,
684
+ documentKind: "quote",
685
+ customerAddressId: entry.customerAddressId ?? null,
686
+ name: entry.name ?? null,
687
+ purpose: entry.purpose ?? null,
688
+ companyName: entry.companyName ?? null,
689
+ addressLine1: entry.addressLine1,
690
+ addressLine2: entry.addressLine2 ?? null,
691
+ city: entry.city ?? null,
692
+ region: entry.region ?? null,
693
+ postalCode: entry.postalCode ?? null,
694
+ country: entry.country ?? null,
695
+ buildingNumber: entry.buildingNumber ?? null,
696
+ flatNumber: entry.flatNumber ?? null,
697
+ latitude: entry.latitude ?? null,
698
+ longitude: entry.longitude ?? null
699
+ })
700
+ );
625
701
  const noteSnapshots = notes.map((entry) => ({
626
702
  id: entry.id,
627
703
  organizationId: entry.organizationId,
@@ -703,6 +779,9 @@ async function loadQuoteSnapshot(em, id) {
703
779
  comment: line.comment ?? null,
704
780
  quantity: line.quantity,
705
781
  quantityUnit: line.quantityUnit ?? null,
782
+ normalizedQuantity: line.normalizedQuantity ?? line.quantity,
783
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
784
+ uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
706
785
  currencyCode: line.currencyCode,
707
786
  unitPriceNet: line.unitPriceNet,
708
787
  unitPriceGross: line.unitPriceGross,
@@ -744,9 +823,26 @@ async function loadQuoteSnapshot(em, id) {
744
823
  async function loadOrderSnapshot(em, id) {
745
824
  const order = await em.findOne(SalesOrder, { id, deletedAt: null });
746
825
  if (!order) return null;
747
- const lines = await em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } });
748
- const adjustments = await em.find(SalesOrderAdjustment, { order }, { orderBy: { position: "asc" } });
749
- const [addresses, notes, tags, shipments, payments, orderCustomFields, lineCustomFields, adjustmentCustomFields] = await Promise.all([
826
+ const lines = await em.find(
827
+ SalesOrderLine,
828
+ { order },
829
+ { orderBy: { lineNumber: "asc" } }
830
+ );
831
+ const adjustments = await em.find(
832
+ SalesOrderAdjustment,
833
+ { order },
834
+ { orderBy: { position: "asc" } }
835
+ );
836
+ const [
837
+ addresses,
838
+ notes,
839
+ tags,
840
+ shipments,
841
+ payments,
842
+ orderCustomFields,
843
+ lineCustomFields,
844
+ adjustmentCustomFields
845
+ ] = await Promise.all([
750
846
  em.find(SalesDocumentAddress, { documentId: id, documentKind: "order" }),
751
847
  em.find(SalesNote, { contextType: "order", contextId: id }),
752
848
  findWithDecryption(
@@ -769,40 +865,54 @@ async function loadOrderSnapshot(em, id) {
769
865
  em,
770
866
  entityId: E.sales.sales_order_line,
771
867
  recordIds: lines.map((line) => line.id),
772
- tenantIdByRecord: Object.fromEntries(lines.map((line) => [line.id, order.tenantId])),
773
- organizationIdByRecord: Object.fromEntries(lines.map((line) => [line.id, order.organizationId]))
868
+ tenantIdByRecord: Object.fromEntries(
869
+ lines.map((line) => [line.id, order.tenantId])
870
+ ),
871
+ organizationIdByRecord: Object.fromEntries(
872
+ lines.map((line) => [line.id, order.organizationId])
873
+ )
774
874
  }) : Promise.resolve({}),
775
875
  adjustments.length ? loadCustomFieldValues({
776
876
  em,
777
877
  entityId: E.sales.sales_order_adjustment,
778
878
  recordIds: adjustments.map((adj) => adj.id),
779
- tenantIdByRecord: Object.fromEntries(adjustments.map((adj) => [adj.id, order.tenantId])),
780
- organizationIdByRecord: Object.fromEntries(adjustments.map((adj) => [adj.id, order.organizationId]))
879
+ tenantIdByRecord: Object.fromEntries(
880
+ adjustments.map((adj) => [adj.id, order.tenantId])
881
+ ),
882
+ organizationIdByRecord: Object.fromEntries(
883
+ adjustments.map((adj) => [adj.id, order.organizationId])
884
+ )
781
885
  }) : Promise.resolve({})
782
886
  ]);
783
- const shipmentSnapshots = (await Promise.all(shipments.map((entry) => loadShipmentSnapshot(em, entry.id)))).filter((entry) => !!entry);
784
- const paymentSnapshots = (await Promise.all(payments.map((entry) => loadPaymentSnapshot(em, entry.id)))).filter((entry) => !!entry);
785
- const addressSnapshots = addresses.map((entry) => ({
786
- id: entry.id,
787
- organizationId: entry.organizationId,
788
- tenantId: entry.tenantId,
789
- documentId: entry.documentId,
790
- documentKind: "order",
791
- customerAddressId: entry.customerAddressId ?? null,
792
- name: entry.name ?? null,
793
- purpose: entry.purpose ?? null,
794
- companyName: entry.companyName ?? null,
795
- addressLine1: entry.addressLine1,
796
- addressLine2: entry.addressLine2 ?? null,
797
- city: entry.city ?? null,
798
- region: entry.region ?? null,
799
- postalCode: entry.postalCode ?? null,
800
- country: entry.country ?? null,
801
- buildingNumber: entry.buildingNumber ?? null,
802
- flatNumber: entry.flatNumber ?? null,
803
- latitude: entry.latitude ?? null,
804
- longitude: entry.longitude ?? null
805
- }));
887
+ const shipmentSnapshots = (await Promise.all(
888
+ shipments.map((entry) => loadShipmentSnapshot(em, entry.id))
889
+ )).filter((entry) => !!entry);
890
+ const paymentSnapshots = (await Promise.all(
891
+ payments.map((entry) => loadPaymentSnapshot(em, entry.id))
892
+ )).filter((entry) => !!entry);
893
+ const addressSnapshots = addresses.map(
894
+ (entry) => ({
895
+ id: entry.id,
896
+ organizationId: entry.organizationId,
897
+ tenantId: entry.tenantId,
898
+ documentId: entry.documentId,
899
+ documentKind: "order",
900
+ customerAddressId: entry.customerAddressId ?? null,
901
+ name: entry.name ?? null,
902
+ purpose: entry.purpose ?? null,
903
+ companyName: entry.companyName ?? null,
904
+ addressLine1: entry.addressLine1,
905
+ addressLine2: entry.addressLine2 ?? null,
906
+ city: entry.city ?? null,
907
+ region: entry.region ?? null,
908
+ postalCode: entry.postalCode ?? null,
909
+ country: entry.country ?? null,
910
+ buildingNumber: entry.buildingNumber ?? null,
911
+ flatNumber: entry.flatNumber ?? null,
912
+ latitude: entry.latitude ?? null,
913
+ longitude: entry.longitude ?? null
914
+ })
915
+ );
806
916
  const noteSnapshots = notes.map((entry) => ({
807
917
  id: entry.id,
808
918
  organizationId: entry.organizationId,
@@ -899,6 +1009,9 @@ async function loadOrderSnapshot(em, id) {
899
1009
  comment: line.comment ?? null,
900
1010
  quantity: line.quantity,
901
1011
  quantityUnit: line.quantityUnit ?? null,
1012
+ normalizedQuantity: line.normalizedQuantity ?? line.quantity,
1013
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
1014
+ uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
902
1015
  reservedQuantity: line.reservedQuantity,
903
1016
  fulfilledQuantity: line.fulfilledQuantity,
904
1017
  invoicedQuantity: line.invoicedQuantity,
@@ -975,6 +1088,391 @@ function toNumeric(value) {
975
1088
  }
976
1089
  return 0;
977
1090
  }
1091
+ const UNIT_DICTIONARY_KEYS = ["unit", "units", "measurement_units"];
1092
+ const UOM_REFERENCE_UNITS = new Set(REFERENCE_UNIT_CODES);
1093
+ const UOM_NORMALIZED_SCALE = 6;
1094
+ const UOM_NORMALIZED_MAX = 1e12;
1095
+ const UNIT_PRICE_AUTOCONVERT_SCALE = 4;
1096
+ function createUomResolver() {
1097
+ return {
1098
+ dictionaryPromise: null,
1099
+ unitExistsCache: /* @__PURE__ */ new Map(),
1100
+ productCache: /* @__PURE__ */ new Map()
1101
+ };
1102
+ }
1103
+ function normalizeUnitCode(value) {
1104
+ return canonicalizeUnitCode(value);
1105
+ }
1106
+ function unitLookupKey(value) {
1107
+ return toUnitLookupKey(value);
1108
+ }
1109
+ function roundNormalizedQuantity(value) {
1110
+ const factor = 10 ** UOM_NORMALIZED_SCALE;
1111
+ if (!Number.isFinite(value)) return 0;
1112
+ return Math.round(value * factor) / factor;
1113
+ }
1114
+ function assertNormalizedPrecision(value) {
1115
+ if (!Number.isFinite(value) || Math.abs(value) >= UOM_NORMALIZED_MAX) {
1116
+ throw new CrudHttpError(422, { error: "uom.precision_overflow" });
1117
+ }
1118
+ }
1119
+ function toOptionalNumber(value) {
1120
+ if (value === null || value === void 0) return null;
1121
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
1122
+ if (typeof value === "string" && value.trim().length) {
1123
+ const parsed = Number(value);
1124
+ return Number.isFinite(parsed) ? parsed : null;
1125
+ }
1126
+ return null;
1127
+ }
1128
+ function resolveSnapshotToBaseFactor(snapshot) {
1129
+ if (!snapshot || typeof snapshot !== "object") return 1;
1130
+ const payload = snapshot;
1131
+ const factor = toOptionalNumber(payload.toBaseFactor ?? payload.to_base_factor);
1132
+ if (!Number.isFinite(factor) || !factor || factor <= 0) return 1;
1133
+ return factor;
1134
+ }
1135
+ function roundAutoConvertedUnitPrice(value) {
1136
+ if (!Number.isFinite(value) || value < 0) return null;
1137
+ const factor = 10 ** UNIT_PRICE_AUTOCONVERT_SCALE;
1138
+ const rounded = Math.round((value + Number.EPSILON) * factor) / factor;
1139
+ return Number.isFinite(rounded) ? rounded : null;
1140
+ }
1141
+ function convertLineUnitPricesOnUnitChange(params) {
1142
+ if (!params.existingSnapshot) {
1143
+ return {
1144
+ unitPriceNet: params.unitPriceNet,
1145
+ unitPriceGross: params.unitPriceGross,
1146
+ didConvert: false
1147
+ };
1148
+ }
1149
+ const existingUnitPriceNet = typeof params.existingSnapshot.unitPriceNet === "number" && Number.isFinite(params.existingSnapshot.unitPriceNet) ? params.existingSnapshot.unitPriceNet : null;
1150
+ const existingUnitPriceGross = typeof params.existingSnapshot.unitPriceGross === "number" && Number.isFinite(params.existingSnapshot.unitPriceGross) ? params.existingSnapshot.unitPriceGross : null;
1151
+ const sameNumber = (left, right) => {
1152
+ if (left === null || right === null) return left === right;
1153
+ return Math.abs(left - right) < 1e-6;
1154
+ };
1155
+ const shouldConvertNet = params.unitPriceNet === null || sameNumber(params.unitPriceNet, existingUnitPriceNet);
1156
+ const shouldConvertGross = params.unitPriceGross === null || sameNumber(params.unitPriceGross, existingUnitPriceGross);
1157
+ if (!shouldConvertNet && !shouldConvertGross) {
1158
+ return {
1159
+ unitPriceNet: params.unitPriceNet,
1160
+ unitPriceGross: params.unitPriceGross,
1161
+ didConvert: false
1162
+ };
1163
+ }
1164
+ const previousUnit = normalizeUnitCode(params.existingSnapshot?.quantityUnit ?? null);
1165
+ const nextUnit = normalizeUnitCode(params.nextQuantityUnit);
1166
+ if (!previousUnit || !nextUnit || previousUnit === nextUnit) {
1167
+ return {
1168
+ unitPriceNet: params.unitPriceNet,
1169
+ unitPriceGross: params.unitPriceGross,
1170
+ didConvert: false
1171
+ };
1172
+ }
1173
+ const previousFactor = resolveSnapshotToBaseFactor(
1174
+ params.existingSnapshot?.uomSnapshot ?? null
1175
+ );
1176
+ const nextFactor = resolveSnapshotToBaseFactor(params.nextUomSnapshot);
1177
+ if (!Number.isFinite(previousFactor) || previousFactor <= 0 || !Number.isFinite(nextFactor) || nextFactor <= 0) {
1178
+ return {
1179
+ unitPriceNet: params.unitPriceNet,
1180
+ unitPriceGross: params.unitPriceGross,
1181
+ didConvert: false
1182
+ };
1183
+ }
1184
+ const convertAmount = (value, shouldConvert) => {
1185
+ if (!shouldConvert) return value;
1186
+ if (value === null || !Number.isFinite(value)) return value;
1187
+ const converted = roundAutoConvertedUnitPrice(value / previousFactor * nextFactor);
1188
+ return converted ?? value;
1189
+ };
1190
+ const nextUnitPriceNet = convertAmount(params.unitPriceNet, shouldConvertNet);
1191
+ const nextUnitPriceGross = convertAmount(
1192
+ params.unitPriceGross,
1193
+ shouldConvertGross
1194
+ );
1195
+ const didConvert = nextUnitPriceNet !== params.unitPriceNet || nextUnitPriceGross !== params.unitPriceGross;
1196
+ return {
1197
+ unitPriceNet: nextUnitPriceNet,
1198
+ unitPriceGross: nextUnitPriceGross,
1199
+ didConvert
1200
+ };
1201
+ }
1202
+ function resolveReferenceUnit(value) {
1203
+ const normalized = toUnitLookupKey(value);
1204
+ if (!normalized) return null;
1205
+ if (!UOM_REFERENCE_UNITS.has(normalized)) return null;
1206
+ return normalized;
1207
+ }
1208
+ async function resolveUnitDictionaryScoped(em, organizationId, tenantId) {
1209
+ return findOneWithDecryption(
1210
+ em,
1211
+ Dictionary,
1212
+ {
1213
+ organizationId,
1214
+ tenantId,
1215
+ key: { $in: [...UNIT_DICTIONARY_KEYS] },
1216
+ deletedAt: null,
1217
+ isActive: true
1218
+ },
1219
+ { orderBy: { createdAt: "asc" } }
1220
+ );
1221
+ }
1222
+ async function assertUnitExists(em, resolver, organizationId, tenantId, unitCode) {
1223
+ const normalizedCode = unitLookupKey(unitCode);
1224
+ if (!normalizedCode) return;
1225
+ const rawUnitCode = unitCode.trim();
1226
+ if (!resolver.dictionaryPromise) {
1227
+ resolver.dictionaryPromise = resolveUnitDictionaryScoped(
1228
+ em,
1229
+ organizationId,
1230
+ tenantId
1231
+ );
1232
+ }
1233
+ const dictionary = await resolver.dictionaryPromise;
1234
+ if (!dictionary) {
1235
+ throw new CrudHttpError(400, { error: "uom.unit_not_found" });
1236
+ }
1237
+ const cacheKey = `${dictionary.id}:${normalizedCode}`;
1238
+ if (resolver.unitExistsCache.has(cacheKey)) {
1239
+ if (!resolver.unitExistsCache.get(cacheKey)) {
1240
+ throw new CrudHttpError(400, { error: "uom.unit_not_found" });
1241
+ }
1242
+ return;
1243
+ }
1244
+ const entry = await findOneWithDecryption(em, DictionaryEntry, {
1245
+ dictionary,
1246
+ organizationId: dictionary.organizationId,
1247
+ tenantId: dictionary.tenantId,
1248
+ $or: [{ normalizedValue: normalizedCode }, { value: rawUnitCode }]
1249
+ });
1250
+ const exists = !!entry;
1251
+ resolver.unitExistsCache.set(cacheKey, exists);
1252
+ if (!exists) {
1253
+ throw new CrudHttpError(400, { error: "uom.unit_not_found" });
1254
+ }
1255
+ }
1256
+ async function resolveProductUomState(em, resolver, organizationId, tenantId, productId) {
1257
+ if (resolver.productCache.has(productId)) {
1258
+ return resolver.productCache.get(productId) ?? null;
1259
+ }
1260
+ const product = await findOneWithDecryption(em, CatalogProduct, {
1261
+ id: productId,
1262
+ organizationId,
1263
+ tenantId,
1264
+ deletedAt: null
1265
+ });
1266
+ if (!product) {
1267
+ resolver.productCache.set(productId, null);
1268
+ return null;
1269
+ }
1270
+ const conversions = await findWithDecryption(em, CatalogProductUnitConversion, {
1271
+ product: product.id,
1272
+ organizationId,
1273
+ tenantId,
1274
+ deletedAt: null,
1275
+ isActive: true
1276
+ });
1277
+ const conversionsByUnitKey = /* @__PURE__ */ new Map();
1278
+ for (const conversion of conversions) {
1279
+ const key = unitLookupKey(conversion.unitCode);
1280
+ if (!key) continue;
1281
+ conversionsByUnitKey.set(key, conversion);
1282
+ }
1283
+ const state = {
1284
+ productId: product.id,
1285
+ baseUnitCode: normalizeUnitCode(product.defaultUnit ?? null),
1286
+ defaultSalesUnit: normalizeUnitCode(product.defaultSalesUnit ?? null),
1287
+ unitPriceEnabled: Boolean(product.unitPriceEnabled),
1288
+ unitPriceReferenceUnit: resolveReferenceUnit(
1289
+ product.unitPriceReferenceUnit
1290
+ ),
1291
+ unitPriceBaseQuantity: product.unitPriceBaseQuantity ?? null,
1292
+ conversionsByUnitKey
1293
+ };
1294
+ resolver.productCache.set(productId, state);
1295
+ return state;
1296
+ }
1297
+ function buildUnitPriceReferenceSnapshot(params) {
1298
+ if (!params.product.unitPriceEnabled) return void 0;
1299
+ const baseQuantityNumber = toNumeric(params.product.unitPriceBaseQuantity);
1300
+ const output = {
1301
+ enabled: true,
1302
+ referenceUnitCode: params.product.unitPriceReferenceUnit ?? null,
1303
+ baseQuantity: params.product.unitPriceBaseQuantity ?? null
1304
+ };
1305
+ if (!params.product.unitPriceReferenceUnit || params.toBaseFactor <= 0 || baseQuantityNumber <= 0) {
1306
+ return output;
1307
+ }
1308
+ if (typeof params.unitPriceGross === "number" && Number.isFinite(params.unitPriceGross)) {
1309
+ output.grossPerReference = toNumericString(
1310
+ params.unitPriceGross / params.toBaseFactor * baseQuantityNumber
1311
+ );
1312
+ }
1313
+ if (typeof params.unitPriceNet === "number" && Number.isFinite(params.unitPriceNet)) {
1314
+ output.netPerReference = toNumericString(
1315
+ params.unitPriceNet / params.toBaseFactor * baseQuantityNumber
1316
+ );
1317
+ }
1318
+ return output;
1319
+ }
1320
+ async function normalizeLineUom(input) {
1321
+ const quantity = toNumeric(input.line.quantity);
1322
+ const existingSnapshot = input.line.uomSnapshot && typeof input.line.uomSnapshot === "object" ? cloneJson(input.line.uomSnapshot) : null;
1323
+ const productId = typeof input.line.productId === "string" ? input.line.productId : null;
1324
+ const variantId = typeof input.line.productVariantId === "string" ? input.line.productVariantId : null;
1325
+ const enteredUnitInput = normalizeUnitCode(input.line.quantityUnit);
1326
+ if (!productId) {
1327
+ if (enteredUnitInput) {
1328
+ await assertUnitExists(
1329
+ input.em,
1330
+ input.resolver,
1331
+ input.organizationId,
1332
+ input.tenantId,
1333
+ enteredUnitInput
1334
+ );
1335
+ }
1336
+ const normalizedQuantity2 = toNumeric(
1337
+ input.line.normalizedQuantity ?? quantity
1338
+ );
1339
+ assertNormalizedPrecision(normalizedQuantity2);
1340
+ return {
1341
+ quantity,
1342
+ quantityUnit: enteredUnitInput ?? null,
1343
+ normalizedQuantity: normalizedQuantity2,
1344
+ normalizedUnit: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitInput ?? null,
1345
+ uomSnapshot: existingSnapshot ?? {
1346
+ version: 1,
1347
+ productId: null,
1348
+ productVariantId: variantId,
1349
+ baseUnitCode: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitInput ?? null,
1350
+ enteredUnitCode: enteredUnitInput,
1351
+ enteredQuantity: toNumericString(quantity) ?? "0",
1352
+ toBaseFactor: "1",
1353
+ normalizedQuantity: toNumericString(normalizedQuantity2) ?? "0",
1354
+ rounding: { mode: "half_up", scale: UOM_NORMALIZED_SCALE },
1355
+ source: { conversionId: null, resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }
1356
+ }
1357
+ };
1358
+ }
1359
+ const productState = await resolveProductUomState(
1360
+ input.em,
1361
+ input.resolver,
1362
+ input.organizationId,
1363
+ input.tenantId,
1364
+ productId
1365
+ );
1366
+ if (!productState) {
1367
+ if (enteredUnitInput) {
1368
+ await assertUnitExists(
1369
+ input.em,
1370
+ input.resolver,
1371
+ input.organizationId,
1372
+ input.tenantId,
1373
+ enteredUnitInput
1374
+ );
1375
+ }
1376
+ const normalizedQuantity2 = toNumeric(
1377
+ input.line.normalizedQuantity ?? quantity
1378
+ );
1379
+ assertNormalizedPrecision(normalizedQuantity2);
1380
+ return {
1381
+ quantity,
1382
+ quantityUnit: enteredUnitInput ?? null,
1383
+ normalizedQuantity: normalizedQuantity2,
1384
+ normalizedUnit: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitInput ?? null,
1385
+ uomSnapshot: existingSnapshot
1386
+ };
1387
+ }
1388
+ const baseUnitCode = productState.baseUnitCode;
1389
+ const enteredUnitCode = enteredUnitInput ?? productState.defaultSalesUnit ?? baseUnitCode ?? normalizeUnitCode(input.line.normalizedUnit);
1390
+ if (enteredUnitCode) {
1391
+ await assertUnitExists(
1392
+ input.em,
1393
+ input.resolver,
1394
+ input.organizationId,
1395
+ input.tenantId,
1396
+ enteredUnitCode
1397
+ );
1398
+ }
1399
+ if (baseUnitCode) {
1400
+ await assertUnitExists(
1401
+ input.em,
1402
+ input.resolver,
1403
+ input.organizationId,
1404
+ input.tenantId,
1405
+ baseUnitCode
1406
+ );
1407
+ }
1408
+ if (!baseUnitCode) {
1409
+ const requiresBase = Boolean(enteredUnitCode) || productState.conversionsByUnitKey.size > 0 || Boolean(productState.defaultSalesUnit);
1410
+ if (requiresBase) {
1411
+ throw new CrudHttpError(400, { error: "uom.default_unit_missing" });
1412
+ }
1413
+ const normalizedQuantity2 = toNumeric(
1414
+ input.line.normalizedQuantity ?? quantity
1415
+ );
1416
+ assertNormalizedPrecision(normalizedQuantity2);
1417
+ return {
1418
+ quantity,
1419
+ quantityUnit: enteredUnitCode ?? null,
1420
+ normalizedQuantity: normalizedQuantity2,
1421
+ normalizedUnit: normalizeUnitCode(input.line.normalizedUnit) ?? enteredUnitCode ?? null,
1422
+ uomSnapshot: existingSnapshot
1423
+ };
1424
+ }
1425
+ const resolvedEnteredUnit = enteredUnitCode ?? baseUnitCode;
1426
+ const enteredKey = unitLookupKey(resolvedEnteredUnit);
1427
+ const baseKey = unitLookupKey(baseUnitCode);
1428
+ let toBaseFactor = 1;
1429
+ let conversionId = null;
1430
+ if (enteredKey && baseKey && enteredKey !== baseKey) {
1431
+ const conversion = productState.conversionsByUnitKey.get(enteredKey);
1432
+ if (!conversion) {
1433
+ throw new CrudHttpError(400, { error: "uom.conversion_not_found" });
1434
+ }
1435
+ toBaseFactor = toNumeric(conversion.toBaseFactor);
1436
+ conversionId = conversion.id;
1437
+ }
1438
+ if (!Number.isFinite(toBaseFactor) || toBaseFactor <= 0) {
1439
+ throw new CrudHttpError(400, { error: "uom.invalid_factor" });
1440
+ }
1441
+ const normalizedQuantity = roundNormalizedQuantity(quantity * toBaseFactor);
1442
+ assertNormalizedPrecision(normalizedQuantity);
1443
+ const unitPriceReference = buildUnitPriceReferenceSnapshot({
1444
+ product: productState,
1445
+ toBaseFactor,
1446
+ unitPriceNet: toOptionalNumber(input.line.unitPriceNet),
1447
+ unitPriceGross: toOptionalNumber(input.line.unitPriceGross)
1448
+ });
1449
+ const snapshot = {
1450
+ version: 1,
1451
+ productId,
1452
+ productVariantId: variantId,
1453
+ baseUnitCode,
1454
+ enteredUnitCode: resolvedEnteredUnit,
1455
+ enteredQuantity: toNumericString(quantity) ?? "0",
1456
+ toBaseFactor: toNumericString(toBaseFactor) ?? "1",
1457
+ normalizedQuantity: toNumericString(normalizedQuantity) ?? "0",
1458
+ rounding: {
1459
+ mode: "half_up",
1460
+ scale: UOM_NORMALIZED_SCALE
1461
+ },
1462
+ source: {
1463
+ conversionId,
1464
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
1465
+ },
1466
+ ...unitPriceReference ? { unitPriceReference } : {}
1467
+ };
1468
+ return {
1469
+ quantity,
1470
+ quantityUnit: resolvedEnteredUnit,
1471
+ normalizedQuantity,
1472
+ normalizedUnit: baseUnitCode,
1473
+ uomSnapshot: snapshot
1474
+ };
1475
+ }
978
1476
  function normalizeShippingMethodContext(snapshot, id, code, currencyCode) {
979
1477
  if (!snapshot || typeof snapshot !== "object") return null;
980
1478
  const metadata = snapshot.metadata;
@@ -1052,6 +1550,9 @@ function mapOrderLineEntityToSnapshot(line) {
1052
1550
  comment: line.comment ?? null,
1053
1551
  quantity: toNumeric(line.quantity),
1054
1552
  quantityUnit: line.quantityUnit ?? null,
1553
+ normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
1554
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
1555
+ uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
1055
1556
  currencyCode: line.currencyCode,
1056
1557
  unitPriceNet: toNumeric(line.unitPriceNet),
1057
1558
  unitPriceGross: toNumeric(line.unitPriceGross),
@@ -1079,6 +1580,9 @@ function mapQuoteLineEntityToSnapshot(line) {
1079
1580
  comment: line.comment ?? null,
1080
1581
  quantity: toNumeric(line.quantity),
1081
1582
  quantityUnit: line.quantityUnit ?? null,
1583
+ normalizedQuantity: toNumeric(line.normalizedQuantity ?? line.quantity),
1584
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
1585
+ uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
1082
1586
  currencyCode: line.currencyCode,
1083
1587
  unitPriceNet: toNumeric(line.unitPriceNet),
1084
1588
  unitPriceGross: toNumeric(line.unitPriceGross),
@@ -1133,6 +1637,10 @@ async function emitTotalsCalculated(eventBus, payload) {
1133
1637
  await eventBus.emitEvent("sales.document.totals.calculated", payload);
1134
1638
  }
1135
1639
  function createLineSnapshotFromInput(line, lineNumber) {
1640
+ const quantity = Number(line.quantity ?? 0);
1641
+ const normalizedQuantity = toNumeric(
1642
+ line.normalizedQuantity ?? quantity
1643
+ );
1136
1644
  return {
1137
1645
  lineNumber,
1138
1646
  kind: line.kind ?? "product",
@@ -1141,8 +1649,11 @@ function createLineSnapshotFromInput(line, lineNumber) {
1141
1649
  name: line.name ?? null,
1142
1650
  description: line.description ?? null,
1143
1651
  comment: line.comment ?? null,
1144
- quantity: Number(line.quantity ?? 0),
1652
+ quantity,
1145
1653
  quantityUnit: line.quantityUnit ?? null,
1654
+ normalizedQuantity,
1655
+ normalizedUnit: typeof line.normalizedUnit === "string" ? line.normalizedUnit : line.quantityUnit ?? null,
1656
+ uomSnapshot: line.uomSnapshot && typeof line.uomSnapshot === "object" ? cloneJson(line.uomSnapshot) : null,
1146
1657
  currencyCode: line.currencyCode,
1147
1658
  unitPriceNet: line.unitPriceNet ?? null,
1148
1659
  unitPriceGross: line.unitPriceGross ?? null,
@@ -1162,7 +1673,9 @@ function createLineSnapshotFromInput(line, lineNumber) {
1162
1673
  function createAdjustmentDraftFromInput(adjustment) {
1163
1674
  const lineRef = "quoteLineId" in adjustment ? adjustment.quoteLineId : adjustment.orderLineId;
1164
1675
  if (adjustment.scope === "line" && lineRef) {
1165
- throw new CrudHttpError(400, { error: "Line-scoped adjustments are not supported yet." });
1676
+ throw new CrudHttpError(400, {
1677
+ error: "Line-scoped adjustments are not supported yet."
1678
+ });
1166
1679
  }
1167
1680
  return {
1168
1681
  id: typeof adjustment.id === "string" ? adjustment.id : void 0,
@@ -1194,8 +1707,13 @@ function convertLineCalculationToEntityInput(lineResult, sourceLine, document, i
1194
1707
  comment: line.comment ?? null,
1195
1708
  quantity: toNumericString(line.quantity) ?? "0",
1196
1709
  quantityUnit: line.quantityUnit ?? null,
1710
+ normalizedQuantity: toNumericString(line.normalizedQuantity ?? line.quantity) ?? "0",
1711
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
1712
+ uomSnapshot: line.uomSnapshot && typeof line.uomSnapshot === "object" ? cloneJson(line.uomSnapshot) : null,
1197
1713
  currencyCode: line.currencyCode,
1198
- unitPriceNet: toNumericString(line.unitPriceNet ?? lineResult.netAmount / Math.max(line.quantity || 1, 1)) ?? "0",
1714
+ unitPriceNet: toNumericString(
1715
+ line.unitPriceNet ?? lineResult.netAmount / Math.max(line.quantity || 1, 1)
1716
+ ) ?? "0",
1199
1717
  unitPriceGross: toNumericString(
1200
1718
  line.unitPriceGross ?? lineResult.grossAmount / Math.max(line.quantity || 1, 1)
1201
1719
  ) ?? "0",
@@ -1251,7 +1769,12 @@ async function applyOrderLineResults(params) {
1251
1769
  const sourceLine = sourceLines[index];
1252
1770
  const statusEntryId = sourceLine.statusEntryId ?? null;
1253
1771
  const statusValue = await resolveStatus(statusEntryId ?? null);
1254
- const payload = convertLineCalculationToEntityInput(lineResult, sourceLine, order, index);
1772
+ const payload = convertLineCalculationToEntityInput(
1773
+ lineResult,
1774
+ sourceLine,
1775
+ order,
1776
+ index
1777
+ );
1255
1778
  const existing = sourceLine.id ? existingMap.get(sourceLine.id) ?? null : null;
1256
1779
  const lineEntity = existing ?? em.create(SalesOrderLine, {
1257
1780
  order,
@@ -1306,7 +1829,12 @@ async function applyQuoteLineResults(params) {
1306
1829
  const sourceLine = sourceLines[index];
1307
1830
  const statusEntryId = sourceLine.statusEntryId ?? null;
1308
1831
  const statusValue = await resolveStatus(statusEntryId ?? null);
1309
- const payload = convertLineCalculationToEntityInput(lineResult, sourceLine, quote, index);
1832
+ const payload = convertLineCalculationToEntityInput(
1833
+ lineResult,
1834
+ sourceLine,
1835
+ quote,
1836
+ index
1837
+ );
1310
1838
  const existing = sourceLine.id ? existingMap.get(sourceLine.id) ?? null : null;
1311
1839
  const lineEntity = existing ?? em.create(SalesQuoteLine, {
1312
1840
  quote,
@@ -1353,7 +1881,12 @@ async function replaceQuoteLines(em, quote, calculation, lineInputs) {
1353
1881
  for (let index = 0; index < calculation.lines.length; index += 1) {
1354
1882
  const lineResult = calculation.lines[index];
1355
1883
  const sourceLine = lineInputs[index];
1356
- const entityInput = convertLineCalculationToEntityInput(lineResult, sourceLine, quote, index);
1884
+ const entityInput = convertLineCalculationToEntityInput(
1885
+ lineResult,
1886
+ sourceLine,
1887
+ quote,
1888
+ index
1889
+ );
1357
1890
  const statusValue = await resolveStatus(sourceLine.statusEntryId ?? null);
1358
1891
  const lineEntity = em.create(SalesQuoteLine, {
1359
1892
  quote,
@@ -1377,7 +1910,11 @@ async function replaceQuoteLines(em, quote, calculation, lineInputs) {
1377
1910
  }
1378
1911
  }
1379
1912
  async function replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs) {
1380
- const existing = await em.find(SalesQuoteAdjustment, { quote }, { orderBy: { position: "asc" } });
1913
+ const existing = await em.find(
1914
+ SalesQuoteAdjustment,
1915
+ { quote },
1916
+ { orderBy: { position: "asc" } }
1917
+ );
1381
1918
  const existingMap = /* @__PURE__ */ new Map();
1382
1919
  existing.forEach((adj) => existingMap.set(adj.id, adj));
1383
1920
  const seen = /* @__PURE__ */ new Set();
@@ -1386,7 +1923,12 @@ async function replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs)
1386
1923
  const draft = adjustmentDrafts[index];
1387
1924
  const sourceById = adjustmentInputs?.find((adj) => adj.id === draft.id) ?? null;
1388
1925
  const source = sourceById ?? (adjustmentInputs ? adjustmentInputs[index] ?? null : null);
1389
- const entityInput = convertAdjustmentResultToEntityInput(draft, source, quote, index);
1926
+ const entityInput = convertAdjustmentResultToEntityInput(
1927
+ draft,
1928
+ source,
1929
+ quote,
1930
+ index
1931
+ );
1390
1932
  const adjustmentId = draft.id ?? source?.id ?? randomUUID();
1391
1933
  const existingEntity = existingMap.get(adjustmentId);
1392
1934
  const entity = existingEntity ?? em.create(SalesQuoteAdjustment, {
@@ -1441,7 +1983,12 @@ async function replaceOrderLines(em, order, calculation, lineInputs) {
1441
1983
  for (let index = 0; index < calculation.lines.length; index += 1) {
1442
1984
  const lineResult = calculation.lines[index];
1443
1985
  const sourceLine = lineInputs[index];
1444
- const entityInput = convertLineCalculationToEntityInput(lineResult, sourceLine, order, index);
1986
+ const entityInput = convertLineCalculationToEntityInput(
1987
+ lineResult,
1988
+ sourceLine,
1989
+ order,
1990
+ index
1991
+ );
1445
1992
  const statusValue = await resolveStatus(sourceLine.statusEntryId ?? null);
1446
1993
  const lineEntity = em.create(SalesOrderLine, {
1447
1994
  order,
@@ -1469,7 +2016,11 @@ async function replaceOrderLines(em, order, calculation, lineInputs) {
1469
2016
  }
1470
2017
  }
1471
2018
  async function replaceOrderAdjustments(em, order, calculation, adjustmentInputs) {
1472
- const existing = await em.find(SalesOrderAdjustment, { order }, { orderBy: { position: "asc" } });
2019
+ const existing = await em.find(
2020
+ SalesOrderAdjustment,
2021
+ { order },
2022
+ { orderBy: { position: "asc" } }
2023
+ );
1473
2024
  const existingMap = /* @__PURE__ */ new Map();
1474
2025
  existing.forEach((adj) => existingMap.set(adj.id, adj));
1475
2026
  const seen = /* @__PURE__ */ new Set();
@@ -1478,7 +2029,12 @@ async function replaceOrderAdjustments(em, order, calculation, adjustmentInputs)
1478
2029
  const draft = adjustmentDrafts[index];
1479
2030
  const sourceById = adjustmentInputs?.find((adj) => adj.id === draft.id) ?? null;
1480
2031
  const source = sourceById ?? (adjustmentInputs ? adjustmentInputs[index] ?? null : null);
1481
- const entityInput = convertAdjustmentResultToEntityInput(draft, source, order, index);
2032
+ const entityInput = convertAdjustmentResultToEntityInput(
2033
+ draft,
2034
+ source,
2035
+ order,
2036
+ index
2037
+ );
1482
2038
  const adjustmentId = draft.id ?? source?.id ?? randomUUID();
1483
2039
  const existingEntity = existingMap.get(adjustmentId);
1484
2040
  const entity = existingEntity ?? em.create(SalesOrderAdjustment, {
@@ -1547,7 +2103,8 @@ function applyOrderTotals(order, totals, lineCount) {
1547
2103
  order.lineItemCount = lineCount;
1548
2104
  }
1549
2105
  function normalizePaymentTotal(value) {
1550
- if (typeof value === "number" && Number.isFinite(value)) return Math.max(value, 0);
2106
+ if (typeof value === "number" && Number.isFinite(value))
2107
+ return Math.max(value, 0);
1551
2108
  if (typeof value === "string" && value.trim().length) {
1552
2109
  const parsed = Number(value);
1553
2110
  return Number.isFinite(parsed) ? Math.max(parsed, 0) : 0;
@@ -1645,7 +2202,10 @@ async function syncSalesDocumentTags(em, params) {
1645
2202
  if (params.tagIds === void 0) return;
1646
2203
  const tagIds = normalizeTagIds(params.tagIds);
1647
2204
  if (tagIds.length === 0) {
1648
- await em.nativeDelete(SalesDocumentTagAssignment, { documentId: params.documentId, documentKind: params.kind });
2205
+ await em.nativeDelete(SalesDocumentTagAssignment, {
2206
+ documentId: params.documentId,
2207
+ documentKind: params.kind
2208
+ });
1649
2209
  return;
1650
2210
  }
1651
2211
  const tagsInScope = await em.find(SalesDocumentTag, {
@@ -1654,10 +2214,15 @@ async function syncSalesDocumentTags(em, params) {
1654
2214
  tenantId: params.tenantId
1655
2215
  });
1656
2216
  if (tagsInScope.length !== tagIds.length) {
1657
- throw new CrudHttpError(400, { error: "One or more tags not found for this scope" });
2217
+ throw new CrudHttpError(400, {
2218
+ error: "One or more tags not found for this scope"
2219
+ });
1658
2220
  }
1659
2221
  const byId = new Map(tagsInScope.map((tag) => [tag.id, tag]));
1660
- await em.nativeDelete(SalesDocumentTagAssignment, { documentId: params.documentId, documentKind: params.kind });
2222
+ await em.nativeDelete(SalesDocumentTagAssignment, {
2223
+ documentId: params.documentId,
2224
+ documentKind: params.kind
2225
+ });
1661
2226
  for (const tagId of tagIds) {
1662
2227
  const tag = byId.get(tagId);
1663
2228
  if (!tag) continue;
@@ -1815,9 +2380,20 @@ async function restoreQuoteGraph(em, snapshot) {
1815
2380
  }
1816
2381
  applyQuoteSnapshot(quote, snapshot.quote);
1817
2382
  await em.flush();
1818
- const existingLines = await em.find(SalesQuoteLine, { quote: quote.id }, { fields: ["id"] });
1819
- const existingAdjustments = await em.find(SalesQuoteAdjustment, { quote: quote.id }, { fields: ["id"] });
1820
- await em.nativeDelete(CustomFieldValue, { entityId: E.sales.sales_quote, recordId: quote.id });
2383
+ const existingLines = await em.find(
2384
+ SalesQuoteLine,
2385
+ { quote: quote.id },
2386
+ { fields: ["id"] }
2387
+ );
2388
+ const existingAdjustments = await em.find(
2389
+ SalesQuoteAdjustment,
2390
+ { quote: quote.id },
2391
+ { fields: ["id"] }
2392
+ );
2393
+ await em.nativeDelete(CustomFieldValue, {
2394
+ entityId: E.sales.sales_quote,
2395
+ recordId: quote.id
2396
+ });
1821
2397
  if (existingLines.length) {
1822
2398
  await em.nativeDelete(CustomFieldValue, {
1823
2399
  entityId: E.sales.sales_quote_line,
@@ -1833,13 +2409,24 @@ async function restoreQuoteGraph(em, snapshot) {
1833
2409
  const addressSnapshots = Array.isArray(snapshot.addresses) ? snapshot.addresses : [];
1834
2410
  const noteSnapshots = Array.isArray(snapshot.notes) ? snapshot.notes : [];
1835
2411
  const tagSnapshots = Array.isArray(snapshot.tags) ? snapshot.tags : [];
1836
- await em.nativeDelete(SalesDocumentAddress, { documentId: quote.id, documentKind: "quote" });
1837
- await em.nativeDelete(SalesNote, { contextType: "quote", contextId: quote.id });
1838
- await em.nativeDelete(SalesDocumentTagAssignment, { documentId: quote.id, documentKind: "quote" });
2412
+ await em.nativeDelete(SalesDocumentAddress, {
2413
+ documentId: quote.id,
2414
+ documentKind: "quote"
2415
+ });
2416
+ await em.nativeDelete(SalesNote, {
2417
+ contextType: "quote",
2418
+ contextId: quote.id
2419
+ });
2420
+ await em.nativeDelete(SalesDocumentTagAssignment, {
2421
+ documentId: quote.id,
2422
+ documentKind: "quote"
2423
+ });
1839
2424
  await em.nativeDelete(SalesQuoteLine, { quote: quote.id });
1840
2425
  await em.nativeDelete(SalesQuoteAdjustment, { quote: quote.id });
1841
2426
  existingLines.forEach((entry) => em.getUnitOfWork().unsetIdentity(entry));
1842
- existingAdjustments.forEach((entry) => em.getUnitOfWork().unsetIdentity(entry));
2427
+ existingAdjustments.forEach(
2428
+ (entry) => em.getUnitOfWork().unsetIdentity(entry)
2429
+ );
1843
2430
  snapshot.lines.forEach((line) => {
1844
2431
  const lineEntity = em.create(SalesQuoteLine, {
1845
2432
  id: line.id,
@@ -1858,6 +2445,9 @@ async function restoreQuoteGraph(em, snapshot) {
1858
2445
  comment: line.comment ?? null,
1859
2446
  quantity: line.quantity,
1860
2447
  quantityUnit: line.quantityUnit ?? null,
2448
+ normalizedQuantity: line.normalizedQuantity ?? line.quantity,
2449
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
2450
+ uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
1861
2451
  currencyCode: line.currencyCode,
1862
2452
  unitPriceNet: line.unitPriceNet,
1863
2453
  unitPriceGross: line.unitPriceGross,
@@ -2056,9 +2646,20 @@ async function restoreOrderGraph(em, snapshot) {
2056
2646
  }
2057
2647
  applyOrderSnapshot(order, snapshot.order);
2058
2648
  await em.flush();
2059
- const existingLines = await em.find(SalesOrderLine, { order: order.id }, { fields: ["id"] });
2060
- const existingAdjustments = await em.find(SalesOrderAdjustment, { order: order.id }, { fields: ["id"] });
2061
- await em.nativeDelete(CustomFieldValue, { entityId: E.sales.sales_order, recordId: order.id });
2649
+ const existingLines = await em.find(
2650
+ SalesOrderLine,
2651
+ { order: order.id },
2652
+ { fields: ["id"] }
2653
+ );
2654
+ const existingAdjustments = await em.find(
2655
+ SalesOrderAdjustment,
2656
+ { order: order.id },
2657
+ { fields: ["id"] }
2658
+ );
2659
+ await em.nativeDelete(CustomFieldValue, {
2660
+ entityId: E.sales.sales_order,
2661
+ recordId: order.id
2662
+ });
2062
2663
  if (existingLines.length) {
2063
2664
  await em.nativeDelete(CustomFieldValue, {
2064
2665
  entityId: E.sales.sales_order_line,
@@ -2079,19 +2680,34 @@ async function restoreOrderGraph(em, snapshot) {
2079
2680
  const existingShipments = await em.find(SalesShipment, { order: order.id });
2080
2681
  const shipmentIds = existingShipments.map((entry) => entry.id);
2081
2682
  if (shipmentIds.length) {
2082
- await em.nativeDelete(SalesShipmentItem, { shipment: { $in: shipmentIds } });
2683
+ await em.nativeDelete(SalesShipmentItem, {
2684
+ shipment: { $in: shipmentIds }
2685
+ });
2083
2686
  await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
2084
- existingShipments.forEach((entry) => em.getUnitOfWork().unsetIdentity(entry));
2687
+ existingShipments.forEach(
2688
+ (entry) => em.getUnitOfWork().unsetIdentity(entry)
2689
+ );
2085
2690
  }
2086
2691
  await em.nativeDelete(SalesPaymentAllocation, { order: order.id });
2087
2692
  await em.nativeDelete(SalesPayment, { order: order.id });
2088
- await em.nativeDelete(SalesDocumentAddress, { documentId: order.id, documentKind: "order" });
2089
- await em.nativeDelete(SalesNote, { contextType: "order", contextId: order.id });
2090
- await em.nativeDelete(SalesDocumentTagAssignment, { documentId: order.id, documentKind: "order" });
2693
+ await em.nativeDelete(SalesDocumentAddress, {
2694
+ documentId: order.id,
2695
+ documentKind: "order"
2696
+ });
2697
+ await em.nativeDelete(SalesNote, {
2698
+ contextType: "order",
2699
+ contextId: order.id
2700
+ });
2701
+ await em.nativeDelete(SalesDocumentTagAssignment, {
2702
+ documentId: order.id,
2703
+ documentKind: "order"
2704
+ });
2091
2705
  await em.nativeDelete(SalesOrderAdjustment, { order: order.id });
2092
2706
  await em.nativeDelete(SalesOrderLine, { order: order.id });
2093
2707
  existingLines.forEach((entry) => em.getUnitOfWork().unsetIdentity(entry));
2094
- existingAdjustments.forEach((entry) => em.getUnitOfWork().unsetIdentity(entry));
2708
+ existingAdjustments.forEach(
2709
+ (entry) => em.getUnitOfWork().unsetIdentity(entry)
2710
+ );
2095
2711
  snapshot.lines.forEach((line) => {
2096
2712
  const lineEntity = em.create(SalesOrderLine, {
2097
2713
  id: line.id,
@@ -2110,6 +2726,9 @@ async function restoreOrderGraph(em, snapshot) {
2110
2726
  comment: line.comment ?? null,
2111
2727
  quantity: line.quantity,
2112
2728
  quantityUnit: line.quantityUnit ?? null,
2729
+ normalizedQuantity: line.normalizedQuantity ?? line.quantity,
2730
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
2731
+ uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
2113
2732
  reservedQuantity: line.reservedQuantity,
2114
2733
  fulfilledQuantity: line.fulfilledQuantity,
2115
2734
  invoicedQuantity: line.invoicedQuantity,
@@ -2257,7 +2876,9 @@ async function restoreOrderGraph(em, snapshot) {
2257
2876
  const createQuoteCommand = {
2258
2877
  id: "sales.quotes.create",
2259
2878
  async execute(rawInput, ctx) {
2260
- const generator = ctx.container.resolve("salesDocumentNumberGenerator");
2879
+ const generator = ctx.container.resolve(
2880
+ "salesDocumentNumberGenerator"
2881
+ );
2261
2882
  const initial = quoteCreateSchema.parse(rawInput ?? {});
2262
2883
  const quoteNumber = typeof initial.quoteNumber === "string" && initial.quoteNumber.trim().length ? initial.quoteNumber.trim() : (await generator.generate({
2263
2884
  kind: "quote",
@@ -2279,7 +2900,10 @@ const createQuoteCommand = {
2279
2900
  deliveryWindow,
2280
2901
  paymentMethod
2281
2902
  } = await resolveDocumentReferences(em, parsed);
2282
- const quoteStatus = await resolveDictionaryEntryValue(em, parsed.statusEntryId ?? null);
2903
+ const quoteStatus = await resolveDictionaryEntryValue(
2904
+ em,
2905
+ parsed.statusEntryId ?? null
2906
+ );
2283
2907
  const quoteId = randomUUID();
2284
2908
  const quote = em.create(SalesQuote, {
2285
2909
  id: quoteId,
@@ -2370,6 +2994,26 @@ const createQuoteCommand = {
2370
2994
  lineNumber: line.lineNumber ?? index + 1
2371
2995
  })
2372
2996
  );
2997
+ const uomResolver = createUomResolver();
2998
+ const normalizedLineInputs = await Promise.all(
2999
+ lineInputs.map(async (line) => {
3000
+ const normalized = await normalizeLineUom({
3001
+ em,
3002
+ resolver: uomResolver,
3003
+ organizationId: parsed.organizationId,
3004
+ tenantId: parsed.tenantId,
3005
+ line
3006
+ });
3007
+ return {
3008
+ ...line,
3009
+ quantity: normalized.quantity,
3010
+ quantityUnit: normalized.quantityUnit,
3011
+ normalizedQuantity: normalized.normalizedQuantity,
3012
+ normalizedUnit: normalized.normalizedUnit,
3013
+ uomSnapshot: normalized.uomSnapshot
3014
+ };
3015
+ })
3016
+ );
2373
3017
  const adjustmentInputs = parsed.adjustments ? parsed.adjustments.map(
2374
3018
  (adj) => quoteAdjustmentCreateSchema.parse({
2375
3019
  ...adj,
@@ -2378,7 +3022,7 @@ const createQuoteCommand = {
2378
3022
  quoteId: quote.id
2379
3023
  })
2380
3024
  ) : null;
2381
- const lineSnapshots = lineInputs.map(
3025
+ const lineSnapshots = normalizedLineInputs.map(
2382
3026
  (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
2383
3027
  );
2384
3028
  const adjustmentDrafts = adjustmentInputs ? adjustmentInputs.map((adj) => createAdjustmentDraftFromInput(adj)) : [];
@@ -2400,7 +3044,7 @@ const createQuoteCommand = {
2400
3044
  adjustments: adjustmentDrafts,
2401
3045
  context: calculationContext
2402
3046
  });
2403
- await replaceQuoteLines(em, quote, calculation, lineInputs);
3047
+ await replaceQuoteLines(em, quote, calculation, normalizedLineInputs);
2404
3048
  await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
2405
3049
  applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
2406
3050
  let eventBus = null;
@@ -2428,7 +3072,9 @@ const createQuoteCommand = {
2428
3072
  await em.flush();
2429
3073
  try {
2430
3074
  const notificationService = resolveNotificationService(ctx.container);
2431
- const typeDef = notificationTypes.find((type) => type.type === "sales.quote.created");
3075
+ const typeDef = notificationTypes.find(
3076
+ (type) => type.type === "sales.quote.created"
3077
+ );
2432
3078
  if (typeDef) {
2433
3079
  const totalAmount = quote.grandTotalGrossAmount && quote.currencyCode ? `${quote.grandTotalGrossAmount} ${quote.currencyCode}` : "";
2434
3080
  const totalDisplay = totalAmount ? ` (${totalAmount})` : "";
@@ -2449,7 +3095,10 @@ const createQuoteCommand = {
2449
3095
  });
2450
3096
  }
2451
3097
  } catch (err) {
2452
- console.error("[sales.quotes.create] Failed to create notification:", err);
3098
+ console.error(
3099
+ "[sales.quotes.create] Failed to create notification:",
3100
+ err
3101
+ );
2453
3102
  }
2454
3103
  const dataEngine = ctx.container.resolve("dataEngine");
2455
3104
  await emitCrudSideEffects({
@@ -2468,7 +3117,11 @@ const createQuoteCommand = {
2468
3117
  await invalidateCrudCache(
2469
3118
  ctx.container,
2470
3119
  resourceKind,
2471
- { id: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },
3120
+ {
3121
+ id: quote.id,
3122
+ organizationId: quote.organizationId,
3123
+ tenantId: quote.tenantId
3124
+ },
2472
3125
  ctx.auth?.tenantId ?? null,
2473
3126
  "created"
2474
3127
  );
@@ -2517,7 +3170,11 @@ const deleteQuoteCommand = {
2517
3170
  const em = ctx.container.resolve("em");
2518
3171
  const snapshot = await loadQuoteSnapshot(em, id);
2519
3172
  if (snapshot) {
2520
- ensureQuoteScope(ctx, snapshot.quote.organizationId, snapshot.quote.tenantId);
3173
+ ensureQuoteScope(
3174
+ ctx,
3175
+ snapshot.quote.organizationId,
3176
+ snapshot.quote.tenantId
3177
+ );
2521
3178
  }
2522
3179
  return snapshot ? { before: snapshot } : {};
2523
3180
  },
@@ -2525,18 +3182,34 @@ const deleteQuoteCommand = {
2525
3182
  const id = requireId(input, "Quote id is required");
2526
3183
  const em = ctx.container.resolve("em").fork();
2527
3184
  const quote = await em.findOne(SalesQuote, { id });
2528
- if (!quote) throw new CrudHttpError(404, { error: "Sales quote not found" });
3185
+ if (!quote)
3186
+ throw new CrudHttpError(404, { error: "Sales quote not found" });
2529
3187
  ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
2530
3188
  const [addresses, notes, tags, adjustments, lines] = await Promise.all([
2531
- em.find(SalesDocumentAddress, { documentId: quote.id, documentKind: "quote" }),
3189
+ em.find(SalesDocumentAddress, {
3190
+ documentId: quote.id,
3191
+ documentKind: "quote"
3192
+ }),
2532
3193
  em.find(SalesNote, { contextType: "quote", contextId: quote.id }),
2533
- em.find(SalesDocumentTagAssignment, { documentId: quote.id, documentKind: "quote" }),
3194
+ em.find(SalesDocumentTagAssignment, {
3195
+ documentId: quote.id,
3196
+ documentKind: "quote"
3197
+ }),
2534
3198
  em.find(SalesQuoteAdjustment, { quote: quote.id }),
2535
3199
  em.find(SalesQuoteLine, { quote: quote.id })
2536
3200
  ]);
2537
- await em.nativeDelete(SalesDocumentAddress, { documentId: quote.id, documentKind: "quote" });
2538
- await em.nativeDelete(SalesNote, { contextType: "quote", contextId: quote.id });
2539
- await em.nativeDelete(SalesDocumentTagAssignment, { documentId: quote.id, documentKind: "quote" });
3201
+ await em.nativeDelete(SalesDocumentAddress, {
3202
+ documentId: quote.id,
3203
+ documentKind: "quote"
3204
+ });
3205
+ await em.nativeDelete(SalesNote, {
3206
+ contextType: "quote",
3207
+ contextId: quote.id
3208
+ });
3209
+ await em.nativeDelete(SalesDocumentTagAssignment, {
3210
+ documentId: quote.id,
3211
+ documentKind: "quote"
3212
+ });
2540
3213
  await em.nativeDelete(SalesQuoteAdjustment, { quote: quote.id });
2541
3214
  await em.nativeDelete(SalesQuoteLine, { quote: quote.id });
2542
3215
  em.remove(quote);
@@ -2545,10 +3218,22 @@ const deleteQuoteCommand = {
2545
3218
  await Promise.all([
2546
3219
  queueDeletionSideEffects(dataEngine, quote, E.sales.sales_quote),
2547
3220
  queueDeletionSideEffects(dataEngine, lines, E.sales.sales_quote_line),
2548
- queueDeletionSideEffects(dataEngine, adjustments, E.sales.sales_quote_adjustment),
2549
- queueDeletionSideEffects(dataEngine, addresses, E.sales.sales_document_address),
3221
+ queueDeletionSideEffects(
3222
+ dataEngine,
3223
+ adjustments,
3224
+ E.sales.sales_quote_adjustment
3225
+ ),
3226
+ queueDeletionSideEffects(
3227
+ dataEngine,
3228
+ addresses,
3229
+ E.sales.sales_document_address
3230
+ ),
2550
3231
  queueDeletionSideEffects(dataEngine, notes, E.sales.sales_note),
2551
- queueDeletionSideEffects(dataEngine, tags, E.sales.sales_document_tag_assignment)
3232
+ queueDeletionSideEffects(
3233
+ dataEngine,
3234
+ tags,
3235
+ E.sales.sales_document_tag_assignment
3236
+ )
2552
3237
  ]);
2553
3238
  return { quoteId: id };
2554
3239
  },
@@ -2587,15 +3272,23 @@ const updateQuoteCommand = {
2587
3272
  const em = ctx.container.resolve("em");
2588
3273
  const snapshot = await loadQuoteSnapshot(em, parsed.id);
2589
3274
  if (snapshot) {
2590
- ensureQuoteScope(ctx, snapshot.quote.organizationId, snapshot.quote.tenantId);
3275
+ ensureQuoteScope(
3276
+ ctx,
3277
+ snapshot.quote.organizationId,
3278
+ snapshot.quote.tenantId
3279
+ );
2591
3280
  }
2592
3281
  return snapshot ? { before: snapshot } : {};
2593
3282
  },
2594
3283
  async execute(rawInput, ctx) {
2595
3284
  const parsed = documentUpdateSchema.parse(rawInput ?? {});
2596
3285
  const em = ctx.container.resolve("em").fork();
2597
- const quote = await em.findOne(SalesQuote, { id: parsed.id, deletedAt: null });
2598
- if (!quote) throw new CrudHttpError(404, { error: "Sales quote not found" });
3286
+ const quote = await em.findOne(SalesQuote, {
3287
+ id: parsed.id,
3288
+ deletedAt: null
3289
+ });
3290
+ if (!quote)
3291
+ throw new CrudHttpError(404, { error: "Sales quote not found" });
2599
3292
  ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
2600
3293
  const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
2601
3294
  if (shouldInvalidateSentToken) {
@@ -2603,7 +3296,12 @@ const updateQuoteCommand = {
2603
3296
  quote.sentAt = null;
2604
3297
  }
2605
3298
  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;
2606
- await applyDocumentUpdate({ kind: "quote", entity: quote, input: parsed, em });
3299
+ await applyDocumentUpdate({
3300
+ kind: "quote",
3301
+ entity: quote,
3302
+ input: parsed,
3303
+ em
3304
+ });
2607
3305
  await em.flush();
2608
3306
  if (shouldInvalidateSentToken) {
2609
3307
  quote.status = "draft";
@@ -2616,7 +3314,11 @@ const updateQuoteCommand = {
2616
3314
  if (shouldRecalculateTotals) {
2617
3315
  const [existingLines, adjustments] = await Promise.all([
2618
3316
  em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
2619
- em.find(SalesQuoteAdjustment, { quote }, { orderBy: { position: "asc" } })
3317
+ em.find(
3318
+ SalesQuoteAdjustment,
3319
+ { quote },
3320
+ { orderBy: { position: "asc" } }
3321
+ )
2620
3322
  ]);
2621
3323
  const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
2622
3324
  const calcLines = lineSnapshots.map(
@@ -2635,7 +3337,9 @@ const updateQuoteCommand = {
2635
3337
  )
2636
3338
  );
2637
3339
  const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
2638
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
3340
+ const salesCalculationService = ctx.container.resolve(
3341
+ "salesCalculationService"
3342
+ );
2639
3343
  const calculationContext = buildCalculationContext({
2640
3344
  tenantId: quote.tenantId,
2641
3345
  organizationId: quote.organizationId,
@@ -2647,12 +3351,14 @@ const updateQuoteCommand = {
2647
3351
  shippingMethodCode: quote.shippingMethodCode ?? null,
2648
3352
  paymentMethodCode: quote.paymentMethodCode ?? null
2649
3353
  });
2650
- const calculation = await salesCalculationService.calculateDocumentTotals({
2651
- documentKind: "quote",
2652
- lines: calcLines,
2653
- adjustments: adjustmentDrafts,
2654
- context: calculationContext
2655
- });
3354
+ const calculation = await salesCalculationService.calculateDocumentTotals(
3355
+ {
3356
+ documentKind: "quote",
3357
+ lines: calcLines,
3358
+ adjustments: adjustmentDrafts,
3359
+ context: calculationContext
3360
+ }
3361
+ );
2656
3362
  const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
2657
3363
  organizationId: quote.organizationId,
2658
3364
  tenantId: quote.tenantId,
@@ -2694,7 +3400,11 @@ const updateQuoteCommand = {
2694
3400
  await invalidateCrudCache(
2695
3401
  ctx.container,
2696
3402
  resourceKind,
2697
- { id: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },
3403
+ {
3404
+ id: quote.id,
3405
+ organizationId: quote.organizationId,
3406
+ tenantId: quote.tenantId
3407
+ },
2698
3408
  ctx.auth?.tenantId ?? null,
2699
3409
  "updated"
2700
3410
  );
@@ -2753,25 +3463,42 @@ const updateOrderCommand = {
2753
3463
  const em = ctx.container.resolve("em");
2754
3464
  const snapshot = await loadOrderSnapshot(em, parsed.id);
2755
3465
  if (snapshot) {
2756
- ensureOrderScope(ctx, snapshot.order.organizationId, snapshot.order.tenantId);
3466
+ ensureOrderScope(
3467
+ ctx,
3468
+ snapshot.order.organizationId,
3469
+ snapshot.order.tenantId
3470
+ );
2757
3471
  }
2758
3472
  return snapshot ? { before: snapshot } : {};
2759
3473
  },
2760
3474
  async execute(rawInput, ctx) {
2761
3475
  const parsed = documentUpdateSchema.parse(rawInput ?? {});
2762
3476
  const em = ctx.container.resolve("em").fork();
2763
- const order = await em.findOne(SalesOrder, { id: parsed.id, deletedAt: null });
2764
- if (!order) throw new CrudHttpError(404, { error: "Sales order not found" });
3477
+ const order = await em.findOne(SalesOrder, {
3478
+ id: parsed.id,
3479
+ deletedAt: null
3480
+ });
3481
+ if (!order)
3482
+ throw new CrudHttpError(404, { error: "Sales order not found" });
2765
3483
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
2766
3484
  const previousStatus = normalizeStatusValue(order.status);
2767
3485
  let statusChangeNote = null;
2768
3486
  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;
2769
- await applyDocumentUpdate({ kind: "order", entity: order, input: parsed, em });
3487
+ await applyDocumentUpdate({
3488
+ kind: "order",
3489
+ entity: order,
3490
+ input: parsed,
3491
+ em
3492
+ });
2770
3493
  await em.flush();
2771
3494
  if (shouldRecalculateTotals) {
2772
3495
  const [existingLines, adjustments] = await Promise.all([
2773
3496
  em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
2774
- em.find(SalesOrderAdjustment, { order }, { orderBy: { position: "asc" } })
3497
+ em.find(
3498
+ SalesOrderAdjustment,
3499
+ { order },
3500
+ { orderBy: { position: "asc" } }
3501
+ )
2775
3502
  ]);
2776
3503
  const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
2777
3504
  const calcLines = lineSnapshots.map(
@@ -2790,7 +3517,9 @@ const updateOrderCommand = {
2790
3517
  )
2791
3518
  );
2792
3519
  const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
2793
- const salesCalculationService = ctx.container.resolve("salesCalculationService");
3520
+ const salesCalculationService = ctx.container.resolve(
3521
+ "salesCalculationService"
3522
+ );
2794
3523
  const calculationContext = buildCalculationContext({
2795
3524
  tenantId: order.tenantId,
2796
3525
  organizationId: order.organizationId,
@@ -2802,13 +3531,15 @@ const updateOrderCommand = {
2802
3531
  shippingMethodCode: order.shippingMethodCode ?? null,
2803
3532
  paymentMethodCode: order.paymentMethodCode ?? null
2804
3533
  });
2805
- const calculation = await salesCalculationService.calculateDocumentTotals({
2806
- documentKind: "order",
2807
- lines: calcLines,
2808
- adjustments: adjustmentDrafts,
2809
- context: calculationContext,
2810
- existingTotals: resolveExistingPaymentTotals(order)
2811
- });
3534
+ const calculation = await salesCalculationService.calculateDocumentTotals(
3535
+ {
3536
+ documentKind: "order",
3537
+ lines: calcLines,
3538
+ adjustments: adjustmentDrafts,
3539
+ context: calculationContext,
3540
+ existingTotals: resolveExistingPaymentTotals(order)
3541
+ }
3542
+ );
2812
3543
  const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
2813
3544
  organizationId: order.organizationId,
2814
3545
  tenantId: order.tenantId,
@@ -2870,7 +3601,11 @@ const updateOrderCommand = {
2870
3601
  await invalidateCrudCache(
2871
3602
  ctx.container,
2872
3603
  resourceKind,
2873
- { id: order.id, organizationId: order.organizationId, tenantId: order.tenantId },
3604
+ {
3605
+ id: order.id,
3606
+ organizationId: order.organizationId,
3607
+ tenantId: order.tenantId
3608
+ },
2874
3609
  ctx.auth?.tenantId ?? null,
2875
3610
  "updated"
2876
3611
  );
@@ -2925,7 +3660,9 @@ const updateOrderCommand = {
2925
3660
  const createOrderCommand = {
2926
3661
  id: "sales.orders.create",
2927
3662
  async execute(rawInput, ctx) {
2928
- const generator = ctx.container.resolve("salesDocumentNumberGenerator");
3663
+ const generator = ctx.container.resolve(
3664
+ "salesDocumentNumberGenerator"
3665
+ );
2929
3666
  const initial = orderCreateSchema.parse(rawInput ?? {});
2930
3667
  const orderNumber = typeof initial.orderNumber === "string" && initial.orderNumber.trim().length ? initial.orderNumber.trim() : (await generator.generate({
2931
3668
  kind: "order",
@@ -3059,6 +3796,26 @@ const createOrderCommand = {
3059
3796
  lineNumber: line.lineNumber ?? index + 1
3060
3797
  })
3061
3798
  );
3799
+ const uomResolver = createUomResolver();
3800
+ const normalizedLineInputs = await Promise.all(
3801
+ lineInputs.map(async (line) => {
3802
+ const normalized = await normalizeLineUom({
3803
+ em,
3804
+ resolver: uomResolver,
3805
+ organizationId: parsed.organizationId,
3806
+ tenantId: parsed.tenantId,
3807
+ line
3808
+ });
3809
+ return {
3810
+ ...line,
3811
+ quantity: normalized.quantity,
3812
+ quantityUnit: normalized.quantityUnit,
3813
+ normalizedQuantity: normalized.normalizedQuantity,
3814
+ normalizedUnit: normalized.normalizedUnit,
3815
+ uomSnapshot: normalized.uomSnapshot
3816
+ };
3817
+ })
3818
+ );
3062
3819
  const adjustmentInputs = parsed.adjustments ? parsed.adjustments.map(
3063
3820
  (adj) => orderAdjustmentCreateSchema.parse({
3064
3821
  ...adj,
@@ -3067,7 +3824,7 @@ const createOrderCommand = {
3067
3824
  orderId: order.id
3068
3825
  })
3069
3826
  ) : null;
3070
- const lineSnapshots = lineInputs.map(
3827
+ const lineSnapshots = normalizedLineInputs.map(
3071
3828
  (line, index) => createLineSnapshotFromInput(line, line.lineNumber ?? index + 1)
3072
3829
  );
3073
3830
  const adjustmentDrafts = adjustmentInputs ? adjustmentInputs.map((adj) => createAdjustmentDraftFromInput(adj)) : [];
@@ -3090,7 +3847,7 @@ const createOrderCommand = {
3090
3847
  context: calculationContext,
3091
3848
  existingTotals: resolveExistingPaymentTotals(order)
3092
3849
  });
3093
- await replaceOrderLines(em, order, calculation, lineInputs);
3850
+ await replaceOrderLines(em, order, calculation, normalizedLineInputs);
3094
3851
  await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
3095
3852
  applyOrderTotals(order, calculation.totals, calculation.lines.length);
3096
3853
  let eventBus = null;
@@ -3118,7 +3875,9 @@ const createOrderCommand = {
3118
3875
  await em.flush();
3119
3876
  try {
3120
3877
  const notificationService = resolveNotificationService(ctx.container);
3121
- const typeDef = notificationTypes.find((type) => type.type === "sales.order.created");
3878
+ const typeDef = notificationTypes.find(
3879
+ (type) => type.type === "sales.order.created"
3880
+ );
3122
3881
  if (typeDef) {
3123
3882
  const totalAmount = order.grandTotalGrossAmount && order.currencyCode ? `${order.grandTotalGrossAmount} ${order.currencyCode}` : "";
3124
3883
  const totalDisplay = totalAmount ? ` (${totalAmount})` : "";
@@ -3139,7 +3898,10 @@ const createOrderCommand = {
3139
3898
  });
3140
3899
  }
3141
3900
  } catch (err) {
3142
- console.error("[sales.orders.create] Failed to create notification:", err);
3901
+ console.error(
3902
+ "[sales.orders.create] Failed to create notification:",
3903
+ err
3904
+ );
3143
3905
  }
3144
3906
  const dataEngine = ctx.container.resolve("dataEngine");
3145
3907
  await emitCrudSideEffects({
@@ -3158,7 +3920,11 @@ const createOrderCommand = {
3158
3920
  await invalidateCrudCache(
3159
3921
  ctx.container,
3160
3922
  resourceKind,
3161
- { id: order.id, organizationId: order.organizationId, tenantId: order.tenantId },
3923
+ {
3924
+ id: order.id,
3925
+ organizationId: order.organizationId,
3926
+ tenantId: order.tenantId
3927
+ },
3162
3928
  ctx.auth?.tenantId ?? null,
3163
3929
  "created"
3164
3930
  );
@@ -3207,7 +3973,11 @@ const deleteOrderCommand = {
3207
3973
  const em = ctx.container.resolve("em");
3208
3974
  const snapshot = await loadOrderSnapshot(em, id);
3209
3975
  if (snapshot) {
3210
- ensureOrderScope(ctx, snapshot.order.organizationId, snapshot.order.tenantId);
3976
+ ensureOrderScope(
3977
+ ctx,
3978
+ snapshot.order.organizationId,
3979
+ snapshot.order.tenantId
3980
+ );
3211
3981
  }
3212
3982
  return snapshot ? { before: snapshot } : {};
3213
3983
  },
@@ -3215,29 +3985,56 @@ const deleteOrderCommand = {
3215
3985
  const id = requireId(input, "Order id is required");
3216
3986
  const em = ctx.container.resolve("em").fork();
3217
3987
  const order = await em.findOne(SalesOrder, { id });
3218
- if (!order) throw new CrudHttpError(404, { error: "Sales order not found" });
3988
+ if (!order)
3989
+ throw new CrudHttpError(404, { error: "Sales order not found" });
3219
3990
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
3220
3991
  const shipments = await em.find(SalesShipment, { order: order.id });
3221
3992
  const shipmentIds = shipments.map((entry) => entry.id);
3222
- const [shipmentItems, payments, paymentAllocations, addresses, notes, tags, adjustments, lines] = await Promise.all([
3993
+ const [
3994
+ shipmentItems,
3995
+ payments,
3996
+ paymentAllocations,
3997
+ addresses,
3998
+ notes,
3999
+ tags,
4000
+ adjustments,
4001
+ lines
4002
+ ] = await Promise.all([
3223
4003
  shipmentIds.length ? em.find(SalesShipmentItem, { shipment: { $in: shipmentIds } }) : Promise.resolve([]),
3224
4004
  em.find(SalesPayment, { order: order.id }),
3225
4005
  em.find(SalesPaymentAllocation, { order: order.id }),
3226
- em.find(SalesDocumentAddress, { documentId: order.id, documentKind: "order" }),
4006
+ em.find(SalesDocumentAddress, {
4007
+ documentId: order.id,
4008
+ documentKind: "order"
4009
+ }),
3227
4010
  em.find(SalesNote, { contextType: "order", contextId: order.id }),
3228
- em.find(SalesDocumentTagAssignment, { documentId: order.id, documentKind: "order" }),
4011
+ em.find(SalesDocumentTagAssignment, {
4012
+ documentId: order.id,
4013
+ documentKind: "order"
4014
+ }),
3229
4015
  em.find(SalesOrderAdjustment, { order: order.id }),
3230
4016
  em.find(SalesOrderLine, { order: order.id })
3231
4017
  ]);
3232
4018
  if (shipmentIds.length) {
3233
- await em.nativeDelete(SalesShipmentItem, { shipment: { $in: shipmentIds } });
4019
+ await em.nativeDelete(SalesShipmentItem, {
4020
+ shipment: { $in: shipmentIds }
4021
+ });
3234
4022
  await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
3235
4023
  }
3236
4024
  await em.nativeDelete(SalesPaymentAllocation, { order: order.id });
3237
4025
  await em.nativeDelete(SalesPayment, { order: order.id });
3238
- await em.nativeDelete(SalesDocumentAddress, { documentId: order.id, documentKind: "order" });
3239
- await em.nativeDelete(SalesNote, { contextType: "order", contextId: order.id });
3240
- await em.nativeDelete(SalesDocumentTagAssignment, { documentId: order.id, documentKind: "order" });
4026
+ await em.nativeDelete(SalesDocumentAddress, {
4027
+ documentId: order.id,
4028
+ documentKind: "order"
4029
+ });
4030
+ await em.nativeDelete(SalesNote, {
4031
+ contextType: "order",
4032
+ contextId: order.id
4033
+ });
4034
+ await em.nativeDelete(SalesDocumentTagAssignment, {
4035
+ documentId: order.id,
4036
+ documentKind: "order"
4037
+ });
3241
4038
  await em.nativeDelete(SalesOrderAdjustment, { order: order.id });
3242
4039
  await em.nativeDelete(SalesOrderLine, { order: order.id });
3243
4040
  em.remove(order);
@@ -3246,14 +4043,34 @@ const deleteOrderCommand = {
3246
4043
  await Promise.all([
3247
4044
  queueDeletionSideEffects(dataEngine, order, E.sales.sales_order),
3248
4045
  queueDeletionSideEffects(dataEngine, lines, E.sales.sales_order_line),
3249
- queueDeletionSideEffects(dataEngine, adjustments, E.sales.sales_order_adjustment),
4046
+ queueDeletionSideEffects(
4047
+ dataEngine,
4048
+ adjustments,
4049
+ E.sales.sales_order_adjustment
4050
+ ),
3250
4051
  queueDeletionSideEffects(dataEngine, shipments, E.sales.sales_shipment),
3251
- queueDeletionSideEffects(dataEngine, shipmentItems, E.sales.sales_shipment_item),
4052
+ queueDeletionSideEffects(
4053
+ dataEngine,
4054
+ shipmentItems,
4055
+ E.sales.sales_shipment_item
4056
+ ),
3252
4057
  queueDeletionSideEffects(dataEngine, payments, E.sales.sales_payment),
3253
- queueDeletionSideEffects(dataEngine, paymentAllocations, E.sales.sales_payment_allocation),
3254
- queueDeletionSideEffects(dataEngine, addresses, E.sales.sales_document_address),
4058
+ queueDeletionSideEffects(
4059
+ dataEngine,
4060
+ paymentAllocations,
4061
+ E.sales.sales_payment_allocation
4062
+ ),
4063
+ queueDeletionSideEffects(
4064
+ dataEngine,
4065
+ addresses,
4066
+ E.sales.sales_document_address
4067
+ ),
3255
4068
  queueDeletionSideEffects(dataEngine, notes, E.sales.sales_note),
3256
- queueDeletionSideEffects(dataEngine, tags, E.sales.sales_document_tag_assignment)
4069
+ queueDeletionSideEffects(
4070
+ dataEngine,
4071
+ tags,
4072
+ E.sales.sales_document_tag_assignment
4073
+ )
3257
4074
  ]);
3258
4075
  return { orderId: id };
3259
4076
  },
@@ -3298,24 +4115,54 @@ const convertQuoteToOrderCommand = {
3298
4115
  if (!quoteId) return {};
3299
4116
  const em = ctx.container.resolve("em");
3300
4117
  const snapshot = await loadQuoteSnapshot(em, quoteId);
3301
- if (snapshot) ensureQuoteScope(ctx, snapshot.quote.organizationId, snapshot.quote.tenantId);
4118
+ if (snapshot)
4119
+ ensureQuoteScope(
4120
+ ctx,
4121
+ snapshot.quote.organizationId,
4122
+ snapshot.quote.tenantId
4123
+ );
3302
4124
  return snapshot ? { before: snapshot } : {};
3303
4125
  },
3304
4126
  async execute(rawInput, ctx) {
3305
4127
  const payload = quoteConvertToOrderSchema.parse(rawInput ?? {});
3306
4128
  const em = ctx.container.resolve("em").fork();
3307
- const quote = await em.findOne(SalesQuote, { id: payload.quoteId, deletedAt: null });
4129
+ const quote = await em.findOne(SalesQuote, {
4130
+ id: payload.quoteId,
4131
+ deletedAt: null
4132
+ });
3308
4133
  const { translate } = await resolveTranslations();
3309
- if (!quote) throw new CrudHttpError(404, { error: translate("sales.documents.detail.error", "Document not found or inaccessible.") });
4134
+ if (!quote)
4135
+ throw new CrudHttpError(404, {
4136
+ error: translate(
4137
+ "sales.documents.detail.error",
4138
+ "Document not found or inaccessible."
4139
+ )
4140
+ });
3310
4141
  ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
3311
4142
  const snapshot = await loadQuoteSnapshot(em, payload.quoteId);
3312
- if (!snapshot) throw new CrudHttpError(404, { error: translate("sales.documents.detail.error", "Document not found or inaccessible.") });
4143
+ if (!snapshot)
4144
+ throw new CrudHttpError(404, {
4145
+ error: translate(
4146
+ "sales.documents.detail.error",
4147
+ "Document not found or inaccessible."
4148
+ )
4149
+ });
3313
4150
  const orderId = payload.orderId ?? quote.id;
3314
- const existingOrder = await em.findOne(SalesOrder, { id: orderId, deletedAt: null });
4151
+ const existingOrder = await em.findOne(SalesOrder, {
4152
+ id: orderId,
4153
+ deletedAt: null
4154
+ });
3315
4155
  if (existingOrder) {
3316
- throw new CrudHttpError(409, { error: translate("sales.documents.detail.convertExists", "Order already exists for this quote.") });
4156
+ throw new CrudHttpError(409, {
4157
+ error: translate(
4158
+ "sales.documents.detail.convertExists",
4159
+ "Order already exists for this quote."
4160
+ )
4161
+ });
3317
4162
  }
3318
- const generator = ctx.container.resolve("salesDocumentNumberGenerator");
4163
+ const generator = ctx.container.resolve(
4164
+ "salesDocumentNumberGenerator"
4165
+ );
3319
4166
  const generatedNumber = snapshot.quote.quoteNumber && snapshot.quote.quoteNumber.trim().length ? snapshot.quote.quoteNumber : (await generator.generate({
3320
4167
  kind: "order",
3321
4168
  organizationId: snapshot.quote.organizationId,
@@ -3328,14 +4175,23 @@ const convertQuoteToOrderCommand = {
3328
4175
  entityId: E.sales.sales_quote,
3329
4176
  recordIds: [snapshot.quote.id],
3330
4177
  tenantIdByRecord: { [snapshot.quote.id]: snapshot.quote.tenantId },
3331
- organizationIdByRecord: { [snapshot.quote.id]: snapshot.quote.organizationId }
4178
+ organizationIdByRecord: {
4179
+ [snapshot.quote.id]: snapshot.quote.organizationId
4180
+ }
3332
4181
  }),
3333
4182
  snapshot.lines.length ? loadCustomFieldValues({
3334
4183
  em,
3335
4184
  entityId: E.sales.sales_quote_line,
3336
4185
  recordIds: snapshot.lines.map((line) => line.id),
3337
- tenantIdByRecord: Object.fromEntries(snapshot.lines.map((line) => [line.id, snapshot.quote.tenantId])),
3338
- organizationIdByRecord: Object.fromEntries(snapshot.lines.map((line) => [line.id, snapshot.quote.organizationId]))
4186
+ tenantIdByRecord: Object.fromEntries(
4187
+ snapshot.lines.map((line) => [line.id, snapshot.quote.tenantId])
4188
+ ),
4189
+ organizationIdByRecord: Object.fromEntries(
4190
+ snapshot.lines.map((line) => [
4191
+ line.id,
4192
+ snapshot.quote.organizationId
4193
+ ])
4194
+ )
3339
4195
  }) : Promise.resolve({})
3340
4196
  ]);
3341
4197
  const order = em.create(SalesOrder, {
@@ -3415,6 +4271,9 @@ const convertQuoteToOrderCommand = {
3415
4271
  comment: line.comment ?? null,
3416
4272
  quantity: line.quantity,
3417
4273
  quantityUnit: line.quantityUnit ?? null,
4274
+ normalizedQuantity: line.normalizedQuantity ?? line.quantity,
4275
+ normalizedUnit: line.normalizedUnit ?? line.quantityUnit ?? null,
4276
+ uomSnapshot: line.uomSnapshot ? cloneJson(line.uomSnapshot) : null,
3418
4277
  reservedQuantity: "0",
3419
4278
  fulfilledQuantity: "0",
3420
4279
  invoicedQuantity: "0",
@@ -3466,9 +4325,18 @@ const convertQuoteToOrderCommand = {
3466
4325
  em.persist(entity);
3467
4326
  });
3468
4327
  const [addresses, notes, tags] = await Promise.all([
3469
- em.find(SalesDocumentAddress, { documentId: snapshot.quote.id, documentKind: "quote" }),
3470
- em.find(SalesNote, { contextType: "quote", contextId: snapshot.quote.id }),
3471
- em.find(SalesDocumentTagAssignment, { documentId: snapshot.quote.id, documentKind: "quote" })
4328
+ em.find(SalesDocumentAddress, {
4329
+ documentId: snapshot.quote.id,
4330
+ documentKind: "quote"
4331
+ }),
4332
+ em.find(SalesNote, {
4333
+ contextType: "quote",
4334
+ contextId: snapshot.quote.id
4335
+ }),
4336
+ em.find(SalesDocumentTagAssignment, {
4337
+ documentId: snapshot.quote.id,
4338
+ documentKind: "quote"
4339
+ })
3472
4340
  ]);
3473
4341
  addresses.forEach((entry) => {
3474
4342
  entry.documentKind = "order";
@@ -3531,7 +4399,10 @@ const convertQuoteToOrderCommand = {
3531
4399
  if (!before) return null;
3532
4400
  const { translate } = await resolveTranslations();
3533
4401
  return {
3534
- actionLabel: translate("sales.audit.quotes.convert", "Convert quote to order"),
4402
+ actionLabel: translate(
4403
+ "sales.audit.quotes.convert",
4404
+ "Convert quote to order"
4405
+ ),
3535
4406
  resourceKind: "sales.order",
3536
4407
  resourceId: result.orderId,
3537
4408
  tenantId: before.quote.tenantId,
@@ -3539,7 +4410,10 @@ const convertQuoteToOrderCommand = {
3539
4410
  snapshotBefore: before,
3540
4411
  snapshotAfter: after ?? null,
3541
4412
  payload: {
3542
- undo: { quote: before, order: after ?? null }
4413
+ undo: {
4414
+ quote: before,
4415
+ order: after ?? null
4416
+ }
3543
4417
  }
3544
4418
  };
3545
4419
  },
@@ -3549,7 +4423,11 @@ const convertQuoteToOrderCommand = {
3549
4423
  const orderSnapshot = payload?.order;
3550
4424
  if (!quoteSnapshot) return;
3551
4425
  const em = ctx.container.resolve("em").fork();
3552
- ensureQuoteScope(ctx, quoteSnapshot.quote.organizationId, quoteSnapshot.quote.tenantId);
4426
+ ensureQuoteScope(
4427
+ ctx,
4428
+ quoteSnapshot.quote.organizationId,
4429
+ quoteSnapshot.quote.tenantId
4430
+ );
3553
4431
  if (orderSnapshot) {
3554
4432
  const orderId = orderSnapshot.order.id;
3555
4433
  const orderLineIds = orderSnapshot.lines.map((line) => line.id);
@@ -3558,20 +4436,34 @@ const convertQuoteToOrderCommand = {
3558
4436
  const shipments = await em.find(SalesShipment, { order: orderId });
3559
4437
  const shipmentIds = shipments.map((entry) => entry.id);
3560
4438
  if (shipmentIds.length) {
3561
- await em.nativeDelete(SalesShipmentItem, { shipment: { $in: shipmentIds } });
4439
+ await em.nativeDelete(SalesShipmentItem, {
4440
+ shipment: { $in: shipmentIds }
4441
+ });
3562
4442
  await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
3563
4443
  }
3564
4444
  await em.nativeDelete(SalesPaymentAllocation, { order: orderId });
3565
4445
  await em.nativeDelete(SalesPayment, { order: orderId });
3566
- await em.nativeDelete(SalesDocumentAddress, { documentId: orderId, documentKind: "order" });
3567
- await em.nativeDelete(SalesDocumentTagAssignment, { documentId: orderId, documentKind: "order" });
4446
+ await em.nativeDelete(SalesDocumentAddress, {
4447
+ documentId: orderId,
4448
+ documentKind: "order"
4449
+ });
4450
+ await em.nativeDelete(SalesDocumentTagAssignment, {
4451
+ documentId: orderId,
4452
+ documentKind: "order"
4453
+ });
3568
4454
  await em.nativeDelete(SalesOrderAdjustment, { order: orderId });
3569
4455
  await em.nativeDelete(SalesOrderLine, { order: orderId });
3570
4456
  em.remove(existingOrder);
3571
4457
  }
3572
- await em.nativeDelete(CustomFieldValue, { entityId: E.sales.sales_order, recordId: orderId });
4458
+ await em.nativeDelete(CustomFieldValue, {
4459
+ entityId: E.sales.sales_order,
4460
+ recordId: orderId
4461
+ });
3573
4462
  if (orderLineIds.length) {
3574
- await em.nativeDelete(CustomFieldValue, { entityId: E.sales.sales_order_line, recordId: { $in: orderLineIds } });
4463
+ await em.nativeDelete(CustomFieldValue, {
4464
+ entityId: E.sales.sales_order_line,
4465
+ recordId: { $in: orderLineIds }
4466
+ });
3575
4467
  }
3576
4468
  }
3577
4469
  const noteIds = quoteSnapshot.notes.map((note) => note.id);
@@ -3582,22 +4474,30 @@ const convertQuoteToOrderCommand = {
3582
4474
  await em.flush();
3583
4475
  }
3584
4476
  };
3585
- const orderLineUpsertSchema = orderLineCreateSchema.extend({ id: z.string().uuid().optional() });
4477
+ const orderLineUpsertSchema = orderLineCreateSchema.extend({
4478
+ id: z.string().uuid().optional()
4479
+ });
3586
4480
  const orderLineDeleteSchema = z.object({
3587
4481
  id: z.string().uuid(),
3588
4482
  orderId: z.string().uuid()
3589
4483
  });
3590
- const quoteLineUpsertSchema = quoteLineCreateSchema.extend({ id: z.string().uuid().optional() });
4484
+ const quoteLineUpsertSchema = quoteLineCreateSchema.extend({
4485
+ id: z.string().uuid().optional()
4486
+ });
3591
4487
  const quoteLineDeleteSchema = z.object({
3592
4488
  id: z.string().uuid(),
3593
4489
  quoteId: z.string().uuid()
3594
4490
  });
3595
- const orderAdjustmentUpsertSchema = orderAdjustmentCreateSchema.extend({ id: z.string().uuid().optional() });
4491
+ const orderAdjustmentUpsertSchema = orderAdjustmentCreateSchema.extend({
4492
+ id: z.string().uuid().optional()
4493
+ });
3596
4494
  const orderAdjustmentDeleteSchema = z.object({
3597
4495
  id: z.string().uuid(),
3598
4496
  orderId: z.string().uuid()
3599
4497
  });
3600
- const quoteAdjustmentUpsertSchema = quoteAdjustmentCreateSchema.extend({ id: z.string().uuid().optional() });
4498
+ const quoteAdjustmentUpsertSchema = quoteAdjustmentCreateSchema.extend({
4499
+ id: z.string().uuid().optional()
4500
+ });
3601
4501
  const quoteAdjustmentDeleteSchema = z.object({
3602
4502
  id: z.string().uuid(),
3603
4503
  quoteId: z.string().uuid()
@@ -3610,18 +4510,32 @@ const orderLineUpsertCommand = {
3610
4510
  if (!orderId) return {};
3611
4511
  const em = ctx.container.resolve("em");
3612
4512
  const snapshot = await loadOrderSnapshot(em, orderId);
3613
- if (snapshot) ensureOrderScope(ctx, snapshot.order.organizationId, snapshot.order.tenantId);
4513
+ if (snapshot)
4514
+ ensureOrderScope(
4515
+ ctx,
4516
+ snapshot.order.organizationId,
4517
+ snapshot.order.tenantId
4518
+ );
3614
4519
  return snapshot ? { before: snapshot } : {};
3615
4520
  },
3616
4521
  async execute(input, ctx) {
3617
- const parsed = orderLineUpsertSchema.parse(input?.body ?? {});
4522
+ const rawBody = input?.body ?? {};
4523
+ const parsed = orderLineUpsertSchema.parse(rawBody);
3618
4524
  const em = ctx.container.resolve("em").fork();
3619
- const order = await em.findOne(SalesOrder, { id: parsed.orderId, deletedAt: null });
3620
- if (!order) throw new CrudHttpError(404, { error: "Sales order not found" });
4525
+ const order = await em.findOne(SalesOrder, {
4526
+ id: parsed.orderId,
4527
+ deletedAt: null
4528
+ });
4529
+ if (!order)
4530
+ throw new CrudHttpError(404, { error: "Sales order not found" });
3621
4531
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
3622
4532
  const [existingLines, adjustments] = await Promise.all([
3623
4533
  em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
3624
- em.find(SalesOrderAdjustment, { order }, { orderBy: { position: "asc" } })
4534
+ em.find(
4535
+ SalesOrderAdjustment,
4536
+ { order },
4537
+ { orderBy: { position: "asc" } }
4538
+ )
3625
4539
  ]);
3626
4540
  const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
3627
4541
  const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
@@ -3632,7 +4546,9 @@ const orderLineUpsertCommand = {
3632
4546
  if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
3633
4547
  let taxService = null;
3634
4548
  try {
3635
- taxService = ctx.container.resolve("taxCalculationService");
4549
+ taxService = ctx.container.resolve(
4550
+ "taxCalculationService"
4551
+ );
3636
4552
  } catch {
3637
4553
  taxService = null;
3638
4554
  }
@@ -3655,6 +4571,49 @@ const orderLineUpsertCommand = {
3655
4571
  if (priceMode) metadata.priceMode = priceMode;
3656
4572
  const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
3657
4573
  const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
4574
+ const lineUomInput = {
4575
+ productId: parsed.productId ?? existingSnapshot?.productId ?? null,
4576
+ productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
4577
+ quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
4578
+ quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
4579
+ normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
4580
+ normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
4581
+ uomSnapshot: existingSnapshot?.uomSnapshot ?? null
4582
+ };
4583
+ const uomResolver = createUomResolver();
4584
+ let normalizedUom = await normalizeLineUom({
4585
+ em,
4586
+ resolver: uomResolver,
4587
+ organizationId: order.organizationId,
4588
+ tenantId: order.tenantId,
4589
+ line: {
4590
+ ...lineUomInput,
4591
+ unitPriceNet: unitPriceNet ?? 0,
4592
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
4593
+ }
4594
+ });
4595
+ const convertedPrices = convertLineUnitPricesOnUnitChange({
4596
+ existingSnapshot,
4597
+ nextQuantityUnit: normalizedUom.quantityUnit,
4598
+ nextUomSnapshot: normalizedUom.uomSnapshot,
4599
+ unitPriceNet,
4600
+ unitPriceGross
4601
+ });
4602
+ if (convertedPrices.didConvert) {
4603
+ unitPriceNet = convertedPrices.unitPriceNet;
4604
+ unitPriceGross = convertedPrices.unitPriceGross;
4605
+ normalizedUom = await normalizeLineUom({
4606
+ em,
4607
+ resolver: uomResolver,
4608
+ organizationId: order.organizationId,
4609
+ tenantId: order.tenantId,
4610
+ line: {
4611
+ ...lineUomInput,
4612
+ unitPriceNet: unitPriceNet ?? 0,
4613
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
4614
+ }
4615
+ });
4616
+ }
3658
4617
  const updatedSnapshot = {
3659
4618
  id: lineId,
3660
4619
  lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
@@ -3664,8 +4623,11 @@ const orderLineUpsertCommand = {
3664
4623
  name: parsed.name ?? existingSnapshot?.name ?? null,
3665
4624
  description: parsed.description ?? existingSnapshot?.description ?? null,
3666
4625
  comment: parsed.comment ?? existingSnapshot?.comment ?? null,
3667
- quantity: Number(parsed.quantity ?? existingSnapshot?.quantity ?? 0),
3668
- quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
4626
+ quantity: normalizedUom.quantity,
4627
+ quantityUnit: normalizedUom.quantityUnit,
4628
+ normalizedQuantity: normalizedUom.normalizedQuantity,
4629
+ normalizedUnit: normalizedUom.normalizedUnit,
4630
+ uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
3669
4631
  currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? order.currencyCode,
3670
4632
  unitPriceNet: unitPriceNet ?? 0,
3671
4633
  unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
@@ -3684,7 +4646,9 @@ const orderLineUpsertCommand = {
3684
4646
  updatedSnapshot.statusEntryId = statusEntryId;
3685
4647
  updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
3686
4648
  updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
3687
- let nextLines = parsed.id ? lineSnapshots.map((line) => line.id === parsed.id ? updatedSnapshot : line) : [...lineSnapshots, updatedSnapshot];
4649
+ let nextLines = parsed.id ? lineSnapshots.map(
4650
+ (line) => line.id === parsed.id ? updatedSnapshot : line
4651
+ ) : [...lineSnapshots, updatedSnapshot];
3688
4652
  nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
3689
4653
  const sourceInputs = nextLines.map((line, index) => ({
3690
4654
  ...line,
@@ -3755,7 +4719,10 @@ const orderLineUpsertCommand = {
3755
4719
  if (!after) return null;
3756
4720
  const { translate } = await resolveTranslations();
3757
4721
  return {
3758
- actionLabel: translate("sales.audit.orders.lines.upsert", "Upsert order line"),
4722
+ actionLabel: translate(
4723
+ "sales.audit.orders.lines.upsert",
4724
+ "Upsert order line"
4725
+ ),
3759
4726
  resourceKind: "sales.order",
3760
4727
  resourceId: result.orderId,
3761
4728
  tenantId: after.order.tenantId,
@@ -3785,15 +4752,31 @@ const orderLineDeleteCommand = {
3785
4752
  if (!orderId) return {};
3786
4753
  const em = ctx.container.resolve("em");
3787
4754
  const snapshot = await loadOrderSnapshot(em, orderId);
3788
- if (snapshot) ensureOrderScope(ctx, snapshot.order.organizationId, snapshot.order.tenantId);
4755
+ if (snapshot)
4756
+ ensureOrderScope(
4757
+ ctx,
4758
+ snapshot.order.organizationId,
4759
+ snapshot.order.tenantId
4760
+ );
3789
4761
  return snapshot ? { before: snapshot } : {};
3790
4762
  },
3791
4763
  async execute(input, ctx) {
3792
4764
  const { translate } = await resolveTranslations();
3793
- const parsed = orderLineDeleteSchema.parse(input?.body ?? {});
4765
+ const parsed = orderLineDeleteSchema.parse(
4766
+ input?.body ?? {}
4767
+ );
3794
4768
  const em = ctx.container.resolve("em").fork();
3795
- const order = await em.findOne(SalesOrder, { id: parsed.orderId, deletedAt: null });
3796
- if (!order) throw new CrudHttpError(404, { error: translate("sales.documents.detail.error", "Document not found or inaccessible.") });
4769
+ const order = await em.findOne(SalesOrder, {
4770
+ id: parsed.orderId,
4771
+ deletedAt: null
4772
+ });
4773
+ if (!order)
4774
+ throw new CrudHttpError(404, {
4775
+ error: translate(
4776
+ "sales.documents.detail.error",
4777
+ "Document not found or inaccessible."
4778
+ )
4779
+ });
3797
4780
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
3798
4781
  const shipmentCount = await em.count(SalesShipmentItem, {
3799
4782
  orderLine: parsed.id,
@@ -3807,11 +4790,24 @@ const orderLineDeleteCommand = {
3807
4790
  )
3808
4791
  });
3809
4792
  }
3810
- const existingLines = await em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } });
3811
- const adjustments = await em.find(SalesOrderAdjustment, { order }, { orderBy: { position: "asc" } });
4793
+ const existingLines = await em.find(
4794
+ SalesOrderLine,
4795
+ { order },
4796
+ { orderBy: { lineNumber: "asc" } }
4797
+ );
4798
+ const adjustments = await em.find(
4799
+ SalesOrderAdjustment,
4800
+ { order },
4801
+ { orderBy: { position: "asc" } }
4802
+ );
3812
4803
  const filtered = existingLines.filter((line) => line.id !== parsed.id);
3813
4804
  if (filtered.length === existingLines.length) {
3814
- throw new CrudHttpError(404, { error: translate("sales.documents.detail.error", "Document not found or inaccessible.") });
4805
+ throw new CrudHttpError(404, {
4806
+ error: translate(
4807
+ "sales.documents.detail.error",
4808
+ "Document not found or inaccessible."
4809
+ )
4810
+ });
3815
4811
  }
3816
4812
  const sourceInputs = filtered.map((line, index) => ({
3817
4813
  ...mapOrderLineEntityToSnapshot(line),
@@ -3882,7 +4878,10 @@ const orderLineDeleteCommand = {
3882
4878
  if (!after) return null;
3883
4879
  const { translate } = await resolveTranslations();
3884
4880
  return {
3885
- actionLabel: translate("sales.audit.orders.lines.delete", "Delete order line"),
4881
+ actionLabel: translate(
4882
+ "sales.audit.orders.lines.delete",
4883
+ "Delete order line"
4884
+ ),
3886
4885
  resourceKind: "sales.order",
3887
4886
  resourceId: result.orderId,
3888
4887
  tenantId: after.order.tenantId,
@@ -3912,18 +4911,32 @@ const quoteLineUpsertCommand = {
3912
4911
  if (!quoteId) return {};
3913
4912
  const em = ctx.container.resolve("em");
3914
4913
  const snapshot = await loadQuoteSnapshot(em, quoteId);
3915
- if (snapshot) ensureQuoteScope(ctx, snapshot.quote.organizationId, snapshot.quote.tenantId);
4914
+ if (snapshot)
4915
+ ensureQuoteScope(
4916
+ ctx,
4917
+ snapshot.quote.organizationId,
4918
+ snapshot.quote.tenantId
4919
+ );
3916
4920
  return snapshot ? { before: snapshot } : {};
3917
4921
  },
3918
4922
  async execute(input, ctx) {
3919
- const parsed = quoteLineUpsertSchema.parse(input?.body ?? {});
4923
+ const rawBody = input?.body ?? {};
4924
+ const parsed = quoteLineUpsertSchema.parse(rawBody);
3920
4925
  const em = ctx.container.resolve("em").fork();
3921
- const quote = await em.findOne(SalesQuote, { id: parsed.quoteId, deletedAt: null });
3922
- if (!quote) throw new CrudHttpError(404, { error: "Sales quote not found" });
4926
+ const quote = await em.findOne(SalesQuote, {
4927
+ id: parsed.quoteId,
4928
+ deletedAt: null
4929
+ });
4930
+ if (!quote)
4931
+ throw new CrudHttpError(404, { error: "Sales quote not found" });
3923
4932
  ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
3924
4933
  const [existingLines, adjustments] = await Promise.all([
3925
4934
  em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
3926
- em.find(SalesQuoteAdjustment, { quote }, { orderBy: { position: "asc" } })
4935
+ em.find(
4936
+ SalesQuoteAdjustment,
4937
+ { quote },
4938
+ { orderBy: { position: "asc" } }
4939
+ )
3927
4940
  ]);
3928
4941
  const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
3929
4942
  const existingSnapshot = parsed.id ? lineSnapshots.find((line) => line.id === parsed.id) ?? null : null;
@@ -3934,7 +4947,9 @@ const quoteLineUpsertCommand = {
3934
4947
  if (priceMode && (unitPriceNet === null || unitPriceGross === null)) {
3935
4948
  let taxService = null;
3936
4949
  try {
3937
- taxService = ctx.container.resolve("taxCalculationService");
4950
+ taxService = ctx.container.resolve(
4951
+ "taxCalculationService"
4952
+ );
3938
4953
  } catch {
3939
4954
  taxService = null;
3940
4955
  }
@@ -3957,6 +4972,49 @@ const quoteLineUpsertCommand = {
3957
4972
  if (priceMode) metadata.priceMode = priceMode;
3958
4973
  const statusEntryId = parsed.statusEntryId ?? existingSnapshot?.statusEntryId ?? null;
3959
4974
  const lineId = parsed.id ?? existingSnapshot?.id ?? randomUUID();
4975
+ const lineUomInput = {
4976
+ productId: parsed.productId ?? existingSnapshot?.productId ?? null,
4977
+ productVariantId: parsed.productVariantId ?? existingSnapshot?.productVariantId ?? null,
4978
+ quantity: parsed.quantity ?? existingSnapshot?.quantity ?? 0,
4979
+ quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
4980
+ normalizedQuantity: existingSnapshot?.normalizedQuantity ?? null,
4981
+ normalizedUnit: existingSnapshot?.normalizedUnit ?? null,
4982
+ uomSnapshot: existingSnapshot?.uomSnapshot ?? null
4983
+ };
4984
+ const uomResolver = createUomResolver();
4985
+ let normalizedUom = await normalizeLineUom({
4986
+ em,
4987
+ resolver: uomResolver,
4988
+ organizationId: quote.organizationId,
4989
+ tenantId: quote.tenantId,
4990
+ line: {
4991
+ ...lineUomInput,
4992
+ unitPriceNet: unitPriceNet ?? 0,
4993
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
4994
+ }
4995
+ });
4996
+ const convertedPrices = convertLineUnitPricesOnUnitChange({
4997
+ existingSnapshot,
4998
+ nextQuantityUnit: normalizedUom.quantityUnit,
4999
+ nextUomSnapshot: normalizedUom.uomSnapshot,
5000
+ unitPriceNet,
5001
+ unitPriceGross
5002
+ });
5003
+ if (convertedPrices.didConvert) {
5004
+ unitPriceNet = convertedPrices.unitPriceNet;
5005
+ unitPriceGross = convertedPrices.unitPriceGross;
5006
+ normalizedUom = await normalizeLineUom({
5007
+ em,
5008
+ resolver: uomResolver,
5009
+ organizationId: quote.organizationId,
5010
+ tenantId: quote.tenantId,
5011
+ line: {
5012
+ ...lineUomInput,
5013
+ unitPriceNet: unitPriceNet ?? 0,
5014
+ unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0
5015
+ }
5016
+ });
5017
+ }
3960
5018
  const updatedSnapshot = {
3961
5019
  id: lineId,
3962
5020
  lineNumber: parsed.lineNumber ?? existingSnapshot?.lineNumber ?? lineSnapshots.length + 1,
@@ -3966,8 +5024,11 @@ const quoteLineUpsertCommand = {
3966
5024
  name: parsed.name ?? existingSnapshot?.name ?? null,
3967
5025
  description: parsed.description ?? existingSnapshot?.description ?? null,
3968
5026
  comment: parsed.comment ?? existingSnapshot?.comment ?? null,
3969
- quantity: Number(parsed.quantity ?? existingSnapshot?.quantity ?? 0),
3970
- quantityUnit: parsed.quantityUnit ?? existingSnapshot?.quantityUnit ?? null,
5027
+ quantity: normalizedUom.quantity,
5028
+ quantityUnit: normalizedUom.quantityUnit,
5029
+ normalizedQuantity: normalizedUom.normalizedQuantity,
5030
+ normalizedUnit: normalizedUom.normalizedUnit,
5031
+ uomSnapshot: normalizedUom.uomSnapshot ? cloneJson(normalizedUom.uomSnapshot) : null,
3971
5032
  currencyCode: parsed.currencyCode ?? existingSnapshot?.currencyCode ?? quote.currencyCode,
3972
5033
  unitPriceNet: unitPriceNet ?? 0,
3973
5034
  unitPriceGross: unitPriceGross ?? unitPriceNet ?? 0,
@@ -3986,7 +5047,9 @@ const quoteLineUpsertCommand = {
3986
5047
  updatedSnapshot.statusEntryId = statusEntryId;
3987
5048
  updatedSnapshot.catalogSnapshot = parsed.catalogSnapshot ?? existingSnapshot?.catalogSnapshot ?? null;
3988
5049
  updatedSnapshot.promotionSnapshot = parsed.promotionSnapshot ?? existingSnapshot?.promotionSnapshot ?? null;
3989
- let nextLines = parsed.id ? lineSnapshots.map((line) => line.id === parsed.id ? updatedSnapshot : line) : [...lineSnapshots, updatedSnapshot];
5050
+ let nextLines = parsed.id ? lineSnapshots.map(
5051
+ (line) => line.id === parsed.id ? updatedSnapshot : line
5052
+ ) : [...lineSnapshots, updatedSnapshot];
3990
5053
  nextLines = nextLines.sort((a, b) => (a.lineNumber ?? 0) - (b.lineNumber ?? 0)).map((line, index) => ({ ...line, lineNumber: index + 1 }));
3991
5054
  const sourceInputs = nextLines.map((line, index) => ({
3992
5055
  ...line,
@@ -4056,7 +5119,10 @@ const quoteLineUpsertCommand = {
4056
5119
  if (!after) return null;
4057
5120
  const { translate } = await resolveTranslations();
4058
5121
  return {
4059
- actionLabel: translate("sales.audit.quotes.lines.upsert", "Upsert quote line"),
5122
+ actionLabel: translate(
5123
+ "sales.audit.quotes.lines.upsert",
5124
+ "Upsert quote line"
5125
+ ),
4060
5126
  resourceKind: "sales.quote",
4061
5127
  resourceId: result.quoteId,
4062
5128
  tenantId: after.quote.tenantId,
@@ -4086,17 +5152,36 @@ const quoteLineDeleteCommand = {
4086
5152
  if (!quoteId) return {};
4087
5153
  const em = ctx.container.resolve("em");
4088
5154
  const snapshot = await loadQuoteSnapshot(em, quoteId);
4089
- if (snapshot) ensureQuoteScope(ctx, snapshot.quote.organizationId, snapshot.quote.tenantId);
5155
+ if (snapshot)
5156
+ ensureQuoteScope(
5157
+ ctx,
5158
+ snapshot.quote.organizationId,
5159
+ snapshot.quote.tenantId
5160
+ );
4090
5161
  return snapshot ? { before: snapshot } : {};
4091
5162
  },
4092
5163
  async execute(input, ctx) {
4093
- const parsed = quoteLineDeleteSchema.parse(input?.body ?? {});
5164
+ const parsed = quoteLineDeleteSchema.parse(
5165
+ input?.body ?? {}
5166
+ );
4094
5167
  const em = ctx.container.resolve("em").fork();
4095
- const quote = await em.findOne(SalesQuote, { id: parsed.quoteId, deletedAt: null });
4096
- if (!quote) throw new CrudHttpError(404, { error: "Sales quote not found" });
5168
+ const quote = await em.findOne(SalesQuote, {
5169
+ id: parsed.quoteId,
5170
+ deletedAt: null
5171
+ });
5172
+ if (!quote)
5173
+ throw new CrudHttpError(404, { error: "Sales quote not found" });
4097
5174
  ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
4098
- const existingLines = await em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } });
4099
- const adjustments = await em.find(SalesQuoteAdjustment, { quote }, { orderBy: { position: "asc" } });
5175
+ const existingLines = await em.find(
5176
+ SalesQuoteLine,
5177
+ { quote },
5178
+ { orderBy: { lineNumber: "asc" } }
5179
+ );
5180
+ const adjustments = await em.find(
5181
+ SalesQuoteAdjustment,
5182
+ { quote },
5183
+ { orderBy: { position: "asc" } }
5184
+ );
4100
5185
  const filtered = existingLines.filter((line) => line.id !== parsed.id);
4101
5186
  if (filtered.length === existingLines.length) {
4102
5187
  throw new CrudHttpError(404, { error: "Quote line not found" });
@@ -4169,7 +5254,10 @@ const quoteLineDeleteCommand = {
4169
5254
  if (!after) return null;
4170
5255
  const { translate } = await resolveTranslations();
4171
5256
  return {
4172
- actionLabel: translate("sales.audit.quotes.lines.delete", "Delete quote line"),
5257
+ actionLabel: translate(
5258
+ "sales.audit.quotes.lines.delete",
5259
+ "Delete quote line"
5260
+ ),
4173
5261
  resourceKind: "sales.quote",
4174
5262
  resourceId: result.quoteId,
4175
5263
  tenantId: after.quote.tenantId,
@@ -4199,21 +5287,38 @@ const orderAdjustmentUpsertCommand = {
4199
5287
  if (!orderId) return {};
4200
5288
  const em = ctx.container.resolve("em");
4201
5289
  const snapshot = await loadOrderSnapshot(em, orderId);
4202
- if (snapshot) ensureOrderScope(ctx, snapshot.order.organizationId, snapshot.order.tenantId);
5290
+ if (snapshot)
5291
+ ensureOrderScope(
5292
+ ctx,
5293
+ snapshot.order.organizationId,
5294
+ snapshot.order.tenantId
5295
+ );
4203
5296
  return snapshot ? { before: snapshot } : {};
4204
5297
  },
4205
5298
  async execute(input, ctx) {
4206
- const parsed = orderAdjustmentUpsertSchema.parse(input?.body ?? {});
5299
+ const parsed = orderAdjustmentUpsertSchema.parse(
5300
+ input?.body ?? {}
5301
+ );
4207
5302
  const em = ctx.container.resolve("em").fork();
4208
- const order = await em.findOne(SalesOrder, { id: parsed.orderId, deletedAt: null });
4209
- if (!order) throw new CrudHttpError(404, { error: "Sales order not found" });
5303
+ const order = await em.findOne(SalesOrder, {
5304
+ id: parsed.orderId,
5305
+ deletedAt: null
5306
+ });
5307
+ if (!order)
5308
+ throw new CrudHttpError(404, { error: "Sales order not found" });
4210
5309
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
4211
5310
  if (parsed.scope === "line") {
4212
- throw new CrudHttpError(400, { error: "Line-scoped adjustments are not supported yet." });
5311
+ throw new CrudHttpError(400, {
5312
+ error: "Line-scoped adjustments are not supported yet."
5313
+ });
4213
5314
  }
4214
5315
  const [existingLines, existingAdjustments] = await Promise.all([
4215
5316
  em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
4216
- em.find(SalesOrderAdjustment, { order }, { orderBy: { position: "asc" } })
5317
+ em.find(
5318
+ SalesOrderAdjustment,
5319
+ { order },
5320
+ { orderBy: { position: "asc" } }
5321
+ )
4217
5322
  ]);
4218
5323
  const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
4219
5324
  const adjustmentDrafts = existingAdjustments.map(mapOrderAdjustmentToDraft);
@@ -4343,7 +5448,10 @@ const orderAdjustmentUpsertCommand = {
4343
5448
  if (!after) return null;
4344
5449
  const { translate } = await resolveTranslations();
4345
5450
  return {
4346
- actionLabel: translate("sales.audit.orders.adjustments.upsert", "Upsert order adjustment"),
5451
+ actionLabel: translate(
5452
+ "sales.audit.orders.adjustments.upsert",
5453
+ "Upsert order adjustment"
5454
+ ),
4347
5455
  resourceKind: "sales.order",
4348
5456
  resourceId: result.orderId,
4349
5457
  tenantId: after.order.tenantId,
@@ -4373,18 +5481,33 @@ const orderAdjustmentDeleteCommand = {
4373
5481
  if (!orderId) return {};
4374
5482
  const em = ctx.container.resolve("em");
4375
5483
  const snapshot = await loadOrderSnapshot(em, orderId);
4376
- if (snapshot) ensureOrderScope(ctx, snapshot.order.organizationId, snapshot.order.tenantId);
5484
+ if (snapshot)
5485
+ ensureOrderScope(
5486
+ ctx,
5487
+ snapshot.order.organizationId,
5488
+ snapshot.order.tenantId
5489
+ );
4377
5490
  return snapshot ? { before: snapshot } : {};
4378
5491
  },
4379
5492
  async execute(input, ctx) {
4380
- const parsed = orderAdjustmentDeleteSchema.parse(input?.body ?? {});
5493
+ const parsed = orderAdjustmentDeleteSchema.parse(
5494
+ input?.body ?? {}
5495
+ );
4381
5496
  const em = ctx.container.resolve("em").fork();
4382
- const order = await em.findOne(SalesOrder, { id: parsed.orderId, deletedAt: null });
4383
- if (!order) throw new CrudHttpError(404, { error: "Sales order not found" });
5497
+ const order = await em.findOne(SalesOrder, {
5498
+ id: parsed.orderId,
5499
+ deletedAt: null
5500
+ });
5501
+ if (!order)
5502
+ throw new CrudHttpError(404, { error: "Sales order not found" });
4384
5503
  ensureOrderScope(ctx, order.organizationId, order.tenantId);
4385
5504
  const [existingLines, adjustments] = await Promise.all([
4386
5505
  em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
4387
- em.find(SalesOrderAdjustment, { order }, { orderBy: { position: "asc" } })
5506
+ em.find(
5507
+ SalesOrderAdjustment,
5508
+ { order },
5509
+ { orderBy: { position: "asc" } }
5510
+ )
4388
5511
  ]);
4389
5512
  const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
4390
5513
  if (filtered.length === adjustments.length) {
@@ -4474,7 +5597,10 @@ const orderAdjustmentDeleteCommand = {
4474
5597
  if (!after) return null;
4475
5598
  const { translate } = await resolveTranslations();
4476
5599
  return {
4477
- actionLabel: translate("sales.audit.orders.adjustments.delete", "Delete order adjustment"),
5600
+ actionLabel: translate(
5601
+ "sales.audit.orders.adjustments.delete",
5602
+ "Delete order adjustment"
5603
+ ),
4478
5604
  resourceKind: "sales.order",
4479
5605
  resourceId: result.orderId,
4480
5606
  tenantId: after.order.tenantId,
@@ -4504,21 +5630,38 @@ const quoteAdjustmentUpsertCommand = {
4504
5630
  if (!quoteId) return {};
4505
5631
  const em = ctx.container.resolve("em");
4506
5632
  const snapshot = await loadQuoteSnapshot(em, quoteId);
4507
- if (snapshot) ensureQuoteScope(ctx, snapshot.quote.organizationId, snapshot.quote.tenantId);
5633
+ if (snapshot)
5634
+ ensureQuoteScope(
5635
+ ctx,
5636
+ snapshot.quote.organizationId,
5637
+ snapshot.quote.tenantId
5638
+ );
4508
5639
  return snapshot ? { before: snapshot } : {};
4509
5640
  },
4510
5641
  async execute(input, ctx) {
4511
- const parsed = quoteAdjustmentUpsertSchema.parse(input?.body ?? {});
5642
+ const parsed = quoteAdjustmentUpsertSchema.parse(
5643
+ input?.body ?? {}
5644
+ );
4512
5645
  const em = ctx.container.resolve("em").fork();
4513
- const quote = await em.findOne(SalesQuote, { id: parsed.quoteId, deletedAt: null });
4514
- if (!quote) throw new CrudHttpError(404, { error: "Sales quote not found" });
5646
+ const quote = await em.findOne(SalesQuote, {
5647
+ id: parsed.quoteId,
5648
+ deletedAt: null
5649
+ });
5650
+ if (!quote)
5651
+ throw new CrudHttpError(404, { error: "Sales quote not found" });
4515
5652
  ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
4516
5653
  if (parsed.scope === "line") {
4517
- throw new CrudHttpError(400, { error: "Line-scoped adjustments are not supported yet." });
5654
+ throw new CrudHttpError(400, {
5655
+ error: "Line-scoped adjustments are not supported yet."
5656
+ });
4518
5657
  }
4519
5658
  const [existingLines, existingAdjustments] = await Promise.all([
4520
5659
  em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
4521
- em.find(SalesQuoteAdjustment, { quote }, { orderBy: { position: "asc" } })
5660
+ em.find(
5661
+ SalesQuoteAdjustment,
5662
+ { quote },
5663
+ { orderBy: { position: "asc" } }
5664
+ )
4522
5665
  ]);
4523
5666
  const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
4524
5667
  const adjustmentDrafts = existingAdjustments.map(mapQuoteAdjustmentToDraft);
@@ -4647,7 +5790,10 @@ const quoteAdjustmentUpsertCommand = {
4647
5790
  if (!after) return null;
4648
5791
  const { translate } = await resolveTranslations();
4649
5792
  return {
4650
- actionLabel: translate("sales.audit.quotes.adjustments.upsert", "Upsert quote adjustment"),
5793
+ actionLabel: translate(
5794
+ "sales.audit.quotes.adjustments.upsert",
5795
+ "Upsert quote adjustment"
5796
+ ),
4651
5797
  resourceKind: "sales.quote",
4652
5798
  resourceId: result.quoteId,
4653
5799
  tenantId: after.quote.tenantId,
@@ -4677,18 +5823,33 @@ const quoteAdjustmentDeleteCommand = {
4677
5823
  if (!quoteId) return {};
4678
5824
  const em = ctx.container.resolve("em");
4679
5825
  const snapshot = await loadQuoteSnapshot(em, quoteId);
4680
- if (snapshot) ensureQuoteScope(ctx, snapshot.quote.organizationId, snapshot.quote.tenantId);
5826
+ if (snapshot)
5827
+ ensureQuoteScope(
5828
+ ctx,
5829
+ snapshot.quote.organizationId,
5830
+ snapshot.quote.tenantId
5831
+ );
4681
5832
  return snapshot ? { before: snapshot } : {};
4682
5833
  },
4683
5834
  async execute(input, ctx) {
4684
- const parsed = quoteAdjustmentDeleteSchema.parse(input?.body ?? {});
5835
+ const parsed = quoteAdjustmentDeleteSchema.parse(
5836
+ input?.body ?? {}
5837
+ );
4685
5838
  const em = ctx.container.resolve("em").fork();
4686
- const quote = await em.findOne(SalesQuote, { id: parsed.quoteId, deletedAt: null });
4687
- if (!quote) throw new CrudHttpError(404, { error: "Sales quote not found" });
5839
+ const quote = await em.findOne(SalesQuote, {
5840
+ id: parsed.quoteId,
5841
+ deletedAt: null
5842
+ });
5843
+ if (!quote)
5844
+ throw new CrudHttpError(404, { error: "Sales quote not found" });
4688
5845
  ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
4689
5846
  const [existingLines, adjustments] = await Promise.all([
4690
5847
  em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
4691
- em.find(SalesQuoteAdjustment, { quote }, { orderBy: { position: "asc" } })
5848
+ em.find(
5849
+ SalesQuoteAdjustment,
5850
+ { quote },
5851
+ { orderBy: { position: "asc" } }
5852
+ )
4692
5853
  ]);
4693
5854
  const filtered = adjustments.filter((adj) => adj.id !== parsed.id);
4694
5855
  if (filtered.length === adjustments.length) {
@@ -4777,7 +5938,10 @@ const quoteAdjustmentDeleteCommand = {
4777
5938
  if (!after) return null;
4778
5939
  const { translate } = await resolveTranslations();
4779
5940
  return {
4780
- actionLabel: translate("sales.audit.quotes.adjustments.delete", "Delete quote adjustment"),
5941
+ actionLabel: translate(
5942
+ "sales.audit.quotes.adjustments.delete",
5943
+ "Delete quote adjustment"
5944
+ ),
4781
5945
  resourceKind: "sales.quote",
4782
5946
  resourceId: result.quoteId,
4783
5947
  tenantId: after.quote.tenantId,