@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
@@ -3,15 +3,9 @@ import * as React from 'react'
3
3
  import { useRouter, useParams } from 'next/navigation'
4
4
  import { Button } from '@open-mercato/ui/primitives/button'
5
5
  import { Tag } from '@open-mercato/ui/primitives/tag'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
7
  import { NumberInput } from '../../../../components/NumberInput'
7
-
8
- const RULE_TYPE_LABELS: Record<string, string> = {
9
- discount_percent: 'Discount %',
10
- discount_absolute: 'Discount $',
11
- surcharge_percent: 'Surcharge %',
12
- surcharge_absolute: 'Surcharge $',
13
- price_override: 'Override',
14
- }
8
+ import { buildRuleTypeLabels } from '../PriceRuleForm'
15
9
 
16
10
  type PriceRule = {
17
11
  id: string
@@ -66,6 +60,8 @@ function ruleToForm(rule: PriceRule): FormData {
66
60
  }
67
61
 
68
62
  export default function PriceRuleDetailPage(props: { params?: { id?: string } }) {
63
+ const t = useT()
64
+ const ruleTypeLabels = React.useMemo(() => buildRuleTypeLabels(t), [t])
69
65
  const router = useRouter()
70
66
  const urlParams = useParams()
71
67
  const ruleId = (props.params?.id ?? urlParams?.id) as string
@@ -102,7 +98,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
102
98
  setError(null)
103
99
  fetch(`/api/cpq/price-rules?id=${ruleId}`)
104
100
  .then(async (r) => {
105
- if (!r.ok) throw new Error('Not found')
101
+ if (!r.ok) throw new Error(t('cpq.priceRules.detail.notFound', 'Not found'))
106
102
  return r.json()
107
103
  })
108
104
  .then((data: PriceRule) => {
@@ -110,7 +106,7 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
110
106
  })
111
107
  .catch((err) => setError(err.message))
112
108
  .finally(() => setLoading(false))
113
- }, [ruleId])
109
+ }, [ruleId, t])
114
110
 
115
111
  React.useEffect(() => { loadRule() }, [loadRule])
116
112
 
@@ -164,14 +160,14 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
164
160
  })
165
161
  if (!res.ok) {
166
162
  const err = await res.json()
167
- throw new Error(err.error ?? 'Failed to update')
163
+ throw new Error(err.error ?? t('cpq.priceRules.errors.updateFailed', 'Failed to update'))
168
164
  }
169
165
  const updated = await res.json()
170
166
  setRule(updated)
171
167
  setEditing(false)
172
168
  setForm(null)
173
169
  } catch (err: unknown) {
174
- setSaveError(err instanceof Error ? err.message : 'Unknown error')
170
+ setSaveError(err instanceof Error ? err.message : t('cpq.priceRules.errors.unknown', 'Unknown error'))
175
171
  } finally {
176
172
  setSaving(false)
177
173
  }
@@ -205,18 +201,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
205
201
  }
206
202
 
207
203
  if (loading) {
208
- return <div className="text-sm text-muted-foreground py-12 text-center">Loading...</div>
204
+ return <div className="text-sm text-muted-foreground py-12 text-center">{t('cpq.common.loading', 'Loading...')}</div>
209
205
  }
210
206
 
