@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,10 +2,12 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { resolveCpqRouteContext } from "../../../context.js";
|
|
4
4
|
import { cpqRecalculateSchema } from "../../../../data/validators.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isQuotingError } from "../../../../services/cpqQuotingService.js";
|
|
6
6
|
import { resolveQuotingService } from "../../../resolveQuotingService.js";
|
|
7
|
+
import { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from "../../../_helpers/quoteMutationGuard.js";
|
|
8
|
+
import { cpqErrorDoc, cpqQuoteIdPathParams } from "../../../_helpers/quoteRouteOpenApi.js";
|
|
7
9
|
const metadata = {
|
|
8
|
-
POST: { requireAuth: true, requireFeatures: ["cpq.quotes.
|
|
10
|
+
POST: { requireAuth: true, requireFeatures: ["cpq.quotes.manage"] }
|
|
9
11
|
};
|
|
10
12
|
async function POST(req, { params }) {
|
|
11
13
|
try {
|
|
@@ -14,22 +16,50 @@ async function POST(req, { params }) {
|
|
|
14
16
|
const { quoteId } = await params;
|
|
15
17
|
const body = cpqRecalculateSchema.parse(await req.json());
|
|
16
18
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId };
|
|
19
|
+
const guard = body.save ? await runCpqMutationGuards(ctx, req, {
|
|
20
|
+
resourceKind: CPQ_QUOTE_RESOURCE_KIND,
|
|
21
|
+
resourceId: quoteId,
|
|
22
|
+
operation: "custom",
|
|
23
|
+
mutationPayload: { action: "recalculate" }
|
|
24
|
+
}) : null;
|
|
25
|
+
if (guard && !guard.ok) return guard.response;
|
|
17
26
|
const service = resolveQuotingService(ctx);
|
|
18
27
|
const result = await service.recalculate(quoteId, body, scope);
|
|
28
|
+
if (guard) await guard.runAfterSuccess();
|
|
19
29
|
return NextResponse.json(result);
|
|
20
30
|
} catch (err) {
|
|
21
31
|
if (err instanceof z.ZodError) {
|
|
22
32
|
return NextResponse.json({ error: "Validation error", details: err.issues }, { status: 400 });
|
|
23
33
|
}
|
|
24
|
-
if (err
|
|
34
|
+
if (isQuotingError(err)) {
|
|
25
35
|
return NextResponse.json({ error: err.message }, { status: err.status });
|
|
26
36
|
}
|
|
27
37
|
console.error("[cpq/quotes/[quoteId]/recalculate.POST]", err);
|
|
28
38
|
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
29
39
|
}
|
|
30
40
|
}
|
|
41
|
+
const openApi = {
|
|
42
|
+
tag: "CPQ Quotes",
|
|
43
|
+
summary: "CPQ quote recalculation",
|
|
44
|
+
pathParams: cpqQuoteIdPathParams,
|
|
45
|
+
methods: {
|
|
46
|
+
POST: {
|
|
47
|
+
summary: "Re-price every line of a CPQ quote",
|
|
48
|
+
description: "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.",
|
|
49
|
+
requestBody: { schema: cpqRecalculateSchema },
|
|
50
|
+
responses: [{ status: 200, description: "Recalculated quote" }],
|
|
51
|
+
errors: [
|
|
52
|
+
cpqErrorDoc(400, "Validation error"),
|
|
53
|
+
cpqErrorDoc(404, "Quote not found"),
|
|
54
|
+
cpqErrorDoc(409, "Saved recalculation on a terminal quote"),
|
|
55
|
+
cpqErrorDoc(422, "Blocked by a mutation guard")
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
31
60
|
export {
|
|
32
61
|
POST,
|
|
33
|
-
metadata
|
|
62
|
+
metadata,
|
|
63
|
+
openApi
|
|
34
64
|
};
|
|
35
65
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/cpq/api/quotes/%5BquoteId%5D/recalculate/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../../../context'\nimport { cpqRecalculateSchema } from '../../../../data/validators'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../../../context'\nimport { cpqRecalculateSchema } from '../../../../data/validators'\nimport { isQuotingError } from '../../../../services/cpqQuotingService'\nimport { resolveQuotingService } from '../../../resolveQuotingService'\nimport { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from '../../../_helpers/quoteMutationGuard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { cpqErrorDoc, cpqQuoteIdPathParams } from '../../../_helpers/quoteRouteOpenApi'\n\n// Recalculation re-resolves and (with `save: true`) persists prices, so it\n// needs the same feature as any other quote write. The quote page always\n// saves; viewers read stored prices through GET /api/cpq/quotes/{quoteId}.\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ quoteId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { quoteId } = await params\n const body = cpqRecalculateSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n // Only a saved recalculation writes; a dry run needs no guard.\n const guard = body.save\n ? await runCpqMutationGuards(ctx, req, {\n resourceKind: CPQ_QUOTE_RESOURCE_KIND,\n resourceId: quoteId,\n operation: 'custom',\n mutationPayload: { action: 'recalculate' },\n })\n : null\n if (guard && !guard.ok) return guard.response\n\n const service = resolveQuotingService(ctx)\n const result = await service.recalculate(quoteId, body, scope)\n if (guard) await guard.runAfterSuccess()\n return NextResponse.json(result)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation error', details: err.issues }, { status: 400 })\n }\n if (isQuotingError(err)) {\n return NextResponse.json({ error: err.message }, { status: err.status })\n }\n console.error('[cpq/quotes/[quoteId]/recalculate.POST]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CPQ Quotes',\n summary: 'CPQ quote recalculation',\n pathParams: cpqQuoteIdPathParams,\n methods: {\n POST: {\n summary: 'Re-price every line of a CPQ quote',\n description:\n '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.',\n requestBody: { schema: cpqRecalculateSchema },\n responses: [{ status: 200, description: 'Recalculated quote' }],\n errors: [\n cpqErrorDoc(400, 'Validation error'),\n cpqErrorDoc(404, 'Quote not found'),\n cpqErrorDoc(409, 'Saved recalculation on a terminal quote'),\n cpqErrorDoc(422, 'Blocked by a mutation guard'),\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,yBAAyB,4BAA4B;AAE9D,SAAS,aAAa,4BAA4B;AAK3C,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACpE;AAEA,eAAsB,KAAK,KAAc,EAAE,OAAO,GAA6C;AAC7F,MAAI;AACF,UAAM,MAAM,MAAM,uBAAuB,GAAG;AAC5C,QAAI,CAAC,IAAI,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAElF,UAAM,EAAE,QAAQ,IAAI,MAAM;AAC1B,UAAM,OAAO,qBAAqB,MAAM,MAAM,IAAI,KAAK,CAAC;AACxD,UAAM,QAAQ,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAG3E,UAAM,QAAQ,KAAK,OACf,MAAM,qBAAqB,KAAK,KAAK;AAAA,MACnC,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,iBAAiB,EAAE,QAAQ,cAAc;AAAA,IAC3C,CAAC,IACD;AACJ,QAAI,SAAS,CAAC,MAAM,GAAI,QAAO,MAAM;AAErC,UAAM,UAAU,sBAAsB,GAAG;AACzC,UAAM,SAAS,MAAM,QAAQ,YAAY,SAAS,MAAM,KAAK;AAC7D,QAAI,MAAO,OAAM,MAAM,gBAAgB;AACvC,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,KAAK;AACZ,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,SAAS,IAAI,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9F;AACA,QAAI,eAAe,GAAG,GAAG;AACvB,aAAO,aAAa,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IACzE;AACA,YAAQ,MAAM,2CAA2C,GAAG;AAC5D,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aACE;AAAA,MACF,aAAa,EAAE,QAAQ,qBAAqB;AAAA,MAC5C,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,qBAAqB,CAAC;AAAA,MAC9D,QAAQ;AAAA,QACN,YAAY,KAAK,kBAAkB;AAAA,QACnC,YAAY,KAAK,iBAAiB;AAAA,QAClC,YAAY,KAAK,yCAAyC;AAAA,QAC1D,YAAY,KAAK,6BAA6B;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { resolveCpqRouteContext } from "../../context.js";
|
|
3
3
|
import { cpqUpdateQuoteSchema } from "../../../data/validators.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isQuotingError } from "../../../services/cpqQuotingService.js";
|
|
5
5
|
import { resolveQuotingService } from "../../resolveQuotingService.js";
|
|
6
6
|
import { CpqQuoteConfiguration, CpqQuoteLineConfiguration } from "../../../data/entities.js";
|
|
7
7
|
import { SalesQuote, SalesQuoteLine } from "@open-mercato/core/modules/sales/data/entities";
|
|
@@ -21,7 +21,7 @@ async function GET(req, { params }) {
|
|
|
21
21
|
const result = await service.getQuote(quoteId, scope);
|
|
22
22
|
return NextResponse.json(result);
|
|
23
23
|
} catch (err) {
|
|
24
|
-
if (err
|
|
24
|
+
if (isQuotingError(err)) {
|
|
25
25
|
return NextResponse.json({ error: err.message }, { status: err.status });
|
|
26
26
|
}
|
|
27
27
|
console.error("[cpq/quotes/[quoteId].GET]", err);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cpq/api/quotes/%5BquoteId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveCpqRouteContext } from '../../context'\nimport { cpqUpdateQuoteSchema } from '../../../data/validators'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveCpqRouteContext } from '../../context'\nimport { cpqUpdateQuoteSchema } from '../../../data/validators'\nimport { isQuotingError } from '../../../services/cpqQuotingService'\nimport { resolveQuotingService } from '../../resolveQuotingService'\nimport { CpqQuoteConfiguration, CpqQuoteLineConfiguration } from '../../../data/entities'\nimport { SalesQuote, SalesQuoteLine } from '@open-mercato/core/modules/sales/data/entities'\nimport { handleArcError } from '../../_helpers/handleArcError'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['cpq.quotes.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },\n}\n\nexport async function GET(req: Request, { params }: { params: Promise<{ quoteId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { quoteId } = await params\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n const service = resolveQuotingService(ctx)\n const result = await service.getQuote(quoteId, scope)\n return NextResponse.json(result)\n } catch (err) {\n if (isQuotingError(err)) {\n return NextResponse.json({ error: err.message }, { status: err.status })\n }\n console.error('[cpq/quotes/[quoteId].GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport async function PATCH(\n req: Request,\n { params }: { params: Promise<{ quoteId: string }> },\n) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { quoteId } = await params\n const body = cpqUpdateQuoteSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n if (body.quoteType !== undefined) {\n // ARC quote-type one-way transition (XD-250 DD-ARC-9). The service\n // enforces 'new' \u2192 'amend'/'renew'/'cancel' only; subsequent changes\n // return 409.\n const service = resolveQuotingService(ctx)\n const result = await service.setQuoteType(quoteId, body.quoteType, scope)\n return NextResponse.json(result)\n }\n\n return NextResponse.json({ error: 'Nothing to update' }, { status: 400 })\n } catch (err) {\n return handleArcError(err, 'cpq/quotes/[quoteId].PATCH')\n }\n}\n\nexport async function DELETE(req: Request, { params }: { params: Promise<{ quoteId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { quoteId } = await params\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n const em = ctx.em\n const now = new Date()\n\n let cpqConfig = await em.findOne(CpqQuoteConfiguration, {\n id: quoteId, organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null,\n })\n if (!cpqConfig) {\n cpqConfig = await em.findOne(CpqQuoteConfiguration, {\n quoteId, organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null,\n })\n }\n if (!cpqConfig) return NextResponse.json({ error: 'Quote not found' }, { status: 404 })\n\n const terminalStatuses = ['accepted', 'rejected', 'cancelled']\n if (terminalStatuses.includes(cpqConfig.cpqStatus)) {\n return NextResponse.json({ error: `Cannot delete a quote in '${cpqConfig.cpqStatus}' status` }, { status: 409 })\n }\n\n const lineConfigs = await em.find(CpqQuoteLineConfiguration, {\n quoteConfigurationId: cpqConfig.id, organizationId: scope.organizationId, tenantId: scope.tenantId, deletedAt: null,\n })\n for (const lc of lineConfigs) {\n lc.deletedAt = now\n const salesLine = await em.findOne(SalesQuoteLine, { id: lc.quoteLineId })\n if (salesLine) salesLine.deletedAt = now\n }\n\n cpqConfig.deletedAt = now\n\n const salesQuote = await em.findOne(SalesQuote, { id: cpqConfig.quoteId })\n if (salesQuote) salesQuote.deletedAt = now\n\n await em.flush()\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n console.error('[cpq/quotes/[quoteId].DELETE]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { resolveCpqRouteContext } from "../../../context.js";
|
|
3
|
+
import { resolveAuditActor } from "../../../../lib/auditActor.js";
|
|
3
4
|
import { cpqTransitionStatusSchema } from "../../../../data/validators.js";
|
|
4
5
|
import { resolveQuotingService } from "../../../resolveQuotingService.js";
|
|
5
6
|
import { handleArcError } from "../../../_helpers/handleArcError.js";
|
|
7
|
+
import { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from "../../../_helpers/quoteMutationGuard.js";
|
|
8
|
+
import { cpqErrorDoc, cpqQuoteIdPathParams } from "../../../_helpers/quoteRouteOpenApi.js";
|
|
6
9
|
const metadata = {
|
|
7
10
|
POST: { requireAuth: true, requireFeatures: ["cpq.quotes.manage"] }
|
|
8
11
|
};
|
|
@@ -13,15 +16,43 @@ async function POST(req, { params }) {
|
|
|
13
16
|
const { quoteId } = await params;
|
|
14
17
|
const body = cpqTransitionStatusSchema.parse(await req.json());
|
|
15
18
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId };
|
|
19
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
20
|
+
resourceKind: CPQ_QUOTE_RESOURCE_KIND,
|
|
21
|
+
resourceId: quoteId,
|
|
22
|
+
operation: "custom",
|
|
23
|
+
mutationPayload: { action: "transition_status", targetStatus: body.targetStatus }
|
|
24
|
+
});
|
|
25
|
+
if (!guard.ok) return guard.response;
|
|
16
26
|
const service = resolveQuotingService(ctx);
|
|
17
|
-
const result = await service.transitionStatus(quoteId, body.targetStatus, scope);
|
|
27
|
+
const result = await service.transitionStatus(quoteId, body.targetStatus, scope, resolveAuditActor(ctx.auth));
|
|
28
|
+
await guard.runAfterSuccess();
|
|
18
29
|
return NextResponse.json(result);
|
|
19
30
|
} catch (err) {
|
|
20
31
|
return handleArcError(err, "cpq/quotes/[quoteId]/status.POST");
|
|
21
32
|
}
|
|
22
33
|
}
|
|
34
|
+
const openApi = {
|
|
35
|
+
tag: "CPQ Quotes",
|
|
36
|
+
summary: "CPQ quote status",
|
|
37
|
+
pathParams: cpqQuoteIdPathParams,
|
|
38
|
+
methods: {
|
|
39
|
+
POST: {
|
|
40
|
+
summary: "Transition a CPQ quote to another status",
|
|
41
|
+
description: "Operator-driven status change. Registered mutation guards (resource kind `cpq.quote`, action `transition_status`) run first \u2014 projects use them to enforce approvals.",
|
|
42
|
+
requestBody: { schema: cpqTransitionStatusSchema },
|
|
43
|
+
responses: [{ status: 200, description: "Quote with its new status" }],
|
|
44
|
+
errors: [
|
|
45
|
+
cpqErrorDoc(400, "Validation error"),
|
|
46
|
+
cpqErrorDoc(404, "Quote not found"),
|
|
47
|
+
cpqErrorDoc(409, "Transition not allowed"),
|
|
48
|
+
cpqErrorDoc(422, "ARC validation failed or blocked by a mutation guard")
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
23
53
|
export {
|
|
24
54
|
POST,
|
|
25
|
-
metadata
|
|
55
|
+
metadata,
|
|
56
|
+
openApi
|
|
26
57
|
};
|
|
27
58
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/cpq/api/quotes/%5BquoteId%5D/status/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveCpqRouteContext } from '../../../context'\nimport { cpqTransitionStatusSchema } from '../../../../data/validators'\nimport { resolveQuotingService } from '../../../resolveQuotingService'\nimport { handleArcError } from '../../../_helpers/handleArcError'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ quoteId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { quoteId } = await params\n const body = cpqTransitionStatusSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n const service = resolveQuotingService(ctx)\n const result = await service.transitionStatus(quoteId, body.targetStatus, scope)\n return NextResponse.json(result)\n } catch (err) {\n return handleArcError(err, 'cpq/quotes/[quoteId]/status.POST')\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAC1C,SAAS,6BAA6B;AACtC,SAAS,sBAAsB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveCpqRouteContext } from '../../../context'\nimport { resolveAuditActor } from '../../../../lib/auditActor'\nimport { cpqTransitionStatusSchema } from '../../../../data/validators'\nimport { resolveQuotingService } from '../../../resolveQuotingService'\nimport { handleArcError } from '../../../_helpers/handleArcError'\nimport { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from '../../../_helpers/quoteMutationGuard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { cpqErrorDoc, cpqQuoteIdPathParams } from '../../../_helpers/quoteRouteOpenApi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ quoteId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { quoteId } = await params\n const body = cpqTransitionStatusSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n // Projects enforce approval rules here (e.g. block `ready \u2192 with_customer`\n // until a discounted quote is approved).\n const guard = await runCpqMutationGuards(ctx, req, {\n resourceKind: CPQ_QUOTE_RESOURCE_KIND,\n resourceId: quoteId,\n operation: 'custom',\n mutationPayload: { action: 'transition_status', targetStatus: body.targetStatus },\n })\n if (!guard.ok) return guard.response\n\n const service = resolveQuotingService(ctx)\n const result = await service.transitionStatus(quoteId, body.targetStatus, scope, resolveAuditActor(ctx.auth))\n await guard.runAfterSuccess()\n return NextResponse.json(result)\n } catch (err) {\n return handleArcError(err, 'cpq/quotes/[quoteId]/status.POST')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CPQ Quotes',\n summary: 'CPQ quote status',\n pathParams: cpqQuoteIdPathParams,\n methods: {\n POST: {\n summary: 'Transition a CPQ quote to another status',\n description:\n 'Operator-driven status change. Registered mutation guards (resource kind `cpq.quote`, action `transition_status`) run first \u2014 projects use them to enforce approvals.',\n requestBody: { schema: cpqTransitionStatusSchema },\n responses: [{ status: 200, description: 'Quote with its new status' }],\n errors: [\n cpqErrorDoc(400, 'Validation error'),\n cpqErrorDoc(404, 'Quote not found'),\n cpqErrorDoc(409, 'Transition not allowed'),\n cpqErrorDoc(422, 'ARC validation failed or blocked by a mutation guard'),\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,yBAAyB;AAClC,SAAS,iCAAiC;AAC1C,SAAS,6BAA6B;AACtC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB,4BAA4B;AAE9D,SAAS,aAAa,4BAA4B;AAE3C,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACpE;AAEA,eAAsB,KAAK,KAAc,EAAE,OAAO,GAA6C;AAC7F,MAAI;AACF,UAAM,MAAM,MAAM,uBAAuB,GAAG;AAC5C,QAAI,CAAC,IAAI,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAElF,UAAM,EAAE,QAAQ,IAAI,MAAM;AAC1B,UAAM,OAAO,0BAA0B,MAAM,MAAM,IAAI,KAAK,CAAC;AAC7D,UAAM,QAAQ,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAI3E,UAAM,QAAQ,MAAM,qBAAqB,KAAK,KAAK;AAAA,MACjD,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,iBAAiB,EAAE,QAAQ,qBAAqB,cAAc,KAAK,aAAa;AAAA,IAClF,CAAC;AACD,QAAI,CAAC,MAAM,GAAI,QAAO,MAAM;AAE5B,UAAM,UAAU,sBAAsB,GAAG;AACzC,UAAM,SAAS,MAAM,QAAQ,iBAAiB,SAAS,KAAK,cAAc,OAAO,kBAAkB,IAAI,IAAI,CAAC;AAC5G,UAAM,MAAM,gBAAgB;AAC5B,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,KAAK;AACZ,WAAO,eAAe,KAAK,kCAAkC;AAAA,EAC/D;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aACE;AAAA,MACF,aAAa,EAAE,QAAQ,0BAA0B;AAAA,MACjD,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,4BAA4B,CAAC;AAAA,MACrE,QAAQ;AAAA,QACN,YAAY,KAAK,kBAAkB;AAAA,QACnC,YAAY,KAAK,iBAAiB;AAAA,QAClC,YAAY,KAAK,wBAAwB;AAAA,QACzC,YAAY,KAAK,sDAAsD;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { cpqErrorDoc } from "../../_helpers/quoteRouteOpenApi.js";
|
|
3
4
|
import { resolveCpqRouteContext } from "../../context.js";
|
|
4
5
|
import { cpqPricePreviewSchema } from "../../../data/validators.js";
|
|
5
6
|
import { getBaseCurrencyCode } from "../../../services/types.js";
|
|
7
|
+
import { withChargeWaterfall, withoutChargeWaterfall } from "../../../services/priceAdjustments/chargeWaterfall.js";
|
|
6
8
|
const metadata = {
|
|
7
9
|
POST: { requireAuth: true, requireFeatures: ["cpq.quotes.view"] }
|
|
8
10
|
};
|
|
@@ -48,9 +50,10 @@ async function POST(req) {
|
|
|
48
50
|
unitPrice: c.unitPrice,
|
|
49
51
|
note: c.note ?? "Usage-based charge \u2014 billed on actual consumption."
|
|
50
52
|
}));
|
|
53
|
+
const presentCharge = body.includeWaterfall ? withChargeWaterfall : withoutChargeWaterfall;
|
|
51
54
|
return NextResponse.json({
|
|
52
|
-
primaryCharges,
|
|
53
|
-
childItems,
|
|
55
|
+
primaryCharges: primaryCharges.map(presentCharge),
|
|
56
|
+
childItems: childItems.map((item) => ({ ...item, charges: item.charges.map(presentCharge) })),
|
|
54
57
|
totals: { nrcTotal, mrcTotal, usageCharges },
|
|
55
58
|
currencyCode
|
|
56
59
|
});
|
|
@@ -62,8 +65,45 @@ async function POST(req) {
|
|
|
62
65
|
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
const chargeDoc = z.object({
|
|
69
|
+
chargeCode: z.string(),
|
|
70
|
+
chargeName: z.string(),
|
|
71
|
+
chargeType: z.enum(["nrc", "mrc", "usage"]),
|
|
72
|
+
pricingMethod: z.enum(["flat", "per_unit", "volume", "tiered"]),
|
|
73
|
+
currencyCode: z.string(),
|
|
74
|
+
unitPrice: z.number(),
|
|
75
|
+
quantity: z.number().nullable(),
|
|
76
|
+
totalPrice: z.number().nullable(),
|
|
77
|
+
listUnitPrice: z.number().optional(),
|
|
78
|
+
listTotalPrice: z.number().nullable().optional(),
|
|
79
|
+
standardUnitPrice: z.number().optional(),
|
|
80
|
+
standardTotalPrice: z.number().nullable().optional(),
|
|
81
|
+
adjustments: z.array(z.object({
|
|
82
|
+
origin: z.literal("rule"),
|
|
83
|
+
ruleCode: z.string(),
|
|
84
|
+
ruleName: z.string(),
|
|
85
|
+
ruleType: z.string(),
|
|
86
|
+
value: z.number(),
|
|
87
|
+
unitPriceBefore: z.number(),
|
|
88
|
+
unitPriceAfter: z.number(),
|
|
89
|
+
delta: z.number()
|
|
90
|
+
}).passthrough()).nullish()
|
|
91
|
+
}).passthrough();
|
|
92
|
+
const openApi = { tag: "CPQ Pricing", methods: { POST: {
|
|
93
|
+
summary: "Calculate catalog prices and price rules without saving",
|
|
94
|
+
description: "Manual quote adjustments are never applied. Set includeWaterfall=true to return list and standard prices; these fields are omitted by default.",
|
|
95
|
+
requestBody: { schema: cpqPricePreviewSchema },
|
|
96
|
+
responses: [{ status: 200, description: "Resolved rule prices and optional waterfall", schema: z.object({
|
|
97
|
+
primaryCharges: z.array(chargeDoc),
|
|
98
|
+
childItems: z.array(z.object({ offeringId: z.string().nullable(), productId: z.string().nullable(), charges: z.array(chargeDoc) })),
|
|
99
|
+
totals: z.object({ nrcTotal: z.number(), mrcTotal: z.number(), usageCharges: z.array(z.unknown()) }),
|
|
100
|
+
currencyCode: z.string()
|
|
101
|
+
}) }],
|
|
102
|
+
errors: [cpqErrorDoc(400, "Invalid pricing request"), cpqErrorDoc(500, "Price calculation failed")]
|
|
103
|
+
} } };
|
|
65
104
|
export {
|
|
66
105
|
POST,
|
|
67
|
-
metadata
|
|
106
|
+
metadata,
|
|
107
|
+
openApi
|
|
68
108
|
};
|
|
69
109
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cpq/api/quotes/price/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../../context'\nimport { cpqPricePreviewSchema } from '../../../data/validators'\nimport type { DefaultCpqPricingService } from '../../../services/cpqPricingService'\nimport type { ResolvedCharge } from '../../../services/types'\nimport { getBaseCurrencyCode } from '../../../services/types'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.quotes.view'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = cpqPricePreviewSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n const currencyCode = body.currencyCode ?? await getBaseCurrencyCode(ctx.em, scope)\n\n const pricingService = ctx.container.resolve('cpqPricingService') as DefaultCpqPricingService\n\n let primaryCharges: ResolvedCharge[] = []\n if (body.primaryItem) {\n primaryCharges = await pricingService.resolveProductCharges({\n offeringId: body.primaryItem.offeringId ?? undefined,\n productId: body.primaryItem.productId ?? undefined,\n configuration: body.primaryItem.configuration,\n currencyCode,\n ...scope,\n })\n }\n\n const childItems: Array<{ offeringId: string | null; productId: string | null; charges: ResolvedCharge[] }> = []\n for (const child of body.childItems) {\n const charges = await pricingService.resolveProductCharges({\n offeringId: child.offeringId ?? undefined,\n productId: child.productId ?? undefined,\n configuration: child.configuration,\n currencyCode,\n ...scope,\n })\n childItems.push({\n offeringId: child.offeringId ?? null,\n productId: child.productId ?? null,\n charges,\n })\n }\n\n const allCharges = [...primaryCharges, ...childItems.flatMap((c) => c.charges)]\n\n const nrcTotal = allCharges\n .filter((c) => c.chargeType === 'nrc' && c.totalPrice != null)\n .reduce((s, c) => s + (c.totalPrice ?? 0), 0)\n const mrcTotal = allCharges\n .filter((c) => c.chargeType === 'mrc' && c.totalPrice != null)\n .reduce((s, c) => s + (c.totalPrice ?? 0), 0)\n const usageCharges = allCharges\n .filter((c) => c.chargeType === 'usage')\n .map((c) => ({\n chargeCode: c.chargeCode,\n chargeName: c.chargeName,\n unitPrice: c.unitPrice,\n note: c.note ?? 'Usage-based charge \u2014 billed on actual consumption.',\n }))\n\n return NextResponse.json({\n primaryCharges,\n childItems,\n totals: { nrcTotal, mrcTotal, usageCharges },\n currencyCode,\n })\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation error', details: err.issues }, { status: 400 })\n }\n console.error('[cpq/quotes/price.POST]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { cpqErrorDoc } from '../../_helpers/quoteRouteOpenApi'\nimport { resolveCpqRouteContext } from '../../context'\nimport { cpqPricePreviewSchema } from '../../../data/validators'\nimport type { DefaultCpqPricingService } from '../../../services/cpqPricingService'\nimport type { ResolvedCharge } from '../../../services/types'\nimport { getBaseCurrencyCode } from '../../../services/types'\nimport { withChargeWaterfall, withoutChargeWaterfall } from '../../../services/priceAdjustments/chargeWaterfall'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.quotes.view'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = cpqPricePreviewSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n const currencyCode = body.currencyCode ?? await getBaseCurrencyCode(ctx.em, scope)\n\n const pricingService = ctx.container.resolve('cpqPricingService') as DefaultCpqPricingService\n\n let primaryCharges: ResolvedCharge[] = []\n if (body.primaryItem) {\n primaryCharges = await pricingService.resolveProductCharges({\n offeringId: body.primaryItem.offeringId ?? undefined,\n productId: body.primaryItem.productId ?? undefined,\n configuration: body.primaryItem.configuration,\n currencyCode,\n ...scope,\n })\n }\n\n const childItems: Array<{ offeringId: string | null; productId: string | null; charges: ResolvedCharge[] }> = []\n for (const child of body.childItems) {\n const charges = await pricingService.resolveProductCharges({\n offeringId: child.offeringId ?? undefined,\n productId: child.productId ?? undefined,\n configuration: child.configuration,\n currencyCode,\n ...scope,\n })\n childItems.push({\n offeringId: child.offeringId ?? null,\n productId: child.productId ?? null,\n charges,\n })\n }\n\n const allCharges = [...primaryCharges, ...childItems.flatMap((c) => c.charges)]\n\n const nrcTotal = allCharges\n .filter((c) => c.chargeType === 'nrc' && c.totalPrice != null)\n .reduce((s, c) => s + (c.totalPrice ?? 0), 0)\n const mrcTotal = allCharges\n .filter((c) => c.chargeType === 'mrc' && c.totalPrice != null)\n .reduce((s, c) => s + (c.totalPrice ?? 0), 0)\n const usageCharges = allCharges\n .filter((c) => c.chargeType === 'usage')\n .map((c) => ({\n chargeCode: c.chargeCode,\n chargeName: c.chargeName,\n unitPrice: c.unitPrice,\n note: c.note ?? 'Usage-based charge \u2014 billed on actual consumption.',\n }))\n\n const presentCharge = body.includeWaterfall ? withChargeWaterfall : withoutChargeWaterfall\n return NextResponse.json({\n primaryCharges: primaryCharges.map(presentCharge),\n childItems: childItems.map((item) => ({ ...item, charges: item.charges.map(presentCharge) })),\n totals: { nrcTotal, mrcTotal, usageCharges },\n currencyCode,\n })\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation error', details: err.issues }, { status: 400 })\n }\n console.error('[cpq/quotes/price.POST]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\n\nconst chargeDoc = z.object({\n chargeCode: z.string(), chargeName: z.string(), chargeType: z.enum(['nrc', 'mrc', 'usage']),\n pricingMethod: z.enum(['flat', 'per_unit', 'volume', 'tiered']), currencyCode: z.string(),\n unitPrice: z.number(), quantity: z.number().nullable(), totalPrice: z.number().nullable(),\n listUnitPrice: z.number().optional(), listTotalPrice: z.number().nullable().optional(),\n standardUnitPrice: z.number().optional(), standardTotalPrice: z.number().nullable().optional(),\n adjustments: z.array(z.object({ origin: z.literal('rule'), ruleCode: z.string(), ruleName: z.string(),\n ruleType: z.string(), value: z.number(), unitPriceBefore: z.number(), unitPriceAfter: z.number(), delta: z.number() }).passthrough()).nullish(),\n}).passthrough()\nexport const openApi: OpenApiRouteDoc = { tag: 'CPQ Pricing', methods: { POST: {\n summary: 'Calculate catalog prices and price rules without saving',\n description: 'Manual quote adjustments are never applied. Set includeWaterfall=true to return list and standard prices; these fields are omitted by default.',\n requestBody: { schema: cpqPricePreviewSchema },\n responses: [{ status: 200, description: 'Resolved rule prices and optional waterfall', schema: z.object({\n primaryCharges: z.array(chargeDoc),\n childItems: z.array(z.object({ offeringId: z.string().nullable(), productId: z.string().nullable(), charges: z.array(chargeDoc) })),\n totals: z.object({ nrcTotal: z.number(), mrcTotal: z.number(), usageCharges: z.array(z.unknown()) }), currencyCode: z.string(),\n }) }],\n errors: [cpqErrorDoc(400, 'Invalid pricing request'), cpqErrorDoc(500, 'Price calculation failed')],\n} } }\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,mBAAmB;AAC5B,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AAGtC,SAAS,2BAA2B;AACpC,SAAS,qBAAqB,8BAA8B;AAErD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,iBAAiB,EAAE;AAClE;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,uBAAuB,GAAG;AAC5C,QAAI,CAAC,IAAI,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAElF,UAAM,OAAO,sBAAsB,MAAM,MAAM,IAAI,KAAK,CAAC;AACzD,UAAM,QAAQ,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAC3E,UAAM,eAAe,KAAK,gBAAgB,MAAM,oBAAoB,IAAI,IAAI,KAAK;AAEjF,UAAM,iBAAiB,IAAI,UAAU,QAAQ,mBAAmB;AAEhE,QAAI,iBAAmC,CAAC;AACxC,QAAI,KAAK,aAAa;AACpB,uBAAiB,MAAM,eAAe,sBAAsB;AAAA,QAC1D,YAAY,KAAK,YAAY,cAAc;AAAA,QAC3C,WAAW,KAAK,YAAY,aAAa;AAAA,QACzC,eAAe,KAAK,YAAY;AAAA,QAChC;AAAA,QACA,GAAG;AAAA,MACL,CAAC;AAAA,IACH;AAEA,UAAM,aAAwG,CAAC;AAC/G,eAAW,SAAS,KAAK,YAAY;AACnC,YAAM,UAAU,MAAM,eAAe,sBAAsB;AAAA,QACzD,YAAY,MAAM,cAAc;AAAA,QAChC,WAAW,MAAM,aAAa;AAAA,QAC9B,eAAe,MAAM;AAAA,QACrB;AAAA,QACA,GAAG;AAAA,MACL,CAAC;AACD,iBAAW,KAAK;AAAA,QACd,YAAY,MAAM,cAAc;AAAA,QAChC,WAAW,MAAM,aAAa;AAAA,QAC9B;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,aAAa,CAAC,GAAG,gBAAgB,GAAG,WAAW,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAE9E,UAAM,WAAW,WACd,OAAO,CAAC,MAAM,EAAE,eAAe,SAAS,EAAE,cAAc,IAAI,EAC5D,OAAO,CAAC,GAAG,MAAM,KAAK,EAAE,cAAc,IAAI,CAAC;AAC9C,UAAM,WAAW,WACd,OAAO,CAAC,MAAM,EAAE,eAAe,SAAS,EAAE,cAAc,IAAI,EAC5D,OAAO,CAAC,GAAG,MAAM,KAAK,EAAE,cAAc,IAAI,CAAC;AAC9C,UAAM,eAAe,WAClB,OAAO,CAAC,MAAM,EAAE,eAAe,OAAO,EACtC,IAAI,CAAC,OAAO;AAAA,MACX,YAAY,EAAE;AAAA,MACd,YAAY,EAAE;AAAA,MACd,WAAW,EAAE;AAAA,MACb,MAAM,EAAE,QAAQ;AAAA,IAClB,EAAE;AAEJ,UAAM,gBAAgB,KAAK,mBAAmB,sBAAsB;AACpE,WAAO,aAAa,KAAK;AAAA,MACvB,gBAAgB,eAAe,IAAI,aAAa;AAAA,MAChD,YAAY,WAAW,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,SAAS,KAAK,QAAQ,IAAI,aAAa,EAAE,EAAE;AAAA,MAC5F,QAAQ,EAAE,UAAU,UAAU,aAAa;AAAA,MAC3C;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,SAAS,IAAI,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9F;AACA,YAAQ,MAAM,2BAA2B,GAAG;AAC5C,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;AAGA,MAAM,YAAY,EAAE,OAAO;AAAA,EACzB,YAAY,EAAE,OAAO;AAAA,EAAG,YAAY,EAAE,OAAO;AAAA,EAAG,YAAY,EAAE,KAAK,CAAC,OAAO,OAAO,OAAO,CAAC;AAAA,EAC1F,eAAe,EAAE,KAAK,CAAC,QAAQ,YAAY,UAAU,QAAQ,CAAC;AAAA,EAAG,cAAc,EAAE,OAAO;AAAA,EACxF,WAAW,EAAE,OAAO;AAAA,EAAG,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAAG,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EACxF,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,EAAG,gBAAgB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACrF,mBAAmB,EAAE,OAAO,EAAE,SAAS;AAAA,EAAG,oBAAoB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC7F,aAAa,EAAE,MAAM,EAAE,OAAO;AAAA,IAAE,QAAQ,EAAE,QAAQ,MAAM;AAAA,IAAG,UAAU,EAAE,OAAO;AAAA,IAAG,UAAU,EAAE,OAAO;AAAA,IAClG,UAAU,EAAE,OAAO;AAAA,IAAG,OAAO,EAAE,OAAO;AAAA,IAAG,iBAAiB,EAAE,OAAO;AAAA,IAAG,gBAAgB,EAAE,OAAO;AAAA,IAAG,OAAO,EAAE,OAAO;AAAA,EAAE,CAAC,EAAE,YAAY,CAAC,EAAE,QAAQ;AAClJ,CAAC,EAAE,YAAY;AACR,MAAM,UAA2B,EAAE,KAAK,eAAe,SAAS,EAAE,MAAM;AAAA,EAC7E,SAAS;AAAA,EACT,aAAa;AAAA,EACb,aAAa,EAAE,QAAQ,sBAAsB;AAAA,EAC7C,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,EAAE,OAAO;AAAA,IACtG,gBAAgB,EAAE,MAAM,SAAS;AAAA,IACjC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,GAAG,WAAW,EAAE,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,MAAM,SAAS,EAAE,CAAC,CAAC;AAAA,IAClI,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,UAAU,EAAE,OAAO,GAAG,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,IAAG,cAAc,EAAE,OAAO;AAAA,EAC/H,CAAC,EAAE,CAAC;AAAA,EACJ,QAAQ,CAAC,YAAY,KAAK,yBAAyB,GAAG,YAAY,KAAK,0BAA0B,CAAC;AACpG,EAAE,EAAE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { resolveCpqRouteContext } from "../context.js";
|
|
4
4
|
import { cpqCreateQuoteSchema } from "../../data/validators.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isQuotingError } from "../../services/cpqQuotingService.js";
|
|
6
6
|
import { resolveQuotingService } from "../resolveQuotingService.js";
|
|
7
7
|
import { CpqQuoteConfiguration } from "../../data/entities.js";
|
|
8
8
|
import { SalesQuote } from "@open-mercato/core/modules/sales/data/entities";
|
|
@@ -81,7 +81,7 @@ function handleError(err, context) {
|
|
|
81
81
|
if (err instanceof z.ZodError) {
|
|
82
82
|
return NextResponse.json({ error: "Validation error", details: err.issues }, { status: 400 });
|
|
83
83
|
}
|
|
84
|
-
if (err
|
|
84
|
+
if (isQuotingError(err)) {
|
|
85
85
|
const body = { error: err.message };
|
|
86
86
|
if (err.cpqStatus) body.cpqStatus = err.cpqStatus;
|
|
87
87
|
return NextResponse.json(body, { status: err.status });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/cpq/api/quotes/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../context'\nimport { cpqCreateQuoteSchema } from '../../data/validators'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../context'\nimport { cpqCreateQuoteSchema } from '../../data/validators'\nimport { isQuotingError } from '../../services/cpqQuotingService'\nimport { resolveQuotingService } from '../resolveQuotingService'\nimport { CpqQuoteConfiguration } from '../../data/entities'\nimport { SalesQuote } from '@open-mercato/core/modules/sales/data/entities'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.quotes.manage'] },\n GET: { requireAuth: true, requireFeatures: ['cpq.quotes.view'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = cpqCreateQuoteSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n const service = resolveQuotingService(ctx)\n const result = await service.createQuote(body, scope)\n return NextResponse.json(result, { status: 201 })\n } catch (err) {\n return handleError(err, 'cpq/quotes.POST')\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId, deletedAt: null }\n\n const page = Math.max(1, Number(url.searchParams.get('page') ?? '1'))\n const pageSize = Math.min(100, Math.max(1, Number(url.searchParams.get('pageSize') ?? '50')))\n\n const filters: Record<string, unknown> = { ...scope }\n\n const cpqStatus = url.searchParams.get('cpqStatus')\n if (cpqStatus) filters.cpqStatus = cpqStatus\n const currencyCode = url.searchParams.get('currencyCode')\n if (currencyCode) filters.currencyCode = currencyCode\n const customerId = url.searchParams.get('customerId')\n if (customerId) filters.customerId = customerId\n\n // Free-text search across quoteId, customerId (case-insensitive)\n const search = url.searchParams.get('search')?.trim()\n if (search) {\n filters.$or = [\n { quoteId: { $ilike: `%${search}%` } },\n { customerId: { $ilike: `%${search}%` } },\n ]\n }\n\n const ALLOWED_SORT_FIELDS = ['createdAt', 'updatedAt', 'cpqStatus', 'version', 'currencyCode'] as const\n const sortFieldParam = url.searchParams.get('sortField') ?? ''\n const sortField = (ALLOWED_SORT_FIELDS as readonly string[]).includes(sortFieldParam)\n ? (sortFieldParam as (typeof ALLOWED_SORT_FIELDS)[number])\n : 'createdAt'\n const sortDir = url.searchParams.get('sortDir') === 'asc' ? 'asc' : 'desc'\n\n const [items, total] = await ctx.em.findAndCount(CpqQuoteConfiguration, filters, {\n limit: pageSize,\n offset: (page - 1) * pageSize,\n orderBy: { [sortField]: sortDir },\n })\n\n // Enrich rows with `quoteNumber` (e.g. \"QUOTE-20260512-00005\") so the\n // list shows human-meaningful identifiers instead of truncated UUIDs.\n // Batched lookup keeps the cost flat regardless of page size.\n const salesQuoteIds = [...new Set(items.map((i) => i.quoteId).filter(Boolean))]\n const salesQuotes = salesQuoteIds.length > 0\n ? await ctx.em.find(SalesQuote, {\n id: { $in: salesQuoteIds },\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n })\n : []\n const quoteNumberById = new Map<string, string | null>(\n salesQuotes.map((q) => [q.id, q.quoteNumber ?? null]),\n )\n\n return NextResponse.json({\n items: items.map((item) => ({\n ...item,\n quoteNumber: quoteNumberById.get(item.quoteId) ?? null,\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n })\n } catch (err) {\n return handleError(err, 'cpq/quotes.GET')\n }\n}\n\nfunction handleError(err: unknown, context: string) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation error', details: err.issues }, { status: 400 })\n }\n if (isQuotingError(err)) {\n const body: Record<string, unknown> = { error: err.message }\n if (err.cpqStatus) body.cpqStatus = err.cpqStatus\n return NextResponse.json(body, { status: err.status })\n }\n console.error(`[${context}]`, err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { useCpqListData } from '../../../components/CpqListView'\nimport { useCpqRowActions } from '../../../components/useCpqRowActions'\nimport { StatusBadge, formatCurrency } from './shared'\n\ntype Asset = {\n id: string\n code: string\n name: string\n status: string\n assetType: string\n purchasePrice: number\n currencyCode: string\n createdAt: string\n}\n\nconst PAGE_SIZE = 50\n\nconst ASSET_STATUS_OPTIONS = [\n { value: 'pending', label: 'Pending' },\n { value: 'delivered', label: 'Delivered' },\n { value: 'active', label: 'Active' },\n { value: 'returned', label: 'Returned' },\n { value: 'cancelled', label: 'Cancelled' },\n] as const\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.status === 'string' && values.status) {\n params.set('status', values.status)\n }\n if (typeof values.assetType === 'string' && values.assetType.trim()) {\n params.set('assetType', values.assetType.trim())\n }\n}\n\nexport function AssetsTable() {\n const t = useT()\n const router = useRouter()\n\n const data = useCpqListData<Asset>({\n endpoint: '/api/cpq/inventory/assets',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.inventory.assets.error.load', 'Failed to load assets'),\n })\n\n // Assets follow a `delivered \u2192 active \u2192 returned` lifecycle; deletion only\n // makes sense before the asset is provisioned, so gate by status.\n const rowActionsApi = useCpqRowActions<Asset>({\n endpoint: '/api/cpq/inventory/assets',\n entityName: t('cpq.inventory.assets.entityName', 'asset'),\n editHref: (row) => `/backend/cpq/inventory/assets/${row.id}`,\n onReload: data.reload,\n canDelete: (row) => row.status === 'pending' || row.status === 'cancelled',\n })\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('cpq.inventory.filters.status', 'Status'),\n type: 'select',\n options: [...ASSET_STATUS_OPTIONS],\n },\n {\n id: 'assetType',\n label: t('cpq.inventory.filters.assetType', 'Asset Type'),\n type: 'text',\n },\n ],\n [t],\n )\n\n const columns = React.useMemo<ColumnDef<Asset>[]>(\n () => [\n {\n accessorKey: 'code',\n header: t('cpq.inventory.assets.table.code', 'Code'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('cpq.inventory.assets.table.name', 'Name'),\n cell: ({ row }) => <span className=\"font-medium text-primary\">{row.original.name}</span>,\n },\n {\n accessorKey: 'assetType',\n header: t('cpq.inventory.assets.table.type', 'Type'),\n },\n {\n accessorKey: 'status',\n header: t('cpq.inventory.assets.table.status', 'Status'),\n cell: ({ row }) => <StatusBadge status={row.original.status} />,\n },\n {\n accessorKey: 'purchasePrice',\n header: t('cpq.inventory.assets.table.price', 'Price'),\n cell: ({ row }) => (\n <span className=\"font-mono\">\n {formatCurrency(row.original.purchasePrice, row.original.currencyCode || 'USD')}\n </span>\n ),\n },\n {\n accessorKey: 'createdAt',\n header: t('cpq.inventory.assets.table.created', 'Created'),\n cell: ({ row }) => (\n <span>\n {row.original.createdAt ? new Date(row.original.createdAt).toLocaleDateString() : '\u2014'}\n </span>\n ),\n },\n ],\n [t],\n )\n\n return (\n <>\n {rowActionsApi.ConfirmDialogElement}\n <DataTable<Asset>\n title={t('cpq.inventory.assets', 'Assets')}\n refreshButton={{\n label: t('cpq.inventory.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={t('cpq.inventory.assets.search.placeholder', 'Search assets...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n onRowClick={(row) => router.push(`/backend/cpq/inventory/assets/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActionsApi.buildItems(row)} />}\n perspective={{ tableId: 'cpq.inventory.assets.list' }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize: PAGE_SIZE,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.inventory.assets.empty', 'No assets found.')}\n </div>\n }\n />\n </>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { useCpqListData } from '../../../components/CpqListView'\nimport { useCpqRowActions } from '../../../components/useCpqRowActions'\nimport { StatusBadge, formatCurrency } from './shared'\n\ntype Asset = {\n id: string\n code: string\n name: string\n status: string\n assetType: string\n purchasePrice: number\n currencyCode: string\n createdAt: string\n}\n\nconst PAGE_SIZE = 50\n\nconst ASSET_STATUS_OPTIONS = [\n { value: 'pending', label: 'Pending' },\n { value: 'delivered', label: 'Delivered' },\n { value: 'active', label: 'Active' },\n { value: 'returned', label: 'Returned' },\n { value: 'cancelled', label: 'Cancelled' },\n] as const\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.status === 'string' && values.status) {\n params.set('status', values.status)\n }\n if (typeof values.assetType === 'string' && values.assetType.trim()) {\n params.set('assetType', values.assetType.trim())\n }\n}\n\nexport function AssetsTable() {\n const t = useT()\n const router = useRouter()\n\n const data = useCpqListData<Asset>({\n endpoint: '/api/cpq/inventory/assets',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.inventory.assets.error.load', 'Failed to load assets'),\n })\n\n // Assets follow a `delivered \u2192 active \u2192 returned` lifecycle; deletion only\n // makes sense before the asset is provisioned, so gate by status.\n const rowActionsApi = useCpqRowActions<Asset>({\n endpoint: '/api/cpq/inventory/assets',\n entityName: t('cpq.inventory.assets.entityName', 'asset'),\n editHref: (row) => `/backend/cpq/inventory/assets/${row.id}`,\n onReload: data.reload,\n canDelete: (row) => row.status === 'pending' || row.status === 'cancelled',\n })\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('cpq.inventory.filters.status', 'Status'),\n type: 'select',\n options: [...ASSET_STATUS_OPTIONS],\n },\n {\n id: 'assetType',\n label: t('cpq.inventory.filters.assetType', 'Asset Type'),\n type: 'text',\n },\n ],\n [t],\n )\n\n const columns = React.useMemo<ColumnDef<Asset>[]>(\n () => [\n {\n accessorKey: 'code',\n header: t('cpq.inventory.assets.table.code', 'Code'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('cpq.inventory.assets.table.name', 'Name'),\n cell: ({ row }) => <span className=\"font-medium text-primary\">{row.original.name}</span>,\n },\n {\n accessorKey: 'assetType',\n header: t('cpq.inventory.assets.table.type', 'Type'),\n },\n {\n accessorKey: 'status',\n header: t('cpq.inventory.assets.table.status', 'Status'),\n cell: ({ row }) => <StatusBadge status={row.original.status} />,\n },\n {\n accessorKey: 'purchasePrice',\n header: t('cpq.inventory.assets.table.price', 'Price'),\n cell: ({ row }) => (\n <span className=\"font-mono\">\n {formatCurrency(row.original.purchasePrice, row.original.currencyCode || 'USD')}\n </span>\n ),\n },\n {\n accessorKey: 'createdAt',\n header: t('cpq.inventory.assets.table.created', 'Created'),\n cell: ({ row }) => (\n <span>\n {row.original.createdAt ? new Date(row.original.createdAt).toLocaleDateString() : '\u2014'}\n </span>\n ),\n },\n ],\n [t],\n )\n\n return (\n <>\n {rowActionsApi.ConfirmDialogElement}\n <DataTable<Asset>\n title={t('cpq.inventory.assets', 'Assets')}\n refreshButton={{\n label: t('cpq.inventory.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={t('cpq.inventory.assets.search.placeholder', 'Search assets...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n onRowClick={(row) => router.push(`/backend/cpq/inventory/assets/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActionsApi.buildItems(row)} />}\n perspective={{ tableId: 'cpq.inventory.assets.list' }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize: PAGE_SIZE,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.inventory.assets.empty', 'No assets found.')}\n </div>\n }\n />\n </>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { useCpqListData } from '../../../components/CpqListView'\nimport { useCpqRowActions } from '../../../components/useCpqRowActions'\nimport { StatusBadge, formatCurrency } from './shared'\n\n// `all` is a sentinel value because Radix Select disallows empty strings\n// as option values. We map it back to \"no filter\" before writing to the\n// query.\nconst EXPIRING_WINDOWS = [\n { value: 'all', label: 'All subscriptions' },\n { value: '7', label: 'Expiring in 7 days' },\n { value: '14', label: 'Expiring in 14 days' },\n { value: '30', label: 'Expiring in 30 days' },\n { value: '60', label: 'Expiring in 60 days' },\n { value: '90', label: 'Expiring in 90 days' },\n]\n\ntype Subscription = {\n id: string\n code: string\n name: string\n status: string\n mrcAmount: number\n nrcAmount: number\n billingCycle: string\n currencyCode: string\n startDate: string\n currentTermEnd: string | null\n}\n\nconst PAGE_SIZE = 50\n\nconst SUBSCRIPTION_STATUS_OPTIONS = [\n { value: 'pending', label: 'Pending' },\n { value: 'active', label: 'Active' },\n { value: 'suspended', label: 'Suspended' },\n { value: 'terminated', label: 'Terminated' },\n { value: 'expired', label: 'Expired' },\n { value: 'cancelled', label: 'Cancelled' },\n] as const\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.status === 'string' && values.status) {\n params.set('status', values.status)\n }\n if (typeof values.billingCycle === 'string' && values.billingCycle.trim()) {\n params.set('billingCycle', values.billingCycle.trim())\n }\n // Expiring window: replaces the standalone /expiring page. Save as a\n // perspective (\"Expiring 30d\", \"Expiring 90d\", \u2026) for quick recall.\n if (typeof values.expiringWithinDays === 'string' && values.expiringWithinDays) {\n params.set('expiringWithinDays', values.expiringWithinDays)\n }\n}\n\nexport function SubscriptionsTable() {\n const t = useT()\n const router = useRouter()\n\n const data = useCpqListData<Subscription>({\n endpoint: '/api/cpq/inventory/subscriptions',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.inventory.subscriptions.error.load', 'Failed to load subscriptions'),\n })\n\n // Subscriptions are lifecycle-managed; deleting an active one usually means\n // \"terminate\", which the API rejects. Allow deletion only from non-active\n // states so the row-actions menu doesn't dangle a useless \"Delete\" item.\n const rowActionsApi = useCpqRowActions<Subscription>({\n endpoint: '/api/cpq/inventory/subscriptions',\n entityName: t('cpq.inventory.subscriptions.entityName', 'subscription'),\n editHref: (row) => `/backend/cpq/inventory/subscriptions/${row.id}`,\n onReload: data.reload,\n canDelete: (row) => row.status !== 'active',\n })\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('cpq.inventory.filters.status', 'Status'),\n type: 'select',\n options: [...SUBSCRIPTION_STATUS_OPTIONS],\n },\n {\n id: 'billingCycle',\n label: t('cpq.inventory.filters.billingCycle', 'Billing Cycle'),\n type: 'text',\n },\n {\n id: 'expiringWithinDays',\n label: t('cpq.inventory.filters.expiringWithinDays', 'Expiring within (days)'),\n type: 'select',\n // Pick from common renewal-watch windows. Backend caps at 365.\n options: [\n { value: '7', label: '7 days' },\n { value: '14', label: '14 days' },\n { value: '30', label: '30 days' },\n { value: '60', label: '60 days' },\n { value: '90', label: '90 days' },\n ],\n },\n ],\n [t],\n )\n\n const columns = React.useMemo<ColumnDef<Subscription>[]>(\n () => [\n {\n accessorKey: 'code',\n header: t('cpq.inventory.subscriptions.table.code', 'Code'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('cpq.inventory.subscriptions.table.name', 'Name'),\n cell: ({ row }) => <span className=\"font-medium text-primary\">{row.original.name}</span>,\n },\n {\n accessorKey: 'status',\n header: t('cpq.inventory.subscriptions.table.status', 'Status'),\n cell: ({ row }) => <StatusBadge status={row.original.status} />,\n },\n {\n id: 'mrc',\n header: t('cpq.inventory.subscriptions.table.mrc', 'MRC'),\n cell: ({ row }) => (\n <span className=\"font-mono\">\n {formatCurrency(row.original.mrcAmount, row.original.currencyCode || 'USD')}\n </span>\n ),\n },\n {\n accessorKey: 'billingCycle',\n header: t('cpq.inventory.subscriptions.table.billing', 'Billing'),\n },\n {\n accessorKey: 'startDate',\n header: t('cpq.inventory.subscriptions.table.startDate', 'Start Date'),\n cell: ({ row }) => (\n <span>\n {row.original.startDate ? new Date(row.original.startDate).toLocaleDateString() : '\u2014'}\n </span>\n ),\n },\n {\n accessorKey: 'currentTermEnd',\n header: t('cpq.inventory.subscriptions.table.termEnd', 'Term End'),\n cell: ({ row }) => (\n <span>\n {row.original.currentTermEnd\n ? new Date(row.original.currentTermEnd).toLocaleDateString()\n : '\u2014'}\n </span>\n ),\n },\n ],\n [t],\n )\n\n // One-click \"Expiring within\" picker that writes into the same filter\n // key the FilterBar uses (`expiringWithinDays`). Operators get instant\n // access without opening the filter overlay, and the value still rides\n // into perspectives because it's stored in `data.filterValues`.\n const currentExpiring = typeof data.filterValues.expiringWithinDays === 'string' && data.filterValues.expiringWithinDays\n ? data.filterValues.expiringWithinDays\n : 'all'\n const onExpiringChange = (value: string) => {\n const next: FilterValues = { ...data.filterValues }\n if (value && value !== 'all') next.expiringWithinDays = value\n else delete next.expiringWithinDays\n data.setFilterValues(next)\n }\n\n // Render the picker inline with the DataTable's title-row actions\n // (right next to Refresh / kebab menu) so it sits flush with the\n // card chrome instead of floating outside.\n const expiringPicker = (\n <Select value={currentExpiring} onValueChange={onExpiringChange}>\n <SelectTrigger className=\"h-9 w-[200px]\">\n <SelectValue placeholder=\"All subscriptions\" />\n </SelectTrigger>\n <SelectContent>\n {EXPIRING_WINDOWS.map((w) => (\n <SelectItem key={w.value} value={w.value}>\n {w.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n\n return (\n <>\n {rowActionsApi.ConfirmDialogElement}\n <DataTable<Subscription>\n title={t('cpq.inventory.subscriptions', 'Subscriptions')}\n actions={expiringPicker}\n refreshButton={{\n label: t('cpq.inventory.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={t('cpq.inventory.subscriptions.search.placeholder', 'Search subscriptions...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n onRowClick={(row) => router.push(`/backend/cpq/inventory/subscriptions/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActionsApi.buildItems(row)} />}\n perspective={{ tableId: 'cpq.inventory.subscriptions.list' }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize: PAGE_SIZE,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.inventory.subscriptions.empty', 'No subscriptions found.')}\n </div>\n }\n />\n </>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { useCpqListData } from '../../../components/CpqListView'\nimport { useCpqRowActions } from '../../../components/useCpqRowActions'\nimport { StatusBadge, formatCurrency } from './shared'\n\n// `all` is a sentinel value because Radix Select disallows empty strings\n// as option values. We map it back to \"no filter\" before writing to the\n// query.\nconst EXPIRING_WINDOWS = [\n { value: 'all', label: 'All subscriptions' },\n { value: '7', label: 'Expiring in 7 days' },\n { value: '14', label: 'Expiring in 14 days' },\n { value: '30', label: 'Expiring in 30 days' },\n { value: '60', label: 'Expiring in 60 days' },\n { value: '90', label: 'Expiring in 90 days' },\n]\n\ntype Subscription = {\n id: string\n code: string\n name: string\n status: string\n mrcAmount: number\n nrcAmount: number\n billingCycle: string\n currencyCode: string\n startDate: string\n currentTermEnd: string | null\n}\n\nconst PAGE_SIZE = 50\n\nconst SUBSCRIPTION_STATUS_OPTIONS = [\n { value: 'pending', label: 'Pending' },\n { value: 'active', label: 'Active' },\n { value: 'suspended', label: 'Suspended' },\n { value: 'terminated', label: 'Terminated' },\n { value: 'expired', label: 'Expired' },\n { value: 'cancelled', label: 'Cancelled' },\n] as const\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.status === 'string' && values.status) {\n params.set('status', values.status)\n }\n if (typeof values.billingCycle === 'string' && values.billingCycle.trim()) {\n params.set('billingCycle', values.billingCycle.trim())\n }\n // Expiring window: replaces the standalone /expiring page. Save as a\n // perspective (\"Expiring 30d\", \"Expiring 90d\", \u2026) for quick recall.\n if (typeof values.expiringWithinDays === 'string' && values.expiringWithinDays) {\n params.set('expiringWithinDays', values.expiringWithinDays)\n }\n}\n\nexport function SubscriptionsTable() {\n const t = useT()\n const router = useRouter()\n\n const data = useCpqListData<Subscription>({\n endpoint: '/api/cpq/inventory/subscriptions',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.inventory.subscriptions.error.load', 'Failed to load subscriptions'),\n })\n\n // Subscriptions are lifecycle-managed; deleting an active one usually means\n // \"terminate\", which the API rejects. Allow deletion only from non-active\n // states so the row-actions menu doesn't dangle a useless \"Delete\" item.\n const rowActionsApi = useCpqRowActions<Subscription>({\n endpoint: '/api/cpq/inventory/subscriptions',\n entityName: t('cpq.inventory.subscriptions.entityName', 'subscription'),\n editHref: (row) => `/backend/cpq/inventory/subscriptions/${row.id}`,\n onReload: data.reload,\n canDelete: (row) => row.status !== 'active',\n })\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('cpq.inventory.filters.status', 'Status'),\n type: 'select',\n options: [...SUBSCRIPTION_STATUS_OPTIONS],\n },\n {\n id: 'billingCycle',\n label: t('cpq.inventory.filters.billingCycle', 'Billing Cycle'),\n type: 'text',\n },\n {\n id: 'expiringWithinDays',\n label: t('cpq.inventory.filters.expiringWithinDays', 'Expiring within (days)'),\n type: 'select',\n // Pick from common renewal-watch windows. Backend caps at 365.\n options: [\n { value: '7', label: '7 days' },\n { value: '14', label: '14 days' },\n { value: '30', label: '30 days' },\n { value: '60', label: '60 days' },\n { value: '90', label: '90 days' },\n ],\n },\n ],\n [t],\n )\n\n const columns = React.useMemo<ColumnDef<Subscription>[]>(\n () => [\n {\n accessorKey: 'code',\n header: t('cpq.inventory.subscriptions.table.code', 'Code'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('cpq.inventory.subscriptions.table.name', 'Name'),\n cell: ({ row }) => <span className=\"font-medium text-primary\">{row.original.name}</span>,\n },\n {\n accessorKey: 'status',\n header: t('cpq.inventory.subscriptions.table.status', 'Status'),\n cell: ({ row }) => <StatusBadge status={row.original.status} />,\n },\n {\n id: 'mrc',\n header: t('cpq.inventory.subscriptions.table.mrc', 'MRC'),\n cell: ({ row }) => (\n <span className=\"font-mono\">\n {formatCurrency(row.original.mrcAmount, row.original.currencyCode || 'USD')}\n </span>\n ),\n },\n {\n accessorKey: 'billingCycle',\n header: t('cpq.inventory.subscriptions.table.billing', 'Billing'),\n },\n {\n accessorKey: 'startDate',\n header: t('cpq.inventory.subscriptions.table.startDate', 'Start Date'),\n cell: ({ row }) => (\n <span>\n {row.original.startDate ? new Date(row.original.startDate).toLocaleDateString() : '\u2014'}\n </span>\n ),\n },\n {\n accessorKey: 'currentTermEnd',\n header: t('cpq.inventory.subscriptions.table.termEnd', 'Term End'),\n cell: ({ row }) => (\n <span>\n {row.original.currentTermEnd\n ? new Date(row.original.currentTermEnd).toLocaleDateString()\n : '\u2014'}\n </span>\n ),\n },\n ],\n [t],\n )\n\n // One-click \"Expiring within\" picker that writes into the same filter\n // key the FilterBar uses (`expiringWithinDays`). Operators get instant\n // access without opening the filter overlay, and the value still rides\n // into perspectives because it's stored in `data.filterValues`.\n const currentExpiring = typeof data.filterValues.expiringWithinDays === 'string' && data.filterValues.expiringWithinDays\n ? data.filterValues.expiringWithinDays\n : 'all'\n const onExpiringChange = (value: string) => {\n const next: FilterValues = { ...data.filterValues }\n if (value && value !== 'all') next.expiringWithinDays = value\n else delete next.expiringWithinDays\n data.setFilterValues(next)\n }\n\n // Render the picker inline with the DataTable's title-row actions\n // (right next to Refresh / kebab menu) so it sits flush with the\n // card chrome instead of floating outside.\n const expiringPicker = (\n <Select value={currentExpiring} onValueChange={onExpiringChange}>\n <SelectTrigger className=\"h-9 w-[200px]\">\n <SelectValue placeholder=\"All subscriptions\" />\n </SelectTrigger>\n <SelectContent>\n {EXPIRING_WINDOWS.map((w) => (\n <SelectItem key={w.value} value={w.value}>\n {w.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n\n return (\n <>\n {rowActionsApi.ConfirmDialogElement}\n <DataTable<Subscription>\n title={t('cpq.inventory.subscriptions', 'Subscriptions')}\n actions={expiringPicker}\n refreshButton={{\n label: t('cpq.inventory.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={t('cpq.inventory.subscriptions.search.placeholder', 'Search subscriptions...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n onRowClick={(row) => router.push(`/backend/cpq/inventory/subscriptions/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActionsApi.buildItems(row)} />}\n perspective={{ tableId: 'cpq.inventory.subscriptions.list' }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize: PAGE_SIZE,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.inventory.subscriptions.empty', 'No subscriptions found.')}\n </div>\n }\n />\n </>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,7 +8,7 @@ const metadata = {
|
|
|
8
8
|
pageTitleKey: "cpq.inventory.title",
|
|
9
9
|
pageGroup: "CPQ",
|
|
10
10
|
pageGroupKey: "customers~sales~cpq.nav.group",
|
|
11
|
-
pageOrder:
|
|
11
|
+
pageOrder: 3,
|
|
12
12
|
icon: inventoryIcon,
|
|
13
13
|
breadcrumb: [{ label: "CPQ Subscriptions", labelKey: "cpq.inventory.title" }]
|
|
14
14
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cpq/backend/cpq/inventory/page.meta.ts"],
|
|
4
|
-
"sourcesContent": ["import React from 'react'\nimport { Package } from 'lucide-react'\n\nconst inventoryIcon = React.createElement(Package, { size: 16 })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['cpq.inventory.view'],\n pageTitle: 'CPQ Subscriptions',\n pageTitleKey: 'cpq.inventory.title',\n pageGroup: 'CPQ',\n pageGroupKey: 'customers~sales~cpq.nav.group',\n pageOrder:
|
|
4
|
+
"sourcesContent": ["import React from 'react'\nimport { Package } from 'lucide-react'\n\nconst inventoryIcon = React.createElement(Package, { size: 16 })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['cpq.inventory.view'],\n pageTitle: 'CPQ Subscriptions',\n pageTitleKey: 'cpq.inventory.title',\n pageGroup: 'CPQ',\n pageGroupKey: 'customers~sales~cpq.nav.group',\n pageOrder: 3,\n icon: inventoryIcon,\n breadcrumb: [{ label: 'CPQ Subscriptions', labelKey: 'cpq.inventory.title' }],\n} as const\n"],
|
|
5
5
|
"mappings": "AAAA,OAAO,WAAW;AAClB,SAAS,eAAe;AAExB,MAAM,gBAAgB,MAAM,cAAc,SAAS,EAAE,MAAM,GAAG,CAAC;AAExD,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,oBAAoB;AAAA,EACtC,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY,CAAC,EAAE,OAAO,qBAAqB,UAAU,sBAAsB,CAAC;AAC9E;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|