@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,169 @@
1
+ const ADJUST_PRICE_FEATURE = "cpq.quotes.adjust_price";
2
+ const ADJUSTMENT_EDITABLE_STATUSES = ["new", "incomplete", "ready"];
3
+ const INVALID_DELETE_BLOCKED = ["accepted", "rejected", "cancelled"];
4
+ function waterfallRows(charge) {
5
+ const entries = charge.adjustments ?? [];
6
+ const rules = entries.filter((entry) => entry.origin !== "manual");
7
+ const manual = entries.filter((entry) => entry.origin === "manual");
8
+ const standard = charge.standardUnitPrice ?? (manual.length ? manual[0].unitPriceBefore : charge.unitPrice);
9
+ const list = charge.listUnitPrice ?? (rules.length ? rules[0].unitPriceBefore : standard);
10
+ const step = (kind) => (entry) => ({
11
+ kind,
12
+ label: entry.ruleName || entry.ruleCode,
13
+ ruleType: entry.ruleType,
14
+ value: entry.value,
15
+ delta: entry.delta,
16
+ unitPriceAfter: entry.unitPriceAfter,
17
+ reasonCode: entry.reasonCode,
18
+ adjustmentId: entry.adjustmentId
19
+ });
20
+ const rows = [{ kind: "list", unitPrice: list }, ...rules.map(step("rule"))];
21
+ if (rules.length || manual.length) rows.push({ kind: "standard", unitPrice: standard });
22
+ rows.push(...manual.map(step("manual")), { kind: "offer", unitPrice: charge.unitPrice });
23
+ return rows;
24
+ }
25
+ function discountDepth(before, after) {
26
+ if (before == null || after == null || before === 0) return null;
27
+ return 1 - after / before;
28
+ }
29
+ function formatDepth(depth) {
30
+ if (depth === null || Math.abs(depth) < 5e-5) return null;
31
+ const percent = Math.round(depth * 1e3) / 10;
32
+ return depth > 0 ? `\u2212${percent}%` : `+${Math.abs(percent)}%`;
33
+ }
34
+ function lineWaterfallTotals(line) {
35
+ let list = 0, standard = 0, offer = 0;
36
+ for (const charge of line.charges) {
37
+ if (charge.chargeType === "usage" || charge.totalPrice == null) continue;
38
+ const offerTotal = charge.totalPrice;
39
+ const standardTotal = charge.standardTotalPrice ?? offerTotal;
40
+ list += (charge.listTotalPrice ?? standardTotal) * line.quantity;
41
+ standard += standardTotal * line.quantity;
42
+ offer += offerTotal * line.quantity;
43
+ }
44
+ return { list, standard, offer };
45
+ }
46
+ function allowedAdjustmentTypes(level, charge) {
47
+ if (level === "line") return ["discount_percent"];
48
+ if (!charge || charge.chargeType === "usage") return [];
49
+ const overridable = charge.pricingMethod == null || charge.pricingMethod === "flat" || charge.pricingMethod === "per_unit";
50
+ return overridable ? ["price_override", "discount_percent", "discount_absolute"] : ["discount_percent", "discount_absolute"];
51
+ }
52
+ function lineAdjustmentBlock(line, quoteType) {
53
+ const mirrored = quoteType !== "new" && typeof line.configuration?._arcMirroredFromItemId === "string";
54
+ if (mirrored) return "mirror";
55
+ if (!line.offeringId && !line.productId) return "free_form";
56
+ return null;
57
+ }
58
+ function adjustmentAccess(input) {
59
+ if (input.enabled === null) return { editable: false, reason: "loading" };
60
+ if (!input.enabled) return { editable: false, reason: "disabled" };
61
+ if (!input.canAdjust) return { editable: false, reason: "no_permission" };
62
+ if (input.convertedOrderId) return { editable: false, reason: "converted" };
63
+ if (!ADJUSTMENT_EDITABLE_STATUSES.includes(input.cpqStatus)) return { editable: false, reason: "locked" };
64
+ return { editable: true, reason: null };
65
+ }
66
+ function canDeleteInvalidAdjustment(input) {
67
+ return input.enabled === true && input.canAdjust && !input.convertedOrderId && !INVALID_DELETE_BLOCKED.includes(input.cpqStatus);
68
+ }
69
+ const ADJUSTMENT_ERROR_FALLBACKS = {
70
+ ADJUSTMENT_TARGET_MISSING: "The adjusted charge no longer exists on this line",
71
+ TARGET_AMBIGUOUS: "The adjusted charge appears more than once on this line",
72
+ ABOVE_STANDARD: "The price is above the standard price",
73
+ CURRENCY_MISMATCH: "The currency does not match the quote",
74
+ TYPE_NOT_ALLOWED: "This kind of adjustment is not allowed for this charge",
75
+ LINE_NOT_ELIGIBLE: "This line cannot be adjusted",
76
+ ADJUSTMENT_OVERLAP: "Another adjustment already targets this charge at the same level",
77
+ ADJUSTMENT_LOCKED: "Adjustments are locked for this quote",
78
+ STALE_REVISION: "Prices changed since you opened this \u2014 reload and try again",
79
+ REASON_NOT_FOUND: "Pick a reason from the list",
80
+ REASON_NOTE_REQUIRED: "Add a note when the reason is \u201COther\u201D",
81
+ INVALID_PERCENT: "Enter a percentage above 0 and up to 100",
82
+ INVALID_AMOUNT: "Enter an amount above 0"
83
+ };
84
+ function buildAdjustmentDraft(form, target, currencyCode, expectedRevision) {
85
+ const value = Number(form.value.trim().replace(",", "."));
86
+ if (!form.value.trim() || !Number.isFinite(value)) return null;
87
+ const note = form.reasonNote.trim();
88
+ return {
89
+ level: target.level,
90
+ quoteLineId: target.quoteLineId,
91
+ ...target.level === "charge" ? { chargeCode: target.chargeCode } : { chargeTypeFilter: form.chargeTypeFilter || null },
92
+ adjustmentType: form.adjustmentType,
93
+ value,
94
+ ...form.adjustmentType === "discount_percent" ? {} : { currencyCode },
95
+ reasonCode: form.reasonCode,
96
+ ...note ? { reasonNote: note } : {},
97
+ ...expectedRevision !== void 0 ? { expectedRevision } : {}
98
+ };
99
+ }
100
+ function toAdjustmentPatch(draft) {
101
+ const { level: _level, quoteLineId: _line, chargeCode: _charge, ...patch } = draft;
102
+ return { ...patch, reasonNote: patch.reasonNote ?? null, currencyCode: patch.currencyCode ?? null };
103
+ }
104
+ function adjustmentErrorCode(status, body) {
105
+ const record = body ?? {};
106
+ const detail = Array.isArray(record.details) ? record.details.find((issue) => typeof issue?.message === "string" && /^[A-Z_]+$/.test(issue.message)) : void 0;
107
+ if (detail) return detail.message;
108
+ if (typeof record.error === "string" && /^[A-Z_]+$/.test(record.error)) return record.error;
109
+ if (status === 403) return "FORBIDDEN";
110
+ if (status === 409) return "ADJUSTMENT_LOCKED";
111
+ return typeof record.error === "string" ? record.error : "UNKNOWN";
112
+ }
113
+ function parseStoredCharge(raw) {
114
+ if (!raw || typeof raw !== "object") return null;
115
+ const record = raw;
116
+ const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
117
+ const chargeType = record.chargeType === "nrc" || record.chargeType === "mrc" || record.chargeType === "usage" ? record.chargeType : null;
118
+ const unitPrice = num(record.unitPrice);
119
+ if (!chargeType || unitPrice === void 0) return null;
120
+ const adjustments = Array.isArray(record.adjustments) ? record.adjustments.filter((entry) => entry && typeof entry === "object" && typeof entry.ruleType === "string" && num(entry.unitPriceAfter) !== void 0 && num(entry.delta) !== void 0) : null;
121
+ return {
122
+ chargeName: typeof record.chargeName === "string" ? record.chargeName : typeof record.chargeCode === "string" ? record.chargeCode : "\u2014",
123
+ chargeType,
124
+ pricingMethod: typeof record.pricingMethod === "string" ? record.pricingMethod : null,
125
+ unitPrice,
126
+ quantity: num(record.quantity) ?? null,
127
+ totalPrice: num(record.totalPrice) ?? null,
128
+ listUnitPrice: num(record.listUnitPrice),
129
+ listTotalPrice: num(record.listTotalPrice) ?? null,
130
+ standardUnitPrice: num(record.standardUnitPrice),
131
+ standardTotalPrice: num(record.standardTotalPrice) ?? null,
132
+ adjustments
133
+ };
134
+ }
135
+ function collectAdjustmentErrors(quote) {
136
+ const map = /* @__PURE__ */ new Map();
137
+ const add = (entries) => {
138
+ for (const entry of entries ?? []) {
139
+ const code = entry.code ?? entry.message;
140
+ if (entry.adjustmentId && code && !map.has(entry.adjustmentId)) map.set(entry.adjustmentId, code);
141
+ }
142
+ };
143
+ add(quote?.adjustmentErrors);
144
+ for (const line of quote?.lines ?? []) {
145
+ for (const charge of line.charges ?? []) add(charge.adjustmentErrors);
146
+ add(line.validationErrors);
147
+ }
148
+ add(quote?.validationResult?.errors);
149
+ return map;
150
+ }
151
+ export {
152
+ ADJUSTMENT_EDITABLE_STATUSES,
153
+ ADJUSTMENT_ERROR_FALLBACKS,
154
+ ADJUST_PRICE_FEATURE,
155
+ adjustmentAccess,
156
+ adjustmentErrorCode,
157
+ allowedAdjustmentTypes,
158
+ buildAdjustmentDraft,
159
+ canDeleteInvalidAdjustment,
160
+ collectAdjustmentErrors,
161
+ discountDepth,
162
+ formatDepth,
163
+ lineAdjustmentBlock,
164
+ lineWaterfallTotals,
165
+ parseStoredCharge,
166
+ toAdjustmentPatch,
167
+ waterfallRows
168
+ };
169
+ //# sourceMappingURL=priceAdjustmentView.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/lib/priceAdjustmentView.ts"],
4
+ "sourcesContent": ["/**\n * Pure view helpers for negotiated quote prices. Client-safe: no service or\n * ORM imports. Rules mirror the server resolver\n * (`services/priceAdjustments/resolveManualAdjustments.ts`) so the UI only\n * offers what the API would accept; the API stays the authority.\n */\n\nexport const ADJUST_PRICE_FEATURE = 'cpq.quotes.adjust_price'\n/** Matches `assertMutable` in the adjustment service. */\nexport const ADJUSTMENT_EDITABLE_STATUSES = ['new', 'incomplete', 'ready'] as const\n/** Statuses where even an invalid adjustment can no longer be deleted. */\nconst INVALID_DELETE_BLOCKED = ['accepted', 'rejected', 'cancelled']\n\nexport type AdjustmentType = 'discount_percent' | 'discount_absolute' | 'price_override'\nexport type AdjustmentLevel = 'charge' | 'line'\n\nexport type WaterfallChargeInput = {\n chargeType: 'nrc' | 'mrc' | 'usage'\n pricingMethod?: string | null\n unitPrice: number\n totalPrice: number | null\n listUnitPrice?: number\n listTotalPrice?: number | null\n standardUnitPrice?: number\n standardTotalPrice?: number | null\n adjustments?: Array<{\n origin?: 'rule' | 'manual'\n adjustmentId?: string\n ruleCode: string\n ruleName: string\n ruleType: string\n value: number\n unitPriceBefore: number\n unitPriceAfter: number\n delta: number\n reasonCode?: string\n createdByUserId?: string\n }> | null\n}\n\nexport type WaterfallRow =\n | { kind: 'list'; unitPrice: number }\n | { kind: 'rule' | 'manual'; label: string; ruleType: string; value: number; delta: number; unitPriceAfter: number; reasonCode?: string; adjustmentId?: string }\n | { kind: 'standard'; unitPrice: number }\n | { kind: 'offer'; unitPrice: number }\n\n/**\n * List \u2192 rule rows \u2192 standard \u2192 manual rows \u2192 offer. Legacy snapshots without\n * waterfall fields fall back to the stored unit price for list and standard.\n */\nexport function waterfallRows(charge: WaterfallChargeInput): WaterfallRow[] {\n const entries = charge.adjustments ?? []\n const rules = entries.filter((entry) => entry.origin !== 'manual')\n const manual = entries.filter((entry) => entry.origin === 'manual')\n const standard = charge.standardUnitPrice ?? (manual.length ? manual[0].unitPriceBefore : charge.unitPrice)\n const list = charge.listUnitPrice ?? (rules.length ? rules[0].unitPriceBefore : standard)\n const step = (kind: 'rule' | 'manual') => (entry: (typeof entries)[number]): WaterfallRow => ({\n kind, label: entry.ruleName || entry.ruleCode, ruleType: entry.ruleType, value: entry.value,\n delta: entry.delta, unitPriceAfter: entry.unitPriceAfter, reasonCode: entry.reasonCode, adjustmentId: entry.adjustmentId,\n })\n const rows: WaterfallRow[] = [{ kind: 'list', unitPrice: list }, ...rules.map(step('rule'))]\n if (rules.length || manual.length) rows.push({ kind: 'standard', unitPrice: standard })\n rows.push(...manual.map(step('manual')), { kind: 'offer', unitPrice: charge.unitPrice })\n return rows\n}\n\n/** Signed fraction: positive = discount, negative = surcharge, null when undefined. */\nexport function discountDepth(before: number | null | undefined, after: number | null | undefined): number | null {\n if (before == null || after == null || before === 0) return null\n return 1 - after / before\n}\n\nexport function formatDepth(depth: number | null): string | null {\n if (depth === null || Math.abs(depth) < 0.00005) return null\n const percent = Math.round(depth * 1000) / 10\n return depth > 0 ? `\u2212${percent}%` : `+${Math.abs(percent)}%`\n}\n\n/** Line-level list / standard / offer totals (per line quantity), ignoring usage. */\nexport function lineWaterfallTotals(line: { quantity: number; charges: WaterfallChargeInput[] }) {\n let list = 0, standard = 0, offer = 0\n for (const charge of line.charges) {\n if (charge.chargeType === 'usage' || charge.totalPrice == null) continue\n const offerTotal = charge.totalPrice\n const standardTotal = charge.standardTotalPrice ?? offerTotal\n list += (charge.listTotalPrice ?? standardTotal) * line.quantity\n standard += standardTotal * line.quantity\n offer += offerTotal * line.quantity\n }\n return { list, standard, offer }\n}\n\n/** Adjustment types the server accepts for a target. */\nexport function allowedAdjustmentTypes(level: AdjustmentLevel, charge?: Pick<WaterfallChargeInput, 'chargeType' | 'pricingMethod'>): AdjustmentType[] {\n if (level === 'line') return ['discount_percent']\n if (!charge || charge.chargeType === 'usage') return []\n const overridable = charge.pricingMethod == null || charge.pricingMethod === 'flat' || charge.pricingMethod === 'per_unit'\n return overridable ? ['price_override', 'discount_percent', 'discount_absolute'] : ['discount_percent', 'discount_absolute']\n}\n\nexport type LineEligibilityInput = {\n offeringId: string | null\n /** Product-only lines are eligible too (server: `offeringId || productId`). */\n productId?: string | null\n configuration?: Record<string, unknown> | null\n}\n\n/** ARC mirror lines and free-form lines take no manual adjustments yet. */\nexport function lineAdjustmentBlock(line: LineEligibilityInput, quoteType: string): 'mirror' | 'free_form' | null {\n const mirrored = quoteType !== 'new' && typeof line.configuration?._arcMirroredFromItemId === 'string'\n if (mirrored) return 'mirror'\n if (!line.offeringId && !line.productId) return 'free_form'\n return null\n}\n\nexport type AdjustmentAccess =\n | { editable: true; reason: null }\n | { editable: false; reason: 'loading' | 'disabled' | 'no_permission' | 'converted' | 'locked' }\n\nexport function adjustmentAccess(input: {\n enabled: boolean | null\n canAdjust: boolean\n cpqStatus: string\n convertedOrderId: string | null\n}): AdjustmentAccess {\n if (input.enabled === null) return { editable: false, reason: 'loading' }\n if (!input.enabled) return { editable: false, reason: 'disabled' }\n if (!input.canAdjust) return { editable: false, reason: 'no_permission' }\n if (input.convertedOrderId) return { editable: false, reason: 'converted' }\n if (!(ADJUSTMENT_EDITABLE_STATUSES as readonly string[]).includes(input.cpqStatus)) return { editable: false, reason: 'locked' }\n return { editable: true, reason: null }\n}\n\n/** An invalid adjustment can still be deleted after submission, unless the quote is final or converted. */\nexport function canDeleteInvalidAdjustment(input: { enabled: boolean | null; canAdjust: boolean; cpqStatus: string; convertedOrderId: string | null }): boolean {\n return input.enabled === true && input.canAdjust && !input.convertedOrderId && !INVALID_DELETE_BLOCKED.includes(input.cpqStatus)\n}\n\n/** English fallbacks for server error codes; keys live under `cpq.priceAdjustments.errors.*`. */\nexport const ADJUSTMENT_ERROR_FALLBACKS: Record<string, string> = {\n ADJUSTMENT_TARGET_MISSING: 'The adjusted charge no longer exists on this line',\n TARGET_AMBIGUOUS: 'The adjusted charge appears more than once on this line',\n ABOVE_STANDARD: 'The price is above the standard price',\n CURRENCY_MISMATCH: 'The currency does not match the quote',\n TYPE_NOT_ALLOWED: 'This kind of adjustment is not allowed for this charge',\n LINE_NOT_ELIGIBLE: 'This line cannot be adjusted',\n ADJUSTMENT_OVERLAP: 'Another adjustment already targets this charge at the same level',\n ADJUSTMENT_LOCKED: 'Adjustments are locked for this quote',\n STALE_REVISION: 'Prices changed since you opened this \u2014 reload and try again',\n REASON_NOT_FOUND: 'Pick a reason from the list',\n REASON_NOTE_REQUIRED: 'Add a note when the reason is \u201COther\u201D',\n INVALID_PERCENT: 'Enter a percentage above 0 and up to 100',\n INVALID_AMOUNT: 'Enter an amount above 0',\n}\n\nexport type AdjustmentForm = {\n adjustmentType: AdjustmentType\n /** Raw text from the input; parsed on submit/preview. */\n value: string\n reasonCode: string\n reasonNote: string\n /** Line level only: '' = all billable charges. */\n chargeTypeFilter: '' | 'nrc' | 'mrc'\n}\n\nexport type AdjustmentTarget =\n | { level: 'charge'; quoteLineId: string; chargeCode: string }\n | { level: 'line'; quoteLineId: string }\n\n/** Shape accepted by POST /price-adjustments and the preview `draft`. */\nexport function buildAdjustmentDraft(form: AdjustmentForm, target: AdjustmentTarget, currencyCode: string, expectedRevision?: number) {\n const value = Number(form.value.trim().replace(',', '.'))\n if (!form.value.trim() || !Number.isFinite(value)) return null\n const note = form.reasonNote.trim()\n return {\n level: target.level,\n quoteLineId: target.quoteLineId,\n ...(target.level === 'charge' ? { chargeCode: target.chargeCode } : { chargeTypeFilter: form.chargeTypeFilter || null }),\n adjustmentType: form.adjustmentType,\n value,\n ...(form.adjustmentType === 'discount_percent' ? {} : { currencyCode }),\n reasonCode: form.reasonCode,\n ...(note ? { reasonNote: note } : {}),\n ...(expectedRevision !== undefined ? { expectedRevision } : {}),\n }\n}\n\n/** PATCH body: target fields are immutable server-side, so they are dropped. */\nexport function toAdjustmentPatch(draft: NonNullable<ReturnType<typeof buildAdjustmentDraft>>) {\n const { level: _level, quoteLineId: _line, chargeCode: _charge, ...patch } = draft as typeof draft & { chargeCode?: string }\n return { ...patch, reasonNote: patch.reasonNote ?? null, currencyCode: patch.currencyCode ?? null }\n}\n\n/** Server error body \u2192 a stable code for messages (`STALE_REVISION`, `INVALID_PERCENT`, \u2026). */\nexport function adjustmentErrorCode(status: number, body: unknown): string {\n const record = (body ?? {}) as { error?: unknown; details?: Array<{ message?: unknown }> }\n const detail = Array.isArray(record.details) ? record.details.find((issue) => typeof issue?.message === 'string' && /^[A-Z_]+$/.test(issue.message as string)) : undefined\n if (detail) return detail.message as string\n if (typeof record.error === 'string' && /^[A-Z_]+$/.test(record.error)) return record.error\n if (status === 403) return 'FORBIDDEN'\n if (status === 409) return 'ADJUSTMENT_LOCKED'\n return typeof record.error === 'string' ? record.error : 'UNKNOWN'\n}\n\n/** Narrow a stored charge jsonb (subscription / order item) to the waterfall shape; null when unusable. */\nexport function parseStoredCharge(raw: unknown): (WaterfallChargeInput & { chargeName: string; quantity: number | null }) | null {\n if (!raw || typeof raw !== 'object') return null\n const record = raw as Record<string, unknown>\n const num = (value: unknown) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined)\n const chargeType = record.chargeType === 'nrc' || record.chargeType === 'mrc' || record.chargeType === 'usage' ? record.chargeType : null\n const unitPrice = num(record.unitPrice)\n if (!chargeType || unitPrice === undefined) return null\n const adjustments = Array.isArray(record.adjustments)\n ? (record.adjustments as Array<Record<string, unknown>>).filter((entry) =>\n entry && typeof entry === 'object' && typeof entry.ruleType === 'string' && num(entry.unitPriceAfter) !== undefined && num(entry.delta) !== undefined) as unknown as WaterfallChargeInput['adjustments']\n : null\n return {\n chargeName: typeof record.chargeName === 'string' ? record.chargeName : typeof record.chargeCode === 'string' ? record.chargeCode : '\u2014',\n chargeType,\n pricingMethod: typeof record.pricingMethod === 'string' ? record.pricingMethod : null,\n unitPrice,\n quantity: num(record.quantity) ?? null,\n totalPrice: num(record.totalPrice) ?? null,\n listUnitPrice: num(record.listUnitPrice),\n listTotalPrice: num(record.listTotalPrice) ?? null,\n standardUnitPrice: num(record.standardUnitPrice),\n standardTotalPrice: num(record.standardTotalPrice) ?? null,\n adjustments,\n }\n}\n\ntype ErrorSource = { adjustmentId?: string; code?: string; message?: string }\nexport type AdjustmentErrorSourceQuote = {\n adjustmentErrors?: ErrorSource[]\n validationResult?: { errors?: ErrorSource[] } | null\n lines?: Array<{ validationErrors?: ErrorSource[] | null; charges?: Array<{ adjustmentErrors?: ErrorSource[] }> }>\n}\n\n/**\n * adjustmentId \u2192 first error code. Recalculation responses carry\n * `adjustmentErrors`; a plain GET carries only the persisted validation\n * errors (code in `message`), which is where target-missing errors live.\n */\nexport function collectAdjustmentErrors(quote: AdjustmentErrorSourceQuote | null | undefined): Map<string, string> {\n const map = new Map<string, string>()\n const add = (entries: ErrorSource[] | null | undefined) => {\n for (const entry of entries ?? []) {\n const code = entry.code ?? entry.message\n if (entry.adjustmentId && code && !map.has(entry.adjustmentId)) map.set(entry.adjustmentId, code)\n }\n }\n add(quote?.adjustmentErrors)\n for (const line of quote?.lines ?? []) {\n for (const charge of line.charges ?? []) add(charge.adjustmentErrors)\n add(line.validationErrors)\n }\n add(quote?.validationResult?.errors)\n return map\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import { hasAllFeatures } from "@open-mercato/shared/security/features";
2
+ const QUOTE_MANAGE_FEATURE = "cpq.quotes.manage";
3
+ const TERMINAL_QUOTE_STATUSES = ["accepted", "rejected", "cancelled"];
4
+ function canRecalculateQuote(grantedFeatures, cpqStatus) {
5
+ if (TERMINAL_QUOTE_STATUSES.includes(cpqStatus)) return false;
6
+ return hasAllFeatures(grantedFeatures, [QUOTE_MANAGE_FEATURE]);
7
+ }
8
+ export {
9
+ QUOTE_MANAGE_FEATURE,
10
+ TERMINAL_QUOTE_STATUSES,
11
+ canRecalculateQuote
12
+ };
13
+ //# sourceMappingURL=quotePermissions.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/lib/quotePermissions.ts"],
4
+ "sourcesContent": ["import { hasAllFeatures } from '@open-mercato/shared/security/features'\n\n/** Feature required to write a quote's prices (recalculate, line edits). */\nexport const QUOTE_MANAGE_FEATURE = 'cpq.quotes.manage'\n\n/**\n * Terminal quote statuses, mirrored from `TERMINAL_STATUSES` in\n * `services/types.ts` so client components can use this helper without\n * pulling the service layer into the browser bundle. A unit test keeps the\n * two lists identical.\n */\nexport const TERMINAL_QUOTE_STATUSES: readonly string[] = ['accepted', 'rejected', 'cancelled']\n\n/**\n * Whether the quote page may offer \"Recalculate\": the user can manage quotes\n * (wildcard-aware) and the quote is not terminal \u2014 a saved recalculation on a\n * terminal quote is refused by the API (409).\n */\nexport function canRecalculateQuote(grantedFeatures: readonly string[], cpqStatus: string): boolean {\n if (TERMINAL_QUOTE_STATUSES.includes(cpqStatus)) return false\n return hasAllFeatures(grantedFeatures, [QUOTE_MANAGE_FEATURE])\n}\n"],
5
+ "mappings": "AAAA,SAAS,sBAAsB;AAGxB,MAAM,uBAAuB;AAQ7B,MAAM,0BAA6C,CAAC,YAAY,YAAY,WAAW;AAOvF,SAAS,oBAAoB,iBAAoC,WAA4B;AAClG,MAAI,wBAAwB,SAAS,SAAS,EAAG,QAAO;AACxD,SAAO,eAAe,iBAAiB,CAAC,oBAAoB,CAAC;AAC/D;",
6
+ "names": []
7
+ }
@@ -0,0 +1,28 @@
1
+ import { discountDepth } from "./priceAdjustmentView.js";
2
+ function chargeTypeDepths(summary) {
3
+ return {
4
+ nrc: discountDepth(summary.listNrcTotal, summary.nrcTotal),
5
+ mrc: discountDepth(summary.listMrcTotal, summary.mrcTotal)
6
+ };
7
+ }
8
+ function arcRecurringDelta(lines) {
9
+ let currentMrc = 0;
10
+ let newMrc = 0;
11
+ for (const line of lines) {
12
+ if (line.arcSource) currentMrc += Number(line.arcSource.mrcAmount) || 0;
13
+ if (line.action !== "cancel") newMrc += Number(line.mrcTotal) || 0;
14
+ }
15
+ const delta = newMrc - currentMrc;
16
+ return { currentMrc, newMrc, delta, deltaPct: currentMrc === 0 ? null : delta / currentMrc };
17
+ }
18
+ function formatChangePct(ratio) {
19
+ if (ratio === null || Math.abs(ratio) < 5e-5) return null;
20
+ const percent = Math.round(Math.abs(ratio) * 1e3) / 10;
21
+ return ratio > 0 ? `+${percent}%` : `\u2212${percent}%`;
22
+ }
23
+ export {
24
+ arcRecurringDelta,
25
+ chargeTypeDepths,
26
+ formatChangePct
27
+ };
28
+ //# sourceMappingURL=quoteSummaryView.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/lib/quoteSummaryView.ts"],
4
+ "sourcesContent": ["import { discountDepth } from './priceAdjustmentView'\n\ntype SummaryTotals = {\n nrcTotal: number\n mrcTotal: number\n listNrcTotal?: number\n listMrcTotal?: number\n}\n\n/**\n * Total discount depth (list \u2192 offer) per charge type for the summary header.\n * `null` when the snapshot predates the waterfall fields or list is zero.\n */\nexport function chargeTypeDepths(summary: SummaryTotals): { nrc: number | null; mrc: number | null } {\n return {\n nrc: discountDepth(summary.listNrcTotal, summary.nrcTotal),\n mrc: discountDepth(summary.listMrcTotal, summary.mrcTotal),\n }\n}\n\ntype ArcSummaryLine = {\n action: 'add' | 'modify' | 'cancel'\n mrcTotal: number\n arcSource: { mrcAmount: number } | null\n}\n\nexport type ArcRecurringDelta = { currentMrc: number; newMrc: number; delta: number; deltaPct: number | null }\n\n/**\n * Amend / Renew: monthly recurring before vs. after the change.\n * \"Current\" sums the live subscription items the quote mirrors (`arcSource`);\n * \"New\" sums every line that survives activation (cancel lines drop to zero).\n * Same before/after semantics as the per-line `ArcLineDiff` on the order page.\n */\nexport function arcRecurringDelta(lines: ArcSummaryLine[]): ArcRecurringDelta {\n let currentMrc = 0\n let newMrc = 0\n for (const line of lines) {\n if (line.arcSource) currentMrc += Number(line.arcSource.mrcAmount) || 0\n if (line.action !== 'cancel') newMrc += Number(line.mrcTotal) || 0\n }\n const delta = newMrc - currentMrc\n return { currentMrc, newMrc, delta, deltaPct: currentMrc === 0 ? null : delta / currentMrc }\n}\n\n/** Signed percent label for a change ratio: `+12.5%`, `\u221220%`, or null when ~0 / undefined. */\nexport function formatChangePct(ratio: number | null): string | null {\n if (ratio === null || Math.abs(ratio) < 0.00005) return null\n const percent = Math.round(Math.abs(ratio) * 1000) / 10\n return ratio > 0 ? `+${percent}%` : `\u2212${percent}%`\n}\n"],
5
+ "mappings": "AAAA,SAAS,qBAAqB;AAavB,SAAS,iBAAiB,SAAoE;AACnG,SAAO;AAAA,IACL,KAAK,cAAc,QAAQ,cAAc,QAAQ,QAAQ;AAAA,IACzD,KAAK,cAAc,QAAQ,cAAc,QAAQ,QAAQ;AAAA,EAC3D;AACF;AAgBO,SAAS,kBAAkB,OAA4C;AAC5E,MAAI,aAAa;AACjB,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,UAAW,eAAc,OAAO,KAAK,UAAU,SAAS,KAAK;AACtE,QAAI,KAAK,WAAW,SAAU,WAAU,OAAO,KAAK,QAAQ,KAAK;AAAA,EACnE;AACA,QAAM,QAAQ,SAAS;AACvB,SAAO,EAAE,YAAY,QAAQ,OAAO,UAAU,eAAe,IAAI,OAAO,QAAQ,WAAW;AAC7F;AAGO,SAAS,gBAAgB,OAAqC;AACnE,MAAI,UAAU,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAS,QAAO;AACxD,QAAM,UAAU,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,GAAI,IAAI;AACrD,SAAO,QAAQ,IAAI,IAAI,OAAO,MAAM,SAAI,OAAO;AACjD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,22 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20261110213052 extends Migration {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.name = "Migration20261110213052";
6
+ }
7
+ up() {
8
+ this.addSql(`create table "cpq_quote_price_adjustments" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "quote_configuration_id" uuid not null, "quote_id" uuid not null, "quote_line_id" uuid not null, "level" text not null, "charge_code" text null, "charge_type_filter" text null, "adjustment_type" text not null, "value" numeric(18, 4) not null, "currency_code" text null, "window_kind" text not null default 'relative', "from_month" int not null default 1, "to_month" int null, "reason_code" text not null, "reason_note" text null, "created_by_user_id" uuid not null, "updated_by_user_id" uuid null, "is_active" boolean not null default true, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, primary key ("id"));`);
9
+ this.addSql(`create index "cpq_qpa_quote_idx" on "cpq_quote_price_adjustments" ("organization_id", "tenant_id", "quote_id");`);
10
+ this.addSql(`create index "cpq_qpa_quote_line_idx" on "cpq_quote_price_adjustments" ("organization_id", "tenant_id", "quote_line_id");`);
11
+ this.addSql(`create index "cpq_qpa_quote_config_idx" on "cpq_quote_price_adjustments" ("organization_id", "tenant_id", "quote_configuration_id");`);
12
+ this.addSql(`alter table "cpq_quote_configurations" add "pricing_revision" int not null default 0;`);
13
+ }
14
+ down() {
15
+ this.addSql(`drop table if exists "cpq_quote_price_adjustments" cascade;`);
16
+ this.addSql(`alter table "cpq_quote_configurations" drop column "pricing_revision";`);
17
+ }
18
+ }
19
+ export {
20
+ Migration20261110213052
21
+ };
22
+ //# sourceMappingURL=Migration20261110213052.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/migrations/Migration20261110213052.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20261110213052 extends Migration {\n\n override name = 'Migration20261110213052';\n\n override up(): void | Promise<void> {\n this.addSql(`create table \"cpq_quote_price_adjustments\" (\"id\" uuid not null default gen_random_uuid(), \"organization_id\" uuid not null, \"tenant_id\" uuid not null, \"quote_configuration_id\" uuid not null, \"quote_id\" uuid not null, \"quote_line_id\" uuid not null, \"level\" text not null, \"charge_code\" text null, \"charge_type_filter\" text null, \"adjustment_type\" text not null, \"value\" numeric(18, 4) not null, \"currency_code\" text null, \"window_kind\" text not null default 'relative', \"from_month\" int not null default 1, \"to_month\" int null, \"reason_code\" text not null, \"reason_note\" text null, \"created_by_user_id\" uuid not null, \"updated_by_user_id\" uuid null, \"is_active\" boolean not null default true, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, \"deleted_at\" timestamptz null, primary key (\"id\"));`);\n this.addSql(`create index \"cpq_qpa_quote_idx\" on \"cpq_quote_price_adjustments\" (\"organization_id\", \"tenant_id\", \"quote_id\");`);\n this.addSql(`create index \"cpq_qpa_quote_line_idx\" on \"cpq_quote_price_adjustments\" (\"organization_id\", \"tenant_id\", \"quote_line_id\");`);\n this.addSql(`create index \"cpq_qpa_quote_config_idx\" on \"cpq_quote_price_adjustments\" (\"organization_id\", \"tenant_id\", \"quote_configuration_id\");`);\n\n this.addSql(`alter table \"cpq_quote_configurations\" add \"pricing_revision\" int not null default 0;`);\n }\n\n override down(): void | Promise<void> {\n this.addSql(`drop table if exists \"cpq_quote_price_adjustments\" cascade;`);\n\n this.addSql(`alter table \"cpq_quote_configurations\" drop column \"pricing_revision\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EAAhD;AAAA;AAEL,SAAS,OAAO;AAAA;AAAA,EAEP,KAA2B;AAClC,SAAK,OAAO,8yBAA8yB;AAC1zB,SAAK,OAAO,iHAAiH;AAC7H,SAAK,OAAO,2HAA2H;AACvI,SAAK,OAAO,sIAAsI;AAElJ,SAAK,OAAO,uFAAuF;AAAA,EACrG;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,6DAA6D;AAEzE,SAAK,OAAO,wEAAwE;AAAA,EACtF;AAEF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,53 @@
1
+ import { isArcMirrorLine } from "./arcPriceFreeze.js";
2
+ const ITEM_REMOVAL_QUOTE_TYPES = ["amend", "renew"];
3
+ function supportsItemRemoval(quoteType) {
4
+ return ITEM_REMOVAL_QUOTE_TYPES.includes(quoteType ?? "");
5
+ }
6
+ function removalCancelsItem(line, quoteType) {
7
+ return supportsItemRemoval(quoteType) && isArcMirrorLine(line.configuration, quoteType);
8
+ }
9
+ function isArcCancelledLine(line, quoteType) {
10
+ return line.action === "cancel" && supportsItemRemoval(quoteType);
11
+ }
12
+ function collectLineSubtree(root, lines) {
13
+ const children = /* @__PURE__ */ new Map();
14
+ for (const line of lines) {
15
+ if (!line.parentLineId) continue;
16
+ const siblings = children.get(line.parentLineId) ?? [];
17
+ siblings.push(line);
18
+ children.set(line.parentLineId, siblings);
19
+ }
20
+ const out = [];
21
+ const seen = /* @__PURE__ */ new Set();
22
+ const queue = [root];
23
+ for (let index = 0; index < queue.length; index++) {
24
+ const line = queue[index];
25
+ if (seen.has(line.quoteLineId)) continue;
26
+ seen.add(line.quoteLineId);
27
+ out.push(line);
28
+ queue.push(...children.get(line.quoteLineId) ?? []);
29
+ }
30
+ return out;
31
+ }
32
+ function planArcLineRemoval(root, lines, quoteType) {
33
+ const subtree = collectLineSubtree(root, lines);
34
+ const cancel = subtree.filter((line) => isArcMirrorLine(line.configuration, quoteType) && line.action !== "cancel");
35
+ const remove = subtree.filter((line) => !isArcMirrorLine(line.configuration, quoteType));
36
+ const removeIds = new Set(remove.map((line) => line.quoteLineId));
37
+ const removeRoots = remove.filter((line) => !line.parentLineId || !removeIds.has(line.parentLineId));
38
+ return { cancel, removeRoots };
39
+ }
40
+ function planArcLineRestore(root, lines, quoteType) {
41
+ const parent = root.parentLineId ? lines.find((line) => line.quoteLineId === root.parentLineId) : void 0;
42
+ if (parent && isArcCancelledLine(parent, quoteType)) return { ok: false, reason: "parent_cancelled" };
43
+ const restore = collectLineSubtree(root, lines).filter((line) => isArcCancelledLine(line, quoteType) && isArcMirrorLine(line.configuration, quoteType));
44
+ return { ok: true, restore };
45
+ }
46
+ export {
47
+ collectLineSubtree,
48
+ isArcCancelledLine,
49
+ planArcLineRemoval,
50
+ planArcLineRestore,
51
+ removalCancelsItem
52
+ };
53
+ //# sourceMappingURL=arcLineRemoval.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/services/arcLineRemoval.ts"],
4
+ "sourcesContent": ["/**\n * Removing an existing subscription item on an Amend / Renew quote.\n *\n * An ARC quote mirrors each live subscription item as a quote line. Activation\n * only acts on explicit line actions \u2014 an item with no line is left untouched \u2014\n * so deleting a mirror line used to keep the item active on the subscription.\n * Instead, \"remove\" turns the mirror line (and its mirrored components) into a\n * `cancel` line, which activation applies by terminating the item. Lines this\n * quote added under it are dropped. A cancelled line can be restored from the\n * subscription item it mirrors.\n *\n * Cancel quotes are excluded: every line there is already a cancel line.\n */\n\nimport { isArcMirrorLine } from './arcPriceFreeze'\n\nexport type ArcRemovalLine = {\n quoteLineId: string\n parentLineId?: string | null\n action: string\n configuration: Record<string, unknown> | null | undefined\n}\n\nconst ITEM_REMOVAL_QUOTE_TYPES = ['amend', 'renew'] as const\n\nfunction supportsItemRemoval(quoteType: string | null | undefined): boolean {\n return (ITEM_REMOVAL_QUOTE_TYPES as readonly string[]).includes(quoteType ?? '')\n}\n\n/** Whether removing this line cancels a subscription item instead of deleting the line. */\nexport function removalCancelsItem(line: Pick<ArcRemovalLine, 'configuration'>, quoteType: string | null | undefined): boolean {\n return supportsItemRemoval(quoteType) && isArcMirrorLine(line.configuration, quoteType)\n}\n\n/** A subscription item marked for removal on an Amend / Renew quote. It is not priced. */\nexport function isArcCancelledLine(line: Pick<ArcRemovalLine, 'action'>, quoteType: string | null | undefined): boolean {\n return line.action === 'cancel' && supportsItemRemoval(quoteType)\n}\n\n/** The line and every descendant, root first. */\nexport function collectLineSubtree<T extends ArcRemovalLine>(root: T, lines: readonly T[]): T[] {\n const children = new Map<string, T[]>()\n for (const line of lines) {\n if (!line.parentLineId) continue\n const siblings = children.get(line.parentLineId) ?? []\n siblings.push(line)\n children.set(line.parentLineId, siblings)\n }\n const out: T[] = []\n const seen = new Set<string>()\n const queue: T[] = [root]\n for (let index = 0; index < queue.length; index++) {\n const line = queue[index]\n if (seen.has(line.quoteLineId)) continue\n seen.add(line.quoteLineId)\n out.push(line)\n queue.push(...(children.get(line.quoteLineId) ?? []))\n }\n return out\n}\n\nexport type ArcRemovalPlan<T> = {\n /** Mirror lines that become `cancel` lines. */\n cancel: T[]\n /** Roots of quote-added subtrees to delete (each deleted with its descendants). */\n removeRoots: T[]\n}\n\nexport function planArcLineRemoval<T extends ArcRemovalLine>(root: T, lines: readonly T[], quoteType: string | null | undefined): ArcRemovalPlan<T> {\n const subtree = collectLineSubtree(root, lines)\n const cancel = subtree.filter((line) => isArcMirrorLine(line.configuration, quoteType) && line.action !== 'cancel')\n const remove = subtree.filter((line) => !isArcMirrorLine(line.configuration, quoteType))\n const removeIds = new Set(remove.map((line) => line.quoteLineId))\n const removeRoots = remove.filter((line) => !line.parentLineId || !removeIds.has(line.parentLineId))\n return { cancel, removeRoots }\n}\n\nexport type ArcRestorePlan<T> = { ok: true; restore: T[] } | { ok: false; reason: 'parent_cancelled' }\n\n/** Restore the line and its cancelled mirrored components; blocked while its parent stays cancelled. */\nexport function planArcLineRestore<T extends ArcRemovalLine>(root: T, lines: readonly T[], quoteType: string | null | undefined): ArcRestorePlan<T> {\n const parent = root.parentLineId ? lines.find((line) => line.quoteLineId === root.parentLineId) : undefined\n if (parent && isArcCancelledLine(parent, quoteType)) return { ok: false, reason: 'parent_cancelled' }\n const restore = collectLineSubtree(root, lines)\n .filter((line) => isArcCancelledLine(line, quoteType) && isArcMirrorLine(line.configuration, quoteType))\n return { ok: true, restore }\n}\n"],
5
+ "mappings": "AAcA,SAAS,uBAAuB;AAShC,MAAM,2BAA2B,CAAC,SAAS,OAAO;AAElD,SAAS,oBAAoB,WAA+C;AAC1E,SAAQ,yBAA+C,SAAS,aAAa,EAAE;AACjF;AAGO,SAAS,mBAAmB,MAA6C,WAA+C;AAC7H,SAAO,oBAAoB,SAAS,KAAK,gBAAgB,KAAK,eAAe,SAAS;AACxF;AAGO,SAAS,mBAAmB,MAAsC,WAA+C;AACtH,SAAO,KAAK,WAAW,YAAY,oBAAoB,SAAS;AAClE;AAGO,SAAS,mBAA6C,MAAS,OAA0B;AAC9F,QAAM,WAAW,oBAAI,IAAiB;AACtC,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,aAAc;AACxB,UAAM,WAAW,SAAS,IAAI,KAAK,YAAY,KAAK,CAAC;AACrD,aAAS,KAAK,IAAI;AAClB,aAAS,IAAI,KAAK,cAAc,QAAQ;AAAA,EAC1C;AACA,QAAM,MAAW,CAAC;AAClB,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,QAAa,CAAC,IAAI;AACxB,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;AACjD,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,KAAK,IAAI,KAAK,WAAW,EAAG;AAChC,SAAK,IAAI,KAAK,WAAW;AACzB,QAAI,KAAK,IAAI;AACb,UAAM,KAAK,GAAI,SAAS,IAAI,KAAK,WAAW,KAAK,CAAC,CAAE;AAAA,EACtD;AACA,SAAO;AACT;AASO,SAAS,mBAA6C,MAAS,OAAqB,WAAyD;AAClJ,QAAM,UAAU,mBAAmB,MAAM,KAAK;AAC9C,QAAM,SAAS,QAAQ,OAAO,CAAC,SAAS,gBAAgB,KAAK,eAAe,SAAS,KAAK,KAAK,WAAW,QAAQ;AAClH,QAAM,SAAS,QAAQ,OAAO,CAAC,SAAS,CAAC,gBAAgB,KAAK,eAAe,SAAS,CAAC;AACvF,QAAM,YAAY,IAAI,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,WAAW,CAAC;AAChE,QAAM,cAAc,OAAO,OAAO,CAAC,SAAS,CAAC,KAAK,gBAAgB,CAAC,UAAU,IAAI,KAAK,YAAY,CAAC;AACnG,SAAO,EAAE,QAAQ,YAAY;AAC/B;AAKO,SAAS,mBAA6C,MAAS,OAAqB,WAAyD;AAClJ,QAAM,SAAS,KAAK,eAAe,MAAM,KAAK,CAAC,SAAS,KAAK,gBAAgB,KAAK,YAAY,IAAI;AAClG,MAAI,UAAU,mBAAmB,QAAQ,SAAS,EAAG,QAAO,EAAE,IAAI,OAAO,QAAQ,mBAAmB;AACpG,QAAM,UAAU,mBAAmB,MAAM,KAAK,EAC3C,OAAO,CAAC,SAAS,mBAAmB,MAAM,SAAS,KAAK,gBAAgB,KAAK,eAAe,SAAS,CAAC;AACzG,SAAO,EAAE,IAAI,MAAM,QAAQ;AAC7B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,79 @@
1
+ const ARC_PRICE_FROZEN_KEY = "_arcPriceFrozen";
2
+ const ARC_REPRICED_KEY = "_arcRepriced";
3
+ const QUOTE_ONLY_ARC_KEYS = [ARC_PRICE_FROZEN_KEY, ARC_REPRICED_KEY];
4
+ const ARC_MARKER_PREFIX = "_arc";
5
+ function isArcPriceFrozen(configuration) {
6
+ return configuration?.[ARC_PRICE_FROZEN_KEY] === true;
7
+ }
8
+ function isArcMirrorLine(configuration, quoteType) {
9
+ return (quoteType === "amend" || quoteType === "renew" || quoteType === "cancel") && typeof configuration?._arcMirroredFromItemId === "string";
10
+ }
11
+ function isFrozenAmendMirrorLine(configuration, quoteType) {
12
+ return quoteType === "amend" && typeof configuration?._arcMirroredFromItemId === "string" && isArcPriceFrozen(configuration);
13
+ }
14
+ function buildArcMirrorConfiguration(itemConfiguration, item, quoteType) {
15
+ return {
16
+ // An item's configuration can carry markers from an earlier ARC quote
17
+ // (activation copies line configuration onto the item); never inherit them.
18
+ ...stripArcMarkers(itemConfiguration),
19
+ _arcMirroredFromItemId: item.id,
20
+ _arcMirroredName: item.name,
21
+ ...quoteType === "amend" ? { [ARC_PRICE_FROZEN_KEY]: true } : {}
22
+ };
23
+ }
24
+ function stripQuoteOnlyArcMarkers(configuration) {
25
+ if (!configuration || !QUOTE_ONLY_ARC_KEYS.some((key) => key in configuration)) return configuration;
26
+ const out = {};
27
+ for (const [key, value] of Object.entries(configuration)) {
28
+ if (!QUOTE_ONLY_ARC_KEYS.includes(key)) out[key] = value;
29
+ }
30
+ return out;
31
+ }
32
+ function stripArcMarkers(configuration) {
33
+ const out = {};
34
+ for (const [key, value] of Object.entries(configuration ?? {})) {
35
+ if (!key.startsWith(ARC_MARKER_PREFIX)) out[key] = value;
36
+ }
37
+ return out;
38
+ }
39
+ function canonical(value) {
40
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
41
+ if (value && typeof value === "object") {
42
+ const entries = Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`);
43
+ return `{${entries.join(",")}}`;
44
+ }
45
+ return JSON.stringify(value ?? null);
46
+ }
47
+ function isoDay(value) {
48
+ if (!value) return null;
49
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
50
+ return String(value).slice(0, 10);
51
+ }
52
+ function arcPricingInputsChanged(before, update) {
53
+ if (canonical(stripArcMarkers(before.configuration)) !== canonical(stripArcMarkers(update.configuration))) {
54
+ return true;
55
+ }
56
+ if (update.quantity !== void 0 && update.quantity !== before.quantity) return true;
57
+ if (update.termMonths !== void 0 && update.termMonths !== (before.termMonths ?? void 0)) return true;
58
+ if (update.startDate !== void 0 && isoDay(update.startDate) !== isoDay(before.startDate)) return true;
59
+ if (update.endDate !== void 0 && isoDay(update.endDate) !== isoDay(before.endDate)) return true;
60
+ if (update.usageEstimates !== void 0) {
61
+ const normalize = (list) => canonical(
62
+ [...list ?? []].map((e) => ({ chargeCode: e.chargeCode, estimatedQuantity: e.estimatedQuantity })).sort((a, b) => a.chargeCode.localeCompare(b.chargeCode))
63
+ );
64
+ if (normalize(update.usageEstimates) !== normalize(before.usageEstimates)) return true;
65
+ }
66
+ return false;
67
+ }
68
+ export {
69
+ ARC_PRICE_FROZEN_KEY,
70
+ ARC_REPRICED_KEY,
71
+ arcPricingInputsChanged,
72
+ buildArcMirrorConfiguration,
73
+ isArcMirrorLine,
74
+ isArcPriceFrozen,
75
+ isFrozenAmendMirrorLine,
76
+ stripArcMarkers,
77
+ stripQuoteOnlyArcMarkers
78
+ };
79
+ //# sourceMappingURL=arcPriceFreeze.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/cpq/services/arcPriceFreeze.ts"],
4
+ "sourcesContent": ["/**\n * Frozen Amend mirror lines.\n *\n * An Amend quote mirrors every live subscription item as a `modify` line.\n * Engine-priced mirror lines used to be re-priced at today's catalog price on\n * every recalculation, and activation wrote that price back to the\n * subscription item \u2014 so amending one item silently moved every other item's\n * price. A mirror line on an Amend quote is therefore created **frozen**: it\n * keeps the source item's stored charges until a pricing input of that line\n * changes, at which point it is re-priced as before.\n *\n * The flag lives in the line configuration jsonb next to the existing\n * `_arcMirrored*` markers, so no schema change is needed.\n */\n\nexport const ARC_PRICE_FROZEN_KEY = '_arcPriceFrozen'\n\n/**\n * Set when the server unfreezes an Amend mirror line because a pricing input\n * changed \u2014 drives the \"re-priced at the current catalog price\" notice (an\n * explicit marker, so Amend quotes created before freezing existed don't\n * show it).\n */\nexport const ARC_REPRICED_KEY = '_arcRepriced'\n\n/** Markers that describe quote-time pricing state and never leave the quote. */\nconst QUOTE_ONLY_ARC_KEYS = [ARC_PRICE_FROZEN_KEY, ARC_REPRICED_KEY] as const\n\nconst ARC_MARKER_PREFIX = '_arc'\n\nexport type ArcMirrorQuoteType = 'amend' | 'cancel' | 'renew'\n\nexport function isArcPriceFrozen(configuration: Record<string, unknown> | null | undefined): boolean {\n return configuration?.[ARC_PRICE_FROZEN_KEY] === true\n}\n\n/**\n * Whether recalculation must keep a line's stored charges. Only server-made\n * mirror lines on Amend quotes qualify \u2014 the flag alone is never trusted\n * (client input is stripped of `_arc*` keys, and a clone of an Amend quote\n * is a new quote).\n */\nexport function isArcMirrorLine(configuration: Record<string, unknown> | null | undefined, quoteType: string | null | undefined): boolean {\n return (quoteType === 'amend' || quoteType === 'renew' || quoteType === 'cancel') && typeof configuration?._arcMirroredFromItemId === 'string'\n}\n\nexport function isFrozenAmendMirrorLine(\n configuration: Record<string, unknown> | null | undefined,\n quoteType: string | null | undefined,\n): boolean {\n return (\n quoteType === 'amend' &&\n typeof configuration?._arcMirroredFromItemId === 'string' &&\n isArcPriceFrozen(configuration)\n )\n}\n\n/**\n * Configuration for a mirror line: the item's configuration plus the ARC\n * markers. Only Amend lines are frozen \u2014 Renew re-prices by product decision\n * and Cancel lines are not priced.\n */\nexport function buildArcMirrorConfiguration(\n itemConfiguration: Record<string, unknown> | null | undefined,\n item: { id: string; name: string },\n quoteType: ArcMirrorQuoteType,\n): Record<string, unknown> {\n return {\n // An item's configuration can carry markers from an earlier ARC quote\n // (activation copies line configuration onto the item); never inherit them.\n ...stripArcMarkers(itemConfiguration),\n _arcMirroredFromItemId: item.id,\n _arcMirroredName: item.name,\n ...(quoteType === 'amend' ? { [ARC_PRICE_FROZEN_KEY]: true } : {}),\n }\n}\n\n/**\n * Line configuration as it may leave the quote (order lines, subscription\n * items, clones): quote-only pricing markers are dropped; the descriptive\n * `_arcMirrored*` markers stay.\n */\nexport function stripQuoteOnlyArcMarkers(\n configuration: Record<string, unknown> | null | undefined,\n): Record<string, unknown> | null | undefined {\n if (!configuration || !QUOTE_ONLY_ARC_KEYS.some((key) => key in configuration)) return configuration\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(configuration)) {\n if (!(QUOTE_ONLY_ARC_KEYS as readonly string[]).includes(key)) out[key] = value\n }\n return out\n}\n\n/**\n * Configuration without any `_arc*` marker. ARC markers are server-owned:\n * client-supplied configuration goes through this before it is stored.\n */\nexport function stripArcMarkers(configuration: Record<string, unknown> | null | undefined): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(configuration ?? {})) {\n if (!key.startsWith(ARC_MARKER_PREFIX)) out[key] = value\n }\n return out\n}\n\nfunction canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`\n if (value && typeof value === 'object') {\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n return `{${entries.join(',')}}`\n }\n return JSON.stringify(value ?? null)\n}\n\nfunction isoDay(value: Date | string | null | undefined): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString().slice(0, 10)\n return String(value).slice(0, 10)\n}\n\nexport type ArcLinePricingState = {\n configuration: Record<string, unknown> | null | undefined\n quantity: number\n termMonths?: number | null\n startDate?: Date | string | null\n endDate?: Date | string | null\n usageEstimates?: Array<{ chargeCode: string; estimatedQuantity: number }> | null\n}\n\nexport type ArcLinePricingUpdate = {\n configuration: Record<string, unknown>\n quantity?: number\n termMonths?: number\n startDate?: string\n endDate?: string\n usageEstimates?: Array<{ chargeCode: string; estimatedQuantity: number }>\n}\n\n/**\n * Whether an update changes any input the pricing engine reads. The quote UI\n * always resubmits the full configuration, so \"a field was sent\" is not a\n * change \u2014 values are compared (ARC markers ignored, key order ignored).\n * Fields the update omits keep their stored value.\n */\nexport function arcPricingInputsChanged(before: ArcLinePricingState, update: ArcLinePricingUpdate): boolean {\n if (canonical(stripArcMarkers(before.configuration)) !== canonical(stripArcMarkers(update.configuration))) {\n return true\n }\n if (update.quantity !== undefined && update.quantity !== before.quantity) return true\n if (update.termMonths !== undefined && update.termMonths !== (before.termMonths ?? undefined)) return true\n if (update.startDate !== undefined && isoDay(update.startDate) !== isoDay(before.startDate)) return true\n if (update.endDate !== undefined && isoDay(update.endDate) !== isoDay(before.endDate)) return true\n if (update.usageEstimates !== undefined) {\n const normalize = (list: Array<{ chargeCode: string; estimatedQuantity: number }> | null | undefined) =>\n canonical(\n [...(list ?? [])]\n .map((e) => ({ chargeCode: e.chargeCode, estimatedQuantity: e.estimatedQuantity }))\n .sort((a, b) => a.chargeCode.localeCompare(b.chargeCode)),\n )\n if (normalize(update.usageEstimates) !== normalize(before.usageEstimates)) return true\n }\n return false\n}\n"],
5
+ "mappings": "AAeO,MAAM,uBAAuB;AAQ7B,MAAM,mBAAmB;AAGhC,MAAM,sBAAsB,CAAC,sBAAsB,gBAAgB;AAEnE,MAAM,oBAAoB;AAInB,SAAS,iBAAiB,eAAoE;AACnG,SAAO,gBAAgB,oBAAoB,MAAM;AACnD;AAQO,SAAS,gBAAgB,eAA2D,WAA+C;AACxI,UAAQ,cAAc,WAAW,cAAc,WAAW,cAAc,aAAa,OAAO,eAAe,2BAA2B;AACxI;AAEO,SAAS,wBACd,eACA,WACS;AACT,SACE,cAAc,WACd,OAAO,eAAe,2BAA2B,YACjD,iBAAiB,aAAa;AAElC;AAOO,SAAS,4BACd,mBACA,MACA,WACyB;AACzB,SAAO;AAAA;AAAA;AAAA,IAGL,GAAG,gBAAgB,iBAAiB;AAAA,IACpC,wBAAwB,KAAK;AAAA,IAC7B,kBAAkB,KAAK;AAAA,IACvB,GAAI,cAAc,UAAU,EAAE,CAAC,oBAAoB,GAAG,KAAK,IAAI,CAAC;AAAA,EAClE;AACF;AAOO,SAAS,yBACd,eAC4C;AAC5C,MAAI,CAAC,iBAAiB,CAAC,oBAAoB,KAAK,CAAC,QAAQ,OAAO,aAAa,EAAG,QAAO;AACvF,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,aAAa,GAAG;AACxD,QAAI,CAAE,oBAA0C,SAAS,GAAG,EAAG,KAAI,GAAG,IAAI;AAAA,EAC5E;AACA,SAAO;AACT;AAMO,SAAS,gBAAgB,eAAoF;AAClH,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,iBAAiB,CAAC,CAAC,GAAG;AAC9D,QAAI,CAAC,IAAI,WAAW,iBAAiB,EAAG,KAAI,GAAG,IAAI;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,UAAU,OAAwB;AACzC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,IAAI,MAAM,IAAI,SAAS,EAAE,KAAK,GAAG,CAAC;AACnE,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,UAAM,UAAU,OAAO,KAAK,KAAgC,EACzD,KAAK,EACL,IAAI,CAAC,QAAQ,GAAG,KAAK,UAAU,GAAG,CAAC,IAAI,UAAW,MAAkC,GAAG,CAAC,CAAC,EAAE;AAC9F,WAAO,IAAI,QAAQ,KAAK,GAAG,CAAC;AAAA,EAC9B;AACA,SAAO,KAAK,UAAU,SAAS,IAAI;AACrC;AAEA,SAAS,OAAO,OAAwD;AACtE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY,EAAE,MAAM,GAAG,EAAE;AACjE,SAAO,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAClC;AA0BO,SAAS,wBAAwB,QAA6B,QAAuC;AAC1G,MAAI,UAAU,gBAAgB,OAAO,aAAa,CAAC,MAAM,UAAU,gBAAgB,OAAO,aAAa,CAAC,GAAG;AACzG,WAAO;AAAA,EACT;AACA,MAAI,OAAO,aAAa,UAAa,OAAO,aAAa,OAAO,SAAU,QAAO;AACjF,MAAI,OAAO,eAAe,UAAa,OAAO,gBAAgB,OAAO,cAAc,QAAY,QAAO;AACtG,MAAI,OAAO,cAAc,UAAa,OAAO,OAAO,SAAS,MAAM,OAAO,OAAO,SAAS,EAAG,QAAO;AACpG,MAAI,OAAO,YAAY,UAAa,OAAO,OAAO,OAAO,MAAM,OAAO,OAAO,OAAO,EAAG,QAAO;AAC9F,MAAI,OAAO,mBAAmB,QAAW;AACvC,UAAM,YAAY,CAAC,SACjB;AAAA,MACE,CAAC,GAAI,QAAQ,CAAC,CAAE,EACb,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,YAAY,mBAAmB,EAAE,kBAAkB,EAAE,EACjF,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,cAAc,EAAE,UAAU,CAAC;AAAA,IAC5D;AACF,QAAI,UAAU,OAAO,cAAc,MAAM,UAAU,OAAO,cAAc,EAAG,QAAO;AAAA,EACpF;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -1,4 +1,6 @@
1
1
  var _a, _b;
