@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
@@ -0,0 +1,38 @@
1
+ import * as React from 'react'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
4
+ import { DepthBadge, PriceWaterfall } from '../PriceWaterfall'
5
+
6
+ const withI18n = (node: React.ReactNode) => <I18nProvider locale="en" dict={{}}>{node}</I18nProvider>
7
+
8
+ const text = (html: string) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
9
+
10
+ it('renders list, rule, standard, negotiated and offer steps with the reason label', () => {
11
+ const html = renderToStaticMarkup(withI18n(
12
+ <PriceWaterfall
13
+ currency="USD"
14
+ reasonLabels={{ competitive_match: 'Competitive match' }}
15
+ charge={{
16
+ chargeType: 'mrc', unitPrice: 80, totalPrice: 80, listUnitPrice: 100, standardUnitPrice: 90,
17
+ adjustments: [
18
+ { origin: 'rule', ruleCode: 'VOL', ruleName: 'Volume', ruleType: 'discount_percent', value: 10, unitPriceBefore: 100, unitPriceAfter: 90, delta: -10 },
19
+ { origin: 'manual', adjustmentId: 'a1', ruleCode: 'a1', ruleName: 'competitive_match', ruleType: 'price_override', value: 80, unitPriceBefore: 90, unitPriceAfter: 80, delta: -10, reasonCode: 'competitive_match' },
20
+ ],
21
+ }}
22
+ renderManualActions={(id) => <button>edit {id}</button>}
23
+ />,
24
+ ))
25
+ const content = text(html)
26
+ expect(content).toMatch(/List price \$100\.00/)
27
+ expect(content).toMatch(/Price rule: Volume 10%/)
28
+ expect(content).toMatch(/Standard price \$90\.00/)
29
+ expect(content).toMatch(/Negotiated: Competitive match \$80\.00/)
30
+ expect(content).toMatch(/Offer price \$80\.00/)
31
+ expect(content).toContain('edit a1')
32
+ expect(content.match(/edit/g)).toHaveLength(1)
33
+ })
34
+
35
+ it('renders a signed depth badge only when prices differ', () => {
36
+ expect(text(renderToStaticMarkup(<DepthBadge before={100} after={80} />))).toBe('−20%')
37
+ expect(renderToStaticMarkup(<DepthBadge before={100} after={100} />)).toBe('')
38
+ })
@@ -11,10 +11,14 @@ export type UseCpqRowActionsOptions<T extends { id: string }> = {
11
11
  /** DELETE endpoint that accepts `{ id }` body (e.g. `/api/cpq/product-offerings`). */
12
12
  endpoint: string
13
13
  /**
14
- * Singular noun for the entity, used in confirm + flash messages.
15
- * E.g. `'offering'`, `'pricing table'`, `'price rule'`.
14
+ * Singular noun for the entity (e.g. `'offering'`).
15
+ *
16
+ * @deprecated Not used in messages any more: splicing a noun into a
17
+ * sentence cannot produce correct grammar in ES / DE / PL (gender, case).
18
+ * Confirm and flash messages are noun-free and count-based
19
+ * (spec 2026-11-15-cpq-multi-language). Kept for API compatibility.
16
20
  */
17
- entityName: string
21
+ entityName?: string
18
22
  /**
19
23
  * Build the href for the row's edit/detail page (e.g. `(row) =>
20
24
  * `/backend/cpq/offerings/${row.id}`).
@@ -59,7 +63,7 @@ export type CpqRowActionsApi<T extends { id: string }> = {
59
63
  export function useCpqRowActions<T extends { id: string }>(
60
64
  options: UseCpqRowActionsOptions<T>,
61
65
  ): CpqRowActionsApi<T> {
62
- const { endpoint, entityName, editHref, onReload, canDelete } = options
66
+ const { endpoint, editHref, onReload, canDelete } = options
63
67
  const t = useT()
64
68
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
65
69
 
@@ -79,25 +83,22 @@ export function useCpqRowActions<T extends { id: string }>(
79
83
  async (row: T) => {
80
84
  if (canDelete && !canDelete(row)) return
81
85
  const confirmed = await confirm({
82
- title: t(
83
- 'cpq.rowActions.deleteOne.confirm',
84
- `Delete this ${entityName}?`,
85
- ),
86
+ title: t('cpq.rowActions.deleteOne.confirm', 'Delete this item?'),
86
87
  variant: 'destructive',
87
88
  })
88
89
  if (!confirmed) return
89
90
  const ok = await deleteOne(row.id)
90
91
  if (!ok) {
91
92
  flash(
92
- t('cpq.rowActions.deleteOne.error', `Failed to delete ${entityName}`),
93
+ t('cpq.rowActions.deleteOne.error', 'Failed to delete the item'),
93
94
  'error',
94
95
  )
95
96
  return
96
97
  }
97
- flash(t('cpq.rowActions.deleteOne.success', `${entityName} deleted`), 'success')
98
+ flash(t('cpq.rowActions.deleteOne.success', 'Item deleted'), 'success')
98
99
  onReload()
99
100
  },
100
- [canDelete, confirm, deleteOne, entityName, onReload, t],
101
+ [canDelete, confirm, deleteOne, onReload, t],
101
102
  )
102
103
 
103
104
  const bulkDelete = React.useCallback(
@@ -106,19 +107,13 @@ export function useCpqRowActions<T extends { id: string }>(
106
107
  const eligible = canDelete ? rows.filter(canDelete) : rows
107
108
  if (!eligible.length) {
108
109
  flash(
109
- t(
110
- 'cpq.rowActions.deleteBulk.noEligible',
111
- `None of the selected ${entityName}s can be deleted in their current state.`,
112
- ),
110
+ t('cpq.rowActions.deleteBulk.noEligible', 'None of the selected items can be deleted in their current state.'),
113
111
  'error',
114
112
  )
115
113
  return { ok: false }
116
114
  }
117
115
  const confirmed = await confirm({
118
- title: t(
119
- 'cpq.rowActions.deleteBulk.confirm',
120
- `Delete ${eligible.length} ${entityName}${eligible.length > 1 ? 's' : ''}?`,
121
- ),
116
+ title: t('cpq.rowActions.deleteBulk.confirm', 'Delete selected items ({count})?', { count: eligible.length }),
122
117
  variant: 'destructive',
123
118
  })
124
119
  if (!confirmed) return { ok: false }
@@ -131,22 +126,19 @@ export function useCpqRowActions<T extends { id: string }>(
131
126
 
132
127
  if (failed > 0) {
133
128
  flash(
134
- t(
135
- 'cpq.rowActions.deleteBulk.partialError',
136
- `Failed to delete ${failed} ${entityName}${failed > 1 ? 's' : ''}`,
137
- ),
129
+ t('cpq.rowActions.deleteBulk.partialError', 'Failed to delete {count} of the selected items', { count: failed }),
138
130
  'error',
139
131
  )
140
132
  } else {
141
133
  flash(
142
- t('cpq.rowActions.deleteBulk.success', `${entityName}s deleted`),
134
+ t('cpq.rowActions.deleteBulk.success', 'Selected items deleted ({count})', { count: eligible.length }),
143
135
  'success',
144
136
  )
145
137
  }
146
138
  onReload()
147
139
  return { ok: failed === 0, affectedCount: eligible.length - failed }
148
140
  },
149
- [canDelete, confirm, deleteOne, entityName, onReload, t],
141
+ [canDelete, confirm, deleteOne, onReload, t],
150
142
  )
151
143
 
152
144
  const buildItems = React.useCallback(
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
+
6
+ type FeatureCheckResponse = { granted: string[] }
7
+
8
+ /**
9
+ * Resolve which of `features` the current user holds, through the framework's
10
+ * `POST /api/auth/feature-check` endpoint (the same one the component-override
11
+ * provider uses). Returns an empty list until the check completes, so callers
12
+ * hide privileged actions by default.
13
+ */
14
+ export function useGrantedFeatures(features: readonly string[]): readonly string[] {
15
+ const [granted, setGranted] = React.useState<readonly string[]>([])
16
+ const key = features.join('|')
17
+
18
+ React.useEffect(() => {
19
+ if (!key) return
20
+ let cancelled = false
21
+ void (async () => {
22
+ const call = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {
23
+ method: 'POST',
24
+ headers: { 'content-type': 'application/json' },
25
+ body: JSON.stringify({ features: key.split('|') }),
26
+ })
27
+ if (!cancelled && call.ok && call.result?.granted) setGranted(call.result.granted)
28
+ })()
29
+ return () => {
30
+ cancelled = true
31
+ }
32
+ }, [key])
33
+
34
+ return granted
35
+ }
@@ -0,0 +1,11 @@
1
+ import { Migration20261110213052 } from '../../migrations/Migration20261110213052'
2
+
3
+ it('generates a rollback that removes the new table before it can be recreated', () => {
4
+ const up: string[] = [], down: string[] = []
5
+ Migration20261110213052.prototype.up.call({ addSql: (sql: string) => up.push(sql) } as never)
6
+ Migration20261110213052.prototype.down.call({ addSql: (sql: string) => down.push(sql) } as never)
7
+ expect(up.join('\n')).toContain('create table "cpq_quote_price_adjustments"')
8
+ expect(down.join('\n')).toContain('drop table if exists "cpq_quote_price_adjustments"')
9
+ expect(up.join('\n')).toContain('add "pricing_revision"')
10
+ expect(down.join('\n')).toContain('drop column "pricing_revision"')
11
+ })
@@ -0,0 +1,26 @@
1
+ import { cpqPriceAdjustmentInputSchema, cpqPriceAdjustmentPatchSchema } from '../price-adjustments'
2
+ const valid = {
3
+ level: 'charge', quoteLineId: '11111111-1111-4111-8111-111111111111', chargeCode: 'setup',
4
+ adjustmentType: 'price_override', value: 80, currencyCode: 'USD', reasonCode: 'competitive_match',
5
+ }
6
+ it('allows a zero override and percentage discounts with no currency', () => {
7
+ expect(cpqPriceAdjustmentInputSchema.parse({ ...valid, value: 0 }).value).toBe(0)
8
+ expect(cpqPriceAdjustmentInputSchema.parse({ ...valid, adjustmentType: 'discount_percent', value: 100, currencyCode: undefined }).value).toBe(100)
9
+ })
10
+ it.each([
11
+ { level: 'quote' }, { quoteLineId: 'bad' }, { chargeCode: null }, { value: -1 }, { value: Infinity }, { value: 1.23456 }, { value: 100_000_000_000_000 },
12
+ { adjustmentType: 'discount_percent', value: 101 }, { adjustmentType: 'discount_percent', value: 0 },
13
+ { adjustmentType: 'discount_absolute', value: 0 }, { currencyCode: null }, { currencyCode: 'usd' },
14
+ { reasonCode: 'other', reasonNote: '' }, { reasonNote: 'x'.repeat(501) },
15
+ { fromMonth: 2 }, { toMonth: 3 }, { windowKind: 'absolute' },
16
+ { level: 'line', chargeCode: undefined, adjustmentType: 'price_override' }, { chargeTypeFilter: 'mrc' },
17
+ ])('rejects malformed or unsupported adjustments: %j', (patch) => {
18
+ expect(cpqPriceAdjustmentInputSchema.safeParse({ ...valid, ...patch }).success).toBe(false)
19
+ })
20
+ it('accepts filtered line percentages and an explanation for other', () => {
21
+ expect(cpqPriceAdjustmentInputSchema.safeParse({ ...valid, level: 'line', chargeCode: undefined, chargeTypeFilter: 'mrc', adjustmentType: 'discount_percent', value: 20, reasonCode: 'other', reasonNote: 'One-time negotiation' }).success).toBe(true)
22
+ })
23
+ it('never accepts target changes through PATCH; merged patches are validated by the full schema', () => {
24
+ expect(cpqPriceAdjustmentPatchSchema.safeParse({ quoteLineId: valid.quoteLineId }).success).toBe(false)
25
+ expect(cpqPriceAdjustmentPatchSchema.parse({ reasonNote: 'New note' })).toEqual({ reasonNote: 'New note' })
26
+ })
@@ -631,6 +631,7 @@ export class CpqPriceRule {
631
631
  export class CpqQuoteConfiguration {
632
632
  [OptionalProps]?:
633
633
  | 'cpqStatus'
634
+ | 'pricingRevision'
634
635
  | 'version'
635
636
  | 'currencyCode'
636
637
  | 'quoteType'
@@ -653,6 +654,9 @@ export class CpqQuoteConfiguration {
653
654
  @Property({ name: 'customer_id', type: 'uuid' })
654
655
  customerId!: string
655
656
 
657
+ @Property({ name: 'pricing_revision', type: 'integer', default: 0 })
658
+ pricingRevision: number = 0
659
+
656
660
  @Property({ name: 'cpq_status', type: 'text', default: 'new' })
657
661
  cpqStatus: string = 'new'
658
662
 
@@ -1475,3 +1479,82 @@ export class CpqSubscriptionChangeLog {
1475
1479
  @Property({ name: 'deleted_at', type: Date, nullable: true })
1476
1480
  deletedAt?: Date | null
1477
1481
  }
1482
+
1483
+
1484
+ // ─── Deal-specific adjustments, separate from catalog price rules ───
1485
+ @Entity({ tableName: 'cpq_quote_price_adjustments' })
1486
+ @Index({ name: 'cpq_qpa_quote_config_idx', properties: ['organizationId', 'tenantId', 'quoteConfigurationId'] })
1487
+ @Index({ name: 'cpq_qpa_quote_line_idx', properties: ['organizationId', 'tenantId', 'quoteLineId'] })
1488
+ @Index({ name: 'cpq_qpa_quote_idx', properties: ['organizationId', 'tenantId', 'quoteId'] })
1489
+ export class CpqQuotePriceAdjustment {
1490
+ [OptionalProps]?: 'windowKind' | 'fromMonth' | 'toMonth' | 'isActive' | 'createdAt' | 'updatedAt' | 'deletedAt'
1491
+
1492
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
1493
+ id!: string
1494
+
1495
+ @Property({ name: 'organization_id', type: 'uuid' })
1496
+ organizationId!: string
1497
+
1498
+ @Property({ name: 'tenant_id', type: 'uuid' })
1499
+ tenantId!: string
1500
+
1501
+ @Property({ name: 'quote_configuration_id', type: 'uuid' })
1502
+ quoteConfigurationId!: string
1503
+
1504
+ @Property({ name: 'quote_id', type: 'uuid' })
1505
+ quoteId!: string
1506
+
1507
+ @Property({ name: 'quote_line_id', type: 'uuid' })
1508
+ quoteLineId!: string
1509
+
1510
+ @Property({ type: 'text' })
1511
+ level!: 'charge' | 'line'
1512
+
1513
+ @Property({ name: 'charge_code', type: 'text', nullable: true })
1514
+ chargeCode: string | null = null
1515
+
1516
+ @Property({ name: 'charge_type_filter', type: 'text', nullable: true })
1517
+ chargeTypeFilter: 'nrc' | 'mrc' | null = null
1518
+
1519
+ @Property({ name: 'adjustment_type', type: 'text' })
1520
+ adjustmentType!: 'discount_percent' | 'discount_absolute' | 'price_override'
1521
+
1522
+ @Property({ type: 'numeric', columnType: 'numeric(18, 4)' })
1523
+ value!: string
1524
+
1525
+ @Property({ name: 'currency_code', type: 'text', nullable: true })
1526
+ currencyCode: string | null = null
1527
+
1528
+ @Property({ name: 'window_kind', type: 'text', default: 'relative' })
1529
+ windowKind: string = 'relative'
1530
+
1531
+ @Property({ name: 'from_month', type: 'integer', default: 1 })
1532
+ fromMonth: number = 1
1533
+
1534
+ @Property({ name: 'to_month', type: 'integer', nullable: true })
1535
+ toMonth: number | null = null
1536
+
1537
+ @Property({ name: 'reason_code', type: 'text' })
1538
+ reasonCode!: string
1539
+
1540
+ @Property({ name: 'reason_note', type: 'text', nullable: true })
1541
+ reasonNote: string | null = null
1542
+
1543
+ @Property({ name: 'created_by_user_id', type: 'uuid' })
1544
+ createdByUserId!: string
1545
+
1546
+ @Property({ name: 'updated_by_user_id', type: 'uuid', nullable: true })
1547
+ updatedByUserId: string | null = null
1548
+
1549
+ @Property({ name: 'is_active', type: 'boolean', default: true })
1550
+ isActive: boolean = true
1551
+
1552
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
1553
+ createdAt: Date = new Date()
1554
+
1555
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
1556
+ updatedAt: Date = new Date()
1557
+
1558
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
1559
+ deletedAt: Date | null = null
1560
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod'
2
+
3
+ export const adjustmentTypeSchema = z.enum(['discount_percent', 'discount_absolute', 'price_override'])
4
+ const adjustmentFields = {
5
+ adjustmentType: adjustmentTypeSchema,
6
+ // Match numeric(18,4), so saving never silently rounds the previewed input.
7
+ value: z.number().finite().min(0).max(99_999_999_999_999).multipleOf(0.0001),
8
+ currencyCode: z.string().regex(/^[A-Z]{3}$/).nullish(),
9
+ chargeTypeFilter: z.enum(['nrc', 'mrc']).nullish(),
10
+ reasonCode: z.string().trim().min(1).max(100),
11
+ reasonNote: z.string().trim().max(500).nullish(),
12
+ expectedRevision: z.number().int().min(0).optional(),
13
+ }
14
+ export const cpqPriceAdjustmentInputSchema = z.object({
15
+ ...adjustmentFields,
16
+ level: z.enum(['charge', 'line']),
17
+ quoteLineId: z.string().uuid(),
18
+ chargeCode: z.string().trim().min(1).max(255).nullish(),
19
+ windowKind: z.literal('relative').optional(),
20
+ fromMonth: z.literal(1).optional(),
21
+ toMonth: z.null().optional(),
22
+ }).strict().superRefine((input, ctx) => {
23
+ const error = (path: string, message: string) => ctx.addIssue({ code: 'custom', path: [path], message })
24
+ if (input.level === 'charge' && !input.chargeCode) error('chargeCode', 'TARGET_NOT_FOUND')
25
+ if (input.level === 'charge' && input.chargeTypeFilter) error('chargeTypeFilter', 'TYPE_NOT_ALLOWED')
26
+ if (input.level === 'line' && input.chargeCode) error('chargeCode', 'TYPE_NOT_ALLOWED')
27
+ if (input.level === 'line' && input.adjustmentType !== 'discount_percent') error('adjustmentType', 'TYPE_NOT_ALLOWED')
28
+ if (input.adjustmentType === 'discount_percent' && (input.value <= 0 || input.value > 100)) error('value', 'INVALID_PERCENT')
29
+ if (input.adjustmentType === 'discount_absolute' && input.value <= 0) error('value', 'INVALID_AMOUNT')
30
+ if (input.adjustmentType !== 'discount_percent' && !input.currencyCode) error('currencyCode', 'CURRENCY_REQUIRED')
31
+ if (input.reasonCode === 'other' && !input.reasonNote) error('reasonNote', 'REASON_NOTE_REQUIRED')
32
+ })
33
+ export const cpqPriceAdjustmentPatchSchema = z.object(adjustmentFields).partial().strict()
34
+ export type PriceAdjustmentInput = z.infer<typeof cpqPriceAdjustmentInputSchema>
35
+ export type PriceAdjustmentPatch = z.infer<typeof cpqPriceAdjustmentPatchSchema>
36
+
37
+ /** Pure pricing representation; persistence metadata stays outside the resolver. */
38
+ export const resolvedPriceAdjustmentSchema = z.object({
39
+ ...cpqPriceAdjustmentInputSchema.shape,
40
+ id: z.string().uuid(),
41
+ createdByUserId: z.string().uuid().optional(),
42
+ })
43
+ export type PriceAdjustment = z.infer<typeof resolvedPriceAdjustmentSchema>
44
+ export const adjustmentErrorSchema = z.object({
45
+ adjustmentId: z.string(),
46
+ quoteLineId: z.string(),
47
+ chargeCode: z.string().optional(),
48
+ code: z.enum(['ADJUSTMENT_TARGET_MISSING', 'TARGET_AMBIGUOUS', 'ABOVE_STANDARD', 'CURRENCY_MISMATCH', 'TYPE_NOT_ALLOWED', 'LINE_NOT_ELIGIBLE', 'ADJUSTMENT_OVERLAP']),
49
+ })
50
+ export type AdjustmentError = z.infer<typeof adjustmentErrorSchema>
51
+
52
+ export const cpqPriceAdjustmentPreviewSchema = z.object({
53
+ draft: cpqPriceAdjustmentInputSchema,
54
+ replaceAdjustmentId: z.string().uuid().optional(),
55
+ }).strict()
56
+
57
+ export const ADJUSTMENT_TOGGLE = 'cpq.manual_price_adjustments'
58
+ export const REASON_DICTIONARY = 'cpq.price_adjustment_reason'
59
+ export const DEFAULT_ADJUSTMENT_REASONS = [
60
+ { value: 'competitive_match', label: 'Competitive match' },
61
+ { value: 'volume_commitment', label: 'Volume commitment' },
62
+ { value: 'loyalty', label: 'Loyalty' },
63
+ { value: 'promotion', label: 'Promotion' },
64
+ { value: 'strategic_deal', label: 'Strategic deal' },
65
+ { value: 'price_correction', label: 'Price correction' },
66
+ { value: 'other', label: 'Other' },
67
+ ] as const
@@ -378,6 +378,7 @@ const cpqPricePreviewItemSchema = z.object({
378
378
  )
379
379
 
380
380
  export const cpqPricePreviewSchema = z.object({
381
+ includeWaterfall: z.boolean().optional().default(false),
381
382
  primaryItem: cpqPricePreviewItemSchema.nullish(),
382
383
  childItems: z.array(cpqPricePreviewItemSchema),
383
384
  currencyCode: z.string().min(1).optional(),
@@ -691,3 +692,5 @@ export const cpqConvertQuoteToOrderSchema = z.object({
691
692
  export const cpqOrderStatusTransitionSchema = z.object({
692
693
  targetStatus: z.string().min(1),
693
694
  })
695
+
696
+ export { cpqPriceAdjustmentInputSchema, cpqPriceAdjustmentPatchSchema } from './price-adjustments'
@@ -9,6 +9,7 @@ import { DefaultCpqQuotingService } from './services/cpqQuotingService'
9
9
  import { DefaultCpqWizardService } from './services/cpqWizardService'
10
10
  import { DefaultCpqInventoryService } from './services/cpqInventoryService'
11
11
  import { DefaultCpqOrderService } from './services/cpqOrderService'
12
+ import { CpqAdjustmentService } from './services/priceAdjustments/adjustmentService'
12
13
  import { DefaultCpqBundleService } from './services/cpqBundleService'
13
14
 
14
15
  /**
@@ -20,6 +21,11 @@ import { DefaultCpqBundleService } from './services/cpqBundleService'
20
21
  */
21
22
  export function register(container: AppContainer) {
22
23
  container.register({
24
+ cpqAdjustmentService: asFunction(() => new CpqAdjustmentService({
25
+ em: container.resolve('em'),
26
+ quotingService: container.resolve('cpqQuotingService'),
27
+ featureTogglesService: container.resolve('featureTogglesService'),
28
+ })).scoped(),
23
29
  cpqPricingService: asFunction(({ em }: { em: EntityManager }) => {
24
30
  return new DefaultCpqPricingService(em)
25
31
  }).scoped().proxy(),
@@ -1,4 +1,26 @@
1
1
  import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+ import type { EventPayloadSchemaField } from '@open-mercato/shared/modules/events'
3
+
4
+ /** Fields shared by both quote pricing signals (see `quotePricingEvents.ts`). */
5
+ const quotePricingSnapshotFields: EventPayloadSchemaField[] = [
6
+ { path: 'tenantId', type: 'text' },
7
+ { path: 'organizationId', type: 'text' },
8
+ { path: 'quoteId', type: 'text', label: 'CPQ quote id' },
9
+ { path: 'quoteType', type: 'select', label: 'Quote type' },
10
+ { path: 'cpqStatus', type: 'text', label: 'Status' },
11
+ { path: 'currencyCode', type: 'text' },
12
+ { path: 'pricingRevision', type: 'number' },
13
+ { path: 'snapshotHash', type: 'text' },
14
+ { path: 'dealValue.list', type: 'number', label: 'Deal value (list)' },
15
+ { path: 'dealValue.standard', type: 'number', label: 'Deal value (standard)' },
16
+ { path: 'dealValue.offer', type: 'number', label: 'Deal value (offer)' },
17
+ { path: 'discountDepth.rule', type: 'number', label: 'Rule discount depth', optional: true },
18
+ { path: 'discountDepth.manual', type: 'number', label: 'Manual discount depth', optional: true },
19
+ { path: 'discountDepth.total', type: 'number', label: 'Total discount depth', optional: true },
20
+ { path: 'discountDepth.maxLineManual', type: 'number', label: 'Max line manual discount depth', optional: true },
21
+ { path: 'hasTimeBasedAdjustments', type: 'boolean' },
22
+ { path: 'occurredAt', type: 'date' },
23
+ ]
2
24
 
3
25
  /**
4
26
  * CPQ Module Events.
@@ -8,8 +30,9 @@ import { createModuleEvents } from '@open-mercato/shared/modules/events'
8
30
  * modules (billing, notifications) subscribe to these to react to
9
31
  * subscription mutations driven by CPQ orders.
10
32
  *
11
- * Emission rule: events fire AFTER the activation transaction commits.
12
- * `cpqOrderService.activateOrder` is the sole emitter site.
33
+ * Emission rule: events fire AFTER their transaction commits.
34
+ * Subscription events: `cpqOrderService.activateOrder` is the sole emitter.
35
+ * Quote events: `withQuotePricingWrite` (plus `cloneQuote` for the copy).
13
36
  */
14
37
  const events = [
15
38
  {
@@ -54,6 +77,34 @@ const events = [
54
77
  category: 'lifecycle',
55
78
  clientBroadcast: true,
56
79
  },
80
+ // Quote pricing signals (manual price adjustments). Emitted after the quote
81
+ // write commits by `withQuotePricingWrite`; payload carries the pricing
82
+ // snapshot (deal value, discount depth, snapshot hash) so project workflows
83
+ // can route approvals without CPQ code.
84
+ {
85
+ id: 'cpq.quote.pricing_changed',
86
+ label: 'Quote Pricing Changed',
87
+ entity: 'quote',
88
+ category: 'custom',
89
+ clientBroadcast: true,
90
+ payloadSchema: { fields: [...quotePricingSnapshotFields, { path: 'cause', type: 'select' }] },
91
+ },
92
+ {
93
+ id: 'cpq.quote.status_changed',
94
+ label: 'Quote Status Changed',
95
+ entity: 'quote',
96
+ category: 'lifecycle',
97
+ clientBroadcast: true,
98
+ payloadSchema: {
99
+ fields: [
100
+ ...quotePricingSnapshotFields,
101
+ { path: 'fromStatus', type: 'text' },
102
+ { path: 'toStatus', type: 'text' },
103
+ { path: 'cause', type: 'select' },
104
+ { path: 'actorUserId', type: 'text', optional: true },
105
+ ],
106
+ },
107
+ },
57
108
  ] as const
58
109
 
59
110
  export const eventsConfig = createModuleEvents({