@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,1867 @@
1
+ import { SalesQuote, SalesQuoteLine } from "@open-mercato/core/modules/sales/data/entities";
2
+ import { CustomerEntity } from "@open-mercato/core/modules/customers/data/entities";
3
+ import {
4
+ CpqQuoteConfiguration,
5
+ CpqQuoteLineConfiguration,
6
+ CpqQuoteTargetSubscription,
7
+ CpqInventorySubscription,
8
+ CpqInventorySubscriptionItem,
9
+ CpqOrderConfiguration,
10
+ CpqProductOffering,
11
+ CpqProductSpecification
12
+ } from "../data/entities.js";
13
+ import {
14
+ ALLOWED_TRANSITIONS,
15
+ TERMINAL_STATUSES,
16
+ ARC_QUOTE_TYPES,
17
+ getBaseCurrencyCode
18
+ } from "./types.js";
19
+ class DefaultCpqQuotingService {
20
+ constructor(deps) {
21
+ this.em = deps.em;
22
+ this.pricingService = deps.cpqPricingService;
23
+ this.validationService = deps.cpqValidationService;
24
+ this.productService = deps.cpqProductService;
25
+ this.numberGenerator = deps.salesDocumentNumberGenerator;
26
+ }
27
+ // ─── Create Quote ──────────────────────────────────────────────
28
+ async createQuote(input, scope) {
29
+ const em = this.em;
30
+ const customer = await em.findOne(CustomerEntity, {
31
+ id: input.customerId,
32
+ tenantId: scope.tenantId
33
+ }, { populate: ["personProfile", "companyProfile"] });
34
+ if (!customer) {
35
+ throw new QuotingError(404, "Customer not found");
36
+ }
37
+ const customerSnapshot = buildCustomerSnapshot(customer);
38
+ const quoteContext = {
39
+ ...input.quoteContext ?? {}
40
+ };
41
+ if (customer.kind) quoteContext.customer_type = customer.kind;
42
+ if (customer.companyProfile?.domain) quoteContext.customer_domain = customer.companyProfile.domain;
43
+ const currencyCode = input.currencyCode ?? await getBaseCurrencyCode(em, scope);
44
+ let salesQuote;
45
+ let salesQuoteId;
46
+ if (input.quoteId) {
47
+ const existing = await em.findOne(SalesQuote, {
48
+ id: input.quoteId,
49
+ organizationId: scope.organizationId,
50
+ tenantId: scope.tenantId
51
+ });
52
+ if (!existing) throw new QuotingError(404, "Sales quote not found");
53
+ const existingConfig = await em.findOne(CpqQuoteConfiguration, {
54
+ quoteId: input.quoteId,
55
+ organizationId: scope.organizationId,
56
+ tenantId: scope.tenantId,
57
+ deletedAt: null
58
+ });
59
+ if (existingConfig) {
60
+ const lines = await this.loadLineConfigs(em, existingConfig.id, scope);
61
+ return this.buildQuoteResult(em, existingConfig, existing, lines);
62
+ }
63
+ salesQuote = existing;
64
+ salesQuoteId = existing.id;
65
+ } else {
66
+ salesQuoteId = crypto.randomUUID();
67
+ const { number: quoteNumber } = await this.numberGenerator.generate({
68
+ kind: "quote",
69
+ organizationId: scope.organizationId,
70
+ tenantId: scope.tenantId
71
+ });
72
+ salesQuote = em.create(SalesQuote, {
73
+ id: salesQuoteId,
74
+ organizationId: scope.organizationId,
75
+ tenantId: scope.tenantId,
76
+ quoteNumber,
77
+ customerEntityId: input.customerId,
78
+ customerSnapshot,
79
+ currencyCode,
80
+ subtotalNetAmount: "0",
81
+ subtotalGrossAmount: "0",
82
+ discountTotalAmount: "0",
83
+ taxTotalAmount: "0",
84
+ grandTotalNetAmount: "0",
85
+ grandTotalGrossAmount: "0",
86
+ lineItemCount: 0,
87
+ createdAt: /* @__PURE__ */ new Date(),
88
+ updatedAt: /* @__PURE__ */ new Date()
89
+ });
90
+ em.persist(salesQuote);
91
+ }
92
+ const cpqConfig = em.create(CpqQuoteConfiguration, {
93
+ organizationId: scope.organizationId,
94
+ tenantId: scope.tenantId,
95
+ quoteId: salesQuoteId,
96
+ customerId: input.customerId,
97
+ cpqStatus: "new",
98
+ version: 1,
99
+ currencyCode,
100
+ quoteContext,
101
+ validationResult: { valid: true, errors: [] },
102
+ pricingSummary: emptyPricingSummary(currencyCode)
103
+ });
104
+ em.persist(cpqConfig);
105
+ await em.flush();
106
+ return this.buildQuoteResult(em, cpqConfig, salesQuote, []);
107
+ }
108
+ // ─── Add Quote Item ────────────────────────────────────────────
109
+ async addQuoteItem(quoteId, item, scope) {
110
+ const em = this.em;
111
+ const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
112
+ const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
113
+ if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
114
+ throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`);
115
+ }
116
+ const { offering, spec } = await this.resolveOffering(em, item, scope);
117
+ const mergedConfig = {
118
+ ...cpqConfig.quoteContext ?? {},
119
+ ...offering?.designTimeValues ?? {},
120
+ ...item.configuration
121
+ };
122
+ if (item.termMonths) {
123
+ mergedConfig.term_months = item.termMonths;
124
+ }
125
+ let validationErrors = [];
126
+ if (offering) {
127
+ const validation = await this.validationService.validateOfferingConfiguration({
128
+ offeringId: offering.id,
129
+ configuration: item.configuration,
130
+ scope
131
+ });
132
+ validationErrors = validation.errors;
133
+ }
134
+ const charges = await this.pricingService.resolveProductCharges({
135
+ offeringId: offering?.id,
136
+ productId: item.productId ?? spec?.productId,
137
+ configuration: mergedConfig,
138
+ currencyCode: cpqConfig.currencyCode,
139
+ ...scope
140
+ });
141
+ const quantity = item.quantity ?? 1;
142
+ const { nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated } = computeLineTotals(charges, quantity, item.usageEstimates);
143
+ const isBundleOffering = offering?.offeringType === "bundle";
144
+ const isConfigured = validationErrors.length === 0 && (charges.length > 0 || isBundleOffering);
145
+ const endDate = computeEndDate(item.startDate, item.termMonths, item.endDate);
146
+ const existingLines = await em.find(SalesQuoteLine, {
147
+ quote: salesQuote,
148
+ organizationId: scope.organizationId,
149
+ tenantId: scope.tenantId,
150
+ deletedAt: null
151
+ });
152
+ const lineNumber = existingLines.length + 1;
153
+ const perInstanceNrc = charges.filter((c) => c.chargeType === "nrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
154
+ const perInstanceMrc = charges.filter((c) => c.chargeType === "mrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
155
+ const unitPriceNet = perInstanceNrc + perInstanceMrc;
156
+ const totalNetAmount = unitPriceNet * quantity;
157
+ const quoteLineId = crypto.randomUUID();
158
+ const quoteLine = em.create(SalesQuoteLine, {
159
+ id: quoteLineId,
160
+ quote: salesQuote,
161
+ organizationId: scope.organizationId,
162
+ tenantId: scope.tenantId,
163
+ lineNumber,
164
+ kind: "product",
165
+ productId: spec?.productId ?? item.productId ?? null,
166
+ name: offering?.name ?? spec?.name ?? "Configured Item",
167
+ quantity: String(quantity),
168
+ unitPriceNet: String(unitPriceNet),
169
+ unitPriceGross: String(unitPriceNet),
170
+ totalNetAmount: String(totalNetAmount),
171
+ totalGrossAmount: String(totalNetAmount),
172
+ discountAmount: "0",
173
+ discountPercent: "0",
174
+ taxRate: "0",
175
+ taxAmount: "0",
176
+ currencyCode: cpqConfig.currencyCode,
177
+ configuration: {
178
+ offeringId: offering?.id ?? null,
179
+ offeringCode: offering?.code ?? null,
180
+ action: item.action ?? "add",
181
+ nrcTotal: Number(nrcTotal),
182
+ mrcTotal: Number(mrcTotal),
183
+ parentLineId: item.parentLineId ?? null
184
+ },
185
+ normalizedQuantity: String(quantity),
186
+ createdAt: /* @__PURE__ */ new Date(),
187
+ updatedAt: /* @__PURE__ */ new Date()
188
+ });
189
+ em.persist(quoteLine);
190
+ const arcTargetSubscriptionId = await resolveArcLineTargetSubscriptionId(
191
+ em,
192
+ cpqConfig,
193
+ scope,
194
+ item.targetSubscriptionId ?? void 0
195
+ );
196
+ const lineConfig = em.create(CpqQuoteLineConfiguration, {
197
+ organizationId: scope.organizationId,
198
+ tenantId: scope.tenantId,
199
+ quoteLineId: quoteLine.id,
200
+ quoteConfigurationId: cpqConfig.id,
201
+ offeringId: offering?.id ?? null,
202
+ productId: spec?.productId ?? item.productId ?? null,
203
+ specId: spec?.id ?? null,
204
+ action: item.action ?? "add",
205
+ parentLineId: item.parentLineId ?? null,
206
+ startDate: item.startDate ? new Date(item.startDate) : null,
207
+ termMonths: item.termMonths ?? null,
208
+ endDate: endDate ? new Date(endDate) : null,
209
+ configuration: item.configuration,
210
+ quantity,
211
+ charges,
212
+ nrcTotal: String(nrcTotal),
213
+ mrcTotal: String(mrcTotal),
214
+ usageEstimates: usageEstimates ?? null,
215
+ usageTotalEstimated: usageTotalEstimated != null ? String(usageTotalEstimated) : null,
216
+ validationErrors: validationErrors.length > 0 ? validationErrors : null,
217
+ isConfigured,
218
+ targetSubscriptionId: arcTargetSubscriptionId
219
+ });
220
+ em.persist(lineConfig);
221
+ await em.flush();
222
+ return this.recalculateInternal(em, cpqConfig, salesQuote, scope, true);
223
+ }
224
+ // ─── Update Quote Item ─────────────────────────────────────────
225
+ async updateQuoteItem(quoteId, lineId, update, scope) {
226
+ const em = this.em;
227
+ const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
228
+ const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
229
+ if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
230
+ throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`);
231
+ }
232
+ const lineConfig = await em.findOne(CpqQuoteLineConfiguration, {
233
+ quoteLineId: lineId,
234
+ quoteConfigurationId: cpqConfig.id,
235
+ organizationId: scope.organizationId,
236
+ tenantId: scope.tenantId,
237
+ deletedAt: null
238
+ });
239
+ if (!lineConfig) throw new QuotingError(404, "Quote line not found");
240
+ const quoteLine = await em.findOne(SalesQuoteLine, {
241
+ id: lineId,
242
+ organizationId: scope.organizationId,
243
+ tenantId: scope.tenantId,
244
+ deletedAt: null
245
+ });
246
+ if (!quoteLine) throw new QuotingError(404, "Quote line not found");
247
+ let offering = null;
248
+ if (lineConfig.offeringId) {
249
+ offering = await em.findOne(CpqProductOffering, {
250
+ id: lineConfig.offeringId,
251
+ organizationId: scope.organizationId,
252
+ tenantId: scope.tenantId,
253
+ deletedAt: null
254
+ });
255
+ }
256
+ const mergedConfig = {
257
+ ...cpqConfig.quoteContext ?? {},
258
+ ...offering?.designTimeValues ?? {},
259
+ ...update.configuration
260
+ };
261
+ if (update.termMonths) {
262
+ mergedConfig.term_months = update.termMonths;
263
+ }
264
+ let validationErrors = [];
265
+ if (lineConfig.offeringId) {
266
+ const validation = await this.validationService.validateOfferingConfiguration({
267
+ offeringId: lineConfig.offeringId,
268
+ configuration: update.configuration,
269
+ scope
270
+ });
271
+ validationErrors = validation.errors;
272
+ }
273
+ const existingCfg = lineConfig.configuration;
274
+ const arcMarkers = {};
275
+ if (existingCfg?._arcMirroredFromItemId !== void 0) {
276
+ arcMarkers._arcMirroredFromItemId = existingCfg._arcMirroredFromItemId;
277
+ }
278
+ if (existingCfg?._arcMirroredName !== void 0) {
279
+ arcMarkers._arcMirroredName = existingCfg._arcMirroredName;
280
+ }
281
+ update.configuration = { ...update.configuration, ...arcMarkers };
282
+ mergedConfig._arcMirroredFromItemId = arcMarkers._arcMirroredFromItemId;
283
+ mergedConfig._arcMirroredName = arcMarkers._arcMirroredName;
284
+ const isArcMirrored = !!arcMarkers._arcMirroredFromItemId;
285
+ let charges;
286
+ if (lineConfig.offeringId || lineConfig.productId) {
287
+ charges = await this.pricingService.resolveProductCharges({
288
+ offeringId: lineConfig.offeringId ?? void 0,
289
+ productId: lineConfig.productId ?? void 0,
290
+ configuration: mergedConfig,
291
+ currencyCode: cpqConfig.currencyCode,
292
+ ...scope
293
+ });
294
+ } else {
295
+ charges = lineConfig.charges ?? [];
296
+ }
297
+ const quantity = update.quantity ?? lineConfig.quantity;
298
+ const { nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated } = computeLineTotals(charges, quantity, update.usageEstimates);
299
+ const isBundleOffering = offering?.offeringType === "bundle";
300
+ const isConfigured = validationErrors.length === 0 && (charges.length > 0 || isBundleOffering || isArcMirrored);
301
+ const endDate = computeEndDate(update.startDate ?? lineConfig.startDate?.toISOString(), update.termMonths ?? lineConfig.termMonths ?? void 0, update.endDate);
302
+ em.assign(lineConfig, {
303
+ configuration: update.configuration,
304
+ quantity,
305
+ charges,
306
+ nrcTotal: String(nrcTotal),
307
+ mrcTotal: String(mrcTotal),
308
+ usageEstimates: usageEstimates ?? null,
309
+ usageTotalEstimated: usageTotalEstimated != null ? String(usageTotalEstimated) : null,
310
+ validationErrors: validationErrors.length > 0 ? validationErrors : null,
311
+ isConfigured,
312
+ startDate: update.startDate ? new Date(update.startDate) : lineConfig.startDate,
313
+ termMonths: update.termMonths ?? lineConfig.termMonths,
314
+ endDate: endDate ? new Date(endDate) : lineConfig.endDate
315
+ });
316
+ const perInstanceNrc = charges.filter((c) => c.chargeType === "nrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
317
+ const perInstanceMrc = charges.filter((c) => c.chargeType === "mrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
318
+ const unitPriceNet = perInstanceNrc + perInstanceMrc;
319
+ em.assign(quoteLine, {
320
+ quantity: String(quantity),
321
+ unitPriceNet: String(unitPriceNet),
322
+ unitPriceGross: String(unitPriceNet),
323
+ totalNetAmount: String(unitPriceNet * quantity),
324
+ totalGrossAmount: String(unitPriceNet * quantity),
325
+ updatedAt: /* @__PURE__ */ new Date()
326
+ });
327
+ await em.flush();
328
+ return this.recalculateInternal(em, cpqConfig, salesQuote, scope, true);
329
+ }
330
+ // ─── Remove Quote Item ─────────────────────────────────────────
331
+ async removeQuoteItem(quoteId, lineId, scope) {
332
+ const em = this.em;
333
+ const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
334
+ const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
335
+ if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus)) {
336
+ throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`);
337
+ }
338
+ const lineConfig = await em.findOne(CpqQuoteLineConfiguration, {
339
+ quoteLineId: lineId,
340
+ quoteConfigurationId: cpqConfig.id,
341
+ organizationId: scope.organizationId,
342
+ tenantId: scope.tenantId,
343
+ deletedAt: null
344
+ });
345
+ if (!lineConfig) throw new QuotingError(404, "Quote line not found");
346
+ const now = /* @__PURE__ */ new Date();
347
+ lineConfig.deletedAt = now;
348
+ const children = await em.find(CpqQuoteLineConfiguration, {
349
+ parentLineId: lineId,
350
+ quoteConfigurationId: cpqConfig.id,
351
+ organizationId: scope.organizationId,
352
+ tenantId: scope.tenantId,
353
+ deletedAt: null
354
+ });
355
+ for (const child of children) {
356
+ child.deletedAt = now;
357
+ const childSalesLine = await em.findOne(SalesQuoteLine, {
358
+ id: child.quoteLineId,
359
+ organizationId: scope.organizationId,
360
+ tenantId: scope.tenantId
361
+ });
362
+ if (childSalesLine) childSalesLine.deletedAt = now;
363
+ }
364
+ const salesLine = await em.findOne(SalesQuoteLine, {
365
+ id: lineId,
366
+ organizationId: scope.organizationId,
367
+ tenantId: scope.tenantId
368
+ });
369
+ if (salesLine) salesLine.deletedAt = now;
370
+ await em.flush();
371
+ return this.recalculateInternal(em, cpqConfig, salesQuote, scope, true);
372
+ }
373
+ // ─── Get Quote (read from DB, no recalculation) ────────────────
374
+ async getQuote(quoteId, scope) {
375
+ const em = this.em;
376
+ const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
377
+ const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
378
+ const lineConfigs = await em.find(CpqQuoteLineConfiguration, {
379
+ quoteConfigurationId: cpqConfig.id,
380
+ organizationId: scope.organizationId,
381
+ tenantId: scope.tenantId,
382
+ deletedAt: null
383
+ });
384
+ return this.buildQuoteResult(em, cpqConfig, salesQuote, lineConfigs);
385
+ }
386
+ // ─── Recalculate ───────────────────────────────────────────────
387
+ async recalculate(quoteId, options, scope) {
388
+ const em = options.save ? this.em.fork() : this.em;
389
+ const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
390
+ const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
391
+ return this.recalculateInternal(em, cpqConfig, salesQuote, scope, options.save);
392
+ }
393
+ // ─── Clone Quote ───────────────────────────────────────────────
394
+ async cloneQuote(quoteId, scope) {
395
+ const em = this.em;
396
+ const original = await this.loadCpqConfig(em, quoteId, scope);
397
+ const originalQuote = await this.loadSalesQuote(em, original.quoteId, scope);
398
+ const { number: quoteNumber } = await this.numberGenerator.generate({
399
+ kind: "quote",
400
+ organizationId: scope.organizationId,
401
+ tenantId: scope.tenantId
402
+ });
403
+ const newSalesQuoteId = crypto.randomUUID();
404
+ const newSalesQuote = em.create(SalesQuote, {
405
+ id: newSalesQuoteId,
406
+ organizationId: scope.organizationId,
407
+ tenantId: scope.tenantId,
408
+ quoteNumber,
409
+ customerEntityId: originalQuote.customerEntityId,
410
+ customerSnapshot: originalQuote.customerSnapshot,
411
+ currencyCode: original.currencyCode,
412
+ subtotalNetAmount: "0",
413
+ subtotalGrossAmount: "0",
414
+ discountTotalAmount: "0",
415
+ taxTotalAmount: "0",
416
+ grandTotalNetAmount: "0",
417
+ grandTotalGrossAmount: "0",
418
+ lineItemCount: 0,
419
+ createdAt: /* @__PURE__ */ new Date(),
420
+ updatedAt: /* @__PURE__ */ new Date()
421
+ });
422
+ em.persist(newSalesQuote);
423
+ const newCpqConfig = em.create(CpqQuoteConfiguration, {
424
+ organizationId: scope.organizationId,
425
+ tenantId: scope.tenantId,
426
+ quoteId: newSalesQuoteId,
427
+ customerId: original.customerId,
428
+ cpqStatus: "new",
429
+ version: original.version + 1,
430
+ parentQuoteId: original.id,
431
+ currencyCode: original.currencyCode,
432
+ quoteContext: original.quoteContext ? { ...original.quoteContext } : null,
433
+ validationResult: { valid: true, errors: [] },
434
+ pricingSummary: emptyPricingSummary(original.currencyCode)
435
+ });
436
+ em.persist(newCpqConfig);
437
+ const originalLines = await em.find(CpqQuoteLineConfiguration, {
438
+ quoteConfigurationId: original.id,
439
+ organizationId: scope.organizationId,
440
+ tenantId: scope.tenantId,
441
+ deletedAt: null
442
+ });
443
+ const lineIdMap = /* @__PURE__ */ new Map();
444
+ for (const origLine of originalLines) {
445
+ const origSalesLine = await em.findOne(SalesQuoteLine, {
446
+ id: origLine.quoteLineId,
447
+ organizationId: scope.organizationId,
448
+ tenantId: scope.tenantId
449
+ });
450
+ const newSalesLineId = crypto.randomUUID();
451
+ const newSalesLine = em.create(SalesQuoteLine, {
452
+ id: newSalesLineId,
453
+ quote: newSalesQuote,
454
+ organizationId: scope.organizationId,
455
+ tenantId: scope.tenantId,
456
+ lineNumber: origSalesLine?.lineNumber ?? 1,
457
+ kind: origSalesLine?.kind ?? "product",
458
+ productId: origSalesLine?.productId ?? null,
459
+ name: origSalesLine?.name ?? null,
460
+ quantity: origSalesLine?.quantity ?? "1",
461
+ unitPriceNet: origSalesLine?.unitPriceNet ?? "0",
462
+ unitPriceGross: origSalesLine?.unitPriceGross ?? "0",
463
+ totalNetAmount: origSalesLine?.totalNetAmount ?? "0",
464
+ totalGrossAmount: origSalesLine?.totalGrossAmount ?? "0",
465
+ discountAmount: origSalesLine?.discountAmount ?? "0",
466
+ discountPercent: origSalesLine?.discountPercent ?? "0",
467
+ taxRate: origSalesLine?.taxRate ?? "0",
468
+ taxAmount: origSalesLine?.taxAmount ?? "0",
469
+ currencyCode: origSalesLine?.currencyCode ?? original.currencyCode,
470
+ configuration: origSalesLine?.configuration ?? null,
471
+ normalizedQuantity: origSalesLine?.normalizedQuantity ?? "0",
472
+ createdAt: /* @__PURE__ */ new Date(),
473
+ updatedAt: /* @__PURE__ */ new Date()
474
+ });
475
+ em.persist(newSalesLine);
476
+ lineIdMap.set(origLine.quoteLineId, newSalesLine.id);
477
+ const newLineConfig = em.create(CpqQuoteLineConfiguration, {
478
+ organizationId: scope.organizationId,
479
+ tenantId: scope.tenantId,
480
+ quoteLineId: newSalesLine.id,
481
+ quoteConfigurationId: newCpqConfig.id,
482
+ offeringId: origLine.offeringId,
483
+ productId: origLine.productId,
484
+ specId: origLine.specId,
485
+ action: origLine.action,
486
+ parentLineId: null,
487
+ // Will be resolved after all lines are created
488
+ startDate: origLine.startDate,
489
+ termMonths: origLine.termMonths,
490
+ endDate: origLine.endDate,
491
+ configuration: { ...origLine.configuration },
492
+ quantity: origLine.quantity,
493
+ charges: origLine.charges ? [...origLine.charges] : null,
494
+ nrcTotal: origLine.nrcTotal,
495
+ mrcTotal: origLine.mrcTotal,
496
+ usageEstimates: origLine.usageEstimates,
497
+ usageTotalEstimated: origLine.usageTotalEstimated,
498
+ validationErrors: origLine.validationErrors,
499
+ isConfigured: origLine.isConfigured
500
+ });
501
+ em.persist(newLineConfig);
502
+ }
503
+ const newLines = await em.find(CpqQuoteLineConfiguration, {
504
+ quoteConfigurationId: newCpqConfig.id,
505
+ organizationId: scope.organizationId,
506
+ tenantId: scope.tenantId
507
+ });
508
+ for (const origLine of originalLines) {
509
+ if (origLine.parentLineId) {
510
+ const newParentLineId = lineIdMap.get(origLine.parentLineId);
511
+ const newLine = newLines.find((l) => l.quoteLineId === lineIdMap.get(origLine.quoteLineId));
512
+ if (newLine && newParentLineId) {
513
+ newLine.parentLineId = newParentLineId;
514
+ }
515
+ }
516
+ }
517
+ await em.flush();
518
+ return this.recalculateInternal(em, newCpqConfig, newSalesQuote, scope, true);
519
+ }
520
+ // ─── Transition Status ─────────────────────────────────────────
521
+ async transitionStatus(quoteId, targetStatus, scope) {
522
+ const em = this.em;
523
+ const cpqConfig = await this.loadCpqConfig(em, quoteId, scope);
524
+ const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope);
525
+ const currentStatus = cpqConfig.cpqStatus;
526
+ const allowed = ALLOWED_TRANSITIONS[currentStatus] ?? [];
527
+ if (!allowed.includes(targetStatus)) {
528
+ throw new QuotingError(409, `Cannot transition from '${currentStatus}' to '${targetStatus}'`);
529
+ }
530
+ const isSubmitForApproval = targetStatus === "in_approval" || targetStatus === "pre_approved" || targetStatus === "with_customer";
531
+ const quoteType = cpqConfig.quoteType ?? "new";
532
+ if (isSubmitForApproval && quoteType !== "new") {
533
+ const validation = await this.validateArcQuote(quoteId, scope);
534
+ if (!validation.ok) {
535
+ throw new QuotingError(
536
+ 422,
537
+ `ARC quote validation failed: ${validation.errors.join("; ")}`
538
+ );
539
+ }
540
+ }
541
+ cpqConfig.cpqStatus = targetStatus;
542
+ await em.flush();
543
+ const lines = await this.loadLineConfigs(em, cpqConfig.id, scope);
544
+ return this.buildQuoteResult(em, cpqConfig, salesQuote, lines);
545
+ }
546
+ // ─── Internal: Recalculate ─────────────────────────────────────
547
+ async recalculateInternal(em, cpqConfig, salesQuote, scope, save) {
548
+ const lineConfigs = await em.find(CpqQuoteLineConfiguration, {
549
+ quoteConfigurationId: cpqConfig.id,
550
+ organizationId: scope.organizationId,
551
+ tenantId: scope.tenantId,
552
+ deletedAt: null
553
+ });
554
+ let allValid = true;
555
+ const allErrors = [];
556
+ const computedLines = [];
557
+ for (const lineConfig of lineConfigs) {
558
+ let offering = null;
559
+ if (lineConfig.offeringId) {
560
+ offering = await em.findOne(CpqProductOffering, {
561
+ id: lineConfig.offeringId,
562
+ organizationId: scope.organizationId,
563
+ tenantId: scope.tenantId,
564
+ deletedAt: null
565
+ });
566
+ }
567
+ const mergedConfig = {
568
+ ...cpqConfig.quoteContext ?? {},
569
+ ...offering?.designTimeValues ?? {},
570
+ ...lineConfig.configuration
571
+ };
572
+ if (lineConfig.termMonths) {
573
+ mergedConfig.term_months = lineConfig.termMonths;
574
+ }
575
+ let validationErrors = [];
576
+ if (lineConfig.offeringId) {
577
+ const validation = await this.validationService.validateOfferingConfiguration({
578
+ offeringId: lineConfig.offeringId,
579
+ configuration: lineConfig.configuration,
580
+ scope
581
+ });
582
+ validationErrors = validation.errors;
583
+ }
584
+ const cfgRecord = lineConfig.configuration;
585
+ const isArcMirrored = !!cfgRecord?._arcMirroredFromItemId;
586
+ let charges;
587
+ if (lineConfig.offeringId || lineConfig.productId) {
588
+ charges = await this.pricingService.resolveProductCharges({
589
+ offeringId: lineConfig.offeringId ?? void 0,
590
+ productId: lineConfig.productId ?? void 0,
591
+ configuration: mergedConfig,
592
+ currencyCode: cpqConfig.currencyCode,
593
+ ...scope
594
+ });
595
+ } else {
596
+ charges = lineConfig.charges ?? [];
597
+ }
598
+ const { nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated } = computeLineTotals(
599
+ charges,
600
+ lineConfig.quantity,
601
+ lineConfig.usageEstimates?.map((e) => ({ chargeCode: e.chargeCode, estimatedQuantity: e.estimatedQuantity }))
602
+ );
603
+ const isBundleOffering = offering?.offeringType === "bundle";
604
+ const isConfigured = validationErrors.length === 0 && (charges.length > 0 || isBundleOffering || isArcMirrored);
605
+ if (!isConfigured) allValid = false;
606
+ allErrors.push(...validationErrors);
607
+ computedLines.push({ lineConfig, charges, nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated, validationErrors, isConfigured });
608
+ if (save) {
609
+ em.assign(lineConfig, {
610
+ charges,
611
+ nrcTotal: String(nrcTotal),
612
+ mrcTotal: String(mrcTotal),
613
+ usageEstimates: usageEstimates ?? null,
614
+ usageTotalEstimated: usageTotalEstimated != null ? String(usageTotalEstimated) : null,
615
+ validationErrors: validationErrors.length > 0 ? validationErrors : null,
616
+ isConfigured
617
+ });
618
+ const salesLine = await em.findOne(SalesQuoteLine, {
619
+ id: lineConfig.quoteLineId,
620
+ organizationId: scope.organizationId,
621
+ tenantId: scope.tenantId
622
+ });
623
+ if (salesLine) {
624
+ const perInstanceNrc = charges.filter((c) => c.chargeType === "nrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
625
+ const perInstanceMrc = charges.filter((c) => c.chargeType === "mrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
626
+ const unitPriceNet = perInstanceNrc + perInstanceMrc;
627
+ em.assign(salesLine, {
628
+ quantity: String(lineConfig.quantity),
629
+ unitPriceNet: String(unitPriceNet),
630
+ unitPriceGross: String(unitPriceNet),
631
+ totalNetAmount: String(unitPriceNet * lineConfig.quantity),
632
+ totalGrossAmount: String(unitPriceNet * lineConfig.quantity),
633
+ updatedAt: /* @__PURE__ */ new Date()
634
+ });
635
+ }
636
+ }
637
+ }
638
+ const relationshipErrors = await this.validationService.validateRelationships({
639
+ lines: lineConfigs.map((l) => ({
640
+ lineId: l.quoteLineId,
641
+ specId: l.specId ?? null,
642
+ parentLineId: l.parentLineId ?? null
643
+ })),
644
+ scope
645
+ });
646
+ allErrors.push(...relationshipErrors);
647
+ if (relationshipErrors.length > 0) allValid = false;
648
+ let cpqStatus;
649
+ if (lineConfigs.length === 0) {
650
+ cpqStatus = "new";
651
+ } else if (!allValid) {
652
+ cpqStatus = "incomplete";
653
+ } else {
654
+ cpqStatus = "ready";
655
+ }
656
+ const autoManagedStatuses = ["new", "incomplete", "ready"];
657
+ if (autoManagedStatuses.includes(cpqConfig.cpqStatus)) {
658
+ cpqConfig.cpqStatus = cpqStatus;
659
+ }
660
+ const pricingSummary = computePricingSummaryFromComputed(computedLines, cpqConfig.currencyCode);
661
+ const validationResult = { valid: allValid, errors: allErrors };
662
+ if (save) {
663
+ cpqConfig.validationResult = validationResult;
664
+ cpqConfig.pricingSummary = pricingSummary;
665
+ const grandTotal = pricingSummary.nrcTotal + pricingSummary.mrcTotal;
666
+ em.assign(salesQuote, {
667
+ subtotalNetAmount: String(grandTotal),
668
+ subtotalGrossAmount: String(grandTotal),
669
+ grandTotalNetAmount: String(grandTotal),
670
+ grandTotalGrossAmount: String(grandTotal),
671
+ lineItemCount: lineConfigs.length,
672
+ updatedAt: /* @__PURE__ */ new Date()
673
+ });
674
+ await em.flush();
675
+ }
676
+ return this.buildQuoteResultFromComputed(em, cpqConfig, salesQuote, computedLines, validationResult, pricingSummary);
677
+ }
678
+ // ─── Helpers ───────────────────────────────────────────────────
679
+ async loadCpqConfig(em, quoteId, scope) {
680
+ let config = await em.findOne(CpqQuoteConfiguration, {
681
+ id: quoteId,
682
+ organizationId: scope.organizationId,
683
+ tenantId: scope.tenantId,
684
+ deletedAt: null
685
+ });
686
+ if (!config) {
687
+ config = await em.findOne(CpqQuoteConfiguration, {
688
+ quoteId,
689
+ organizationId: scope.organizationId,
690
+ tenantId: scope.tenantId,
691
+ deletedAt: null
692
+ });
693
+ }
694
+ if (!config) throw new QuotingError(404, "Quote not found");
695
+ return config;
696
+ }
697
+ async loadSalesQuote(em, quoteId, scope) {
698
+ const quote = await em.findOne(SalesQuote, {
699
+ id: quoteId,
700
+ organizationId: scope.organizationId,
701
+ tenantId: scope.tenantId
702
+ });
703
+ if (!quote) throw new QuotingError(404, "Sales quote not found");
704
+ return quote;
705
+ }
706
+ async loadLineConfigs(em, cpqConfigId, scope) {
707
+ return em.find(CpqQuoteLineConfiguration, {
708
+ quoteConfigurationId: cpqConfigId,
709
+ organizationId: scope.organizationId,
710
+ tenantId: scope.tenantId,
711
+ deletedAt: null
712
+ });
713
+ }
714
+ async resolveOffering(em, item, scope) {
715
+ const dbScope = { organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null };
716
+ if (item.offeringId) {
717
+ const offering = await em.findOne(CpqProductOffering, { id: item.offeringId, ...dbScope });
718
+ if (!offering) throw new QuotingError(404, `Offering ${item.offeringId} not found`);
719
+ const spec = await em.findOne(CpqProductSpecification, { id: offering.specId, ...dbScope });
720
+ return { offering, spec };
721
+ }
722
+ if (item.productId) {
723
+ const spec = await em.findOne(CpqProductSpecification, { productId: item.productId, ...dbScope });
724
+ return { offering: null, spec };
725
+ }
726
+ throw new QuotingError(400, "Either offeringId or productId is required");
727
+ }
728
+ async buildQuoteResult(em, cpqConfig, salesQuote, lineConfigs, validationResult, pricingSummary) {
729
+ const lines = [];
730
+ const arcSourceMap = await loadArcSourceMap(em, lineConfigs, cpqConfig);
731
+ for (const lc of lineConfigs) {
732
+ let offeringName = null;
733
+ let offeringType = null;
734
+ if (lc.offeringId) {
735
+ const offering = await em.findOne(CpqProductOffering, {
736
+ id: lc.offeringId,
737
+ organizationId: cpqConfig.organizationId,
738
+ tenantId: cpqConfig.tenantId
739
+ });
740
+ if (offering) {
741
+ offeringName = offering.name;
742
+ offeringType = offering.offeringType ?? "simple";
743
+ }
744
+ }
745
+ if (!offeringName) {
746
+ const cfg = lc.configuration;
747
+ offeringName = cfg?._arcMirroredName ?? cfg?.offeringName ?? "Configured Item";
748
+ }
749
+ lines.push({
750
+ lineId: lc.quoteLineId,
751
+ offeringId: lc.offeringId ?? null,
752
+ offeringName,
753
+ offeringType,
754
+ productId: lc.productId ?? null,
755
+ action: lc.action,
756
+ parentLineId: lc.parentLineId ?? null,
757
+ quantity: lc.quantity,
758
+ configuration: lc.configuration ?? {},
759
+ startDate: lc.startDate?.toISOString() ?? null,
760
+ termMonths: lc.termMonths ?? null,
761
+ endDate: lc.endDate?.toISOString() ?? null,
762
+ nrcTotal: Number(lc.nrcTotal),
763
+ mrcTotal: Number(lc.mrcTotal),
764
+ usageEstimates: lc.usageEstimates ?? null,
765
+ usageTotalEstimated: lc.usageTotalEstimated != null ? Number(lc.usageTotalEstimated) : null,
766
+ charges: lc.charges ?? [],
767
+ isConfigured: lc.isConfigured,
768
+ validationErrors: lc.validationErrors ?? null,
769
+ arcSource: pickArcSource(lc, arcSourceMap),
770
+ targetSubscriptionId: lc.targetSubscriptionId ?? null
771
+ });
772
+ }
773
+ const finalValidation = validationResult ?? cpqConfig.validationResult ?? { valid: true, errors: [] };
774
+ const finalPricing = pricingSummary ?? computePricingSummary(lineConfigs, cpqConfig.currencyCode);
775
+ const { customerName, convertedOrderId } = await resolveQuoteSideData(em, cpqConfig);
776
+ return {
777
+ id: cpqConfig.id,
778
+ quoteId: cpqConfig.quoteId,
779
+ quoteNumber: salesQuote.quoteNumber ?? "",
780
+ customerId: cpqConfig.customerId,
781
+ customerName,
782
+ cpqStatus: cpqConfig.cpqStatus,
783
+ version: cpqConfig.version,
784
+ parentQuoteId: cpqConfig.parentQuoteId ?? null,
785
+ currencyCode: cpqConfig.currencyCode,
786
+ convertedOrderId,
787
+ validationResult: finalValidation,
788
+ pricingSummary: finalPricing,
789
+ lines
790
+ };
791
+ }
792
+ /**
793
+ * Build quote result from freshly computed line data instead of (potentially stale) entity fields.
794
+ * Used by recalculateInternal to ensure both save:true and save:false return current prices.
795
+ */
796
+ async buildQuoteResultFromComputed(em, cpqConfig, salesQuote, computedLines, validationResult, pricingSummary) {
797
+ const lines = [];
798
+ const arcSourceMap = await loadArcSourceMap(
799
+ em,
800
+ computedLines.map((c) => c.lineConfig),
801
+ cpqConfig
802
+ );
803
+ for (const { lineConfig: lc, charges, nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated, validationErrors, isConfigured } of computedLines) {
804
+ let offeringName = null;
805
+ let offeringType = null;
806
+ if (lc.offeringId) {
807
+ const offering = await em.findOne(CpqProductOffering, {
808
+ id: lc.offeringId,
809
+ organizationId: cpqConfig.organizationId,
810
+ tenantId: cpqConfig.tenantId
811
+ });
812
+ if (offering) {
813
+ offeringName = offering.name;
814
+ offeringType = offering.offeringType ?? "simple";
815
+ }
816
+ }
817
+ if (!offeringName) {
818
+ const cfg = lc.configuration;
819
+ offeringName = cfg?._arcMirroredName ?? cfg?.offeringName ?? "Configured Item";
820
+ }
821
+ lines.push({
822
+ lineId: lc.quoteLineId,
823
+ offeringId: lc.offeringId ?? null,
824
+ offeringName,
825
+ offeringType,
826
+ productId: lc.productId ?? null,
827
+ action: lc.action,
828
+ parentLineId: lc.parentLineId ?? null,
829
+ quantity: lc.quantity,
830
+ configuration: lc.configuration ?? {},
831
+ startDate: lc.startDate?.toISOString() ?? null,
832
+ termMonths: lc.termMonths ?? null,
833
+ endDate: lc.endDate?.toISOString() ?? null,
834
+ nrcTotal,
835
+ mrcTotal,
836
+ usageEstimates,
837
+ usageTotalEstimated,
838
+ charges,
839
+ isConfigured,
840
+ validationErrors: validationErrors.length > 0 ? validationErrors : null,
841
+ arcSource: pickArcSource(lc, arcSourceMap),
842
+ targetSubscriptionId: lc.targetSubscriptionId ?? null
843
+ });
844
+ }
845
+ const { customerName, convertedOrderId } = await resolveQuoteSideData(em, cpqConfig);
846
+ return {
847
+ id: cpqConfig.id,
848
+ quoteId: cpqConfig.quoteId,
849
+ quoteNumber: salesQuote.quoteNumber ?? "",
850
+ customerId: cpqConfig.customerId,
851
+ customerName,
852
+ cpqStatus: cpqConfig.cpqStatus,
853
+ version: cpqConfig.version,
854
+ parentQuoteId: cpqConfig.parentQuoteId ?? null,
855
+ currencyCode: cpqConfig.currencyCode,
856
+ convertedOrderId,
857
+ validationResult,
858
+ pricingSummary,
859
+ lines
860
+ };
861
+ }
862
+ }
863
+ async function resolveQuoteSideData(em, cpqConfig) {
864
+ const scope = {
865
+ organizationId: cpqConfig.organizationId,
866
+ tenantId: cpqConfig.tenantId
867
+ };
868
+ const [customer, existingOrder] = await Promise.all([
869
+ em.findOne(CustomerEntity, { id: cpqConfig.customerId, ...scope }),
870
+ em.findOne(
871
+ CpqOrderConfiguration,
872
+ { sourceQuoteId: cpqConfig.id, ...scope, deletedAt: null },
873
+ { fields: ["id", "orderId"] }
874
+ )
875
+ ]);
876
+ return {
877
+ customerName: customer?.displayName ?? null,
878
+ convertedOrderId: existingOrder?.orderId ?? null
879
+ };
880
+ }
881
+ function buildCustomerSnapshot(customer) {
882
+ return {
883
+ customer: {
884
+ id: customer.id,
885
+ kind: customer.kind,
886
+ displayName: customer.displayName,
887
+ primaryEmail: customer.primaryEmail ?? null,
888
+ primaryPhone: customer.primaryPhone ?? null,
889
+ personProfile: customer.personProfile ? {
890
+ id: customer.personProfile.id,
891
+ firstName: customer.personProfile.firstName ?? null,
892
+ lastName: customer.personProfile.lastName ?? null,
893
+ preferredName: customer.personProfile.preferredName ?? null
894
+ } : null,
895
+ companyProfile: customer.companyProfile ? {
896
+ id: customer.companyProfile.id,
897
+ legalName: customer.companyProfile.legalName ?? null,
898
+ brandName: customer.companyProfile.brandName ?? null,
899
+ domain: customer.companyProfile.domain ?? null,
900
+ websiteUrl: customer.companyProfile.websiteUrl ?? null
901
+ } : null
902
+ },
903
+ contact: null
904
+ };
905
+ }
906
+ function computeLineTotals(charges, quantity, usageEstimateInputs) {
907
+ const nrcPerInstance = charges.filter((c) => c.chargeType === "nrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
908
+ const mrcPerInstance = charges.filter((c) => c.chargeType === "mrc").reduce((s, c) => s + (c.totalPrice ?? 0), 0);
909
+ const nrcTotal = nrcPerInstance * quantity;
910
+ const mrcTotal = mrcPerInstance * quantity;
911
+ let usageEstimates = null;
912
+ let usageTotalEstimated = null;
913
+ if (usageEstimateInputs && usageEstimateInputs.length > 0) {
914
+ const usageCharges = charges.filter((c) => c.chargeType === "usage");
915
+ usageEstimates = usageEstimateInputs.map((est) => {
916
+ const charge = usageCharges.find((c) => c.chargeCode === est.chargeCode);
917
+ const estimatedTotal = (charge?.unitPrice ?? 0) * est.estimatedQuantity;
918
+ return {
919
+ chargeCode: est.chargeCode,
920
+ estimatedQuantity: est.estimatedQuantity,
921
+ estimatedTotal
922
+ };
923
+ });
924
+ usageTotalEstimated = usageEstimates.reduce((s, e) => s + e.estimatedTotal, 0) * quantity;
925
+ }
926
+ return { nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated };
927
+ }
928
+ function computeEndDate(startDate, termMonths, endDate) {
929
+ if (endDate) return endDate;
930
+ if (startDate && termMonths) {
931
+ const start = new Date(startDate);
932
+ start.setMonth(start.getMonth() + termMonths);
933
+ return start.toISOString().split("T")[0];
934
+ }
935
+ return null;
936
+ }
937
+ function aggregateAdjustmentTotals(allCharges) {
938
+ let discountTotal = 0;
939
+ let surchargeTotal = 0;
940
+ for (const charge of allCharges) {
941
+ if (!charge.adjustments) continue;
942
+ for (const adj of charge.adjustments) {
943
+ if (adj.delta < 0) {
944
+ discountTotal += Math.abs(adj.delta) * (charge.quantity ?? 1);
945
+ } else if (adj.delta > 0) {
946
+ surchargeTotal += adj.delta * (charge.quantity ?? 1);
947
+ }
948
+ }
949
+ }
950
+ return { discountTotal, surchargeTotal };
951
+ }
952
+ function computePricingSummary(lineConfigs, currencyCode) {
953
+ let nrcTotal = 0;
954
+ let mrcTotal = 0;
955
+ let usageTotalEstimated = null;
956
+ const usageCharges = [];
957
+ const allCharges = [];
958
+ for (const lc of lineConfigs) {
959
+ nrcTotal += Number(lc.nrcTotal);
960
+ mrcTotal += Number(lc.mrcTotal);
961
+ if (lc.usageTotalEstimated != null) {
962
+ usageTotalEstimated = (usageTotalEstimated ?? 0) + Number(lc.usageTotalEstimated);
963
+ }
964
+ const charges = lc.charges ?? [];
965
+ allCharges.push(...charges);
966
+ for (const charge of charges) {
967
+ if (charge.chargeType === "usage") {
968
+ usageCharges.push({
969
+ chargeCode: charge.chargeCode,
970
+ chargeName: charge.chargeName,
971
+ unitPrice: charge.unitPrice,
972
+ note: charge.note ?? null
973
+ });
974
+ }
975
+ }
976
+ }
977
+ const { discountTotal, surchargeTotal } = aggregateAdjustmentTotals(allCharges);
978
+ return {
979
+ nrcTotal,
980
+ mrcTotal,
981
+ usageCharges,
982
+ usageTotalEstimated,
983
+ discountTotal,
984
+ surchargeTotal,
985
+ currencyCode
986
+ };
987
+ }
988
+ function computePricingSummaryFromComputed(computedLines, currencyCode) {
989
+ let nrcTotal = 0;
990
+ let mrcTotal = 0;
991
+ let usageTotalEstimated = null;
992
+ const usageCharges = [];
993
+ const allCharges = [];
994
+ for (const { charges, nrcTotal: lineNrc, mrcTotal: lineMrc, usageTotalEstimated: lineUsageEst } of computedLines) {
995
+ nrcTotal += lineNrc;
996
+ mrcTotal += lineMrc;
997
+ if (lineUsageEst != null) {
998
+ usageTotalEstimated = (usageTotalEstimated ?? 0) + lineUsageEst;
999
+ }
1000
+ allCharges.push(...charges);
1001
+ for (const charge of charges) {
1002
+ if (charge.chargeType === "usage") {
1003
+ usageCharges.push({
1004
+ chargeCode: charge.chargeCode,
1005
+ chargeName: charge.chargeName,
1006
+ unitPrice: charge.unitPrice,
1007
+ note: charge.note ?? null
1008
+ });
1009
+ }
1010
+ }
1011
+ }
1012
+ const { discountTotal, surchargeTotal } = aggregateAdjustmentTotals(allCharges);
1013
+ return {
1014
+ nrcTotal,
1015
+ mrcTotal,
1016
+ usageCharges,
1017
+ usageTotalEstimated,
1018
+ discountTotal,
1019
+ surchargeTotal,
1020
+ currencyCode
1021
+ };
1022
+ }
1023
+ function emptyPricingSummary(currencyCode) {
1024
+ return {
1025
+ nrcTotal: 0,
1026
+ mrcTotal: 0,
1027
+ usageCharges: [],
1028
+ usageTotalEstimated: null,
1029
+ discountTotal: 0,
1030
+ surchargeTotal: 0,
1031
+ currencyCode
1032
+ };
1033
+ }
1034
+ class QuotingError extends Error {
1035
+ constructor(status, message, cpqStatus, details) {
1036
+ super(message);
1037
+ this.status = status;
1038
+ this.cpqStatus = cpqStatus;
1039
+ this.details = details;
1040
+ this.name = "QuotingError";
1041
+ }
1042
+ }
1043
+ const EDITABLE_QUOTE_STATUSES = /* @__PURE__ */ new Set([
1044
+ "new",
1045
+ "incomplete",
1046
+ "ready"
1047
+ ]);
1048
+ function ensureEditable(cpqConfig) {
1049
+ if (!EDITABLE_QUOTE_STATUSES.has(cpqConfig.cpqStatus)) {
1050
+ throw new QuotingError(
1051
+ 409,
1052
+ `Cannot modify ARC attachments \u2014 quote is in '${cpqConfig.cpqStatus}' status`
1053
+ );
1054
+ }
1055
+ }
1056
+ function toDateOrNull(value) {
1057
+ if (value == null) return null;
1058
+ return value instanceof Date ? value : new Date(value);
1059
+ }
1060
+ function toIsoOrNull(value) {
1061
+ if (value == null) return null;
1062
+ if (value instanceof Date) return value.toISOString();
1063
+ return String(value);
1064
+ }
1065
+ DefaultCpqQuotingService.prototype.setQuoteType = async function(quoteId, type, scope) {
1066
+ const em = this.em;
1067
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1068
+ ensureEditable(cpqConfig);
1069
+ if (cpqConfig.quoteType === type) {
1070
+ } else if (cpqConfig.quoteType !== "new") {
1071
+ throw new QuotingError(409, `Quote type is immutable (current: '${cpqConfig.quoteType}')`);
1072
+ } else if (type === "new") {
1073
+ throw new QuotingError(400, `Cannot transition quote type back to 'new'`);
1074
+ } else {
1075
+ cpqConfig.quoteType = type;
1076
+ await em.flush();
1077
+ }
1078
+ const salesQuote = await loadSalesQuoteStrict(em, cpqConfig.quoteId, scope);
1079
+ const lines = await loadLines(em, cpqConfig.id, scope);
1080
+ return this.buildQuoteResult(em, cpqConfig, salesQuote, lines);
1081
+ };
1082
+ DefaultCpqQuotingService.prototype.createQuoteFromSubscription = async function(input, scope) {
1083
+ const em = this.em;
1084
+ const sub = await em.findOne(CpqInventorySubscription, {
1085
+ id: input.subscriptionId,
1086
+ organizationId: scope.organizationId,
1087
+ tenantId: scope.tenantId,
1088
+ deletedAt: null
1089
+ });
1090
+ if (!sub) throw new QuotingError(404, "Subscription not found");
1091
+ if (sub.status !== "active" && sub.status !== "suspended") {
1092
+ throw new QuotingError(
1093
+ 409,
1094
+ `Subscription must be active or suspended (current: '${sub.status}')`
1095
+ );
1096
+ }
1097
+ await ensureNoConcurrentArcQuote(em, sub.id, scope);
1098
+ const items = await em.find(CpqInventorySubscriptionItem, {
1099
+ subscriptionId: sub.id,
1100
+ organizationId: scope.organizationId,
1101
+ tenantId: scope.tenantId,
1102
+ deletedAt: null,
1103
+ status: { $nin: ["terminated", "expired", "superseded"] }
1104
+ });
1105
+ const numberGenerator = this.numberGenerator;
1106
+ const { number: quoteNumber } = await numberGenerator.generate({
1107
+ kind: "quote",
1108
+ organizationId: scope.organizationId,
1109
+ tenantId: scope.tenantId
1110
+ });
1111
+ const salesQuoteId = crypto.randomUUID();
1112
+ const salesQuote = em.create(SalesQuote, {
1113
+ id: salesQuoteId,
1114
+ organizationId: scope.organizationId,
1115
+ tenantId: scope.tenantId,
1116
+ quoteNumber,
1117
+ customerEntityId: sub.customerId,
1118
+ currencyCode: sub.currencyCode,
1119
+ subtotalNetAmount: "0",
1120
+ subtotalGrossAmount: "0",
1121
+ discountTotalAmount: "0",
1122
+ taxTotalAmount: "0",
1123
+ grandTotalNetAmount: "0",
1124
+ grandTotalGrossAmount: "0",
1125
+ lineItemCount: 0,
1126
+ createdAt: /* @__PURE__ */ new Date(),
1127
+ updatedAt: /* @__PURE__ */ new Date()
1128
+ });
1129
+ em.persist(salesQuote);
1130
+ const cpqConfigId = crypto.randomUUID();
1131
+ const cpqConfig = em.create(CpqQuoteConfiguration, {
1132
+ id: cpqConfigId,
1133
+ organizationId: scope.organizationId,
1134
+ tenantId: scope.tenantId,
1135
+ quoteId: salesQuoteId,
1136
+ customerId: sub.customerId,
1137
+ cpqStatus: "new",
1138
+ version: 1,
1139
+ currencyCode: sub.currencyCode,
1140
+ quoteType: input.type,
1141
+ quoteContext: { arcSourceSubscriptionId: sub.id },
1142
+ validationResult: { valid: true, errors: [] },
1143
+ pricingSummary: emptyArcPricingSummary(sub.currencyCode)
1144
+ });
1145
+ em.persist(cpqConfig);
1146
+ const renewDefaults = input.type === "renew" ? buildRenewDefaults(sub, input.renewTerm) : null;
1147
+ const target = em.create(CpqQuoteTargetSubscription, {
1148
+ organizationId: scope.organizationId,
1149
+ tenantId: scope.tenantId,
1150
+ quoteId: cpqConfig.id,
1151
+ subscriptionId: sub.id,
1152
+ quoteType: input.type,
1153
+ mergeAction: input.type === "renew" ? "standalone" : null,
1154
+ newTermStart: renewDefaults?.start ?? null,
1155
+ newTermEnd: renewDefaults?.end ?? null,
1156
+ newTermMonths: renewDefaults?.months ?? null
1157
+ });
1158
+ em.persist(target);
1159
+ await mirrorTargetSubscriptionItems(em, cpqConfig, sub, salesQuote, input.type, scope);
1160
+ await em.flush();
1161
+ return { quoteId: salesQuoteId, cpqConfigId: cpqConfig.id };
1162
+ };
1163
+ DefaultCpqQuotingService.prototype.listTargetSubscriptions = async function(quoteId, scope) {
1164
+ const em = this.em;
1165
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1166
+ const targets = await em.find(CpqQuoteTargetSubscription, {
1167
+ quoteId: cpqConfig.id,
1168
+ organizationId: scope.organizationId,
1169
+ tenantId: scope.tenantId,
1170
+ deletedAt: null
1171
+ });
1172
+ if (targets.length === 0) return [];
1173
+ const subs = await em.find(CpqInventorySubscription, {
1174
+ id: { $in: targets.map((t) => t.subscriptionId) },
1175
+ organizationId: scope.organizationId,
1176
+ tenantId: scope.tenantId,
1177
+ deletedAt: null
1178
+ });
1179
+ const subMap = new Map(subs.map((s) => [s.id, s]));
1180
+ return targets.map((t) => {
1181
+ const s = subMap.get(t.subscriptionId);
1182
+ return {
1183
+ id: t.id,
1184
+ subscriptionId: t.subscriptionId,
1185
+ quoteType: t.quoteType,
1186
+ mergeAction: t.mergeAction ?? null,
1187
+ newTermStart: toIsoOrNull(t.newTermStart),
1188
+ newTermEnd: toIsoOrNull(t.newTermEnd),
1189
+ newTermMonths: t.newTermMonths ?? null,
1190
+ subscription: s ? {
1191
+ code: s.code,
1192
+ name: s.name,
1193
+ status: s.status,
1194
+ customerId: s.customerId,
1195
+ currencyCode: s.currencyCode,
1196
+ billingCycle: s.billingCycle,
1197
+ currentTermStart: toIsoOrNull(s.currentTermStart),
1198
+ currentTermEnd: toIsoOrNull(s.currentTermEnd),
1199
+ termMonths: s.termMonths ?? null,
1200
+ mrcAmount: Number(s.mrcAmount)
1201
+ } : null
1202
+ };
1203
+ });
1204
+ };
1205
+ DefaultCpqQuotingService.prototype.attachTargetSubscription = async function(quoteId, input, scope) {
1206
+ const em = this.em;
1207
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1208
+ ensureEditable(cpqConfig);
1209
+ if (cpqConfig.quoteType === "new") {
1210
+ throw new QuotingError(409, `Cannot attach a target subscription \u2014 quote_type is 'new'`);
1211
+ }
1212
+ if (cpqConfig.quoteType !== input.quoteType) {
1213
+ throw new QuotingError(
1214
+ 400,
1215
+ `Quote type mismatch: quote is '${cpqConfig.quoteType}', payload says '${input.quoteType}'`
1216
+ );
1217
+ }
1218
+ const existing = await em.findOne(CpqQuoteTargetSubscription, {
1219
+ organizationId: scope.organizationId,
1220
+ tenantId: scope.tenantId,
1221
+ deletedAt: null,
1222
+ quoteId: cpqConfig.id,
1223
+ subscriptionId: input.subscriptionId
1224
+ });
1225
+ if (existing) {
1226
+ return mapTargetView(em, existing, scope);
1227
+ }
1228
+ const sub = await em.findOne(CpqInventorySubscription, {
1229
+ id: input.subscriptionId,
1230
+ organizationId: scope.organizationId,
1231
+ tenantId: scope.tenantId,
1232
+ deletedAt: null
1233
+ });
1234
+ if (!sub) throw new QuotingError(404, "Subscription not found");
1235
+ if (sub.status !== "active" && sub.status !== "suspended") {
1236
+ throw new QuotingError(
1237
+ 409,
1238
+ `Subscription must be active or suspended (current: '${sub.status}')`
1239
+ );
1240
+ }
1241
+ if (sub.customerId !== cpqConfig.customerId) {
1242
+ throw new QuotingError(400, "Subscription customer must match quote customer");
1243
+ }
1244
+ if (sub.currencyCode !== cpqConfig.currencyCode) {
1245
+ throw new QuotingError(400, "Subscription currency must match quote currency");
1246
+ }
1247
+ const otherTargets = await em.find(CpqQuoteTargetSubscription, {
1248
+ organizationId: scope.organizationId,
1249
+ tenantId: scope.tenantId,
1250
+ deletedAt: null,
1251
+ quoteId: cpqConfig.id
1252
+ });
1253
+ if (input.quoteType === "renew" && otherTargets.length > 0) {
1254
+ const otherSubs = await em.find(CpqInventorySubscription, {
1255
+ id: { $in: otherTargets.map((t) => t.subscriptionId) },
1256
+ organizationId: scope.organizationId,
1257
+ tenantId: scope.tenantId,
1258
+ deletedAt: null
1259
+ });
1260
+ for (const o of otherSubs) {
1261
+ if (o.billingCycle !== sub.billingCycle) {
1262
+ throw new QuotingError(
1263
+ 400,
1264
+ "Merge targets must share the same billing cycle"
1265
+ );
1266
+ }
1267
+ }
1268
+ }
1269
+ await ensureNoConcurrentArcQuote(em, input.subscriptionId, scope, cpqConfig.id);
1270
+ const target = em.create(CpqQuoteTargetSubscription, {
1271
+ organizationId: scope.organizationId,
1272
+ tenantId: scope.tenantId,
1273
+ quoteId: cpqConfig.id,
1274
+ subscriptionId: input.subscriptionId,
1275
+ quoteType: input.quoteType,
1276
+ mergeAction: input.quoteType === "renew" ? input.mergeAction ?? (otherTargets.length === 0 ? "standalone" : "absorb") : null,
1277
+ newTermStart: toDateOrNull(input.newTermStart),
1278
+ newTermEnd: toDateOrNull(input.newTermEnd),
1279
+ newTermMonths: input.newTermMonths ?? null
1280
+ });
1281
+ em.persist(target);
1282
+ await em.flush();
1283
+ let renewMergeJustEntered = false;
1284
+ if (input.quoteType === "renew") {
1285
+ const allTargets = await em.find(CpqQuoteTargetSubscription, {
1286
+ quoteId: cpqConfig.id,
1287
+ organizationId: scope.organizationId,
1288
+ tenantId: scope.tenantId,
1289
+ deletedAt: null
1290
+ });
1291
+ if (allTargets.length >= 2) {
1292
+ const wasStandalone = allTargets.some((t) => t.mergeAction === "standalone");
1293
+ for (const t of allTargets) {
1294
+ t.mergeAction = "absorb";
1295
+ t.newTermStart = null;
1296
+ t.newTermEnd = null;
1297
+ t.newTermMonths = null;
1298
+ }
1299
+ await em.flush();
1300
+ renewMergeJustEntered = wasStandalone;
1301
+ }
1302
+ }
1303
+ const salesQuote = await em.findOne(SalesQuote, {
1304
+ id: cpqConfig.quoteId,
1305
+ organizationId: scope.organizationId,
1306
+ tenantId: scope.tenantId
1307
+ });
1308
+ if (salesQuote) {
1309
+ const isRenewMerge = input.quoteType === "renew" && otherTargets.length > 0;
1310
+ await mirrorTargetSubscriptionItems(
1311
+ em,
1312
+ cpqConfig,
1313
+ sub,
1314
+ salesQuote,
1315
+ input.quoteType,
1316
+ scope,
1317
+ { mergeMode: isRenewMerge }
1318
+ );
1319
+ if (renewMergeJustEntered) {
1320
+ const existingLines = await em.find(CpqQuoteLineConfiguration, {
1321
+ quoteConfigurationId: cpqConfig.id,
1322
+ organizationId: scope.organizationId,
1323
+ tenantId: scope.tenantId,
1324
+ deletedAt: null
1325
+ });
1326
+ for (const lc of existingLines) {
1327
+ if (lc.targetSubscriptionId !== null) lc.targetSubscriptionId = null;
1328
+ }
1329
+ }
1330
+ await em.flush();
1331
+ }
1332
+ return mapTargetView(em, target, scope);
1333
+ };
1334
+ DefaultCpqQuotingService.prototype.detachTargetSubscription = async function(quoteId, targetId, scope) {
1335
+ const em = this.em;
1336
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1337
+ ensureEditable(cpqConfig);
1338
+ const target = await em.findOne(CpqQuoteTargetSubscription, {
1339
+ id: targetId,
1340
+ quoteId: cpqConfig.id,
1341
+ organizationId: scope.organizationId,
1342
+ tenantId: scope.tenantId,
1343
+ deletedAt: null
1344
+ });
1345
+ if (!target) throw new QuotingError(404, "Target subscription not found");
1346
+ const lines = await em.find(CpqQuoteLineConfiguration, {
1347
+ quoteConfigurationId: cpqConfig.id,
1348
+ targetSubscriptionId: target.subscriptionId,
1349
+ organizationId: scope.organizationId,
1350
+ tenantId: scope.tenantId,
1351
+ deletedAt: null
1352
+ });
1353
+ const now = /* @__PURE__ */ new Date();
1354
+ for (const line of lines) {
1355
+ line.deletedAt = now;
1356
+ const salesLine = await em.findOne(SalesQuoteLine, { id: line.quoteLineId });
1357
+ if (salesLine) salesLine.deletedAt = now;
1358
+ }
1359
+ target.deletedAt = now;
1360
+ if (target.quoteType === "renew") {
1361
+ const remaining = await em.find(CpqQuoteTargetSubscription, {
1362
+ quoteId: cpqConfig.id,
1363
+ organizationId: scope.organizationId,
1364
+ tenantId: scope.tenantId,
1365
+ deletedAt: null,
1366
+ id: { $ne: target.id }
1367
+ });
1368
+ if (remaining.length === 1) {
1369
+ remaining[0].mergeAction = "standalone";
1370
+ }
1371
+ }
1372
+ await em.flush();
1373
+ return { ok: true };
1374
+ };
1375
+ DefaultCpqQuotingService.prototype.updateTargetSubscription = async function(quoteId, targetId, patch, scope) {
1376
+ const em = this.em;
1377
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1378
+ ensureEditable(cpqConfig);
1379
+ const target = await em.findOne(CpqQuoteTargetSubscription, {
1380
+ id: targetId,
1381
+ quoteId: cpqConfig.id,
1382
+ organizationId: scope.organizationId,
1383
+ tenantId: scope.tenantId,
1384
+ deletedAt: null
1385
+ });
1386
+ if (!target) throw new QuotingError(404, "Target subscription not found");
1387
+ if (target.quoteType !== "renew") {
1388
+ throw new QuotingError(
1389
+ 409,
1390
+ `Cannot update target on a '${target.quoteType}' quote \u2014 only renew targets carry term/merge fields`
1391
+ );
1392
+ }
1393
+ if (patch.mergeAction !== void 0) target.mergeAction = patch.mergeAction;
1394
+ if (patch.newTermStart !== void 0) target.newTermStart = toDateOrNull(patch.newTermStart);
1395
+ if (patch.newTermEnd !== void 0) target.newTermEnd = toDateOrNull(patch.newTermEnd);
1396
+ if (patch.newTermMonths !== void 0) target.newTermMonths = patch.newTermMonths ?? null;
1397
+ await em.flush();
1398
+ return mapTargetView(em, target, scope);
1399
+ };
1400
+ DefaultCpqQuotingService.prototype.setCancelMeta = async function(quoteId, meta, scope) {
1401
+ const em = this.em;
1402
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1403
+ ensureEditable(cpqConfig);
1404
+ if (cpqConfig.quoteType !== "cancel") {
1405
+ throw new QuotingError(
1406
+ 409,
1407
+ `Cancel meta can only be set on a cancel quote (current: '${cpqConfig.quoteType}')`
1408
+ );
1409
+ }
1410
+ cpqConfig.arcReasonCode = meta.reasonCode;
1411
+ cpqConfig.arcReasonText = meta.reasonText ?? null;
1412
+ cpqConfig.arcEtfAmount = meta.etfAmount != null ? String(meta.etfAmount) : null;
1413
+ cpqConfig.arcEtfCurrency = meta.etfCurrency ?? cpqConfig.currencyCode;
1414
+ await em.flush();
1415
+ const salesQuote = await loadSalesQuoteStrict(em, cpqConfig.quoteId, scope);
1416
+ const lines = await loadLines(em, cpqConfig.id, scope);
1417
+ return this.buildQuoteResult(em, cpqConfig, salesQuote, lines);
1418
+ };
1419
+ DefaultCpqQuotingService.prototype.setMergeMeta = async function(quoteId, meta, scope) {
1420
+ const em = this.em;
1421
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1422
+ ensureEditable(cpqConfig);
1423
+ if (cpqConfig.quoteType !== "renew") {
1424
+ throw new QuotingError(
1425
+ 409,
1426
+ `Merge meta can only be set on a renew quote (current: '${cpqConfig.quoteType}')`
1427
+ );
1428
+ }
1429
+ cpqConfig.arcMergeNewTermStart = toDateOrNull(meta.newTermStart);
1430
+ cpqConfig.arcMergeNewTermEnd = toDateOrNull(meta.newTermEnd);
1431
+ cpqConfig.arcMergeNewTermMonths = meta.newTermMonths ?? null;
1432
+ cpqConfig.arcMergeNewSubCode = meta.newSubCode ?? null;
1433
+ cpqConfig.arcMergeNewSubName = meta.newSubName ?? null;
1434
+ await em.flush();
1435
+ const salesQuote = await loadSalesQuoteStrict(em, cpqConfig.quoteId, scope);
1436
+ const lines = await loadLines(em, cpqConfig.id, scope);
1437
+ return this.buildQuoteResult(em, cpqConfig, salesQuote, lines);
1438
+ };
1439
+ DefaultCpqQuotingService.prototype.validateArcQuote = async function(quoteId, scope) {
1440
+ const em = this.em;
1441
+ const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope);
1442
+ const errors = [];
1443
+ if (cpqConfig.quoteType === "new") {
1444
+ return { ok: true, errors: [] };
1445
+ }
1446
+ if (!ARC_QUOTE_TYPES.includes(cpqConfig.quoteType)) {
1447
+ errors.push(`Unknown quote type '${cpqConfig.quoteType}'`);
1448
+ return { ok: false, errors };
1449
+ }
1450
+ const targets = await em.find(CpqQuoteTargetSubscription, {
1451
+ quoteId: cpqConfig.id,
1452
+ organizationId: scope.organizationId,
1453
+ tenantId: scope.tenantId,
1454
+ deletedAt: null
1455
+ });
1456
+ if (targets.length === 0) {
1457
+ errors.push(`ARC quote requires at least one target subscription`);
1458
+ return { ok: false, errors };
1459
+ }
1460
+ const subs = await em.find(CpqInventorySubscription, {
1461
+ id: { $in: targets.map((t) => t.subscriptionId) },
1462
+ organizationId: scope.organizationId,
1463
+ tenantId: scope.tenantId,
1464
+ deletedAt: null
1465
+ });
1466
+ const subMap = new Map(subs.map((s) => [s.id, s]));
1467
+ for (const t of targets) {
1468
+ const s = subMap.get(t.subscriptionId);
1469
+ if (!s) {
1470
+ errors.push(`Target subscription ${t.subscriptionId} not found`);
1471
+ continue;
1472
+ }
1473
+ if (s.status !== "active" && s.status !== "suspended") {
1474
+ errors.push(
1475
+ `Target subscription ${s.code} must be active or suspended (current: '${s.status}')`
1476
+ );
1477
+ }
1478
+ if (s.customerId !== cpqConfig.customerId) {
1479
+ errors.push(`Target subscription ${s.code} belongs to a different customer`);
1480
+ }
1481
+ if (s.currencyCode !== cpqConfig.currencyCode) {
1482
+ errors.push(`Target subscription ${s.code} has currency mismatch`);
1483
+ }
1484
+ }
1485
+ if (cpqConfig.quoteType === "renew") {
1486
+ const standaloneCount = targets.filter((t) => t.mergeAction === "standalone").length;
1487
+ const absorbCount = targets.filter((t) => t.mergeAction === "absorb").length;
1488
+ if (targets.length === 1) {
1489
+ if (standaloneCount !== 1) {
1490
+ errors.push(`Single-target renew must have merge_action='standalone'`);
1491
+ }
1492
+ const t = targets[0];
1493
+ if (!t.newTermStart || !t.newTermEnd) {
1494
+ errors.push(`Standalone renew target must have newTermStart and newTermEnd`);
1495
+ } else if (t.newTermStart >= t.newTermEnd) {
1496
+ errors.push(`newTermStart must be earlier than newTermEnd`);
1497
+ }
1498
+ } else {
1499
+ if (absorbCount !== targets.length) {
1500
+ errors.push(`Multi-target renew (merge) requires every target merge_action='absorb'`);
1501
+ }
1502
+ if (!cpqConfig.arcMergeNewTermStart || !cpqConfig.arcMergeNewTermEnd) {
1503
+ errors.push(`Merge renew requires quote-level arc_merge_new_term_start and arc_merge_new_term_end`);
1504
+ } else if (cpqConfig.arcMergeNewTermStart >= cpqConfig.arcMergeNewTermEnd) {
1505
+ errors.push(`arc_merge_new_term_start must be earlier than arc_merge_new_term_end`);
1506
+ }
1507
+ const cycles = new Set(
1508
+ targets.map((t) => subMap.get(t.subscriptionId)?.billingCycle).filter(Boolean)
1509
+ );
1510
+ if (cycles.size > 1) {
1511
+ errors.push(`Merge targets must share the same billing cycle`);
1512
+ }
1513
+ }
1514
+ }
1515
+ if (cpqConfig.quoteType === "cancel") {
1516
+ if (!cpqConfig.arcReasonCode) {
1517
+ errors.push(`Cancel quote requires arc_reason_code (call /cancel-meta first)`);
1518
+ }
1519
+ }
1520
+ const lines = await loadLines(em, cpqConfig.id, scope);
1521
+ const targetSubIds = new Set(targets.map((t) => t.subscriptionId));
1522
+ const isMerge = cpqConfig.quoteType === "renew" && targets.length >= 2 && targets.every((t) => t.mergeAction === "absorb");
1523
+ for (const line of lines) {
1524
+ if (cpqConfig.quoteType === "amend" || cpqConfig.quoteType === "cancel") {
1525
+ if (!line.targetSubscriptionId) {
1526
+ errors.push(`Quote line ${line.quoteLineId} missing target_subscription_id`);
1527
+ } else if (!targetSubIds.has(line.targetSubscriptionId)) {
1528
+ errors.push(
1529
+ `Quote line ${line.quoteLineId} target_subscription_id does not match any attached target`
1530
+ );
1531
+ }
1532
+ } else if (cpqConfig.quoteType === "renew") {
1533
+ if (isMerge) {
1534
+ if (line.targetSubscriptionId) {
1535
+ errors.push(
1536
+ `Merge-renew line ${line.quoteLineId} must have null target_subscription_id`
1537
+ );
1538
+ }
1539
+ } else {
1540
+ if (!line.targetSubscriptionId || !targetSubIds.has(line.targetSubscriptionId)) {
1541
+ errors.push(
1542
+ `Standalone renew line ${line.quoteLineId} must target the renewing subscription`
1543
+ );
1544
+ }
1545
+ }
1546
+ }
1547
+ if ((line.action === "cancel" || line.action === "modify") && cpqConfig.quoteType !== "cancel" && !line.sourceSubscriptionItemId) {
1548
+ errors.push(
1549
+ `Quote line ${line.quoteLineId} action='${line.action}' requires source_subscription_item_id`
1550
+ );
1551
+ }
1552
+ }
1553
+ for (const t of targets) {
1554
+ await ensureNoConcurrentArcQuoteForValidation(em, t.subscriptionId, scope, cpqConfig.id, errors);
1555
+ }
1556
+ return { ok: errors.length === 0, errors };
1557
+ };
1558
+ async function loadCpqConfigStrict(em, quoteId, scope) {
1559
+ let config = await em.findOne(CpqQuoteConfiguration, {
1560
+ id: quoteId,
1561
+ organizationId: scope.organizationId,
1562
+ tenantId: scope.tenantId,
1563
+ deletedAt: null
1564
+ });
1565
+ if (!config) {
1566
+ config = await em.findOne(CpqQuoteConfiguration, {
1567
+ quoteId,
1568
+ organizationId: scope.organizationId,
1569
+ tenantId: scope.tenantId,
1570
+ deletedAt: null
1571
+ });
1572
+ }
1573
+ if (!config) throw new QuotingError(404, "Quote not found");
1574
+ return config;
1575
+ }
1576
+ async function loadSalesQuoteStrict(em, quoteId, scope) {
1577
+ const quote = await em.findOne(SalesQuote, {
1578
+ id: quoteId,
1579
+ organizationId: scope.organizationId,
1580
+ tenantId: scope.tenantId
1581
+ });
1582
+ if (!quote) throw new QuotingError(404, "Sales quote not found");
1583
+ return quote;
1584
+ }
1585
+ async function loadLines(em, cpqConfigId, scope) {
1586
+ return em.find(CpqQuoteLineConfiguration, {
1587
+ quoteConfigurationId: cpqConfigId,
1588
+ organizationId: scope.organizationId,
1589
+ tenantId: scope.tenantId,
1590
+ deletedAt: null
1591
+ });
1592
+ }
1593
+ async function ensureNoConcurrentArcQuote(em, subscriptionId, scope, excludeQuoteConfigId) {
1594
+ const conflicting = await em.find(CpqQuoteTargetSubscription, {
1595
+ subscriptionId,
1596
+ organizationId: scope.organizationId,
1597
+ tenantId: scope.tenantId,
1598
+ deletedAt: null
1599
+ });
1600
+ if (conflicting.length === 0) return;
1601
+ const cfgIds = conflicting.map((c) => c.quoteId);
1602
+ const cfgs = await em.find(CpqQuoteConfiguration, {
1603
+ id: { $in: cfgIds },
1604
+ organizationId: scope.organizationId,
1605
+ tenantId: scope.tenantId,
1606
+ deletedAt: null
1607
+ });
1608
+ for (const cfg of cfgs) {
1609
+ if (excludeQuoteConfigId && cfg.id === excludeQuoteConfigId) continue;
1610
+ if (!TERMINAL_STATUSES.includes(cfg.cpqStatus)) {
1611
+ throw new QuotingError(
1612
+ 409,
1613
+ `Subscription has another ARC quote in progress (quoteId=${cfg.quoteId})`,
1614
+ void 0,
1615
+ // Surface the conflicting quote id so the UI can offer "Open the
1616
+ // existing quote" instead of just showing a dead-end alert. Use
1617
+ // `quoteId` (the SalesQuote UUID, what the URL bar shows after the
1618
+ // first Amend) — not `cfg.id` (the CpqQuoteConfiguration UUID) —
1619
+ // so the redirect URL matches the URL the operator would see on a
1620
+ // fresh first-time create, and so the test assertion can compare
1621
+ // the two with `toContain`.
1622
+ { existingQuoteId: cfg.quoteId, existingQuoteStatus: cfg.cpqStatus }
1623
+ );
1624
+ }
1625
+ }
1626
+ }
1627
+ async function ensureNoConcurrentArcQuoteForValidation(em, subscriptionId, scope, excludeQuoteConfigId, errors) {
1628
+ try {
1629
+ await ensureNoConcurrentArcQuote(em, subscriptionId, scope, excludeQuoteConfigId);
1630
+ } catch (err) {
1631
+ if (err instanceof QuotingError) errors.push(err.message);
1632
+ else throw err;
1633
+ }
1634
+ }
1635
+ async function mapTargetView(em, target, scope) {
1636
+ const sub = await em.findOne(CpqInventorySubscription, {
1637
+ id: target.subscriptionId,
1638
+ organizationId: scope.organizationId,
1639
+ tenantId: scope.tenantId,
1640
+ deletedAt: null
1641
+ });
1642
+ return {
1643
+ id: target.id,
1644
+ subscriptionId: target.subscriptionId,
1645
+ quoteType: target.quoteType,
1646
+ mergeAction: target.mergeAction ?? null,
1647
+ newTermStart: toIsoOrNull(target.newTermStart),
1648
+ newTermEnd: toIsoOrNull(target.newTermEnd),
1649
+ newTermMonths: target.newTermMonths ?? null,
1650
+ subscription: sub ? {
1651
+ code: sub.code,
1652
+ name: sub.name,
1653
+ status: sub.status,
1654
+ customerId: sub.customerId,
1655
+ currencyCode: sub.currencyCode,
1656
+ billingCycle: sub.billingCycle,
1657
+ currentTermStart: toIsoOrNull(sub.currentTermStart),
1658
+ currentTermEnd: toIsoOrNull(sub.currentTermEnd),
1659
+ termMonths: sub.termMonths ?? null,
1660
+ mrcAmount: Number(sub.mrcAmount)
1661
+ } : null
1662
+ };
1663
+ }
1664
+ function emptyArcPricingSummary(currencyCode) {
1665
+ return {
1666
+ nrcTotal: 0,
1667
+ mrcTotal: 0,
1668
+ usageCharges: [],
1669
+ usageTotalEstimated: null,
1670
+ discountTotal: 0,
1671
+ surchargeTotal: 0,
1672
+ currencyCode
1673
+ };
1674
+ }
1675
+ async function loadArcSourceMap(em, lineConfigs, scope) {
1676
+ const ids = /* @__PURE__ */ new Set();
1677
+ for (const lc of lineConfigs) {
1678
+ const fromMarker = lc.configuration?._arcMirroredFromItemId;
1679
+ if (typeof fromMarker === "string") ids.add(fromMarker);
1680
+ if (lc.sourceSubscriptionItemId) ids.add(lc.sourceSubscriptionItemId);
1681
+ }
1682
+ if (ids.size === 0) return /* @__PURE__ */ new Map();
1683
+ const items = await em.find(CpqInventorySubscriptionItem, {
1684
+ id: { $in: Array.from(ids) },
1685
+ organizationId: scope.organizationId,
1686
+ tenantId: scope.tenantId,
1687
+ deletedAt: null
1688
+ });
1689
+ return new Map(items.map((it) => [it.id, it]));
1690
+ }
1691
+ function pickArcSource(lc, map) {
1692
+ const fromMarker = lc.configuration?._arcMirroredFromItemId;
1693
+ const id = (typeof fromMarker === "string" ? fromMarker : null) ?? lc.sourceSubscriptionItemId ?? null;
1694
+ if (!id) return null;
1695
+ const item = map.get(id);
1696
+ if (!item) return null;
1697
+ return {
1698
+ subscriptionItemId: item.id,
1699
+ name: item.name,
1700
+ mrcAmount: Number(item.mrcAmount),
1701
+ nrcAmount: Number(item.nrcAmount),
1702
+ quantity: item.quantity
1703
+ };
1704
+ }
1705
+ function buildRenewDefaults(sub, override) {
1706
+ if (override) {
1707
+ const start2 = toDateOrNull(override.newTermStart);
1708
+ const end2 = toDateOrNull(override.newTermEnd);
1709
+ const months2 = override.newTermMonths ?? null;
1710
+ if (start2 && end2 && months2 != null) return { start: start2, end: end2, months: months2 };
1711
+ }
1712
+ const months = sub.termMonths ?? 12;
1713
+ const start = sub.currentTermEnd ? new Date(sub.currentTermEnd) : /* @__PURE__ */ new Date();
1714
+ const end = new Date(start);
1715
+ end.setMonth(end.getMonth() + months);
1716
+ return { start, end, months };
1717
+ }
1718
+ async function mirrorTargetSubscriptionItems(em, cpqConfig, sub, salesQuote, quoteType, scope, options) {
1719
+ const items = await em.find(CpqInventorySubscriptionItem, {
1720
+ subscriptionId: sub.id,
1721
+ organizationId: scope.organizationId,
1722
+ tenantId: scope.tenantId,
1723
+ deletedAt: null,
1724
+ status: { $nin: ["terminated", "expired", "superseded"] }
1725
+ });
1726
+ if (items.length === 0) return;
1727
+ const existingLines = await em.find(SalesQuoteLine, {
1728
+ quote: salesQuote,
1729
+ organizationId: scope.organizationId,
1730
+ tenantId: scope.tenantId,
1731
+ deletedAt: null
1732
+ });
1733
+ let nextLineNumber = existingLines.length + 1;
1734
+ const subItemIdToSalesLineId = /* @__PURE__ */ new Map();
1735
+ for (const item of items) {
1736
+ subItemIdToSalesLineId.set(item.id, crypto.randomUUID());
1737
+ }
1738
+ const lineAction = quoteType === "cancel" ? "cancel" : "modify";
1739
+ for (const item of items) {
1740
+ const salesLineId = subItemIdToSalesLineId.get(item.id);
1741
+ const parentLineId = item.parentItemId ? subItemIdToSalesLineId.get(item.parentItemId) ?? null : null;
1742
+ const salesLine = em.create(SalesQuoteLine, {
1743
+ id: salesLineId,
1744
+ quote: salesQuote,
1745
+ organizationId: scope.organizationId,
1746
+ tenantId: scope.tenantId,
1747
+ lineNumber: nextLineNumber++,
1748
+ kind: "product",
1749
+ productId: item.productId ?? null,
1750
+ name: item.name,
1751
+ quantity: String(item.quantity),
1752
+ unitPriceNet: "0",
1753
+ unitPriceGross: "0",
1754
+ totalNetAmount: "0",
1755
+ totalGrossAmount: "0",
1756
+ discountAmount: "0",
1757
+ discountPercent: "0",
1758
+ taxRate: "0",
1759
+ taxAmount: "0",
1760
+ currencyCode: sub.currencyCode,
1761
+ configuration: item.configuration ?? null,
1762
+ normalizedQuantity: "0",
1763
+ createdAt: /* @__PURE__ */ new Date(),
1764
+ updatedAt: /* @__PURE__ */ new Date()
1765
+ });
1766
+ em.persist(salesLine);
1767
+ const lineConfig = em.create(CpqQuoteLineConfiguration, {
1768
+ organizationId: scope.organizationId,
1769
+ tenantId: scope.tenantId,
1770
+ quoteLineId: salesLineId,
1771
+ quoteConfigurationId: cpqConfig.id,
1772
+ offeringId: item.offeringId ?? null,
1773
+ productId: item.productId ?? null,
1774
+ specId: item.specId ?? null,
1775
+ action: lineAction,
1776
+ parentLineId,
1777
+ startDate: null,
1778
+ termMonths: null,
1779
+ endDate: null,
1780
+ configuration: {
1781
+ ...item.configuration,
1782
+ _arcMirroredFromItemId: item.id,
1783
+ _arcMirroredName: item.name
1784
+ },
1785
+ quantity: item.quantity,
1786
+ charges: pickArcMirrorCharges(item, sub.currencyCode),
1787
+ nrcTotal: String(item.nrcAmount ?? 0),
1788
+ mrcTotal: String(item.mrcAmount),
1789
+ isConfigured: true,
1790
+ // XD-250 spec: merge-renew lines must have null `target_subscription_id`
1791
+ // (the source subs are absorbed into a brand-new sub at activation, so
1792
+ // there's no existing item to bind the line to). For amend / cancel /
1793
+ // standalone-renew, bind to the source sub so per-target validation /
1794
+ // groupings work.
1795
+ targetSubscriptionId: options?.mergeMode ? null : sub.id,
1796
+ sourceSubscriptionItemId: item.id
1797
+ });
1798
+ em.persist(lineConfig);
1799
+ }
1800
+ }
1801
+ async function resolveArcLineTargetSubscriptionId(em, cpqConfig, scope, explicit) {
1802
+ const quoteType = cpqConfig.quoteType ?? "new";
1803
+ if (quoteType === "new") return null;
1804
+ const targets = await em.find(CpqQuoteTargetSubscription, {
1805
+ quoteId: cpqConfig.id,
1806
+ organizationId: scope.organizationId,
1807
+ tenantId: scope.tenantId,
1808
+ deletedAt: null
1809
+ });
1810
+ if (targets.length === 0) return null;
1811
+ const isMerge = quoteType === "renew" && targets.length >= 2 && targets.every((t) => t.mergeAction === "absorb");
1812
+ if (isMerge) return null;
1813
+ if (explicit) {
1814
+ if (!targets.some((t) => t.subscriptionId === explicit)) {
1815
+ throw new QuotingError(
1816
+ 422,
1817
+ `targetSubscriptionId '${explicit}' is not attached to this quote`
1818
+ );
1819
+ }
1820
+ return explicit;
1821
+ }
1822
+ if (targets.length === 1) return targets[0].subscriptionId;
1823
+ throw new QuotingError(
1824
+ 422,
1825
+ `Quote has ${targets.length} attached targets \u2014 specify targetSubscriptionId on the line`
1826
+ );
1827
+ }
1828
+ function pickArcMirrorCharges(item, currencyCode) {
1829
+ const existing = item.charges ?? null;
1830
+ if (existing && existing.length > 0) return [...existing];
1831
+ const sourceQty = Math.max(1, Number(item.quantity) || 1);
1832
+ const mrc = Number(item.mrcAmount) || 0;
1833
+ const nrc = Number(item.nrcAmount ?? 0) || 0;
1834
+ const synth = [];
1835
+ if (mrc > 0) {
1836
+ const perInstance = mrc / sourceQty;
1837
+ synth.push({
1838
+ chargeCode: "mrc",
1839
+ chargeName: "Monthly Recurring",
1840
+ chargeType: "mrc",
1841
+ pricingMethod: "flat",
1842
+ unitPrice: perInstance,
1843
+ quantity: 1,
1844
+ totalPrice: perInstance,
1845
+ currencyCode
1846
+ });
1847
+ }
1848
+ if (nrc > 0) {
1849
+ const perInstance = nrc / sourceQty;
1850
+ synth.push({
1851
+ chargeCode: "nrc",
1852
+ chargeName: "Non-Recurring",
1853
+ chargeType: "nrc",
1854
+ pricingMethod: "flat",
1855
+ unitPrice: perInstance,
1856
+ quantity: 1,
1857
+ totalPrice: perInstance,
1858
+ currencyCode
1859
+ });
1860
+ }
1861
+ return synth.length > 0 ? synth : null;
1862
+ }
1863
+ export {
1864
+ DefaultCpqQuotingService,
1865
+ QuotingError
1866
+ };
1867
+ //# sourceMappingURL=cpqQuotingService.js.map