@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,1230 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { SalesOrder, SalesOrderLine } from '@open-mercato/core/modules/sales/data/entities'
3
+ import type { SalesDocumentNumberGenerator } from '@open-mercato/core/modules/sales/services/salesDocumentNumberGenerator'
4
+ import {
5
+ CpqQuoteConfiguration,
6
+ CpqQuoteLineConfiguration,
7
+ CpqQuoteTargetSubscription,
8
+ CpqInventorySubscription,
9
+ CpqOrderConfiguration,
10
+ CpqOrderLineConfiguration,
11
+ CpqProductOffering,
12
+ CpqProductSpecification,
13
+ CpqInventorySubscriptionItem,
14
+ } from '../data/entities'
15
+ import type {
16
+ ApplyArcResult,
17
+ ApplyMergeRenewalResult,
18
+ ArcLineChange,
19
+ CreateSubscriptionItemInput,
20
+ DefaultCpqInventoryService,
21
+ } from './cpqInventoryService'
22
+ import {
23
+ CPQ_ORDER_TRANSITIONS,
24
+ type CpqOrderStatus,
25
+ type TenantScope,
26
+ } from './types'
27
+ import { emitCpqEvent } from '../events'
28
+
29
+ // ─── Types ──────────────────────────────────────────────────────
30
+
31
+ interface OrderListFilters {
32
+ customerId?: string
33
+ sourceQuoteId?: string
34
+ salesOrderId?: string
35
+ cpqStatus?: string | string[]
36
+ currencyCode?: string
37
+ search?: string
38
+ sortField?: 'createdAt' | 'updatedAt' | 'cpqStatus' | 'activatedAt' | 'currencyCode'
39
+ sortDir?: 'asc' | 'desc'
40
+ page?: number
41
+ pageSize?: number
42
+ }
43
+
44
+ interface PaginatedResult<T> {
45
+ items: T[]
46
+ total: number
47
+ page: number
48
+ pageSize: number
49
+ totalPages: number
50
+ }
51
+
52
+ interface OrderLineResult {
53
+ lineId: string
54
+ offeringId: string | null
55
+ offeringName: string
56
+ offeringType: string | null
57
+ productId: string | null
58
+ specId: string | null
59
+ action: string
60
+ parentLineId: string | null
61
+ quantity: number
62
+ configuration: Record<string, unknown>
63
+ startDate: string | null
64
+ termMonths: number | null
65
+ endDate: string | null
66
+ nrcTotal: number
67
+ mrcTotal: number
68
+ charges: Array<Record<string, unknown>>
69
+ sourceQuoteLineId: string | null
70
+ /** XD-250 ARC: snapshot of the source subscription item's current state.
71
+ * Null for non-ARC lines and for fresh `add` lines on amend quotes. */
72
+ arcSource: {
73
+ subscriptionItemId: string
74
+ name: string
75
+ mrcAmount: number
76
+ nrcAmount: number
77
+ quantity: number
78
+ } | null
79
+ }
80
+
81
+ interface OrderResult {
82
+ id: string
83
+ orderId: string
84
+ orderNumber: string
85
+ customerId: string
86
+ cpqStatus: string
87
+ sourceQuoteId: string | null
88
+ /** XD-250 ARC quote type carried over from the source quote — used by the
89
+ * order detail page to render the AMEND / RENEW / CANCEL badge. */
90
+ quoteType: string
91
+ currencyCode: string
92
+ pricingSummary: {
93
+ nrcTotal: number
94
+ mrcTotal: number
95
+ currencyCode: string
96
+ }
97
+ activatedAt: string | null
98
+ createdAt: string
99
+ lines: OrderLineResult[]
100
+ }
101
+
102
+ interface OrderServiceDeps {
103
+ em: EntityManager
104
+ cpqInventoryService: DefaultCpqInventoryService
105
+ salesDocumentNumberGenerator: SalesDocumentNumberGenerator
106
+ }
107
+
108
+ const CPQ_TO_SALES_ORDER_STATUS: Record<CpqOrderStatus, string> = {
109
+ draft: 'draft',
110
+ pending_activation: 'pending',
111
+ active: 'active',
112
+ cancelled: 'cancelled',
113
+ fulfilled: 'fulfilled',
114
+ }
115
+
116
+ // ─── Service ────────────────────────────────────────────────────
117
+
118
+ export class DefaultCpqOrderService {
119
+ private em: EntityManager
120
+ private inventoryService: DefaultCpqInventoryService
121
+ private numberGenerator: SalesDocumentNumberGenerator
122
+
123
+ constructor(deps: OrderServiceDeps) {
124
+ this.em = deps.em
125
+ this.inventoryService = deps.cpqInventoryService
126
+ this.numberGenerator = deps.salesDocumentNumberGenerator
127
+ }
128
+
129
+ // ─── Convert Quote to Order ─────────────────────────────────
130
+
131
+ async convertQuoteToOrder(quoteId: string, scope: TenantScope): Promise<OrderResult> {
132
+ const em = this.em
133
+
134
+ const cpqConfig = await em.findOne(CpqQuoteConfiguration, {
135
+ id: quoteId,
136
+ organizationId: scope.organizationId,
137
+ tenantId: scope.tenantId,
138
+ deletedAt: null,
139
+ })
140
+ if (!cpqConfig) {
141
+ const byQuoteId = await em.findOne(CpqQuoteConfiguration, {
142
+ quoteId,
143
+ organizationId: scope.organizationId,
144
+ tenantId: scope.tenantId,
145
+ deletedAt: null,
146
+ })
147
+ if (!byQuoteId) throw new OrderError(404, 'Quote not found')
148
+ return this.convertFromConfig(byQuoteId, scope)
149
+ }
150
+ return this.convertFromConfig(cpqConfig, scope)
151
+ }
152
+
153
+ private async convertFromConfig(cpqConfig: CpqQuoteConfiguration, scope: TenantScope): Promise<OrderResult> {
154
+ const em = this.em
155
+
156
+ const blockedStatuses = ['cancelled', 'rejected']
157
+ if (blockedStatuses.includes(cpqConfig.cpqStatus)) {
158
+ throw new OrderError(409, `Cannot convert a quote in '${cpqConfig.cpqStatus}' status`)
159
+ }
160
+
161
+ // Idempotency guard: a quote may only convert once. If an order with
162
+ // this quote as its `sourceQuoteId` already exists, the UI should
163
+ // surface "Go to order" instead of re-running conversion — return
164
+ // 409 with the existing id so the caller can redirect.
165
+ const existingOrder = await em.findOne(CpqOrderConfiguration, {
166
+ sourceQuoteId: cpqConfig.id,
167
+ organizationId: scope.organizationId,
168
+ tenantId: scope.tenantId,
169
+ deletedAt: null,
170
+ })
171
+ if (existingOrder) {
172
+ throw new OrderError(
173
+ 409,
174
+ `Quote has already been converted to order ${existingOrder.orderId}`,
175
+ )
176
+ }
177
+
178
+ const quoteLines = await em.find(CpqQuoteLineConfiguration, {
179
+ quoteConfigurationId: cpqConfig.id,
180
+ organizationId: scope.organizationId,
181
+ tenantId: scope.tenantId,
182
+ deletedAt: null,
183
+ })
184
+
185
+ if (quoteLines.length === 0) {
186
+ throw new OrderError(422, 'Cannot convert a quote with no line items')
187
+ }
188
+
189
+ const { number: orderNumber } = await this.numberGenerator.generate({
190
+ kind: 'order',
191
+ organizationId: scope.organizationId,
192
+ tenantId: scope.tenantId,
193
+ })
194
+
195
+ const salesOrderId = crypto.randomUUID()
196
+ const pricingSummary = cpqConfig.pricingSummary as Record<string, unknown> | null
197
+ const nrcTotal = Number(pricingSummary?.nrcTotal ?? 0)
198
+ const mrcTotal = Number(pricingSummary?.mrcTotal ?? 0)
199
+ const grandTotal = nrcTotal + mrcTotal
200
+
201
+ const salesOrder = em.create(SalesOrder, {
202
+ id: salesOrderId,
203
+ organizationId: scope.organizationId,
204
+ tenantId: scope.tenantId,
205
+ orderNumber,
206
+ customerEntityId: cpqConfig.customerId,
207
+ currencyCode: cpqConfig.currencyCode,
208
+ status: 'draft',
209
+ subtotalNetAmount: String(grandTotal),
210
+ subtotalGrossAmount: String(grandTotal),
211
+ discountTotalAmount: '0',
212
+ taxTotalAmount: '0',
213
+ shippingNetAmount: '0',
214
+ shippingGrossAmount: '0',
215
+ surchargeTotalAmount: '0',
216
+ paidTotalAmount: '0',
217
+ grandTotalNetAmount: String(grandTotal),
218
+ grandTotalGrossAmount: String(grandTotal),
219
+ lineItemCount: quoteLines.length,
220
+ metadata: { sourceQuoteId: cpqConfig.id },
221
+ createdAt: new Date(),
222
+ updatedAt: new Date(),
223
+ } as never)
224
+ em.persist(salesOrder)
225
+
226
+ const cpqOrderConfigId = crypto.randomUUID()
227
+ const cpqOrderConfig = em.create(CpqOrderConfiguration, {
228
+ id: cpqOrderConfigId,
229
+ organizationId: scope.organizationId,
230
+ tenantId: scope.tenantId,
231
+ orderId: salesOrderId,
232
+ sourceQuoteId: cpqConfig.id,
233
+ customerId: cpqConfig.customerId,
234
+ cpqStatus: 'draft',
235
+ currencyCode: cpqConfig.currencyCode,
236
+ pricingSummary: {
237
+ nrcTotal,
238
+ mrcTotal,
239
+ currencyCode: cpqConfig.currencyCode,
240
+ },
241
+ })
242
+ em.persist(cpqOrderConfig)
243
+
244
+ const quoteLineIdToOrderLineId = new Map<string, string>()
245
+
246
+ for (let i = 0; i < quoteLines.length; i++) {
247
+ const ql = quoteLines[i]
248
+
249
+ const perInstanceNrc = Number(ql.nrcTotal)
250
+ const perInstanceMrc = Number(ql.mrcTotal)
251
+ const unitPriceNet = (perInstanceNrc + perInstanceMrc) / ql.quantity
252
+ const totalNet = perInstanceNrc + perInstanceMrc
253
+
254
+ const orderLineId = crypto.randomUUID()
255
+ quoteLineIdToOrderLineId.set(ql.quoteLineId, orderLineId)
256
+
257
+ const offeringName = await this.resolveOfferingName(
258
+ em,
259
+ ql.offeringId,
260
+ scope,
261
+ ql.configuration as Record<string, unknown> | null,
262
+ )
263
+
264
+ const orderLine = em.create(SalesOrderLine, {
265
+ id: orderLineId,
266
+ order: salesOrder,
267
+ organizationId: scope.organizationId,
268
+ tenantId: scope.tenantId,
269
+ lineNumber: i + 1,
270
+ kind: 'product',
271
+ productId: ql.productId ?? null,
272
+ name: offeringName,
273
+ quantity: String(ql.quantity),
274
+ unitPriceNet: String(unitPriceNet),
275
+ unitPriceGross: String(unitPriceNet),
276
+ totalNetAmount: String(totalNet),
277
+ totalGrossAmount: String(totalNet),
278
+ discountAmount: '0',
279
+ discountPercent: '0',
280
+ taxRate: '0',
281
+ taxAmount: '0',
282
+ currencyCode: cpqConfig.currencyCode,
283
+ configuration: {
284
+ offeringId: ql.offeringId ?? null,
285
+ action: ql.action,
286
+ nrcTotal: perInstanceNrc,
287
+ mrcTotal: perInstanceMrc,
288
+ parentLineId: ql.parentLineId ?? null,
289
+ },
290
+ normalizedQuantity: String(ql.quantity),
291
+ reservedQuantity: '0',
292
+ fulfilledQuantity: '0',
293
+ invoicedQuantity: '0',
294
+ returnedQuantity: '0',
295
+ createdAt: new Date(),
296
+ updatedAt: new Date(),
297
+ } as never)
298
+ em.persist(orderLine)
299
+ const orderLineConfig = em.create(CpqOrderLineConfiguration, {
300
+ organizationId: scope.organizationId,
301
+ tenantId: scope.tenantId,
302
+ orderLineId,
303
+ orderConfigurationId: cpqOrderConfig.id,
304
+ offeringId: ql.offeringId ?? null,
305
+ productId: ql.productId ?? null,
306
+ specId: ql.specId ?? null,
307
+ action: ql.action,
308
+ parentLineId: null,
309
+ startDate: ql.startDate,
310
+ termMonths: ql.termMonths ?? null,
311
+ endDate: ql.endDate,
312
+ configuration: { ...ql.configuration, offeringName },
313
+ quantity: ql.quantity,
314
+ charges: ql.charges ? [...ql.charges] : null,
315
+ nrcTotal: ql.nrcTotal,
316
+ mrcTotal: ql.mrcTotal,
317
+ sourceQuoteLineId: ql.quoteLineId,
318
+ })
319
+ em.persist(orderLineConfig)
320
+ }
321
+
322
+ await em.flush()
323
+
324
+ // Resolve parent line references using the quote→order line ID map
325
+ const orderLineConfigs = await em.find(CpqOrderLineConfiguration, {
326
+ orderConfigurationId: cpqOrderConfig.id,
327
+ organizationId: scope.organizationId,
328
+ tenantId: scope.tenantId,
329
+ })
330
+ for (const olc of orderLineConfigs) {
331
+ if (olc.sourceQuoteLineId) {
332
+ const sourceQl = quoteLines.find((ql) => ql.quoteLineId === olc.sourceQuoteLineId)
333
+ if (sourceQl?.parentLineId) {
334
+ const mappedParent = quoteLineIdToOrderLineId.get(sourceQl.parentLineId)
335
+ if (mappedParent) olc.parentLineId = mappedParent
336
+ }
337
+ }
338
+ }
339
+
340
+ await em.flush()
341
+
342
+ return this.buildOrderResult(em, cpqOrderConfig, salesOrder, orderLineConfigs, scope)
343
+ }
344
+
345
+ // ─── Activate Order ─────────────────────────────────────────
346
+
347
+ async activateOrder(orderId: string, scope: TenantScope): Promise<OrderResult> {
348
+ const em = this.em
349
+
350
+ const orderConfig = await this.loadOrderConfig(em, orderId, scope)
351
+
352
+ const currentStatus = orderConfig.cpqStatus as CpqOrderStatus
353
+ if (currentStatus !== 'draft' && currentStatus !== 'pending_activation') {
354
+ throw new OrderError(409, `Cannot activate order in '${currentStatus}' status`)
355
+ }
356
+
357
+ const salesOrder = await this.loadSalesOrder(em, orderConfig.orderId, scope)
358
+ const orderLines = await em.find(CpqOrderLineConfiguration, {
359
+ orderConfigurationId: orderConfig.id,
360
+ organizationId: scope.organizationId,
361
+ tenantId: scope.tenantId,
362
+ deletedAt: null,
363
+ })
364
+
365
+ // Branch on the source quote's quoteType. ARC quotes mutate existing
366
+ // subscriptions; new-sale quotes materialize a fresh subscription.
367
+ const sourceQuote = orderConfig.sourceQuoteId
368
+ ? await em.findOne(CpqQuoteConfiguration, {
369
+ id: orderConfig.sourceQuoteId,
370
+ organizationId: scope.organizationId,
371
+ tenantId: scope.tenantId,
372
+ deletedAt: null,
373
+ })
374
+ : null
375
+
376
+ const quoteType = sourceQuote?.quoteType ?? 'new'
377
+ const arcEvents: Array<() => Promise<void>> = []
378
+
379
+ if (quoteType === 'new') {
380
+ await this.createInventoryFromOrder(orderConfig, orderLines, scope)
381
+ } else {
382
+ arcEvents.push(
383
+ ...(await this.applyArcOrder(orderConfig, sourceQuote!, orderLines, scope)),
384
+ )
385
+ }
386
+
387
+ orderConfig.cpqStatus = 'active'
388
+ orderConfig.activatedAt = new Date()
389
+ salesOrder.status = 'active'
390
+ salesOrder.placedAt = new Date()
391
+
392
+ await em.flush()
393
+
394
+ // Emit ARC events AFTER the activation transaction commits.
395
+ for (const fire of arcEvents) {
396
+ try {
397
+ await fire()
398
+ } catch (err) {
399
+ // Don't roll back the activation just because a subscriber blew up —
400
+ // events are best-effort once the DB state is committed.
401
+ console.error('[cpq.arc] event emission failed', err)
402
+ }
403
+ }
404
+
405
+ return this.buildOrderResult(em, orderConfig, salesOrder, orderLines, scope)
406
+ }
407
+
408
+ // ─── ARC activation branch (XD-250) ─────────────────────────
409
+ //
410
+ // Reads the source quote's targets + lines, then dispatches per target to
411
+ // cpqInventoryService.apply* primitives. Returns the list of post-commit
412
+ // event emissions so the caller can fire them outside the activation TX.
413
+ private async applyArcOrder(
414
+ orderConfig: CpqOrderConfiguration,
415
+ sourceQuote: CpqQuoteConfiguration,
416
+ orderLines: CpqOrderLineConfiguration[],
417
+ scope: TenantScope,
418
+ ): Promise<Array<() => Promise<void>>> {
419
+ const em = this.em
420
+ const quoteType = sourceQuote.quoteType as 'amend' | 'renew' | 'cancel'
421
+
422
+ // Load attached targets.
423
+ const targets = await em.find(CpqQuoteTargetSubscription, {
424
+ quoteId: sourceQuote.id,
425
+ organizationId: scope.organizationId,
426
+ tenantId: scope.tenantId,
427
+ deletedAt: null,
428
+ })
429
+ if (targets.length === 0) {
430
+ throw new OrderError(
431
+ 409,
432
+ `ARC quote ${sourceQuote.id} has no target subscriptions to activate`,
433
+ )
434
+ }
435
+
436
+ const inventory = this.inventoryService
437
+
438
+ // Group order lines by target_subscription_id — ARC order lines mirror the
439
+ // quote lines' ARC fields (target_subscription_id is preserved through
440
+ // convertQuoteToOrder via line.configuration when needed).
441
+ // Source quote lines are the source of truth for ARC line data because
442
+ // CpqOrderLineConfiguration doesn't carry target/source-item fields.
443
+ const sourceQuoteLines = await em.find(CpqQuoteLineConfiguration, {
444
+ quoteConfigurationId: sourceQuote.id,
445
+ organizationId: scope.organizationId,
446
+ tenantId: scope.tenantId,
447
+ deletedAt: null,
448
+ })
449
+ const linesByTarget = new Map<string | null, CpqQuoteLineConfiguration[]>()
450
+ for (const line of sourceQuoteLines) {
451
+ const key = line.targetSubscriptionId ?? null
452
+ const list = linesByTarget.get(key) ?? []
453
+ list.push(line)
454
+ linesByTarget.set(key, list)
455
+ }
456
+
457
+ // Look up specs for `add` lines so we can build itemInput correctly.
458
+ const specIds = [
459
+ ...new Set(sourceQuoteLines.map((l) => l.specId).filter((x): x is string => Boolean(x))),
460
+ ]
461
+ const specs =
462
+ specIds.length > 0
463
+ ? await em.find(CpqProductSpecification, {
464
+ id: { $in: specIds },
465
+ organizationId: scope.organizationId,
466
+ tenantId: scope.tenantId,
467
+ deletedAt: null,
468
+ })
469
+ : []
470
+ const specMap = new Map(specs.map((s) => [s.id, s]))
471
+
472
+ const events: Array<() => Promise<void>> = []
473
+ const eventBaseEnvelope = {
474
+ organizationId: scope.organizationId,
475
+ tenantId: scope.tenantId,
476
+ sourceQuoteId: sourceQuote.id,
477
+ sourceOrderId: orderConfig.id,
478
+ } as const
479
+
480
+ if (quoteType === 'amend') {
481
+ for (const target of targets) {
482
+ const lines = linesByTarget.get(target.subscriptionId) ?? []
483
+ const result = await inventory.applyAmendment(
484
+ {
485
+ subscriptionId: target.subscriptionId,
486
+ sourceQuoteId: sourceQuote.id,
487
+ sourceOrderId: orderConfig.id,
488
+ performedByUserId: null,
489
+ lineChanges: lines.map((l) => buildArcLineChange(l, specMap)),
490
+ },
491
+ scope,
492
+ )
493
+ events.push(() =>
494
+ emitArcEvent('cpq.subscription.amended', result, eventBaseEnvelope),
495
+ )
496
+ }
497
+ } else if (quoteType === 'cancel') {
498
+ for (const target of targets) {
499
+ const result = await inventory.applyCancel(
500
+ {
501
+ subscriptionId: target.subscriptionId,
502
+ sourceQuoteId: sourceQuote.id,
503
+ sourceOrderId: orderConfig.id,
504
+ performedByUserId: null,
505
+ etfAmount: sourceQuote.arcEtfAmount ?? null,
506
+ etfCurrency: sourceQuote.arcEtfCurrency ?? null,
507
+ reasonCode: sourceQuote.arcReasonCode ?? null,
508
+ reasonText: sourceQuote.arcReasonText ?? null,
509
+ },
510
+ scope,
511
+ )
512
+ events.push(() =>
513
+ emitArcEvent('cpq.subscription.cancelled', result, {
514
+ ...eventBaseEnvelope,
515
+ etfAmount: sourceQuote.arcEtfAmount ?? null,
516
+ etfCurrency: sourceQuote.arcEtfCurrency ?? null,
517
+ reasonCode: sourceQuote.arcReasonCode ?? null,
518
+ reasonText: sourceQuote.arcReasonText ?? null,
519
+ }),
520
+ )
521
+ }
522
+ } else if (quoteType === 'renew') {
523
+ const standalone = targets.filter((t) => t.mergeAction === 'standalone')
524
+ const absorb = targets.filter((t) => t.mergeAction === 'absorb')
525
+
526
+ if (standalone.length > 0 && absorb.length === 0) {
527
+ for (const target of standalone) {
528
+ if (!target.newTermStart || !target.newTermEnd) {
529
+ throw new OrderError(
530
+ 409,
531
+ `Standalone renew target ${target.id} missing newTermStart/newTermEnd`,
532
+ )
533
+ }
534
+ const lines = linesByTarget.get(target.subscriptionId) ?? []
535
+ const result = await inventory.applyRenewal(
536
+ {
537
+ subscriptionId: target.subscriptionId,
538
+ sourceQuoteId: sourceQuote.id,
539
+ sourceOrderId: orderConfig.id,
540
+ performedByUserId: null,
541
+ term: {
542
+ newTermStart: target.newTermStart,
543
+ newTermEnd: target.newTermEnd,
544
+ newTermMonths: target.newTermMonths ?? null,
545
+ },
546
+ lineChanges: lines.map((l) => buildArcLineChange(l, specMap)),
547
+ },
548
+ scope,
549
+ )
550
+ events.push(() =>
551
+ emitArcEvent('cpq.subscription.renewed', result, {
552
+ ...eventBaseEnvelope,
553
+ term: {
554
+ newTermStart: target.newTermStart!.toISOString(),
555
+ newTermEnd: target.newTermEnd!.toISOString(),
556
+ },
557
+ }),
558
+ )
559
+ }
560
+ } else if (absorb.length >= 2 && standalone.length === 0) {
561
+ if (!sourceQuote.arcMergeNewTermStart || !sourceQuote.arcMergeNewTermEnd) {
562
+ throw new OrderError(
563
+ 409,
564
+ `Merge renew quote ${sourceQuote.id} missing arc_merge_new_term_*`,
565
+ )
566
+ }
567
+ // Lines targeting null are operator-edits on the new merge sub M.
568
+ const mergeLines = linesByTarget.get(null) ?? []
569
+ const merge = await inventory.applyMergeRenewal(
570
+ {
571
+ sourceIds: absorb.map((t) => t.subscriptionId),
572
+ sourceQuoteId: sourceQuote.id,
573
+ sourceOrderId: orderConfig.id,
574
+ performedByUserId: null,
575
+ term: {
576
+ newTermStart: sourceQuote.arcMergeNewTermStart,
577
+ newTermEnd: sourceQuote.arcMergeNewTermEnd,
578
+ newTermMonths: sourceQuote.arcMergeNewTermMonths ?? null,
579
+ },
580
+ mergeMeta: {
581
+ newSubCode: sourceQuote.arcMergeNewSubCode ?? null,
582
+ newSubName: sourceQuote.arcMergeNewSubName ?? null,
583
+ },
584
+ lineChanges: mergeLines.map((l) => buildArcLineChange(l, specMap)),
585
+ },
586
+ scope,
587
+ )
588
+
589
+ // One event per source (`superseded`) + one for M (`merged`).
590
+ const mergedFromSubscriptionIds = absorb.map((t) => t.subscriptionId)
591
+ events.push(() =>
592
+ emitMergedEvent(merge, eventBaseEnvelope, mergedFromSubscriptionIds),
593
+ )
594
+ for (const sourceLog of merge.sourceChangeLogs) {
595
+ events.push(() =>
596
+ emitSupersededEvent(sourceLog, merge.mergedSubscription.id, eventBaseEnvelope),
597
+ )
598
+ }
599
+ } else {
600
+ throw new OrderError(
601
+ 409,
602
+ `Inconsistent renew targets — must be either single standalone or ≥2 absorb`,
603
+ )
604
+ }
605
+ } else {
606
+ throw new OrderError(500, `Unknown quote_type '${quoteType}' for ARC activation`)
607
+ }
608
+
609
+ return events
610
+ }
611
+
612
+ // ─── Inventory Creation ─────────────────────────────────────
613
+
614
+ private async createInventoryFromOrder(
615
+ orderConfig: CpqOrderConfiguration,
616
+ orderLines: CpqOrderLineConfiguration[],
617
+ scope: TenantScope,
618
+ ): Promise<void> {
619
+ if (orderLines.length === 0) return
620
+
621
+ const specIds = [...new Set(orderLines.map((l) => l.specId).filter(Boolean))] as string[]
622
+ const specs = specIds.length > 0
623
+ ? await this.em.find(CpqProductSpecification, {
624
+ id: { $in: specIds },
625
+ organizationId: scope.organizationId,
626
+ tenantId: scope.tenantId,
627
+ deletedAt: null,
628
+ })
629
+ : []
630
+ const specMap = new Map(specs.map((s) => [s.id, s]))
631
+
632
+ const subscriptionLines = orderLines.filter((l) => {
633
+ const spec = l.specId ? specMap.get(l.specId) : null
634
+ return !spec?.isAssetizable
635
+ })
636
+ const assetLines = orderLines.filter((l) => {
637
+ const spec = l.specId ? specMap.get(l.specId) : null
638
+ return spec?.isAssetizable === true
639
+ })
640
+
641
+ const hasSubscriptionContent = subscriptionLines.some(
642
+ (l) => Number(l.mrcTotal) > 0 || Number(l.nrcTotal) > 0,
643
+ )
644
+ if (!hasSubscriptionContent && assetLines.length === 0) return
645
+
646
+ const subscriptionName = await this.buildSubscriptionName(orderConfig, orderLines, scope)
647
+
648
+ const lineWithDates = orderLines.find((l) => l.startDate != null) ?? orderLines[0]
649
+ const lineWithTerm = orderLines.find((l) => l.termMonths != null) ?? orderLines[0]
650
+ const subscriptionItems = subscriptionLines.map((line) => ({
651
+ productId: line.productId ?? undefined,
652
+ offeringId: line.offeringId ?? undefined,
653
+ specId: line.specId ?? undefined,
654
+ name: this.getLineName(line),
655
+ configuration: line.configuration ?? {},
656
+ charges: (line.charges ?? []) as Array<Record<string, unknown>>,
657
+ mrcAmount: Number(line.mrcTotal),
658
+ nrcAmount: Number(line.nrcTotal),
659
+ currencyCode: orderConfig.currencyCode,
660
+ quantity: line.quantity,
661
+ sourceQuoteLineId: line.sourceQuoteLineId ?? undefined,
662
+ sortOrder: orderLines.indexOf(line),
663
+ }))
664
+
665
+ const subscription = await this.inventoryService.createSubscription(
666
+ {
667
+ customerId: orderConfig.customerId,
668
+ name: subscriptionName,
669
+ billingCycle: 'monthly',
670
+ currencyCode: orderConfig.currencyCode,
671
+ startDate: lineWithDates?.startDate?.toISOString?.() ?? undefined,
672
+ termMonths: lineWithTerm?.termMonths ?? undefined,
673
+ sourceQuoteId: orderConfig.sourceQuoteId ?? undefined,
674
+ sourceOrderId: orderConfig.id,
675
+ items: subscriptionItems.length > 0
676
+ ? subscriptionItems
677
+ : [{ name: subscriptionName, mrcAmount: 0, nrcAmount: 0 }],
678
+ },
679
+ scope,
680
+ )
681
+
682
+ const orderLineToSubItem = new Map<string, string>()
683
+ const allSubItems = await this.em.find(CpqInventorySubscriptionItem, {
684
+ subscriptionId: subscription.id,
685
+ organizationId: scope.organizationId,
686
+ tenantId: scope.tenantId,
687
+ deletedAt: null,
688
+ }, { orderBy: { sortOrder: 'asc' } })
689
+
690
+ for (let i = 0; i < subscriptionLines.length && i < allSubItems.length; i++) {
691
+ orderLineToSubItem.set(subscriptionLines[i].orderLineId, allSubItems[i].id)
692
+ }
693
+
694
+ for (let i = 0; i < subscriptionLines.length && i < allSubItems.length; i++) {
695
+ const orderLine = subscriptionLines[i]
696
+ if (orderLine.parentLineId) {
697
+ const parentSubItemId = orderLineToSubItem.get(orderLine.parentLineId)
698
+ if (parentSubItemId) {
699
+ allSubItems[i].parentItemId = parentSubItemId
700
+ }
701
+ }
702
+ }
703
+ await this.em.flush()
704
+
705
+ for (const line of assetLines) {
706
+ let subscriptionItemId: string | undefined
707
+ if (line.parentLineId) {
708
+ subscriptionItemId = orderLineToSubItem.get(line.parentLineId) ?? undefined
709
+ }
710
+
711
+ await this.inventoryService.createAsset(
712
+ {
713
+ customerId: orderConfig.customerId,
714
+ subscriptionId: subscription.id,
715
+ subscriptionItemId,
716
+ productId: line.productId ?? undefined,
717
+ offeringId: line.offeringId ?? undefined,
718
+ specId: line.specId ?? undefined,
719
+ name: this.getLineName(line),
720
+ assetType: 'one_time',
721
+ currencyCode: orderConfig.currencyCode,
722
+ purchasePrice: Number(line.nrcTotal),
723
+ quantity: line.quantity,
724
+ purchaseDate: new Date().toISOString().split('T')[0],
725
+ configuration: line.configuration ?? {},
726
+ charges: (line.charges ?? []) as Array<Record<string, unknown>>,
727
+ sourceOrderId: orderConfig.id,
728
+ sourceQuoteLineId: line.sourceQuoteLineId ?? undefined,
729
+ },
730
+ scope,
731
+ )
732
+ }
733
+ }
734
+
735
+ // ─── Get / List ─────────────────────────────────────────────
736
+
737
+ async getOrder(orderId: string, scope: TenantScope): Promise<OrderResult> {
738
+ const em = this.em
739
+ const orderConfig = await this.loadOrderConfig(em, orderId, scope)
740
+ const salesOrder = await this.loadSalesOrder(em, orderConfig.orderId, scope)
741
+ const lines = await em.find(CpqOrderLineConfiguration, {
742
+ orderConfigurationId: orderConfig.id,
743
+ organizationId: scope.organizationId,
744
+ tenantId: scope.tenantId,
745
+ deletedAt: null,
746
+ })
747
+ return this.buildOrderResult(em, orderConfig, salesOrder, lines, scope)
748
+ }
749
+
750
+ async listOrders(filters: OrderListFilters, scope: TenantScope) {
751
+ const page = Math.max(1, filters.page ?? 1)
752
+ const pageSize = Math.min(100, Math.max(1, filters.pageSize ?? 50))
753
+ const offset = (page - 1) * pageSize
754
+
755
+ const where: Record<string, unknown> = {
756
+ organizationId: scope.organizationId,
757
+ tenantId: scope.tenantId,
758
+ deletedAt: null,
759
+ }
760
+
761
+ if (filters.customerId) where.customerId = filters.customerId
762
+ if (filters.sourceQuoteId) where.sourceQuoteId = filters.sourceQuoteId
763
+ if (filters.salesOrderId) where.orderId = filters.salesOrderId
764
+ if (filters.currencyCode) where.currencyCode = filters.currencyCode
765
+ if (filters.cpqStatus) {
766
+ where.cpqStatus = Array.isArray(filters.cpqStatus)
767
+ ? { $in: filters.cpqStatus }
768
+ : filters.cpqStatus
769
+ }
770
+
771
+ const search = filters.search?.trim()
772
+ if (search) {
773
+ where.$or = [
774
+ { orderId: { $ilike: `%${search}%` } },
775
+ { customerId: { $ilike: `%${search}%` } },
776
+ ]
777
+ }
778
+
779
+ const sortField = filters.sortField ?? 'createdAt'
780
+ const sortDir = filters.sortDir === 'asc' ? 'asc' : 'desc'
781
+
782
+ const [configs, total] = await this.em.findAndCount(
783
+ CpqOrderConfiguration,
784
+ where,
785
+ { limit: pageSize, offset, orderBy: { [sortField]: sortDir } },
786
+ )
787
+
788
+ const orderIds = configs.map((c) => c.orderId)
789
+ const salesOrders = orderIds.length > 0
790
+ ? await this.em.find(SalesOrder, { id: { $in: orderIds } })
791
+ : []
792
+ const salesOrderMap = new Map(salesOrders.map((o) => [o.id, o]))
793
+
794
+ const items = configs.map((c) => ({
795
+ ...c,
796
+ orderNumber: salesOrderMap.get(c.orderId)?.orderNumber ?? null,
797
+ }))
798
+
799
+ return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) }
800
+ }
801
+
802
+ // ─── Status Transition ──────────────────────────────────────
803
+
804
+ async transitionOrderStatus(orderId: string, targetStatus: string, scope: TenantScope): Promise<OrderResult> {
805
+ const em = this.em
806
+ const orderConfig = await this.loadOrderConfig(em, orderId, scope)
807
+ const salesOrder = await this.loadSalesOrder(em, orderConfig.orderId, scope)
808
+
809
+ const currentStatus = orderConfig.cpqStatus as CpqOrderStatus
810
+ const allowed = CPQ_ORDER_TRANSITIONS[currentStatus] ?? []
811
+
812
+ if (!allowed.includes(targetStatus as CpqOrderStatus)) {
813
+ throw new OrderError(409, `Cannot transition from '${currentStatus}' to '${targetStatus}'`)
814
+ }
815
+
816
+ if (targetStatus === 'active') {
817
+ return this.activateOrder(orderId, scope)
818
+ }
819
+
820
+ orderConfig.cpqStatus = targetStatus
821
+ salesOrder.status = CPQ_TO_SALES_ORDER_STATUS[targetStatus as CpqOrderStatus] ?? targetStatus
822
+
823
+ await em.flush()
824
+
825
+ const lines = await em.find(CpqOrderLineConfiguration, {
826
+ orderConfigurationId: orderConfig.id,
827
+ organizationId: scope.organizationId,
828
+ tenantId: scope.tenantId,
829
+ deletedAt: null,
830
+ })
831
+ return this.buildOrderResult(em, orderConfig, salesOrder, lines, scope)
832
+ }
833
+
834
+ // ─── Helpers ────────────────────────────────────────────────
835
+
836
+ private async loadOrderConfig(em: EntityManager, orderId: string, scope: TenantScope): Promise<CpqOrderConfiguration> {
837
+ let config = await em.findOne(CpqOrderConfiguration, {
838
+ id: orderId,
839
+ organizationId: scope.organizationId,
840
+ tenantId: scope.tenantId,
841
+ deletedAt: null,
842
+ })
843
+ if (!config) {
844
+ config = await em.findOne(CpqOrderConfiguration, {
845
+ orderId,
846
+ organizationId: scope.organizationId,
847
+ tenantId: scope.tenantId,
848
+ deletedAt: null,
849
+ })
850
+ }
851
+ if (!config) throw new OrderError(404, 'Order not found')
852
+ return config
853
+ }
854
+
855
+ private async loadSalesOrder(em: EntityManager, orderId: string, scope: TenantScope): Promise<SalesOrder> {
856
+ const order = await em.findOne(SalesOrder, {
857
+ id: orderId,
858
+ organizationId: scope.organizationId,
859
+ tenantId: scope.tenantId,
860
+ })
861
+ if (!order) throw new OrderError(404, 'Sales order not found')
862
+ return order
863
+ }
864
+
865
+ private async resolveOfferingInfo(
866
+ em: EntityManager,
867
+ offeringId: string | null | undefined,
868
+ scope: TenantScope,
869
+ fallbackConfig?: Record<string, unknown> | null,
870
+ ): Promise<{ name: string; type: string | null }> {
871
+ if (offeringId) {
872
+ const offering = await em.findOne(CpqProductOffering, {
873
+ id: offeringId,
874
+ organizationId: scope.organizationId,
875
+ tenantId: scope.tenantId,
876
+ deletedAt: null,
877
+ })
878
+ if (offering) return { name: offering.name, type: offering.offeringType ?? 'simple' }
879
+ }
880
+ const cfg = fallbackConfig ?? null
881
+ const fallbackName =
882
+ (cfg?._arcMirroredName as string | undefined) ??
883
+ (cfg?.offeringName as string | undefined) ??
884
+ 'Configured Item'
885
+ return { name: fallbackName, type: null }
886
+ }
887
+
888
+ private async resolveOfferingName(
889
+ em: EntityManager,
890
+ offeringId: string | null | undefined,
891
+ scope: TenantScope,
892
+ fallbackConfig?: Record<string, unknown> | null,
893
+ ): Promise<string> {
894
+ const info = await this.resolveOfferingInfo(em, offeringId, scope, fallbackConfig)
895
+ return info.name
896
+ }
897
+
898
+ private getLineName(line: CpqOrderLineConfiguration): string {
899
+ const config = line.configuration as Record<string, unknown> | null
900
+ return (
901
+ (config?._arcMirroredName as string | undefined) ??
902
+ (config?.offeringName as string | undefined) ??
903
+ (config?.name as string | undefined) ??
904
+ 'Configured Item'
905
+ )
906
+ }
907
+
908
+ private async buildSubscriptionName(
909
+ orderConfig: CpqOrderConfiguration,
910
+ orderLines: CpqOrderLineConfiguration[],
911
+ scope: TenantScope,
912
+ ): Promise<string> {
913
+ if (orderLines.length === 1 && orderLines[0].offeringId) {
914
+ return this.resolveOfferingName(this.em, orderLines[0].offeringId, scope)
915
+ }
916
+ const names: string[] = []
917
+ for (const line of orderLines.slice(0, 3)) {
918
+ if (line.offeringId) {
919
+ names.push(await this.resolveOfferingName(this.em, line.offeringId, scope))
920
+ }
921
+ }
922
+ if (names.length > 0) {
923
+ const suffix = orderLines.length > 3 ? ` (+${orderLines.length - 3} more)` : ''
924
+ return names.join(', ') + suffix
925
+ }
926
+ return `Order ${orderConfig.orderId.slice(0, 8)}`
927
+ }
928
+
929
+ private async buildOrderResult(
930
+ em: EntityManager,
931
+ orderConfig: CpqOrderConfiguration,
932
+ salesOrder: SalesOrder,
933
+ lineConfigs: CpqOrderLineConfiguration[],
934
+ scope: TenantScope,
935
+ ): Promise<OrderResult> {
936
+ const lines: OrderLineResult[] = []
937
+
938
+ // XD-250 ARC: batch-load source subscription items so we can render the
939
+ // before/after diff per line on the order detail page.
940
+ const arcItemIds = new Set<string>()
941
+ for (const lc of lineConfigs) {
942
+ const m = (lc.configuration as Record<string, unknown> | null)?._arcMirroredFromItemId
943
+ if (typeof m === 'string') arcItemIds.add(m)
944
+ }
945
+ const arcItemMap = new Map<string, CpqInventorySubscriptionItem>()
946
+ if (arcItemIds.size > 0) {
947
+ const items = await em.find(CpqInventorySubscriptionItem, {
948
+ id: { $in: Array.from(arcItemIds) },
949
+ organizationId: scope.organizationId,
950
+ tenantId: scope.tenantId,
951
+ deletedAt: null,
952
+ })
953
+ for (const it of items) arcItemMap.set(it.id, it)
954
+ }
955
+
956
+ for (const lc of lineConfigs) {
957
+ const offeringInfo = await this.resolveOfferingInfo(
958
+ em,
959
+ lc.offeringId,
960
+ scope,
961
+ lc.configuration as Record<string, unknown> | null,
962
+ )
963
+ const arcSource = pickArcSourceFromConfig(lc.configuration as Record<string, unknown> | null, arcItemMap)
964
+ lines.push({
965
+ lineId: lc.orderLineId,
966
+ offeringId: lc.offeringId ?? null,
967
+ offeringName: offeringInfo.name,
968
+ offeringType: offeringInfo.type,
969
+ productId: lc.productId ?? null,
970
+ specId: lc.specId ?? null,
971
+ action: lc.action,
972
+ parentLineId: lc.parentLineId ?? null,
973
+ quantity: lc.quantity,
974
+ configuration: lc.configuration ?? {},
975
+ startDate: lc.startDate?.toISOString() ?? null,
976
+ termMonths: lc.termMonths ?? null,
977
+ endDate: lc.endDate?.toISOString() ?? null,
978
+ nrcTotal: Number(lc.nrcTotal),
979
+ mrcTotal: Number(lc.mrcTotal),
980
+ charges: (lc.charges ?? []) as Array<Record<string, unknown>>,
981
+ sourceQuoteLineId: lc.sourceQuoteLineId ?? null,
982
+ arcSource,
983
+ })
984
+ }
985
+
986
+ // Source quote's quoteType — surfaced to the order header so the UI can
987
+ // render an AMEND / RENEW / CANCEL badge alongside the order number.
988
+ // CpqOrderConfiguration.sourceQuoteId stores CpqQuoteConfiguration.id
989
+ // (see convertFromConfig — sourceQuoteId: cpqConfig.id), NOT the
990
+ // SalesQuote.id, so we look it up by `id` rather than by `quoteId`.
991
+ let quoteType = 'new'
992
+ if (orderConfig.sourceQuoteId) {
993
+ const sourceCfg = await em.findOne(CpqQuoteConfiguration, {
994
+ id: orderConfig.sourceQuoteId,
995
+ organizationId: scope.organizationId,
996
+ tenantId: scope.tenantId,
997
+ })
998
+ if (sourceCfg?.quoteType) quoteType = sourceCfg.quoteType
999
+ }
1000
+
1001
+ const pricing = orderConfig.pricingSummary ?? {}
1002
+
1003
+ return {
1004
+ id: orderConfig.id,
1005
+ orderId: orderConfig.orderId,
1006
+ orderNumber: salesOrder.orderNumber ?? '',
1007
+ customerId: orderConfig.customerId,
1008
+ cpqStatus: orderConfig.cpqStatus,
1009
+ sourceQuoteId: orderConfig.sourceQuoteId ?? null,
1010
+ quoteType,
1011
+ currencyCode: orderConfig.currencyCode,
1012
+ pricingSummary: {
1013
+ nrcTotal: Number((pricing as Record<string, unknown>).nrcTotal ?? 0),
1014
+ mrcTotal: Number((pricing as Record<string, unknown>).mrcTotal ?? 0),
1015
+ currencyCode: orderConfig.currencyCode,
1016
+ },
1017
+ activatedAt: orderConfig.activatedAt?.toISOString() ?? null,
1018
+ createdAt: orderConfig.createdAt.toISOString(),
1019
+ lines,
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ function pickArcSourceFromConfig(
1025
+ cfg: Record<string, unknown> | null,
1026
+ map: Map<string, CpqInventorySubscriptionItem>,
1027
+ ): OrderLineResult['arcSource'] {
1028
+ const m = cfg?._arcMirroredFromItemId
1029
+ if (typeof m !== 'string') return null
1030
+ const item = map.get(m)
1031
+ if (!item) return null
1032
+ return {
1033
+ subscriptionItemId: item.id,
1034
+ name: item.name,
1035
+ mrcAmount: Number(item.mrcAmount),
1036
+ nrcAmount: Number(item.nrcAmount),
1037
+ quantity: item.quantity,
1038
+ }
1039
+ }
1040
+
1041
+ // ─── Error class ──────────────────────────────────────────────────
1042
+
1043
+ export class OrderError extends Error {
1044
+ constructor(
1045
+ public status: number,
1046
+ message: string,
1047
+ ) {
1048
+ super(message)
1049
+ this.name = 'OrderError'
1050
+ }
1051
+ }
1052
+
1053
+ // ─── ARC helpers (XD-250) ─────────────────────────────────────────
1054
+
1055
+ /**
1056
+ * Build a service-layer ArcLineChange from a CpqQuoteLineConfiguration.
1057
+ * For 'add' actions, materializes the itemInput from line + spec data.
1058
+ */
1059
+ function buildArcLineChange(
1060
+ line: CpqQuoteLineConfiguration,
1061
+ specMap: Map<string, CpqProductSpecification>,
1062
+ ): ArcLineChange {
1063
+ const action = line.action as 'add' | 'modify' | 'cancel'
1064
+ if (action === 'add') {
1065
+ const itemInput: Omit<CreateSubscriptionItemInput, 'name'> & { name: string } = {
1066
+ productId: line.productId ?? undefined,
1067
+ offeringId: line.offeringId ?? undefined,
1068
+ specId: line.specId ?? undefined,
1069
+ name: resolveLineName(line, specMap),
1070
+ configuration: line.configuration ?? {},
1071
+ charges: (line.charges ?? null) as Array<Record<string, unknown>> | undefined,
1072
+ mrcAmount: Number(line.mrcTotal),
1073
+ nrcAmount: Number(line.nrcTotal),
1074
+ quantity: line.quantity,
1075
+ sourceQuoteLineId: line.quoteLineId,
1076
+ }
1077
+ return {
1078
+ quoteLineId: line.quoteLineId,
1079
+ action,
1080
+ itemInput,
1081
+ parentQuoteLineId: line.parentLineId ?? null,
1082
+ }
1083
+ }
1084
+ if (action === 'cancel') {
1085
+ return {
1086
+ quoteLineId: line.quoteLineId,
1087
+ action,
1088
+ sourceSubscriptionItemId: line.sourceSubscriptionItemId ?? null,
1089
+ }
1090
+ }
1091
+ // modify
1092
+ return {
1093
+ quoteLineId: line.quoteLineId,
1094
+ action,
1095
+ sourceSubscriptionItemId: line.sourceSubscriptionItemId ?? null,
1096
+ modifyPatch: {
1097
+ configuration: line.configuration ?? undefined,
1098
+ charges: (line.charges ?? undefined) as Array<Record<string, unknown>> | undefined,
1099
+ mrcAmount: Number(line.mrcTotal),
1100
+ nrcAmount: Number(line.nrcTotal),
1101
+ quantity: line.quantity,
1102
+ },
1103
+ }
1104
+ }
1105
+
1106
+ function resolveLineName(
1107
+ line: CpqQuoteLineConfiguration,
1108
+ specMap: Map<string, CpqProductSpecification>,
1109
+ ): string {
1110
+ const cfg = line.configuration as Record<string, unknown> | null
1111
+ const cfgName = (cfg?._arcMirroredName as string | undefined) ?? null
1112
+ if (cfgName) return cfgName
1113
+ if (line.specId) {
1114
+ const spec = specMap.get(line.specId)
1115
+ if (spec?.name) return spec.name
1116
+ }
1117
+ return 'Configured Item'
1118
+ }
1119
+
1120
+ interface ArcEventEnvelope {
1121
+ organizationId: string
1122
+ tenantId: string
1123
+ sourceQuoteId: string
1124
+ sourceOrderId: string
1125
+ }
1126
+
1127
+ async function emitArcEvent(
1128
+ eventId:
1129
+ | 'cpq.subscription.amended'
1130
+ | 'cpq.subscription.renewed'
1131
+ | 'cpq.subscription.cancelled',
1132
+ result: ApplyArcResult,
1133
+ envelope: ArcEventEnvelope & {
1134
+ term?: { newTermStart: string; newTermEnd: string }
1135
+ etfAmount?: string | null
1136
+ etfCurrency?: string | null
1137
+ reasonCode?: string | null
1138
+ reasonText?: string | null
1139
+ },
1140
+ ): Promise<void> {
1141
+ const sub = result.subscription
1142
+ await emitCpqEvent(eventId as never, {
1143
+ subscriptionId: sub.id,
1144
+ customerId: sub.customerId,
1145
+ organizationId: envelope.organizationId,
1146
+ tenantId: envelope.tenantId,
1147
+ timestamp: new Date().toISOString(),
1148
+ sourceQuoteId: envelope.sourceQuoteId,
1149
+ sourceOrderId: envelope.sourceOrderId,
1150
+ performedByUserId: null,
1151
+ changeLogId: result.changeLog.id,
1152
+ proration: buildProrationPayload(sub, result.changeLog.beforeSnapshot ?? null),
1153
+ ...(envelope.term ? { term: envelope.term } : {}),
1154
+ ...(envelope.etfAmount !== undefined ? { etfAmount: envelope.etfAmount } : {}),
1155
+ ...(envelope.etfCurrency !== undefined ? { etfCurrency: envelope.etfCurrency } : {}),
1156
+ ...(envelope.reasonCode !== undefined ? { reasonCode: envelope.reasonCode } : {}),
1157
+ ...(envelope.reasonText !== undefined ? { reasonText: envelope.reasonText } : {}),
1158
+ } as never)
1159
+ }
1160
+
1161
+ async function emitMergedEvent(
1162
+ merge: ApplyMergeRenewalResult,
1163
+ envelope: ArcEventEnvelope,
1164
+ mergedFromSubscriptionIds: string[],
1165
+ ): Promise<void> {
1166
+ const m = merge.mergedSubscription
1167
+ await emitCpqEvent('cpq.subscription.merged' as never, {
1168
+ subscriptionId: m.id,
1169
+ customerId: m.customerId,
1170
+ organizationId: envelope.organizationId,
1171
+ tenantId: envelope.tenantId,
1172
+ timestamp: new Date().toISOString(),
1173
+ sourceQuoteId: envelope.sourceQuoteId,
1174
+ sourceOrderId: envelope.sourceOrderId,
1175
+ performedByUserId: null,
1176
+ changeLogId: merge.mergeResultChangeLog.id,
1177
+ mergedFromSubscriptionIds,
1178
+ term: {
1179
+ newTermStart: m.currentTermStart?.toISOString() ?? null,
1180
+ newTermEnd: m.currentTermEnd?.toISOString() ?? null,
1181
+ newTermMonths: m.termMonths ?? null,
1182
+ },
1183
+ proration: buildProrationPayload(m, null),
1184
+ } as never)
1185
+ }
1186
+
1187
+ async function emitSupersededEvent(
1188
+ sourceLog: { id: string; subscriptionId: string },
1189
+ newMergeSubId: string,
1190
+ envelope: ArcEventEnvelope,
1191
+ ): Promise<void> {
1192
+ await emitCpqEvent('cpq.subscription.superseded' as never, {
1193
+ subscriptionId: sourceLog.subscriptionId,
1194
+ organizationId: envelope.organizationId,
1195
+ tenantId: envelope.tenantId,
1196
+ timestamp: new Date().toISOString(),
1197
+ sourceQuoteId: envelope.sourceQuoteId,
1198
+ sourceOrderId: envelope.sourceOrderId,
1199
+ performedByUserId: null,
1200
+ changeLogId: sourceLog.id,
1201
+ mergedIntoSubscriptionId: newMergeSubId,
1202
+ } as never)
1203
+ }
1204
+
1205
+ /**
1206
+ * Compute proration payload sent on every ARC subscription event.
1207
+ *
1208
+ * Currently a placeholder. Proration math against the live billing cycle
1209
+ * lives in the billing module — CPQ's contract is to surface the *inputs*
1210
+ * (old/new MRC, cycle bounds) on the event payload so billing can compute
1211
+ * the delta. Until billing integration exists, we expose the essentials and
1212
+ * leave precise cycle math to follow-up work.
1213
+ */
1214
+ function buildProrationPayload(
1215
+ sub: CpqInventorySubscription,
1216
+ beforeSnapshot: Record<string, unknown> | null,
1217
+ ): Record<string, unknown> {
1218
+ const oldMrc = beforeSnapshot
1219
+ ? Number((beforeSnapshot.mrcAmount as string | undefined) ?? 0)
1220
+ : 0
1221
+ return {
1222
+ oldMrcAmount: oldMrc,
1223
+ newMrcAmount: Number(sub.mrcAmount),
1224
+ currency: sub.currencyCode,
1225
+ billingCycleStart: null,
1226
+ billingCycleEnd: null,
1227
+ daysElapsedInCycle: null,
1228
+ daysRemainingInCycle: null,
1229
+ }
1230
+ }