2
+ import { storedAdjustmentFailure } from "./priceAdjustments/storedAdjustmentFailure.js";
3
+ import { withQuotePricingWrite } from "./priceAdjustments/quotePricingWrite.js";
2
4
  import { SalesOrder, SalesOrderLine } from "@open-mercato/core/modules/sales/data/entities";
3
5
  import {
4
6
  CpqQuoteConfiguration,
@@ -14,6 +16,8 @@ import {
14
16
  CPQ_ORDER_TRANSITIONS
15
17
  } from "./types.js";
16
18
  import { emitCpqEvent } from "../events.js";
19
+ import { stripQuoteOnlyArcMarkers } from "./arcPriceFreeze.js";
20
+ import { summarizeOrderPricing } from "./orderPricingSummary.js";
17
21
  const CPQ_TO_SALES_ORDER_STATUS = {
18
22
  draft: "draft",
19
23
  pending_activation: "pending",
@@ -29,27 +33,15 @@ class DefaultCpqOrderService {
29
33
  }
30
34
  // ─── Convert Quote to Order ─────────────────────────────────
31
35
  async convertQuoteToOrder(quoteId, scope) {
32
- const em = this.em;
33
- const cpqConfig = await em.findOne(CpqQuoteConfiguration, {
34
- id: quoteId,
35
- organizationId: scope.organizationId,
36
- tenantId: scope.tenantId,
37
- deletedAt: null
38
- });
39
- if (!cpqConfig) {
40
- const byQuoteId = await em.findOne(CpqQuoteConfiguration, {
41
- quoteId,
42
- organizationId: scope.organizationId,
43
- tenantId: scope.tenantId,
44
- deletedAt: null
45
- });
46
- if (!byQuoteId) throw new OrderError(404, "Quote not found");
47
- return this.convertFromConfig(byQuoteId, scope);
48
- }
49
- return this.convertFromConfig(cpqConfig, scope);
36
+ return withQuotePricingWrite(
37
+ this.em,
38
+ quoteId,
39
+ scope,
40
+ (em, quote) => this.convertFromConfig(quote, scope, em),
41
+ (status, message) => new OrderError(status, message)
42
+ );
50
43
  }
51
- async convertFromConfig(cpqConfig, scope) {
52
- const em = this.em;
44
+ async convertFromConfig(cpqConfig, scope, em) {
53
45
  const blockedStatuses = ["cancelled", "rejected"];
54
46
  if (blockedStatuses.includes(cpqConfig.cpqStatus)) {
55
47
  throw new OrderError(409, `Cannot convert a quote in '${cpqConfig.cpqStatus}' status`);
@@ -75,6 +67,8 @@ class DefaultCpqOrderService {
75
67
  if (quoteLines.length === 0) {
76
68
  throw new OrderError(422, "Cannot convert a quote with no line items");
77
69
  }
70
+ const invalidAdjustment = storedAdjustmentFailure(cpqConfig, quoteLines);
71
+ if (invalidAdjustment) throw new OrderError(422, "ADJUSTMENT_INVALID", invalidAdjustment);
78
72
  const { number: orderNumber } = await this.numberGenerator.generate({
79
73
  kind: "order",
80
74
  organizationId: scope.organizationId,
@@ -189,7 +183,8 @@ class DefaultCpqOrderService {
189
183
  startDate: ql.startDate,
190
184
  termMonths: ql.termMonths ?? null,
191
185
  endDate: ql.endDate,
192
- configuration: { ...ql.configuration, offeringName },
186
+ // Quote-only ARC pricing markers stay on the quote.
187
+ configuration: { ...stripQuoteOnlyArcMarkers(ql.configuration), offeringName },
193
188
  quantity: ql.quantity,
194
189
  charges: ql.charges ? [...ql.charges] : null,
195
190
  nrcTotal: ql.nrcTotal,
@@ -457,7 +452,7 @@ class DefaultCpqOrderService {
457
452
  offeringId: line.offeringId ?? void 0,
458
453
  specId: line.specId ?? void 0,
459
454
  name: this.getLineName(line),
460
- configuration: line.configuration ?? {},
455
+ configuration: stripQuoteOnlyArcMarkers(line.configuration) ?? {},
461
456
  charges: line.charges ?? [],
462
457
  mrcAmount: Number(line.mrcTotal),
463
458
  nrcAmount: Number(line.nrcTotal),
@@ -519,7 +514,7 @@ class DefaultCpqOrderService {
519
514
  purchasePrice: Number(line.nrcTotal),
520
515
  quantity: line.quantity,
521
516
  purchaseDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
522
- configuration: line.configuration ?? {},
517
+ configuration: stripQuoteOnlyArcMarkers(line.configuration) ?? {},
523
518
  charges: line.charges ?? [],
524
519
  sourceOrderId: orderConfig.id,
525
520
  sourceQuoteLineId: line.sourceQuoteLineId ?? void 0
@@ -712,7 +707,7 @@ class DefaultCpqOrderService {
712
707
  action: lc.action,
713
708
  parentLineId: lc.parentLineId ?? null,
714
709
  quantity: lc.quantity,
715
- configuration: lc.configuration ?? {},
710
+ configuration: stripQuoteOnlyArcMarkers(lc.configuration) ?? {},
716
711
  startDate: lc.startDate?.toISOString() ?? null,
717
712
  termMonths: lc.termMonths ?? null,
718
713
  endDate: lc.endDate?.toISOString() ?? null,
@@ -745,7 +740,8 @@ class DefaultCpqOrderService {
745
740
  pricingSummary: {
746
741
  nrcTotal: Number(pricing.nrcTotal ?? 0),
747
742
  mrcTotal: Number(pricing.mrcTotal ?? 0),
748
- currencyCode: orderConfig.currencyCode
743
+ currencyCode: orderConfig.currencyCode,
744
+ ...summarizeOrderPricing(lines, quoteType, orderConfig.currencyCode)
749
745
  },
750
746
  activatedAt: orderConfig.activatedAt?.toISOString() ?? null,
751
747
  createdAt: orderConfig.createdAt.toISOString(),
@@ -768,9 +764,10 @@ function pickArcSourceFromConfig(cfg, map) {
768
764
  }
769
765
  const ORDER_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@dainamite/cpq/OrderError");
770
766
  class OrderError extends (_b = Error, _a = ORDER_ERROR_BRAND, _b) {
771
- constructor(status, message) {
767
+ constructor(status, message, details) {
772
768
  super(message);
773
769
  this.status = status;
770
+ this.details = details;
774
771
  this[_a] = true;
775
772
  this.name = "OrderError";
776
773
  }
@@ -786,7 +783,7 @@ function buildArcLineChange(line, specMap) {
786
783
  offeringId: line.offeringId ?? void 0,
787
784
  specId: line.specId ?? void 0,
788
785
  name: resolveLineName(line, specMap),
789
- configuration: line.configuration ?? {},
786
+ configuration: stripQuoteOnlyArcMarkers(line.configuration) ?? {},
790
787
  charges: line.charges ?? null,
791
788
  mrcAmount: Number(line.mrcTotal),
792
789
  nrcAmount: Number(line.nrcTotal),
@@ -812,7 +809,7 @@ function buildArcLineChange(line, specMap) {
812
809
  action,
813
810
  sourceSubscriptionItemId: line.sourceSubscriptionItemId ?? null,
814
811
  modifyPatch: {
815
- configuration: line.configuration ?? void 0,
812
+ configuration: stripQuoteOnlyArcMarkers(line.configuration) ?? void 0,
816
813
  charges: line.charges ?? void 0,
817
814
  mrcAmount: Number(line.mrcTotal),
818
815
  nrcAmount: Number(line.nrcTotal),