@dainamite/cpq 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/MIGRATION.md +74 -0
  3. package/dist/modules/cpq/acl.js +1 -0
  4. package/dist/modules/cpq/acl.js.map +2 -2
  5. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js +45 -0
  6. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js.map +7 -0
  7. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
  8. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
  9. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
  10. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
  11. package/dist/modules/cpq/api/orders/[orderId]/route.js +17 -1
  12. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
  13. package/dist/modules/cpq/api/orders/route.js +47 -2
  14. package/dist/modules/cpq/api/orders/route.js.map +2 -2
  15. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
  16. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
  17. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +42 -2
  18. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
  19. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
  20. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
  21. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
  22. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
  23. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
  24. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
  25. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
  26. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
  27. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +32 -2
  28. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
  29. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
  30. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
  31. package/dist/modules/cpq/api/quotes/price/route.js +43 -3
  32. package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
  33. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +3 -9
  34. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
  35. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
  36. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
  37. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
  38. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
  39. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
  40. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
  41. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +3 -3
  42. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
  43. package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
  44. package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
  45. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +174 -184
  46. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  47. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
  48. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
  49. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
  50. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
  51. package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
  52. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
  53. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +140 -280
  54. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  55. package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
  56. package/dist/modules/cpq/backend/cpq/orders/page.js.map +2 -2
  57. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
  58. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
  59. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +41 -36
  60. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
  61. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
  62. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
  63. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
  64. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
  65. package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
  66. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
  67. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
  68. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
  69. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
  70. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
  71. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
  72. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
  73. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
  74. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
  75. package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
  76. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
  77. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
  78. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
  79. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +208 -0
  80. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
  81. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
  82. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
  83. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
  84. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
  85. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
  86. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
  87. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
  88. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
  89. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +269 -311
  90. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
  91. package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
  92. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
  93. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
  94. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
  95. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +19 -15
  96. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
  97. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
  98. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
  99. package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
  100. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
  101. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
  102. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
  103. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
  104. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
  105. package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
  106. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
  107. package/dist/modules/cpq/ce.js +8 -0
  108. package/dist/modules/cpq/ce.js.map +2 -2
  109. package/dist/modules/cpq/cli.js +34 -1
  110. package/dist/modules/cpq/cli.js.map +2 -2
  111. package/dist/modules/cpq/commands/price-adjustments.js +107 -0
  112. package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
  113. package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
  114. package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
  115. package/dist/modules/cpq/components/ChargeBreakdownTable.js +148 -0
  116. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
  117. package/dist/modules/cpq/components/PriceWaterfall.js +68 -0
  118. package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
  119. package/dist/modules/cpq/components/PricingSummaryCard.js +112 -0
  120. package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
  121. package/dist/modules/cpq/components/StatusPath.js +5 -3
  122. package/dist/modules/cpq/components/StatusPath.js.map +2 -2
  123. package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
  124. package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
  125. package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
  126. package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
  127. package/dist/modules/cpq/data/entities.js +97 -0
  128. package/dist/modules/cpq/data/entities.js.map +2 -2
  129. package/dist/modules/cpq/data/price-adjustments.js +70 -0
  130. package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
  131. package/dist/modules/cpq/data/validators.js +4 -0
  132. package/dist/modules/cpq/data/validators.js.map +2 -2
  133. package/dist/modules/cpq/di.js +6 -0
  134. package/dist/modules/cpq/di.js.map +2 -2
  135. package/dist/modules/cpq/events.js +47 -0
  136. package/dist/modules/cpq/events.js.map +2 -2
  137. package/dist/modules/cpq/i18n/de.json +1220 -0
  138. package/dist/modules/cpq/i18n/en.json +1220 -0
  139. package/dist/modules/cpq/i18n/es.json +1220 -0
  140. package/dist/modules/cpq/i18n/pl.json +1220 -0
  141. package/dist/modules/cpq/index.js +1 -1
  142. package/dist/modules/cpq/index.js.map +2 -2
  143. package/dist/modules/cpq/lib/auditActor.js +10 -0
  144. package/dist/modules/cpq/lib/auditActor.js.map +7 -0
  145. package/dist/modules/cpq/lib/formatMoney.js +7 -0
  146. package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
  147. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
  148. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
  149. package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
  150. package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
  151. package/dist/modules/cpq/lib/quotePermissions.js +13 -0
  152. package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
  153. package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
  154. package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
  155. package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
  156. package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
  157. package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
  158. package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
  159. package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
  160. package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
  161. package/dist/modules/cpq/services/cpqOrderService.js +25 -28
  162. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  163. package/dist/modules/cpq/services/cpqPricingService.js +7 -0
  164. package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
  165. package/dist/modules/cpq/services/cpqQuotingService.js +330 -109
  166. package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
  167. package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
  168. package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
  169. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
  170. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
  171. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
  172. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
  173. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
  174. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
  175. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
  176. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
  177. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
  178. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
  179. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
  180. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
  181. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
  182. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
  183. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
  184. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
  185. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
  186. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
  187. package/dist/modules/cpq/services/types.js.map +2 -2
  188. package/dist/modules/cpq/setup.js +6 -0
  189. package/dist/modules/cpq/setup.js.map +2 -2
  190. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
  191. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
  192. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
  193. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
  194. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
  195. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
  196. package/dist/modules/cpq/widgets/injection-table.js +2 -2
  197. package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
  198. package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
  199. package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
  200. package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
  201. package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
  202. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
  203. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
  204. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
  205. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
  206. package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
  207. package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
  208. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
  209. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
  210. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
  211. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
  212. package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
  213. package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
  214. package/dist/modules/cpq/workflows/steps/index.js +12 -0
  215. package/dist/modules/cpq/workflows/steps/index.js.map +2 -2
  216. package/package.json +1 -1
  217. package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
  218. package/src/modules/cpq/__tests__/i18n/dictionaries.test.ts +62 -0
  219. package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
  220. package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -0
  221. package/src/modules/cpq/acl.ts +1 -0
  222. package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
  223. package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
  224. package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
  225. package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
  226. package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
  227. package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
  228. package/src/modules/cpq/api/orders/[orderId]/route.ts +18 -0
  229. package/src/modules/cpq/api/orders/route.ts +51 -1
  230. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
  231. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +48 -1
  232. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
  233. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
  234. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
  235. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
  236. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
  237. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +40 -1
  238. package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
  239. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
  240. package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
  241. package/src/modules/cpq/api/quotes/price/route.ts +28 -2
  242. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +3 -9
  243. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
  244. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
  245. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
  246. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +3 -3
  247. package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
  248. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
  249. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +164 -120
  250. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
  251. package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
  252. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +122 -308
  253. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
  254. package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
  255. package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
  256. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
  257. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
  258. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
  259. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
  260. package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
  261. package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
  262. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
  263. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  264. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
  265. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
  266. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
  267. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
  268. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
  269. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
  270. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
  271. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +304 -348
  272. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
  273. package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
  274. package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
  275. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
  276. package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
  277. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
  278. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
  279. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  280. package/src/modules/cpq/ce.ts +8 -0
  281. package/src/modules/cpq/cli.ts +48 -1
  282. package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
  283. package/src/modules/cpq/commands/price-adjustments.ts +90 -0
  284. package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
  285. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +203 -0
  286. package/src/modules/cpq/components/PriceWaterfall.tsx +96 -0
  287. package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
  288. package/src/modules/cpq/components/StatusPath.tsx +7 -4
  289. package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
  290. package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
  291. package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
  292. package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
  293. package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
  294. package/src/modules/cpq/data/entities.ts +83 -0
  295. package/src/modules/cpq/data/price-adjustments.ts +67 -0
  296. package/src/modules/cpq/data/validators.ts +3 -0
  297. package/src/modules/cpq/di.ts +6 -0
  298. package/src/modules/cpq/events.ts +53 -2
  299. package/src/modules/cpq/i18n/de.json +1220 -0
  300. package/src/modules/cpq/i18n/en.json +1220 -0
  301. package/src/modules/cpq/i18n/es.json +1220 -0
  302. package/src/modules/cpq/i18n/pl.json +1220 -0
  303. package/src/modules/cpq/index.ts +1 -1
  304. package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
  305. package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
  306. package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
  307. package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
  308. package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
  309. package/src/modules/cpq/lib/auditActor.ts +13 -0
  310. package/src/modules/cpq/lib/formatMoney.ts +4 -0
  311. package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
  312. package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
  313. package/src/modules/cpq/lib/quotePermissions.ts +22 -0
  314. package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
  315. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
  316. package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
  317. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
  318. package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
  319. package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
  320. package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
  321. package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
  322. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
  323. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
  324. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
  325. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
  326. package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
  327. package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
  328. package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
  329. package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
  330. package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
  331. package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
  332. package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
  333. package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
  334. package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
  335. package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
  336. package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
  337. package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
  338. package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
  339. package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
  340. package/src/modules/cpq/services/cpqOrderService.ts +22 -28
  341. package/src/modules/cpq/services/cpqPricingService.ts +8 -0
  342. package/src/modules/cpq/services/cpqQuotingService.ts +374 -125
  343. package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
  344. package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
  345. package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
  346. package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
  347. package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
  348. package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
  349. package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
  350. package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
  351. package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
  352. package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
  353. package/src/modules/cpq/services/types.ts +34 -1
  354. package/src/modules/cpq/setup.ts +7 -1
  355. package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
  356. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
  357. package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
  358. package/src/modules/cpq/widgets/injection-table.ts +8 -2
  359. package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
  360. package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
  361. package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
  362. package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
  363. package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
  364. package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
  365. package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
  366. package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
  367. package/src/modules/cpq/workflows/steps/index.ts +12 -0
  368. package/src/modules/cpq/workflows/types.ts +4 -0
