@dainamite/cpq 0.5.0 → 0.7.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 +64 -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/AssetsTable.js +3 -9
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +21 -25
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +48 -47
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +3 -3
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/shared.js +37 -2
- package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +174 -184
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +168 -143
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/page.js +36 -34
- package/dist/modules/cpq/backend/cpq/offerings/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +140 -280
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/orders/page.js +6 -6
- package/dist/modules/cpq/backend/cpq/orders/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/price-rules/PriceRuleForm.js +41 -36
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +76 -72
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +1 -1
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/price-rules/page.js +18 -15
- package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +23 -21
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +22 -50
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +74 -52
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +20 -17
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/page.js +1 -1
- package/dist/modules/cpq/backend/cpq/pricing/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +115 -123
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +208 -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 +269 -311
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
- package/dist/modules/cpq/backend/cpq/quotes/page.js +23 -19
- package/dist/modules/cpq/backend/cpq/quotes/page.js.map +2 -2
- 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/[id]/DependsOnRulesEditor.js +19 -15
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +171 -178
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/page.js +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +60 -50
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +22 -18
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/wizards/page.js +1 -1
- package/dist/modules/cpq/backend/cpq/wizards/page.js.map +2 -2
- 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 +148 -0
- package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
- package/dist/modules/cpq/components/PriceWaterfall.js +68 -0
- package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js +112 -0
- package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
- package/dist/modules/cpq/components/StatusPath.js +5 -3
- package/dist/modules/cpq/components/StatusPath.js.map +2 -2
- package/dist/modules/cpq/components/useCpqRowActions.js +10 -22
- package/dist/modules/cpq/components/useCpqRowActions.js.map +2 -2
- 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/i18n/de.json +1220 -0
- package/dist/modules/cpq/i18n/en.json +1220 -0
- package/dist/modules/cpq/i18n/es.json +1220 -0
- package/dist/modules/cpq/i18n/pl.json +1220 -0
- 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/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +39 -31
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +13 -5
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +41 -21
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +2 -2
- package/dist/modules/cpq/widgets/injection-table.js +2 -2
- package/dist/modules/cpq/widgets/injection-table.js.map +2 -2
- package/dist/modules/cpq/workflows/WizardRunner.js +10 -8
- package/dist/modules/cpq/workflows/WizardRunner.js.map +2 -2
- package/dist/modules/cpq/workflows/WizardTrigger.js +15 -9
- package/dist/modules/cpq/workflows/WizardTrigger.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +9 -7
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +9 -4
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ItemListStep.js +16 -19
- package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +19 -32
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +8 -6
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/ReviewStep.js +32 -30
- package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +2 -2
- package/dist/modules/cpq/workflows/steps/index.js +12 -0
- package/dist/modules/cpq/workflows/steps/index.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/__tests__/i18n/dictionaries.test.ts +62 -0
- package/src/modules/cpq/__tests__/i18n/hardcodedStrings.test.ts +35 -0
- package/src/modules/cpq/__tests__/i18n/sourceScan.ts +109 -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/AssetsTable.tsx +3 -9
- package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +22 -26
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +43 -46
- package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +3 -3
- package/src/modules/cpq/backend/cpq/inventory/shared.tsx +37 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +164 -120
- package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +169 -130
- package/src/modules/cpq/backend/cpq/offerings/page.tsx +34 -24
- package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +122 -308
- package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/orders/page.tsx +6 -6
- package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +48 -34
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +76 -70
- package/src/modules/cpq/backend/cpq/price-rules/page.tsx +25 -16
- package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -18
- package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +29 -19
- package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +95 -47
- package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +23 -15
- package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +111 -88
- 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 +304 -348
- package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
- package/src/modules/cpq/backend/cpq/quotes/page.tsx +29 -21
- package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +19 -15
- package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +171 -171
- package/src/modules/cpq/backend/cpq/specifications/page.tsx +2 -2
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +51 -41
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +17 -13
- 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 +203 -0
- package/src/modules/cpq/components/PriceWaterfall.tsx +96 -0
- package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
- package/src/modules/cpq/components/StatusPath.tsx +7 -4
- package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
- package/src/modules/cpq/components/useCpqRowActions.tsx +17 -25
- 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/i18n/de.json +1220 -0
- package/src/modules/cpq/i18n/en.json +1220 -0
- package/src/modules/cpq/i18n/es.json +1220 -0
- package/src/modules/cpq/i18n/pl.json +1220 -0
- 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/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +38 -31
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +7 -5
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +36 -21
- package/src/modules/cpq/widgets/injection-table.ts +8 -2
- package/src/modules/cpq/workflows/WizardRunner.tsx +8 -5
- package/src/modules/cpq/workflows/WizardTrigger.tsx +15 -9
- package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +11 -7
- package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +9 -5
- package/src/modules/cpq/workflows/steps/ItemListStep.tsx +18 -12
- package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +20 -16
- package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +8 -6
- package/src/modules/cpq/workflows/steps/ReviewStep.tsx +33 -18
- package/src/modules/cpq/workflows/steps/index.ts +12 -0
- package/src/modules/cpq/workflows/types.ts +4 -0
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
5
|
function fmt(n, currency = "USD") {
|
|
5
6
|
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
|
|
6
7
|
}
|
|
8
|
+
function chargeTypeLabel(t, chargeType) {
|
|
9
|
+
switch (chargeType) {
|
|
10
|
+
case "nrc":
|
|
11
|
+
return t("cpq.lines.chargeType.nrc", "nrc");
|
|
12
|
+
case "mrc":
|
|
13
|
+
return t("cpq.lines.chargeType.mrc", "mrc");
|
|
14
|
+
case "usage":
|
|
15
|
+
return t("cpq.lines.chargeType.usage", "usage");
|
|
16
|
+
default:
|
|
17
|
+
return chargeType;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
7
20
|
function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
|
|
21
|
+
const t = useT();
|
|
8
22
|
const [quote, setQuote] = useState(null);
|
|
9
23
|
const [loading, setLoading] = useState(true);
|
|
10
24
|
const [submitting, setSubmitting] = useState(false);
|
|
11
25
|
const showChargeBreakdown = config.showChargeBreakdown !== false;
|
|
12
26
|
const showAdjustments = config.showAdjustments !== false;
|
|
13
27
|
const submitAction = config.submitAction ?? "save";
|
|
14
|
-
const submitLabel = config.submitLabel ?? "Complete";
|
|
28
|
+
const submitLabel = config.submitLabel ?? t("cpq.wizard.steps.review.complete", "Complete");
|
|
15
29
|
const loadQuote = useCallback(async () => {
|
|
16
30
|
if (!quoteId) {
|
|
17
31
|
setLoading(false);
|
|
@@ -54,33 +68,33 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
|
|
|
54
68
|
}
|
|
55
69
|
}, [quoteId, submitAction, quote, onComplete]);
|
|
56
70
|
if (loading) {
|
|
57
|
-
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading quote summary..." });
|
|
71
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("cpq.wizard.steps.review.loading", "Loading quote summary...") });
|
|
58
72
|
}
|
|
59
73
|
if (!quote) {
|
|
60
|
-
return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: "No quote data available. Please go back and configure items." });
|
|
74
|
+
return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground", children: t("cpq.wizard.steps.review.noData", "No quote data available. Please go back and configure items.") });
|
|
61
75
|
}
|
|
62
76
|
const { pricingSummary, lines } = quote;
|
|
63
77
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
64
78
|
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/30 p-4 space-y-3", children: [
|
|
65
|
-
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: "Quote Summary" }),
|
|
79
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.review.summaryTitle", "Quote Summary") }),
|
|
66
80
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
67
81
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
68
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "NRC Total" }),
|
|
82
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.nrcTotal", "NRC Total") }),
|
|
69
83
|
/* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode) })
|
|
70
84
|
] }),
|
|
71
85
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
72
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "MRC Total" }),
|
|
86
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.mrcTotal", "MRC Total") }),
|
|
73
87
|
/* @__PURE__ */ jsx("p", { className: "text-lg font-semibold", children: fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode) })
|
|
74
88
|
] }),
|
|
75
89
|
pricingSummary.discountTotal > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
76
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Discounts" }),
|
|
90
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.discounts", "Discounts") }),
|
|
77
91
|
/* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
|
|
78
92
|
"-",
|
|
79
93
|
fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)
|
|
80
94
|
] })
|
|
81
95
|
] }),
|
|
82
96
|
pricingSummary.surchargeTotal > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
83
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Surcharges" }),
|
|
97
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.surcharges", "Surcharges") }),
|
|
84
98
|
/* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
|
|
85
99
|
"+",
|
|
86
100
|
fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)
|
|
@@ -89,36 +103,24 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
|
|
|
89
103
|
] })
|
|
90
104
|
] }),
|
|
91
105
|
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
92
|
-
/* @__PURE__ */
|
|
93
|
-
"Items (",
|
|
94
|
-
lines.length,
|
|
95
|
-
")"
|
|
96
|
-
] }),
|
|
106
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-medium", children: t("cpq.wizard.steps.review.items", "Items ({count})", { count: lines.length }) }),
|
|
97
107
|
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: lines.map((line) => /* @__PURE__ */ jsxs("div", { className: "rounded-lg border p-3 space-y-2", children: [
|
|
98
108
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
99
109
|
/* @__PURE__ */ jsx("strong", { className: "text-sm", children: line.offeringName }),
|
|
100
|
-
/* @__PURE__ */
|
|
101
|
-
fmt(line.nrcTotal, pricingSummary.currencyCode),
|
|
102
|
-
" NRC / ",
|
|
103
|
-
fmt(line.mrcTotal, pricingSummary.currencyCode),
|
|
104
|
-
" MRC"
|
|
105
|
-
] })
|
|
106
|
-
] }),
|
|
107
|
-
line.quantity > 1 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
108
|
-
"Quantity: ",
|
|
109
|
-
line.quantity
|
|
110
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: t("cpq.wizard.linePricing", "{nrc} NRC / {mrc} MRC", { nrc: fmt(line.nrcTotal, pricingSummary.currencyCode), mrc: fmt(line.mrcTotal, pricingSummary.currencyCode) }) })
|
|
110
111
|
] }),
|
|
112
|
+
line.quantity > 1 && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("cpq.wizard.steps.review.quantity", "Quantity: {quantity}", { quantity: line.quantity }) }),
|
|
111
113
|
showChargeBreakdown && line.charges?.length > 0 && /* @__PURE__ */ jsx("div", { className: "rounded border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
|
|
112
114
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b bg-muted/30", children: [
|
|
113
|
-
/* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: "Charge" }),
|
|
114
|
-
/* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: "Type" }),
|
|
115
|
-
/* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-right font-medium", children: "Price" })
|
|
115
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: t("cpq.wizard.steps.review.charges.charge", "Charge") }),
|
|
116
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-left font-medium", children: t("cpq.wizard.steps.review.charges.type", "Type") }),
|
|
117
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-1.5 text-right font-medium", children: t("cpq.wizard.steps.review.charges.price", "Price") })
|
|
116
118
|
] }) }),
|
|
117
119
|
/* @__PURE__ */ jsx("tbody", { children: line.charges.map((charge) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
118
120
|
/* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0", children: [
|
|
119
121
|
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5", children: charge.chargeName }),
|
|
120
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 uppercase", children: charge.chargeType }),
|
|
121
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right", children: charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) :
|
|
122
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 uppercase", children: chargeTypeLabel(t, charge.chargeType) }),
|
|
123
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right", children: charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : t("cpq.lines.perUnit", "{price}/unit", { price: fmt(charge.unitPrice, charge.currencyCode) }) })
|
|
122
124
|
] }),
|
|
123
125
|
showAdjustments && charge.adjustments?.map((adj) => /* @__PURE__ */ jsxs("tr", { className: "text-[11px] text-muted-foreground", children: [
|
|
124
126
|
/* @__PURE__ */ jsx("td", { colSpan: 2, className: "px-3 py-0.5 pl-6", children: adj.ruleName }),
|
|
@@ -132,7 +134,7 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
|
|
|
132
134
|
] }, line.lineId)) })
|
|
133
135
|
] }),
|
|
134
136
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
|
|
135
|
-
completedSteps.length > 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onBack, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: "Back" }),
|
|
137
|
+
completedSteps.length > 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onBack, className: "inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted", children: t("cpq.common.back", "Back") }),
|
|
136
138
|
/* @__PURE__ */ jsx(
|
|
137
139
|
"button",
|
|
138
140
|
{
|
|
@@ -140,7 +142,7 @@ function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }) {
|
|
|
140
142
|
onClick: handleSubmit,
|
|
141
143
|
disabled: submitting,
|
|
142
144
|
className: "inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
|
|
143
|
-
children: submitting ? "Submitting..." : submitLabel
|
|
145
|
+
children: submitting ? t("cpq.wizard.steps.review.submitting", "Submitting...") : submitLabel
|
|
144
146
|
}
|
|
145
147
|
)
|
|
146
148
|
] })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/cpq/workflows/steps/ReviewStep.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport type { WizardStepProps } from '../types'\n\ntype ResolvedCharge = {\n chargeCode: string\n chargeName: string\n chargeType: string\n unitPrice: number\n totalPrice: number | null\n currencyCode: string\n adjustments?: Array<{\n ruleCode: string\n ruleName: string\n ruleType: string\n delta: number\n }> | null\n}\n\ntype QuoteLine = {\n lineId: string\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n charges: ResolvedCharge[]\n isConfigured: boolean\n}\n\ntype QuoteData = {\n id: string\n quoteNumber: string\n cpqStatus: string\n currencyCode: string\n pricingSummary: {\n nrcTotal: number\n mrcTotal: number\n discountTotal: number\n surchargeTotal: number\n currencyCode: string\n }\n lines: QuoteLine[]\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nexport function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }: WizardStepProps) {\n const [quote, setQuote] = useState<QuoteData | null>(null)\n const [loading, setLoading] = useState(true)\n const [submitting, setSubmitting] = useState(false)\n\n const showChargeBreakdown = config.showChargeBreakdown !== false\n const showAdjustments = config.showAdjustments !== false\n const submitAction = (config.submitAction as string) ?? 'save'\n const submitLabel = (config.submitLabel as string) ?? 'Complete'\n\n const loadQuote = useCallback(async () => {\n if (!quoteId) { setLoading(false); return }\n setLoading(true)\n try {\n const res = await fetch(`/api/cpq/quotes/${quoteId}`)\n if (res.ok) {\n setQuote(await res.json())\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId])\n\n useEffect(() => { loadQuote() }, [loadQuote])\n\n const handleSubmit = useCallback(async () => {\n setSubmitting(true)\n try {\n if (quoteId && submitAction !== 'none') {\n const targetStatus =\n submitAction === 'submit_for_approval' ? 'in_approval'\n : submitAction === 'send_to_customer' ? 'with_customer'\n : undefined\n\n if (targetStatus) {\n await fetch(`/api/cpq/quotes/${quoteId}/status`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ targetStatus }),\n })\n }\n }\n\n onComplete({\n quoteId,\n action: submitAction,\n nrcTotal: quote?.pricingSummary.nrcTotal ?? 0,\n mrcTotal: quote?.pricingSummary.mrcTotal ?? 0,\n })\n } finally {\n setSubmitting(false)\n }\n }, [quoteId, submitAction, quote, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">Loading quote summary
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { WizardStepProps } from '../types'\n\ntype ResolvedCharge = {\n chargeCode: string\n chargeName: string\n chargeType: string\n unitPrice: number\n totalPrice: number | null\n currencyCode: string\n adjustments?: Array<{\n ruleCode: string\n ruleName: string\n ruleType: string\n delta: number\n }> | null\n}\n\ntype QuoteLine = {\n lineId: string\n offeringName: string\n quantity: number\n nrcTotal: number\n mrcTotal: number\n charges: ResolvedCharge[]\n isConfigured: boolean\n}\n\ntype QuoteData = {\n id: string\n quoteNumber: string\n cpqStatus: string\n currencyCode: string\n pricingSummary: {\n nrcTotal: number\n mrcTotal: number\n discountTotal: number\n surchargeTotal: number\n currencyCode: string\n }\n lines: QuoteLine[]\n}\n\nfunction fmt(n: number, currency = 'USD') {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n}\n\nfunction chargeTypeLabel(t: TranslateFn, chargeType: string): string {\n switch (chargeType) {\n case 'nrc':\n return t('cpq.lines.chargeType.nrc', 'nrc')\n case 'mrc':\n return t('cpq.lines.chargeType.mrc', 'mrc')\n case 'usage':\n return t('cpq.lines.chargeType.usage', 'usage')\n default:\n return chargeType\n }\n}\n\nexport function ReviewStep({ config, onComplete, onBack, completedSteps, quoteId }: WizardStepProps) {\n const t = useT()\n const [quote, setQuote] = useState<QuoteData | null>(null)\n const [loading, setLoading] = useState(true)\n const [submitting, setSubmitting] = useState(false)\n\n const showChargeBreakdown = config.showChargeBreakdown !== false\n const showAdjustments = config.showAdjustments !== false\n const submitAction = (config.submitAction as string) ?? 'save'\n const submitLabel = (config.submitLabel as string) ?? t('cpq.wizard.steps.review.complete', 'Complete')\n\n const loadQuote = useCallback(async () => {\n if (!quoteId) { setLoading(false); return }\n setLoading(true)\n try {\n const res = await fetch(`/api/cpq/quotes/${quoteId}`)\n if (res.ok) {\n setQuote(await res.json())\n }\n } finally {\n setLoading(false)\n }\n }, [quoteId])\n\n useEffect(() => { loadQuote() }, [loadQuote])\n\n const handleSubmit = useCallback(async () => {\n setSubmitting(true)\n try {\n if (quoteId && submitAction !== 'none') {\n const targetStatus =\n submitAction === 'submit_for_approval' ? 'in_approval'\n : submitAction === 'send_to_customer' ? 'with_customer'\n : undefined\n\n if (targetStatus) {\n await fetch(`/api/cpq/quotes/${quoteId}/status`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ targetStatus }),\n })\n }\n }\n\n onComplete({\n quoteId,\n action: submitAction,\n nrcTotal: quote?.pricingSummary.nrcTotal ?? 0,\n mrcTotal: quote?.pricingSummary.mrcTotal ?? 0,\n })\n } finally {\n setSubmitting(false)\n }\n }, [quoteId, submitAction, quote, onComplete])\n\n if (loading) {\n return <div className=\"text-sm text-muted-foreground\">{t('cpq.wizard.steps.review.loading', 'Loading quote summary...')}</div>\n }\n\n if (!quote) {\n return (\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizard.steps.review.noData', 'No quote data available. Please go back and configure items.')}\n </div>\n )\n }\n\n const { pricingSummary, lines } = quote\n\n return (\n <div className=\"space-y-5\">\n {/* Quote summary */}\n <div className=\"rounded-lg border bg-muted/30 p-4 space-y-3\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.review.summaryTitle', 'Quote Summary')}</h4>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.nrcTotal', 'NRC Total')}</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.nrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.mrcTotal', 'MRC Total')}</span>\n <p className=\"text-lg font-semibold\">{fmt(pricingSummary.mrcTotal, pricingSummary.currencyCode)}</p>\n </div>\n {pricingSummary.discountTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.discounts', 'Discounts')}</span>\n <p className=\"text-sm\">-{fmt(pricingSummary.discountTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n {pricingSummary.surchargeTotal > 0 && (\n <div>\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.surcharges', 'Surcharges')}</span>\n <p className=\"text-sm\">+{fmt(pricingSummary.surchargeTotal, pricingSummary.currencyCode)}</p>\n </div>\n )}\n </div>\n </div>\n\n {/* Line items */}\n <div className=\"space-y-3\">\n <h4 className=\"text-sm font-medium\">{t('cpq.wizard.steps.review.items', 'Items ({count})', { count: lines.length })}</h4>\n <div className=\"space-y-2\">\n {lines.map((line) => (\n <div key={line.lineId} className=\"rounded-lg border p-3 space-y-2\">\n <div className=\"flex items-center justify-between\">\n <strong className=\"text-sm\">{line.offeringName}</strong>\n <span className=\"text-sm\">\n {t('cpq.wizard.linePricing', '{nrc} NRC / {mrc} MRC', { nrc: fmt(line.nrcTotal, pricingSummary.currencyCode), mrc: fmt(line.mrcTotal, pricingSummary.currencyCode) })}\n </span>\n </div>\n {line.quantity > 1 && (\n <span className=\"text-xs text-muted-foreground\">{t('cpq.wizard.steps.review.quantity', 'Quantity: {quantity}', { quantity: line.quantity })}</span>\n )}\n\n {showChargeBreakdown && line.charges?.length > 0 && (\n <div className=\"rounded border\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"border-b bg-muted/30\">\n <th className=\"px-3 py-1.5 text-left font-medium\">{t('cpq.wizard.steps.review.charges.charge', 'Charge')}</th>\n <th className=\"px-3 py-1.5 text-left font-medium\">{t('cpq.wizard.steps.review.charges.type', 'Type')}</th>\n <th className=\"px-3 py-1.5 text-right font-medium\">{t('cpq.wizard.steps.review.charges.price', 'Price')}</th>\n </tr>\n </thead>\n <tbody>\n {line.charges.map((charge) => (\n <React.Fragment key={charge.chargeCode}>\n <tr className=\"border-b last:border-0\">\n <td className=\"px-3 py-1.5\">{charge.chargeName}</td>\n <td className=\"px-3 py-1.5 uppercase\">{chargeTypeLabel(t, charge.chargeType)}</td>\n <td className=\"px-3 py-1.5 text-right\">\n {charge.totalPrice != null ? fmt(charge.totalPrice, charge.currencyCode) : t('cpq.lines.perUnit', '{price}/unit', { price: fmt(charge.unitPrice, charge.currencyCode) })}\n </td>\n </tr>\n {showAdjustments && charge.adjustments?.map((adj) => (\n <tr key={adj.ruleCode} className=\"text-[11px] text-muted-foreground\">\n <td colSpan={2} className=\"px-3 py-0.5 pl-6\">{adj.ruleName}</td>\n <td className=\"px-3 py-0.5 text-right\">\n {adj.delta < 0 ? '' : '+'}{fmt(adj.delta, charge.currencyCode)}\n </td>\n </tr>\n ))}\n </React.Fragment>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"flex gap-2 justify-end pt-2\">\n {completedSteps.length > 0 && (\n <button type=\"button\" onClick={onBack} className=\"inline-flex items-center justify-center rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted\">\n {t('cpq.common.back', 'Back')}\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={submitting}\n className=\"inline-flex items-center justify-center rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n >\n {submitting ? t('cpq.wizard.steps.review.submitting', 'Submitting...') : submitLabel}\n </button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,36 +8,48 @@ import { InventorySelectStep } from "./InventorySelectStep.js";
|
|
|
8
8
|
function registerBuiltInStepTypes() {
|
|
9
9
|
registerStepType({
|
|
10
10
|
type: "customer_select",
|
|
11
|
+
labelKey: "cpq.wizard.steps.customerSelect.label",
|
|
12
|
+
descriptionKey: "cpq.wizard.steps.customerSelect.description",
|
|
11
13
|
label: "Customer Selection",
|
|
12
14
|
description: "Search and select a customer for the quote.",
|
|
13
15
|
component: CustomerSelectStep
|
|
14
16
|
});
|
|
15
17
|
registerStepType({
|
|
16
18
|
type: "offering_select",
|
|
19
|
+
labelKey: "cpq.wizard.steps.offeringSelect.label",
|
|
20
|
+
descriptionKey: "cpq.wizard.steps.offeringSelect.description",
|
|
17
21
|
label: "Offering Selection",
|
|
18
22
|
description: "Browse and select a product offering from the catalog.",
|
|
19
23
|
component: OfferingSelectStep
|
|
20
24
|
});
|
|
21
25
|
registerStepType({
|
|
22
26
|
type: "product_configure",
|
|
27
|
+
labelKey: "cpq.wizard.steps.productConfigure.label",
|
|
28
|
+
descriptionKey: "cpq.wizard.steps.productConfigure.description",
|
|
23
29
|
label: "Product Configuration",
|
|
24
30
|
description: "Configure run-time attributes for the selected offering.",
|
|
25
31
|
component: ProductConfigureStep
|
|
26
32
|
});
|
|
27
33
|
registerStepType({
|
|
28
34
|
type: "item_list",
|
|
35
|
+
labelKey: "cpq.wizard.steps.itemList.label",
|
|
36
|
+
descriptionKey: "cpq.wizard.steps.itemList.description",
|
|
29
37
|
label: "Item List",
|
|
30
38
|
description: "View and manage child items on the quote.",
|
|
31
39
|
component: ItemListStep
|
|
32
40
|
});
|
|
33
41
|
registerStepType({
|
|
34
42
|
type: "review",
|
|
43
|
+
labelKey: "cpq.wizard.steps.review.label",
|
|
44
|
+
descriptionKey: "cpq.wizard.steps.review.description",
|
|
35
45
|
label: "Review & Submit",
|
|
36
46
|
description: "Review the quote summary and submit.",
|
|
37
47
|
component: ReviewStep
|
|
38
48
|
});
|
|
39
49
|
registerStepType({
|
|
40
50
|
type: "inventory_select",
|
|
51
|
+
labelKey: "cpq.wizard.steps.inventorySelect.label",
|
|
52
|
+
descriptionKey: "cpq.wizard.steps.inventorySelect.description",
|
|
41
53
|
label: "Inventory Selection",
|
|
42
54
|
description: "Select existing customer inventory items (stub \u2014 pending inventory domain).",
|
|
43
55
|
component: InventorySelectStep
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/cpq/workflows/steps/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { registerStepType } from '../registry'\nimport { CustomerSelectStep } from './CustomerSelectStep'\nimport { OfferingSelectStep } from './OfferingSelectStep'\nimport { ProductConfigureStep } from './ProductConfigureStep'\nimport { ItemListStep } from './ItemListStep'\nimport { ReviewStep } from './ReviewStep'\nimport { InventorySelectStep } from './InventorySelectStep'\n\nexport function registerBuiltInStepTypes() {\n registerStepType({\n type: 'customer_select',\n label: 'Customer Selection',\n description: 'Search and select a customer for the quote.',\n component: CustomerSelectStep,\n })\n\n registerStepType({\n type: 'offering_select',\n label: 'Offering Selection',\n description: 'Browse and select a product offering from the catalog.',\n component: OfferingSelectStep,\n })\n\n registerStepType({\n type: 'product_configure',\n label: 'Product Configuration',\n description: 'Configure run-time attributes for the selected offering.',\n component: ProductConfigureStep,\n })\n\n registerStepType({\n type: 'item_list',\n label: 'Item List',\n description: 'View and manage child items on the quote.',\n component: ItemListStep,\n })\n\n registerStepType({\n type: 'review',\n label: 'Review & Submit',\n description: 'Review the quote summary and submit.',\n component: ReviewStep,\n })\n\n registerStepType({\n type: 'inventory_select',\n label: 'Inventory Selection',\n description: 'Select existing customer inventory items (stub \u2014 pending inventory domain).',\n component: InventorySelectStep,\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAE7B,SAAS,2BAA2B;AACzC,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AACH;",
|
|
4
|
+
"sourcesContent": ["import { registerStepType } from '../registry'\nimport { CustomerSelectStep } from './CustomerSelectStep'\nimport { OfferingSelectStep } from './OfferingSelectStep'\nimport { ProductConfigureStep } from './ProductConfigureStep'\nimport { ItemListStep } from './ItemListStep'\nimport { ReviewStep } from './ReviewStep'\nimport { InventorySelectStep } from './InventorySelectStep'\n\nexport function registerBuiltInStepTypes() {\n registerStepType({\n type: 'customer_select',\n labelKey: 'cpq.wizard.steps.customerSelect.label',\n descriptionKey: 'cpq.wizard.steps.customerSelect.description',\n label: 'Customer Selection',\n description: 'Search and select a customer for the quote.',\n component: CustomerSelectStep,\n })\n\n registerStepType({\n type: 'offering_select',\n labelKey: 'cpq.wizard.steps.offeringSelect.label',\n descriptionKey: 'cpq.wizard.steps.offeringSelect.description',\n label: 'Offering Selection',\n description: 'Browse and select a product offering from the catalog.',\n component: OfferingSelectStep,\n })\n\n registerStepType({\n type: 'product_configure',\n labelKey: 'cpq.wizard.steps.productConfigure.label',\n descriptionKey: 'cpq.wizard.steps.productConfigure.description',\n label: 'Product Configuration',\n description: 'Configure run-time attributes for the selected offering.',\n component: ProductConfigureStep,\n })\n\n registerStepType({\n type: 'item_list',\n labelKey: 'cpq.wizard.steps.itemList.label',\n descriptionKey: 'cpq.wizard.steps.itemList.description',\n label: 'Item List',\n description: 'View and manage child items on the quote.',\n component: ItemListStep,\n })\n\n registerStepType({\n type: 'review',\n labelKey: 'cpq.wizard.steps.review.label',\n descriptionKey: 'cpq.wizard.steps.review.description',\n label: 'Review & Submit',\n description: 'Review the quote summary and submit.',\n component: ReviewStep,\n })\n\n registerStepType({\n type: 'inventory_select',\n labelKey: 'cpq.wizard.steps.inventorySelect.label',\n descriptionKey: 'cpq.wizard.steps.inventorySelect.description',\n label: 'Inventory Selection',\n description: 'Select existing customer inventory items (stub \u2014 pending inventory domain).',\n component: InventorySelectStep,\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAE7B,SAAS,2BAA2B;AACzC,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AAED,mBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainamite/cpq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Dainamite CPQ — Configure, Price, Quote engine for Open Mercato (configurable products, multi-charge pricing tables, guided quoting, customer inventory, orders).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/modules/cpq/index.js",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { test, expect, type Page } from '@playwright/test'
|
|
2
|
+
import { login } from '@open-mercato/core/helpers/integration/auth'
|
|
3
|
+
import { getAuthToken, apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
import {
|
|
5
|
+
seedActiveSubscription,
|
|
6
|
+
seedCustomer,
|
|
7
|
+
deleteSubscription,
|
|
8
|
+
deleteCustomer,
|
|
9
|
+
hideFloatingOverlays,
|
|
10
|
+
uniq,
|
|
11
|
+
} from './_helpers/cpqArcFixtures'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TC-CPQ-ARC-012 — Amend: removing an existing item cancels it on the subscription
|
|
15
|
+
* (manual: xd-250-arc-flow.md → "Removing an existing item on Amend / Renew quotes")
|
|
16
|
+
*
|
|
17
|
+
* Deleting a line that mirrors a live subscription item must not drop the
|
|
18
|
+
* line — activation only applies explicit line actions, so the item would
|
|
19
|
+
* silently stay active. Instead a confirmation dialog explains the effect,
|
|
20
|
+
* the line becomes a greyed-out `remove` line, the ARC summary keeps it in
|
|
21
|
+
* Current MRC but not in New MRC, and Restore brings it back. After
|
|
22
|
+
* convert + activate, the removed item is terminated and the kept one stays.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type QuoteLine = { lineId: string; offeringName: string; action: string }
|
|
26
|
+
type SubscriptionItem = { id: string; name: string; status: string }
|
|
27
|
+
|
|
28
|
+
function lineRow(page: Page, name: string) {
|
|
29
|
+
return page.getByTestId('cpq-quote-line').filter({ hasText: name })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test.describe('TC-CPQ-ARC-012 — Amend: remove an existing item', () => {
|
|
33
|
+
test('confirm → remove line → MRC delta → restore → remove → activate terminates the item', async ({ page, request }) => {
|
|
34
|
+
const token = await getAuthToken(request, 'admin')
|
|
35
|
+
const customerId = await seedCustomer(request, token)
|
|
36
|
+
const keptName = `Kept Port ${uniq()}`
|
|
37
|
+
const removedName = `Removed Storage ${uniq()}`
|
|
38
|
+
const sub = await seedActiveSubscription(request, token, {
|
|
39
|
+
customerId,
|
|
40
|
+
name: 'Remove-item target',
|
|
41
|
+
items: [
|
|
42
|
+
{ name: keptName, mrcAmount: 250 },
|
|
43
|
+
{ name: removedName, mrcAmount: 100 },
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await login(page, 'admin')
|
|
49
|
+
|
|
50
|
+
// 1. Amend from the subscription.
|
|
51
|
+
await page.goto(`/backend/cpq/inventory/subscriptions/${sub.id}`, { waitUntil: 'domcontentloaded' })
|
|
52
|
+
await hideFloatingOverlays(page)
|
|
53
|
+
await page.getByRole('button', { name: /^Amend$/ }).click()
|
|
54
|
+
await page.waitForURL(/\/backend\/cpq\/quotes\/[0-9a-f-]+$/, { timeout: 15_000 })
|
|
55
|
+
const quoteId = page.url().split('/').pop()!
|
|
56
|
+
await hideFloatingOverlays(page)
|
|
57
|
+
|
|
58
|
+
const removedRow = lineRow(page, removedName)
|
|
59
|
+
await expect(removedRow).toBeVisible()
|
|
60
|
+
await expect(removedRow).not.toHaveAttribute('data-removed', 'true')
|
|
61
|
+
|
|
62
|
+
// 2. Trash → confirmation dialog explains the item is cancelled. Cancel keeps the line.
|
|
63
|
+
await removedRow.getByTitle('Remove').click()
|
|
64
|
+
const dialog = page.getByRole('alertdialog')
|
|
65
|
+
await expect(dialog).toContainText(`Remove ${removedName} from the subscription?`)
|
|
66
|
+
await expect(dialog).toContainText('cancelled on the subscription')
|
|
67
|
+
await dialog.getByRole('button', { name: /^Cancel$/ }).click()
|
|
68
|
+
await expect(dialog).toBeHidden()
|
|
69
|
+
await expect(removedRow).not.toHaveAttribute('data-removed', 'true')
|
|
70
|
+
|
|
71
|
+
// 3. Confirm → the line stays as a `remove` line with a Restore action.
|
|
72
|
+
await removedRow.getByTitle('Remove').click()
|
|
73
|
+
await page.getByRole('alertdialog').getByRole('button', { name: 'Remove item' }).click()
|
|
74
|
+
await expect(removedRow).toHaveAttribute('data-removed', 'true')
|
|
75
|
+
await expect(removedRow.getByText(/^remove$/)).toBeVisible()
|
|
76
|
+
await expect(removedRow.getByTestId('cpq-line-restore')).toBeVisible()
|
|
77
|
+
|
|
78
|
+
// 4. Summary: removed item stays in Current MRC, drops out of New MRC.
|
|
79
|
+
await expect(page.getByTestId('cpq-arc-current-mrc')).toContainText('$350.00')
|
|
80
|
+
await expect(page.getByTestId('cpq-arc-new-mrc')).toContainText('$250.00')
|
|
81
|
+
await expect(page.getByTestId('cpq-arc-mrc-change')).toContainText('-$100.00')
|
|
82
|
+
|
|
83
|
+
// 5. Restore → back to a normal line; totals back to unchanged.
|
|
84
|
+
await removedRow.getByTestId('cpq-line-restore').click()
|
|
85
|
+
await expect(removedRow).not.toHaveAttribute('data-removed', 'true')
|
|
86
|
+
await expect(page.getByTestId('cpq-arc-new-mrc')).toContainText('$350.00')
|
|
87
|
+
|
|
88
|
+
// 6. Remove again for the activation leg.
|
|
89
|
+
await removedRow.getByTitle('Remove').click()
|
|
90
|
+
await page.getByRole('alertdialog').getByRole('button', { name: 'Remove item' }).click()
|
|
91
|
+
await expect(removedRow).toHaveAttribute('data-removed', 'true')
|
|
92
|
+
|
|
93
|
+
// The API agrees: one cancel line, one modify line.
|
|
94
|
+
const quoteRes = await apiRequest(request, 'GET', `/api/cpq/quotes/${quoteId}`, { token })
|
|
95
|
+
expect(quoteRes.ok()).toBeTruthy()
|
|
96
|
+
const quote = (await quoteRes.json()) as { cpqStatus: string; lines: QuoteLine[] }
|
|
97
|
+
expect(quote.lines.find((l) => l.offeringName === removedName)?.action).toBe('cancel')
|
|
98
|
+
expect(quote.lines.find((l) => l.offeringName === keptName)?.action).toBe('modify')
|
|
99
|
+
|
|
100
|
+
// 7. API — approval ladder (from wherever recalculation left the quote), convert, activate.
|
|
101
|
+
const ladder = ['ready', 'in_approval', 'approved', 'with_customer', 'accepted']
|
|
102
|
+
for (const targetStatus of ladder.slice(ladder.indexOf(quote.cpqStatus) + 1)) {
|
|
103
|
+
const res = await apiRequest(request, 'POST', `/api/cpq/quotes/${quoteId}/status`, { token, data: { targetStatus } })
|
|
104
|
+
expect(res.ok(), `transition to ${targetStatus} returned ${res.status()}: ${await res.text()}`).toBeTruthy()
|
|
105
|
+
}
|
|
106
|
+
const convertRes = await apiRequest(request, 'POST', '/api/cpq/orders', { token, data: { quoteId } })
|
|
107
|
+
expect(convertRes.ok(), `convert returned ${convertRes.status()}: ${await convertRes.text()}`).toBeTruthy()
|
|
108
|
+
const order = (await convertRes.json()) as { id: string }
|
|
109
|
+
|
|
110
|
+
// The order page mirrors the quote: same ARC summary, removed line marked "remove".
|
|
111
|
+
await page.goto(`/backend/cpq/orders/${order.id}`, { waitUntil: 'domcontentloaded' })
|
|
112
|
+
await hideFloatingOverlays(page)
|
|
113
|
+
await expect(page.getByTestId('cpq-pricing-summary')).toBeVisible()
|
|
114
|
+
await expect(page.getByTestId('cpq-arc-current-mrc')).toContainText('$350.00')
|
|
115
|
+
await expect(page.getByTestId('cpq-arc-new-mrc')).toContainText('$250.00')
|
|
116
|
+
const removedOrderLine = page.getByTestId('cpq-order-line').filter({ hasText: removedName })
|
|
117
|
+
await expect(removedOrderLine).toHaveAttribute('data-removed', 'true')
|
|
118
|
+
await expect(removedOrderLine.getByText(/^remove$/)).toBeVisible()
|
|
119
|
+
const activateRes = await apiRequest(request, 'POST', `/api/cpq/orders/${order.id}/activate`, { token })
|
|
120
|
+
expect(activateRes.ok(), `activate returned ${activateRes.status()}: ${await activateRes.text()}`).toBeTruthy()
|
|
121
|
+
|
|
122
|
+
// 8. The removed item is terminated; the other one is untouched.
|
|
123
|
+
const itemsRes = await apiRequest(request, 'GET', `/api/cpq/inventory/subscription-items?subscriptionId=${sub.id}`, { token })
|
|
124
|
+
expect(itemsRes.ok()).toBeTruthy()
|
|
125
|
+
const body = (await itemsRes.json()) as SubscriptionItem[] | { items: SubscriptionItem[] }
|
|
126
|
+
const items = Array.isArray(body) ? body : body.items
|
|
127
|
+
expect(items.find((i) => i.name === removedName)?.status).toBe('terminated')
|
|
128
|
+
expect(items.find((i) => i.name === keptName)?.status).toBe('active')
|
|
129
|
+
} finally {
|
|
130
|
+
await deleteSubscription(request, token, sub.id).catch(() => undefined)
|
|
131
|
+
await deleteCustomer(request, token, customerId).catch(() => undefined)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { MODULE_ROOT, collectTranslationKeys, listSourceFiles } from './sourceScan'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPQ dictionary guardrails (spec 2026-11-15-cpq-multi-language):
|
|
7
|
+
* - every `i18n/<locale>.json` mirrors `en.json` key for key, keeps every
|
|
8
|
+
* `{placeholder}` and has no empty value;
|
|
9
|
+
* - every key the source uses exists in `en.json` (a missing key renders
|
|
10
|
+
* its English fallback in every language), and every dynamic key prefix
|
|
11
|
+
* has at least one entry;
|
|
12
|
+
* - `en.json` carries no orphaned keys.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const I18N_DIR = join(MODULE_ROOT, 'i18n')
|
|
16
|
+
const load = (locale: string) => JSON.parse(readFileSync(join(I18N_DIR, `${locale}.json`), 'utf8')) as Record<string, string>
|
|
17
|
+
const placeholders = (value: string) => [...value.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).sort().join(',')
|
|
18
|
+
|
|
19
|
+
const locales = readdirSync(I18N_DIR).filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, '')).sort()
|
|
20
|
+
const en = load('en')
|
|
21
|
+
const enKeys = Object.keys(en)
|
|
22
|
+
const translated = locales.filter((l) => l !== 'en')
|
|
23
|
+
// `describe.each` rejects an empty table; en vs en is a trivially-true row.
|
|
24
|
+
const parityRows = translated.length ? translated : ['en']
|
|
25
|
+
const { keys: usedKeys, prefixes } = collectTranslationKeys(listSourceFiles(MODULE_ROOT))
|
|
26
|
+
|
|
27
|
+
describe('cpq i18n dictionaries', () => {
|
|
28
|
+
it('ships en plus the supported translations', () => {
|
|
29
|
+
expect(locales).toEqual(expect.arrayContaining(['en', 'es', 'de', 'pl']))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('en.json is sorted by key and has no empty values', () => {
|
|
33
|
+
expect(enKeys).toEqual([...enKeys].sort())
|
|
34
|
+
expect(enKeys.filter((k) => !en[k]?.trim())).toEqual([])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('every key used in source exists in en.json', () => {
|
|
38
|
+
expect([...usedKeys].filter((k) => !(k in en)).sort()).toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('every dynamic key prefix has entries in en.json', () => {
|
|
42
|
+
expect([...prefixes].filter((p) => !enKeys.some((k) => k.startsWith(p))).sort()).toEqual([])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('en.json has no orphaned keys', () => {
|
|
46
|
+
const orphans = enKeys.filter((k) => !usedKeys.has(k) && ![...prefixes].some((p) => k.startsWith(p)))
|
|
47
|
+
expect(orphans).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe.each(parityRows)('%s.json', (locale) => {
|
|
51
|
+
const dict = load(locale)
|
|
52
|
+
it('has exactly the keys of en.json, in the same order', () => {
|
|
53
|
+
expect(Object.keys(dict)).toEqual(enKeys)
|
|
54
|
+
})
|
|
55
|
+
it('keeps every {placeholder} of the English value', () => {
|
|
56
|
+
expect(enKeys.filter((k) => typeof dict[k] === 'string' && placeholders(dict[k]) !== placeholders(en[k]))).toEqual([])
|
|
57
|
+
})
|
|
58
|
+
it('has no empty values', () => {
|
|
59
|
+
expect(enKeys.filter((k) => typeof dict[k] !== 'string' || !dict[k].trim())).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
import { MODULE_ROOT, UI_DIRS, collectHardcodedStrings, listSourceFiles } from './sourceScan'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* No user-facing English literal may bypass `t()` in CPQ UI code
|
|
6
|
+
* (spec 2026-11-15-cpq-multi-language). Data (product / charge / customer
|
|
7
|
+
* names) is never a literal, so it is not affected.
|
|
8
|
+
*
|
|
9
|
+
* ALLOWLIST is for literals that are intentionally not translated; every
|
|
10
|
+
* entry carries a reason. Keyed by `<file>:<text>` so a moved line does not
|
|
11
|
+
* break it.
|
|
12
|
+
*/
|
|
13
|
+
const ALLOWLIST: Record<string, string> = {
|
|
14
|
+
// Injection-widget metadata has no `titleKey` in the framework's widget
|
|
15
|
+
// contract; the host renders `metadata.title`. Tracked as a framework gap.
|
|
16
|
+
'widgets/injection/order-cpq-tab/widget.ts:CPQ Order': 'framework: widget metadata has no titleKey',
|
|
17
|
+
'widgets/injection/customer-inventory/widget.ts:Customer Inventory': 'framework: widget metadata has no titleKey',
|
|
18
|
+
'widgets/injection/quote-configurator/widget.ts:CPQ Configuration': 'framework: widget metadata has no titleKey',
|
|
19
|
+
'widgets/injection/order-cpq-tab/widget.ts:View the CPQ order associated with this sales order':
|
|
20
|
+
'framework: widget metadata has no descriptionKey',
|
|
21
|
+
'widgets/injection/customer-inventory/widget.ts:Products and services purchased by this customer':
|
|
22
|
+
'framework: widget metadata has no descriptionKey',
|
|
23
|
+
'widgets/injection/quote-configurator/widget.ts:Configure, price, and manage quote offerings':
|
|
24
|
+
'framework: widget metadata has no descriptionKey',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('cpq UI has no hard-coded user-facing strings', () => {
|
|
28
|
+
it('routes every UI literal through t()', () => {
|
|
29
|
+
const files = UI_DIRS.flatMap((dir) => listSourceFiles(join(MODULE_ROOT, dir)))
|
|
30
|
+
const offenders = collectHardcodedStrings(files)
|
|
31
|
+
.filter((hit) => !(`${hit.file}:${hit.text}` in ALLOWLIST))
|
|
32
|
+
.map((hit) => `${hit.file}:${hit.line} ${hit.text}`)
|
|
33
|
+
expect(offenders).toEqual([])
|
|
34
|
+
})
|
|
35
|
+
})
|