@dainamite/cpq 0.4.0 → 0.6.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 (273) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/MIGRATION.md +101 -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/handleArcError.js +2 -2
  8. package/dist/modules/cpq/api/_helpers/handleArcError.js.map +2 -2
  9. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
  10. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
  11. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
  12. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
  13. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +2 -2
  14. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +2 -2
  15. package/dist/modules/cpq/api/orders/[orderId]/route.js +19 -3
  16. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
  17. package/dist/modules/cpq/api/orders/[orderId]/status/route.js +2 -2
  18. package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +2 -2
  19. package/dist/modules/cpq/api/orders/route.js +49 -4
  20. package/dist/modules/cpq/api/orders/route.js.map +2 -2
  21. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +2 -2
  22. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +2 -2
  23. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
  24. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
  25. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +45 -5
  26. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
  27. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
  28. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
  29. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
  30. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
  31. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
  32. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
  33. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
  34. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
  35. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +34 -4
  36. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
  37. package/dist/modules/cpq/api/quotes/[quoteId]/route.js +2 -2
  38. package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +2 -2
  39. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
  40. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
  41. package/dist/modules/cpq/api/quotes/price/route.js +43 -3
  42. package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
  43. package/dist/modules/cpq/api/quotes/route.js +2 -2
  44. package/dist/modules/cpq/api/quotes/route.js.map +2 -2
  45. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +1 -1
  46. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +1 -1
  47. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +1 -1
  48. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +1 -1
  49. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +22 -8
  50. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  51. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +1 -1
  52. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +89 -232
  53. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  54. package/dist/modules/cpq/backend/cpq/orders/page.js.map +1 -1
  55. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
  56. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
  57. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +1 -1
  58. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +1 -1
  59. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +205 -0
  60. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
  61. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
  62. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
  63. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
  64. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
  65. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
  66. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
  67. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
  68. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
  69. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +144 -221
  70. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
  71. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +1 -1
  72. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
  73. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
  74. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +1 -1
  75. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +1 -1
  76. package/dist/modules/cpq/ce.js +8 -0
  77. package/dist/modules/cpq/ce.js.map +2 -2
  78. package/dist/modules/cpq/cli.js +34 -1
  79. package/dist/modules/cpq/cli.js.map +2 -2
  80. package/dist/modules/cpq/commands/price-adjustments.js +107 -0
  81. package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
  82. package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
  83. package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
  84. package/dist/modules/cpq/components/ChargeBreakdownTable.js +154 -0
  85. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
  86. package/dist/modules/cpq/components/CpqListView.js.map +2 -2
  87. package/dist/modules/cpq/components/PriceWaterfall.js +66 -0
  88. package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
  89. package/dist/modules/cpq/components/PricingSummaryCard.js +117 -0
  90. package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
  91. package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
  92. package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
  93. package/dist/modules/cpq/data/entities.js +97 -0
  94. package/dist/modules/cpq/data/entities.js.map +2 -2
  95. package/dist/modules/cpq/data/price-adjustments.js +70 -0
  96. package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
  97. package/dist/modules/cpq/data/validators.js +4 -0
  98. package/dist/modules/cpq/data/validators.js.map +2 -2
  99. package/dist/modules/cpq/di.js +6 -0
  100. package/dist/modules/cpq/di.js.map +2 -2
  101. package/dist/modules/cpq/events.js +47 -0
  102. package/dist/modules/cpq/events.js.map +2 -2
  103. package/dist/modules/cpq/index.js +1 -1
  104. package/dist/modules/cpq/index.js.map +2 -2
  105. package/dist/modules/cpq/lib/auditActor.js +10 -0
  106. package/dist/modules/cpq/lib/auditActor.js.map +7 -0
  107. package/dist/modules/cpq/lib/formatMoney.js +7 -0
  108. package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
  109. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
  110. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
  111. package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
  112. package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
  113. package/dist/modules/cpq/lib/quotePermissions.js +13 -0
  114. package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
  115. package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
  116. package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
  117. package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
  118. package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
  119. package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
  120. package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
  121. package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
  122. package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
  123. package/dist/modules/cpq/services/cpqOrderService.js +34 -30
  124. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  125. package/dist/modules/cpq/services/cpqPricingService.js +7 -0
  126. package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
  127. package/dist/modules/cpq/services/cpqQuotingService.js +339 -111
  128. package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
  129. package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
  130. package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
  131. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
  132. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
  133. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
  134. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
  135. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
  136. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
  137. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
  138. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
  139. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
  140. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
  141. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
  142. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
  143. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
  144. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
  145. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
  146. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
  147. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
  148. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
  149. package/dist/modules/cpq/services/types.js.map +2 -2
  150. package/dist/modules/cpq/setup.js +6 -0
  151. package/dist/modules/cpq/setup.js.map +2 -2
  152. package/package.json +9 -9
  153. package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +8 -5
  154. package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
  155. package/src/modules/cpq/acl.ts +1 -0
  156. package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
  157. package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
  158. package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
  159. package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
  160. package/src/modules/cpq/api/_helpers/handleArcError.ts +12 -12
  161. package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
  162. package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
  163. package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +2 -2
  164. package/src/modules/cpq/api/orders/[orderId]/route.ts +20 -2
  165. package/src/modules/cpq/api/orders/[orderId]/status/route.ts +2 -2
  166. package/src/modules/cpq/api/orders/route.ts +53 -3
  167. package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +2 -2
  168. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
  169. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +51 -4
  170. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
  171. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
  172. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
  173. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
  174. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
  175. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +42 -3
  176. package/src/modules/cpq/api/quotes/[quoteId]/route.ts +2 -2
  177. package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
  178. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
  179. package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
  180. package/src/modules/cpq/api/quotes/price/route.ts +28 -2
  181. package/src/modules/cpq/api/quotes/route.ts +2 -2
  182. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +1 -1
  183. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +1 -1
  184. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +1 -1
  185. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +32 -10
  186. package/src/modules/cpq/backend/cpq/offerings/page.tsx +1 -1
  187. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +60 -269
  188. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
  189. package/src/modules/cpq/backend/cpq/orders/page.tsx +1 -1
  190. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +1 -1
  191. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  192. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
  193. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
  194. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
  195. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
  196. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
  197. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
  198. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +178 -266
  199. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
  200. package/src/modules/cpq/backend/cpq/quotes/page.tsx +1 -1
  201. package/src/modules/cpq/backend/cpq/specifications/page.tsx +1 -1
  202. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  203. package/src/modules/cpq/ce.ts +8 -0
  204. package/src/modules/cpq/cli.ts +48 -1
  205. package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
  206. package/src/modules/cpq/commands/price-adjustments.ts +90 -0
  207. package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
  208. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +201 -0
  209. package/src/modules/cpq/components/CpqListView.tsx +2 -1
  210. package/src/modules/cpq/components/PriceWaterfall.tsx +94 -0
  211. package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
  212. package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
  213. package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
  214. package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
  215. package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
  216. package/src/modules/cpq/data/entities.ts +83 -0
  217. package/src/modules/cpq/data/price-adjustments.ts +67 -0
  218. package/src/modules/cpq/data/validators.ts +3 -0
  219. package/src/modules/cpq/di.ts +6 -0
  220. package/src/modules/cpq/events.ts +53 -2
  221. package/src/modules/cpq/index.ts +1 -1
  222. package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
  223. package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
  224. package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
  225. package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
  226. package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
  227. package/src/modules/cpq/lib/auditActor.ts +13 -0
  228. package/src/modules/cpq/lib/formatMoney.ts +4 -0
  229. package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
  230. package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
  231. package/src/modules/cpq/lib/quotePermissions.ts +22 -0
  232. package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
  233. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
  234. package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
  235. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
  236. package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
  237. package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
  238. package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
  239. package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
  240. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
  241. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
  242. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
  243. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
  244. package/src/modules/cpq/services/__tests__/errorBrands.test.ts +72 -0
  245. package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
  246. package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
  247. package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
  248. package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
  249. package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
  250. package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
  251. package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
  252. package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
  253. package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
  254. package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
  255. package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
  256. package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
  257. package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
  258. package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
  259. package/src/modules/cpq/services/cpqOrderService.ts +32 -28
  260. package/src/modules/cpq/services/cpqPricingService.ts +8 -0
  261. package/src/modules/cpq/services/cpqQuotingService.ts +388 -125
  262. package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
  263. package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
  264. package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
  265. package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
  266. package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
  267. package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
  268. package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
  269. package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
  270. package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
  271. package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
  272. package/src/modules/cpq/services/types.ts +34 -1
  273. package/src/modules/cpq/setup.ts +7 -1
