@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,260 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
7
+ import { Input } from '@open-mercato/ui/primitives/input'
8
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
9
+ import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
10
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'
12
+ import {
13
+ ADJUSTMENT_ERROR_FALLBACKS,
14
+ adjustmentErrorCode,
15
+ allowedAdjustmentTypes,
16
+ buildAdjustmentDraft,
17
+ formatDepth,
18
+ discountDepth,
19
+ toAdjustmentPatch,
20
+ type AdjustmentForm,
21
+ type AdjustmentTarget,
22
+ type AdjustmentType,
23
+ type WaterfallChargeInput,
24
+ } from '../../../../../lib/priceAdjustmentView'
25
+
26
+ export type AdjustmentReason = { value: string; label: string }
27
+
28
+ /** Minimal view of the quote the dialog needs, kept local so the page owns the full type. */
29
+ type PreviewQuote = {
30
+ pricingRevision?: number
31
+ lines: Array<{ lineId: string; mrcTotal: number; nrcTotal: number; charges: Array<WaterfallChargeInput & { chargeCode: string; adjustmentErrors?: Array<{ adjustmentId: string; code: string }> }> }>
32
+ }
33
+
34
+ export type ExistingAdjustment = {
35
+ id: string
36
+ adjustmentType: AdjustmentType
37
+ value: number
38
+ reasonCode: string
39
+ reasonNote?: string | null
40
+ chargeTypeFilter?: 'nrc' | 'mrc' | null
41
+ }
42
+
43
+ export type PriceAdjustmentDialogProps<Q extends PreviewQuote> = {
44
+ open: boolean
45
+ quoteId: string
46
+ currency: string
47
+ pricingRevision: number
48
+ target: AdjustmentTarget
49
+ /** Display name of the line / charge being adjusted. */
50
+ targetLabel: string
51
+ /** The charge for charge-level targets (drives type options and the current price). */
52
+ charge?: WaterfallChargeInput & { chargeCode: string; chargeName: string }
53
+ existing?: ExistingAdjustment | null
54
+ /** Ids of adjustments already stored on the quote; their errors don't block this draft. */
55
+ storedAdjustmentIds: string[]
56
+ reasons: AdjustmentReason[]
57
+ onClose: () => void
58
+ onSaved: (quote: Q) => void
59
+ /** Called when the server reports a stale revision; the page reloads the quote. */
60
+ onStale: () => void
61
+ }
62
+
63
+ const TYPE_FALLBACKS: Record<AdjustmentType, string> = {
64
+ price_override: 'Set a new unit price',
65
+ discount_percent: 'Percentage discount',
66
+ discount_absolute: 'Amount off the charge total',
67
+ }
68
+
69
+ function money(amount: number, currency: string) {
70
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
71
+ }
72
+
73
+ export function PriceAdjustmentDialog<Q extends PreviewQuote>(props: PriceAdjustmentDialogProps<Q>) {
74
+ const { open, quoteId, currency, pricingRevision, target, targetLabel, charge, existing, storedAdjustmentIds, reasons, onClose, onSaved, onStale } = props
75
+ const t = useT()
76
+ const types = allowedAdjustmentTypes(target.level, charge)
77
+ const initialForm = React.useCallback((): AdjustmentForm => ({
78
+ adjustmentType: existing?.adjustmentType ?? types[0] ?? 'discount_percent',
79
+ value: existing ? String(existing.value) : '',
80
+ reasonCode: existing?.reasonCode ?? '',
81
+ reasonNote: existing?.reasonNote ?? '',
82
+ chargeTypeFilter: existing?.chargeTypeFilter ?? '',
83
+ }), [existing, types])
84
+ const [form, setForm] = React.useState<AdjustmentForm>(initialForm)
85
+ const [preview, setPreview] = React.useState<{ quote: Q; errors: Array<{ adjustmentId: string; code: string }> } | null>(null)
86
+ const [previewing, setPreviewing] = React.useState(false)
87
+ const [errorCode, setErrorCode] = React.useState<string | null>(null)
88
+ const [saving, setSaving] = React.useState(false)
89
+
90
+ React.useEffect(() => {
91
+ if (open) { setForm(initialForm()); setPreview(null); setErrorCode(null) }
92
+ // Reset only when the dialog (re)opens.
93
+ // eslint-disable-next-line react-hooks/exhaustive-deps
94
+ }, [open])
95
+
96
+ const draft = buildAdjustmentDraft(form, target, currency, pricingRevision)
97
+ const draftKey = draft && form.reasonCode ? JSON.stringify(draft) : null
98
+ const message = (code: string) => t(`cpq.priceAdjustments.errors.${code}`, ADJUSTMENT_ERROR_FALLBACKS[code] ?? code)
99
+
100
+ // Debounced, side-effect-free preview of the draft.
101
+ React.useEffect(() => {
102
+ if (!open || !draftKey) { setPreview(null); return }
103
+ let cancelled = false
104
+ const timer = window.setTimeout(async () => {
105
+ setPreviewing(true)
106
+ const call = await apiCall<{ quote: Q; errors: Array<{ adjustmentId: string; code: string }> }>(
107
+ `/api/cpq/quotes/${quoteId}/price-adjustments/preview`,
108
+ { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ draft: JSON.parse(draftKey), ...(existing ? { replaceAdjustmentId: existing.id } : {}) }) },
109
+ )
110
+ if (cancelled) return
111
+ setPreviewing(false)
112
+ if (call.ok && call.result) { setPreview(call.result); setErrorCode(null) }
113
+ else { setPreview(null); setErrorCode(adjustmentErrorCode(call.status, call.result)) }
114
+ }, 400)
115
+ return () => { cancelled = true; window.clearTimeout(timer) }
116
+ }, [open, draftKey, quoteId, existing])
117
+
118
+ const previewLine = preview?.quote.lines.find((line) => line.lineId === target.quoteLineId)
119
+ const previewCharge = target.level === 'charge' ? previewLine?.charges.find((entry) => entry.chargeCode === target.chargeCode) : undefined
120
+ // The draft's id is server-generated on create, so "not stored" identifies it; on edit it keeps its id.
121
+ const previewErrors = preview?.errors.filter((error) => existing ? error.adjustmentId === existing.id : !storedAdjustmentIds.includes(error.adjustmentId)) ?? []
122
+ const noteRequired = form.reasonCode === 'other' && !form.reasonNote.trim()
123
+ const canSave = !!draft && !!form.reasonCode && !noteRequired && !saving && previewErrors.length === 0
124
+
125
+ const save = async () => {
126
+ if (!draft || !canSave) return
127
+ setSaving(true); setErrorCode(null)
128
+ const call = existing
129
+ ? await apiCall<{ quote: Q }>(`/api/cpq/quotes/${quoteId}/price-adjustments/${existing.id}`, {
130
+ method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify(toAdjustmentPatch(draft)),
131
+ })
132
+ : await apiCall<{ quote: Q }>(`/api/cpq/quotes/${quoteId}/price-adjustments`, {
133
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(draft),
134
+ })
135
+ setSaving(false)
136
+ if (call.ok && call.result?.quote) { onSaved(call.result.quote); onClose(); return }
137
+ setErrorCode(adjustmentErrorCode(call.status, call.result))
138
+ }
139
+
140
+ const onKeyDown = (event: React.KeyboardEvent) => {
141
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); void save() }
142
+ }
143
+
144
+ const standardUnit = charge?.standardUnitPrice ?? charge?.unitPrice
145
+ const valueHint = form.adjustmentType === 'discount_percent'
146
+ ? t('cpq.priceAdjustments.dialog.percentHint', 'Percent off the standard price (0–100)')
147
+ : form.adjustmentType === 'price_override'
148
+ ? t('cpq.priceAdjustments.dialog.overrideHint', 'New unit price, at most the standard price')
149
+ : t('cpq.priceAdjustments.dialog.absoluteHint', 'Amount taken off this charge’s total')
150
+
151
+ return (
152
+ <Dialog open={open} onOpenChange={(next) => { if (!next) onClose() }}>
153
+ <DialogContent onKeyDown={onKeyDown} data-testid="cpq-price-adjustment-dialog">
154
+ <DialogHeader>
155
+ <DialogTitle>
156
+ {existing
157
+ ? t('cpq.priceAdjustments.dialog.editTitle', 'Edit price adjustment')
158
+ : target.level === 'line'
159
+ ? t('cpq.priceAdjustments.dialog.lineTitle', 'Discount line')
160
+ : t('cpq.priceAdjustments.dialog.chargeTitle', 'Adjust price')}
161
+ </DialogTitle>
162
+ <DialogDescription>
163
+ {targetLabel}
164
+ {charge && standardUnit !== undefined && (
165
+ <> · {t('cpq.priceAdjustments.dialog.standard', 'Standard')} {money(standardUnit, currency)}</>
166
+ )}
167
+ </DialogDescription>
168
+ </DialogHeader>
169
+
170
+ <div className="space-y-3 text-sm">
171
+ <label className="block space-y-1">
172
+ <span className="text-xs font-medium">{t('cpq.priceAdjustments.dialog.type', 'Adjustment')}</span>
173
+ <Select value={form.adjustmentType} onValueChange={(value) => setForm({ ...form, adjustmentType: value as AdjustmentType })}>
174
+ <SelectTrigger><SelectValue /></SelectTrigger>
175
+ <SelectContent>
176
+ {types.map((type) => (
177
+ <SelectItem key={type} value={type}>{t(`cpq.priceAdjustments.dialog.types.${type}`, TYPE_FALLBACKS[type])}</SelectItem>
178
+ ))}
179
+ </SelectContent>
180
+ </Select>
181
+ </label>
182
+
183
+ {target.level === 'line' && (
184
+ <label className="block space-y-1">
185
+ <span className="text-xs font-medium">{t('cpq.priceAdjustments.dialog.appliesTo', 'Applies to')}</span>
186
+ <Select value={form.chargeTypeFilter || 'all'} onValueChange={(value) => setForm({ ...form, chargeTypeFilter: value === 'all' ? '' : value as 'nrc' | 'mrc' })}>
187
+ <SelectTrigger><SelectValue /></SelectTrigger>
188
+ <SelectContent>
189
+ <SelectItem value="all">{t('cpq.priceAdjustments.dialog.allCharges', 'All one-time and recurring charges')}</SelectItem>
190
+ <SelectItem value="mrc">{t('cpq.priceAdjustments.dialog.mrcOnly', 'Recurring charges only')}</SelectItem>
191
+ <SelectItem value="nrc">{t('cpq.priceAdjustments.dialog.nrcOnly', 'One-time charges only')}</SelectItem>
192
+ </SelectContent>
193
+ </Select>
194
+ </label>
195
+ )}
196
+
197
+ <label className="block space-y-1">
198
+ <span className="text-xs font-medium">{t('cpq.priceAdjustments.dialog.value', 'Value')}</span>
199
+ <Input inputMode="decimal" autoFocus value={form.value} onChange={(event) => setForm({ ...form, value: event.target.value })} aria-describedby="cpq-adjustment-value-hint" />
200
+ <span id="cpq-adjustment-value-hint" className="block text-xs text-muted-foreground">{valueHint}</span>
201
+ </label>
202
+
203
+ <label className="block space-y-1">
204
+ <span className="text-xs font-medium">{t('cpq.priceAdjustments.dialog.reason', 'Reason')}</span>
205
+ <Select value={form.reasonCode} onValueChange={(value) => setForm({ ...form, reasonCode: value })}>
206
+ <SelectTrigger><SelectValue placeholder={t('cpq.priceAdjustments.dialog.reasonPlaceholder', 'Pick a reason')} /></SelectTrigger>
207
+ <SelectContent>
208
+ {reasons.map((reason) => <SelectItem key={reason.value} value={reason.value}>{reason.label}</SelectItem>)}
209
+ </SelectContent>
210
+ </Select>
211
+ </label>
212
+
213
+ <label className="block space-y-1">
214
+ <span className="text-xs font-medium">
215
+ {t('cpq.priceAdjustments.dialog.note', 'Note')}{form.reasonCode === 'other' ? ' *' : ''}
216
+ </span>
217
+ <Textarea rows={2} maxLength={500} value={form.reasonNote} onChange={(event) => setForm({ ...form, reasonNote: event.target.value })} />
218
+ </label>
219
+
220
+ <div className="rounded border bg-muted/20 p-2 text-xs" aria-live="polite" data-testid="cpq-adjustment-preview">
221
+ {!draftKey && <span className="text-muted-foreground">{t('cpq.priceAdjustments.dialog.previewEmpty', 'Enter a value and reason to preview the new price.')}</span>}
222
+ {draftKey && previewing && <span className="text-muted-foreground">{t('cpq.priceAdjustments.dialog.previewing', 'Calculating…')}</span>}
223
+ {draftKey && !previewing && previewCharge && (
224
+ <span>
225
+ {t('cpq.priceAdjustments.dialog.previewCharge', 'New offer price')}: <span className="font-mono font-medium">{money(previewCharge.unitPrice, currency)}</span>
226
+ {' '}{formatDepth(discountDepth(previewCharge.standardUnitPrice ?? standardUnit, previewCharge.unitPrice)) ?? ''}
227
+ </span>
228
+ )}
229
+ {draftKey && !previewing && !previewCharge && previewLine && (
230
+ <span>
231
+ {t('cpq.priceAdjustments.dialog.previewLine', 'Line totals')}: {t('cpq.lines.mrc', 'MRC')} <span className="font-mono font-medium">{money(previewLine.mrcTotal, currency)}</span>
232
+ {' · '}{t('cpq.lines.nrc', 'NRC')} <span className="font-mono font-medium">{money(previewLine.nrcTotal, currency)}</span>
233
+ </span>
234
+ )}
235
+ </div>
236
+
237
+ {(errorCode || previewErrors.length > 0 || noteRequired) && (
238
+ <Alert variant="warning">
239
+ <AlertDescription>
240
+ {errorCode ? message(errorCode) : previewErrors.length > 0 ? message(previewErrors[0].code) : message('REASON_NOTE_REQUIRED')}
241
+ {errorCode === 'STALE_REVISION' && (
242
+ <Button variant="outline" size="sm" className="ml-2" onClick={() => { onStale(); onClose() }}>
243
+ {t('cpq.priceAdjustments.dialog.reload', 'Reload')}
244
+ </Button>
245
+ )}
246
+ </AlertDescription>
247
+ </Alert>
248
+ )}
249
+ </div>
250
+
251
+ <DialogFooter>
252
+ <Button variant="outline" onClick={onClose}>{t('cpq.priceAdjustments.dialog.cancel', 'Cancel')}</Button>
253
+ <Button onClick={() => void save()} disabled={!canSave} title={t('cpq.priceAdjustments.dialog.saveShortcut', 'Ctrl/Cmd+Enter')}>
254
+ {saving ? t('cpq.priceAdjustments.dialog.saving', 'Saving…') : t('cpq.priceAdjustments.dialog.save', 'Save')}
255
+ </Button>
256
+ </DialogFooter>
257
+ </DialogContent>
258
+ </Dialog>
259
+ )
260
+ }
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { Tag } from '@open-mercato/ui/primitives/tag'
6
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
7
+ import { ADJUSTMENT_ERROR_FALLBACKS } from '../../../../../lib/priceAdjustmentView'
8
+ import { ManualAdjustmentActions, useAccessReasonText, usePriceAdjustmentUi } from './priceAdjustmentContext'
9
+
10
+ export const PRICE_ADJUSTMENTS_SPOT_ID = 'data-table:cpq.quote_price_adjustments'
11
+
12
+ export type StoredAdjustment = {
13
+ id: string
14
+ level: 'charge' | 'line'
15
+ quoteLineId: string
16
+ chargeCode?: string | null
17
+ chargeTypeFilter?: 'nrc' | 'mrc' | null
18
+ adjustmentType: 'discount_percent' | 'discount_absolute' | 'price_override'
19
+ value: number
20
+ currencyCode?: string | null
21
+ reasonCode: string
22
+ reasonNote?: string | null
23
+ createdByUserId: string
24
+ createdAt: string
25
+ }
26
+
27
+ type LineRef = { lineId: string; offeringName: string; offeringId: string | null; configuration?: Record<string, unknown> | null; charges: Array<{ chargeCode: string; chargeName: string }> }
28
+
29
+ const TYPE_FALLBACKS: Record<StoredAdjustment['adjustmentType'], string> = {
30
+ price_override: 'Price override',
31
+ discount_percent: '% discount',
32
+ discount_absolute: 'Amount off',
33
+ }
34
+
35
+ function money(amount: number, currency: string) {
36
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
37
+ }
38
+
39
+ export function PriceAdjustmentsPanel({ quoteId, items, total, lines, currency, errorsById }: {
40
+ quoteId: string
41
+ items: StoredAdjustment[]
42
+ total: number
43
+ lines: LineRef[]
44
+ currency: string
45
+ /** adjustmentId → first error code reported by the latest pricing. */
46
+ errorsById: Map<string, string>
47
+ }) {
48
+ const t = useT()
49
+ const ui = usePriceAdjustmentUi()
50
+ const lineById = React.useMemo(() => new Map(lines.map((line) => [line.lineId, line])), [lines])
51
+ const accessText = useAccessReasonText()
52
+ // Toggle off: stored adjustments still price the quote, so keep them visible (read-only).
53
+ if (!ui || (ui.access.reason === 'disabled' && items.length === 0)) return null
54
+
55
+ return (
56
+ <div className="rounded-lg border bg-card" data-testid="cpq-price-adjustments-panel">
57
+ <div className="flex items-center justify-between border-b px-4 py-3">
58
+ <h3 className="text-sm font-medium">
59
+ {t('cpq.priceAdjustments.panel.title', 'Price adjustments')}
60
+ <span className="ml-2 text-xs text-muted-foreground">{total}</span>
61
+ </h3>
62
+ {accessText && ui.access.reason !== 'loading' && (
63
+ <span className="text-xs text-muted-foreground" data-testid="cpq-price-adjustments-locked">{accessText}</span>
64
+ )}
65
+ </div>
66
+ {items.length === 0 ? (
67
+ <p className="px-4 py-3 text-xs text-muted-foreground">
68
+ {t('cpq.priceAdjustments.panel.empty', 'No negotiated prices yet. Use “Adjust price” on a charge or “Discount line” on a line.')}
69
+ </p>
70
+ ) : (
71
+ <table className="w-full text-xs">
72
+ <thead>
73
+ <tr className="text-muted-foreground">
74
+ <th className="px-4 py-1.5 text-left font-medium">{t('cpq.priceAdjustments.panel.target', 'Applies to')}</th>
75
+ <th className="py-1.5 text-left font-medium">{t('cpq.priceAdjustments.panel.type', 'Type')}</th>
76
+ <th className="py-1.5 text-right font-medium">{t('cpq.priceAdjustments.panel.value', 'Value')}</th>
77
+ <th className="py-1.5 pl-4 text-left font-medium">{t('cpq.priceAdjustments.panel.reason', 'Reason')}</th>
78
+ <th className="py-1.5 text-left font-medium">{t('cpq.priceAdjustments.panel.author', 'Author')}</th>
79
+ <th className="py-1.5 text-left font-medium">{t('cpq.priceAdjustments.panel.date', 'Date')}</th>
80
+ <th className="w-16" />
81
+ </tr>
82
+ </thead>
83
+ <tbody>
84
+ {items.map((item) => {
85
+ const line = lineById.get(item.quoteLineId)
86
+ const chargeName = item.chargeCode ? line?.charges.find((charge) => charge.chargeCode === item.chargeCode)?.chargeName ?? item.chargeCode : null
87
+ const scope = item.level === 'line'
88
+ ? item.chargeTypeFilter === 'mrc' ? t('cpq.priceAdjustments.panel.lineMrc', 'whole line, recurring')
89
+ : item.chargeTypeFilter === 'nrc' ? t('cpq.priceAdjustments.panel.lineNrc', 'whole line, one-time')
90
+ : t('cpq.priceAdjustments.panel.line', 'whole line')
91
+ : chargeName
92
+ const error = errorsById.get(item.id)
93
+ return (
94
+ <tr key={item.id} className="border-t" data-testid="cpq-price-adjustment-row">
95
+ <td className="px-4 py-1.5">
96
+ <span className="font-medium">{line?.offeringName ?? t('cpq.priceAdjustments.panel.removedLine', 'Removed line')}</span>
97
+ <span className="text-muted-foreground"> · {scope}</span>
98
+ {error && (
99
+ <Tag variant="warning" className="ml-1.5 px-1.5 text-xs">
100
+ {t('cpq.priceAdjustments.invalid', 'Invalid')}: {t(`cpq.priceAdjustments.errors.${error}`, ADJUSTMENT_ERROR_FALLBACKS[error] ?? error)}
101
+ </Tag>
102
+ )}
103
+ </td>
104
+ <td className="py-1.5">{t(`cpq.priceAdjustments.panel.types.${item.adjustmentType}`, TYPE_FALLBACKS[item.adjustmentType])}</td>
105
+ <td className="py-1.5 text-right font-mono">
106
+ {item.adjustmentType === 'discount_percent' ? `${item.value}%` : money(item.value, item.currencyCode ?? currency)}
107
+ </td>
108
+ <td className="py-1.5 pl-4">
109
+ {ui.reasonLabels[item.reasonCode] ?? item.reasonCode}
110
+ {item.reasonNote && <span className="block text-muted-foreground">{item.reasonNote}</span>}
111
+ </td>
112
+ <td className="py-1.5 font-mono text-muted-foreground" title={item.createdByUserId}>{item.createdByUserId.slice(0, 8)}</td>
113
+ <td className="py-1.5 text-muted-foreground">{new Date(item.createdAt).toLocaleDateString()}</td>
114
+ <td className="py-1.5 pr-2 text-right"><ManualAdjustmentActions adjustmentId={item.id} /></td>
115
+ </tr>
116
+ )
117
+ })}
118
+ </tbody>
119
+ </table>
120
+ )}
121
+ <InjectionSpot spotId={PRICE_ADJUSTMENTS_SPOT_ID} context={{ quoteId, adjustments: items }} />
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,76 @@
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 { PriceAdjustmentsPanel, PRICE_ADJUSTMENTS_SPOT_ID } from '../PriceAdjustmentsPanel'
5
+ import { AdjustChargeAction, DiscountLineAction, PriceAdjustmentProvider, type PriceAdjustmentUi } from '../priceAdjustmentContext'
6
+ import { adjustmentAccess } from '../../../../../../lib/priceAdjustmentView'
7
+
8
+ jest.mock('@open-mercato/ui/backend/injection/InjectionSpot', () => ({
9
+ InjectionSpot: ({ spotId }: { spotId: string }) => <div data-spot={spotId} />,
10
+ }))
11
+
12
+ const text = (html: string) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
13
+ const line = { lineId: 'l1', offeringName: 'Fiber 1G', offeringId: 'o1', configuration: {}, charges: [{ chargeCode: 'fee', chargeName: 'Monthly fee' }] }
14
+ const item = {
15
+ id: 'a1', level: 'charge' as const, quoteLineId: 'l1', chargeCode: 'fee', adjustmentType: 'price_override' as const, value: 80,
16
+ currencyCode: 'USD', reasonCode: 'competitive_match', reasonNote: 'Beat rival', createdByUserId: '77777777-7777-4777-8777-777777777777', createdAt: '2026-11-10T10:00:00.000Z',
17
+ }
18
+
19
+ function ui(patch: Partial<Parameters<typeof adjustmentAccess>[0]> = {}, extra: Partial<PriceAdjustmentUi> = {}): PriceAdjustmentUi {
20
+ const input = { enabled: true, canAdjust: true, cpqStatus: 'ready', convertedOrderId: null, ...patch }
21
+ return {
22
+ access: adjustmentAccess(input), quoteType: 'new', cpqStatus: input.cpqStatus, reasonLabels: { competitive_match: 'Competitive match' },
23
+ invalidIds: new Set(), canDeleteInvalid: false, openCharge: jest.fn(), openLine: jest.fn(), edit: jest.fn(), remove: jest.fn(), ...extra,
24
+ }
25
+ }
26
+ const render = (value: PriceAdjustmentUi, node: React.ReactNode) =>
27
+ renderToStaticMarkup(<I18nProvider locale="en" dict={{}}><PriceAdjustmentProvider value={value}>{node}</PriceAdjustmentProvider></I18nProvider>)
28
+ const panel = (errors = new Map<string, string>()) => <PriceAdjustmentsPanel quoteId="q1" items={[item]} total={1} lines={[line]} currency="USD" errorsById={errors} />
29
+
30
+ it('lists the adjustment with target, value, reason label, note and edit/delete, plus the injection spot', () => {
31
+ const html = render(ui(), panel())
32
+ const content = text(html)
33
+ expect(content).toContain('Fiber 1G · Monthly fee')
34
+ expect(content).toContain('Price override')
35
+ expect(content).toContain('$80.00')
36
+ expect(content).toContain('Competitive match Beat rival')
37
+ expect(html).toContain('aria-label="Edit adjustment"')
38
+ expect(html).toContain('aria-label="Delete adjustment"')
39
+ expect(html).toContain(`data-spot="${PRICE_ADJUSTMENTS_SPOT_ID}"`)
40
+ })
41
+
42
+ it('when locked, explains why and offers no edit or delete for valid rows', () => {
43
+ const html = render(ui({ cpqStatus: 'in_approval' }), panel())
44
+ expect(text(html)).toContain('Adjustments are locked in status in_approval')
45
+ expect(html).not.toContain('Edit adjustment')
46
+ expect(html).not.toContain('Delete adjustment')
47
+ })
48
+
49
+ it('keeps Delete for an invalid row while locked and names the error in text', () => {
50
+ const html = render(ui({ cpqStatus: 'in_approval' }, { invalidIds: new Set(['a1']), canDeleteInvalid: true }), panel(new Map([['a1', 'ABOVE_STANDARD']])))
51
+ expect(text(html)).toContain('Invalid: The price is above the standard price')
52
+ expect(html).toContain('Delete adjustment')
53
+ expect(html).not.toContain('Edit adjustment')
54
+ })
55
+
56
+ it('stays visible but read-only when the toggle is off and adjustments exist', () => {
57
+ const html = render(ui({ enabled: false }), panel())
58
+ expect(text(html)).toContain('Manual price adjustments are turned off')
59
+ expect(text(html)).toContain('Competitive match')
60
+ expect(html).not.toContain('Edit adjustment')
61
+ expect(html).not.toContain('Delete adjustment')
62
+ })
63
+
64
+ it('renders nothing when the toggle is off and nothing is stored', () => {
65
+ expect(render(ui({ enabled: false }), <PriceAdjustmentsPanel quoteId="q1" items={[]} total={0} lines={[line]} currency="USD" errorsById={new Map()} />)).toBe('')
66
+ })
67
+
68
+ it('hides actions without permission and disables them with a tooltip on ARC mirror lines', () => {
69
+ const charge = { chargeCode: 'fee', chargeName: 'Monthly fee', chargeType: 'mrc' as const, unitPrice: 100, totalPrice: 100 }
70
+ expect(render(ui({ canAdjust: false }), <><DiscountLineAction line={line} /><AdjustChargeAction line={line} charge={charge} /></>)).toBe('')
71
+ const mirror = { ...line, configuration: { _arcMirroredFromItemId: 'item' } }
72
+ const html = render(ui({}, { quoteType: 'amend' }), <AdjustChargeAction line={mirror} charge={charge} />)
73
+ expect(html).toContain('disabled')
74
+ expect(html).toContain('Price changes on existing subscription items arrive with a later release')
75
+ expect(render(ui(), <AdjustChargeAction line={line} charge={{ ...charge, chargeType: 'usage' }} />)).toBe('')
76
+ })
@@ -0,0 +1,76 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
7
+ import { isArcCancelledLine, removalCancelsItem, type ArcRemovalLine } from '../../../../../services/arcLineRemoval'
8
+
9
+ type RemovableLine = Pick<ArcRemovalLine, 'action' | 'configuration'> & { lineId: string; offeringName: string }
10
+
11
+ export type ArcLineRemovalUi = {
12
+ /** The line is an existing subscription item marked for removal. */
13
+ isRemoved: (line: Pick<RemovableLine, 'action'>) => boolean
14
+ restore: (lineId: string) => void
15
+ }
16
+
17
+ const ArcLineRemovalContext = React.createContext<ArcLineRemovalUi | null>(null)
18
+
19
+ export const ArcLineRemovalProvider = ArcLineRemovalContext.Provider
20
+
21
+ export function useArcLineRemovalUi(): ArcLineRemovalUi | null {
22
+ return React.useContext(ArcLineRemovalContext)
23
+ }
24
+
25
+ /**
26
+ * Line removal on the quote page. On an Amend / Renew quote, removing a line
27
+ * that mirrors a live subscription item asks for confirmation and marks the
28
+ * item for removal (server turns it into a `cancel` line); it can be restored.
29
+ * Other lines are deleted as before.
30
+ */
31
+ export function useArcLineRemoval<Q extends { id: string; lines: RemovableLine[] }>({ quote, quoteType, onQuote, onError }: {
32
+ quote: Q | null
33
+ quoteType: string
34
+ onQuote: (quote: Q) => void
35
+ onError: (message: string | null) => void
36
+ }) {
37
+ const t = useT()
38
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
39
+
40
+ const send = React.useCallback(async (url: string, method: 'DELETE' | 'POST') => {
41
+ onError(null)
42
+ const call = await apiCall<Q & { error?: string }>(url, { method })
43
+ if (call.ok && call.result) onQuote(call.result)
44
+ else onError(call.result?.error ?? t('cpq.quotes.lineUpdateFailed', 'Could not update the line'))
45
+ }, [onError, onQuote, t])
46
+
47
+ const remove = React.useCallback(async (lineId: string) => {
48
+ if (!quote) return
49
+ const line = quote.lines.find((candidate) => candidate.lineId === lineId)
50
+ if (line && removalCancelsItem(line, quoteType)) {
51
+ const ok = await confirm({
52
+ title: t('cpq.quotes.arc.removeConfirm.title', 'Remove {name} from the subscription?').replace('{name}', line.offeringName),
53
+ text: t(
54
+ 'cpq.quotes.arc.removeConfirm.text',
55
+ 'This item already exists on the subscription. Removing it here does not delete it from the quote — it stays as a "remove" line, and when the order is activated the item (and its components) is cancelled on the subscription and its recurring charge stops. You can restore it until then.',
56
+ ),
57
+ confirmText: t('cpq.quotes.arc.removeConfirm.confirm', 'Remove item'),
58
+ variant: 'destructive',
59
+ })
60
+ if (!ok) return
61
+ }
62
+ await send(`/api/cpq/quotes/${quote.id}/items/${lineId}`, 'DELETE')
63
+ }, [confirm, quote, quoteType, send, t])
64
+
65
+ const restore = React.useCallback((lineId: string) => {
66
+ if (!quote) return
67
+ void send(`/api/cpq/quotes/${quote.id}/items/${lineId}/restore`, 'POST')
68
+ }, [quote, send])
69
+
70
+ const ui = React.useMemo<ArcLineRemovalUi>(() => ({
71
+ isRemoved: (line) => isArcCancelledLine(line, quoteType),
72
+ restore,
73
+ }), [quoteType, restore])
74
+
75
+ return { remove, ui, ConfirmDialogElement }
76
+ }