@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
@@ -0,0 +1,205 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
6
+ import { Button } from "@open-mercato/ui/primitives/button";
7
+ import { Input } from "@open-mercato/ui/primitives/input";
8
+ import { Textarea } from "@open-mercato/ui/primitives/textarea";
9
+ import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
10
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@open-mercato/ui/primitives/dialog";
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@open-mercato/ui/primitives/select";
12
+ import {
13
+ ADJUSTMENT_ERROR_FALLBACKS,
14
+ adjustmentErrorCode,
15
+ allowedAdjustmentTypes,
16
+ buildAdjustmentDraft,
17
+ formatDepth,
18
+ discountDepth,
19
+ toAdjustmentPatch
20
+ } from "../../../../../lib/priceAdjustmentView.js";
21
+ const TYPE_FALLBACKS = {
22
+ price_override: "Set a new unit price",
23
+ discount_percent: "Percentage discount",
24
+ discount_absolute: "Amount off the charge total"
25
+ };
26
+ function money(amount, currency) {
27
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
28
+ }
29
+ function PriceAdjustmentDialog(props) {
30
+ const { open, quoteId, currency, pricingRevision, target, targetLabel, charge, existing, storedAdjustmentIds, reasons, onClose, onSaved, onStale } = props;
31
+ const t = useT();
32
+ const types = allowedAdjustmentTypes(target.level, charge);
33
+ const initialForm = React.useCallback(() => ({
34
+ adjustmentType: existing?.adjustmentType ?? types[0] ?? "discount_percent",
35
+ value: existing ? String(existing.value) : "",
36
+ reasonCode: existing?.reasonCode ?? "",
37
+ reasonNote: existing?.reasonNote ?? "",
38
+ chargeTypeFilter: existing?.chargeTypeFilter ?? ""
39
+ }), [existing, types]);
40
+ const [form, setForm] = React.useState(initialForm);
41
+ const [preview, setPreview] = React.useState(null);
42
+ const [previewing, setPreviewing] = React.useState(false);
43
+ const [errorCode, setErrorCode] = React.useState(null);
44
+ const [saving, setSaving] = React.useState(false);
45
+ React.useEffect(() => {
46
+ if (open) {
47
+ setForm(initialForm());
48
+ setPreview(null);
49
+ setErrorCode(null);
50
+ }
51
+ }, [open]);
52
+ const draft = buildAdjustmentDraft(form, target, currency, pricingRevision);
53
+ const draftKey = draft && form.reasonCode ? JSON.stringify(draft) : null;
54
+ const message = (code) => t(`cpq.priceAdjustments.errors.${code}`, ADJUSTMENT_ERROR_FALLBACKS[code] ?? code);
55
+ React.useEffect(() => {
56
+ if (!open || !draftKey) {
57
+ setPreview(null);
58
+ return;
59
+ }
60
+ let cancelled = false;
61
+ const timer = window.setTimeout(async () => {
62
+ setPreviewing(true);
63
+ const call = await apiCall(
64
+ `/api/cpq/quotes/${quoteId}/price-adjustments/preview`,
65
+ { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ draft: JSON.parse(draftKey), ...existing ? { replaceAdjustmentId: existing.id } : {} }) }
66
+ );
67
+ if (cancelled) return;
68
+ setPreviewing(false);
69
+ if (call.ok && call.result) {
70
+ setPreview(call.result);
71
+ setErrorCode(null);
72
+ } else {
73
+ setPreview(null);
74
+ setErrorCode(adjustmentErrorCode(call.status, call.result));
75
+ }
76
+ }, 400);
77
+ return () => {
78
+ cancelled = true;
79
+ window.clearTimeout(timer);
80
+ };
81
+ }, [open, draftKey, quoteId, existing]);
82
+ const previewLine = preview?.quote.lines.find((line) => line.lineId === target.quoteLineId);
83
+ const previewCharge = target.level === "charge" ? previewLine?.charges.find((entry) => entry.chargeCode === target.chargeCode) : void 0;
84
+ const previewErrors = preview?.errors.filter((error) => existing ? error.adjustmentId === existing.id : !storedAdjustmentIds.includes(error.adjustmentId)) ?? [];
85
+ const noteRequired = form.reasonCode === "other" && !form.reasonNote.trim();
86
+ const canSave = !!draft && !!form.reasonCode && !noteRequired && !saving && previewErrors.length === 0;
87
+ const save = async () => {
88
+ if (!draft || !canSave) return;
89
+ setSaving(true);
90
+ setErrorCode(null);
91
+ const call = existing ? await apiCall(`/api/cpq/quotes/${quoteId}/price-adjustments/${existing.id}`, {
92
+ method: "PATCH",
93
+ headers: { "content-type": "application/json" },
94
+ body: JSON.stringify(toAdjustmentPatch(draft))
95
+ }) : await apiCall(`/api/cpq/quotes/${quoteId}/price-adjustments`, {
96
+ method: "POST",
97
+ headers: { "content-type": "application/json" },
98
+ body: JSON.stringify(draft)
99
+ });
100
+ setSaving(false);
101
+ if (call.ok && call.result?.quote) {
102
+ onSaved(call.result.quote);
103
+ onClose();
104
+ return;
105
+ }
106
+ setErrorCode(adjustmentErrorCode(call.status, call.result));
107
+ };
108
+ const onKeyDown = (event) => {
109
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
110
+ event.preventDefault();
111
+ void save();
112
+ }
113
+ };
114
+ const standardUnit = charge?.standardUnitPrice ?? charge?.unitPrice;
115
+ const valueHint = form.adjustmentType === "discount_percent" ? t("cpq.priceAdjustments.dialog.percentHint", "Percent off the standard price (0\u2013100)") : form.adjustmentType === "price_override" ? t("cpq.priceAdjustments.dialog.overrideHint", "New unit price, at most the standard price") : t("cpq.priceAdjustments.dialog.absoluteHint", "Amount taken off this charge\u2019s total");
116
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (next) => {
117
+ if (!next) onClose();
118
+ }, children: /* @__PURE__ */ jsxs(DialogContent, { onKeyDown, "data-testid": "cpq-price-adjustment-dialog", children: [
119
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
120
+ /* @__PURE__ */ jsx(DialogTitle, { children: existing ? t("cpq.priceAdjustments.dialog.editTitle", "Edit price adjustment") : target.level === "line" ? t("cpq.priceAdjustments.dialog.lineTitle", "Discount line") : t("cpq.priceAdjustments.dialog.chargeTitle", "Adjust price") }),
121
+ /* @__PURE__ */ jsxs(DialogDescription, { children: [
122
+ targetLabel,
123
+ charge && standardUnit !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
124
+ " \xB7 ",
125
+ t("cpq.priceAdjustments.dialog.standard", "Standard"),
126
+ " ",
127
+ money(standardUnit, currency)
128
+ ] })
129
+ ] })
130
+ ] }),
131
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3 text-sm", children: [
132
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1", children: [
133
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: t("cpq.priceAdjustments.dialog.type", "Adjustment") }),
134
+ /* @__PURE__ */ jsxs(Select, { value: form.adjustmentType, onValueChange: (value) => setForm({ ...form, adjustmentType: value }), children: [
135
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
136
+ /* @__PURE__ */ jsx(SelectContent, { children: types.map((type) => /* @__PURE__ */ jsx(SelectItem, { value: type, children: t(`cpq.priceAdjustments.dialog.types.${type}`, TYPE_FALLBACKS[type]) }, type)) })
137
+ ] })
138
+ ] }),
139
+ target.level === "line" && /* @__PURE__ */ jsxs("label", { className: "block space-y-1", children: [
140
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: t("cpq.priceAdjustments.dialog.appliesTo", "Applies to") }),
141
+ /* @__PURE__ */ jsxs(Select, { value: form.chargeTypeFilter || "all", onValueChange: (value) => setForm({ ...form, chargeTypeFilter: value === "all" ? "" : value }), children: [
142
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
143
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
144
+ /* @__PURE__ */ jsx(SelectItem, { value: "all", children: t("cpq.priceAdjustments.dialog.allCharges", "All one-time and recurring charges") }),
145
+ /* @__PURE__ */ jsx(SelectItem, { value: "mrc", children: t("cpq.priceAdjustments.dialog.mrcOnly", "Recurring charges only") }),
146
+ /* @__PURE__ */ jsx(SelectItem, { value: "nrc", children: t("cpq.priceAdjustments.dialog.nrcOnly", "One-time charges only") })
147
+ ] })
148
+ ] })
149
+ ] }),
150
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1", children: [
151
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: t("cpq.priceAdjustments.dialog.value", "Value") }),
152
+ /* @__PURE__ */ jsx(Input, { inputMode: "decimal", autoFocus: true, value: form.value, onChange: (event) => setForm({ ...form, value: event.target.value }), "aria-describedby": "cpq-adjustment-value-hint" }),
153
+ /* @__PURE__ */ jsx("span", { id: "cpq-adjustment-value-hint", className: "block text-xs text-muted-foreground", children: valueHint })
154
+ ] }),
155
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1", children: [
156
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: t("cpq.priceAdjustments.dialog.reason", "Reason") }),
157
+ /* @__PURE__ */ jsxs(Select, { value: form.reasonCode, onValueChange: (value) => setForm({ ...form, reasonCode: value }), children: [
158
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("cpq.priceAdjustments.dialog.reasonPlaceholder", "Pick a reason") }) }),
159
+ /* @__PURE__ */ jsx(SelectContent, { children: reasons.map((reason) => /* @__PURE__ */ jsx(SelectItem, { value: reason.value, children: reason.label }, reason.value)) })
160
+ ] })
161
+ ] }),
162
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1", children: [
163
+ /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium", children: [
164
+ t("cpq.priceAdjustments.dialog.note", "Note"),
165
+ form.reasonCode === "other" ? " *" : ""
166
+ ] }),
167
+ /* @__PURE__ */ jsx(Textarea, { rows: 2, maxLength: 500, value: form.reasonNote, onChange: (event) => setForm({ ...form, reasonNote: event.target.value }) })
168
+ ] }),
169
+ /* @__PURE__ */ jsxs("div", { className: "rounded border bg-muted/20 p-2 text-xs", "aria-live": "polite", "data-testid": "cpq-adjustment-preview", children: [
170
+ !draftKey && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("cpq.priceAdjustments.dialog.previewEmpty", "Enter a value and reason to preview the new price.") }),
171
+ draftKey && previewing && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("cpq.priceAdjustments.dialog.previewing", "Calculating\u2026") }),
172
+ draftKey && !previewing && previewCharge && /* @__PURE__ */ jsxs("span", { children: [
173
+ t("cpq.priceAdjustments.dialog.previewCharge", "New offer price"),
174
+ ": ",
175
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium", children: money(previewCharge.unitPrice, currency) }),
176
+ " ",
177
+ formatDepth(discountDepth(previewCharge.standardUnitPrice ?? standardUnit, previewCharge.unitPrice)) ?? ""
178
+ ] }),
179
+ draftKey && !previewing && !previewCharge && previewLine && /* @__PURE__ */ jsxs("span", { children: [
180
+ t("cpq.priceAdjustments.dialog.previewLine", "Line totals"),
181
+ ": MRC ",
182
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium", children: money(previewLine.mrcTotal, currency) }),
183
+ " \xB7 ",
184
+ "NRC ",
185
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium", children: money(previewLine.nrcTotal, currency) })
186
+ ] })
187
+ ] }),
188
+ (errorCode || previewErrors.length > 0 || noteRequired) && /* @__PURE__ */ jsx(Alert, { variant: "warning", children: /* @__PURE__ */ jsxs(AlertDescription, { children: [
189
+ errorCode ? message(errorCode) : previewErrors.length > 0 ? message(previewErrors[0].code) : message("REASON_NOTE_REQUIRED"),
190
+ errorCode === "STALE_REVISION" && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "ml-2", onClick: () => {
191
+ onStale();
192
+ onClose();
193
+ }, children: t("cpq.priceAdjustments.dialog.reload", "Reload") })
194
+ ] }) })
195
+ ] }),
196
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
197
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("cpq.priceAdjustments.dialog.cancel", "Cancel") }),
198
+ /* @__PURE__ */ jsx(Button, { onClick: () => void save(), disabled: !canSave, title: "Ctrl/Cmd+Enter", children: saving ? t("cpq.priceAdjustments.dialog.saving", "Saving\u2026") : t("cpq.priceAdjustments.dialog.save", "Save") })
199
+ ] })
200
+ ] }) });
201
+ }
202
+ export {
203
+ PriceAdjustmentDialog
204
+ };
205
+ //# sourceMappingURL=PriceAdjustmentDialog.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/cpq/backend/cpq/quotes/%5Bid%5D/_components/PriceAdjustmentDialog.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport {\n ADJUSTMENT_ERROR_FALLBACKS,\n adjustmentErrorCode,\n allowedAdjustmentTypes,\n buildAdjustmentDraft,\n formatDepth,\n discountDepth,\n toAdjustmentPatch,\n type AdjustmentForm,\n type AdjustmentTarget,\n type AdjustmentType,\n type WaterfallChargeInput,\n} from '../../../../../lib/priceAdjustmentView'\n\nexport type AdjustmentReason = { value: string; label: string }\n\n/** Minimal view of the quote the dialog needs, kept local so the page owns the full type. */\ntype PreviewQuote = {\n pricingRevision?: number\n lines: Array<{ lineId: string; mrcTotal: number; nrcTotal: number; charges: Array<WaterfallChargeInput & { chargeCode: string; adjustmentErrors?: Array<{ adjustmentId: string; code: string }> }> }>\n}\n\nexport type ExistingAdjustment = {\n id: string\n adjustmentType: AdjustmentType\n value: number\n reasonCode: string\n reasonNote?: string | null\n chargeTypeFilter?: 'nrc' | 'mrc' | null\n}\n\nexport type PriceAdjustmentDialogProps<Q extends PreviewQuote> = {\n open: boolean\n quoteId: string\n currency: string\n pricingRevision: number\n target: AdjustmentTarget\n /** Display name of the line / charge being adjusted. */\n targetLabel: string\n /** The charge for charge-level targets (drives type options and the current price). */\n charge?: WaterfallChargeInput & { chargeCode: string; chargeName: string }\n existing?: ExistingAdjustment | null\n /** Ids of adjustments already stored on the quote; their errors don't block this draft. */\n storedAdjustmentIds: string[]\n reasons: AdjustmentReason[]\n onClose: () => void\n onSaved: (quote: Q) => void\n /** Called when the server reports a stale revision; the page reloads the quote. */\n onStale: () => void\n}\n\nconst TYPE_FALLBACKS: Record<AdjustmentType, string> = {\n price_override: 'Set a new unit price',\n discount_percent: 'Percentage discount',\n discount_absolute: 'Amount off the charge total',\n}\n\nfunction money(amount: number, currency: string) {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)\n}\n\nexport function PriceAdjustmentDialog<Q extends PreviewQuote>(props: PriceAdjustmentDialogProps<Q>) {\n const { open, quoteId, currency, pricingRevision, target, targetLabel, charge, existing, storedAdjustmentIds, reasons, onClose, onSaved, onStale } = props\n const t = useT()\n const types = allowedAdjustmentTypes(target.level, charge)\n const initialForm = React.useCallback((): AdjustmentForm => ({\n adjustmentType: existing?.adjustmentType ?? types[0] ?? 'discount_percent',\n value: existing ? String(existing.value) : '',\n reasonCode: existing?.reasonCode ?? '',\n reasonNote: existing?.reasonNote ?? '',\n chargeTypeFilter: existing?.chargeTypeFilter ?? '',\n }), [existing, types])\n const [form, setForm] = React.useState<AdjustmentForm>(initialForm)\n const [preview, setPreview] = React.useState<{ quote: Q; errors: Array<{ adjustmentId: string; code: string }> } | null>(null)\n const [previewing, setPreviewing] = React.useState(false)\n const [errorCode, setErrorCode] = React.useState<string | null>(null)\n const [saving, setSaving] = React.useState(false)\n\n React.useEffect(() => {\n if (open) { setForm(initialForm()); setPreview(null); setErrorCode(null) }\n // Reset only when the dialog (re)opens.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const draft = buildAdjustmentDraft(form, target, currency, pricingRevision)\n const draftKey = draft && form.reasonCode ? JSON.stringify(draft) : null\n const message = (code: string) => t(`cpq.priceAdjustments.errors.${code}`, ADJUSTMENT_ERROR_FALLBACKS[code] ?? code)\n\n // Debounced, side-effect-free preview of the draft.\n React.useEffect(() => {\n if (!open || !draftKey) { setPreview(null); return }\n let cancelled = false\n const timer = window.setTimeout(async () => {\n setPreviewing(true)\n const call = await apiCall<{ quote: Q; errors: Array<{ adjustmentId: string; code: string }> }>(\n `/api/cpq/quotes/${quoteId}/price-adjustments/preview`,\n { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ draft: JSON.parse(draftKey), ...(existing ? { replaceAdjustmentId: existing.id } : {}) }) },\n )\n if (cancelled) return\n setPreviewing(false)\n if (call.ok && call.result) { setPreview(call.result); setErrorCode(null) }\n else { setPreview(null); setErrorCode(adjustmentErrorCode(call.status, call.result)) }\n }, 400)\n return () => { cancelled = true; window.clearTimeout(timer) }\n }, [open, draftKey, quoteId, existing])\n\n const previewLine = preview?.quote.lines.find((line) => line.lineId === target.quoteLineId)\n const previewCharge = target.level === 'charge' ? previewLine?.charges.find((entry) => entry.chargeCode === target.chargeCode) : undefined\n // The draft's id is server-generated on create, so \"not stored\" identifies it; on edit it keeps its id.\n const previewErrors = preview?.errors.filter((error) => existing ? error.adjustmentId === existing.id : !storedAdjustmentIds.includes(error.adjustmentId)) ?? []\n const noteRequired = form.reasonCode === 'other' && !form.reasonNote.trim()\n const canSave = !!draft && !!form.reasonCode && !noteRequired && !saving && previewErrors.length === 0\n\n const save = async () => {\n if (!draft || !canSave) return\n setSaving(true); setErrorCode(null)\n const call = existing\n ? await apiCall<{ quote: Q }>(`/api/cpq/quotes/${quoteId}/price-adjustments/${existing.id}`, {\n method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify(toAdjustmentPatch(draft)),\n })\n : await apiCall<{ quote: Q }>(`/api/cpq/quotes/${quoteId}/price-adjustments`, {\n method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(draft),\n })\n setSaving(false)\n if (call.ok && call.result?.quote) { onSaved(call.result.quote); onClose(); return }\n setErrorCode(adjustmentErrorCode(call.status, call.result))\n }\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); void save() }\n }\n\n const standardUnit = charge?.standardUnitPrice ?? charge?.unitPrice\n const valueHint = form.adjustmentType === 'discount_percent'\n ? t('cpq.priceAdjustments.dialog.percentHint', 'Percent off the standard price (0\u2013100)')\n : form.adjustmentType === 'price_override'\n ? t('cpq.priceAdjustments.dialog.overrideHint', 'New unit price, at most the standard price')\n : t('cpq.priceAdjustments.dialog.absoluteHint', 'Amount taken off this charge\u2019s total')\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) onClose() }}>\n <DialogContent onKeyDown={onKeyDown} data-testid=\"cpq-price-adjustment-dialog\">\n <DialogHeader>\n <DialogTitle>\n {existing\n ? t('cpq.priceAdjustments.dialog.editTitle', 'Edit price adjustment')\n : target.level === 'line'\n ? t('cpq.priceAdjustments.dialog.lineTitle', 'Discount line')\n : t('cpq.priceAdjustments.dialog.chargeTitle', 'Adjust price')}\n </DialogTitle>\n <DialogDescription>\n {targetLabel}\n {charge && standardUnit !== undefined && (\n <> \u00B7 {t('cpq.priceAdjustments.dialog.standard', 'Standard')} {money(standardUnit, currency)}</>\n )}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-3 text-sm\">\n <label className=\"block space-y-1\">\n <span className=\"text-xs font-medium\">{t('cpq.priceAdjustments.dialog.type', 'Adjustment')}</span>\n <Select value={form.adjustmentType} onValueChange={(value) => setForm({ ...form, adjustmentType: value as AdjustmentType })}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n {types.map((type) => (\n <SelectItem key={type} value={type}>{t(`cpq.priceAdjustments.dialog.types.${type}`, TYPE_FALLBACKS[type])}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n\n {target.level === 'line' && (\n <label className=\"block space-y-1\">\n <span className=\"text-xs font-medium\">{t('cpq.priceAdjustments.dialog.appliesTo', 'Applies to')}</span>\n <Select value={form.chargeTypeFilter || 'all'} onValueChange={(value) => setForm({ ...form, chargeTypeFilter: value === 'all' ? '' : value as 'nrc' | 'mrc' })}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">{t('cpq.priceAdjustments.dialog.allCharges', 'All one-time and recurring charges')}</SelectItem>\n <SelectItem value=\"mrc\">{t('cpq.priceAdjustments.dialog.mrcOnly', 'Recurring charges only')}</SelectItem>\n <SelectItem value=\"nrc\">{t('cpq.priceAdjustments.dialog.nrcOnly', 'One-time charges only')}</SelectItem>\n </SelectContent>\n </Select>\n </label>\n )}\n\n <label className=\"block space-y-1\">\n <span className=\"text-xs font-medium\">{t('cpq.priceAdjustments.dialog.value', 'Value')}</span>\n <Input inputMode=\"decimal\" autoFocus value={form.value} onChange={(event) => setForm({ ...form, value: event.target.value })} aria-describedby=\"cpq-adjustment-value-hint\" />\n <span id=\"cpq-adjustment-value-hint\" className=\"block text-xs text-muted-foreground\">{valueHint}</span>\n </label>\n\n <label className=\"block space-y-1\">\n <span className=\"text-xs font-medium\">{t('cpq.priceAdjustments.dialog.reason', 'Reason')}</span>\n <Select value={form.reasonCode} onValueChange={(value) => setForm({ ...form, reasonCode: value })}>\n <SelectTrigger><SelectValue placeholder={t('cpq.priceAdjustments.dialog.reasonPlaceholder', 'Pick a reason')} /></SelectTrigger>\n <SelectContent>\n {reasons.map((reason) => <SelectItem key={reason.value} value={reason.value}>{reason.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </label>\n\n <label className=\"block space-y-1\">\n <span className=\"text-xs font-medium\">\n {t('cpq.priceAdjustments.dialog.note', 'Note')}{form.reasonCode === 'other' ? ' *' : ''}\n </span>\n <Textarea rows={2} maxLength={500} value={form.reasonNote} onChange={(event) => setForm({ ...form, reasonNote: event.target.value })} />\n </label>\n\n <div className=\"rounded border bg-muted/20 p-2 text-xs\" aria-live=\"polite\" data-testid=\"cpq-adjustment-preview\">\n {!draftKey && <span className=\"text-muted-foreground\">{t('cpq.priceAdjustments.dialog.previewEmpty', 'Enter a value and reason to preview the new price.')}</span>}\n {draftKey && previewing && <span className=\"text-muted-foreground\">{t('cpq.priceAdjustments.dialog.previewing', 'Calculating\u2026')}</span>}\n {draftKey && !previewing && previewCharge && (\n <span>\n {t('cpq.priceAdjustments.dialog.previewCharge', 'New offer price')}: <span className=\"font-mono font-medium\">{money(previewCharge.unitPrice, currency)}</span>\n {' '}{formatDepth(discountDepth(previewCharge.standardUnitPrice ?? standardUnit, previewCharge.unitPrice)) ?? ''}\n </span>\n )}\n {draftKey && !previewing && !previewCharge && previewLine && (\n <span>\n {t('cpq.priceAdjustments.dialog.previewLine', 'Line totals')}: MRC <span className=\"font-mono font-medium\">{money(previewLine.mrcTotal, currency)}</span>\n {' \u00B7 '}NRC <span className=\"font-mono font-medium\">{money(previewLine.nrcTotal, currency)}</span>\n </span>\n )}\n </div>\n\n {(errorCode || previewErrors.length > 0 || noteRequired) && (\n <Alert variant=\"warning\">\n <AlertDescription>\n {errorCode ? message(errorCode) : previewErrors.length > 0 ? message(previewErrors[0].code) : message('REASON_NOTE_REQUIRED')}\n {errorCode === 'STALE_REVISION' && (\n <Button variant=\"outline\" size=\"sm\" className=\"ml-2\" onClick={() => { onStale(); onClose() }}>\n {t('cpq.priceAdjustments.dialog.reload', 'Reload')}\n </Button>\n )}\n </AlertDescription>\n </Alert>\n )}\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={onClose}>{t('cpq.priceAdjustments.dialog.cancel', 'Cancel')}</Button>\n <Button onClick={() => void save()} disabled={!canSave} title=\"Ctrl/Cmd+Enter\">\n {saving ? t('cpq.priceAdjustments.dialog.saving', 'Saving\u2026') : t('cpq.priceAdjustments.dialog.save', 'Save')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
+ "mappings": ";AA0JU,SAUI,UAVJ,KAUI,YAVJ;AAxJV,YAAY,WAAW;AACvB,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB,SAAS,OAAO,wBAAwB;AACxC,SAAS,QAAQ,eAAe,mBAAmB,cAAc,cAAc,mBAAmB;AAClG,SAAS,QAAQ,eAAe,YAAY,eAAe,mBAAmB;AAC9E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAuCP,MAAM,iBAAiD;AAAA,EACrD,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,mBAAmB;AACrB;AAEA,SAAS,MAAM,QAAgB,UAAkB;AAC/C,SAAO,IAAI,KAAK,aAAa,SAAS,EAAE,OAAO,YAAY,SAAS,CAAC,EAAE,OAAO,MAAM;AACtF;AAEO,SAAS,sBAA8C,OAAsC;AAClG,QAAM,EAAE,MAAM,SAAS,UAAU,iBAAiB,QAAQ,aAAa,QAAQ,UAAU,qBAAqB,SAAS,SAAS,SAAS,QAAQ,IAAI;AACrJ,QAAM,IAAI,KAAK;AACf,QAAM,QAAQ,uBAAuB,OAAO,OAAO,MAAM;AACzD,QAAM,cAAc,MAAM,YAAY,OAAuB;AAAA,IAC3D,gBAAgB,UAAU,kBAAkB,MAAM,CAAC,KAAK;AAAA,IACxD,OAAO,WAAW,OAAO,SAAS,KAAK,IAAI;AAAA,IAC3C,YAAY,UAAU,cAAc;AAAA,IACpC,YAAY,UAAU,cAAc;AAAA,IACpC,kBAAkB,UAAU,oBAAoB;AAAA,EAClD,IAAI,CAAC,UAAU,KAAK,CAAC;AACrB,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAyB,WAAW;AAClE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAqF,IAAI;AAC7H,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAEhD,QAAM,UAAU,MAAM;AACpB,QAAI,MAAM;AAAE,cAAQ,YAAY,CAAC;AAAG,iBAAW,IAAI;AAAG,mBAAa,IAAI;AAAA,IAAE;AAAA,EAG3E,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,QAAQ,qBAAqB,MAAM,QAAQ,UAAU,eAAe;AAC1E,QAAM,WAAW,SAAS,KAAK,aAAa,KAAK,UAAU,KAAK,IAAI;AACpE,QAAM,UAAU,CAAC,SAAiB,EAAE,+BAA+B,IAAI,IAAI,2BAA2B,IAAI,KAAK,IAAI;AAGnH,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAQ,CAAC,UAAU;AAAE,iBAAW,IAAI;AAAG;AAAA,IAAO;AACnD,QAAI,YAAY;AAChB,UAAM,QAAQ,OAAO,WAAW,YAAY;AAC1C,oBAAc,IAAI;AAClB,YAAM,OAAO,MAAM;AAAA,QACjB,mBAAmB,OAAO;AAAA,QAC1B,EAAE,QAAQ,QAAQ,SAAS,EAAE,gBAAgB,mBAAmB,GAAG,MAAM,KAAK,UAAU,EAAE,OAAO,KAAK,MAAM,QAAQ,GAAG,GAAI,WAAW,EAAE,qBAAqB,SAAS,GAAG,IAAI,CAAC,EAAG,CAAC,EAAE;AAAA,MACtL;AACA,UAAI,UAAW;AACf,oBAAc,KAAK;AACnB,UAAI,KAAK,MAAM,KAAK,QAAQ;AAAE,mBAAW,KAAK,MAAM;AAAG,qBAAa,IAAI;AAAA,MAAE,OACrE;AAAE,mBAAW,IAAI;AAAG,qBAAa,oBAAoB,KAAK,QAAQ,KAAK,MAAM,CAAC;AAAA,MAAE;AAAA,IACvF,GAAG,GAAG;AACN,WAAO,MAAM;AAAE,kBAAY;AAAM,aAAO,aAAa,KAAK;AAAA,IAAE;AAAA,EAC9D,GAAG,CAAC,MAAM,UAAU,SAAS,QAAQ,CAAC;AAEtC,QAAM,cAAc,SAAS,MAAM,MAAM,KAAK,CAAC,SAAS,KAAK,WAAW,OAAO,WAAW;AAC1F,QAAM,gBAAgB,OAAO,UAAU,WAAW,aAAa,QAAQ,KAAK,CAAC,UAAU,MAAM,eAAe,OAAO,UAAU,IAAI;AAEjI,QAAM,gBAAgB,SAAS,OAAO,OAAO,CAAC,UAAU,WAAW,MAAM,iBAAiB,SAAS,KAAK,CAAC,oBAAoB,SAAS,MAAM,YAAY,CAAC,KAAK,CAAC;AAC/J,QAAM,eAAe,KAAK,eAAe,WAAW,CAAC,KAAK,WAAW,KAAK;AAC1E,QAAM,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,cAAc,CAAC,gBAAgB,CAAC,UAAU,cAAc,WAAW;AAErG,QAAM,OAAO,YAAY;AACvB,QAAI,CAAC,SAAS,CAAC,QAAS;AACxB,cAAU,IAAI;AAAG,iBAAa,IAAI;AAClC,UAAM,OAAO,WACT,MAAM,QAAsB,mBAAmB,OAAO,sBAAsB,SAAS,EAAE,IAAI;AAAA,MAC3F,QAAQ;AAAA,MAAS,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAAG,MAAM,KAAK,UAAU,kBAAkB,KAAK,CAAC;AAAA,IACjH,CAAC,IACC,MAAM,QAAsB,mBAAmB,OAAO,sBAAsB;AAAA,MAC5E,QAAQ;AAAA,MAAQ,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAAG,MAAM,KAAK,UAAU,KAAK;AAAA,IAC7F,CAAC;AACH,cAAU,KAAK;AACf,QAAI,KAAK,MAAM,KAAK,QAAQ,OAAO;AAAE,cAAQ,KAAK,OAAO,KAAK;AAAG,cAAQ;AAAG;AAAA,IAAO;AACnF,iBAAa,oBAAoB,KAAK,QAAQ,KAAK,MAAM,CAAC;AAAA,EAC5D;AAEA,QAAM,YAAY,CAAC,UAA+B;AAChD,SAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAAE,YAAM,eAAe;AAAG,WAAK,KAAK;AAAA,IAAE;AAAA,EACvG;AAEA,QAAM,eAAe,QAAQ,qBAAqB,QAAQ;AAC1D,QAAM,YAAY,KAAK,mBAAmB,qBACtC,EAAE,2CAA2C,6CAAwC,IACrF,KAAK,mBAAmB,mBACtB,EAAE,4CAA4C,4CAA4C,IAC1F,EAAE,4CAA4C,2CAAsC;AAE1F,SACE,oBAAC,UAAO,MAAY,cAAc,CAAC,SAAS;AAAE,QAAI,CAAC,KAAM,SAAQ;AAAA,EAAE,GACjE,+BAAC,iBAAc,WAAsB,eAAY,+BAC/C;AAAA,yBAAC,gBACC;AAAA,0BAAC,eACE,qBACG,EAAE,yCAAyC,uBAAuB,IAClE,OAAO,UAAU,SACf,EAAE,yCAAyC,eAAe,IAC1D,EAAE,2CAA2C,cAAc,GACnE;AAAA,MACA,qBAAC,qBACE;AAAA;AAAA,QACA,UAAU,iBAAiB,UAC1B,iCAAE;AAAA;AAAA,UAAI,EAAE,wCAAwC,UAAU;AAAA,UAAE;AAAA,UAAE,MAAM,cAAc,QAAQ;AAAA,WAAE;AAAA,SAEhG;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,qBACb;AAAA,2BAAC,WAAM,WAAU,mBACf;AAAA,4BAAC,UAAK,WAAU,uBAAuB,YAAE,oCAAoC,YAAY,GAAE;AAAA,QAC3F,qBAAC,UAAO,OAAO,KAAK,gBAAgB,eAAe,CAAC,UAAU,QAAQ,EAAE,GAAG,MAAM,gBAAgB,MAAwB,CAAC,GACxH;AAAA,8BAAC,iBAAc,8BAAC,eAAY,GAAE;AAAA,UAC9B,oBAAC,iBACE,gBAAM,IAAI,CAAC,SACV,oBAAC,cAAsB,OAAO,MAAO,YAAE,qCAAqC,IAAI,IAAI,eAAe,IAAI,CAAC,KAAvF,IAAyF,CAC3G,GACH;AAAA,WACF;AAAA,SACF;AAAA,MAEC,OAAO,UAAU,UAChB,qBAAC,WAAM,WAAU,mBACf;AAAA,4BAAC,UAAK,WAAU,uBAAuB,YAAE,yCAAyC,YAAY,GAAE;AAAA,QAChG,qBAAC,UAAO,OAAO,KAAK,oBAAoB,OAAO,eAAe,CAAC,UAAU,QAAQ,EAAE,GAAG,MAAM,kBAAkB,UAAU,QAAQ,KAAK,MAAuB,CAAC,GAC3J;AAAA,8BAAC,iBAAc,8BAAC,eAAY,GAAE;AAAA,UAC9B,qBAAC,iBACC;AAAA,gCAAC,cAAW,OAAM,OAAO,YAAE,0CAA0C,oCAAoC,GAAE;AAAA,YAC3G,oBAAC,cAAW,OAAM,OAAO,YAAE,uCAAuC,wBAAwB,GAAE;AAAA,YAC5F,oBAAC,cAAW,OAAM,OAAO,YAAE,uCAAuC,uBAAuB,GAAE;AAAA,aAC7F;AAAA,WACF;AAAA,SACF;AAAA,MAGF,qBAAC,WAAM,WAAU,mBACf;AAAA,4BAAC,UAAK,WAAU,uBAAuB,YAAE,qCAAqC,OAAO,GAAE;AAAA,QACvF,oBAAC,SAAM,WAAU,WAAU,WAAS,MAAC,OAAO,KAAK,OAAO,UAAU,CAAC,UAAU,QAAQ,EAAE,GAAG,MAAM,OAAO,MAAM,OAAO,MAAM,CAAC,GAAG,oBAAiB,6BAA4B;AAAA,QAC3K,oBAAC,UAAK,IAAG,6BAA4B,WAAU,uCAAuC,qBAAU;AAAA,SAClG;AAAA,MAEA,qBAAC,WAAM,WAAU,mBACf;AAAA,4BAAC,UAAK,WAAU,uBAAuB,YAAE,sCAAsC,QAAQ,GAAE;AAAA,QACzF,qBAAC,UAAO,OAAO,KAAK,YAAY,eAAe,CAAC,UAAU,QAAQ,EAAE,GAAG,MAAM,YAAY,MAAM,CAAC,GAC9F;AAAA,8BAAC,iBAAc,8BAAC,eAAY,aAAa,EAAE,iDAAiD,eAAe,GAAG,GAAE;AAAA,UAChH,oBAAC,iBACE,kBAAQ,IAAI,CAAC,WAAW,oBAAC,cAA8B,OAAO,OAAO,OAAQ,iBAAO,SAA3C,OAAO,KAA0C,CAAa,GAC1G;AAAA,WACF;AAAA,SACF;AAAA,MAEA,qBAAC,WAAM,WAAU,mBACf;AAAA,6BAAC,UAAK,WAAU,uBACb;AAAA,YAAE,oCAAoC,MAAM;AAAA,UAAG,KAAK,eAAe,UAAU,OAAO;AAAA,WACvF;AAAA,QACA,oBAAC,YAAS,MAAM,GAAG,WAAW,KAAK,OAAO,KAAK,YAAY,UAAU,CAAC,UAAU,QAAQ,EAAE,GAAG,MAAM,YAAY,MAAM,OAAO,MAAM,CAAC,GAAG;AAAA,SACxI;AAAA,MAEA,qBAAC,SAAI,WAAU,0CAAyC,aAAU,UAAS,eAAY,0BACpF;AAAA,SAAC,YAAY,oBAAC,UAAK,WAAU,yBAAyB,YAAE,4CAA4C,oDAAoD,GAAE;AAAA,QAC1J,YAAY,cAAc,oBAAC,UAAK,WAAU,yBAAyB,YAAE,0CAA0C,mBAAc,GAAE;AAAA,QAC/H,YAAY,CAAC,cAAc,iBAC1B,qBAAC,UACE;AAAA,YAAE,6CAA6C,iBAAiB;AAAA,UAAE;AAAA,UAAE,oBAAC,UAAK,WAAU,yBAAyB,gBAAM,cAAc,WAAW,QAAQ,GAAE;AAAA,UACtJ;AAAA,UAAK,YAAY,cAAc,cAAc,qBAAqB,cAAc,cAAc,SAAS,CAAC,KAAK;AAAA,WAChH;AAAA,QAED,YAAY,CAAC,cAAc,CAAC,iBAAiB,eAC5C,qBAAC,UACE;AAAA,YAAE,2CAA2C,aAAa;AAAA,UAAE;AAAA,UAAM,oBAAC,UAAK,WAAU,yBAAyB,gBAAM,YAAY,UAAU,QAAQ,GAAE;AAAA,UACjJ;AAAA,UAAM;AAAA,UAAI,oBAAC,UAAK,WAAU,yBAAyB,gBAAM,YAAY,UAAU,QAAQ,GAAE;AAAA,WAC5F;AAAA,SAEJ;AAAA,OAEE,aAAa,cAAc,SAAS,KAAK,iBACzC,oBAAC,SAAM,SAAQ,WACb,+BAAC,oBACE;AAAA,oBAAY,QAAQ,SAAS,IAAI,cAAc,SAAS,IAAI,QAAQ,cAAc,CAAC,EAAE,IAAI,IAAI,QAAQ,sBAAsB;AAAA,QAC3H,cAAc,oBACb,oBAAC,UAAO,SAAQ,WAAU,MAAK,MAAK,WAAU,QAAO,SAAS,MAAM;AAAE,kBAAQ;AAAG,kBAAQ;AAAA,QAAE,GACxF,YAAE,sCAAsC,QAAQ,GACnD;AAAA,SAEJ,GACF;AAAA,OAEJ;AAAA,IAEA,qBAAC,gBACC;AAAA,0BAAC,UAAO,SAAQ,WAAU,SAAS,SAAU,YAAE,sCAAsC,QAAQ,GAAE;AAAA,MAC/F,oBAAC,UAAO,SAAS,MAAM,KAAK,KAAK,GAAG,UAAU,CAAC,SAAS,OAAM,kBAC3D,mBAAS,EAAE,sCAAsC,cAAS,IAAI,EAAE,oCAAoC,MAAM,GAC7G;AAAA,OACF;AAAA,KACF,GACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { Tag } from "@open-mercato/ui/primitives/tag";
6
+ import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
7
+ import { ADJUSTMENT_ERROR_FALLBACKS } from "../../../../../lib/priceAdjustmentView.js";
8
+ import { ManualAdjustmentActions, useAccessReasonText, usePriceAdjustmentUi } from "./priceAdjustmentContext.js";
9
+ const PRICE_ADJUSTMENTS_SPOT_ID = "data-table:cpq.quote_price_adjustments";
10
+ const TYPE_FALLBACKS = {
11
+ price_override: "Price override",
12
+ discount_percent: "% discount",
13
+ discount_absolute: "Amount off"
14
+ };
15
+ function money(amount, currency) {
16
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
17
+ }
18
+ function PriceAdjustmentsPanel({ quoteId, items, total, lines, currency, errorsById }) {
19
+ const t = useT();
20
+ const ui = usePriceAdjustmentUi();
21
+ const lineById = React.useMemo(() => new Map(lines.map((line) => [line.lineId, line])), [lines]);
22
+ const accessText = useAccessReasonText();
23
+ if (!ui || ui.access.reason === "disabled" && items.length === 0) return null;
24
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card", "data-testid": "cpq-price-adjustments-panel", children: [
25
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b px-4 py-3", children: [
26
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-medium", children: [
27
+ t("cpq.priceAdjustments.panel.title", "Price adjustments"),
28
+ /* @__PURE__ */ jsx("span", { className: "ml-2 text-xs text-muted-foreground", children: total })
29
+ ] }),
30
+ accessText && ui.access.reason !== "loading" && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", "data-testid": "cpq-price-adjustments-locked", children: accessText })
31
+ ] }),
32
+ items.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-xs text-muted-foreground", children: t("cpq.priceAdjustments.panel.empty", "No negotiated prices yet. Use \u201CAdjust price\u201D on a charge or \u201CDiscount line\u201D on a line.") }) : /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
33
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-muted-foreground", children: [
34
+ /* @__PURE__ */ jsx("th", { className: "px-4 py-1.5 text-left font-medium", children: t("cpq.priceAdjustments.panel.target", "Applies to") }),
35
+ /* @__PURE__ */ jsx("th", { className: "py-1.5 text-left font-medium", children: t("cpq.priceAdjustments.panel.type", "Type") }),
36
+ /* @__PURE__ */ jsx("th", { className: "py-1.5 text-right font-medium", children: t("cpq.priceAdjustments.panel.value", "Value") }),
37
+ /* @__PURE__ */ jsx("th", { className: "py-1.5 pl-4 text-left font-medium", children: t("cpq.priceAdjustments.panel.reason", "Reason") }),
38
+ /* @__PURE__ */ jsx("th", { className: "py-1.5 text-left font-medium", children: t("cpq.priceAdjustments.panel.author", "Author") }),
39
+ /* @__PURE__ */ jsx("th", { className: "py-1.5 text-left font-medium", children: t("cpq.priceAdjustments.panel.date", "Date") }),
40
+ /* @__PURE__ */ jsx("th", { className: "w-16" })
41
+ ] }) }),
42
+ /* @__PURE__ */ jsx("tbody", { children: items.map((item) => {
43
+ const line = lineById.get(item.quoteLineId);
44
+ const chargeName = item.chargeCode ? line?.charges.find((charge) => charge.chargeCode === item.chargeCode)?.chargeName ?? item.chargeCode : null;
45
+ const scope = item.level === "line" ? item.chargeTypeFilter === "mrc" ? t("cpq.priceAdjustments.panel.lineMrc", "whole line, recurring") : item.chargeTypeFilter === "nrc" ? t("cpq.priceAdjustments.panel.lineNrc", "whole line, one-time") : t("cpq.priceAdjustments.panel.line", "whole line") : chargeName;
46
+ const error = errorsById.get(item.id);
47
+ return /* @__PURE__ */ jsxs("tr", { className: "border-t", "data-testid": "cpq-price-adjustment-row", children: [
48
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-1.5", children: [
49
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: line?.offeringName ?? t("cpq.priceAdjustments.panel.removedLine", "Removed line") }),
50
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
51
+ " \xB7 ",
52
+ scope
53
+ ] }),
54
+ error && /* @__PURE__ */ jsxs(Tag, { variant: "warning", className: "ml-1.5 px-1.5 text-xs", children: [
55
+ t("cpq.priceAdjustments.invalid", "Invalid"),
56
+ ": ",
57
+ t(`cpq.priceAdjustments.errors.${error}`, ADJUSTMENT_ERROR_FALLBACKS[error] ?? error)
58
+ ] })
59
+ ] }),
60
+ /* @__PURE__ */ jsx("td", { className: "py-1.5", children: t(`cpq.priceAdjustments.panel.types.${item.adjustmentType}`, TYPE_FALLBACKS[item.adjustmentType]) }),
61
+ /* @__PURE__ */ jsx("td", { className: "py-1.5 text-right font-mono", children: item.adjustmentType === "discount_percent" ? `${item.value}%` : money(item.value, item.currencyCode ?? currency) }),
62
+ /* @__PURE__ */ jsxs("td", { className: "py-1.5 pl-4", children: [
63
+ ui.reasonLabels[item.reasonCode] ?? item.reasonCode,
64
+ item.reasonNote && /* @__PURE__ */ jsx("span", { className: "block text-muted-foreground", children: item.reasonNote })
65
+ ] }),
66
+ /* @__PURE__ */ jsx("td", { className: "py-1.5 font-mono text-muted-foreground", title: item.createdByUserId, children: item.createdByUserId.slice(0, 8) }),
67
+ /* @__PURE__ */ jsx("td", { className: "py-1.5 text-muted-foreground", children: new Date(item.createdAt).toLocaleDateString() }),
68
+ /* @__PURE__ */ jsx("td", { className: "py-1.5 pr-2 text-right", children: /* @__PURE__ */ jsx(ManualAdjustmentActions, { adjustmentId: item.id }) })
69
+ ] }, item.id);
70
+ }) })
71
+ ] }),
72
+ /* @__PURE__ */ jsx(InjectionSpot, { spotId: PRICE_ADJUSTMENTS_SPOT_ID, context: { quoteId, adjustments: items } })
73
+ ] });
74
+ }
75
+ export {
76
+ PRICE_ADJUSTMENTS_SPOT_ID,
77
+ PriceAdjustmentsPanel
78
+ };
79
+ //# sourceMappingURL=PriceAdjustmentsPanel.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/cpq/backend/cpq/quotes/%5Bid%5D/_components/PriceAdjustmentsPanel.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { ADJUSTMENT_ERROR_FALLBACKS } from '../../../../../lib/priceAdjustmentView'\nimport { ManualAdjustmentActions, useAccessReasonText, usePriceAdjustmentUi } from './priceAdjustmentContext'\n\nexport const PRICE_ADJUSTMENTS_SPOT_ID = 'data-table:cpq.quote_price_adjustments'\n\nexport type StoredAdjustment = {\n id: string\n level: 'charge' | 'line'\n quoteLineId: string\n chargeCode?: string | null\n chargeTypeFilter?: 'nrc' | 'mrc' | null\n adjustmentType: 'discount_percent' | 'discount_absolute' | 'price_override'\n value: number\n currencyCode?: string | null\n reasonCode: string\n reasonNote?: string | null\n createdByUserId: string\n createdAt: string\n}\n\ntype LineRef = { lineId: string; offeringName: string; offeringId: string | null; configuration?: Record<string, unknown> | null; charges: Array<{ chargeCode: string; chargeName: string }> }\n\nconst TYPE_FALLBACKS: Record<StoredAdjustment['adjustmentType'], string> = {\n price_override: 'Price override',\n discount_percent: '% discount',\n discount_absolute: 'Amount off',\n}\n\nfunction money(amount: number, currency: string) {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)\n}\n\nexport function PriceAdjustmentsPanel({ quoteId, items, total, lines, currency, errorsById }: {\n quoteId: string\n items: StoredAdjustment[]\n total: number\n lines: LineRef[]\n currency: string\n /** adjustmentId \u2192 first error code reported by the latest pricing. */\n errorsById: Map<string, string>\n}) {\n const t = useT()\n const ui = usePriceAdjustmentUi()\n const lineById = React.useMemo(() => new Map(lines.map((line) => [line.lineId, line])), [lines])\n const accessText = useAccessReasonText()\n // Toggle off: stored adjustments still price the quote, so keep them visible (read-only).\n if (!ui || (ui.access.reason === 'disabled' && items.length === 0)) return null\n\n return (\n <div className=\"rounded-lg border bg-card\" data-testid=\"cpq-price-adjustments-panel\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"text-sm font-medium\">\n {t('cpq.priceAdjustments.panel.title', 'Price adjustments')}\n <span className=\"ml-2 text-xs text-muted-foreground\">{total}</span>\n </h3>\n {accessText && ui.access.reason !== 'loading' && (\n <span className=\"text-xs text-muted-foreground\" data-testid=\"cpq-price-adjustments-locked\">{accessText}</span>\n )}\n </div>\n {items.length === 0 ? (\n <p className=\"px-4 py-3 text-xs text-muted-foreground\">\n {t('cpq.priceAdjustments.panel.empty', 'No negotiated prices yet. Use \u201CAdjust price\u201D on a charge or \u201CDiscount line\u201D on a line.')}\n </p>\n ) : (\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"text-muted-foreground\">\n <th className=\"px-4 py-1.5 text-left font-medium\">{t('cpq.priceAdjustments.panel.target', 'Applies to')}</th>\n <th className=\"py-1.5 text-left font-medium\">{t('cpq.priceAdjustments.panel.type', 'Type')}</th>\n <th className=\"py-1.5 text-right font-medium\">{t('cpq.priceAdjustments.panel.value', 'Value')}</th>\n <th className=\"py-1.5 pl-4 text-left font-medium\">{t('cpq.priceAdjustments.panel.reason', 'Reason')}</th>\n <th className=\"py-1.5 text-left font-medium\">{t('cpq.priceAdjustments.panel.author', 'Author')}</th>\n <th className=\"py-1.5 text-left font-medium\">{t('cpq.priceAdjustments.panel.date', 'Date')}</th>\n <th className=\"w-16\" />\n </tr>\n </thead>\n <tbody>\n {items.map((item) => {\n const line = lineById.get(item.quoteLineId)\n const chargeName = item.chargeCode ? line?.charges.find((charge) => charge.chargeCode === item.chargeCode)?.chargeName ?? item.chargeCode : null\n const scope = item.level === 'line'\n ? item.chargeTypeFilter === 'mrc' ? t('cpq.priceAdjustments.panel.lineMrc', 'whole line, recurring')\n : item.chargeTypeFilter === 'nrc' ? t('cpq.priceAdjustments.panel.lineNrc', 'whole line, one-time')\n : t('cpq.priceAdjustments.panel.line', 'whole line')\n : chargeName\n const error = errorsById.get(item.id)\n return (\n <tr key={item.id} className=\"border-t\" data-testid=\"cpq-price-adjustment-row\">\n <td className=\"px-4 py-1.5\">\n <span className=\"font-medium\">{line?.offeringName ?? t('cpq.priceAdjustments.panel.removedLine', 'Removed line')}</span>\n <span className=\"text-muted-foreground\"> \u00B7 {scope}</span>\n {error && (\n <Tag variant=\"warning\" className=\"ml-1.5 px-1.5 text-xs\">\n {t('cpq.priceAdjustments.invalid', 'Invalid')}: {t(`cpq.priceAdjustments.errors.${error}`, ADJUSTMENT_ERROR_FALLBACKS[error] ?? error)}\n </Tag>\n )}\n </td>\n <td className=\"py-1.5\">{t(`cpq.priceAdjustments.panel.types.${item.adjustmentType}`, TYPE_FALLBACKS[item.adjustmentType])}</td>\n <td className=\"py-1.5 text-right font-mono\">\n {item.adjustmentType === 'discount_percent' ? `${item.value}%` : money(item.value, item.currencyCode ?? currency)}\n </td>\n <td className=\"py-1.5 pl-4\">\n {ui.reasonLabels[item.reasonCode] ?? item.reasonCode}\n {item.reasonNote && <span className=\"block text-muted-foreground\">{item.reasonNote}</span>}\n </td>\n <td className=\"py-1.5 font-mono text-muted-foreground\" title={item.createdByUserId}>{item.createdByUserId.slice(0, 8)}</td>\n <td className=\"py-1.5 text-muted-foreground\">{new Date(item.createdAt).toLocaleDateString()}</td>\n <td className=\"py-1.5 pr-2 text-right\"><ManualAdjustmentActions adjustmentId={item.id} /></td>\n </tr>\n )\n })}\n </tbody>\n </table>\n )}\n <InjectionSpot spotId={PRICE_ADJUSTMENTS_SPOT_ID} context={{ quoteId, adjustments: items }} />\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
6
+ import { isArcCancelledLine, removalCancelsItem } from "../../../../../services/arcLineRemoval.js";
7
+ const ArcLineRemovalContext = React.createContext(null);
8
+ const ArcLineRemovalProvider = ArcLineRemovalContext.Provider;
9
+ function useArcLineRemovalUi() {
10
+ return React.useContext(ArcLineRemovalContext);
11
+ }
12
+ function useArcLineRemoval({ quote, quoteType, onQuote, onError }) {
13
+ const t = useT();
14
+ const { confirm, ConfirmDialogElement } = useConfirmDialog();
15
+ const send = React.useCallback(async (url, method) => {
16
+ onError(null);
17
+ const call = await apiCall(url, { method });
18
+ if (call.ok && call.result) onQuote(call.result);
19
+ else onError(call.result?.error ?? t("cpq.quotes.lineUpdateFailed", "Could not update the line"));
20
+ }, [onError, onQuote, t]);
21
+ const remove = React.useCallback(async (lineId) => {
22
+ if (!quote) return;
23
+ const line = quote.lines.find((candidate) => candidate.lineId === lineId);
24
+ if (line && removalCancelsItem(line, quoteType)) {
25
+ const ok = await confirm({
26
+ title: t("cpq.quotes.arc.removeConfirm.title", "Remove {name} from the subscription?").replace("{name}", line.offeringName),
27
+ text: t(
28
+ "cpq.quotes.arc.removeConfirm.text",
29
+ 'This item already exists on the subscription. Removing it here does not delete it from the quote \u2014 it stays as a "remove" line, and when the order is activated the item (and its components) is cancelled on the subscription and its recurring charge stops. You can restore it until then.'
30
+ ),
31
+ confirmText: t("cpq.quotes.arc.removeConfirm.confirm", "Remove item"),
32
+ variant: "destructive"
33
+ });
34
+ if (!ok) return;
35
+ }
36
+ await send(`/api/cpq/quotes/${quote.id}/items/${lineId}`, "DELETE");
37
+ }, [confirm, quote, quoteType, send, t]);
38
+ const restore = React.useCallback((lineId) => {
39
+ if (!quote) return;
40
+ void send(`/api/cpq/quotes/${quote.id}/items/${lineId}/restore`, "POST");
41
+ }, [quote, send]);
42
+ const ui = React.useMemo(() => ({
43
+ isRemoved: (line) => isArcCancelledLine(line, quoteType),
44
+ restore
45
+ }), [quoteType, restore]);
46
+ return { remove, ui, ConfirmDialogElement };
47
+ }
48
+ export {
49
+ ArcLineRemovalProvider,
50
+ useArcLineRemoval,
51
+ useArcLineRemovalUi
52
+ };
53
+ //# sourceMappingURL=arcLineRemoval.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/cpq/backend/cpq/quotes/%5Bid%5D/_components/arcLineRemoval.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { isArcCancelledLine, removalCancelsItem, type ArcRemovalLine } from '../../../../../services/arcLineRemoval'\n\ntype RemovableLine = Pick<ArcRemovalLine, 'action' | 'configuration'> & { lineId: string; offeringName: string }\n\nexport type ArcLineRemovalUi = {\n /** The line is an existing subscription item marked for removal. */\n isRemoved: (line: Pick<RemovableLine, 'action'>) => boolean\n restore: (lineId: string) => void\n}\n\nconst ArcLineRemovalContext = React.createContext<ArcLineRemovalUi | null>(null)\n\nexport const ArcLineRemovalProvider = ArcLineRemovalContext.Provider\n\nexport function useArcLineRemovalUi(): ArcLineRemovalUi | null {\n return React.useContext(ArcLineRemovalContext)\n}\n\n/**\n * Line removal on the quote page. On an Amend / Renew quote, removing a line\n * that mirrors a live subscription item asks for confirmation and marks the\n * item for removal (server turns it into a `cancel` line); it can be restored.\n * Other lines are deleted as before.\n */\nexport function useArcLineRemoval<Q extends { id: string; lines: RemovableLine[] }>({ quote, quoteType, onQuote, onError }: {\n quote: Q | null\n quoteType: string\n onQuote: (quote: Q) => void\n onError: (message: string | null) => void\n}) {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const send = React.useCallback(async (url: string, method: 'DELETE' | 'POST') => {\n onError(null)\n const call = await apiCall<Q & { error?: string }>(url, { method })\n if (call.ok && call.result) onQuote(call.result)\n else onError(call.result?.error ?? t('cpq.quotes.lineUpdateFailed', 'Could not update the line'))\n }, [onError, onQuote, t])\n\n const remove = React.useCallback(async (lineId: string) => {\n if (!quote) return\n const line = quote.lines.find((candidate) => candidate.lineId === lineId)\n if (line && removalCancelsItem(line, quoteType)) {\n const ok = await confirm({\n title: t('cpq.quotes.arc.removeConfirm.title', 'Remove {name} from the subscription?').replace('{name}', line.offeringName),\n text: t(\n 'cpq.quotes.arc.removeConfirm.text',\n 'This item already exists on the subscription. Removing it here does not delete it from the quote \u2014 it stays as a \"remove\" line, and when the order is activated the item (and its components) is cancelled on the subscription and its recurring charge stops. You can restore it until then.',\n ),\n confirmText: t('cpq.quotes.arc.removeConfirm.confirm', 'Remove item'),\n variant: 'destructive',\n })\n if (!ok) return\n }\n await send(`/api/cpq/quotes/${quote.id}/items/${lineId}`, 'DELETE')\n }, [confirm, quote, quoteType, send, t])\n\n const restore = React.useCallback((lineId: string) => {\n if (!quote) return\n void send(`/api/cpq/quotes/${quote.id}/items/${lineId}/restore`, 'POST')\n }, [quote, send])\n\n const ui = React.useMemo<ArcLineRemovalUi>(() => ({\n isRemoved: (line) => isArcCancelledLine(line, quoteType),\n restore,\n }), [quoteType, restore])\n\n return { remove, ui, ConfirmDialogElement }\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,wBAAwB;AACjC,SAAS,oBAAoB,0BAA+C;AAU5E,MAAM,wBAAwB,MAAM,cAAuC,IAAI;AAExE,MAAM,yBAAyB,sBAAsB;AAErD,SAAS,sBAA+C;AAC7D,SAAO,MAAM,WAAW,qBAAqB;AAC/C;AAQO,SAAS,kBAAoE,EAAE,OAAO,WAAW,SAAS,QAAQ,GAKtH;AACD,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,SAAS,qBAAqB,IAAI,iBAAiB;AAE3D,QAAM,OAAO,MAAM,YAAY,OAAO,KAAa,WAA8B;AAC/E,YAAQ,IAAI;AACZ,UAAM,OAAO,MAAM,QAAgC,KAAK,EAAE,OAAO,CAAC;AAClE,QAAI,KAAK,MAAM,KAAK,OAAQ,SAAQ,KAAK,MAAM;AAAA,QAC1C,SAAQ,KAAK,QAAQ,SAAS,EAAE,+BAA+B,2BAA2B,CAAC;AAAA,EAClG,GAAG,CAAC,SAAS,SAAS,CAAC,CAAC;AAExB,QAAM,SAAS,MAAM,YAAY,OAAO,WAAmB;AACzD,QAAI,CAAC,MAAO;AACZ,UAAM,OAAO,MAAM,MAAM,KAAK,CAAC,cAAc,UAAU,WAAW,MAAM;AACxE,QAAI,QAAQ,mBAAmB,MAAM,SAAS,GAAG;AAC/C,YAAM,KAAK,MAAM,QAAQ;AAAA,QACvB,OAAO,EAAE,sCAAsC,sCAAsC,EAAE,QAAQ,UAAU,KAAK,YAAY;AAAA,QAC1H,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,QACA,aAAa,EAAE,wCAAwC,aAAa;AAAA,QACpE,SAAS;AAAA,MACX,CAAC;AACD,UAAI,CAAC,GAAI;AAAA,IACX;AACA,UAAM,KAAK,mBAAmB,MAAM,EAAE,UAAU,MAAM,IAAI,QAAQ;AAAA,EACpE,GAAG,CAAC,SAAS,OAAO,WAAW,MAAM,CAAC,CAAC;AAEvC,QAAM,UAAU,MAAM,YAAY,CAAC,WAAmB;AACpD,QAAI,CAAC,MAAO;AACZ,SAAK,KAAK,mBAAmB,MAAM,EAAE,UAAU,MAAM,YAAY,MAAM;AAAA,EACzE,GAAG,CAAC,OAAO,IAAI,CAAC;AAEhB,QAAM,KAAK,MAAM,QAA0B,OAAO;AAAA,IAChD,WAAW,CAAC,SAAS,mBAAmB,MAAM,SAAS;AAAA,IACvD;AAAA,EACF,IAAI,CAAC,WAAW,OAAO,CAAC;AAExB,SAAO,EAAE,QAAQ,IAAI,qBAAqB;AAC5C;",
6
+ "names": []
7
+ }
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { Percent, Pencil, Trash2 } from "lucide-react";
6
+ import { IconButton } from "@open-mercato/ui/primitives/icon-button";
7
+ import { SimpleTooltip } from "@open-mercato/ui/primitives/tooltip";
8
+ import { lineAdjustmentBlock } from "../../../../../lib/priceAdjustmentView.js";
9
+ const Context = React.createContext(null);
10
+ const PriceAdjustmentProvider = Context.Provider;
11
+ const usePriceAdjustmentUi = () => React.useContext(Context);
12
+ function useAccessReasonText() {
13
+ const t = useT();
14
+ const ui = usePriceAdjustmentUi();
15
+ if (!ui) return null;
16
+ switch (ui.access.reason) {
17
+ case null:
18
+ return null;
19
+ case "locked":
20
+ return t("cpq.priceAdjustments.blocked.locked", "Adjustments are locked in status {status} \u2014 move the quote back to Ready to change them").replace("{status}", ui.cpqStatus);
21
+ case "converted":
22
+ return t("cpq.priceAdjustments.blocked.converted", "This quote has been converted to an order");
23
+ case "no_permission":
24
+ return t("cpq.priceAdjustments.blocked.permission", "You do not have permission to adjust prices");
25
+ case "disabled":
26
+ return t("cpq.priceAdjustments.blocked.disabled", "Manual price adjustments are turned off");
27
+ default:
28
+ return t("cpq.priceAdjustments.blocked.loading", "Loading\u2026");
29
+ }
30
+ }
31
+ function useAdjustmentBlockReason(line) {
32
+ const t = useT();
33
+ const ui = usePriceAdjustmentUi();
34
+ const accessText = useAccessReasonText();
35
+ if (!ui) return null;
36
+ const block = lineAdjustmentBlock(line, ui.quoteType);
37
+ if (block === "mirror") return t("cpq.priceAdjustments.blocked.mirror", "Price changes on existing subscription items arrive with a later release");
38
+ if (block === "free_form") return t("cpq.priceAdjustments.blocked.freeForm", "Free-form lines cannot be adjusted");
39
+ return accessText;
40
+ }
41
+ function BlockedReason({ reason, children }) {
42
+ if (!reason) return /* @__PURE__ */ jsx(Fragment, { children });
43
+ return /* @__PURE__ */ jsx(SimpleTooltip, { content: reason, size: "sm", children: /* @__PURE__ */ jsx("span", { className: "inline-flex cursor-not-allowed", tabIndex: 0, "aria-label": reason, children }) });
44
+ }
45
+ function AdjustChargeAction({ line, charge }) {
46
+ const t = useT();
47
+ const ui = usePriceAdjustmentUi();
48
+ const reason = useAdjustmentBlockReason(line);
49
+ if (!ui || charge.chargeType === "usage") return null;
50
+ if (ui.access.reason === "disabled" || ui.access.reason === "no_permission" || ui.access.reason === "loading") return null;
51
+ const label = t("cpq.priceAdjustments.adjustPrice", "Adjust price");
52
+ return /* @__PURE__ */ jsx(BlockedReason, { reason, children: /* @__PURE__ */ jsx(
53
+ IconButton,
54
+ {
55
+ variant: "ghost",
56
+ size: "xs",
57
+ "aria-label": label,
58
+ title: reason ? void 0 : label,
59
+ disabled: !!reason,
60
+ onClick: () => ui.openCharge(line, charge),
61
+ "data-testid": "cpq-adjust-charge",
62
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
63
+ }
64
+ ) });
65
+ }
66
+ function DiscountLineAction({ line }) {
67
+ const t = useT();
68
+ const ui = usePriceAdjustmentUi();
69
+ const reason = useAdjustmentBlockReason(line);
70
+ if (!ui || ui.access.reason === "disabled" || ui.access.reason === "no_permission" || ui.access.reason === "loading") return null;
71
+ const label = t("cpq.priceAdjustments.discountLine", "Discount line");
72
+ return /* @__PURE__ */ jsx(BlockedReason, { reason, children: /* @__PURE__ */ jsx(
73
+ IconButton,
74
+ {
75
+ variant: "ghost",
76
+ size: "sm",
77
+ "aria-label": label,
78
+ title: reason ? void 0 : label,
79
+ disabled: !!reason,
80
+ onClick: () => ui.openLine(line),
81
+ "data-testid": "cpq-discount-line",
82
+ children: /* @__PURE__ */ jsx(Percent, { className: "h-4 w-4" })
83
+ }
84
+ ) });
85
+ }
86
+ function ManualAdjustmentActions({ adjustmentId }) {
87
+ const t = useT();
88
+ const ui = usePriceAdjustmentUi();
89
+ if (!ui) return null;
90
+ const editable = ui.access.editable;
91
+ const deletable = editable || ui.canDeleteInvalid && ui.invalidIds.has(adjustmentId);
92
+ if (!editable && !deletable) return null;
93
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5", children: [
94
+ editable && /* @__PURE__ */ jsx(IconButton, { variant: "ghost", size: "xs", "aria-label": t("cpq.priceAdjustments.edit", "Edit adjustment"), onClick: () => ui.edit(adjustmentId), children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" }) }),
95
+ /* @__PURE__ */ jsx(IconButton, { variant: "ghost", size: "xs", "aria-label": t("cpq.priceAdjustments.delete", "Delete adjustment"), onClick: () => ui.remove(adjustmentId), children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" }) })
96
+ ] });
97
+ }
98
+ export {
99
+ AdjustChargeAction,
100
+ DiscountLineAction,
101
+ ManualAdjustmentActions,
102
+ PriceAdjustmentProvider,
103
+ useAccessReasonText,
104
+ useAdjustmentBlockReason,
105
+ usePriceAdjustmentUi
106
+ };
107
+ //# sourceMappingURL=priceAdjustmentContext.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/cpq/backend/cpq/quotes/%5Bid%5D/_components/priceAdjustmentContext.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Percent, Pencil, Trash2 } from 'lucide-react'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport type { AdjustmentAccess, LineEligibilityInput, WaterfallChargeInput } from '../../../../../lib/priceAdjustmentView'\nimport { lineAdjustmentBlock } from '../../../../../lib/priceAdjustmentView'\n\nexport type AdjustableLine = LineEligibilityInput & { lineId: string; offeringName: string }\nexport type AdjustableCharge = WaterfallChargeInput & { chargeCode: string; chargeName: string }\n\nexport type PriceAdjustmentUi = {\n access: AdjustmentAccess\n quoteType: string\n cpqStatus: string\n reasonLabels: Record<string, string>\n /** Ids of adjustments currently flagged invalid (they keep Delete while locked). */\n invalidIds: Set<string>\n canDeleteInvalid: boolean\n openCharge: (line: AdjustableLine, charge: AdjustableCharge) => void\n openLine: (line: AdjustableLine) => void\n edit: (adjustmentId: string) => void\n remove: (adjustmentId: string) => void\n}\n\nconst Context = React.createContext<PriceAdjustmentUi | null>(null)\nexport const PriceAdjustmentProvider = Context.Provider\nexport const usePriceAdjustmentUi = () => React.useContext(Context)\n\n/** Quote-wide explanation for why adjustments are unavailable; null when editable. */\nexport function useAccessReasonText(): string | null {\n const t = useT()\n const ui = usePriceAdjustmentUi()\n if (!ui) return null\n switch (ui.access.reason) {\n case null: return null\n case 'locked':\n return t('cpq.priceAdjustments.blocked.locked', 'Adjustments are locked in status {status} \u2014 move the quote back to Ready to change them')\n .replace('{status}', ui.cpqStatus)\n case 'converted': return t('cpq.priceAdjustments.blocked.converted', 'This quote has been converted to an order')\n case 'no_permission': return t('cpq.priceAdjustments.blocked.permission', 'You do not have permission to adjust prices')\n case 'disabled': return t('cpq.priceAdjustments.blocked.disabled', 'Manual price adjustments are turned off')\n default: return t('cpq.priceAdjustments.blocked.loading', 'Loading\u2026')\n }\n}\n\n/** Disabled-state explanation for one line; null when the action is available. */\nexport function useAdjustmentBlockReason(line: AdjustableLine): string | null {\n const t = useT()\n const ui = usePriceAdjustmentUi()\n const accessText = useAccessReasonText()\n if (!ui) return null\n const block = lineAdjustmentBlock(line, ui.quoteType)\n if (block === 'mirror') return t('cpq.priceAdjustments.blocked.mirror', 'Price changes on existing subscription items arrive with a later release')\n if (block === 'free_form') return t('cpq.priceAdjustments.blocked.freeForm', 'Free-form lines cannot be adjusted')\n return accessText\n}\n\n/**\n * Disabled buttons swallow no pointer events, so a native `title` never shows.\n * Wrap a blocked action in a focusable span that carries the explanation.\n */\nfunction BlockedReason({ reason, children }: { reason: string | null; children: React.ReactNode }) {\n if (!reason) return <>{children}</>\n return (\n <SimpleTooltip content={reason} size=\"sm\">\n <span className=\"inline-flex cursor-not-allowed\" tabIndex={0} aria-label={reason}>{children}</span>\n </SimpleTooltip>\n )\n}\n\n/** Per-charge \"Adjust price\" action. Hidden entirely when the feature is off or not permitted. */\nexport function AdjustChargeAction({ line, charge }: { line: AdjustableLine; charge: AdjustableCharge }) {\n const t = useT()\n const ui = usePriceAdjustmentUi()\n const reason = useAdjustmentBlockReason(line)\n if (!ui || charge.chargeType === 'usage') return null\n if (ui.access.reason === 'disabled' || ui.access.reason === 'no_permission' || ui.access.reason === 'loading') return null\n const label = t('cpq.priceAdjustments.adjustPrice', 'Adjust price')\n return (\n <BlockedReason reason={reason}>\n <IconButton\n variant=\"ghost\" size=\"xs\" aria-label={label} title={reason ? undefined : label} disabled={!!reason}\n onClick={() => ui.openCharge(line, charge)} data-testid=\"cpq-adjust-charge\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </IconButton>\n </BlockedReason>\n )\n}\n\n/** Line-level \"Discount line\" action. */\nexport function DiscountLineAction({ line }: { line: AdjustableLine }) {\n const t = useT()\n const ui = usePriceAdjustmentUi()\n const reason = useAdjustmentBlockReason(line)\n if (!ui || ui.access.reason === 'disabled' || ui.access.reason === 'no_permission' || ui.access.reason === 'loading') return null\n const label = t('cpq.priceAdjustments.discountLine', 'Discount line')\n return (\n <BlockedReason reason={reason}>\n <IconButton\n variant=\"ghost\" size=\"sm\" aria-label={label} title={reason ? undefined : label} disabled={!!reason}\n onClick={() => ui.openLine(line)} data-testid=\"cpq-discount-line\"\n >\n <Percent className=\"h-4 w-4\" />\n </IconButton>\n </BlockedReason>\n )\n}\n\n/** Edit / delete actions on a manual waterfall row or panel row. */\nexport function ManualAdjustmentActions({ adjustmentId }: { adjustmentId: string }) {\n const t = useT()\n const ui = usePriceAdjustmentUi()\n if (!ui) return null\n const editable = ui.access.editable\n const deletable = editable || (ui.canDeleteInvalid && ui.invalidIds.has(adjustmentId))\n if (!editable && !deletable) return null\n return (\n <span className=\"inline-flex items-center gap-0.5\">\n {editable && (\n <IconButton variant=\"ghost\" size=\"xs\" aria-label={t('cpq.priceAdjustments.edit', 'Edit adjustment')} onClick={() => ui.edit(adjustmentId)}>\n <Pencil className=\"h-3.5 w-3.5\" />\n </IconButton>\n )}\n <IconButton variant=\"ghost\" size=\"xs\" aria-label={t('cpq.priceAdjustments.delete', 'Delete adjustment')} onClick={() => ui.remove(adjustmentId)}>\n <Trash2 className=\"h-3.5 w-3.5\" />\n </IconButton>\n </span>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }