@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.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +236 -0
- package/README.md +108 -0
- package/dist/modules/cpq/acl.js +30 -0
- package/dist/modules/cpq/acl.js.map +7 -0
- package/dist/modules/cpq/api/_helpers/handleArcError.js +37 -0
- package/dist/modules/cpq/api/_helpers/handleArcError.js.map +7 -0
- package/dist/modules/cpq/api/attribute-options/route.js +47 -0
- package/dist/modules/cpq/api/attribute-options/route.js.map +7 -0
- package/dist/modules/cpq/api/bundle-slots/route.js +101 -0
- package/dist/modules/cpq/api/bundle-slots/route.js.map +7 -0
- package/dist/modules/cpq/api/bundle-tree/route.js +28 -0
- package/dist/modules/cpq/api/bundle-tree/route.js.map +7 -0
- package/dist/modules/cpq/api/context.js +29 -0
- package/dist/modules/cpq/api/context.js.map +7 -0
- package/dist/modules/cpq/api/inventory/assets/route.js +113 -0
- package/dist/modules/cpq/api/inventory/assets/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/assets/status/route.js +35 -0
- package/dist/modules/cpq/api/inventory/assets/status/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.js +30 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/route.js +31 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscription-items/route.js +99 -0
- package/dist/modules/cpq/api/inventory/subscription-items/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.js +57 -0
- package/dist/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/expiring/route.js +62 -0
- package/dist/modules/cpq/api/inventory/subscriptions/expiring/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/route.js +116 -0
- package/dist/modules/cpq/api/inventory/subscriptions/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/status/route.js +35 -0
- package/dist/modules/cpq/api/inventory/subscriptions/status/route.js.map +7 -0
- package/dist/modules/cpq/api/offering-components/route.js +91 -0
- package/dist/modules/cpq/api/offering-components/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +39 -0
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/route.js +28 -0
- package/dist/modules/cpq/api/orders/[orderId]/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js +38 -0
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/route.js +71 -0
- package/dist/modules/cpq/api/orders/route.js.map +7 -0
- package/dist/modules/cpq/api/price-rules/route.js +180 -0
- package/dist/modules/cpq/api/price-rules/route.js.map +7 -0
- package/dist/modules/cpq/api/pricing-table-entries/route.js +142 -0
- package/dist/modules/cpq/api/pricing-table-entries/route.js.map +7 -0
- package/dist/modules/cpq/api/pricing-tables/route.js +179 -0
- package/dist/modules/cpq/api/pricing-tables/route.js.map +7 -0
- package/dist/modules/cpq/api/product-attributes/route.js +160 -0
- package/dist/modules/cpq/api/product-attributes/route.js.map +7 -0
- package/dist/modules/cpq/api/product-charges/route.js +166 -0
- package/dist/modules/cpq/api/product-charges/route.js.map +7 -0
- package/dist/modules/cpq/api/product-offerings/route.js +294 -0
- package/dist/modules/cpq/api/product-offerings/route.js.map +7 -0
- package/dist/modules/cpq/api/product-relationships/route.js +144 -0
- package/dist/modules/cpq/api/product-relationships/route.js.map +7 -0
- package/dist/modules/cpq/api/product-specifications/route.js +292 -0
- package/dist/modules/cpq/api/product-specifications/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.js +71 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +29 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +54 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/merge-meta/route.js +72 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/merge-meta/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +35 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js +102 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +27 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.js +79 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.js +95 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/attributes/route.js +52 -0
- package/dist/modules/cpq/api/quotes/attributes/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/from-subscription/route.js +72 -0
- package/dist/modules/cpq/api/quotes/from-subscription/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/price/route.js +69 -0
- package/dist/modules/cpq/api/quotes/price/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/route.js +97 -0
- package/dist/modules/cpq/api/quotes/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/validate-relationships/route.js +86 -0
- package/dist/modules/cpq/api/quotes/validate-relationships/route.js.map +7 -0
- package/dist/modules/cpq/api/resolveQuotingService.js +7 -0
- package/dist/modules/cpq/api/resolveQuotingService.js.map +7 -0
- package/dist/modules/cpq/api/wizards/resolve/route.js +38 -0
- package/dist/modules/cpq/api/wizards/resolve/route.js.map +7 -0
- package/dist/modules/cpq/api/wizards/route.js +111 -0
- package/dist/modules/cpq/api/wizards/route.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +136 -0
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +185 -0
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +343 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +13 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.js +35 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/shared.js +20 -0
- package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +794 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +13 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +917 -0
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.js +405 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +677 -0
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/orders/page.js +134 -0
- package/dist/modules/cpq/backend/cpq/orders/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +272 -0
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +407 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +13 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.js +211 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +275 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +112 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +492 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.js +50 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +249 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.js +87 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +652 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +1289 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.js +283 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +214 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +1399 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.js +148 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +204 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +191 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.js +119 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.meta.js.map +7 -0
- package/dist/modules/cpq/ce.js +151 -0
- package/dist/modules/cpq/ce.js.map +7 -0
- package/dist/modules/cpq/cli.js +11 -0
- package/dist/modules/cpq/cli.js.map +7 -0
- package/dist/modules/cpq/components/CpqListView.js +265 -0
- package/dist/modules/cpq/components/CpqListView.js.map +7 -0
- package/dist/modules/cpq/components/NumberInput.js +95 -0
- package/dist/modules/cpq/components/NumberInput.js.map +7 -0
- package/dist/modules/cpq/components/StatusPath.js +60 -0
- package/dist/modules/cpq/components/StatusPath.js.map +7 -0
- package/dist/modules/cpq/components/statusMaps.js +66 -0
- package/dist/modules/cpq/components/statusMaps.js.map +7 -0
- package/dist/modules/cpq/components/useCpqRowActions.js +125 -0
- package/dist/modules/cpq/components/useCpqRowActions.js.map +7 -0
- package/dist/modules/cpq/data/entities.js +1556 -0
- package/dist/modules/cpq/data/entities.js.map +7 -0
- package/dist/modules/cpq/data/validators.js +575 -0
- package/dist/modules/cpq/data/validators.js.map +7 -0
- package/dist/modules/cpq/di.js +57 -0
- package/dist/modules/cpq/di.js.map +7 -0
- package/dist/modules/cpq/events.js +50 -0
- package/dist/modules/cpq/events.js.map +7 -0
- package/dist/modules/cpq/index.js +16 -0
- package/dist/modules/cpq/index.js.map +7 -0
- package/dist/modules/cpq/lib/seeds/api.js +32 -0
- package/dist/modules/cpq/lib/seeds/api.js.map +7 -0
- package/dist/modules/cpq/lib/seeds/tenant-provisioning.js +158 -0
- package/dist/modules/cpq/lib/seeds/tenant-provisioning.js.map +7 -0
- package/dist/modules/cpq/lib/seeds.js +6 -0
- package/dist/modules/cpq/lib/seeds.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260220220000_cpq.js +40 -0
- package/dist/modules/cpq/migrations/Migration20260220220000_cpq.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.js +54 -0
- package/dist/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.js +15 -0
- package/dist/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.js +62 -0
- package/dist/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260402000000_cpq_workflows.js +56 -0
- package/dist/modules/cpq/migrations/Migration20260402000000_cpq_workflows.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.js +26 -0
- package/dist/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.js +37 -0
- package/dist/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.js +35 -0
- package/dist/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.js +21 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.js +46 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260410100000_cpq_inventory.js +118 -0
- package/dist/modules/cpq/migrations/Migration20260410100000_cpq_inventory.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260411000000_cpq_orders.js +63 -0
- package/dist/modules/cpq/migrations/Migration20260411000000_cpq_orders.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.js +28 -0
- package/dist/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260504064621_cpq_arc.js +36 -0
- package/dist/modules/cpq/migrations/Migration20260504064621_cpq_arc.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.js +14 -0
- package/dist/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.js +14 -0
- package/dist/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.js +58 -0
- package/dist/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.js +29 -0
- package/dist/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.js.map +7 -0
- package/dist/modules/cpq/services/cpqBundleService.js +447 -0
- package/dist/modules/cpq/services/cpqBundleService.js.map +7 -0
- package/dist/modules/cpq/services/cpqInventoryService.js +1371 -0
- package/dist/modules/cpq/services/cpqInventoryService.js.map +7 -0
- package/dist/modules/cpq/services/cpqOrderService.js +901 -0
- package/dist/modules/cpq/services/cpqOrderService.js.map +7 -0
- package/dist/modules/cpq/services/cpqPricingService.js +289 -0
- package/dist/modules/cpq/services/cpqPricingService.js.map +7 -0
- package/dist/modules/cpq/services/cpqProductService.js +204 -0
- package/dist/modules/cpq/services/cpqProductService.js.map +7 -0
- package/dist/modules/cpq/services/cpqQuotingService.js +1867 -0
- package/dist/modules/cpq/services/cpqQuotingService.js.map +7 -0
- package/dist/modules/cpq/services/cpqValidationService.js +234 -0
- package/dist/modules/cpq/services/cpqValidationService.js.map +7 -0
- package/dist/modules/cpq/services/cpqWizardService.js +137 -0
- package/dist/modules/cpq/services/cpqWizardService.js.map +7 -0
- package/dist/modules/cpq/services/types.js +114 -0
- package/dist/modules/cpq/services/types.js.map +7 -0
- package/dist/modules/cpq/setup.js +49 -0
- package/dist/modules/cpq/setup.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-amended-log.js +18 -0
- package/dist/modules/cpq/subscribers/subscription-amended-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-cancelled-log.js +19 -0
- package/dist/modules/cpq/subscribers/subscription-cancelled-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-merged-log.js +20 -0
- package/dist/modules/cpq/subscribers/subscription-merged-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-renewed-log.js +18 -0
- package/dist/modules/cpq/subscribers/subscription-renewed-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-superseded-log.js +18 -0
- package/dist/modules/cpq/subscribers/subscription-superseded-log.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +192 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.js +17 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +79 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.js +17 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +181 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.js +17 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.js.map +7 -0
- package/dist/modules/cpq/widgets/injection-table.js +44 -0
- package/dist/modules/cpq/widgets/injection-table.js.map +7 -0
- package/dist/modules/cpq/workflows/WizardRunner.js +113 -0
- package/dist/modules/cpq/workflows/WizardRunner.js.map +7 -0
- package/dist/modules/cpq/workflows/WizardTrigger.js +116 -0
- package/dist/modules/cpq/workflows/WizardTrigger.js.map +7 -0
- package/dist/modules/cpq/workflows/engine.js +188 -0
- package/dist/modules/cpq/workflows/engine.js.map +7 -0
- package/dist/modules/cpq/workflows/registry.js +16 -0
- package/dist/modules/cpq/workflows/registry.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +119 -0
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +24 -0
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/ItemListStep.js +115 -0
- package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +327 -0
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +231 -0
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/ReviewStep.js +152 -0
- package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/index.js +49 -0
- package/dist/modules/cpq/workflows/steps/index.js.map +7 -0
- package/dist/modules/cpq/workflows/types.js +1 -0
- package/dist/modules/cpq/workflows/types.js.map +7 -0
- package/package.json +150 -0
- package/src/modules/cpq/__integration__/TC-CPQ-001.spec.ts +177 -0
- package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +123 -0
- package/src/modules/cpq/__integration__/TC-CPQ-003.spec.ts +115 -0
- package/src/modules/cpq/__integration__/TC-CPQ-004.spec.ts +184 -0
- package/src/modules/cpq/__integration__/TC-CPQ-005.spec.ts +43 -0
- package/src/modules/cpq/__integration__/TC-CPQ-006.spec.ts +87 -0
- package/src/modules/cpq/__integration__/TC-CPQ-007.spec.ts +79 -0
- package/src/modules/cpq/__integration__/TC-CPQ-008.spec.ts +55 -0
- package/src/modules/cpq/__integration__/TC-CPQ-009.spec.ts +45 -0
- package/src/modules/cpq/__integration__/TC-CPQ-010.spec.ts +54 -0
- package/src/modules/cpq/__integration__/TC-CPQ-011.spec.ts +24 -0
- package/src/modules/cpq/__integration__/TC-CPQ-012.spec.ts +62 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-001.spec.ts +52 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-002.spec.ts +65 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-003.spec.ts +107 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-004.spec.ts +74 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-005.spec.ts +86 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-006.spec.ts +79 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-007.spec.ts +63 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-008.spec.ts +62 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-009.spec.ts +62 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-010.spec.ts +59 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-011.spec.ts +128 -0
- package/src/modules/cpq/__integration__/_helpers/cpqArcFixtures.ts +208 -0
- package/src/modules/cpq/__tests__/package-isolation.test.ts +232 -0
- package/src/modules/cpq/acl.ts +26 -0
- package/src/modules/cpq/api/_helpers/__tests__/handleArcError.test.ts +117 -0
- package/src/modules/cpq/api/_helpers/handleArcError.ts +81 -0
- package/src/modules/cpq/api/attribute-options/route.ts +55 -0
- package/src/modules/cpq/api/bundle-slots/route.ts +112 -0
- package/src/modules/cpq/api/bundle-tree/route.ts +31 -0
- package/src/modules/cpq/api/context.ts +39 -0
- package/src/modules/cpq/api/inventory/assets/route.ts +123 -0
- package/src/modules/cpq/api/inventory/assets/status/route.ts +34 -0
- package/src/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.ts +30 -0
- package/src/modules/cpq/api/inventory/customer/[customerId]/route.ts +31 -0
- package/src/modules/cpq/api/inventory/subscription-items/route.ts +107 -0
- package/src/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.ts +64 -0
- package/src/modules/cpq/api/inventory/subscriptions/expiring/route.ts +66 -0
- package/src/modules/cpq/api/inventory/subscriptions/route.ts +126 -0
- package/src/modules/cpq/api/inventory/subscriptions/status/route.ts +34 -0
- package/src/modules/cpq/api/offering-components/route.ts +103 -0
- package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +48 -0
- package/src/modules/cpq/api/orders/[orderId]/route.ts +28 -0
- package/src/modules/cpq/api/orders/[orderId]/status/route.ts +38 -0
- package/src/modules/cpq/api/orders/route.ts +77 -0
- package/src/modules/cpq/api/price-rules/route.ts +210 -0
- package/src/modules/cpq/api/pricing-table-entries/route.ts +159 -0
- package/src/modules/cpq/api/pricing-tables/route.ts +202 -0
- package/src/modules/cpq/api/product-attributes/route.ts +175 -0
- package/src/modules/cpq/api/product-charges/route.ts +181 -0
- package/src/modules/cpq/api/product-offerings/route.ts +329 -0
- package/src/modules/cpq/api/product-relationships/route.ts +156 -0
- package/src/modules/cpq/api/product-specifications/route.ts +325 -0
- package/src/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.ts +77 -0
- package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +28 -0
- package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +55 -0
- package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +26 -0
- package/src/modules/cpq/api/quotes/[quoteId]/merge-meta/route.ts +78 -0
- package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +34 -0
- package/src/modules/cpq/api/quotes/[quoteId]/route.ts +109 -0
- package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +26 -0
- package/src/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.ts +91 -0
- package/src/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.ts +107 -0
- package/src/modules/cpq/api/quotes/attributes/route.ts +61 -0
- package/src/modules/cpq/api/quotes/from-subscription/route.ts +85 -0
- package/src/modules/cpq/api/quotes/price/route.ts +81 -0
- package/src/modules/cpq/api/quotes/route.ts +114 -0
- package/src/modules/cpq/api/quotes/validate-relationships/route.ts +100 -0
- package/src/modules/cpq/api/resolveQuotingService.ts +11 -0
- package/src/modules/cpq/api/wizards/resolve/route.ts +39 -0
- package/src/modules/cpq/api/wizards/route.ts +127 -0
- package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +166 -0
- package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +251 -0
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +9 -0
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +440 -0
- package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/inventory/page.tsx +49 -0
- package/src/modules/cpq/backend/cpq/inventory/shared.tsx +32 -0
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +9 -0
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +1085 -0
- package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +1165 -0
- package/src/modules/cpq/backend/cpq/offerings/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/offerings/page.tsx +468 -0
- package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +921 -0
- package/src/modules/cpq/backend/cpq/orders/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/orders/page.tsx +182 -0
- package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +310 -0
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +9 -0
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +472 -0
- package/src/modules/cpq/backend/cpq/price-rules/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/price-rules/page.tsx +253 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +431 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +122 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +588 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.ts +90 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +338 -0
- package/src/modules/cpq/backend/cpq/pricing/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/pricing/page.tsx +111 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +737 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +1721 -0
- package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/quotes/page.tsx +370 -0
- package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +300 -0
- package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +1722 -0
- package/src/modules/cpq/backend/cpq/specifications/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/specifications/page.tsx +191 -0
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +319 -0
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +234 -0
- package/src/modules/cpq/backend/cpq/wizards/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/wizards/page.tsx +141 -0
- package/src/modules/cpq/ce.ts +150 -0
- package/src/modules/cpq/cli.ts +10 -0
- package/src/modules/cpq/components/CpqListView.tsx +431 -0
- package/src/modules/cpq/components/NumberInput.tsx +166 -0
- package/src/modules/cpq/components/StatusPath.tsx +116 -0
- package/src/modules/cpq/components/statusMaps.ts +148 -0
- package/src/modules/cpq/components/useCpqRowActions.tsx +183 -0
- package/src/modules/cpq/data/__tests__/validators.arc.test.ts +271 -0
- package/src/modules/cpq/data/__tests__/validators.test.ts +782 -0
- package/src/modules/cpq/data/entities.ts +1467 -0
- package/src/modules/cpq/data/validators.ts +663 -0
- package/src/modules/cpq/di.ts +70 -0
- package/src/modules/cpq/events.ts +62 -0
- package/src/modules/cpq/index.ts +15 -0
- package/src/modules/cpq/lib/seeds/__tests__/api.test.ts +86 -0
- package/src/modules/cpq/lib/seeds/__tests__/tenant-provisioning.test.ts +349 -0
- package/src/modules/cpq/lib/seeds/api.ts +119 -0
- package/src/modules/cpq/lib/seeds/tenant-provisioning.ts +239 -0
- package/src/modules/cpq/lib/seeds.ts +18 -0
- package/src/modules/cpq/migrations/.snapshot-open-mercato.json +7473 -0
- package/src/modules/cpq/migrations/Migration20260220220000_cpq.ts +51 -0
- package/src/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.ts +70 -0
- package/src/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.ts +15 -0
- package/src/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.ts +73 -0
- package/src/modules/cpq/migrations/Migration20260402000000_cpq_workflows.ts +59 -0
- package/src/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.ts +30 -0
- package/src/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.ts +36 -0
- package/src/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.ts +33 -0
- package/src/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.ts +22 -0
- package/src/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.ts +56 -0
- package/src/modules/cpq/migrations/Migration20260410100000_cpq_inventory.ts +118 -0
- package/src/modules/cpq/migrations/Migration20260411000000_cpq_orders.ts +62 -0
- package/src/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.ts +33 -0
- package/src/modules/cpq/migrations/Migration20260504064621_cpq_arc.ts +68 -0
- package/src/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.ts +30 -0
- package/src/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.ts +27 -0
- package/src/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.ts +82 -0
- package/src/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.ts +52 -0
- package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +59 -0
- package/src/modules/cpq/services/__tests__/cpqBundleService.test.ts +661 -0
- package/src/modules/cpq/services/__tests__/cpqInventoryService.arc.test.ts +357 -0
- package/src/modules/cpq/services/__tests__/cpqInventoryService.test.ts +718 -0
- package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +603 -0
- package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +1012 -0
- package/src/modules/cpq/services/__tests__/cpqProductService.test.ts +603 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +143 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +658 -0
- package/src/modules/cpq/services/__tests__/cpqValidationService.test.ts +811 -0
- package/src/modules/cpq/services/__tests__/cpqWizardService.test.ts +389 -0
- package/src/modules/cpq/services/__tests__/types.test.ts +359 -0
- package/src/modules/cpq/services/cpqBundleService.ts +671 -0
- package/src/modules/cpq/services/cpqInventoryService.ts +1929 -0
- package/src/modules/cpq/services/cpqOrderService.ts +1230 -0
- package/src/modules/cpq/services/cpqPricingService.ts +412 -0
- package/src/modules/cpq/services/cpqProductService.ts +311 -0
- package/src/modules/cpq/services/cpqQuotingService.ts +2638 -0
- package/src/modules/cpq/services/cpqValidationService.ts +323 -0
- package/src/modules/cpq/services/cpqWizardService.ts +204 -0
- package/src/modules/cpq/services/types.ts +303 -0
- package/src/modules/cpq/setup.ts +63 -0
- package/src/modules/cpq/subscribers/subscription-amended-log.ts +20 -0
- package/src/modules/cpq/subscribers/subscription-cancelled-log.ts +19 -0
- package/src/modules/cpq/subscribers/subscription-merged-log.ts +22 -0
- package/src/modules/cpq/subscribers/subscription-renewed-log.ts +18 -0
- package/src/modules/cpq/subscribers/subscription-superseded-log.ts +19 -0
- package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +339 -0
- package/src/modules/cpq/widgets/injection/customer-inventory/widget.ts +16 -0
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +96 -0
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.ts +16 -0
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +265 -0
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.ts +16 -0
- package/src/modules/cpq/widgets/injection-table.ts +42 -0
- package/src/modules/cpq/workflows/WizardRunner.tsx +158 -0
- package/src/modules/cpq/workflows/WizardTrigger.tsx +142 -0
- package/src/modules/cpq/workflows/engine.tsx +278 -0
- package/src/modules/cpq/workflows/registry.ts +15 -0
- package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +136 -0
- package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +34 -0
- package/src/modules/cpq/workflows/steps/ItemListStep.tsx +146 -0
- package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +438 -0
- package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +287 -0
- package/src/modules/cpq/workflows/steps/ReviewStep.tsx +219 -0
- package/src/modules/cpq/workflows/steps/index.ts +51 -0
- 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
|