@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,16 +1,19 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { useRouter, useParams } from 'next/navigation'
4
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
4
5
  import { Alert } from '@open-mercato/ui/primitives/alert'
5
6
  import { Button } from '@open-mercato/ui/primitives/button'
6
7
  import { Tag } from '@open-mercato/ui/primitives/tag'
7
8
  import { FormHeader } from '@open-mercato/ui/backend/forms'
8
9
  import {
9
- formatStatusLabel,
10
10
  subscriptionStatusMap,
11
11
  type SubscriptionStatus,
12
12
  } from '../../../../../components/statusMaps'
13
13
  import { StatusPath } from '../../../../../components/StatusPath'
14
+ import { DepthBadge, PriceWaterfall } from '../../../../../components/PriceWaterfall'
15
+ import { parseStoredCharge } from '../../../../../lib/priceAdjustmentView'
16
+ import { buildInventoryStatusLabels, inventoryStatusLabel } from '../../shared'
14
17
 
15
18
  // XD-250 — ARC integration on subscription detail.
16
19
  // • header buttons (Amend / Renew / Cancel) call /from-subscription
@@ -62,13 +65,16 @@ type ChangeLogRow = {
62
65
  mergedFromSubscriptionIds: string[] | null
63
66
  }
64
67
 
65
- const CHANGE_TYPE_LABELS: Record<string, string> = {
66
- created: 'Created',
67
- amend: 'Amended',
68
- renew: 'Renewed',
69
- cancel: 'Cancelled',
70
- 'merge-result': 'Created from merging contracts',
71
- 'merge-source': 'Merged into a new contract',
68
+ function changeTypeLabel(t: TranslateFn, changeType: string): string {
69
+ switch (changeType) {
70
+ case 'created': return t('cpq.inventory.subscriptions.detail.changeType.created', 'Created')
71
+ case 'amend': return t('cpq.inventory.subscriptions.detail.changeType.amend', 'Amended')
72
+ case 'renew': return t('cpq.inventory.subscriptions.detail.changeType.renew', 'Renewed')
73
+ case 'cancel': return t('cpq.inventory.subscriptions.detail.changeType.cancel', 'Cancelled')
74
+ case 'merge-result': return t('cpq.inventory.subscriptions.detail.changeType.mergeResult', 'Created from merging contracts')
75
+ case 'merge-source': return t('cpq.inventory.subscriptions.detail.changeType.mergeSource', 'Merged into a new contract')
76
+ default: return changeType
77
+ }
72
78
  }
73
79
 
74
80
  const CHANGE_TYPE_VARIANTS: Record<string, 'info' | 'success' | 'error' | 'brand' | 'warning' | 'neutral'> = {
@@ -154,27 +160,23 @@ const ALLOWED_TRANSITIONS: Record<string, string[]> = {
154
160
  expired: [],
155
161
  }
156
162
 
157
- const STATUS_LABELS: Record<string, string> = {
158
- pending: 'Pending',
159
- active: 'Active',
160
- suspended: 'Suspended',
161
- terminated: 'Terminated',
162
- expired: 'Expired',
163
- }
163
+ const SUBSCRIPTION_STATUSES = ['pending', 'active', 'suspended', 'terminated', 'expired'] as const
164
164
 
165
165
  function fmt(amount: number | string, currency: string): string {
166
166
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(Number(amount))
167
167
  }
168
168
 
169
169
  function StatusBadge({ status }: { status: string }) {
170
+ const t = useT()
170
171
  return (
171
172
  <Tag variant={subscriptionStatusMap[status as SubscriptionStatus] ?? 'neutral'} dot>
172
- {formatStatusLabel(status)}
173
+ {inventoryStatusLabel(t, status)}
173
174
  </Tag>
174
175
  )
175
176
  }
176
177
 
177
178
  export default function SubscriptionDetailPage(props: { params?: { id?: string } }) {
179
+ const t = useT()
178
180
  const router = useRouter()
179
181
  const urlParams = useParams()
180
182
  const subId = (props.params?.id ?? urlParams?.id) as string
@@ -218,15 +220,16 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
218
220
  // Offer to navigate to that quote instead of showing a dead alert.
219
221
  if (body.existingQuoteId) {
220
222
  const ok = window.confirm(
221
- `${body.error ?? 'Subscription has another ARC quote in progress.'}\n\n` +
222
- `Open the existing quote now?`,
223
+ t('cpq.inventory.subscriptions.detail.arc.existingQuoteConfirm', '{error}\n\nOpen the existing quote now?', {
224
+ error: body.error ?? t('cpq.inventory.subscriptions.detail.arc.existingQuoteInProgress', 'Subscription has another ARC quote in progress.'),
225
+ }),
223
226
  )
224
227
  if (ok) {
225
228
  router.push(`/backend/cpq/quotes/${body.existingQuoteId}`)
226
229
  }
227
230
  return
228
231
  }
229
- alert(`Failed to start ${type} quote: ${body.error ?? res.statusText}`)
232
+ alert(t('cpq.inventory.subscriptions.detail.arc.startFailed', 'Failed to start {type} quote: {error}', { type, error: body.error ?? res.statusText }))
230
233
  return
231
234
  }
232
235
  const data = await res.json()
@@ -235,7 +238,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
235
238
  setArcInFlight(null)
236
239
  }
237
240
  },
