@zauru-sdk/common 6.11.1 → 6.13.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,31 @@
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.13.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.12.0...@zauru-sdk/common@6.13.0) (2026-10-29)
7
+
8
+
9
+ ### Features
10
+
11
+ * armar cost de línea como qty × unit_cost ([707d6b25](https://github.com/intuitiva/zauru-typescript-sdk/commit/707d6b25))
12
+
13
+
14
+ # [6.12.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.10.1...@zauru-sdk/common@6.12.0) (2026-10-26)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * rechazar restricciones que dejan al proveedor sin opciones ([24e6609](https://github.com/intuitiva/zauru-typescript-sdk/commit/24e6609b9cbf1498bec52bbeae9cd52a57945b45))
20
+
21
+
22
+ ### Features
23
+
24
+ * agregar matriz de restricciones de recepción ([11a07b0](https://github.com/intuitiva/zauru-typescript-sdk/commit/11a07b04aa71dd6a38b79f10c4209d08e8f7607c))
25
+ * motor de restricciones de tipos y verduras por proveedor ([ea09fd6](https://github.com/intuitiva/zauru-typescript-sdk/commit/ea09fd6a4cbed61fc29640baf1bc2b0b78db18da))
26
+
27
+
28
+
29
+
30
+
6
31
  # [6.11.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.10.1...@zauru-sdk/common@6.11.0) (2026-10-23)
7
32
 
8
33
 
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, RejectionPercentageLayers } from "@zauru-sdk/types";
2
+ import type { PayeeGraphQL, AxiosUtilsResponse, BasketSchema, SelectFieldOption, JsonMemoType, CalculatePurchaseOrderFinancialsInput, CalculatePurchaseOrderFinancialsResult, PurchaseOrderDetailCostAttribute, PurchaseOrderFinancialDetail, 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;
@@ -38,6 +38,11 @@ export declare const resolveRejectionPercentageBase: (memo?: string | object) =>
38
38
  * `rejectionPercentage` in the result is the effective % that was charged.
39
39
  */
40
40
  export declare const calculatePurchaseOrderFinancials: (input: CalculatePurchaseOrderFinancialsInput) => CalculatePurchaseOrderFinancialsResult;
41
+ /**
42
+ * Nested attributes for Zauru: `cost` must be sent with `unit_cost`.
43
+ * Baculo does not recalculate `purchase_order_details.cost` on save.
44
+ */
45
+ export declare const buildPurchaseOrderDetailsCostAttributes: (details: PurchaseOrderFinancialDetail[] | null | undefined, unitCostByDetailId?: Map<number, number>) => Record<string, PurchaseOrderDetailCostAttribute>;
41
46
  /**
42
47
  * Obtener el objeto de canastas en base al memo
43
48
  * @param memo
@@ -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.hasSuccessiveRejectionPercentage = exports.getRejectionPercentageLayers = exports.resolveRejectionPercentageLayers = 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.buildPurchaseOrderDetailsCostAttributes = 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;
@@ -174,6 +174,30 @@ const calculatePurchaseOrderFinancials = (input) => {
174
174
  };
175
175
  };
176
176
  exports.calculatePurchaseOrderFinancials = calculatePurchaseOrderFinancials;
177
+ /**
178
+ * Nested attributes for Zauru: `cost` must be sent with `unit_cost`.
179
+ * Baculo does not recalculate `purchase_order_details.cost` on save.
180
+ */
181
+ const buildPurchaseOrderDetailsCostAttributes = (details, unitCostByDetailId) => {
182
+ const attributes = {};
183
+ let index = 0;
184
+ for (const detail of details ?? []) {
185
+ const id = toFiniteNumber(detail.id);
186
+ const itemId = toFiniteNumber(detail.item_id);
187
+ if (id == null || itemId == null)
188
+ continue;
189
+ const unitCost = unitCostByDetailId?.get(id) ?? toFiniteNumber(detail.unit_cost) ?? 0;
190
+ attributes[String(index)] = {
191
+ id,
192
+ item_id: itemId,
193
+ unit_cost: unitCost,
194
+ cost: roundMoney(lineQuantity(detail) * unitCost),
195
+ };
196
+ index += 1;
197
+ }
198
+ return attributes;
199
+ };
200
+ exports.buildPurchaseOrderDetailsCostAttributes = buildPurchaseOrderDetailsCostAttributes;
177
201
  /**
178
202
  * Obtener el objeto de canastas en base al memo
179
203
  * @param memo
package/dist/esm/index.js CHANGED
@@ -19,4 +19,5 @@ __exportStar(require("./gtZones.js"), exports);
19
19
  __exportStar(require("./httpClientPolicy.js"), exports);
20
20
  __exportStar(require("./oauthErrors.js"), exports);
21
21
  __exportStar(require("./providerRestrictions.js"), exports);
22
+ __exportStar(require("./providerReceptionMatrix.js"), exports);
22
23
  __exportStar(require("./rejectionPercentageAdjustment.js"), exports);
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluateProviderReceptionMatrix = exports.validateProviderReceptionException = exports.providerReceptionMatrixLeavesNoAllowedCombination = exports.resolveAllowedProviderReceptionTypes = exports.resolveAllowedProviderReceptionItems = exports.PROVIDER_RECEPTION_EXCEPTIONS_TABLE_VAR = exports.PROVIDER_RECEPTION_MATRIX_TABLE_VAR = void 0;
4
+ const providerRestrictions_js_1 = require("./providerRestrictions.js");
5
+ exports.PROVIDER_RECEPTION_MATRIX_TABLE_VAR = "ctl_calidad_admin_matriz_recepcion_proveedor_web_app_table_id";
6
+ exports.PROVIDER_RECEPTION_EXCEPTIONS_TABLE_VAR = "ctl_calidad_admin_excepciones_recepcion_proveedor_web_app_table_id";
7
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
8
+ const isActive = (row, date) => {
9
+ if ((0, providerRestrictions_js_1.normalizeRestrictionValue)(row.fechaEliminacion) || !DATE_RE.test(date)) {
10
+ return false;
11
+ }
12
+ const start = (0, providerRestrictions_js_1.normalizeRestrictionValue)(row.start_date);
13
+ const end = (0, providerRestrictions_js_1.normalizeRestrictionValue)(row.end_date);
14
+ return DATE_RE.test(start) && DATE_RE.test(end) && start <= date && date <= end;
15
+ };
16
+ const matchesProviderAndType = (row, payeeId, receptionTypeCode) => Number(row.payee_id) === Number(payeeId) &&
17
+ (0, providerRestrictions_js_1.normalizeRestrictionValue)(row.reception_type_code) ===
18
+ (0, providerRestrictions_js_1.normalizeRestrictionValue)(receptionTypeCode);
19
+ const resolveAllowedProviderReceptionItems = (input) => {
20
+ const universe = input.universe
21
+ .map(providerRestrictions_js_1.normalizeRestrictionValue)
22
+ .filter(Boolean);
23
+ const universeSet = new Set(universe);
24
+ const blocked = new Set((input.blockRules ?? [])
25
+ .filter((row) => !(0, providerRestrictions_js_1.normalizeRestrictionValue)(row.fechaEliminacion) &&
26
+ matchesProviderAndType(row, input.payeeId, input.receptionTypeCode))
27
+ .flatMap((row) => (0, providerRestrictions_js_1.normalizeProviderRestrictionValues)(row.blocked_item_ids)));
28
+ const activeExceptions = (input.exceptions ?? []).filter((row) => matchesProviderAndType(row, input.payeeId, input.receptionTypeCode) && isActive(row, input.date));
29
+ const temporarilyAllowed = new Set(activeExceptions
30
+ .filter((row) => row.mode === "allow")
31
+ .flatMap((row) => (0, providerRestrictions_js_1.normalizeProviderRestrictionValues)(row.item_ids)));
32
+ const temporarilyBlocked = new Set(activeExceptions
33
+ .filter((row) => row.mode === "block")
34
+ .flatMap((row) => (0, providerRestrictions_js_1.normalizeProviderRestrictionValues)(row.item_ids)));
35
+ const allowed = new Set(universe.filter((itemId) => !blocked.has(itemId)));
36
+ for (const itemId of temporarilyAllowed) {
37
+ if (universeSet.has(itemId)) {
38
+ allowed.add(itemId);
39
+ }
40
+ }
41
+ for (const itemId of temporarilyBlocked) {
42
+ allowed.delete(itemId);
43
+ }
44
+ return universe.filter((itemId) => allowed.has(itemId));
45
+ };
46
+ exports.resolveAllowedProviderReceptionItems = resolveAllowedProviderReceptionItems;
47
+ const resolveAllowedProviderReceptionTypes = (input) => input.receptionTypeCodes
48
+ .map(providerRestrictions_js_1.normalizeRestrictionValue)
49
+ .filter(Boolean)
50
+ .filter((receptionTypeCode) => (0, exports.resolveAllowedProviderReceptionItems)({
51
+ ...input,
52
+ receptionTypeCode,
53
+ universe: input.itemIds,
54
+ }).length > 0);
55
+ exports.resolveAllowedProviderReceptionTypes = resolveAllowedProviderReceptionTypes;
56
+ const providerReceptionMatrixLeavesNoAllowedCombination = (input) => {
57
+ const relevantExceptions = (input.exceptions ?? []).filter((row) => Number(row.payee_id) === Number(input.payeeId) &&
58
+ !(0, providerRestrictions_js_1.normalizeRestrictionValue)(row.fechaEliminacion));
59
+ const dates = (0, providerRestrictions_js_1.sampleDatesForRestrictionRules)(relevantExceptions);
60
+ return dates.some((date) => (0, exports.resolveAllowedProviderReceptionTypes)({
61
+ ...input,
62
+ date,
63
+ }).length === 0);
64
+ };
65
+ exports.providerReceptionMatrixLeavesNoAllowedCombination = providerReceptionMatrixLeavesNoAllowedCombination;
66
+ const validateProviderReceptionException = (row, options) => {
67
+ const errors = [];
68
+ const typeCodes = new Set(options.receptionTypeCodes.map(providerRestrictions_js_1.normalizeRestrictionValue).filter(Boolean));
69
+ const itemIds = new Set(options.itemIds.map(providerRestrictions_js_1.normalizeRestrictionValue).filter(Boolean));
70
+ const selectedItems = (0, providerRestrictions_js_1.normalizeProviderRestrictionValues)(row.item_ids);
71
+ if (!Number(row.payee_id) || Number(row.payee_id) < 1) {
72
+ errors.push("Seleccione un proveedor.");
73
+ }
74
+ if (!typeCodes.has((0, providerRestrictions_js_1.normalizeRestrictionValue)(row.reception_type_code))) {
75
+ errors.push("Seleccione un tipo de recepción válido.");
76
+ }
77
+ if (row.mode !== "allow" && row.mode !== "block") {
78
+ errors.push("Seleccione si la excepción permite o bloquea verduras.");
79
+ }
80
+ if (selectedItems.length === 0 ||
81
+ selectedItems.some((itemId) => !itemIds.has(itemId))) {
82
+ errors.push("Seleccione al menos una verdura válida.");
83
+ }
84
+ if (!DATE_RE.test(row.start_date) ||
85
+ !DATE_RE.test(row.end_date) ||
86
+ row.start_date > row.end_date) {
87
+ errors.push("Indique un rango de fechas válido.");
88
+ }
89
+ if (!(0, providerRestrictions_js_1.normalizeRestrictionValue)(row.motivo)) {
90
+ errors.push("Indique el motivo de la excepción.");
91
+ }
92
+ return errors;
93
+ };
94
+ exports.validateProviderReceptionException = validateProviderReceptionException;
95
+ const evaluateProviderReceptionMatrix = (input) => {
96
+ const itemId = (0, providerRestrictions_js_1.normalizeRestrictionValue)(input.itemId);
97
+ const allowed = (0, exports.resolveAllowedProviderReceptionItems)({
98
+ ...input,
99
+ universe: input.itemIds,
100
+ });
101
+ if (allowed.includes(itemId)) {
102
+ return { allowed: true };
103
+ }
104
+ const blockingException = (input.exceptions ?? []).find((row) => row.mode === "block" &&
105
+ matchesProviderAndType(row, input.payeeId, input.receptionTypeCode) &&
106
+ isActive(row, input.date) &&
107
+ (0, providerRestrictions_js_1.normalizeProviderRestrictionValues)(row.item_ids).includes(itemId));
108
+ const itemLabel = (0, providerRestrictions_js_1.normalizeRestrictionValue)(input.itemLabel) || itemId;
109
+ if (blockingException) {
110
+ return {
111
+ allowed: false,
112
+ mode: "exclude",
113
+ matchedValues: [itemLabel],
114
+ title: "Verdura no permitida para este proveedor",
115
+ description: `Del ${blockingException.start_date} al ${blockingException.end_date}, la verdura "${itemLabel}" está bloqueada para el tipo ${(0, providerRestrictions_js_1.normalizeRestrictionValue)(input.receptionTypeCode)}. Motivo: ${(0, providerRestrictions_js_1.normalizeRestrictionValue)(blockingException.motivo)}.`,
116
+ };
117
+ }
118
+ return {
119
+ allowed: false,
120
+ mode: "exclude",
121
+ matchedValues: [itemLabel],
122
+ title: "Verdura no permitida para este proveedor",
123
+ description: `La combinación de tipo ${(0, providerRestrictions_js_1.normalizeRestrictionValue)(input.receptionTypeCode)} y verdura "${itemLabel}" está desmarcada en la matriz del proveedor.`,
124
+ };
125
+ };
126
+ exports.evaluateProviderReceptionMatrix = evaluateProviderReceptionMatrix;
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export * from "./gtZones.js";
3
3
  export * from "./httpClientPolicy.js";
4
4
  export * from "./oauthErrors.js";
5
5
  export * from "./providerRestrictions.js";
6
+ export * from "./providerReceptionMatrix.js";
6
7
  export * from "./rejectionPercentageAdjustment.js";
@@ -0,0 +1,40 @@
1
+ import type { ProviderReceptionBlockRule, ProviderReceptionException, ProviderRestrictionCheckResult } from "@zauru-sdk/types";
2
+ export declare const PROVIDER_RECEPTION_MATRIX_TABLE_VAR = "ctl_calidad_admin_matriz_recepcion_proveedor_web_app_table_id";
3
+ export declare const PROVIDER_RECEPTION_EXCEPTIONS_TABLE_VAR = "ctl_calidad_admin_excepciones_recepcion_proveedor_web_app_table_id";
4
+ export declare const resolveAllowedProviderReceptionItems: (input: {
5
+ payeeId: number;
6
+ receptionTypeCode: string;
7
+ date: string;
8
+ universe: Array<string | number>;
9
+ blockRules?: ProviderReceptionBlockRule[];
10
+ exceptions?: ProviderReceptionException[];
11
+ }) => string[];
12
+ export declare const resolveAllowedProviderReceptionTypes: (input: {
13
+ payeeId: number;
14
+ date: string;
15
+ receptionTypeCodes: Array<string | number>;
16
+ itemIds: Array<string | number>;
17
+ blockRules?: ProviderReceptionBlockRule[];
18
+ exceptions?: ProviderReceptionException[];
19
+ }) => string[];
20
+ export declare const providerReceptionMatrixLeavesNoAllowedCombination: (input: {
21
+ payeeId: number;
22
+ receptionTypeCodes: Array<string | number>;
23
+ itemIds: Array<string | number>;
24
+ blockRules?: ProviderReceptionBlockRule[];
25
+ exceptions?: ProviderReceptionException[];
26
+ }) => boolean;
27
+ export declare const validateProviderReceptionException: (row: ProviderReceptionException, options: {
28
+ receptionTypeCodes: Array<string | number>;
29
+ itemIds: Array<string | number>;
30
+ }) => string[];
31
+ export declare const evaluateProviderReceptionMatrix: (input: {
32
+ payeeId: number;
33
+ receptionTypeCode: string;
34
+ date: string;
35
+ itemId: string | number;
36
+ itemLabel?: string;
37
+ itemIds: Array<string | number>;
38
+ blockRules?: ProviderReceptionBlockRule[];
39
+ exceptions?: ProviderReceptionException[];
40
+ }) => ProviderRestrictionCheckResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/common",
3
- "version": "6.11.1",
3
+ "version": "6.13.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",
@@ -12,7 +12,7 @@
12
12
  "scripts": {
13
13
  "build": "npm run build:esm",
14
14
  "build:esm": "tsc -p tsconfig.esm.json",
15
- "test": "tsx --test src/httpClientPolicy.test.ts src/providerRestrictions.test.ts"
15
+ "test": "tsx --test src/httpClientPolicy.test.ts src/providerRestrictions.test.ts src/providerReceptionMatrix.test.ts src/purchaseOrderFinancials.test.ts"
16
16
  },
17
17
  "author": "Oscar Cuéllar",
18
18
  "license": "MIT",
@@ -22,9 +22,9 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@remix-run/node": "^2.8.1",
25
- "@zauru-sdk/types": "^6.27.0",
25
+ "@zauru-sdk/types": "^6.29.0",
26
26
  "moment": "^2.30.1",
27
27
  "moment-timezone": "^0.5.45"
28
28
  },
29
- "gitHead": "04f79cf77f767191f4be6358d9b23a3a0f1641b0"
29
+ "gitHead": "671859beecfce09a1ccf138055a4de26a8c1f059"
30
30
  }
package/src/common.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  JsonMemoType,
10
10
  CalculatePurchaseOrderFinancialsInput,
11
11
  CalculatePurchaseOrderFinancialsResult,
12
+ PurchaseOrderDetailCostAttribute,
12
13
  PurchaseOrderFinancialDetail,
13
14
  RejectionPercentageLayers,
14
15
  } from "@zauru-sdk/types";
@@ -232,6 +233,36 @@ export const calculatePurchaseOrderFinancials = (
232
233
  };
233
234
  };
234
235
 
236
+ /**
237
+ * Nested attributes for Zauru: `cost` must be sent with `unit_cost`.
238
+ * Baculo does not recalculate `purchase_order_details.cost` on save.
239
+ */
240
+ export const buildPurchaseOrderDetailsCostAttributes = (
241
+ details: PurchaseOrderFinancialDetail[] | null | undefined,
242
+ unitCostByDetailId?: Map<number, number>,
243
+ ): Record<string, PurchaseOrderDetailCostAttribute> => {
244
+ const attributes: Record<string, PurchaseOrderDetailCostAttribute> = {};
245
+ let index = 0;
246
+
247
+ for (const detail of details ?? []) {
248
+ const id = toFiniteNumber(detail.id);
249
+ const itemId = toFiniteNumber(detail.item_id);
250
+ if (id == null || itemId == null) continue;
251
+
252
+ const unitCost =
253
+ unitCostByDetailId?.get(id) ?? toFiniteNumber(detail.unit_cost) ?? 0;
254
+ attributes[String(index)] = {
255
+ id,
256
+ item_id: itemId,
257
+ unit_cost: unitCost,
258
+ cost: roundMoney(lineQuantity(detail) * unitCost),
259
+ };
260
+ index += 1;
261
+ }
262
+
263
+ return attributes;
264
+ };
265
+
235
266
  /**
236
267
  * Obtener el objeto de canastas en base al memo
237
268
  * @param memo
package/src/index.ts CHANGED
@@ -3,4 +3,5 @@ export * from "./gtZones.js";
3
3
  export * from "./httpClientPolicy.js";
4
4
  export * from "./oauthErrors.js";
5
5
  export * from "./providerRestrictions.js";
6
+ export * from "./providerReceptionMatrix.js";
6
7
  export * from "./rejectionPercentageAdjustment.js";
@@ -0,0 +1,184 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type {
4
+ ProviderReceptionBlockRule,
5
+ ProviderReceptionException,
6
+ } from "@zauru-sdk/types";
7
+ import {
8
+ evaluateProviderReceptionMatrix,
9
+ providerReceptionMatrixLeavesNoAllowedCombination,
10
+ resolveAllowedProviderReceptionItems,
11
+ resolveAllowedProviderReceptionTypes,
12
+ validateProviderReceptionException,
13
+ } from "./providerReceptionMatrix.js";
14
+
15
+ const block = (
16
+ partial: Partial<ProviderReceptionBlockRule> = {},
17
+ ): ProviderReceptionBlockRule => ({
18
+ payee_id: 10,
19
+ reception_type_code: "1",
20
+ blocked_item_ids: [],
21
+ ...partial,
22
+ });
23
+
24
+ const exception = (
25
+ partial: Partial<ProviderReceptionException> = {},
26
+ ): ProviderReceptionException => ({
27
+ payee_id: 10,
28
+ reception_type_code: "1",
29
+ mode: "block",
30
+ item_ids: [100],
31
+ start_date: "2026-10-01",
32
+ end_date: "2026-10-31",
33
+ motivo: "Suspensión temporal",
34
+ ...partial,
35
+ });
36
+
37
+ describe("resolveAllowedProviderReceptionItems", () => {
38
+ it("allows the whole universe when no matrix row exists", () => {
39
+ assert.deepEqual(
40
+ resolveAllowedProviderReceptionItems({
41
+ payeeId: 10,
42
+ receptionTypeCode: "1",
43
+ date: "2026-10-15",
44
+ universe: [100, 200],
45
+ }),
46
+ ["100", "200"],
47
+ );
48
+ });
49
+
50
+ it("blocks only the exact provider, type and item combination", () => {
51
+ const rules = [block({ blocked_item_ids: [100] })];
52
+ assert.deepEqual(
53
+ resolveAllowedProviderReceptionItems({
54
+ payeeId: 10,
55
+ receptionTypeCode: "1",
56
+ date: "2026-10-15",
57
+ universe: [100, 200],
58
+ blockRules: rules,
59
+ }),
60
+ ["200"],
61
+ );
62
+ assert.deepEqual(
63
+ resolveAllowedProviderReceptionItems({
64
+ payeeId: 10,
65
+ receptionTypeCode: "Normal",
66
+ date: "2026-10-15",
67
+ universe: [100, 200],
68
+ blockRules: rules,
69
+ }),
70
+ ["100", "200"],
71
+ );
72
+ });
73
+
74
+ it("allows a base-blocked item only inside an allow exception range", () => {
75
+ const input = {
76
+ payeeId: 10,
77
+ receptionTypeCode: "1",
78
+ universe: [100, 200],
79
+ blockRules: [block({ blocked_item_ids: [100] })],
80
+ exceptions: [exception({ mode: "allow" as const })],
81
+ };
82
+ assert.deepEqual(
83
+ resolveAllowedProviderReceptionItems({ ...input, date: "2026-10-15" }),
84
+ ["100", "200"],
85
+ );
86
+ assert.deepEqual(
87
+ resolveAllowedProviderReceptionItems({ ...input, date: "2026-11-01" }),
88
+ ["200"],
89
+ );
90
+ });
91
+
92
+ it("makes a temporary block win over an overlapping allow", () => {
93
+ assert.deepEqual(
94
+ resolveAllowedProviderReceptionItems({
95
+ payeeId: 10,
96
+ receptionTypeCode: "1",
97
+ date: "2026-10-15",
98
+ universe: [100, 200],
99
+ exceptions: [
100
+ exception({ mode: "allow" }),
101
+ exception({ mode: "block" }),
102
+ ],
103
+ }),
104
+ ["200"],
105
+ );
106
+ });
107
+
108
+ it("ignores soft-deleted matrix rows and exceptions", () => {
109
+ assert.deepEqual(
110
+ resolveAllowedProviderReceptionItems({
111
+ payeeId: 10,
112
+ receptionTypeCode: "1",
113
+ date: "2026-10-15",
114
+ universe: [100, 200],
115
+ blockRules: [
116
+ block({ blocked_item_ids: [100], fechaEliminacion: "2026-10-01" }),
117
+ ],
118
+ exceptions: [exception({ fechaEliminacion: "2026-10-01" })],
119
+ }),
120
+ ["100", "200"],
121
+ );
122
+ });
123
+ });
124
+
125
+ describe("provider reception matrix validation", () => {
126
+ it("keeps only reception types with at least one allowed item", () => {
127
+ assert.deepEqual(
128
+ resolveAllowedProviderReceptionTypes({
129
+ payeeId: 10,
130
+ date: "2026-10-15",
131
+ receptionTypeCodes: ["Normal", "1"],
132
+ itemIds: [100],
133
+ blockRules: [block({ blocked_item_ids: [100] })],
134
+ }),
135
+ ["Normal"],
136
+ );
137
+ });
138
+
139
+ it("rejects rules whose dated overlap leaves no valid combination", () => {
140
+ assert.equal(
141
+ providerReceptionMatrixLeavesNoAllowedCombination({
142
+ payeeId: 10,
143
+ receptionTypeCodes: ["Normal", "1"],
144
+ itemIds: [100],
145
+ blockRules: [block({ blocked_item_ids: [100] })],
146
+ exceptions: [
147
+ exception({
148
+ reception_type_code: "Normal",
149
+ mode: "block",
150
+ item_ids: [100],
151
+ }),
152
+ ],
153
+ }),
154
+ true,
155
+ );
156
+ });
157
+
158
+ it("requires valid dates, items and motivo for exceptions", () => {
159
+ const errors = validateProviderReceptionException(
160
+ exception({ end_date: "2026-09-01", motivo: "", item_ids: [999] }),
161
+ { receptionTypeCodes: ["1"], itemIds: [100, 200] },
162
+ );
163
+ assert.equal(errors.length, 3);
164
+ });
165
+ });
166
+
167
+ describe("evaluateProviderReceptionMatrix", () => {
168
+ it("includes the temporary reason and inclusive range when blocked", () => {
169
+ const result = evaluateProviderReceptionMatrix({
170
+ payeeId: 10,
171
+ receptionTypeCode: "1",
172
+ date: "2026-10-31",
173
+ itemId: 100,
174
+ itemLabel: "Ejote",
175
+ itemIds: [100, 200],
176
+ exceptions: [exception()],
177
+ });
178
+ assert.equal(result.allowed, false);
179
+ if (!result.allowed) {
180
+ assert.match(result.description, /Suspensión temporal/);
181
+ assert.match(result.description, /2026-10-31/);
182
+ }
183
+ });
184
+ });
@@ -0,0 +1,237 @@
1
+ import type {
2
+ ProviderReceptionBlockRule,
3
+ ProviderReceptionException,
4
+ ProviderRestrictionCheckResult,
5
+ } from "@zauru-sdk/types";
6
+ import {
7
+ normalizeProviderRestrictionValues,
8
+ normalizeRestrictionValue,
9
+ sampleDatesForRestrictionRules,
10
+ } from "./providerRestrictions.js";
11
+
12
+ export const PROVIDER_RECEPTION_MATRIX_TABLE_VAR =
13
+ "ctl_calidad_admin_matriz_recepcion_proveedor_web_app_table_id";
14
+
15
+ export const PROVIDER_RECEPTION_EXCEPTIONS_TABLE_VAR =
16
+ "ctl_calidad_admin_excepciones_recepcion_proveedor_web_app_table_id";
17
+
18
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
19
+
20
+ const isActive = (
21
+ row: Pick<ProviderReceptionException, "start_date" | "end_date" | "fechaEliminacion">,
22
+ date: string,
23
+ ): boolean => {
24
+ if (normalizeRestrictionValue(row.fechaEliminacion) || !DATE_RE.test(date)) {
25
+ return false;
26
+ }
27
+ const start = normalizeRestrictionValue(row.start_date);
28
+ const end = normalizeRestrictionValue(row.end_date);
29
+ return DATE_RE.test(start) && DATE_RE.test(end) && start <= date && date <= end;
30
+ };
31
+
32
+ const matchesProviderAndType = (
33
+ row: { payee_id: number; reception_type_code: string },
34
+ payeeId: number,
35
+ receptionTypeCode: string,
36
+ ): boolean =>
37
+ Number(row.payee_id) === Number(payeeId) &&
38
+ normalizeRestrictionValue(row.reception_type_code) ===
39
+ normalizeRestrictionValue(receptionTypeCode);
40
+
41
+ export const resolveAllowedProviderReceptionItems = (input: {
42
+ payeeId: number;
43
+ receptionTypeCode: string;
44
+ date: string;
45
+ universe: Array<string | number>;
46
+ blockRules?: ProviderReceptionBlockRule[];
47
+ exceptions?: ProviderReceptionException[];
48
+ }): string[] => {
49
+ const universe = input.universe
50
+ .map(normalizeRestrictionValue)
51
+ .filter(Boolean);
52
+ const universeSet = new Set(universe);
53
+ const blocked = new Set(
54
+ (input.blockRules ?? [])
55
+ .filter(
56
+ (row) =>
57
+ !normalizeRestrictionValue(row.fechaEliminacion) &&
58
+ matchesProviderAndType(
59
+ row,
60
+ input.payeeId,
61
+ input.receptionTypeCode,
62
+ ),
63
+ )
64
+ .flatMap((row) =>
65
+ normalizeProviderRestrictionValues(row.blocked_item_ids),
66
+ ),
67
+ );
68
+
69
+ const activeExceptions = (input.exceptions ?? []).filter(
70
+ (row) =>
71
+ matchesProviderAndType(
72
+ row,
73
+ input.payeeId,
74
+ input.receptionTypeCode,
75
+ ) && isActive(row, input.date),
76
+ );
77
+
78
+ const temporarilyAllowed = new Set(
79
+ activeExceptions
80
+ .filter((row) => row.mode === "allow")
81
+ .flatMap((row) => normalizeProviderRestrictionValues(row.item_ids)),
82
+ );
83
+ const temporarilyBlocked = new Set(
84
+ activeExceptions
85
+ .filter((row) => row.mode === "block")
86
+ .flatMap((row) => normalizeProviderRestrictionValues(row.item_ids)),
87
+ );
88
+
89
+ const allowed = new Set(universe.filter((itemId) => !blocked.has(itemId)));
90
+ for (const itemId of temporarilyAllowed) {
91
+ if (universeSet.has(itemId)) {
92
+ allowed.add(itemId);
93
+ }
94
+ }
95
+ for (const itemId of temporarilyBlocked) {
96
+ allowed.delete(itemId);
97
+ }
98
+
99
+ return universe.filter((itemId) => allowed.has(itemId));
100
+ };
101
+
102
+ export const resolveAllowedProviderReceptionTypes = (input: {
103
+ payeeId: number;
104
+ date: string;
105
+ receptionTypeCodes: Array<string | number>;
106
+ itemIds: Array<string | number>;
107
+ blockRules?: ProviderReceptionBlockRule[];
108
+ exceptions?: ProviderReceptionException[];
109
+ }): string[] =>
110
+ input.receptionTypeCodes
111
+ .map(normalizeRestrictionValue)
112
+ .filter(Boolean)
113
+ .filter(
114
+ (receptionTypeCode) =>
115
+ resolveAllowedProviderReceptionItems({
116
+ ...input,
117
+ receptionTypeCode,
118
+ universe: input.itemIds,
119
+ }).length > 0,
120
+ );
121
+
122
+ export const providerReceptionMatrixLeavesNoAllowedCombination = (input: {
123
+ payeeId: number;
124
+ receptionTypeCodes: Array<string | number>;
125
+ itemIds: Array<string | number>;
126
+ blockRules?: ProviderReceptionBlockRule[];
127
+ exceptions?: ProviderReceptionException[];
128
+ }): boolean => {
129
+ const relevantExceptions = (input.exceptions ?? []).filter(
130
+ (row) =>
131
+ Number(row.payee_id) === Number(input.payeeId) &&
132
+ !normalizeRestrictionValue(row.fechaEliminacion),
133
+ );
134
+ const dates = sampleDatesForRestrictionRules(relevantExceptions);
135
+ return dates.some(
136
+ (date) =>
137
+ resolveAllowedProviderReceptionTypes({
138
+ ...input,
139
+ date,
140
+ }).length === 0,
141
+ );
142
+ };
143
+
144
+ export const validateProviderReceptionException = (
145
+ row: ProviderReceptionException,
146
+ options: {
147
+ receptionTypeCodes: Array<string | number>;
148
+ itemIds: Array<string | number>;
149
+ },
150
+ ): string[] => {
151
+ const errors: string[] = [];
152
+ const typeCodes = new Set(
153
+ options.receptionTypeCodes.map(normalizeRestrictionValue).filter(Boolean),
154
+ );
155
+ const itemIds = new Set(
156
+ options.itemIds.map(normalizeRestrictionValue).filter(Boolean),
157
+ );
158
+ const selectedItems = normalizeProviderRestrictionValues(row.item_ids);
159
+
160
+ if (!Number(row.payee_id) || Number(row.payee_id) < 1) {
161
+ errors.push("Seleccione un proveedor.");
162
+ }
163
+ if (!typeCodes.has(normalizeRestrictionValue(row.reception_type_code))) {
164
+ errors.push("Seleccione un tipo de recepción válido.");
165
+ }
166
+ if (row.mode !== "allow" && row.mode !== "block") {
167
+ errors.push("Seleccione si la excepción permite o bloquea verduras.");
168
+ }
169
+ if (
170
+ selectedItems.length === 0 ||
171
+ selectedItems.some((itemId) => !itemIds.has(itemId))
172
+ ) {
173
+ errors.push("Seleccione al menos una verdura válida.");
174
+ }
175
+ if (
176
+ !DATE_RE.test(row.start_date) ||
177
+ !DATE_RE.test(row.end_date) ||
178
+ row.start_date > row.end_date
179
+ ) {
180
+ errors.push("Indique un rango de fechas válido.");
181
+ }
182
+ if (!normalizeRestrictionValue(row.motivo)) {
183
+ errors.push("Indique el motivo de la excepción.");
184
+ }
185
+ return errors;
186
+ };
187
+
188
+ export const evaluateProviderReceptionMatrix = (input: {
189
+ payeeId: number;
190
+ receptionTypeCode: string;
191
+ date: string;
192
+ itemId: string | number;
193
+ itemLabel?: string;
194
+ itemIds: Array<string | number>;
195
+ blockRules?: ProviderReceptionBlockRule[];
196
+ exceptions?: ProviderReceptionException[];
197
+ }): ProviderRestrictionCheckResult => {
198
+ const itemId = normalizeRestrictionValue(input.itemId);
199
+ const allowed = resolveAllowedProviderReceptionItems({
200
+ ...input,
201
+ universe: input.itemIds,
202
+ });
203
+ if (allowed.includes(itemId)) {
204
+ return { allowed: true };
205
+ }
206
+
207
+ const blockingException = (input.exceptions ?? []).find(
208
+ (row) =>
209
+ row.mode === "block" &&
210
+ matchesProviderAndType(
211
+ row,
212
+ input.payeeId,
213
+ input.receptionTypeCode,
214
+ ) &&
215
+ isActive(row, input.date) &&
216
+ normalizeProviderRestrictionValues(row.item_ids).includes(itemId),
217
+ );
218
+ const itemLabel = normalizeRestrictionValue(input.itemLabel) || itemId;
219
+
220
+ if (blockingException) {
221
+ return {
222
+ allowed: false,
223
+ mode: "exclude",
224
+ matchedValues: [itemLabel],
225
+ title: "Verdura no permitida para este proveedor",
226
+ description: `Del ${blockingException.start_date} al ${blockingException.end_date}, la verdura "${itemLabel}" está bloqueada para el tipo ${normalizeRestrictionValue(input.receptionTypeCode)}. Motivo: ${normalizeRestrictionValue(blockingException.motivo)}.`,
227
+ };
228
+ }
229
+
230
+ return {
231
+ allowed: false,
232
+ mode: "exclude",
233
+ matchedValues: [itemLabel],
234
+ title: "Verdura no permitida para este proveedor",
235
+ description: `La combinación de tipo ${normalizeRestrictionValue(input.receptionTypeCode)} y verdura "${itemLabel}" está desmarcada en la matriz del proveedor.`,
236
+ };
237
+ };
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ buildPurchaseOrderDetailsCostAttributes,
5
+ calculatePurchaseOrderFinancials,
6
+ } from "./common.js";
7
+
8
+ describe("buildPurchaseOrderDetailsCostAttributes", () => {
9
+ it("sends cost as qty × unit_cost so Baculo can refresh subtotal", () => {
10
+ const attributes = buildPurchaseOrderDetailsCostAttributes(
11
+ [
12
+ {
13
+ id: 2827653,
14
+ item_id: 490543,
15
+ booked_quantity: 14,
16
+ delivered_quantity: 14,
17
+ unit_cost: 8.2,
18
+ },
19
+ ],
20
+ new Map([[2827653, 7.7]]),
21
+ );
22
+
23
+ assert.deepEqual(attributes, {
24
+ "0": {
25
+ id: 2827653,
26
+ item_id: 490543,
27
+ unit_cost: 7.7,
28
+ cost: 107.8,
29
+ },
30
+ });
31
+ });
32
+
33
+ it("keeps current unit_cost when there is no override", () => {
34
+ const attributes = buildPurchaseOrderDetailsCostAttributes([
35
+ {
36
+ id: 1,
37
+ item_id: 2,
38
+ booked_quantity: 10,
39
+ unit_cost: 4.5,
40
+ },
41
+ ]);
42
+
43
+ assert.equal(attributes["0"].unit_cost, 4.5);
44
+ assert.equal(attributes["0"].cost, 45);
45
+ });
46
+ });
47
+
48
+ describe("calculatePurchaseOrderFinancials", () => {
49
+ it("uses the new unit cost as the rejection discount base", () => {
50
+ const financials = calculatePurchaseOrderFinancials({
51
+ details: [
52
+ {
53
+ booked_quantity: 14,
54
+ delivered_quantity: 14,
55
+ unit_cost: 7.7,
56
+ },
57
+ ],
58
+ rejectionPercentage: 7,
59
+ });
60
+
61
+ assert.equal(financials.subtotal, 107.8);
62
+ assert.equal(financials.discount, 7.55);
63
+ });
64
+ });