@dainamite/cpq 0.5.0 → 0.7.0

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 (368) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/MIGRATION.md +74 -0
  3. package/dist/modules/cpq/acl.js +1 -0
  4. package/dist/modules/cpq/acl.js.map +2 -2
  5. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js +45 -0
  6. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js.map +7 -0
  7. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
  8. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
  9. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
  10. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
  11. package/dist/modules/cpq/api/orders/[orderId]/route.js +17 -1
  12. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
  13. package/dist/modules/cpq/api/orders/route.js +47 -2
  14. package/dist/modules/cpq/api/orders/route.js.map +2 -2
  15. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
  16. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
  17. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +42 -2
  18. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
  19. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
  20. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
  21. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
  22. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
  23. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
  24. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
  25. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
  26. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
  27. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +32 -2
  28. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
  29. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
  30. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
  31. package/dist/modules/cpq/api/quotes/price/route.js +43 -3
  32. package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
  33. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
  34. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
  35. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
  36. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
  37. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
  38. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
  39. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
  40. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
  41. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +3 -3
  42. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
  43. package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
  44. package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
  45. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +174 -184
  46. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  47. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
  48. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
  49. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
  50. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
  51. package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
  52. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
  53. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +140 -280
  54. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  55. package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
  56. package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
  57. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
  58. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
  59. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
  60. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
  61. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
  62. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
  63. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
  64. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
  65. package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
  66. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
  67. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
  68. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
  69. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
  70. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
  71. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
  72. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
  73. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
  74. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
  75. package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
  76. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
  77. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
  78. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
  79. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +208 -0
  80. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
  81. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
  82. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
  83. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
  84. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
  85. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
  86. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
  87. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
  88. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
  89. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +269 -311
  90. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
  91. package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
  92. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
  93. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
  94. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
  95. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
  96. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
  97. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
  98. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
  99. package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
  100. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
  101. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
  102. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
  103. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
  104. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
  105. package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
  106. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
  107. package/dist/modules/cpq/ce.js +8 -0
  108. package/dist/modules/cpq/ce.js.map +2 -2
  109. package/dist/modules/cpq/cli.js +34 -1
  110. package/dist/modules/cpq/cli.js.map +2 -2
  111. package/dist/modules/cpq/commands/price-adjustments.js +107 -0
  112. package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
  113. package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
  114. package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
  115. package/dist/modules/cpq/components/ChargeBreakdownTable.js +148 -0
  116. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
  117. package/dist/modules/cpq/components/PriceWaterfall.js +68 -0
  118. package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
  119. package/dist/modules/cpq/components/PricingSummaryCard.js +112 -0
  120. package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
  121. package/dist/modules/cpq/components/StatusPath.js +5 -3
  122. package/dist/modules/cpq/components/StatusPath.js.map +2 -2
  123. package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
  124. package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
  125. package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
  126. package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
  127. package/dist/modules/cpq/data/entities.js +97 -0
  128. package/dist/modules/cpq/data/entities.js.map +2 -2
  129. package/dist/modules/cpq/data/price-adjustments.js +70 -0
  130. package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
  131. package/dist/modules/cpq/data/validators.js +4 -0
  132. package/dist/modules/cpq/data/validators.js.map +2 -2
  133. package/dist/modules/cpq/di.js +6 -0
  134. package/dist/modules/cpq/di.js.map +2 -2
  135. package/dist/modules/cpq/events.js +47 -0
  136. package/dist/modules/cpq/events.js.map +2 -2
  137. package/dist/modules/cpq/i18n/de.json +1220 -0
  138. package/dist/modules/cpq/i18n/en.json +1220 -0
  139. package/dist/modules/cpq/i18n/es.json +1220 -0
  140. package/dist/modules/cpq/i18n/pl.json +1220 -0
  141. package/dist/modules/cpq/index.js +1 -1
  142. package/dist/modules/cpq/index.js.map +2 -2
  143. package/dist/modules/cpq/lib/auditActor.js +10 -0
  144. package/dist/modules/cpq/lib/auditActor.js.map +7 -0
  145. package/dist/modules/cpq/lib/formatMoney.js +7 -0
  146. package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
  147. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
  148. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
  149. package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
  150. package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
  151. package/dist/modules/cpq/lib/quotePermissions.js +13 -0
  152. package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
  153. package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
  154. package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
  155. package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
  156. package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
  157. package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
  158. package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
  159. package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
  160. package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
  161. package/dist/modules/cpq/services/cpqOrderService.js +25 -28
  162. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  163. package/dist/modules/cpq/services/cpqPricingService.js +7 -0
  164. package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
  165. package/dist/modules/cpq/services/cpqQuotingService.js +330 -109
  166. package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
  167. package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
  168. package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
  169. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
  170. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
  171. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
  172. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
  173. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
  174. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
  175. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
  176. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
  177. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
  178. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
  179. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
  180. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
  181. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
  182. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
  183. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
  184. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
  185. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
  186. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
  187. package/dist/modules/cpq/services/types.js.map +2 -2
  188. package/dist/modules/cpq/setup.js +6 -0
  189. package/dist/modules/cpq/setup.js.map +2 -2
  190. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
  191. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
  192. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
  193. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
  194. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
  195. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
  196. package/dist/modules/cpq/widgets/injection-table.js +2 -2
  197. package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
  198. package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
  199. package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
  200. package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
  201. package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
  202. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
  203. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
  204. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
  205. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
  206. package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
  207. package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
  208. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
  209. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
  210. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
  211. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
  212. package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
  213. package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
  214. package/dist/modules/cpq/workflows/steps/index.js +12 -0
  215. package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
  216. package/package.json +1 -1
  217. package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
  218. package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
  219. package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
  220. package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
  221. package/src/modules/cpq/acl.ts +1 -0
  222. package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
  223. package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
  224. package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
  225. package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
  226. package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
  227. package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
  228. package/src/modules/cpq/api/orders/[orderId]/route.ts +18 -0
  229. package/src/modules/cpq/api/orders/route.ts +51 -1
  230. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
  231. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +48 -1
  232. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
  233. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
  234. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
  235. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
  236. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
  237. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +40 -1
  238. package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
  239. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
  240. package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
  241. package/src/modules/cpq/api/quotes/price/route.ts +28 -2
  242. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
  243. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
  244. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
  245. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
  246. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +3 -3
  247. package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
  248. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
  249. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +164 -120
  250. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
  251. package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
  252. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +122 -308
  253. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
  254. package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
  255. package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
  256. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
  257. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
  258. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
  259. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
  260. package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
  261. package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
  262. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
  263. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  264. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
  265. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
  266. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
  267. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
  268. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
  269. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
  270. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
  271. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +304 -348
  272. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
  273. package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
  274. package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
  275. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
  276. package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
  277. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
  278. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
  279. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  280. package/src/modules/cpq/ce.ts +8 -0
  281. package/src/modules/cpq/cli.ts +48 -1
  282. package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
  283. package/src/modules/cpq/commands/price-adjustments.ts +90 -0
  284. package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
  285. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +203 -0
  286. package/src/modules/cpq/components/PriceWaterfall.tsx +96 -0
  287. package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
  288. package/src/modules/cpq/components/StatusPath.tsx +7 -4
  289. package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
  290. package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
  291. package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
  292. package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
  293. package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
  294. package/src/modules/cpq/data/entities.ts +83 -0
  295. package/src/modules/cpq/data/price-adjustments.ts +67 -0
  296. package/src/modules/cpq/data/validators.ts +3 -0
  297. package/src/modules/cpq/di.ts +6 -0
  298. package/src/modules/cpq/events.ts +53 -2
  299. package/src/modules/cpq/i18n/de.json +1220 -0
  300. package/src/modules/cpq/i18n/en.json +1220 -0
  301. package/src/modules/cpq/i18n/es.json +1220 -0
  302. package/src/modules/cpq/i18n/pl.json +1220 -0
  303. package/src/modules/cpq/index.ts +1 -1
  304. package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
  305. package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
  306. package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
  307. package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
  308. package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
  309. package/src/modules/cpq/lib/auditActor.ts +13 -0
  310. package/src/modules/cpq/lib/formatMoney.ts +4 -0
  311. package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
  312. package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
  313. package/src/modules/cpq/lib/quotePermissions.ts +22 -0
  314. package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
  315. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
  316. package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
  317. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
  318. package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
  319. package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
  320. package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
  321. package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
  322. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
  323. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
  324. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
  325. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
  326. package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
  327. package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
  328. package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
  329. package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
  330. package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
  331. package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
  332. package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
  333. package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
  334. package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
  335. package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
  336. package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
  337. package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
  338. package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
  339. package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
  340. package/src/modules/cpq/services/cpqOrderService.ts +22 -28
  341. package/src/modules/cpq/services/cpqPricingService.ts +8 -0
  342. package/src/modules/cpq/services/cpqQuotingService.ts +374 -125
  343. package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
  344. package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
  345. package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
  346. package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
  347. package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
  348. package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
  349. package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
  350. package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
  351. package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
  352. package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
  353. package/src/modules/cpq/services/types.ts +34 -1
  354. package/src/modules/cpq/setup.ts +7 -1
  355. package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
  356. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
  357. package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
  358. package/src/modules/cpq/widgets/injection-table.ts +8 -2
  359. package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
  360. package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
  361. package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
  362. package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
  363. package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
  364. package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
  365. package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
  366. package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
  367. package/src/modules/cpq/workflows/steps/index.ts +12 -0
  368. package/src/modules/cpq/workflows/types.ts +4 -0