238
- [router, sub],
241
+ [router, sub, t],
239
242
  )
240
243
 
241
244
  const transitionStatus = async (targetStatus: string) => {
@@ -249,7 +252,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
249
252
  body: JSON.stringify({ id: sub.id, targetStatus }),
250
253
  })
251
254
  const data = await res.json()
252
- if (!res.ok) throw new Error(data?.error ?? 'Status transition failed')
255
+ if (!res.ok) throw new Error(data?.error ?? t('cpq.inventory.subscriptions.detail.error.transitionFailed', 'Status transition failed'))
253
256
  // The status endpoint cascades to items and linked assets server-side,
254
257
  // but only returns the subscription scalar fields. Re-fetch the full
255
258
  // detail (which includes the item tree) and the asset list so the UI
@@ -322,7 +325,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
322
325
  async function load() {
323
326
  try {
324
327
  const res = await fetch(`/api/cpq/inventory/subscriptions?id=${subId}`)
325
- if (!res.ok) throw new Error('Subscription not found')
328
+ if (!res.ok) throw new Error(t('cpq.inventory.subscriptions.detail.error.notFound', 'Subscription not found'))
326
329
  const data: SubscriptionDetail = await res.json()
327
330
  if (!cancelled) {
328
331
  setSub(data)
@@ -409,7 +412,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
409
412
  }
410
413
  load()
411
414
  return () => { cancelled = true }
412
- }, [subId])
415
+ }, [subId, t])
413
416
 
