@zauru-sdk/common 6.11.0 → 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 +17 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/providerReceptionMatrix.js +126 -0
- package/dist/esm/providerRestrictions.js +71 -2
- package/dist/index.d.ts +1 -0
- package/dist/providerReceptionMatrix.d.ts +40 -0
- package/dist/providerRestrictions.d.ts +12 -1
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/providerReceptionMatrix.test.ts +184 -0
- package/src/providerReceptionMatrix.ts +237 -0
- package/src/providerRestrictions.test.ts +83 -0
- package/src/providerRestrictions.ts +95 -0
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.checkProviderRestrictionValue = exports.validateProviderRestrictionInput = exports.matchReceptionTypeCode = exports.isRestrictionValueAllowed = exports.resolveAllowedRestrictionValues = exports.filterActiveProviderRestrictions = exports.isRestrictionActiveOnDate = exports.restrictionHasDateRange = exports.normalizeProviderRestrictionValues = exports.isProviderRestrictionMode = exports.formatRestrictionTypeValue = exports.normalizeRestrictionValue = exports.PROVIDER_ITEM_RESTRICTIONS_TABLE_VAR = exports.PROVIDER_TYPE_RESTRICTIONS_TABLE_VAR = void 0;
|
|
3
|
+
exports.checkProviderRestrictionValue = exports.validateProviderRestrictionInput = exports.restrictionLeavesNoAllowedValues = exports.sampleDatesForRestrictionRules = exports.matchReceptionTypeCode = exports.isRestrictionValueAllowed = exports.resolveAllowedRestrictionValues = exports.filterActiveProviderRestrictions = exports.isRestrictionActiveOnDate = exports.restrictionHasDateRange = exports.normalizeProviderRestrictionValues = exports.isProviderRestrictionMode = exports.formatRestrictionTypeValue = exports.normalizeRestrictionValue = exports.PROVIDER_ITEM_RESTRICTIONS_TABLE_VAR = exports.PROVIDER_TYPE_RESTRICTIONS_TABLE_VAR = void 0;
|
|
4
4
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
5
|
exports.PROVIDER_TYPE_RESTRICTIONS_TABLE_VAR = "ctl_calidad_admin_restricciones_tipos_proveedor_web_app_table_id";
|
|
6
6
|
exports.PROVIDER_ITEM_RESTRICTIONS_TABLE_VAR = "ctl_calidad_admin_restricciones_verduras_proveedor_web_app_table_id";
|
|
@@ -119,7 +119,47 @@ const matchReceptionTypeCode = (formValue, types) => {
|
|
|
119
119
|
return wanted;
|
|
120
120
|
};
|
|
121
121
|
exports.matchReceptionTypeCode = matchReceptionTypeCode;
|
|
122
|
-
const
|
|
122
|
+
const shiftYmd = (ymd, days) => {
|
|
123
|
+
const [year, month, day] = ymd.split("-").map(Number);
|
|
124
|
+
const utc = new Date(Date.UTC(year, month - 1, day + days));
|
|
125
|
+
const nextYear = utc.getUTCFullYear();
|
|
126
|
+
const nextMonth = String(utc.getUTCMonth() + 1).padStart(2, "0");
|
|
127
|
+
const nextDay = String(utc.getUTCDate()).padStart(2, "0");
|
|
128
|
+
return `${nextYear}-${nextMonth}-${nextDay}`;
|
|
129
|
+
};
|
|
130
|
+
const sampleDatesForRestrictionRules = (rules) => {
|
|
131
|
+
const dates = new Set(["1970-01-01", "2999-12-31"]);
|
|
132
|
+
for (const rule of rules) {
|
|
133
|
+
const start = (0, exports.normalizeRestrictionValue)(rule.start_date);
|
|
134
|
+
const end = (0, exports.normalizeRestrictionValue)(rule.end_date);
|
|
135
|
+
if (DATE_RE.test(start)) {
|
|
136
|
+
dates.add(start);
|
|
137
|
+
dates.add(shiftYmd(start, -1));
|
|
138
|
+
}
|
|
139
|
+
if (DATE_RE.test(end)) {
|
|
140
|
+
dates.add(end);
|
|
141
|
+
dates.add(shiftYmd(end, 1));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [...dates].sort();
|
|
145
|
+
};
|
|
146
|
+
exports.sampleDatesForRestrictionRules = sampleDatesForRestrictionRules;
|
|
147
|
+
const restrictionLeavesNoAllowedValues = (input) => {
|
|
148
|
+
const universe = input.universe
|
|
149
|
+
.map(exports.normalizeRestrictionValue)
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
if (universe.length === 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return (0, exports.sampleDatesForRestrictionRules)(input.rules ?? []).some((date) => (0, exports.resolveAllowedRestrictionValues)({
|
|
155
|
+
payeeId: input.payeeId,
|
|
156
|
+
date,
|
|
157
|
+
rules: input.rules,
|
|
158
|
+
universe,
|
|
159
|
+
}).length === 0);
|
|
160
|
+
};
|
|
161
|
+
exports.restrictionLeavesNoAllowedValues = restrictionLeavesNoAllowedValues;
|
|
162
|
+
const validateProviderRestrictionInput = (input, options) => {
|
|
123
163
|
const errors = [];
|
|
124
164
|
if (!Number(input.payee_id) || Number(input.payee_id) < 1) {
|
|
125
165
|
errors.push({ field: "payee_id", message: "Seleccione un proveedor." });
|
|
@@ -166,6 +206,35 @@ const validateProviderRestrictionInput = (input) => {
|
|
|
166
206
|
});
|
|
167
207
|
}
|
|
168
208
|
}
|
|
209
|
+
const universe = (options?.universe ?? [])
|
|
210
|
+
.map(exports.normalizeRestrictionValue)
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
if (universe.length > 0 &&
|
|
213
|
+
Number(input.payee_id) > 0 &&
|
|
214
|
+
(0, exports.isProviderRestrictionMode)(input.mode) &&
|
|
215
|
+
(0, exports.normalizeProviderRestrictionValues)(input.values).length > 0) {
|
|
216
|
+
const proposed = {
|
|
217
|
+
payee_id: Number(input.payee_id),
|
|
218
|
+
mode: input.mode,
|
|
219
|
+
values: (0, exports.normalizeProviderRestrictionValues)(input.values),
|
|
220
|
+
start_date: start,
|
|
221
|
+
end_date: end,
|
|
222
|
+
motivo: (0, exports.normalizeRestrictionValue)(input.motivo),
|
|
223
|
+
};
|
|
224
|
+
const otherRules = (options?.otherRules ?? []).filter((rule) => Number(rule.payee_id) === Number(input.payee_id) &&
|
|
225
|
+
!(0, exports.normalizeRestrictionValue)(rule.fechaEliminacion));
|
|
226
|
+
if ((0, exports.restrictionLeavesNoAllowedValues)({
|
|
227
|
+
payeeId: Number(input.payee_id),
|
|
228
|
+
rules: [...otherRules, proposed],
|
|
229
|
+
universe,
|
|
230
|
+
})) {
|
|
231
|
+
const noun = (0, exports.normalizeRestrictionValue)(options?.valueNoun) || "opciones";
|
|
232
|
+
errors.push({
|
|
233
|
+
field: "values",
|
|
234
|
+
message: `Esta regla dejaría al proveedor sin ${noun} para escoger. Debe quedar al menos uno.`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
169
238
|
return errors;
|
|
170
239
|
};
|
|
171
240
|
exports.validateProviderRestrictionInput = validateProviderRestrictionInput;
|
package/dist/index.d.ts
CHANGED
|
@@ -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;
|
|
@@ -39,7 +39,18 @@ export type ProviderRestrictionInputError = {
|
|
|
39
39
|
field: string;
|
|
40
40
|
message: string;
|
|
41
41
|
};
|
|
42
|
-
export
|
|
42
|
+
export type ValidateProviderRestrictionOptions = {
|
|
43
|
+
universe?: Array<string | number>;
|
|
44
|
+
otherRules?: ProviderRestrictionRule[];
|
|
45
|
+
valueNoun?: string;
|
|
46
|
+
};
|
|
47
|
+
export declare const sampleDatesForRestrictionRules: (rules: Array<Pick<ProviderRestrictionRule, "start_date" | "end_date">>) => string[];
|
|
48
|
+
export declare const restrictionLeavesNoAllowedValues: (input: {
|
|
49
|
+
payeeId: number;
|
|
50
|
+
rules: ProviderRestrictionRule[] | undefined;
|
|
51
|
+
universe: Array<string | number>;
|
|
52
|
+
}) => boolean;
|
|
53
|
+
export declare const validateProviderRestrictionInput: (input: ProviderRestrictionInput, options?: ValidateProviderRestrictionOptions) => ProviderRestrictionInputError[];
|
|
43
54
|
export declare const checkProviderRestrictionValue: (input: {
|
|
44
55
|
payeeId: number;
|
|
45
56
|
date: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zauru-sdk/common",
|
|
3
|
-
"version": "6.
|
|
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.
|
|
25
|
+
"@zauru-sdk/types": "^6.28.0",
|
|
26
26
|
"moment": "^2.30.1",
|
|
27
27
|
"moment-timezone": "^0.5.45"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "2fda4a18e3f62d22c38fbb91052869db9ebc16f4"
|
|
30
30
|
}
|
package/src/index.ts
CHANGED
|
@@ -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
|
+
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isRestrictionValueAllowed,
|
|
9
9
|
matchReceptionTypeCode,
|
|
10
10
|
resolveAllowedRestrictionValues,
|
|
11
|
+
restrictionLeavesNoAllowedValues,
|
|
11
12
|
validateProviderRestrictionInput,
|
|
12
13
|
} from "./providerRestrictions.js";
|
|
13
14
|
|
|
@@ -274,4 +275,86 @@ describe("validateProviderRestrictionInput", () => {
|
|
|
274
275
|
true,
|
|
275
276
|
);
|
|
276
277
|
});
|
|
278
|
+
|
|
279
|
+
it("When exclude removes every value in the universe, it should reject it", () => {
|
|
280
|
+
const errors = validateProviderRestrictionInput(
|
|
281
|
+
{
|
|
282
|
+
payee_id: 1,
|
|
283
|
+
mode: "exclude",
|
|
284
|
+
values: ["N", "1"],
|
|
285
|
+
},
|
|
286
|
+
{ universe: ["N", "1"], valueNoun: "tipos de recepción" },
|
|
287
|
+
);
|
|
288
|
+
assert.equal(
|
|
289
|
+
errors.some((error) => error.message.includes("sin tipos de recepción")),
|
|
290
|
+
true,
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("When exclude leaves at least one value, it should allow it", () => {
|
|
295
|
+
const errors = validateProviderRestrictionInput(
|
|
296
|
+
{
|
|
297
|
+
payee_id: 1,
|
|
298
|
+
mode: "exclude",
|
|
299
|
+
values: ["1"],
|
|
300
|
+
},
|
|
301
|
+
{ universe: ["N", "1"] },
|
|
302
|
+
);
|
|
303
|
+
assert.deepEqual(errors, []);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("When an overlapping exclude would empty an include, it should reject it", () => {
|
|
307
|
+
const errors = validateProviderRestrictionInput(
|
|
308
|
+
{
|
|
309
|
+
payee_id: 10,
|
|
310
|
+
mode: "exclude",
|
|
311
|
+
values: ["Normal"],
|
|
312
|
+
start_date: "2026-10-27",
|
|
313
|
+
end_date: "2026-11-02",
|
|
314
|
+
motivo: "Auditoría",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
universe: ["Normal", "Certificado 1"],
|
|
318
|
+
otherRules: [
|
|
319
|
+
rule({
|
|
320
|
+
payee_id: 10,
|
|
321
|
+
mode: "include",
|
|
322
|
+
values: ["Normal"],
|
|
323
|
+
}),
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
assert.equal(
|
|
328
|
+
errors.some((error) => error.field === "values"),
|
|
329
|
+
true,
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("restrictionLeavesNoAllowedValues", () => {
|
|
335
|
+
it("When two dated excludes only empty the overlap, it should detect that day", () => {
|
|
336
|
+
assert.equal(
|
|
337
|
+
restrictionLeavesNoAllowedValues({
|
|
338
|
+
payeeId: 3,
|
|
339
|
+
universe: ["A", "B"],
|
|
340
|
+
rules: [
|
|
341
|
+
rule({
|
|
342
|
+
payee_id: 3,
|
|
343
|
+
mode: "exclude",
|
|
344
|
+
values: ["A"],
|
|
345
|
+
start_date: "2026-10-01",
|
|
346
|
+
end_date: "2026-10-20",
|
|
347
|
+
}),
|
|
348
|
+
rule({
|
|
349
|
+
payee_id: 3,
|
|
350
|
+
mode: "exclude",
|
|
351
|
+
values: ["B"],
|
|
352
|
+
start_date: "2026-10-10",
|
|
353
|
+
end_date: "2026-10-15",
|
|
354
|
+
}),
|
|
355
|
+
],
|
|
356
|
+
}),
|
|
357
|
+
true,
|
|
358
|
+
);
|
|
359
|
+
});
|
|
277
360
|
});
|
|
@@ -196,8 +196,65 @@ export type ProviderRestrictionInputError = {
|
|
|
196
196
|
message: string;
|
|
197
197
|
};
|
|
198
198
|
|
|
199
|
+
export type ValidateProviderRestrictionOptions = {
|
|
200
|
+
universe?: Array<string | number>;
|
|
201
|
+
otherRules?: ProviderRestrictionRule[];
|
|
202
|
+
valueNoun?: string;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const shiftYmd = (ymd: string, days: number): string => {
|
|
206
|
+
const [year, month, day] = ymd.split("-").map(Number);
|
|
207
|
+
const utc = new Date(Date.UTC(year, month - 1, day + days));
|
|
208
|
+
const nextYear = utc.getUTCFullYear();
|
|
209
|
+
const nextMonth = String(utc.getUTCMonth() + 1).padStart(2, "0");
|
|
210
|
+
const nextDay = String(utc.getUTCDate()).padStart(2, "0");
|
|
211
|
+
return `${nextYear}-${nextMonth}-${nextDay}`;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const sampleDatesForRestrictionRules = (
|
|
215
|
+
rules: Array<Pick<ProviderRestrictionRule, "start_date" | "end_date">>,
|
|
216
|
+
): string[] => {
|
|
217
|
+
const dates = new Set<string>(["1970-01-01", "2999-12-31"]);
|
|
218
|
+
for (const rule of rules) {
|
|
219
|
+
const start = normalizeRestrictionValue(rule.start_date);
|
|
220
|
+
const end = normalizeRestrictionValue(rule.end_date);
|
|
221
|
+
if (DATE_RE.test(start)) {
|
|
222
|
+
dates.add(start);
|
|
223
|
+
dates.add(shiftYmd(start, -1));
|
|
224
|
+
}
|
|
225
|
+
if (DATE_RE.test(end)) {
|
|
226
|
+
dates.add(end);
|
|
227
|
+
dates.add(shiftYmd(end, 1));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return [...dates].sort();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const restrictionLeavesNoAllowedValues = (input: {
|
|
234
|
+
payeeId: number;
|
|
235
|
+
rules: ProviderRestrictionRule[] | undefined;
|
|
236
|
+
universe: Array<string | number>;
|
|
237
|
+
}): boolean => {
|
|
238
|
+
const universe = input.universe
|
|
239
|
+
.map(normalizeRestrictionValue)
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
if (universe.length === 0) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return sampleDatesForRestrictionRules(input.rules ?? []).some(
|
|
245
|
+
(date) =>
|
|
246
|
+
resolveAllowedRestrictionValues({
|
|
247
|
+
payeeId: input.payeeId,
|
|
248
|
+
date,
|
|
249
|
+
rules: input.rules,
|
|
250
|
+
universe,
|
|
251
|
+
}).length === 0,
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
199
255
|
export const validateProviderRestrictionInput = (
|
|
200
256
|
input: ProviderRestrictionInput,
|
|
257
|
+
options?: ValidateProviderRestrictionOptions,
|
|
201
258
|
): ProviderRestrictionInputError[] => {
|
|
202
259
|
const errors: ProviderRestrictionInputError[] = [];
|
|
203
260
|
if (!Number(input.payee_id) || Number(input.payee_id) < 1) {
|
|
@@ -245,6 +302,44 @@ export const validateProviderRestrictionInput = (
|
|
|
245
302
|
}
|
|
246
303
|
}
|
|
247
304
|
|
|
305
|
+
const universe = (options?.universe ?? [])
|
|
306
|
+
.map(normalizeRestrictionValue)
|
|
307
|
+
.filter(Boolean);
|
|
308
|
+
if (
|
|
309
|
+
universe.length > 0 &&
|
|
310
|
+
Number(input.payee_id) > 0 &&
|
|
311
|
+
isProviderRestrictionMode(input.mode) &&
|
|
312
|
+
normalizeProviderRestrictionValues(input.values).length > 0
|
|
313
|
+
) {
|
|
314
|
+
const proposed: ProviderRestrictionRule = {
|
|
315
|
+
payee_id: Number(input.payee_id),
|
|
316
|
+
mode: input.mode,
|
|
317
|
+
values: normalizeProviderRestrictionValues(input.values),
|
|
318
|
+
start_date: start,
|
|
319
|
+
end_date: end,
|
|
320
|
+
motivo: normalizeRestrictionValue(input.motivo),
|
|
321
|
+
};
|
|
322
|
+
const otherRules = (options?.otherRules ?? []).filter(
|
|
323
|
+
(rule) =>
|
|
324
|
+
Number(rule.payee_id) === Number(input.payee_id) &&
|
|
325
|
+
!normalizeRestrictionValue(rule.fechaEliminacion),
|
|
326
|
+
);
|
|
327
|
+
if (
|
|
328
|
+
restrictionLeavesNoAllowedValues({
|
|
329
|
+
payeeId: Number(input.payee_id),
|
|
330
|
+
rules: [...otherRules, proposed],
|
|
331
|
+
universe,
|
|
332
|
+
})
|
|
333
|
+
) {
|
|
334
|
+
const noun =
|
|
335
|
+
normalizeRestrictionValue(options?.valueNoun) || "opciones";
|
|
336
|
+
errors.push({
|
|
337
|
+
field: "values",
|
|
338
|
+
message: `Esta regla dejaría al proveedor sin ${noun} para escoger. Debe quedar al menos uno.`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
248
343
|
return errors;
|
|
249
344
|
};
|
|
250
345
|
|