@@ -1,17 +1,31 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import React, { useState, useEffect, useCallback } from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
4
5
  function fmt(n, currency = "USD") {
5
6
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
6
7
  }
8
+ function chargeTypeLabel(t, chargeType) {
9
+ switch (chargeType) {
10
+ case "nrc":
11
+ return t("cpq.lines.chargeType.nrc", "nrc");
12
+ case "mrc":
13
+ return t("cpq.lines.chargeType.mrc", "mrc");
14
+ case "usage":
15
+ return t("cpq.lines.chargeType.usage", "usage");
16
+ default:
17
+ return chargeType;
18
+ }
19
+ }
7
20
  function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
21
+ const t = useT();
8
22
  const [quote, setQuote] = useState(null);
9
23
  const [loading, setLoading] = useState(true);
10
24
  const [submitting, setSubmitting] = useState(false);
11
25
  const showChargeBreakdown = config.showChargeBreakdown !== false;
12
26
  const showAdjustments = config.showAdjustments !== false;
13
27
  const submitAction = config.submitAction ?? "save";
14
- const submitLabel = config.submitLabel ?? "Complete";
28
+ const submitLabel = config.submitLabel ?? t("cpq.wizard.steps.review.complete", "Complete");
15
29
  const loadQuote = useCallback(async () => {
16
30
  if (!quoteId) {
17
31
  setLoading(false);
@@ -54,33 +68,33 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
54
68
  }
55
69
  }, [quoteId, submitAction, quote, onComplete]);
56
70
  if (loading) {
57
- return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading quote summary..." });
71
+ return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.steps.review.loading", "Loading quote summary...") });
58
72
  }
59
73
  if (!quote) {
60
- return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: "No quote data available. Please go back and configure items." });
74
+ return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: t("cpq.wizard.steps.review.noData", "No quote data available. Please go back and configure items.") });
61
75
  }
62
76
  const { pricingSummary, lines } = quote;
63
77
  return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
64
78
  /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 space-y-3", children: [
65
- /* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: "Quote Summary" }),
79
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.review.summaryTitle", "Quote Summary") }),
66
80
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
67
81
  /* @__PURE__ */ jsxs("div", { children: [
68
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "NRC Total" }),
82
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.nrcTotal", "NRC Total") }),
69
83
  /* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode) })
70
84
  ] }),
71
85
  /* @__PURE__ */ jsxs("div", { children: [
72
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "MRC Total" }),
86
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.mrcTotal", "MRC Total") }),
73
87
  /* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode) })
74
88
  ] }),
75
89
  pricingSummary.discountTotal > 0 && /* @__PURE__ */ jsxs("div", { children: [
76
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Discounts" }),
90
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.discounts", "Discounts") }),
77
91
  /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
78
92
  "-",
79
93
  fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)
80
94
  ] })
81
95
  ] }),