211
207
  if (error || !rule) {
212
208
  return (
213
209
  <div className="space-y-4 py-12 text-center">
214
- <div className="text-sm text-destructive">{error ?? 'Rule not found'}</div>
210
+ <div className="text-sm text-destructive">{error ?? t('cpq.priceRules.detail.ruleNotFound', 'Rule not found')}</div>
215
211
  <button
216
212
  onClick={() => router.push('/backend/cpq/price-rules')}
217
213
  className="text-sm text-primary hover:underline"
218
214
  >
219
- &larr; Back to Price Rules
215
+ ← {t('cpq.priceRules.detail.backToList', 'Back to Price Rules')}
220
216
  </button>
221
217
  </div>
222
218
  )
@@ -224,6 +220,8 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
224
220
 
225
221
  const cond = rule.applicabilityCondition as { attribute?: string; operator?: string; value?: string } | null
226
222
  const offering = rule.productOfferingId ? offeringLookup.get(rule.productOfferingId) : null
223
+ // `{name}` is left unresolved so the rule name can be rendered in <strong>.
224
+ const deleteConfirmParts = t('cpq.priceRules.detail.deleteConfirm', 'Are you sure you want to delete {name}?').split('{name}')
227
225
 
228
226
  return (
229
227
  <div className="space-y-6">
@@ -234,30 +232,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
234
232
  onClick={() => router.push('/backend/cpq/price-rules')}
235
233
  className="text-sm text-muted-foreground hover:text-foreground transition-colors"
236
234
  >
237
- &larr; Back
235
+ ← {t('cpq.common.back', 'Back')}
238
236
  </button>
239
237
  <h1 className="text-2xl font-bold">{rule.name}</h1>
240
238
  <Tag variant={rule.isActive ? 'success' : 'neutral'} dot>
241
- {rule.isActive ? 'Active' : 'Inactive'}
239
+ {rule.isActive ? t('cpq.common.active', 'Active') : t('cpq.common.inactive', 'Inactive')}
242
240
  </Tag>
243
241
  </div>
244
242
  <div className="flex items-center gap-2">
245
243
  <Button type="button" variant="outline" onClick={handleToggleActive}>
246
- {rule.isActive ? 'Deactivate' : 'Activate'}
244
+ {rule.isActive
245
+ ? t('cpq.priceRules.detail.deactivate', 'Deactivate')
246
+ : t('cpq.priceRules.detail.activate', 'Activate')}
247
247
  </Button>
248
248
  {!editing && (
249
249
  <button
250
250
  onClick={startEdit}
251
251
  className="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
252
252
  >
253
- Edit
253
+ {t('cpq.common.edit', 'Edit')}
254
254
  </button>
255
255
  )}
256
256
  <button
257
257
  onClick={() => setShowDeleteConfirm(true)}
258
258
  className="rounded-md border border-destructive/30 px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors"
259
259
  >
260
- Delete
260
+ {t('cpq.common.delete', 'Delete')}
261
261
  </button>
262
262
  </div>
263
263
  </div>
@@ -265,20 +265,26 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
265
265
  {/* Delete confirmation */}
266
266
  {showDeleteConfirm && (
267
267
  <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 flex items-center justify-between">
268
- <span className="text-sm">Are you sure you want to delete <strong>{rule.name}</strong>?</span>
268
+ <span className="text-sm">
269
+ {deleteConfirmParts[0]}
270
+ <strong>{rule.name}</strong>
271
+ {deleteConfirmParts[1]}
272
+ </span>
269
273
  <div className="flex gap-2">
270
274
  <button
271
275
  onClick={handleDelete}
272
276
  disabled={deleting}
273
277
  className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50"
274
278
  >
275
- {deleting ? 'Deleting...' : 'Yes, Delete'}
279
+ {deleting
280
+ ? t('cpq.priceRules.detail.deleting', 'Deleting...')
281
+ : t('cpq.priceRules.detail.confirmDelete', 'Yes, Delete')}
276
282
  </button>
277
283
  <button
278
284
  onClick={() => setShowDeleteConfirm(false)}
279
285
  className="rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted/50"
280
286
  >
281
- Cancel
287
+ {t('cpq.common.cancel', 'Cancel')}
282
288
  </button>
283
289
  </div>
284
290
  </div>
@@ -287,32 +293,32 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
287
293
  {/* Edit mode */}
288
294
  {editing && form ? (
289
295
  <div className="rounded-lg border bg-card p-6 space-y-4">
290
- <h2 className="text-lg font-semibold">Edit Rule</h2>
296
+ <h2 className="text-lg font-semibold">{t('cpq.priceRules.form.editTitle', 'Edit Rule')}</h2>
291
297
  {saveError && <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{saveError}</div>}
292
298
  <div className="grid grid-cols-2 gap-4">
293
299
  <label className="space-y-1">
294
- <span className="text-sm font-medium">Code</span>
300
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.code', 'Code')}</span>
295
301
  <input className="w-full rounded-md border bg-muted/30 px-3 py-2 text-sm" value={rule.code} disabled />
296
302
  </label>
297
303
  <label className="space-y-1">
298
- <span className="text-sm font-medium">Name</span>
304
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.name', 'Name')}</span>
299
305
  <input className="w-full rounded-md border px-3 py-2 text-sm" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
300
306
  </label>
301
307
  <label className="space-y-1">
302
- <span className="text-sm font-medium">Rule Type</span>
308
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.ruleType', 'Rule Type')}</span>
303
309
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.ruleType} onChange={(e) => setForm({ ...form, ruleType: e.target.value })}>
304
- {Object.entries(RULE_TYPE_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
310
+ {Object.entries(ruleTypeLabels).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
305
311
  </select>
306
312
  </label>
307
313
  <label className="space-y-1">
308
- <span className="text-sm font-medium">Value</span>
314
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.value', 'Value')}</span>
309
315
  <NumberInput
310
316
  value={form.value === '' ? null : Number(form.value)}
311
317
  onChange={(n) => setForm({ ...form, value: n == null ? '' : String(n) })}
312
318
  />
313
319
  </label>
314
320
  <label className="space-y-1">
315
- <span className="text-sm font-medium">Sort Order</span>
321
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.sortOrder', 'Sort Order')}</span>
316
322
  <NumberInput
317
323
  integer
318
324
  value={form.sortOrder === '' ? null : Number(form.sortOrder)}
@@ -320,52 +326,52 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
320
326
  />
321
327
  </label>
322
328
  <label className="space-y-1">
323
- <span className="text-sm font-medium">Description</span>
324
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="Optional" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
329
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.description', 'Description')}</span>
330
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.descriptionPlaceholder', 'Optional')} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
325
331
  </label>
326
332
  <label className="space-y-1">
327
- <span className="text-sm font-medium">Charge Type Filter</span>
333
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.chargeTypeFilter', 'Charge Type Filter')}</span>
328
334
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.chargeTypeFilter} onChange={(e) => setForm({ ...form, chargeTypeFilter: e.target.value })}>
329
- <option value="">All charge types</option>
330
- <option value="nrc">NRC only</option>
331
- <option value="mrc">MRC only</option>
332
- <option value="usage">Usage only</option>
335
+ <option value="">{t('cpq.priceRules.form.allChargeTypes', 'All charge types')}</option>
336
+ <option value="nrc">{t('cpq.priceRules.form.nrcOnly', 'NRC only')}</option>
337
+ <option value="mrc">{t('cpq.priceRules.form.mrcOnly', 'MRC only')}</option>
338
+ <option value="usage">{t('cpq.priceRules.form.usageOnly', 'Usage only')}</option>
333
339
  </select>
334
340
  </label>
335
341
  <label className="space-y-1">
336
- <span className="text-sm font-medium">Charge Code Filter</span>
337
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="e.g. setup_fee (leave empty for all)" value={form.chargeCodeFilter} onChange={(e) => setForm({ ...form, chargeCodeFilter: e.target.value })} />
342
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.chargeCodeFilter', 'Charge Code Filter')}</span>
343
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.chargeCodeFilterPlaceholder', 'e.g. setup_fee (leave empty for all)')} value={form.chargeCodeFilter} onChange={(e) => setForm({ ...form, chargeCodeFilter: e.target.value })} />
338
344
  </label>
339
345
  <label className="space-y-1 col-span-2">
340
- <span className="text-sm font-medium">Product Offering</span>
346
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.productOffering', 'Product Offering')}</span>
341
347
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.productOfferingId} onChange={(e) => setForm({ ...form, productOfferingId: e.target.value })}>
342
- <option value="">Global (all products)</option>
348
+ <option value="">{t('cpq.priceRules.form.globalAllProducts', 'Global (all products)')}</option>
343
349
  {offerings.map((o) => <option key={o.id} value={o.id}>{o.name} ({o.code})</option>)}
344
350
  </select>
345
351
  </label>
346
352
  <div className="col-span-2 space-y-1">
347
- <span className="text-sm font-medium">Applicability Condition</span>
348
- <span className="ml-2 text-xs text-muted-foreground">(optional)</span>
353
+ <span className="text-sm font-medium">{t('cpq.priceRules.form.applicabilityCondition', 'Applicability Condition')}</span>
354
+ <span className="ml-2 text-xs text-muted-foreground">{t('cpq.priceRules.form.optionalHint', '(optional)')}</span>
349
355
  <div className="grid grid-cols-3 gap-2">
350
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="Attribute (e.g. port_size)" value={form.conditionAttribute} onChange={(e) => setForm({ ...form, conditionAttribute: e.target.value })} />
356
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionAttributePlaceholder', 'Attribute (e.g. port_size)')} value={form.conditionAttribute} onChange={(e) => setForm({ ...form, conditionAttribute: e.target.value })} />
351
357
  <select className="w-full rounded-md border px-3 py-2 text-sm" value={form.conditionOperator} onChange={(e) => setForm({ ...form, conditionOperator: e.target.value })}>
352
- <option value="eq">equals (=)</option>
353
- <option value="neq">not equals (&#8800;)</option>
358
+ <option value="eq">{t('cpq.priceRules.form.operatorEq', 'equals (=)')}</option>
359
+ <option value="neq">{t('cpq.priceRules.form.operatorNeq', 'not equals (≠)')}</option>
354
360
  </select>
355
- <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder="Value" value={form.conditionValue} onChange={(e) => setForm({ ...form, conditionValue: e.target.value })} />
361
+ <input className="w-full rounded-md border px-3 py-2 text-sm" placeholder={t('cpq.priceRules.form.conditionValuePlaceholderShort', 'Value')} value={form.conditionValue} onChange={(e) => setForm({ ...form, conditionValue: e.target.value })} />
356
362
  </div>
357
363
  </div>
358
364
  </div>
359
365
  <label className="flex items-center gap-2 text-sm">
360
366
  <input type="checkbox" checked={form.isActive} onChange={(e) => setForm({ ...form, isActive: e.target.checked })} />
361
- Active
367
+ {t('cpq.common.active', 'Active')}
362
368
  </label>
363
369
  <div className="flex gap-2">
364
370
  <button onClick={handleSave} disabled={saving} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
365
- {saving ? 'Saving...' : 'Save Changes'}
371
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.priceRules.detail.saveChanges', 'Save Changes')}
366
372
  </button>
367
373
  <button onClick={cancelEdit} className="rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted/50">
368
- Cancel
374
+ {t('cpq.common.cancel', 'Cancel')}
369
375
  </button>
370
376
  </div>
371
377
  </div>
@@ -374,29 +380,29 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
374
380
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
375
381
  {/* Main info */}
376
382
  <div className="rounded-lg border bg-card p-6 space-y-4">
377
- <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Rule Configuration</h2>
383
+ <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.ruleConfiguration', 'Rule Configuration')}</h2>
378
384
  <dl className="space-y-3">
379
385
  <div className="flex justify-between">
380
- <dt className="text-sm text-muted-foreground">Code</dt>
386
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.code', 'Code')}</dt>
381
387
  <dd className="text-sm font-mono">{rule.code}</dd>
382
388
  </div>
383
389
  <div className="flex justify-between border-t pt-3">
384
- <dt className="text-sm text-muted-foreground">Rule Type</dt>
385
- <dd className="text-sm font-medium">{RULE_TYPE_LABELS[rule.ruleType] ?? rule.ruleType}</dd>
390
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.ruleType', 'Rule Type')}</dt>
391
+ <dd className="text-sm font-medium">{ruleTypeLabels[rule.ruleType] ?? rule.ruleType}</dd>
386
392
  </div>
387
393
  <div className="flex justify-between border-t pt-3">
388
- <dt className="text-sm text-muted-foreground">Value</dt>
394
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.value', 'Value')}</dt>
389
395
  <dd className="text-sm font-mono font-medium">
390
396
  {rule.ruleType.includes('percent') ? `${rule.value}%` : rule.ruleType === 'price_override' ? `= $${rule.value}` : `$${rule.value}`}
391
397
  </dd>
392
398
  </div>
393
399
  <div className="flex justify-between border-t pt-3">
394
- <dt className="text-sm text-muted-foreground">Sort Order</dt>
400
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.sortOrder', 'Sort Order')}</dt>
395
401
  <dd className="text-sm font-mono">{rule.sortOrder}</dd>
396
402
  </div>
397
403
  {rule.description && (
398
404
  <div className="border-t pt-3">
399
- <dt className="text-sm text-muted-foreground mb-1">Description</dt>
405
+ <dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.description', 'Description')}</dt>
400
406
  <dd className="text-sm">{rule.description}</dd>
401
407
  </div>
402
408
  )}
@@ -405,10 +411,10 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
405
411
 
406
412
  {/* Scope & Filters */}
407
413
  <div className="rounded-lg border bg-card p-6 space-y-4">
408
- <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Scope &amp; Filters</h2>
414
+ <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.scopeAndFilters', 'Scope & Filters')}</h2>
409
415
  <dl className="space-y-3">
410
416
  <div className="flex justify-between">
411
- <dt className="text-sm text-muted-foreground">Scope</dt>
417
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.scope', 'Scope')}</dt>
412
418
  <dd className="text-sm">
413
419
  {offering ? (
414
420
  <span className="inline-flex items-center gap-1.5">
@@ -419,28 +425,28 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
419
425
  ) : (
420
426
  <span className="inline-flex items-center gap-1.5">
421
427
  <span className="inline-block w-2 h-2 rounded-full bg-muted-foreground" />
422
- Global (all offerings)
428
+ {t('cpq.priceRules.detail.globalAllOfferings', 'Global (all offerings)')}
423
429
  </span>
424
430
  )}
425
431
  </dd>
426
432
  </div>
427
433
  <div className="flex justify-between border-t pt-3">
428
- <dt className="text-sm text-muted-foreground">Charge Type</dt>
429
- <dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : 'All types'}</dd>
434
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeType', 'Charge Type')}</dt>
435
+ <dd className="text-sm">{rule.chargeTypeFilter ? rule.chargeTypeFilter.toUpperCase() : t('cpq.priceRules.detail.allTypes', 'All types')}</dd>
430
436
  </div>
431
437
  <div className="flex justify-between border-t pt-3">
432
- <dt className="text-sm text-muted-foreground">Charge Code</dt>
433
- <dd className="text-sm font-mono">{rule.chargeCodeFilter ?? 'All charges'}</dd>
438
+ <dt className="text-sm text-muted-foreground">{t('cpq.priceRules.detail.chargeCode', 'Charge Code')}</dt>
439
+ <dd className="text-sm font-mono">{rule.chargeCodeFilter ?? t('cpq.priceRules.detail.allCharges', 'All charges')}</dd>
434
440
  </div>
435
441
  <div className="border-t pt-3">
436
- <dt className="text-sm text-muted-foreground mb-1">Applicability Condition</dt>
442
+ <dt className="text-sm text-muted-foreground mb-1">{t('cpq.priceRules.detail.applicabilityCondition', 'Applicability Condition')}</dt>
437
443
  <dd className="text-sm">
438
444
  {cond?.attribute ? (
439
445
  <code className="rounded bg-muted px-2 py-0.5 text-xs">
440
446
  {cond.attribute} {cond.operator === 'neq' ? '≠' : '='} {cond.value ?? '""'}
441
447
  </code>
442
448
  ) : (
443
- <span className="text-muted-foreground">No condition (always applies)</span>
449
+ <span className="text-muted-foreground">{t('cpq.priceRules.detail.noCondition', 'No condition (always applies)')}</span>
444
450
  )}
445
451
  </dd>
446
452
  </div>
@@ -449,18 +455,18 @@ export default function PriceRuleDetailPage(props: { params?: { id?: string } })
449
455
 
450
456
  {/* Metadata */}
451
457
  <div className="rounded-lg border bg-card p-6 space-y-4 lg:col-span-2">
452
- <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Metadata</h2>
458
+ <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">{t('cpq.priceRules.detail.metadata', 'Metadata')}</h2>
453
459
  <dl className="flex gap-8 text-sm">
454
460
  <div>
455
- <dt className="text-muted-foreground">Created</dt>
461
+ <dt className="text-muted-foreground">{t('cpq.priceRules.detail.created', 'Created')}</dt>
456
462
  <dd>{new Date(rule.createdAt).toLocaleString()}</dd>
457
463
  </div>
458
464
  <div>
459
- <dt className="text-muted-foreground">Updated</dt>
465
+ <dt className="text-muted-foreground">{t('cpq.priceRules.detail.updated', 'Updated')}</dt>
460
466
  <dd>{new Date(rule.updatedAt).toLocaleString()}</dd>
461
467
  </div>
462
468
  <div>
463
- <dt className="text-muted-foreground">ID</dt>
469
+ <dt className="text-muted-foreground">{t('cpq.priceRules.detail.id', 'ID')}</dt>
464
470
  <dd className="font-mono text-xs">{rule.id}</dd>
465
471
  </div>
466
472
  </dl>
@@ -9,7 +9,7 @@ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar
9
9
  import { CpqListView, useCpqListData } from '../../../components/CpqListView'
10
10
  import {
11
11
  PriceRuleForm,
12
- RULE_TYPE_LABELS,
12
+ buildRuleTypeLabels,
13
13
  emptyPriceRuleForm,
14
14
  priceRuleFormToPayload,
15
15
  type PriceRule,
@@ -62,18 +62,20 @@ export default function PriceRulesPage() {
62
62
  .catch(() => {})
63
63
  }, [])
64
64
 
65
+ const ruleTypeLabels = React.useMemo(() => buildRuleTypeLabels(t), [t])
66
+
65
67
  const ruleTypeOptions = React.useMemo(
66
- () => Object.entries(RULE_TYPE_LABELS).map(([value, label]) => ({ value, label })),
67
- [],
68
+ () => Object.entries(ruleTypeLabels).map(([value, label]) => ({ value, label })),
69
+ [ruleTypeLabels],
68
70
  )
69
71
 
70
72
  const chargeTypeOptions = React.useMemo(
71
73
  () => [
72
- { value: 'nrc', label: 'NRC' },
73
- { value: 'mrc', label: 'MRC' },
74
- { value: 'usage', label: 'Usage' },
74
+ { value: 'nrc', label: t('cpq.priceRules.chargeTypes.nrc', 'NRC') },
75
+ { value: 'mrc', label: t('cpq.priceRules.chargeTypes.mrc', 'MRC') },
76
+ { value: 'usage', label: t('cpq.priceRules.chargeTypes.usage', 'Usage') },
75
77
  ],
76
- [],
78
+ [t],
77
79
  )
78
80
 
79
81
  const filters = React.useMemo<FilterDef[]>(
@@ -122,16 +124,20 @@ export default function PriceRulesPage() {
122
124
  })
123
125
  if (!res.ok) {
124
126
  const err = await res.json()
125
- throw new Error(err.details ? JSON.stringify(err.details) : err.error ?? 'Failed to create')
127
+ throw new Error(
128
+ err.details
129
+ ? JSON.stringify(err.details)
130
+ : err.error ?? t('cpq.priceRules.errors.createFailed', 'Failed to create'),
131
+ )
126
132
  }
127
133
  setShowForm(false)
128
134
  data.reload()
129
135
  } catch (err: unknown) {
130
- setError(err instanceof Error ? err.message : 'Unknown error')
136
+ setError(err instanceof Error ? err.message : t('cpq.priceRules.errors.unknown', 'Unknown error'))
131
137
  } finally {
132
138
  setSaving(false)
133
139
  }
134
- }, [data, form])
140
+ }, [data, form, t])
135
141
 
136
142
  const columns = React.useMemo<ColumnDef<PriceRule>[]>(
137
143
  () => [
@@ -154,7 +160,7 @@ export default function PriceRulesPage() {
154
160
  accessorKey: 'ruleType',
155
161
  header: t('cpq.priceRules.table.type', 'Type'),
156
162
  cell: ({ row }) => (
157
- <span>{RULE_TYPE_LABELS[row.original.ruleType] ?? row.original.ruleType}</span>
163
+ <span>{ruleTypeLabels[row.original.ruleType] ?? row.original.ruleType}</span>
158
164
  ),
159
165
  },
160
166
  {
@@ -168,8 +174,9 @@ export default function PriceRulesPage() {
168
174
  cell: ({ row }) => (
169
175
  <span className="text-xs">
170
176
  {row.original.productOfferingId
171
- ? row.original.productOfferingName ?? 'Product-scoped'
172
- : 'Global'}
177
+ ? row.original.productOfferingName ??
178
+ t('cpq.priceRules.scope.productScoped', 'Product-scoped')
179
+ : t('cpq.priceRules.scope.global', 'Global')}
173
180
  </span>
174
181
  ),
175
182
  },
@@ -201,12 +208,14 @@ export default function PriceRulesPage() {
201
208
  header: t('cpq.priceRules.table.status', 'Status'),
202
209
  cell: ({ row }) => (
203
210
  <Tag variant={row.original.isActive ? 'success' : 'neutral'} dot>
204
- {row.original.isActive ? 'Active' : 'Inactive'}
211
+ {row.original.isActive
212
+ ? t('cpq.common.active', 'Active')
213
+ : t('cpq.common.inactive', 'Inactive')}
205
214
  </Tag>
206
215
  ),
207
216
  },
208
217
  ],
209
- [t],
218
+ [ruleTypeLabels, t],
210
219
  )
211
220
 
212
221
  return (
@@ -245,7 +254,7 @@ export default function PriceRulesPage() {
245
254
  }
246
255
  emptyState={
247
256
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
248
- No price rules found. Click &quot;Create Rule&quot; to add one.
257
+ {t('cpq.priceRules.empty', 'No price rules found. Click "Create Rule" to add one.')}
249
258
  </div>
250
259
  }
251
260
  />
@@ -11,6 +11,7 @@ import {
11
11
  SelectValue,
12
12
  } from '@open-mercato/ui/primitives/select'
13
13
  import { Trash2 } from 'lucide-react'
14
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
14
15
 
15
16
  // ─── Types ──────────────────────────────────────────────────────
16
17
 
@@ -54,6 +55,7 @@ export function EntriesEditor({
54
55
  initialEntries: Entry[]
55
56
  onSaved: (entries: Entry[]) => void
56
57
  }) {
58
+ const t = useT()
57
59
  const [drafts, setDrafts] = React.useState<DraftEntry[]>(() =>
58
60
  initialEntries.map((e) => ({ ...e, _state: null, _localId: e.id })),
59
61
  )
@@ -142,7 +144,7 @@ export function EntriesEditor({
142
144
  })
143
145
  if (!res.ok) {
144
146
  const body = await res.json().catch(() => ({}))
145
- return { ok: false, error: body.error ?? 'Failed to create entries' }
147
+ return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.createFailed', 'Failed to create entries') }
146
148
  }
147
149
  }
148
150
 
@@ -164,7 +166,7 @@ export function EntriesEditor({
164
166
  })
165
167
  if (!res.ok) {
166
168
  const body = await res.json().catch(() => ({}))
167
- return { ok: false, error: body.error ?? `Failed to update entry ${d.id}` }
169
+ return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.updateFailed', 'Failed to update entry {id}', { id: d.id }) }
168
170
  }
169
171
  }
170
172
 
@@ -178,13 +180,13 @@ export function EntriesEditor({
178
180
  })
179
181
  if (!res.ok) {
180
182
  const body = await res.json().catch(() => ({}))
181
- return { ok: false, error: body.error ?? `Failed to delete entry ${d.id}` }
183
+ return { ok: false, error: body.error ?? t('cpq.pricing.entries.errors.deleteFailed', 'Failed to delete entry {id}', { id: d.id }) }
182
184
  }
183
185
  }
