@dainamite/cpq 0.2.2

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 (504) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.md +236 -0
  4. package/README.md +108 -0
  5. package/dist/modules/cpq/acl.js +30 -0
  6. package/dist/modules/cpq/acl.js.map +7 -0
  7. package/dist/modules/cpq/api/_helpers/handleArcError.js +37 -0
  8. package/dist/modules/cpq/api/_helpers/handleArcError.js.map +7 -0
  9. package/dist/modules/cpq/api/attribute-options/route.js +47 -0
  10. package/dist/modules/cpq/api/attribute-options/route.js.map +7 -0
  11. package/dist/modules/cpq/api/bundle-slots/route.js +101 -0
  12. package/dist/modules/cpq/api/bundle-slots/route.js.map +7 -0
  13. package/dist/modules/cpq/api/bundle-tree/route.js +28 -0
  14. package/dist/modules/cpq/api/bundle-tree/route.js.map +7 -0
  15. package/dist/modules/cpq/api/context.js +29 -0
  16. package/dist/modules/cpq/api/context.js.map +7 -0
  17. package/dist/modules/cpq/api/inventory/assets/route.js +113 -0
  18. package/dist/modules/cpq/api/inventory/assets/route.js.map +7 -0
  19. package/dist/modules/cpq/api/inventory/assets/status/route.js +35 -0
  20. package/dist/modules/cpq/api/inventory/assets/status/route.js.map +7 -0
  21. package/dist/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.js +30 -0
  22. package/dist/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.js.map +7 -0
  23. package/dist/modules/cpq/api/inventory/customer/[customerId]/route.js +31 -0
  24. package/dist/modules/cpq/api/inventory/customer/[customerId]/route.js.map +7 -0
  25. package/dist/modules/cpq/api/inventory/subscription-items/route.js +99 -0
  26. package/dist/modules/cpq/api/inventory/subscription-items/route.js.map +7 -0
  27. package/dist/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.js +57 -0
  28. package/dist/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.js.map +7 -0
  29. package/dist/modules/cpq/api/inventory/subscriptions/expiring/route.js +62 -0
  30. package/dist/modules/cpq/api/inventory/subscriptions/expiring/route.js.map +7 -0
  31. package/dist/modules/cpq/api/inventory/subscriptions/route.js +116 -0
  32. package/dist/modules/cpq/api/inventory/subscriptions/route.js.map +7 -0
  33. package/dist/modules/cpq/api/inventory/subscriptions/status/route.js +35 -0
  34. package/dist/modules/cpq/api/inventory/subscriptions/status/route.js.map +7 -0
  35. package/dist/modules/cpq/api/offering-components/route.js +91 -0
  36. package/dist/modules/cpq/api/offering-components/route.js.map +7 -0
  37. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +39 -0
  38. package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +7 -0
  39. package/dist/modules/cpq/api/orders/[orderId]/route.js +28 -0
  40. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +7 -0
  41. package/dist/modules/cpq/api/orders/[orderId]/status/route.js +38 -0
  42. package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +7 -0
  43. package/dist/modules/cpq/api/orders/route.js +71 -0
  44. package/dist/modules/cpq/api/orders/route.js.map +7 -0
  45. package/dist/modules/cpq/api/price-rules/route.js +180 -0
  46. package/dist/modules/cpq/api/price-rules/route.js.map +7 -0
  47. package/dist/modules/cpq/api/pricing-table-entries/route.js +142 -0
  48. package/dist/modules/cpq/api/pricing-table-entries/route.js.map +7 -0
  49. package/dist/modules/cpq/api/pricing-tables/route.js +179 -0
  50. package/dist/modules/cpq/api/pricing-tables/route.js.map +7 -0
  51. package/dist/modules/cpq/api/product-attributes/route.js +160 -0
  52. package/dist/modules/cpq/api/product-attributes/route.js.map +7 -0
  53. package/dist/modules/cpq/api/product-charges/route.js +166 -0
  54. package/dist/modules/cpq/api/product-charges/route.js.map +7 -0
  55. package/dist/modules/cpq/api/product-offerings/route.js +294 -0
  56. package/dist/modules/cpq/api/product-offerings/route.js.map +7 -0
  57. package/dist/modules/cpq/api/product-relationships/route.js +144 -0
  58. package/dist/modules/cpq/api/product-relationships/route.js.map +7 -0
  59. package/dist/modules/cpq/api/product-specifications/route.js +292 -0
  60. package/dist/modules/cpq/api/product-specifications/route.js.map +7 -0
  61. package/dist/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.js +71 -0
  62. package/dist/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.js.map +7 -0
  63. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +29 -0
  64. package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +7 -0
  65. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +54 -0
  66. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +7 -0
  67. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -0
  68. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +7 -0
  69. package/dist/modules/cpq/api/quotes/[quoteId]/merge-meta/route.js +72 -0
  70. package/dist/modules/cpq/api/quotes/[quoteId]/merge-meta/route.js.map +7 -0
  71. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +35 -0
  72. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +7 -0
  73. package/dist/modules/cpq/api/quotes/[quoteId]/route.js +102 -0
  74. package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +7 -0
  75. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +27 -0
  76. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +7 -0
  77. package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.js +79 -0
  78. package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.js.map +7 -0
  79. package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.js +95 -0
  80. package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.js.map +7 -0
  81. package/dist/modules/cpq/api/quotes/attributes/route.js +52 -0
  82. package/dist/modules/cpq/api/quotes/attributes/route.js.map +7 -0
  83. package/dist/modules/cpq/api/quotes/from-subscription/route.js +72 -0
  84. package/dist/modules/cpq/api/quotes/from-subscription/route.js.map +7 -0
  85. package/dist/modules/cpq/api/quotes/price/route.js +69 -0
  86. package/dist/modules/cpq/api/quotes/price/route.js.map +7 -0
  87. package/dist/modules/cpq/api/quotes/route.js +97 -0
  88. package/dist/modules/cpq/api/quotes/route.js.map +7 -0
  89. package/dist/modules/cpq/api/quotes/validate-relationships/route.js +86 -0
  90. package/dist/modules/cpq/api/quotes/validate-relationships/route.js.map +7 -0
  91. package/dist/modules/cpq/api/resolveQuotingService.js +7 -0
  92. package/dist/modules/cpq/api/resolveQuotingService.js.map +7 -0
  93. package/dist/modules/cpq/api/wizards/resolve/route.js +38 -0
  94. package/dist/modules/cpq/api/wizards/resolve/route.js.map +7 -0
  95. package/dist/modules/cpq/api/wizards/route.js +111 -0
  96. package/dist/modules/cpq/api/wizards/route.js.map +7 -0
  97. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +136 -0
  98. package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +7 -0
  99. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +185 -0
  100. package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +7 -0
  101. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +343 -0
  102. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +7 -0
  103. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +13 -0
  104. package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +7 -0
  105. package/dist/modules/cpq/backend/cpq/inventory/page.js +35 -0
  106. package/dist/modules/cpq/backend/cpq/inventory/page.js.map +7 -0
  107. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +18 -0
  108. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +7 -0
  109. package/dist/modules/cpq/backend/cpq/inventory/shared.js +20 -0
  110. package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +7 -0
  111. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +794 -0
  112. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +7 -0
  113. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +13 -0
  114. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +7 -0
  115. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +917 -0
  116. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +7 -0
  117. package/dist/modules/cpq/backend/cpq/offerings/page.js +405 -0
  118. package/dist/modules/cpq/backend/cpq/offerings/page.js.map +7 -0
  119. package/dist/modules/cpq/backend/cpq/offerings/page.meta.js +18 -0
  120. package/dist/modules/cpq/backend/cpq/offerings/page.meta.js.map +7 -0
  121. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +677 -0
  122. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +7 -0
  123. package/dist/modules/cpq/backend/cpq/orders/page.js +134 -0
  124. package/dist/modules/cpq/backend/cpq/orders/page.js.map +7 -0
  125. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +18 -0
  126. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +7 -0
  127. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +272 -0
  128. package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +7 -0
  129. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +407 -0
  130. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +7 -0
  131. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +13 -0
  132. package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +7 -0
  133. package/dist/modules/cpq/backend/cpq/price-rules/page.js +211 -0
  134. package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +7 -0
  135. package/dist/modules/cpq/backend/cpq/price-rules/page.meta.js +18 -0
  136. package/dist/modules/cpq/backend/cpq/price-rules/page.meta.js.map +7 -0
  137. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +275 -0
  138. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +7 -0
  139. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +112 -0
  140. package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +7 -0
  141. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +492 -0
  142. package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +7 -0
  143. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.js +50 -0
  144. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.js.map +7 -0
  145. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +249 -0
  146. package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +7 -0
  147. package/dist/modules/cpq/backend/cpq/pricing/page.js +87 -0
  148. package/dist/modules/cpq/backend/cpq/pricing/page.js.map +7 -0
  149. package/dist/modules/cpq/backend/cpq/pricing/page.meta.js +18 -0
  150. package/dist/modules/cpq/backend/cpq/pricing/page.meta.js.map +7 -0
  151. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +652 -0
  152. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +7 -0
  153. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +1289 -0
  154. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +7 -0
  155. package/dist/modules/cpq/backend/cpq/quotes/page.js +283 -0
  156. package/dist/modules/cpq/backend/cpq/quotes/page.js.map +7 -0
  157. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +18 -0
  158. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +7 -0
  159. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +214 -0
  160. package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +7 -0
  161. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +1399 -0
  162. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +7 -0
  163. package/dist/modules/cpq/backend/cpq/specifications/page.js +148 -0
  164. package/dist/modules/cpq/backend/cpq/specifications/page.js.map +7 -0
  165. package/dist/modules/cpq/backend/cpq/specifications/page.meta.js +18 -0
  166. package/dist/modules/cpq/backend/cpq/specifications/page.meta.js.map +7 -0
  167. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +204 -0
  168. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +7 -0
  169. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +191 -0
  170. package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +7 -0
  171. package/dist/modules/cpq/backend/cpq/wizards/page.js +119 -0
  172. package/dist/modules/cpq/backend/cpq/wizards/page.js.map +7 -0
  173. package/dist/modules/cpq/backend/cpq/wizards/page.meta.js +18 -0
  174. package/dist/modules/cpq/backend/cpq/wizards/page.meta.js.map +7 -0
  175. package/dist/modules/cpq/ce.js +151 -0
  176. package/dist/modules/cpq/ce.js.map +7 -0
  177. package/dist/modules/cpq/cli.js +11 -0
  178. package/dist/modules/cpq/cli.js.map +7 -0
  179. package/dist/modules/cpq/components/CpqListView.js +265 -0
  180. package/dist/modules/cpq/components/CpqListView.js.map +7 -0
  181. package/dist/modules/cpq/components/NumberInput.js +95 -0
  182. package/dist/modules/cpq/components/NumberInput.js.map +7 -0
  183. package/dist/modules/cpq/components/StatusPath.js +60 -0
  184. package/dist/modules/cpq/components/StatusPath.js.map +7 -0
  185. package/dist/modules/cpq/components/statusMaps.js +66 -0
  186. package/dist/modules/cpq/components/statusMaps.js.map +7 -0
  187. package/dist/modules/cpq/components/useCpqRowActions.js +125 -0
  188. package/dist/modules/cpq/components/useCpqRowActions.js.map +7 -0
  189. package/dist/modules/cpq/data/entities.js +1556 -0
  190. package/dist/modules/cpq/data/entities.js.map +7 -0
  191. package/dist/modules/cpq/data/validators.js +575 -0
  192. package/dist/modules/cpq/data/validators.js.map +7 -0
  193. package/dist/modules/cpq/di.js +57 -0
  194. package/dist/modules/cpq/di.js.map +7 -0
  195. package/dist/modules/cpq/events.js +50 -0
  196. package/dist/modules/cpq/events.js.map +7 -0
  197. package/dist/modules/cpq/index.js +16 -0
  198. package/dist/modules/cpq/index.js.map +7 -0
  199. package/dist/modules/cpq/lib/seeds/api.js +32 -0
  200. package/dist/modules/cpq/lib/seeds/api.js.map +7 -0
  201. package/dist/modules/cpq/lib/seeds/tenant-provisioning.js +158 -0
  202. package/dist/modules/cpq/lib/seeds/tenant-provisioning.js.map +7 -0
  203. package/dist/modules/cpq/lib/seeds.js +6 -0
  204. package/dist/modules/cpq/lib/seeds.js.map +7 -0
  205. package/dist/modules/cpq/migrations/Migration20260220220000_cpq.js +40 -0
  206. package/dist/modules/cpq/migrations/Migration20260220220000_cpq.js.map +7 -0
  207. package/dist/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.js +54 -0
  208. package/dist/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.js.map +7 -0
  209. package/dist/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.js +15 -0
  210. package/dist/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.js.map +7 -0
  211. package/dist/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.js +62 -0
  212. package/dist/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.js.map +7 -0
  213. package/dist/modules/cpq/migrations/Migration20260402000000_cpq_workflows.js +56 -0
  214. package/dist/modules/cpq/migrations/Migration20260402000000_cpq_workflows.js.map +7 -0
  215. package/dist/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.js +26 -0
  216. package/dist/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.js.map +7 -0
  217. package/dist/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.js +37 -0
  218. package/dist/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.js.map +7 -0
  219. package/dist/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.js +35 -0
  220. package/dist/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.js.map +7 -0
  221. package/dist/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.js +21 -0
  222. package/dist/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.js.map +7 -0
  223. package/dist/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.js +46 -0
  224. package/dist/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.js.map +7 -0
  225. package/dist/modules/cpq/migrations/Migration20260410100000_cpq_inventory.js +118 -0
  226. package/dist/modules/cpq/migrations/Migration20260410100000_cpq_inventory.js.map +7 -0
  227. package/dist/modules/cpq/migrations/Migration20260411000000_cpq_orders.js +63 -0
  228. package/dist/modules/cpq/migrations/Migration20260411000000_cpq_orders.js.map +7 -0
  229. package/dist/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.js +28 -0
  230. package/dist/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.js.map +7 -0
  231. package/dist/modules/cpq/migrations/Migration20260504064621_cpq_arc.js +36 -0
  232. package/dist/modules/cpq/migrations/Migration20260504064621_cpq_arc.js.map +7 -0
  233. package/dist/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.js +14 -0
  234. package/dist/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.js.map +7 -0
  235. package/dist/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.js +14 -0
  236. package/dist/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.js.map +7 -0
  237. package/dist/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.js +58 -0
  238. package/dist/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.js.map +7 -0
  239. package/dist/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.js +29 -0
  240. package/dist/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.js.map +7 -0
  241. package/dist/modules/cpq/services/cpqBundleService.js +447 -0
  242. package/dist/modules/cpq/services/cpqBundleService.js.map +7 -0
  243. package/dist/modules/cpq/services/cpqInventoryService.js +1371 -0
  244. package/dist/modules/cpq/services/cpqInventoryService.js.map +7 -0
  245. package/dist/modules/cpq/services/cpqOrderService.js +901 -0
  246. package/dist/modules/cpq/services/cpqOrderService.js.map +7 -0
  247. package/dist/modules/cpq/services/cpqPricingService.js +289 -0
  248. package/dist/modules/cpq/services/cpqPricingService.js.map +7 -0
  249. package/dist/modules/cpq/services/cpqProductService.js +204 -0
  250. package/dist/modules/cpq/services/cpqProductService.js.map +7 -0
  251. package/dist/modules/cpq/services/cpqQuotingService.js +1867 -0
  252. package/dist/modules/cpq/services/cpqQuotingService.js.map +7 -0
  253. package/dist/modules/cpq/services/cpqValidationService.js +234 -0
  254. package/dist/modules/cpq/services/cpqValidationService.js.map +7 -0
  255. package/dist/modules/cpq/services/cpqWizardService.js +137 -0
  256. package/dist/modules/cpq/services/cpqWizardService.js.map +7 -0
  257. package/dist/modules/cpq/services/types.js +114 -0
  258. package/dist/modules/cpq/services/types.js.map +7 -0
  259. package/dist/modules/cpq/setup.js +49 -0
  260. package/dist/modules/cpq/setup.js.map +7 -0
  261. package/dist/modules/cpq/subscribers/subscription-amended-log.js +18 -0
  262. package/dist/modules/cpq/subscribers/subscription-amended-log.js.map +7 -0
  263. package/dist/modules/cpq/subscribers/subscription-cancelled-log.js +19 -0
  264. package/dist/modules/cpq/subscribers/subscription-cancelled-log.js.map +7 -0
  265. package/dist/modules/cpq/subscribers/subscription-merged-log.js +20 -0
  266. package/dist/modules/cpq/subscribers/subscription-merged-log.js.map +7 -0
  267. package/dist/modules/cpq/subscribers/subscription-renewed-log.js +18 -0
  268. package/dist/modules/cpq/subscribers/subscription-renewed-log.js.map +7 -0
  269. package/dist/modules/cpq/subscribers/subscription-superseded-log.js +18 -0
  270. package/dist/modules/cpq/subscribers/subscription-superseded-log.js.map +7 -0
  271. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +192 -0
  272. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +7 -0
  273. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.js +17 -0
  274. package/dist/modules/cpq/widgets/injection/customer-inventory/widget.js.map +7 -0
  275. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +79 -0
  276. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +7 -0
  277. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.js +17 -0
  278. package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.js.map +7 -0
  279. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +181 -0
  280. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +7 -0
  281. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.js +17 -0
  282. package/dist/modules/cpq/widgets/injection/quote-configurator/widget.js.map +7 -0
  283. package/dist/modules/cpq/widgets/injection-table.js +44 -0
  284. package/dist/modules/cpq/widgets/injection-table.js.map +7 -0
  285. package/dist/modules/cpq/workflows/WizardRunner.js +113 -0
  286. package/dist/modules/cpq/workflows/WizardRunner.js.map +7 -0
  287. package/dist/modules/cpq/workflows/WizardTrigger.js +116 -0
  288. package/dist/modules/cpq/workflows/WizardTrigger.js.map +7 -0
  289. package/dist/modules/cpq/workflows/engine.js +188 -0
  290. package/dist/modules/cpq/workflows/engine.js.map +7 -0
  291. package/dist/modules/cpq/workflows/registry.js +16 -0
  292. package/dist/modules/cpq/workflows/registry.js.map +7 -0
  293. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +119 -0
  294. package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +7 -0
  295. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +24 -0
  296. package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +7 -0
  297. package/dist/modules/cpq/workflows/steps/ItemListStep.js +115 -0
  298. package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +7 -0
  299. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +327 -0
  300. package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +7 -0
  301. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +231 -0
  302. package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +7 -0
  303. package/dist/modules/cpq/workflows/steps/ReviewStep.js +152 -0
  304. package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +7 -0
  305. package/dist/modules/cpq/workflows/steps/index.js +49 -0
  306. package/dist/modules/cpq/workflows/steps/index.js.map +7 -0
  307. package/dist/modules/cpq/workflows/types.js +1 -0
  308. package/dist/modules/cpq/workflows/types.js.map +7 -0
  309. package/package.json +150 -0
  310. package/src/modules/cpq/__integration__/TC-CPQ-001.spec.ts +177 -0
  311. package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +123 -0
  312. package/src/modules/cpq/__integration__/TC-CPQ-003.spec.ts +115 -0
  313. package/src/modules/cpq/__integration__/TC-CPQ-004.spec.ts +184 -0
  314. package/src/modules/cpq/__integration__/TC-CPQ-005.spec.ts +43 -0
  315. package/src/modules/cpq/__integration__/TC-CPQ-006.spec.ts +87 -0
  316. package/src/modules/cpq/__integration__/TC-CPQ-007.spec.ts +79 -0
  317. package/src/modules/cpq/__integration__/TC-CPQ-008.spec.ts +55 -0
  318. package/src/modules/cpq/__integration__/TC-CPQ-009.spec.ts +45 -0
  319. package/src/modules/cpq/__integration__/TC-CPQ-010.spec.ts +54 -0
  320. package/src/modules/cpq/__integration__/TC-CPQ-011.spec.ts +24 -0
  321. package/src/modules/cpq/__integration__/TC-CPQ-012.spec.ts +62 -0
  322. package/src/modules/cpq/__integration__/TC-CPQ-ARC-001.spec.ts +52 -0
  323. package/src/modules/cpq/__integration__/TC-CPQ-ARC-002.spec.ts +65 -0
  324. package/src/modules/cpq/__integration__/TC-CPQ-ARC-003.spec.ts +107 -0
  325. package/src/modules/cpq/__integration__/TC-CPQ-ARC-004.spec.ts +74 -0
  326. package/src/modules/cpq/__integration__/TC-CPQ-ARC-005.spec.ts +86 -0
  327. package/src/modules/cpq/__integration__/TC-CPQ-ARC-006.spec.ts +79 -0
  328. package/src/modules/cpq/__integration__/TC-CPQ-ARC-007.spec.ts +63 -0
  329. package/src/modules/cpq/__integration__/TC-CPQ-ARC-008.spec.ts +62 -0
  330. package/src/modules/cpq/__integration__/TC-CPQ-ARC-009.spec.ts +62 -0
  331. package/src/modules/cpq/__integration__/TC-CPQ-ARC-010.spec.ts +59 -0
  332. package/src/modules/cpq/__integration__/TC-CPQ-ARC-011.spec.ts +128 -0
  333. package/src/modules/cpq/__integration__/_helpers/cpqArcFixtures.ts +208 -0
  334. package/src/modules/cpq/__tests__/package-isolation.test.ts +232 -0
  335. package/src/modules/cpq/acl.ts +26 -0
  336. package/src/modules/cpq/api/_helpers/__tests__/handleArcError.test.ts +117 -0
  337. package/src/modules/cpq/api/_helpers/handleArcError.ts +81 -0
  338. package/src/modules/cpq/api/attribute-options/route.ts +55 -0
  339. package/src/modules/cpq/api/bundle-slots/route.ts +112 -0
  340. package/src/modules/cpq/api/bundle-tree/route.ts +31 -0
  341. package/src/modules/cpq/api/context.ts +39 -0
  342. package/src/modules/cpq/api/inventory/assets/route.ts +123 -0
  343. package/src/modules/cpq/api/inventory/assets/status/route.ts +34 -0
  344. package/src/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.ts +30 -0
  345. package/src/modules/cpq/api/inventory/customer/[customerId]/route.ts +31 -0
  346. package/src/modules/cpq/api/inventory/subscription-items/route.ts +107 -0
  347. package/src/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.ts +64 -0
  348. package/src/modules/cpq/api/inventory/subscriptions/expiring/route.ts +66 -0
  349. package/src/modules/cpq/api/inventory/subscriptions/route.ts +126 -0
  350. package/src/modules/cpq/api/inventory/subscriptions/status/route.ts +34 -0
  351. package/src/modules/cpq/api/offering-components/route.ts +103 -0
  352. package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +48 -0
  353. package/src/modules/cpq/api/orders/[orderId]/route.ts +28 -0
  354. package/src/modules/cpq/api/orders/[orderId]/status/route.ts +38 -0
  355. package/src/modules/cpq/api/orders/route.ts +77 -0
  356. package/src/modules/cpq/api/price-rules/route.ts +210 -0
  357. package/src/modules/cpq/api/pricing-table-entries/route.ts +159 -0
  358. package/src/modules/cpq/api/pricing-tables/route.ts +202 -0
  359. package/src/modules/cpq/api/product-attributes/route.ts +175 -0
  360. package/src/modules/cpq/api/product-charges/route.ts +181 -0
  361. package/src/modules/cpq/api/product-offerings/route.ts +329 -0
  362. package/src/modules/cpq/api/product-relationships/route.ts +156 -0
  363. package/src/modules/cpq/api/product-specifications/route.ts +325 -0
  364. package/src/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.ts +77 -0
  365. package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +28 -0
  366. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +55 -0
  367. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +26 -0
  368. package/src/modules/cpq/api/quotes/[quoteId]/merge-meta/route.ts +78 -0
  369. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +34 -0
  370. package/src/modules/cpq/api/quotes/[quoteId]/route.ts +109 -0
  371. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +26 -0
  372. package/src/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.ts +91 -0
  373. package/src/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.ts +107 -0
  374. package/src/modules/cpq/api/quotes/attributes/route.ts +61 -0
  375. package/src/modules/cpq/api/quotes/from-subscription/route.ts +85 -0
  376. package/src/modules/cpq/api/quotes/price/route.ts +81 -0
  377. package/src/modules/cpq/api/quotes/route.ts +114 -0
  378. package/src/modules/cpq/api/quotes/validate-relationships/route.ts +100 -0
  379. package/src/modules/cpq/api/resolveQuotingService.ts +11 -0
  380. package/src/modules/cpq/api/wizards/resolve/route.ts +39 -0
  381. package/src/modules/cpq/api/wizards/route.ts +127 -0
  382. package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +166 -0
  383. package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +251 -0
  384. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +9 -0
  385. package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +440 -0
  386. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +16 -0
  387. package/src/modules/cpq/backend/cpq/inventory/page.tsx +49 -0
  388. package/src/modules/cpq/backend/cpq/inventory/shared.tsx +32 -0
  389. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +9 -0
  390. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +1085 -0
  391. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +1165 -0
  392. package/src/modules/cpq/backend/cpq/offerings/page.meta.ts +16 -0
  393. package/src/modules/cpq/backend/cpq/offerings/page.tsx +468 -0
  394. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +921 -0
  395. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +16 -0
  396. package/src/modules/cpq/backend/cpq/orders/page.tsx +182 -0
  397. package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +310 -0
  398. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +9 -0
  399. package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +472 -0
  400. package/src/modules/cpq/backend/cpq/price-rules/page.meta.ts +16 -0
  401. package/src/modules/cpq/backend/cpq/price-rules/page.tsx +253 -0
  402. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +431 -0
  403. package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +122 -0
  404. package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +588 -0
  405. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.ts +90 -0
  406. package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +338 -0
  407. package/src/modules/cpq/backend/cpq/pricing/page.meta.ts +16 -0
  408. package/src/modules/cpq/backend/cpq/pricing/page.tsx +111 -0
  409. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +737 -0
  410. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +1721 -0
  411. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +16 -0
  412. package/src/modules/cpq/backend/cpq/quotes/page.tsx +370 -0
  413. package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +300 -0
  414. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +1722 -0
  415. package/src/modules/cpq/backend/cpq/specifications/page.meta.ts +16 -0
  416. package/src/modules/cpq/backend/cpq/specifications/page.tsx +191 -0
  417. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +319 -0
  418. package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +234 -0
  419. package/src/modules/cpq/backend/cpq/wizards/page.meta.ts +16 -0
  420. package/src/modules/cpq/backend/cpq/wizards/page.tsx +141 -0
  421. package/src/modules/cpq/ce.ts +150 -0
  422. package/src/modules/cpq/cli.ts +10 -0
  423. package/src/modules/cpq/components/CpqListView.tsx +431 -0
  424. package/src/modules/cpq/components/NumberInput.tsx +166 -0
  425. package/src/modules/cpq/components/StatusPath.tsx +116 -0
  426. package/src/modules/cpq/components/statusMaps.ts +148 -0
  427. package/src/modules/cpq/components/useCpqRowActions.tsx +183 -0
  428. package/src/modules/cpq/data/__tests__/validators.arc.test.ts +271 -0
  429. package/src/modules/cpq/data/__tests__/validators.test.ts +782 -0
  430. package/src/modules/cpq/data/entities.ts +1467 -0
  431. package/src/modules/cpq/data/validators.ts +663 -0
  432. package/src/modules/cpq/di.ts +70 -0
  433. package/src/modules/cpq/events.ts +62 -0
  434. package/src/modules/cpq/index.ts +15 -0
  435. package/src/modules/cpq/lib/seeds/__tests__/api.test.ts +86 -0
  436. package/src/modules/cpq/lib/seeds/__tests__/tenant-provisioning.test.ts +349 -0
  437. package/src/modules/cpq/lib/seeds/api.ts +119 -0
  438. package/src/modules/cpq/lib/seeds/tenant-provisioning.ts +239 -0
  439. package/src/modules/cpq/lib/seeds.ts +18 -0
  440. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +7473 -0
  441. package/src/modules/cpq/migrations/Migration20260220220000_cpq.ts +51 -0
  442. package/src/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.ts +70 -0
  443. package/src/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.ts +15 -0
  444. package/src/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.ts +73 -0
  445. package/src/modules/cpq/migrations/Migration20260402000000_cpq_workflows.ts +59 -0
  446. package/src/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.ts +30 -0
  447. package/src/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.ts +36 -0
  448. package/src/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.ts +33 -0
  449. package/src/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.ts +22 -0
  450. package/src/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.ts +56 -0
  451. package/src/modules/cpq/migrations/Migration20260410100000_cpq_inventory.ts +118 -0
  452. package/src/modules/cpq/migrations/Migration20260411000000_cpq_orders.ts +62 -0
  453. package/src/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.ts +33 -0
  454. package/src/modules/cpq/migrations/Migration20260504064621_cpq_arc.ts +68 -0
  455. package/src/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.ts +30 -0
  456. package/src/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.ts +27 -0
  457. package/src/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.ts +82 -0
  458. package/src/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.ts +52 -0
  459. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +59 -0
  460. package/src/modules/cpq/services/__tests__/cpqBundleService.test.ts +661 -0
  461. package/src/modules/cpq/services/__tests__/cpqInventoryService.arc.test.ts +357 -0
  462. package/src/modules/cpq/services/__tests__/cpqInventoryService.test.ts +718 -0
  463. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +603 -0
  464. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +1012 -0
  465. package/src/modules/cpq/services/__tests__/cpqProductService.test.ts +603 -0
  466. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +143 -0
  467. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +658 -0
  468. package/src/modules/cpq/services/__tests__/cpqValidationService.test.ts +811 -0
  469. package/src/modules/cpq/services/__tests__/cpqWizardService.test.ts +389 -0
  470. package/src/modules/cpq/services/__tests__/types.test.ts +359 -0
  471. package/src/modules/cpq/services/cpqBundleService.ts +671 -0
  472. package/src/modules/cpq/services/cpqInventoryService.ts +1929 -0
  473. package/src/modules/cpq/services/cpqOrderService.ts +1230 -0
  474. package/src/modules/cpq/services/cpqPricingService.ts +412 -0
  475. package/src/modules/cpq/services/cpqProductService.ts +311 -0
  476. package/src/modules/cpq/services/cpqQuotingService.ts +2638 -0
  477. package/src/modules/cpq/services/cpqValidationService.ts +323 -0
  478. package/src/modules/cpq/services/cpqWizardService.ts +204 -0
  479. package/src/modules/cpq/services/types.ts +303 -0
  480. package/src/modules/cpq/setup.ts +63 -0
  481. package/src/modules/cpq/subscribers/subscription-amended-log.ts +20 -0
  482. package/src/modules/cpq/subscribers/subscription-cancelled-log.ts +19 -0
  483. package/src/modules/cpq/subscribers/subscription-merged-log.ts +22 -0
  484. package/src/modules/cpq/subscribers/subscription-renewed-log.ts +18 -0
  485. package/src/modules/cpq/subscribers/subscription-superseded-log.ts +19 -0
  486. package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +339 -0
  487. package/src/modules/cpq/widgets/injection/customer-inventory/widget.ts +16 -0
  488. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +96 -0
  489. package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.ts +16 -0
  490. package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +265 -0
  491. package/src/modules/cpq/widgets/injection/quote-configurator/widget.ts +16 -0
  492. package/src/modules/cpq/widgets/injection-table.ts +42 -0
  493. package/src/modules/cpq/workflows/WizardRunner.tsx +158 -0
  494. package/src/modules/cpq/workflows/WizardTrigger.tsx +142 -0
  495. package/src/modules/cpq/workflows/engine.tsx +278 -0
  496. package/src/modules/cpq/workflows/registry.ts +15 -0
  497. package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +136 -0
  498. package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +34 -0
  499. package/src/modules/cpq/workflows/steps/ItemListStep.tsx +146 -0
  500. package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +438 -0
  501. package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +287 -0
  502. package/src/modules/cpq/workflows/steps/ReviewStep.tsx +219 -0
  503. package/src/modules/cpq/workflows/steps/index.ts +51 -0
  504. package/src/modules/cpq/workflows/types.ts +102 -0
