@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.
Files changed (233) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/MIGRATION.md +74 -0
  3. package/dist/modules/cpq/acl.js +1 -0
  4. package/dist/modules/cpq/acl.js.map +2 -2
  5. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js +45 -0
  6. package/dist/modules/cpq/api/_helpers/adjustmentCommandRoute.js.map +7 -0
  7. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js +23 -0
  8. package/dist/modules/cpq/api/_helpers/quoteMutationGuard.js.map +7 -0
  9. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js +19 -0
  10. package/dist/modules/cpq/api/_helpers/quoteRouteOpenApi.js.map +7 -0
  11. package/dist/modules/cpq/api/orders/[orderId]/route.js +17 -1
  12. package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
  13. package/dist/modules/cpq/api/orders/route.js +47 -2
  14. package/dist/modules/cpq/api/orders/route.js.map +2 -2
  15. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js +54 -0
  16. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.js.map +7 -0
  17. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +42 -2
  18. package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
  19. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -2
  20. package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +2 -2
  21. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js +47 -0
  22. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.js.map +7 -0
  23. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js +30 -0
  24. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.js.map +7 -0
  25. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js +47 -0
  26. package/dist/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.js.map +7 -0
  27. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +32 -2
  28. package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
  29. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +33 -2
  30. package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +2 -2
  31. package/dist/modules/cpq/api/quotes/price/route.js +43 -3
  32. package/dist/modules/cpq/api/quotes/price/route.js.map +2 -2
  33. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +1 -1
  34. package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +1 -1
  35. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +22 -8
  36. package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +2 -2
  37. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +89 -232
  38. package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +2 -2
  39. package/dist/modules/cpq/backend/cpq/orders/page.meta.js +1 -1
  40. package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +1 -1
  41. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js +205 -0
  42. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.js.map +7 -0
  43. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js +79 -0
  44. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.js.map +7 -0
  45. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js +53 -0
  46. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.js.map +7 -0
  47. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js +107 -0
  48. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.js.map +7 -0
  49. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js +130 -0
  50. package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.js.map +7 -0
  51. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +144 -221
  52. package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +3 -3
  53. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +1 -1
  54. package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +1 -1
  55. package/dist/modules/cpq/ce.js +8 -0
  56. package/dist/modules/cpq/ce.js.map +2 -2
  57. package/dist/modules/cpq/cli.js +34 -1
  58. package/dist/modules/cpq/cli.js.map +2 -2
  59. package/dist/modules/cpq/commands/price-adjustments.js +107 -0
  60. package/dist/modules/cpq/commands/price-adjustments.js.map +7 -0
  61. package/dist/modules/cpq/components/ArcLineDiff.js +53 -0
  62. package/dist/modules/cpq/components/ArcLineDiff.js.map +7 -0
  63. package/dist/modules/cpq/components/ChargeBreakdownTable.js +154 -0
  64. package/dist/modules/cpq/components/ChargeBreakdownTable.js.map +7 -0
  65. package/dist/modules/cpq/components/PriceWaterfall.js +66 -0
  66. package/dist/modules/cpq/components/PriceWaterfall.js.map +7 -0
  67. package/dist/modules/cpq/components/PricingSummaryCard.js +117 -0
  68. package/dist/modules/cpq/components/PricingSummaryCard.js.map +7 -0
  69. package/dist/modules/cpq/components/useGrantedFeatures.js +27 -0
  70. package/dist/modules/cpq/components/useGrantedFeatures.js.map +7 -0
  71. package/dist/modules/cpq/data/entities.js +97 -0
  72. package/dist/modules/cpq/data/entities.js.map +2 -2
  73. package/dist/modules/cpq/data/price-adjustments.js +70 -0
  74. package/dist/modules/cpq/data/price-adjustments.js.map +7 -0
  75. package/dist/modules/cpq/data/validators.js +4 -0
  76. package/dist/modules/cpq/data/validators.js.map +2 -2
  77. package/dist/modules/cpq/di.js +6 -0
  78. package/dist/modules/cpq/di.js.map +2 -2
  79. package/dist/modules/cpq/events.js +47 -0
  80. package/dist/modules/cpq/events.js.map +2 -2
  81. package/dist/modules/cpq/index.js +1 -1
  82. package/dist/modules/cpq/index.js.map +2 -2
  83. package/dist/modules/cpq/lib/auditActor.js +10 -0
  84. package/dist/modules/cpq/lib/auditActor.js.map +7 -0
  85. package/dist/modules/cpq/lib/formatMoney.js +7 -0
  86. package/dist/modules/cpq/lib/formatMoney.js.map +7 -0
  87. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js +60 -0
  88. package/dist/modules/cpq/lib/priceAdjustmentDefaults.js.map +7 -0
  89. package/dist/modules/cpq/lib/priceAdjustmentView.js +169 -0
  90. package/dist/modules/cpq/lib/priceAdjustmentView.js.map +7 -0
  91. package/dist/modules/cpq/lib/quotePermissions.js +13 -0
  92. package/dist/modules/cpq/lib/quotePermissions.js.map +7 -0
  93. package/dist/modules/cpq/lib/quoteSummaryView.js +28 -0
  94. package/dist/modules/cpq/lib/quoteSummaryView.js.map +7 -0
  95. package/dist/modules/cpq/migrations/Migration20261110213052.js +22 -0
  96. package/dist/modules/cpq/migrations/Migration20261110213052.js.map +7 -0
  97. package/dist/modules/cpq/services/arcLineRemoval.js +53 -0
  98. package/dist/modules/cpq/services/arcLineRemoval.js.map +7 -0
  99. package/dist/modules/cpq/services/arcPriceFreeze.js +79 -0
  100. package/dist/modules/cpq/services/arcPriceFreeze.js.map +7 -0
  101. package/dist/modules/cpq/services/cpqOrderService.js +25 -28
  102. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  103. package/dist/modules/cpq/services/cpqPricingService.js +7 -0
  104. package/dist/modules/cpq/services/cpqPricingService.js.map +2 -2
  105. package/dist/modules/cpq/services/cpqQuotingService.js +330 -109
  106. package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
  107. package/dist/modules/cpq/services/orderPricingSummary.js +26 -0
  108. package/dist/modules/cpq/services/orderPricingSummary.js.map +7 -0
  109. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js +264 -0
  110. package/dist/modules/cpq/services/priceAdjustments/adjustmentService.js.map +7 -0
  111. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js +25 -0
  112. package/dist/modules/cpq/services/priceAdjustments/chargeWaterfall.js.map +7 -0
  113. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js +38 -0
  114. package/dist/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.js.map +7 -0
  115. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js +104 -0
  116. package/dist/modules/cpq/services/priceAdjustments/pricingSnapshot.js.map +7 -0
  117. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js +102 -0
  118. package/dist/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.js.map +7 -0
  119. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js +52 -0
  120. package/dist/modules/cpq/services/priceAdjustments/quotePricingEvents.js.map +7 -0
  121. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js +37 -0
  122. package/dist/modules/cpq/services/priceAdjustments/quotePricingWrite.js.map +7 -0
  123. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js +107 -0
  124. package/dist/modules/cpq/services/priceAdjustments/resolveManualAdjustments.js.map +7 -0
  125. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js +30 -0
  126. package/dist/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.js.map +7 -0
  127. package/dist/modules/cpq/services/types.js.map +2 -2
  128. package/dist/modules/cpq/setup.js +6 -0
  129. package/dist/modules/cpq/setup.js.map +2 -2
  130. package/package.json +1 -1
  131. package/src/modules/cpq/__integration__/TC-CPQ-ARC-012.spec.ts +134 -0
  132. package/src/modules/cpq/acl.ts +1 -0
  133. package/src/modules/cpq/api/__tests__/adjustmentRoutes.test.ts +79 -0
  134. package/src/modules/cpq/api/__tests__/quoteWriteGuards.test.ts +100 -0
  135. package/src/modules/cpq/api/_helpers/__tests__/quoteMutationGuard.test.ts +48 -0
  136. package/src/modules/cpq/api/_helpers/adjustmentCommandRoute.ts +45 -0
  137. package/src/modules/cpq/api/_helpers/quoteMutationGuard.ts +38 -0
  138. package/src/modules/cpq/api/_helpers/quoteRouteOpenApi.ts +19 -0
  139. package/src/modules/cpq/api/orders/[orderId]/route.ts +18 -0
  140. package/src/modules/cpq/api/orders/route.ts +51 -1
  141. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/restore/route.ts +56 -0
  142. package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +48 -1
  143. package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +31 -1
  144. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/[adjustmentId]/route.ts +39 -0
  145. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/preview/route.ts +24 -0
  146. package/src/modules/cpq/api/quotes/[quoteId]/price-adjustments/route.ts +40 -0
  147. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/__tests__/route.test.ts +95 -0
  148. package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +40 -1
  149. package/src/modules/cpq/api/quotes/[quoteId]/status/__tests__/route.test.ts +74 -0
  150. package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +37 -1
  151. package/src/modules/cpq/api/quotes/price/__tests__/route.test.ts +37 -0
  152. package/src/modules/cpq/api/quotes/price/route.ts +28 -2
  153. package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +1 -1
  154. package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +32 -10
  155. package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +60 -269
  156. package/src/modules/cpq/backend/cpq/orders/page.meta.ts +1 -1
  157. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentDialog.tsx +260 -0
  158. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/PriceAdjustmentsPanel.tsx +124 -0
  159. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/__tests__/PriceAdjustmentsPanel.test.tsx +76 -0
  160. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/arcLineRemoval.tsx +76 -0
  161. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/priceAdjustmentContext.tsx +133 -0
  162. package/src/modules/cpq/backend/cpq/quotes/[id]/_components/usePriceAdjustments.tsx +165 -0
  163. package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +174 -263
  164. package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +1 -1
  165. package/src/modules/cpq/ce.ts +8 -0
  166. package/src/modules/cpq/cli.ts +48 -1
  167. package/src/modules/cpq/commands/__tests__/price-adjustments.test.ts +54 -0
  168. package/src/modules/cpq/commands/price-adjustments.ts +90 -0
  169. package/src/modules/cpq/components/ArcLineDiff.tsx +87 -0
  170. package/src/modules/cpq/components/ChargeBreakdownTable.tsx +201 -0
  171. package/src/modules/cpq/components/PriceWaterfall.tsx +94 -0
  172. package/src/modules/cpq/components/PricingSummaryCard.tsx +128 -0
  173. package/src/modules/cpq/components/__tests__/PriceWaterfall.test.tsx +38 -0
  174. package/src/modules/cpq/components/useGrantedFeatures.ts +35 -0
  175. package/src/modules/cpq/data/__tests__/price-adjustment-migration.test.ts +11 -0
  176. package/src/modules/cpq/data/__tests__/price-adjustments.test.ts +26 -0
  177. package/src/modules/cpq/data/entities.ts +83 -0
  178. package/src/modules/cpq/data/price-adjustments.ts +67 -0
  179. package/src/modules/cpq/data/validators.ts +3 -0
  180. package/src/modules/cpq/di.ts +6 -0
  181. package/src/modules/cpq/events.ts +53 -2
  182. package/src/modules/cpq/index.ts +1 -1
  183. package/src/modules/cpq/lib/__tests__/auditActor.test.ts +12 -0
  184. package/src/modules/cpq/lib/__tests__/priceAdjustmentDefaults.test.ts +37 -0
  185. package/src/modules/cpq/lib/__tests__/priceAdjustmentView.test.ts +152 -0
  186. package/src/modules/cpq/lib/__tests__/quotePermissions.test.ts +30 -0
  187. package/src/modules/cpq/lib/__tests__/quoteSummaryView.test.ts +56 -0
  188. package/src/modules/cpq/lib/auditActor.ts +13 -0
  189. package/src/modules/cpq/lib/formatMoney.ts +4 -0
  190. package/src/modules/cpq/lib/priceAdjustmentDefaults.ts +36 -0
  191. package/src/modules/cpq/lib/priceAdjustmentView.ts +259 -0
  192. package/src/modules/cpq/lib/quotePermissions.ts +22 -0
  193. package/src/modules/cpq/lib/quoteSummaryView.ts +51 -0
  194. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +3298 -2423
  195. package/src/modules/cpq/migrations/Migration20261110213052.ts +22 -0
  196. package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +9 -2
  197. package/src/modules/cpq/services/__tests__/adjustmentService.test.ts +183 -0
  198. package/src/modules/cpq/services/__tests__/arcLineRemoval.test.ts +109 -0
  199. package/src/modules/cpq/services/__tests__/arcPriceFreeze.test.ts +141 -0
  200. package/src/modules/cpq/services/__tests__/chargeWaterfall.test.ts +28 -0
  201. package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +35 -2
  202. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +57 -0
  203. package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +237 -0
  204. package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +58 -3
  205. package/src/modules/cpq/services/__tests__/orderPricingSummary.test.ts +50 -0
  206. package/src/modules/cpq/services/__tests__/pricingSnapshot.test.ts +62 -0
  207. package/src/modules/cpq/services/__tests__/quoteAdjustmentLifecycle.test.ts +35 -0
  208. package/src/modules/cpq/services/__tests__/quoteAdjustmentPricing.test.ts +65 -0
  209. package/src/modules/cpq/services/__tests__/quoteCloneAdjustments.test.ts +45 -0
  210. package/src/modules/cpq/services/__tests__/quoteDetachTargetAdjustments.test.ts +54 -0
  211. package/src/modules/cpq/services/__tests__/quoteEventsService.test.ts +72 -0
  212. package/src/modules/cpq/services/__tests__/quotePricingEvents.test.ts +126 -0
  213. package/src/modules/cpq/services/__tests__/quotePricingIsolation.test.ts +52 -0
  214. package/src/modules/cpq/services/__tests__/quotePricingWrite.test.ts +74 -0
  215. package/src/modules/cpq/services/__tests__/resolveManualAdjustments.test.ts +90 -0
  216. package/src/modules/cpq/services/__tests__/storedAdjustmentFailure.test.ts +9 -0
  217. package/src/modules/cpq/services/arcLineRemoval.ts +87 -0
  218. package/src/modules/cpq/services/arcPriceFreeze.ts +165 -0
  219. package/src/modules/cpq/services/cpqOrderService.ts +22 -28
  220. package/src/modules/cpq/services/cpqPricingService.ts +8 -0
  221. package/src/modules/cpq/services/cpqQuotingService.ts +374 -125
  222. package/src/modules/cpq/services/orderPricingSummary.ts +47 -0
  223. package/src/modules/cpq/services/priceAdjustments/adjustmentService.ts +249 -0
  224. package/src/modules/cpq/services/priceAdjustments/chargeWaterfall.ts +28 -0
  225. package/src/modules/cpq/services/priceAdjustments/loadQuoteAdjustments.ts +25 -0
  226. package/src/modules/cpq/services/priceAdjustments/pricingSnapshot.ts +86 -0
  227. package/src/modules/cpq/services/priceAdjustments/quoteAdjustmentLifecycle.ts +68 -0
  228. package/src/modules/cpq/services/priceAdjustments/quotePricingEvents.ts +94 -0
  229. package/src/modules/cpq/services/priceAdjustments/quotePricingWrite.ts +51 -0
  230. package/src/modules/cpq/services/priceAdjustments/resolveManualAdjustments.ts +116 -0
  231. package/src/modules/cpq/services/priceAdjustments/storedAdjustmentFailure.ts +31 -0
  232. package/src/modules/cpq/services/types.ts +34 -1
  233. package/src/modules/cpq/setup.ts +7 -1
