@zauru-sdk/common 6.11.1 → 6.12.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,23 @@
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.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)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * rechazar restricciones que dejan al proveedor sin opciones ([24e6609](https://github.com/intuitiva/zauru-typescript-sdk/commit/24e6609b9cbf1498bec52bbeae9cd52a57945b45))
12
+
13
+
14
+ ### Features
15
+
16
+ * agregar matriz de restricciones de recepción ([11a07b0](https://github.com/intuitiva/zauru-typescript-sdk/commit/11a07b04aa71dd6a38b79f10c4209d08e8f7607c))
17
+ * motor de restricciones de tipos y verduras por proveedor ([ea09fd6](https://github.com/intuitiva/zauru-typescript-sdk/commit/ea09fd6a4cbed61fc29640baf1bc2b0b78db18da))
18
+
19
+
20
+
21
+
22
+
6
23
  # [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
24
 
8
25
 
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.12.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"
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.28.0",
26
26
  "moment": "^2.30.1",
27
27
  "moment-timezone": "^0.5.45"
28
28
  },
29
- "gitHead": "04f79cf77f767191f4be6358d9b23a3a0f1641b0"
29
+ "gitHead": "2fda4a18e3f62d22c38fbb91052869db9ebc16f4"
30
30
  }
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
+ };