@zauru-sdk/common 6.6.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [6.8.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.7.0...@zauru-sdk/common@6.8.0) (2026-09-18)
7
+
8
+
9
+ ### Features
10
+
11
+ * descuento sucesivo en porcentajes de rechazo ([c437bbd](https://github.com/intuitiva/zauru-typescript-sdk/commit/c437bbd6a1305fd417c4ac55f8e07eb244f2fe3d))
12
+
13
+
14
+
15
+
16
+
17
+ # [6.7.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.6.0...@zauru-sdk/common@6.7.0) (2026-09-18)
18
+
19
+
20
+ ### Features
21
+
22
+ * registrar reglas automáticas de % rechazo en el historial ([4784849](https://github.com/intuitiva/zauru-typescript-sdk/commit/4784849904f9ac6570e60543895a858dece8e039))
23
+
24
+
25
+
26
+
27
+
6
28
  # [6.6.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.5.0...@zauru-sdk/common@6.6.0) (2026-09-14)
7
29
 
8
30
 
package/dist/common.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import "moment-timezone";
2
- import type { PayeeGraphQL, AxiosUtilsResponse, BasketSchema, SelectFieldOption, JsonMemoType, CalculatePurchaseOrderFinancialsInput, CalculatePurchaseOrderFinancialsResult } from "@zauru-sdk/types";
2
+ import type { PayeeGraphQL, AxiosUtilsResponse, BasketSchema, SelectFieldOption, JsonMemoType, CalculatePurchaseOrderFinancialsInput, CalculatePurchaseOrderFinancialsResult, RejectionPercentageLayers } from "@zauru-sdk/types";
3
3
  export declare const DESTINOS_MUESTRA_OPTIONS: SelectFieldOption[];
4
4
  export declare const parseJsonMemo: (memo?: string | object) => JsonMemoType;
5
5
  export declare const stringifyJsonMemo: (memo: JsonMemoType) => string;
@@ -12,20 +12,30 @@ export declare const getRejectionPercentage: (source: string | {
12
12
  memo?: string | object;
13
13
  } | JsonMemoType | undefined) => number;
14
14
  export declare const setRejectionPercentage: (memo: string | object | undefined, percentage: number) => string;
15
+ /**
16
+ * User rejection layers (before automatic rules). Legacy memos without
17
+ * `rejectionLayers` use the stored origin as additive and no successive rates.
18
+ */
19
+ export declare const resolveRejectionPercentageLayers: (memo?: string | object | JsonMemoType, fallbackAdditive?: number) => RejectionPercentageLayers;
20
+ export declare const getRejectionPercentageLayers: (source: string | {
21
+ memo?: string | object;
22
+ } | JsonMemoType | undefined) => RejectionPercentageLayers;
23
+ export declare const hasSuccessiveRejectionPercentage: (source: string | {
24
+ memo?: string | object;
25
+ } | JsonMemoType | undefined) => boolean;
15
26
  /**
16
27
  * Rejection % of origin stored in the memo, ignoring the adjustment rules
17
28
  * already applied on top of it.
18
29
  */
19
30
  export declare const resolveRejectionPercentageBase: (memo?: string | object) => number;
20
31
  /**
21
- * Header money for a purchase order: subtotal = qty × unit_cost,
22
- * discount = subtotal × rejectionPercentage / 100.
32
+ * Header money for a purchase order: subtotal = qty × unit_cost.
33
+ * Additive rates are summed first; successive rates then apply to the remainder.
23
34
  * Does not change unit cost. purchase_orders.discount is the monetary column.
24
35
  *
25
- * If `rules` is given, the rejection % is resolved back to the origin stored in
26
- * the memo and the rules are stacked on top of it before calculating the money.
27
- * `rejectionPercentage` in the result is the % that was actually charged and
28
- * `rejectionCalculations` is the trace to store in the memo.
36
+ * User layers (before rules) are returned as `rejectionLayers` for the memo.
37
+ * Automatic rules add percentage points on the additive block only.
38
+ * `rejectionPercentage` in the result is the effective % that was charged.
29
39
  */
30
40
  export declare const calculatePurchaseOrderFinancials: (input: CalculatePurchaseOrderFinancialsInput) => CalculatePurchaseOrderFinancialsResult;
31
41
  /**
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.parsedObject = exports.sortByProperty = exports.labFormPatter = exports.labServicePattern = exports.getRandomNum = exports.CURRENCY_PREFIX = exports.truncateDecimals = exports.ZAURU_REGEX = exports.priceToText = exports.arrayToObject = exports.isNumeric = exports.toFixedIfNeeded = exports.formatTimeToTimePicker = exports.formatDateToDatePicker = exports.getFormattedDate = exports.parsedBaculoFormValue = exports.getPayeeInfoOptions = exports.getPayeeInfoIdOptions = exports.getPayeeFormated = exports.getDateAfterDays = exports.getTimePickerCurrentTime = exports.obtenerFechaActualConZonaHoraria = exports.getDatePickerCurrentDate = exports.stringDateToParsedUTCDate = exports.localDateToUSDate = exports.getStringFullDate = exports.getTodayMinutesDifference = exports.getTodayDaysDifference = exports.getStringDate = exports.getZauruDateByText = exports.getNewDateByFormat = exports.getFechaJuliana = exports.getBasketsSchema = exports.calculatePurchaseOrderFinancials = exports.resolveRejectionPercentageBase = exports.setRejectionPercentage = exports.getRejectionPercentage = exports.mergeJsonMemo = exports.stringifyJsonMemo = exports.parseJsonMemo = exports.DESTINOS_MUESTRA_OPTIONS = void 0;
6
+ exports.parsedObject = exports.sortByProperty = exports.labFormPatter = exports.labServicePattern = exports.getRandomNum = exports.CURRENCY_PREFIX = exports.truncateDecimals = exports.ZAURU_REGEX = exports.priceToText = exports.arrayToObject = exports.isNumeric = exports.toFixedIfNeeded = exports.formatTimeToTimePicker = exports.formatDateToDatePicker = exports.getFormattedDate = exports.parsedBaculoFormValue = exports.getPayeeInfoOptions = exports.getPayeeInfoIdOptions = exports.getPayeeFormated = exports.getDateAfterDays = exports.getTimePickerCurrentTime = exports.obtenerFechaActualConZonaHoraria = exports.getDatePickerCurrentDate = exports.stringDateToParsedUTCDate = exports.localDateToUSDate = exports.getStringFullDate = exports.getTodayMinutesDifference = exports.getTodayDaysDifference = exports.getStringDate = exports.getZauruDateByText = exports.getNewDateByFormat = exports.getFechaJuliana = exports.getBasketsSchema = exports.calculatePurchaseOrderFinancials = exports.resolveRejectionPercentageBase = exports.hasSuccessiveRejectionPercentage = exports.getRejectionPercentageLayers = exports.resolveRejectionPercentageLayers = exports.setRejectionPercentage = exports.getRejectionPercentage = exports.mergeJsonMemo = exports.stringifyJsonMemo = exports.parseJsonMemo = exports.DESTINOS_MUESTRA_OPTIONS = void 0;
7
7
  exports.generateClientUUID = generateClientUUID;
8
8
  exports.extractValueBetweenTags = extractValueBetweenTags;
9
9
  exports.isJsonArray = isJsonArray;
@@ -81,11 +81,44 @@ const getRejectionPercentage = (source) => {
81
81
  exports.getRejectionPercentage = getRejectionPercentage;
82
82
  const setRejectionPercentage = (memo, percentage) => (0, exports.mergeJsonMemo)(memo, { rejectionPercentage: percentage });
83
83
  exports.setRejectionPercentage = setRejectionPercentage;
84
+ const memoFromRejectionSource = (source) => {
85
+ if (source == null || source === "")
86
+ return {};
87
+ if (typeof source === "string")
88
+ return (0, exports.parseJsonMemo)(source);
89
+ if (typeof source !== "object")
90
+ return {};
91
+ const record = source;
92
+ if (record.memo != null && record.memo !== "") {
93
+ return (0, exports.parseJsonMemo)(record.memo);
94
+ }
95
+ return (0, exports.parseJsonMemo)(record);
96
+ };
97
+ /**
98
+ * User rejection layers (before automatic rules). Legacy memos without
99
+ * `rejectionLayers` use the stored origin as additive and no successive rates.
100
+ */
101
+ const resolveRejectionPercentageLayers = (memo, fallbackAdditive) => {
102
+ const parsed = (0, exports.parseJsonMemo)(memo);
103
+ if (parsed.rejectionLayers) {
104
+ return (0, rejectionPercentageAdjustment_js_1.normalizeRejectionPercentageLayers)(parsed.rejectionLayers);
105
+ }
106
+ const origin = (0, rejectionPercentageAdjustment_js_1.resolveRejectionPercentageOrigin)(fallbackAdditive ?? (0, exports.getRejectionPercentage)(parsed), parsed.rejectionCalculations);
107
+ return (0, rejectionPercentageAdjustment_js_1.normalizeRejectionPercentageLayers)({
108
+ additive: origin,
109
+ successive: [],
110
+ });
111
+ };
112
+ exports.resolveRejectionPercentageLayers = resolveRejectionPercentageLayers;
113
+ const getRejectionPercentageLayers = (source) => (0, exports.resolveRejectionPercentageLayers)(memoFromRejectionSource(source));
114
+ exports.getRejectionPercentageLayers = getRejectionPercentageLayers;
115
+ const hasSuccessiveRejectionPercentage = (source) => (0, rejectionPercentageAdjustment_js_1.hasSuccessiveRejectionLayers)((0, exports.getRejectionPercentageLayers)(source));
116
+ exports.hasSuccessiveRejectionPercentage = hasSuccessiveRejectionPercentage;
84
117
  /**
85
118
  * Rejection % of origin stored in the memo, ignoring the adjustment rules
86
119
  * already applied on top of it.
87
120
  */
88
- const resolveRejectionPercentageBase = (memo) => (0, rejectionPercentageAdjustment_js_1.resolveRejectionPercentageOrigin)((0, exports.getRejectionPercentage)(memo), (0, exports.parseJsonMemo)(memo).rejectionCalculations);
121
+ const resolveRejectionPercentageBase = (memo) => (0, exports.resolveRejectionPercentageLayers)(memo).additive;
89
122
  exports.resolveRejectionPercentageBase = resolveRejectionPercentageBase;
90
123
  const roundMoney = (value) => Math.round((value + Number.EPSILON) * 100) / 100;
91
124
  const lineQuantity = (detail) => {
@@ -99,14 +132,13 @@ const lineQuantity = (detail) => {
99
132
  0);
100
133
  };
101
134
  /**
102
- * Header money for a purchase order: subtotal = qty × unit_cost,
103
- * discount = subtotal × rejectionPercentage / 100.
135
+ * Header money for a purchase order: subtotal = qty × unit_cost.
136
+ * Additive rates are summed first; successive rates then apply to the remainder.
104
137
  * Does not change unit cost. purchase_orders.discount is the monetary column.
105
138
  *
106
- * If `rules` is given, the rejection % is resolved back to the origin stored in
107
- * the memo and the rules are stacked on top of it before calculating the money.
108
- * `rejectionPercentage` in the result is the % that was actually charged and
109
- * `rejectionCalculations` is the trace to store in the memo.
139
+ * User layers (before rules) are returned as `rejectionLayers` for the memo.
140
+ * Automatic rules add percentage points on the additive block only.
141
+ * `rejectionPercentage` in the result is the effective % that was charged.
110
142
  */
111
143
  const calculatePurchaseOrderFinancials = (input) => {
112
144
  const details = input.details ?? [];
@@ -115,17 +147,28 @@ const calculatePurchaseOrderFinancials = (input) => {
115
147
  return sum + lineQuantity(detail) * unitCost;
116
148
  }, 0));
117
149
  const safeSubtotal = Number.isFinite(subtotal) ? subtotal : 0;
118
- const passedPercentage = toFiniteNumber(input.rejectionPercentage) ?? 0;
150
+ const parsedMemo = (0, exports.parseJsonMemo)(input.memo);
151
+ let userLayers = input.rejectionLayers
152
+ ? (0, rejectionPercentageAdjustment_js_1.normalizeRejectionPercentageLayers)(input.rejectionLayers)
153
+ : (0, exports.resolveRejectionPercentageLayers)(parsedMemo, toFiniteNumber(input.rejectionPercentage));
154
+ if (input.rejectionApplication) {
155
+ userLayers = (0, rejectionPercentageAdjustment_js_1.applyRejectionPercentageLayers)(userLayers, input.rejectionApplication);
156
+ }
119
157
  const rejectionCalculations = input.rules
120
- ? (0, rejectionPercentageAdjustment_js_1.applyRejectionPercentageAdjustmentRules)((0, rejectionPercentageAdjustment_js_1.resolveRejectionPercentageOrigin)(passedPercentage, (0, exports.parseJsonMemo)(input.memo).rejectionCalculations), input.rules, input.ctx ?? { itemIds: [] })
158
+ ? (0, rejectionPercentageAdjustment_js_1.applyRejectionPercentageAdjustmentRules)(userLayers.additive, input.rules, input.ctx ?? { itemIds: [] })
121
159
  : undefined;
122
- const rejectionPercentage = rejectionCalculations?.finalPercentage ?? passedPercentage;
160
+ const chargedLayers = {
161
+ additive: rejectionCalculations?.finalPercentage ?? userLayers.additive,
162
+ successive: userLayers.successive,
163
+ };
164
+ const rejectionPercentage = (0, rejectionPercentageAdjustment_js_1.computeEffectiveRejectionPercentage)(chargedLayers);
123
165
  return {
124
166
  subtotal: safeSubtotal,
125
- discount: rejectionPercentage > 0 && safeSubtotal > 0
167
+ discount: rejectionPercentage !== 0 && safeSubtotal > 0
126
168
  ? roundMoney(safeSubtotal * (rejectionPercentage / 100))
127
169
  : 0,
128
170
  rejectionPercentage,
171
+ rejectionLayers: userLayers,
129
172
  rejectionCalculations,
130
173
  };
131
174
  };
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyRejectionPercentageAdjustmentRules = exports.resolveRejectionPercentageOrigin = exports.formatRejectionPercentageAdjustmentDescription = exports.rejectionPercentageAdjustmentRuleMatches = exports.filterActiveRejectionPercentageAdjustmentRules = exports.REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = void 0;
3
+ exports.applyRejectionPercentageAdjustmentRules = exports.isAutomaticRejectionRuleHistoryType = exports.formatAutomaticRejectionRuleHistoryDescription = exports.getNewlyAppliedRejectionRuleSteps = exports.rejectionRuleStepKey = exports.resolveRejectionPercentageOrigin = exports.formatRejectionPercentageAdjustmentDescription = exports.hasSuccessiveRejectionLayers = exports.computeEffectiveRejectionPercentage = exports.applyRejectionPercentageLayers = exports.normalizeRejectionPercentageLayers = exports.rejectionPercentageAdjustmentRuleMatches = exports.filterActiveRejectionPercentageAdjustmentRules = exports.REJECTION_PERCENTAGE_RULE_HISTORY_TYPE = exports.REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = void 0;
4
4
  exports.REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = "rejection_percentage_adjustment_rules_web_app_table_id";
5
+ exports.REJECTION_PERCENTAGE_RULE_HISTORY_TYPE = "rechazo_regla_automatica";
5
6
  const defaultFilters = () => ({
6
7
  itemMode: "all",
7
8
  itemIds: [],
@@ -56,6 +57,58 @@ const roundPercentage = (value, digits = 4) => {
56
57
  return Math.round((value + Number.EPSILON) * factor) / factor;
57
58
  };
58
59
  const clampPercentage = (value) => Math.min(100, Math.max(0, value));
60
+ const toLayerNumber = (value) => {
61
+ const parsed = Number(value);
62
+ return Number.isFinite(parsed) ? parsed : 0;
63
+ };
64
+ const normalizeRejectionPercentageLayers = (layers) => ({
65
+ additive: clampPercentage(roundPercentage(toLayerNumber(layers?.additive))),
66
+ successive: Array.isArray(layers?.successive)
67
+ ? layers.successive.map((value) => toLayerNumber(value))
68
+ : [],
69
+ });
70
+ exports.normalizeRejectionPercentageLayers = normalizeRejectionPercentageLayers;
71
+ const applyRejectionPercentageLayers = (layers, application) => {
72
+ const current = (0, exports.normalizeRejectionPercentageLayers)(layers);
73
+ const value = toLayerNumber(application.value);
74
+ switch (application.mode) {
75
+ case "add":
76
+ return {
77
+ additive: clampPercentage(roundPercentage(current.additive + value)),
78
+ successive: [...current.successive],
79
+ };
80
+ case "replace":
81
+ return {
82
+ additive: clampPercentage(roundPercentage(value)),
83
+ successive: [],
84
+ };
85
+ case "successive":
86
+ return {
87
+ additive: current.additive,
88
+ successive: [...current.successive, value],
89
+ };
90
+ default: {
91
+ const _exhaustive = application.mode;
92
+ return _exhaustive;
93
+ }
94
+ }
95
+ };
96
+ exports.applyRejectionPercentageLayers = applyRejectionPercentageLayers;
97
+ /**
98
+ * Effective rejection % after additive then each successive rate on the remainder.
99
+ * 10% then 10% successive → 19, not 20.
100
+ */
101
+ const computeEffectiveRejectionPercentage = (layers, digits = 2) => {
102
+ const normalized = (0, exports.normalizeRejectionPercentageLayers)(layers);
103
+ let remainingFactor = 1 - normalized.additive / 100;
104
+ for (const rate of normalized.successive) {
105
+ remainingFactor *= 1 - rate / 100;
106
+ }
107
+ return roundPercentage((1 - remainingFactor) * 100, digits);
108
+ };
109
+ exports.computeEffectiveRejectionPercentage = computeEffectiveRejectionPercentage;
110
+ const hasSuccessiveRejectionLayers = (layers) => Array.isArray(layers?.successive) && layers.successive.length > 0;
111
+ exports.hasSuccessiveRejectionLayers = hasSuccessiveRejectionLayers;
59
112
  const formatPercentage = (value) => Number.isInteger(value) ? String(value) : String(roundPercentage(value));
60
113
  const formatFinalPercentage = (value) => roundPercentage(value, 2).toFixed(2);
61
114
  const formatRuleEffect = (step) => {
@@ -91,6 +144,27 @@ const resolveRejectionPercentageOrigin = (passedPercentage, previousCalculations
91
144
  return clampPercentage(safePreviousBase + (passed - previousFinal));
92
145
  };
93
146
  exports.resolveRejectionPercentageOrigin = resolveRejectionPercentageOrigin;
147
+ const rejectionRuleStepKey = (step) => `${step.ruleName}|${step.operation}|${step.value}`;
148
+ exports.rejectionRuleStepKey = rejectionRuleStepKey;
149
+ /**
150
+ * Rules already stored in the memo are skipped so a later edit does not
151
+ * create a second history line (or a second +2%) for the same rule.
152
+ */
153
+ const getNewlyAppliedRejectionRuleSteps = (previous, next) => {
154
+ if (!next?.steps?.length) {
155
+ return [];
156
+ }
157
+ const previousKeys = new Set((previous?.steps ?? []).map(exports.rejectionRuleStepKey));
158
+ return next.steps.filter((step) => !previousKeys.has((0, exports.rejectionRuleStepKey)(step)));
159
+ };
160
+ exports.getNewlyAppliedRejectionRuleSteps = getNewlyAppliedRejectionRuleSteps;
161
+ const formatAutomaticRejectionRuleHistoryDescription = (step) => {
162
+ const sign = step.operation === "subtract" ? "-" : "+";
163
+ return `% Rechazo aplicado automáticamente por regla "${step.ruleName}": ${sign}${formatPercentage(step.value)}%.`;
164
+ };
165
+ exports.formatAutomaticRejectionRuleHistoryDescription = formatAutomaticRejectionRuleHistoryDescription;
166
+ const isAutomaticRejectionRuleHistoryType = (type) => type === exports.REJECTION_PERCENTAGE_RULE_HISTORY_TYPE;
167
+ exports.isAutomaticRejectionRuleHistoryType = isAutomaticRejectionRuleHistoryType;
94
168
  const applyRejectionPercentageAdjustmentRules = (basePercentage, rules, ctx) => {
95
169
  const start = Number(basePercentage);
96
170
  const safeBase = Number.isFinite(start) ? clampPercentage(start) : 0;
@@ -1,11 +1,28 @@
1
- import type { RejectionCalculationMemo, RejectionPercentageAdjustmentContext, RejectionPercentageAdjustmentResult, RejectionPercentageAdjustmentRule, WebAppRowGraphQL } from "@zauru-sdk/types";
1
+ import type { RejectionCalculationMemo, RejectionPercentageAdjustmentContext, RejectionPercentageAdjustmentResult, RejectionPercentageAdjustmentRule, RejectionPercentageAdjustmentStep, RejectionPercentageApplication, RejectionPercentageLayers, WebAppRowGraphQL } from "@zauru-sdk/types";
2
2
  export declare const REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = "rejection_percentage_adjustment_rules_web_app_table_id";
3
+ export declare const REJECTION_PERCENTAGE_RULE_HISTORY_TYPE: "rechazo_regla_automatica";
3
4
  export declare const filterActiveRejectionPercentageAdjustmentRules: (rules?: WebAppRowGraphQL<RejectionPercentageAdjustmentRule>[]) => WebAppRowGraphQL<RejectionPercentageAdjustmentRule>[];
4
5
  export declare const rejectionPercentageAdjustmentRuleMatches: (rule: RejectionPercentageAdjustmentRule, ctx: RejectionPercentageAdjustmentContext) => boolean;
6
+ export declare const normalizeRejectionPercentageLayers: (layers?: Partial<RejectionPercentageLayers> | null) => RejectionPercentageLayers;
7
+ export declare const applyRejectionPercentageLayers: (layers: RejectionPercentageLayers, application: RejectionPercentageApplication) => RejectionPercentageLayers;
8
+ /**
9
+ * Effective rejection % after additive then each successive rate on the remainder.
10
+ * 10% then 10% successive → 19, not 20.
11
+ */
12
+ export declare const computeEffectiveRejectionPercentage: (layers: RejectionPercentageLayers, digits?: number) => number;
13
+ export declare const hasSuccessiveRejectionLayers: (layers?: Partial<RejectionPercentageLayers> | null) => boolean;
5
14
  export declare const formatRejectionPercentageAdjustmentDescription: (result: Pick<RejectionPercentageAdjustmentResult, "basePercentage" | "finalPercentage" | "steps">, baseLabel?: string) => string;
6
15
  /**
7
16
  * Callers may pass the origin % or the previously stored final %. With the
8
17
  * previous calculations we go back to the origin so the rules never stack twice.
9
18
  */
10
19
  export declare const resolveRejectionPercentageOrigin: (passedPercentage: number, previousCalculations?: RejectionCalculationMemo | null) => number;
20
+ export declare const rejectionRuleStepKey: (step: Pick<RejectionPercentageAdjustmentStep, "ruleName" | "operation" | "value">) => string;
21
+ /**
22
+ * Rules already stored in the memo are skipped so a later edit does not
23
+ * create a second history line (or a second +2%) for the same rule.
24
+ */
25
+ export declare const getNewlyAppliedRejectionRuleSteps: (previous?: RejectionCalculationMemo | null, next?: RejectionCalculationMemo | null) => RejectionPercentageAdjustmentStep[];
26
+ export declare const formatAutomaticRejectionRuleHistoryDescription: (step: RejectionPercentageAdjustmentStep) => string;
27
+ export declare const isAutomaticRejectionRuleHistoryType: (type: string | undefined) => boolean;
11
28
  export declare const applyRejectionPercentageAdjustmentRules: (basePercentage: number, rules: Array<RejectionPercentageAdjustmentRule | WebAppRowGraphQL<RejectionPercentageAdjustmentRule>>, ctx: RejectionPercentageAdjustmentContext) => RejectionPercentageAdjustmentResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/common",
3
- "version": "6.6.0",
3
+ "version": "6.8.0",
4
4
  "description": "Funciones de uso común en el desarrollo de webapps en Zauru",
5
5
  "main": "./dist/esm/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -21,9 +21,9 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@remix-run/node": "^2.8.1",
24
- "@zauru-sdk/types": "^6.16.0",
24
+ "@zauru-sdk/types": "^6.18.0",
25
25
  "moment": "^2.30.1",
26
26
  "moment-timezone": "^0.5.45"
27
27
  },
28
- "gitHead": "9e840d01a540f2ab49233dda2cb2b0c916ffba0b"
28
+ "gitHead": "c5b785baa29fdd2ab7f42a9077ba5cbe5c7c9707"
29
29
  }
package/src/common.ts CHANGED
@@ -10,10 +10,15 @@ import type {
10
10
  CalculatePurchaseOrderFinancialsInput,
11
11
  CalculatePurchaseOrderFinancialsResult,
12
12
  PurchaseOrderFinancialDetail,
13
+ RejectionPercentageLayers,
13
14
  } from "@zauru-sdk/types";
14
15
  import { MONTHS } from "@zauru-sdk/types";
15
16
  import {
16
17
  applyRejectionPercentageAdjustmentRules,
18
+ applyRejectionPercentageLayers,
19
+ computeEffectiveRejectionPercentage,
20
+ hasSuccessiveRejectionLayers,
21
+ normalizeRejectionPercentageLayers,
17
22
  resolveRejectionPercentageOrigin,
18
23
  } from "./rejectionPercentageAdjustment.js";
19
24
 
@@ -89,17 +94,58 @@ export const setRejectionPercentage = (
89
94
  percentage: number,
90
95
  ): string => mergeJsonMemo(memo, { rejectionPercentage: percentage });
91
96
 
97
+ const memoFromRejectionSource = (
98
+ source: string | { memo?: string | object } | JsonMemoType | undefined,
99
+ ): JsonMemoType => {
100
+ if (source == null || source === "") return {};
101
+ if (typeof source === "string") return parseJsonMemo(source);
102
+ if (typeof source !== "object") return {};
103
+ const record = source as { memo?: string | object } & JsonMemoType;
104
+ if (record.memo != null && record.memo !== "") {
105
+ return parseJsonMemo(record.memo);
106
+ }
107
+ return parseJsonMemo(record);
108
+ };
109
+
110
+ /**
111
+ * User rejection layers (before automatic rules). Legacy memos without
112
+ * `rejectionLayers` use the stored origin as additive and no successive rates.
113
+ */
114
+ export const resolveRejectionPercentageLayers = (
115
+ memo?: string | object | JsonMemoType,
116
+ fallbackAdditive?: number,
117
+ ): RejectionPercentageLayers => {
118
+ const parsed = parseJsonMemo(memo);
119
+ if (parsed.rejectionLayers) {
120
+ return normalizeRejectionPercentageLayers(parsed.rejectionLayers);
121
+ }
122
+
123
+ const origin = resolveRejectionPercentageOrigin(
124
+ fallbackAdditive ?? getRejectionPercentage(parsed),
125
+ parsed.rejectionCalculations,
126
+ );
127
+ return normalizeRejectionPercentageLayers({
128
+ additive: origin,
129
+ successive: [],
130
+ });
131
+ };
132
+
133
+ export const getRejectionPercentageLayers = (
134
+ source: string | { memo?: string | object } | JsonMemoType | undefined,
135
+ ): RejectionPercentageLayers =>
136
+ resolveRejectionPercentageLayers(memoFromRejectionSource(source));
137
+
138
+ export const hasSuccessiveRejectionPercentage = (
139
+ source: string | { memo?: string | object } | JsonMemoType | undefined,
140
+ ): boolean => hasSuccessiveRejectionLayers(getRejectionPercentageLayers(source));
141
+
92
142
  /**
93
143
  * Rejection % of origin stored in the memo, ignoring the adjustment rules
94
144
  * already applied on top of it.
95
145
  */
96
146
  export const resolveRejectionPercentageBase = (
97
147
  memo?: string | object,
98
- ): number =>
99
- resolveRejectionPercentageOrigin(
100
- getRejectionPercentage(memo),
101
- parseJsonMemo(memo).rejectionCalculations,
102
- );
148
+ ): number => resolveRejectionPercentageLayers(memo).additive;
103
149
 
104
150
  const roundMoney = (value: number): number =>
105
151
  Math.round((value + Number.EPSILON) * 100) / 100;
@@ -119,14 +165,13 @@ const lineQuantity = (detail: PurchaseOrderFinancialDetail): number => {
119
165
  };
120
166
 
121
167
  /**
122
- * Header money for a purchase order: subtotal = qty × unit_cost,
123
- * discount = subtotal × rejectionPercentage / 100.
168
+ * Header money for a purchase order: subtotal = qty × unit_cost.
169
+ * Additive rates are summed first; successive rates then apply to the remainder.
124
170
  * Does not change unit cost. purchase_orders.discount is the monetary column.
125
171
  *
126
- * If `rules` is given, the rejection % is resolved back to the origin stored in
127
- * the memo and the rules are stacked on top of it before calculating the money.
128
- * `rejectionPercentage` in the result is the % that was actually charged and
129
- * `rejectionCalculations` is the trace to store in the memo.
172
+ * User layers (before rules) are returned as `rejectionLayers` for the memo.
173
+ * Automatic rules add percentage points on the additive block only.
174
+ * `rejectionPercentage` in the result is the effective % that was charged.
130
175
  */
131
176
  export const calculatePurchaseOrderFinancials = (
132
177
  input: CalculatePurchaseOrderFinancialsInput,
@@ -140,28 +185,45 @@ export const calculatePurchaseOrderFinancials = (
140
185
  );
141
186
  const safeSubtotal = Number.isFinite(subtotal) ? subtotal : 0;
142
187
 
143
- const passedPercentage = toFiniteNumber(input.rejectionPercentage) ?? 0;
188
+ const parsedMemo = parseJsonMemo(input.memo);
189
+ let userLayers = input.rejectionLayers
190
+ ? normalizeRejectionPercentageLayers(input.rejectionLayers)
191
+ : resolveRejectionPercentageLayers(
192
+ parsedMemo,
193
+ toFiniteNumber(input.rejectionPercentage),
194
+ );
195
+
196
+ if (input.rejectionApplication) {
197
+ userLayers = applyRejectionPercentageLayers(
198
+ userLayers,
199
+ input.rejectionApplication,
200
+ );
201
+ }
202
+
144
203
  const rejectionCalculations = input.rules
145
204
  ? applyRejectionPercentageAdjustmentRules(
146
- resolveRejectionPercentageOrigin(
147
- passedPercentage,
148
- parseJsonMemo(input.memo).rejectionCalculations,
149
- ),
205
+ userLayers.additive,
150
206
  input.rules,
151
207
  input.ctx ?? { itemIds: [] },
152
208
  )
153
209
  : undefined;
154
210
 
211
+ const chargedLayers = {
212
+ additive:
213
+ rejectionCalculations?.finalPercentage ?? userLayers.additive,
214
+ successive: userLayers.successive,
215
+ };
155
216
  const rejectionPercentage =
156
- rejectionCalculations?.finalPercentage ?? passedPercentage;
217
+ computeEffectiveRejectionPercentage(chargedLayers);
157
218
 
158
219
  return {
159
220
  subtotal: safeSubtotal,
160
221
  discount:
161
- rejectionPercentage > 0 && safeSubtotal > 0
222
+ rejectionPercentage !== 0 && safeSubtotal > 0
162
223
  ? roundMoney(safeSubtotal * (rejectionPercentage / 100))
163
224
  : 0,
164
225
  rejectionPercentage,
226
+ rejectionLayers: userLayers,
165
227
  rejectionCalculations,
166
228
  };
167
229
  };
@@ -6,12 +6,17 @@ import type {
6
6
  RejectionPercentageAdjustmentResult,
7
7
  RejectionPercentageAdjustmentRule,
8
8
  RejectionPercentageAdjustmentStep,
9
+ RejectionPercentageApplication,
10
+ RejectionPercentageLayers,
9
11
  WebAppRowGraphQL,
10
12
  } from "@zauru-sdk/types";
11
13
 
12
14
  export const REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR =
13
15
  "rejection_percentage_adjustment_rules_web_app_table_id";
14
16
 
17
+ export const REJECTION_PERCENTAGE_RULE_HISTORY_TYPE =
18
+ "rechazo_regla_automatica" as const;
19
+
15
20
  const defaultFilters = (): PriceAdjustmentFilters => ({
16
21
  itemMode: "all",
17
22
  itemIds: [],
@@ -108,6 +113,71 @@ const roundPercentage = (value: number, digits = 4) => {
108
113
 
109
114
  const clampPercentage = (value: number) => Math.min(100, Math.max(0, value));
110
115
 
116
+ const toLayerNumber = (value: unknown): number => {
117
+ const parsed = Number(value);
118
+ return Number.isFinite(parsed) ? parsed : 0;
119
+ };
120
+
121
+ export const normalizeRejectionPercentageLayers = (
122
+ layers?: Partial<RejectionPercentageLayers> | null,
123
+ ): RejectionPercentageLayers => ({
124
+ additive: clampPercentage(roundPercentage(toLayerNumber(layers?.additive))),
125
+ successive: Array.isArray(layers?.successive)
126
+ ? layers.successive.map((value) => toLayerNumber(value))
127
+ : [],
128
+ });
129
+
130
+ export const applyRejectionPercentageLayers = (
131
+ layers: RejectionPercentageLayers,
132
+ application: RejectionPercentageApplication,
133
+ ): RejectionPercentageLayers => {
134
+ const current = normalizeRejectionPercentageLayers(layers);
135
+ const value = toLayerNumber(application.value);
136
+
137
+ switch (application.mode) {
138
+ case "add":
139
+ return {
140
+ additive: clampPercentage(roundPercentage(current.additive + value)),
141
+ successive: [...current.successive],
142
+ };
143
+ case "replace":
144
+ return {
145
+ additive: clampPercentage(roundPercentage(value)),
146
+ successive: [],
147
+ };
148
+ case "successive":
149
+ return {
150
+ additive: current.additive,
151
+ successive: [...current.successive, value],
152
+ };
153
+ default: {
154
+ const _exhaustive: never = application.mode;
155
+ return _exhaustive;
156
+ }
157
+ }
158
+ };
159
+
160
+ /**
161
+ * Effective rejection % after additive then each successive rate on the remainder.
162
+ * 10% then 10% successive → 19, not 20.
163
+ */
164
+ export const computeEffectiveRejectionPercentage = (
165
+ layers: RejectionPercentageLayers,
166
+ digits = 2,
167
+ ): number => {
168
+ const normalized = normalizeRejectionPercentageLayers(layers);
169
+ let remainingFactor = 1 - normalized.additive / 100;
170
+ for (const rate of normalized.successive) {
171
+ remainingFactor *= 1 - rate / 100;
172
+ }
173
+ return roundPercentage((1 - remainingFactor) * 100, digits);
174
+ };
175
+
176
+ export const hasSuccessiveRejectionLayers = (
177
+ layers?: Partial<RejectionPercentageLayers> | null,
178
+ ): boolean =>
179
+ Array.isArray(layers?.successive) && layers.successive.length > 0;
180
+
111
181
  const formatPercentage = (value: number) =>
112
182
  Number.isInteger(value) ? String(value) : String(roundPercentage(value));
113
183
 
@@ -162,6 +232,43 @@ export const resolveRejectionPercentageOrigin = (
162
232
  return clampPercentage(safePreviousBase + (passed - previousFinal));
163
233
  };
164
234
 
235
+ export const rejectionRuleStepKey = (
236
+ step: Pick<
237
+ RejectionPercentageAdjustmentStep,
238
+ "ruleName" | "operation" | "value"
239
+ >,
240
+ ): string => `${step.ruleName}|${step.operation}|${step.value}`;
241
+
242
+ /**
243
+ * Rules already stored in the memo are skipped so a later edit does not
244
+ * create a second history line (or a second +2%) for the same rule.
245
+ */
246
+ export const getNewlyAppliedRejectionRuleSteps = (
247
+ previous?: RejectionCalculationMemo | null,
248
+ next?: RejectionCalculationMemo | null,
249
+ ): RejectionPercentageAdjustmentStep[] => {
250
+ if (!next?.steps?.length) {
251
+ return [];
252
+ }
253
+ const previousKeys = new Set(
254
+ (previous?.steps ?? []).map(rejectionRuleStepKey),
255
+ );
256
+ return next.steps.filter(
257
+ (step) => !previousKeys.has(rejectionRuleStepKey(step)),
258
+ );
259
+ };
260
+
261
+ export const formatAutomaticRejectionRuleHistoryDescription = (
262
+ step: RejectionPercentageAdjustmentStep,
263
+ ): string => {
264
+ const sign = step.operation === "subtract" ? "-" : "+";
265
+ return `% Rechazo aplicado automáticamente por regla "${step.ruleName}": ${sign}${formatPercentage(step.value)}%.`;
266
+ };
267
+
268
+ export const isAutomaticRejectionRuleHistoryType = (
269
+ type: string | undefined,
270
+ ): boolean => type === REJECTION_PERCENTAGE_RULE_HISTORY_TYPE;
271
+
165
272
  export const applyRejectionPercentageAdjustmentRules = (
166
273
  basePercentage: number,
167
274
  rules: Array<