@@ -0,0 +1,116 @@
1
+ import type { AdjustmentError, PriceAdjustment } from '../../data/price-adjustments'
2
+ import type { ResolvedCharge } from '../types'
3
+ import { withChargeWaterfall } from './chargeWaterfall'
4
+
5
+ export interface AdjustmentPricingLine {
6
+ lineId: string
7
+ parentLineId: string | null
8
+ enginePriced: boolean
9
+ mirrored: boolean
10
+ charges: ResolvedCharge[]
11
+ }
12
+
13
+ /** Most-specific valid manual adjustment wins; each is evaluated against standard. */
14
+ export function resolveManualAdjustments(
15
+ lines: AdjustmentPricingLine[],
16
+ adjustments: PriceAdjustment[],
17
+ currencyCode: string,
18
+ ): { lines: AdjustmentPricingLine[]; errors: AdjustmentError[] } {
19
+ const errors: AdjustmentError[] = []
20
+ const byId = new Map(lines.map((line) => [line.lineId, line]))
21
+ const eligible = (line: AdjustmentPricingLine) => line.enginePriced && !line.mirrored
22
+ const output = lines.map((line) => ({
23
+ ...line,
24
+ charges: line.charges.map((charge) => {
25
+ const normalized = withChargeWaterfall(charge)
26
+ return { ...normalized, unitPrice: eligible(line) ? normalized.standardUnitPrice! : normalized.unitPrice,
27
+ totalPrice: eligible(line) ? normalized.standardTotalPrice! : normalized.totalPrice,
28
+ adjustments: (eligible(line) ? normalized.adjustments?.filter((entry) => entry.origin !== 'manual') : normalized.adjustments) ?? [], adjustmentErrors: [] as AdjustmentError[] }
29
+ }),
30
+ }))
31
+ const outById = new Map(output.map((line) => [line.lineId, line]))
32
+ const addError = (row: PriceAdjustment, code: AdjustmentError['code'], chargeCode?: string, lineId = row.quoteLineId) => {
33
+ if (errors.some((entry) => entry.adjustmentId === row.id && entry.quoteLineId === lineId && entry.chargeCode === chargeCode && entry.code === code)) return
34
+ const error: AdjustmentError = { adjustmentId: row.id, quoteLineId: lineId, ...(chargeCode ? { chargeCode } : {}), code }
35
+ errors.push(error)
36
+ for (const charge of outById.get(lineId)?.charges ?? []) {
37
+ if (!chargeCode || charge.chargeCode === chargeCode) charge.adjustmentErrors.push(error)
38
+ }
39
+ }
40
+ // Missing/ambiguous targets are invalid even if another adjustment could win.
41
+ const invalid = new Set<string>()
42
+ for (const row of adjustments) {
43
+ const target = byId.get(row.quoteLineId)
44
+ let code: AdjustmentError['code'] | undefined
45
+ if (!target) code = 'ADJUSTMENT_TARGET_MISSING'
46
+ else if (!eligible(target)) code = 'LINE_NOT_ELIGIBLE'
47
+ else if (row.level === 'charge') {
48
+ const matches = target.charges.filter((charge) => charge.chargeCode === row.chargeCode)
49
+ if (!matches.length) code = 'ADJUSTMENT_TARGET_MISSING'
50
+ else if (matches.length > 1) code = 'TARGET_AMBIGUOUS'
51
+ }
52
+ if (code) { addError(row, code, row.chargeCode ?? undefined); invalid.add(row.id) }
53
+ }
54
+ for (const line of output) {
55
+ if (!eligible(line)) continue
56
+ const lineage: string[] = []
57
+ const visited = new Set<string>([line.lineId])
58
+ let parent = line.parentLineId
59
+ while (parent && !visited.has(parent)) {
60
+ visited.add(parent); lineage.push(parent); parent = byId.get(parent)?.parentLineId ?? null
61
+ }
62
+ // Declared before use: `.sort()` below calls it eagerly (a `const` after it would hit the TDZ).
63
+ const rank = (row: PriceAdjustment) =>
64
+ row.level === 'charge' ? 0 : row.quoteLineId === line.lineId ? 1 : 2 + lineage.indexOf(row.quoteLineId)
65
+ for (const charge of line.charges) {
66
+ const candidates = adjustments.filter((row) => !invalid.has(row.id) && (
67
+ row.level === 'charge'
68
+ ? row.quoteLineId === line.lineId && row.chargeCode === charge.chargeCode
69
+ : (row.quoteLineId === line.lineId || lineage.includes(row.quoteLineId)) &&
70
+ (!row.chargeTypeFilter || row.chargeTypeFilter === charge.chargeType)
71
+ )).sort((a, b) => rank(a) - rank(b) || a.id.localeCompare(b.id))
72
+ const standard = charge.standardUnitPrice!
73
+ const total = charge.standardTotalPrice
74
+ const valid = candidates.filter((row) => {
75
+ // Line discounts cover billable NRC/MRC, never metered usage.
76
+ if (charge.chargeType === 'usage' && row.level === 'line') return false
77
+ let code: AdjustmentError['code'] | undefined
78
+ if (charge.chargeType === 'usage' || total === null || !Number.isFinite(row.value) || row.value < 0 ||
79
+ (row.adjustmentType === 'price_override' && charge.pricingMethod !== 'flat' && charge.pricingMethod !== 'per_unit') ||
80
+ (row.level === 'line' && row.adjustmentType !== 'discount_percent') ||
81
+ (row.adjustmentType === 'discount_percent' && (row.value <= 0 || row.value > 100)) ||
82
+ (row.adjustmentType === 'discount_absolute' && row.value <= 0)) code = 'TYPE_NOT_ALLOWED'
83
+ else if ((row.currencyCode && row.currencyCode !== currencyCode) || charge.currencyCode !== currencyCode ||
84
+ (row.adjustmentType !== 'discount_percent' && !row.currencyCode)) code = 'CURRENCY_MISMATCH'
85
+ else if (row.adjustmentType === 'price_override' && row.value > standard) code = 'ABOVE_STANDARD'
86
+ if (code) addError(row, code, charge.chargeCode, line.lineId)
87
+ return !code
88
+ })
89
+ let applied = false
90
+ for (const row of valid) {
91
+ if (valid.filter((other) => rank(other) === rank(row)).length > 1) {
92
+ addError(row, 'ADJUSTMENT_OVERLAP', charge.chargeCode, line.lineId)
93
+ continue
94
+ }
95
+ if (applied) continue
96
+ const quantity = charge.quantity ?? 1
97
+ const afterTotal = row.adjustmentType === 'discount_absolute' ? Math.max(0, total! - row.value)
98
+ : row.adjustmentType === 'discount_percent' ? Math.max(0, total! * (1 - row.value / 100))
99
+ : row.value * quantity
100
+ const afterUnit = row.adjustmentType === 'discount_percent' ? standard * (1 - row.value / 100)
101
+ : row.adjustmentType === 'price_override' ? row.value : quantity > 0 ? afterTotal / quantity : 0
102
+ charge.unitPrice = afterUnit
103
+ charge.totalPrice = afterTotal
104
+ charge.adjustments.push({
105
+ origin: 'manual', adjustmentId: row.id, class: 'rep', level: row.level,
106
+ windowKind: 'relative', fromMonth: 1, toMonth: null,
107
+ ruleCode: row.id, ruleName: row.reasonCode, ruleType: row.adjustmentType,
108
+ reasonCode: row.reasonCode, createdByUserId: row.createdByUserId,
109
+ value: row.value, unitPriceBefore: standard, unitPriceAfter: afterUnit, delta: afterUnit - standard,
110
+ })
111
+ applied = true
112
+ }
113
+ }
114
+ }
115
+ return { lines: output, errors }
116
+ }
@@ -0,0 +1,31 @@
1
+ import type { CpqQuoteConfiguration, CpqQuoteLineConfiguration } from '../../data/entities'
2
+ import { adjustmentErrorSchema } from '../../data/price-adjustments'
3
+
4
+ type Failure = { lineId: string | null; code: string }
5
+ function recordedError(value: unknown, fallback: string | null): Failure | null {
6
+ if (!value || typeof value !== 'object') return null
7
+ const record = value as Record<string, unknown>
8
+ const parsed = adjustmentErrorSchema.shape.code.safeParse(record.code ?? record.message)
9
+ return parsed.success ? { lineId: typeof record.lineId === 'string' ? record.lineId : fallback, code: parsed.data } : null
10
+ }
11
+
12
+ /** Conversion validates the reviewed stored snapshot, without calling the price engine. */
13
+ export function storedAdjustmentFailure(quote: CpqQuoteConfiguration, lines: CpqQuoteLineConfiguration[]): Failure | null {
14
+ for (const line of lines) {
15
+ for (const charge of line.charges ?? []) {
16
+ if (Array.isArray(charge.adjustmentErrors) && charge.adjustmentErrors.length) {
17
+ return recordedError(charge.adjustmentErrors[0], line.quoteLineId) ?? { lineId: line.quoteLineId, code: 'ADJUSTMENT_INVALID' }
18
+ }
19
+ }
20
+ for (const error of line.validationErrors ?? []) {
21
+ const failure = recordedError(error, line.quoteLineId)
22
+ if (failure) return failure
23
+ }
24
+ }
25
+ const errors = quote.validationResult?.errors
26
+ if (Array.isArray(errors)) for (const error of errors) {
27
+ const failure = recordedError(error, null)
28
+ if (failure) return failure
29
+ }
30
+ return null
31
+ }
@@ -92,6 +92,8 @@ export interface QuoteItemInput {
92
92
  }
