@dainamite/cpq 0.4.0 → 0.6.0
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/MIGRATION.md +101 -0
- package/dist/modules/cpq/acl.js +1 -0
- package/dist/modules/cpq/acl.js.map +2 -2
- package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js +45 -0
- package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js.map +7 -0
- package/dist/modules/cpq/api/_helpers/handleArcError.js +2 -2
- package/dist/modules/cpq/api/_helpers/handleArcError.js.map +2 -2
- package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
- package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
- package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
- package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/route.js +19 -3
- package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/route.js +49 -4
- package/dist/modules/cpq/api/orders/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +45 -5
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +34 -4
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/price/route.js +43 -3
- package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/route.js +2 -2
- package/dist/modules/cpq/api/quotes/route.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +22 -8
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +89 -232
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/pricing/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +205 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +144 -221
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
- package/dist/modules/cpq/backend/cpq/quotes/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/specifications/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/wizards/page.js.map +1 -1
- package/dist/modules/cpq/ce.js +8 -0
- package/dist/modules/cpq/ce.js.map +2 -2
- package/dist/modules/cpq/cli.js +34 -1
- package/dist/modules/cpq/cli.js.map +2 -2
- package/dist/modules/cpq/commands/price-adjustments.js +107 -0
- package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
- package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
- package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
- package/dist/modules/cpq/components/ChargeBreakdownTable.js +154 -0
- package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
- package/dist/modules/cpq/components/CpqListView.js.map +2 -2
- package/dist/modules/cpq/components/PriceWaterfall.js +66 -0
- package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js +117 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
- package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
- package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
- package/dist/modules/cpq/data/entities.js +97 -0
- package/dist/modules/cpq/data/entities.js.map +2 -2
- package/dist/modules/cpq/data/price-adjustments.js +70 -0
- package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
- package/dist/modules/cpq/data/validators.js +4 -0
- package/dist/modules/cpq/data/validators.js.map +2 -2
- package/dist/modules/cpq/di.js +6 -0
- package/dist/modules/cpq/di.js.map +2 -2
- package/dist/modules/cpq/events.js +47 -0
- package/dist/modules/cpq/events.js.map +2 -2
- package/dist/modules/cpq/index.js +1 -1
- package/dist/modules/cpq/index.js.map +2 -2
- package/dist/modules/cpq/lib/auditActor.js +10 -0
- package/dist/modules/cpq/lib/auditActor.js.map +7 -0
- package/dist/modules/cpq/lib/formatMoney.js +7 -0
- package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
- package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
- package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
- package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
- package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
- package/dist/modules/cpq/lib/quotePermissions.js +13 -0
- package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
- package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
- package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
- package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
- package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
- package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
- package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
- package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
- package/dist/modules/cpq/services/cpqOrderService.js +34 -30
- package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
- package/dist/modules/cpq/services/cpqPricingService.js +7 -0
- package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
- package/dist/modules/cpq/services/cpqQuotingService.js +339 -111
- package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
- package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
- package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
- package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
- package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
- package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
- package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
- package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
- package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
- package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
- package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
- package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
- package/dist/modules/cpq/services/types.js.map +2 -2
- package/dist/modules/cpq/setup.js +6 -0
- package/dist/modules/cpq/setup.js.map +2 -2
- package/package.json +9 -9
- package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +8 -5
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
- package/src/modules/cpq/acl.ts +1 -0
- package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
- package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
- package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
- package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
- package/src/modules/cpq/api/_helpers/handleArcError.ts +12 -12
- package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
- package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
- package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +2 -2
- package/src/modules/cpq/api/orders/[orderId]/route.ts +20 -2
- package/src/modules/cpq/api/orders/[orderId]/status/route.ts +2 -2
- package/src/modules/cpq/api/orders/route.ts +53 -3
- package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +2 -2
- package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
- package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +51 -4
- package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
- package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
- package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
- package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
- package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
- package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +42 -3
- package/src/modules/cpq/api/quotes/[quoteId]/route.ts +2 -2
- package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
- package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
- package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
- package/src/modules/cpq/api/quotes/price/route.ts +28 -2
- package/src/modules/cpq/api/quotes/route.ts +2 -2
- package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +1 -1
- package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +1 -1
- package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +32 -10
- package/src/modules/cpq/backend/cpq/offerings/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +60 -269
- package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/orders/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/price-rules/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +178 -266
- package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/quotes/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/specifications/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
- package/src/modules/cpq/ce.ts +8 -0
- package/src/modules/cpq/cli.ts +48 -1
- package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
- package/src/modules/cpq/commands/price-adjustments.ts +90 -0
- package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
- package/src/modules/cpq/components/ChargeBreakdownTable.tsx +201 -0
- package/src/modules/cpq/components/CpqListView.tsx +2 -1
- package/src/modules/cpq/components/PriceWaterfall.tsx +94 -0
- package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
- package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
- package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
- package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
- package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
- package/src/modules/cpq/data/entities.ts +83 -0
- package/src/modules/cpq/data/price-adjustments.ts +67 -0
- package/src/modules/cpq/data/validators.ts +3 -0
- package/src/modules/cpq/di.ts +6 -0
- package/src/modules/cpq/events.ts +53 -2
- package/src/modules/cpq/index.ts +1 -1
- package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
- package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
- package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
- package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
- package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
- package/src/modules/cpq/lib/auditActor.ts +13 -0
- package/src/modules/cpq/lib/formatMoney.ts +4 -0
- package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
- package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
- package/src/modules/cpq/lib/quotePermissions.ts +22 -0
- package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
- package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
- package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
- package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
- package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
- package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
- package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
- package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
- package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
- package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
- package/src/modules/cpq/services/__tests__/errorBrands.test.ts +72 -0
- package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
- package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
- package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
- package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
- package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
- package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
- package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
- package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
- package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
- package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
- package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
- package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
- package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
- package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
- package/src/modules/cpq/services/cpqOrderService.ts +32 -28
- package/src/modules/cpq/services/cpqPricingService.ts +8 -0
- package/src/modules/cpq/services/cpqQuotingService.ts +388 -125
- package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
- package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
- package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
- package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
- package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
- package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
- package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
- package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
- package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
- package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
- package/src/modules/cpq/services/types.ts +34 -1
- package/src/modules/cpq/setup.ts +7 -1
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { cloneQuoteAdjustments, removeQuoteLineTree } from './priceAdjustments/quoteAdjustmentLifecycle'
|
|
2
|
+
import type { PriceAdjustment } from '../data/price-adjustments'
|
|
3
|
+
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
4
|
+
import { loadQuoteAdjustments } from './priceAdjustments/loadQuoteAdjustments'
|
|
5
|
+
import { resolveManualAdjustments } from './priceAdjustments/resolveManualAdjustments'
|
|
6
|
+
import { withChargeWaterfall } from './priceAdjustments/chargeWaterfall'
|
|
7
|
+
import { withQuotePricingWrite } from './priceAdjustments/quotePricingWrite'
|
|
8
|
+
import { emitQuoteWriteEvents, type QuoteWriteEvents } from './priceAdjustments/quotePricingEvents'
|
|
9
|
+
import { computeWaterfallSummary } from './priceAdjustments/pricingSnapshot'
|
|
1
10
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
11
|
import { SalesQuote, SalesQuoteLine } from '@open-mercato/core/modules/sales/data/entities'
|
|
3
12
|
import { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'
|
|
@@ -36,6 +45,17 @@ import {
|
|
|
36
45
|
ARC_QUOTE_TYPES,
|
|
37
46
|
getBaseCurrencyCode,
|
|
38
47
|
} from './types'
|
|
48
|
+
import {
|
|
49
|
+
ARC_PRICE_FROZEN_KEY,
|
|
50
|
+
ARC_REPRICED_KEY,
|
|
51
|
+
arcPricingInputsChanged,
|
|
52
|
+
buildArcMirrorConfiguration,
|
|
53
|
+
isFrozenAmendMirrorLine,
|
|
54
|
+
isArcMirrorLine,
|
|
55
|
+
stripArcMarkers,
|
|
56
|
+
stripQuoteOnlyArcMarkers,
|
|
57
|
+
} from './arcPriceFreeze'
|
|
58
|
+
import { isArcCancelledLine, planArcLineRemoval, planArcLineRestore, removalCancelsItem } from './arcLineRemoval'
|
|
39
59
|
|
|
40
60
|
interface QuotingServiceDeps {
|
|
41
61
|
em: EntityManager
|
|
@@ -169,15 +189,32 @@ export class DefaultCpqQuotingService {
|
|
|
169
189
|
// ─── Add Quote Item ────────────────────────────────────────────
|
|
170
190
|
|
|
171
191
|
async addQuoteItem(quoteId: string, item: QuoteItemInput, scope: TenantScope): Promise<QuoteResult> {
|
|
172
|
-
|
|
192
|
+
return this.writeQuote(quoteId, scope, (em, quote) => this.addQuoteItemInTransaction(quoteId, item, scope, em, quote), undefined, undefined, LINE_EVENTS)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async addQuoteItemInTransaction(quoteId: string, item: QuoteItemInput, scope: TenantScope, em: EntityManager, cpqConfig: CpqQuoteConfiguration): Promise<QuoteResult> {
|
|
196
|
+
// ARC markers (`_arc*`) are server-owned — never accept them from callers.
|
|
197
|
+
item = { ...item, configuration: stripArcMarkers(item.configuration) }
|
|
173
198
|
|
|
174
|
-
const cpqConfig = await this.loadCpqConfig(em, quoteId, scope)
|
|
175
199
|
const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope)
|
|
176
200
|
|
|
177
201
|
if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus as CpqStatus)) {
|
|
178
202
|
throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`)
|
|
179
203
|
}
|
|
180
204
|
|
|
205
|
+
if (item.parentLineId && cpqConfig.quoteType && cpqConfig.quoteType !== 'new') {
|
|
206
|
+
const parent = await em.findOne(CpqQuoteLineConfiguration, {
|
|
207
|
+
quoteLineId: item.parentLineId,
|
|
208
|
+
quoteConfigurationId: cpqConfig.id,
|
|
209
|
+
organizationId: scope.organizationId,
|
|
210
|
+
tenantId: scope.tenantId,
|
|
211
|
+
deletedAt: null,
|
|
212
|
+
})
|
|
213
|
+
if (parent && isArcCancelledLine(parent, cpqConfig.quoteType)) {
|
|
214
|
+
throw new QuotingError(409, 'Cannot add a component to a subscription item marked for removal')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
181
218
|
// Resolve offering -> spec -> product
|
|
182
219
|
const { offering, spec } = await this.resolveOffering(em, item, scope)
|
|
183
220
|
|
|
@@ -322,15 +359,23 @@ export class DefaultCpqQuotingService {
|
|
|
322
359
|
|
|
323
360
|
// ─── Update Quote Item ─────────────────────────────────────────
|
|
324
361
|
|
|
325
|
-
async updateQuoteItem(
|
|
362
|
+
async updateQuoteItem(quoteId: string,
|
|
363
|
+
lineId: string,
|
|
364
|
+
update: { configuration: Record<string, unknown>; quantity?: number; usageEstimates?: Array<{ chargeCode: string; estimatedQuantity: number }>; startDate?: string; termMonths?: number; endDate?: string },
|
|
365
|
+
scope: TenantScope): Promise<QuoteResult> {
|
|
366
|
+
return this.writeQuote(quoteId, scope, (em, quote) => this.updateQuoteItemInTransaction(quoteId, lineId, update, scope, em, quote), undefined, undefined, LINE_EVENTS)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async updateQuoteItemInTransaction(
|
|
326
370
|
quoteId: string,
|
|
327
371
|
lineId: string,
|
|
328
372
|
update: { configuration: Record<string, unknown>; quantity?: number; usageEstimates?: Array<{ chargeCode: string; estimatedQuantity: number }>; startDate?: string; termMonths?: number; endDate?: string },
|
|
329
|
-
scope: TenantScope,
|
|
330
|
-
|
|
331
|
-
|
|
373
|
+
scope: TenantScope, em: EntityManager, cpqConfig: CpqQuoteConfiguration): Promise<QuoteResult> {
|
|
374
|
+
// ARC markers (`_arc*`) are server-owned: the quote page echoes the line's
|
|
375
|
+
// stored configuration (markers included) back, so drop them here and
|
|
376
|
+
// re-apply the server's own markers below.
|
|
377
|
+
update = { ...update, configuration: stripArcMarkers(update.configuration) }
|
|
332
378
|
|
|
333
|
-
const cpqConfig = await this.loadCpqConfig(em, quoteId, scope)
|
|
334
379
|
const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope)
|
|
335
380
|
|
|
336
381
|
if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus as CpqStatus)) {
|
|
@@ -345,6 +390,9 @@ export class DefaultCpqQuotingService {
|
|
|
345
390
|
deletedAt: null,
|
|
346
391
|
})
|
|
347
392
|
if (!lineConfig) throw new QuotingError(404, 'Quote line not found')
|
|
393
|
+
if (isArcCancelledLine(lineConfig, cpqConfig.quoteType)) {
|
|
394
|
+
throw new QuotingError(409, 'This subscription item is marked for removal — restore it before changing it')
|
|
395
|
+
}
|
|
348
396
|
|
|
349
397
|
const quoteLine = await em.findOne(SalesQuoteLine, {
|
|
350
398
|
id: lineId,
|
|
@@ -386,10 +434,11 @@ export class DefaultCpqQuotingService {
|
|
|
386
434
|
validationErrors = validation.errors
|
|
387
435
|
}
|
|
388
436
|
|
|
389
|
-
// Preserve XD-250 ARC markers across edits —
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// item" rendering
|
|
437
|
+
// Preserve XD-250 ARC markers across edits — client-sent markers were
|
|
438
|
+
// stripped above, so carry `_arcMirroredFromItemId` / `_arcMirroredName`
|
|
439
|
+
// over from the stored configuration (downstream "Mirrored from existing
|
|
440
|
+
// subscription item" rendering plus the recalc / validate paths key off
|
|
441
|
+
// them).
|
|
393
442
|
const existingCfg = lineConfig.configuration as Record<string, unknown> | null
|
|
394
443
|
const arcMarkers: Record<string, unknown> = {}
|
|
395
444
|
if (existingCfg?._arcMirroredFromItemId !== undefined) {
|
|
@@ -398,6 +447,26 @@ export class DefaultCpqQuotingService {
|
|
|
398
447
|
if (existingCfg?._arcMirroredName !== undefined) {
|
|
399
448
|
arcMarkers._arcMirroredName = existingCfg._arcMirroredName
|
|
400
449
|
}
|
|
450
|
+
// A frozen Amend mirror line keeps the subscription's stored price until
|
|
451
|
+
// a pricing input changes; then it is unfrozen, marked re-priced and
|
|
452
|
+
// priced as usual.
|
|
453
|
+
const wasFrozen = isFrozenAmendMirrorLine(existingCfg, cpqConfig.quoteType)
|
|
454
|
+
const pricingInputsChanged =
|
|
455
|
+
wasFrozen &&
|
|
456
|
+
arcPricingInputsChanged(
|
|
457
|
+
{
|
|
458
|
+
configuration: existingCfg,
|
|
459
|
+
quantity: lineConfig.quantity,
|
|
460
|
+
termMonths: lineConfig.termMonths,
|
|
461
|
+
startDate: lineConfig.startDate,
|
|
462
|
+
endDate: lineConfig.endDate,
|
|
463
|
+
usageEstimates: lineConfig.usageEstimates,
|
|
464
|
+
},
|
|
465
|
+
update,
|
|
466
|
+
)
|
|
467
|
+
const staysFrozen = wasFrozen && !pricingInputsChanged
|
|
468
|
+
if (staysFrozen) arcMarkers[ARC_PRICE_FROZEN_KEY] = true
|
|
469
|
+
if (pricingInputsChanged || existingCfg?.[ARC_REPRICED_KEY] === true) arcMarkers[ARC_REPRICED_KEY] = true
|
|
401
470
|
update.configuration = { ...update.configuration, ...arcMarkers }
|
|
402
471
|
mergedConfig._arcMirroredFromItemId = arcMarkers._arcMirroredFromItemId
|
|
403
472
|
mergedConfig._arcMirroredName = arcMarkers._arcMirroredName
|
|
@@ -409,7 +478,7 @@ export class DefaultCpqQuotingService {
|
|
|
409
478
|
// to "incomplete" with mrcTotal=0.
|
|
410
479
|
const isArcMirrored = !!(arcMarkers._arcMirroredFromItemId)
|
|
411
480
|
let charges: ResolvedCharge[]
|
|
412
|
-
if (lineConfig.offeringId || lineConfig.productId) {
|
|
481
|
+
if ((lineConfig.offeringId || lineConfig.productId) && !staysFrozen) {
|
|
413
482
|
charges = await this.pricingService.resolveProductCharges({
|
|
414
483
|
offeringId: lineConfig.offeringId ?? undefined,
|
|
415
484
|
productId: lineConfig.productId ?? undefined,
|
|
@@ -467,9 +536,11 @@ export class DefaultCpqQuotingService {
|
|
|
467
536
|
// ─── Remove Quote Item ─────────────────────────────────────────
|
|
468
537
|
|
|
469
538
|
async removeQuoteItem(quoteId: string, lineId: string, scope: TenantScope): Promise<QuoteResult> {
|
|
470
|
-
|
|
539
|
+
return this.writeQuote(quoteId, scope, (em, quote) => this.removeQuoteItemInTransaction(quoteId, lineId, scope, em, quote), undefined, undefined, LINE_EVENTS)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private async removeQuoteItemInTransaction(quoteId: string, lineId: string, scope: TenantScope, em: EntityManager, cpqConfig: CpqQuoteConfiguration): Promise<QuoteResult> {
|
|
471
543
|
|
|
472
|
-
const cpqConfig = await this.loadCpqConfig(em, quoteId, scope)
|
|
473
544
|
const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope)
|
|
474
545
|
|
|
475
546
|
if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus as CpqStatus)) {
|
|
@@ -486,37 +557,92 @@ export class DefaultCpqQuotingService {
|
|
|
486
557
|
})
|
|
487
558
|
if (!lineConfig) throw new QuotingError(404, 'Quote line not found')
|
|
488
559
|
|
|
489
|
-
|
|
490
|
-
|
|
560
|
+
if (removalCancelsItem(lineConfig, cpqConfig.quoteType)) {
|
|
561
|
+
// Amend / Renew: a line mirroring a live subscription item becomes a
|
|
562
|
+
// `cancel` line so activation terminates the item (see arcLineRemoval.ts).
|
|
563
|
+
if (lineConfig.action === 'cancel') {
|
|
564
|
+
throw new QuotingError(409, 'This subscription item is already marked for removal')
|
|
565
|
+
}
|
|
566
|
+
const plan = planArcLineRemoval(lineConfig, await loadLines(em, cpqConfig.id, scope), cpqConfig.quoteType)
|
|
567
|
+
for (const root of plan.removeRoots) await removeQuoteLineTree(em, cpqConfig.id, root, scope)
|
|
568
|
+
await withAtomicFlush(em, [() => {
|
|
569
|
+
for (const line of plan.cancel) {
|
|
570
|
+
line.action = 'cancel'
|
|
571
|
+
line.charges = []
|
|
572
|
+
line.nrcTotal = '0'
|
|
573
|
+
line.mrcTotal = '0'
|
|
574
|
+
line.isConfigured = true
|
|
575
|
+
line.validationErrors = null
|
|
576
|
+
}
|
|
577
|
+
}])
|
|
578
|
+
} else {
|
|
579
|
+
await removeQuoteLineTree(em, cpqConfig.id, lineConfig, scope)
|
|
580
|
+
}
|
|
491
581
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
tenantId: scope.tenantId,
|
|
507
|
-
})
|
|
508
|
-
if (childSalesLine) childSalesLine.deletedAt = now
|
|
582
|
+
return this.recalculateInternal(em, cpqConfig, salesQuote, scope, true)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ─── Restore a removed subscription item (Amend / Renew) ───────
|
|
586
|
+
|
|
587
|
+
async restoreQuoteItem(quoteId: string, lineId: string, scope: TenantScope): Promise<QuoteResult> {
|
|
588
|
+
return this.writeQuote(quoteId, scope, (em, quote) => this.restoreQuoteItemInTransaction(lineId, scope, em, quote), undefined, undefined, LINE_EVENTS)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private async restoreQuoteItemInTransaction(lineId: string, scope: TenantScope, em: EntityManager, cpqConfig: CpqQuoteConfiguration): Promise<QuoteResult> {
|
|
592
|
+
const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope)
|
|
593
|
+
|
|
594
|
+
if (TERMINAL_STATUSES.includes(cpqConfig.cpqStatus as CpqStatus)) {
|
|
595
|
+
throw new QuotingError(409, `Cannot modify quote in '${cpqConfig.cpqStatus}' status`)
|
|
509
596
|
}
|
|
510
597
|
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
598
|
+
const lines = await loadLines(em, cpqConfig.id, scope)
|
|
599
|
+
const lineConfig = lines.find((line) => line.quoteLineId === lineId)
|
|
600
|
+
if (!lineConfig) throw new QuotingError(404, 'Quote line not found')
|
|
601
|
+
if (!isArcCancelledLine(lineConfig, cpqConfig.quoteType) || !isArcMirrorLine(lineConfig.configuration, cpqConfig.quoteType)) {
|
|
602
|
+
throw new QuotingError(409, 'Only subscription items marked for removal can be restored')
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const plan = planArcLineRestore(lineConfig, lines, cpqConfig.quoteType)
|
|
606
|
+
if (!plan.ok) throw new QuotingError(409, 'Restore the parent item first')
|
|
607
|
+
|
|
608
|
+
const itemIdOf = (line: CpqQuoteLineConfiguration) =>
|
|
609
|
+
line.sourceSubscriptionItemId ?? ((line.configuration as Record<string, unknown> | null)?._arcMirroredFromItemId as string | undefined) ?? null
|
|
610
|
+
const itemIds = plan.restore.map(itemIdOf).filter((id): id is string => !!id)
|
|
611
|
+
const items = itemIds.length
|
|
612
|
+
? await em.find(CpqInventorySubscriptionItem, { id: { $in: itemIds }, organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null })
|
|
613
|
+
: []
|
|
614
|
+
const itemById = new Map(items.map((item) => [item.id, item]))
|
|
615
|
+
const salesLines = await em.find(SalesQuoteLine, {
|
|
616
|
+
id: { $in: plan.restore.map((line) => line.quoteLineId) },
|
|
514
617
|
organizationId: scope.organizationId,
|
|
515
618
|
tenantId: scope.tenantId,
|
|
619
|
+
deletedAt: null,
|
|
516
620
|
})
|
|
517
|
-
|
|
621
|
+
const salesLineById = new Map(salesLines.map((line) => [line.id, line]))
|
|
518
622
|
|
|
519
|
-
|
|
623
|
+
const restores = plan.restore.map((line) => {
|
|
624
|
+
const itemId = itemIdOf(line)
|
|
625
|
+
const item = itemId ? itemById.get(itemId) : undefined
|
|
626
|
+
if (!item || ['terminated', 'expired', 'superseded'].includes(item.status)) {
|
|
627
|
+
throw new QuotingError(409, 'The subscription item is no longer active and cannot be restored')
|
|
628
|
+
}
|
|
629
|
+
return { line, item }
|
|
630
|
+
})
|
|
631
|
+
const arcType = cpqConfig.quoteType as 'amend' | 'renew'
|
|
632
|
+
await withAtomicFlush(em, [() => {
|
|
633
|
+
for (const { line, item } of restores) {
|
|
634
|
+
line.action = 'modify'
|
|
635
|
+
line.configuration = buildArcMirrorConfiguration(item.configuration, item, arcType)
|
|
636
|
+
line.charges = pickArcMirrorCharges(item, cpqConfig.currencyCode)
|
|
637
|
+
line.quantity = item.quantity
|
|
638
|
+
line.nrcTotal = String(item.nrcAmount ?? 0)
|
|
639
|
+
line.mrcTotal = String(item.mrcAmount)
|
|
640
|
+
line.isConfigured = true
|
|
641
|
+
line.validationErrors = null
|
|
642
|
+
const salesLine = salesLineById.get(line.quoteLineId)
|
|
643
|
+
if (salesLine) salesLine.quantity = String(item.quantity)
|
|
644
|
+
}
|
|
645
|
+
}])
|
|
520
646
|
|
|
521
647
|
return this.recalculateInternal(em, cpqConfig, salesQuote, scope, true)
|
|
522
648
|
}
|
|
@@ -542,20 +668,46 @@ export class DefaultCpqQuotingService {
|
|
|
542
668
|
// ─── Recalculate ───────────────────────────────────────────────
|
|
543
669
|
|
|
544
670
|
async recalculate(quoteId: string, options: { save: boolean }, scope: TenantScope): Promise<QuoteResult> {
|
|
545
|
-
|
|
671
|
+
if (options.save) {
|
|
672
|
+
return this.writeQuote(quoteId, scope, (em, quote) => this.recalculateFromConfig(em, quote, scope, true), undefined,
|
|
673
|
+
(quote) => {
|
|
674
|
+
if (TERMINAL_STATUSES.includes(quote.cpqStatus as CpqStatus)) throw new QuotingError(409, `Cannot recalculate quote in '${quote.cpqStatus}' status`)
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
const cpqConfig = await this.loadCpqConfig(this.em, quoteId, scope)
|
|
678
|
+
return this.recalculateFromConfig(this.em, { ...cpqConfig } as CpqQuoteConfiguration, scope, false)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private async recalculateFromConfig(em: EntityManager, cpqConfig: CpqQuoteConfiguration, scope: TenantScope, save: boolean): Promise<QuoteResult> {
|
|
682
|
+
|
|
683
|
+
// A terminal quote's stored prices are final (accepted quotes are the
|
|
684
|
+
// basis for order conversion) — a saved recalculation must not move them.
|
|
685
|
+
if (save && TERMINAL_STATUSES.includes(cpqConfig.cpqStatus as CpqStatus)) {
|
|
686
|
+
throw new QuotingError(409, `Cannot recalculate quote in '${cpqConfig.cpqStatus}' status`)
|
|
687
|
+
}
|
|
546
688
|
|
|
547
|
-
const cpqConfig = await this.loadCpqConfig(em, quoteId, scope)
|
|
548
689
|
const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope)
|
|
549
690
|
|
|
550
|
-
return this.recalculateInternal(em, cpqConfig, salesQuote, scope,
|
|
691
|
+
return this.recalculateInternal(em, cpqConfig, salesQuote, scope, save)
|
|
551
692
|
}
|
|
552
693
|
|
|
553
694
|
// ─── Clone Quote ───────────────────────────────────────────────
|
|
554
695
|
|
|
555
696
|
async cloneQuote(quoteId: string, scope: TenantScope): Promise<QuoteResult> {
|
|
556
|
-
const
|
|
697
|
+
const created: { quote?: CpqQuoteConfiguration } = {}
|
|
698
|
+
const result = await this.writeQuote(quoteId, scope,
|
|
699
|
+
(em, quote) => this.cloneQuoteInTransaction(quoteId, scope, em, quote, created),
|
|
700
|
+
undefined, undefined, { pricingCause: 'clone' })
|
|
701
|
+
// The copy is a new aggregate: its first committed snapshot is a pricing signal.
|
|
702
|
+
if (created.quote) await emitQuoteWriteEvents(null, created.quote, { pricingCause: 'clone' })
|
|
703
|
+
return result
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private async cloneQuoteInTransaction(
|
|
707
|
+
quoteId: string, scope: TenantScope, em: EntityManager, original: CpqQuoteConfiguration,
|
|
708
|
+
created: { quote?: CpqQuoteConfiguration } = {},
|
|
709
|
+
): Promise<QuoteResult> {
|
|
557
710
|
|
|
558
|
-
const original = await this.loadCpqConfig(em, quoteId, scope)
|
|
559
711
|
const originalQuote = await this.loadSalesQuote(em, original.quoteId, scope)
|
|
560
712
|
|
|
561
713
|
// Generate new quote number
|
|
@@ -589,11 +741,13 @@ export class DefaultCpqQuotingService {
|
|
|
589
741
|
|
|
590
742
|
// Create new CpqQuoteConfiguration
|
|
591
743
|
const newCpqConfig = em.create(CpqQuoteConfiguration, {
|
|
744
|
+
id: crypto.randomUUID(),
|
|
592
745
|
organizationId: scope.organizationId,
|
|
593
746
|
tenantId: scope.tenantId,
|
|
594
747
|
quoteId: newSalesQuoteId,
|
|
595
748
|
customerId: original.customerId,
|
|
596
749
|
cpqStatus: 'new',
|
|
750
|
+
pricingRevision: 1,
|
|
597
751
|
version: original.version + 1,
|
|
598
752
|
parentQuoteId: original.id,
|
|
599
753
|
currencyCode: original.currencyCode,
|
|
@@ -602,6 +756,8 @@ export class DefaultCpqQuotingService {
|
|
|
602
756
|
pricingSummary: emptyPricingSummary(original.currencyCode),
|
|
603
757
|
})
|
|
604
758
|
em.persist(newCpqConfig)
|
|
759
|
+
created.quote = newCpqConfig
|
|
760
|
+
await em.flush()
|
|
605
761
|
|
|
606
762
|
// Copy all lines
|
|
607
763
|
const originalLines = await em.find(CpqQuoteLineConfiguration, {
|
|
@@ -663,7 +819,8 @@ export class DefaultCpqQuotingService {
|
|
|
663
819
|
startDate: origLine.startDate,
|
|
664
820
|
termMonths: origLine.termMonths,
|
|
665
821
|
endDate: origLine.endDate,
|
|
666
|
-
|
|
822
|
+
// A clone is a new quote: quote-only ARC pricing markers don't apply.
|
|
823
|
+
configuration: { ...stripQuoteOnlyArcMarkers(origLine.configuration) },
|
|
667
824
|
quantity: origLine.quantity,
|
|
668
825
|
charges: origLine.charges ? [...origLine.charges] : null,
|
|
669
826
|
nrcTotal: origLine.nrcTotal,
|
|
@@ -695,15 +852,21 @@ export class DefaultCpqQuotingService {
|
|
|
695
852
|
await em.flush()
|
|
696
853
|
|
|
697
854
|
// Recalculate the new quote at current rates
|
|
855
|
+
await cloneQuoteAdjustments(em, original.id, newCpqConfig, lineIdMap, scope)
|
|
856
|
+
await em.flush()
|
|
857
|
+
|
|
698
858
|
return this.recalculateInternal(em, newCpqConfig, newSalesQuote, scope, true)
|
|
699
859
|
}
|
|
700
860
|
|
|
701
861
|
// ─── Transition Status ─────────────────────────────────────────
|
|
702
862
|
|
|
703
|
-
async transitionStatus(quoteId: string, targetStatus: string, scope: TenantScope): Promise<QuoteResult> {
|
|
704
|
-
|
|
863
|
+
async transitionStatus(quoteId: string, targetStatus: string, scope: TenantScope, actorUserId: string | null = null): Promise<QuoteResult> {
|
|
864
|
+
return this.writeQuote(quoteId, scope, (em, quote) => this.transitionStatusInTransaction(quoteId, targetStatus, scope, em, quote),
|
|
865
|
+
undefined, undefined, { pricingCause: 'recalculate', statusCause: 'manual', actorUserId })
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
private async transitionStatusInTransaction(quoteId: string, targetStatus: string, scope: TenantScope, em: EntityManager, cpqConfig: CpqQuoteConfiguration): Promise<QuoteResult> {
|
|
705
869
|
|
|
706
|
-
const cpqConfig = await this.loadCpqConfig(em, quoteId, scope)
|
|
707
870
|
const salesQuote = await this.loadSalesQuote(em, cpqConfig.quoteId, scope)
|
|
708
871
|
|
|
709
872
|
const currentStatus = cpqConfig.cpqStatus as CpqStatus
|
|
@@ -725,7 +888,7 @@ export class DefaultCpqQuotingService {
|
|
|
725
888
|
targetStatus === 'with_customer'
|
|
726
889
|
const quoteType = cpqConfig.quoteType ?? 'new'
|
|
727
890
|
if (isSubmitForApproval && quoteType !== 'new') {
|
|
728
|
-
const validation = await this.validateArcQuote(quoteId, scope)
|
|
891
|
+
const validation = await this.validateArcQuote(quoteId, scope, em)
|
|
729
892
|
if (!validation.ok) {
|
|
730
893
|
throw new QuotingError(
|
|
731
894
|
422,
|
|
@@ -747,6 +910,28 @@ export class DefaultCpqQuotingService {
|
|
|
747
910
|
return this.buildQuoteResult(em, cpqConfig, salesQuote, lines)
|
|
748
911
|
}
|
|
749
912
|
|
|
913
|
+
private async writeQuote(
|
|
914
|
+
quoteId: string,
|
|
915
|
+
scope: TenantScope,
|
|
916
|
+
execute: (em: EntityManager, quote: CpqQuoteConfiguration) => Promise<QuoteResult>,
|
|
917
|
+
expectedRevision?: number,
|
|
918
|
+
validate?: (quote: CpqQuoteConfiguration) => void,
|
|
919
|
+
events: QuoteWriteEvents = { pricingCause: 'recalculate' },
|
|
920
|
+
): Promise<QuoteResult> {
|
|
921
|
+
return withQuotePricingWrite(this.em, quoteId, scope, execute,
|
|
922
|
+
(status, message) => new QuotingError(status, message), expectedRevision, validate, events)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** Called by adjustment commands that already own the quote lock/revision. */
|
|
926
|
+
async recalculateLockedQuote(
|
|
927
|
+
em: EntityManager, quote: CpqQuoteConfiguration, scope: TenantScope,
|
|
928
|
+
save = true, adjustments?: PriceAdjustment[],
|
|
929
|
+
): Promise<QuoteResult> {
|
|
930
|
+
const salesQuote = await this.loadSalesQuote(em, quote.quoteId, scope)
|
|
931
|
+
const config = save ? quote : { ...quote } as CpqQuoteConfiguration
|
|
932
|
+
return this.recalculateInternal(em, config, salesQuote, scope, save, adjustments)
|
|
933
|
+
}
|
|
934
|
+
|
|
750
935
|
// ─── Internal: Recalculate ─────────────────────────────────────
|
|
751
936
|
|
|
752
937
|
private async recalculateInternal(
|
|
@@ -755,6 +940,7 @@ export class DefaultCpqQuotingService {
|
|
|
755
940
|
salesQuote: SalesQuote,
|
|
756
941
|
scope: TenantScope,
|
|
757
942
|
save: boolean,
|
|
943
|
+
draftAdjustments?: PriceAdjustment[],
|
|
758
944
|
): Promise<QuoteResult> {
|
|
759
945
|
const lineConfigs = await em.find(CpqQuoteLineConfiguration, {
|
|
760
946
|
quoteConfigurationId: cpqConfig.id,
|
|
@@ -800,7 +986,7 @@ export class DefaultCpqQuotingService {
|
|
|
800
986
|
}
|
|
801
987
|
|
|
802
988
|
let validationErrors: ValidationError[] = []
|
|
803
|
-
if (lineConfig.offeringId) {
|
|
989
|
+
if (lineConfig.offeringId && !isArcCancelledLine(lineConfig, cpqConfig.quoteType)) {
|
|
804
990
|
const validation = await this.validationService.validateOfferingConfiguration({
|
|
805
991
|
offeringId: lineConfig.offeringId,
|
|
806
992
|
configuration: lineConfig.configuration,
|
|
@@ -817,8 +1003,16 @@ export class DefaultCpqQuotingService {
|
|
|
817
1003
|
// line to "incomplete".
|
|
818
1004
|
const cfgRecord = lineConfig.configuration as Record<string, unknown> | null
|
|
819
1005
|
const isArcMirrored = !!(cfgRecord?._arcMirroredFromItemId)
|
|
1006
|
+
// Frozen Amend mirror lines keep the subscription item's stored charges
|
|
1007
|
+
// (see arcPriceFreeze.ts) — re-pricing them would silently move the
|
|
1008
|
+
// contracted price of items the amendment does not touch.
|
|
1009
|
+
const isFrozen = isFrozenAmendMirrorLine(cfgRecord, cpqConfig.quoteType)
|
|
820
1010
|
let charges: ResolvedCharge[]
|
|
821
|
-
if (lineConfig
|
|
1011
|
+
if (isArcCancelledLine(lineConfig, cpqConfig.quoteType)) {
|
|
1012
|
+
// Item marked for removal: it leaves the subscription, so it adds
|
|
1013
|
+
// nothing to the quote (its current price is shown via `arcSource`).
|
|
1014
|
+
charges = []
|
|
1015
|
+
} else if ((lineConfig.offeringId || lineConfig.productId) && !isFrozen) {
|
|
822
1016
|
charges = await this.pricingService.resolveProductCharges({
|
|
823
1017
|
offeringId: lineConfig.offeringId ?? undefined,
|
|
824
1018
|
productId: lineConfig.productId ?? undefined,
|
|
@@ -844,37 +1038,72 @@ export class DefaultCpqQuotingService {
|
|
|
844
1038
|
|
|
845
1039
|
computedLines.push({ lineConfig, charges, nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated, validationErrors, isConfigured })
|
|
846
1040
|
|
|
1041
|
+
}
|
|
1042
|
+
const adjustments = draftAdjustments ?? await loadQuoteAdjustments(em, cpqConfig.id, scope)
|
|
1043
|
+
const adjusted = resolveManualAdjustments(computedLines.map(({ lineConfig, charges }) => ({
|
|
1044
|
+
lineId: lineConfig.quoteLineId, parentLineId: lineConfig.parentLineId ?? null,
|
|
1045
|
+
enginePriced: !!(lineConfig.offeringId || lineConfig.productId),
|
|
1046
|
+
mirrored: isArcMirrorLine(lineConfig.configuration, cpqConfig.quoteType), charges,
|
|
1047
|
+
})), adjustments, cpqConfig.currencyCode)
|
|
1048
|
+
const adjustedById = new Map(adjusted.lines.map((line) => [line.lineId, line]))
|
|
1049
|
+
const computedIds = new Set(computedLines.map((line) => line.lineConfig.quoteLineId))
|
|
1050
|
+
for (const computed of computedLines) {
|
|
1051
|
+
const { lineConfig } = computed
|
|
1052
|
+
// No adjustments: preserve legacy/frozen snapshots byte-for-byte on disk.
|
|
1053
|
+
if (adjustments.length) computed.charges = adjustedById.get(lineConfig.quoteLineId)!.charges
|
|
1054
|
+
const adjustmentErrors = adjusted.errors.filter((error) => error.quoteLineId === lineConfig.quoteLineId)
|
|
1055
|
+
if (adjustmentErrors.length) {
|
|
1056
|
+
computed.isConfigured = false
|
|
1057
|
+
allValid = false
|
|
1058
|
+
const errors = adjustmentErrors.map((error) => ({ message: error.code, attribute: error.chargeCode, lineId: error.quoteLineId, adjustmentId: error.adjustmentId }))
|
|
1059
|
+
computed.validationErrors.push(...errors)
|
|
1060
|
+
allErrors.push(...errors)
|
|
1061
|
+
}
|
|
1062
|
+
Object.assign(computed, computeLineTotals(computed.charges, lineConfig.quantity,
|
|
1063
|
+
lineConfig.usageEstimates?.map((entry) => ({ chargeCode: entry.chargeCode, estimatedQuantity: entry.estimatedQuantity }))))
|
|
1064
|
+
const { charges, nrcTotal, mrcTotal, usageEstimates, usageTotalEstimated, validationErrors, isConfigured } = computed
|
|
847
1065
|
if (save) {
|
|
848
|
-
em
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1066
|
+
await withAtomicFlush(em, [
|
|
1067
|
+
() => {
|
|
1068
|
+
em.assign(lineConfig, {
|
|
1069
|
+
charges: charges as unknown as Array<Record<string, unknown>>,
|
|
1070
|
+
nrcTotal: String(nrcTotal),
|
|
1071
|
+
mrcTotal: String(mrcTotal),
|
|
1072
|
+
usageEstimates: usageEstimates ?? null,
|
|
1073
|
+
usageTotalEstimated: usageTotalEstimated != null ? String(usageTotalEstimated) : null,
|
|
1074
|
+
validationErrors: validationErrors.length > 0 ? validationErrors as unknown as Array<Record<string, unknown>> : null,
|
|
1075
|
+
isConfigured,
|
|
1076
|
+
})
|
|
1077
|
+
},
|
|
1078
|
+
async () => {
|
|
1079
|
+
const salesLine = await em.findOne(SalesQuoteLine, {
|
|
1080
|
+
id: lineConfig.quoteLineId,
|
|
1081
|
+
organizationId: scope.organizationId,
|
|
1082
|
+
tenantId: scope.tenantId,
|
|
1083
|
+
})
|
|
1084
|
+
if (salesLine) {
|
|
1085
|
+
const perInstanceNrc = charges.filter((charge) => charge.chargeType === 'nrc').reduce((sum, charge) => sum + (charge.totalPrice ?? 0), 0)
|
|
1086
|
+
const perInstanceMrc = charges.filter((charge) => charge.chargeType === 'mrc').reduce((sum, charge) => sum + (charge.totalPrice ?? 0), 0)
|
|
1087
|
+
const unitPriceNet = perInstanceNrc + perInstanceMrc
|
|
1088
|
+
em.assign(salesLine, {
|
|
1089
|
+
quantity: String(lineConfig.quantity),
|
|
1090
|
+
unitPriceNet: String(unitPriceNet),
|
|
1091
|
+
unitPriceGross: String(unitPriceNet),
|
|
1092
|
+
totalNetAmount: String(unitPriceNet * lineConfig.quantity),
|
|
1093
|
+
totalGrossAmount: String(unitPriceNet * lineConfig.quantity),
|
|
1094
|
+
updatedAt: new Date(),
|
|
1095
|
+
})
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
])
|
|
876
1099
|
}
|
|
877
1100
|
}
|
|
1101
|
+
// A missing line target must still invalidate the quote (normally removal
|
|
1102
|
+
// soft-deletes its adjustments in the same aggregate transaction).
|
|
1103
|
+
for (const error of adjusted.errors.filter((entry) => !computedIds.has(entry.quoteLineId))) {
|
|
1104
|
+
allValid = false
|
|
1105
|
+
allErrors.push({ message: error.code, attribute: error.chargeCode, lineId: error.quoteLineId, adjustmentId: error.adjustmentId })
|
|
1106
|
+
}
|
|
878
1107
|
|
|
879
1108
|
const relationshipErrors = await this.validationService.validateRelationships({
|
|
880
1109
|
lines: lineConfigs.map((l) => ({
|
|
@@ -903,6 +1132,10 @@ export class DefaultCpqQuotingService {
|
|
|
903
1132
|
|
|
904
1133
|
// Build pricing summary from freshly computed data (not from stale entity fields)
|
|
905
1134
|
const pricingSummary = computePricingSummaryFromComputed(computedLines, cpqConfig.currencyCode)
|
|
1135
|
+
Object.assign(pricingSummary, computeWaterfallSummary(computedLines.map(({ lineConfig, charges }) => ({
|
|
1136
|
+
lineId: lineConfig.quoteLineId, quantity: lineConfig.quantity, termMonths: lineConfig.termMonths,
|
|
1137
|
+
includeInDeal: !isArcMirrorLine(lineConfig.configuration, cpqConfig.quoteType), charges,
|
|
1138
|
+
})), adjustments, cpqConfig.currencyCode))
|
|
906
1139
|
|
|
907
1140
|
const validationResult = { valid: allValid, errors: allErrors }
|
|
908
1141
|
|
|
@@ -923,7 +1156,9 @@ export class DefaultCpqQuotingService {
|
|
|
923
1156
|
await em.flush()
|
|
924
1157
|
}
|
|
925
1158
|
|
|
926
|
-
|
|
1159
|
+
const result = await this.buildQuoteResultFromComputed(em, cpqConfig, salesQuote, computedLines, validationResult, pricingSummary)
|
|
1160
|
+
result.adjustmentErrors = adjusted.errors
|
|
1161
|
+
return result
|
|
927
1162
|
}
|
|
928
1163
|
|
|
929
1164
|
// ─── Helpers ───────────────────────────────────────────────────
|
|
@@ -1039,7 +1274,7 @@ export class DefaultCpqQuotingService {
|
|
|
1039
1274
|
mrcTotal: Number(lc.mrcTotal),
|
|
1040
1275
|
usageEstimates: lc.usageEstimates ?? null,
|
|
1041
1276
|
usageTotalEstimated: lc.usageTotalEstimated != null ? Number(lc.usageTotalEstimated) : null,
|
|
1042
|
-
charges: (lc.charges ?? []) as unknown as ResolvedCharge[],
|
|
1277
|
+
charges: ((lc.charges ?? []) as unknown as ResolvedCharge[]).map(withChargeWaterfall),
|
|
1043
1278
|
isConfigured: lc.isConfigured,
|
|
1044
1279
|
validationErrors: lc.validationErrors as unknown as ValidationError[] ?? null,
|
|
1045
1280
|
arcSource: pickArcSource(lc, arcSourceMap),
|
|
@@ -1049,6 +1284,13 @@ export class DefaultCpqQuotingService {
|
|
|
1049
1284
|
|
|
1050
1285
|
const finalValidation = validationResult ?? (cpqConfig.validationResult as { valid: boolean; errors: ValidationError[] }) ?? { valid: true, errors: [] }
|
|
1051
1286
|
const finalPricing = pricingSummary ?? computePricingSummary(lineConfigs, cpqConfig.currencyCode)
|
|
1287
|
+
if (!pricingSummary) {
|
|
1288
|
+
const adjustments = await loadQuoteAdjustments(em, cpqConfig.id, { organizationId: cpqConfig.organizationId, tenantId: cpqConfig.tenantId })
|
|
1289
|
+
Object.assign(finalPricing, computeWaterfallSummary(lines.map((line) => ({
|
|
1290
|
+
lineId: line.lineId, quantity: line.quantity, termMonths: line.termMonths,
|
|
1291
|
+
includeInDeal: !isArcMirrorLine(line.configuration, cpqConfig.quoteType), charges: line.charges,
|
|
1292
|
+
})), adjustments, cpqConfig.currencyCode))
|
|
1293
|
+
}
|
|
1052
1294
|
|
|
1053
1295
|
const { customerName, convertedOrderId } = await resolveQuoteSideData(em, cpqConfig)
|
|
1054
1296
|
|
|
@@ -1060,6 +1302,7 @@ export class DefaultCpqQuotingService {
|
|
|
1060
1302
|
customerName,
|
|
1061
1303
|
cpqStatus: cpqConfig.cpqStatus,
|
|
1062
1304
|
version: cpqConfig.version,
|
|
1305
|
+
pricingRevision: cpqConfig.pricingRevision ?? 0,
|
|
1063
1306
|
parentQuoteId: cpqConfig.parentQuoteId ?? null,
|
|
1064
1307
|
currencyCode: cpqConfig.currencyCode,
|
|
1065
1308
|
convertedOrderId,
|
|
@@ -1136,7 +1379,7 @@ export class DefaultCpqQuotingService {
|
|
|
1136
1379
|
mrcTotal,
|
|
1137
1380
|
usageEstimates,
|
|
1138
1381
|
usageTotalEstimated,
|
|
1139
|
-
charges,
|
|
1382
|
+
charges: charges.map(withChargeWaterfall),
|
|
1140
1383
|
isConfigured,
|
|
1141
1384
|
validationErrors: validationErrors.length > 0 ? validationErrors : null,
|
|
1142
1385
|
arcSource: pickArcSource(lc, arcSourceMap),
|
|
@@ -1154,6 +1397,7 @@ export class DefaultCpqQuotingService {
|
|
|
1154
1397
|
customerName,
|
|
1155
1398
|
cpqStatus: cpqConfig.cpqStatus,
|
|
1156
1399
|
version: cpqConfig.version,
|
|
1400
|
+
pricingRevision: cpqConfig.pricingRevision ?? 0,
|
|
1157
1401
|
parentQuoteId: cpqConfig.parentQuoteId ?? null,
|
|
1158
1402
|
currencyCode: cpqConfig.currencyCode,
|
|
1159
1403
|
convertedOrderId,
|
|
@@ -1272,6 +1516,7 @@ function aggregateAdjustmentTotals(allCharges: ResolvedCharge[]): { discountTota
|
|
|
1272
1516
|
for (const charge of allCharges) {
|
|
1273
1517
|
if (!charge.adjustments) continue
|
|
1274
1518
|
for (const adj of charge.adjustments) {
|
|
1519
|
+
if (adj.origin === 'manual') continue
|
|
1275
1520
|
if (adj.delta < 0) {
|
|
1276
1521
|
discountTotal += Math.abs(adj.delta) * (charge.quantity ?? 1)
|
|
1277
1522
|
} else if (adj.delta > 0) {
|
|
@@ -1373,8 +1618,13 @@ function computePricingSummaryFromComputed(
|
|
|
1373
1618
|
}
|
|
1374
1619
|
}
|
|
1375
1620
|
|
|
1621
|
+
const LINE_EVENTS: QuoteWriteEvents = { pricingCause: 'line' }
|
|
1622
|
+
|
|
1376
1623
|
function emptyPricingSummary(currencyCode: string): Record<string, unknown> {
|
|
1377
1624
|
return {
|
|
1625
|
+
// A new quote starts with a real baseline hash, so its first priced line is
|
|
1626
|
+
// a pricing change; only pre-feature quotes have no stored hash.
|
|
1627
|
+
snapshotHash: computeWaterfallSummary([], [], currencyCode).snapshotHash,
|
|
1378
1628
|
nrcTotal: 0,
|
|
1379
1629
|
mrcTotal: 0,
|
|
1380
1630
|
usageCharges: [],
|
|
@@ -1387,7 +1637,17 @@ function emptyPricingSummary(currencyCode: string): Record<string, unknown> {
|
|
|
1387
1637
|
|
|
1388
1638
|
// ─── Error class ──────────────────────────────────────────────────
|
|
1389
1639
|
|
|
1640
|
+
// Cross-realm brand. Next bundles this package once per server layer (page
|
|
1641
|
+
// SSR vs route handler), and the DI container is shared through globalThis,
|
|
1642
|
+
// so a route handler can catch a QuotingError built from the *other* copy of
|
|
1643
|
+
// this class — `instanceof` is then false and a typed 404/409 turns into a
|
|
1644
|
+
// 500. `Symbol.for` resolves to the same symbol in every copy. Same approach
|
|
1645
|
+
// as `isCrudHttpError` in @open-mercato/shared. Use `isQuotingError`.
|
|
1646
|
+
const QUOTING_ERROR_BRAND = Symbol.for('@dainamite/cpq/QuotingError')
|
|
1647
|
+
|
|
1390
1648
|
export class QuotingError extends Error {
|
|
1649
|
+
readonly [QUOTING_ERROR_BRAND] = true
|
|
1650
|
+
|
|
1391
1651
|
constructor(
|
|
1392
1652
|
public status: number,
|
|
1393
1653
|
message: string,
|
|
@@ -1406,6 +1666,10 @@ export class QuotingError extends Error {
|
|
|
1406
1666
|
}
|
|
1407
1667
|
}
|
|
1408
1668
|
|
|
1669
|
+
export function isQuotingError(err: unknown): err is QuotingError {
|
|
1670
|
+
return !!err && typeof err === 'object' && (err as Record<symbol, unknown>)[QUOTING_ERROR_BRAND] === true
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1409
1673
|
// ─── ARC orchestration (XD-250) ────────────────────────────────────
|
|
1410
1674
|
//
|
|
1411
1675
|
// All ARC user-journey logic lives on the CPQ Quote module:
|
|
@@ -1544,7 +1808,7 @@ declare module './cpqQuotingService' {
|
|
|
1544
1808
|
): Promise<TargetSubscriptionView>
|
|
1545
1809
|
setCancelMeta(quoteId: string, meta: CancelMetaInput, scope: TenantScope): Promise<QuoteResult>
|
|
1546
1810
|
setMergeMeta(quoteId: string, meta: MergeMetaInput, scope: TenantScope): Promise<QuoteResult>
|
|
1547
|
-
validateArcQuote(quoteId: string, scope: TenantScope): Promise<ArcQuoteValidationResult>
|
|
1811
|
+
validateArcQuote(quoteId: string, scope: TenantScope, transactionEm?: EntityManager): Promise<ArcQuoteValidationResult>
|
|
1548
1812
|
}
|
|
1549
1813
|
}
|
|
1550
1814
|
|
|
@@ -1908,50 +2172,51 @@ DefaultCpqQuotingService.prototype.detachTargetSubscription = async function (
|
|
|
1908
2172
|
scope: TenantScope,
|
|
1909
2173
|
): Promise<{ ok: true }> {
|
|
1910
2174
|
const em = (this as unknown as { em: EntityManager }).em
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
organizationId: scope.organizationId,
|
|
1918
|
-
tenantId: scope.tenantId,
|
|
1919
|
-
deletedAt: null,
|
|
1920
|
-
})
|
|
1921
|
-
if (!target) throw new QuotingError(404, 'Target subscription not found')
|
|
1922
|
-
|
|
1923
|
-
// Drop any quote lines that pointed at this target.
|
|
1924
|
-
const lines = await em.find(CpqQuoteLineConfiguration, {
|
|
1925
|
-
quoteConfigurationId: cpqConfig.id,
|
|
1926
|
-
targetSubscriptionId: target.subscriptionId,
|
|
1927
|
-
organizationId: scope.organizationId,
|
|
1928
|
-
tenantId: scope.tenantId,
|
|
1929
|
-
deletedAt: null,
|
|
1930
|
-
})
|
|
1931
|
-
const now = new Date()
|
|
1932
|
-
for (const line of lines) {
|
|
1933
|
-
line.deletedAt = now
|
|
1934
|
-
const salesLine = await em.findOne(SalesQuoteLine, { id: line.quoteLineId })
|
|
1935
|
-
if (salesLine) salesLine.deletedAt = now
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
target.deletedAt = now
|
|
1939
|
-
|
|
1940
|
-
// Auto-revert merge mode if only one renew target remains.
|
|
1941
|
-
if (target.quoteType === 'renew') {
|
|
1942
|
-
const remaining = await em.find(CpqQuoteTargetSubscription, {
|
|
2175
|
+
// Detaching removes priced lines (rep-added lines may carry negotiated
|
|
2176
|
+
// prices), so it takes the same aggregate lock and revision bump as any
|
|
2177
|
+
// other pricing write and re-prices the remaining lines.
|
|
2178
|
+
await withQuotePricingWrite(em, quoteId, scope, async (tx, cpqConfig) => {
|
|
2179
|
+
const target = await tx.findOne(CpqQuoteTargetSubscription, {
|
|
2180
|
+
id: targetId,
|
|
1943
2181
|
quoteId: cpqConfig.id,
|
|
1944
2182
|
organizationId: scope.organizationId,
|
|
1945
2183
|
tenantId: scope.tenantId,
|
|
1946
2184
|
deletedAt: null,
|
|
1947
|
-
id: { $ne: target.id },
|
|
1948
2185
|
})
|
|
1949
|
-
if (
|
|
1950
|
-
|
|
2186
|
+
if (!target) throw new QuotingError(404, 'Target subscription not found')
|
|
2187
|
+
|
|
2188
|
+
// Drop quote lines that pointed at this target, with their bundle
|
|
2189
|
+
// children, sales lines and price adjustments.
|
|
2190
|
+
const lines = await tx.find(CpqQuoteLineConfiguration, {
|
|
2191
|
+
quoteConfigurationId: cpqConfig.id,
|
|
2192
|
+
targetSubscriptionId: target.subscriptionId,
|
|
2193
|
+
organizationId: scope.organizationId,
|
|
2194
|
+
tenantId: scope.tenantId,
|
|
2195
|
+
deletedAt: null,
|
|
2196
|
+
})
|
|
2197
|
+
for (const line of lines) {
|
|
2198
|
+
if (line.deletedAt) continue
|
|
2199
|
+
await removeQuoteLineTree(tx, cpqConfig.id, line, scope)
|
|
1951
2200
|
}
|
|
1952
|
-
}
|
|
1953
2201
|
|
|
1954
|
-
|
|
2202
|
+
// Read the remaining renew targets before mutating, so no query runs
|
|
2203
|
+
// between a scalar change and its flush.
|
|
2204
|
+
const remaining = target.quoteType === 'renew'
|
|
2205
|
+
? await tx.find(CpqQuoteTargetSubscription, {
|
|
2206
|
+
quoteId: cpqConfig.id,
|
|
2207
|
+
organizationId: scope.organizationId,
|
|
2208
|
+
tenantId: scope.tenantId,
|
|
2209
|
+
deletedAt: null,
|
|
2210
|
+
id: { $ne: target.id },
|
|
2211
|
+
})
|
|
2212
|
+
: []
|
|
2213
|
+
await withAtomicFlush(tx, [() => {
|
|
2214
|
+
target.deletedAt = new Date()
|
|
2215
|
+
// Auto-revert merge mode if only one renew target remains.
|
|
2216
|
+
if (remaining.length === 1) remaining[0].mergeAction = 'standalone'
|
|
2217
|
+
}])
|
|
2218
|
+
await this.recalculateLockedQuote(tx, cpqConfig, scope, true)
|
|
2219
|
+
}, (status, message) => new QuotingError(status, message), undefined, ensureEditable, { pricingCause: 'line' })
|
|
1955
2220
|
return { ok: true }
|
|
1956
2221
|
}
|
|
1957
2222
|
|
|
@@ -2048,8 +2313,9 @@ DefaultCpqQuotingService.prototype.validateArcQuote = async function (
|
|
|
2048
2313
|
this: DefaultCpqQuotingService,
|
|
2049
2314
|
quoteId: string,
|
|
2050
2315
|
scope: TenantScope,
|
|
2316
|
+
transactionEm?: EntityManager,
|
|
2051
2317
|
): Promise<ArcQuoteValidationResult> {
|
|
2052
|
-
const em = (this as unknown as { em: EntityManager }).em
|
|
2318
|
+
const em = transactionEm ?? (this as unknown as { em: EntityManager }).em
|
|
2053
2319
|
const cpqConfig = await loadCpqConfigStrict(em, quoteId, scope)
|
|
2054
2320
|
const errors: string[] = []
|
|
2055
2321
|
|
|
@@ -2352,6 +2618,7 @@ async function mapTargetView(
|
|
|
2352
2618
|
|
|
2353
2619
|
function emptyArcPricingSummary(currencyCode: string): Record<string, unknown> {
|
|
2354
2620
|
return {
|
|
2621
|
+
snapshotHash: computeWaterfallSummary([], [], currencyCode).snapshotHash,
|
|
2355
2622
|
nrcTotal: 0,
|
|
2356
2623
|
mrcTotal: 0,
|
|
2357
2624
|
usageCharges: [],
|
|
@@ -2522,11 +2789,7 @@ async function mirrorTargetSubscriptionItems(
|
|
|
2522
2789
|
startDate: null,
|
|
2523
2790
|
termMonths: null,
|
|
2524
2791
|
endDate: null,
|
|
2525
|
-
configuration:
|
|
2526
|
-
...item.configuration,
|
|
2527
|
-
_arcMirroredFromItemId: item.id,
|
|
2528
|
-
_arcMirroredName: item.name,
|
|
2529
|
-
},
|
|
2792
|
+
configuration: buildArcMirrorConfiguration(item.configuration, item, quoteType),
|
|
2530
2793
|
quantity: item.quantity,
|
|
2531
2794
|
charges: pickArcMirrorCharges(item, sub.currencyCode),
|
|
2532
2795
|
nrcTotal: String(item.nrcAmount ?? 0),
|