@@ -1,5 +1,11 @@
1
1
  import { DefaultCpqQuotingService } from '../cpqQuotingService'
2
2
  import { createMockEm, TEST_SCOPE as SCOPE } from './_helpers/mockEm'
3
+ import { SalesQuote, SalesQuoteLine } from '@open-mercato/core/modules/sales/data/entities'
4
+ import {
5
+ CpqProductOffering,
6
+ CpqQuoteConfiguration,
7
+ CpqQuoteLineConfiguration,
8
+ } from '../../data/entities'
3
9
 
4
10
  /**
5
11
  * XD-250 ARC unit tests for cpqQuotingService — focused on the
@@ -141,3 +147,234 @@ describe('transitionStatus → validateArcQuote hook', () => {
141
147
  expect(em.findOne).toHaveBeenCalledTimes(4)
142
148
  })
143
149
  })
150
+
151
+ // ─── Amend mirror-line pricing on recalculation ──────────────────
152
+
153
+
154
+ function makePricedService() {
155
+ const em = Object.assign(createMockEm(), {
156
+ assign: jest.fn((entity: Record<string, unknown>, updates: Record<string, unknown>) =>
157
+ Object.assign(entity, updates),
158
+ ),
159
+ fork: jest.fn(),
160
+ })
161
+ em.fork.mockReturnValue(em)
162
+ const pricingService = { resolveProductCharges: jest.fn() }
163
+ const validationService = {
164
+ validateOfferingConfiguration: jest.fn().mockResolvedValue({ valid: true, errors: [] }),
165
+ validateRelationships: jest.fn().mockResolvedValue([]),
166
+ }
167
+ const service = new DefaultCpqQuotingService({
168
+ em: em as never,
169
+ cpqPricingService: pricingService as never,
170
+ cpqValidationService: validationService as never,
171
+ cpqProductService: {} as never,
172
+ salesDocumentNumberGenerator: {} as never,
173
+ })
174
+ return { em, pricingService, service }
175
+ }
176
+
177
+ function mrcCharge(price: number) {
178
+ return {
179
+ chargeCode: 'monthly_port_fee',
180
+ chargeName: 'Monthly Port Fee',
181
+ chargeType: 'mrc',
182
+ pricingMethod: 'flat',
183
+ unitPrice: price,
184
+ quantity: 1,
185
+ totalPrice: price,
186
+ currencyCode: 'USD',
187
+ adjustments: [],
188
+ }
189
+ }
190
+
191
+ function amendFixture(configuration: Record<string, unknown>) {
192
+ const cpqConfig = { ...ARC_QUOTE_BASE, quoteType: 'amend', organizationId: SCOPE.organizationId, tenantId: SCOPE.tenantId }
193
+ const lineConfig = {
194
+ id: 'lc-1',
195
+ quoteLineId: 'ql-1',
196
+ quoteConfigurationId: 'cpq-1',
197
+ offeringId: 'off-1',
198
+ productId: 'prod-1',
199
+ specId: null,
200
+ action: 'modify',
201
+ parentLineId: null,
202
+ startDate: null,
203
+ termMonths: null,
204
+ endDate: null,
205
+ configuration,
206
+ quantity: 1,
207
+ charges: [mrcCharge(100)],
208
+ nrcTotal: '0',
209
+ mrcTotal: '100',
210
+ usageEstimates: null,
211
+ usageTotalEstimated: null,
212
+ validationErrors: null,
213
+ isConfigured: true,
214
+ targetSubscriptionId: 'sub-1',
215
+ sourceSubscriptionItemId: 'item-1',
216
+ }
217
+ return { cpqConfig, lineConfig }
218
+ }
219
+
220
+ function routeEm(
221
+ em: ReturnType<typeof makePricedService>['em'],
222
+ cpqConfig: Record<string, unknown>,
223
+ lineConfig: Record<string, unknown>,
224
+ ) {
225
+ em.findOne.mockImplementation(async (entity: unknown) => {
226
+ if (entity === CpqQuoteConfiguration) return cpqConfig
227
+ if (entity === SalesQuote) return { id: 'sq-1', currencyCode: 'USD', quoteNumber: 'Q-001' }
228
+ if (entity === SalesQuoteLine) return { id: 'ql-1' }
229
+ if (entity === CpqProductOffering) return { id: 'off-1', name: 'GIX Access Port', offeringType: 'simple' }
230
+ if (entity === CpqQuoteLineConfiguration) return lineConfig
231
+ return null
232
+ })
233
+ em.find.mockImplementation(async (entity: unknown) => {
234
+ if (entity === CpqQuoteLineConfiguration) return [lineConfig]
235
+ return []
236
+ })
237
+ }
238
+
239
+ describe('recalculate → Amend mirror lines', () => {
240
+ it('re-prices a legacy Amend mirror line (created before the frozen marker) at the current standard price', async () => {
241
+ const { em, pricingService, service } = makePricedService()
242
+ const { cpqConfig, lineConfig } = amendFixture({
243
+ port_size: '1G',
244
+ _arcMirroredFromItemId: 'item-1',
245
+ _arcMirroredName: 'GIX Access Port',
246
+ })
247
+ routeEm(em, cpqConfig, lineConfig)
248
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(120)])
249
+
250
+ await service.recalculate('cpq-1', { save: true }, SCOPE)
251
+
252
+ expect(pricingService.resolveProductCharges).toHaveBeenCalledTimes(1)
253
+ expect(lineConfig.mrcTotal).toBe('120')
254
+ })
255
+
256
+ it('keeps a frozen Amend mirror line at its stored price without calling the pricing engine', async () => {
257
+ const { em, pricingService, service } = makePricedService()
258
+ const { cpqConfig, lineConfig } = amendFixture({
259
+ port_size: '1G',
260
+ _arcMirroredFromItemId: 'item-1',
261
+ _arcMirroredName: 'GIX Access Port',
262
+ _arcPriceFrozen: true,
263
+ })
264
+ routeEm(em, cpqConfig, lineConfig)
265
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(120)])
266
+
267
+ const result = await service.recalculate('cpq-1', { save: true }, SCOPE)
268
+
269
+ expect(pricingService.resolveProductCharges).not.toHaveBeenCalled()
270
+ expect(lineConfig.mrcTotal).toBe('100')
271
+ expect(result.lines[0].mrcTotal).toBe(100)
272
+ })
273
+ })
274
+
275
+ describe('updateQuoteItem → frozen Amend mirror lines', () => {
276
+ const FROZEN_CFG = {
277
+ port_size: '1G',
278
+ _arcMirroredFromItemId: 'item-1',
279
+ _arcMirroredName: 'GIX Access Port',
280
+ _arcPriceFrozen: true,
281
+ }
282
+
283
+ it('stays frozen when the UI resubmits an unchanged configuration', async () => {
284
+ const { em, pricingService, service } = makePricedService()
285
+ const { cpqConfig, lineConfig } = amendFixture({ ...FROZEN_CFG })
286
+ routeEm(em, cpqConfig, lineConfig)
287
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(120)])
288
+
289
+ await service.updateQuoteItem('cpq-1', 'ql-1', { configuration: { port_size: '1G' }, quantity: 1 }, SCOPE)
290
+
291
+ expect(pricingService.resolveProductCharges).not.toHaveBeenCalled()
292
+ expect(lineConfig.configuration).toMatchObject({ _arcPriceFrozen: true, _arcMirroredFromItemId: 'item-1' })
293
+ expect(lineConfig.mrcTotal).toBe('100')
294
+ })
295
+
296
+ it('unfreezes and re-prices when the configuration changes', async () => {
297
+ const { em, pricingService, service } = makePricedService()
298
+ const { cpqConfig, lineConfig } = amendFixture({ ...FROZEN_CFG })
299
+ routeEm(em, cpqConfig, lineConfig)
300
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(150)])
301
+
302
+ await service.updateQuoteItem('cpq-1', 'ql-1', { configuration: { port_size: '10G' } }, SCOPE)
303
+
304
+ expect(pricingService.resolveProductCharges).toHaveBeenCalled()
305
+ expect((lineConfig.configuration as Record<string, unknown>)._arcPriceFrozen).toBeUndefined()
306
+ expect(lineConfig.configuration).toMatchObject({ port_size: '10G', _arcMirroredFromItemId: 'item-1' })
307
+ expect(lineConfig.mrcTotal).toBe('150')
308
+ })
309
+
310
+ it('unfreezes and re-prices when the quantity changes', async () => {
311
+ const { em, pricingService, service } = makePricedService()
312
+ const { cpqConfig, lineConfig } = amendFixture({ ...FROZEN_CFG })
313
+ routeEm(em, cpqConfig, lineConfig)
314
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(110)])
315
+
316
+ await service.updateQuoteItem('cpq-1', 'ql-1', { configuration: { port_size: '1G' }, quantity: 2 }, SCOPE)
317
+
318
+ expect(pricingService.resolveProductCharges).toHaveBeenCalled()
319
+ expect(lineConfig.mrcTotal).toBe('220')
320
+ })
321
+
322
+ it('unfreezes and marks the line re-priced when the UI echoes the stored markers with a changed attribute', async () => {
323
+ const { em, pricingService, service } = makePricedService()
324
+ const { cpqConfig, lineConfig } = amendFixture({ ...FROZEN_CFG })
325
+ routeEm(em, cpqConfig, lineConfig)
326
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(150)])
327
+
328
+ // The quote page sends back the configuration it loaded — markers included.
329
+ await service.updateQuoteItem('cpq-1', 'ql-1', { configuration: { ...FROZEN_CFG, port_size: '10G' }, quantity: 1 }, SCOPE)
330
+
331
+ const stored = lineConfig.configuration as Record<string, unknown>
332
+ expect(pricingService.resolveProductCharges).toHaveBeenCalled()
333
+ expect(stored._arcPriceFrozen).toBeUndefined()
334
+ expect(stored._arcRepriced).toBe(true)
335
+ expect(stored._arcMirroredFromItemId).toBe('item-1')
336
+ expect(lineConfig.mrcTotal).toBe('150')
337
+ })
338
+
339
+ it('stays frozen when the UI echoes the stored markers unchanged', async () => {
340
+ const { em, pricingService, service } = makePricedService()
341
+ const { cpqConfig, lineConfig } = amendFixture({ ...FROZEN_CFG })
342
+ routeEm(em, cpqConfig, lineConfig)
343
+
344
+ await service.updateQuoteItem('cpq-1', 'ql-1', { configuration: { ...FROZEN_CFG }, quantity: 1 }, SCOPE)
345
+
346
+ expect(pricingService.resolveProductCharges).not.toHaveBeenCalled()
347
+ expect((lineConfig.configuration as Record<string, unknown>)._arcRepriced).toBeUndefined()
348
+ })
349
+
350
+ it('never lets a client set the freeze marker', async () => {
351
+ const { em, pricingService, service } = makePricedService()
352
+ const { cpqConfig, lineConfig } = amendFixture({ port_size: '1G', _arcMirroredFromItemId: 'item-1', _arcMirroredName: 'GIX Access Port' })
353
+ routeEm(em, cpqConfig, lineConfig)
354
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(120)])
355
+
356
+ await service.updateQuoteItem('cpq-1', 'ql-1', { configuration: { port_size: '1G', _arcPriceFrozen: true } }, SCOPE)
357
+
358
+ expect(pricingService.resolveProductCharges).toHaveBeenCalled()
359
+ expect((lineConfig.configuration as Record<string, unknown>)._arcPriceFrozen).toBeUndefined()
360
+ })
361
+ })
362
+
363
+ describe('recalculate → freeze marker outside Amend mirror lines', () => {
364
+ it('ignores the freeze marker on a line of a new (non-ARC) quote', async () => {
365
+ const { em, pricingService, service } = makePricedService()
366
+ const { cpqConfig, lineConfig } = amendFixture({
367
+ port_size: '1G',
368
+ _arcMirroredFromItemId: 'item-1',
369
+ _arcPriceFrozen: true,
370
+ })
371
+ cpqConfig.quoteType = 'new'
372
+ routeEm(em, cpqConfig, lineConfig)
373
+ pricingService.resolveProductCharges.mockResolvedValue([mrcCharge(120)])
374
+
375
+ await service.recalculate('cpq-1', { save: true }, SCOPE)
376
+
377
+ expect(pricingService.resolveProductCharges).toHaveBeenCalledTimes(1)
378
+ expect(lineConfig.mrcTotal).toBe('120')
379
+ })
380
+ })
@@ -1,7 +1,9 @@
1
+ import { LockMode } from '@mikro-orm/core'
1
2
  import { DefaultCpqQuotingService, QuotingError } from '../cpqQuotingService'
2
3
  import type { TenantScope } from '../types'
3
4
 
4
5
  interface MockEm {
6
+ transactional: jest.MockedFunction<(callback: (em: MockEm) => Promise<unknown>) => Promise<unknown>>
5
7
  findOne: jest.MockedFunction<(entity: unknown, where: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>>
6
8
  find: jest.MockedFunction<
7
9
  (entity: unknown, where: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown[]>
@@ -32,8 +34,9 @@ interface MockNumberGenerator {
32
34
 
33
35
  function createMockEm(): MockEm {
34
36
  const em: MockEm = {
37
+ transactional: jest.fn(),
35
38
  findOne: jest.fn(),
36
- find: jest.fn(),
39
+ find: jest.fn().mockResolvedValue([]),
37
40
  create: jest.fn().mockImplementation((_e, data: Record<string, unknown>) => ({ ...data, id: data.id ?? 'gen-id' })),
38
41
  persist: jest.fn(),
39
42
  assign: jest.fn().mockImplementation((entity: Record<string, unknown>, updates: Record<string, unknown>) => {
@@ -43,6 +46,7 @@ function createMockEm(): MockEm {
43
46
  flush: jest.fn().mockResolvedValue(undefined),
44
47
  fork: jest.fn(),
45
48
  }
49
+ em.transactional.mockImplementation((callback) => callback(em))
46
50
  em.fork.mockReturnValue(em)
47
51
  return em
48
52
  }
@@ -584,7 +588,7 @@ describe('DefaultCpqQuotingService.recalculate', () => {
584
588
  expect(em.flush).not.toHaveBeenCalled()
585
589
  })
586
590
 
587
- it('forks the EM when save=true (write path uses isolated UoW)', async () => {
591
+ it('locks the quote in a transaction when save=true', async () => {
588
592
  const { em, service } = makeService()
589
593
  em.findOne
590
594
  .mockResolvedValueOnce({
@@ -604,7 +608,9 @@ describe('DefaultCpqQuotingService.recalculate', () => {
604
608
 
605
609
  await service.recalculate('cpq-1', { save: true }, SCOPE)
606
610
 
607
- expect(em.fork).toHaveBeenCalledTimes(1)
611
+ expect(em.transactional).toHaveBeenCalledTimes(1)
612
+ expect(em.fork).toHaveBeenCalledWith({ clear: true, useContext: false })
613
+ expect(em.findOne.mock.calls[0][2]).toMatchObject({ lockMode: LockMode.PESSIMISTIC_WRITE })
608
614
  })
609
615
 
610
616
  it('marks status "new" when no lines exist (auto-managed status)', async () => {
@@ -655,4 +661,53 @@ describe('DefaultCpqQuotingService.recalculate', () => {
655
661
 
656
662
  expect(cpqConfig.cpqStatus).toBe('with_customer') // unchanged
657
663
  })
664
+
665
+ it.each(['accepted', 'rejected', 'cancelled'])(
666
+ 'rejects a saved recalculation on a %s (terminal) quote with 409 and persists nothing',
667
+ async (cpqStatus) => {
668
+ const { em, service } = makeService()
669
+ em.findOne.mockResolvedValueOnce({
670
+ id: 'cpq-1',
671
+ quoteId: 'sq-1',
672
+ cpqStatus,
673
+ customerId: 'cust',
674
+ currencyCode: 'USD',
675
+ version: 1,
676
+ parentQuoteId: null,
677
+ validationResult: null,
678
+ pricingSummary: null,
679
+ quoteContext: null,
680
+ })
681
+
682
+ await expect(service.recalculate('cpq-1', { save: true }, SCOPE)).rejects.toMatchObject({
683
+ status: 409,
684
+ message: `Cannot recalculate quote in '${cpqStatus}' status`,
685
+ })
686
+ expect(em.flush).not.toHaveBeenCalled()
687
+ },
688
+ )
689
+
690
+ it('still allows a read-only (save=false) recalculation on an accepted quote', async () => {
691
+ const { em, service } = makeService()
692
+ em.findOne
693
+ .mockResolvedValueOnce({
694
+ id: 'cpq-1',
695
+ quoteId: 'sq-1',
696
+ cpqStatus: 'accepted',
697
+ customerId: 'cust',
698
+ currencyCode: 'USD',
699
+ version: 1,
700
+ parentQuoteId: null,
701
+ validationResult: null,
702
+ pricingSummary: null,
703
+ quoteContext: null,
704
+ })
705
+ .mockResolvedValueOnce({ id: 'sq-1', currencyCode: 'USD', quoteNumber: 'Q-0001' })
706
+ em.find.mockResolvedValueOnce([])
707
+
708
+ const result = await service.recalculate('cpq-1', { save: false }, SCOPE)
709
+
710
+ expect(result.cpqStatus).toBe('accepted')
711
+ expect(em.flush).not.toHaveBeenCalled()
712
+ })
658
713
  })
@@ -0,0 +1,50 @@
1
+ import { summarizeOrderPricing } from '../orderPricingSummary'
2
+
3
+ const charge = (chargeType: 'nrc' | 'mrc', list: number, offer: number, standard = offer) => ({
4
+ chargeCode: `${chargeType}-fee`,
5
+ chargeName: `${chargeType} fee`,
6
+ chargeType,
7
+ pricingMethod: 'flat',
8
+ unitPrice: offer,
9
+ quantity: 1,
10
+ totalPrice: offer,
11
+ listUnitPrice: list,
12
+ listTotalPrice: list,
13
+ standardUnitPrice: standard,
14
+ standardTotalPrice: standard,
15
+ currencyCode: 'USD',
16
+ })
17
+
18
+ describe('summarizeOrderPricing', () => {
19
+ it('reproduces the quote figures: list totals, deal value over the term and total depth', () => {
20
+ const summary = summarizeOrderPricing([
21
+ { lineId: 'l1', quantity: 1, termMonths: 12, configuration: {}, charges: [charge('nrc', 890, 0), charge('mrc', 1200, 1050)] },
22
+ ], 'new', 'USD')
23
+
24
+ expect(summary.listNrcTotal).toBe(890)
25
+ expect(summary.listMrcTotal).toBe(1200)
26
+ expect(summary.dealValue).toEqual({ list: 890 + 1200 * 12, standard: 1050 * 12, offer: 1050 * 12 })
27
+ expect(summary.discountDepth.total).toBeCloseTo(1 - 12600 / 15290)
28
+ expect(summary).not.toHaveProperty('snapshotHash')
29
+ expect(summary).not.toHaveProperty('hasTimeBasedAdjustments')
30
+ })
31
+
32
+ it('leaves ARC mirror lines out of deal value, as on the quote', () => {
33
+ const summary = summarizeOrderPricing([
34
+ { lineId: 'kept', quantity: 1, termMonths: 12, configuration: { _arcMirroredFromItemId: 'item-1' }, charges: [charge('mrc', 400, 400)] },
35
+ { lineId: 'new', quantity: 1, termMonths: 12, configuration: {}, charges: [charge('mrc', 1200, 1050)] },
36
+ ], 'amend', 'USD')
37
+
38
+ expect(summary.dealValue.offer).toBe(1050 * 12)
39
+ expect(summary.listMrcTotal).toBe(1600)
40
+ })
41
+
42
+ it('tolerates legacy stored charges without code, type or waterfall fields', () => {
43
+ const summary = summarizeOrderPricing([
44
+ { lineId: 'l1', quantity: 2, termMonths: null, configuration: null, charges: [{ unitPrice: 50, totalPrice: 50 }] },
45
+ ], 'new', 'USD')
46
+
47
+ expect(summary.listMrcTotal).toBe(100)
48
+ expect(summary.dealValue.offer).toBe(50 * 2 * 12)
49
+ })
50
+ })
@@ -0,0 +1,62 @@
1
+ import { computeWaterfallSummary, type SnapshotLine } from '../priceAdjustments/pricingSnapshot'
2
+ import { resolveManualAdjustments } from '../priceAdjustments/resolveManualAdjustments'
3
+ import type { PriceAdjustment } from '../../data/price-adjustments'
4
+ const line: SnapshotLine = {
5
+ lineId: 'line', quantity: 2, termMonths: 3,
6
+ charges: [{ chargeCode: 'mrc', chargeName: 'MRC', chargeType: 'mrc', pricingMethod: 'flat', currencyCode: 'USD', quantity: 1,
7
+ listUnitPrice: 100, listTotalPrice: 100, standardUnitPrice: 90, standardTotalPrice: 90, unitPrice: 80, totalPrice: 80,
8
+ adjustments: [
9
+ { origin: 'rule', ruleCode: 'rule', ruleName: 'Rule', ruleType: 'discount_absolute', value: 10, unitPriceBefore: 100, unitPriceAfter: 90, delta: -10 },
10
+ { origin: 'manual', ruleCode: 'manual', ruleName: 'Manual', ruleType: 'discount_absolute', value: 10, unitPriceBefore: 90, unitPriceAfter: 80, delta: -10 },
11
+ ] }],
12
+ }
13
+ const row: PriceAdjustment = { id: 'a', quoteLineId: 'line', level: 'charge', chargeCode: 'mrc', adjustmentType: 'price_override', value: 80, currencyCode: 'USD', reasonCode: 'competitive_match' }
14
+ it('includes line quantity, term horizon and separate rule/manual totals', () => {
15
+ const summary = computeWaterfallSummary([line], [row], 'USD')
16
+ expect(summary).toMatchObject({ listMrcTotal: 200, standardMrcTotal: 180, ruleDiscountTotal: 20, manualDiscountTotal: 20, dealValue: { list: 600, standard: 540, offer: 480 } })
17
+ expect(summary.discountDepth.manual).toBeCloseTo(1 - 480 / 540)
18
+ expect(summary.discountDepth.total).toBeCloseTo(0.2)
19
+ })
20
+ it('counts NRC once and excludes usage even over a long horizon', () => {
21
+ const nrc = { ...line.charges[0], chargeType: 'nrc' as const }
22
+ const usage = { ...line.charges[0], chargeType: 'usage' as const }
23
+ expect(computeWaterfallSummary([{ ...line, termMonths: 24, charges: [nrc, usage] }], [], 'USD').dealValue).toEqual({ list: 200, standard: 180, offer: 160 })
24
+ })
25
+ it('uses 12 months without a term; excludes unchanged ARC mirrors from deal value', () => {
26
+ expect(computeWaterfallSummary([{ ...line, termMonths: null }], [], 'USD').dealValue.list).toBe(2400)
27
+ expect(computeWaterfallSummary([{ ...line, includeInDeal: false }], [], 'USD').dealValue).toEqual({ list: 0, standard: 0, offer: 0 })
28
+ })
29
+ it('represents zero baselines by null and rule markups by negative depth', () => {
30
+ expect(computeWaterfallSummary([], [], 'USD').discountDepth).toEqual({ rule: null, manual: null, total: null, maxLineManual: null })
31
+ const marked = { ...line.charges[0], standardTotalPrice: 110, totalPrice: 110 }
32
+ expect(computeWaterfallSummary([{ ...line, charges: [marked] }], [], 'USD').discountDepth.rule).toBeCloseTo(-0.1)
33
+ })
34
+ it('hash ignores ordering, reason notes and authors but tracks price, quantity, horizon and adjustment changes', () => {
35
+ const other = { ...line, lineId: 'other' }
36
+ const initial = computeWaterfallSummary([line, other], [row], 'USD').snapshotHash
37
+ expect(computeWaterfallSummary([other, line], [{ ...row, reasonNote: 'Edited', reasonCode: 'other', createdByUserId: 'user' }], 'USD').snapshotHash).toBe(initial)
38
+ for (const changed of [{ ...line, quantity: 3 }, { ...line, termMonths: 6 }, { ...line, charges: [{ ...line.charges[0], totalPrice: 70 }] }]) {
39
+ expect(computeWaterfallSummary([changed, other], [row], 'USD').snapshotHash).not.toBe(initial)
40
+ }
41
+ expect(computeWaterfallSummary([line, other], [{ ...row, value: 70 }], 'USD').snapshotHash).not.toBe(initial)
42
+ })
43
+ it('normalizes legacy waterfall fields to the same hash and never changes legacy discountTotal', () => {
44
+ const legacy = { ...line.charges[0] }
45
+ delete legacy.listUnitPrice; delete legacy.listTotalPrice; delete legacy.standardUnitPrice; delete legacy.standardTotalPrice
46
+ const result = computeWaterfallSummary([{ ...line, charges: [legacy] }], [row], 'USD')
47
+ expect(result.snapshotHash).toBe(computeWaterfallSummary([line], [row], 'USD').snapshotHash)
48
+ expect(result).not.toHaveProperty('discountTotal')
49
+ })
50
+ it('stable hash and money after repeated resolver passes', () => {
51
+ const input = [{ ...line, parentLineId: null, enginePriced: true, mirrored: false }]
52
+ const first = resolveManualAdjustments(input, [row], 'USD')
53
+ const second = resolveManualAdjustments(first.lines, [row], 'USD')
54
+ expect(computeWaterfallSummary(first.lines.map((priced) => ({ ...priced, quantity: 2 })), [row], 'USD')).toEqual(computeWaterfallSummary(second.lines.map((priced) => ({ ...priced, quantity: 2 })), [row], 'USD'))
55
+ })
56
+ it('hash tracks deal eligibility and normalizes omitted eligibility to true', () => {
57
+ const baseline = computeWaterfallSummary([line], [], 'USD')
58
+ expect(computeWaterfallSummary([{ ...line, includeInDeal: true }], [], 'USD').snapshotHash).toBe(baseline.snapshotHash)
59
+ const excluded = computeWaterfallSummary([{ ...line, includeInDeal: false }], [], 'USD')
60
+ expect(excluded.dealValue.offer).toBe(0)
61
+ expect(excluded.snapshotHash).not.toBe(baseline.snapshotHash)
62
+ })
@@ -0,0 +1,35 @@
1
+ import { cloneQuoteAdjustments, removeQuoteLineTree } from '../priceAdjustments/quoteAdjustmentLifecycle'
2
+ import { CpqQuoteLineConfiguration, CpqQuotePriceAdjustment } from '../../data/entities'
3
+ import { SalesQuoteLine } from '@open-mercato/core/modules/sales/data/entities'
4
+ const scope = { tenantId: 'tenant', organizationId: 'org' }
5
+ it('clones rows with new ids and remapped lines without changing reasons or the source', async () => {
6
+ const source = { id: 'adjustment', quoteLineId: 'old-line', level: 'charge', chargeCode: 'fee', adjustmentType: 'price_override', value: '80', currencyCode: 'USD', reasonCode: 'inactive_reason', createdByUserId: 'author', windowKind: 'relative', fromMonth: 1, toMonth: null, isActive: true }
7
+ const created: Array<Record<string, unknown>> = []
8
+ const em = { find: jest.fn().mockResolvedValue([source]), create: jest.fn((_entity: unknown, values: Record<string, unknown>) => { created.push(values); return values }), persist: jest.fn() }
9
+ await cloneQuoteAdjustments(em as never, 'source', { id: 'target', quoteId: 'new-sales' }, new Map([['old-line', 'new-line']]), scope)
10
+ expect(created[0]).toMatchObject({ quoteLineId: 'new-line', quoteConfigurationId: 'target', quoteId: 'new-sales', reasonCode: 'inactive_reason', createdByUserId: 'author', ...scope })
11
+ expect(created[0].id).not.toBe(source.id)
12
+ expect(source.quoteLineId).toBe('old-line')
13
+ expect(em.find).toHaveBeenCalledWith(CpqQuotePriceAdjustment, expect.objectContaining({ quoteConfigurationId: 'source', ...scope, deletedAt: null }), expect.objectContaining({ limit: 500 }))
14
+ })
15
+ it('keeps missing targets invalid with a fresh orphan id rather than a source-quote pointer', async () => {
16
+ const source = { id: 'adjustment', quoteLineId: 'missing', reasonCode: 'promotion' }
17
+ const create = jest.fn((_entity: unknown, values: Record<string, unknown>) => values)
18
+ await cloneQuoteAdjustments({ find: jest.fn().mockResolvedValue([source]), create, persist: jest.fn() } as never, 'source', { id: 'target', quoteId: 'sales' }, new Map(), scope)
19
+ expect(create.mock.calls[0][1].quoteLineId).not.toBe('missing')
20
+ })
21
+ it('soft-deletes the whole nested bundle subtree and its adjustments before returning', async () => {
22
+ const root = { id: 'root-config', quoteLineId: 'root', parentLineId: null, deletedAt: null }
23
+ const child = { id: 'child-config', quoteLineId: 'child', parentLineId: 'root', deletedAt: null }
24
+ const grandchild = { id: 'grandchild-config', quoteLineId: 'grandchild', parentLineId: 'child', deletedAt: null }
25
+ const unrelated = { id: 'other-config', quoteLineId: 'other', parentLineId: null, deletedAt: null }
26
+ const sales = [{ id: 'root', deletedAt: null }, { id: 'child', deletedAt: null }, { id: 'grandchild', deletedAt: null }]
27
+ const adjustments = [{ id: 'adjustment', quoteLineId: 'grandchild', deletedAt: null, isActive: true }]
28
+ const em = { find: jest.fn(async (entity: unknown) => entity === CpqQuoteLineConfiguration ? [root, child, grandchild, unrelated] : entity === SalesQuoteLine ? sales : adjustments), flush: jest.fn().mockResolvedValue(undefined) }
29
+ await removeQuoteLineTree(em as never, 'quote', root as never, scope)
30
+ for (const row of [root, child, grandchild, ...sales, ...adjustments]) expect(row.deletedAt).toBeInstanceOf(Date)
31
+ expect(unrelated.deletedAt).toBe(null)
32
+ expect(adjustments[0].isActive).toBe(false)
33
+ expect(em.flush).toHaveBeenCalled()
34
+ expect(em.find).toHaveBeenCalledWith(CpqQuotePriceAdjustment, expect.objectContaining({ quoteConfigurationId: 'quote', quoteLineId: { $in: ['root', 'child', 'grandchild'] }, ...scope }), expect.anything())
35
+ })
@@ -0,0 +1,65 @@
1
+ import { DefaultCpqQuotingService } from '../cpqQuotingService'
2
+ import { CpqProductOffering, CpqQuoteConfiguration, CpqQuoteLineConfiguration, CpqQuotePriceAdjustment } from '../../data/entities'
3
+ import { SalesQuote, SalesQuoteLine } from '@open-mercato/core/modules/sales/data/entities'
4
+ const scope = { tenantId: '11111111-1111-4111-8111-111111111111', organizationId: '22222222-2222-4222-8222-222222222222' }
5
+ const ids = { quote: '33333333-3333-4333-8333-333333333333', sales: '44444444-4444-4444-8444-444444444444', line: '55555555-5555-4555-8555-555555555555', adjustment: '66666666-6666-4666-8666-666666666666', user: '77777777-7777-4777-8777-777777777777' }
6
+ function setup() {
7
+ const quote = { id: ids.quote, quoteId: ids.sales, customerId: 'customer', quoteType: 'new', cpqStatus: 'new', currencyCode: 'USD', pricingRevision: 0, ...scope }
8
+ const sales = { id: ids.sales, quoteNumber: 'Q-1', currencyCode: 'USD' }
9
+ const line = { id: 'config', quoteLineId: ids.line, quoteConfigurationId: ids.quote, offeringId: 'offer', productId: null, parentLineId: null, quantity: 2, configuration: {}, nrcTotal: '0', mrcTotal: '200', isConfigured: true, charges: [], usageEstimates: null, ...scope }
10
+ const salesLine = { id: ids.line, totalNetAmount: '200' }
11
+ const row = { id: ids.adjustment, quoteConfigurationId: ids.quote, quoteId: ids.sales, quoteLineId: ids.line, level: 'charge', chargeCode: 'fee', chargeTypeFilter: null, adjustmentType: 'discount_absolute', value: '20', currencyCode: 'USD', reasonCode: 'competitive_match', reasonNote: null, windowKind: 'relative', fromMonth: 1, toMonth: null, createdByUserId: ids.user, ...scope }
12
+ const findOne = jest.fn(async (entity: unknown) => entity === CpqQuoteConfiguration ? quote : entity === SalesQuote ? sales : entity === CpqProductOffering ? { id: 'offer', name: 'Offering', offeringType: 'simple' } : entity === SalesQuoteLine ? salesLine : null)
13
+ const find = jest.fn(async (entity: unknown) => entity === CpqQuoteLineConfiguration ? [line] : entity === CpqQuotePriceAdjustment ? [row] : [])
14
+ const tx = { findOne, find, assign: jest.fn((entity: object, updates: object) => Object.assign(entity, updates)), flush: jest.fn().mockResolvedValue(undefined) }
15
+ const em = { ...tx, fork: jest.fn(), transactional: jest.fn(async (execute: (manager: typeof tx) => Promise<unknown>) => execute(tx)) }
16
+ em.fork.mockReturnValue(em)
17
+ const pricing = { resolveProductCharges: jest.fn().mockResolvedValue([{ chargeCode: 'fee', chargeName: 'Fee', chargeType: 'mrc', pricingMethod: 'flat', unitPrice: 100, totalPrice: 100, quantity: 1, currencyCode: 'USD', adjustments: [] }]) }
18
+ const service = new DefaultCpqQuotingService({ em: em as never, cpqPricingService: pricing as never, cpqValidationService: { validateOfferingConfiguration: async () => ({ errors: [] }), validateRelationships: async () => [] } as never, cpqProductService: {} as never, salesDocumentNumberGenerator: {} as never })
19
+ return { service, quote, line, row, salesLine, em, tx }
20
+ }
21
+ it('applies stored adjustments to quote/sales totals exactly once and preserves deprecated discountTotal', async () => {
22
+ const { service, quote, line, salesLine, em } = setup()
23
+ const first = await service.recalculate(ids.quote, { save: true }, scope)
24
+ expect(first.lines[0].mrcTotal).toBe(160)
25
+ expect(first.lines[0].charges[0]).toMatchObject({ listTotalPrice: 100, standardTotalPrice: 100, totalPrice: 80 })
26
+ expect(first.pricingSummary).toMatchObject({ mrcTotal: 160, manualDiscountTotal: 40, ruleDiscountTotal: 0, discountTotal: 0 })
27
+ expect(first.pricingRevision).toBe(1)
28
+ expect(salesLine.totalNetAmount).toBe('160')
29
+ expect(line.mrcTotal).toBe('160')
30
+ const second = await service.recalculate(ids.quote, { save: true }, scope)
31
+ expect(second.lines[0].charges).toEqual(first.lines[0].charges)
32
+ expect(second.pricingSummary.snapshotHash).toBe(first.pricingSummary.snapshotHash)
33
+ expect(quote.pricingRevision).toBe(2)
34
+ expect(em.transactional).toHaveBeenCalledTimes(2)
35
+ })
36
+ it('keeps an invalid row, marks the line and quote incomplete, and charges standard', async () => {
37
+ const { service, row } = setup()
38
+ row.chargeCode = 'removed-charge'
39
+ const result = await service.recalculate(ids.quote, { save: true }, scope)
40
+ expect(result.cpqStatus).toBe('incomplete')
41
+ expect(result.lines[0].isConfigured).toBe(false)
42
+ expect(result.lines[0].mrcTotal).toBe(200)
43
+ expect(result.lines[0].validationErrors).toEqual([expect.objectContaining({ message: 'ADJUSTMENT_TARGET_MISSING' })])
44
+ expect(row.chargeCode).toBe('removed-charge')
45
+ })
46
+ it('previews saved adjustments without changing a managed entity, revision or persisted snapshot', async () => {
47
+ const { service, quote, line, tx, em } = setup()
48
+ const result = await service.recalculate(ids.quote, { save: false }, scope)
49
+ expect(result.lines[0].mrcTotal).toBe(160)
50
+ expect(quote.cpqStatus).toBe('new')
51
+ expect(quote.pricingRevision).toBe(0)
52
+ expect(quote).not.toHaveProperty('pricingSummary')
53
+ expect(line.mrcTotal).toBe('200')
54
+ expect(line.charges).toEqual([])
55
+ expect(tx.flush).not.toHaveBeenCalled()
56
+ expect(tx.assign).not.toHaveBeenCalled()
57
+ expect(em.transactional).not.toHaveBeenCalled()
58
+ })
59
+ it('loads adjustments only once per recalculation and scopes the query', async () => {
60
+ const { service, tx } = setup()
61
+ await service.recalculate(ids.quote, { save: false }, scope)
62
+ const calls = tx.find.mock.calls.filter(([entity]) => entity === CpqQuotePriceAdjustment)
63
+ expect(calls).toHaveLength(1)
64
+ expect(calls[0]).toEqual([CpqQuotePriceAdjustment, expect.objectContaining({ quoteConfigurationId: ids.quote, ...scope, deletedAt: null }), { limit: 500, orderBy: { id: 'asc' } }])
65
+ })
@@ -0,0 +1,45 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { DefaultCpqQuotingService } from '../cpqQuotingService'
3
+ import { CpqProductOffering, CpqQuoteConfiguration, CpqQuoteLineConfiguration, CpqQuotePriceAdjustment } from '../../data/entities'
4
+ import { SalesQuote, SalesQuoteLine } from '@open-mercato/core/modules/sales/data/entities'
5
+ const scope = { tenantId: randomUUID(), organizationId: randomUUID() }
6
+
7
+ it.each([false, true])('clones with remapped adjustments and safe pricing markers (ARC source=%s)', async (arc) => {
8
+ const quoteId = randomUUID(), salesId = randomUUID(), lineId = randomUUID(), userId = randomUUID()
9
+ const charge = { chargeCode: 'fee', chargeName: 'Fee', chargeType: 'mrc', pricingMethod: 'flat', unitPrice: 100, totalPrice: 100, quantity: 1, currencyCode: 'USD', adjustments: [] }
10
+ const original = { id: quoteId, quoteId: salesId, customerId: userId, cpqStatus: 'ready', quoteType: arc ? 'amend' : 'new', version: 1, pricingRevision: 0, currencyCode: 'USD', ...scope }
11
+ const line = { id: randomUUID(), quoteLineId: lineId, quoteConfigurationId: quoteId, offeringId: 'offer', productId: null, parentLineId: null, quantity: 1, configuration: arc ? { _arcPriceFrozen: true, _arcMirroredFromItemId: randomUUID() } : {}, charges: [charge], mrcTotal: '100', nrcTotal: '0', isConfigured: true, deletedAt: null, ...scope }
12
+ const rows = new Map<unknown, Array<Record<string, unknown>>>([
13
+ [CpqQuoteConfiguration, [original]], [CpqQuoteLineConfiguration, [line]],
14
+ [SalesQuote, [{ id: salesId, quoteNumber: 'Q-1', customerEntityId: userId, currencyCode: 'USD', ...scope }]],
15
+ [SalesQuoteLine, [{ id: lineId, lineNumber: 1, quantity: '1', ...scope }]],
16
+ [CpqQuotePriceAdjustment, arc ? [] : [{ id: randomUUID(), quoteConfigurationId: quoteId, quoteId: salesId, quoteLineId: lineId, level: 'charge', chargeCode: 'fee', chargeTypeFilter: null, adjustmentType: 'price_override', value: '80', currencyCode: 'USD', reasonCode: 'inactive_reason', reasonNote: null, windowKind: 'relative', fromMonth: 1, toMonth: null, createdByUserId: userId, deletedAt: null, isActive: true, ...scope }]],
17
+ ])
18
+ const matches = (row: Record<string, unknown>, where: Record<string, unknown>) => Object.entries(where).every(([key, value]) =>
19
+ value === null ? row[key] == null : typeof value === 'object' ? true : row[key] === value)
20
+ const em = {
21
+ fork: jest.fn(), transactional: jest.fn(), persist: jest.fn(), flush: jest.fn().mockResolvedValue(undefined),
22
+ assign: jest.fn((entity: object, values: object) => Object.assign(entity, values)),
23
+ create: jest.fn((entity: unknown, values: Record<string, unknown>) => {
24
+ const row = { id: randomUUID(), quoteType: 'new', createdAt: new Date(), updatedAt: new Date(), deletedAt: null, ...values }
25
+ const group = rows.get(entity) ?? []; group.push(row); rows.set(entity, group); return row
26
+ }),
27
+ findOne: jest.fn(async (entity: unknown, where: Record<string, unknown>) => entity === CpqProductOffering ? { id: 'offer', name: 'Offering', offeringType: 'simple' } : (rows.get(entity) ?? []).find((row) => matches(row, where)) ?? null),
28
+ find: jest.fn(async (entity: unknown, where: Record<string, unknown>) => (rows.get(entity) ?? []).filter((row) => matches(row, where))),
29
+ }
30
+ em.fork.mockReturnValue(em)
31
+ em.transactional.mockImplementation(async (execute: (tx: typeof em) => Promise<unknown>) => execute(em))
32
+ const service = new DefaultCpqQuotingService({ em: em as never,
33
+ cpqPricingService: { resolveProductCharges: async () => [charge] } as never,
34
+ cpqValidationService: { validateOfferingConfiguration: async () => ({ errors: [] }), validateRelationships: async () => [] } as never,
35
+ cpqProductService: {} as never, salesDocumentNumberGenerator: { generate: async () => ({ number: 'Q-2' }) } as never })
36
+ const result = await service.cloneQuote(quoteId, scope)
37
+ expect(result.id).not.toBe(quoteId)
38
+ expect(result.lines[0].lineId).not.toBe(lineId)
39
+ expect(result.lines[0].configuration).not.toHaveProperty('_arcPriceFrozen')
40
+ expect(result.lines[0].charges[0].totalPrice).toBe(arc ? 100 : 80)
41
+ if (!arc) {
42
+ const copied = (rows.get(CpqQuotePriceAdjustment) ?? []).find((row) => row.quoteConfigurationId === result.id)
43
+ expect(copied).toMatchObject({ quoteLineId: result.lines[0].lineId, value: '80', reasonCode: 'inactive_reason' })
44
+ } else expect(line.configuration).toHaveProperty('_arcPriceFrozen', true)
45
+ })