93
93
 
94
94
  export interface ValidationError {
95
+ lineId?: string
96
+ adjustmentId?: string
95
97
  type?: 'attribute' | 'relationship'
96
98
  attribute?: string
97
99
  relationshipType?: string
@@ -137,7 +139,20 @@ export interface QuoteLineResult {
137
139
  targetSubscriptionId: string | null
138
140
  }
139
141
 
140
- export interface PricingSummary {
142
+ export interface PricingWaterfallSummary {
143
+ listNrcTotal: number
144
+ listMrcTotal: number
145
+ standardNrcTotal: number
146
+ standardMrcTotal: number
147
+ ruleDiscountTotal: number
148
+ manualDiscountTotal: number
149
+ dealValue: { list: number; standard: number; offer: number }
150
+ discountDepth: { rule: number | null; manual: number | null; total: number | null; maxLineManual: number | null }
151
+ hasTimeBasedAdjustments: boolean
152
+ snapshotHash: string
153
+ }
154
+
155
+ export interface PricingSummary extends Partial<PricingWaterfallSummary> {
141
156
  nrcTotal: number
142
157
  mrcTotal: number
143
158
  usageCharges: Array<{
@@ -153,6 +168,8 @@ export interface PricingSummary {
153
168
  }
154
169
 
155
170
  export interface QuoteResult {
171
+ adjustmentErrors?: import('../data/price-adjustments').AdjustmentError[]
172
+ pricingRevision?: number
156
173
  id: string
157
174
  quoteId: string
158
175
  quoteNumber: string
@@ -180,6 +197,16 @@ export interface QuoteResult {
180
197
  // ─── Resolved charge (extended for v2) ───────────────────────────
181
198
 
182
199
  export interface RuleAdjustment {
200
+ adjustmentId?: string
201
+ class?: 'rep' | 'carried_over'
202
+ level?: 'charge' | 'line' | 'quote'
203
+ windowKind?: 'relative' | 'absolute'
204
+ fromMonth?: number
205
+ toMonth?: number | null
206
+ reasonCode?: string
207
+ createdByUserId?: string
208
+ /** Missing on legacy snapshots. */
209
+ origin?: 'rule' | 'manual'
183
210
  ruleCode: string
184
211
  ruleName: string
185
212
  ruleType: string
@@ -210,6 +237,12 @@ export interface ResolvedCharge {
210
237
  quantity: number | null
211
238
  totalPrice: number | null
212
239
  currencyCode: string
240
+ /** Additive waterfall fields; absent on snapshots saved before negotiated pricing. */
241
+ adjustmentErrors?: import('../data/price-adjustments').AdjustmentError[]
242
+ listUnitPrice?: number
243
+ listTotalPrice?: number | null
244
+ standardUnitPrice?: number
245
+ standardTotalPrice?: number | null
213
246
  breakdown?: TierBreakdown | null
214
247
  note?: string | null
215
248
  adjustments?: RuleAdjustment[] | null
@@ -1,3 +1,4 @@
1
+ import { seedPriceAdjustmentDefaults } from './lib/priceAdjustmentDefaults'
1
2
  import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
2
3
 
3
4
  /**
@@ -9,7 +10,7 @@ import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
9
10
  *
10
11
  * What still lives here:
11
12
  * - `defaultRoleFeatures` — required for any tenant that has cpq enabled
12
- * - structural defaults (currently none beyond role features)
13
+ * - structural defaults (price adjustment reasons and rollout toggle)
13
14
  *
14
15
  * What used to live here and moved out:
15
16
  * - GIX product/pricing/charge/rule/bundle/wizard seed → `src/modules/demo_gix/`
@@ -17,10 +18,14 @@ import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
17
18
  * - GIX wizard definition → `src/modules/demo_gix/seeds/wizards.ts`
18
19
  */
19
20
  export const setup: ModuleSetupConfig = {
21
+ async seedDefaults({ em, tenantId, organizationId }) {
22
+ if (tenantId && organizationId) await seedPriceAdjustmentDefaults(em, { tenantId, organizationId })
23
+ },
20
24
  defaultRoleFeatures: {
21
25
  admin: [
22
26
  'cpq.quotes.view',
23
27
  'cpq.quotes.manage',
28
+ 'cpq.quotes.adjust_price',
24
29
  'cpq.pricing.view',
25
30
  'cpq.pricing.manage',
26
31
  'cpq.products.view',
@@ -44,6 +49,7 @@ export const setup: ModuleSetupConfig = {
44
49
  employee: [
45
50
  'cpq.quotes.view',
46
51
  'cpq.quotes.manage',
52
+ 'cpq.quotes.adjust_price',
47
53
  'cpq.pricing.view',
48
54
  'cpq.products.view',
49
55
  'cpq.specifications.view',