@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,1085 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useRouter, useParams } from 'next/navigation'
4
+ import { Alert } from '@open-mercato/ui/primitives/alert'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Tag } from '@open-mercato/ui/primitives/tag'
7
+ import { FormHeader } from '@open-mercato/ui/backend/forms'
8
+ import {
9
+ formatStatusLabel,
10
+ subscriptionStatusMap,
11
+ type SubscriptionStatus,
12
+ } from '../../../../../components/statusMaps'
13
+ import { StatusPath } from '../../../../../components/StatusPath'
14
+
15
+ // XD-250 — ARC integration on subscription detail.
16
+ // • header buttons (Amend / Renew / Cancel) call /from-subscription
17
+ // and redirect to the new ARC quote
18
+ // • "Change History" section reads /change-log
19
+
20
+ type SnapshotItem = {
21
+ id: string
22
+ name?: string
23
+ mrcAmount?: number | string
24
+ nrcAmount?: number | string
25
+ quantity?: number
26
+ status?: string
27
+ }
28
+
29
+ type SubscriptionSnapshot = {
30
+ id?: string
31
+ currencyCode?: string
32
+ items?: SnapshotItem[]
33
+ } | null
34
+
35
+ type LineChangeEntry = {
36
+ action: 'add' | 'modify' | 'cancel'
37
+ itemId?: string
38
+ quoteLineId?: string | null
39
+ mrcDelta?: number
40
+ mrcAmount?: number
41
+ nrcAmount?: number
42
+ quantity?: number
43
+ }
44
+
45
+ type ChangeLogRow = {
46
+ id: string
47
+ changeType: string
48
+ sourceQuoteId: string | null
49
+ sourceOrderId: string | null
50
+ performedByUserId: string | null
51
+ effectiveAt: string
52
+ createdAt: string
53
+ reasonCode: string | null
54
+ reasonText: string | null
55
+ etfAmount: string | null
56
+ etfCurrency: string | null
57
+ termChange: Record<string, unknown> | null
58
+ lineChanges: LineChangeEntry[] | null
59
+ beforeSnapshot: SubscriptionSnapshot
60
+ afterSnapshot: SubscriptionSnapshot
61
+ mergedIntoSubscriptionId: string | null
62
+ mergedFromSubscriptionIds: string[] | null
63
+ }
64
+
65
+ const CHANGE_TYPE_LABELS: Record<string, string> = {
66
+ created: 'Created',
67
+ amend: 'Amended',
68
+ renew: 'Renewed',
69
+ cancel: 'Cancelled',
70
+ 'merge-result': 'Created from merging contracts',
71
+ 'merge-source': 'Merged into a new contract',
72
+ }
73
+
74
+ const CHANGE_TYPE_VARIANTS: Record<string, 'info' | 'success' | 'error' | 'brand' | 'warning' | 'neutral'> = {
75
+ created: 'neutral',
76
+ amend: 'info',
77
+ renew: 'success',
78
+ cancel: 'error',
79
+ 'merge-result': 'brand',
80
+ 'merge-source': 'warning',
81
+ }
82
+
83
+ type SubscriptionItem = {
84
+ id: string
85
+ subscriptionId: string
86
+ parentItemId: string | null
87
+ productId: string | null
88
+ offeringId: string | null
89
+ specId: string | null
90
+ name: string
91
+ status: string
92
+ configuration: Record<string, unknown>
93
+ charges: Array<Record<string, unknown>> | null
94
+ mrcAmount: number
95
+ nrcAmount: number
96
+ currencyCode: string
97
+ quantity: number
98
+ capacityTotal: string | null
99
+ capacityUsed: string | null
100
+ capacityUnit: string | null
101
+ sourceQuoteLineId: string | null
102
+ metadata: Record<string, unknown> | null
103
+ sortOrder: number
104
+ createdAt: string
105
+ updatedAt: string
106
+ children?: SubscriptionItem[]
107
+ }
108
+
109
+ type LinkedAsset = {
110
+ id: string
111
+ code: string
112
+ name: string
113
+ status: string
114
+ assetType: string
115
+ currencyCode: string
116
+ purchasePrice: number
117
+ quantity: number
118
+ subscriptionItemId: string | null
119
+ createdAt: string
120
+ }
121
+
122
+ type SubscriptionDetail = {
123
+ id: string
124
+ customerId: string
125
+ code: string
126
+ name: string
127
+ description: string | null
128
+ status: string
129
+ billingCycle: string
130
+ currencyCode: string
131
+ mrcAmount: number
132
+ nrcAmount: number
133
+ startDate: string | null
134
+ currentTermEnd: string | null
135
+ termMonths: number | null
136
+ autoRenew: boolean
137
+ activatedAt: string | null
138
+ suspendedAt: string | null
139
+ terminatedAt: string | null
140
+ pricingSummary: Record<string, unknown> | null
141
+ sourceQuoteId: string | null
142
+ sourceOrderId: string | null
143
+ metadata: Record<string, unknown> | null
144
+ createdAt: string
145
+ updatedAt: string
146
+ items: SubscriptionItem[]
147
+ }
148
+
149
+ const ALLOWED_TRANSITIONS: Record<string, string[]> = {
150
+ pending: ['active', 'terminated'],
151
+ active: ['suspended', 'terminated', 'expired'],
152
+ suspended: ['active', 'terminated'],
153
+ terminated: [],
154
+ expired: [],
155
+ }
156
+
157
+ const STATUS_LABELS: Record<string, string> = {
158
+ pending: 'Pending',
159
+ active: 'Active',
160
+ suspended: 'Suspended',
161
+ terminated: 'Terminated',
162
+ expired: 'Expired',
163
+ }
164
+
165
+ function fmt(amount: number | string, currency: string): string {
166
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(Number(amount))
167
+ }
168
+
169
+ function StatusBadge({ status }: { status: string }) {
170
+ return (
171
+ <Tag variant={subscriptionStatusMap[status as SubscriptionStatus] ?? 'neutral'} dot>
172
+ {formatStatusLabel(status)}
173
+ </Tag>
174
+ )
175
+ }
176
+
177
+ export default function SubscriptionDetailPage(props: { params?: { id?: string } }) {
178
+ const router = useRouter()
179
+ const urlParams = useParams()
180
+ const subId = (props.params?.id ?? urlParams?.id) as string
181
+
182
+ const [sub, setSub] = React.useState<SubscriptionDetail | null>(null)
183
+ const [loading, setLoading] = React.useState(true)
184
+ const [error, setError] = React.useState<string | null>(null)
185
+ const [customerName, setCustomerName] = React.useState<string | null>(null)
186
+ const [assets, setAssets] = React.useState<LinkedAsset[]>([])
187
+ const [expandedItems, setExpandedItems] = React.useState<Set<string>>(new Set())
188
+ const [transitioning, setTransitioning] = React.useState(false)
189
+ // Resolved display names for ID references — fetched alongside the main
190
+ // subscription record so the UI never shows "62e90a3d…" placeholders.
191
+ const [sourceQuoteNumber, setSourceQuoteNumber] = React.useState<string | null>(null)
192
+ const [sourceOrderNumber, setSourceOrderNumber] = React.useState<string | null>(null)
193
+ const [productNames, setProductNames] = React.useState<Record<string, string>>({})
194
+ const [offeringNames, setOfferingNames] = React.useState<Record<string, string>>({})
195
+ const [specNames, setSpecNames] = React.useState<Record<string, string>>({})
196
+ // Resolved quote / order numbers per change log row (id → number).
197
+ const [logQuoteNumbers, setLogQuoteNumbers] = React.useState<Record<string, string>>({})
198
+ const [logOrderNumbers, setLogOrderNumbers] = React.useState<Record<string, string>>({})
199
+ // XD-250 ARC state
200
+ const [arcInFlight, setArcInFlight] = React.useState<'amend' | 'renew' | 'cancel' | null>(null)
201
+ const [changeLog, setChangeLog] = React.useState<ChangeLogRow[]>([])
202
+ const [changeLogLoading, setChangeLogLoading] = React.useState(false)
203
+
204
+ const startArcQuote = React.useCallback(
205
+ async (type: 'amend' | 'renew' | 'cancel') => {
206
+ if (!sub) return
207
+ setArcInFlight(type)
208
+ try {
209
+ const res = await fetch('/api/cpq/quotes/from-subscription', {
210
+ method: 'POST',
211
+ headers: { 'content-type': 'application/json' },
212
+ body: JSON.stringify({ subscriptionId: sub.id, type }),
213
+ })
214
+ if (!res.ok) {
215
+ const body = await res.json().catch(() => ({})) as { error?: string; existingQuoteId?: string }
216
+ // XD-250 spec § UI: when the subscription already has a non-terminal
217
+ // ARC quote attached, the server returns 409 with `existingQuoteId`.
218
+ // Offer to navigate to that quote instead of showing a dead alert.
219
+ if (body.existingQuoteId) {
220
+ const ok = window.confirm(
221
+ `${body.error ?? 'Subscription has another ARC quote in progress.'}\n\n` +
222
+ `Open the existing quote now?`,
223
+ )
224
+ if (ok) {
225
+ router.push(`/backend/cpq/quotes/${body.existingQuoteId}`)
226
+ }
227
+ return
228
+ }
229
+ alert(`Failed to start ${type} quote: ${body.error ?? res.statusText}`)
230
+ return
231
+ }
232
+ const data = await res.json()
233
+ router.push(`/backend/cpq/quotes/${data.quoteId}`)
234
+ } finally {
235
+ setArcInFlight(null)
236
+ }
237
+ },
238
+ [router, sub],
239
+ )
240
+
241
+ const transitionStatus = async (targetStatus: string) => {
242
+ if (!sub) return
243
+ try {
244
+ setTransitioning(true)
245
+ setError(null)
246
+ const res = await fetch('/api/cpq/inventory/subscriptions/status', {
247
+ method: 'POST',
248
+ headers: { 'Content-Type': 'application/json' },
249
+ body: JSON.stringify({ id: sub.id, targetStatus }),
250
+ })
251
+ const data = await res.json()
252
+ if (!res.ok) throw new Error(data?.error ?? 'Status transition failed')
253
+ // The status endpoint cascades to items and linked assets server-side,
254
+ // but only returns the subscription scalar fields. Re-fetch the full
255
+ // detail (which includes the item tree) and the asset list so the UI
256
+ // reflects every cascaded change without a hard refresh.
257
+ const [freshSub, freshAssets] = await Promise.all([
258
+ fetch(`/api/cpq/inventory/subscriptions?id=${sub.id}`).then((r) => (r.ok ? r.json() : null)),
259
+ fetch(`/api/cpq/inventory/assets?subscriptionId=${sub.id}&pageSize=50`)
260
+ .then((r) => (r.ok ? r.json() : null))
261
+ .then((r) => (r?.items ?? []) as LinkedAsset[]),
262
+ ])
263
+ if (freshSub) {
264
+ setSub(freshSub)
265
+ } else {
266
+ setSub((prev) => prev ? { ...prev, ...data } : prev)
267
+ }
268
+ setAssets(freshAssets)
269
+ } catch (err) {
270
+ setError((err as Error).message)
271
+ } finally {
272
+ setTransitioning(false)
273
+ }
274
+ }
275
+
276
+ React.useEffect(() => {
277
+ let cancelled = false
278
+ async function loadLog() {
279
+ setChangeLogLoading(true)
280
+ try {
281
+ const r = await fetch(`/api/cpq/inventory/subscriptions/${subId}/change-log?pageSize=50`)
282
+ if (r.ok && !cancelled) {
283
+ const data = await r.json()
284
+ const rows: ChangeLogRow[] = data.items ?? []
285
+ setChangeLog(rows)
286
+ // Resolve the quote / order numbers referenced by each row so
287
+ // the history list shows "Quote QUOTE-…-00005" instead of
288
+ // "Quote 270b8e08…".
289
+ const qIds = new Set(rows.map((r2) => r2.sourceQuoteId).filter((x): x is string => !!x))
290
+ const oIds = new Set(rows.map((r2) => r2.sourceOrderId).filter((x): x is string => !!x))
291
+ for (const qId of qIds) {
292
+ fetch(`/api/cpq/quotes/${qId}`)
293
+ .then((r2) => (r2.ok ? r2.json() : null))
294
+ .then((q) => {
295
+ if (q?.quoteNumber && !cancelled) {
296
+ setLogQuoteNumbers((prev) => ({ ...prev, [qId]: q.quoteNumber }))
297
+ }
298
+ })
299
+ .catch(() => {})
300
+ }
301
+ for (const oId of oIds) {
302
+ fetch(`/api/cpq/orders/${oId}`)
303
+ .then((r2) => (r2.ok ? r2.json() : null))
304
+ .then((o) => {
305
+ if (o?.orderNumber && !cancelled) {
306
+ setLogOrderNumbers((prev) => ({ ...prev, [oId]: o.orderNumber }))
307
+ }
308
+ })
309
+ .catch(() => {})
310
+ }
311
+ }
312
+ } finally {
313
+ if (!cancelled) setChangeLogLoading(false)
314
+ }
315
+ }
316
+ if (subId) void loadLog()
317
+ return () => { cancelled = true }
318
+ }, [subId])
319
+
320
+ React.useEffect(() => {
321
+ let cancelled = false
322
+ async function load() {
323
+ try {
324
+ const res = await fetch(`/api/cpq/inventory/subscriptions?id=${subId}`)
325
+ if (!res.ok) throw new Error('Subscription not found')
326
+ const data: SubscriptionDetail = await res.json()
327
+ if (!cancelled) {
328
+ setSub(data)
329
+ fetch(`/api/customers/companies?id=${data.customerId}&pageSize=1`)
330
+ .then((r) => r.json())
331
+ .then((result) => {
332
+ const items = result.items ?? result ?? []
333
+ const c = items[0]
334
+ if (c && !cancelled) setCustomerName(c.display_name || c.displayName || c.name || null)
335
+ })
336
+ .catch(() => {})
337
+ fetch(`/api/cpq/inventory/assets?subscriptionId=${subId}&pageSize=50`)
338
+ .then((r) => r.json())
339
+ .then((result) => {
340
+ if (!cancelled) setAssets(result.items ?? [])
341
+ })
342
+ .catch(() => {})
343
+
344
+ // Resolve display names for the ID references shown in the
345
+ // header and on every item row. Each lookup is fire-and-forget;
346
+ // the UI falls back to a short UUID until the name lands.
347
+ if (data.sourceQuoteId) {
348
+ fetch(`/api/cpq/quotes/${data.sourceQuoteId}`)
349
+ .then((r) => (r.ok ? r.json() : null))
350
+ .then((q) => {
351
+ if (q && !cancelled) setSourceQuoteNumber(q.quoteNumber ?? null)
352
+ })
353
+ .catch(() => {})
354
+ }
355
+ if (data.sourceOrderId) {
356
+ fetch(`/api/cpq/orders/${data.sourceOrderId}`)
357
+ .then((r) => (r.ok ? r.json() : null))
358
+ .then((o) => {
359
+ if (o && !cancelled) setSourceOrderNumber(o.orderNumber ?? null)
360
+ })
361
+ .catch(() => {})
362
+ }
363
+
364
+ // For items, dedupe IDs across the tree and resolve each once.
365
+ const flatChildren = (nodes: SubscriptionItem[]): SubscriptionItem[] =>
366
+ nodes.flatMap((n) => [n, ...flatChildren(n.children ?? [])])
367
+ const allItems = flatChildren(data.items ?? [])
368
+ const productIds = new Set(allItems.map((i) => i.productId).filter((x): x is string => !!x))
369
+ const offeringIds = new Set(allItems.map((i) => i.offeringId).filter((x): x is string => !!x))
370
+ const specIds = new Set(allItems.map((i) => i.specId).filter((x): x is string => !!x))
371
+
372
+ for (const offeringId of offeringIds) {
373
+ fetch(`/api/cpq/product-offerings?id=${offeringId}`)
374
+ .then((r) => (r.ok ? r.json() : null))
375
+ .then((o) => {
376
+ if (!o || cancelled) return
377
+ if (o.name) setOfferingNames((prev) => ({ ...prev, [offeringId]: o.name }))
378
+ if (o.specification?.name) setSpecNames((prev) => ({ ...prev, [o.specification.id]: o.specification.name }))
379
+ })
380
+ .catch(() => {})
381
+ }
382
+ for (const specId of specIds) {
383
+ fetch(`/api/cpq/product-specifications?id=${specId}`)
384
+ .then((r) => (r.ok ? r.json() : null))
385
+ .then((s) => {
386
+ if (s?.name && !cancelled) setSpecNames((prev) => ({ ...prev, [specId]: s.name }))
387
+ })
388
+ .catch(() => {})
389
+ }
390
+ for (const productId of productIds) {
391
+ // Catalog products route is paginated — `?id=…` filters to one
392
+ // row but the response is still `{ items: […] }`.
393
+ fetch(`/api/catalog/products?id=${productId}`)
394
+ .then((r) => (r.ok ? r.json() : null))
395
+ .then((res) => {
396
+ if (!res || cancelled) return
397
+ const p = (res.items ?? res)[0] ?? res
398
+ const name = p?.title ?? p?.name ?? p?.handle ?? null
399
+ if (name) setProductNames((prev) => ({ ...prev, [productId]: name }))
400
+ })
401
+ .catch(() => {})
402
+ }
403
+ }
404
+ } catch (err) {
405
+ if (!cancelled) setError((err as Error).message)
406
+ } finally {
407
+ if (!cancelled) setLoading(false)
408
+ }
409
+ }
410
+ load()
411
+ return () => { cancelled = true }
412
+ }, [subId])
413
+
414
+ const toggleItem = (id: string) => {
415
+ setExpandedItems((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n })
416
+ }
417
+
418
+ if (loading) {
419
+ return (
420
+ <div className="flex items-center gap-2 py-12 justify-center text-sm text-muted-foreground">
421
+ <Spinner /> Loading…
422
+ </div>
423
+ )
424
+ }
425
+
426
+ if (!sub) {
427
+ return (
428
+ <div className="space-y-4">
429
+ <button onClick={() => router.push('/backend/cpq/inventory')} className="text-sm text-muted-foreground hover:text-foreground">← Back</button>
430
+ {error && <ErrorBanner message={error} />}
431
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">Subscription not found.</div>
432
+ </div>
433
+ )
434
+ }
435
+
436
+ const currency = sub.currencyCode || 'USD'
437
+ const flatItems = flattenItems(sub.items)
438
+
439
+ const canArc = sub.status === 'active' || sub.status === 'suspended'
440
+ const statusTransitions = ALLOWED_TRANSITIONS[sub.status] ?? []
441
+
442
+ return (
443
+ <div className="space-y-6">
444
+ {/* Header — standard FormHeader detail mode, mirrors the CPQ quote /
445
+ order layout. Title is the human subscription name; status /
446
+ cycle tags sit under it; ARC actions (Amend / Renew / Cancel)
447
+ + a "Change status" dropdown live on the right. The short
448
+ subscription code lives in the Subscription Details card
449
+ below — it's reference info, not header content. */}
450
+ <FormHeader
451
+ mode="detail"
452
+ backHref="/backend/cpq/inventory"
453
+ backLabel="Back to Customer Inventory"
454
+ entityTypeLabel="Subscription"
455
+ title={sub.name}
456
+ statusBadge={
457
+ <div className="flex flex-wrap items-center gap-2 pt-1">
458
+ <Tag variant={subscriptionStatusMap[sub.status as SubscriptionStatus] ?? 'neutral'} dot>
459
+ {STATUS_LABELS[sub.status] ?? formatStatusLabel(sub.status)}
460
+ </Tag>
461
+ <Tag variant="neutral" className="capitalize">{sub.billingCycle}</Tag>
462
+ {transitioning && <Spinner />}
463
+ </div>
464
+ }
465
+ actionsContent={
466
+ canArc ? (
467
+ <>
468
+ <Button
469
+ type="button"
470
+ variant="outline"
471
+ disabled={arcInFlight !== null}
472
+ onClick={() => startArcQuote('amend')}
473
+ >
474
+ Amend
475
+ </Button>
476
+ <Button
477
+ type="button"
478
+ variant="outline"
479
+ disabled={arcInFlight !== null}
480
+ onClick={() => startArcQuote('renew')}
481
+ >
482
+ Renew
483
+ </Button>
484
+ <Button
485
+ type="button"
486
+ variant="destructive-outline"
487
+ disabled={arcInFlight !== null}
488
+ onClick={() => startArcQuote('cancel')}
489
+ >
490
+ Cancel
491
+ </Button>
492
+ </>
493
+ ) : null
494
+ }
495
+ />
496
+
497
+ {/* Status path — visual progress only, like the order page.
498
+ Transitions still happen via the dedicated endpoints. */}
499
+ <div className="flex items-center gap-3 flex-wrap">
500
+ <StatusPath
501
+ current={sub.status}
502
+ path={['pending', 'active']}
503
+ terminals={['suspended', 'terminated', 'expired']}
504
+ statusMap={subscriptionStatusMap}
505
+ labels={STATUS_LABELS}
506
+ onTransition={transitionStatus}
507
+ allowedTransitions={statusTransitions}
508
+ disabled={transitioning}
509
+ ariaLabel="Subscription status path"
510
+ />
511
+ </div>
512
+
513
+ {error && <ErrorBanner message={error} />}
514
+
515
+ {/* Detail Cards */}
516
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
517
+ <div className="rounded-lg border bg-card p-4 space-y-3">
518
+ <h3 className="text-sm font-medium">Subscription Details</h3>
519
+ <div className="grid grid-cols-2 gap-x-4 gap-y-2">
520
+ <Field label="Code" value={sub.code} mono />
521
+ <Field label="Status" value={sub.status} />
522
+ <Field label="Billing Cycle" value={sub.billingCycle} />
523
+ <Field label="Currency" value={currency} />
524
+ <Field label="Auto-Renew" value={sub.autoRenew ? 'Yes' : 'No'} />
525
+ {sub.termMonths != null && <Field label="Term" value={`${sub.termMonths} months`} />}
526
+ {sub.startDate && <Field label="Start Date" value={new Date(sub.startDate).toLocaleDateString()} />}
527
+ {sub.currentTermEnd && <Field label="Term End" value={new Date(sub.currentTermEnd).toLocaleDateString()} />}
528
+ {sub.activatedAt && <Field label="Activated" value={new Date(sub.activatedAt).toLocaleString()} />}
529
+ {sub.suspendedAt && <Field label="Suspended" value={new Date(sub.suspendedAt).toLocaleString()} />}
530
+ {sub.terminatedAt && <Field label="Terminated" value={new Date(sub.terminatedAt).toLocaleString()} />}
531
+ </div>
532
+ {sub.description && (
533
+ <div className="pt-1">
534
+ <p className="text-xs text-muted-foreground">Description</p>
535
+ <p className="text-sm">{sub.description}</p>
536
+ </div>
537
+ )}
538
+ </div>
539
+
540
+ <div className="rounded-lg border bg-card p-4 space-y-3">
541
+ <h3 className="text-sm font-medium">Pricing & References</h3>
542
+ <div className="grid grid-cols-2 gap-4">
543
+ <div>
544
+ <p className="text-xs text-muted-foreground">Monthly Recurring (MRC)</p>
545
+ <p className="text-lg font-semibold font-mono">{fmt(sub.mrcAmount, currency)}</p>
546
+ </div>
547
+ <div>
548
+ <p className="text-xs text-muted-foreground">Non-Recurring (NRC)</p>
549
+ <p className="text-lg font-semibold font-mono">{fmt(sub.nrcAmount, currency)}</p>
550
+ </div>
551
+ </div>
552
+ <div className="grid grid-cols-2 gap-x-4 gap-y-2 pt-1">
553
+ <Field label="Customer" value={customerName ?? sub.customerId.slice(0, 12) + '…'} />
554
+ <Field label="Created" value={new Date(sub.createdAt).toLocaleString()} />
555
+ {sub.sourceQuoteId && (
556
+ <div>
557
+ <p className="text-xs text-muted-foreground">Source Quote</p>
558
+ <button
559
+ onClick={() => router.push(`/backend/cpq/quotes/${sub.sourceQuoteId}`)}
560
+ className="text-sm text-primary hover:underline"
561
+ >
562
+ {sourceQuoteNumber ?? `${sub.sourceQuoteId.slice(0, 8)}…`}
563
+ </button>
564
+ </div>
565
+ )}
566
+ {sub.sourceOrderId && (
567
+ <div>
568
+ <p className="text-xs text-muted-foreground">Source Order</p>
569
+ <button
570
+ onClick={() => router.push(`/backend/cpq/orders/${sub.sourceOrderId}`)}
571
+ className="text-sm text-primary hover:underline"
572
+ >
573
+ {sourceOrderNumber ?? `${sub.sourceOrderId.slice(0, 8)}…`}
574
+ </button>
575
+ </div>
576
+ )}
577
+ </div>
578
+ </div>
579
+ </div>
580
+
581
+ {/* Metadata */}
582
+ {sub.metadata && Object.keys(sub.metadata).length > 0 && (
583
+ <div className="rounded-lg border bg-card p-4">
584
+ <h3 className="text-sm font-medium mb-2">Metadata</h3>
585
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
586
+ {Object.entries(sub.metadata).map(([key, value]) => (
587
+ <div key={key} className="text-xs">
588
+ <span className="text-muted-foreground">{key}: </span>
589
+ <span className="font-medium">{String(value)}</span>
590
+ </div>
591
+ ))}
592
+ </div>
593
+ </div>
594
+ )}
595
+
596
+ {/* Subscription Items */}
597
+ <div>
598
+ <h3 className="text-sm font-medium mb-2 px-1">Subscription Items ({flatItems.length})</h3>
599
+ {flatItems.length === 0 ? (
600
+ <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No items.</div>
601
+ ) : (
602
+ <div className="rounded-lg border bg-card divide-y">
603
+ {flatItems.map((item) => {
604
+ const isExpanded = expandedItems.has(item.id)
605
+ const indent = item._depth ?? 0
606
+ return (
607
+ <div key={item.id}>
608
+ <div
609
+ className="flex items-center gap-3 px-4 py-3 hover:bg-muted/30 transition-colors cursor-pointer"
610
+ style={{ paddingLeft: `${16 + indent * 24}px` }}
611
+ onClick={() => toggleItem(item.id)}
612
+ >
613
+ <button className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground">
614
+ <svg className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor">
615
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
616
+ </svg>
617
+ </button>
618
+ <div className="flex-1 min-w-0">
619
+ <div className="flex items-center gap-2">
620
+ <span className="font-medium text-sm">{item.name}</span>
621
+ {item.quantity > 1 && <span className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">x{item.quantity}</span>}
622
+ <StatusBadge status={item.status} />
623
+ </div>
624
+ </div>
625
+ <div className="shrink-0 text-right text-xs space-y-0.5">
626
+ {Number(item.mrcAmount) > 0 && <div><span className="text-muted-foreground">MRC </span><span className="font-mono font-medium">{fmt(item.mrcAmount, item.currencyCode || currency)}</span></div>}
627
+ {Number(item.nrcAmount) > 0 && <div><span className="text-muted-foreground">NRC </span><span className="font-mono font-medium">{fmt(item.nrcAmount, item.currencyCode || currency)}</span></div>}
628
+ </div>
629
+ </div>
630
+
631
+ {isExpanded && (
632
+ <div className="border-t bg-muted/10 px-12 py-3 space-y-3" style={{ paddingLeft: `${40 + indent * 24}px` }}>
633
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2">
634
+ {item.offeringId && (
635
+ <RefField
636
+ label="Offering"
637
+ name={offeringNames[item.offeringId]}
638
+ fallback={item.offeringId}
639
+ onClick={() => router.push(`/backend/cpq/offerings/${item.offeringId}`)}
640
+ />
641
+ )}
642
+ {item.specId && (
643
+ <RefField
644
+ label="Specification"
645
+ name={specNames[item.specId]}
646
+ fallback={item.specId}
647
+ onClick={() => router.push(`/backend/cpq/specifications/${item.specId}`)}
648
+ />
649
+ )}
650
+ {item.productId && (
651
+ <RefField
652
+ label="Product"
653
+ name={productNames[item.productId]}
654
+ fallback={item.productId}
655
+ onClick={() => router.push(`/backend/catalog/products/${item.productId}`)}
656
+ />
657
+ )}
658
+ <Field label="Currency" value={item.currencyCode || currency} />
659
+ <Field label="Quantity" value={String(item.quantity)} />
660
+ {item.capacityTotal && <Field label="Capacity" value={`${item.capacityUsed ?? '0'} / ${item.capacityTotal} ${item.capacityUnit ?? ''}`} />}
661
+ </div>
662
+
663
+ {item.configuration && Object.keys(item.configuration).length > 0 && (
664
+ <div>
665
+ <p className="text-xs font-medium text-muted-foreground mb-1">Configuration</p>
666
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
667
+ {Object.entries(item.configuration).map(([key, value]) => (
668
+ <div key={key} className="text-xs">
669
+ <span className="text-muted-foreground">{key}: </span>
670
+ <span className="font-medium">{String(value)}</span>
671
+ </div>
672
+ ))}
673
+ </div>
674
+ </div>
675
+ )}
676
+
677
+ {item.charges && item.charges.length > 0 && (
678
+ <div>
679
+ <p className="text-xs font-medium text-muted-foreground mb-1">Charges</p>
680
+ <table className="w-full text-xs">
681
+ <thead>
682
+ <tr className="text-muted-foreground">
683
+ <th className="text-left py-1 font-medium">Charge</th>
684
+ <th className="text-left py-1 font-medium">Type</th>
685
+ <th className="text-right py-1 font-medium">Unit Price</th>
686
+ <th className="text-right py-1 font-medium">Qty</th>
687
+ <th className="text-right py-1 font-medium">Total</th>
688
+ </tr>
689
+ </thead>
690
+ <tbody>
691
+ {item.charges.map((c, i) => (
692
+ <tr key={i} className="border-t border-dashed border-border/50">
693
+ <td className="py-1">{(c as any).chargeName ?? '—'}</td>
694
+ <td className="py-1 uppercase">{(c as any).chargeType ?? '—'}</td>
695
+ <td className="py-1 text-right font-mono">{(c as any).unitPrice != null ? fmt((c as any).unitPrice, currency) : '—'}</td>
696
+ <td className="py-1 text-right font-mono">{(c as any).quantity ?? '—'}</td>
697
+ <td className="py-1 text-right font-mono">{(c as any).totalPrice != null ? fmt((c as any).totalPrice, currency) : '—'}</td>
698
+ </tr>
699
+ ))}
700
+ </tbody>
701
+ </table>
702
+ </div>
703
+ )}
704
+
705
+ {item.metadata && Object.keys(item.metadata).length > 0 && (
706
+ <div>
707
+ <p className="text-xs font-medium text-muted-foreground mb-1">Metadata</p>
708
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1">
709
+ {Object.entries(item.metadata).map(([key, value]) => (
710
+ <div key={key} className="text-xs">
711
+ <span className="text-muted-foreground">{key}: </span>
712
+ <span className="font-medium">{String(value)}</span>
713
+ </div>
714
+ ))}
715
+ </div>
716
+ </div>
717
+ )}
718
+
719
+ <div className="flex gap-4 text-xs text-muted-foreground">
720
+ <span>Created: {new Date(item.createdAt).toLocaleString()}</span>
721
+ <span>Updated: {new Date(item.updatedAt).toLocaleString()}</span>
722
+ </div>
723
+ </div>
724
+ )}
725
+ </div>
726
+ )
727
+ })}
728
+ </div>
729
+ )}
730
+ </div>
731
+
732
+ {/* XD-250 Change History */}
733
+ <div>
734
+ <h3 className="text-sm font-medium mb-2 px-1">Change History ({changeLog.length})</h3>
735
+ {changeLogLoading ? (
736
+ <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">Loading…</div>
737
+ ) : changeLog.length === 0 ? (
738
+ <div className="rounded-lg border bg-card py-6 text-center text-sm text-muted-foreground">No ARC changes yet.</div>
739
+ ) : (
740
+ <div className="rounded-lg border bg-card divide-y">
741
+ {changeLog.map((row) => (
742
+ <div key={row.id} className="px-4 py-3 space-y-1">
743
+ <div className="flex items-center gap-2">
744
+ <Tag variant={CHANGE_TYPE_VARIANTS[row.changeType] ?? 'neutral'}>
745
+ {CHANGE_TYPE_LABELS[row.changeType] ?? row.changeType}
746
+ </Tag>
747
+ <span className="text-xs text-muted-foreground">
748
+ {new Date(row.effectiveAt).toLocaleString()}
749
+ </span>
750
+ {row.sourceQuoteId && (
751
+ <button
752
+ onClick={() => router.push(`/backend/cpq/quotes/${row.sourceQuoteId}`)}
753
+ className="text-xs text-primary hover:underline"
754
+ >
755
+ Quote {logQuoteNumbers[row.sourceQuoteId] ?? `${row.sourceQuoteId.slice(0, 8)}…`}
756
+ </button>
757
+ )}
758
+ {row.sourceOrderId && (
759
+ <button
760
+ onClick={() => router.push(`/backend/cpq/orders/${row.sourceOrderId}`)}
761
+ className="text-xs text-primary hover:underline"
762
+ >
763
+ Order {logOrderNumbers[row.sourceOrderId] ?? `${row.sourceOrderId.slice(0, 8)}…`}
764
+ </button>
765
+ )}
766
+ </div>
767
+ <ChangeLogLineDetails row={row} currency={currency} />
768
+ {row.termChange && (
769
+ <div className="text-xs text-muted-foreground">
770
+ Term: {String(row.termChange.oldTermEnd ?? '—')} → {String(row.termChange.newTermEnd ?? '—')}
771
+ </div>
772
+ )}
773
+ {row.reasonCode && (
774
+ <div className="text-xs text-muted-foreground">
775
+ Reason: {row.reasonCode}{row.reasonText ? ` — ${row.reasonText}` : ''}
776
+ </div>
777
+ )}
778
+ {row.etfAmount && (
779
+ <div className="text-xs text-muted-foreground">
780
+ ETF: {row.etfAmount} {row.etfCurrency ?? ''}
781
+ </div>
782
+ )}
783
+ {row.mergedIntoSubscriptionId && (
784
+ <div className="text-xs text-muted-foreground">
785
+ Merged into{' '}
786
+ <button
787
+ onClick={() =>
788
+ router.push(`/backend/cpq/inventory/subscriptions/${row.mergedIntoSubscriptionId}`)
789
+ }
790
+ className="text-primary hover:underline"
791
+ >
792
+ subscription {row.mergedIntoSubscriptionId.slice(0, 8)}…
793
+ </button>
794
+ </div>
795
+ )}
796
+ {row.mergedFromSubscriptionIds && row.mergedFromSubscriptionIds.length > 0 && (
797
+ <div className="text-xs text-muted-foreground">
798
+ Merged from {row.mergedFromSubscriptionIds.length} subscriptions
799
+ </div>
800
+ )}
801
+ </div>
802
+ ))}
803
+ </div>
804
+ )}
805
+ </div>
806
+
807
+ {/* Linked Assets */}
808
+ <div>
809
+ <h3 className="text-sm font-medium mb-2 px-1">Linked Assets ({assets.length})</h3>
810
+ {assets.length === 0 ? (
811
+ <div className="rounded-lg border bg-card py-8 text-center text-sm text-muted-foreground">No assets linked to this subscription.</div>
812
+ ) : (
813
+ <div className="rounded-lg border">
814
+ <table className="w-full text-sm">
815
+ <thead>
816
+ <tr className="border-b bg-muted/50">
817
+ <th className="px-4 py-3 text-left font-medium">Code</th>
818
+ <th className="px-4 py-3 text-left font-medium">Name</th>
819
+ <th className="px-4 py-3 text-left font-medium">Type</th>
820
+ <th className="px-4 py-3 text-left font-medium">Status</th>
821
+ <th className="px-4 py-3 text-right font-medium">Price</th>
822
+ <th className="px-4 py-3 text-right font-medium">Qty</th>
823
+ </tr>
824
+ </thead>
825
+ <tbody>
826
+ {assets.map((a) => (
827
+ <tr key={a.id} onClick={() => router.push(`/backend/cpq/inventory/assets/${a.id}`)} className="border-b cursor-pointer hover:bg-muted/30 transition-colors">
828
+ <td className="px-4 py-3 text-muted-foreground">{a.code}</td>
829
+ <td className="px-4 py-3 font-medium text-primary">{a.name}</td>
830
+ <td className="px-4 py-3">{a.assetType.replace(/_/g, ' ')}</td>
831
+ <td className="px-4 py-3"><StatusBadge status={a.status} /></td>
832
+ <td className="px-4 py-3 text-right font-mono">{fmt(a.purchasePrice, a.currencyCode || currency)}</td>
833
+ <td className="px-4 py-3 text-right font-mono">{a.quantity}</td>
834
+ </tr>
835
+ ))}
836
+ </tbody>
837
+ </table>
838
+ </div>
839
+ )}
840
+ </div>
841
+ </div>
842
+ )
843
+ }
844
+
845
+ type FlatItem = SubscriptionItem & { _depth: number }
846
+
847
+ function flattenItems(items: SubscriptionItem[], depth = 0): FlatItem[] {
848
+ const result: FlatItem[] = []
849
+ for (const item of items) {
850
+ result.push({ ...item, _depth: depth })
851
+ if (item.children && item.children.length > 0) {
852
+ result.push(...flattenItems(item.children, depth + 1))
853
+ }
854
+ }
855
+ return result
856
+ }
857
+
858
+ function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
859
+ return (
860
+ <div>
861
+ <p className="text-xs text-muted-foreground">{label}</p>
862
+ <p className={`text-sm ${mono ? 'font-mono' : ''}`}>{value}</p>
863
+ </div>
864
+ )
865
+ }
866
+
867
+ // Field for a foreign-key reference: shows the resolved display name as
868
+ // a primary-coloured link, falling back to a short UUID until the name
869
+ // lands. Clicking navigates to the referenced record.
870
+ function RefField({
871
+ label,
872
+ name,
873
+ fallback,
874
+ onClick,
875
+ }: {
876
+ label: string
877
+ name: string | undefined
878
+ fallback: string
879
+ onClick: () => void
880
+ }) {
881
+ return (
882
+ <div>
883
+ <p className="text-xs text-muted-foreground">{label}</p>
884
+ <button
885
+ type="button"
886
+ onClick={onClick}
887
+ className="text-sm text-primary hover:underline text-left truncate max-w-full"
888
+ title={name ?? fallback}
889
+ >
890
+ {name ?? `${fallback.slice(0, 12)}…`}
891
+ </button>
892
+ </div>
893
+ )
894
+ }
895
+
896
+ function Spinner() {
897
+ return (
898
+ <svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
899
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
900
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
901
+ </svg>
902
+ )
903
+ }
904
+
905
+ function ErrorBanner({ message }: { message: string }) {
906
+ return <Alert variant="destructive">{message}</Alert>
907
+ }
908
+
909
+ // XD-250 — render the per-line summary of an ARC change, with inline product
910
+ // names and before/after MRC values. Falls back gracefully when snapshots are
911
+ // missing (older log rows) or when lineChanges is null (cancel / merge).
912
+ function ChangeLogLineDetails({ row, currency }: { row: ChangeLogRow; currency: string }) {
913
+ const beforeItems = new Map<string, SnapshotItem>(
914
+ (row.beforeSnapshot?.items ?? []).map((it) => [it.id, it]),
915
+ )
916
+ const afterItems = new Map<string, SnapshotItem>(
917
+ (row.afterSnapshot?.items ?? []).map((it) => [it.id, it]),
918
+ )
919
+
920
+ type DerivedChange = {
921
+ key: string
922
+ action: 'add' | 'modify' | 'cancel'
923
+ name: string
924
+ beforeMrc: number | null
925
+ afterMrc: number | null
926
+ delta: number | null
927
+ }
928
+
929
+ const derived: DerivedChange[] = []
930
+
931
+ if (row.lineChanges && row.lineChanges.length > 0) {
932
+ for (let i = 0; i < row.lineChanges.length; i++) {
933
+ const lc = row.lineChanges[i]
934
+ const itemId = lc.itemId ?? ''
935
+ const before = beforeItems.get(itemId)
936
+ const after = afterItems.get(itemId)
937
+ const item = after ?? before
938
+ const beforeMrc = before ? Number(before.mrcAmount ?? 0) : null
939
+ const afterMrc = after ? Number(after.mrcAmount ?? 0) : null
940
+ const explicitDelta = lc.mrcDelta != null ? Number(lc.mrcDelta) : null
941
+ const computedDelta =
942
+ beforeMrc != null && afterMrc != null ? afterMrc - beforeMrc : null
943
+ derived.push({
944
+ key: `${itemId}-${i}`,
945
+ action: lc.action,
946
+ name: item?.name ?? (lc.action === 'add' ? 'New item' : 'Item'),
947
+ beforeMrc,
948
+ afterMrc:
949
+ lc.action === 'add' && afterMrc == null && lc.mrcAmount != null
950
+ ? Number(lc.mrcAmount)
951
+ : afterMrc,
952
+ delta: explicitDelta ?? computedDelta,
953
+ })
954
+ }
955
+ } else if (row.changeType === 'cancel' && row.beforeSnapshot?.items) {
956
+ // applyCancel writes lineChanges=null; surface the items that got terminated.
957
+ for (const it of row.beforeSnapshot.items) {
958
+ const mrc = Number(it.mrcAmount ?? 0)
959
+ derived.push({
960
+ key: it.id,
961
+ action: 'cancel',
962
+ name: it.name ?? 'Item',
963
+ beforeMrc: mrc,
964
+ afterMrc: 0,
965
+ delta: -mrc,
966
+ })
967
+ }
968
+ } else if (row.changeType === 'merge-result' && row.afterSnapshot?.items) {
969
+ for (const it of row.afterSnapshot.items) {
970
+ const mrc = Number(it.mrcAmount ?? 0)
971
+ derived.push({
972
+ key: it.id,
973
+ action: 'add',
974
+ name: it.name ?? 'Item',
975
+ beforeMrc: null,
976
+ afterMrc: mrc,
977
+ delta: null,
978
+ })
979
+ }
980
+ } else if (row.changeType === 'merge-source' && row.beforeSnapshot?.items) {
981
+ for (const it of row.beforeSnapshot.items) {
982
+ const mrc = Number(it.mrcAmount ?? 0)
983
+ derived.push({
984
+ key: it.id,
985
+ action: 'cancel',
986
+ name: it.name ?? 'Item',
987
+ beforeMrc: mrc,
988
+ afterMrc: 0,
989
+ delta: -mrc,
990
+ })
991
+ }
992
+ }
993
+
994
+ if (derived.length === 0) {
995
+ // Amend / renew without any actual line edits — service writes lineChanges=[]
996
+ // because line 1641 of cpqInventoryService.applyLineChanges only records modify
997
+ // entries with a real delta. Show an explicit note so the operator knows
998
+ // the activation happened but nothing on items moved.
999
+ if (row.changeType === 'amend' || row.changeType === 'renew') {
1000
+ return (
1001
+ <div className="mt-1.5 text-xs text-muted-foreground italic">
1002
+ No item-level changes recorded.
1003
+ </div>
1004
+ )
1005
+ }
1006
+ return null
1007
+ }
1008
+
1009
+ return (
1010
+ <div className="mt-1.5 rounded border bg-muted/20 px-3 py-2 space-y-1">
1011
+ {derived.map((d) => (
1012
+ <ChangeLogLineRow key={d.key} change={d} currency={currency} />
1013
+ ))}
1014
+ </div>
1015
+ )
1016
+ }
1017
+
1018
+ const ACTION_GLYPH: Record<'add' | 'modify' | 'cancel', { sign: string; color: string; label: string }> = {
1019
+ add: { sign: '+', color: 'text-status-success-text', label: 'added' },
1020
+ modify: { sign: '~', color: 'text-status-info-text', label: 'modified' },
1021
+ cancel: { sign: '−', color: 'text-destructive', label: 'removed' },
1022
+ }
1023
+
1024
+ function ChangeLogLineRow({
1025
+ change,
1026
+ currency,
1027
+ }: {
1028
+ change: {
1029
+ action: 'add' | 'modify' | 'cancel'
1030
+ name: string
1031
+ beforeMrc: number | null
1032
+ afterMrc: number | null
1033
+ delta: number | null
1034
+ }
1035
+ currency: string
1036
+ }) {
1037
+ const glyph = ACTION_GLYPH[change.action]
1038
+ const showPriceChange =
1039
+ change.action === 'modify' &&
1040
+ change.beforeMrc != null &&
1041
+ change.afterMrc != null &&
1042
+ change.beforeMrc !== change.afterMrc
1043
+ const showSinglePrice =
1044
+ !showPriceChange &&
1045
+ ((change.action === 'add' && change.afterMrc != null && change.afterMrc > 0) ||
1046
+ (change.action === 'cancel' && change.beforeMrc != null && change.beforeMrc > 0))
1047
+ const showDelta = change.delta != null && change.delta !== 0
1048
+ const isNoopModify =
1049
+ change.action === 'modify' && !showPriceChange && !showDelta
1050
+
1051
+ return (
1052
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
1053
+ <span className={`font-bold w-3 text-center ${glyph.color}`} title={glyph.label}>
1054
+ {glyph.sign}
1055
+ </span>
1056
+ <span className="font-medium text-foreground">{change.name}</span>
1057
+ {showPriceChange && (
1058
+ <span className="font-mono text-muted-foreground">
1059
+ {fmt(change.beforeMrc!, currency)} → {fmt(change.afterMrc!, currency)}/mo
1060
+ </span>
1061
+ )}
1062
+ {showSinglePrice && change.action === 'add' && (
1063
+ <span className="font-mono text-muted-foreground">
1064
+ {fmt(change.afterMrc!, currency)}/mo
1065
+ </span>
1066
+ )}
1067
+ {showSinglePrice && change.action === 'cancel' && (
1068
+ <span className="font-mono text-muted-foreground">
1069
+ was {fmt(change.beforeMrc!, currency)}/mo
1070
+ </span>
1071
+ )}
1072
+ {showDelta && (
1073
+ <span
1074
+ className={`font-mono font-medium ${change.delta! > 0 ? 'text-status-success-text' : 'text-destructive'}`}
1075
+ >
1076
+ ({change.delta! > 0 ? '+' : ''}
1077
+ {fmt(change.delta!, currency)}/mo)
1078
+ </span>
1079
+ )}
1080
+ {isNoopModify && (
1081
+ <span className="text-muted-foreground italic">no price change</span>
1082
+ )}
1083
+ </div>
1084
+ )
1085
+ }