@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,1165 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
4
+ import { useParams, useRouter } from 'next/navigation'
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 { NumberInput } from '../../../../components/NumberInput'
9
+ import {
10
+ chargeTypeMap,
11
+ formatStatusLabel,
12
+ lifecycleStatusMap,
13
+ type ChargeType,
14
+ type LifecycleStatus,
15
+ } from '../../../../components/statusMaps'
16
+
17
+ // ─── Types ──────────────────────────────────────────────────────
18
+
19
+ type OfferingComponent = {
20
+ id: string
21
+ slotId: string
22
+ slot: { id: string; componentGroup: string; name: string; cardinalityMin: number; cardinalityMax: number | null } | null
23
+ childOfferingId: string
24
+ childOffering: { id: string; code: string; name: string; offeringType: string; specId: string } | null
25
+ isDefault: boolean
26
+ sortOrder: number
27
+ isActive: boolean
28
+ }
29
+
30
+ type BundleSlot = {
31
+ id: string
32
+ componentGroup: string
33
+ name: string
34
+ targetSpecId: string
35
+ targetSpec: { id: string; code: string; name: string; specType: string } | null
36
+ cardinalityMin: number
37
+ cardinalityMax: number | null
38
+ sortOrder: number
39
+ }
40
+
41
+ type Offering = {
42
+ id: string
43
+ specId: string
44
+ catalogOfferId: string | null
45
+ code: string
46
+ name: string
47
+ description: string | null
48
+ offeringType: string
49
+ designTimeValues: Record<string, unknown>
50
+ lifecycleStatus: string
51
+ effectiveFrom: string | null
52
+ effectiveTo: string | null
53
+ metadata: Record<string, unknown> | null
54
+ isActive: boolean
55
+ specification: { id: string; code: string; name: string; productId: string; specType?: string } | null
56
+ charges: Charge[]
57
+ components?: OfferingComponent[]
58
+ }
59
+
60
+ type Charge = {
61
+ id: string
62
+ code: string
63
+ name: string
64
+ description: string | null
65
+ chargeType: string
66
+ pricingMethod: string
67
+ pricingTableId: string | null
68
+ priceColumnKey: string | null
69
+ fixedPrice: string | null
70
+ currencyCode: string | null
71
+ quantityAttributeCode: string | null
72
+ applicabilityCondition: Record<string, unknown> | null
73
+ sortOrder: number
74
+ isActive: boolean
75
+ }
76
+
77
+ type Attribute = {
78
+ id: string
79
+ code: string
80
+ name: string
81
+ attributeType: string
82
+ resolutionTime: string
83
+ options: Array<{ value: string; label: string }> | null
84
+ constraints: Record<string, unknown> | null
85
+ defaultValue: unknown | null
86
+ helpText: string | null
87
+ sortOrder: number
88
+ isRequired: boolean
89
+ isActive: boolean
90
+ }
91
+
92
+ type Specification = {
93
+ id: string
94
+ code: string
95
+ name: string
96
+ productId: string
97
+ }
98
+
99
+ type PricingTableRef = { id: string; code: string; name: string; dimensions: Array<{ key: string; label: string }>; priceColumns: Array<{ key: string; label: string }> }
100
+
101
+ type EditingCharge = Omit<Charge, 'id'> & { id?: string }
102
+
103
+ // ─── Defaults ───────────────────────────────────────────────────
104
+
105
+ const EMPTY_FORM = {
106
+ code: '',
107
+ name: '',
108
+ description: '',
109
+ offeringType: 'simple' as string,
110
+ lifecycleStatus: 'draft',
111
+ effectiveFrom: null as string | null,
112
+ effectiveTo: null as string | null,
113
+ isActive: true,
114
+ specId: '',
115
+ }
116
+
117
+ const EMPTY_CHARGE: EditingCharge = {
118
+ code: '',
119
+ name: '',
120
+ description: null,
121
+ chargeType: 'mrc',
122
+ pricingMethod: 'flat',
123
+ pricingTableId: null,
124
+ priceColumnKey: null,
125
+ fixedPrice: null,
126
+ currencyCode: 'USD',
127
+ quantityAttributeCode: null,
128
+ applicabilityCondition: null,
129
+ sortOrder: 0,
130
+ isActive: true,
131
+ }
132
+
133
+
134
+ const PRICING_METHOD_LABELS: Record<string, string> = {
135
+ flat: 'flat price',
136
+ fixed: 'flat price', // legacy alias — see validators.ts
137
+ per_unit: 'per unit',
138
+ tiered: 'tiered',
139
+ }
140
+
141
+ // V-CHG-1 in `validators.ts`: per_unit/tiered need table + column + qty
142
+ // attribute; flat needs fixedPrice + currencyCode. Mirror it in the UI so
143
+ // users see what's missing without round-tripping to the server.
144
+ type ChargeShape = {
145
+ pricingMethod: string
146
+ pricingTableId: string | null
147
+ priceColumnKey: string | null
148
+ fixedPrice: string | null
149
+ currencyCode: string | null
150
+ quantityAttributeCode: string | null
151
+ }
152
+
153
+ function isChargeComplete(charge: ChargeShape): boolean {
154
+ return chargeMissingFields(charge).length === 0
155
+ }
156
+
157
+ function chargeMissingFields(charge: ChargeShape): string[] {
158
+ const method = charge.pricingMethod === 'fixed' ? 'flat' : charge.pricingMethod
159
+ const missing: string[] = []
160
+ if (method === 'flat') {
161
+ if (!charge.fixedPrice) missing.push('Fixed Price')
162
+ if (!charge.currencyCode) missing.push('Currency')
163
+ } else if (method === 'per_unit' || method === 'tiered') {
164
+ if (!charge.pricingTableId) missing.push('Pricing Table')
165
+ if (!charge.priceColumnKey) missing.push('Price Column')
166
+ if (!charge.quantityAttributeCode) missing.push('Quantity Attribute')
167
+ }
168
+ return missing
169
+ }
170
+
171
+ // ─── Component ──────────────────────────────────────────────────
172
+
173
+ export default function OfferingDetailPage(props: { params?: { id?: string } }) {
174
+ const t = useT()
175
+ const router = useRouter()
176
+ const urlParams = useParams()
177
+ const offeringId = (props.params?.id ?? urlParams?.id) as string
178
+ const isNew = offeringId === 'new'
179
+
180
+ // ─── State ──────────────────────────────────────────────────
181
+
182
+ const [tab, setTab] = React.useState<'general' | 'charges' | 'components'>('general')
183
+ const [form, setForm] = React.useState(EMPTY_FORM)
184
+ const [designTimeValues, setDesignTimeValues] = React.useState<Record<string, unknown>>({})
185
+ const [charges, setCharges] = React.useState<Charge[]>([])
186
+ const [attributes, setAttributes] = React.useState<Attribute[]>([])
187
+ const [specs, setSpecs] = React.useState<Specification[]>([])
188
+ const [specDetail, setSpecDetail] = React.useState<{ id: string; code: string; name: string; specType?: string } | null>(null)
189
+ const [pricingTables, setPricingTables] = React.useState<PricingTableRef[]>([])
190
+ const [components, setComponents] = React.useState<OfferingComponent[]>([])
191
+ const [bundleSlots, setBundleSlots] = React.useState<BundleSlot[]>([])
192
+ const [addingComponent, setAddingComponent] = React.useState<{ slotId: string; childOfferingId: string; isDefault: boolean } | null>(null)
193
+ const [slotOfferings, setSlotOfferings] = React.useState<Record<string, Array<{ id: string; code: string; name: string; offeringType: string }>>>({})
194
+
195
+ const [loading, setLoading] = React.useState(!isNew)
196
+ const [saving, setSaving] = React.useState(false)
197
+ const [error, setError] = React.useState<string | null>(null)
198
+
199
+ const [editingCharge, setEditingCharge] = React.useState<EditingCharge | null>(null)
200
+
201
+ // OM-standard detail page: existing offerings open read-only and require
202
+ // an explicit Edit click to allow changes. New offerings skip view mode.
203
+ const [generalMode, setGeneralMode] = React.useState<'view' | 'edit'>(isNew ? 'edit' : 'view')
204
+ const [formSnapshot, setFormSnapshot] = React.useState<typeof EMPTY_FORM | null>(null)
205
+ const [designTimeSnapshot, setDesignTimeSnapshot] = React.useState<Record<string, unknown> | null>(null)
206
+
207
+ // ─── Load data ──────────────────────────────────────────────
208
+
209
+ React.useEffect(() => {
210
+ let cancelled = false
211
+
212
+ async function load() {
213
+ try {
214
+ // Load specs for dropdown (needed for new + showing spec name)
215
+ const specsRes = await fetch('/api/cpq/product-specifications?pageSize=200')
216
+ if (specsRes.ok) {
217
+ const d = await specsRes.json()
218
+ if (!cancelled) setSpecs(d.items ?? [])
219
+ }
220
+
221
+ if (isNew) return
222
+
223
+ // Load offering detail
224
+ const res = await fetch(`/api/cpq/product-offerings?id=${encodeURIComponent(offeringId)}`)
225
+ if (!res.ok) {
226
+ if (!cancelled) setError('Failed to load offering')
227
+ return
228
+ }
229
+
230
+ const data: Offering = await res.json()
231
+ if (!cancelled) {
232
+ setForm({
233
+ code: data.code,
234
+ name: data.name,
235
+ description: data.description ?? '',
236
+ offeringType: data.offeringType ?? 'simple',
237
+ lifecycleStatus: data.lifecycleStatus,
238
+ effectiveFrom: data.effectiveFrom,
239
+ effectiveTo: data.effectiveTo,
240
+ isActive: data.isActive,
241
+ specId: data.specId,
242
+ })
243
+ setDesignTimeValues(data.designTimeValues ?? {})
244
+ setCharges(data.charges ?? [])
245
+ setSpecDetail(data.specification ?? null)
246
+ setComponents(data.components ?? [])
247
+
248
+ // Load attributes for the spec
249
+ if (data.specId) {
250
+ const attrRes = await fetch(`/api/cpq/product-attributes?specId=${encodeURIComponent(data.specId)}&pageSize=200`)
251
+ if (attrRes.ok) {
252
+ const attrData = await attrRes.json()
253
+ if (!cancelled) setAttributes(attrData.items ?? [])
254
+ }
255
+ }
256
+
257
+ // Load bundle slots if this is a bundle offering
258
+ if (data.offeringType === 'bundle' && data.specId) {
259
+ const slotsRes = await fetch(`/api/cpq/bundle-slots?specId=${encodeURIComponent(data.specId)}`)
260
+ if (slotsRes.ok) {
261
+ const slotsData = await slotsRes.json()
262
+ if (!cancelled) setBundleSlots(slotsData.items ?? [])
263
+ }
264
+ }
265
+ }
266
+ } catch (err) {
267
+ console.error('Failed to load', err)
268
+ if (!cancelled) setError('Failed to load')
269
+ } finally {
270
+ if (!cancelled) setLoading(false)
271
+ }
272
+ }
273
+
274
+ load()
275
+ return () => { cancelled = true }
276
+ }, [offeringId, isNew])
277
+
278
+ // Load attributes when spec changes (for new offering)
279
+ const loadSpecAttributes = async (specId: string) => {
280
+ if (!specId) { setAttributes([]); return }
281
+ try {
282
+ const res = await fetch(`/api/cpq/product-attributes?specId=${encodeURIComponent(specId)}&pageSize=200`)
283
+ if (res.ok) {
284
+ const data = await res.json()
285
+ setAttributes(data.items ?? [])
286
+ }
287
+ } catch {
288
+ console.error('Failed to load spec attributes')
289
+ }
290
+ }
291
+
292
+ // ─── Save offering ─────────────────────────────────────────
293
+
294
+ const saveOffering = async () => {
295
+ setSaving(true)
296
+ setError(null)
297
+ try {
298
+ const payload = isNew
299
+ ? { ...form, description: form.description || null, designTimeValues, specId: form.specId }
300
+ : { id: offeringId, ...form, description: form.description || null, designTimeValues }
301
+
302
+ const res = await fetch('/api/cpq/product-offerings', {
303
+ method: isNew ? 'POST' : 'PUT',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify(payload),
306
+ })
307
+
308
+ if (!res.ok) {
309
+ const d = await res.json()
310
+ setError(d.error ?? 'Failed to save')
311
+ return
312
+ }
313
+
314
+ const saved = await res.json()
315
+ if (isNew) {
316
+ router.push(`/backend/cpq/offerings/${saved.id}`)
317
+ } else {
318
+ setForm({
319
+ code: saved.code,
320
+ name: saved.name,
321
+ description: saved.description ?? '',
322
+ offeringType: saved.offeringType ?? form.offeringType,
323
+ lifecycleStatus: saved.lifecycleStatus,
324
+ effectiveFrom: saved.effectiveFrom,
325
+ effectiveTo: saved.effectiveTo,
326
+ isActive: saved.isActive,
327
+ specId: saved.specId,
328
+ })
329
+ setDesignTimeValues(saved.designTimeValues ?? {})
330
+ // Save succeeded — drop snapshots and flip back to read-only.
331
+ setFormSnapshot(null)
332
+ setDesignTimeSnapshot(null)
333
+ setGeneralMode('view')
334
+ }
335
+ } catch {
336
+ setError('Failed to save')
337
+ } finally {
338
+ setSaving(false)
339
+ }
340
+ }
341
+
342
+ const deleteOffering = async () => {
343
+ if (!confirm(t('cpq.offerings.confirmDelete', 'Delete this offering and all its charges?'))) return
344
+ try {
345
+ const res = await fetch('/api/cpq/product-offerings', {
346
+ method: 'DELETE',
347
+ headers: { 'Content-Type': 'application/json' },
348
+ body: JSON.stringify({ id: offeringId }),
349
+ })
350
+ if (res.ok) {
351
+ router.push('/backend/cpq/offerings')
352
+ } else {
353
+ const d = await res.json()
354
+ setError(d.error ?? 'Failed to delete')
355
+ }
356
+ } catch {
357
+ setError('Failed to delete')
358
+ }
359
+ }
360
+
361
+ // ─── Charge CRUD ──────────────────────────────────────────
362
+
363
+ const loadPricingTables = async () => {
364
+ if (pricingTables.length > 0) return
365
+ const res = await fetch('/api/cpq/pricing-tables?pageSize=200')
366
+ if (res.ok) {
367
+ const data = await res.json()
368
+ setPricingTables(data.items ?? [])
369
+ }
370
+ }
371
+
372
+ const startEditCharge = (charge?: Charge) => {
373
+ const editing = charge ? { ...charge } : { ...EMPTY_CHARGE }
374
+ setEditingCharge(editing)
375
+ loadPricingTables()
376
+ }
377
+
378
+ const saveCharge = async () => {
379
+ if (!editingCharge) return
380
+ setSaving(true)
381
+ setError(null)
382
+ try {
383
+ const isEdit = !!editingCharge.id
384
+ const payload = {
385
+ ...editingCharge,
386
+ offeringId,
387
+ }
388
+ const res = await fetch('/api/cpq/product-charges', {
389
+ method: isEdit ? 'PUT' : 'POST',
390
+ headers: { 'Content-Type': 'application/json' },
391
+ body: JSON.stringify(payload),
392
+ })
393
+ if (!res.ok) {
394
+ const d = await res.json()
395
+ setError(d.error ?? 'Failed to save charge')
396
+ return
397
+ }
398
+ // Reload offering to get updated charges
399
+ const offeringRes = await fetch(`/api/cpq/product-offerings?id=${encodeURIComponent(offeringId)}`)
400
+ if (offeringRes.ok) {
401
+ const data = await offeringRes.json()
402
+ setCharges(data.charges ?? [])
403
+ }
404
+ setEditingCharge(null)
405
+ } catch {
406
+ setError('Failed to save charge')
407
+ } finally {
408
+ setSaving(false)
409
+ }
410
+ }
411
+
412
+ const deleteCharge = async (chargeId: string) => {
413
+ if (!confirm('Delete this charge?')) return
414
+ try {
415
+ const res = await fetch('/api/cpq/product-charges', {
416
+ method: 'DELETE',
417
+ headers: { 'Content-Type': 'application/json' },
418
+ body: JSON.stringify({ id: chargeId }),
419
+ })
420
+ if (res.ok) setCharges((prev) => prev.filter((c) => c.id !== chargeId))
421
+ } catch {
422
+ setError('Failed to delete charge')
423
+ }
424
+ }
425
+
426
+ const selectedPricingTable = editingCharge?.pricingTableId
427
+ ? pricingTables.find((pt) => pt.id === editingCharge.pricingTableId)
428
+ : null
429
+
430
+ // ─── Helpers ────────────────────────────────────────────────
431
+
432
+ const designTimeAttrs = attributes.filter((a) => a.resolutionTime === 'design_time')
433
+
434
+ // ─── Render ─────────────────────────────────────────────────
435
+
436
+ if (loading) {
437
+ return <div className="text-sm text-muted-foreground">{t('common.loading', 'Loading...')}</div>
438
+ }
439
+
440
+ return (
441
+ <div className="space-y-6">
442
+ {/* Header */}
443
+ <div className="flex items-center justify-between">
444
+ <div className="flex items-center gap-3">
445
+ <button
446
+ onClick={() => router.push('/backend/cpq/offerings')}
447
+ className="text-sm text-muted-foreground hover:text-foreground"
448
+ >
449
+ ← {t('common.back', 'Back')}
450
+ </button>
451
+ <h1 className="text-2xl font-bold">
452
+ {isNew ? t('cpq.offerings.new', 'New Offering') : form.name}
453
+ </h1>
454
+ {!isNew && (
455
+ <Tag variant={lifecycleStatusMap[form.lifecycleStatus as LifecycleStatus] ?? 'neutral'} dot>
456
+ {formatStatusLabel(form.lifecycleStatus)}
457
+ </Tag>
458
+ )}
459
+ {specDetail && (
460
+ <span className="text-sm text-muted-foreground">
461
+ Spec: <button onClick={() => router.push(`/backend/cpq/specifications/${specDetail.id}`)} className="text-primary hover:underline">{specDetail.name}</button>
462
+ </span>
463
+ )}
464
+ </div>
465
+ {!isNew && (
466
+ <div className="flex items-center gap-2">
467
+ {generalMode === 'view' && tab === 'general' && (
468
+ <Button
469
+ type="button"
470
+ onClick={() => {
471
+ setFormSnapshot(form)
472
+ setDesignTimeSnapshot(designTimeValues)
473
+ setGeneralMode('edit')
474
+ }}
475
+ >
476
+ {t('common.edit', 'Edit')}
477
+ </Button>
478
+ )}
479
+ <Button type="button" variant="destructive" onClick={deleteOffering}>
480
+ {t('common.delete', 'Delete')}
481
+ </Button>
482
+ </div>
483
+ )}
484
+ </div>
485
+
486
+ {error && (
487
+ <Alert variant="destructive">{error}</Alert>
488
+ )}
489
+
490
+ {/* Tabs */}
491
+ {!isNew && (
492
+ <div className="flex gap-1 border-b">
493
+ {([
494
+ 'general',
495
+ 'charges',
496
+ ...(form.offeringType === 'bundle' ? ['components' as const] : []),
497
+ ] as const).map((t_) => (
498
+ <button
499
+ key={t_}
500
+ onClick={() => setTab(t_ as typeof tab)}
501
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
502
+ tab === t_ ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground'
503
+ }`}
504
+ >
505
+ {t_ === 'general' && 'General'}
506
+ {t_ === 'charges' && `Charges (${charges.length})`}
507
+ {t_ === 'components' && `Components (${components.length})`}
508
+ </button>
509
+ ))}
510
+ </div>
511
+ )}
512
+
513
+ {/* ─── General Tab / New Form ─────────────────────────────── */}
514
+ {(isNew || tab === 'general') && (
515
+ <div className="rounded-lg border bg-card p-6 space-y-4">
516
+ {isNew && (
517
+ <div>
518
+ <label className="block text-sm font-medium mb-1">Specification</label>
519
+ <select
520
+ value={form.specId}
521
+ onChange={(e) => {
522
+ setForm({ ...form, specId: e.target.value })
523
+ loadSpecAttributes(e.target.value)
524
+ setDesignTimeValues({})
525
+ }}
526
+ className="w-full rounded-md border px-3 py-2 text-sm"
527
+ >
528
+ <option value="">Select a specification...</option>
529
+ {specs.map((s) => (
530
+ <option key={s.id} value={s.id}>{s.name} ({s.code})</option>
531
+ ))}
532
+ </select>
533
+ </div>
534
+ )}
535
+
536
+ <div className="grid grid-cols-2 gap-4">
537
+ <div>
538
+ <label className="block text-sm font-medium mb-1">{t('cpq.offerings.code', 'Code')}</label>
539
+ {generalMode === 'view' && !isNew ? (
540
+ <p className="text-sm font-mono py-2">{form.code || '—'}</p>
541
+ ) : (
542
+ <input
543
+ type="text"
544
+ value={form.code}
545
+ onChange={(e) => setForm({ ...form, code: e.target.value })}
546
+ placeholder="e.g. gix-cloud-connect-aws"
547
+ className="w-full rounded-md border px-3 py-2 text-sm"
548
+ />
549
+ )}
550
+ </div>
551
+ <div>
552
+ <label className="block text-sm font-medium mb-1">{t('cpq.offerings.name', 'Name')}</label>
553
+ {generalMode === 'view' && !isNew ? (
554
+ <p className="text-sm py-2">{form.name || '—'}</p>
555
+ ) : (
556
+ <input
557
+ type="text"
558
+ value={form.name}
559
+ onChange={(e) => setForm({ ...form, name: e.target.value })}
560
+ className="w-full rounded-md border px-3 py-2 text-sm"
561
+ />
562
+ )}
563
+ </div>
564
+ <div>
565
+ <label className="block text-sm font-medium mb-1">{t('cpq.offerings.lifecycleStatus', 'Lifecycle Status')}</label>
566
+ {generalMode === 'view' && !isNew ? (
567
+ <div className="py-2">
568
+ <Tag variant={lifecycleStatusMap[form.lifecycleStatus as LifecycleStatus] ?? 'neutral'} dot>
569
+ {formatStatusLabel(form.lifecycleStatus)}
570
+ </Tag>
571
+ </div>
572
+ ) : (
573
+ <select
574
+ value={form.lifecycleStatus}
575
+ onChange={(e) => setForm({ ...form, lifecycleStatus: e.target.value })}
576
+ className="w-full rounded-md border px-3 py-2 text-sm"
577
+ >
578
+ <option value="draft">Draft</option>
579
+ <option value="active">Active</option>
580
+ <option value="deprecated">Deprecated</option>
581
+ </select>
582
+ )}
583
+ </div>
584
+ <div>
585
+ <label className="block text-sm font-medium mb-1">{t('cpq.offerings.isActive', 'Is Active?')}</label>
586
+ <div className="flex items-center h-[38px]">
587
+ {generalMode === 'view' && !isNew ? (
588
+ <Tag variant={form.isActive ? 'success' : 'neutral'} dot>
589
+ {form.isActive ? t('common.yes', 'Yes') : t('common.no', 'No')}
590
+ </Tag>
591
+ ) : (
592
+ <input
593
+ type="checkbox"
594
+ checked={form.isActive}
595
+ onChange={(e) => setForm({ ...form, isActive: e.target.checked })}
596
+ className="rounded border"
597
+ />
598
+ )}
599
+ </div>
600
+ </div>
601
+ </div>
602
+ <div>
603
+ <label className="block text-sm font-medium mb-1">{t('cpq.offerings.description', 'Description')}</label>
604
+ {generalMode === 'view' && !isNew ? (
605
+ <p className="text-sm text-muted-foreground py-2 whitespace-pre-wrap">{form.description || '—'}</p>
606
+ ) : (
607
+ <textarea
608
+ value={form.description ?? ''}
609
+ onChange={(e) => setForm({ ...form, description: e.target.value })}
610
+ rows={3}
611
+ className="w-full rounded-md border px-3 py-2 text-sm"
612
+ />
613
+ )}
614
+ </div>
615
+
616
+ {/* Design-time values */}
617
+ {designTimeAttrs.length > 0 && (
618
+ <div className="space-y-3">
619
+ <label className="block text-sm font-medium">Design-Time Values</label>
620
+ <div className="grid grid-cols-2 gap-3">
621
+ {designTimeAttrs.map((attr) => {
622
+ const value = designTimeValues[attr.code]
623
+ const isViewing = generalMode === 'view' && !isNew
624
+ // Lookup the human label for select-type values so the
625
+ // read-only view doesn't show raw codes.
626
+ const optionLabel =
627
+ (attr.attributeType === 'select' || attr.attributeType === 'enum') && attr.options
628
+ ? attr.options.find((o) => o.value === value)?.label
629
+ : undefined
630
+ return (
631
+ <div key={attr.code}>
632
+ <label className="block text-xs font-medium mb-1">
633
+ {attr.name}
634
+ {attr.isRequired && <span className="text-destructive ml-0.5">*</span>}
635
+ </label>
636
+ {isViewing ? (
637
+ <p className="text-sm py-2">
638
+ {value == null || value === '' ? (
639
+ <span className="text-muted-foreground">—</span>
640
+ ) : attr.attributeType === 'boolean' ? (
641
+ <Tag variant={value ? 'success' : 'neutral'} dot>
642
+ {value ? t('common.yes', 'Yes') : t('common.no', 'No')}
643
+ </Tag>
644
+ ) : (
645
+ optionLabel ?? String(value)
646
+ )}
647
+ </p>
648
+ ) : (attr.attributeType === 'select' || attr.attributeType === 'enum') && attr.options ? (
649
+ <select
650
+ value={String(value ?? '')}
651
+ onChange={(e) => setDesignTimeValues({ ...designTimeValues, [attr.code]: e.target.value || null })}
652
+ className="w-full rounded-md border px-2 py-1.5 text-sm"
653
+ >
654
+ <option value="">Select {attr.name}...</option>
655
+ {attr.options.map((opt) => (
656
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
657
+ ))}
658
+ </select>
659
+ ) : attr.attributeType === 'number' ? (
660
+ <NumberInput
661
+ value={typeof value === 'number' ? (value as number) : null}
662
+ onChange={(n) => setDesignTimeValues({ ...designTimeValues, [attr.code]: n })}
663
+ />
664
+ ) : attr.attributeType === 'boolean' ? (
665
+ <div className="pt-1">
666
+ <label className="flex items-center gap-1.5 text-sm">
667
+ <input
668
+ type="checkbox"
669
+ checked={!!value}
670
+ onChange={(e) => setDesignTimeValues({ ...designTimeValues, [attr.code]: e.target.checked })}
671
+ className="rounded border"
672
+ />
673
+ {attr.name}
674
+ </label>
675
+ </div>
676
+ ) : (
677
+ <input
678
+ type="text"
679
+ value={String(value ?? '')}
680
+ onChange={(e) => setDesignTimeValues({ ...designTimeValues, [attr.code]: e.target.value || null })}
681
+ className="w-full rounded-md border px-2 py-1.5 text-sm"
682
+ placeholder={attr.helpText ?? undefined}
683
+ />
684
+ )}
685
+ {attr.helpText && <p className="text-xs text-muted-foreground mt-0.5">{attr.helpText}</p>}
686
+ </div>
687
+ )
688
+ })}
689
+ </div>
690
+ </div>
691
+ )}
692
+
693
+ {/* In view mode the Edit button lives in the page header next to
694
+ Delete (OM standard). Here we only render the Save/Cancel
695
+ pair when actually editing. */}
696
+ {!(generalMode === 'view' && !isNew) && (
697
+ <div className="flex gap-3">
698
+ <Button
699
+ type="button"
700
+ onClick={saveOffering}
701
+ disabled={saving || !form.code || !form.name || (isNew && !form.specId)}
702
+ >
703
+ {saving ? t('common.saving', 'Saving...') : t('common.save', 'Save')}
704
+ </Button>
705
+ <Button
706
+ type="button"
707
+ variant="outline"
708
+ onClick={() => {
709
+ if (isNew) {
710
+ router.push('/backend/cpq/offerings')
711
+ return
712
+ }
713
+ if (formSnapshot) setForm(formSnapshot)
714
+ if (designTimeSnapshot) setDesignTimeValues(designTimeSnapshot)
715
+ setFormSnapshot(null)
716
+ setDesignTimeSnapshot(null)
717
+ setGeneralMode('view')
718
+ }}
719
+ >
720
+ {t('common.cancel', 'Cancel')}
721
+ </Button>
722
+ </div>
723
+ )}
724
+ </div>
725
+ )}
726
+
727
+ {/* ─── Charges Tab ──────────────────────────────────────── */}
728
+ {tab === 'charges' && !isNew && (
729
+ <div className="space-y-4">
730
+ <div className="flex justify-end">
731
+ <button
732
+ onClick={() => startEditCharge()}
733
+ className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
734
+ >
735
+ {t('cpq.offerings.addCharge', 'Add Charge')}
736
+ </button>
737
+ </div>
738
+
739
+ {/* Charge edit form */}
740
+ {editingCharge && (
741
+ <div className="rounded-lg border bg-card p-4 space-y-3">
742
+ <h3 className="font-medium text-sm">
743
+ {editingCharge.id ? 'Edit Charge' : 'New Charge'}
744
+ </h3>
745
+ <div className="grid grid-cols-3 gap-3">
746
+ <div>
747
+ <label className="block text-xs font-medium mb-1">Code</label>
748
+ <input type="text" value={editingCharge.code} onChange={(e) => setEditingCharge({ ...editingCharge, code: e.target.value })} placeholder="e.g. mrc-monthly" className="w-full rounded-md border px-2 py-1.5 text-sm" />
749
+ </div>
750
+ <div>
751
+ <label className="block text-xs font-medium mb-1">Name</label>
752
+ <input type="text" value={editingCharge.name} onChange={(e) => setEditingCharge({ ...editingCharge, name: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
753
+ </div>
754
+ <div>
755
+ <label className="block text-xs font-medium mb-1">Charge Type</label>
756
+ <select value={editingCharge.chargeType} onChange={(e) => setEditingCharge({ ...editingCharge, chargeType: e.target.value })} className="w-full rounded-md border px-2 py-1.5 text-sm">
757
+ <option value="mrc">MRC (Monthly Recurring)</option>
758
+ <option value="nrc">NRC (Non-Recurring)</option>
759
+ <option value="usage">Usage</option>
760
+ </select>
761
+ </div>
762
+ <div>
763
+ <label className="block text-xs font-medium mb-1">Pricing Method</label>
764
+ <select value={editingCharge.pricingMethod} onChange={(e) => {
765
+ const method = e.target.value
766
+ if (method === 'flat') {
767
+ setEditingCharge({ ...editingCharge, pricingMethod: method, pricingTableId: null, priceColumnKey: null, quantityAttributeCode: null })
768
+ } else {
769
+ setEditingCharge({ ...editingCharge, pricingMethod: method, fixedPrice: null })
770
+ }
771
+ }} className="w-full rounded-md border px-2 py-1.5 text-sm">
772
+ <option value="flat">Flat Price</option>
773
+ <option value="per_unit">Per Unit (table lookup)</option>
774
+ <option value="tiered">Tiered (table lookup)</option>
775
+ </select>
776
+ </div>
777
+ {(editingCharge.pricingMethod === 'flat' || editingCharge.pricingMethod === 'fixed') ? (
778
+ <>
779
+ <div>
780
+ <label className="block text-xs font-medium mb-1">Fixed Price <span className="text-destructive">*</span></label>
781
+ <input type="text" value={editingCharge.fixedPrice ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, fixedPrice: e.target.value || null })} placeholder="0.00" className="w-full rounded-md border px-2 py-1.5 text-sm" />
782
+ </div>
783
+ <div>
784
+ <label className="block text-xs font-medium mb-1">Currency <span className="text-destructive">*</span></label>
785
+ <input type="text" value={editingCharge.currencyCode ?? 'USD'} onChange={(e) => setEditingCharge({ ...editingCharge, currencyCode: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
786
+ </div>
787
+ </>
788
+ ) : (
789
+ <>
790
+ <div>
791
+ <label className="block text-xs font-medium mb-1">Pricing Table <span className="text-destructive">*</span></label>
792
+ <select value={editingCharge.pricingTableId ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, pricingTableId: e.target.value || null, priceColumnKey: null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
793
+ <option value="">Select table...</option>
794
+ {pricingTables.map((pt) => (
795
+ <option key={pt.id} value={pt.id}>{pt.name} ({pt.code})</option>
796
+ ))}
797
+ </select>
798
+ {pricingTables.length === 0 && (
799
+ <p className="text-xs text-warning mt-0.5">
800
+ No pricing tables yet —{' '}
801
+ <a href="/backend/cpq/pricing-tables" className="text-primary hover:underline">create one</a>{' '}
802
+ before using {editingCharge.pricingMethod} pricing.
803
+ </p>
804
+ )}
805
+ </div>
806
+ <div>
807
+ <label className="block text-xs font-medium mb-1">Price Column <span className="text-destructive">*</span></label>
808
+ <select value={editingCharge.priceColumnKey ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, priceColumnKey: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" disabled={!selectedPricingTable}>
809
+ <option value="">Select column...</option>
810
+ {selectedPricingTable?.priceColumns.map((col) => (
811
+ <option key={col.key} value={col.key}>{col.label}</option>
812
+ ))}
813
+ </select>
814
+ </div>
815
+ <div>
816
+ <label className="block text-xs font-medium mb-1">Quantity Attribute <span className="text-destructive">*</span></label>
817
+ <select value={editingCharge.quantityAttributeCode ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, quantityAttributeCode: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm">
818
+ <option value="">Select attribute...</option>
819
+ {attributes.filter((a) => a.attributeType === 'number').map((a) => (
820
+ <option key={a.code} value={a.code}>{a.name} ({a.code})</option>
821
+ ))}
822
+ </select>
823
+ <p className="text-xs text-muted-foreground mt-0.5">Attribute whose value drives the quantity for pricing</p>
824
+ {attributes.filter((a) => a.attributeType === 'number').length === 0 && form.specId && (
825
+ <p className="text-xs text-warning mt-0.5">
826
+ Spec has no number attributes —{' '}
827
+ <a href={`/backend/cpq/specifications/${form.specId}`} className="text-primary hover:underline">
828
+ add one to the spec
829
+ </a>{' '}
830
+ first.
831
+ </p>
832
+ )}
833
+ </div>
834
+ </>
835
+ )}
836
+ <div>
837
+ <label className="block text-xs font-medium mb-1">Sort Order</label>
838
+ <NumberInput
839
+ integer
840
+ value={editingCharge.sortOrder}
841
+ onChange={(n) => setEditingCharge({ ...editingCharge, sortOrder: n ?? 0 })}
842
+ />
843
+ </div>
844
+ </div>
845
+ <div>
846
+ <label className="block text-xs font-medium mb-1">Description</label>
847
+ <input type="text" value={editingCharge.description ?? ''} onChange={(e) => setEditingCharge({ ...editingCharge, description: e.target.value || null })} className="w-full rounded-md border px-2 py-1.5 text-sm" />
848
+ </div>
849
+ <div className="flex items-end gap-4">
850
+ <label className="flex items-center gap-1.5 text-sm">
851
+ <input type="checkbox" checked={editingCharge.isActive} onChange={(e) => setEditingCharge({ ...editingCharge, isActive: e.target.checked })} className="rounded border" />
852
+ Active
853
+ </label>
854
+ </div>
855
+ {(() => {
856
+ const missing = chargeMissingFields(editingCharge)
857
+ const blocked = saving || !editingCharge.code || !editingCharge.name || missing.length > 0
858
+ return (
859
+ <>
860
+ {missing.length > 0 && (
861
+ <Alert variant="destructive">
862
+ Missing required field{missing.length > 1 ? 's' : ''} for {editingCharge.pricingMethod} pricing: {missing.join(', ')}
863
+ </Alert>
864
+ )}
865
+ <div className="flex gap-2">
866
+ <button onClick={saveCharge} disabled={blocked} className="inline-flex items-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed">
867
+ {saving ? 'Saving...' : 'Save'}
868
+ </button>
869
+ <button onClick={() => setEditingCharge(null)} className="inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted">Cancel</button>
870
+ </div>
871
+ </>
872
+ )
873
+ })()}
874
+ </div>
875
+ )}
876
+
877
+ {/* Charges table */}
878
+ {charges.length === 0 ? (
879
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
880
+ No charges defined. Add recurring, non-recurring, or usage charges to this offering.
881
+ </div>
882
+ ) : (
883
+ <div className="rounded-lg border">
884
+ <table className="w-full text-sm">
885
+ <thead>
886
+ <tr className="border-b bg-muted/50">
887
+ <th className="px-4 py-3 text-left font-medium">Code</th>
888
+ <th className="px-4 py-3 text-left font-medium">Name</th>
889
+ <th className="px-4 py-3 text-left font-medium">Type</th>
890
+ <th className="px-4 py-3 text-left font-medium">Pricing</th>
891
+ <th className="px-4 py-3 text-left font-medium">Price</th>
892
+ <th className="px-4 py-3 text-left font-medium">Active</th>
893
+ <th className="px-4 py-3 text-right font-medium">Actions</th>
894
+ </tr>
895
+ </thead>
896
+ <tbody>
897
+ {charges.map((charge) => {
898
+ const incomplete = !isChargeComplete(charge)
899
+ const tableRef = charge.pricingTableId
900
+ ? pricingTables.find((pt) => pt.id === charge.pricingTableId)
901
+ : null
902
+ const qtyAttr = charge.quantityAttributeCode
903
+ ? attributes.find((a) => a.code === charge.quantityAttributeCode)
904
+ : null
905
+ const isFlat = charge.pricingMethod === 'flat' || charge.pricingMethod === 'fixed'
906
+ return (
907
+ <tr key={charge.id} className="border-b">
908
+ <td className="px-4 py-3 font-mono text-xs">{charge.code}</td>
909
+ <td className="px-4 py-3">
910
+ {charge.name}
911
+ {incomplete && (
912
+ <Tag variant="error" className="ml-2 px-2 text-[10px]" dot>Incomplete</Tag>
913
+ )}
914
+ </td>
915
+ <td className="px-4 py-3">
916
+ <Tag variant={chargeTypeMap[charge.chargeType as ChargeType] ?? 'neutral'} className="px-2 text-xs">
917
+ {charge.chargeType.toUpperCase()}
918
+ </Tag>
919
+ </td>
920
+ <td className="px-4 py-3 text-xs">{PRICING_METHOD_LABELS[charge.pricingMethod] ?? charge.pricingMethod}</td>
921
+ <td className="px-4 py-3 text-xs">
922
+ {isFlat ? (
923
+ charge.fixedPrice != null
924
+ ? `${charge.currencyCode ?? 'USD'} ${charge.fixedPrice}`
925
+ : <span className="text-muted-foreground">—</span>
926
+ ) : (
927
+ <div className="flex flex-col gap-0.5">
928
+ <span>
929
+ <span className="text-muted-foreground">Table:</span>{' '}
930
+ {tableRef ? `${tableRef.name} → ${charge.priceColumnKey ?? '?'}` : <span className="text-destructive">missing</span>}
931
+ </span>
932
+ <span>
933
+ <span className="text-muted-foreground">× Qty:</span>{' '}
934
+ {qtyAttr ? `${qtyAttr.name} (${qtyAttr.code})` : <span className="text-destructive">missing</span>}
935
+ </span>
936
+ </div>
937
+ )}
938
+ </td>
939
+ <td className="px-4 py-3">{charge.isActive ? 'Yes' : 'No'}</td>
940
+ <td className="px-4 py-3 text-right">
941
+ <button onClick={() => startEditCharge(charge)} className="text-xs text-primary hover:underline mr-2">Edit</button>
942
+ <button onClick={() => deleteCharge(charge.id)} className="text-xs text-destructive hover:underline">Delete</button>
943
+ </td>
944
+ </tr>
945
+ )
946
+ })}
947
+ </tbody>
948
+ </table>
949
+ </div>
950
+ )}
951
+ </div>
952
+ )}
953
+
954
+ {/* ─── Components Tab (bundle offerings) ────────────────────── */}
955
+ {tab === 'components' && !isNew && form.offeringType === 'bundle' && (
956
+ <div className="space-y-4">
957
+ {bundleSlots.length === 0 ? (
958
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
959
+ No bundle slots defined on this specification. Add slots on the specification first.
960
+ </div>
961
+ ) : (
962
+ <>
963
+ {/* Group components by slot */}
964
+ {bundleSlots.map((slot) => {
965
+ const slotComponents = components.filter((c) => c.slotId === slot.id)
966
+ return (
967
+ <div key={slot.id} className="rounded-lg border bg-card overflow-hidden">
968
+ <div className="bg-muted/50 px-4 py-3 border-b flex items-center justify-between">
969
+ <div>
970
+ <span className="font-medium text-sm">{slot.name}</span>
971
+ <Tag variant="info" className="ml-2 px-2 text-[10px]">
972
+ {slot.componentGroup}
973
+ </Tag>
974
+ <span className="ml-2 text-xs text-muted-foreground">
975
+ {slot.cardinalityMin}..{slot.cardinalityMax ?? '∞'}
976
+ </span>
977
+ {slot.targetSpec && (
978
+ <span className="ml-2 text-xs text-muted-foreground">→ {slot.targetSpec.name}</span>
979
+ )}
980
+ </div>
981
+ <button
982
+ onClick={async () => {
983
+ setAddingComponent({ slotId: slot.id, childOfferingId: '', isDefault: false })
984
+ if (!slotOfferings[slot.targetSpecId]) {
985
+ const res = await fetch(`/api/cpq/product-offerings?specId=${encodeURIComponent(slot.targetSpecId)}&pageSize=100`)
986
+ if (res.ok) {
987
+ const d = await res.json()
988
+ setSlotOfferings((prev) => ({ ...prev, [slot.targetSpecId]: d.items ?? [] }))
989
+ }
990
+ }
991
+ }}
992
+ className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
993
+ >
994
+ + Add Component
995
+ </button>
996
+ </div>
997
+
998
+ {addingComponent?.slotId === slot.id && (
999
+ <div className="p-4 border-b bg-muted/20 space-y-3">
1000
+ <div className="grid grid-cols-3 gap-3">
1001
+ <div className="col-span-2">
1002
+ <label className="block text-xs font-medium mb-1">Component Offering</label>
1003
+ <select
1004
+ value={addingComponent.childOfferingId}
1005
+ onChange={(e) => setAddingComponent({ ...addingComponent, childOfferingId: e.target.value })}
1006
+ className="w-full rounded-md border px-3 py-2 text-sm"
1007
+ >
1008
+ <option value="">— Select offering —</option>
1009
+ {(slotOfferings[slot.targetSpecId] ?? []).map((o) => (
1010
+ <option key={o.id} value={o.id}>{o.name} ({o.code}){o.offeringType === 'bundle' ? ' [bundle]' : ''}</option>
1011
+ ))}
1012
+ </select>
1013
+ </div>
1014
+ <div>
1015
+ <label className="block text-xs font-medium mb-1">Default?</label>
1016
+ <div className="flex items-center h-[38px]">
1017
+ <input
1018
+ type="checkbox"
1019
+ checked={addingComponent.isDefault}
1020
+ onChange={(e) => setAddingComponent({ ...addingComponent, isDefault: e.target.checked })}
1021
+ className="rounded border"
1022
+ />
1023
+ <span className="ml-2 text-xs text-muted-foreground">Pre-selected in quote</span>
1024
+ </div>
1025
+ </div>
1026
+ </div>
1027
+ <div className="flex gap-2">
1028
+ <button
1029
+ disabled={saving || !addingComponent.childOfferingId}
1030
+ onClick={async () => {
1031
+ setSaving(true)
1032
+ setError(null)
1033
+ try {
1034
+ const res = await fetch('/api/cpq/offering-components', {
1035
+ method: 'POST',
1036
+ headers: { 'Content-Type': 'application/json' },
1037
+ body: JSON.stringify({
1038
+ offeringId,
1039
+ slotId: addingComponent.slotId,
1040
+ childOfferingId: addingComponent.childOfferingId,
1041
+ isDefault: addingComponent.isDefault,
1042
+ }),
1043
+ })
1044
+ if (!res.ok) {
1045
+ const d = await res.json()
1046
+ setError(d.error ?? 'Failed to add component')
1047
+ return
1048
+ }
1049
+ setAddingComponent(null)
1050
+ const compRes = await fetch(`/api/cpq/offering-components?offeringId=${offeringId}`)
1051
+ if (compRes.ok) {
1052
+ const d = await compRes.json()
1053
+ setComponents(d.items ?? [])
1054
+ }
1055
+ } catch {
1056
+ setError('Failed to add component')
1057
+ } finally {
1058
+ setSaving(false)
1059
+ }
1060
+ }}
1061
+ className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
1062
+ >
1063
+ {saving ? 'Adding...' : 'Add'}
1064
+ </button>
1065
+ <button
1066
+ onClick={() => setAddingComponent(null)}
1067
+ className="inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-muted"
1068
+ >
1069
+ Cancel
1070
+ </button>
1071
+ </div>
1072
+ </div>
1073
+ )}
1074
+
1075
+ {slotComponents.length === 0 ? (
1076
+ <div className="p-4 text-center text-xs text-muted-foreground">
1077
+ No components assigned to this slot
1078
+ {slot.cardinalityMin > 0 && (
1079
+ <span className="ml-1 text-status-warning-text font-medium">(required: min {slot.cardinalityMin})</span>
1080
+ )}
1081
+ </div>
1082
+ ) : (
1083
+ <table className="w-full text-sm">
1084
+ <thead>
1085
+ <tr className="border-b bg-muted/30">
1086
+ <th className="px-4 py-2 text-left text-xs font-medium">Offering</th>
1087
+ <th className="px-4 py-2 text-left text-xs font-medium">Code</th>
1088
+ <th className="px-4 py-2 text-left text-xs font-medium">Type</th>
1089
+ <th className="px-4 py-2 text-left text-xs font-medium">Default</th>
1090
+ <th className="px-4 py-2 text-right text-xs font-medium">Actions</th>
1091
+ </tr>
1092
+ </thead>
1093
+ <tbody>
1094
+ {slotComponents.map((comp) => (
1095
+ <tr key={comp.id} className="border-b last:border-0">
1096
+ <td className="px-4 py-2.5 font-medium">{comp.childOffering?.name ?? 'Unknown'}</td>
1097
+ <td className="px-4 py-2.5 text-muted-foreground text-xs">{comp.childOffering?.code ?? '—'}</td>
1098
+ <td className="px-4 py-2.5">
1099
+ {comp.childOffering?.offeringType === 'bundle' ? (
1100
+ <Tag variant="brand" className="px-2 text-[10px]">bundle</Tag>
1101
+ ) : (
1102
+ <Tag variant="neutral" className="px-2 text-[10px]">simple</Tag>
1103
+ )}
1104
+ </td>
1105
+ <td className="px-4 py-2.5">
1106
+ <button
1107
+ onClick={async () => {
1108
+ try {
1109
+ const res = await fetch('/api/cpq/offering-components', {
1110
+ method: 'PUT',
1111
+ headers: { 'Content-Type': 'application/json' },
1112
+ body: JSON.stringify({ id: comp.id, isDefault: !comp.isDefault }),
1113
+ })
1114
+ if (res.ok) {
1115
+ setComponents((prev) => prev.map((c) => c.id === comp.id ? { ...c, isDefault: !c.isDefault } : c))
1116
+ }
1117
+ } catch { /* ignore */ }
1118
+ }}
1119
+ className="cursor-pointer"
1120
+ >
1121
+ <Tag variant={comp.isDefault ? 'success' : 'neutral'} className="px-2 text-[10px]">
1122
+ {comp.isDefault ? 'Yes' : 'No'}
1123
+ </Tag>
1124
+ </button>
1125
+ </td>
1126
+ <td className="px-4 py-2.5 text-right">
1127
+ <button
1128
+ onClick={async () => {
1129
+ if (!confirm('Remove this component from the bundle?')) return
1130
+ try {
1131
+ const res = await fetch('/api/cpq/offering-components', {
1132
+ method: 'DELETE',
1133
+ headers: { 'Content-Type': 'application/json' },
1134
+ body: JSON.stringify({ id: comp.id }),
1135
+ })
1136
+ if (res.ok) {
1137
+ setComponents((prev) => prev.filter((c) => c.id !== comp.id))
1138
+ } else {
1139
+ const d = await res.json()
1140
+ setError(d.error ?? 'Failed to remove')
1141
+ }
1142
+ } catch {
1143
+ setError('Failed to remove component')
1144
+ }
1145
+ }}
1146
+ className="text-xs text-destructive hover:underline"
1147
+ >
1148
+ Remove
1149
+ </button>
1150
+ </td>
1151
+ </tr>
1152
+ ))}
1153
+ </tbody>
1154
+ </table>
1155
+ )}
1156
+ </div>
1157
+ )
1158
+ })}
1159
+ </>
1160
+ )}
1161
+ </div>
1162
+ )}
1163
+ </div>
1164
+ )
1165
+ }