@dainamite/cpq 0.4.0 → 0.6.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 (273) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/MIGRATION.md +101 -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/handleArcError.js +2 -2
  8. package/dist/modules/cpq/api/_helpers/handleArcError.js.map +2 -2
  9. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
  10. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
  11. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
  12. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
  13. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +2 -2
  14. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +2 -2
  15. package/dist/modules/cpq/api/orders/[orderId]/route.js +19 -3
  16. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
  17. package/dist/modules/cpq/api/orders/[orderId]/status/route.js +2 -2
  18. package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +2 -2
  19. package/dist/modules/cpq/api/orders/route.js +49 -4
  20. package/dist/modules/cpq/api/orders/route.js.map +2 -2
  21. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +2 -2
  22. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +2 -2
  23. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
  24. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
  25. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +45 -5
  26. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
  27. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
  28. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
  29. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
  30. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
  31. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
  32. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
  33. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
  34. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
  35. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +34 -4
  36. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
  37. package/dist/modules/cpq/api/quotes/[quoteId]/route.js +2 -2
  38. package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +2 -2
  39. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
  40. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
  41. package/dist/modules/cpq/api/quotes/price/route.js +43 -3
  42. package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
  43. package/dist/modules/cpq/api/quotes/route.js +2 -2
  44. package/dist/modules/cpq/api/quotes/route.js.map +2 -2
  45. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +1 -1
  46. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +1 -1
  47. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +1 -1
  48. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +1 -1
  49. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +22 -8
  50. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  51. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +1 -1
  52. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +89 -232
  53. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  54. package/dist/modules/cpq/backend/cpq/orders/page.js.map +1 -1
  55. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
  56. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
  57. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +1 -1
  58. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +1 -1
  59. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +205 -0
  60. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
  61. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
  62. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
  63. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
  64. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
  65. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
  66. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
  67. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
  68. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
  69. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +144 -221
  70. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
  71. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +1 -1
  72. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
  73. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
  74. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +1 -1
  75. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +1 -1
  76. package/dist/modules/cpq/ce.js +8 -0
  77. package/dist/modules/cpq/ce.js.map +2 -2
  78. package/dist/modules/cpq/cli.js +34 -1
  79. package/dist/modules/cpq/cli.js.map +2 -2
  80. package/dist/modules/cpq/commands/price-adjustments.js +107 -0
  81. package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
  82. package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
  83. package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
  84. package/dist/modules/cpq/components/ChargeBreakdownTable.js +154 -0
  85. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
  86. package/dist/modules/cpq/components/CpqListView.js.map +2 -2
  87. package/dist/modules/cpq/components/PriceWaterfall.js +66 -0
  88. package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
  89. package/dist/modules/cpq/components/PricingSummaryCard.js +117 -0
  90. package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
  91. package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
  92. package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
  93. package/dist/modules/cpq/data/entities.js +97 -0
  94. package/dist/modules/cpq/data/entities.js.map +2 -2
  95. package/dist/modules/cpq/data/price-adjustments.js +70 -0
  96. package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
  97. package/dist/modules/cpq/data/validators.js +4 -0
  98. package/dist/modules/cpq/data/validators.js.map +2 -2
  99. package/dist/modules/cpq/di.js +6 -0
  100. package/dist/modules/cpq/di.js.map +2 -2
  101. package/dist/modules/cpq/events.js +47 -0
  102. package/dist/modules/cpq/events.js.map +2 -2
  103. package/dist/modules/cpq/index.js +1 -1
  104. package/dist/modules/cpq/index.js.map +2 -2
  105. package/dist/modules/cpq/lib/auditActor.js +10 -0
  106. package/dist/modules/cpq/lib/auditActor.js.map +7 -0
  107. package/dist/modules/cpq/lib/formatMoney.js +7 -0
  108. package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
  109. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
  110. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
  111. package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
  112. package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
  113. package/dist/modules/cpq/lib/quotePermissions.js +13 -0
  114. package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
  115. package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
  116. package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
  117. package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
  118. package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
  119. package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
  120. package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
  121. package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
  122. package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
  123. package/dist/modules/cpq/services/cpqOrderService.js +34 -30
  124. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  125. package/dist/modules/cpq/services/cpqPricingService.js +7 -0
  126. package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
  127. package/dist/modules/cpq/services/cpqQuotingService.js +339 -111
  128. package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
  129. package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
  130. package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
  131. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
  132. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
  133. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
  134. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
  135. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
  136. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
  137. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
  138. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
  139. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
  140. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
  141. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
  142. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
  143. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
  144. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
  145. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
  146. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
  147. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
  148. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
  149. package/dist/modules/cpq/services/types.js.map +2 -2
  150. package/dist/modules/cpq/setup.js +6 -0
  151. package/dist/modules/cpq/setup.js.map +2 -2
  152. package/package.json +9 -9
  153. package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +8 -5
  154. package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
  155. package/src/modules/cpq/acl.ts +1 -0
  156. package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
  157. package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
  158. package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
  159. package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
  160. package/src/modules/cpq/api/_helpers/handleArcError.ts +12 -12
  161. package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
  162. package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
  163. package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +2 -2
  164. package/src/modules/cpq/api/orders/[orderId]/route.ts +20 -2
  165. package/src/modules/cpq/api/orders/[orderId]/status/route.ts +2 -2
  166. package/src/modules/cpq/api/orders/route.ts +53 -3
  167. package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +2 -2
  168. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
  169. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +51 -4
  170. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
  171. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
  172. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
  173. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
  174. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
  175. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +42 -3
  176. package/src/modules/cpq/api/quotes/[quoteId]/route.ts +2 -2
  177. package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
  178. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
  179. package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
  180. package/src/modules/cpq/api/quotes/price/route.ts +28 -2
  181. package/src/modules/cpq/api/quotes/route.ts +2 -2
  182. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +1 -1
  183. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +1 -1
  184. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +1 -1
  185. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +32 -10
  186. package/src/modules/cpq/backend/cpq/offerings/page.tsx +1 -1
  187. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +60 -269
  188. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
  189. package/src/modules/cpq/backend/cpq/orders/page.tsx +1 -1
  190. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +1 -1
  191. package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
  192. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
  193. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
  194. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
  195. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
  196. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
  197. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
  198. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +178 -266
  199. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
  200. package/src/modules/cpq/backend/cpq/quotes/page.tsx +1 -1
  201. package/src/modules/cpq/backend/cpq/specifications/page.tsx +1 -1
  202. package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
  203. package/src/modules/cpq/ce.ts +8 -0
  204. package/src/modules/cpq/cli.ts +48 -1
  205. package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
  206. package/src/modules/cpq/commands/price-adjustments.ts +90 -0
  207. package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
  208. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +201 -0
  209. package/src/modules/cpq/components/CpqListView.tsx +2 -1
  210. package/src/modules/cpq/components/PriceWaterfall.tsx +94 -0
  211. package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
  212. package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
  213. package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
  214. package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
  215. package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
  216. package/src/modules/cpq/data/entities.ts +83 -0
  217. package/src/modules/cpq/data/price-adjustments.ts +67 -0
  218. package/src/modules/cpq/data/validators.ts +3 -0
  219. package/src/modules/cpq/di.ts +6 -0
  220. package/src/modules/cpq/events.ts +53 -2
  221. package/src/modules/cpq/index.ts +1 -1
  222. package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
  223. package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
  224. package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
  225. package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
  226. package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
  227. package/src/modules/cpq/lib/auditActor.ts +13 -0
  228. package/src/modules/cpq/lib/formatMoney.ts +4 -0
  229. package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
  230. package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
  231. package/src/modules/cpq/lib/quotePermissions.ts +22 -0
  232. package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
  233. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
  234. package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
  235. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
  236. package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
  237. package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
  238. package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
  239. package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
  240. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
  241. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
  242. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
  243. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
  244. package/src/modules/cpq/services/__tests__/errorBrands.test.ts +72 -0
  245. package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
  246. package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
  247. package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
  248. package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
  249. package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
  250. package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
  251. package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
  252. package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
  253. package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
  254. package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
  255. package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
  256. package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
  257. package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
  258. package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
  259. package/src/modules/cpq/services/cpqOrderService.ts +32 -28
  260. package/src/modules/cpq/services/cpqPricingService.ts +8 -0
  261. package/src/modules/cpq/services/cpqQuotingService.ts +388 -125
  262. package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
  263. package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
  264. package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
  265. package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
  266. package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
  267. package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
  268. package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
  269. package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
  270. package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
  271. package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
  272. package/src/modules/cpq/services/types.ts +34 -1
  273. package/src/modules/cpq/setup.ts +7 -1
