@zauru-sdk/common 6.5.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md 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.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)
7
+
8
+
9
+ ### Features
10
+
11
+ * registrar reglas automáticas de % rechazo en el historial ([4784849](https://github.com/intuitiva/zauru-typescript-sdk/commit/4784849904f9ac6570e60543895a858dece8e039))
12
+
13
+
14
+
15
+
16
+
17
+ # [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)
18
+
19
+
20
+ ### Features
21
+
22
+ * apply rejection percentage rules inside calculatePurchaseOrderFinancials ([e1852ba](https://github.com/intuitiva/zauru-typescript-sdk/commit/e1852ba86e8263df8a365adda884e6b88802a001))
23
+
24
+
25
+
26
+
27
+
6
28
  # [6.5.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.4.1...@zauru-sdk/common@6.5.0) (2026-09-14)
7
29
 
8
30
 
package/dist/common.d.ts CHANGED
@@ -12,10 +12,20 @@ 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
+ * Rejection % of origin stored in the memo, ignoring the adjustment rules
17
+ * already applied on top of it.
18
+ */
19
+ export declare const resolveRejectionPercentageBase: (memo?: string | object) => number;
15
20
  /**
16
21
  * Header money for a purchase order: subtotal = qty × unit_cost,
17
22
  * discount = subtotal × rejectionPercentage / 100.
18
23
  * Does not change unit cost. purchase_orders.discount is the monetary column.
24
+ *
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.
19
29
  */
20
30
  export declare const calculatePurchaseOrderFinancials: (input: CalculatePurchaseOrderFinancialsInput) => CalculatePurchaseOrderFinancialsResult;
21
31
  /**
@@ -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.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.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;
@@ -24,6 +24,7 @@ exports.parseCode = parseCode;
24
24
  const moment_1 = __importDefault(require("moment"));
25
25
  require("moment-timezone");
26
26
  const types_1 = require("@zauru-sdk/types");
27
+ const rejectionPercentageAdjustment_js_1 = require("./rejectionPercentageAdjustment.js");
27
28
  exports.DESTINOS_MUESTRA_OPTIONS = [
28
29
  { label: "Microbiología", value: "microbiologa" },
29
30
  { label: "Residuos de plaguicidas", value: "residuos_de_plaguicidas" },
@@ -80,6 +81,12 @@ const getRejectionPercentage = (source) => {
80
81
  exports.getRejectionPercentage = getRejectionPercentage;
81
82
  const setRejectionPercentage = (memo, percentage) => (0, exports.mergeJsonMemo)(memo, { rejectionPercentage: percentage });
82
83
  exports.setRejectionPercentage = setRejectionPercentage;
84
+ /**
85
+ * Rejection % of origin stored in the memo, ignoring the adjustment rules
86
+ * already applied on top of it.
87
+ */
88
+ const resolveRejectionPercentageBase = (memo) => (0, rejectionPercentageAdjustment_js_1.resolveRejectionPercentageOrigin)((0, exports.getRejectionPercentage)(memo), (0, exports.parseJsonMemo)(memo).rejectionCalculations);
89
+ exports.resolveRejectionPercentageBase = resolveRejectionPercentageBase;
83
90
  const roundMoney = (value) => Math.round((value + Number.EPSILON) * 100) / 100;
84
91
  const lineQuantity = (detail) => {
85
92
  const delivered = toFiniteNumber(detail.delivered_quantity) ??
@@ -95,6 +102,11 @@ const lineQuantity = (detail) => {
95
102
  * Header money for a purchase order: subtotal = qty × unit_cost,
96
103
  * discount = subtotal × rejectionPercentage / 100.
97
104
  * Does not change unit cost. purchase_orders.discount is the monetary column.
105
+ *
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.
98
110
  */
99
111
  const calculatePurchaseOrderFinancials = (input) => {
100
112
  const details = input.details ?? [];
@@ -102,16 +114,19 @@ const calculatePurchaseOrderFinancials = (input) => {
102
114
  const unitCost = toFiniteNumber(detail.unit_cost) ?? 0;
103
115
  return sum + lineQuantity(detail) * unitCost;
104
116
  }, 0));
105
- const rejectionPercentage = toFiniteNumber(input.rejectionPercentage);
106
- if (rejectionPercentage == null ||
107
- rejectionPercentage <= 0 ||
108
- !Number.isFinite(subtotal) ||
109
- subtotal <= 0) {
110
- return { subtotal: Number.isFinite(subtotal) ? subtotal : 0, discount: 0 };
111
- }
117
+ const safeSubtotal = Number.isFinite(subtotal) ? subtotal : 0;
118
+ const passedPercentage = toFiniteNumber(input.rejectionPercentage) ?? 0;
119
+ 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: [] })
121
+ : undefined;
122
+ const rejectionPercentage = rejectionCalculations?.finalPercentage ?? passedPercentage;
112
123
  return {
113
- subtotal,
114
- discount: roundMoney(subtotal * (rejectionPercentage / 100)),
124
+ subtotal: safeSubtotal,
125
+ discount: rejectionPercentage > 0 && safeSubtotal > 0
126
+ ? roundMoney(safeSubtotal * (rejectionPercentage / 100))
127
+ : 0,
128
+ rejectionPercentage,
129
+ rejectionCalculations,
115
130
  };
116
131
  };
117
132
  exports.calculatePurchaseOrderFinancials = calculatePurchaseOrderFinancials;
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyRejectionPercentageRulesToFinancials = exports.applyRejectionPercentageAdjustmentRules = exports.resolveRejectionPercentageBase = exports.resolveRejectionPercentageOrigin = exports.formatRejectionPercentageAdjustmentDescription = exports.rejectionPercentageAdjustmentRuleMatches = exports.filterActiveRejectionPercentageAdjustmentRules = exports.REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = void 0;
4
- const common_js_1 = require("./common.js");
3
+ exports.applyRejectionPercentageAdjustmentRules = exports.isAutomaticRejectionRuleHistoryType = exports.formatAutomaticRejectionRuleHistoryDescription = exports.getNewlyAppliedRejectionRuleSteps = exports.rejectionRuleStepKey = exports.resolveRejectionPercentageOrigin = exports.formatRejectionPercentageAdjustmentDescription = exports.rejectionPercentageAdjustmentRuleMatches = exports.filterActiveRejectionPercentageAdjustmentRules = exports.REJECTION_PERCENTAGE_RULE_HISTORY_TYPE = exports.REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR = void 0;
5
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";
6
6
  const defaultFilters = () => ({
7
7
  itemMode: "all",
8
8
  itemIds: [],
@@ -71,10 +71,13 @@ const formatRejectionPercentageAdjustmentDescription = (result, baseLabel = "ori
71
71
  return `${base} ${result.steps.map(formatRuleEffect).join(" ")} = ${formatFinalPercentage(result.finalPercentage)}`;
72
72
  };
73
73
  exports.formatRejectionPercentageAdjustmentDescription = formatRejectionPercentageAdjustmentDescription;
74
- const resolveRejectionPercentageOrigin = (passedPercentage, memo) => {
75
- const parsed = (0, common_js_1.parseJsonMemo)(memo);
74
+ /**
75
+ * Callers may pass the origin % or the previously stored final %. With the
76
+ * previous calculations we go back to the origin so the rules never stack twice.
77
+ */
78
+ const resolveRejectionPercentageOrigin = (passedPercentage, previousCalculations) => {
76
79
  const passed = clampPercentage(Number.isFinite(Number(passedPercentage)) ? Number(passedPercentage) : 0);
77
- const calc = parsed.rejectionCalculations;
80
+ const calc = previousCalculations;
78
81
  if (!calc || !Number.isFinite(Number(calc.finalPercentage))) {
79
82
  return passed;
80
83
  }
@@ -89,8 +92,27 @@ const resolveRejectionPercentageOrigin = (passedPercentage, memo) => {
89
92
  return clampPercentage(safePreviousBase + (passed - previousFinal));
90
93
  };
91
94
  exports.resolveRejectionPercentageOrigin = resolveRejectionPercentageOrigin;
92
- const resolveRejectionPercentageBase = (memo) => (0, exports.resolveRejectionPercentageOrigin)((0, common_js_1.getRejectionPercentage)(memo), memo);
93
- exports.resolveRejectionPercentageBase = resolveRejectionPercentageBase;
95
+ const rejectionRuleStepKey = (step) => `${step.ruleName}|${step.operation}|${step.value}`;
96
+ exports.rejectionRuleStepKey = rejectionRuleStepKey;
97
+ /**
98
+ * Rules already stored in the memo are skipped so a later edit does not
99
+ * create a second history line (or a second +2%) for the same rule.
100
+ */
101
+ const getNewlyAppliedRejectionRuleSteps = (previous, next) => {
102
+ if (!next?.steps?.length) {
103
+ return [];
104
+ }
105
+ const previousKeys = new Set((previous?.steps ?? []).map(exports.rejectionRuleStepKey));
106
+ return next.steps.filter((step) => !previousKeys.has((0, exports.rejectionRuleStepKey)(step)));
107
+ };
108
+ exports.getNewlyAppliedRejectionRuleSteps = getNewlyAppliedRejectionRuleSteps;
109
+ const formatAutomaticRejectionRuleHistoryDescription = (step) => {
110
+ const sign = step.operation === "subtract" ? "-" : "+";
111
+ return `% Rechazo aplicado automáticamente por regla "${step.ruleName}": ${sign}${formatPercentage(step.value)}%.`;
112
+ };
113
+ exports.formatAutomaticRejectionRuleHistoryDescription = formatAutomaticRejectionRuleHistoryDescription;
114
+ const isAutomaticRejectionRuleHistoryType = (type) => type === exports.REJECTION_PERCENTAGE_RULE_HISTORY_TYPE;
115
+ exports.isAutomaticRejectionRuleHistoryType = isAutomaticRejectionRuleHistoryType;
94
116
  const applyRejectionPercentageAdjustmentRules = (basePercentage, rules, ctx) => {
95
117
  const start = Number(basePercentage);
96
118
  const safeBase = Number.isFinite(start) ? clampPercentage(start) : 0;
@@ -128,25 +150,3 @@ const applyRejectionPercentageAdjustmentRules = (basePercentage, rules, ctx) =>
128
150
  return result;
129
151
  };
130
152
  exports.applyRejectionPercentageAdjustmentRules = applyRejectionPercentageAdjustmentRules;
131
- const applyRejectionPercentageRulesToFinancials = (input) => {
132
- const origin = (0, exports.resolveRejectionPercentageOrigin)(input.originPercentage, input.memo);
133
- const result = (0, exports.applyRejectionPercentageAdjustmentRules)(origin, input.rules, input.ctx);
134
- const { discount } = (0, common_js_1.calculatePurchaseOrderFinancials)({
135
- details: input.details,
136
- rejectionPercentage: result.finalPercentage,
137
- });
138
- return {
139
- memo: (0, common_js_1.mergeJsonMemo)(input.memo, {
140
- rejectionPercentage: result.finalPercentage,
141
- rejectionCalculations: {
142
- basePercentage: result.basePercentage,
143
- finalPercentage: result.finalPercentage,
144
- description: result.description,
145
- steps: result.steps,
146
- },
147
- }),
148
- discount,
149
- finalPercentage: result.finalPercentage,
150
- };
151
- };
152
- exports.applyRejectionPercentageRulesToFinancials = applyRejectionPercentageRulesToFinancials;
@@ -1,19 +1,20 @@
1
- import type { PurchaseOrderFinancialDetail, RejectionPercentageAdjustmentContext, RejectionPercentageAdjustmentResult, RejectionPercentageAdjustmentRule, WebAppRowGraphQL } from "@zauru-sdk/types";
1
+ import type { RejectionCalculationMemo, RejectionPercentageAdjustmentContext, RejectionPercentageAdjustmentResult, RejectionPercentageAdjustmentRule, RejectionPercentageAdjustmentStep, 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;
5
6
  export declare const formatRejectionPercentageAdjustmentDescription: (result: Pick<RejectionPercentageAdjustmentResult, "basePercentage" | "finalPercentage" | "steps">, baseLabel?: string) => string;
6
- export declare const resolveRejectionPercentageOrigin: (passedPercentage: number, memo?: string | object) => number;
7
- export declare const resolveRejectionPercentageBase: (memo?: string | object) => number;
7
+ /**
8
+ * Callers may pass the origin % or the previously stored final %. With the
9
+ * previous calculations we go back to the origin so the rules never stack twice.
10
+ */
11
+ export declare const resolveRejectionPercentageOrigin: (passedPercentage: number, previousCalculations?: RejectionCalculationMemo | null) => number;
12
+ export declare const rejectionRuleStepKey: (step: Pick<RejectionPercentageAdjustmentStep, "ruleName" | "operation" | "value">) => string;
13
+ /**
14
+ * Rules already stored in the memo are skipped so a later edit does not
15
+ * create a second history line (or a second +2%) for the same rule.
16
+ */
17
+ export declare const getNewlyAppliedRejectionRuleSteps: (previous?: RejectionCalculationMemo | null, next?: RejectionCalculationMemo | null) => RejectionPercentageAdjustmentStep[];
18
+ export declare const formatAutomaticRejectionRuleHistoryDescription: (step: RejectionPercentageAdjustmentStep) => string;
19
+ export declare const isAutomaticRejectionRuleHistoryType: (type: string | undefined) => boolean;
8
20
  export declare const applyRejectionPercentageAdjustmentRules: (basePercentage: number, rules: Array<RejectionPercentageAdjustmentRule | WebAppRowGraphQL<RejectionPercentageAdjustmentRule>>, ctx: RejectionPercentageAdjustmentContext) => RejectionPercentageAdjustmentResult;
9
- export declare const applyRejectionPercentageRulesToFinancials: (input: {
10
- memo?: string | object;
11
- originPercentage: number;
12
- details: PurchaseOrderFinancialDetail[];
13
- rules: Array<RejectionPercentageAdjustmentRule | WebAppRowGraphQL<RejectionPercentageAdjustmentRule>>;
14
- ctx: RejectionPercentageAdjustmentContext;
15
- }) => {
16
- memo: string;
17
- discount: number;
18
- finalPercentage: number;
19
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/common",
3
- "version": "6.5.0",
3
+ "version": "6.7.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.15.0",
24
+ "@zauru-sdk/types": "^6.17.0",
25
25
  "moment": "^2.30.1",
26
26
  "moment-timezone": "^0.5.45"
27
27
  },
28
- "gitHead": "2dceb781cae3f4c45ceecb915977c6d142bca6b9"
28
+ "gitHead": "d3483aa3b55a9a692f955e6a56d13f33b70db1c4"
29
29
  }
package/src/common.ts CHANGED
@@ -12,6 +12,10 @@ import type {
12
12
  PurchaseOrderFinancialDetail,
13
13
  } from "@zauru-sdk/types";
14
14
  import { MONTHS } from "@zauru-sdk/types";
15
+ import {
16
+ applyRejectionPercentageAdjustmentRules,
17
+ resolveRejectionPercentageOrigin,
18
+ } from "./rejectionPercentageAdjustment.js";
15
19
 
16
20
  export const DESTINOS_MUESTRA_OPTIONS: SelectFieldOption[] = [
17
21
  { label: "Microbiología", value: "microbiologa" },
@@ -85,6 +89,18 @@ export const setRejectionPercentage = (
85
89
  percentage: number,
86
90
  ): string => mergeJsonMemo(memo, { rejectionPercentage: percentage });
87
91
 
92
+ /**
93
+ * Rejection % of origin stored in the memo, ignoring the adjustment rules
94
+ * already applied on top of it.
95
+ */
96
+ export const resolveRejectionPercentageBase = (
97
+ memo?: string | object,
98
+ ): number =>
99
+ resolveRejectionPercentageOrigin(
100
+ getRejectionPercentage(memo),
101
+ parseJsonMemo(memo).rejectionCalculations,
102
+ );
103
+
88
104
  const roundMoney = (value: number): number =>
89
105
  Math.round((value + Number.EPSILON) * 100) / 100;
90
106
 
@@ -106,6 +122,11 @@ const lineQuantity = (detail: PurchaseOrderFinancialDetail): number => {
106
122
  * Header money for a purchase order: subtotal = qty × unit_cost,
107
123
  * discount = subtotal × rejectionPercentage / 100.
108
124
  * Does not change unit cost. purchase_orders.discount is the monetary column.
125
+ *
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.
109
130
  */
110
131
  export const calculatePurchaseOrderFinancials = (
111
132
  input: CalculatePurchaseOrderFinancialsInput,
@@ -117,20 +138,31 @@ export const calculatePurchaseOrderFinancials = (
117
138
  return sum + lineQuantity(detail) * unitCost;
118
139
  }, 0),
119
140
  );
120
-
121
- const rejectionPercentage = toFiniteNumber(input.rejectionPercentage);
122
- if (
123
- rejectionPercentage == null ||
124
- rejectionPercentage <= 0 ||
125
- !Number.isFinite(subtotal) ||
126
- subtotal <= 0
127
- ) {
128
- return { subtotal: Number.isFinite(subtotal) ? subtotal : 0, discount: 0 };
129
- }
141
+ const safeSubtotal = Number.isFinite(subtotal) ? subtotal : 0;
142
+
143
+ const passedPercentage = toFiniteNumber(input.rejectionPercentage) ?? 0;
144
+ const rejectionCalculations = input.rules
145
+ ? applyRejectionPercentageAdjustmentRules(
146
+ resolveRejectionPercentageOrigin(
147
+ passedPercentage,
148
+ parseJsonMemo(input.memo).rejectionCalculations,
149
+ ),
150
+ input.rules,
151
+ input.ctx ?? { itemIds: [] },
152
+ )
153
+ : undefined;
154
+
155
+ const rejectionPercentage =
156
+ rejectionCalculations?.finalPercentage ?? passedPercentage;
130
157
 
131
158
  return {
132
- subtotal,
133
- discount: roundMoney(subtotal * (rejectionPercentage / 100)),
159
+ subtotal: safeSubtotal,
160
+ discount:
161
+ rejectionPercentage > 0 && safeSubtotal > 0
162
+ ? roundMoney(safeSubtotal * (rejectionPercentage / 100))
163
+ : 0,
164
+ rejectionPercentage,
165
+ rejectionCalculations,
134
166
  };
135
167
  };
136
168
 
@@ -1,23 +1,20 @@
1
1
  import type {
2
2
  PriceAdjustmentFilterMode,
3
3
  PriceAdjustmentFilters,
4
- PurchaseOrderFinancialDetail,
4
+ RejectionCalculationMemo,
5
5
  RejectionPercentageAdjustmentContext,
6
6
  RejectionPercentageAdjustmentResult,
7
7
  RejectionPercentageAdjustmentRule,
8
8
  RejectionPercentageAdjustmentStep,
9
9
  WebAppRowGraphQL,
10
10
  } from "@zauru-sdk/types";
11
- import {
12
- calculatePurchaseOrderFinancials,
13
- getRejectionPercentage,
14
- mergeJsonMemo,
15
- parseJsonMemo,
16
- } from "./common.js";
17
11
 
18
12
  export const REJECTION_PERCENTAGE_ADJUSTMENT_RULES_TABLE_VAR =
19
13
  "rejection_percentage_adjustment_rules_web_app_table_id";
20
14
 
15
+ export const REJECTION_PERCENTAGE_RULE_HISTORY_TYPE =
16
+ "rechazo_regla_automatica" as const;
17
+
21
18
  const defaultFilters = (): PriceAdjustmentFilters => ({
22
19
  itemMode: "all",
23
20
  itemIds: [],
@@ -139,15 +136,18 @@ export const formatRejectionPercentageAdjustmentDescription = (
139
136
  return `${base} ${result.steps.map(formatRuleEffect).join(" ")} = ${formatFinalPercentage(result.finalPercentage)}`;
140
137
  };
141
138
 
139
+ /**
140
+ * Callers may pass the origin % or the previously stored final %. With the
141
+ * previous calculations we go back to the origin so the rules never stack twice.
142
+ */
142
143
  export const resolveRejectionPercentageOrigin = (
143
144
  passedPercentage: number,
144
- memo?: string | object,
145
+ previousCalculations?: RejectionCalculationMemo | null,
145
146
  ): number => {
146
- const parsed = parseJsonMemo(memo);
147
147
  const passed = clampPercentage(
148
148
  Number.isFinite(Number(passedPercentage)) ? Number(passedPercentage) : 0,
149
149
  );
150
- const calc = parsed.rejectionCalculations;
150
+ const calc = previousCalculations;
151
151
  if (!calc || !Number.isFinite(Number(calc.finalPercentage))) {
152
152
  return passed;
153
153
  }
@@ -165,10 +165,42 @@ export const resolveRejectionPercentageOrigin = (
165
165
  return clampPercentage(safePreviousBase + (passed - previousFinal));
166
166
  };
167
167
 
168
- export const resolveRejectionPercentageBase = (
169
- memo?: string | object,
170
- ): number =>
171
- resolveRejectionPercentageOrigin(getRejectionPercentage(memo), memo);
168
+ export const rejectionRuleStepKey = (
169
+ step: Pick<
170
+ RejectionPercentageAdjustmentStep,
171
+ "ruleName" | "operation" | "value"
172
+ >,
173
+ ): string => `${step.ruleName}|${step.operation}|${step.value}`;
174
+
175
+ /**
176
+ * Rules already stored in the memo are skipped so a later edit does not
177
+ * create a second history line (or a second +2%) for the same rule.
178
+ */
179
+ export const getNewlyAppliedRejectionRuleSteps = (
180
+ previous?: RejectionCalculationMemo | null,
181
+ next?: RejectionCalculationMemo | null,
182
+ ): RejectionPercentageAdjustmentStep[] => {
183
+ if (!next?.steps?.length) {
184
+ return [];
185
+ }
186
+ const previousKeys = new Set(
187
+ (previous?.steps ?? []).map(rejectionRuleStepKey),
188
+ );
189
+ return next.steps.filter(
190
+ (step) => !previousKeys.has(rejectionRuleStepKey(step)),
191
+ );
192
+ };
193
+
194
+ export const formatAutomaticRejectionRuleHistoryDescription = (
195
+ step: RejectionPercentageAdjustmentStep,
196
+ ): string => {
197
+ const sign = step.operation === "subtract" ? "-" : "+";
198
+ return `% Rechazo aplicado automáticamente por regla "${step.ruleName}": ${sign}${formatPercentage(step.value)}%.`;
199
+ };
200
+
201
+ export const isAutomaticRejectionRuleHistoryType = (
202
+ type: string | undefined,
203
+ ): boolean => type === REJECTION_PERCENTAGE_RULE_HISTORY_TYPE;
172
204
 
173
205
  export const applyRejectionPercentageAdjustmentRules = (
174
206
  basePercentage: number,
@@ -217,42 +249,3 @@ export const applyRejectionPercentageAdjustmentRules = (
217
249
  result.description = formatRejectionPercentageAdjustmentDescription(result);
218
250
  return result;
219
251
  };
220
-
221
- export const applyRejectionPercentageRulesToFinancials = (input: {
222
- memo?: string | object;
223
- originPercentage: number;
224
- details: PurchaseOrderFinancialDetail[];
225
- rules: Array<
226
- | RejectionPercentageAdjustmentRule
227
- | WebAppRowGraphQL<RejectionPercentageAdjustmentRule>
228
- >;
229
- ctx: RejectionPercentageAdjustmentContext;
230
- }): { memo: string; discount: number; finalPercentage: number } => {
231
- const origin = resolveRejectionPercentageOrigin(
232
- input.originPercentage,
233
- input.memo,
234
- );
235
- const result = applyRejectionPercentageAdjustmentRules(
236
- origin,
237
- input.rules,
238
- input.ctx,
239
- );
240
- const { discount } = calculatePurchaseOrderFinancials({
241
- details: input.details,
242
- rejectionPercentage: result.finalPercentage,
243
- });
244
-
245
- return {
246
- memo: mergeJsonMemo(input.memo, {
247
- rejectionPercentage: result.finalPercentage,
248
- rejectionCalculations: {
249
- basePercentage: result.basePercentage,
250
- finalPercentage: result.finalPercentage,
251
- description: result.description,
252
- steps: result.steps,
253
- },
254
- }),
255
- discount,
256
- finalPercentage: result.finalPercentage,
257
- };
258
- };