414
417
  const toggleItem = (id: string) => {
415
418
  setExpandedItems((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n })
@@ -418,7 +421,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
418
421
  if (loading) {
419
422
  return (
420
423
  <div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
421
- <Spinner /> Loading…
424
+ <Spinner /> {t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}
422
425
  </div>
423
426
  )
424
427
  }
@@ -426,9 +429,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
426
429
  if (!sub) {
427
430
  return (
428
431
  <div className="space-y-4">
429
- <button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← Back</button>
432
+ <button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← {t('cpq.common.back', 'Back')}</button>
430
433
  {error && <ErrorBanner message={error} />}
431
- <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Subscription not found.</div>
434
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.notFound', 'Subscription not found.')}</div>
432
435
  </div>
433
436
  )
434
437
  }
@@ -438,6 +441,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
438
441
 
439
442
  const canArc = sub.status === 'active' || sub.status === 'suspended'
440
443
  const statusTransitions = ALLOWED_TRANSITIONS[sub.status] ?? []
444
+ const statusLabels = buildInventoryStatusLabels(t, SUBSCRIPTION_STATUSES)
441
445
 
442
446
  return (
443
447
  <div className="space-y-6">
@@ -450,13 +454,13 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
450
454
  <FormHeader
451
455
  mode="detail"
452
456
  backHref="/backend/cpq/inventory"
453
- backLabel="Back to Customer Inventory"
454
- entityTypeLabel="Subscription"
457
+ backLabel={t('cpq.inventory.detail.backToInventory', 'Back to Customer Inventory')}
458
+ entityTypeLabel={t('cpq.inventory.subscriptions.detail.entityType', 'Subscription')}
455
459
  title={sub.name}
456
460
  statusBadge={
457
461
  <div className="flex flex-wrap items-center gap-2 pt-1">
458
462
  <Tag variant={subscriptionStatusMap[sub.status as SubscriptionStatus] ?? 'neutral'} dot>
459
- {STATUS_LABELS[sub.status] ?? formatStatusLabel(sub.status)}
463
+ {inventoryStatusLabel(t, sub.status)}
460
464
  </Tag>
461
465
  <Tag variant="neutral" className="capitalize">{sub.billingCycle}</Tag>
462
466
  {transitioning && <Spinner />}
@@ -471,7 +475,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
471
475
  disabled={arcInFlight !== null}
472
476
  onClick={() => startArcQuote('amend')}
473
477
  >
474
- Amend
478
+ {t('cpq.inventory.subscriptions.detail.arc.amend', 'Amend')}
475
479
  </Button>
476
480
  <Button
477
481
  type="button"
@@ -479,7 +483,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
479
483
  disabled={arcInFlight !== null}
480
484
  onClick={() => startArcQuote('renew')}
481
485
  >
482
- Renew
486
+ {t('cpq.inventory.subscriptions.detail.arc.renew', 'Renew')}
483
487
  </Button>
484
488
  <Button
485
489
  type="button"
@@ -487,7 +491,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
487
491
  disabled={arcInFlight !== null}
488
492
  onClick={() => startArcQuote('cancel')}
489
493
  >
490
- Cancel
494
+ {t('cpq.inventory.subscriptions.detail.arc.cancel', 'Cancel')}
491
495
  </Button>
492
496
  </>
493
497
  ) : null
@@ -502,11 +506,11 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
502
506
  path={['pending', 'active']}
503
507
  terminals={['suspended', 'terminated', 'expired']}
504
508
  statusMap={subscriptionStatusMap}
505
- labels={STATUS_LABELS}
509
+ labels={statusLabels}
506
510
  onTransition={transitionStatus}
507
511
  allowedTransitions={statusTransitions}
508
512
  disabled={transitioning}
509
- ariaLabel="Subscription status path"
513
+ ariaLabel={t('cpq.inventory.subscriptions.detail.statusPath', 'Subscription status path')}
510
514
  />
511
515
  </div>
512
516
 
@@ -515,46 +519,46 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
515
519
  {/* Detail Cards */}
516
520
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
517
521
  <div className="rounded-lg border bg-card p-4 space-y-3">
518
- <h3 className="text-sm font-medium">Subscription Details</h3>
522
+ <h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.details', 'Subscription Details')}</h3>
519
523
  <div className="grid grid-cols-2 gap-x-4 gap-y-2">
520
- <Field label="Code" value={sub.code} mono />
521
- <Field label="Status" value={sub.status} />
522
- <Field label="Billing Cycle" value={sub.billingCycle} />
523
- <Field label="Currency" value={currency} />
524
- <Field label="Auto-Renew" value={sub.autoRenew ? 'Yes' : 'No'} />
525
- {sub.termMonths != null && <Field label="Term" value={`${sub.termMonths} months`} />}
526
- {sub.startDate && <Field label="Start Date" value={new Date(sub.startDate).toLocaleDateString()} />}
527
- {sub.currentTermEnd && <Field label="Term End" value={new Date(sub.currentTermEnd).toLocaleDateString()} />}
528
- {sub.activatedAt && <Field label="Activated" value={new Date(sub.activatedAt).toLocaleString()} />}
529
- {sub.suspendedAt && <Field label="Suspended" value={new Date(sub.suspendedAt).toLocaleString()} />}
530
- {sub.terminatedAt && <Field label="Terminated" value={new Date(sub.terminatedAt).toLocaleString()} />}
524
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.code', 'Code')} value={sub.code} mono />
525
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.status', 'Status')} value={sub.status} />
526
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.billingCycle', 'Billing Cycle')} value={sub.billingCycle} />
527
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={currency} />
528
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.autoRenew', 'Auto-Renew')} value={sub.autoRenew ? t('cpq.common.yes', 'Yes') : t('cpq.common.no', 'No')} />
529
+ {sub.termMonths != null && <Field label={t('cpq.inventory.subscriptions.detail.fields.term', 'Term')} value={t('cpq.inventory.subscriptions.detail.fields.termMonths', '{count} months', { count: sub.termMonths })} />}
530
+ {sub.startDate && <Field label={t('cpq.inventory.subscriptions.detail.fields.startDate', 'Start Date')} value={new Date(sub.startDate).toLocaleDateString()} />}
531
+ {sub.currentTermEnd && <Field label={t('cpq.inventory.subscriptions.detail.fields.termEnd', 'Term End')} value={new Date(sub.currentTermEnd).toLocaleDateString()} />}
532
+ {sub.activatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.activated', 'Activated')} value={new Date(sub.activatedAt).toLocaleString()} />}
533
+ {sub.suspendedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.suspended', 'Suspended')} value={new Date(sub.suspendedAt).toLocaleString()} />}
534
+ {sub.terminatedAt && <Field label={t('cpq.inventory.subscriptions.detail.fields.terminated', 'Terminated')} value={new Date(sub.terminatedAt).toLocaleString()} />}
531
535
  </div>
532
536
  {sub.description && (
533
537
  <div className="pt-1">
534
- <p className="text-xs text-muted-foreground">Description</p>
538
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.description', 'Description')}</p>
535
539
  <p className="text-sm">{sub.description}</p>
536
540
  </div>
537
541
  )}
538
542
  </div>
539
543
 
540
544
  <div className="rounded-lg border bg-card p-4 space-y-3">
541
- <h3 className="text-sm font-medium">Pricing & References</h3>
545
+ <h3 className="text-sm font-medium">{t('cpq.inventory.subscriptions.detail.sections.pricing', 'Pricing & References')}</h3>
542
546
  <div className="grid grid-cols-2 gap-4">
543
547
  <div>
544
- <p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
548
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.mrc', 'Monthly Recurring (MRC)')}</p>
545
549
  <p className="text-lg font-semibold font-mono">{fmt(sub.mrcAmount, currency)}</p>
546
550
  </div>
547
551
  <div>
548
- <p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
552
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.nrc', 'Non-Recurring (NRC)')}</p>
549
553
  <p className="text-lg font-semibold font-mono">{fmt(sub.nrcAmount, currency)}</p>
550
554
  </div>
551
555
  </div>
552
556
  <div className="grid grid-cols-2 gap-x-4 gap-y-2 pt-1">
553
- <Field label="Customer" value={customerName ?? sub.customerId.slice(0, 12) + '…'} />
554
- <Field label="Created" value={new Date(sub.createdAt).toLocaleString()} />
557
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.customer', 'Customer')} value={customerName ?? sub.customerId.slice(0, 12) + '…'} />
558
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.created', 'Created')} value={new Date(sub.createdAt).toLocaleString()} />
555
559
  {sub.sourceQuoteId && (
556
560
  <div>
557
- <p className="text-xs text-muted-foreground">Source Quote</p>
561
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceQuote', 'Source Quote')}</p>
558
562
  <button
559
563
  onClick={() => router.push(`/backend/cpq/quotes/${sub.sourceQuoteId}`)}
560
564
  className="text-sm text-primary hover:underline"
@@ -565,7 +569,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
565
569
  )}
566
570
  {sub.sourceOrderId && (
567
571
  <div>
568
- <p className="text-xs text-muted-foreground">Source Order</p>
572
+ <p className="text-xs text-muted-foreground">{t('cpq.inventory.subscriptions.detail.fields.sourceOrder', 'Source Order')}</p>
569
573
  <button
570
574
  onClick={() => router.push(`/backend/cpq/orders/${sub.sourceOrderId}`)}
571
575
  className="text-sm text-primary hover:underline"
@@ -581,7 +585,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
581
585
  {/* Metadata */}
582
586
  {sub.metadata && Object.keys(sub.metadata).length > 0 && (
583
587
  <div className="rounded-lg border bg-card p-4">
584
- <h3 className="text-sm font-medium mb-2">Metadata</h3>
588
+ <h3 className="text-sm font-medium mb-2">{t('cpq.inventory.subscriptions.detail.sections.metadata', 'Metadata')}</h3>
585
589
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
586
590
  {Object.entries(sub.metadata).map(([key, value]) => (
587
591
  <div key={key} className="text-xs">
@@ -595,9 +599,9 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
595
599
 
596
600
  {/* Subscription Items */}
597
601
  <div>
598
- <h3 className="text-sm font-medium mb-2 px-1">Subscription Items ({flatItems.length})</h3>
602
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.items.title', 'Subscription Items ({count})', { count: flatItems.length })}</h3>
599
603
  {flatItems.length === 0 ? (
600
- <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No items.</div>
604
+ <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.empty', 'No items.')}</div>
601
605
  ) : (
602
606
  <div className="rounded-lg border bg-card divide-y">
603
607
  {flatItems.map((item) => {
@@ -623,8 +627,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
623
627
  </div>
624
628
  </div>
625
629
  <div className="shrink-0 text-right text-xs space-y-0.5">
626
- {Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
627
- {Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
630
+ {Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.mrc', 'MRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
631
+ {Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">{t('cpq.inventory.subscriptions.detail.items.nrc', 'NRC')}{' '}</span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
628
632
  </div>
629
633
  </div>
630
634
 
@@ -633,7 +637,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
633
637
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2">
634
638
  {item.offeringId && (
635
639
  <RefField
636
- label="Offering"
640
+ label={t('cpq.inventory.subscriptions.detail.fields.offering', 'Offering')}
637
641
  name={offeringNames[item.offeringId]}
638
642
  fallback={item.offeringId}
639
643
  onClick={() => router.push(`/backend/cpq/offerings/${item.offeringId}`)}
@@ -641,7 +645,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
641
645
  )}
642
646
  {item.specId && (
643
647
  <RefField
644
- label="Specification"
648
+ label={t('cpq.inventory.subscriptions.detail.fields.specification', 'Specification')}
645
649
  name={specNames[item.specId]}
646
650
  fallback={item.specId}
647
651
  onClick={() => router.push(`/backend/cpq/specifications/${item.specId}`)}
@@ -649,20 +653,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
649
653
  )}
650
654
  {item.productId && (
651
655
  <RefField
652
- label="Product"
656
+ label={t('cpq.inventory.subscriptions.detail.fields.product', 'Product')}
653
657
  name={productNames[item.productId]}
654
658
  fallback={item.productId}
655
659
  onClick={() => router.push(`/backend/catalog/products/${item.productId}`)}
656
660
  />
657
661
  )}
658
- <Field label="Currency" value={item.currencyCode || currency} />
659
- <Field label="Quantity" value={String(item.quantity)} />
660
- {item.capacityTotal && <Field label="Capacity" value={`${item.capacityUsed ?? '0'} / ${item.capacityTotal} ${item.capacityUnit ?? ''}`} />}
662
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.currency', 'Currency')} value={item.currencyCode || currency} />
663
+ <Field label={t('cpq.inventory.subscriptions.detail.fields.quantity', 'Quantity')} value={String(item.quantity)} />
664
+ {item.capacityTotal && <Field label={t('cpq.inventory.subscriptions.detail.fields.capacity', 'Capacity')} value={`${item.capacityUsed ?? '0'} / ${item.capacityTotal} ${item.capacityUnit ?? ''}`} />}
661
665
  </div>
662
666
 
663
667
  {item.configuration && Object.keys(item.configuration).length > 0 && (
664
668
  <div>
665
- <p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
669
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.configuration', 'Configuration')}</p>
666
670
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
667
671
  {Object.entries(item.configuration).map(([key, value]) => (
668
672
  <div key={key} className="text-xs">
@@ -676,27 +680,47 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
676
680
 
677
681
  {item.charges && item.charges.length > 0 && (
678
682
  <div>
679
- <p className="text-xs font-medium text-muted-foreground mb-1">Charges</p>
683
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.charges', 'Charges')}</p>
680
684
  <table className="w-full text-xs">
681
685
  <thead>
682
686
  <tr className="text-muted-foreground">
683
- <th className="text-left py-1 font-medium">Charge</th>
684
- <th className="text-left py-1 font-medium">Type</th>
685
- <th className="text-right py-1 font-medium">Unit Price</th>
686
- <th className="text-right py-1 font-medium">Qty</th>
687
- <th className="text-right py-1 font-medium">Total</th>
687
+ <th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.charge', 'Charge')}</th>
688
+ <th className="text-left py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.type', 'Type')}</th>
689
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.list', 'List')}</th>
690
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.offer', 'Offer')}</th>
691
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.qty', 'Qty')}</th>
692
+ <th className="text-right py-1 font-medium">{t('cpq.inventory.subscriptions.detail.charges.total', 'Total')}</th>
688
693
  </tr>
689
694
  </thead>
690
695
  <tbody>
691
- {item.charges.map((c, i) => (
692
- <tr key={i} className="border-t border-dashed border-border/50">
693
- <td className="py-1">{(c as any).chargeName ?? '—'}</td>
694
- <td className="py-1 uppercase">{(c as any).chargeType ?? '—'}</td>
695
- <td className="py-1 text-right font-mono">{(c as any).unitPrice != null ? fmt((c as any).unitPrice, currency) : '—'}</td>
696
- <td className="py-1 text-right font-mono">{(c as any).quantity ?? '—'}</td>
697
- <td className="py-1 text-right font-mono">{(c as any).totalPrice != null ? fmt((c as any).totalPrice, currency) : '—'}</td>
698
- </tr>
699
- ))}
696
+ {item.charges.map((raw, i) => {
697
+ const c = parseStoredCharge(raw)
698
+ if (!c) return null
699
+ return (
700
+ <React.Fragment key={i}>
701
+ <tr className="border-t border-dashed border-border/50">
702
+ <td className="py-1">{c.chargeName}</td>
703
+ <td className="py-1 uppercase">{c.chargeType}</td>
704
+ <td className="py-1 text-right font-mono text-muted-foreground">{fmt(c.listUnitPrice ?? c.unitPrice, currency)}</td>
705
+ <td className="py-1 text-right font-mono">
706
+ <span className="inline-flex items-center gap-1">
707
+ <DepthBadge before={c.listUnitPrice ?? c.unitPrice} after={c.unitPrice} />
708
+ {fmt(c.unitPrice, currency)}
709
+ </span>
710
+ </td>
711
+ <td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
712
+ <td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : '—'}</td>
713
+ </tr>
714
+ {(c.adjustments?.length ?? 0) > 0 && (
715
+ <tr>
716
+ <td colSpan={6} className="pb-2 pl-4">
717
+ <PriceWaterfall charge={c} currency={currency} />
718
+ </td>
719
+ </tr>
720
+ )}
721
+ </React.Fragment>
722
+ )
723
+ })}
700
724
  </tbody>
701
725
  </table>
702
726
  </div>
@@ -704,7 +728,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
704
728
 
705
729
  {item.metadata && Object.keys(item.metadata).length > 0 && (
706
730
  <div>
707
- <p className="text-xs font-medium text-muted-foreground mb-1">Metadata</p>
731
+ <p className="text-xs font-medium text-muted-foreground mb-1">{t('cpq.inventory.subscriptions.detail.items.metadata', 'Metadata')}</p>
708
732
  <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
709
733
  {Object.entries(item.metadata).map(([key, value]) => (
710
734
  <div key={key} className="text-xs">
@@ -717,8 +741,8 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
717
741
  )}
718
742
 
719
743
  <div className="flex gap-4 text-xs text-muted-foreground">
720
- <span>Created: {new Date(item.createdAt).toLocaleString()}</span>
721
- <span>Updated: {new Date(item.updatedAt).toLocaleString()}</span>
744
+ <span>{t('cpq.inventory.subscriptions.detail.items.createdAt', 'Created: {date}', { date: new Date(item.createdAt).toLocaleString() })}</span>
745
+ <span>{t('cpq.inventory.subscriptions.detail.items.updatedAt', 'Updated: {date}', { date: new Date(item.updatedAt).toLocaleString() })}</span>
722
746
  </div>
723
747
  </div>
724
748
  )}
@@ -731,18 +755,18 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
731
755
 
732
756
  {/* XD-250 Change History */}
733
757
  <div>
734
- <h3 className="text-sm font-medium mb-2 px-1">Change History ({changeLog.length})</h3>
758
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.changeLog.title', 'Change History ({count})', { count: changeLog.length })}</h3>
735
759
  {changeLogLoading ? (
736
- <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">Loading…</div>
760
+ <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.loading', 'Loading…')}</div>
737
761
  ) : changeLog.length === 0 ? (
738
- <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">No ARC changes yet.</div>
762
+ <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.changeLog.empty', 'No ARC changes yet.')}</div>
739
763
  ) : (
740
764
  <div className="rounded-lg border bg-card divide-y">
741
765
  {changeLog.map((row) => (
742
766
  <div key={row.id} className="px-4 py-3 space-y-1">
743
767
  <div className="flex items-center gap-2">
744
768
  <Tag variant={CHANGE_TYPE_VARIANTS[row.changeType] ?? 'neutral'}>
745
- {CHANGE_TYPE_LABELS[row.changeType] ?? row.changeType}
769
+ {changeTypeLabel(t, row.changeType)}
746
770
  </Tag>
747
771
  <span className="text-xs text-muted-foreground">
748
772
  {new Date(row.effectiveAt).toLocaleString()}
@@ -752,7 +776,7 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
752
776
  onClick={() => router.push(`/backend/cpq/quotes/${row.sourceQuoteId}`)}
753
777
  className="text-xs text-primary hover:underline"
754
778
  >
755
- Quote {logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…`}
779
+ {t('cpq.inventory.subscriptions.detail.changeLog.quote', 'Quote {number}', { number: logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…` })}
756
780
  </button>
757
781
  )}
758
782
  {row.sourceOrderId && (
@@ -760,42 +784,47 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
760
784
  onClick={() => router.push(`/backend/cpq/orders/${row.sourceOrderId}`)}
761
785
  className="text-xs text-primary hover:underline"
762
786
  >
763
- Order {logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…`}
787
+ {t('cpq.inventory.subscriptions.detail.changeLog.order', 'Order {number}', { number: logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…` })}
764
788
  </button>
765
789
  )}
766
790
  </div>
767
791
  <ChangeLogLineDetails row={row} currency={currency} />
768
792
  {row.termChange && (
769
793
  <div className="text-xs text-muted-foreground">
770
- Term: {String(row.termChange.oldTermEnd ?? '—')} → {String(row.termChange.newTermEnd ?? '—')}
794
+ {t('cpq.inventory.subscriptions.detail.changeLog.term', 'Term: {from} → {to}', {
795
+ from: String(row.termChange.oldTermEnd ?? '—'),
796
+ to: String(row.termChange.newTermEnd ?? '—'),
797
+ })}
771
798
  </div>
772
799
  )}
773
800
  {row.reasonCode && (
774
801
  <div className="text-xs text-muted-foreground">
775
- Reason: {row.reasonCode}{row.reasonText ? ` — ${row.reasonText}` : ''}
802
+ {row.reasonText
803
+ ? t('cpq.inventory.subscriptions.detail.changeLog.reasonWithText', 'Reason: {code} — {text}', { code: row.reasonCode, text: row.reasonText })
804
+ : t('cpq.inventory.subscriptions.detail.changeLog.reason', 'Reason: {code}', { code: row.reasonCode })}
776
805
  </div>
777
806
  )}
778
807
  {row.etfAmount && (
779
808
  <div className="text-xs text-muted-foreground">
780
- ETF: {row.etfAmount} {row.etfCurrency ?? ''}
809
+ {t('cpq.inventory.subscriptions.detail.changeLog.etf', 'ETF: {amount} {currency}', { amount: row.etfAmount, currency: row.etfCurrency ?? '' })}
781
810
  </div>
782
811
  )}
783
812
  {row.mergedIntoSubscriptionId && (
784
813
  <div className="text-xs text-muted-foreground">
785
- Merged into{' '}
814
+ {t('cpq.inventory.subscriptions.detail.changeLog.mergedInto', 'Merged into')}{' '}
786
815
  <button
787
816
  onClick={() =>
788
817
  router.push(`/backend/cpq/inventory/subscriptions/${row.mergedIntoSubscriptionId}`)
789
818
  }
790
819
  className="text-primary hover:underline"
791
820
  >
792
- subscription {row.mergedIntoSubscriptionId.slice(0, 8)}…
821
+ {t('cpq.inventory.subscriptions.detail.changeLog.mergedIntoSubscription', 'subscription {id}…', { id: row.mergedIntoSubscriptionId.slice(0, 8) })}
793
822
  </button>
794
823
  </div>
795
824
  )}
796
825
  {row.mergedFromSubscriptionIds && row.mergedFromSubscriptionIds.length > 0 && (
797
826
  <div className="text-xs text-muted-foreground">
798
- Merged from {row.mergedFromSubscriptionIds.length} subscriptions
827
+ {t('cpq.inventory.subscriptions.detail.changeLog.mergedFrom', 'Merged from {count} subscriptions', { count: row.mergedFromSubscriptionIds.length })}
799
828
  </div>
800
829
  )}
801
830
  </div>
@@ -806,20 +835,20 @@ export default function SubscriptionDetailPage(props: { params?: { id?: string }
806
835
 
807
836
  {/* Linked Assets */}
808
837
  <div>
809
- <h3 className="text-sm font-medium mb-2 px-1">Linked Assets ({assets.length})</h3>
838
+ <h3 className="text-sm font-medium mb-2 px-1">{t('cpq.inventory.subscriptions.detail.assets.title', 'Linked Assets ({count})', { count: assets.length })}</h3>
810
839
  {assets.length === 0 ? (
811
- <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No assets linked to this subscription.</div>
840
+ <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">{t('cpq.inventory.subscriptions.detail.assets.empty', 'No assets linked to this subscription.')}</div>
812
841
  ) : (
813
842
  <div className="rounded-lg border">
814
843
  <table className="w-full text-sm">
815
844
  <thead>
816
845
  <tr className="border-b bg-muted/50">
817
- <th className="px-4 py-3 text-left font-medium">Code</th>
818
- <th className="px-4 py-3 text-left font-medium">Name</th>
819
- <th className="px-4 py-3 text-left font-medium">Type</th>
820
- <th className="px-4 py-3 text-left font-medium">Status</th>
821
- <th className="px-4 py-3 text-right font-medium">Price</th>
822
- <th className="px-4 py-3 text-right font-medium">Qty</th>
846
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.code', 'Code')}</th>
847
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.name', 'Name')}</th>
848
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.type', 'Type')}</th>
849
+ <th className="px-4 py-3 text-left font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.status', 'Status')}</th>
850
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.price', 'Price')}</th>
851
+ <th className="px-4 py-3 text-right font-medium">{t('cpq.inventory.subscriptions.detail.assets.table.qty', 'Qty')}</th>
823
852
  </tr>
824
853
  </thead>
825
854
  <tbody>
@@ -910,6 +939,8 @@ function ErrorBanner({ message }: { message: string }) {
910
939
  // names and before/after MRC values. Falls back gracefully when snapshots are
911
940
  // missing (older log rows) or when lineChanges is null (cancel / merge).
912
941
  function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency: string }) {
942
+ const t = useT()
943
+ const itemFallback = t('cpq.inventory.subscriptions.detail.changeLog.item', 'Item')
913
944
  const beforeItems = new Map<string, SnapshotItem>(
914
945
  (row.beforeSnapshot?.items ?? []).map((it) => [it.id, it]),
915
946
  )
@@ -943,7 +974,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
943
974
  derived.push({
944
975
  key: `${itemId}-${i}`,
945
976
  action: lc.action,
946
- name: item?.name ?? (lc.action === 'add' ? 'New item' : 'Item'),
977
+ name: item?.name ?? (lc.action === 'add' ? t('cpq.inventory.subscriptions.detail.changeLog.newItem', 'New item') : itemFallback),
947
978
  beforeMrc,
948
979
  afterMrc:
949
980
  lc.action === 'add' && afterMrc == null && lc.mrcAmount != null
@@ -959,7 +990,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
959
990
  derived.push({
960
991
  key: it.id,
961
992
  action: 'cancel',
962
- name: it.name ?? 'Item',
993
+ name: it.name ?? itemFallback,
963
994
  beforeMrc: mrc,
964
995
  afterMrc: 0,
965
996
  delta: -mrc,
@@ -971,7 +1002,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
971
1002
  derived.push({
972
1003
  key: it.id,
973
1004
  action: 'add',
974
- name: it.name ?? 'Item',
1005
+ name: it.name ?? itemFallback,
975
1006
  beforeMrc: null,
976
1007
  afterMrc: mrc,
977
1008
  delta: null,
@@ -983,7 +1014,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
983
1014
  derived.push({
984
1015
  key: it.id,
985
1016
  action: 'cancel',
986
- name: it.name ?? 'Item',
1017
+ name: it.name ?? itemFallback,
987
1018
  beforeMrc: mrc,
988
1019
  afterMrc: 0,
989
1020
  delta: -mrc,
@@ -999,7 +1030,7 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
999
1030
  if (row.changeType === 'amend' || row.changeType === 'renew') {
1000
1031
  return (
1001
1032
  <div className="mt-1.5 text-xs text-muted-foreground italic">
1002
- No item-level changes recorded.
1033
+ {t('cpq.inventory.subscriptions.detail.changeLog.noItemChanges', 'No item-level changes recorded.')}
1003
1034
  </div>
1004
1035
  )
1005
1036
  }
@@ -1015,10 +1046,18 @@ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency:
1015
1046
  )
1016
1047
  }
1017
1048
 
1018
- const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string; label: string }> = {
1019
- add: { sign: '+', color: 'text-status-success-text', label: 'added' },
1020
- modify: { sign: '~', color: 'text-status-info-text', label: 'modified' },
1021
- cancel: { sign: '−', color: 'text-destructive', label: 'removed' },
1049
+ const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string }> = {
1050
+ add: { sign: '+', color: 'text-status-success-text' },
1051
+ modify: { sign: '~', color: 'text-status-info-text' },
1052
+ cancel: { sign: '−', color: 'text-destructive' },
1053
+ }
1054
+
1055
+ function actionGlyphLabel(t: TranslateFn, action: 'add' | 'modify' | 'cancel'): string {
1056
+ switch (action) {
1057
+ case 'add': return t('cpq.inventory.subscriptions.detail.changeLog.action.add', 'added')
1058
+ case 'modify': return t('cpq.inventory.subscriptions.detail.changeLog.action.modify', 'modified')
1059
+ case 'cancel': return t('cpq.inventory.subscriptions.detail.changeLog.action.cancel', 'removed')
1060
+ }
1022
1061
  }
1023
1062
 
1024
1063
  function ChangeLogLineRow({
@@ -1034,6 +1073,7 @@ function ChangeLogLineRow({
1034
1073
  }
1035
1074
  currency: string
1036
1075
  }) {
1076
+ const t = useT()
1037
1077
  const glyph = ACTION_GLYPH[change.action]
1038
1078
  const showPriceChange =
1039
1079
  change.action === 'modify' &&
@@ -1050,35 +1090,39 @@ function ChangeLogLineRow({
1050
1090
 
1051
1091
  return (
1052
1092
  <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
1053
- <span className={`font-bold w-3 text-center ${glyph.color}`} title={glyph.label}>
1093
+ <span className={`font-bold w-3 text-center ${glyph.color}`} title={actionGlyphLabel(t, change.action)}>
1054
1094
  {glyph.sign}
1055
1095
  </span>
1056
1096
  <span className="font-medium text-foreground">{change.name}</span>
1057
1097
  {showPriceChange && (
1058
1098
  <span className="font-mono text-muted-foreground">
1059
- {fmt(change.beforeMrc!, currency)} → {fmt(change.afterMrc!, currency)}/mo
1099
+ {t('cpq.inventory.subscriptions.detail.changeLog.priceChangePerMonth', '{before} → {after}/mo', {
1100
+ before: fmt(change.beforeMrc!, currency),
1101
+ after: fmt(change.afterMrc!, currency),
1102
+ })}
1060
1103
  </span>
1061
1104
  )}
1062
1105
  {showSinglePrice && change.action === 'add' && (
1063
1106
  <span className="font-mono text-muted-foreground">
1064
- {fmt(change.afterMrc!, currency)}/mo
1107
+ {t('cpq.inventory.subscriptions.detail.changeLog.pricePerMonth', '{price}/mo', { price: fmt(change.afterMrc!, currency) })}
1065
1108
  </span>
1066
1109
  )}
1067
1110
  {showSinglePrice && change.action === 'cancel' && (
1068
1111
  <span className="font-mono text-muted-foreground">
1069
- was {fmt(change.beforeMrc!, currency)}/mo
1112
+ {t('cpq.inventory.subscriptions.detail.changeLog.wasPricePerMonth', 'was {price}/mo', { price: fmt(change.beforeMrc!, currency) })}
1070
1113
  </span>
1071
1114
  )}
1072
1115
  {showDelta && (
1073
1116
  <span
1074
1117
  className={`font-mono font-medium ${change.delta! > 0 ? 'text-status-success-text' : 'text-destructive'}`}
1075
1118
  >
1076
- ({change.delta! > 0 ? '+' : ''}
1077
- {fmt(change.delta!, currency)}/mo)
1119
+ {t('cpq.inventory.subscriptions.detail.changeLog.deltaPerMonth', '({delta}/mo)', {
1120
+ delta: `${change.delta! > 0 ? '+' : ''}${fmt(change.delta!, currency)}`,
1121
+ })}
1078
1122
  </span>
1079
1123
  )}
1080
1124
  {isNoopModify && (
1081
- <span className="text-muted-foreground italic">no price change</span>
1125
+ <span className="text-muted-foreground italic">{t('cpq.inventory.subscriptions.detail.changeLog.noPriceChange', 'no price change')}</span>
1082
1126
  )}
1083
1127
  </div>
1084
1128
  )