82
96
  pricingSummary.surchargeTotal > 0 && /* @__PURE__ */ jsxs("div", { children: [
83
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Surcharges" }),
97
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.surcharges", "Surcharges") }),
84
98
  /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
85
99
  "+",
86
100
  fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)
@@ -89,36 +103,24 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
89
103
  ] })
90
104
  ] }),
91
105
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
92
- /* @__PURE__ */ jsxs("h4", { className: "text-sm font-medium", children: [
93
- "Items (",
94
- lines.length,
95
- ")"
96
- ] }),
106
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.review.items", "Items ({count})", { count: lines.length }) }),
97
107
  /* @__PURE__ */ jsx("div", { className: "space-y-2", children: lines.map((line) => /* @__PURE__ */ jsxs("div", { className: "rounded-lg border p-3 space-y-2", children: [
98
108
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
99
109
  /* @__PURE__ */ jsx("strong", { className: "text-sm", children: line.offeringName }),
100
- /* @__PURE__ */ jsxs("span", { className: "text-sm", children: [
101
- fmt(line.nrcTotal, pricingSummary.currencyCode),
102
- " NRC / ",
103
- fmt(line.mrcTotal, pricingSummary.currencyCode),
104
- " MRC"
105
- ] })
106
- ] }),
107
- line.quantity > 1 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
108
- "Quantity: ",
109
- line.quantity
110
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: t("cpq.wizard.linePricing", "{nrc} NRC / {mrc} MRC", { nrc: fmt(line.nrcTotal, pricingSummary.currencyCode), mrc: fmt(line.mrcTotal, pricingSummary.currencyCode) }) })
110
111
  ] }),
112
+ line.quantity > 1 && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.quantity", "Quantity: {quantity}", { quantity: line.quantity }) }),
111
113
  showChargeBreakdown && line.charges?.length > 0 && /* @__PURE__ */ jsx("div", { className: "rounded border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
112
114
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b bg-muted/30", children: [
113
- /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: "Charge" }),
114
- /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: "Type" }),
115
- /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-right font-medium", children: "Price" })
115
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: t("cpq.wizard.steps.review.charges.charge", "Charge") }),
116
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: t("cpq.wizard.steps.review.charges.type", "Type") }),
117
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-right font-medium", children: t("cpq.wizard.steps.review.charges.price", "Price") })
116
118
  ] }) }),
117
119
  /* @__PURE__ */ jsx("tbody", { children: line.charges.map((charge) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
118
120
  /* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0", children: [
119
121
  /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5", children: charge.chargeName }),
120
- /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 uppercase", children: charge.chargeType }),
121
- /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right", children: charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : `${fmt(charge.unitPrice, charge.currencyCode)}/unit` })
122
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 uppercase", children: chargeTypeLabel(t, charge.chargeType) }),
123
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right", children: charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : t("cpq.lines.perUnit", "{price}/unit", { price: fmt(charge.unitPrice, charge.currencyCode) }) })
122
124
  ] }),
123
125
  showAdjustments && charge.adjustments?.map((adj) => /* @__PURE__ */ jsxs("tr", { className: "text-[11px] text-muted-foreground", children: [
124
126
  /* @__PURE__ */ jsx("td", { colSpan: 2, className: "px-3 py-0.5 pl-6", children: adj.ruleName }),
@@ -132,7 +134,7 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
132
134
  ] }, line.lineId)) })
133
135
  ] }),
134
136
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
135
- completedSteps.length > 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onBack, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: "Back" }),
137
+ completedSteps.length > 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onBack, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: t("cpq.common.back", "Back") }),
136
138
  /* @__PURE__ */ jsx(
137
139
  "button",
138
140
  {
@@ -140,7 +142,7 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
140
142
  onClick: handleSubmit,
141
143
  disabled: submitting,
142
144
  className: "inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
143
- children: submitting ? "Submitting..." : submitLabel
145
+ children: submitting ? t("cpq.wizard.steps.review.submitting", "Submitting...") : submitLabel
144
146
  }
145
147
  )
