@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { computeWaterfallSummary } from "./priceAdjustments/pricingSnapshot.js";
|
|
2
|
+
import { isArcMirrorLine } from "./arcPriceFreeze.js";
|
|
3
|
+
function toResolvedCharge(charge) {
|
|
4
|
+
return {
|
|
5
|
+
...charge,
|
|
6
|
+
chargeCode: String(charge.chargeCode ?? ""),
|
|
7
|
+
chargeType: charge.chargeType ?? "mrc",
|
|
8
|
+
unitPrice: Number(charge.unitPrice ?? 0),
|
|
9
|
+
totalPrice: charge.totalPrice == null ? null : Number(charge.totalPrice)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function summarizeOrderPricing(lines, quoteType, currencyCode) {
|
|
13
|
+
const summary = computeWaterfallSummary(lines.map((line) => ({
|
|
14
|
+
lineId: line.lineId,
|
|
15
|
+
quantity: line.quantity,
|
|
16
|
+
termMonths: line.termMonths,
|
|
17
|
+
includeInDeal: !isArcMirrorLine(line.configuration, quoteType),
|
|
18
|
+
charges: line.charges.map(toResolvedCharge)
|
|
19
|
+
})), [], currencyCode);
|
|
20
|
+
const { snapshotHash: _snapshotHash, hasTimeBasedAdjustments: _timeBased, ...orderSummary } = summary;
|
|
21
|
+
return orderSummary;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
summarizeOrderPricing
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=orderPricingSummary.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cpq/services/orderPricingSummary.ts"],
|
|
4
|
+
"sourcesContent": ["import { computeWaterfallSummary } from './priceAdjustments/pricingSnapshot'\nimport { isArcMirrorLine } from './arcPriceFreeze'\nimport type { PricingWaterfallSummary, ResolvedCharge } from './types'\n\nexport type OrderSummaryLine = {\n lineId: string\n quantity: number\n termMonths: number | null\n configuration: Record<string, unknown> | null\n charges: Array<Record<string, unknown>>\n}\n\n/** The quote's list / standard / offer figures and deal value, minus quote-only fields. */\nexport type OrderWaterfallSummary = Omit<PricingWaterfallSummary, 'snapshotHash' | 'hasTimeBasedAdjustments'>\n\n/** Order lines store the quote's charges as plain JSON; fill the fields the waterfall needs. */\nfunction toResolvedCharge(charge: Record<string, unknown>): ResolvedCharge {\n return {\n ...charge,\n chargeCode: String(charge.chargeCode ?? ''),\n chargeType: (charge.chargeType ?? 'mrc') as ResolvedCharge['chargeType'],\n unitPrice: Number(charge.unitPrice ?? 0),\n totalPrice: charge.totalPrice == null ? null : Number(charge.totalPrice),\n } as ResolvedCharge\n}\n\n/**\n * The same pricing summary the source quote shows (list totals, discount\n * depth, deal value), computed from the order's line charges, which are\n * copied verbatim from the quote at conversion. ARC mirror lines are left out\n * of deal value exactly as on the quote.\n */\nexport function summarizeOrderPricing(\n lines: readonly OrderSummaryLine[],\n quoteType: string,\n currencyCode: string,\n): OrderWaterfallSummary {\n const summary = computeWaterfallSummary(lines.map((line) => ({\n lineId: line.lineId,\n quantity: line.quantity,\n termMonths: line.termMonths,\n includeInDeal: !isArcMirrorLine(line.configuration, quoteType),\n charges: line.charges.map(toResolvedCharge),\n })), [], currencyCode)\n const { snapshotHash: _snapshotHash, hasTimeBasedAdjustments: _timeBased, ...orderSummary } = summary\n return orderSummary\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,+BAA+B;AACxC,SAAS,uBAAuB;AAehC,SAAS,iBAAiB,QAAiD;AACzE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,YAAY,OAAO,OAAO,cAAc,EAAE;AAAA,IAC1C,YAAa,OAAO,cAAc;AAAA,IAClC,WAAW,OAAO,OAAO,aAAa,CAAC;AAAA,IACvC,YAAY,OAAO,cAAc,OAAO,OAAO,OAAO,OAAO,UAAU;AAAA,EACzE;AACF;AAQO,SAAS,sBACd,OACA,WACA,cACuB;AACvB,QAAM,UAAU,wBAAwB,MAAM,IAAI,CAAC,UAAU;AAAA,IAC3D,QAAQ,KAAK;AAAA,IACb,UAAU,KAAK;AAAA,IACf,YAAY,KAAK;AAAA,IACjB,eAAe,CAAC,gBAAgB,KAAK,eAAe,SAAS;AAAA,IAC7D,SAAS,KAAK,QAAQ,IAAI,gBAAgB;AAAA,EAC5C,EAAE,GAAG,CAAC,GAAG,YAAY;AACrB,QAAM,EAAE,cAAc,eAAe,yBAAyB,YAAY,GAAG,aAAa,IAAI;AAC9F,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { isArcMirrorLine } from "../arcPriceFreeze.js";
|
|
2
|
+
import { ADJUSTMENT_TOGGLE, REASON_DICTIONARY } from "../../data/price-adjustments.js";
|
|
3
|
+
import { ADJUSTMENT_TOGGLE as ADJUSTMENT_TOGGLE2, REASON_DICTIONARY as REASON_DICTIONARY2 } from "../../data/price-adjustments.js";
|
|
4
|
+
import { loadQuoteAdjustments } from "./loadQuoteAdjustments.js";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
7
|
+
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
8
|
+
import { Dictionary, DictionaryEntry } from "@open-mercato/core/modules/dictionaries/data/entities";
|
|
9
|
+
import { CpqOrderConfiguration, CpqQuoteConfiguration, CpqQuoteLineConfiguration, CpqQuotePriceAdjustment } from "../../data/entities.js";
|
|
10
|
+
import { cpqPriceAdjustmentInputSchema, cpqPriceAdjustmentPatchSchema } from "../../data/price-adjustments.js";
|
|
11
|
+
import { QuotingError } from "../cpqQuotingService.js";
|
|
12
|
+
import { TERMINAL_STATUSES } from "../types.js";
|
|
13
|
+
import { withQuotePricingWrite } from "./quotePricingWrite.js";
|
|
14
|
+
const ADJUSTMENT_MUTABLE_STATUSES = ["new", "incomplete", "ready"];
|
|
15
|
+
function adjustmentInput(row) {
|
|
16
|
+
return cpqPriceAdjustmentInputSchema.parse({
|
|
17
|
+
level: row.level,
|
|
18
|
+
quoteLineId: row.quoteLineId,
|
|
19
|
+
chargeCode: row.chargeCode,
|
|
20
|
+
chargeTypeFilter: row.chargeTypeFilter,
|
|
21
|
+
adjustmentType: row.adjustmentType,
|
|
22
|
+
value: Number(row.value),
|
|
23
|
+
currencyCode: row.currencyCode,
|
|
24
|
+
reasonCode: row.reasonCode,
|
|
25
|
+
reasonNote: row.reasonNote
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function adjustmentSnapshot(row) {
|
|
29
|
+
return {
|
|
30
|
+
id: row.id,
|
|
31
|
+
...adjustmentInput(row),
|
|
32
|
+
quoteConfigurationId: row.quoteConfigurationId,
|
|
33
|
+
quoteId: row.quoteId,
|
|
34
|
+
tenantId: row.tenantId,
|
|
35
|
+
organizationId: row.organizationId,
|
|
36
|
+
createdByUserId: row.createdByUserId,
|
|
37
|
+
createdAt: row.createdAt.toISOString(),
|
|
38
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
39
|
+
deletedAt: row.deletedAt?.toISOString() ?? null,
|
|
40
|
+
isActive: row.isActive
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function adjustmentsOverlap(a, b) {
|
|
44
|
+
if (a.id === b.id || a.level !== b.level || a.quoteLineId !== b.quoteLineId) return false;
|
|
45
|
+
if (a.level === "charge") return (a.chargeCode ?? null) === (b.chargeCode ?? null);
|
|
46
|
+
return !a.chargeTypeFilter || !b.chargeTypeFilter || a.chargeTypeFilter === b.chargeTypeFilter;
|
|
47
|
+
}
|
|
48
|
+
class CpqAdjustmentService {
|
|
49
|
+
constructor(deps) {
|
|
50
|
+
this.deps = deps;
|
|
51
|
+
}
|
|
52
|
+
async enabled(tenantId) {
|
|
53
|
+
const flag = await this.deps.featureTogglesService.getBoolConfig(ADJUSTMENT_TOGGLE, tenantId);
|
|
54
|
+
return flag.ok && flag.value === true;
|
|
55
|
+
}
|
|
56
|
+
async list(quoteId, scope, page, pageSize) {
|
|
57
|
+
const em = this.deps.em.fork({ clear: true, useContext: false });
|
|
58
|
+
const scoped = { ...scope, deletedAt: null };
|
|
59
|
+
const quote = await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, id: quoteId }, void 0, scope) ?? await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, quoteId }, void 0, scope);
|
|
60
|
+
if (!quote) throw new QuotingError(404, "Quote not found");
|
|
61
|
+
const filter = { ...scoped, quoteConfigurationId: quote.id, isActive: true };
|
|
62
|
+
const [rows, total, enabled, reasons] = await Promise.all([
|
|
63
|
+
findWithDecryption(em, CpqQuotePriceAdjustment, filter, { limit: pageSize, offset: (page - 1) * pageSize, orderBy: { createdAt: "asc", id: "asc" } }, scope),
|
|
64
|
+
em.count(CpqQuotePriceAdjustment, filter),
|
|
65
|
+
this.enabled(scope.tenantId),
|
|
66
|
+
this.reasons(em, scope)
|
|
67
|
+
]);
|
|
68
|
+
return { items: rows.map(adjustmentSnapshot), total, page, pageSize, totalPages: Math.ceil(total / pageSize), pricingRevision: quote.pricingRevision ?? 0, enabled, reasons };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Reason options for the adjustment dialog. Served here so reps need only
|
|
72
|
+
* `cpq.quotes.view`, not `dictionaries.view`. An inactive dictionary yields
|
|
73
|
+
* no options, matching `requireReason`.
|
|
74
|
+
*/
|
|
75
|
+
async reasons(em, scope) {
|
|
76
|
+
const dictionary = await findOneWithDecryption(em, Dictionary, { key: REASON_DICTIONARY, ...scope, deletedAt: null, isActive: true }, void 0, scope);
|
|
77
|
+
if (!dictionary) return [];
|
|
78
|
+
const entries = await findWithDecryption(em, DictionaryEntry, { dictionary: dictionary.id, ...scope }, { orderBy: { position: "asc", label: "asc" } }, scope);
|
|
79
|
+
return entries.map((entry) => ({ value: entry.value, label: entry.label }));
|
|
80
|
+
}
|
|
81
|
+
async preview(quoteId, input, replaceAdjustmentId, scope) {
|
|
82
|
+
const parsed = cpqPriceAdjustmentInputSchema.parse(input);
|
|
83
|
+
await this.requireEnabled(scope);
|
|
84
|
+
const em = this.deps.em.fork({ clear: true, useContext: false });
|
|
85
|
+
const scoped = { ...scope, deletedAt: null };
|
|
86
|
+
const quote = await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, id: quoteId }, void 0, scope) ?? await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, quoteId }, void 0, scope);
|
|
87
|
+
if (!quote) throw new QuotingError(404, "Quote not found");
|
|
88
|
+
if (parsed.expectedRevision !== void 0 && parsed.expectedRevision !== (quote.pricingRevision ?? 0)) throw new QuotingError(409, "STALE_REVISION");
|
|
89
|
+
await this.assertMutable(em, quote, scope);
|
|
90
|
+
await this.requireTarget(em, quote, parsed.quoteLineId, scope);
|
|
91
|
+
const stored = await loadQuoteAdjustments(em, quote.id, scope);
|
|
92
|
+
const replaced = stored.find((row) => row.id === replaceAdjustmentId);
|
|
93
|
+
if (replaceAdjustmentId && !replaced) throw new QuotingError(404, "Adjustment not found");
|
|
94
|
+
if (replaced && (replaced.level !== parsed.level || replaced.quoteLineId !== parsed.quoteLineId || (replaced.chargeCode ?? null) !== (parsed.chargeCode ?? null))) throw new QuotingError(422, "Adjustment target is immutable");
|
|
95
|
+
if (!replaced || replaced.reasonCode !== parsed.reasonCode) await this.requireReason(em, parsed.reasonCode, scope);
|
|
96
|
+
const draft = { ...parsed, id: replaceAdjustmentId ?? randomUUID() };
|
|
97
|
+
const adjustments = [...stored.filter((row) => row.id !== replaceAdjustmentId), draft];
|
|
98
|
+
const result = await this.deps.quotingService.recalculateLockedQuote(em, quote, scope, false, adjustments);
|
|
99
|
+
return { quote: result, errors: result.adjustmentErrors ?? [] };
|
|
100
|
+
}
|
|
101
|
+
async create(quoteId, input, actor, scope) {
|
|
102
|
+
const parsed = cpqPriceAdjustmentInputSchema.parse(input);
|
|
103
|
+
await this.requireEnabled(scope);
|
|
104
|
+
return this.write(quoteId, scope, parsed.expectedRevision, async (em, quote) => {
|
|
105
|
+
await this.assertMutable(em, quote, scope);
|
|
106
|
+
await this.requireReason(em, parsed.reasonCode, scope);
|
|
107
|
+
await this.requireTarget(em, quote, parsed.quoteLineId, scope);
|
|
108
|
+
const id = randomUUID();
|
|
109
|
+
await this.rejectOverlap(em, quote, { id, ...parsed }, scope);
|
|
110
|
+
const row = em.create(CpqQuotePriceAdjustment, {
|
|
111
|
+
...scope,
|
|
112
|
+
id,
|
|
113
|
+
quoteConfigurationId: quote.id,
|
|
114
|
+
quoteId: quote.quoteId,
|
|
115
|
+
...this.values(parsed),
|
|
116
|
+
createdByUserId: actor
|
|
117
|
+
});
|
|
118
|
+
return this.save(em, quote, row, null, scope, true);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async update(quoteId, id, patch, actor, scope) {
|
|
122
|
+
const parsed = cpqPriceAdjustmentPatchSchema.parse(patch);
|
|
123
|
+
await this.requireEnabled(scope);
|
|
124
|
+
return this.write(quoteId, scope, parsed.expectedRevision, async (em, quote) => {
|
|
125
|
+
await this.assertMutable(em, quote, scope);
|
|
126
|
+
const row = await this.loadRow(em, quote.id, id, scope);
|
|
127
|
+
const before = adjustmentSnapshot(row);
|
|
128
|
+
await this.requireTarget(em, quote, row.quoteLineId, scope);
|
|
129
|
+
const merged = cpqPriceAdjustmentInputSchema.parse({ ...adjustmentInput(row), ...parsed });
|
|
130
|
+
if (merged.reasonCode !== row.reasonCode) await this.requireReason(em, merged.reasonCode, scope);
|
|
131
|
+
await this.rejectOverlap(em, quote, { id: row.id, ...merged }, scope);
|
|
132
|
+
em.assign(row, { ...this.values(merged), updatedByUserId: actor, updatedAt: /* @__PURE__ */ new Date() });
|
|
133
|
+
return this.save(em, quote, row, before, scope, true);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async remove(quoteId, id, actor, scope, expectedRevision) {
|
|
137
|
+
await this.requireEnabled(scope);
|
|
138
|
+
return this.write(quoteId, scope, expectedRevision, async (em, quote) => {
|
|
139
|
+
const row = await this.loadRow(em, quote.id, id, scope);
|
|
140
|
+
const preview = await this.deps.quotingService.recalculateLockedQuote(em, quote, scope, false);
|
|
141
|
+
const invalid = preview.adjustmentErrors?.some((error) => error.adjustmentId === id) ?? false;
|
|
142
|
+
await this.assertMutable(em, quote, scope, invalid);
|
|
143
|
+
const before = adjustmentSnapshot(row);
|
|
144
|
+
row.deletedAt = /* @__PURE__ */ new Date();
|
|
145
|
+
row.isActive = false;
|
|
146
|
+
row.updatedByUserId = actor;
|
|
147
|
+
row.updatedAt = /* @__PURE__ */ new Date();
|
|
148
|
+
return this.save(em, quote, row, before, scope, false);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Restores an audited previous row under the same quote lock; never reopens a locked quote. */
|
|
152
|
+
async undo(quoteId, id, before, after, actor, scope) {
|
|
153
|
+
await this.requireEnabled(scope);
|
|
154
|
+
await this.write(quoteId, scope, void 0, async (em, quote) => {
|
|
155
|
+
await this.assertMutable(em, quote, scope);
|
|
156
|
+
for (const snapshot of [before, after]) {
|
|
157
|
+
if (snapshot && (snapshot.tenantId !== scope.tenantId || snapshot.organizationId !== scope.organizationId || snapshot.quoteConfigurationId !== quote.id || snapshot.id !== id)) {
|
|
158
|
+
throw new QuotingError(403, "Adjustment undo scope mismatch");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const row = await this.loadRow(em, quote.id, id, scope, true);
|
|
162
|
+
if (snapshotKey(adjustmentSnapshot(row)) !== snapshotKey(after)) throw new QuotingError(409, "Adjustment changed since this action");
|
|
163
|
+
const restored = before ? cpqPriceAdjustmentInputSchema.parse(this.inputFromSnapshot(before)) : null;
|
|
164
|
+
if (restored && before?.isActive && !before.deletedAt) await this.rejectOverlap(em, quote, { id, ...restored }, scope);
|
|
165
|
+
const beforeUndo = adjustmentSnapshot(row);
|
|
166
|
+
if (before && restored) em.assign(row, {
|
|
167
|
+
...this.values(restored),
|
|
168
|
+
deletedAt: before.deletedAt ? new Date(before.deletedAt) : null,
|
|
169
|
+
isActive: before.isActive,
|
|
170
|
+
updatedByUserId: actor,
|
|
171
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
172
|
+
});
|
|
173
|
+
else {
|
|
174
|
+
row.deletedAt = /* @__PURE__ */ new Date();
|
|
175
|
+
row.isActive = false;
|
|
176
|
+
row.updatedByUserId = actor;
|
|
177
|
+
row.updatedAt = /* @__PURE__ */ new Date();
|
|
178
|
+
}
|
|
179
|
+
await this.save(em, quote, row, beforeUndo, scope, false);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
inputFromSnapshot(snapshot) {
|
|
183
|
+
const { id, quoteConfigurationId, quoteId, tenantId, organizationId, createdByUserId, createdAt, updatedAt, deletedAt, isActive, ...input } = snapshot;
|
|
184
|
+
return input;
|
|
185
|
+
}
|
|
186
|
+
values(input) {
|
|
187
|
+
return {
|
|
188
|
+
level: input.level,
|
|
189
|
+
quoteLineId: input.quoteLineId,
|
|
190
|
+
chargeCode: input.chargeCode ?? null,
|
|
191
|
+
chargeTypeFilter: input.chargeTypeFilter ?? null,
|
|
192
|
+
adjustmentType: input.adjustmentType,
|
|
193
|
+
value: String(input.value),
|
|
194
|
+
currencyCode: input.currencyCode ?? null,
|
|
195
|
+
reasonCode: input.reasonCode,
|
|
196
|
+
reasonNote: input.reasonNote ?? null
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
write(quoteId, scope, revision, execute) {
|
|
200
|
+
return withQuotePricingWrite(this.deps.em, quoteId, scope, execute, (status, message) => new QuotingError(status, message), revision, void 0, { pricingCause: "adjustment" });
|
|
201
|
+
}
|
|
202
|
+
async save(em, quote, row, before, scope, rejectInvalid) {
|
|
203
|
+
let priced;
|
|
204
|
+
await withAtomicFlush(em, [
|
|
205
|
+
() => {
|
|
206
|
+
em.persist(row);
|
|
207
|
+
},
|
|
208
|
+
async () => {
|
|
209
|
+
priced = await this.deps.quotingService.recalculateLockedQuote(em, quote, scope);
|
|
210
|
+
}
|
|
211
|
+
]);
|
|
212
|
+
if (!priced) throw new Error("Adjustment recalculation did not return a quote");
|
|
213
|
+
const invalid = priced.adjustmentErrors?.find((error) => error.adjustmentId === row.id);
|
|
214
|
+
if (rejectInvalid && invalid) throw new QuotingError(422, invalid.code, void 0, { code: invalid.code, adjustmentId: row.id });
|
|
215
|
+
return { adjustment: adjustmentSnapshot(row), before, quote: priced };
|
|
216
|
+
}
|
|
217
|
+
async requireEnabled(scope) {
|
|
218
|
+
if (!await this.enabled(scope.tenantId)) throw new QuotingError(403, "Manual price adjustments are disabled");
|
|
219
|
+
}
|
|
220
|
+
async requireTarget(em, quote, quoteLineId, scope) {
|
|
221
|
+
const line = await findOneWithDecryption(em, CpqQuoteLineConfiguration, { quoteConfigurationId: quote.id, quoteLineId, ...scope, deletedAt: null }, void 0, scope);
|
|
222
|
+
if (!line) throw new QuotingError(404, "Quote line not found");
|
|
223
|
+
if (!(line.offeringId || line.productId) || isArcMirrorLine(line.configuration, quote.quoteType)) throw new QuotingError(422, "LINE_NOT_ELIGIBLE");
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Refuse overlaps against every stored row, valid or not: an overlap hidden
|
|
227
|
+
* behind a currently-invalid row would start applying once that row is
|
|
228
|
+
* deleted, changing the price of a submitted quote. Runs before any mutation.
|
|
229
|
+
*/
|
|
230
|
+
async rejectOverlap(em, quote, candidate, scope) {
|
|
231
|
+
const stored = await loadQuoteAdjustments(em, quote.id, scope);
|
|
232
|
+
if (stored.some((other) => adjustmentsOverlap(candidate, other))) {
|
|
233
|
+
throw new QuotingError(422, "ADJUSTMENT_OVERLAP", void 0, { code: "ADJUSTMENT_OVERLAP", adjustmentId: candidate.id });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async requireReason(em, code, scope) {
|
|
237
|
+
const dictionary = await findOneWithDecryption(em, Dictionary, { key: REASON_DICTIONARY, ...scope, deletedAt: null, isActive: true }, void 0, scope);
|
|
238
|
+
const entry = dictionary && await findOneWithDecryption(em, DictionaryEntry, { dictionary: dictionary.id, ...scope, value: code }, void 0, scope);
|
|
239
|
+
if (!entry) throw new QuotingError(422, "REASON_NOT_FOUND");
|
|
240
|
+
}
|
|
241
|
+
async assertMutable(em, quote, scope, invalidDelete = false) {
|
|
242
|
+
const order = await findOneWithDecryption(em, CpqOrderConfiguration, { sourceQuoteId: quote.id, ...scope, deletedAt: null }, void 0, scope);
|
|
243
|
+
const allowed = ADJUSTMENT_MUTABLE_STATUSES.includes(quote.cpqStatus);
|
|
244
|
+
const invalidException = invalidDelete && !TERMINAL_STATUSES.includes(quote.cpqStatus);
|
|
245
|
+
if (order || !allowed && !invalidException) throw new QuotingError(409, "ADJUSTMENT_LOCKED");
|
|
246
|
+
}
|
|
247
|
+
async loadRow(em, quoteConfigurationId, id, scope, deleted = false) {
|
|
248
|
+
const row = await findOneWithDecryption(em, CpqQuotePriceAdjustment, { id, quoteConfigurationId, ...scope, ...deleted ? {} : { deletedAt: null, isActive: true } }, void 0, scope);
|
|
249
|
+
if (!row) throw new QuotingError(404, "Adjustment not found");
|
|
250
|
+
return row;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function snapshotKey(snapshot) {
|
|
254
|
+
return JSON.stringify(Object.entries(snapshot).sort(([left], [right]) => left.localeCompare(right)));
|
|
255
|
+
}
|
|
256
|
+
export {
|
|
257
|
+
ADJUSTMENT_TOGGLE2 as ADJUSTMENT_TOGGLE,
|
|
258
|
+
CpqAdjustmentService,
|
|
259
|
+
REASON_DICTIONARY2 as REASON_DICTIONARY,
|
|
260
|
+
adjustmentInput,
|
|
261
|
+
adjustmentSnapshot,
|
|
262
|
+
adjustmentsOverlap
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=adjustmentService.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/cpq/services/priceAdjustments/adjustmentService.ts"],
|
|
4
|
+
"sourcesContent": ["import { isArcMirrorLine } from '../arcPriceFreeze'\nimport { ADJUSTMENT_TOGGLE, REASON_DICTIONARY } from '../../data/price-adjustments'\nexport { ADJUSTMENT_TOGGLE, REASON_DICTIONARY } from '../../data/price-adjustments'\nimport { loadQuoteAdjustments } from './loadQuoteAdjustments'\nimport { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { FeatureTogglesService } from '@open-mercato/core/modules/feature_toggles/lib/feature-flag-check'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { CpqOrderConfiguration, CpqQuoteConfiguration, CpqQuoteLineConfiguration, CpqQuotePriceAdjustment } from '../../data/entities'\nimport { cpqPriceAdjustmentInputSchema, cpqPriceAdjustmentPatchSchema, type PriceAdjustmentInput, type PriceAdjustmentPatch } from '../../data/price-adjustments'\nimport { QuotingError, type DefaultCpqQuotingService } from '../cpqQuotingService'\nimport { TERMINAL_STATUSES, type CpqStatus, type QuoteResult, type TenantScope } from '../types'\nimport { withQuotePricingWrite } from './quotePricingWrite'\n\n/** Quote statuses in which reps may still add / edit / delete manual adjustments. */\nconst ADJUSTMENT_MUTABLE_STATUSES: readonly CpqStatus[] = ['new', 'incomplete', 'ready']\n\n\ntype Dependencies = {\n em: EntityManager\n quotingService: DefaultCpqQuotingService\n featureTogglesService: Pick<FeatureTogglesService, 'getBoolConfig'>\n}\n\nexport function adjustmentInput(row: CpqQuotePriceAdjustment): PriceAdjustmentInput {\n return cpqPriceAdjustmentInputSchema.parse({ level: row.level, quoteLineId: row.quoteLineId,\n chargeCode: row.chargeCode, chargeTypeFilter: row.chargeTypeFilter, adjustmentType: row.adjustmentType,\n value: Number(row.value), currencyCode: row.currencyCode, reasonCode: row.reasonCode, reasonNote: row.reasonNote })\n}\nexport function adjustmentSnapshot(row: CpqQuotePriceAdjustment) {\n return { id: row.id, ...adjustmentInput(row), quoteConfigurationId: row.quoteConfigurationId,\n quoteId: row.quoteId, tenantId: row.tenantId, organizationId: row.organizationId,\n createdByUserId: row.createdByUserId, createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(), deletedAt: row.deletedAt?.toISOString() ?? null, isActive: row.isActive }\n}\nexport type AdjustmentSnapshot = ReturnType<typeof adjustmentSnapshot>\n\ntype OverlapKey = { id: string; level: string; quoteLineId: string; chargeCode?: string | null; chargeTypeFilter?: string | null }\n/** Same level and target; line filters overlap when either covers all charges or both match. */\nexport function adjustmentsOverlap(a: OverlapKey, b: OverlapKey): boolean {\n if (a.id === b.id || a.level !== b.level || a.quoteLineId !== b.quoteLineId) return false\n if (a.level === 'charge') return (a.chargeCode ?? null) === (b.chargeCode ?? null)\n return !a.chargeTypeFilter || !b.chargeTypeFilter || a.chargeTypeFilter === b.chargeTypeFilter\n}\nexport interface AdjustmentWriteResult {\n adjustment: AdjustmentSnapshot\n before: AdjustmentSnapshot | null\n quote: QuoteResult\n}\n\nexport class CpqAdjustmentService {\n constructor(private readonly deps: Dependencies) {}\n\n async enabled(tenantId: string): Promise<boolean> {\n const flag = await this.deps.featureTogglesService.getBoolConfig(ADJUSTMENT_TOGGLE, tenantId)\n return flag.ok && flag.value === true\n }\n\n async list(quoteId: string, scope: TenantScope, page: number, pageSize: number) {\n const em = this.deps.em.fork({ clear: true, useContext: false })\n const scoped = { ...scope, deletedAt: null }\n const quote = await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, id: quoteId }, undefined, scope)\n ?? await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, quoteId }, undefined, scope)\n if (!quote) throw new QuotingError(404, 'Quote not found')\n const filter = { ...scoped, quoteConfigurationId: quote.id, isActive: true }\n const [rows, total, enabled, reasons] = await Promise.all([\n findWithDecryption(em, CpqQuotePriceAdjustment, filter, { limit: pageSize, offset: (page - 1) * pageSize, orderBy: { createdAt: 'asc', id: 'asc' } }, scope),\n em.count(CpqQuotePriceAdjustment, filter),\n this.enabled(scope.tenantId),\n this.reasons(em, scope),\n ])\n return { items: rows.map(adjustmentSnapshot), total, page, pageSize, totalPages: Math.ceil(total / pageSize), pricingRevision: quote.pricingRevision ?? 0, enabled, reasons }\n }\n\n /**\n * Reason options for the adjustment dialog. Served here so reps need only\n * `cpq.quotes.view`, not `dictionaries.view`. An inactive dictionary yields\n * no options, matching `requireReason`.\n */\n async reasons(em: EntityManager, scope: TenantScope): Promise<Array<{ value: string; label: string }>> {\n const dictionary = await findOneWithDecryption(em, Dictionary, { key: REASON_DICTIONARY, ...scope, deletedAt: null, isActive: true }, undefined, scope)\n if (!dictionary) return []\n const entries = await findWithDecryption(em, DictionaryEntry, { dictionary: dictionary.id, ...scope }, { orderBy: { position: 'asc', label: 'asc' } }, scope)\n return entries.map((entry) => ({ value: entry.value, label: entry.label }))\n }\n\n async preview(quoteId: string, input: PriceAdjustmentInput, replaceAdjustmentId: string | undefined, scope: TenantScope) {\n const parsed = cpqPriceAdjustmentInputSchema.parse(input)\n await this.requireEnabled(scope)\n const em = this.deps.em.fork({ clear: true, useContext: false })\n const scoped = { ...scope, deletedAt: null }\n const quote = await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, id: quoteId }, undefined, scope)\n ?? await findOneWithDecryption(em, CpqQuoteConfiguration, { ...scoped, quoteId }, undefined, scope)\n if (!quote) throw new QuotingError(404, 'Quote not found')\n if (parsed.expectedRevision !== undefined && parsed.expectedRevision !== (quote.pricingRevision ?? 0)) throw new QuotingError(409, 'STALE_REVISION')\n await this.assertMutable(em, quote, scope)\n await this.requireTarget(em, quote, parsed.quoteLineId, scope)\n const stored = await loadQuoteAdjustments(em, quote.id, scope)\n const replaced = stored.find((row) => row.id === replaceAdjustmentId)\n if (replaceAdjustmentId && !replaced) throw new QuotingError(404, 'Adjustment not found')\n if (replaced && (replaced.level !== parsed.level || replaced.quoteLineId !== parsed.quoteLineId || (replaced.chargeCode ?? null) !== (parsed.chargeCode ?? null))) throw new QuotingError(422, 'Adjustment target is immutable')\n if (!replaced || replaced.reasonCode !== parsed.reasonCode) await this.requireReason(em, parsed.reasonCode, scope)\n const draft = { ...parsed, id: replaceAdjustmentId ?? randomUUID() }\n const adjustments = [...stored.filter((row) => row.id !== replaceAdjustmentId), draft]\n const result = await this.deps.quotingService.recalculateLockedQuote(em, quote, scope, false, adjustments)\n return { quote: result, errors: result.adjustmentErrors ?? [] }\n }\n\n async create(quoteId: string, input: PriceAdjustmentInput, actor: string, scope: TenantScope): Promise<AdjustmentWriteResult> {\n const parsed = cpqPriceAdjustmentInputSchema.parse(input)\n await this.requireEnabled(scope)\n return this.write(quoteId, scope, parsed.expectedRevision, async (em, quote) => {\n await this.assertMutable(em, quote, scope)\n await this.requireReason(em, parsed.reasonCode, scope)\n await this.requireTarget(em, quote, parsed.quoteLineId, scope)\n const id = randomUUID()\n await this.rejectOverlap(em, quote, { id, ...parsed }, scope)\n const row = em.create(CpqQuotePriceAdjustment, { ...scope, id, quoteConfigurationId: quote.id,\n quoteId: quote.quoteId, ...this.values(parsed), createdByUserId: actor })\n return this.save(em, quote, row, null, scope, true)\n })\n }\n\n async update(quoteId: string, id: string, patch: PriceAdjustmentPatch, actor: string, scope: TenantScope): Promise<AdjustmentWriteResult> {\n const parsed = cpqPriceAdjustmentPatchSchema.parse(patch)\n await this.requireEnabled(scope)\n return this.write(quoteId, scope, parsed.expectedRevision, async (em, quote) => {\n await this.assertMutable(em, quote, scope)\n const row = await this.loadRow(em, quote.id, id, scope)\n const before = adjustmentSnapshot(row)\n await this.requireTarget(em, quote, row.quoteLineId, scope)\n const merged = cpqPriceAdjustmentInputSchema.parse({ ...adjustmentInput(row), ...parsed })\n if (merged.reasonCode !== row.reasonCode) await this.requireReason(em, merged.reasonCode, scope)\n await this.rejectOverlap(em, quote, { id: row.id, ...merged }, scope)\n em.assign(row, { ...this.values(merged), updatedByUserId: actor, updatedAt: new Date() })\n return this.save(em, quote, row, before, scope, true)\n })\n }\n\n async remove(quoteId: string, id: string, actor: string, scope: TenantScope, expectedRevision?: number): Promise<AdjustmentWriteResult> {\n await this.requireEnabled(scope)\n return this.write(quoteId, scope, expectedRevision, async (em, quote) => {\n const row = await this.loadRow(em, quote.id, id, scope)\n const preview = await this.deps.quotingService.recalculateLockedQuote(em, quote, scope, false)\n const invalid = preview.adjustmentErrors?.some((error) => error.adjustmentId === id) ?? false\n await this.assertMutable(em, quote, scope, invalid)\n const before = adjustmentSnapshot(row)\n row.deletedAt = new Date(); row.isActive = false; row.updatedByUserId = actor; row.updatedAt = new Date()\n return this.save(em, quote, row, before, scope, false)\n })\n }\n\n /** Restores an audited previous row under the same quote lock; never reopens a locked quote. */\n async undo(quoteId: string, id: string, before: AdjustmentSnapshot | null, after: AdjustmentSnapshot, actor: string, scope: TenantScope): Promise<void> {\n await this.requireEnabled(scope)\n await this.write(quoteId, scope, undefined, async (em, quote) => {\n await this.assertMutable(em, quote, scope)\n for (const snapshot of [before, after]) {\n if (snapshot && (snapshot.tenantId !== scope.tenantId || snapshot.organizationId !== scope.organizationId || snapshot.quoteConfigurationId !== quote.id || snapshot.id !== id)) {\n throw new QuotingError(403, 'Adjustment undo scope mismatch')\n }\n }\n const row = await this.loadRow(em, quote.id, id, scope, true)\n if (snapshotKey(adjustmentSnapshot(row)) !== snapshotKey(after)) throw new QuotingError(409, 'Adjustment changed since this action')\n const restored = before ? cpqPriceAdjustmentInputSchema.parse(this.inputFromSnapshot(before)) : null\n // Restoring an active row must not create an overlap a rep added since.\n if (restored && before?.isActive && !before.deletedAt) await this.rejectOverlap(em, quote, { id, ...restored }, scope)\n const beforeUndo = adjustmentSnapshot(row)\n if (before && restored) em.assign(row, { ...this.values(restored),\n deletedAt: before.deletedAt ? new Date(before.deletedAt) : null, isActive: before.isActive, updatedByUserId: actor, updatedAt: new Date() })\n else { row.deletedAt = new Date(); row.isActive = false; row.updatedByUserId = actor; row.updatedAt = new Date() }\n await this.save(em, quote, row, beforeUndo, scope, false)\n })\n }\n\n private inputFromSnapshot(snapshot: AdjustmentSnapshot): PriceAdjustmentInput {\n const { id, quoteConfigurationId, quoteId, tenantId, organizationId, createdByUserId, createdAt, updatedAt, deletedAt, isActive, ...input } = snapshot\n return input\n }\n\n private values(input: PriceAdjustmentInput) {\n return { level: input.level, quoteLineId: input.quoteLineId, chargeCode: input.chargeCode ?? null,\n chargeTypeFilter: input.chargeTypeFilter ?? null, adjustmentType: input.adjustmentType, value: String(input.value),\n currencyCode: input.currencyCode ?? null, reasonCode: input.reasonCode, reasonNote: input.reasonNote ?? null }\n }\n\n private write<T>(quoteId: string, scope: TenantScope, revision: number | undefined, execute: (em: EntityManager, quote: CpqQuoteConfiguration) => Promise<T>) {\n return withQuotePricingWrite(this.deps.em, quoteId, scope, execute, (status, message) => new QuotingError(status, message), revision, undefined, { pricingCause: 'adjustment' })\n }\n\n private async save(em: EntityManager, quote: CpqQuoteConfiguration, row: CpqQuotePriceAdjustment, before: AdjustmentSnapshot | null, scope: TenantScope, rejectInvalid: boolean): Promise<AdjustmentWriteResult> {\n let priced: QuoteResult | undefined\n await withAtomicFlush(em, [\n () => { em.persist(row) },\n async () => { priced = await this.deps.quotingService.recalculateLockedQuote(em, quote, scope) },\n ])\n if (!priced) throw new Error('Adjustment recalculation did not return a quote')\n const invalid = priced.adjustmentErrors?.find((error) => error.adjustmentId === row.id)\n if (rejectInvalid && invalid) throw new QuotingError(422, invalid.code, undefined, { code: invalid.code, adjustmentId: row.id })\n return { adjustment: adjustmentSnapshot(row), before, quote: priced }\n }\n\n private async requireEnabled(scope: TenantScope) {\n if (!(await this.enabled(scope.tenantId))) throw new QuotingError(403, 'Manual price adjustments are disabled')\n }\n\n private async requireTarget(em: EntityManager, quote: CpqQuoteConfiguration, quoteLineId: string, scope: TenantScope) {\n const line = await findOneWithDecryption(em, CpqQuoteLineConfiguration, { quoteConfigurationId: quote.id, quoteLineId, ...scope, deletedAt: null }, undefined, scope)\n if (!line) throw new QuotingError(404, 'Quote line not found')\n if (!(line.offeringId || line.productId) || isArcMirrorLine(line.configuration, quote.quoteType)) throw new QuotingError(422, 'LINE_NOT_ELIGIBLE')\n }\n\n /**\n * Refuse overlaps against every stored row, valid or not: an overlap hidden\n * behind a currently-invalid row would start applying once that row is\n * deleted, changing the price of a submitted quote. Runs before any mutation.\n */\n private async rejectOverlap(em: EntityManager, quote: CpqQuoteConfiguration, candidate: Parameters<typeof adjustmentsOverlap>[0], scope: TenantScope) {\n const stored = await loadQuoteAdjustments(em, quote.id, scope)\n if (stored.some((other) => adjustmentsOverlap(candidate, other))) {\n throw new QuotingError(422, 'ADJUSTMENT_OVERLAP', undefined, { code: 'ADJUSTMENT_OVERLAP', adjustmentId: candidate.id })\n }\n }\n\n private async requireReason(em: EntityManager, code: string, scope: TenantScope) {\n const dictionary = await findOneWithDecryption(em, Dictionary, { key: REASON_DICTIONARY, ...scope, deletedAt: null, isActive: true }, undefined, scope)\n const entry = dictionary && await findOneWithDecryption(em, DictionaryEntry, { dictionary: dictionary.id, ...scope, value: code }, undefined, scope)\n if (!entry) throw new QuotingError(422, 'REASON_NOT_FOUND')\n }\n\n private async assertMutable(em: EntityManager, quote: CpqQuoteConfiguration, scope: TenantScope, invalidDelete = false) {\n const order = await findOneWithDecryption(em, CpqOrderConfiguration, { sourceQuoteId: quote.id, ...scope, deletedAt: null }, undefined, scope)\n const allowed = ADJUSTMENT_MUTABLE_STATUSES.includes(quote.cpqStatus as CpqStatus)\n const invalidException = invalidDelete && !TERMINAL_STATUSES.includes(quote.cpqStatus as CpqStatus)\n if (order || (!allowed && !invalidException)) throw new QuotingError(409, 'ADJUSTMENT_LOCKED')\n }\n\n private async loadRow(em: EntityManager, quoteConfigurationId: string, id: string, scope: TenantScope, deleted = false) {\n const row = await findOneWithDecryption(em, CpqQuotePriceAdjustment, { id, quoteConfigurationId, ...scope, ...(deleted ? {} : { deletedAt: null, isActive: true }) }, undefined, scope)\n if (!row) throw new QuotingError(404, 'Adjustment not found')\n return row\n }\n}\n\nfunction snapshotKey(snapshot: AdjustmentSnapshot): string {\n return JSON.stringify(Object.entries(snapshot).sort(([left], [right]) => left.localeCompare(right)))\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,qBAAAA,oBAAmB,qBAAAC,0BAAyB;AACrD,SAAS,4BAA4B;AACrC,SAAS,kBAAkB;AAE3B,SAAS,oBAAoB,6BAA6B;AAC1D,SAAS,uBAAuB;AAEhC,SAAS,YAAY,uBAAuB;AAC5C,SAAS,uBAAuB,uBAAuB,2BAA2B,+BAA+B;AACjH,SAAS,+BAA+B,qCAA2F;AACnI,SAAS,oBAAmD;AAC5D,SAAS,yBAA6E;AACtF,SAAS,6BAA6B;AAGtC,MAAM,8BAAoD,CAAC,OAAO,cAAc,OAAO;AAShF,SAAS,gBAAgB,KAAoD;AAClF,SAAO,8BAA8B,MAAM;AAAA,IAAE,OAAO,IAAI;AAAA,IAAO,aAAa,IAAI;AAAA,IAC9E,YAAY,IAAI;AAAA,IAAY,kBAAkB,IAAI;AAAA,IAAkB,gBAAgB,IAAI;AAAA,IACxF,OAAO,OAAO,IAAI,KAAK;AAAA,IAAG,cAAc,IAAI;AAAA,IAAc,YAAY,IAAI;AAAA,IAAY,YAAY,IAAI;AAAA,EAAW,CAAC;AACtH;AACO,SAAS,mBAAmB,KAA8B;AAC/D,SAAO;AAAA,IAAE,IAAI,IAAI;AAAA,IAAI,GAAG,gBAAgB,GAAG;AAAA,IAAG,sBAAsB,IAAI;AAAA,IACtE,SAAS,IAAI;AAAA,IAAS,UAAU,IAAI;AAAA,IAAU,gBAAgB,IAAI;AAAA,IAClE,iBAAiB,IAAI;AAAA,IAAiB,WAAW,IAAI,UAAU,YAAY;AAAA,IAC3E,WAAW,IAAI,UAAU,YAAY;AAAA,IAAG,WAAW,IAAI,WAAW,YAAY,KAAK;AAAA,IAAM,UAAU,IAAI;AAAA,EAAS;AACpH;AAKO,SAAS,mBAAmB,GAAe,GAAwB;AACxE,MAAI,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAa,QAAO;AACpF,MAAI,EAAE,UAAU,SAAU,SAAQ,EAAE,cAAc,WAAW,EAAE,cAAc;AAC7E,SAAO,CAAC,EAAE,oBAAoB,CAAC,EAAE,oBAAoB,EAAE,qBAAqB,EAAE;AAChF;AAOO,MAAM,qBAAqB;AAAA,EAChC,YAA6B,MAAoB;AAApB;AAAA,EAAqB;AAAA,EAElD,MAAM,QAAQ,UAAoC;AAChD,UAAM,OAAO,MAAM,KAAK,KAAK,sBAAsB,cAAc,mBAAmB,QAAQ;AAC5F,WAAO,KAAK,MAAM,KAAK,UAAU;AAAA,EACnC;AAAA,EAEA,MAAM,KAAK,SAAiB,OAAoB,MAAc,UAAkB;AAC9E,UAAM,KAAK,KAAK,KAAK,GAAG,KAAK,EAAE,OAAO,MAAM,YAAY,MAAM,CAAC;AAC/D,UAAM,SAAS,EAAE,GAAG,OAAO,WAAW,KAAK;AAC3C,UAAM,QAAQ,MAAM,sBAAsB,IAAI,uBAAuB,EAAE,GAAG,QAAQ,IAAI,QAAQ,GAAG,QAAW,KAAK,KAC5G,MAAM,sBAAsB,IAAI,uBAAuB,EAAE,GAAG,QAAQ,QAAQ,GAAG,QAAW,KAAK;AACpG,QAAI,CAAC,MAAO,OAAM,IAAI,aAAa,KAAK,iBAAiB;AACzD,UAAM,SAAS,EAAE,GAAG,QAAQ,sBAAsB,MAAM,IAAI,UAAU,KAAK;AAC3E,UAAM,CAAC,MAAM,OAAO,SAAS,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,MACxD,mBAAmB,IAAI,yBAAyB,QAAQ,EAAE,OAAO,UAAU,SAAS,OAAO,KAAK,UAAU,SAAS,EAAE,WAAW,OAAO,IAAI,MAAM,EAAE,GAAG,KAAK;AAAA,MAC3J,GAAG,MAAM,yBAAyB,MAAM;AAAA,MACxC,KAAK,QAAQ,MAAM,QAAQ;AAAA,MAC3B,KAAK,QAAQ,IAAI,KAAK;AAAA,IACxB,CAAC;AACD,WAAO,EAAE,OAAO,KAAK,IAAI,kBAAkB,GAAG,OAAO,MAAM,UAAU,YAAY,KAAK,KAAK,QAAQ,QAAQ,GAAG,iBAAiB,MAAM,mBAAmB,GAAG,SAAS,QAAQ;AAAA,EAC9K;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAQ,IAAmB,OAAsE;AACrG,UAAM,aAAa,MAAM,sBAAsB,IAAI,YAAY,EAAE,KAAK,mBAAmB,GAAG,OAAO,WAAW,MAAM,UAAU,KAAK,GAAG,QAAW,KAAK;AACtJ,QAAI,CAAC,WAAY,QAAO,CAAC;AACzB,UAAM,UAAU,MAAM,mBAAmB,IAAI,iBAAiB,EAAE,YAAY,WAAW,IAAI,GAAG,MAAM,GAAG,EAAE,SAAS,EAAE,UAAU,OAAO,OAAO,MAAM,EAAE,GAAG,KAAK;AAC5J,WAAO,QAAQ,IAAI,CAAC,WAAW,EAAE,OAAO,MAAM,OAAO,OAAO,MAAM,MAAM,EAAE;AAAA,EAC5E;AAAA,EAEA,MAAM,QAAQ,SAAiB,OAA6B,qBAAyC,OAAoB;AACvH,UAAM,SAAS,8BAA8B,MAAM,KAAK;AACxD,UAAM,KAAK,eAAe,KAAK;AAC/B,UAAM,KAAK,KAAK,KAAK,GAAG,KAAK,EAAE,OAAO,MAAM,YAAY,MAAM,CAAC;AAC/D,UAAM,SAAS,EAAE,GAAG,OAAO,WAAW,KAAK;AAC3C,UAAM,QAAQ,MAAM,sBAAsB,IAAI,uBAAuB,EAAE,GAAG,QAAQ,IAAI,QAAQ,GAAG,QAAW,KAAK,KAC5G,MAAM,sBAAsB,IAAI,uBAAuB,EAAE,GAAG,QAAQ,QAAQ,GAAG,QAAW,KAAK;AACpG,QAAI,CAAC,MAAO,OAAM,IAAI,aAAa,KAAK,iBAAiB;AACzD,QAAI,OAAO,qBAAqB,UAAa,OAAO,sBAAsB,MAAM,mBAAmB,GAAI,OAAM,IAAI,aAAa,KAAK,gBAAgB;AACnJ,UAAM,KAAK,cAAc,IAAI,OAAO,KAAK;AACzC,UAAM,KAAK,cAAc,IAAI,OAAO,OAAO,aAAa,KAAK;AAC7D,UAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM,IAAI,KAAK;AAC7D,UAAM,WAAW,OAAO,KAAK,CAAC,QAAQ,IAAI,OAAO,mBAAmB;AACpE,QAAI,uBAAuB,CAAC,SAAU,OAAM,IAAI,aAAa,KAAK,sBAAsB;AACxF,QAAI,aAAa,SAAS,UAAU,OAAO,SAAS,SAAS,gBAAgB,OAAO,gBAAgB,SAAS,cAAc,WAAW,OAAO,cAAc,OAAQ,OAAM,IAAI,aAAa,KAAK,gCAAgC;AAC/N,QAAI,CAAC,YAAY,SAAS,eAAe,OAAO,WAAY,OAAM,KAAK,cAAc,IAAI,OAAO,YAAY,KAAK;AACjH,UAAM,QAAQ,EAAE,GAAG,QAAQ,IAAI,uBAAuB,WAAW,EAAE;AACnE,UAAM,cAAc,CAAC,GAAG,OAAO,OAAO,CAAC,QAAQ,IAAI,OAAO,mBAAmB,GAAG,KAAK;AACrF,UAAM,SAAS,MAAM,KAAK,KAAK,eAAe,uBAAuB,IAAI,OAAO,OAAO,OAAO,WAAW;AACzG,WAAO,EAAE,OAAO,QAAQ,QAAQ,OAAO,oBAAoB,CAAC,EAAE;AAAA,EAChE;AAAA,EAEA,MAAM,OAAO,SAAiB,OAA6B,OAAe,OAAoD;AAC5H,UAAM,SAAS,8BAA8B,MAAM,KAAK;AACxD,UAAM,KAAK,eAAe,KAAK;AAC/B,WAAO,KAAK,MAAM,SAAS,OAAO,OAAO,kBAAkB,OAAO,IAAI,UAAU;AAC9E,YAAM,KAAK,cAAc,IAAI,OAAO,KAAK;AACzC,YAAM,KAAK,cAAc,IAAI,OAAO,YAAY,KAAK;AACrD,YAAM,KAAK,cAAc,IAAI,OAAO,OAAO,aAAa,KAAK;AAC7D,YAAM,KAAK,WAAW;AACtB,YAAM,KAAK,cAAc,IAAI,OAAO,EAAE,IAAI,GAAG,OAAO,GAAG,KAAK;AAC5D,YAAM,MAAM,GAAG,OAAO,yBAAyB;AAAA,QAAE,GAAG;AAAA,QAAO;AAAA,QAAI,sBAAsB,MAAM;AAAA,QACzF,SAAS,MAAM;AAAA,QAAS,GAAG,KAAK,OAAO,MAAM;AAAA,QAAG,iBAAiB;AAAA,MAAM,CAAC;AAC1E,aAAO,KAAK,KAAK,IAAI,OAAO,KAAK,MAAM,OAAO,IAAI;AAAA,IACpD,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,SAAiB,IAAY,OAA6B,OAAe,OAAoD;AACxI,UAAM,SAAS,8BAA8B,MAAM,KAAK;AACxD,UAAM,KAAK,eAAe,KAAK;AAC/B,WAAO,KAAK,MAAM,SAAS,OAAO,OAAO,kBAAkB,OAAO,IAAI,UAAU;AAC9E,YAAM,KAAK,cAAc,IAAI,OAAO,KAAK;AACzC,YAAM,MAAM,MAAM,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI,KAAK;AACtD,YAAM,SAAS,mBAAmB,GAAG;AACrC,YAAM,KAAK,cAAc,IAAI,OAAO,IAAI,aAAa,KAAK;AAC1D,YAAM,SAAS,8BAA8B,MAAM,EAAE,GAAG,gBAAgB,GAAG,GAAG,GAAG,OAAO,CAAC;AACzF,UAAI,OAAO,eAAe,IAAI,WAAY,OAAM,KAAK,cAAc,IAAI,OAAO,YAAY,KAAK;AAC/F,YAAM,KAAK,cAAc,IAAI,OAAO,EAAE,IAAI,IAAI,IAAI,GAAG,OAAO,GAAG,KAAK;AACpE,SAAG,OAAO,KAAK,EAAE,GAAG,KAAK,OAAO,MAAM,GAAG,iBAAiB,OAAO,WAAW,oBAAI,KAAK,EAAE,CAAC;AACxF,aAAO,KAAK,KAAK,IAAI,OAAO,KAAK,QAAQ,OAAO,IAAI;AAAA,IACtD,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,SAAiB,IAAY,OAAe,OAAoB,kBAA2D;AACtI,UAAM,KAAK,eAAe,KAAK;AAC/B,WAAO,KAAK,MAAM,SAAS,OAAO,kBAAkB,OAAO,IAAI,UAAU;AACvE,YAAM,MAAM,MAAM,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI,KAAK;AACtD,YAAM,UAAU,MAAM,KAAK,KAAK,eAAe,uBAAuB,IAAI,OAAO,OAAO,KAAK;AAC7F,YAAM,UAAU,QAAQ,kBAAkB,KAAK,CAAC,UAAU,MAAM,iBAAiB,EAAE,KAAK;AACxF,YAAM,KAAK,cAAc,IAAI,OAAO,OAAO,OAAO;AAClD,YAAM,SAAS,mBAAmB,GAAG;AACrC,UAAI,YAAY,oBAAI,KAAK;AAAG,UAAI,WAAW;AAAO,UAAI,kBAAkB;AAAO,UAAI,YAAY,oBAAI,KAAK;AACxG,aAAO,KAAK,KAAK,IAAI,OAAO,KAAK,QAAQ,OAAO,KAAK;AAAA,IACvD,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,KAAK,SAAiB,IAAY,QAAmC,OAA2B,OAAe,OAAmC;AACtJ,UAAM,KAAK,eAAe,KAAK;AAC/B,UAAM,KAAK,MAAM,SAAS,OAAO,QAAW,OAAO,IAAI,UAAU;AAC/D,YAAM,KAAK,cAAc,IAAI,OAAO,KAAK;AACzC,iBAAW,YAAY,CAAC,QAAQ,KAAK,GAAG;AACtC,YAAI,aAAa,SAAS,aAAa,MAAM,YAAY,SAAS,mBAAmB,MAAM,kBAAkB,SAAS,yBAAyB,MAAM,MAAM,SAAS,OAAO,KAAK;AAC9K,gBAAM,IAAI,aAAa,KAAK,gCAAgC;AAAA,QAC9D;AAAA,MACF;AACA,YAAM,MAAM,MAAM,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI,OAAO,IAAI;AAC5D,UAAI,YAAY,mBAAmB,GAAG,CAAC,MAAM,YAAY,KAAK,EAAG,OAAM,IAAI,aAAa,KAAK,sCAAsC;AACnI,YAAM,WAAW,SAAS,8BAA8B,MAAM,KAAK,kBAAkB,MAAM,CAAC,IAAI;AAEhG,UAAI,YAAY,QAAQ,YAAY,CAAC,OAAO,UAAW,OAAM,KAAK,cAAc,IAAI,OAAO,EAAE,IAAI,GAAG,SAAS,GAAG,KAAK;AACrH,YAAM,aAAa,mBAAmB,GAAG;AACzC,UAAI,UAAU,SAAU,IAAG,OAAO,KAAK;AAAA,QAAE,GAAG,KAAK,OAAO,QAAQ;AAAA,QAC9D,WAAW,OAAO,YAAY,IAAI,KAAK,OAAO,SAAS,IAAI;AAAA,QAAM,UAAU,OAAO;AAAA,QAAU,iBAAiB;AAAA,QAAO,WAAW,oBAAI,KAAK;AAAA,MAAE,CAAC;AAAA,WACxI;AAAE,YAAI,YAAY,oBAAI,KAAK;AAAG,YAAI,WAAW;AAAO,YAAI,kBAAkB;AAAO,YAAI,YAAY,oBAAI,KAAK;AAAA,MAAE;AACjH,YAAM,KAAK,KAAK,IAAI,OAAO,KAAK,YAAY,OAAO,KAAK;AAAA,IAC1D,CAAC;AAAA,EACH;AAAA,EAEQ,kBAAkB,UAAoD;AAC5E,UAAM,EAAE,IAAI,sBAAsB,SAAS,UAAU,gBAAgB,iBAAiB,WAAW,WAAW,WAAW,UAAU,GAAG,MAAM,IAAI;AAC9I,WAAO;AAAA,EACT;AAAA,EAEQ,OAAO,OAA6B;AAC1C,WAAO;AAAA,MAAE,OAAO,MAAM;AAAA,MAAO,aAAa,MAAM;AAAA,MAAa,YAAY,MAAM,cAAc;AAAA,MAC3F,kBAAkB,MAAM,oBAAoB;AAAA,MAAM,gBAAgB,MAAM;AAAA,MAAgB,OAAO,OAAO,MAAM,KAAK;AAAA,MACjH,cAAc,MAAM,gBAAgB;AAAA,MAAM,YAAY,MAAM;AAAA,MAAY,YAAY,MAAM,cAAc;AAAA,IAAK;AAAA,EACjH;AAAA,EAEQ,MAAS,SAAiB,OAAoB,UAA8B,SAA0E;AAC5J,WAAO,sBAAsB,KAAK,KAAK,IAAI,SAAS,OAAO,SAAS,CAAC,QAAQ,YAAY,IAAI,aAAa,QAAQ,OAAO,GAAG,UAAU,QAAW,EAAE,cAAc,aAAa,CAAC;AAAA,EACjL;AAAA,EAEA,MAAc,KAAK,IAAmB,OAA8B,KAA8B,QAAmC,OAAoB,eAAwD;AAC/M,QAAI;AACJ,UAAM,gBAAgB,IAAI;AAAA,MACxB,MAAM;AAAE,WAAG,QAAQ,GAAG;AAAA,MAAE;AAAA,MACxB,YAAY;AAAE,iBAAS,MAAM,KAAK,KAAK,eAAe,uBAAuB,IAAI,OAAO,KAAK;AAAA,MAAE;AAAA,IACjG,CAAC;AACD,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,iDAAiD;AAC9E,UAAM,UAAU,OAAO,kBAAkB,KAAK,CAAC,UAAU,MAAM,iBAAiB,IAAI,EAAE;AACtF,QAAI,iBAAiB,QAAS,OAAM,IAAI,aAAa,KAAK,QAAQ,MAAM,QAAW,EAAE,MAAM,QAAQ,MAAM,cAAc,IAAI,GAAG,CAAC;AAC/H,WAAO,EAAE,YAAY,mBAAmB,GAAG,GAAG,QAAQ,OAAO,OAAO;AAAA,EACtE;AAAA,EAEA,MAAc,eAAe,OAAoB;AAC/C,QAAI,CAAE,MAAM,KAAK,QAAQ,MAAM,QAAQ,EAAI,OAAM,IAAI,aAAa,KAAK,uCAAuC;AAAA,EAChH;AAAA,EAEA,MAAc,cAAc,IAAmB,OAA8B,aAAqB,OAAoB;AACpH,UAAM,OAAO,MAAM,sBAAsB,IAAI,2BAA2B,EAAE,sBAAsB,MAAM,IAAI,aAAa,GAAG,OAAO,WAAW,KAAK,GAAG,QAAW,KAAK;AACpK,QAAI,CAAC,KAAM,OAAM,IAAI,aAAa,KAAK,sBAAsB;AAC7D,QAAI,EAAE,KAAK,cAAc,KAAK,cAAc,gBAAgB,KAAK,eAAe,MAAM,SAAS,EAAG,OAAM,IAAI,aAAa,KAAK,mBAAmB;AAAA,EACnJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,cAAc,IAAmB,OAA8B,WAAqD,OAAoB;AACpJ,UAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM,IAAI,KAAK;AAC7D,QAAI,OAAO,KAAK,CAAC,UAAU,mBAAmB,WAAW,KAAK,CAAC,GAAG;AAChE,YAAM,IAAI,aAAa,KAAK,sBAAsB,QAAW,EAAE,MAAM,sBAAsB,cAAc,UAAU,GAAG,CAAC;AAAA,IACzH;AAAA,EACF;AAAA,EAEA,MAAc,cAAc,IAAmB,MAAc,OAAoB;AAC/E,UAAM,aAAa,MAAM,sBAAsB,IAAI,YAAY,EAAE,KAAK,mBAAmB,GAAG,OAAO,WAAW,MAAM,UAAU,KAAK,GAAG,QAAW,KAAK;AACtJ,UAAM,QAAQ,cAAc,MAAM,sBAAsB,IAAI,iBAAiB,EAAE,YAAY,WAAW,IAAI,GAAG,OAAO,OAAO,KAAK,GAAG,QAAW,KAAK;AACnJ,QAAI,CAAC,MAAO,OAAM,IAAI,aAAa,KAAK,kBAAkB;AAAA,EAC5D;AAAA,EAEA,MAAc,cAAc,IAAmB,OAA8B,OAAoB,gBAAgB,OAAO;AACtH,UAAM,QAAQ,MAAM,sBAAsB,IAAI,uBAAuB,EAAE,eAAe,MAAM,IAAI,GAAG,OAAO,WAAW,KAAK,GAAG,QAAW,KAAK;AAC7I,UAAM,UAAU,4BAA4B,SAAS,MAAM,SAAsB;AACjF,UAAM,mBAAmB,iBAAiB,CAAC,kBAAkB,SAAS,MAAM,SAAsB;AAClG,QAAI,SAAU,CAAC,WAAW,CAAC,iBAAmB,OAAM,IAAI,aAAa,KAAK,mBAAmB;AAAA,EAC/F;AAAA,EAEA,MAAc,QAAQ,IAAmB,sBAA8B,IAAY,OAAoB,UAAU,OAAO;AACtH,UAAM,MAAM,MAAM,sBAAsB,IAAI,yBAAyB,EAAE,IAAI,sBAAsB,GAAG,OAAO,GAAI,UAAU,CAAC,IAAI,EAAE,WAAW,MAAM,UAAU,KAAK,EAAG,GAAG,QAAW,KAAK;AACtL,QAAI,CAAC,IAAK,OAAM,IAAI,aAAa,KAAK,sBAAsB;AAC5D,WAAO;AAAA,EACT;AACF;AAEA,SAAS,YAAY,UAAsC;AACzD,SAAO,KAAK,UAAU,OAAO,QAAQ,QAAQ,EAAE,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,MAAM,KAAK,cAAc,KAAK,CAAC,CAAC;AACrG;",
|
|
6
|
+
"names": ["ADJUSTMENT_TOGGLE", "REASON_DICTIONARY"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function withChargeWaterfall(charge) {
|
|
2
|
+
const adjustments = charge.adjustments ?? [];
|
|
3
|
+
const rules = adjustments.filter((entry) => entry.origin !== "manual");
|
|
4
|
+
const manual = adjustments.find((entry) => entry.origin === "manual");
|
|
5
|
+
const listUnitPrice = charge.listUnitPrice ?? rules[0]?.unitPriceBefore ?? manual?.unitPriceBefore ?? charge.unitPrice;
|
|
6
|
+
const standardUnitPrice = charge.standardUnitPrice ?? rules.at(-1)?.unitPriceAfter ?? manual?.unitPriceBefore ?? charge.unitPrice;
|
|
7
|
+
const totalAt = (unit) => charge.totalPrice === null ? null : unit === charge.unitPrice ? charge.totalPrice : unit * (charge.quantity ?? 1);
|
|
8
|
+
return {
|
|
9
|
+
...charge,
|
|
10
|
+
listUnitPrice,
|
|
11
|
+
listTotalPrice: charge.listTotalPrice !== void 0 ? charge.listTotalPrice : totalAt(listUnitPrice),
|
|
12
|
+
standardUnitPrice,
|
|
13
|
+
standardTotalPrice: charge.standardTotalPrice !== void 0 ? charge.standardTotalPrice : totalAt(standardUnitPrice),
|
|
14
|
+
...charge.adjustments ? { adjustments: adjustments.map((entry) => ({ ...entry, origin: entry.origin ?? "rule" })) } : {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function withoutChargeWaterfall(charge) {
|
|
18
|
+
const { listUnitPrice, listTotalPrice, standardUnitPrice, standardTotalPrice, ...publicCharge } = charge;
|
|
19
|
+
return publicCharge;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
withChargeWaterfall,
|
|
23
|
+
withoutChargeWaterfall
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=chargeWaterfall.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ResolvedCharge } from '../types'\n\n/** Normalize legacy snapshots without changing their offer, rules or tier breakdown. */\nexport function withChargeWaterfall(charge: ResolvedCharge): ResolvedCharge {\n const adjustments = charge.adjustments ?? []\n const rules = adjustments.filter((entry) => entry.origin !== 'manual')\n const manual = adjustments.find((entry) => entry.origin === 'manual')\n const listUnitPrice = charge.listUnitPrice ?? rules[0]?.unitPriceBefore ?? manual?.unitPriceBefore ?? charge.unitPrice\n const standardUnitPrice = charge.standardUnitPrice ?? rules.at(-1)?.unitPriceAfter ?? manual?.unitPriceBefore ?? charge.unitPrice\n // Keep the exact stored total when no recalculation is needed (tier averages\n // may not multiply back to the byte-identical stored total).\n const totalAt = (unit: number) => charge.totalPrice === null ? null\n : unit === charge.unitPrice ? charge.totalPrice : unit * (charge.quantity ?? 1)\n return {\n ...charge,\n listUnitPrice,\n listTotalPrice: charge.listTotalPrice !== undefined ? charge.listTotalPrice : totalAt(listUnitPrice),\n standardUnitPrice,\n standardTotalPrice: charge.standardTotalPrice !== undefined ? charge.standardTotalPrice : totalAt(standardUnitPrice),\n ...(charge.adjustments ? { adjustments: adjustments.map((entry) => ({ ...entry, origin: entry.origin ?? 'rule' })) } : {}),\n }\n}\n\n/** Keep the public calculator's default response compatible and free of waterfall data. */\nexport function withoutChargeWaterfall(charge: ResolvedCharge): ResolvedCharge {\n const { listUnitPrice, listTotalPrice, standardUnitPrice, standardTotalPrice, ...publicCharge } = charge\n return publicCharge\n}\n"],
|
|
5
|
+
"mappings": "AAGO,SAAS,oBAAoB,QAAwC;AAC1E,QAAM,cAAc,OAAO,eAAe,CAAC;AAC3C,QAAM,QAAQ,YAAY,OAAO,CAAC,UAAU,MAAM,WAAW,QAAQ;AACrE,QAAM,SAAS,YAAY,KAAK,CAAC,UAAU,MAAM,WAAW,QAAQ;AACpE,QAAM,gBAAgB,OAAO,iBAAiB,MAAM,CAAC,GAAG,mBAAmB,QAAQ,mBAAmB,OAAO;AAC7G,QAAM,oBAAoB,OAAO,qBAAqB,MAAM,GAAG,EAAE,GAAG,kBAAkB,QAAQ,mBAAmB,OAAO;AAGxH,QAAM,UAAU,CAAC,SAAiB,OAAO,eAAe,OAAO,OAC3D,SAAS,OAAO,YAAY,OAAO,aAAa,QAAQ,OAAO,YAAY;AAC/E,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,gBAAgB,OAAO,mBAAmB,SAAY,OAAO,iBAAiB,QAAQ,aAAa;AAAA,IACnG;AAAA,IACA,oBAAoB,OAAO,uBAAuB,SAAY,OAAO,qBAAqB,QAAQ,iBAAiB;AAAA,IACnH,GAAI,OAAO,cAAc,EAAE,aAAa,YAAY,IAAI,CAAC,WAAW,EAAE,GAAG,OAAO,QAAQ,MAAM,UAAU,OAAO,EAAE,EAAE,IAAI,CAAC;AAAA,EAC1H;AACF;AAGO,SAAS,uBAAuB,QAAwC;AAC7E,QAAM,EAAE,eAAe,gBAAgB,mBAAmB,oBAAoB,GAAG,aAAa,IAAI;AAClG,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { CpqQuotePriceAdjustment } from "../../data/entities.js";
|
|
3
|
+
import { resolvedPriceAdjustmentSchema } from "../../data/price-adjustments.js";
|
|
4
|
+
async function loadQuoteAdjustments(em, quoteConfigurationId, scope) {
|
|
5
|
+
const result = [];
|
|
6
|
+
let after;
|
|
7
|
+
for (; ; ) {
|
|
8
|
+
const rows = await findWithDecryption(em, CpqQuotePriceAdjustment, {
|
|
9
|
+
quoteConfigurationId,
|
|
10
|
+
...scope,
|
|
11
|
+
deletedAt: null,
|
|
12
|
+
isActive: true,
|
|
13
|
+
...after ? { id: { $gt: after } } : {}
|
|
14
|
+
}, { limit: 500, orderBy: { id: "asc" } }, scope);
|
|
15
|
+
for (const row of rows) result.push(resolvedPriceAdjustmentSchema.parse({
|
|
16
|
+
id: row.id,
|
|
17
|
+
level: row.level,
|
|
18
|
+
quoteLineId: row.quoteLineId,
|
|
19
|
+
chargeCode: row.chargeCode,
|
|
20
|
+
chargeTypeFilter: row.chargeTypeFilter,
|
|
21
|
+
adjustmentType: row.adjustmentType,
|
|
22
|
+
value: Number(row.value),
|
|
23
|
+
currencyCode: row.currencyCode,
|
|
24
|
+
reasonCode: row.reasonCode,
|
|
25
|
+
reasonNote: row.reasonNote,
|
|
26
|
+
fromMonth: row.fromMonth,
|
|
27
|
+
toMonth: row.toMonth,
|
|
28
|
+
windowKind: row.windowKind,
|
|
29
|
+
createdByUserId: row.createdByUserId
|
|
30
|
+
}));
|
|
31
|
+
if (rows.length < 500) return result;
|
|
32
|
+
after = rows[rows.length - 1].id;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
loadQuoteAdjustments
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=loadQuoteAdjustments.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CpqQuotePriceAdjustment } from '../../data/entities'\nimport { resolvedPriceAdjustmentSchema, type PriceAdjustment } from '../../data/price-adjustments'\nimport type { TenantScope } from '../types'\n\n/** One aggregate load per recalculation; keyset batches avoid an unbounded query. */\nexport async function loadQuoteAdjustments(em: EntityManager, quoteConfigurationId: string, scope: TenantScope): Promise<PriceAdjustment[]> {\n const result: PriceAdjustment[] = []\n let after: string | undefined\n for (;;) {\n const rows = await findWithDecryption(em, CpqQuotePriceAdjustment, {\n quoteConfigurationId, ...scope, deletedAt: null, isActive: true,\n ...(after ? { id: { $gt: after } } : {}),\n }, { limit: 500, orderBy: { id: 'asc' } }, scope)\n for (const row of rows) result.push(resolvedPriceAdjustmentSchema.parse({\n id: row.id, level: row.level, quoteLineId: row.quoteLineId, chargeCode: row.chargeCode,\n chargeTypeFilter: row.chargeTypeFilter, adjustmentType: row.adjustmentType, value: Number(row.value),\n currencyCode: row.currencyCode, reasonCode: row.reasonCode, reasonNote: row.reasonNote,\n fromMonth: row.fromMonth, toMonth: row.toMonth, windowKind: row.windowKind, createdByUserId: row.createdByUserId,\n }))\n if (rows.length < 500) return result\n after = rows[rows.length - 1].id\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,qCAA2D;AAIpE,eAAsB,qBAAqB,IAAmB,sBAA8B,OAAgD;AAC1I,QAAM,SAA4B,CAAC;AACnC,MAAI;AACJ,aAAS;AACP,UAAM,OAAO,MAAM,mBAAmB,IAAI,yBAAyB;AAAA,MACjE;AAAA,MAAsB,GAAG;AAAA,MAAO,WAAW;AAAA,MAAM,UAAU;AAAA,MAC3D,GAAI,QAAQ,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,IAAI,CAAC;AAAA,IACxC,GAAG,EAAE,OAAO,KAAK,SAAS,EAAE,IAAI,MAAM,EAAE,GAAG,KAAK;AAChD,eAAW,OAAO,KAAM,QAAO,KAAK,8BAA8B,MAAM;AAAA,MACtE,IAAI,IAAI;AAAA,MAAI,OAAO,IAAI;AAAA,MAAO,aAAa,IAAI;AAAA,MAAa,YAAY,IAAI;AAAA,MAC5E,kBAAkB,IAAI;AAAA,MAAkB,gBAAgB,IAAI;AAAA,MAAgB,OAAO,OAAO,IAAI,KAAK;AAAA,MACnG,cAAc,IAAI;AAAA,MAAc,YAAY,IAAI;AAAA,MAAY,YAAY,IAAI;AAAA,MAC5E,WAAW,IAAI;AAAA,MAAW,SAAS,IAAI;AAAA,MAAS,YAAY,IAAI;AAAA,MAAY,iBAAiB,IAAI;AAAA,IACnG,CAAC,CAAC;AACF,QAAI,KAAK,SAAS,IAAK,QAAO;AAC9B,YAAQ,KAAK,KAAK,SAAS,CAAC,EAAE;AAAA,EAChC;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { withChargeWaterfall } from "./chargeWaterfall.js";
|
|
3
|
+
function canonical(value) {
|
|
4
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
5
|
+
if (value !== null && typeof value === "object") {
|
|
6
|
+
const object = value;
|
|
7
|
+
return `{${Object.keys(object).sort().filter((key) => object[key] !== void 0).map((key) => `${JSON.stringify(key)}:${canonical(object[key])}`).join(",")}}`;
|
|
8
|
+
}
|
|
9
|
+
return JSON.stringify(value ?? null);
|
|
10
|
+
}
|
|
11
|
+
function depth(before, after) {
|
|
12
|
+
return before === 0 ? null : 1 - after / before;
|
|
13
|
+
}
|
|
14
|
+
function computeWaterfallSummary(lines, adjustments, currencyCode) {
|
|
15
|
+
let listNrcTotal = 0, listMrcTotal = 0, standardNrcTotal = 0, standardMrcTotal = 0;
|
|
16
|
+
let ruleDiscountTotal = 0, manualDiscountTotal = 0;
|
|
17
|
+
const dealValue = { list: 0, standard: 0, offer: 0 };
|
|
18
|
+
const lineDepths = [];
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
let standardDeal = 0, offerDeal = 0;
|
|
21
|
+
for (const input of line.charges) {
|
|
22
|
+
const charge = withChargeWaterfall(input);
|
|
23
|
+
if (charge.chargeType === "usage") continue;
|
|
24
|
+
const list = (charge.listTotalPrice ?? 0) * line.quantity;
|
|
25
|
+
const standard = (charge.standardTotalPrice ?? 0) * line.quantity;
|
|
26
|
+
const offer = (charge.totalPrice ?? 0) * line.quantity;
|
|
27
|
+
if (charge.chargeType === "nrc") {
|
|
28
|
+
listNrcTotal += list;
|
|
29
|
+
standardNrcTotal += standard;
|
|
30
|
+
} else {
|
|
31
|
+
listMrcTotal += list;
|
|
32
|
+
standardMrcTotal += standard;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of charge.adjustments ?? []) {
|
|
35
|
+
const discount = Math.max(0, -entry.delta) * (charge.quantity ?? 1) * line.quantity;
|
|
36
|
+
if (entry.origin === "manual") manualDiscountTotal += discount;
|
|
37
|
+
else ruleDiscountTotal += discount;
|
|
38
|
+
}
|
|
39
|
+
if (line.includeInDeal === false) continue;
|
|
40
|
+
const months = charge.chargeType === "nrc" ? 1 : line.termMonths ?? 12;
|
|
41
|
+
dealValue.list += list * months;
|
|
42
|
+
dealValue.standard += standard * months;
|
|
43
|
+
dealValue.offer += offer * months;
|
|
44
|
+
standardDeal += standard * months;
|
|
45
|
+
offerDeal += offer * months;
|
|
46
|
+
}
|
|
47
|
+
const manualDepth = depth(standardDeal, offerDeal);
|
|
48
|
+
if (manualDepth !== null) lineDepths.push(manualDepth);
|
|
49
|
+
}
|
|
50
|
+
const pricedLines = lines.map((line) => ({
|
|
51
|
+
lineId: line.lineId,
|
|
52
|
+
quantity: line.quantity,
|
|
53
|
+
termMonths: line.termMonths ?? 12,
|
|
54
|
+
includeInDeal: line.includeInDeal !== false,
|
|
55
|
+
charges: line.charges.map((input) => {
|
|
56
|
+
const charge = withChargeWaterfall(input);
|
|
57
|
+
return {
|
|
58
|
+
chargeCode: charge.chargeCode,
|
|
59
|
+
chargeType: charge.chargeType,
|
|
60
|
+
currencyCode: charge.currencyCode,
|
|
61
|
+
listUnitPrice: charge.listUnitPrice,
|
|
62
|
+
listTotalPrice: charge.listTotalPrice,
|
|
63
|
+
standardUnitPrice: charge.standardUnitPrice,
|
|
64
|
+
standardTotalPrice: charge.standardTotalPrice,
|
|
65
|
+
unitPrice: charge.unitPrice,
|
|
66
|
+
totalPrice: charge.totalPrice
|
|
67
|
+
};
|
|
68
|
+
}).sort((a, b) => a.chargeCode.localeCompare(b.chargeCode) || canonical(a).localeCompare(canonical(b)))
|
|
69
|
+
})).sort((a, b) => a.lineId.localeCompare(b.lineId));
|
|
70
|
+
const pricedAdjustments = adjustments.map((row) => ({
|
|
71
|
+
id: row.id,
|
|
72
|
+
level: row.level,
|
|
73
|
+
quoteLineId: row.quoteLineId,
|
|
74
|
+
chargeCode: row.chargeCode ?? null,
|
|
75
|
+
chargeTypeFilter: row.chargeTypeFilter ?? null,
|
|
76
|
+
adjustmentType: row.adjustmentType,
|
|
77
|
+
value: row.value,
|
|
78
|
+
currencyCode: row.currencyCode ?? null,
|
|
79
|
+
windowKind: row.windowKind ?? "relative",
|
|
80
|
+
fromMonth: row.fromMonth ?? 1,
|
|
81
|
+
toMonth: row.toMonth ?? null
|
|
82
|
+
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
83
|
+
return {
|
|
84
|
+
listNrcTotal,
|
|
85
|
+
listMrcTotal,
|
|
86
|
+
standardNrcTotal,
|
|
87
|
+
standardMrcTotal,
|
|
88
|
+
ruleDiscountTotal,
|
|
89
|
+
manualDiscountTotal,
|
|
90
|
+
dealValue,
|
|
91
|
+
discountDepth: {
|
|
92
|
+
rule: depth(dealValue.list, dealValue.standard),
|
|
93
|
+
manual: depth(dealValue.standard, dealValue.offer),
|
|
94
|
+
total: depth(dealValue.list, dealValue.offer),
|
|
95
|
+
maxLineManual: lineDepths.length ? Math.max(...lineDepths) : null
|
|
96
|
+
},
|
|
97
|
+
hasTimeBasedAdjustments: false,
|
|
98
|
+
snapshotHash: createHash("sha256").update(canonical({ currencyCode, lines: pricedLines, adjustments: pricedAdjustments })).digest("hex")
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
computeWaterfallSummary
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=pricingSnapshot.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { PriceAdjustment } from '../../data/price-adjustments'\nimport type { PricingWaterfallSummary, ResolvedCharge } from '../types'\nimport { withChargeWaterfall } from './chargeWaterfall'\n\nexport interface SnapshotLine {\n lineId: string\n quantity: number\n termMonths?: number | null\n /** ARC mirrors are excluded from deal value until their negotiated re-rate is supported. */\n includeInDeal?: boolean\n charges: ResolvedCharge[]\n}\n\nfunction canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`\n if (value !== null && typeof value === 'object') {\n const object = value as Record<string, unknown>\n return `{${Object.keys(object).sort().filter((key) => object[key] !== undefined).map((key) => `${JSON.stringify(key)}:${canonical(object[key])}`).join(',')}}`\n }\n return JSON.stringify(value ?? null)\n}\n\n/** Signed fractions: a rule surcharge yields a negative discount depth. */\nfunction depth(before: number, after: number): number | null {\n return before === 0 ? null : 1 - after / before\n}\n\nexport function computeWaterfallSummary(\n lines: SnapshotLine[],\n adjustments: PriceAdjustment[],\n currencyCode: string,\n): PricingWaterfallSummary {\n let listNrcTotal = 0, listMrcTotal = 0, standardNrcTotal = 0, standardMrcTotal = 0\n let ruleDiscountTotal = 0, manualDiscountTotal = 0\n const dealValue = { list: 0, standard: 0, offer: 0 }\n const lineDepths: number[] = []\n for (const line of lines) {\n let standardDeal = 0, offerDeal = 0\n for (const input of line.charges) {\n const charge = withChargeWaterfall(input)\n if (charge.chargeType === 'usage') continue\n const list = (charge.listTotalPrice ?? 0) * line.quantity\n const standard = (charge.standardTotalPrice ?? 0) * line.quantity\n const offer = (charge.totalPrice ?? 0) * line.quantity\n if (charge.chargeType === 'nrc') { listNrcTotal += list; standardNrcTotal += standard }\n else { listMrcTotal += list; standardMrcTotal += standard }\n for (const entry of charge.adjustments ?? []) {\n const discount = Math.max(0, -entry.delta) * (charge.quantity ?? 1) * line.quantity\n if (entry.origin === 'manual') manualDiscountTotal += discount\n else ruleDiscountTotal += discount\n }\n if (line.includeInDeal === false) continue\n const months = charge.chargeType === 'nrc' ? 1 : line.termMonths ?? 12\n dealValue.list += list * months\n dealValue.standard += standard * months\n dealValue.offer += offer * months\n standardDeal += standard * months; offerDeal += offer * months\n }\n const manualDepth = depth(standardDeal, offerDeal)\n if (manualDepth !== null) lineDepths.push(manualDepth)\n }\n const pricedLines = lines.map((line) => ({\n lineId: line.lineId, quantity: line.quantity, termMonths: line.termMonths ?? 12, includeInDeal: line.includeInDeal !== false,\n charges: line.charges.map((input) => {\n const charge = withChargeWaterfall(input)\n return { chargeCode: charge.chargeCode, chargeType: charge.chargeType, currencyCode: charge.currencyCode,\n listUnitPrice: charge.listUnitPrice, listTotalPrice: charge.listTotalPrice,\n standardUnitPrice: charge.standardUnitPrice, standardTotalPrice: charge.standardTotalPrice,\n unitPrice: charge.unitPrice, totalPrice: charge.totalPrice }\n }).sort((a, b) => a.chargeCode.localeCompare(b.chargeCode) || canonical(a).localeCompare(canonical(b))),\n })).sort((a, b) => a.lineId.localeCompare(b.lineId))\n const pricedAdjustments = adjustments.map((row) => ({\n id: row.id, level: row.level, quoteLineId: row.quoteLineId, chargeCode: row.chargeCode ?? null,\n chargeTypeFilter: row.chargeTypeFilter ?? null, adjustmentType: row.adjustmentType, value: row.value,\n currencyCode: row.currencyCode ?? null, windowKind: row.windowKind ?? 'relative', fromMonth: row.fromMonth ?? 1, toMonth: row.toMonth ?? null,\n })).sort((a, b) => a.id.localeCompare(b.id))\n return {\n listNrcTotal, listMrcTotal, standardNrcTotal, standardMrcTotal,\n ruleDiscountTotal, manualDiscountTotal, dealValue,\n discountDepth: { rule: depth(dealValue.list, dealValue.standard), manual: depth(dealValue.standard, dealValue.offer),\n total: depth(dealValue.list, dealValue.offer), maxLineManual: lineDepths.length ? Math.max(...lineDepths) : null },\n hasTimeBasedAdjustments: false,\n snapshotHash: createHash('sha256').update(canonical({ currencyCode, lines: pricedLines, adjustments: pricedAdjustments })).digest('hex'),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|