@@ -1,3 +1,12 @@
1
+ var _a, _b;
2
+ import { cloneQuoteAdjustments, removeQuoteLineTree } from "./priceAdjustments/quoteAdjustmentLifecycle.js";
3
+ import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
4
+ import { loadQuoteAdjustments } from "./priceAdjustments/loadQuoteAdjustments.js";
5
+ import { resolveManualAdjustments } from "./priceAdjustments/resolveManualAdjustments.js";
6
+ import { withChargeWaterfall } from "./priceAdjustments/chargeWaterfall.js";
7
+ import { withQuotePricingWrite } from "./priceAdjustments/quotePricingWrite.js";
8
+ import { emitQuoteWriteEvents } from "./priceAdjustments/quotePricingEvents.js";
9
+ import { computeWaterfallSummary } from "./priceAdjustments/pricingSnapshot.js";
1
10
  import { SalesQuote, SalesQuoteLine } from "@open-mercato/core/modules/sales/data/entities";
2
11
  import { CustomerEntity } from "@open-mercato/core/modules/customers/data/entities";
3
12
  import {
@@ -16,6 +25,17 @@ import {
16
25
  ARC_QUOTE_TYPES,
17
26
  getBaseCurrencyCode
18
27
  } from "./types.js";
28
+ import {
29
+ ARC_PRICE_FROZEN_KEY,
30
+ ARC_REPRICED_KEY,
31
+ arcPricingInputsChanged,
32
+ buildArcMirrorConfiguration,
33
+ isFrozenAmendMirrorLine,
34
+ isArcMirrorLine,
35
+ stripArcMarkers,
36
+ stripQuoteOnlyArcMarkers
37
+ } from "./arcPriceFreeze.js";
38
+ import { isArcCancelledLine, planArcLineRemoval, planArcLineRestore, removalCancelsItem } from "./arcLineRemoval.js";
19
39
  class DefaultCpqQuotingService {
20
40
  constructor(deps) {
21
41
  this.em = deps.em;
@@ -107,12 +127,26 @@ class DefaultCpqQuotingService {
107
127
  }
108
128
  // ─── Add Quote Item ────────────────────────────────────────────
109
129
  async addQuoteItem(quoteId, item, scope) {
110
- const em = this.em;
111
- const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
130
+ return this.writeQuote(quoteId, scope, (em, quote) => this.addQuoteItemInTransaction(quoteId, item, scope, em, quote), void 0, void 0, LINE_EVENTS);
131
+ }
132
+ async addQuoteItemInTransaction(quoteId, item, scope, em, cpqConfig) {
133
+ item = { ...item, configuration: stripArcMarkers(item.configuration) };
112
134
  const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
113
135
  if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
114
136
  throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`);
115
137
  }
138
+ if (item.parentLineId && cpqConfig.quoteType && cpqConfig.quoteType !== "new") {
139
+ const parent = await em.findOne(CpqQuoteLineConfiguration, {
140
+ quoteLineId: item.parentLineId,
141
+ quoteConfigurationId: cpqConfig.id,
142
+ organizationId: scope.organizationId,
143
+ tenantId: scope.tenantId,
144
+ deletedAt: null
145
+ });
146
+ if (parent && isArcCancelledLine(parent, cpqConfig.quoteType)) {
147
+ throw new QuotingError(409, "Cannot add a component to a subscription item marked for removal");
148
+ }
149
+ }
116
150
  const { offering, spec } = await this.resolveOffering(em, item, scope);
117
151
  const mergedConfig = {
118
152
  ...cpqConfig.quoteContext ?? {},
@@ -223,8 +257,10 @@ class DefaultCpqQuotingService {
223
257
  }
224
258
  // ─── Update Quote Item ─────────────────────────────────────────
225
259
  async updateQuoteItem(quoteId, lineId, update, scope) {
226
- const em = this.em;
227
- const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
260
+ return this.writeQuote(quoteId, scope, (em, quote) => this.updateQuoteItemInTransaction(quoteId, lineId, update, scope, em, quote), void 0, void 0, LINE_EVENTS);
261
+ }
262
+ async updateQuoteItemInTransaction(quoteId, lineId, update, scope, em, cpqConfig) {
263
+ update = { ...update, configuration: stripArcMarkers(update.configuration) };
228
264
  const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
229
265
  if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
230
266
  throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`);
@@ -237,6 +273,9 @@ class DefaultCpqQuotingService {
237
273
  deletedAt: null
238
274
  });
239
275
  if (!lineConfig) throw new QuotingError(404, "Quote line not found");
276
+ if (isArcCancelledLine(lineConfig, cpqConfig.quoteType)) {
277
+ throw new QuotingError(409, "This subscription item is marked for removal \u2014 restore it before changing it");
278
+ }
240
279
  const quoteLine = await em.findOne(SalesQuoteLine, {
241
280
  id: lineId,
242
281
  organizationId: scope.organizationId,
@@ -278,12 +317,27 @@ class DefaultCpqQuotingService {
278
317
  if (existingCfg?._arcMirroredName !== void 0) {
279
318
  arcMarkers._arcMirroredName = existingCfg._arcMirroredName;
280
319
  }
320
+ const wasFrozen = isFrozenAmendMirrorLine(existingCfg, cpqConfig.quoteType);
321
+ const pricingInputsChanged = wasFrozen && arcPricingInputsChanged(
322
+ {
323
+ configuration: existingCfg,
324
+ quantity: lineConfig.quantity,
325
+ termMonths: lineConfig.termMonths,
326
+ startDate: lineConfig.startDate,
327
+ endDate: lineConfig.endDate,
328
+ usageEstimates: lineConfig.usageEstimates
329
+ },
330
+ update
331
+ );
332
+ const staysFrozen = wasFrozen && !pricingInputsChanged;
333
+ if (staysFrozen) arcMarkers[ARC_PRICE_FROZEN_KEY] = true;
334
+ if (pricingInputsChanged || existingCfg?.[ARC_REPRICED_KEY] === true) arcMarkers[ARC_REPRICED_KEY] = true;
281
335
  update.configuration = { ...update.configuration, ...arcMarkers };
282
336
  mergedConfig._arcMirroredFromItemId = arcMarkers._arcMirroredFromItemId;
283
337
  mergedConfig._arcMirroredName = arcMarkers._arcMirroredName;
284
338
  const isArcMirrored = !!arcMarkers._arcMirroredFromItemId;
285
339
  let charges;
286
- if (lineConfig.offeringId || lineConfig.productId) {
340
+ if ((lineConfig.offeringId || lineConfig.productId) && !staysFrozen) {
287
341
  charges = await this.pricingService.resolveProductCharges({
288
342
  offeringId: lineConfig.offeringId ?? void 0,
289
343
  productId: lineConfig.productId ?? void 0,
@@ -329,8 +383,9 @@ class DefaultCpqQuotingService {
329
383
  }
330
384
  // ─── Remove Quote Item ─────────────────────────────────────────
331
385
  async removeQuoteItem(quoteId, lineId, scope) {
332
- const em = this.em;
333
- const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
386
+ return this.writeQuote(quoteId, scope, (em, quote) => this.removeQuoteItemInTransaction(quoteId, lineId, scope, em, quote), void 0, void 0, LINE_EVENTS);
387
+ }
388
+ async removeQuoteItemInTransaction(quoteId, lineId, scope, em, cpqConfig) {
334
389
  const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
335
390
  if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
336
391
  throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`);
@@ -343,31 +398,78 @@ class DefaultCpqQuotingService {
343
398
  deletedAt: null
344
399
  });
345
400
  if (!lineConfig) throw new QuotingError(404, "Quote line not found");
346
- const now = /* @__PURE__ */ new Date();
347
- lineConfig.deletedAt = now;
348
- const children = await em.find(CpqQuoteLineConfiguration, {
349
- parentLineId: lineId,
350
- quoteConfigurationId: cpqConfig.id,
401
+ if (removalCancelsItem(lineConfig, cpqConfig.quoteType)) {
402
+ if (lineConfig.action === "cancel") {
403
+ throw new QuotingError(409, "This subscription item is already marked for removal");
404
+ }
405
+ const plan = planArcLineRemoval(lineConfig, await loadLines(em, cpqConfig.id, scope), cpqConfig.quoteType);
406
+ for (const root of plan.removeRoots) await removeQuoteLineTree(em, cpqConfig.id, root, scope);
407
+ await withAtomicFlush(em, [() => {
408
+ for (const line of plan.cancel) {
409
+ line.action = "cancel";
410
+ line.charges = [];
411
+ line.nrcTotal = "0";
412
+ line.mrcTotal = "0";
413
+ line.isConfigured = true;
414
+ line.validationErrors = null;
415
+ }
416
+ }]);
417
+ } else {
418
+ await removeQuoteLineTree(em, cpqConfig.id, lineConfig, scope);
419
+ }
420
+ return this.recalculateInternal(em, cpqConfig, salesQuote, scope, true);
421
+ }
422
+ // ─── Restore a removed subscription item (Amend / Renew) ───────
423
+ async restoreQuoteItem(quoteId, lineId, scope) {
424
+ return this.writeQuote(quoteId, scope, (em, quote) => this.restoreQuoteItemInTransaction(lineId, scope, em, quote), void 0, void 0, LINE_EVENTS);
425
+ }
426
+ async restoreQuoteItemInTransaction(lineId, scope, em, cpqConfig) {
427
+ const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
428
+ if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
429
+ throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`);
430
+ }
431
+ const lines = await loadLines(em, cpqConfig.id, scope);
432
+ const lineConfig = lines.find((line) => line.quoteLineId === lineId);
433
+ if (!lineConfig) throw new QuotingError(404, "Quote line not found");
434
+ if (!isArcCancelledLine(lineConfig, cpqConfig.quoteType) || !isArcMirrorLine(lineConfig.configuration, cpqConfig.quoteType)) {
435
+ throw new QuotingError(409, "Only subscription items marked for removal can be restored");
436
+ }
437
+ const plan = planArcLineRestore(lineConfig, lines, cpqConfig.quoteType);
438
+ if (!plan.ok) throw new QuotingError(409, "Restore the parent item first");
439
+ const itemIdOf = (line) => line.sourceSubscriptionItemId ?? line.configuration?._arcMirroredFromItemId ?? null;
440
+ const itemIds = plan.restore.map(itemIdOf).filter((id) => !!id);
441
+ const items = itemIds.length ? await em.find(CpqInventorySubscriptionItem, { id: { $in: itemIds }, organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null }) : [];
442
+ const itemById = new Map(items.map((item) => [item.id, item]));
443
+ const salesLines = await em.find(SalesQuoteLine, {
444
+ id: { $in: plan.restore.map((line) => line.quoteLineId) },
351
445
  organizationId: scope.organizationId,
352
446
  tenantId: scope.tenantId,
353
447
  deletedAt: null
354
448
  });
355
- for (const child of children) {
356
- child.deletedAt = now;
357
- const childSalesLine = await em.findOne(SalesQuoteLine, {
358
- id: child.quoteLineId,
359
- organizationId: scope.organizationId,
360
- tenantId: scope.tenantId
361
- });
362
- if (childSalesLine) childSalesLine.deletedAt = now;
363
- }
364
- const salesLine = await em.findOne(SalesQuoteLine, {
365
- id: lineId,
366
- organizationId: scope.organizationId,
367
- tenantId: scope.tenantId
449
+ const salesLineById = new Map(salesLines.map((line) => [line.id, line]));
450
+ const restores = plan.restore.map((line) => {
451
+ const itemId = itemIdOf(line);
452
+ const item = itemId ? itemById.get(itemId) : void 0;
453
+ if (!item || ["terminated", "expired", "superseded"].includes(item.status)) {
454
+ throw new QuotingError(409, "The subscription item is no longer active and cannot be restored");
455
+ }
456
+ return { line, item };
368
457
  });
369
- if (salesLine) salesLine.deletedAt = now;
370
- await em.flush();
458
+ const arcType = cpqConfig.quoteType;
459
+ await withAtomicFlush(em, [() => {
460
+ for (const { line, item } of restores) {
461
+ line.action = "modify";
462
+ line.configuration = buildArcMirrorConfiguration(item.configuration, item, arcType);
463
+ line.charges = pickArcMirrorCharges(item, cpqConfig.currencyCode);
464
+ line.quantity = item.quantity;
465
+ line.nrcTotal = String(item.nrcAmount ?? 0);
466
+ line.mrcTotal = String(item.mrcAmount);
467
+ line.isConfigured = true;
468
+ line.validationErrors = null;
469
+ const salesLine = salesLineById.get(line.quoteLineId);
470
+ if (salesLine) salesLine.quantity = String(item.quantity);
471
+ }
472
+ }]);
371
473
  return this.recalculateInternal(em, cpqConfig, salesQuote, scope, true);
372
474
  }
373
475
  // ─── Get Quote (read from DB, no recalculation) ────────────────
@@ -385,15 +487,42 @@ class DefaultCpqQuotingService {
385
487
  }
386
488
  // ─── Recalculate ───────────────────────────────────────────────
387
489
  async recalculate(quoteId, options, scope) {
388
- const em = options.save ? this.em.fork() : this.em;
389
- const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
490
+ if (options.save) {
491
+ return this.writeQuote(
492
+ quoteId,
493
+ scope,
494
+ (em, quote) => this.recalculateFromConfig(em, quote, scope, true),
495
+ void 0,
496
+ (quote) => {
497
+ if (TERMINAL_STATUSES.includes(quote.cpqStatus)) throw new QuotingError(409, `Cannot recalculate quote in '${quote.cpqStatus}' status`);
498
+ }
499
+ );
500
+ }
501
+ const cpqConfig = await this.loadCpqConfig(this.em, quoteId, scope);
502
+ return this.recalculateFromConfig(this.em, { ...cpqConfig }, scope, false);
503
+ }
504
+ async recalculateFromConfig(em, cpqConfig, scope, save) {
505
+ if (save && TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
506
+ throw new QuotingError(409, `Cannot recalculate quote in '${cpqConfig.cpqStatus}' status`);
507
+ }
390
508
  const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
391
- return this.recalculateInternal(em, cpqConfig, salesQuote, scope, options.save);
509
+ return this.recalculateInternal(em, cpqConfig, salesQuote, scope, save);
392
510
  }
393
511
  // ─── Clone Quote ───────────────────────────────────────────────
394
512
  async cloneQuote(quoteId, scope) {
395
- const em = this.em;
396
- const original = await this.loadCpqConfig(em, quoteId, scope);
513
+ const created = {};
514
+ const result = await this.writeQuote(
515
+ quoteId,
516
+ scope,
517
+ (em, quote) => this.cloneQuoteInTransaction(quoteId, scope, em, quote, created),
518
+ void 0,
519
+ void 0,
520
+ { pricingCause: "clone" }
521
+ );
522
+ if (created.quote) await emitQuoteWriteEvents(null, created.quote, { pricingCause: "clone" });
523
+ return result;
524
+ }
525
+ async cloneQuoteInTransaction(quoteId, scope, em, original, created = {}) {
397
526
  const originalQuote = await this.loadSalesQuote(em, original.quoteId, scope);
398
527
  const { number: quoteNumber } = await this.numberGenerator.generate({
399
528
  kind: "quote",
@@ -421,11 +550,13 @@ class DefaultCpqQuotingService {
421
550
  });
422
551
  em.persist(newSalesQuote);
423
552
  const newCpqConfig = em.create(CpqQuoteConfiguration, {
553
+ id: crypto.randomUUID(),
424
554
  organizationId: scope.organizationId,
425
555
  tenantId: scope.tenantId,
426
556
  quoteId: newSalesQuoteId,
427
557
  customerId: original.customerId,
428
558
  cpqStatus: "new",
559
+ pricingRevision: 1,
429
560
  version: original.version + 1,
430
561
  parentQuoteId: original.id,
431
562
  currencyCode: original.currencyCode,
@@ -434,6 +565,8 @@ class DefaultCpqQuotingService {
434
565
  pricingSummary: emptyPricingSummary(original.currencyCode)
435
566
  });
436
567
  em.persist(newCpqConfig);
568
+ created.quote = newCpqConfig;
569
+ await em.flush();
437
570
  const originalLines = await em.find(CpqQuoteLineConfiguration, {
438
571
  quoteConfigurationId: original.id,
439
572
  organizationId: scope.organizationId,
@@ -488,7 +621,8 @@ class DefaultCpqQuotingService {
488
621
  startDate: origLine.startDate,
489
622
  termMonths: origLine.termMonths,
490
623
  endDate: origLine.endDate,
491
- configuration: { ...origLine.configuration },
624
+ // A clone is a new quote: quote-only ARC pricing markers don't apply.
625
+ configuration: { ...stripQuoteOnlyArcMarkers(origLine.configuration) },
492
626
  quantity: origLine.quantity,
493
627
  charges: origLine.charges ? [...origLine.charges] : null,
494
628
  nrcTotal: origLine.nrcTotal,
@@ -515,12 +649,22 @@ class DefaultCpqQuotingService {
515
649
  }
516
650
  }
517
651
  await em.flush();
652
+ await cloneQuoteAdjustments(em, original.id, newCpqConfig, lineIdMap, scope);
653
+ await em.flush();
518
654
  return this.recalculateInternal(em, newCpqConfig, newSalesQuote, scope, true);
519
655
  }
520
656
  // ─── Transition Status ─────────────────────────────────────────
521
- async transitionStatus(quoteId, targetStatus, scope) {
522
- const em = this.em;
523
- const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
657
+ async transitionStatus(quoteId, targetStatus, scope, actorUserId = null) {
658
+ return this.writeQuote(
659
+ quoteId,
660
+ scope,
661
+ (em, quote) => this.transitionStatusInTransaction(quoteId, targetStatus, scope, em, quote),
662
+ void 0,
663
+ void 0,
664
+ { pricingCause: "recalculate", statusCause: "manual", actorUserId }
665
+ );
666
+ }
667
+ async transitionStatusInTransaction(quoteId, targetStatus, scope, em, cpqConfig) {
524
668
  const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
525
669
  const currentStatus = cpqConfig.cpqStatus;
526
670
  const allowed = ALLOWED_TRANSITIONS[currentStatus] ?? [];
@@ -530,7 +674,7 @@ class DefaultCpqQuotingService {
530
674
  const isSubmitForApproval = targetStatus === "in_approval" || targetStatus === "pre_approved" || targetStatus === "with_customer";
531
675
  const quoteType = cpqConfig.quoteType ?? "new";
532
676
  if (isSubmitForApproval && quoteType !== "new") {
533
- const validation = await this.validateArcQuote(quoteId, scope);
677
+ const validation = await this.validateArcQuote(quoteId, scope, em);
534
678
  if (!validation.ok) {
535
679
  throw new QuotingError(
536
680
  422,
@@ -543,8 +687,26 @@ class DefaultCpqQuotingService {
543
687
  const lines = await this.loadLineConfigs(em, cpqConfig.id, scope);
544
688
  return this.buildQuoteResult(em, cpqConfig, salesQuote, lines);
545
689
  }
690
+ async writeQuote(quoteId, scope, execute, expectedRevision, validate, events = { pricingCause: "recalculate" }) {
691
+ return withQuotePricingWrite(
692
+ this.em,
693
+ quoteId,
694
+ scope,
695
+ execute,
696
+ (status, message) => new QuotingError(status, message),
697
+ expectedRevision,
698
+ validate,
699
+ events
700
+ );
701
+ }
702
+ /** Called by adjustment commands that already own the quote lock/revision. */
703
+ async recalculateLockedQuote(em, quote, scope, save = true, adjustments) {
704
+ const salesQuote = await this.loadSalesQuote(em, quote.quoteId, scope);
705
+ const config = save ? quote : { ...quote };
706
+ return this.recalculateInternal(em, config, salesQuote, scope, save, adjustments);
707
+ }
546
708
  // ─── Internal: Recalculate ─────────────────────────────────────
547
- async recalculateInternal(em, cpqConfig, salesQuote, scope, save) {
709
+ async recalculateInternal(em, cpqConfig, salesQuote, scope, save, draftAdjustments) {
548
710
  const lineConfigs = await em.find(CpqQuoteLineConfiguration, {
549
711
  quoteConfigurationId: cpqConfig.id,
550
712
  organizationId: scope.organizationId,
@@ -573,7 +735,7 @@ class DefaultCpqQuotingService {
573
735
  mergedConfig.term_months = lineConfig.termMonths;
574
736
  }
575
737
  let validationErrors = [];
576
- if (lineConfig.offeringId) {
738
+ if (lineConfig.offeringId && !isArcCancelledLine(lineConfig, cpqConfig.quoteType)) {
577
739
  const validation = await this.validationService.validateOfferingConfiguration({
578
740
  offeringId: lineConfig.offeringId,
579
741
  configuration: lineConfig.configuration,
@@ -583,8 +745,11 @@ class DefaultCpqQuotingService {
583
745
  }
584
746
  const cfgRecord = lineConfig.configuration;
585
747
  const isArcMirrored = !!cfgRecord?._arcMirroredFromItemId;
748
+ const isFrozen = isFrozenAmendMirrorLine(cfgRecord, cpqConfig.quoteType);
586
749
  let charges;
587
- if (lineConfig.offeringId || lineConfig.productId) {
750
+ if (isArcCancelledLine(lineConfig, cpqConfig.quoteType)) {
751
+ charges = [];
752
+ } else if ((lineConfig.offeringId || lineConfig.productId) && !isFrozen) {
588
753
  charges = await this.pricingService.resolveProductCharges({
589
754
  offeringId: lineConfig.offeringId ?? void 0,
590
755
  productId: lineConfig.productId ?? void 0,
@@ -605,36 +770,74 @@ class DefaultCpqQuotingService {
605
770
  if (!isConfigured) allValid = false;
606
771
  allErrors.push(...validationErrors);
607
772
  computedLines.push({ lineConfig, charges, nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated, validationErrors, isConfigured });
773
+ }
774
+ const adjustments = draftAdjustments ?? await loadQuoteAdjustments(em, cpqConfig.id, scope);
775
+ const adjusted = resolveManualAdjustments(computedLines.map(({ lineConfig, charges }) => ({
776
+ lineId: lineConfig.quoteLineId,
777
+ parentLineId: lineConfig.parentLineId ?? null,
778
+ enginePriced: !!(lineConfig.offeringId || lineConfig.productId),
779
+ mirrored: isArcMirrorLine(lineConfig.configuration, cpqConfig.quoteType),
780
+ charges
781
+ })), adjustments, cpqConfig.currencyCode);
782
+ const adjustedById = new Map(adjusted.lines.map((line) => [line.lineId, line]));
783
+ const computedIds = new Set(computedLines.map((line) => line.lineConfig.quoteLineId));
784
+ for (const computed of computedLines) {
785
+ const { lineConfig } = computed;
786
+ if (adjustments.length) computed.charges = adjustedById.get(lineConfig.quoteLineId).charges;
787
+ const adjustmentErrors = adjusted.errors.filter((error) => error.quoteLineId === lineConfig.quoteLineId);
788
+ if (adjustmentErrors.length) {
789
+ computed.isConfigured = false;
790
+ allValid = false;
791
+ const errors = adjustmentErrors.map((error) => ({ message: error.code, attribute: error.chargeCode, lineId: error.quoteLineId, adjustmentId: error.adjustmentId }));
792
+ computed.validationErrors.push(...errors);
793
+ allErrors.push(...errors);
794
+ }
795
+ Object.assign(computed, computeLineTotals(
796
+ computed.charges,
797
+ lineConfig.quantity,
798
+ lineConfig.usageEstimates?.map((entry) => ({ chargeCode: entry.chargeCode, estimatedQuantity: entry.estimatedQuantity }))
799
+ ));
800
+ const { charges, nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated, validationErrors, isConfigured } = computed;
608
801
  if (save) {
609
- em.assign(lineConfig, {
610
- charges,
611
- nrcTotal: String(nrcTotal),
612
- mrcTotal: String(mrcTotal),
613
- usageEstimates: usageEstimates ?? null,
614
- usageTotalEstimated: usageTotalEstimated != null ? String(usageTotalEstimated) : null,
615
- validationErrors: validationErrors.length > 0 ? validationErrors : null,
616
- isConfigured
617
- });
618
- const salesLine = await em.findOne(SalesQuoteLine, {
619
- id: lineConfig.quoteLineId,
620
- organizationId: scope.organizationId,
621
- tenantId: scope.tenantId
622
- });
623
- if (salesLine) {
624
- const perInstanceNrc = charges.filter((c) => c.chargeType === "nrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
625
- const perInstanceMrc = charges.filter((c) => c.chargeType === "mrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
626
- const unitPriceNet = perInstanceNrc + perInstanceMrc;
627
- em.assign(salesLine, {
628
- quantity: String(lineConfig.quantity),
629
- unitPriceNet: String(unitPriceNet),
630
- unitPriceGross: String(unitPriceNet),
631
- totalNetAmount: String(unitPriceNet * lineConfig.quantity),
632
- totalGrossAmount: String(unitPriceNet * lineConfig.quantity),
633
- updatedAt: /* @__PURE__ */ new Date()
634
- });
635
- }
802
+ await withAtomicFlush(em, [
803
+ () => {
804
+ em.assign(lineConfig, {
805
+ charges,
806
+ nrcTotal: String(nrcTotal),
807
+ mrcTotal: String(mrcTotal),
808
+ usageEstimates: usageEstimates ?? null,
809
+ usageTotalEstimated: usageTotalEstimated != null ? String(usageTotalEstimated) : null,
810
+ validationErrors: validationErrors.length > 0 ? validationErrors : null,
811
+ isConfigured
812
+ });
813
+ },
814
+ async () => {
815
+ const salesLine = await em.findOne(SalesQuoteLine, {
816
+ id: lineConfig.quoteLineId,
817
+ organizationId: scope.organizationId,
818
+ tenantId: scope.tenantId
819
+ });
820
+ if (salesLine) {
821
+ const perInstanceNrc = charges.filter((charge) => charge.chargeType === "nrc").reduce((sum, charge) => sum + (charge.totalPrice ?? 0), 0);
822
+ const perInstanceMrc = charges.filter((charge) => charge.chargeType === "mrc").reduce((sum, charge) => sum + (charge.totalPrice ?? 0), 0);
823
+ const unitPriceNet = perInstanceNrc + perInstanceMrc;
824
+ em.assign(salesLine, {
825
+ quantity: String(lineConfig.quantity),
826
+ unitPriceNet: String(unitPriceNet),
827
+ unitPriceGross: String(unitPriceNet),
828
+ totalNetAmount: String(unitPriceNet * lineConfig.quantity),
829
+ totalGrossAmount: String(unitPriceNet * lineConfig.quantity),
830
+ updatedAt: /* @__PURE__ */ new Date()
831
+ });
832
+ }
833
+ }
834
+ ]);
636
835
  }
637
836
  }
837
+ for (const error of adjusted.errors.filter((entry) => !computedIds.has(entry.quoteLineId))) {
838
+ allValid = false;
839
+ allErrors.push({ message: error.code, attribute: error.chargeCode, lineId: error.quoteLineId, adjustmentId: error.adjustmentId });
840
+ }
638
841
  const relationshipErrors = await this.validationService.validateRelationships({
639
842
  lines: lineConfigs.map((l) => ({
640
843
  lineId: l.quoteLineId,
@@ -658,6 +861,13 @@ class DefaultCpqQuotingService {
658
861
  cpqConfig.cpqStatus = cpqStatus;
659
862
  }
660
863
  const pricingSummary = computePricingSummaryFromComputed(computedLines, cpqConfig.currencyCode);
864
+ Object.assign(pricingSummary, computeWaterfallSummary(computedLines.map(({ lineConfig, charges }) => ({
865
+ lineId: lineConfig.quoteLineId,
866
+ quantity: lineConfig.quantity,
867
+ termMonths: lineConfig.termMonths,
868
+ includeInDeal: !isArcMirrorLine(lineConfig.configuration, cpqConfig.quoteType),
869
+ charges
870
+ })), adjustments, cpqConfig.currencyCode));
661
871
  const validationResult = { valid: allValid, errors: allErrors };
662
872
  if (save) {
663
873
  cpqConfig.validationResult = validationResult;
@@ -673,7 +883,9 @@ class DefaultCpqQuotingService {
673
883
  });
674
884
  await em.flush();
675
885
  }
676
- return this.buildQuoteResultFromComputed(em, cpqConfig, salesQuote, computedLines, validationResult, pricingSummary);
886
+ const result = await this.buildQuoteResultFromComputed(em, cpqConfig, salesQuote, computedLines, validationResult, pricingSummary);
887
+ result.adjustmentErrors = adjusted.errors;
888
+ return result;
677
889
  }
678
890
  // ─── Helpers ───────────────────────────────────────────────────
679
891
  async loadCpqConfig(em, quoteId, scope) {
@@ -763,7 +975,7 @@ class DefaultCpqQuotingService {
763
975
  mrcTotal: Number(lc.mrcTotal),
764
976
  usageEstimates: lc.usageEstimates ?? null,
765
977
  usageTotalEstimated: lc.usageTotalEstimated != null ? Number(lc.usageTotalEstimated) : null,
766
- charges: lc.charges ?? [],
978
+ charges: (lc.charges ?? []).map(withChargeWaterfall),
767
979
  isConfigured: lc.isConfigured,
768
980
  validationErrors: lc.validationErrors ?? null,
769
981
  arcSource: pickArcSource(lc, arcSourceMap),
@@ -772,6 +984,16 @@ class DefaultCpqQuotingService {
772
984
  }
773
985
  const finalValidation = validationResult ?? cpqConfig.validationResult ?? { valid: true, errors: [] };
774
986
  const finalPricing = pricingSummary ?? computePricingSummary(lineConfigs, cpqConfig.currencyCode);
987
+ if (!pricingSummary) {
988
+ const adjustments = await loadQuoteAdjustments(em, cpqConfig.id, { organizationId: cpqConfig.organizationId, tenantId: cpqConfig.tenantId });
989
+ Object.assign(finalPricing, computeWaterfallSummary(lines.map((line) => ({
990
+ lineId: line.lineId,
991
+ quantity: line.quantity,
992
+ termMonths: line.termMonths,
993
+ includeInDeal: !isArcMirrorLine(line.configuration, cpqConfig.quoteType),
994
+ charges: line.charges
995
+ })), adjustments, cpqConfig.currencyCode));
996
+ }
775
997
  const { customerName, convertedOrderId } = await resolveQuoteSideData(em, cpqConfig);
776
998
  return {
777
999
  id: cpqConfig.id,
@@ -781,6 +1003,7 @@ class DefaultCpqQuotingService {
781
1003
  customerName,
782
1004
  cpqStatus: cpqConfig.cpqStatus,
783
1005
  version: cpqConfig.version,
1006
+ pricingRevision: cpqConfig.pricingRevision ?? 0,
784
1007
  parentQuoteId: cpqConfig.parentQuoteId ?? null,
785
1008
  currencyCode: cpqConfig.currencyCode,
786
1009
  convertedOrderId,
@@ -835,7 +1058,7 @@ class DefaultCpqQuotingService {
835
1058
  mrcTotal,
836
1059
  usageEstimates,
837
1060
  usageTotalEstimated,
838
- charges,
1061
+ charges: charges.map(withChargeWaterfall),
839
1062
  isConfigured,
840
1063
  validationErrors: validationErrors.length > 0 ? validationErrors : null,
841
1064
  arcSource: pickArcSource(lc, arcSourceMap),
@@ -851,6 +1074,7 @@ class DefaultCpqQuotingService {
851
1074
  customerName,
852
1075
  cpqStatus: cpqConfig.cpqStatus,
853
1076
  version: cpqConfig.version,
1077
+ pricingRevision: cpqConfig.pricingRevision ?? 0,
854
1078
  parentQuoteId: cpqConfig.parentQuoteId ?? null,
855
1079
  currencyCode: cpqConfig.currencyCode,
856
1080
  convertedOrderId,
@@ -940,6 +1164,7 @@ function aggregateAdjustmentTotals(allCharges) {
940
1164
  for (const charge of allCharges) {
941
1165
  if (!charge.adjustments) continue;
942
1166
  for (const adj of charge.adjustments) {
1167
+ if (adj.origin === "manual") continue;
943
1168
  if (adj.delta < 0) {
944
1169
  discountTotal += Math.abs(adj.delta) * (charge.quantity ?? 1);
945
1170
  } else if (adj.delta > 0) {
@@ -1020,8 +1245,12 @@ function computePricingSummaryFromComputed(computedLines, currencyCode) {
1020
1245
  currencyCode
1021
1246
  };
1022
1247
  }
1248
+ const LINE_EVENTS = { pricingCause: "line" };
1023
1249
  function emptyPricingSummary(currencyCode) {
1024
1250
  return {
1251
+ // A new quote starts with a real baseline hash, so its first priced line is
1252
+ // a pricing change; only pre-feature quotes have no stored hash.
1253
+ snapshotHash: computeWaterfallSummary([], [], currencyCode).snapshotHash,
1025
1254
  nrcTotal: 0,
1026
1255
  mrcTotal: 0,
1027
1256
  usageCharges: [],
@@ -1031,15 +1260,20 @@ function emptyPricingSummary(currencyCode) {
1031
1260
  currencyCode
1032
1261
  };
1033
1262
  }
1034
- class QuotingError extends Error {
1263
+ const QUOTING_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@dainamite/cpq/QuotingError");
1264
+ class QuotingError extends (_b = Error, _a = QUOTING_ERROR_BRAND, _b) {
1035
1265
  constructor(status, message, cpqStatus, details) {
1036
1266
  super(message);
1037
1267
  this.status = status;
1038
1268
  this.cpqStatus = cpqStatus;
1039
1269
  this.details = details;
1270
+ this[_a] = true;
1040
1271
  this.name = "QuotingError";
1041
1272
  }
1042
1273
  }
1274
+ function isQuotingError(err) {
1275
+ return !!err && typeof err === "object" && err[QUOTING_ERROR_BRAND] === true;
1276
+ }
1043
1277
  const EDITABLE_QUOTE_STATUSES = /* @__PURE__ */ new Set([
1044
1278
  "new",
1045
1279
  "incomplete",
@@ -1333,43 +1567,39 @@ DefaultCpqQuotingService.prototype.attachTargetSubscription = async function(quo
1333
1567
  };
1334
1568
  DefaultCpqQuotingService.prototype.detachTargetSubscription = async function(quoteId, targetId, scope) {
1335
1569
  const em = this.em;
1336
- const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1337
- ensureEditable(cpqConfig);
1338
- const target = await em.findOne(CpqQuoteTargetSubscription, {
1339
- id: targetId,
1340
- quoteId: cpqConfig.id,
1341
- organizationId: scope.organizationId,
1342
- tenantId: scope.tenantId,
1343
- deletedAt: null
1344
- });
1345
- if (!target) throw new QuotingError(404, "Target subscription not found");
1346
- const lines = await em.find(CpqQuoteLineConfiguration, {
1347
- quoteConfigurationId: cpqConfig.id,
1348
- targetSubscriptionId: target.subscriptionId,
1349
- organizationId: scope.organizationId,
1350
- tenantId: scope.tenantId,
1351
- deletedAt: null
1352
- });
1353
- const now = /* @__PURE__ */ new Date();
1354
- for (const line of lines) {
1355
- line.deletedAt = now;
1356
- const salesLine = await em.findOne(SalesQuoteLine, { id: line.quoteLineId });
1357
- if (salesLine) salesLine.deletedAt = now;
1358
- }
1359
- target.deletedAt = now;
1360
- if (target.quoteType === "renew") {
1361
- const remaining = await em.find(CpqQuoteTargetSubscription, {
1570
+ await withQuotePricingWrite(em, quoteId, scope, async (tx, cpqConfig) => {
1571
+ const target = await tx.findOne(CpqQuoteTargetSubscription, {
1572
+ id: targetId,
1362
1573
  quoteId: cpqConfig.id,
1363
1574
  organizationId: scope.organizationId,
1364
1575
  tenantId: scope.tenantId,
1365
- deletedAt: null,
1366
- id: { $ne: target.id }
1576
+ deletedAt: null
1367
1577
  });
1368
- if (remaining.length === 1) {
1369
- remaining[0].mergeAction = "standalone";
1578
+ if (!target) throw new QuotingError(404, "Target subscription not found");
1579
+ const lines = await tx.find(CpqQuoteLineConfiguration, {
1580
+ quoteConfigurationId: cpqConfig.id,
1581
+ targetSubscriptionId: target.subscriptionId,
1582
+ organizationId: scope.organizationId,
1583
+ tenantId: scope.tenantId,
1584
+ deletedAt: null
1585
+ });
1586
+ for (const line of lines) {
1587
+ if (line.deletedAt) continue;
1588
+ await removeQuoteLineTree(tx, cpqConfig.id, line, scope);
1370
1589
  }
1371
- }
1372
- await em.flush();
1590
+ const remaining = target.quoteType === "renew" ? await tx.find(CpqQuoteTargetSubscription, {
1591
+ quoteId: cpqConfig.id,
1592
+ organizationId: scope.organizationId,
1593
+ tenantId: scope.tenantId,
1594
+ deletedAt: null,
1595
+ id: { $ne: target.id }
1596
+ }) : [];
1597
+ await withAtomicFlush(tx, [() => {
1598
+ target.deletedAt = /* @__PURE__ */ new Date();
1599
+ if (remaining.length === 1) remaining[0].mergeAction = "standalone";
1600
+ }]);
1601
+ await this.recalculateLockedQuote(tx, cpqConfig, scope, true);
1602
+ }, (status, message) => new QuotingError(status, message), void 0, ensureEditable, { pricingCause: "line" });
1373
1603
  return { ok: true };
1374
1604
  };
1375
1605
  DefaultCpqQuotingService.prototype.updateTargetSubscription = async function(quoteId, targetId, patch, scope) {
@@ -1436,8 +1666,8 @@ DefaultCpqQuotingService.prototype.setMergeMeta = async function(quoteId, meta,
1436
1666
  const lines = await loadLines(em, cpqConfig.id, scope);
1437
1667
  return this.buildQuoteResult(em, cpqConfig, salesQuote, lines);
1438
1668
  };
1439
- DefaultCpqQuotingService.prototype.validateArcQuote = async function(quoteId, scope) {
1440
- const em = this.em;
1669
+ DefaultCpqQuotingService.prototype.validateArcQuote = async function(quoteId, scope, transactionEm) {
1670
+ const em = transactionEm ?? this.em;
1441
1671
  const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1442
1672
  const errors = [];
1443
1673
  if (cpqConfig.quoteType === "new") {
@@ -1663,6 +1893,7 @@ async function mapTargetView(em, target, scope) {
1663
1893
  }
1664
1894
  function emptyArcPricingSummary(currencyCode) {
1665
1895
  return {
1896
+ snapshotHash: computeWaterfallSummary([], [], currencyCode).snapshotHash,
1666
1897
  nrcTotal: 0,
1667
1898
  mrcTotal: 0,
1668
1899
  usageCharges: [],
@@ -1777,11 +2008,7 @@ async function mirrorTargetSubscriptionItems(em, cpqConfig, sub, salesQuote, quo
1777
2008
  startDate: null,
1778
2009
  termMonths: null,
1779
2010
  endDate: null,
1780
- configuration: {
1781
- ...item.configuration,
1782
- _arcMirroredFromItemId: item.id,
1783
- _arcMirroredName: item.name
1784
- },
2011
+ configuration: buildArcMirrorConfiguration(item.configuration, item, quoteType),
1785
2012
  quantity: item.quantity,
1786
2013
  charges: pickArcMirrorCharges(item, sub.currencyCode),
1787
2014
  nrcTotal: String(item.nrcAmount ?? 0),
@@ -1862,6 +2089,7 @@ function pickArcMirrorCharges(item, currencyCode) {
1862
2089
  }
1863
2090
  export {
1864
2091
  DefaultCpqQuotingService,
1865
- QuotingError
2092
+ QuotingError,
2093
+ isQuotingError
1866
2094
  };
1867
2095
  //# sourceMappingURL=cpqQuotingService.js.map