@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,90 @@
1
+ import { z } from 'zod'
2
+ import { resolveAuditActor } from '../lib/auditActor'
3
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
+ import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
+ import { registerCommand, type CommandHandler, type CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
6
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
+ import { cpqPriceAdjustmentInputSchema, cpqPriceAdjustmentPatchSchema } from '../data/price-adjustments'
8
+ import { QuotingError } from '../services/cpqQuotingService'
9
+ import type { CpqAdjustmentService, AdjustmentWriteResult, AdjustmentSnapshot } from '../services/priceAdjustments/adjustmentService'
10
+
11
+ const id = z.string().uuid()
12
+ const createSchema = z.object({ quoteId: id, adjustment: cpqPriceAdjustmentInputSchema }).strict()
13
+ const updateSchema = z.object({ quoteId: id, adjustmentId: id, patch: cpqPriceAdjustmentPatchSchema }).strict()
14
+ const deleteSchema = z.object({ quoteId: id, adjustmentId: id, expectedRevision: z.number().int().min(0).optional() }).strict()
15
+ // Undo data is parsed independently of the original request and never trusted
16
+ // to supply actor/scope. Its target is checked again under the aggregate lock.
17
+ const snapshotSchema = z.object({
18
+ id, quoteConfigurationId: id, quoteId: id, tenantId: id, organizationId: id,
19
+ createdByUserId: id, createdAt: z.string().datetime(), updatedAt: z.string().datetime(),
20
+ deletedAt: z.string().datetime().nullable(), isActive: z.boolean(),
21
+ level: z.enum(['charge', 'line']), quoteLineId: id, chargeCode: z.string().nullish(),
22
+ chargeTypeFilter: z.enum(['nrc', 'mrc']).nullish(), adjustmentType: z.enum(['discount_percent', 'discount_absolute', 'price_override']),
23
+ value: z.number(), currencyCode: z.string().nullish(), reasonCode: z.string(), reasonNote: z.string().nullish(),
24
+ })
25
+ const undoSchema = z.object({ quoteId: id, before: snapshotSchema.nullable(), after: snapshotSchema })
26
+
27
+ async function context(ctx: CommandRuntimeContext) {
28
+ // RBAC is checked for the authenticated subject (user or API key); the
29
+ // stored author must be a user id (uuid columns).
30
+ const subject = ctx.auth?.sub
31
+ const actor = resolveAuditActor(ctx.auth)
32
+ const tenantId = ctx.organizationScope?.tenantId ?? ctx.auth?.tenantId
33
+ const organizationId = ctx.selectedOrganizationId
34
+ if (!subject || !tenantId || !organizationId) throw new QuotingError(403, 'An authenticated tenant and organization are required')
35
+ if (!actor) throw new QuotingError(403, 'Price adjustments require a user or a user-bound API key')
36
+ const scope = { tenantId, organizationId }
37
+ const rbac = ctx.container.resolve<RbacService>('rbacService')
38
+ if (!(await rbac.userHasAllFeatures(subject, ['cpq.quotes.manage', 'cpq.quotes.adjust_price'], scope))) throw new QuotingError(403, 'Price adjustment permission required')
39
+ return { actor, scope, service: ctx.container.resolve<CpqAdjustmentService>('cpqAdjustmentService') }
40
+ }
41
+
42
+ async function buildLog(quoteId: string, result: AdjustmentWriteResult) {
43
+ const { translate } = await resolveTranslations()
44
+ return { actionLabel: translate('cpq.priceAdjustments.audit.adjust', 'Adjust quote price'), resourceKind: 'cpq.quote_price_adjustment', resourceId: result.adjustment.id,
45
+ tenantId: result.adjustment.tenantId, organizationId: result.adjustment.organizationId,
46
+ snapshotBefore: result.before, snapshotAfter: result.adjustment,
47
+ payload: { undo: { quoteId, before: result.before, after: result.adjustment } } }
48
+ }
49
+
50
+ async function undo({ logEntry, ctx }: Parameters<NonNullable<CommandHandler['undo']>>[0]) {
51
+ const envelope = undoSchema.safeParse(extractUndoPayload<unknown>(logEntry))
52
+ if (!envelope.success) throw new QuotingError(422, 'Invalid adjustment undo data')
53
+ const { actor, scope, service } = await context(ctx)
54
+ const { quoteId, before, after } = envelope.data
55
+ await service.undo(quoteId, after.id, before as AdjustmentSnapshot | null, after as AdjustmentSnapshot, actor, scope)
56
+ }
57
+
58
+ export const createAdjustmentCommand: CommandHandler<z.infer<typeof createSchema>, AdjustmentWriteResult> = {
59
+ id: 'cpq.quote_price_adjustment.create', isUndoable: true,
60
+ async execute(input, ctx) {
61
+ const parsed = createSchema.parse(input)
62
+ const { actor, scope, service } = await context(ctx)
63
+ return service.create(parsed.quoteId, parsed.adjustment, actor, scope)
64
+ },
65
+ buildLog: ({ input, result }) => buildLog(input.quoteId, result),
66
+ undo,
67
+ }
68
+ export const updateAdjustmentCommand: CommandHandler<z.infer<typeof updateSchema>, AdjustmentWriteResult> = {
69
+ id: 'cpq.quote_price_adjustment.update', isUndoable: true,
70
+ async execute(input, ctx) {
71
+ const parsed = updateSchema.parse(input)
72
+ const { actor, scope, service } = await context(ctx)
73
+ return service.update(parsed.quoteId, parsed.adjustmentId, parsed.patch, actor, scope)
74
+ },
75
+ buildLog: ({ input, result }) => buildLog(input.quoteId, result),
76
+ undo,
77
+ }
78
+ export const deleteAdjustmentCommand: CommandHandler<z.infer<typeof deleteSchema>, AdjustmentWriteResult> = {
79
+ id: 'cpq.quote_price_adjustment.delete', isUndoable: true,
80
+ async execute(input, ctx) {
81
+ const parsed = deleteSchema.parse(input)
82
+ const { actor, scope, service } = await context(ctx)
83
+ return service.remove(parsed.quoteId, parsed.adjustmentId, actor, scope, parsed.expectedRevision)
84
+ },
85
+ buildLog: ({ input, result }) => buildLog(input.quoteId, result),
86
+ undo,
87
+ }
88
+ registerCommand(createAdjustmentCommand)
89
+ registerCommand(updateAdjustmentCommand)
90
+ registerCommand(deleteAdjustmentCommand)
@@ -0,0 +1,87 @@
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 { formatMoney } from '../lib/formatMoney'
7
+
8
+ export type LineAction = 'add' | 'modify' | 'cancel'
9
+
10
+ const ACTION_VARIANTS: Record<LineAction, 'info' | 'warning' | 'error'> = {
11
+ add: 'info',
12
+ modify: 'warning',
13
+ cancel: 'error',
14
+ }
15
+
16
+ /** Coloured add / modify / cancel tag on quote and order lines. */
17
+ export function LineActionBadge({ action, label, title }: { action: string; label?: string; title?: string }) {
18
+ const variant = ACTION_VARIANTS[action as LineAction] ?? 'info'
19
+ return (
20
+ <Tag variant={variant} className="px-1.5 text-xs" title={title}>
21
+ {label ?? action}
22
+ </Tag>
23
+ )
24
+ }
25
+
26
+ export type ArcDiffLine = {
27
+ action: string
28
+ quantity: number
29
+ nrcTotal: number
30
+ mrcTotal: number
31
+ arcSource: { mrcAmount: number; nrcAmount: number; quantity: number } | null
32
+ }
33
+
34
+ /** Before / after / Δ preview for a line that changes an existing subscription item. */
35
+ export function ArcLineDiff({ line, currency }: { line: ArcDiffLine; currency: string }) {
36
+ const t = useT()
37
+ const src = line.arcSource
38
+ if (!src) return null
39
+ const isCancel = line.action === 'cancel'
40
+ const rows: Array<{ label: string; before: number; after: number; asCurrency: boolean }> = [
41
+ { label: t('cpq.arc.diff.quantity', 'Quantity'), before: src.quantity, after: isCancel ? 0 : line.quantity, asCurrency: false },
42
+ { label: t('cpq.arc.diff.mrc', 'MRC'), before: src.mrcAmount, after: isCancel ? 0 : line.mrcTotal, asCurrency: true },
43
+ { label: t('cpq.arc.diff.nrc', 'NRC'), before: src.nrcAmount, after: isCancel ? 0 : line.nrcTotal, asCurrency: true },
44
+ ]
45
+ const fmtVal = (value: number, asCurrency: boolean) => (asCurrency ? formatMoney(value, currency) : String(value))
46
+
47
+ return (
48
+ <div className="space-y-1.5" data-testid="cpq-arc-line-diff">
49
+ <p className="text-xs font-medium text-muted-foreground">{t('cpq.arc.diff.title', 'Change Preview')}</p>
50
+ <div className="rounded border bg-background overflow-hidden">
51
+ <table className="w-full text-xs">
52
+ <thead className="bg-muted/40">
53
+ <tr className="text-muted-foreground">
54
+ <th className="text-left px-3 py-1.5 font-medium" />
55
+ <th className="text-right px-3 py-1.5 font-medium">{t('cpq.arc.diff.before', 'Before')}</th>
56
+ <th className="text-right px-3 py-1.5 font-medium">{t('cpq.arc.diff.after', 'After')}</th>
57
+ <th className="text-right px-3 py-1.5 font-medium">Δ</th>
58
+ </tr>
59
+ </thead>
60
+ <tbody>
61
+ {rows.map((r) => {
62
+ const delta = r.after - r.before
63
+ const showDelta = delta !== 0
64
+ return (
65
+ <tr key={r.label} className="border-t border-dashed border-border/40">
66
+ <td className="px-3 py-1 text-muted-foreground">{r.label}</td>
67
+ <td className="px-3 py-1 text-right font-mono">{fmtVal(r.before, r.asCurrency)}</td>
68
+ <td className={`px-3 py-1 text-right font-mono ${isCancel ? 'text-muted-foreground line-through' : ''}`}>
69
+ {fmtVal(r.after, r.asCurrency)}
70
+ </td>
71
+ <td className={`px-3 py-1 text-right font-mono font-medium ${showDelta ? (delta > 0 ? 'text-status-success-text' : 'text-destructive') : 'text-muted-foreground'}`}>
72
+ {showDelta ? `${delta > 0 ? '+' : ''}${fmtVal(delta, r.asCurrency)}` : '—'}
73
+ </td>
74
+ </tr>
75
+ )
76
+ })}
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ {isCancel && (
81
+ <p className="text-xs text-destructive italic">
82
+ {t('cpq.arc.diff.cancelNote', 'Cancellation — item will be terminated on activation.')}
83
+ </p>
84
+ )}
85
+ </div>
86
+ )
87
+ }
@@ -0,0 +1,203 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { Info } from 'lucide-react'
6
+ import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
7
+ import { Tag } from '@open-mercato/ui/primitives/tag'
8
+ import { DepthBadge, PriceWaterfall } from './PriceWaterfall'
9
+ import { ADJUSTMENT_ERROR_FALLBACKS, type WaterfallChargeInput } from '../lib/priceAdjustmentView'
10
+ import { formatMoney } from '../lib/formatMoney'
11
+
12
+ type TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }
13
+
14
+ /** One priced charge as stored on a quote or order line. */
15
+ export type ChargeRowView = WaterfallChargeInput & {
16
+ chargeName: string
17
+ quantity?: number | null
18
+ note?: string | null
19
+ breakdown?: { tiers: TierBreakdownEntry[] } | null
20
+ adjustmentErrors?: Array<{ code: string }>
21
+ }
22
+
23
+ /** Right-aligned price column header with an explanatory hover tooltip. */
24
+ function PriceColumnHeader({ label, hint }: { label: string; hint: string }) {
25
+ return (
26
+ <th className="text-right py-1 font-medium">
27
+ <SimpleTooltip content={hint} size="sm">
28
+ <span className="inline-flex items-center gap-1 cursor-help" tabIndex={0} aria-label={`${label}: ${hint}`}>
29
+ {label}
30
+ <Info className="h-3 w-3" aria-hidden />
31
+ </span>
32
+ </SimpleTooltip>
33
+ </th>
34
+ )
35
+ }
36
+
37
+ /**
38
+ * Charge table shared by the quote and order detail pages: list / standard /
39
+ * offer unit prices with depth badge, and a per-charge pricing detail (tiers +
40
+ * price waterfall). Editing hooks are optional render props.
41
+ */
42
+ export function ChargeBreakdownTable<C extends ChargeRowView>({
43
+ charges,
44
+ currency,
45
+ openDetailIndex,
46
+ onToggleDetail,
47
+ reasonLabels,
48
+ renderChargeActions,
49
+ renderManualActions,
50
+ }: {
51
+ charges: C[]
52
+ currency: string
53
+ openDetailIndex: number | null
54
+ onToggleDetail: (chargeIndex: number) => void
55
+ reasonLabels?: Record<string, string>
56
+ renderChargeActions?: (charge: C) => React.ReactNode
57
+ renderManualActions?: (adjustmentId: string) => React.ReactNode
58
+ }) {
59
+ const t = useT()
60
+ return (
61
+ <table className="w-full text-xs" data-testid="cpq-charge-breakdown">
62
+ <thead>
63
+ <tr className="text-muted-foreground">
64
+ <th className="text-left py-1 font-medium">{t('cpq.charges.charge', 'Charge')}</th>
65
+ <th className="text-left py-1 font-medium">{t('cpq.charges.type', 'Type')}</th>
66
+ <PriceColumnHeader
67
+ label={t('cpq.priceAdjustments.listUnit', 'List')}
68
+ hint={t('cpq.priceAdjustments.listUnitHint', 'Catalog unit price from the pricing table, before any price rules or manual adjustments.')}
69
+ />
70
+ <PriceColumnHeader
71
+ label={t('cpq.priceAdjustments.standardUnit', 'Standard')}
72
+ hint={t('cpq.priceAdjustments.standardUnitHint', 'Unit price after automatic price rules (e.g. volume or segment discounts), before manual adjustments.')}
73
+ />
74
+ <PriceColumnHeader
75
+ label={t('cpq.priceAdjustments.offerUnit', 'Offer')}
76
+ hint={t('cpq.priceAdjustments.offerUnitHint', 'Final unit price quoted to the customer, after price rules and manual adjustments. The badge shows the total change vs list.')}
77
+ />
78
+ <th className="text-right py-1 font-medium">{t('cpq.charges.qty', 'Qty')}</th>
79
+ <th className="text-right py-1 font-medium">{t('cpq.charges.total', 'Total')}</th>
80
+ <th className="w-16" />
81
+ </tr>
82
+ </thead>
83
+ <tbody>
84
+ {charges.map((c, i) => {
85
+ const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)
86
+ const isDetailOpen = openDetailIndex === i
87
+ const listUnit = c.listUnitPrice ?? c.unitPrice
88
+ const standardUnit = c.standardUnitPrice ?? c.unitPrice
89
+ const errors = c.adjustmentErrors ?? []
90
+ return (
91
+ <React.Fragment key={i}>
92
+ <tr className="border-t border-dashed border-border/50">
93
+ <td className="py-1">
94
+ <span>{c.chargeName}</span>
95
+ {errors.length > 0 && (
96
+ <Tag variant="warning" className="ml-1.5 px-1.5 text-xs" data-testid="cpq-adjustment-invalid">
97
+ {t('cpq.priceAdjustments.invalidReason', 'Invalid: {reason}', {
98
+ reason: t(`cpq.priceAdjustments.errors.${errors[0].code}`, ADJUSTMENT_ERROR_FALLBACKS[errors[0].code] ?? errors[0].code),
99
+ })}
100
+ </Tag>
101
+ )}
102
+ </td>
103
+ <td className="py-1 uppercase">{c.chargeType}</td>
104
+ <td className="py-1 text-right font-mono text-muted-foreground">{formatMoney(listUnit, currency)}</td>
105
+ <td className="py-1 text-right font-mono text-muted-foreground">{formatMoney(standardUnit, currency)}</td>
106
+ <td className="py-1 text-right font-mono">
107
+ <span className="inline-flex items-center gap-1">
108
+ <DepthBadge before={listUnit} after={c.unitPrice} />
109
+ {formatMoney(c.unitPrice, currency)}
110
+ </span>
111
+ </td>
112
+ <td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
113
+ <td className="py-1 text-right font-mono">{c.totalPrice != null ? formatMoney(c.totalPrice, currency) : c.note ?? '—'}</td>
114
+ <td className="py-1 text-right whitespace-nowrap">
115
+ {renderChargeActions?.(c)}
116
+ {hasDetail && (
117
+ <button
118
+ type="button"
119
+ onClick={() => onToggleDetail(i)}
120
+ className="inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
121
+ title={t('cpq.priceAdjustments.showBreakdown', 'Show price breakdown')}
122
+ >
123
+ <Info className="h-3.5 w-3.5" />
124
+ </button>
125
+ )}
126
+ </td>
127
+ </tr>
128
+ {isDetailOpen && (
129
+ <tr>
130
+ <td colSpan={8} className="pb-3 pt-1">
131
+ <ChargePricingDetail charge={c} currency={currency} reasonLabels={reasonLabels} renderManualActions={renderManualActions} />
132
+ </td>
133
+ </tr>
134
+ )}
135
+ </React.Fragment>
136
+ )
137
+ })}
138
+ </tbody>
139
+ </table>
140
+ )
141
+ }
142
+
143
+ function ChargePricingDetail({ charge: c, currency, reasonLabels, renderManualActions }: {
144
+ charge: ChargeRowView
145
+ currency: string
146
+ reasonLabels?: Record<string, string>
147
+ renderManualActions?: (adjustmentId: string) => React.ReactNode
148
+ }) {
149
+ const t = useT()
150
+ const tiers = c.breakdown?.tiers ?? []
151
+ return (
152
+ <div className="rounded border bg-background p-3 space-y-3 text-xs">
153
+ <div className="flex items-center justify-between">
154
+ <span className="font-semibold text-sm">{t('cpq.charges.pricingDetailsFor', 'Pricing Details — {name}', { name: c.chargeName })}</span>
155
+ <span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
156
+ </div>
157
+
158
+ {tiers.length > 0 && (
159
+ <div>
160
+ <div className="font-medium text-muted-foreground mb-1">{t('cpq.charges.tierBreakdown', 'Tier Breakdown')}</div>
161
+ <table className="w-full">
162
+ <thead>
163
+ <tr className="text-muted-foreground">
164
+ <th className="text-left py-0.5 font-medium">{t('cpq.charges.tier', 'Tier')}</th>
165
+ <th className="text-right py-0.5 font-medium">{t('cpq.charges.range', 'Range')}</th>
166
+ <th className="text-right py-0.5 font-medium">{t('cpq.charges.pricePerUnit', 'Price/Unit')}</th>
167
+ <th className="text-right py-0.5 font-medium">{t('cpq.charges.qty', 'Qty')}</th>
168
+ <th className="text-right py-0.5 font-medium">{t('cpq.charges.subtotal', 'Subtotal')}</th>
169
+ </tr>
170
+ </thead>
171
+ <tbody>
172
+ {tiers.map((tier) => (
173
+ <tr key={tier.tier} className="border-t border-border/30">
174
+ <td className="py-0.5">{tier.tier}</td>
175
+ <td className="py-0.5 text-right font-mono">{tier.from}–{tier.to}</td>
176
+ <td className="py-0.5 text-right font-mono">{formatMoney(tier.pricePerUnit, currency)}</td>
177
+ <td className="py-0.5 text-right font-mono">{tier.quantity}</td>
178
+ <td className="py-0.5 text-right font-mono">{formatMoney(tier.subtotal, currency)}</td>
179
+ </tr>
180
+ ))}
181
+ <tr className="border-t font-medium">
182
+ <td colSpan={4} className="py-0.5 text-right">{t('cpq.charges.tieredTotal', 'Tiered Total')}</td>
183
+ <td className="py-0.5 text-right font-mono">
184
+ {formatMoney(tiers.reduce((sum, tier) => sum + tier.subtotal, 0), currency)}
185
+ </td>
186
+ </tr>
187
+ </tbody>
188
+ </table>
189
+ </div>
190
+ )}
191
+
192
+ <div>
193
+ <div className="font-medium text-muted-foreground mb-1">{t('cpq.priceAdjustments.waterfall.title', 'Price waterfall')}</div>
194
+ <PriceWaterfall charge={c} currency={currency} reasonLabels={reasonLabels} renderManualActions={renderManualActions} />
195
+ </div>
196
+
197
+ <div className="flex items-center justify-between pt-1 border-t text-sm font-semibold">
198
+ <span>{t('cpq.charges.finalTotal', 'Final Total')}</span>
199
+ <span className="font-mono">{c.totalPrice != null ? formatMoney(c.totalPrice, currency) : '—'}</span>
200
+ </div>
201
+ </div>
202
+ )
203
+ }
@@ -0,0 +1,96 @@
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 {
7
+ discountDepth,
8
+ formatDepth,
9
+ waterfallRows,
10
+ type WaterfallChargeInput,
11
+ } from '../lib/priceAdjustmentView'
12
+
13
+ function money(amount: number, currency: string): string {
14
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
15
+ }
16
+
17
+ const TYPE_FALLBACKS: Record<string, string> = {
18
+ discount_percent: '% discount',
19
+ discount_absolute: 'amount off',
20
+ surcharge_percent: '% surcharge',
21
+ surcharge_absolute: 'fixed surcharge',
22
+ price_override: 'price override',
23
+ }
24
+
25
+ /** Signed depth badge: "−20%" for a discount, "+5%" for a surcharge; text, never colour only. */
26
+ export function DepthBadge({ before, after, title }: { before: number | null | undefined; after: number | null | undefined; title?: string }) {
27
+ const label = formatDepth(discountDepth(before, after))
28
+ if (!label) return null
29
+ return <Tag variant="neutral" className="px-1.5 text-xs font-mono" title={title}>{label}</Tag>
30
+ }
31
+
32
+ export type PriceWaterfallProps = {
33
+ charge: WaterfallChargeInput & { chargeName?: string }
34
+ currency: string
35
+ /** Reason code → label; manual rows fall back to the code. */
36
+ reasonLabels?: Record<string, string>
37
+ /** Optional per-adjustment actions (edit / delete) rendered on manual rows. */
38
+ renderManualActions?: (adjustmentId: string) => React.ReactNode
39
+ }
40
+
41
+ /** Read-only list → rule → standard → manual → offer breakdown for one charge (unit prices). */
42
+ export function PriceWaterfall({ charge, currency, reasonLabels, renderManualActions }: PriceWaterfallProps) {
43
+ const t = useT()
44
+ const rows = waterfallRows(charge)
45
+ const fmtValue = (ruleType: string, value: number) =>
46
+ ruleType.endsWith('_percent') ? `${value}%` : money(value, currency)
47
+ return (
48
+ <table className="w-full text-xs" data-testid="cpq-price-waterfall">
49
+ <thead>
50
+ <tr className="text-muted-foreground">
51
+ <th className="text-left py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.step', 'Step')}</th>
52
+ <th className="text-left py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.source', 'Source')}</th>
53
+ <th className="text-right py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.value', 'Value')}</th>
54
+ <th className="text-right py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.delta', 'Delta')}</th>
55
+ <th className="text-right py-0.5 font-medium">{t('cpq.priceAdjustments.waterfall.unitPrice', 'Unit price')}</th>
56
+ {renderManualActions && <th className="w-16" />}
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ {rows.map((row, index) => {
61
+ if (row.kind === 'list' || row.kind === 'standard' || row.kind === 'offer') {
62
+ const label = row.kind === 'list'
63
+ ? t('cpq.priceAdjustments.waterfall.list', 'List price')
64
+ : row.kind === 'standard'
65
+ ? t('cpq.priceAdjustments.waterfall.standard', 'Standard price')
66
+ : t('cpq.priceAdjustments.waterfall.offer', 'Offer price')
67
+ return (
68
+ <tr key={`${row.kind}-${index}`} className={`border-t border-border/30 ${row.kind === 'offer' ? 'font-semibold' : 'font-medium'}`}>
69
+ <td className="py-0.5" colSpan={4}>{label}</td>
70
+ <td className="py-0.5 text-right font-mono">{money(row.unitPrice, currency)}</td>
71
+ {renderManualActions && <td />}
72
+ </tr>
73
+ )
74
+ }
75
+ const source = row.kind === 'rule'
76
+ ? t('cpq.priceAdjustments.waterfall.ruleSource', 'Price rule: {name}', { name: row.label })
77
+ : t('cpq.priceAdjustments.waterfall.manualSource', 'Negotiated: {reason}', {
78
+ reason: reasonLabels?.[row.reasonCode ?? ''] ?? row.reasonCode ?? row.label,
79
+ })
80
+ return (
81
+ <tr key={`${row.kind}-${index}`} className="border-t border-border/30">
82
+ <td className="py-0.5 pl-3 text-muted-foreground">{t(`cpq.priceAdjustments.types.${row.ruleType}`, TYPE_FALLBACKS[row.ruleType] ?? row.ruleType)}</td>
83
+ <td className="py-0.5">{source}</td>
84
+ <td className="py-0.5 text-right font-mono">{fmtValue(row.ruleType, row.value)}</td>
85
+ <td className="py-0.5 text-right font-mono">{row.delta > 0 ? '+' : ''}{money(row.delta, currency)}</td>
86
+ <td className="py-0.5 text-right font-mono">{money(row.unitPriceAfter, currency)}</td>
87
+ {renderManualActions && (
88
+ <td className="py-0.5 text-right">{row.kind === 'manual' && row.adjustmentId ? renderManualActions(row.adjustmentId) : null}</td>
89
+ )}
90
+ </tr>
91
+ )
92
+ })}
93
+ </tbody>
94
+ </table>
95
+ )
96
+ }
@@ -0,0 +1,128 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { formatDepth } from '../lib/priceAdjustmentView'
6
+ import { chargeTypeDepths, formatChangePct, type ArcRecurringDelta } from '../lib/quoteSummaryView'
7
+ import { formatMoney } from '../lib/formatMoney'
8
+
9
+ /** Pricing summary fields shared by quote and order results. Waterfall fields are absent on legacy snapshots. */
10
+ export type PricingSummaryView = {
11
+ nrcTotal: number
12
+ mrcTotal: number
13
+ listNrcTotal?: number
14
+ listMrcTotal?: number
15
+ dealValue?: { list: number; standard: number; offer: number }
16
+ discountDepth?: { total: number | null } | null
17
+ usageTotalEstimated?: number | null
18
+ usageCharges?: Array<{ chargeName: string; unitPrice: number }>
19
+ }
20
+
21
+ export function SummaryFigure({
22
+ label,
23
+ value,
24
+ note,
25
+ title,
26
+ size = 'lg',
27
+ valueClassName,
28
+ testId,
29
+ }: {
30
+ label: string
31
+ value: string
32
+ note?: string | null
33
+ title?: string
34
+ size?: 'lg' | 'sm'
35
+ valueClassName?: string
36
+ testId?: string
37
+ }) {
38
+ return (
39
+ <div data-testid={testId}>
40
+ <p className="text-xs text-muted-foreground" title={title}>{label}</p>
41
+ <p className={`${size === 'lg' ? 'text-lg' : 'text-sm'} font-semibold font-mono ${valueClassName ?? ''}`}>
42
+ {value}
43
+ {note && <span className="ml-1.5 text-xs font-normal text-muted-foreground">({note})</span>}
44
+ </p>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ /**
50
+ * NRC / MRC / deal value with discount depth in brackets, plus the ARC
51
+ * current → new MRC row on Amend / Renew documents. Shared by the quote and
52
+ * order detail pages so both read the same.
53
+ */
54
+ export function PricingSummaryCard({ summary, currency, arcDelta }: {
55
+ summary: PricingSummaryView
56
+ currency: string
57
+ arcDelta?: ArcRecurringDelta | null
58
+ }) {
59
+ const t = useT()
60
+ const typeDepths = chargeTypeDepths(summary)
61
+ const usageCharges = summary.usageCharges ?? []
62
+ return (
63
+ <div className="rounded-lg border bg-card p-4" data-testid="cpq-pricing-summary">
64
+ <h3 className="text-sm font-medium mb-3">{t('cpq.quotes.summary.title', 'Pricing Summary')}</h3>
65
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
66
+ <SummaryFigure
67
+ label={t('cpq.quotes.summary.nrc', 'Non-Recurring (NRC)')}
68
+ value={formatMoney(summary.nrcTotal, currency)}
69
+ note={formatDepth(typeDepths.nrc)}
70
+ testId="cpq-summary-nrc"
71
+ />
72
+ <SummaryFigure
73
+ label={t('cpq.quotes.summary.mrc', 'Monthly Recurring (MRC)')}
74
+ value={formatMoney(summary.mrcTotal, currency)}
75
+ note={formatDepth(typeDepths.mrc)}
76
+ testId="cpq-summary-mrc"
77
+ />
78
+ {summary.dealValue && (
79
+ <SummaryFigure
80
+ label={t('cpq.priceAdjustments.summary.dealValue', 'Deal value')}
81
+ title={t('cpq.priceAdjustments.summary.hint', 'Deal value counts recurring charges over the term plus one-time charges; existing subscription items on change quotes are excluded.')}
82
+ value={formatMoney(summary.dealValue.offer, currency)}
83
+ note={formatDepth(summary.discountDepth?.total ?? null)}
84
+ testId="cpq-deal-value"
85
+ />
86
+ )}
87
+ {summary.usageTotalEstimated != null && (
88
+ <SummaryFigure
89
+ label={t('cpq.quotes.summary.usageEstimated', 'Usage (est.)')}
90
+ value={formatMoney(summary.usageTotalEstimated, currency)}
91
+ />
92
+ )}
93
+ {usageCharges.length > 0 && (
94
+ <div>
95
+ <p className="text-xs text-muted-foreground">{t('cpq.quotes.summary.usageCharges', 'Usage Charges')}</p>
96
+ {usageCharges.map((uc, i) => (
97
+ <p key={i} className="text-xs mt-0.5">{t('cpq.quotes.summary.usageChargeLine', '{name}: {price}/unit', { name: uc.chargeName, price: formatMoney(uc.unitPrice, currency) })}</p>
98
+ ))}
99
+ </div>
100
+ )}
101
+ </div>
102
+ {arcDelta && (
103
+ <div className="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-4 border-t pt-3" data-testid="cpq-arc-delta">
104
+ <SummaryFigure
105
+ size="sm"
106
+ testId="cpq-arc-current-mrc"
107
+ label={t('cpq.quotes.summary.arc.currentMrc', 'Current MRC')}
108
+ value={formatMoney(arcDelta.currentMrc, currency)}
109
+ />
110
+ <SummaryFigure
111
+ size="sm"
112
+ testId="cpq-arc-new-mrc"
113
+ label={t('cpq.quotes.summary.arc.newMrc', 'New MRC')}
114
+ value={formatMoney(arcDelta.newMrc, currency)}
115
+ />
116
+ <SummaryFigure
117
+ size="sm"
118
+ testId="cpq-arc-mrc-change"
119
+ label={t('cpq.quotes.summary.arc.mrcChange', 'MRC change')}
120
+ value={`${arcDelta.delta > 0 ? '+' : ''}${formatMoney(arcDelta.delta, currency)}`}
121
+ valueClassName={arcDelta.delta > 0 ? 'text-status-success-text' : arcDelta.delta < 0 ? 'text-destructive' : undefined}
122
+ note={formatChangePct(arcDelta.deltaPct)}
123
+ />
124
+ </div>
125
+ )}
126
+ </div>
127
+ )
128
+ }
@@ -2,6 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import { ChevronRight } from 'lucide-react'
4
4
  import { Tag } from '@open-mercato/ui/primitives/tag'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
6
  import type { TagMap } from '@open-mercato/ui/primitives/tag'
6
7
  import { formatStatusLabel } from './statusMaps'
7
8
 
@@ -30,7 +31,7 @@ export function StatusPath({
30
31
  onTransition,
31
32
  allowedTransitions,
32
33
  disabled,
33
- ariaLabel = 'Status path',
34
+ ariaLabel,
34
35
  }: {
35
36
  current: string
36
37
  /** Happy-path statuses in order. */
@@ -39,15 +40,17 @@ export function StatusPath({
39
40
  terminals?: string[]
40
41
  /** Status → Tag variant. Missing entries fall back to "neutral". */
41
42
  statusMap: Record<string, TagVariant>
42
- /** Optional human labels (overrides `formatStatusLabel`). */
43
+ /** Optional human labels, already translated by the caller (overrides `formatStatusLabel`). */
43
44
  labels?: Record<string, string>
44
45
  /** Click handler. Omit for read-only path. */
45
46
  onTransition?: (target: string) => void
46
47
  /** If set, only these targets are clickable. Else any non-current. */
47
48
  allowedTransitions?: string[]
48
49
  disabled?: boolean
50
+ /** Translated accessible name; defaults to "Status path". */
49
51
  ariaLabel?: string
50
52
  }) {
53
+ const t = useT()
51
54
  // Build the displayed sequence. Aliases can swap into the happy
52
55
  // path slot (e.g. quote's `pre_approved` shown as `approved`). We
53
56
  // don't handle aliasing here — consumers can swap entries in
@@ -61,7 +64,7 @@ export function StatusPath({
61
64
  const interactiveMode = typeof onTransition === 'function'
62
65
 
63
66
  return (
64
- <nav aria-label={ariaLabel} className="flex items-center gap-1.5 flex-wrap text-xs">
67
+ <nav aria-label={ariaLabel ?? t('cpq.statusPath.ariaLabel', 'Status path')} className="flex items-center gap-1.5 flex-wrap text-xs">
65
68
  {sequence.map((status, idx) => {
66
69
  const reached = currentIdx >= 0 && idx <= currentIdx
67
70
  const isCurrent = status === current
@@ -91,7 +94,7 @@ export function StatusPath({
91
94
  type="button"
92
95
  onClick={() => onTransition!(status)}
93
96
  aria-current={isCurrent ? 'step' : undefined}
94
- title={`Transition to ${label}`}
97
+ title={t('cpq.statusPath.transitionTo', 'Transition to {status}', { status: label })}
95
98
  className={wrapperClass}
96
99
  >
97
100
  <Tag variant={variant} dot={isCurrent} className={tagClass}>