146
148
  ] })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/cpq/workflows/steps/ReviewStep.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport type { WizardStepProps } from '../types'\n\ntype ResolvedCharge = {\n chargeCode: string\n chargeName: string\n chargeType: string\n unitPrice: number\n totalPrice: number | null\n currencyCode: string\n adjustments?: Array<{\n ruleCode: string\n ruleName: string\n ruleType: string\n delta: number\n }> | null\n}\n\ntype QuoteLine = {\n lineId: string\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n charges: ResolvedCharge[]\n isConfigured: boolean\n}\n\ntype QuoteData = {\n id: string\n quoteNumber: string\n cpqStatus: string\n currencyCode: string\n pricingSummary: {\n nrcTotal: number\n mrcTotal: number\n discountTotal: number\n surchargeTotal: number\n currencyCode: string\n }\n lines: QuoteLine[]\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nexport function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }: WizardStepProps) {\n const [quote, setQuote] = useState<QuoteData | null>(null)\n const [loading, setLoading] = useState(true)\n const [submitting, setSubmitting] = useState(false)\n\n const showChargeBreakdown = config.showChargeBreakdown !== false\n const showAdjustments = config.showAdjustments !== false\n const submitAction = (config.submitAction as string) ?? 'save'\n const submitLabel = (config.submitLabel as string) ?? 'Complete'\n\n const loadQuote = useCallback(async () => {\n if (!quoteId) { setLoading(false); return }\n setLoading(true)\n try {\n const res = await fetch(`/api/cpq/quotes/${quoteId}`)\n if (res.ok) {\n setQuote(await res.json())\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId])\n\n useEffect(() => { loadQuote() }, [loadQuote])\n\n const handleSubmit = useCallback(async () => {\n setSubmitting(true)\n try {\n if (quoteId && submitAction !== 'none') {\n const targetStatus =\n submitAction === 'submit_for_approval' ? 'in_approval'\n : submitAction === 'send_to_customer' ? 'with_customer'\n : undefined\n\n if (targetStatus) {\n await fetch(`/api/cpq/quotes/${quoteId}/status`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ targetStatus }),\n })\n }\n }\n\n onComplete({\n quoteId,\n action: submitAction,\n nrcTotal: quote?.pricingSummary.nrcTotal ?? 0,\n mrcTotal: quote?.pricingSummary.mrcTotal ?? 0,\n })\n } finally {\n setSubmitting(false)\n }\n }, [quoteId, submitAction, quote, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">Loading quote summary...</div>\n }\n\n if (!quote) {\n return (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n No quote data available. Please go back and configure items.\n </div>\n )\n }\n\n const { pricingSummary, lines } = quote\n\n return (\n <div className=\"space-y-5\">\n {/* Quote summary */}\n <div className=\"rounded-lg border bg-muted/30 p-4 space-y-3\">\n <h4 className=\"text-sm font-medium\">Quote Summary</h4>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <span className=\"text-xs text-muted-foreground\">NRC Total</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n <div>\n <span className=\"text-xs text-muted-foreground\">MRC Total</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n {pricingSummary.discountTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">Discounts</span>\n <p className=\"text-sm\">-{fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n {pricingSummary.surchargeTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">Surcharges</span>\n <p className=\"text-sm\">+{fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n </div>\n </div>\n\n {/* Line items */}\n <div className=\"space-y-3\">\n <h4 className=\"text-sm font-medium\">Items ({lines.length})</h4>\n <div className=\"space-y-2\">\n {lines.map((line) => (\n <div key={line.lineId} className=\"rounded-lg border p-3 space-y-2\">\n <div className=\"flex items-center justify-between\">\n <strong className=\"text-sm\">{line.offeringName}</strong>\n <span className=\"text-sm\">\n {fmt(line.nrcTotal, pricingSummary.currencyCode)} NRC / {fmt(line.mrcTotal, pricingSummary.currencyCode)} MRC\n </span>\n </div>\n {line.quantity > 1 && (\n <span className=\"text-xs text-muted-foreground\">Quantity: {line.quantity}</span>\n )}\n\n {showChargeBreakdown && line.charges?.length > 0 && (\n <div className=\"rounded border\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"border-b bg-muted/30\">\n <th className=\"px-3 py-1.5 text-left font-medium\">Charge</th>\n <th className=\"px-3 py-1.5 text-left font-medium\">Type</th>\n <th className=\"px-3 py-1.5 text-right font-medium\">Price</th>\n </tr>\n </thead>\n <tbody>\n {line.charges.map((charge) => (\n <React.Fragment key={charge.chargeCode}>\n <tr className=\"border-b last:border-0\">\n <td className=\"px-3 py-1.5\">{charge.chargeName}</td>\n <td className=\"px-3 py-1.5 uppercase\">{charge.chargeType}</td>\n <td className=\"px-3 py-1.5 text-right\">\n {charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : `${fmt(charge.unitPrice, charge.currencyCode)}/unit`}\n </td>\n </tr>\n {showAdjustments && charge.adjustments?.map((adj) => (\n <tr key={adj.ruleCode} className=\"text-[11px] text-muted-foreground\">\n <td colSpan={2} className=\"px-3 py-0.5 pl-6\">{adj.ruleName}</td>\n <td className=\"px-3 py-0.5 text-right\">\n {adj.delta < 0 ? '' : '+'}{fmt(adj.delta, charge.currencyCode)}\n </td>\n </tr>\n ))}\n </React.Fragment>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"flex gap-2 justify-end pt-2\">\n {completedSteps.length > 0 && (\n <button type=\"button\" onClick={onBack} className=\"inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted\">\n Back\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={submitting}\n className=\"inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n >\n {submitting ? 'Submitting...' : submitLabel}\n </button>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { WizardStepProps } from '../types'\n\ntype ResolvedCharge = {\n chargeCode: string\n chargeName: string\n chargeType: string\n unitPrice: number\n totalPrice: number | null\n currencyCode: string\n adjustments?: Array<{\n ruleCode: string\n ruleName: string\n ruleType: string\n delta: number\n }> | null\n}\n\ntype QuoteLine = {\n lineId: string\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n charges: ResolvedCharge[]\n isConfigured: boolean\n}\n\ntype QuoteData = {\n id: string\n quoteNumber: string\n cpqStatus: string\n currencyCode: string\n pricingSummary: {\n nrcTotal: number\n mrcTotal: number\n discountTotal: number\n surchargeTotal: number\n currencyCode: string\n }\n lines: QuoteLine[]\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nfunction chargeTypeLabel(t: TranslateFn, chargeType: string): string {\n switch (chargeType) {\n case 'nrc':\n return t('cpq.lines.chargeType.nrc', 'nrc')\n case 'mrc':\n return t('cpq.lines.chargeType.mrc', 'mrc')\n case 'usage':\n return t('cpq.lines.chargeType.usage', 'usage')\n default:\n return chargeType\n }\n}\n\nexport function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }: WizardStepProps) {\n const t = useT()\n const [quote, setQuote] = useState<QuoteData | null>(null)\n const [loading, setLoading] = useState(true)\n const [submitting, setSubmitting] = useState(false)\n\n const showChargeBreakdown = config.showChargeBreakdown !== false\n const showAdjustments = config.showAdjustments !== false\n const submitAction = (config.submitAction as string) ?? 'save'\n const submitLabel = (config.submitLabel as string) ?? t('cpq.wizard.steps.review.complete', 'Complete')\n\n const loadQuote = useCallback(async () => {\n if (!quoteId) { setLoading(false); return }\n setLoading(true)\n try {\n const res = await fetch(`/api/cpq/quotes/${quoteId}`)\n if (res.ok) {\n setQuote(await res.json())\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId])\n\n useEffect(() => { loadQuote() }, [loadQuote])\n\n const handleSubmit = useCallback(async () => {\n setSubmitting(true)\n try {\n if (quoteId && submitAction !== 'none') {\n const targetStatus =\n submitAction === 'submit_for_approval' ? 'in_approval'\n : submitAction === 'send_to_customer' ? 'with_customer'\n : undefined\n\n if (targetStatus) {\n await fetch(`/api/cpq/quotes/${quoteId}/status`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ targetStatus }),\n })\n }\n }\n\n onComplete({\n quoteId,\n action: submitAction,\n nrcTotal: quote?.pricingSummary.nrcTotal ?? 0,\n mrcTotal: quote?.pricingSummary.mrcTotal ?? 0,\n })\n } finally {\n setSubmitting(false)\n }\n }, [quoteId, submitAction, quote, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.steps.review.loading', 'Loading quote summary...')}</div>\n }\n\n if (!quote) {\n return (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizard.steps.review.noData', 'No quote data available. Please go back and configure items.')}\n </div>\n )\n }\n\n const { pricingSummary, lines } = quote\n\n return (\n <div className=\"space-y-5\">\n {/* Quote summary */}\n <div className=\"rounded-lg border bg-muted/30 p-4 space-y-3\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.review.summaryTitle', 'Quote Summary')}</h4>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.nrcTotal', 'NRC Total')}</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.mrcTotal', 'MRC Total')}</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n {pricingSummary.discountTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.discounts', 'Discounts')}</span>\n <p className=\"text-sm\">-{fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n {pricingSummary.surchargeTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.surcharges', 'Surcharges')}</span>\n <p className=\"text-sm\">+{fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n </div>\n </div>\n\n {/* Line items */}\n <div className=\"space-y-3\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.review.items', 'Items ({count})', { count: lines.length })}</h4>\n <div className=\"space-y-2\">\n {lines.map((line) => (\n <div key={line.lineId} className=\"rounded-lg border p-3 space-y-2\">\n <div className=\"flex items-center justify-between\">\n <strong className=\"text-sm\">{line.offeringName}</strong>\n <span className=\"text-sm\">\n {t('cpq.wizard.linePricing', '{nrc} NRC / {mrc} MRC', { nrc: fmt(line.nrcTotal, pricingSummary.currencyCode), mrc: fmt(line.mrcTotal, pricingSummary.currencyCode) })}\n </span>\n </div>\n {line.quantity > 1 && (\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.quantity', 'Quantity: {quantity}', { quantity: line.quantity })}</span>\n )}\n\n {showChargeBreakdown && line.charges?.length > 0 && (\n <div className=\"rounded border\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"border-b bg-muted/30\">\n <th className=\"px-3 py-1.5 text-left font-medium\">{t('cpq.wizard.steps.review.charges.charge', 'Charge')}</th>\n <th className=\"px-3 py-1.5 text-left font-medium\">{t('cpq.wizard.steps.review.charges.type', 'Type')}</th>\n <th className=\"px-3 py-1.5 text-right font-medium\">{t('cpq.wizard.steps.review.charges.price', 'Price')}</th>\n </tr>\n </thead>\n <tbody>\n {line.charges.map((charge) => (\n <React.Fragment key={charge.chargeCode}>\n <tr className=\"border-b last:border-0\">\n <td className=\"px-3 py-1.5\">{charge.chargeName}</td>\n <td className=\"px-3 py-1.5 uppercase\">{chargeTypeLabel(t, charge.chargeType)}</td>\n <td className=\"px-3 py-1.5 text-right\">\n {charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : t('cpq.lines.perUnit', '{price}/unit', { price: fmt(charge.unitPrice, charge.currencyCode) })}\n </td>\n </tr>\n {showAdjustments && charge.adjustments?.map((adj) => (\n <tr key={adj.ruleCode} className=\"text-[11px] text-muted-foreground\">\n <td colSpan={2} className=\"px-3 py-0.5 pl-6\">{adj.ruleName}</td>\n <td className=\"px-3 py-0.5 text-right\">\n {adj.delta < 0 ? '' : '+'}{fmt(adj.delta, charge.currencyCode)}\n </td>\n </tr>\n ))}\n </React.Fragment>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"flex gap-2 justify-end pt-2\">\n {completedSteps.length > 0 && (\n <button type=\"button\" onClick={onBack} className=\"inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted\">\n {t('cpq.common.back', 'Back')}\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={submitting}\n className=\"inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n >\n {submitting ? t('cpq.wizard.steps.review.submitting', 'Submitting...') : submitLabel}\n </button>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,36 +8,48 @@ import { InventorySelectStep } from "./InventorySelectStep.js";
8
8
  function registerBuiltInStepTypes() {
9
9
  registerStepType({
10
10
  type: "customer_select",
11
+ labelKey: "cpq.wizard.steps.customerSelect.label",
12
+ descriptionKey: "cpq.wizard.steps.customerSelect.description",
11
13
  label: "Customer Selection",
12
14
  description: "Search and select a customer for the quote.",
13
15
  component: CustomerSelectStep
14
16
  });
15
17
  registerStepType({
16
18
  type: "offering_select",
19
+ labelKey: "cpq.wizard.steps.offeringSelect.label",
20
+ descriptionKey: "cpq.wizard.steps.offeringSelect.description",
17
21
  label: "Offering Selection",
18
22
  description: "Browse and select a product offering from the catalog.",
19
23
  component: OfferingSelectStep
20
24
  });
21
25
  registerStepType({
22
26
  type: "product_configure",
27
+ labelKey: "cpq.wizard.steps.productConfigure.label",
28
+ descriptionKey: "cpq.wizard.steps.productConfigure.description",
23
29
  label: "Product Configuration",
24
30
  description: "Configure run-time attributes for the selected offering.",
25
31
  component: ProductConfigureStep
26
32
  });
27
33
  registerStepType({
28
34
  type: "item_list",
35
+ labelKey: "cpq.wizard.steps.itemList.label",
36
+ descriptionKey: "cpq.wizard.steps.itemList.description",
29
37
  label: "Item List",
30
38
  description: "View and manage child items on the quote.",
31
39
  component: ItemListStep
32
40
  });
33
41
  registerStepType({
34
42
  type: "review",
43
+ labelKey: "cpq.wizard.steps.review.label",
44
+ descriptionKey: "cpq.wizard.steps.review.description",
35
45
  label: "Review & Submit",
36
46
  description: "Review the quote summary and submit.",
37
47
  component: ReviewStep
38
48
  });
39
49
  registerStepType({
40
50
  type: "inventory_select",
51
+ labelKey: "cpq.wizard.steps.inventorySelect.label",
52
+ descriptionKey: "cpq.wizard.steps.inventorySelect.description",
41
53
  label: "Inventory Selection",
42
54
  description: "Select existing customer inventory items (stub \u2014 pending inventory domain).",
43
55
  component: InventorySelectStep
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/cpq/workflows/steps/index.ts"],
4
- "sourcesContent": ["import { registerStepType } from '../registry'\nimport { CustomerSelectStep } from './CustomerSelectStep'\nimport { OfferingSelectStep } from './OfferingSelectStep'\nimport { ProductConfigureStep } from './ProductConfigureStep'\nimport { ItemListStep } from './ItemListStep'\nimport { ReviewStep } from './ReviewStep'\nimport { InventorySelectStep } from './InventorySelectStep'\n\nexport function registerBuiltInStepTypes() {\n registerStepType({\n type: 'customer_select',\n label: 'Customer Selection',\n description: 'Search and select a customer for the quote.',\n component: CustomerSelectStep,\n })\n\n registerStepType({\n type: 'offering_select',\n label: 'Offering Selection',\n description: 'Browse and select a product offering from the catalog.',\n component: OfferingSelectStep,\n })\n\n registerStepType({\n type: 'product_configure',\n label: 'Product Configuration',\n description: 'Configure run-time attributes for the selected offering.',\n component: ProductConfigureStep,\n })\n\n registerStepType({\n type: 'item_list',\n label: 'Item List',\n description: 'View and manage child items on the quote.',\n component: ItemListStep,\n })\n\n registerStepType({\n type: 'review',\n label: 'Review & Submit',\n description: 'Review the quote summary and submit.',\n component: ReviewStep,\n })\n\n registerStepType({\n type: 'inventory_select',\n label: 'Inventory Selection',\n description: 'Select existing customer inventory items (stub \u2014 pending inventory domain).',\n component: InventorySelectStep,\n })\n}\n"],
5
- "mappings": "AAAA,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAE7B,SAAS,2BAA2B;AACzC,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AACH;",
4
+ "sourcesContent": ["import { registerStepType } from '../registry'\nimport { CustomerSelectStep } from './CustomerSelectStep'\nimport { OfferingSelectStep } from './OfferingSelectStep'\nimport { ProductConfigureStep } from './ProductConfigureStep'\nimport { ItemListStep } from './ItemListStep'\nimport { ReviewStep } from './ReviewStep'\nimport { InventorySelectStep } from './InventorySelectStep'\n\nexport function registerBuiltInStepTypes() {\n registerStepType({\n type: 'customer_select',\n labelKey: 'cpq.wizard.steps.customerSelect.label',\n descriptionKey: 'cpq.wizard.steps.customerSelect.description',\n label: 'Customer Selection',\n description: 'Search and select a customer for the quote.',\n component: CustomerSelectStep,\n })\n\n registerStepType({\n type: 'offering_select',\n labelKey: 'cpq.wizard.steps.offeringSelect.label',\n descriptionKey: 'cpq.wizard.steps.offeringSelect.description',\n label: 'Offering Selection',\n description: 'Browse and select a product offering from the catalog.',\n component: OfferingSelectStep,\n })\n\n registerStepType({\n type: 'product_configure',\n labelKey: 'cpq.wizard.steps.productConfigure.label',\n descriptionKey: 'cpq.wizard.steps.productConfigure.description',\n label: 'Product Configuration',\n description: 'Configure run-time attributes for the selected offering.',\n component: ProductConfigureStep,\n })\n\n registerStepType({\n type: 'item_list',\n labelKey: 'cpq.wizard.steps.itemList.label',\n descriptionKey: 'cpq.wizard.steps.itemList.description',\n label: 'Item List',\n description: 'View and manage child items on the quote.',\n component: ItemListStep,\n })\n\n registerStepType({\n type: 'review',\n labelKey: 'cpq.wizard.steps.review.label',\n descriptionKey: 'cpq.wizard.steps.review.description',\n label: 'Review & Submit',\n description: 'Review the quote summary and submit.',\n component: ReviewStep,\n })\n\n registerStepType({\n type: 'inventory_select',\n labelKey: 'cpq.wizard.steps.inventorySelect.label',\n descriptionKey: 'cpq.wizard.steps.inventorySelect.description',\n label: 'Inventory Selection',\n description: 'Select existing customer inventory items (stub \u2014 pending inventory domain).',\n component: InventorySelectStep,\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAE7B,SAAS,2BAA2B;AACzC,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AACH;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dainamite/cpq",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Dainamite CPQ — Configure, Price, Quote engine for Open Mercato (configurable products, multi-charge pricing tables, guided quoting, customer inventory, orders).",
5
5
  "type": "module",
6
6
  "main": "./dist/modules/cpq/index.js",
@@ -0,0 +1,134 @@
1
+ import { test, expect, type Page } from '@playwright/test'
2
+ import { login } from '@open-mercato/core/helpers/integration/auth'
3
+ import { getAuthToken, apiRequest } from '@open-mercato/core/helpers/integration/api'
4
+ import {
5
+ seedActiveSubscription,
6
+ seedCustomer,
7
+ deleteSubscription,
8
+ deleteCustomer,
9
+ hideFloatingOverlays,
10
+ uniq,
11
+ } from './_helpers/cpqArcFixtures'
12
+
13
+ /**
14
+ * TC-CPQ-ARC-012 — Amend: removing an existing item cancels it on the subscription
15
+ * (manual: xd-250-arc-flow.md → "Removing an existing item on Amend / Renew quotes")
16
+ *
17
+ * Deleting a line that mirrors a live subscription item must not drop the
18
+ * line — activation only applies explicit line actions, so the item would
19
+ * silently stay active. Instead a confirmation dialog explains the effect,
20
+ * the line becomes a greyed-out `remove` line, the ARC summary keeps it in
21
+ * Current MRC but not in New MRC, and Restore brings it back. After
22
+ * convert + activate, the removed item is terminated and the kept one stays.
23
+ */
24
+
25
+ type QuoteLine = { lineId: string; offeringName: string; action: string }
26
+ type SubscriptionItem = { id: string; name: string; status: string }
27
+
28
+ function lineRow(page: Page, name: string) {
29
+ return page.getByTestId('cpq-quote-line').filter({ hasText: name })
30
+ }
31
+
32
+ test.describe('TC-CPQ-ARC-012 — Amend: remove an existing item', () => {
33
+ test('confirm → remove line → MRC delta → restore → remove → activate terminates the item', async ({ page, request }) => {
34
+ const token = await getAuthToken(request, 'admin')
35
+ const customerId = await seedCustomer(request, token)
36
+ const keptName = `Kept Port ${uniq()}`
37
+ const removedName = `Removed Storage ${uniq()}`
38
+ const sub = await seedActiveSubscription(request, token, {
39
+ customerId,
40
+ name: 'Remove-item target',
41
+ items: [
42
+ { name: keptName, mrcAmount: 250 },
43
+ { name: removedName, mrcAmount: 100 },
44
+ ],
45
+ })
46
+
47
+ try {
48
+ await login(page, 'admin')
49
+
50
+ // 1. Amend from the subscription.
51
+ await page.goto(`/backend/cpq/inventory/subscriptions/${sub.id}`, { waitUntil: 'domcontentloaded' })
52
+ await hideFloatingOverlays(page)
53
+ await page.getByRole('button', { name: /^Amend$/ }).click()
54
+ await page.waitForURL(/\/backend\/cpq\/quotes\/[0-9a-f-]+$/, { timeout: 15_000 })
55
+ const quoteId = page.url().split('/').pop()!
56
+ await hideFloatingOverlays(page)
57
+
58
+ const removedRow = lineRow(page, removedName)
59
+ await expect(removedRow).toBeVisible()
60
+ await expect(removedRow).not.toHaveAttribute('data-removed', 'true')
61
+
62
+ // 2. Trash → confirmation dialog explains the item is cancelled. Cancel keeps the line.
63
+ await removedRow.getByTitle('Remove').click()
64
+ const dialog = page.getByRole('alertdialog')
65
+ await expect(dialog).toContainText(`Remove ${removedName} from the subscription?`)
66
+ await expect(dialog).toContainText('cancelled on the subscription')
67
+ await dialog.getByRole('button', { name: /^Cancel$/ }).click()
68
+ await expect(dialog).toBeHidden()
69
+ await expect(removedRow).not.toHaveAttribute('data-removed', 'true')
70
+
71
+ // 3. Confirm → the line stays as a `remove` line with a Restore action.
72
+ await removedRow.getByTitle('Remove').click()
73
+ await page.getByRole('alertdialog').getByRole('button', { name: 'Remove item' }).click()
74
+ await expect(removedRow).toHaveAttribute('data-removed', 'true')
75
+ await expect(removedRow.getByText(/^remove$/)).toBeVisible()
76
+ await expect(removedRow.getByTestId('cpq-line-restore')).toBeVisible()
77
+
78
+ // 4. Summary: removed item stays in Current MRC, drops out of New MRC.
79
+ await expect(page.getByTestId('cpq-arc-current-mrc')).toContainText('$350.00')
80
+ await expect(page.getByTestId('cpq-arc-new-mrc')).toContainText('$250.00')
81
+ await expect(page.getByTestId('cpq-arc-mrc-change')).toContainText('-$100.00')
82
+
83
+ // 5. Restore → back to a normal line; totals back to unchanged.
84
+ await removedRow.getByTestId('cpq-line-restore').click()
85
+ await expect(removedRow).not.toHaveAttribute('data-removed', 'true')
86
+ await expect(page.getByTestId('cpq-arc-new-mrc')).toContainText('$350.00')
87
+
88
+ // 6. Remove again for the activation leg.
89
+ await removedRow.getByTitle('Remove').click()
90
+ await page.getByRole('alertdialog').getByRole('button', { name: 'Remove item' }).click()
91
+ await expect(removedRow).toHaveAttribute('data-removed', 'true')
92
+
93
+ // The API agrees: one cancel line, one modify line.
94
+ const quoteRes = await apiRequest(request, 'GET', `/api/cpq/quotes/${quoteId}`, { token })
95
+ expect(quoteRes.ok()).toBeTruthy()
96
+ const quote = (await quoteRes.json()) as { cpqStatus: string; lines: QuoteLine[] }
97
+ expect(quote.lines.find((l) => l.offeringName === removedName)?.action).toBe('cancel')
98
+ expect(quote.lines.find((l) => l.offeringName === keptName)?.action).toBe('modify')
99
+
100
+ // 7. API — approval ladder (from wherever recalculation left the quote), convert, activate.
101
+ const ladder = ['ready', 'in_approval', 'approved', 'with_customer', 'accepted']
102
+ for (const targetStatus of ladder.slice(ladder.indexOf(quote.cpqStatus) + 1)) {
103
+ const res = await apiRequest(request, 'POST', `/api/cpq/quotes/${quoteId}/status`, { token, data: { targetStatus } })
104
+ expect(res.ok(), `transition to ${targetStatus} returned ${res.status()}: ${await res.text()}`).toBeTruthy()
105
+ }
106
+ const convertRes = await apiRequest(request, 'POST', '/api/cpq/orders', { token, data: { quoteId } })
107
+ expect(convertRes.ok(), `convert returned ${convertRes.status()}: ${await convertRes.text()}`).toBeTruthy()
108
+ const order = (await convertRes.json()) as { id: string }
109
+
110
+ // The order page mirrors the quote: same ARC summary, removed line marked "remove".
111
+ await page.goto(`/backend/cpq/orders/${order.id}`, { waitUntil: 'domcontentloaded' })
112
+ await hideFloatingOverlays(page)
113
+ await expect(page.getByTestId('cpq-pricing-summary')).toBeVisible()
114
+ await expect(page.getByTestId('cpq-arc-current-mrc')).toContainText('$350.00')
115
+ await expect(page.getByTestId('cpq-arc-new-mrc')).toContainText('$250.00')
116
+ const removedOrderLine = page.getByTestId('cpq-order-line').filter({ hasText: removedName })
117
+ await expect(removedOrderLine).toHaveAttribute('data-removed', 'true')
118
+ await expect(removedOrderLine.getByText(/^remove$/)).toBeVisible()
119
+ const activateRes = await apiRequest(request, 'POST', `/api/cpq/orders/${order.id}/activate`, { token })
120
+ expect(activateRes.ok(), `activate returned ${activateRes.status()}: ${await activateRes.text()}`).toBeTruthy()
121
+
122
+ // 8. The removed item is terminated; the other one is untouched.
123
+ const itemsRes = await apiRequest(request, 'GET', `/api/cpq/inventory/subscription-items?subscriptionId=${sub.id}`, { token })
124
+ expect(itemsRes.ok()).toBeTruthy()
125
+ const body = (await itemsRes.json()) as SubscriptionItem[] | { items: SubscriptionItem[] }
126
+ const items = Array.isArray(body) ? body : body.items
127
+ expect(items.find((i) => i.name === removedName)?.status).toBe('terminated')
128
+ expect(items.find((i) => i.name === keptName)?.status).toBe('active')
129
+ } finally {
130
+ await deleteSubscription(request, token, sub.id).catch(() => undefined)
131
+ await deleteCustomer(request, token, customerId).catch(() => undefined)
132
+ }
133
+ })
134
+ })
@@ -0,0 +1,62 @@
1
+ import { readdirSync, readFileSync } from 'fs'
2
+ import { join } from 'path'
3
+ import { MODULE_ROOT, collectTranslationKeys, listSourceFiles } from './sourceScan'
4
+
5
+ /**
6
+ * CPQ dictionary guardrails (spec 2026-11-15-cpq-multi-language):
7
+ * - every `i18n/<locale>.json` mirrors `en.json` key for key, keeps every
8
+ * `{placeholder}` and has no empty value;
9
+ * - every key the source uses exists in `en.json` (a missing key renders
10
+ * its English fallback in every language), and every dynamic key prefix
11
+ * has at least one entry;
12
+ * - `en.json` carries no orphaned keys.
13
+ */
14
+
15
+ const I18N_DIR = join(MODULE_ROOT, 'i18n')
16
+ const load = (locale: string) => JSON.parse(readFileSync(join(I18N_DIR, `${locale}.json`), 'utf8')) as Record<string, string>
17
+ const placeholders = (value: string) => [...value.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).sort().join(',')
18
+
19
+ const locales = readdirSync(I18N_DIR).filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, '')).sort()
20
+ const en = load('en')
21
+ const enKeys = Object.keys(en)
22
+ const translated = locales.filter((l) => l !== 'en')
23
+ // `describe.each` rejects an empty table; en vs en is a trivially-true row.
24
+ const parityRows = translated.length ? translated : ['en']
25
+ const { keys: usedKeys, prefixes } = collectTranslationKeys(listSourceFiles(MODULE_ROOT))
26
+
27
+ describe('cpq i18n dictionaries', () => {
28
+ it('ships en plus the supported translations', () => {
29
+ expect(locales).toEqual(expect.arrayContaining(['en', 'es', 'de', 'pl']))
30
+ })
31
+
32
+ it('en.json is sorted by key and has no empty values', () => {
33
+ expect(enKeys).toEqual([...enKeys].sort())
34
+ expect(enKeys.filter((k) => !en[k]?.trim())).toEqual([])
35
+ })
36
+
37
+ it('every key used in source exists in en.json', () => {
38
+ expect([...usedKeys].filter((k) => !(k in en)).sort()).toEqual([])
39
+ })
40
+
41
+ it('every dynamic key prefix has entries in en.json', () => {
42
+ expect([...prefixes].filter((p) => !enKeys.some((k) => k.startsWith(p))).sort()).toEqual([])
43
+ })
44
+
45
+ it('en.json has no orphaned keys', () => {
46
+ const orphans = enKeys.filter((k) => !usedKeys.has(k) && ![...prefixes].some((p) => k.startsWith(p)))
47
+ expect(orphans).toEqual([])
48
+ })
49
+
50
+ describe.each(parityRows)('%s.json', (locale) => {
51
+ const dict = load(locale)
52
+ it('has exactly the keys of en.json, in the same order', () => {
53
+ expect(Object.keys(dict)).toEqual(enKeys)
54
+ })
55
+ it('keeps every {placeholder} of the English value', () => {
56
+ expect(enKeys.filter((k) => typeof dict[k] === 'string' && placeholders(dict[k]) !== placeholders(en[k]))).toEqual([])
57
+ })
58
+ it('has no empty values', () => {
59
+ expect(enKeys.filter((k) => typeof dict[k] !== 'string' || !dict[k].trim())).toEqual([])
60
+ })
61
+ })
62
+ })
@@ -0,0 +1,35 @@
1
+ import { join } from 'path'
2
+ import { MODULE_ROOT, UI_DIRS, collectHardcodedStrings, listSourceFiles } from './sourceScan'
3
+
4
+ /**
5
+ * No user-facing English literal may bypass `t()` in CPQ UI code
6
+ * (spec 2026-11-15-cpq-multi-language). Data (product / charge / customer
7
+ * names) is never a literal, so it is not affected.
8
+ *
9
+ * ALLOWLIST is for literals that are intentionally not translated; every
10
+ * entry carries a reason. Keyed by `<file>:<text>` so a moved line does not
11
+ * break it.
12
+ */
13
+ const ALLOWLIST: Record<string, string> = {
14
+ // Injection-widget metadata has no `titleKey` in the framework's widget
15
+ // contract; the host renders `metadata.title`. Tracked as a framework gap.
16
+ 'widgets/injection/order-cpq-tab/widget.ts:CPQ Order': 'framework: widget metadata has no titleKey',
17
+ 'widgets/injection/customer-inventory/widget.ts:Customer Inventory': 'framework: widget metadata has no titleKey',
18
+ 'widgets/injection/quote-configurator/widget.ts:CPQ Configuration': 'framework: widget metadata has no titleKey',
19
+ 'widgets/injection/order-cpq-tab/widget.ts:View the CPQ order associated with this sales order':
20
+ 'framework: widget metadata has no descriptionKey',
21
+ 'widgets/injection/customer-inventory/widget.ts:Products and services purchased by this customer':
22
+ 'framework: widget metadata has no descriptionKey',
23
+ 'widgets/injection/quote-configurator/widget.ts:Configure, price, and manage quote offerings':
24
+ 'framework: widget metadata has no descriptionKey',
25
+ }
26
+
27
+ describe('cpq UI has no hard-coded user-facing strings', () => {
28
+ it('routes every UI literal through t()', () => {
29
+ const files = UI_DIRS.flatMap((dir) => listSourceFiles(join(MODULE_ROOT, dir)))
30
+ const offenders = collectHardcodedStrings(files)
31
+ .filter((hit) => !(`${hit.file}:${hit.text}` in ALLOWLIST))
32
+ .map((hit) => `${hit.file}:${hit.line} ${hit.text}`)
33
+ expect(offenders).toEqual([])
34
+ })
35
+ })