@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
|
@@ -2,8 +2,11 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { resolveCpqRouteContext } from '../context'
|
|
4
4
|
import { cpqConvertQuoteToOrderSchema } from '../../data/validators'
|
|
5
|
-
import {
|
|
5
|
+
import { isOrderError } from '../../services/cpqOrderService'
|
|
6
6
|
import type { DefaultCpqOrderService } from '../../services/cpqOrderService'
|
|
7
|
+
import { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from '../_helpers/quoteMutationGuard'
|
|
8
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
|
+
import { cpqErrorDoc } from '../_helpers/quoteRouteOpenApi'
|
|
7
10
|
|
|
8
11
|
export const metadata = {
|
|
9
12
|
POST: { requireAuth: true, requireFeatures: ['cpq.orders.manage'] },
|
|
@@ -18,8 +21,19 @@ export async function POST(req: Request) {
|
|
|
18
21
|
const body = cpqConvertQuoteToOrderSchema.parse(await req.json())
|
|
19
22
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
20
23
|
|
|
24
|
+
// Conversion turns a quote into an order — projects can block it (e.g.
|
|
25
|
+
// until an approval workflow has signed a discounted quote off).
|
|
26
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
27
|
+
resourceKind: CPQ_QUOTE_RESOURCE_KIND,
|
|
28
|
+
resourceId: body.quoteId,
|
|
29
|
+
operation: 'custom',
|
|
30
|
+
mutationPayload: { action: 'convert_to_order' },
|
|
31
|
+
})
|
|
32
|
+
if (!guard.ok) return guard.response
|
|
33
|
+
|
|
21
34
|
const service = ctx.container.resolve('cpqOrderService') as DefaultCpqOrderService
|
|
22
35
|
const result = await service.convertQuoteToOrder(body.quoteId, scope)
|
|
36
|
+
await guard.runAfterSuccess()
|
|
23
37
|
return NextResponse.json(result, { status: 201 })
|
|
24
38
|
} catch (err) {
|
|
25
39
|
return handleError(err, 'cpq/orders.POST')
|
|
@@ -65,8 +79,8 @@ function handleError(err: unknown, context: string) {
|
|
|
65
79
|
if (err instanceof z.ZodError) {
|
|
66
80
|
return NextResponse.json({ error: 'Validation error', details: err.issues }, { status: 400 })
|
|
67
81
|
}
|
|
68
|
-
if (err
|
|
69
|
-
return NextResponse.json({ error: err.message }, { status: err.status })
|
|
82
|
+
if (isOrderError(err)) {
|
|
83
|
+
return NextResponse.json({ error: err.message, ...err.details }, { status: err.status })
|
|
70
84
|
}
|
|
71
85
|
const errObj = err as Record<string, unknown>
|
|
72
86
|
if (typeof errObj?.status === 'number' && typeof errObj?.error === 'string') {
|
|
@@ -75,3 +89,39 @@ function handleError(err: unknown, context: string) {
|
|
|
75
89
|
console.error(`[${context}]`, err)
|
|
76
90
|
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
77
91
|
}
|
|
92
|
+
|
|
93
|
+
export const openApi: OpenApiRouteDoc = {
|
|
94
|
+
tag: 'CPQ Orders',
|
|
95
|
+
summary: 'CPQ orders',
|
|
96
|
+
methods: {
|
|
97
|
+
POST: {
|
|
98
|
+
summary: 'Convert a CPQ quote into an order',
|
|
99
|
+
description:
|
|
100
|
+
'Creates the sales order and CPQ order configuration from the quote. Refused for cancelled or rejected quotes; mutation guards (resource kind `cpq.quote`, action `convert_to_order`) run first.',
|
|
101
|
+
requestBody: { schema: cpqConvertQuoteToOrderSchema },
|
|
102
|
+
responses: [{ status: 201, description: 'Created order' }],
|
|
103
|
+
errors: [
|
|
104
|
+
cpqErrorDoc(400, 'Validation error'),
|
|
105
|
+
cpqErrorDoc(404, 'Quote not found'),
|
|
106
|
+
cpqErrorDoc(409, 'Quote cannot be converted in its status or is already converted'),
|
|
107
|
+
cpqErrorDoc(422, 'Quote has no lines or has invalid stored adjustments, or blocked by a mutation guard'),
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
GET: {
|
|
111
|
+
summary: 'List CPQ orders',
|
|
112
|
+
query: z.object({
|
|
113
|
+
customerId: z.string().uuid().optional(),
|
|
114
|
+
sourceQuoteId: z.string().uuid().optional(),
|
|
115
|
+
salesOrderId: z.string().uuid().optional(),
|
|
116
|
+
cpqStatus: z.string().optional(),
|
|
117
|
+
currencyCode: z.string().optional(),
|
|
118
|
+
search: z.string().optional(),
|
|
119
|
+
sortField: z.enum(['createdAt', 'updatedAt', 'cpqStatus', 'activatedAt', 'currencyCode']).optional(),
|
|
120
|
+
sortDir: z.enum(['asc', 'desc']).optional(),
|
|
121
|
+
page: z.coerce.number().int().min(1).optional(),
|
|
122
|
+
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
|
123
|
+
}),
|
|
124
|
+
responses: [{ status: 200, description: 'Paged list of CPQ orders' }],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { resolveCpqRouteContext } from '../../../context'
|
|
3
|
-
import {
|
|
3
|
+
import { isQuotingError } from '../../../../services/cpqQuotingService'
|
|
4
4
|
import { resolveQuotingService } from '../../../resolveQuotingService'
|
|
5
5
|
|
|
6
6
|
export const metadata = {
|
|
@@ -19,7 +19,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ quoteId
|
|
|
19
19
|
const result = await service.cloneQuote(quoteId, scope)
|
|
20
20
|
return NextResponse.json(result, { status: 201 })
|
|
21
21
|
} catch (err) {
|
|
22
|
-
if (err
|
|
22
|
+
if (isQuotingError(err)) {
|
|
23
23
|
return NextResponse.json({ error: err.message }, { status: err.status })
|
|
24
24
|
}
|
|
25
25
|
console.error('[cpq/quotes/[quoteId]/clone.POST]', err)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { resolveCpqRouteContext } from '../../../../../context'
|
|
4
|
+
import { isQuotingError } from '../../../../../../services/cpqQuotingService'
|
|
5
|
+
import { resolveQuotingService } from '../../../../../resolveQuotingService'
|
|
6
|
+
import { CPQ_QUOTE_LINE_RESOURCE_KIND, runCpqMutationGuards } from '../../../../../_helpers/quoteMutationGuard'
|
|
7
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
8
|
+
import { CPQ_GUARDED_WRITE_ERRORS } from '../../../../../_helpers/quoteRouteOpenApi'
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(req: Request, { params }: { params: Promise<{ quoteId: string; lineId: string }> }) {
|
|
15
|
+
try {
|
|
16
|
+
const ctx = await resolveCpqRouteContext(req)
|
|
17
|
+
if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
18
|
+
|
|
19
|
+
const { quoteId, lineId } = await params
|
|
20
|
+
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
21
|
+
|
|
22
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
23
|
+
resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
24
|
+
resourceId: lineId,
|
|
25
|
+
operation: 'update',
|
|
26
|
+
mutationPayload: { quoteId, restore: true },
|
|
27
|
+
})
|
|
28
|
+
if (!guard.ok) return guard.response
|
|
29
|
+
|
|
30
|
+
const service = resolveQuotingService(ctx)
|
|
31
|
+
const result = await service.restoreQuoteItem(quoteId, lineId, scope)
|
|
32
|
+
await guard.runAfterSuccess()
|
|
33
|
+
return NextResponse.json(result)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (isQuotingError(err)) {
|
|
36
|
+
return NextResponse.json({ error: err.message }, { status: err.status })
|
|
37
|
+
}
|
|
38
|
+
console.error('[cpq/quotes/[quoteId]/items/[lineId]/restore.POST]', err)
|
|
39
|
+
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const openApi: OpenApiRouteDoc = {
|
|
44
|
+
tag: 'CPQ Quotes',
|
|
45
|
+
summary: 'Restore a subscription item marked for removal',
|
|
46
|
+
pathParams: z.object({ quoteId: z.string().uuid(), lineId: z.string().uuid() }),
|
|
47
|
+
methods: {
|
|
48
|
+
POST: {
|
|
49
|
+
summary: 'Restore a removed subscription item on an Amend / Renew quote',
|
|
50
|
+
description:
|
|
51
|
+
'Turns a `cancel` line that mirrors a live subscription item back into a `modify` line, rebuilt from the item (configuration, quantity, stored price). Its cancelled mirrored components are restored with it. 409 when the line is not a removed item, its parent is still removed, or the item is no longer active. Mutation guards run first (resource kind `cpq.quote_line`, update).',
|
|
52
|
+
responses: [{ status: 200, description: 'Quote after the restore' }],
|
|
53
|
+
errors: CPQ_GUARDED_WRITE_ERRORS.filter((doc) => doc.status !== 400),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
@@ -2,8 +2,11 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { resolveCpqRouteContext } from '../../../../context'
|
|
4
4
|
import { cpqUpdateQuoteItemSchema } from '../../../../../data/validators'
|
|
5
|
-
import {
|
|
5
|
+
import { isQuotingError } from '../../../../../services/cpqQuotingService'
|
|
6
6
|
import { resolveQuotingService } from '../../../../resolveQuotingService'
|
|
7
|
+
import { CPQ_QUOTE_LINE_RESOURCE_KIND, runCpqMutationGuards } from '../../../../_helpers/quoteMutationGuard'
|
|
8
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
|
+
import { CPQ_GUARDED_WRITE_ERRORS } from '../../../../_helpers/quoteRouteOpenApi'
|
|
7
10
|
|
|
8
11
|
export const metadata = {
|
|
9
12
|
PUT: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },
|
|
@@ -19,14 +22,26 @@ export async function PUT(req: Request, { params }: { params: Promise<{ quoteId:
|
|
|
19
22
|
const body = cpqUpdateQuoteItemSchema.parse(await req.json())
|
|
20
23
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
21
24
|
|
|
25
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
26
|
+
resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
27
|
+
resourceId: lineId,
|
|
28
|
+
operation: 'update',
|
|
29
|
+
mutationPayload: { quoteId, ...body },
|
|
30
|
+
})
|
|
31
|
+
if (!guard.ok) return guard.response
|
|
32
|
+
const effective = guard.modifiedPayload
|
|
33
|
+
? cpqUpdateQuoteItemSchema.parse({ ...body, ...guard.modifiedPayload })
|
|
34
|
+
: body
|
|
35
|
+
|
|
22
36
|
const service = resolveQuotingService(ctx)
|
|
23
|
-
const result = await service.updateQuoteItem(quoteId, lineId,
|
|
37
|
+
const result = await service.updateQuoteItem(quoteId, lineId, effective, scope)
|
|
38
|
+
await guard.runAfterSuccess()
|
|
24
39
|
return NextResponse.json(result)
|
|
25
40
|
} catch (err) {
|
|
26
41
|
if (err instanceof z.ZodError) {
|
|
27
42
|
return NextResponse.json({ error: 'Validation error', details: err.issues }, { status: 400 })
|
|
28
43
|
}
|
|
29
|
-
if (err
|
|
44
|
+
if (isQuotingError(err)) {
|
|
30
45
|
return NextResponse.json({ error: err.message }, { status: err.status })
|
|
31
46
|
}
|
|
32
47
|
console.error('[cpq/quotes/[quoteId]/items/[lineId].PUT]', err)
|
|
@@ -42,14 +57,46 @@ export async function DELETE(req: Request, { params }: { params: Promise<{ quote
|
|
|
42
57
|
const { quoteId, lineId } = await params
|
|
43
58
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
44
59
|
|
|
60
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
61
|
+
resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
62
|
+
resourceId: lineId,
|
|
63
|
+
operation: 'delete',
|
|
64
|
+
mutationPayload: { quoteId },
|
|
65
|
+
})
|
|
66
|
+
if (!guard.ok) return guard.response
|
|
67
|
+
|
|
45
68
|
const service = resolveQuotingService(ctx)
|
|
46
69
|
const result = await service.removeQuoteItem(quoteId, lineId, scope)
|
|
70
|
+
await guard.runAfterSuccess()
|
|
47
71
|
return NextResponse.json(result)
|
|
48
72
|
} catch (err) {
|
|
49
|
-
if (err
|
|
73
|
+
if (isQuotingError(err)) {
|
|
50
74
|
return NextResponse.json({ error: err.message }, { status: err.status })
|
|
51
75
|
}
|
|
52
76
|
console.error('[cpq/quotes/[quoteId]/items/[lineId].DELETE]', err)
|
|
53
77
|
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
54
78
|
}
|
|
55
79
|
}
|
|
80
|
+
|
|
81
|
+
export const openApi: OpenApiRouteDoc = {
|
|
82
|
+
tag: 'CPQ Quotes',
|
|
83
|
+
summary: 'CPQ quote line',
|
|
84
|
+
pathParams: z.object({ quoteId: z.string().uuid(), lineId: z.string().uuid() }),
|
|
85
|
+
methods: {
|
|
86
|
+
PUT: {
|
|
87
|
+
summary: 'Update a quote line (configuration, quantity, term)',
|
|
88
|
+
description:
|
|
89
|
+
'Re-prices the line unless it is a frozen Amend mirror line whose pricing inputs are unchanged. Mutation guards run first (resource kind `cpq.quote_line`, update). `_arc*` configuration keys are server-owned and ignored.',
|
|
90
|
+
requestBody: { schema: cpqUpdateQuoteItemSchema },
|
|
91
|
+
responses: [{ status: 200, description: 'Quote after the update' }],
|
|
92
|
+
errors: CPQ_GUARDED_WRITE_ERRORS,
|
|
93
|
+
},
|
|
94
|
+
DELETE: {
|
|
95
|
+
summary: 'Remove a quote line',
|
|
96
|
+
description:
|
|
97
|
+
'On an Amend / Renew quote, a line mirroring a live subscription item is not deleted: it (and its mirrored components) becomes a `cancel` line with zero totals, and activation terminates the item; lines this quote added under it are deleted. 409 when the item is already marked for removal — use `POST .../restore` to undo. Other lines are deleted with their descendants. Mutation guards run first (resource kind `cpq.quote_line`, delete).',
|
|
98
|
+
responses: [{ status: 200, description: 'Quote after the removal' }],
|
|
99
|
+
errors: CPQ_GUARDED_WRITE_ERRORS.filter((doc) => doc.status !== 400),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
@@ -3,6 +3,9 @@ import { resolveCpqRouteContext } from '../../../context'
|
|
|
3
3
|
import { cpqAddQuoteItemSchema } from '../../../../data/validators'
|
|
4
4
|
import { resolveQuotingService } from '../../../resolveQuotingService'
|
|
5
5
|
import { handleArcError } from '../../../_helpers/handleArcError'
|
|
6
|
+
import { CPQ_QUOTE_LINE_RESOURCE_KIND, runCpqMutationGuards } from '../../../_helpers/quoteMutationGuard'
|
|
7
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
8
|
+
import { CPQ_GUARDED_WRITE_ERRORS, cpqQuoteIdPathParams } from '../../../_helpers/quoteRouteOpenApi'
|
|
6
9
|
|
|
7
10
|
export const metadata = {
|
|
8
11
|
POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },
|
|
@@ -17,10 +20,37 @@ export async function POST(req: Request, { params }: { params: Promise<{ quoteId
|
|
|
17
20
|
const body = cpqAddQuoteItemSchema.parse(await req.json())
|
|
18
21
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
19
22
|
|
|
23
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
24
|
+
resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
25
|
+
operation: 'create',
|
|
26
|
+
mutationPayload: { quoteId, ...body },
|
|
27
|
+
})
|
|
28
|
+
if (!guard.ok) return guard.response
|
|
29
|
+
const effective = guard.modifiedPayload
|
|
30
|
+
? cpqAddQuoteItemSchema.parse({ ...body, ...guard.modifiedPayload })
|
|
31
|
+
: body
|
|
32
|
+
|
|
20
33
|
const service = resolveQuotingService(ctx)
|
|
21
|
-
const result = await service.addQuoteItem(quoteId,
|
|
34
|
+
const result = await service.addQuoteItem(quoteId, effective, scope)
|
|
35
|
+
await guard.runAfterSuccess()
|
|
22
36
|
return NextResponse.json(result, { status: 201 })
|
|
23
37
|
} catch (err) {
|
|
24
38
|
return handleArcError(err, 'cpq/quotes/[quoteId]/items.POST')
|
|
25
39
|
}
|
|
26
40
|
}
|
|
41
|
+
|
|
42
|
+
export const openApi: OpenApiRouteDoc = {
|
|
43
|
+
tag: 'CPQ Quotes',
|
|
44
|
+
summary: 'CPQ quote lines',
|
|
45
|
+
pathParams: cpqQuoteIdPathParams,
|
|
46
|
+
methods: {
|
|
47
|
+
POST: {
|
|
48
|
+
summary: 'Add a configured line to a CPQ quote',
|
|
49
|
+
description:
|
|
50
|
+
'Prices the line and recalculates the quote. Mutation guards run first (resource kind `cpq.quote_line`, create). `_arc*` configuration keys are server-owned and ignored.',
|
|
51
|
+
requestBody: { schema: cpqAddQuoteItemSchema },
|
|
52
|
+
responses: [{ status: 201, description: 'Quote after the line was added' }],
|
|
53
|
+
errors: CPQ_GUARDED_WRITE_ERRORS,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { resolveCpqRouteContext } from '../../../../context'
|
|
5
|
+
import { cpqPriceAdjustmentPatchSchema } from '../../../../../data/price-adjustments'
|
|
6
|
+
import { adjustmentCommandRoute } from '../../../../_helpers/adjustmentCommandRoute'
|
|
7
|
+
import { handleArcError } from '../../../../_helpers/handleArcError'
|
|
8
|
+
import { cpqErrorDoc } from '../../../../_helpers/quoteRouteOpenApi'
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
PATCH: { requireAuth: true, requireFeatures: ['cpq.quotes.manage', 'cpq.quotes.adjust_price'] },
|
|
12
|
+
DELETE: { requireAuth: true, requireFeatures: ['cpq.quotes.manage', 'cpq.quotes.adjust_price'] },
|
|
13
|
+
}
|
|
14
|
+
const pathParams = z.object({ quoteId: z.string().uuid(), adjustmentId: z.string().uuid() })
|
|
15
|
+
const deletionSchema = z.object({ expectedRevision: z.coerce.number().int().min(0).optional() }).strict()
|
|
16
|
+
type Params = { params: Promise<{ quoteId: string; adjustmentId: string }> }
|
|
17
|
+
export async function PATCH(req: Request, { params }: Params) {
|
|
18
|
+
try {
|
|
19
|
+
const ctx = await resolveCpqRouteContext(req)
|
|
20
|
+
if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
21
|
+
const { quoteId, adjustmentId } = pathParams.parse(await params)
|
|
22
|
+
const body = cpqPriceAdjustmentPatchSchema.parse(await req.json())
|
|
23
|
+
return await adjustmentCommandRoute(req, ctx, quoteId, body, cpqPriceAdjustmentPatchSchema, 'update', adjustmentId)
|
|
24
|
+
} catch (error) { return handleArcError(error, 'cpq/price-adjustments.PATCH') }
|
|
25
|
+
}
|
|
26
|
+
export async function DELETE(req: Request, { params }: Params) {
|
|
27
|
+
try {
|
|
28
|
+
const ctx = await resolveCpqRouteContext(req)
|
|
29
|
+
if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
30
|
+
const { quoteId, adjustmentId } = pathParams.parse(await params)
|
|
31
|
+
const body = deletionSchema.parse(Object.fromEntries(new URL(req.url).searchParams))
|
|
32
|
+
return await adjustmentCommandRoute(req, ctx, quoteId, body, deletionSchema, 'delete', adjustmentId)
|
|
33
|
+
} catch (error) { return handleArcError(error, 'cpq/price-adjustments.DELETE') }
|
|
34
|
+
}
|
|
35
|
+
const errors = [cpqErrorDoc(400, 'Invalid input'), cpqErrorDoc(403, 'Missing permission or disabled feature'), cpqErrorDoc(404, 'Quote or adjustment not found'), cpqErrorDoc(409, 'Adjustment locked, stale revision or changed undo target'), cpqErrorDoc(422, 'Invalid adjustment or guard rejection')]
|
|
36
|
+
export const openApi: OpenApiRouteDoc = { tag: 'CPQ Quotes', pathParams, methods: {
|
|
37
|
+
PATCH: { summary: 'Update a negotiated quote price adjustment', requestBody: { schema: cpqPriceAdjustmentPatchSchema }, responses: [{ status: 200, description: 'Adjustment and updated quote' }], errors },
|
|
38
|
+
DELETE: { summary: 'Remove a negotiated quote price adjustment', query: deletionSchema, responses: [{ status: 200, description: 'Updated quote' }], errors },
|
|
39
|
+
} }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { resolveCpqRouteContext } from '../../../../context'
|
|
4
|
+
import { cpqPriceAdjustmentPreviewSchema } from '../../../../../data/price-adjustments'
|
|
5
|
+
import type { CpqAdjustmentService } from '../../../../../services/priceAdjustments/adjustmentService'
|
|
6
|
+
import { handleArcError } from '../../../../_helpers/handleArcError'
|
|
7
|
+
import { cpqErrorDoc, cpqQuoteIdPathParams } from '../../../../_helpers/quoteRouteOpenApi'
|
|
8
|
+
|
|
9
|
+
export const metadata = { POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage', 'cpq.quotes.adjust_price'] } }
|
|
10
|
+
export async function POST(req: Request, { params }: { params: Promise<{ quoteId: string }> }) {
|
|
11
|
+
try {
|
|
12
|
+
const ctx = await resolveCpqRouteContext(req)
|
|
13
|
+
if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
14
|
+
const { quoteId } = cpqQuoteIdPathParams.parse(await params)
|
|
15
|
+
const body = cpqPriceAdjustmentPreviewSchema.parse(await req.json())
|
|
16
|
+
const service = ctx.container.resolve<CpqAdjustmentService>('cpqAdjustmentService')
|
|
17
|
+
return NextResponse.json(await service.preview(quoteId, body.draft, body.replaceAdjustmentId, { tenantId: ctx.tenantId, organizationId: ctx.organizationId }))
|
|
18
|
+
} catch (error) { return handleArcError(error, 'cpq/price-adjustments/preview.POST') }
|
|
19
|
+
}
|
|
20
|
+
export const openApi: OpenApiRouteDoc = { tag: 'CPQ Quotes', pathParams: cpqQuoteIdPathParams, methods: { POST: {
|
|
21
|
+
summary: 'Preview a draft adjustment without saving or emitting events', requestBody: { schema: cpqPriceAdjustmentPreviewSchema },
|
|
22
|
+
responses: [{ status: 200, description: 'Draft quote and adjustment errors; nothing persisted' }],
|
|
23
|
+
errors: [cpqErrorDoc(400, 'Invalid draft'), cpqErrorDoc(403, 'Missing permission or disabled feature'), cpqErrorDoc(404, 'Quote, line or replaced adjustment not found'), cpqErrorDoc(409, 'Adjustment locked or stale revision'), cpqErrorDoc(422, 'Ineligible target or invalid reason')],
|
|
24
|
+
} } }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { resolveCpqRouteContext } from '../../../context'
|
|
5
|
+
import { cpqPriceAdjustmentInputSchema } from '../../../../data/price-adjustments'
|
|
6
|
+
import type { CpqAdjustmentService } from '../../../../services/priceAdjustments/adjustmentService'
|
|
7
|
+
import { adjustmentCommandRoute } from '../../../_helpers/adjustmentCommandRoute'
|
|
8
|
+
import { handleArcError } from '../../../_helpers/handleArcError'
|
|
9
|
+
import { cpqErrorDoc, cpqQuoteIdPathParams } from '../../../_helpers/quoteRouteOpenApi'
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
GET: { requireAuth: true, requireFeatures: ['cpq.quotes.view'] },
|
|
13
|
+
POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage', 'cpq.quotes.adjust_price'] },
|
|
14
|
+
}
|
|
15
|
+
const listQuery = z.object({ page: z.coerce.number().int().min(1).default(1), pageSize: z.coerce.number().int().min(1).max(100).default(25) })
|
|
16
|
+
type Params = { params: Promise<{ quoteId: string }> }
|
|
17
|
+
export async function GET(req: Request, { params }: Params) {
|
|
18
|
+
try {
|
|
19
|
+
const ctx = await resolveCpqRouteContext(req)
|
|
20
|
+
if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
21
|
+
const { quoteId } = cpqQuoteIdPathParams.parse(await params)
|
|
22
|
+
const { page, pageSize } = listQuery.parse(Object.fromEntries(new URL(req.url).searchParams))
|
|
23
|
+
const service = ctx.container.resolve<CpqAdjustmentService>('cpqAdjustmentService')
|
|
24
|
+
return NextResponse.json(await service.list(quoteId, { organizationId: ctx.organizationId, tenantId: ctx.tenantId }, page, pageSize))
|
|
25
|
+
} catch (error) { return handleArcError(error, 'cpq/price-adjustments.GET') }
|
|
26
|
+
}
|
|
27
|
+
export async function POST(req: Request, { params }: Params) {
|
|
28
|
+
try {
|
|
29
|
+
const ctx = await resolveCpqRouteContext(req)
|
|
30
|
+
if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
31
|
+
const { quoteId } = cpqQuoteIdPathParams.parse(await params)
|
|
32
|
+
const body = cpqPriceAdjustmentInputSchema.parse(await req.json())
|
|
33
|
+
return await adjustmentCommandRoute(req, ctx, quoteId, body, cpqPriceAdjustmentInputSchema, 'create')
|
|
34
|
+
} catch (error) { return handleArcError(error, 'cpq/price-adjustments.POST') }
|
|
35
|
+
}
|
|
36
|
+
const errors = [cpqErrorDoc(400, 'Invalid input'), cpqErrorDoc(403, 'Missing permission or disabled feature'), cpqErrorDoc(404, 'Quote not found'), cpqErrorDoc(409, 'Adjustment locked or stale revision'), cpqErrorDoc(422, 'Invalid adjustment or guard rejection')]
|
|
37
|
+
export const openApi: OpenApiRouteDoc = { tag: 'CPQ Quotes', pathParams: cpqQuoteIdPathParams, methods: {
|
|
38
|
+
GET: { summary: 'List quote price adjustments', query: listQuery, responses: [{ status: 200, description: 'Paged adjustments, pricing revision, feature state and the reason options (`reasons: [{ value, label }]`)' }], errors },
|
|
39
|
+
POST: { summary: 'Create a negotiated quote price adjustment', requestBody: { schema: cpqPriceAdjustmentInputSchema }, responses: [{ status: 201, description: 'Adjustment and updated quote' }], errors },
|
|
40
|
+
} }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recalculate route contract.
|
|
3
|
+
*
|
|
4
|
+
* Route-level `requireFeatures` is enforced by the API dispatcher, so the
|
|
5
|
+
* permission contract is pinned through the exported `metadata`; the handler
|
|
6
|
+
* contract (body → guard → service call) is pinned with mocked collaborators.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const recalculate = jest.fn()
|
|
10
|
+
const runCpqMutationGuards = jest.fn()
|
|
11
|
+
const runAfterSuccess = jest.fn()
|
|
12
|
+
|
|
13
|
+
jest.mock('../../../../context', () => ({
|
|
14
|
+
resolveCpqRouteContext: jest.fn().mockResolvedValue({
|
|
15
|
+
auth: { sub: 'user-1', tenantId: 't-1' },
|
|
16
|
+
organizationId: 'org-1',
|
|
17
|
+
tenantId: 't-1',
|
|
18
|
+
}),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
jest.mock('../../../../resolveQuotingService', () => ({
|
|
22
|
+
resolveQuotingService: () => ({ recalculate }),
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
jest.mock('../../../../_helpers/quoteMutationGuard', () => ({
|
|
26
|
+
CPQ_QUOTE_RESOURCE_KIND: 'cpq.quote',
|
|
27
|
+
runCpqMutationGuards: (...args: unknown[]) => runCpqMutationGuards(...args),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
import { metadata, openApi, POST } from '../route'
|
|
31
|
+
|
|
32
|
+
function postRequest(body: unknown): Request {
|
|
33
|
+
return new Request('http://localhost/api/cpq/quotes/cpq-1/recalculate', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const params = { params: Promise.resolve({ quoteId: 'cpq-1' }) }
|
|
41
|
+
|
|
42
|
+
describe('POST /api/cpq/quotes/{quoteId}/recalculate', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
recalculate.mockReset()
|
|
45
|
+
recalculate.mockResolvedValue({ id: 'cpq-1' })
|
|
46
|
+
runAfterSuccess.mockReset()
|
|
47
|
+
runCpqMutationGuards.mockReset()
|
|
48
|
+
runCpqMutationGuards.mockResolvedValue({ ok: true, runAfterSuccess })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('requires an authenticated user with cpq.quotes.manage (view-only users get 403 from the dispatcher)', () => {
|
|
52
|
+
expect(metadata.POST).toEqual({ requireAuth: true, requireFeatures: ['cpq.quotes.manage'] })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('documents the route in openApi', () => {
|
|
56
|
+
expect(openApi.methods?.POST?.errors).toEqual(expect.arrayContaining([expect.objectContaining({ status: 409 })]))
|
|
57
|
+
expect(openApi.methods?.POST?.requestBody).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('runs the mutation guards before a saved recalculation and their after-success hooks after it', async () => {
|
|
61
|
+
const res = await POST(postRequest({ save: true }), params)
|
|
62
|
+
|
|
63
|
+
expect(res.status).toBe(200)
|
|
64
|
+
expect(runCpqMutationGuards).toHaveBeenCalledWith(
|
|
65
|
+
expect.anything(),
|
|
66
|
+
expect.any(Request),
|
|
67
|
+
expect.objectContaining({ resourceKind: 'cpq.quote', resourceId: 'cpq-1', operation: 'custom' }),
|
|
68
|
+
)
|
|
69
|
+
expect(recalculate).toHaveBeenCalledWith(
|
|
70
|
+
'cpq-1',
|
|
71
|
+
expect.objectContaining({ save: true }),
|
|
72
|
+
{ organizationId: 'org-1', tenantId: 't-1' },
|
|
73
|
+
)
|
|
74
|
+
expect(runAfterSuccess).toHaveBeenCalledTimes(1)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('skips the guards for a dry run (save=false)', async () => {
|
|
78
|
+
await POST(postRequest({ save: false }), params)
|
|
79
|
+
|
|
80
|
+
expect(runCpqMutationGuards).not.toHaveBeenCalled()
|
|
81
|
+
expect(recalculate).toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('returns the guard response and does not recalculate when a guard blocks', async () => {
|
|
85
|
+
runCpqMutationGuards.mockResolvedValue({
|
|
86
|
+
ok: false,
|
|
87
|
+
response: Response.json({ error: 'Quote awaits approval' }, { status: 423 }),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const res = await POST(postRequest({ save: true }), params)
|
|
91
|
+
|
|
92
|
+
expect(res.status).toBe(423)
|
|
93
|
+
expect(recalculate).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -2,11 +2,17 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { resolveCpqRouteContext } from '../../../context'
|
|
4
4
|
import { cpqRecalculateSchema } from '../../../../data/validators'
|
|
5
|
-
import {
|
|
5
|
+
import { isQuotingError } from '../../../../services/cpqQuotingService'
|
|
6
6
|
import { resolveQuotingService } from '../../../resolveQuotingService'
|
|
7
|
+
import { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from '../../../_helpers/quoteMutationGuard'
|
|
8
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
|
+
import { cpqErrorDoc, cpqQuoteIdPathParams } from '../../../_helpers/quoteRouteOpenApi'
|
|
7
10
|
|
|
11
|
+
// Recalculation re-resolves and (with `save: true`) persists prices, so it
|
|
12
|
+
// needs the same feature as any other quote write. The quote page always
|
|
13
|
+
// saves; viewers read stored prices through GET /api/cpq/quotes/{quoteId}.
|
|
8
14
|
export const metadata = {
|
|
9
|
-
POST: { requireAuth: true, requireFeatures: ['cpq.quotes.
|
|
15
|
+
POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
export async function POST(req: Request, { params }: { params: Promise<{ quoteId: string }> }) {
|
|
@@ -18,17 +24,50 @@ export async function POST(req: Request, { params }: { params: Promise<{ quoteId
|
|
|
18
24
|
const body = cpqRecalculateSchema.parse(await req.json())
|
|
19
25
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
20
26
|
|
|
27
|
+
// Only a saved recalculation writes; a dry run needs no guard.
|
|
28
|
+
const guard = body.save
|
|
29
|
+
? await runCpqMutationGuards(ctx, req, {
|
|
30
|
+
resourceKind: CPQ_QUOTE_RESOURCE_KIND,
|
|
31
|
+
resourceId: quoteId,
|
|
32
|
+
operation: 'custom',
|
|
33
|
+
mutationPayload: { action: 'recalculate' },
|
|
34
|
+
})
|
|
35
|
+
: null
|
|
36
|
+
if (guard && !guard.ok) return guard.response
|
|
37
|
+
|
|
21
38
|
const service = resolveQuotingService(ctx)
|
|
22
39
|
const result = await service.recalculate(quoteId, body, scope)
|
|
40
|
+
if (guard) await guard.runAfterSuccess()
|
|
23
41
|
return NextResponse.json(result)
|
|
24
42
|
} catch (err) {
|
|
25
43
|
if (err instanceof z.ZodError) {
|
|
26
44
|
return NextResponse.json({ error: 'Validation error', details: err.issues }, { status: 400 })
|
|
27
45
|
}
|
|
28
|
-
if (err
|
|
46
|
+
if (isQuotingError(err)) {
|
|
29
47
|
return NextResponse.json({ error: err.message }, { status: err.status })
|
|
30
48
|
}
|
|
31
49
|
console.error('[cpq/quotes/[quoteId]/recalculate.POST]', err)
|
|
32
50
|
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
33
51
|
}
|
|
34
52
|
}
|
|
53
|
+
|
|
54
|
+
export const openApi: OpenApiRouteDoc = {
|
|
55
|
+
tag: 'CPQ Quotes',
|
|
56
|
+
summary: 'CPQ quote recalculation',
|
|
57
|
+
pathParams: cpqQuoteIdPathParams,
|
|
58
|
+
methods: {
|
|
59
|
+
POST: {
|
|
60
|
+
summary: 'Re-price every line of a CPQ quote',
|
|
61
|
+
description:
|
|
62
|
+
'Re-resolves charges and price rules. With `save: true` the result is persisted (refused on accepted, rejected and cancelled quotes) and mutation guards (resource kind `cpq.quote`, action `recalculate`) run first. Frozen Amend mirror lines keep their stored price.',
|
|
63
|
+
requestBody: { schema: cpqRecalculateSchema },
|
|
64
|
+
responses: [{ status: 200, description: 'Recalculated quote' }],
|
|
65
|
+
errors: [
|
|
66
|
+
cpqErrorDoc(400, 'Validation error'),
|
|
67
|
+
cpqErrorDoc(404, 'Quote not found'),
|
|
68
|
+
cpqErrorDoc(409, 'Saved recalculation on a terminal quote'),
|
|
69
|
+
cpqErrorDoc(422, 'Blocked by a mutation guard'),
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { resolveCpqRouteContext } from '../../context'
|
|
3
3
|
import { cpqUpdateQuoteSchema } from '../../../data/validators'
|
|
4
|
-
import {
|
|
4
|
+
import { isQuotingError } from '../../../services/cpqQuotingService'
|
|
5
5
|
import { resolveQuotingService } from '../../resolveQuotingService'
|
|
6
6
|
import { CpqQuoteConfiguration, CpqQuoteLineConfiguration } from '../../../data/entities'
|
|
7
7
|
import { SalesQuote, SalesQuoteLine } from '@open-mercato/core/modules/sales/data/entities'
|
|
@@ -25,7 +25,7 @@ export async function GET(req: Request, { params }: { params: Promise<{ quoteId:
|
|
|
25
25
|
const result = await service.getQuote(quoteId, scope)
|
|
26
26
|
return NextResponse.json(result)
|
|
27
27
|
} catch (err) {
|
|
28
|
-
if (err
|
|
28
|
+
if (isQuotingError(err)) {
|
|
29
29
|
return NextResponse.json({ error: err.message }, { status: err.status })
|
|
30
30
|
}
|
|
31
31
|
console.error('[cpq/quotes/[quoteId].GET]', err)
|