184
186
 
185
187
  return { ok: true }
186
188
  } catch (err) {
187
- return { ok: false, error: err instanceof Error ? err.message : 'Failed to save entries' }
189
+ return { ok: false, error: err instanceof Error ? err.message : t('cpq.pricing.entries.errors.saveFailed', 'Failed to save entries') }
188
190
  } finally {
189
191
  setSaving(false)
190
192
  }
@@ -208,26 +210,26 @@ export function EntriesEditor({
208
210
  <div className="space-y-3">
209
211
  <div className="flex items-center justify-between">
210
212
  <h2 className="text-lg font-semibold">
211
- Entries ({visibleDrafts.length})
213
+ {t('cpq.pricing.entries.title', 'Entries ({count})', { count: visibleDrafts.length })}
212
214
  </h2>
213
215
  <div className="flex items-center gap-2">
214
216
  {mode === 'view' ? (
215
217
  <Button type="button" variant="outline" size="sm" onClick={() => setMode('edit')}>
216
- Edit Entries
218
+ {t('cpq.pricing.entries.edit', 'Edit Entries')}
217
219
  </Button>
218
220
  ) : (
219
221
  <>
220
222
  <Button type="button" variant="outline" size="sm" onClick={() => addRow(false)}>
221
- + Add Row
223
+ {t('cpq.pricing.entries.addRow', '+ Add Row')}
222
224
  </Button>
223
225
  <Button type="button" variant="outline" size="sm" onClick={() => addRow(true)}>
224
- + Add Tier
226
+ {t('cpq.pricing.entries.addTier', '+ Add Tier')}
225
227
  </Button>
226
228
  <Button type="button" size="sm" onClick={handleSave} disabled={saving || !dirty}>
227
- {saving ? 'Saving...' : 'Save'}
229
+ {saving ? t('cpq.common.saving', 'Saving...') : t('cpq.common.save', 'Save')}
228
230
  </Button>
229
231
  <Button type="button" variant="outline" size="sm" onClick={cancelEdit} disabled={saving}>
230
- Cancel
232
+ {t('cpq.common.cancel', 'Cancel')}
231
233
  </Button>
232
234
  </>
233
235
  )}
@@ -239,8 +241,11 @@ export function EntriesEditor({
239
241
  {visibleDrafts.length === 0 ? (
240
242
  <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
241
243
  {mode === 'edit'
242
- ? 'No entries yet — use "+ Add Row" (dimensions lookup) or "+ Add Tier" (range lookup).'
243
- : 'No entries in this pricing table.'}
244
+ ? t(
245
+ 'cpq.pricing.entries.emptyEditing',
246
+ 'No entries yet — use "+ Add Row" (dimensions lookup) or "+ Add Tier" (range lookup).',
247
+ )
248
+ : t('cpq.pricing.entries.empty', 'No entries in this pricing table.')}
244
249
  </div>
245
250
  ) : (
246
251
  <div className="rounded-lg border overflow-x-auto">
@@ -256,9 +261,9 @@ export function EntriesEditor({
256
261
  prices they qualify, so the row reads "what → how much". */}
257
262
  {hasTieredEntries && (
258
263
  <>
259
- <th className="px-3 py-3 text-left font-medium whitespace-nowrap">Tier #</th>
260
- <th className="px-3 py-3 text-left font-medium whitespace-nowrap">Range From</th>
261
- <th className="px-3 py-3 text-left font-medium whitespace-nowrap">Range To</th>
264
+ <th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.tierNumber', 'Tier #')}</th>
265
+ <th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.rangeFrom', 'Range From')}</th>
266
+ <th className="px-3 py-3 text-left font-medium whitespace-nowrap">{t('cpq.pricing.entries.rangeTo', 'Range To')}</th>
262
267
  </>
263
268
  )}
264
269
  {table.priceColumns.map((col) => (
@@ -266,8 +271,8 @@ export function EntriesEditor({
266
271
  {col.label}
267
272
  </th>
268
273
  ))}
269
- <th className="px-3 py-3 text-left font-medium">Currency</th>
270
- {mode === 'edit' && <th className="px-3 py-3 w-8" aria-label="Actions" />}
274
+ <th className="px-3 py-3 text-left font-medium">{t('cpq.pricing.entries.currency', 'Currency')}</th>
275
+ {mode === 'edit' && <th className="px-3 py-3 w-8" aria-label={t('cpq.pricing.entries.actions', 'Actions')} />}
271
276
  </tr>
272
277
  </thead>
273
278
  <tbody>
@@ -307,6 +312,7 @@ function EntryRow({
307
312
  onChange: (patch: Partial<DraftEntry>) => void
308
313
  onDelete: () => void
309
314
  }) {
315
+ const t = useT()
310
316
  const readOnly = mode === 'view'
311
317
  const rowBg = entry._state === 'new'
312
318
  ? 'bg-success/5'
@@ -421,7 +427,7 @@ function EntryRow({
421
427
  type="button"
422
428
  onClick={onDelete}
423
429
  className="text-destructive hover:bg-destructive/10 rounded p-1"
424
- aria-label="Delete row"
430
+ aria-label={t('cpq.pricing.entries.deleteRow', 'Delete row')}
425
431
  >
426
432
  <Trash2 className="h-4 w-4" />
427
433
  </button>