@dainamite/cpq 0.5.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 +48 -0
- package/MIGRATION.md +74 -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/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]/route.js +17 -1
- package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/route.js +47 -2
- package/dist/modules/cpq/api/orders/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 +42 -2
- 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 +32 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/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/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/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.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/orders/page.meta.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.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.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/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 +25 -28
- 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 +330 -109
- 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 +1 -1
- 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/quoteMutationGuard.ts +38 -0
- package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
- package/src/modules/cpq/api/orders/[orderId]/route.ts +18 -0
- package/src/modules/cpq/api/orders/route.ts +51 -1
- 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 +48 -1
- 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 +40 -1
- 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/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/orders/[id]/page.tsx +60 -269
- package/src/modules/cpq/backend/cpq/orders/page.meta.ts +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 +174 -263
- package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +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/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__/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 +22 -28
- package/src/modules/cpq/services/cpqPricingService.ts +8 -0
- package/src/modules/cpq/services/cpqQuotingService.ts +374 -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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# @dainamite/cpq
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- adcb50f: Negotiated prices on quotes (manual price adjustments) with a price waterfall.
|
|
8
|
+
|
|
9
|
+
- New table `cpq_quote_price_adjustments` and column
|
|
10
|
+
`cpq_quote_configurations.pricing_revision` (migration
|
|
11
|
+
`Migration20261110213052`). **Upgrade steps for existing tenants (required):**
|
|
12
|
+
`yarn mercato db migrate`, then `yarn mercato cpq seed-price-adjustments`
|
|
13
|
+
(rollout toggle + reason dictionary) and `yarn mercato auth sync-role-acls`
|
|
14
|
+
(`cpq.quotes.adjust_price` grant). Without them writes return `403` and the
|
|
15
|
+
UI hides the actions. New tenants get all three at initialization.
|
|
16
|
+
- Reps with `cpq.quotes.manage` + `cpq.quotes.adjust_price` can override a
|
|
17
|
+
charge's unit price, take an amount off a charge or give a percentage on a
|
|
18
|
+
charge or line (`/api/cpq/quotes/{quoteId}/price-adjustments`, plus a
|
|
19
|
+
no-write `preview`). Adjustments are permanent, never exceed the standard
|
|
20
|
+
price, never compound, and lock once the quote leaves New / Incomplete /
|
|
21
|
+
Ready. Rollout toggle `cpq.manual_price_adjustments` (seeded on) and reason
|
|
22
|
+
dictionary `cpq.price_adjustment_reason`.
|
|
23
|
+
- Charges gain additive `listUnitPrice`, `listTotalPrice`, `standardUnitPrice`,
|
|
24
|
+
`standardTotalPrice` and `adjustmentErrors`; adjustment entries gain
|
|
25
|
+
`origin: 'rule' | 'manual'`. `pricingSummary` gains list / standard totals,
|
|
26
|
+
rule and manual discount totals, deal value, discount depth and
|
|
27
|
+
`snapshotHash`; `QuoteResult` gains `pricingRevision`. `discountTotal` is
|
|
28
|
+
unchanged.
|
|
29
|
+
- New events `cpq.quote.pricing_changed` and `cpq.quote.status_changed`
|
|
30
|
+
(after commit, with the pricing snapshot) for project approval workflows.
|
|
31
|
+
- Order conversion copies stored prices and returns `422 ADJUSTMENT_INVALID`
|
|
32
|
+
when a stored adjustment is invalid. `POST /api/cpq/quotes/price` accepts
|
|
33
|
+
`includeWaterfall` and never applies negotiated prices.
|
|
34
|
+
- Quote, order and subscription pages show List / Offer prices and the
|
|
35
|
+
waterfall; the quote page adds the adjustment dialog and panel.
|
|
36
|
+
|
|
37
|
+
- 8d57b1b: Harden quote pricing ahead of manual price adjustments.
|
|
38
|
+
|
|
39
|
+
- `POST /api/cpq/quotes/{quoteId}/recalculate` now requires `cpq.quotes.manage`
|
|
40
|
+
(was `cpq.quotes.view`), and a saved recalculation (`save: true`) on an
|
|
41
|
+
accepted, rejected or cancelled quote returns `409`. Integrations that
|
|
42
|
+
recalculate with a view-only API key must be granted `cpq.quotes.manage`.
|
|
43
|
+
- Amend quotes no longer re-price items the operator does not touch: mirror
|
|
44
|
+
lines keep the subscription's stored price until their configuration,
|
|
45
|
+
quantity, term, dates or usage estimates change.
|
|
46
|
+
- Status transitions, line add / update / remove, saved recalculations and
|
|
47
|
+
quote-to-order conversion run every registered mutation guard
|
|
48
|
+
(resource kinds `cpq.quote` and `cpq.quote_line`), so projects can enforce
|
|
49
|
+
approvals. No behaviour change when no guard is registered.
|
|
50
|
+
|
|
3
51
|
## 0.5.0
|
|
4
52
|
|
|
5
53
|
### Minor Changes
|
package/MIGRATION.md
CHANGED
|
@@ -166,6 +166,80 @@ the `@dainamite/cpq` package, per
|
|
|
166
166
|
|
|
167
167
|
## Schema / API changelog (track breaking changes here)
|
|
168
168
|
|
|
169
|
+
### 2026-11-10 — Manual price adjustments and price waterfall (price adjustments, Phase 1)
|
|
170
|
+
|
|
171
|
+
Spec: `.ai/specs/2026-11-10-cpq-manual-price-adjustments.md`. Manual:
|
|
172
|
+
`manuals/cpq-price-adjustments.md`.
|
|
173
|
+
|
|
174
|
+
**Schema (additive).** Migration `Migration20261110213052` adds
|
|
175
|
+
`cpq_quote_price_adjustments` (tenant-scoped, soft-deleted, FK-string ids only)
|
|
176
|
+
and `cpq_quote_configurations.pricing_revision int default 0`. Rollback drops
|
|
177
|
+
both. No data migration: existing quotes keep their stored charges; list /
|
|
178
|
+
standard prices are derived for responses until the next saved recalculation.
|
|
179
|
+
|
|
180
|
+
**Upgrade steps for existing tenants (required).** The toggle, the reason
|
|
181
|
+
dictionary and the role grant are created by `setup.ts` at tenant
|
|
182
|
+
initialization only. After `yarn mercato db migrate`, run
|
|
183
|
+
`yarn mercato cpq seed-price-adjustments` (idempotent; `--tenant` / `--org`
|
|
184
|
+
to target one) and `yarn mercato auth sync-role-acls`. Skipping them leaves the
|
|
185
|
+
feature fail-closed: writes `403`, actions hidden, and every reason
|
|
186
|
+
`REASON_NOT_FOUND` if the toggle is switched on by hand.
|
|
187
|
+
|
|
188
|
+
**Write serialization.** Every pricing write (line add / update / remove, ARC
|
|
189
|
+
target detach, saved recalculation, clone, status change, adjustment CRUD,
|
|
190
|
+
order conversion) runs in
|
|
191
|
+
a transaction that locks the quote row (`SELECT … FOR UPDATE`) and bumps
|
|
192
|
+
`pricing_revision`. Adjustment writes accept `expectedRevision` and return
|
|
193
|
+
`409 STALE_REVISION` on mismatch.
|
|
194
|
+
|
|
195
|
+
**Response shapes (additive).** See the changeset. `discountTotal` keeps its
|
|
196
|
+
old meaning (rule discounts only) for existing consumers; use
|
|
197
|
+
`ruleDiscountTotal` / `manualDiscountTotal` for the split.
|
|
198
|
+
|
|
199
|
+
**Events.** `cpq.quote.pricing_changed` fires after a committed save whose
|
|
200
|
+
`snapshotHash` changed; `cpq.quote.status_changed` after every committed status
|
|
201
|
+
change (`cause: manual | auto`). Quotes saved before this release have no
|
|
202
|
+
stored hash: their first save records a baseline silently. New and ARC quotes
|
|
203
|
+
start with an empty-quote hash, so their first priced line is a signal.
|
|
204
|
+
`transitionStatus(quoteId, status, scope, actorUserId?)` gained an optional
|
|
205
|
+
actor argument.
|
|
206
|
+
|
|
207
|
+
**Conversion.** A quote whose stored waterfall contains an invalid manual
|
|
208
|
+
adjustment can no longer be converted (`422 ADJUSTMENT_INVALID` with
|
|
209
|
+
`lineId` / `code`).
|
|
210
|
+
|
|
211
|
+
### 2026-11-10 — Quote pricing integrity (price adjustments, Phase 1)
|
|
212
|
+
|
|
213
|
+
Spec: `.ai/specs/2026-11-10-cpq-quote-pricing-integrity.md`. No schema change.
|
|
214
|
+
|
|
215
|
+
**Recalculate needs `cpq.quotes.manage` (breaking for view-only callers).**
|
|
216
|
+
`POST /api/cpq/quotes/{quoteId}/recalculate` required only `cpq.quotes.view`,
|
|
217
|
+
so a read-only user or API key could persist re-priced quotes. It now requires
|
|
218
|
+
`cpq.quotes.manage`. A saved recalculation (`save: true`) on an `accepted`,
|
|
219
|
+
`rejected` or `cancelled` quote returns `409` — an accepted quote's prices are
|
|
220
|
+
the basis for order conversion. Dry runs (`save: false`) on terminal quotes
|
|
221
|
+
still work for managers.
|
|
222
|
+
|
|
223
|
+
**Amend mirror lines are frozen (behaviour change).** Engine-priced mirror
|
|
224
|
+
lines on Amend quotes were re-priced at today's catalog price on every
|
|
225
|
+
recalculation, and activation wrote that price back to the subscription item.
|
|
226
|
+
They now carry `_arcPriceFrozen: true` in the line `configuration` and keep the
|
|
227
|
+
source item's stored charges until a pricing input of the line changes
|
|
228
|
+
(configuration — ARC markers and key order ignored — quantity, `termMonths`,
|
|
229
|
+
`startDate`, `endDate`, `usageEstimates`); then the marker is dropped and the
|
|
230
|
+
line is re-priced as before. Renew and Cancel quotes are unchanged. Amend
|
|
231
|
+
quotes created before this release keep the old behaviour. The marker never
|
|
232
|
+
reaches subscription items (stripped at activation) and mirror lines ignore any
|
|
233
|
+
stale `_arc*` keys on an item's configuration.
|
|
234
|
+
|
|
235
|
+
**Mutation guards on quote writes.** The status, items, recalculate and
|
|
236
|
+
order-conversion routes now call `runRouteMutationGuards`
|
|
237
|
+
(`@open-mercato/shared/lib/crud/route-mutation-guard`) with resource kinds
|
|
238
|
+
`cpq.quote` (actions `transition_status`, `recalculate`, `convert_to_order`)
|
|
239
|
+
and `cpq.quote_line` (create / update / delete). A guard can block the write or
|
|
240
|
+
modify the line payload; after-success hooks run once the write commits.
|
|
241
|
+
These routes now export `openApi`.
|
|
242
|
+
|
|
169
243
|
### 2026-11-03 — Open Mercato 0.9.0 peers; `isQuotingError` / `isOrderError`
|
|
170
244
|
|
|
171
245
|
**Peer ranges (breaking for consumers).** `@open-mercato/*` `^0.9.0`,
|
package/dist/modules/cpq/acl.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const features = [
|
|
2
|
+
{ id: "cpq.quotes.adjust_price", title: "Adjust CPQ quote prices", module: "cpq" },
|
|
2
3
|
{ id: "cpq.quotes.view", title: "View CPQ Quotes", module: "cpq" },
|
|
3
4
|
{ id: "cpq.quotes.manage", title: "Manage CPQ Quotes", module: "cpq" },
|
|
4
5
|
{ id: "cpq.pricing.view", title: "View Pricing", module: "cpq" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/cpq/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'cpq.quotes.view', title: 'View CPQ Quotes', module: 'cpq' },\n { id: 'cpq.quotes.manage', title: 'Manage CPQ Quotes', module: 'cpq' },\n { id: 'cpq.pricing.view', title: 'View Pricing', module: 'cpq' },\n { id: 'cpq.pricing.manage', title: 'Manage Pricing', module: 'cpq' },\n { id: 'cpq.products.view', title: 'View CPQ Product Config', module: 'cpq' },\n { id: 'cpq.products.manage', title: 'Manage CPQ Product Config', module: 'cpq' },\n { id: 'cpq.specifications.view', title: 'View Product Specifications', module: 'cpq' },\n { id: 'cpq.specifications.manage', title: 'Manage Product Specifications', module: 'cpq' },\n { id: 'cpq.offerings.view', title: 'View Product Offerings', module: 'cpq' },\n { id: 'cpq.offerings.manage', title: 'Manage Product Offerings', module: 'cpq' },\n { id: 'cpq.wizards.view', title: 'View Wizards', module: 'cpq' },\n { id: 'cpq.wizards.manage', title: 'Manage Wizards', module: 'cpq' },\n { id: 'cpq.orders.view', title: 'View CPQ Orders', module: 'cpq' },\n { id: 'cpq.orders.manage', title: 'Manage CPQ Orders', module: 'cpq' },\n // XD-250 ARC (Amend / Renew / Cancel)\n { id: 'cpq.arc.amend.manage', title: 'Manage Amend Quotes', module: 'cpq' },\n { id: 'cpq.arc.renew.manage', title: 'Manage Renew Quotes', module: 'cpq' },\n { id: 'cpq.arc.cancel.manage', title: 'Manage Cancel Quotes', module: 'cpq' },\n { id: 'cpq.arc.changelog.view', title: 'View Subscription Change History', module: 'cpq' },\n // Expiring-subscriptions feature retired \u2014 the dedicated /expiring page\n // was replaced by an \"Expiring within (days)\" filter + saved perspective\n // on the main subscriptions list, which uses `cpq.inventory.view`.\n]\n\nexport default features\n"],
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,mBAAmB,OAAO,mBAAmB,QAAQ,MAAM;AAAA,EACjE,EAAE,IAAI,qBAAqB,OAAO,qBAAqB,QAAQ,MAAM;AAAA,EACrE,EAAE,IAAI,oBAAoB,OAAO,gBAAgB,QAAQ,MAAM;AAAA,EAC/D,EAAE,IAAI,sBAAsB,OAAO,kBAAkB,QAAQ,MAAM;AAAA,EACnE,EAAE,IAAI,qBAAqB,OAAO,2BAA2B,QAAQ,MAAM;AAAA,EAC3E,EAAE,IAAI,uBAAuB,OAAO,6BAA6B,QAAQ,MAAM;AAAA,EAC/E,EAAE,IAAI,2BAA2B,OAAO,+BAA+B,QAAQ,MAAM;AAAA,EACrF,EAAE,IAAI,6BAA6B,OAAO,iCAAiC,QAAQ,MAAM;AAAA,EACzF,EAAE,IAAI,sBAAsB,OAAO,0BAA0B,QAAQ,MAAM;AAAA,EAC3E,EAAE,IAAI,wBAAwB,OAAO,4BAA4B,QAAQ,MAAM;AAAA,EAC/E,EAAE,IAAI,oBAAoB,OAAO,gBAAgB,QAAQ,MAAM;AAAA,EAC/D,EAAE,IAAI,sBAAsB,OAAO,kBAAkB,QAAQ,MAAM;AAAA,EACnE,EAAE,IAAI,mBAAmB,OAAO,mBAAmB,QAAQ,MAAM;AAAA,EACjE,EAAE,IAAI,qBAAqB,OAAO,qBAAqB,QAAQ,MAAM;AAAA;AAAA,EAErE,EAAE,IAAI,wBAAwB,OAAO,uBAAuB,QAAQ,MAAM;AAAA,EAC1E,EAAE,IAAI,wBAAwB,OAAO,uBAAuB,QAAQ,MAAM;AAAA,EAC1E,EAAE,IAAI,yBAAyB,OAAO,wBAAwB,QAAQ,MAAM;AAAA,EAC5E,EAAE,IAAI,0BAA0B,OAAO,oCAAoC,QAAQ,MAAM;AAAA;AAAA;AAAA;AAI3F;AAEA,IAAO,cAAQ;",
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'cpq.quotes.adjust_price', title: 'Adjust CPQ quote prices', module: 'cpq' },\n { id: 'cpq.quotes.view', title: 'View CPQ Quotes', module: 'cpq' },\n { id: 'cpq.quotes.manage', title: 'Manage CPQ Quotes', module: 'cpq' },\n { id: 'cpq.pricing.view', title: 'View Pricing', module: 'cpq' },\n { id: 'cpq.pricing.manage', title: 'Manage Pricing', module: 'cpq' },\n { id: 'cpq.products.view', title: 'View CPQ Product Config', module: 'cpq' },\n { id: 'cpq.products.manage', title: 'Manage CPQ Product Config', module: 'cpq' },\n { id: 'cpq.specifications.view', title: 'View Product Specifications', module: 'cpq' },\n { id: 'cpq.specifications.manage', title: 'Manage Product Specifications', module: 'cpq' },\n { id: 'cpq.offerings.view', title: 'View Product Offerings', module: 'cpq' },\n { id: 'cpq.offerings.manage', title: 'Manage Product Offerings', module: 'cpq' },\n { id: 'cpq.wizards.view', title: 'View Wizards', module: 'cpq' },\n { id: 'cpq.wizards.manage', title: 'Manage Wizards', module: 'cpq' },\n { id: 'cpq.orders.view', title: 'View CPQ Orders', module: 'cpq' },\n { id: 'cpq.orders.manage', title: 'Manage CPQ Orders', module: 'cpq' },\n // XD-250 ARC (Amend / Renew / Cancel)\n { id: 'cpq.arc.amend.manage', title: 'Manage Amend Quotes', module: 'cpq' },\n { id: 'cpq.arc.renew.manage', title: 'Manage Renew Quotes', module: 'cpq' },\n { id: 'cpq.arc.cancel.manage', title: 'Manage Cancel Quotes', module: 'cpq' },\n { id: 'cpq.arc.changelog.view', title: 'View Subscription Change History', module: 'cpq' },\n // Expiring-subscriptions feature retired \u2014 the dedicated /expiring page\n // was replaced by an \"Expiring within (days)\" filter + saved perspective\n // on the main subscriptions list, which uses `cpq.inventory.view`.\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,2BAA2B,OAAO,2BAA2B,QAAQ,MAAM;AAAA,EACjF,EAAE,IAAI,mBAAmB,OAAO,mBAAmB,QAAQ,MAAM;AAAA,EACjE,EAAE,IAAI,qBAAqB,OAAO,qBAAqB,QAAQ,MAAM;AAAA,EACrE,EAAE,IAAI,oBAAoB,OAAO,gBAAgB,QAAQ,MAAM;AAAA,EAC/D,EAAE,IAAI,sBAAsB,OAAO,kBAAkB,QAAQ,MAAM;AAAA,EACnE,EAAE,IAAI,qBAAqB,OAAO,2BAA2B,QAAQ,MAAM;AAAA,EAC3E,EAAE,IAAI,uBAAuB,OAAO,6BAA6B,QAAQ,MAAM;AAAA,EAC/E,EAAE,IAAI,2BAA2B,OAAO,+BAA+B,QAAQ,MAAM;AAAA,EACrF,EAAE,IAAI,6BAA6B,OAAO,iCAAiC,QAAQ,MAAM;AAAA,EACzF,EAAE,IAAI,sBAAsB,OAAO,0BAA0B,QAAQ,MAAM;AAAA,EAC3E,EAAE,IAAI,wBAAwB,OAAO,4BAA4B,QAAQ,MAAM;AAAA,EAC/E,EAAE,IAAI,oBAAoB,OAAO,gBAAgB,QAAQ,MAAM;AAAA,EAC/D,EAAE,IAAI,sBAAsB,OAAO,kBAAkB,QAAQ,MAAM;AAAA,EACnE,EAAE,IAAI,mBAAmB,OAAO,mBAAmB,QAAQ,MAAM;AAAA,EACjE,EAAE,IAAI,qBAAqB,OAAO,qBAAqB,QAAQ,MAAM;AAAA;AAAA,EAErE,EAAE,IAAI,wBAAwB,OAAO,uBAAuB,QAAQ,MAAM;AAAA,EAC1E,EAAE,IAAI,wBAAwB,OAAO,uBAAuB,QAAQ,MAAM;AAAA,EAC1E,EAAE,IAAI,yBAAyB,OAAO,wBAAwB,QAAQ,MAAM;AAAA,EAC5E,EAAE,IAAI,0BAA0B,OAAO,oCAAoC,QAAQ,MAAM;AAAA;AAAA;AAAA;AAI3F;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
3
|
+
import { runCpqMutationGuards } from "./quoteMutationGuard.js";
|
|
4
|
+
async function adjustmentCommandRoute(req, ctx, quoteId, body, schema, operation, adjustmentId) {
|
|
5
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
6
|
+
resourceKind: "cpq.quote_price_adjustment",
|
|
7
|
+
resourceId: adjustmentId,
|
|
8
|
+
operation,
|
|
9
|
+
mutationPayload: { quoteId, ...body }
|
|
10
|
+
});
|
|
11
|
+
if (!guard.ok) return guard.response;
|
|
12
|
+
const modified = Object.fromEntries(Object.entries(guard.modifiedPayload ?? {}).filter(([key]) => key !== "quoteId" && key !== "adjustmentId"));
|
|
13
|
+
const effective = schema.parse({ ...body, ...modified });
|
|
14
|
+
const input = operation === "create" ? { quoteId, adjustment: effective } : operation === "update" ? { quoteId, adjustmentId, patch: effective } : { quoteId, adjustmentId, ...effective };
|
|
15
|
+
const commandCtx = {
|
|
16
|
+
container: ctx.container,
|
|
17
|
+
auth: ctx.auth,
|
|
18
|
+
organizationScope: ctx.organizationScope,
|
|
19
|
+
selectedOrganizationId: ctx.organizationId,
|
|
20
|
+
organizationIds: [ctx.organizationId],
|
|
21
|
+
request: req
|
|
22
|
+
};
|
|
23
|
+
const bus = ctx.container.resolve("commandBus");
|
|
24
|
+
const commandId = `cpq.quote_price_adjustment.${operation}`;
|
|
25
|
+
const { result, logEntry } = await bus.execute(commandId, { input, ctx: commandCtx });
|
|
26
|
+
if (!result) throw new Error("Adjustment command did not return a result");
|
|
27
|
+
await guard.runAfterSuccess();
|
|
28
|
+
const response = NextResponse.json(operation === "delete" ? { quote: result.quote } : { adjustment: result.adjustment, quote: result.quote }, { status: operation === "create" ? 201 : 200 });
|
|
29
|
+
if (logEntry?.undoToken && logEntry.id && logEntry.commandId) {
|
|
30
|
+
response.headers.set("x-om-operation", serializeOperationMetadata({
|
|
31
|
+
id: logEntry.id,
|
|
32
|
+
undoToken: logEntry.undoToken,
|
|
33
|
+
commandId: logEntry.commandId,
|
|
34
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
35
|
+
resourceKind: "cpq.quote_price_adjustment",
|
|
36
|
+
resourceId: result.adjustment.id,
|
|
37
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : typeof logEntry.createdAt === "string" ? logEntry.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
adjustmentCommandRoute
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=adjustmentCommandRoute.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { z } from 'zod'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CpqRouteContext } from '../context'\nimport { runCpqMutationGuards } from './quoteMutationGuard'\nimport type { AdjustmentWriteResult } from '../../services/priceAdjustments/adjustmentService'\n\nexport async function adjustmentCommandRoute<T extends object>(\n req: Request, ctx: CpqRouteContext, quoteId: string, body: T,\n schema: z.ZodType<T>, operation: 'create' | 'update' | 'delete', adjustmentId?: string,\n): Promise<NextResponse | Response> {\n const guard = await runCpqMutationGuards(ctx, req, {\n resourceKind: 'cpq.quote_price_adjustment', resourceId: adjustmentId,\n operation, mutationPayload: { quoteId, ...body },\n })\n if (!guard.ok) return guard.response\n // Path identifiers belong to the server, even when a project guard returns\n // the full payload including the original quoteId metadata.\n const modified = Object.fromEntries(Object.entries(guard.modifiedPayload ?? {})\n .filter(([key]) => key !== 'quoteId' && key !== 'adjustmentId'))\n const effective = schema.parse({ ...body, ...modified })\n const input = operation === 'create' ? { quoteId, adjustment: effective }\n : operation === 'update' ? { quoteId, adjustmentId, patch: effective }\n : { quoteId, adjustmentId, ...effective }\n const commandCtx: CommandRuntimeContext = {\n container: ctx.container, auth: ctx.auth, organizationScope: ctx.organizationScope,\n selectedOrganizationId: ctx.organizationId, organizationIds: [ctx.organizationId], request: req,\n }\n const bus = ctx.container.resolve<CommandBus>('commandBus')\n const commandId = `cpq.quote_price_adjustment.${operation}`\n const { result, logEntry } = await bus.execute<typeof input, AdjustmentWriteResult>(commandId, { input, ctx: commandCtx })\n if (!result) throw new Error('Adjustment command did not return a result')\n await guard.runAfterSuccess()\n const response = NextResponse.json(operation === 'delete' ? { quote: result.quote }\n : { adjustment: result.adjustment, quote: result.quote }, { status: operation === 'create' ? 201 : 200 })\n if (logEntry?.undoToken && logEntry.id && logEntry.commandId) {\n response.headers.set('x-om-operation', serializeOperationMetadata({ id: logEntry.id, undoToken: logEntry.undoToken,\n commandId: logEntry.commandId, actionLabel: logEntry.actionLabel ?? null,\n resourceKind: 'cpq.quote_price_adjustment', resourceId: result.adjustment.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string' ? logEntry.createdAt : new Date().toISOString() }))\n }\n return response\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAG7B,SAAS,kCAAkC;AAE3C,SAAS,4BAA4B;AAGrC,eAAsB,uBACpB,KAAc,KAAsB,SAAiB,MACrD,QAAsB,WAA2C,cAC/B;AAClC,QAAM,QAAQ,MAAM,qBAAqB,KAAK,KAAK;AAAA,IACjD,cAAc;AAAA,IAA8B,YAAY;AAAA,IACxD;AAAA,IAAW,iBAAiB,EAAE,SAAS,GAAG,KAAK;AAAA,EACjD,CAAC;AACD,MAAI,CAAC,MAAM,GAAI,QAAO,MAAM;AAG5B,QAAM,WAAW,OAAO,YAAY,OAAO,QAAQ,MAAM,mBAAmB,CAAC,CAAC,EAC3E,OAAO,CAAC,CAAC,GAAG,MAAM,QAAQ,aAAa,QAAQ,cAAc,CAAC;AACjE,QAAM,YAAY,OAAO,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC;AACvD,QAAM,QAAQ,cAAc,WAAW,EAAE,SAAS,YAAY,UAAU,IACpE,cAAc,WAAW,EAAE,SAAS,cAAc,OAAO,UAAU,IACjE,EAAE,SAAS,cAAc,GAAG,UAAU;AAC5C,QAAM,aAAoC;AAAA,IACxC,WAAW,IAAI;AAAA,IAAW,MAAM,IAAI;AAAA,IAAM,mBAAmB,IAAI;AAAA,IACjE,wBAAwB,IAAI;AAAA,IAAgB,iBAAiB,CAAC,IAAI,cAAc;AAAA,IAAG,SAAS;AAAA,EAC9F;AACA,QAAM,MAAM,IAAI,UAAU,QAAoB,YAAY;AAC1D,QAAM,YAAY,8BAA8B,SAAS;AACzD,QAAM,EAAE,QAAQ,SAAS,IAAI,MAAM,IAAI,QAA6C,WAAW,EAAE,OAAO,KAAK,WAAW,CAAC;AACzH,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4CAA4C;AACzE,QAAM,MAAM,gBAAgB;AAC5B,QAAM,WAAW,aAAa,KAAK,cAAc,WAAW,EAAE,OAAO,OAAO,MAAM,IAC9E,EAAE,YAAY,OAAO,YAAY,OAAO,OAAO,MAAM,GAAG,EAAE,QAAQ,cAAc,WAAW,MAAM,IAAI,CAAC;AAC1G,MAAI,UAAU,aAAa,SAAS,MAAM,SAAS,WAAW;AAC5D,aAAS,QAAQ,IAAI,kBAAkB,2BAA2B;AAAA,MAAE,IAAI,SAAS;AAAA,MAAI,WAAW,SAAS;AAAA,MACvG,WAAW,SAAS;AAAA,MAAW,aAAa,SAAS,eAAe;AAAA,MACpE,cAAc;AAAA,MAA8B,YAAY,OAAO,WAAW;AAAA,MAC1E,YAAY,SAAS,qBAAqB,OAAO,SAAS,UAAU,YAAY,IAC5E,OAAO,SAAS,cAAc,WAAW,SAAS,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IAAE,CAAC,CAAC;AAAA,EACjG;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runRouteMutationGuards
|
|
3
|
+
} from "@open-mercato/shared/lib/crud/route-mutation-guard";
|
|
4
|
+
const CPQ_QUOTE_RESOURCE_KIND = "cpq.quote";
|
|
5
|
+
const CPQ_QUOTE_LINE_RESOURCE_KIND = "cpq.quote_line";
|
|
6
|
+
function runCpqMutationGuards(ctx, req, input) {
|
|
7
|
+
return runRouteMutationGuards({
|
|
8
|
+
container: ctx.container,
|
|
9
|
+
req,
|
|
10
|
+
auth: {
|
|
11
|
+
userId: ctx.auth?.sub ?? "",
|
|
12
|
+
tenantId: ctx.tenantId,
|
|
13
|
+
organizationId: ctx.organizationId || null
|
|
14
|
+
},
|
|
15
|
+
input
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
20
|
+
CPQ_QUOTE_RESOURCE_KIND,
|
|
21
|
+
runCpqMutationGuards
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=quoteMutationGuard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/cpq/api/_helpers/quoteMutationGuard.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n runRouteMutationGuards,\n type RouteMutationGuardInput,\n type RouteMutationGuardResult,\n} from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport type { CpqRouteContext } from '../context'\n\n/** Mutation-guard resource kinds for CPQ quote writes (`module.entity`). */\nexport const CPQ_QUOTE_RESOURCE_KIND = 'cpq.quote'\nexport const CPQ_QUOTE_LINE_RESOURCE_KIND = 'cpq.quote_line'\n\n/**\n * Run every registered mutation guard for a CPQ custom write route.\n *\n * Quote routes are hand-written (not `makeCrudRoute`), so they wire guards\n * explicitly through the framework's registry-based wrapper. Projects use\n * this hook to enforce rules CPQ does not own \u2014 e.g. blocking\n * `ready \u2192 with_customer` or order conversion until an approval workflow\n * signs a discounted quote off. With no guard registered the call passes.\n *\n * Call `runAfterSuccess()` on the passed result only after the write commits.\n */\nexport function runCpqMutationGuards(\n ctx: Pick<CpqRouteContext, 'container' | 'auth' | 'tenantId' | 'organizationId'>,\n req: Request,\n input: RouteMutationGuardInput,\n): Promise<RouteMutationGuardResult> {\n return runRouteMutationGuards({\n container: ctx.container,\n req,\n auth: {\n userId: ctx.auth?.sub ?? '',\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId || null,\n },\n input,\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,OAGK;AAIA,MAAM,0BAA0B;AAChC,MAAM,+BAA+B;AAarC,SAAS,qBACd,KACA,KACA,OACmC;AACnC,SAAO,uBAAuB;AAAA,IAC5B,WAAW,IAAI;AAAA,IACf;AAAA,IACA,MAAM;AAAA,MACJ,QAAQ,IAAI,MAAM,OAAO;AAAA,MACzB,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI,kBAAkB;AAAA,IACxC;AAAA,IACA;AAAA,EACF,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const cpqErrorResponseSchema = z.object({ error: z.string() }).passthrough();
|
|
3
|
+
function cpqErrorDoc(status, description) {
|
|
4
|
+
return { status, description, schema: cpqErrorResponseSchema };
|
|
5
|
+
}
|
|
6
|
+
const cpqQuoteIdPathParams = z.object({ quoteId: z.string().uuid() });
|
|
7
|
+
const CPQ_GUARDED_WRITE_ERRORS = [
|
|
8
|
+
cpqErrorDoc(400, "Validation error"),
|
|
9
|
+
cpqErrorDoc(404, "Quote or line not found"),
|
|
10
|
+
cpqErrorDoc(409, "Quote is in a terminal status"),
|
|
11
|
+
cpqErrorDoc(422, "Blocked by a mutation guard")
|
|
12
|
+
];
|
|
13
|
+
export {
|
|
14
|
+
CPQ_GUARDED_WRITE_ERRORS,
|
|
15
|
+
cpqErrorDoc,
|
|
16
|
+
cpqErrorResponseSchema,
|
|
17
|
+
cpqQuoteIdPathParams
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=quoteRouteOpenApi.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiResponseDoc } from '@open-mercato/shared/lib/openapi'\n\n/** Error body every CPQ quote route returns (`{ error, ... }`). */\nexport const cpqErrorResponseSchema = z.object({ error: z.string() }).passthrough()\n\nexport function cpqErrorDoc(status: number, description: string): OpenApiResponseDoc {\n return { status, description, schema: cpqErrorResponseSchema }\n}\n\nexport const cpqQuoteIdPathParams = z.object({ quoteId: z.string().uuid() })\n\n/** Shared error docs for quote writes guarded by mutation guards. */\nexport const CPQ_GUARDED_WRITE_ERRORS: OpenApiResponseDoc[] = [\n cpqErrorDoc(400, 'Validation error'),\n cpqErrorDoc(404, 'Quote or line not found'),\n cpqErrorDoc(409, 'Quote is in a terminal status'),\n cpqErrorDoc(422, 'Blocked by a mutation guard'),\n]\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAIX,MAAM,yBAAyB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY;AAE3E,SAAS,YAAY,QAAgB,aAAyC;AACnF,SAAO,EAAE,QAAQ,aAAa,QAAQ,uBAAuB;AAC/D;AAEO,MAAM,uBAAuB,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAGpE,MAAM,2BAAiD;AAAA,EAC5D,YAAY,KAAK,kBAAkB;AAAA,EACnC,YAAY,KAAK,yBAAyB;AAAA,EAC1C,YAAY,KAAK,+BAA+B;AAAA,EAChD,YAAY,KAAK,6BAA6B;AAChD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { cpqErrorDoc } from "../../_helpers/quoteRouteOpenApi.js";
|
|
2
4
|
import { resolveCpqRouteContext } from "../../context.js";
|
|
3
5
|
import { isOrderError } from "../../../services/cpqOrderService.js";
|
|
4
6
|
const metadata = {
|
|
@@ -21,8 +23,22 @@ async function GET(req, { params }) {
|
|
|
21
23
|
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
22
24
|
}
|
|
23
25
|
}
|
|
26
|
+
const openApi = {
|
|
27
|
+
tag: "CPQ Orders",
|
|
28
|
+
summary: "CPQ order",
|
|
29
|
+
pathParams: z.object({ orderId: z.string().uuid() }),
|
|
30
|
+
methods: {
|
|
31
|
+
GET: {
|
|
32
|
+
summary: "Get a CPQ order with its lines and pricing summary",
|
|
33
|
+
description: "`pricingSummary` carries NRC / MRC totals plus the source quote's waterfall figures (`listNrcTotal`, `listMrcTotal`, `standard*Total`, `ruleDiscountTotal`, `manualDiscountTotal`, `dealValue`, `discountDepth`), computed from the line charges copied at conversion. ARC lines carry `arcSource` (the subscription item before the change).",
|
|
34
|
+
responses: [{ status: 200, description: "The order" }],
|
|
35
|
+
errors: [cpqErrorDoc(401, "Unauthorized"), cpqErrorDoc(404, "Order not found")]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
24
39
|
export {
|
|
25
40
|
GET,
|
|
26
|
-
metadata
|
|
41
|
+
metadata,
|
|
42
|
+
openApi
|
|
27
43
|
};
|
|
28
44
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cpq/api/orders/%5BorderId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveCpqRouteContext } from '../../context'\nimport { isOrderError } from '../../../services/cpqOrderService'\nimport type { DefaultCpqOrderService } from '../../../services/cpqOrderService'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['cpq.orders.view'] },\n}\n\nexport async function GET(req: Request, { params }: { params: Promise<{ orderId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { orderId } = await params\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n const service = ctx.container.resolve('cpqOrderService') as DefaultCpqOrderService\n const result = await service.getOrder(orderId, scope)\n return NextResponse.json(result)\n } catch (err) {\n if (isOrderError(err)) {\n return NextResponse.json({ error: err.message }, { status: err.status })\n }\n console.error('[cpq/orders/[orderId].GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAGtB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,iBAAiB,EAAE;AACjE;AAEA,eAAsB,IAAI,KAAc,EAAE,OAAO,GAA6C;AAC5F,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,QAAQ,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAE3E,UAAM,UAAU,IAAI,UAAU,QAAQ,iBAAiB;AACvD,UAAM,SAAS,MAAM,QAAQ,SAAS,SAAS,KAAK;AACpD,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,KAAK;AACZ,QAAI,aAAa,GAAG,GAAG;AACrB,aAAO,aAAa,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IACzE;AACA,YAAQ,MAAM,8BAA8B,GAAG;AAC/C,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;",
|
|
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 { isOrderError } from '../../../services/cpqOrderService'\nimport type { DefaultCpqOrderService } from '../../../services/cpqOrderService'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['cpq.orders.view'] },\n}\n\nexport async function GET(req: Request, { params }: { params: Promise<{ orderId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { orderId } = await params\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n const service = ctx.container.resolve('cpqOrderService') as DefaultCpqOrderService\n const result = await service.getOrder(orderId, scope)\n return NextResponse.json(result)\n } catch (err) {\n if (isOrderError(err)) {\n return NextResponse.json({ error: err.message }, { status: err.status })\n }\n console.error('[cpq/orders/[orderId].GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CPQ Orders',\n summary: 'CPQ order',\n pathParams: z.object({ orderId: z.string().uuid() }),\n methods: {\n GET: {\n summary: 'Get a CPQ order with its lines and pricing summary',\n description:\n '`pricingSummary` carries NRC / MRC totals plus the source quote\\'s waterfall figures (`listNrcTotal`, `listMrcTotal`, `standard*Total`, `ruleDiscountTotal`, `manualDiscountTotal`, `dealValue`, `discountDepth`), computed from the line charges copied at conversion. ARC lines carry `arcSource` (the subscription item before the change).',\n responses: [{ status: 200, description: 'The order' }],\n errors: [cpqErrorDoc(401, 'Unauthorized'), cpqErrorDoc(404, 'Order not found')],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,mBAAmB;AAC5B,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAGtB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,iBAAiB,EAAE;AACjE;AAEA,eAAsB,IAAI,KAAc,EAAE,OAAO,GAA6C;AAC5F,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,QAAQ,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAE3E,UAAM,UAAU,IAAI,UAAU,QAAQ,iBAAiB;AACvD,UAAM,SAAS,MAAM,QAAQ,SAAS,SAAS,KAAK;AACpD,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,KAAK;AACZ,QAAI,aAAa,GAAG,GAAG;AACrB,aAAO,aAAa,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IACzE;AACA,YAAQ,MAAM,8BAA8B,GAAG;AAC/C,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,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,EACnD,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,YAAY,CAAC;AAAA,MACrD,QAAQ,CAAC,YAAY,KAAK,cAAc,GAAG,YAAY,KAAK,iBAAiB,CAAC;AAAA,IAChF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,8 @@ import { z } from "zod";
|
|
|
3
3
|
import { resolveCpqRouteContext } from "../context.js";
|
|
4
4
|
import { cpqConvertQuoteToOrderSchema } from "../../data/validators.js";
|
|
5
5
|
import { isOrderError } from "../../services/cpqOrderService.js";
|
|
6
|
+
import { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from "../_helpers/quoteMutationGuard.js";
|
|
7
|
+
import { cpqErrorDoc } from "../_helpers/quoteRouteOpenApi.js";
|
|
6
8
|
const metadata = {
|
|
7
9
|
POST: { requireAuth: true, requireFeatures: ["cpq.orders.manage"] },
|
|
8
10
|
GET: { requireAuth: true, requireFeatures: ["cpq.orders.view"] }
|
|
@@ -13,8 +15,16 @@ async function POST(req) {
|
|
|
13
15
|
if (!ctx.auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
14
16
|
const body = cpqConvertQuoteToOrderSchema.parse(await req.json());
|
|
15
17
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId };
|
|
18
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
19
|
+
resourceKind: CPQ_QUOTE_RESOURCE_KIND,
|
|
20
|
+
resourceId: body.quoteId,
|
|
21
|
+
operation: "custom",
|
|
22
|
+
mutationPayload: { action: "convert_to_order" }
|
|
23
|
+
});
|
|
24
|
+
if (!guard.ok) return guard.response;
|
|
16
25
|
const service = ctx.container.resolve("cpqOrderService");
|
|
17
26
|
const result = await service.convertQuoteToOrder(body.quoteId, scope);
|
|
27
|
+
await guard.runAfterSuccess();
|
|
18
28
|
return NextResponse.json(result, { status: 201 });
|
|
19
29
|
} catch (err) {
|
|
20
30
|
return handleError(err, "cpq/orders.POST");
|
|
@@ -54,7 +64,7 @@ function handleError(err, context) {
|
|
|
54
64
|
return NextResponse.json({ error: "Validation error", details: err.issues }, { status: 400 });
|
|
55
65
|
}
|
|
56
66
|
if (isOrderError(err)) {
|
|
57
|
-
return NextResponse.json({ error: err.message }, { status: err.status });
|
|
67
|
+
return NextResponse.json({ error: err.message, ...err.details }, { status: err.status });
|
|
58
68
|
}
|
|
59
69
|
const errObj = err;
|
|
60
70
|
if (typeof errObj?.status === "number" && typeof errObj?.error === "string") {
|
|
@@ -63,9 +73,44 @@ function handleError(err, context) {
|
|
|
63
73
|
console.error(`[${context}]`, err);
|
|
64
74
|
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
65
75
|
}
|
|
76
|
+
const openApi = {
|
|
77
|
+
tag: "CPQ Orders",
|
|
78
|
+
summary: "CPQ orders",
|
|
79
|
+
methods: {
|
|
80
|
+
POST: {
|
|
81
|
+
summary: "Convert a CPQ quote into an order",
|
|
82
|
+
description: "Creates the sales order and CPQ order configuration from the quote. Refused for cancelled or rejected quotes; mutation guards (resource kind `cpq.quote`, action `convert_to_order`) run first.",
|
|
83
|
+
requestBody: { schema: cpqConvertQuoteToOrderSchema },
|
|
84
|
+
responses: [{ status: 201, description: "Created order" }],
|
|
85
|
+
errors: [
|
|
86
|
+
cpqErrorDoc(400, "Validation error"),
|
|
87
|
+
cpqErrorDoc(404, "Quote not found"),
|
|
88
|
+
cpqErrorDoc(409, "Quote cannot be converted in its status or is already converted"),
|
|
89
|
+
cpqErrorDoc(422, "Quote has no lines or has invalid stored adjustments, or blocked by a mutation guard")
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
GET: {
|
|
93
|
+
summary: "List CPQ orders",
|
|
94
|
+
query: z.object({
|
|
95
|
+
customerId: z.string().uuid().optional(),
|
|
96
|
+
sourceQuoteId: z.string().uuid().optional(),
|
|
97
|
+
salesOrderId: z.string().uuid().optional(),
|
|
98
|
+
cpqStatus: z.string().optional(),
|
|
99
|
+
currencyCode: z.string().optional(),
|
|
100
|
+
search: z.string().optional(),
|
|
101
|
+
sortField: z.enum(["createdAt", "updatedAt", "cpqStatus", "activatedAt", "currencyCode"]).optional(),
|
|
102
|
+
sortDir: z.enum(["asc", "desc"]).optional(),
|
|
103
|
+
page: z.coerce.number().int().min(1).optional(),
|
|
104
|
+
pageSize: z.coerce.number().int().min(1).max(100).optional()
|
|
105
|
+
}),
|
|
106
|
+
responses: [{ status: 200, description: "Paged list of CPQ orders" }]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
66
110
|
export {
|
|
67
111
|
GET,
|
|
68
112
|
POST,
|
|
69
|
-
metadata
|
|
113
|
+
metadata,
|
|
114
|
+
openApi
|
|
70
115
|
};
|
|
71
116
|
//# sourceMappingURL=route.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/cpq/api/orders/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../context'\nimport { cpqConvertQuoteToOrderSchema } from '../../data/validators'\nimport { isOrderError } from '../../services/cpqOrderService'\nimport type { DefaultCpqOrderService } from '../../services/cpqOrderService'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.orders.manage'] },\n GET: { requireAuth: true, requireFeatures: ['cpq.orders.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 = cpqConvertQuoteToOrderSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n const service = ctx.container.resolve('cpqOrderService') as DefaultCpqOrderService\n const result = await service.convertQuoteToOrder(body.quoteId, scope)\n return NextResponse.json(result, { status: 201 })\n } catch (err) {\n return handleError(err, 'cpq/orders.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 scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n const url = new URL(req.url)\n const ALLOWED_SORT_FIELDS = ['createdAt', 'updatedAt', 'cpqStatus', 'activatedAt', '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 : undefined\n const sortDir = url.searchParams.get('sortDir') === 'asc' ? 'asc' : 'desc'\n\n const filters = {\n customerId: url.searchParams.get('customerId') ?? undefined,\n sourceQuoteId: url.searchParams.get('sourceQuoteId') ?? undefined,\n salesOrderId: url.searchParams.get('salesOrderId') ?? undefined,\n cpqStatus: url.searchParams.get('cpqStatus') ?? undefined,\n currencyCode: url.searchParams.get('currencyCode') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField,\n sortDir,\n page: url.searchParams.has('page') ? Number(url.searchParams.get('page')) : undefined,\n pageSize: url.searchParams.has('pageSize') ? Number(url.searchParams.get('pageSize')) : undefined,\n } as const\n\n const service = ctx.container.resolve('cpqOrderService') as DefaultCpqOrderService\n const result = await service.listOrders(filters, scope)\n return NextResponse.json(result)\n } catch (err) {\n return handleError(err, 'cpq/orders.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 (isOrderError(err)) {\n return NextResponse.json({ error: err.message }, { status: err.status })\n }\n const errObj = err as Record<string, unknown>\n if (typeof errObj?.status === 'number' && typeof errObj?.error === 'string') {\n return NextResponse.json({ error: errObj.error }, { status: errObj.status as number })\n }\n console.error(`[${context}]`, err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,oCAAoC;AAC7C,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../context'\nimport { cpqConvertQuoteToOrderSchema } from '../../data/validators'\nimport { isOrderError } from '../../services/cpqOrderService'\nimport type { DefaultCpqOrderService } from '../../services/cpqOrderService'\nimport { CPQ_QUOTE_RESOURCE_KIND, runCpqMutationGuards } from '../_helpers/quoteMutationGuard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { cpqErrorDoc } from '../_helpers/quoteRouteOpenApi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['cpq.orders.manage'] },\n GET: { requireAuth: true, requireFeatures: ['cpq.orders.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 = cpqConvertQuoteToOrderSchema.parse(await req.json())\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n // Conversion turns a quote into an order \u2014 projects can block it (e.g.\n // until an approval workflow has signed a discounted quote off).\n const guard = await runCpqMutationGuards(ctx, req, {\n resourceKind: CPQ_QUOTE_RESOURCE_KIND,\n resourceId: body.quoteId,\n operation: 'custom',\n mutationPayload: { action: 'convert_to_order' },\n })\n if (!guard.ok) return guard.response\n\n const service = ctx.container.resolve('cpqOrderService') as DefaultCpqOrderService\n const result = await service.convertQuoteToOrder(body.quoteId, scope)\n await guard.runAfterSuccess()\n return NextResponse.json(result, { status: 201 })\n } catch (err) {\n return handleError(err, 'cpq/orders.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 scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n const url = new URL(req.url)\n const ALLOWED_SORT_FIELDS = ['createdAt', 'updatedAt', 'cpqStatus', 'activatedAt', '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 : undefined\n const sortDir = url.searchParams.get('sortDir') === 'asc' ? 'asc' : 'desc'\n\n const filters = {\n customerId: url.searchParams.get('customerId') ?? undefined,\n sourceQuoteId: url.searchParams.get('sourceQuoteId') ?? undefined,\n salesOrderId: url.searchParams.get('salesOrderId') ?? undefined,\n cpqStatus: url.searchParams.get('cpqStatus') ?? undefined,\n currencyCode: url.searchParams.get('currencyCode') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField,\n sortDir,\n page: url.searchParams.has('page') ? Number(url.searchParams.get('page')) : undefined,\n pageSize: url.searchParams.has('pageSize') ? Number(url.searchParams.get('pageSize')) : undefined,\n } as const\n\n const service = ctx.container.resolve('cpqOrderService') as DefaultCpqOrderService\n const result = await service.listOrders(filters, scope)\n return NextResponse.json(result)\n } catch (err) {\n return handleError(err, 'cpq/orders.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 (isOrderError(err)) {\n return NextResponse.json({ error: err.message, ...err.details }, { status: err.status })\n }\n const errObj = err as Record<string, unknown>\n if (typeof errObj?.status === 'number' && typeof errObj?.error === 'string') {\n return NextResponse.json({ error: errObj.error }, { status: errObj.status as number })\n }\n console.error(`[${context}]`, err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CPQ Orders',\n summary: 'CPQ orders',\n methods: {\n POST: {\n summary: 'Convert a CPQ quote into an order',\n description:\n 'Creates the sales order and CPQ order configuration from the quote. Refused for cancelled or rejected quotes; mutation guards (resource kind `cpq.quote`, action `convert_to_order`) run first.',\n requestBody: { schema: cpqConvertQuoteToOrderSchema },\n responses: [{ status: 201, description: 'Created order' }],\n errors: [\n cpqErrorDoc(400, 'Validation error'),\n cpqErrorDoc(404, 'Quote not found'),\n cpqErrorDoc(409, 'Quote cannot be converted in its status or is already converted'),\n cpqErrorDoc(422, 'Quote has no lines or has invalid stored adjustments, or blocked by a mutation guard'),\n ],\n },\n GET: {\n summary: 'List CPQ orders',\n query: z.object({\n customerId: z.string().uuid().optional(),\n sourceQuoteId: z.string().uuid().optional(),\n salesOrderId: z.string().uuid().optional(),\n cpqStatus: z.string().optional(),\n currencyCode: z.string().optional(),\n search: z.string().optional(),\n sortField: z.enum(['createdAt', 'updatedAt', 'cpqStatus', 'activatedAt', 'currencyCode']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n }),\n responses: [{ status: 200, description: 'Paged list of CPQ orders' }],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { resolveCpqRouteContext } from "../../../../../context.js";
|
|
4
|
+
import { isQuotingError } from "../../../../../../services/cpqQuotingService.js";
|
|
5
|
+
import { resolveQuotingService } from "../../../../../resolveQuotingService.js";
|
|
6
|
+
import { CPQ_QUOTE_LINE_RESOURCE_KIND, runCpqMutationGuards } from "../../../../../_helpers/quoteMutationGuard.js";
|
|
7
|
+
import { CPQ_GUARDED_WRITE_ERRORS } from "../../../../../_helpers/quoteRouteOpenApi.js";
|
|
8
|
+
const metadata = {
|
|
9
|
+
POST: { requireAuth: true, requireFeatures: ["cpq.quotes.manage"] }
|
|
10
|
+
};
|
|
11
|
+
async function POST(req, { params }) {
|
|
12
|
+
try {
|
|
13
|
+
const ctx = await resolveCpqRouteContext(req);
|
|
14
|
+
if (!ctx.auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
15
|
+
const { quoteId, lineId } = await params;
|
|
16
|
+
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId };
|
|
17
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
18
|
+
resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
19
|
+
resourceId: lineId,
|
|
20
|
+
operation: "update",
|
|
21
|
+
mutationPayload: { quoteId, restore: true }
|
|
22
|
+
});
|
|
23
|
+
if (!guard.ok) return guard.response;
|
|
24
|
+
const service = resolveQuotingService(ctx);
|
|
25
|
+
const result = await service.restoreQuoteItem(quoteId, lineId, scope);
|
|
26
|
+
await guard.runAfterSuccess();
|
|
27
|
+
return NextResponse.json(result);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (isQuotingError(err)) {
|
|
30
|
+
return NextResponse.json({ error: err.message }, { status: err.status });
|
|
31
|
+
}
|
|
32
|
+
console.error("[cpq/quotes/[quoteId]/items/[lineId]/restore.POST]", err);
|
|
33
|
+
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const openApi = {
|
|
37
|
+
tag: "CPQ Quotes",
|
|
38
|
+
summary: "Restore a subscription item marked for removal",
|
|
39
|
+
pathParams: z.object({ quoteId: z.string().uuid(), lineId: z.string().uuid() }),
|
|
40
|
+
methods: {
|
|
41
|
+
POST: {
|
|
42
|
+
summary: "Restore a removed subscription item on an Amend / Renew quote",
|
|
43
|
+
description: "Turns a `cancel` line that mirrors a live subscription item back into a `modify` line, rebuilt from the item (configuration, quantity, stored price). Its cancelled mirrored components are restored with it. 409 when the line is not a removed item, its parent is still removed, or the item is no longer active. Mutation guards run first (resource kind `cpq.quote_line`, update).",
|
|
44
|
+
responses: [{ status: 200, description: "Quote after the restore" }],
|
|
45
|
+
errors: CPQ_GUARDED_WRITE_ERRORS.filter((doc) => doc.status !== 400)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export {
|
|
50
|
+
POST,
|
|
51
|
+
metadata,
|
|
52
|
+
openApi
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../../src/modules/cpq/api/quotes/%5BquoteId%5D/items/%5BlineId%5D/restore/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveCpqRouteContext } from '../../../../../context'\nimport { isQuotingError } from '../../../../../../services/cpqQuotingService'\nimport { resolveQuotingService } from '../../../../../resolveQuotingService'\nimport { CPQ_QUOTE_LINE_RESOURCE_KIND, runCpqMutationGuards } from '../../../../../_helpers/quoteMutationGuard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CPQ_GUARDED_WRITE_ERRORS } 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; lineId: string }> }) {\n try {\n const ctx = await resolveCpqRouteContext(req)\n if (!ctx.auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { quoteId, lineId } = await params\n const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId }\n\n const guard = await runCpqMutationGuards(ctx, req, {\n resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,\n resourceId: lineId,\n operation: 'update',\n mutationPayload: { quoteId, restore: true },\n })\n if (!guard.ok) return guard.response\n\n const service = resolveQuotingService(ctx)\n const result = await service.restoreQuoteItem(quoteId, lineId, scope)\n await guard.runAfterSuccess()\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]/items/[lineId]/restore.POST]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CPQ Quotes',\n summary: 'Restore a subscription item marked for removal',\n pathParams: z.object({ quoteId: z.string().uuid(), lineId: z.string().uuid() }),\n methods: {\n POST: {\n summary: 'Restore a removed subscription item on an Amend / Renew quote',\n description:\n 'Turns a `cancel` line that mirrors a live subscription item back into a `modify` line, rebuilt from the item (configuration, quantity, stored price). Its cancelled mirrored components are restored with it. 409 when the line is not a removed item, its parent is still removed, or the item is no longer active. Mutation guards run first (resource kind `cpq.quote_line`, update).',\n responses: [{ status: 200, description: 'Quote after the restore' }],\n errors: CPQ_GUARDED_WRITE_ERRORS.filter((doc) => doc.status !== 400),\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,8BAA8B,4BAA4B;AAEnE,SAAS,gCAAgC;AAElC,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACpE;AAEA,eAAsB,KAAK,KAAc,EAAE,OAAO,GAA6D;AAC7G,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,SAAS,OAAO,IAAI,MAAM;AAClC,UAAM,QAAQ,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAE3E,UAAM,QAAQ,MAAM,qBAAqB,KAAK,KAAK;AAAA,MACjD,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,iBAAiB,EAAE,SAAS,SAAS,KAAK;AAAA,IAC5C,CAAC;AACD,QAAI,CAAC,MAAM,GAAI,QAAO,MAAM;AAE5B,UAAM,UAAU,sBAAsB,GAAG;AACzC,UAAM,SAAS,MAAM,QAAQ,iBAAiB,SAAS,QAAQ,KAAK;AACpE,UAAM,MAAM,gBAAgB;AAC5B,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,KAAK;AACZ,QAAI,eAAe,GAAG,GAAG;AACvB,aAAO,aAAa,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IACzE;AACA,YAAQ,MAAM,sDAAsD,GAAG;AACvE,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,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,GAAG,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,EAC9E,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,CAAC;AAAA,MACnE,QAAQ,yBAAyB,OAAO,CAAC,QAAQ,IAAI,WAAW,GAAG;AAAA,IACrE;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -4,6 +4,8 @@ import { resolveCpqRouteContext } from "../../../../context.js";
|
|
|
4
4
|
import { cpqUpdateQuoteItemSchema } from "../../../../../data/validators.js";
|
|
5
5
|
import { isQuotingError } from "../../../../../services/cpqQuotingService.js";
|
|
6
6
|
import { resolveQuotingService } from "../../../../resolveQuotingService.js";
|
|
7
|
+
import { CPQ_QUOTE_LINE_RESOURCE_KIND, runCpqMutationGuards } from "../../../../_helpers/quoteMutationGuard.js";
|
|
8
|
+
import { CPQ_GUARDED_WRITE_ERRORS } from "../../../../_helpers/quoteRouteOpenApi.js";
|
|
7
9
|
const metadata = {
|
|
8
10
|
PUT: { requireAuth: true, requireFeatures: ["cpq.quotes.manage"] },
|
|
9
11
|
DELETE: { requireAuth: true, requireFeatures: ["cpq.quotes.manage"] }
|
|
@@ -15,8 +17,17 @@ async function PUT(req, { params }) {
|
|
|
15
17
|
const { quoteId, lineId } = await params;
|
|
16
18
|
const body = cpqUpdateQuoteItemSchema.parse(await req.json());
|
|
17
19
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId };
|
|
20
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
21
|
+
resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
22
|
+
resourceId: lineId,
|
|
23
|
+
operation: "update",
|
|
24
|
+
mutationPayload: { quoteId, ...body }
|
|
25
|
+
});
|
|
26
|
+
if (!guard.ok) return guard.response;
|
|
27
|
+
const effective = guard.modifiedPayload ? cpqUpdateQuoteItemSchema.parse({ ...body, ...guard.modifiedPayload }) : body;
|
|
18
28
|
const service = resolveQuotingService(ctx);
|
|
19
|
-
const result = await service.updateQuoteItem(quoteId, lineId,
|
|
29
|
+
const result = await service.updateQuoteItem(quoteId, lineId, effective, scope);
|
|
30
|
+
await guard.runAfterSuccess();
|
|
20
31
|
return NextResponse.json(result);
|
|
21
32
|
} catch (err) {
|
|
22
33
|
if (err instanceof z.ZodError) {
|
|
@@ -35,8 +46,16 @@ async function DELETE(req, { params }) {
|
|
|
35
46
|
if (!ctx.auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
36
47
|
const { quoteId, lineId } = await params;
|
|
37
48
|
const scope = { organizationId: ctx.organizationId, tenantId: ctx.tenantId };
|
|
49
|
+
const guard = await runCpqMutationGuards(ctx, req, {
|
|
50
|
+
resourceKind: CPQ_QUOTE_LINE_RESOURCE_KIND,
|
|
51
|
+
resourceId: lineId,
|
|
52
|
+
operation: "delete",
|
|
53
|
+
mutationPayload: { quoteId }
|
|
54
|
+
});
|
|
55
|
+
if (!guard.ok) return guard.response;
|
|
38
56
|
const service = resolveQuotingService(ctx);
|
|
39
57
|
const result = await service.removeQuoteItem(quoteId, lineId, scope);
|
|
58
|
+
await guard.runAfterSuccess();
|
|
40
59
|
return NextResponse.json(result);
|
|
41
60
|
} catch (err) {
|
|
42
61
|
if (isQuotingError(err)) {
|
|
@@ -46,9 +65,30 @@ async function DELETE(req, { params }) {
|
|
|
46
65
|
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
47
66
|
}
|
|
48
67
|
}
|
|
68
|
+
const openApi = {
|
|
69
|
+
tag: "CPQ Quotes",
|
|
70
|
+
summary: "CPQ quote line",
|
|
71
|
+
pathParams: z.object({ quoteId: z.string().uuid(), lineId: z.string().uuid() }),
|
|
72
|
+
methods: {
|
|
73
|
+
PUT: {
|
|
74
|
+
summary: "Update a quote line (configuration, quantity, term)",
|
|
75
|
+
description: "Re-prices the line unless it is a frozen Amend mirror line whose pricing inputs are unchanged. Mutation guards run first (resource kind `cpq.quote_line`, update). `_arc*` configuration keys are server-owned and ignored.",
|
|
76
|
+
requestBody: { schema: cpqUpdateQuoteItemSchema },
|
|
77
|
+
responses: [{ status: 200, description: "Quote after the update" }],
|
|
78
|
+
errors: CPQ_GUARDED_WRITE_ERRORS
|
|
79
|
+
},
|
|
80
|
+
DELETE: {
|
|
81
|
+
summary: "Remove a quote line",
|
|
82
|
+
description: "On an Amend / Renew quote, a line mirroring a live subscription item is not deleted: it (and its mirrored components) becomes a `cancel` line with zero totals, and activation terminates the item; lines this quote added under it are deleted. 409 when the item is already marked for removal \u2014 use `POST .../restore` to undo. Other lines are deleted with their descendants. Mutation guards run first (resource kind `cpq.quote_line`, delete).",
|
|
83
|
+
responses: [{ status: 200, description: "Quote after the removal" }],
|
|
84
|
+
errors: CPQ_GUARDED_WRITE_ERRORS.filter((doc) => doc.status !== 400)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
49
88
|
export {
|
|
50
89
|
DELETE,
|
|
51
90
|
PUT,
|
|
52
|
-
metadata
|
|
91
|
+
metadata,
|
|
92
|
+
openApi
|
|
53
93
|
};
|
|
54
94
|
//# sourceMappingURL=route.js.map
|