@@ -0,0 +1,921 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useRouter, useParams } from 'next/navigation'
4
+ import { ArrowRight, ExternalLink, Play, X } from 'lucide-react'
5
+ import { Alert } from '@open-mercato/ui/primitives/alert'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
7
+ import { Tag } from '@open-mercato/ui/primitives/tag'
8
+ import { FormHeader } from '@open-mercato/ui/backend/forms'
9
+ import {
10
+ formatStatusLabel,
11
+ orderCpqStatusMap,
12
+ type OrderCpqStatus,
13
+ } from '../../../../components/statusMaps'
14
+ import { StatusPath } from '../../../../components/StatusPath'
15
+
16
+ // ─── Types ───────────────────────────────────────────────────────
17
+
18
+ type ArcLineSource = {
19
+ subscriptionItemId: string
20
+ name: string
21
+ mrcAmount: number
22
+ nrcAmount: number
23
+ quantity: number
24
+ }
25
+
26
+ type TierBreakdownEntry = { tier: number; from: number; to: number; pricePerUnit: number; quantity: number; subtotal: number }
27
+ type RuleAdjustment = { ruleCode: string; ruleName: string; ruleType: string; value: number; unitPriceBefore: number; unitPriceAfter: number; delta: number }
28
+
29
+ type ResolvedCharge = {
30
+ chargeName?: string
31
+ chargeType?: string
32
+ pricingMethod?: 'flat' | 'tiered' | 'per_unit'
33
+ unitPrice?: number
34
+ quantity?: number | null
35
+ totalPrice?: number | null
36
+ note?: string | null
37
+ breakdown?: { tiers: TierBreakdownEntry[] } | null
38
+ adjustments?: RuleAdjustment[] | null
39
+ }
40
+
41
+ type OrderLineResult = {
42
+ lineId: string
43
+ offeringId: string | null
44
+ offeringName: string
45
+ offeringType: string | null
46
+ productId: string | null
47
+ specId: string | null
48
+ action: string
49
+ parentLineId: string | null
50
+ quantity: number
51
+ configuration: Record<string, unknown>
52
+ startDate: string | null
53
+ termMonths: number | null
54
+ endDate: string | null
55
+ nrcTotal: number
56
+ mrcTotal: number
57
+ charges: ResolvedCharge[]
58
+ sourceQuoteLineId: string | null
59
+ arcSource: ArcLineSource | null
60
+ }
61
+
62
+ type OrderResult = {
63
+ id: string
64
+ orderId: string
65
+ orderNumber: string
66
+ customerId: string
67
+ cpqStatus: string
68
+ sourceQuoteId: string | null
69
+ quoteType: string
70
+ currencyCode: string
71
+ pricingSummary: {
72
+ nrcTotal: number
73
+ mrcTotal: number
74
+ currencyCode: string
75
+ }
76
+ activatedAt: string | null
77
+ createdAt: string
78
+ lines: OrderLineResult[]
79
+ }
80
+
81
+ const ARC_BADGE_VARIANTS: Record<string, 'brand' | 'success' | 'error'> = {
82
+ amend: 'brand',
83
+ renew: 'success',
84
+ cancel: 'error',
85
+ }
86
+
87
+ const RULE_TYPE_LABELS: Record<string, string> = {
88
+ discount_percent: '% discount',
89
+ discount_absolute: 'fixed discount',
90
+ surcharge_percent: '% surcharge',
91
+ surcharge_absolute: 'fixed surcharge',
92
+ price_override: 'override',
93
+ }
94
+
95
+ function fmtRuleValue(adj: RuleAdjustment, currency?: string): string {
96
+ if (adj.ruleType === 'discount_percent' || adj.ruleType === 'surcharge_percent') return `${adj.value}%`
97
+ if (currency) return fmt(adj.value, currency)
98
+ return String(adj.value)
99
+ }
100
+
101
+ // ─── Constants ───────────────────────────────────────────────────
102
+
103
+ function fmt(amount: number, currency: string): string {
104
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
105
+ }
106
+
107
+ async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
108
+ const res = await fetch(url, init)
109
+ const json = await res.json()
110
+ if (!res.ok) throw new Error(json?.error ?? `Request failed (${res.status})`)
111
+ return json as T
112
+ }
113
+
114
+ // ─── Main Page ───────────────────────────────────────────────────
115
+
116
+ export default function CpqOrderDetailPage(props: { params?: { id?: string } }) {
117
+ const router = useRouter()
118
+ const urlParams = useParams()
119
+ const cpqOrderId = (props.params?.id ?? urlParams?.id) as string
120
+
121
+ const [order, setOrder] = React.useState<OrderResult | null>(null)
122
+ const [loading, setLoading] = React.useState(true)
123
+ const [error, setError] = React.useState<string | null>(null)
124
+ const [activating, setActivating] = React.useState(false)
125
+ const [expandedLines, setExpandedLines] = React.useState<Set<string>>(new Set())
126
+ const [pricingDetail, setPricingDetail] = React.useState<{ lineIdx: number; chargeIdx: number } | null>(null)
127
+ const [customerName, setCustomerName] = React.useState<string | null>(null)
128
+ const [linkedSubscriptions, setLinkedSubscriptions] = React.useState<Array<{
129
+ id: string
130
+ name: string
131
+ code: string
132
+ status: string
133
+ }>>([])
134
+
135
+ // Find every subscription this order touches. NEW orders write
136
+ // `sourceOrderId` on the freshly-created subscription, so a
137
+ // `?sourceOrderId=…` filter is enough there. ARC orders (amend /
138
+ // renew / cancel) mutate existing subscriptions in place — the
139
+ // subscription's `sourceOrderId` stays pinned to the original
140
+ // order, so we also resolve targets via the source quote.
141
+ const fetchLinkedSubscriptions = React.useCallback(
142
+ async (orderConfigId: string, sourceQuoteId: string | null) => {
143
+ const byOrderId = fetch(
144
+ `/api/cpq/inventory/subscriptions?sourceOrderId=${orderConfigId}&pageSize=10`,
145
+ )
146
+ .then((r) => (r.ok ? r.json() : null))
147
+ .then((d) => (d?.items ?? (Array.isArray(d) ? d : [])) as Array<{ id: string; name: string; code: string; status: string }>)
148
+ .catch(() => [])
149
+
150
+ const byTargets = sourceQuoteId
151
+ ? fetch(`/api/cpq/quotes/${sourceQuoteId}/target-subscriptions`)
152
+ .then((r) => (r.ok ? r.json() : null))
153
+ .then(async (d) => {
154
+ const targets: Array<{ subscriptionId: string }> = d?.items ?? []
155
+ const fetched = await Promise.all(
156
+ targets.map((t) =>
157
+ fetch(`/api/cpq/inventory/subscriptions?id=${t.subscriptionId}`)
158
+ .then((r) => (r.ok ? r.json() : null))
159
+ .catch(() => null),
160
+ ),
161
+ )
162
+ return fetched.filter(Boolean) as Array<{ id: string; name: string; code: string; status: string }>
163
+ })
164
+ .catch(() => [] as Array<{ id: string; name: string; code: string; status: string }>)
165
+ : Promise.resolve([] as Array<{ id: string; name: string; code: string; status: string }>)
166
+
167
+ const [a, b] = await Promise.all([byOrderId, byTargets])
168
+ // Dedupe by id — a single subscription may appear in both lists
169
+ // (e.g. a NEW order that was later amended into the same sub).
170
+ const seen = new Set<string>()
171
+ const merged: Array<{ id: string; name: string; code: string; status: string }> = []
172
+ for (const sub of [...a, ...b]) {
173
+ if (!sub || seen.has(sub.id)) continue
174
+ seen.add(sub.id)
175
+ merged.push({ id: sub.id, name: sub.name, code: sub.code, status: sub.status })
176
+ }
177
+ setLinkedSubscriptions(merged)
178
+ },
179
+ [],
180
+ )
181
+
182
+ React.useEffect(() => {
183
+ let cancelled = false
184
+ async function load() {
185
+ try {
186
+ const result = await apiJson<OrderResult>(`/api/cpq/orders/${cpqOrderId}`)
187
+ if (!cancelled) {
188
+ setOrder(result)
189
+ fetch(`/api/customers/companies?id=${result.customerId}&pageSize=1`)
190
+ .then((r) => r.json())
191
+ .then((data) => {
192
+ const items = data.items ?? data ?? []
193
+ const c = items[0]
194
+ if (c && !cancelled) setCustomerName(c.display_name || c.displayName || c.name || null)
195
+ })
196
+ .catch(() => {})
197
+ if (result.cpqStatus === 'active' || result.cpqStatus === 'fulfilled') {
198
+ void fetchLinkedSubscriptions(result.id, result.sourceQuoteId)
199
+ }
200
+ }
201
+ } catch (err) {
202
+ if (!cancelled) setError((err as Error).message)
203
+ } finally {
204
+ if (!cancelled) setLoading(false)
205
+ }
206
+ }
207
+ load()
208
+ return () => { cancelled = true }
209
+ }, [cpqOrderId, fetchLinkedSubscriptions])
210
+
211
+ const activateOrder = async () => {
212
+ if (!order) return
213
+ try {
214
+ setActivating(true)
215
+ setError(null)
216
+ const result = await apiJson<OrderResult>(`/api/cpq/orders/${order.id}/activate`, {
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify({}),
220
+ })
221
+ setOrder(result)
222
+ void fetchLinkedSubscriptions(result.id, result.sourceQuoteId)
223
+ } catch (err) {
224
+ setError((err as Error).message)
225
+ } finally {
226
+ setActivating(false)
227
+ }
228
+ }
229
+
230
+ const cancelOrder = async () => {
231
+ if (!order) return
232
+ try {
233
+ setError(null)
234
+ const result = await apiJson<OrderResult>(`/api/cpq/orders/${order.id}/status`, {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json' },
237
+ body: JSON.stringify({ targetStatus: 'cancelled' }),
238
+ })
239
+ setOrder(result)
240
+ } catch (err) {
241
+ setError((err as Error).message)
242
+ }
243
+ }
244
+
245
+ const toggleLine = (id: string) => {
246
+ setExpandedLines((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n })
247
+ }
248
+
249
+ // ─── Render: loading / error ─────────────────────────────────
250
+
251
+ if (loading) {
252
+ return (
253
+ <div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
254
+ <Spinner /> Loading…
255
+ </div>
256
+ )
257
+ }
258
+
259
+ if (!order) {
260
+ return (
261
+ <div className="space-y-4">
262
+ <button onClick={() => router.push('/backend/cpq/orders')} className="text-sm text-muted-foreground hover:text-foreground">← Back to Orders</button>
263
+ {error && <ErrorBanner message={error} />}
264
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Order not found.</div>
265
+ </div>
266
+ )
267
+ }
268
+
269
+ // ─── Render: detail ──────────────────────────────────────────
270
+
271
+ const canActivate = order.cpqStatus === 'draft' || order.cpqStatus === 'pending_activation'
272
+ const canCancel = order.cpqStatus === 'draft' || order.cpqStatus === 'pending_activation' || order.cpqStatus === 'active'
273
+ const currency = order.currencyCode
274
+
275
+ const togglePricingDetail = (lineIdx: number, chargeIdx: number) => {
276
+ setPricingDetail((prev) =>
277
+ prev?.lineIdx === lineIdx && prev?.chargeIdx === chargeIdx ? null : { lineIdx, chargeIdx },
278
+ )
279
+ }
280
+
281
+ return (
282
+ <div className="space-y-6">
283
+ {/* Header — standard FormHeader detail mode, mirrors the CPQ quote
284
+ layout so order ↔ quote stays consistent. Title is just the order
285
+ number; status / ARC / converted tags sit underneath; primary
286
+ actions (Activate, then Cancel) live on the right next to the
287
+ View Sales Order / Source Quote utility links. */}
288
+ <FormHeader
289
+ mode="detail"
290
+ backHref="/backend/cpq/orders"
291
+ backLabel="Back to CPQ Orders"
292
+ entityTypeLabel="CPQ Order"
293
+ title={order.orderNumber || order.orderId.slice(0, 8)}
294
+ statusBadge={
295
+ <div className="flex flex-wrap items-center gap-2 pt-1">
296
+ <Tag variant={orderCpqStatusMap[order.cpqStatus as OrderCpqStatus] ?? 'neutral'} dot>
297
+ {formatStatusLabel(order.cpqStatus)}
298
+ </Tag>
299
+ {order.quoteType && order.quoteType !== 'new' && (
300
+ <Tag variant={ARC_BADGE_VARIANTS[order.quoteType] ?? 'neutral'} className="uppercase">
301
+ {order.quoteType}
302
+ </Tag>
303
+ )}
304
+ </div>
305
+ }
306
+ actionsContent={
307
+ <>
308
+ <Button
309
+ type="button"
310
+ variant="outline"
311
+ onClick={() => router.push(`/backend/sales/orders/${order.orderId}`)}
312
+ >
313
+ <ExternalLink className="h-3.5 w-3.5" />
314
+ View Sales Order
315
+ </Button>
316
+ {order.sourceQuoteId && (
317
+ <Button
318
+ type="button"
319
+ variant="outline"
320
+ onClick={() => router.push(`/backend/cpq/quotes/${order.sourceQuoteId}`)}
321
+ >
322
+ Source Quote
323
+ </Button>
324
+ )}
325
+ {canActivate && (
326
+ <Button type="button" onClick={activateOrder} disabled={activating}>
327
+ {activating ? <Spinner /> : <Play className="h-4 w-4" />}
328
+ Activate
329
+ </Button>
330
+ )}
331
+ {canCancel && (
332
+ <Button type="button" variant="destructive-outline" onClick={cancelOrder}>
333
+ <X className="h-4 w-4" />
334
+ Cancel
335
+ </Button>
336
+ )}
337
+ </>
338
+ }
339
+ />
340
+
341
+ {/* Status path — visual progress only. Orders advance via the
342
+ dedicated Activate / Cancel buttons because activation has
343
+ backend side effects (subscription / asset creation). */}
344
+ <div className="flex items-center gap-3 flex-wrap">
345
+ <StatusPath
346
+ // Order path is read-only — activation has backend side effects
347
+ // (subscription / asset creation) so transitions go through the
348
+ // dedicated Activate / Cancel buttons.
349
+ current={order.cpqStatus}
350
+ path={['draft', 'pending_activation', 'active', 'fulfilled']}
351
+ terminals={['cancelled']}
352
+ statusMap={orderCpqStatusMap}
353
+ ariaLabel="Order status path"
354
+ />
355
+ </div>
356
+
357
+ {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}
358
+
359
+ {/* Linked subscriptions — one card per subscription this order
360
+ touches. NEW orders write one fresh subscription; ARC orders
361
+ (amend / renew / cancel) reach one or more existing
362
+ subscriptions through the source quote's target list. */}
363
+ {linkedSubscriptions.length > 0 && (
364
+ <div className="space-y-2">
365
+ {linkedSubscriptions.map((sub) => (
366
+ <div
367
+ key={sub.id}
368
+ className="rounded-lg border bg-card p-4 flex items-center gap-4"
369
+ >
370
+ <div className="flex-1 min-w-0 space-y-1">
371
+ <div className="flex items-center gap-2 flex-wrap">
372
+ <span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
373
+ Linked Subscription
374
+ </span>
375
+ <Tag
376
+ variant={sub.status === 'active' ? 'success' : 'info'}
377
+ dot
378
+ className="text-[10px] px-1.5"
379
+ >
380
+ {sub.status}
381
+ </Tag>
382
+ </div>
383
+ <div className="text-sm font-medium">
384
+ {sub.name}{' '}
385
+ <span className="text-muted-foreground font-mono text-xs">({sub.code})</span>
386
+ </div>
387
+ {order.activatedAt && (
388
+ <div className="text-xs text-muted-foreground">
389
+ Activated on {new Date(order.activatedAt).toLocaleString()}
390
+ </div>
391
+ )}
392
+ </div>
393
+ <Button
394
+ type="button"
395
+ variant="outline"
396
+ onClick={() => router.push(`/backend/cpq/inventory/subscriptions/${sub.id}`)}
397
+ >
398
+ <ArrowRight className="h-3.5 w-3.5" />
399
+ Open
400
+ </Button>
401
+ </div>
402
+ ))}
403
+ </div>
404
+ )}
405
+
406
+ {/* Meta cards — secondary reference info, matches the quote layout. */}
407
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
408
+ <MetaCard label="Customer">
409
+ {customerName ? (
410
+ <span className="font-medium">{customerName}</span>
411
+ ) : (
412
+ <span className="font-mono text-xs text-muted-foreground">
413
+ {order.customerId.slice(0, 8)}…
414
+ </span>
415
+ )}
416
+ </MetaCard>
417
+ <MetaCard label="Created">
418
+ <span className="font-medium">{new Date(order.createdAt).toLocaleDateString()}</span>
419
+ </MetaCard>
420
+ <MetaCard label="Currency">
421
+ <span className="font-medium">{currency}</span>
422
+ </MetaCard>
423
+ </div>
424
+
425
+ {/* Pricing Summary Card */}
426
+ <div className="rounded-lg border bg-card p-4">
427
+ <h3 className="text-sm font-medium mb-3">Pricing Summary</h3>
428
+ <div className="grid grid-cols-2 gap-4">
429
+ <div>
430
+ <p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
431
+ <p className="text-lg font-semibold font-mono">{fmt(order.pricingSummary.nrcTotal, currency)}</p>
432
+ </div>
433
+ <div>
434
+ <p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
435
+ <p className="text-lg font-semibold font-mono">{fmt(order.pricingSummary.mrcTotal, currency)}</p>
436
+ </div>
437
+ </div>
438
+ </div>
439
+
440
+ {/* Order Lines */}
441
+ <OrderLineTree
442
+ lines={order.lines}
443
+ currency={currency}
444
+ expandedLines={expandedLines}
445
+ pricingDetail={pricingDetail}
446
+ onToggleLine={toggleLine}
447
+ onTogglePricingDetail={togglePricingDetail}
448
+ />
449
+ </div>
450
+ )
451
+ }
452
+
453
+ function MetaCard({ label, children }: { label: string; children: React.ReactNode }) {
454
+ return (
455
+ <div className="rounded-lg border bg-card p-4">
456
+ <div className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
457
+ {label}
458
+ </div>
459
+ <div className="mt-1 text-sm">{children}</div>
460
+ </div>
461
+ )
462
+ }
463
+
464
+ // ─── Sub-components ──────────────────────────────────────────────
465
+
466
+ function OrderLineTree({
467
+ lines,
468
+ currency,
469
+ expandedLines,
470
+ pricingDetail,
471
+ onToggleLine,
472
+ onTogglePricingDetail,
473
+ }: {
474
+ lines: OrderLineResult[]
475
+ currency: string
476
+ expandedLines: Set<string>
477
+ pricingDetail: { lineIdx: number; chargeIdx: number } | null
478
+ onToggleLine: (id: string) => void
479
+ onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
480
+ }) {
481
+ const rootLines = lines.filter((l) => !l.parentLineId)
482
+ const childrenByParent = new Map<string, OrderLineResult[]>()
483
+ for (const line of lines) {
484
+ if (line.parentLineId) {
485
+ const arr = childrenByParent.get(line.parentLineId) ?? []
486
+ arr.push(line)
487
+ childrenByParent.set(line.parentLineId, arr)
488
+ }
489
+ }
490
+
491
+ // Stable index per visible line so the pricing-detail toggle can be
492
+ // keyed by (lineIdx, chargeIdx) the same way the quote page does.
493
+ const indexOf = new Map<string, number>()
494
+ lines.forEach((l, i) => indexOf.set(l.lineId, i))
495
+
496
+ return (
497
+ <div>
498
+ <h3 className="text-sm font-medium mb-2 px-1">Line Items ({lines.length})</h3>
499
+ {lines.length === 0 ? (
500
+ <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">
501
+ No line items.
502
+ </div>
503
+ ) : (
504
+ <div className="rounded-lg border bg-card divide-y">
505
+ {rootLines.map((line) => {
506
+ const isBundle = line.offeringType === 'bundle'
507
+ const children = childrenByParent.get(line.lineId) ?? []
508
+ const isExpanded = expandedLines.has(line.lineId)
509
+ const lineIdx = indexOf.get(line.lineId) ?? 0
510
+ return (
511
+ <div key={line.lineId}>
512
+ <OrderLineRow
513
+ line={line}
514
+ lineIdx={lineIdx}
515
+ currency={currency}
516
+ isExpanded={isExpanded}
517
+ isBundle={isBundle}
518
+ childCount={children.length}
519
+ indent={0}
520
+ pricingDetail={pricingDetail}
521
+ onToggle={() => onToggleLine(line.lineId)}
522
+ onTogglePricingDetail={onTogglePricingDetail}
523
+ />
524
+ {isBundle && isExpanded && children.length > 0 && (
525
+ <div className="border-t border-dashed border-brand-violet/30 bg-brand-violet/5">
526
+ <div className="px-6 py-1.5">
527
+ <span className="text-[10px] font-medium text-brand-violet uppercase tracking-wider">Bundle Components ({children.length})</span>
528
+ </div>
529
+ {children.map((child) => {
530
+ const childIdx = indexOf.get(child.lineId) ?? 0
531
+ return (
532
+ <OrderLineRow
533
+ key={child.lineId}
534
+ line={child}
535
+ lineIdx={childIdx}
536
+ currency={currency}
537
+ isExpanded={expandedLines.has(child.lineId)}
538
+ isBundle={false}
539
+ childCount={0}
540
+ indent={1}
541
+ pricingDetail={pricingDetail}
542
+ onToggle={() => onToggleLine(child.lineId)}
543
+ onTogglePricingDetail={onTogglePricingDetail}
544
+ />
545
+ )
546
+ })}
547
+ </div>
548
+ )}
549
+ </div>
550
+ )
551
+ })}
552
+ </div>
553
+ )}
554
+ </div>
555
+ )
556
+ }
557
+
558
+ function OrderLineRow({ line, lineIdx, currency, isExpanded, isBundle, childCount, indent, pricingDetail, onToggle, onTogglePricingDetail }: {
559
+ line: OrderLineResult; lineIdx: number; currency: string; isExpanded: boolean; isBundle: boolean; childCount: number; indent: number
560
+ pricingDetail: { lineIdx: number; chargeIdx: number } | null
561
+ onToggle: () => void
562
+ onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
563
+ }) {
564
+ const paddingLeft = indent > 0 ? `${indent * 2 + 1}rem` : undefined
565
+
566
+ return (
567
+ <div>
568
+ <div
569
+ className="flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors cursor-pointer"
570
+ style={paddingLeft ? { paddingLeft } : undefined}
571
+ onClick={onToggle}
572
+ >
573
+ <button className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground">
574
+ <svg className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
575
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
576
+ </svg>
577
+ </button>
578
+
579
+ <div className="flex-1 min-w-0">
580
+ <div className="flex items-center gap-2">
581
+ {indent > 0 && <span className="text-muted-foreground text-xs">└</span>}
582
+ {isBundle && (
583
+ <Tag variant="brand" className="px-1.5 text-xs">bundle</Tag>
584
+ )}
585
+ <span className="font-medium text-sm">{line.offeringName}</span>
586
+ {line.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">×{line.quantity}</span>}
587
+ <Tag variant="info" className="px-1.5 text-xs">{line.action}</Tag>
588
+ {isBundle && <span className="text-xs text-muted-foreground">{childCount} component{childCount !== 1 ? 's' : ''}</span>}
589
+ </div>
590
+ </div>
591
+
592
+ <div className="shrink-0 text-right text-xs space-y-0.5">
593
+ {line.nrcTotal > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(line.nrcTotal, currency)}</span></div>}
594
+ {line.mrcTotal > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(line.mrcTotal, currency)}</span></div>}
595
+ </div>
596
+ </div>
597
+
598
+ {isExpanded && (() => {
599
+ const internalKeys = new Set(['_arcMirroredName', '_arcMirroredFromItemId', 'offeringName'])
600
+ const visibleConfigEntries = Object.entries(line.configuration).filter(
601
+ ([key]) => !internalKeys.has(key),
602
+ )
603
+ const hasConfig = visibleConfigEntries.length > 0
604
+ const hasCharges = line.charges.length > 0
605
+ const hasTerm = !!(line.startDate || line.termMonths || line.endDate)
606
+ const hasArcDiff = !!line.arcSource
607
+ const mirroredFromItemId = (line.configuration?._arcMirroredFromItemId as string | undefined) ?? null
608
+ const isEmpty = !hasConfig && !hasCharges && !hasTerm && !hasArcDiff
609
+
610
+ return (
611
+ <div
612
+ className="border-t bg-muted/10 px-12 py-3 space-y-3"
613
+ style={paddingLeft ? { paddingLeft: `calc(${paddingLeft} + 2rem)` } : undefined}
614
+ >
615
+ {hasArcDiff && (
616
+ <ArcLineDiff line={line} currency={currency} />
617
+ )}
618
+ {isEmpty ? (
619
+ <div className="text-xs text-muted-foreground space-y-1">
620
+ <div>
621
+ No detailed breakdown for this line.
622
+ {mirroredFromItemId && ' Mirrored from existing subscription item.'}
623
+ </div>
624
+ {(line.nrcTotal > 0 || line.mrcTotal > 0) && (
625
+ <div className="flex gap-4">
626
+ {line.nrcTotal > 0 && (
627
+ <span>NRC <span className="font-mono font-medium text-foreground">{fmt(line.nrcTotal, currency)}</span></span>
628
+ )}
629
+ {line.mrcTotal > 0 && (
630
+ <span>MRC <span className="font-mono font-medium text-foreground">{fmt(line.mrcTotal, currency)}</span></span>
631
+ )}
632
+ </div>
633
+ )}
634
+ </div>
635
+ ) : (
636
+ <>
637
+ {hasConfig && (
638
+ <div>
639
+ <p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
640
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
641
+ {visibleConfigEntries.map(([key, value]) => (
642
+ <div key={key} className="text-xs">
643
+ <span className="text-muted-foreground">{key}: </span>
644
+ <span className="font-medium">{String(value)}</span>
645
+ </div>
646
+ ))}
647
+ </div>
648
+ </div>
649
+ )}
650
+
651
+ {hasCharges && (
652
+ <div>
653
+ <p className="text-xs font-medium text-muted-foreground mb-1">Charges</p>
654
+ <ChargeBreakdown
655
+ line={line}
656
+ lineIdx={lineIdx}
657
+ currency={currency}
658
+ pricingDetail={pricingDetail}
659
+ onTogglePricingDetail={onTogglePricingDetail}
660
+ />
661
+ </div>
662
+ )}
663
+
664
+ {hasTerm && (
665
+ <div className="flex gap-4 text-xs text-muted-foreground">
666
+ {line.startDate && <span>Start: {new Date(line.startDate).toLocaleDateString()}</span>}
667
+ {line.termMonths && <span>Term: {line.termMonths}mo</span>}
668
+ {line.endDate && <span>End: {new Date(line.endDate).toLocaleDateString()}</span>}
669
+ </div>
670
+ )}
671
+ </>
672
+ )}
673
+ </div>
674
+ )
675
+ })()}
676
+ </div>
677
+ )
678
+ }
679
+
680
+ // Charge breakdown table with the waterfall ("show pricing details")
681
+ // toggle. Same shape as the quote page so a charge looks identical on
682
+ // both surfaces, including tier-by-tier subtotals and any price-rule
683
+ // adjustments that fired during calculation.
684
+ function ChargeBreakdown({ line, lineIdx, currency, pricingDetail, onTogglePricingDetail }: {
685
+ line: OrderLineResult; lineIdx: number; currency: string
686
+ pricingDetail: { lineIdx: number; chargeIdx: number } | null
687
+ onTogglePricingDetail: (lineIdx: number, chargeIdx: number) => void
688
+ }) {
689
+ return (
690
+ <table className="w-full text-xs">
691
+ <thead>
692
+ <tr className="text-muted-foreground">
693
+ <th className="text-left py-1 font-medium">Charge</th>
694
+ <th className="text-left py-1 font-medium">Type</th>
695
+ <th className="text-right py-1 font-medium">Unit Price</th>
696
+ <th className="text-right py-1 font-medium">Qty</th>
697
+ <th className="text-right py-1 font-medium">Total</th>
698
+ <th className="w-8" />
699
+ </tr>
700
+ </thead>
701
+ <tbody>
702
+ {line.charges.map((c, i) => {
703
+ const hasDetail = !!(c.breakdown?.tiers?.length || c.adjustments?.length)
704
+ const isDetailOpen = pricingDetail?.lineIdx === lineIdx && pricingDetail?.chargeIdx === i
705
+ return (
706
+ <React.Fragment key={i}>
707
+ <tr className="border-t border-dashed border-border/50">
708
+ <td className="py-1">{c.chargeName ?? '—'}</td>
709
+ <td className="py-1 uppercase">{c.chargeType ?? '—'}</td>
710
+ <td className="py-1 text-right font-mono">{c.unitPrice != null ? fmt(c.unitPrice, currency) : '—'}</td>
711
+ <td className="py-1 text-right font-mono">{c.quantity ?? '—'}</td>
712
+ <td className="py-1 text-right font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : c.note ?? '—'}</td>
713
+ <td className="py-1 text-center">
714
+ {hasDetail && (
715
+ <button
716
+ onClick={() => onTogglePricingDetail(lineIdx, i)}
717
+ className="inline-flex items-center justify-center rounded p-0.5 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
718
+ title="Show pricing breakdown"
719
+ >
720
+ <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">
721
+ <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" />
722
+ </svg>
723
+ </button>
724
+ )}
725
+ </td>
726
+ </tr>
727
+ {isDetailOpen && (
728
+ <tr>
729
+ <td colSpan={6} className="pb-3 pt-1">
730
+ <div className="rounded border bg-background p-3 space-y-3 text-xs">
731
+ <div className="flex items-center justify-between">
732
+ <span className="font-semibold text-sm">Pricing Details — {c.chargeName}</span>
733
+ <span className="text-muted-foreground">{c.pricingMethod ?? 'flat'}</span>
734
+ </div>
735
+
736
+ {c.breakdown?.tiers && c.breakdown.tiers.length > 0 && (
737
+ <div>
738
+ <div className="font-medium text-muted-foreground mb-1">Tier Breakdown</div>
739
+ <table className="w-full">
740
+ <thead>
741
+ <tr className="text-muted-foreground">
742
+ <th className="text-left py-0.5 font-medium">Tier</th>
743
+ <th className="text-right py-0.5 font-medium">Range</th>
744
+ <th className="text-right py-0.5 font-medium">Price/Unit</th>
745
+ <th className="text-right py-0.5 font-medium">Qty</th>
746
+ <th className="text-right py-0.5 font-medium">Subtotal</th>
747
+ </tr>
748
+ </thead>
749
+ <tbody>
750
+ {c.breakdown.tiers.map((t) => (
751
+ <tr key={t.tier} className="border-t border-border/30">
752
+ <td className="py-0.5">{t.tier}</td>
753
+ <td className="py-0.5 text-right font-mono">{t.from}–{t.to}</td>
754
+ <td className="py-0.5 text-right font-mono">{fmt(t.pricePerUnit, currency)}</td>
755
+ <td className="py-0.5 text-right font-mono">{t.quantity}</td>
756
+ <td className="py-0.5 text-right font-mono">{fmt(t.subtotal, currency)}</td>
757
+ </tr>
758
+ ))}
759
+ <tr className="border-t font-medium">
760
+ <td colSpan={4} className="py-0.5 text-right">Tiered Total</td>
761
+ <td className="py-0.5 text-right font-mono">
762
+ {fmt(c.breakdown.tiers.reduce((s, t) => s + t.subtotal, 0), currency)}
763
+ </td>
764
+ </tr>
765
+ </tbody>
766
+ </table>
767
+ </div>
768
+ )}
769
+
770
+ {c.adjustments && c.adjustments.length > 0 && (
771
+ <div>
772
+ <div className="font-medium text-muted-foreground mb-1">Applied Price Rules</div>
773
+ <table className="w-full">
774
+ <thead>
775
+ <tr className="text-muted-foreground">
776
+ <th className="text-left py-0.5 font-medium">Rule</th>
777
+ <th className="text-left py-0.5 font-medium">Type</th>
778
+ <th className="text-right py-0.5 font-medium">Value</th>
779
+ <th className="text-right py-0.5 font-medium">Before</th>
780
+ <th className="text-right py-0.5 font-medium">After</th>
781
+ <th className="text-right py-0.5 font-medium">Delta</th>
782
+ </tr>
783
+ </thead>
784
+ <tbody>
785
+ {c.adjustments.map((adj, ai) => (
786
+ <tr key={ai} className="border-t border-border/30">
787
+ <td className="py-0.5">{adj.ruleName}</td>
788
+ <td className="py-0.5">{RULE_TYPE_LABELS[adj.ruleType] ?? adj.ruleType}</td>
789
+ <td className="py-0.5 text-right font-mono">{fmtRuleValue(adj, currency)}</td>
790
+ <td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceBefore, currency)}</td>
791
+ <td className="py-0.5 text-right font-mono">{fmt(adj.unitPriceAfter, currency)}</td>
792
+ <td className={`py-0.5 text-right font-mono ${adj.delta < 0 ? 'text-destructive' : adj.delta > 0 ? 'text-status-success-text' : ''}`}>
793
+ {adj.delta > 0 ? '+' : ''}{fmt(adj.delta, currency)}
794
+ </td>
795
+ </tr>
796
+ ))}
797
+ </tbody>
798
+ </table>
799
+ </div>
800
+ )}
801
+
802
+ <div className="flex items-center justify-between pt-1 border-t text-sm font-semibold">
803
+ <span>Final Total</span>
804
+ <span className="font-mono">{c.totalPrice != null ? fmt(c.totalPrice, currency) : '—'}</span>
805
+ </div>
806
+ </div>
807
+ </td>
808
+ </tr>
809
+ )}
810
+ </React.Fragment>
811
+ )
812
+ })}
813
+ </tbody>
814
+ </table>
815
+ )
816
+ }
817
+
818
+ function Spinner() {
819
+ return (
820
+ <svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
821
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
822
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
823
+ </svg>
824
+ )
825
+ }
826
+
827
+ function ErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () => void }) {
828
+ return (
829
+ <Alert variant="destructive" className="flex items-start gap-2">
830
+ <span className="flex-1">{message}</span>
831
+ {onDismiss && (
832
+ <button
833
+ onClick={onDismiss}
834
+ className="text-destructive/70 hover:text-destructive text-sm font-bold"
835
+ aria-label="Dismiss"
836
+ >
837
+ ×
838
+ </button>
839
+ )}
840
+ </Alert>
841
+ )
842
+ }
843
+
844
+ // XD-250 ARC: side-by-side "Before / After" snapshot for a quote / order line
845
+ // that mirrors an existing subscription item. "Before" comes from the live
846
+ // CpqInventorySubscriptionItem at render time; "After" is what the line will
847
+ // apply on activation. For 'cancel' lines the after column is greyed out.
848
+ function ArcLineDiff({ line, currency }: { line: OrderLineResult; currency: string }) {
849
+ const src = line.arcSource
850
+ if (!src) return null
851
+ const isCancel = line.action === 'cancel'
852
+
853
+ const beforeMrc = src.mrcAmount
854
+ const beforeNrc = src.nrcAmount
855
+ const beforeQty = src.quantity
856
+ const afterMrc = isCancel ? 0 : line.mrcTotal
857
+ const afterNrc = isCancel ? 0 : line.nrcTotal
858
+ const afterQty = isCancel ? 0 : line.quantity
859
+ const mrcDelta = afterMrc - beforeMrc
860
+ const nrcDelta = afterNrc - beforeNrc
861
+ const qtyDelta = afterQty - beforeQty
862
+
863
+ return (
864
+ <div>
865
+ <p className="text-xs font-medium text-muted-foreground mb-1.5">Change Preview</p>
866
+ <div className="rounded border bg-background overflow-hidden">
867
+ <table className="w-full text-xs">
868
+ <thead className="bg-muted/40">
869
+ <tr className="text-muted-foreground">
870
+ <th className="text-left px-3 py-1.5 font-medium" />
871
+ <th className="text-right px-3 py-1.5 font-medium">Before</th>
872
+ <th className="text-right px-3 py-1.5 font-medium">After</th>
873
+ <th className="text-right px-3 py-1.5 font-medium">Δ</th>
874
+ </tr>
875
+ </thead>
876
+ <tbody>
877
+ <ArcDiffRow label="Quantity" before={beforeQty} after={afterQty} delta={qtyDelta} currency={null} dimAfter={isCancel} />
878
+ <ArcDiffRow label="MRC" before={beforeMrc} after={afterMrc} delta={mrcDelta} currency={currency} dimAfter={isCancel} />
879
+ <ArcDiffRow label="NRC" before={beforeNrc} after={afterNrc} delta={nrcDelta} currency={currency} dimAfter={isCancel} />
880
+ </tbody>
881
+ </table>
882
+ </div>
883
+ {isCancel && (
884
+ <p className="text-xs text-destructive mt-1 italic">
885
+ Cancellation — item will be terminated on activation.
886
+ </p>
887
+ )}
888
+ </div>
889
+ )
890
+ }
891
+
892
+ function ArcDiffRow({
893
+ label,
894
+ before,
895
+ after,
896
+ delta,
897
+ currency,
898
+ dimAfter,
899
+ }: {
900
+ label: string
901
+ before: number
902
+ after: number
903
+ delta: number
904
+ currency: string | null
905
+ dimAfter: boolean
906
+ }) {
907
+ const fmtVal = (v: number) => (currency ? fmt(v, currency) : String(v))
908
+ const showDelta = delta !== 0
909
+ return (
910
+ <tr className="border-t border-dashed border-border/40">
911
+ <td className="px-3 py-1 text-muted-foreground">{label}</td>
912
+ <td className="px-3 py-1 text-right font-mono">{fmtVal(before)}</td>
913
+ <td className={`px-3 py-1 text-right font-mono ${dimAfter ? 'text-muted-foreground line-through' : ''}`}>
914
+ {fmtVal(after)}
915
+ </td>
916
+ <td className={`px-3 py-1 text-right font-mono font-medium ${showDelta ? (delta > 0 ? 'text-status-success-text' : 'text-destructive') : 'text-muted-foreground'}`}>
917
+ {showDelta ? `${delta > 0 ? '+' : ''}${fmtVal(delta)}` : '—'}
918
+ </td>
919
+ </tr>
920
+ )
921
+ }