@@ -1,17 +1,29 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
3
4
  import { useRouter, useParams, useSearchParams } from 'next/navigation'
4
5
  import ArcQuoteConfigurator, {
5
6
  type AttachedTarget,
6
7
  } from './_components/ArcQuoteConfigurator'
7
8
  import { StatusPath } from '../../../../components/StatusPath'
8
- import { ArrowRight, ExternalLink, Plus } from 'lucide-react'
9
+ import { ArrowRight, ExternalLink, Plus, RotateCcw } from 'lucide-react'
9
10
  import { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'
10
11
  import { Button } from '@open-mercato/ui/primitives/button'
11
12
  import { Tag } from '@open-mercato/ui/primitives/tag'
12
13
  import { FormHeader } from '@open-mercato/ui/backend/forms'
13
14
  import type { ActionItem } from '@open-mercato/ui/backend/forms/ActionsDropdown'
14
15
  import { NumberInput } from '../../../../components/NumberInput'
16
+ import { ChargeBreakdownTable } from '../../../../components/ChargeBreakdownTable'
17
+ import { PricingSummaryCard } from '../../../../components/PricingSummaryCard'
18
+ import { ArcLineDiff, LineActionBadge } from '../../../../components/ArcLineDiff'
19
+ import { ADJUST_PRICE_FEATURE } from '../../../../lib/priceAdjustmentView'
20
+ import { arcRecurringDelta } from '../../../../lib/quoteSummaryView'
21
+ import { usePriceAdjustments } from './_components/usePriceAdjustments'
22
+ import { PriceAdjustmentsPanel } from './_components/PriceAdjustmentsPanel'
23
+ import { ArcLineRemovalProvider, useArcLineRemoval, useArcLineRemovalUi } from './_components/arcLineRemoval'
24
+ import { AdjustChargeAction, DiscountLineAction, ManualAdjustmentActions, PriceAdjustmentProvider, usePriceAdjustmentUi } from './_components/priceAdjustmentContext'
25
+ import { useGrantedFeatures } from '../../../../components/useGrantedFeatures'
26
+ import { canRecalculateQuote, QUOTE_MANAGE_FEATURE } from '../../../../lib/quotePermissions'
15
27
  import {
16
28
  formatStatusLabel,
17
29
  quoteCpqStatusMap,
@@ -32,7 +44,12 @@ type ConstrainedAttribute = {
32
44
  }
33
45
 
34
46
  type TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }
35
- type RuleAdjustment = { ruleCode: string; ruleName: string; ruleType: string; value: number; unitPriceBefore: number; unitPriceAfter: number; delta: number }
47
+ type RuleAdjustment = {
48
+ ruleCode: string; ruleName: string; ruleType: string; value: number; unitPriceBefore: number; unitPriceAfter: number; delta: number
49
+ /** Negotiated-pricing fields; absent on legacy snapshots (treated as rules). */
50
+ origin?: 'rule' | 'manual'; adjustmentId?: string; reasonCode?: string; createdByUserId?: string
51
+ }
52
+ type AdjustmentErrorEntry = { adjustmentId: string; quoteLineId: string; chargeCode?: string; code: string }
36
53
 
37
54
  type ResolvedCharge = {
38
55
  chargeCode: string
@@ -46,6 +63,11 @@ type ResolvedCharge = {
46
63
  breakdown?: { tiers: TierBreakdownEntry[] } | null
47
64
  note?: string | null
48
65
  adjustments?: RuleAdjustment[] | null
66
+ listUnitPrice?: number
67
+ listTotalPrice?: number | null
68
+ standardUnitPrice?: number
69
+ standardTotalPrice?: number | null
70
+ adjustmentErrors?: AdjustmentErrorEntry[]
49
71
  }
50
72
 
51
73
  type QuoteLineAction = 'add' | 'modify' | 'cancel'
@@ -61,6 +83,7 @@ type ArcLineSource = {
61
83
  type QuoteLine = {
62
84
  lineId: string
63
85
  offeringId: string | null
86
+ productId?: string | null
64
87
  offeringName: string
65
88
  offeringType: string | null
66
89
  parentLineId: string | null
@@ -84,15 +107,23 @@ type QuoteResult = {
84
107
  customerName: string | null
85
108
  cpqStatus: string
86
109
  version: number
110
+ pricingRevision?: number
87
111
  currencyCode: string
88
112
  convertedOrderId: string | null
89
113
  validationResult: { valid: boolean; errors: Array<{ message: string }> }
114
+ adjustmentErrors?: AdjustmentErrorEntry[]
90
115
  pricingSummary: {
91
116
  nrcTotal: number
92
117
  mrcTotal: number
93
118
  usageCharges: Array<{ chargeCode: string; chargeName: string; unitPrice: number; note: string | null }>
94
119
  usageTotalEstimated: number | null
95
120
  currencyCode: string
121
+ listNrcTotal?: number
122
+ listMrcTotal?: number
123
+ dealValue?: { list: number; standard: number; offer: number }
124
+ discountDepth?: { rule: number | null; manual: number | null; total: number | null; maxLineManual: number | null }
125
+ ruleDiscountTotal?: number
126
+ manualDiscountTotal?: number
96
127
  }
97
128
  lines: QuoteLine[]
98
129
  }
@@ -138,20 +169,6 @@ function fmt(amount: number, currency: string): string {
138
169
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
139
170
  }
140
171
 
141
- const RULE_TYPE_LABELS: Record<string, string> = {
142
- discount_percent: '% discount',
143
- discount_absolute: 'fixed discount',
144
- surcharge_percent: '% surcharge',
145
- surcharge_absolute: 'fixed surcharge',
146
- price_override: 'override',
147
- }
148
-
149
- function fmtRuleValue(adj: RuleAdjustment, currency?: string): string {
150
- if (adj.ruleType === 'discount_percent' || adj.ruleType === 'surcharge_percent') return `${adj.value}%`
151
- if (currency) return fmt(adj.value, currency)
152
- return String(adj.value)
153
- }
154
-
155
172
  async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
156
173
  const res = await fetch(url, init)
157
174
  const json = await res.json()
@@ -172,6 +189,8 @@ function MetaCard({ label, children }: { label: string; children: React.ReactNod
172
189
  )
173
190
  }
174
191
 
192
+ // Pricing-summary figure: label, monospace amount, and an optional smaller
193
+ // bracketed note (discount depth / change %) inline after the amount.
175
194
  function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
176
195
  // Local dropdown — `ActionsDropdown` inside FormHeader.actionsContent
177
196
  // would conflict with the inline button placement. This is a thin
@@ -231,26 +250,31 @@ function MoreActionsDropdown({ items }: { items: ActionItem[] }) {
231
250
 
232
251
  function buildMoreActions({
233
252
  recalculate,
253
+ canRecalculate,
234
254
  submitting,
235
255
  deletable,
236
256
  onDelete,
237
257
  deleting,
238
258
  }: {
239
259
  recalculate: () => void
260
+ canRecalculate: boolean
240
261
  submitting: boolean
241
262
  deletable: boolean
242
263
  onDelete: () => void
243
264
  deleting: boolean
244
265
  }): ActionItem[] {
245
- const items: ActionItem[] = [
246
- {
266
+ const items: ActionItem[] = []
267
+ // Recalculate persists prices: offered only to users with cpq.quotes.manage
268
+ // and only on non-terminal quotes (the API refuses both otherwise).
269
+ if (canRecalculate) {
270
+ items.push({
247
271
  id: 'recalculate',
248
272
  label: 'Recalculate',
249
273
  onSelect: recalculate,
250
274
  loading: submitting,
251
275
  disabled: submitting,
252
- },
253
- ]
276
+ })
277
+ }
254
278
  if (deletable) {
255
279
  items.push({
256
280
  id: 'delete',
@@ -272,7 +296,9 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
272
296
  const cpqConfigId = (props.params?.id ?? urlParams?.id) as string
273
297
  const isNew = cpqConfigId === 'new'
274
298
 
299
+ const t = useT()
275
300
  const [cpqQuote, setCpqQuote] = React.useState<QuoteResult | null>(null)
301
+ const grantedFeatures = useGrantedFeatures([QUOTE_MANAGE_FEATURE, ADJUST_PRICE_FEATURE])
276
302
  const [loading, setLoading] = React.useState(true)
277
303
  const [error, setError] = React.useState<string | null>(null)
278
304
  const [submitting, setSubmitting] = React.useState(false)
@@ -327,6 +353,22 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
327
353
  void loadArcTargets()
328
354
  }, [cpqConfigId, isNew, loadArcTargets])
329
355
 
356
+ // ─── Negotiated prices ─────────────────────────────────────────
357
+ const reloadQuote = React.useCallback(async () => {
358
+ try {
359
+ setCpqQuote(await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqConfigId}`))
360
+ } catch (err) {
361
+ setError((err as Error).message)
362
+ }
363
+ }, [cpqConfigId])
364
+ const priceAdjustments = usePriceAdjustments<QuoteResult>({
365
+ quote: isNew ? null : cpqQuote,
366
+ quoteType: arcQuoteType,
367
+ canAdjust: grantedFeatures.includes(QUOTE_MANAGE_FEATURE) && grantedFeatures.includes(ADJUST_PRICE_FEATURE),
368
+ onQuote: setCpqQuote,
369
+ reloadQuote,
370
+ })
371
+
330
372
  // ─── Load or create ──────────────────────────────────────────
331
373
 
332
374
  React.useEffect(() => {
@@ -436,14 +478,15 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
436
478
  finally { setSubmitting(false) }
437
479
  }
438
480
 
439
- const removeItem = async (lineId: string) => {
440
- if (!cpqQuote) return
441
- try {
442
- setError(null)
443
- const result = await apiJson<QuoteResult>(`/api/cpq/quotes/${cpqQuote.id}/items/${lineId}`, { method: 'DELETE' })
444
- setCpqQuote(result)
445
- } catch (err) { setError((err as Error).message) }
446
- }
481
+ // Amend / Renew: removing an existing subscription item confirms first and
482
+ // marks it for removal (a `cancel` line) instead of deleting the line.
483
+ const lineRemoval = useArcLineRemoval<QuoteResult>({
484
+ quote: isNew ? null : cpqQuote,
485
+ quoteType: arcQuoteType,
486
+ onQuote: setCpqQuote,
487
+ onError: setError,
488
+ })
489
+ const removeItem = lineRemoval.remove
447
490
 
448
491
  const recalculate = async () => {
449
492
  if (!cpqQuote) return
@@ -582,6 +625,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
582
625
  // ─── Render: detail ──────────────────────────────────────────
583
626
 
584
627
  const { pricingSummary: summary, currencyCode: currency } = cpqQuote
628
+ const arcDelta = arcQuoteType === 'amend' || arcQuoteType === 'renew' ? arcRecurringDelta(cpqQuote.lines) : null
585
629
 
586
630
  const arcEditable = ['new', 'incomplete', 'ready'].includes(cpqQuote.cpqStatus)
587
631
  const isMergeRenew =
@@ -601,7 +645,11 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
601
645
  }
602
646
 
603
647
  return (
648
+ <PriceAdjustmentProvider value={priceAdjustments.ui}>
649
+ <ArcLineRemovalProvider value={lineRemoval.ui}>
604
650
  <div className="space-y-6">
651
+ {priceAdjustments.elements}
652
+ {lineRemoval.ConfirmDialogElement}
605
653
  {arcDrawerOpen && (
606
654
  <ArcQuoteConfigurator
607
655
  quoteId={cpqQuote.id}
@@ -693,6 +741,7 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
693
741
  <MoreActionsDropdown
694
742
  items={buildMoreActions({
695
743
  recalculate,
744
+ canRecalculate: canRecalculateQuote(grantedFeatures, cpqQuote.cpqStatus),
696
745
  submitting,
697
746
  deletable: !['accepted', 'rejected', 'cancelled'].includes(cpqQuote.cpqStatus),
698
747
  onDelete: () => setShowDeleteConfirm(true),
@@ -816,43 +865,17 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
816
865
  {/* Summary view */}
817
866
  {view === 'summary' && (
818
867
  <>
819
- {/* Pricing Summary Card */}
820
- <div className="rounded-lg border bg-card p-4">
821
- <h3 className="text-sm font-medium mb-3">Pricing Summary</h3>
822
- <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
823
- <div>
824
- <p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
825
- <p className="text-lg font-semibold font-mono">{fmt(summary.nrcTotal, currency)}</p>
826
- </div>
827
- <div>
828
- <p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
829
- <p className="text-lg font-semibold font-mono">{fmt(summary.mrcTotal, currency)}</p>
830
- </div>
831
- {summary.usageTotalEstimated != null && (
832
- <div>
833
- <p className="text-xs text-muted-foreground">Usage (est.)</p>
834
- <p className="text-lg font-semibold font-mono">{fmt(summary.usageTotalEstimated, currency)}</p>
835
- </div>
836
- )}
837
- {summary.usageCharges.length > 0 && (
838
- <div>
839
- <p className="text-xs text-muted-foreground">Usage Charges</p>
840
- {summary.usageCharges.map((uc, i) => (
841
- <p key={i} className="text-xs mt-0.5">{uc.chargeName}: {fmt(uc.unitPrice, currency)}/unit</p>
842
- ))}
843
- </div>
844
- )}
845
- </div>
846
- </div>
868
+ <PricingSummaryCard summary={summary} currency={currency} arcDelta={arcDelta} />
847
869
 
848
870
  {/* Validation errors */}
849
871
  {!cpqQuote.validationResult.valid && (
850
872
  <Alert variant="warning">
851
873
  <AlertTitle>Validation Issues</AlertTitle>
852
874
  <AlertDescription>
853
- {/* AlertDescription renders a <p>, so the error list MUST be
854
- stacked <span>s — `<ul>`/`<li>` inside `<p>` is invalid
855
- HTML and Next 16 surfaces it as a hydration error. */}
875
+ {/* Stacked <span>s rather than <ul>/<li>: before Open Mercato
876
+ 0.8 AlertDescription rendered a <p>, where a list is invalid
877
+ HTML and a Next 16 hydration error. It is a <div> now, so a
878
+ list would be fine; the spans are kept as harmless. */}
856
879
  <span className="text-xs space-y-0.5 block">
857
880
  {cpqQuote.validationResult.errors.map((e, i) => (
858
881
  <span key={i} className="block">— {e.message}</span>
@@ -862,6 +885,23 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
862
885
  </Alert>
863
886
  )}
864
887
 
888
+ {/* Amend lines the rep reconfigured are re-priced at today's catalog
889
+ price (server marks them `_arcRepriced`); negotiated prices are
890
+ not carried over yet (later release). */}
891
+ {arcQuoteType === 'amend' && cpqQuote.lines.some((l) => l.action !== 'cancel' && l.configuration?._arcRepriced === true) && (
892
+ <Alert status="information" style="lighter">
893
+ <AlertTitle>
894
+ {t('cpq.quotes.arc.repricedTitle', 'Some existing items use the current catalog price')}
895
+ </AlertTitle>
896
+ <AlertDescription>
897
+ {t(
898
+ 'cpq.quotes.arc.repricedDescription',
899
+ "Reconfigured lines were re-priced at today's catalog price. Negotiated prices from the subscription are not carried over to reconfigured lines yet.",
900
+ )}
901
+ </AlertDescription>
902
+ </Alert>
903
+ )}
904
+
865
905
  {/* Quote Lines */}
866
906
  <QuoteLineTree
867
907
  lines={cpqQuote.lines}
@@ -881,6 +921,15 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
881
921
  )
882
922
  }
883
923
  />
924
+
925
+ <PriceAdjustmentsPanel
926
+ quoteId={cpqQuote.id}
927
+ items={priceAdjustments.items}
928
+ total={priceAdjustments.total}
929
+ lines={cpqQuote.lines}
930
+ currency={currency}
931
+ errorsById={priceAdjustments.errorsById}
932
+ />
884
933
  </>
885
934
  )}
886
935
 
@@ -959,6 +1008,8 @@ export default function CpqQuoteDetailPage(props: { params?: { id?: string } })
959
1008
  />
960
1009
  )}
961
1010
  </div>
1011
+ </ArcLineRemovalProvider>
1012
+ </PriceAdjustmentProvider>
962
1013
  )
963
1014
  }
964
1015
 
@@ -1214,11 +1265,20 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
1214
1265
  onAddComponent?: () => void
1215
1266
  onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
1216
1267
  }) {
1268
+ const t = useT()
1269
+ const adjustmentUi = usePriceAdjustmentUi()
1270
+ const removalUi = useArcLineRemovalUi()
1271
+ const isRemoved = removalUi?.isRemoved(line) ?? false
1217
1272
  const paddingLeft = indent > 0 ? `${indent * 2 + 1}rem` : undefined
1273
+ const chargeActions = {
1274
+ reasonLabels: adjustmentUi?.reasonLabels,
1275
+ renderChargeActions: (charge: ResolvedCharge) => <AdjustChargeAction line={line} charge={charge} />,
1276
+ renderManualActions: (adjustmentId: string) => <ManualAdjustmentActions adjustmentId={adjustmentId} />,
1277
+ }
1218
1278
 
1219
1279
  return (
1220
1280
  <div>
1221
- <div className="flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors" style={paddingLeft ? { paddingLeft } : undefined}>
1281
+ <div className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors ${isRemoved ? 'bg-muted/20' : ''}`} style={paddingLeft ? { paddingLeft } : undefined} data-testid="cpq-quote-line" data-removed={isRemoved ? 'true' : undefined}>
1222
1282
  {/* Expand toggle */}
1223
1283
  <button onClick={onToggle} className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground">
1224
1284
  <svg className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
@@ -1235,9 +1295,25 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
1235
1295
  {isBundle && (
1236
1296
  <Tag variant="brand" className="px-1.5 text-xs">bundle</Tag>
1237
1297
  )}
1238
- <span className="font-medium text-sm">{line.offeringName}</span>
1298
+ <span className={`font-medium text-sm ${isRemoved ? 'text-muted-foreground line-through' : ''}`}>{line.offeringName}</span>
1239
1299
  {line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
1240
- <ActionBadge action={line.action} />
1300
+ <LineActionBadge
1301
+ action={line.action}
1302
+ label={isRemoved ? t('cpq.quotes.arc.lineRemoved', 'remove') : undefined}
1303
+ title={isRemoved ? t('cpq.quotes.arc.lineRemovedHint', 'This item is cancelled on the subscription when the order is activated') : undefined}
1304
+ />
1305
+ {!isRemoved && line.configuration?._arcPriceFrozen === true && (
1306
+ <Tag
1307
+ variant="neutral"
1308
+ className="px-1.5 text-xs"
1309
+ title={t(
1310
+ 'cpq.quotes.arc.priceKeptHint',
1311
+ 'Price kept from the subscription — it changes only if you reconfigure this line',
1312
+ )}
1313
+ >
1314
+ {t('cpq.quotes.arc.priceKept', 'price kept')}
1315
+ </Tag>
1316
+ )}
1241
1317
  {targetCodes.size >= 2 && line.targetSubscriptionId && targetCodes.get(line.targetSubscriptionId) && (
1242
1318
  <Tag
1243
1319
  variant="brand"
@@ -1247,7 +1323,7 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
1247
1323
  → {targetCodes.get(line.targetSubscriptionId)}
1248
1324
  </Tag>
1249
1325
  )}
1250
- {line.isConfigured ? (
1326
+ {isRemoved ? null : line.isConfigured ? (
1251
1327
  <Tag variant="success" className="px-1.5 text-xs">configured</Tag>
1252
1328
  ) : (
1253
1329
  <Tag variant="warning" className="px-1.5 text-xs">incomplete</Tag>
@@ -1263,12 +1339,32 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
1263
1339
 
1264
1340
  {/* Pricing */}
1265
1341
  <div className="shrink-0 text-right text-xs space-y-0.5">
1342
+ {isRemoved && line.arcSource && line.arcSource.mrcAmount > 0 && (
1343
+ <div>
1344
+ <span className="text-muted-foreground">MRC </span>
1345
+ <span className="font-mono text-muted-foreground line-through">{fmt(line.arcSource.mrcAmount, currency)}</span>
1346
+ </div>
1347
+ )}
1266
1348
  {line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
1267
1349
  {line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
1268
1350
  </div>
1269
1351
 
1270
1352
  {/* Actions */}
1353
+ {isRemoved ? (
1354
+ <div className="flex items-center gap-1 shrink-0">
1355
+ <Button
1356
+ type="button" variant="ghost" size="sm"
1357
+ onClick={() => removalUi?.restore(line.lineId)}
1358
+ title={t('cpq.quotes.arc.restoreHint', 'Keep this item on the subscription')}
1359
+ data-testid="cpq-line-restore"
1360
+ >
1361
+ <RotateCcw className="h-4 w-4" />
1362
+ {t('cpq.quotes.arc.restore', 'Restore')}
1363
+ </Button>
1364
+ </div>
1365
+ ) : (
1271
1366
  <div className="flex items-center gap-1 shrink-0">
1367
+ <DiscountLineAction line={line} />
1272
1368
  {onAddComponent && (
1273
1369
  <button onClick={onAddComponent} className="rounded p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors" title="Add Component">
1274
1370
  <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
@@ -1287,19 +1383,22 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
1287
1383
  </svg>
1288
1384
  </button>
1289
1385
  </div>
1386
+ )}
1290
1387
  </div>
1291
1388
 
1292
- {/* XD-250 ARC: before/after preview when line mirrors a sub item */}
1293
- {isExpanded && line.arcSource && (
1294
- <ArcLineDiff line={line} currency={currency} />
1295
- )}
1296
1389
  {/* Charge breakdown (non-bundle or any expanded line) */}
1297
1390
  {isExpanded && !isBundle && line.charges.length > 0 && (
1298
- <ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} />
1391
+ <ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} {...chargeActions} />
1299
1392
  )}
1300
1393
  {/* For bundles: show own charges first, then children are rendered by parent */}
1301
1394
  {isExpanded && isBundle && line.charges.length > 0 && (
1302
- <ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} />
1395
+ <ChargeBreakdown line={line} lineIdx={lineIdx} currency={currency} pricingDetail={pricingDetail} onTogglePricingDetail={onTogglePricingDetail} {...chargeActions} />
1396
+ )}
1397
+ {/* XD-250 ARC: before/after preview, below the charges, when line mirrors a sub item */}
1398
+ {isExpanded && line.arcSource && (
1399
+ <div className="border-t bg-muted/10 px-12 py-2">
1400
+ <ArcLineDiff line={line} currency={currency} />
1401
+ </div>
1303
1402
  )}
1304
1403
  {/* Fallback: line has no per-charge breakdown and no ARC diff (truly empty) */}
1305
1404
  {isExpanded && line.charges.length === 0 && !line.arcSource && (
@@ -1309,77 +1408,6 @@ function QuoteLineRow({ line, lineIdx, currency, isExpanded, pricingDetail, isBu
1309
1408
  )
1310
1409
  }
1311
1410
 
1312
- function ArcLineDiff({ line, currency }: { line: QuoteLine; currency: string }) {
1313
- const src = line.arcSource
1314
- if (!src) return null
1315
- const isCancel = line.action === 'cancel'
1316
- const beforeMrc = src.mrcAmount
1317
- const beforeNrc = src.nrcAmount
1318
- const beforeQty = src.quantity
1319
- const afterMrc = isCancel ? 0 : line.mrcTotal
1320
- const afterNrc = isCancel ? 0 : line.nrcTotal
1321
- const afterQty = isCancel ? 0 : line.quantity
1322
- const fmtVal = (v: number, asCurrency: boolean) =>
1323
- asCurrency ? fmt(v, currency) : String(v)
1324
- const rows: Array<{ label: string; before: number; after: number; asCurrency: boolean }> = [
1325
- { label: 'Quantity', before: beforeQty, after: afterQty, asCurrency: false },
1326
- { label: 'MRC', before: beforeMrc, after: afterMrc, asCurrency: true },
1327
- { label: 'NRC', before: beforeNrc, after: afterNrc, asCurrency: true },
1328
- ]
1329
-
1330
- return (
1331
- <div className="border-t bg-muted/10 px-12 py-2 space-y-1.5">
1332
- <p className="text-xs font-medium text-muted-foreground">Change Preview</p>
1333
- <div className="rounded border bg-background overflow-hidden">
1334
- <table className="w-full text-xs">
1335
- <thead className="bg-muted/40">
1336
- <tr className="text-muted-foreground">
1337
- <th className="text-left px-3 py-1.5 font-medium" />
1338
- <th className="text-right px-3 py-1.5 font-medium">Before</th>
1339
- <th className="text-right px-3 py-1.5 font-medium">After</th>
1340
- <th className="text-right px-3 py-1.5 font-medium">Δ</th>
1341
- </tr>
1342
- </thead>
1343
- <tbody>
1344
- {rows.map((r) => {
1345
- const delta = r.after - r.before
1346
- const showDelta = delta !== 0
1347
- return (
1348
- <tr key={r.label} className="border-t border-dashed border-border/40">
1349
- <td className="px-3 py-1 text-muted-foreground">{r.label}</td>
1350
- <td className="px-3 py-1 text-right font-mono">{fmtVal(r.before, r.asCurrency)}</td>
1351
- <td className={`px-3 py-1 text-right font-mono ${isCancel ? 'text-muted-foreground line-through' : ''}`}>
1352
- {fmtVal(r.after, r.asCurrency)}
1353
- </td>
1354
- <td className={`px-3 py-1 text-right font-mono font-medium ${showDelta ? (delta > 0 ? 'text-status-success-text' : 'text-destructive') : 'text-muted-foreground'}`}>
1355
- {showDelta ? `${delta > 0 ? '+' : ''}${fmtVal(delta, r.asCurrency)}` : '—'}
1356
- </td>
1357
- </tr>
1358
- )
1359
- })}
1360
- </tbody>
1361
- </table>
1362
- </div>
1363
- {isCancel && (
1364
- <p className="text-xs text-destructive italic">Cancellation — item will be terminated on activation.</p>
1365
- )}
1366
- </div>
1367
- )
1368
- }
1369
-
1370
- function ActionBadge({ action }: { action: QuoteLineAction }) {
1371
- const variantByAction: Record<QuoteLineAction, 'info' | 'warning' | 'error'> = {
1372
- add: 'info',
1373
- modify: 'warning',
1374
- cancel: 'error',
1375
- }
1376
- return (
1377
- <Tag variant={variantByAction[action]} className="px-1.5 text-xs">
1378
- {action}
1379
- </Tag>
1380
- )
1381
- }
1382
-
1383
1411
  function LineSummaryFallback({ line, currency }: { line: QuoteLine; currency: string }) {
1384
1412
  const hasTotals = line.nrcTotal > 0 || line.mrcTotal > 0
1385
1413
  const mirroredFromItemId = (line.configuration?._arcMirroredFromItemId as string | undefined) ?? null
@@ -1403,138 +1431,22 @@ function LineSummaryFallback({ line, currency }: { line: QuoteLine; currency: st
1403
1431
  )
1404
1432
  }
1405
1433
 
1406
- function ChargeBreakdown({ line, lineIdx, currency, pricingDetail, onTogglePricingDetail }: {
1434
+ function ChargeBreakdown({ line, lineIdx, pricingDetail, onTogglePricingDetail, ...tableProps }: {
1407
1435
  line: QuoteLine; lineIdx: number; currency: string
1408
1436
  pricingDetail: { lineIdx: number; chargeIdx: number } | null
1409
1437
  onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
1438
+ reasonLabels?: Record<string, string>
1439
+ renderChargeActions?: (charge: ResolvedCharge) => React.ReactNode
1440
+ renderManualActions?: (adjustmentId: string) => React.ReactNode
1410
1441
  }) {
1411
1442
  return (
1412
1443
  <div className="border-t bg-muted/10 px-12 py-2">
1413
- <table className="w-full text-xs">
1414
- <thead>
1415
- <tr className="text-muted-foreground">
1416
- <th className="text-left py-1 font-medium">Charge</th>
1417
- <th className="text-left py-1 font-medium">Type</th>
1418
- <th className="text-right py-1 font-medium">Unit Price</th>
1419
- <th className="text-right py-1 font-medium">Qty</th>
1420
- <th className="text-right py-1 font-medium">Total</th>
1421
- <th className="w-8" />
1422
- </tr>
1423
- </thead>
1424
- <tbody>
1425
- {line.charges.map((c, i) => {
1426
- const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)
1427
- const isDetailOpen = pricingDetail?.lineIdx === lineIdx && pricingDetail?.chargeIdx === i
1428
- return (
1429
- <React.Fragment key={i}>
1430
- <tr className="border-t border-dashed border-border/50">
1431
- <td className="py-1">{c.chargeName}</td>
1432
- <td className="py-1 uppercase">{c.chargeType}</td>
1433
- <td className="py-1 text-right font-mono">{fmt(c.unitPrice, currency)}</td>
1434
- <td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
1435
- <td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : c.note ?? '—'}</td>
1436
- <td className="py-1 text-center">
1437
- {hasDetail && (
1438
- <button
1439
- onClick={() => onTogglePricingDetail(lineIdx, i)}
1440
- className="inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
1441
- title="Show pricing breakdown"
1442
- >
1443
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3.5 h-3.5">
1444
- <path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
1445
- </svg>
1446
- </button>
1447
- )}
1448
- </td>
1449
- </tr>
1450
- {isDetailOpen && (
1451
- <tr>
1452
- <td colSpan={6} className="pb-3 pt-1">
1453
- <div className="rounded border bg-background p-3 space-y-3 text-xs">
1454
- <div className="flex items-center justify-between">
1455
- <span className="font-semibold text-sm">Pricing Details — {c.chargeName}</span>
1456
- <span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
1457
- </div>
1458
-
1459
- {c.breakdown?.tiers && c.breakdown.tiers.length > 0 && (
1460
- <div>
1461
- <div className="font-medium text-muted-foreground mb-1">Tier Breakdown</div>
1462
- <table className="w-full">
1463
- <thead>
1464
- <tr className="text-muted-foreground">
1465
- <th className="text-left py-0.5 font-medium">Tier</th>
1466
- <th className="text-right py-0.5 font-medium">Range</th>
1467
- <th className="text-right py-0.5 font-medium">Price/Unit</th>
1468
- <th className="text-right py-0.5 font-medium">Qty</th>
1469
- <th className="text-right py-0.5 font-medium">Subtotal</th>
1470
- </tr>
1471
- </thead>
1472
- <tbody>
1473
- {c.breakdown.tiers.map((t) => (
1474
- <tr key={t.tier} className="border-t border-border/30">
1475
- <td className="py-0.5">{t.tier}</td>
1476
- <td className="py-0.5 text-right font-mono">{t.from}–{t.to}</td>
1477
- <td className="py-0.5 text-right font-mono">{fmt(t.pricePerUnit, currency)}</td>
1478
- <td className="py-0.5 text-right font-mono">{t.quantity}</td>
1479
- <td className="py-0.5 text-right font-mono">{fmt(t.subtotal, currency)}</td>
1480
- </tr>
1481
- ))}
1482
- <tr className="border-t font-medium">
1483
- <td colSpan={4} className="py-0.5 text-right">Tiered Total</td>
1484
- <td className="py-0.5 text-right font-mono">
1485
- {fmt(c.breakdown.tiers.reduce((s, t) => s + t.subtotal, 0), currency)}
1486
- </td>
1487
- </tr>
1488
- </tbody>
1489
- </table>
1490
- </div>
1491
- )}
1492
-
1493
- {c.adjustments && c.adjustments.length > 0 && (
1494
- <div>
1495
- <div className="font-medium text-muted-foreground mb-1">Applied Price Rules</div>
1496
- <table className="w-full">
1497
- <thead>
1498
- <tr className="text-muted-foreground">
1499
- <th className="text-left py-0.5 font-medium">Rule</th>
1500
- <th className="text-left py-0.5 font-medium">Type</th>
1501
- <th className="text-right py-0.5 font-medium">Value</th>
1502
- <th className="text-right py-0.5 font-medium">Before</th>
1503
- <th className="text-right py-0.5 font-medium">After</th>
1504
- <th className="text-right py-0.5 font-medium">Delta</th>
1505
- </tr>
1506
- </thead>
1507
- <tbody>
1508
- {c.adjustments.map((adj, ai) => (
1509
- <tr key={ai} className="border-t border-border/30">
1510
- <td className="py-0.5">{adj.ruleName}</td>
1511
- <td className="py-0.5">{RULE_TYPE_LABELS[adj.ruleType] ?? adj.ruleType}</td>
1512
- <td className="py-0.5 text-right font-mono">{fmtRuleValue(adj, currency)}</td>
1513
- <td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceBefore, currency)}</td>
1514
- <td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceAfter, currency)}</td>
1515
- <td className={`py-0.5 text-right font-mono ${adj.delta < 0 ? 'text-destructive' : adj.delta > 0 ? 'text-status-success-text' : ''}`}>
1516
- {adj.delta > 0 ? '+' : ''}{fmt(adj.delta, currency)}
1517
- </td>
1518
- </tr>
1519
- ))}
1520
- </tbody>
1521
- </table>
1522
- </div>
1523
- )}
1524
-
1525
- <div className="flex items-center justify-between pt-1 border-t text-sm font-semibold">
1526
- <span>Final Total</span>
1527
- <span className="font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : '—'}</span>
1528
- </div>
1529
- </div>
1530
- </td>
1531
- </tr>
1532
- )}
1533
- </React.Fragment>
1534
- )
1535
- })}
1536
- </tbody>
1537
- </table>
1444
+ <ChargeBreakdownTable
1445
+ charges={line.charges}
1446
+ openDetailIndex={pricingDetail?.lineIdx === lineIdx ? pricingDetail.chargeIdx : null}
1447
+ onToggleDetail={(chargeIdx) => onTogglePricingDetail(lineIdx, chargeIdx)}
1448
+ {...tableProps}
1449
+ />
1538
1450
  </div>
1539
1451
  )
1540
1452
  }