@zauru-sdk/common 6.10.1 → 6.11.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,17 @@
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.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
+
8
+
9
+ ### Features
10
+
11
+ * motor de restricciones de tipos y verduras por proveedor ([ea09fd6](https://github.com/intuitiva/zauru-typescript-sdk/commit/ea09fd6a4cbed61fc29640baf1bc2b0b78db18da))
12
+
13
+
14
+
15
+
16
+
6
17
  ## [6.10.1](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.10.0...@zauru-sdk/common@6.10.1) (2026-10-23)
7
18
 
8
19
  **Note:** Version bump only for package @zauru-sdk/common
package/dist/esm/index.js CHANGED
@@ -18,4 +18,5 @@ __exportStar(require("./common.js"), exports);
18
18
  __exportStar(require("./gtZones.js"), exports);
19
19
  __exportStar(require("./httpClientPolicy.js"), exports);
20
20
  __exportStar(require("./oauthErrors.js"), exports);
21
+ __exportStar(require("./providerRestrictions.js"), exports);
21
22
  __exportStar(require("./rejectionPercentageAdjustment.js"), exports);
@@ -0,0 +1,200 @@
1
+ "use strict";
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;
4
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
5
+ exports.PROVIDER_TYPE_RESTRICTIONS_TABLE_VAR = "ctl_calidad_admin_restricciones_tipos_proveedor_web_app_table_id";
6
+ exports.PROVIDER_ITEM_RESTRICTIONS_TABLE_VAR = "ctl_calidad_admin_restricciones_verduras_proveedor_web_app_table_id";
7
+ const normalizeRestrictionValue = (value) => String(value ?? "").replace(/\s+/g, " ").trim();
8
+ exports.normalizeRestrictionValue = normalizeRestrictionValue;
9
+ const formatRestrictionTypeValue = (type) => [type?.Nombre, type?.Codigo]
10
+ .map((part) => String(part ?? "").trim())
11
+ .filter(Boolean)
12
+ .join(" ");
13
+ exports.formatRestrictionTypeValue = formatRestrictionTypeValue;
14
+ const assertNever = (value) => {
15
+ throw new Error(`Unexpected restriction mode: ${String(value)}`);
16
+ };
17
+ const isProviderRestrictionMode = (value) => value === "include" || value === "exclude";
18
+ exports.isProviderRestrictionMode = isProviderRestrictionMode;
19
+ const normalizeProviderRestrictionValues = (values) => {
20
+ if (Array.isArray(values)) {
21
+ return values.map(exports.normalizeRestrictionValue).filter(Boolean);
22
+ }
23
+ if (typeof values === "string") {
24
+ const trimmed = values.trim();
25
+ if (!trimmed) {
26
+ return [];
27
+ }
28
+ try {
29
+ const parsed = JSON.parse(trimmed);
30
+ if (Array.isArray(parsed)) {
31
+ return parsed.map(exports.normalizeRestrictionValue).filter(Boolean);
32
+ }
33
+ }
34
+ catch {
35
+ return trimmed
36
+ .split(",")
37
+ .map((part) => (0, exports.normalizeRestrictionValue)(part))
38
+ .filter(Boolean);
39
+ }
40
+ }
41
+ return [];
42
+ };
43
+ exports.normalizeProviderRestrictionValues = normalizeProviderRestrictionValues;
44
+ const restrictionHasDateRange = (rule) => Boolean((0, exports.normalizeRestrictionValue)(rule.start_date)) ||
45
+ Boolean((0, exports.normalizeRestrictionValue)(rule.end_date));
46
+ exports.restrictionHasDateRange = restrictionHasDateRange;
47
+ const isRestrictionActiveOnDate = (rule, date) => {
48
+ if ((0, exports.normalizeRestrictionValue)(rule.fechaEliminacion)) {
49
+ return false;
50
+ }
51
+ if (!(0, exports.isProviderRestrictionMode)(rule.mode)) {
52
+ return false;
53
+ }
54
+ const start = (0, exports.normalizeRestrictionValue)(rule.start_date);
55
+ const end = (0, exports.normalizeRestrictionValue)(rule.end_date);
56
+ if (!start && !end) {
57
+ return true;
58
+ }
59
+ if (!DATE_RE.test(date)) {
60
+ return false;
61
+ }
62
+ if (start && date < start) {
63
+ return false;
64
+ }
65
+ if (end && date > end) {
66
+ return false;
67
+ }
68
+ return true;
69
+ };
70
+ exports.isRestrictionActiveOnDate = isRestrictionActiveOnDate;
71
+ const filterActiveProviderRestrictions = (rules, payeeId, date) => (rules ?? []).filter((rule) => Number(rule.payee_id) === Number(payeeId) &&
72
+ (0, exports.isRestrictionActiveOnDate)(rule, date));
73
+ exports.filterActiveProviderRestrictions = filterActiveProviderRestrictions;
74
+ const resolveAllowedRestrictionValues = (input) => {
75
+ const universe = input.universe
76
+ .map(exports.normalizeRestrictionValue)
77
+ .filter(Boolean);
78
+ const active = (0, exports.filterActiveProviderRestrictions)(input.rules, input.payeeId, input.date);
79
+ if (active.length === 0) {
80
+ return universe;
81
+ }
82
+ const includes = active.filter((rule) => rule.mode === "include");
83
+ const excludes = active.filter((rule) => rule.mode === "exclude");
84
+ let allowed = universe;
85
+ if (includes.length > 0) {
86
+ const union = new Set(includes.flatMap((rule) => (0, exports.normalizeProviderRestrictionValues)(rule.values)));
87
+ allowed = allowed.filter((value) => union.has(value));
88
+ }
89
+ if (excludes.length > 0) {
90
+ const blocked = new Set(excludes.flatMap((rule) => (0, exports.normalizeProviderRestrictionValues)(rule.values)));
91
+ allowed = allowed.filter((value) => !blocked.has(value));
92
+ }
93
+ return allowed;
94
+ };
95
+ exports.resolveAllowedRestrictionValues = resolveAllowedRestrictionValues;
96
+ const isRestrictionValueAllowed = (input) => {
97
+ const allowed = (0, exports.resolveAllowedRestrictionValues)(input);
98
+ return allowed.includes((0, exports.normalizeRestrictionValue)(input.value));
99
+ };
100
+ exports.isRestrictionValueAllowed = isRestrictionValueAllowed;
101
+ const matchReceptionTypeCode = (formValue, types) => {
102
+ const wanted = (0, exports.normalizeRestrictionValue)(formValue);
103
+ if (!wanted) {
104
+ return "";
105
+ }
106
+ for (const entry of types ?? []) {
107
+ const type = entry && typeof entry === "object" && "data" in entry
108
+ ? entry.data
109
+ : entry;
110
+ if (!type) {
111
+ continue;
112
+ }
113
+ const code = (0, exports.normalizeRestrictionValue)(type.Codigo);
114
+ const formatted = (0, exports.formatRestrictionTypeValue)(type);
115
+ if (wanted === code || wanted === formatted) {
116
+ return code || formatted;
117
+ }
118
+ }
119
+ return wanted;
120
+ };
121
+ exports.matchReceptionTypeCode = matchReceptionTypeCode;
122
+ const validateProviderRestrictionInput = (input) => {
123
+ const errors = [];
124
+ if (!Number(input.payee_id) || Number(input.payee_id) < 1) {
125
+ errors.push({ field: "payee_id", message: "Seleccione un proveedor." });
126
+ }
127
+ if (!(0, exports.isProviderRestrictionMode)(input.mode)) {
128
+ errors.push({
129
+ field: "mode",
130
+ message: "Seleccione si se permiten o se bloquean esos valores.",
131
+ });
132
+ }
133
+ if ((0, exports.normalizeProviderRestrictionValues)(input.values).length === 0) {
134
+ errors.push({
135
+ field: "values",
136
+ message: "Seleccione al menos un valor.",
137
+ });
138
+ }
139
+ const start = (0, exports.normalizeRestrictionValue)(input.start_date);
140
+ const end = (0, exports.normalizeRestrictionValue)(input.end_date);
141
+ if (start || end) {
142
+ if (!start || !end) {
143
+ errors.push({
144
+ field: "start_date",
145
+ message: "Si usa fechas, indique inicio y fin.",
146
+ });
147
+ }
148
+ else {
149
+ if (!DATE_RE.test(start) || !DATE_RE.test(end)) {
150
+ errors.push({
151
+ field: "start_date",
152
+ message: "Las fechas deben ir en formato YYYY-MM-DD.",
153
+ });
154
+ }
155
+ else if (start > end) {
156
+ errors.push({
157
+ field: "end_date",
158
+ message: "La fecha fin no puede ser anterior a la fecha inicio.",
159
+ });
160
+ }
161
+ }
162
+ if (!(0, exports.normalizeRestrictionValue)(input.motivo)) {
163
+ errors.push({
164
+ field: "motivo",
165
+ message: "Indique el motivo de la excepción con rango de fechas.",
166
+ });
167
+ }
168
+ }
169
+ return errors;
170
+ };
171
+ exports.validateProviderRestrictionInput = validateProviderRestrictionInput;
172
+ const describeMode = (mode) => {
173
+ switch (mode) {
174
+ case "include":
175
+ return "solo puede usar";
176
+ case "exclude":
177
+ return "no puede usar";
178
+ default:
179
+ return assertNever(mode);
180
+ }
181
+ };
182
+ const checkProviderRestrictionValue = (input) => {
183
+ const allowed = (0, exports.isRestrictionValueAllowed)(input);
184
+ if (allowed) {
185
+ return { allowed: true };
186
+ }
187
+ const active = (0, exports.filterActiveProviderRestrictions)(input.rules, input.payeeId, input.date);
188
+ const includes = active.filter((rule) => rule.mode === "include");
189
+ const mode = includes.length > 0 ? "include" : "exclude";
190
+ const valueLabel = (0, exports.normalizeRestrictionValue)(input.valueLabel) ||
191
+ (0, exports.normalizeRestrictionValue)(input.value);
192
+ return {
193
+ allowed: false,
194
+ mode,
195
+ matchedValues: [valueLabel],
196
+ title: `${input.label} no permitido para este proveedor`,
197
+ description: `En la fecha ${input.date} este proveedor ${describeMode(mode)} ${input.label.toLowerCase()} "${valueLabel}".`,
198
+ };
199
+ };
200
+ exports.checkProviderRestrictionValue = checkProviderRestrictionValue;
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export * from "./common.js";
2
2
  export * from "./gtZones.js";
3
3
  export * from "./httpClientPolicy.js";
4
4
  export * from "./oauthErrors.js";
5
+ export * from "./providerRestrictions.js";
5
6
  export * from "./rejectionPercentageAdjustment.js";
@@ -0,0 +1,51 @@
1
+ import type { ProviderRestrictionCheckResult, ProviderRestrictionMode, ProviderRestrictionRule, ReceptionType } from "@zauru-sdk/types";
2
+ export declare const PROVIDER_TYPE_RESTRICTIONS_TABLE_VAR = "ctl_calidad_admin_restricciones_tipos_proveedor_web_app_table_id";
3
+ export declare const PROVIDER_ITEM_RESTRICTIONS_TABLE_VAR = "ctl_calidad_admin_restricciones_verduras_proveedor_web_app_table_id";
4
+ export declare const normalizeRestrictionValue: (value: string | number | undefined | null) => string;
5
+ export declare const formatRestrictionTypeValue: (type?: {
6
+ Nombre?: string;
7
+ Codigo?: string;
8
+ } | null) => string;
9
+ export declare const isProviderRestrictionMode: (value: unknown) => value is ProviderRestrictionMode;
10
+ export declare const normalizeProviderRestrictionValues: (values: unknown) => string[];
11
+ export declare const restrictionHasDateRange: (rule: Pick<ProviderRestrictionRule, "start_date" | "end_date">) => boolean;
12
+ export declare const isRestrictionActiveOnDate: (rule: ProviderRestrictionRule, date: string) => boolean;
13
+ export declare const filterActiveProviderRestrictions: (rules: ProviderRestrictionRule[] | undefined, payeeId: number, date: string) => ProviderRestrictionRule[];
14
+ export declare const resolveAllowedRestrictionValues: (input: {
15
+ payeeId: number;
16
+ date: string;
17
+ rules: ProviderRestrictionRule[] | undefined;
18
+ universe: Array<string | number>;
19
+ }) => string[];
20
+ export declare const isRestrictionValueAllowed: (input: {
21
+ payeeId: number;
22
+ date: string;
23
+ value: string | number;
24
+ rules: ProviderRestrictionRule[] | undefined;
25
+ universe: Array<string | number>;
26
+ }) => boolean;
27
+ export declare const matchReceptionTypeCode: (formValue: string, types: Array<ReceptionType | {
28
+ data?: ReceptionType;
29
+ }> | undefined) => string;
30
+ export type ProviderRestrictionInput = {
31
+ payee_id: number;
32
+ mode: ProviderRestrictionMode | string;
33
+ values: unknown;
34
+ start_date?: string;
35
+ end_date?: string;
36
+ motivo?: string;
37
+ };
38
+ export type ProviderRestrictionInputError = {
39
+ field: string;
40
+ message: string;
41
+ };
42
+ export declare const validateProviderRestrictionInput: (input: ProviderRestrictionInput) => ProviderRestrictionInputError[];
43
+ export declare const checkProviderRestrictionValue: (input: {
44
+ payeeId: number;
45
+ date: string;
46
+ value: string | number;
47
+ rules: ProviderRestrictionRule[] | undefined;
48
+ universe: Array<string | number>;
49
+ label: string;
50
+ valueLabel?: string;
51
+ }) => ProviderRestrictionCheckResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/common",
3
- "version": "6.10.1",
3
+ "version": "6.11.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"
15
+ "test": "tsx --test src/httpClientPolicy.test.ts src/providerRestrictions.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.26.0",
25
+ "@zauru-sdk/types": "^6.27.0",
26
26
  "moment": "^2.30.1",
27
27
  "moment-timezone": "^0.5.45"
28
28
  },
29
- "gitHead": "868c8fc18ec36bd771fbd2b3886d15b6d68fb7bb"
29
+ "gitHead": "d8141f1d30edee6edb8d2777d0e12e2f4f1b2726"
30
30
  }
package/src/index.ts CHANGED
@@ -2,4 +2,5 @@ export * from "./common.js";
2
2
  export * from "./gtZones.js";
3
3
  export * from "./httpClientPolicy.js";
4
4
  export * from "./oauthErrors.js";
5
+ export * from "./providerRestrictions.js";
5
6
  export * from "./rejectionPercentageAdjustment.js";
@@ -0,0 +1,277 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { ProviderRestrictionRule } from "@zauru-sdk/types";
4
+ import {
5
+ checkProviderRestrictionValue,
6
+ filterActiveProviderRestrictions,
7
+ isRestrictionActiveOnDate,
8
+ isRestrictionValueAllowed,
9
+ matchReceptionTypeCode,
10
+ resolveAllowedRestrictionValues,
11
+ validateProviderRestrictionInput,
12
+ } from "./providerRestrictions.js";
13
+
14
+ const rule = (
15
+ partial: Partial<ProviderRestrictionRule> &
16
+ Pick<ProviderRestrictionRule, "payee_id" | "mode" | "values">,
17
+ ): ProviderRestrictionRule => ({
18
+ ...partial,
19
+ });
20
+
21
+ describe("isRestrictionActiveOnDate", () => {
22
+ it("When there is no date range, it should apply every day", () => {
23
+ assert.equal(
24
+ isRestrictionActiveOnDate(
25
+ rule({ payee_id: 1, mode: "exclude", values: ["A"] }),
26
+ "2026-10-27",
27
+ ),
28
+ true,
29
+ );
30
+ });
31
+
32
+ it("When the date is inside the range, it should apply on the boundaries", () => {
33
+ const dated = rule({
34
+ payee_id: 1,
35
+ mode: "exclude",
36
+ values: ["A"],
37
+ start_date: "2026-10-27",
38
+ end_date: "2026-11-02",
39
+ });
40
+ assert.equal(isRestrictionActiveOnDate(dated, "2026-10-27"), true);
41
+ assert.equal(isRestrictionActiveOnDate(dated, "2026-11-02"), true);
42
+ assert.equal(isRestrictionActiveOnDate(dated, "2026-10-26"), false);
43
+ assert.equal(isRestrictionActiveOnDate(dated, "2026-11-03"), false);
44
+ });
45
+
46
+ it("When the rule is soft-deleted, it should never apply", () => {
47
+ assert.equal(
48
+ isRestrictionActiveOnDate(
49
+ rule({
50
+ payee_id: 1,
51
+ mode: "exclude",
52
+ values: ["A"],
53
+ fechaEliminacion: "2026-10-01T00:00:00.000Z",
54
+ }),
55
+ "2026-10-27",
56
+ ),
57
+ false,
58
+ );
59
+ });
60
+ });
61
+
62
+ describe("resolveAllowedRestrictionValues", () => {
63
+ const universe = ["Normal", "Certificado 1", "Certificado 2"];
64
+
65
+ it("When there are no rules, it should allow the whole universe", () => {
66
+ assert.deepEqual(
67
+ resolveAllowedRestrictionValues({
68
+ payeeId: 10,
69
+ date: "2026-10-27",
70
+ rules: [],
71
+ universe,
72
+ }),
73
+ universe,
74
+ );
75
+ });
76
+
77
+ it("When an exclude rule is active, it should remove those codes", () => {
78
+ assert.deepEqual(
79
+ resolveAllowedRestrictionValues({
80
+ payeeId: 10,
81
+ date: "2026-10-28",
82
+ rules: [
83
+ rule({
84
+ payee_id: 10,
85
+ mode: "exclude",
86
+ values: ["Certificado 1", "Certificado 2"],
87
+ start_date: "2026-10-27",
88
+ end_date: "2026-11-02",
89
+ }),
90
+ ],
91
+ universe,
92
+ }),
93
+ ["Normal"],
94
+ );
95
+ });
96
+
97
+ it("When the date is outside the exclude range, it should allow everything", () => {
98
+ assert.deepEqual(
99
+ resolveAllowedRestrictionValues({
100
+ payeeId: 10,
101
+ date: "2026-11-10",
102
+ rules: [
103
+ rule({
104
+ payee_id: 10,
105
+ mode: "exclude",
106
+ values: ["Certificado 1"],
107
+ start_date: "2026-10-27",
108
+ end_date: "2026-11-02",
109
+ }),
110
+ ],
111
+ universe,
112
+ }),
113
+ universe,
114
+ );
115
+ });
116
+
117
+ it("When an include rule is active, it should keep only those values", () => {
118
+ assert.deepEqual(
119
+ resolveAllowedRestrictionValues({
120
+ payeeId: 10,
121
+ date: "2026-10-28",
122
+ rules: [
123
+ rule({
124
+ payee_id: 10,
125
+ mode: "include",
126
+ values: ["Normal", "Certificado 1"],
127
+ }),
128
+ ],
129
+ universe,
130
+ }),
131
+ ["Normal", "Certificado 1"],
132
+ );
133
+ });
134
+
135
+ it("When include and exclude overlap, it should subtract the excluded values", () => {
136
+ assert.deepEqual(
137
+ resolveAllowedRestrictionValues({
138
+ payeeId: 10,
139
+ date: "2026-10-28",
140
+ rules: [
141
+ rule({
142
+ payee_id: 10,
143
+ mode: "include",
144
+ values: ["Normal", "Certificado 1"],
145
+ }),
146
+ rule({
147
+ payee_id: 10,
148
+ mode: "exclude",
149
+ values: ["Certificado 1"],
150
+ start_date: "2026-10-27",
151
+ end_date: "2026-11-02",
152
+ }),
153
+ ],
154
+ universe,
155
+ }),
156
+ ["Normal"],
157
+ );
158
+ });
159
+
160
+ it("When the rule belongs to another payee, it should ignore it", () => {
161
+ assert.equal(
162
+ filterActiveProviderRestrictions(
163
+ [
164
+ rule({
165
+ payee_id: 99,
166
+ mode: "exclude",
167
+ values: ["Normal"],
168
+ }),
169
+ ],
170
+ 10,
171
+ "2026-10-28",
172
+ ).length,
173
+ 0,
174
+ );
175
+ });
176
+ });
177
+
178
+ describe("matchReceptionTypeCode", () => {
179
+ const types = [
180
+ { Nombre: "Certificado", Codigo: "1" },
181
+ { Nombre: "Normal", Codigo: "N" },
182
+ ];
183
+
184
+ it("When the form value is Nombre + Codigo, it should return the code", () => {
185
+ assert.equal(matchReceptionTypeCode("Certificado 1", types), "1");
186
+ assert.equal(matchReceptionTypeCode("1", types), "1");
187
+ });
188
+ });
189
+
190
+ describe("isRestrictionValueAllowed", () => {
191
+ it("When the selected type is excluded, it should reject it", () => {
192
+ assert.equal(
193
+ isRestrictionValueAllowed({
194
+ payeeId: 7,
195
+ date: "2026-10-28",
196
+ value: "1",
197
+ universe: ["N", "1"],
198
+ rules: [
199
+ rule({
200
+ payee_id: 7,
201
+ mode: "exclude",
202
+ values: ["1"],
203
+ start_date: "2026-10-27",
204
+ end_date: "2026-11-02",
205
+ }),
206
+ ],
207
+ }),
208
+ false,
209
+ );
210
+ });
211
+ });
212
+
213
+ describe("checkProviderRestrictionValue", () => {
214
+ it("When blocked, it should return a title and description", () => {
215
+ const result = checkProviderRestrictionValue({
216
+ payeeId: 7,
217
+ date: "2026-10-28",
218
+ value: "1",
219
+ valueLabel: "Certificado 1",
220
+ label: "Tipo",
221
+ universe: ["N", "1"],
222
+ rules: [
223
+ rule({
224
+ payee_id: 7,
225
+ mode: "exclude",
226
+ values: ["1"],
227
+ start_date: "2026-10-27",
228
+ end_date: "2026-11-02",
229
+ }),
230
+ ],
231
+ });
232
+ assert.equal(result.allowed, false);
233
+ if (!result.allowed) {
234
+ assert.match(result.description, /no puede usar tipo "Certificado 1"/);
235
+ }
236
+ });
237
+ });
238
+
239
+ describe("validateProviderRestrictionInput", () => {
240
+ it("When dates are set without a reason, it should require motivo", () => {
241
+ const errors = validateProviderRestrictionInput({
242
+ payee_id: 1,
243
+ mode: "exclude",
244
+ values: ["1"],
245
+ start_date: "2026-10-27",
246
+ end_date: "2026-11-02",
247
+ motivo: "",
248
+ });
249
+ assert.equal(
250
+ errors.some((error) => error.field === "motivo"),
251
+ true,
252
+ );
253
+ });
254
+
255
+ it("When there is no date range, it should not require motivo", () => {
256
+ const errors = validateProviderRestrictionInput({
257
+ payee_id: 1,
258
+ mode: "include",
259
+ values: ["1"],
260
+ });
261
+ assert.deepEqual(errors, []);
262
+ });
263
+
264
+ it("When only one date is set, it should require both", () => {
265
+ const errors = validateProviderRestrictionInput({
266
+ payee_id: 1,
267
+ mode: "exclude",
268
+ values: ["1"],
269
+ start_date: "2026-10-27",
270
+ motivo: "Auditoría",
271
+ });
272
+ assert.equal(
273
+ errors.some((error) => error.field === "start_date"),
274
+ true,
275
+ );
276
+ });
277
+ });
@@ -0,0 +1,295 @@
1
+ import type {
2
+ ProviderRestrictionCheckResult,
3
+ ProviderRestrictionMode,
4
+ ProviderRestrictionRule,
5
+ ReceptionType,
6
+ } from "@zauru-sdk/types";
7
+
8
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
9
+
10
+ export const PROVIDER_TYPE_RESTRICTIONS_TABLE_VAR =
11
+ "ctl_calidad_admin_restricciones_tipos_proveedor_web_app_table_id";
12
+
13
+ export const PROVIDER_ITEM_RESTRICTIONS_TABLE_VAR =
14
+ "ctl_calidad_admin_restricciones_verduras_proveedor_web_app_table_id";
15
+
16
+ export const normalizeRestrictionValue = (
17
+ value: string | number | undefined | null,
18
+ ): string => String(value ?? "").replace(/\s+/g, " ").trim();
19
+
20
+ export const formatRestrictionTypeValue = (
21
+ type?: { Nombre?: string; Codigo?: string } | null,
22
+ ): string =>
23
+ [type?.Nombre, type?.Codigo]
24
+ .map((part) => String(part ?? "").trim())
25
+ .filter(Boolean)
26
+ .join(" ");
27
+
28
+ const assertNever = (value: never): never => {
29
+ throw new Error(`Unexpected restriction mode: ${String(value)}`);
30
+ };
31
+
32
+ export const isProviderRestrictionMode = (
33
+ value: unknown,
34
+ ): value is ProviderRestrictionMode =>
35
+ value === "include" || value === "exclude";
36
+
37
+ export const normalizeProviderRestrictionValues = (
38
+ values: unknown,
39
+ ): string[] => {
40
+ if (Array.isArray(values)) {
41
+ return values.map(normalizeRestrictionValue).filter(Boolean);
42
+ }
43
+ if (typeof values === "string") {
44
+ const trimmed = values.trim();
45
+ if (!trimmed) {
46
+ return [];
47
+ }
48
+ try {
49
+ const parsed = JSON.parse(trimmed);
50
+ if (Array.isArray(parsed)) {
51
+ return parsed.map(normalizeRestrictionValue).filter(Boolean);
52
+ }
53
+ } catch {
54
+ return trimmed
55
+ .split(",")
56
+ .map((part) => normalizeRestrictionValue(part))
57
+ .filter(Boolean);
58
+ }
59
+ }
60
+ return [];
61
+ };
62
+
63
+ export const restrictionHasDateRange = (
64
+ rule: Pick<ProviderRestrictionRule, "start_date" | "end_date">,
65
+ ): boolean =>
66
+ Boolean(normalizeRestrictionValue(rule.start_date)) ||
67
+ Boolean(normalizeRestrictionValue(rule.end_date));
68
+
69
+ export const isRestrictionActiveOnDate = (
70
+ rule: ProviderRestrictionRule,
71
+ date: string,
72
+ ): boolean => {
73
+ if (normalizeRestrictionValue(rule.fechaEliminacion)) {
74
+ return false;
75
+ }
76
+ if (!isProviderRestrictionMode(rule.mode)) {
77
+ return false;
78
+ }
79
+ const start = normalizeRestrictionValue(rule.start_date);
80
+ const end = normalizeRestrictionValue(rule.end_date);
81
+ if (!start && !end) {
82
+ return true;
83
+ }
84
+ if (!DATE_RE.test(date)) {
85
+ return false;
86
+ }
87
+ if (start && date < start) {
88
+ return false;
89
+ }
90
+ if (end && date > end) {
91
+ return false;
92
+ }
93
+ return true;
94
+ };
95
+
96
+ export const filterActiveProviderRestrictions = (
97
+ rules: ProviderRestrictionRule[] | undefined,
98
+ payeeId: number,
99
+ date: string,
100
+ ): ProviderRestrictionRule[] =>
101
+ (rules ?? []).filter(
102
+ (rule) =>
103
+ Number(rule.payee_id) === Number(payeeId) &&
104
+ isRestrictionActiveOnDate(rule, date),
105
+ );
106
+
107
+ export const resolveAllowedRestrictionValues = (input: {
108
+ payeeId: number;
109
+ date: string;
110
+ rules: ProviderRestrictionRule[] | undefined;
111
+ universe: Array<string | number>;
112
+ }): string[] => {
113
+ const universe = input.universe
114
+ .map(normalizeRestrictionValue)
115
+ .filter(Boolean);
116
+ const active = filterActiveProviderRestrictions(
117
+ input.rules,
118
+ input.payeeId,
119
+ input.date,
120
+ );
121
+
122
+ if (active.length === 0) {
123
+ return universe;
124
+ }
125
+
126
+ const includes = active.filter((rule) => rule.mode === "include");
127
+ const excludes = active.filter((rule) => rule.mode === "exclude");
128
+
129
+ let allowed = universe;
130
+ if (includes.length > 0) {
131
+ const union = new Set(
132
+ includes.flatMap((rule) => normalizeProviderRestrictionValues(rule.values)),
133
+ );
134
+ allowed = allowed.filter((value) => union.has(value));
135
+ }
136
+
137
+ if (excludes.length > 0) {
138
+ const blocked = new Set(
139
+ excludes.flatMap((rule) => normalizeProviderRestrictionValues(rule.values)),
140
+ );
141
+ allowed = allowed.filter((value) => !blocked.has(value));
142
+ }
143
+
144
+ return allowed;
145
+ };
146
+
147
+ export const isRestrictionValueAllowed = (input: {
148
+ payeeId: number;
149
+ date: string;
150
+ value: string | number;
151
+ rules: ProviderRestrictionRule[] | undefined;
152
+ universe: Array<string | number>;
153
+ }): boolean => {
154
+ const allowed = resolveAllowedRestrictionValues(input);
155
+ return allowed.includes(normalizeRestrictionValue(input.value));
156
+ };
157
+
158
+ export const matchReceptionTypeCode = (
159
+ formValue: string,
160
+ types: Array<ReceptionType | { data?: ReceptionType }> | undefined,
161
+ ): string => {
162
+ const wanted = normalizeRestrictionValue(formValue);
163
+ if (!wanted) {
164
+ return "";
165
+ }
166
+
167
+ for (const entry of types ?? []) {
168
+ const type =
169
+ entry && typeof entry === "object" && "data" in entry
170
+ ? entry.data
171
+ : (entry as ReceptionType);
172
+ if (!type) {
173
+ continue;
174
+ }
175
+ const code = normalizeRestrictionValue(type.Codigo);
176
+ const formatted = formatRestrictionTypeValue(type);
177
+ if (wanted === code || wanted === formatted) {
178
+ return code || formatted;
179
+ }
180
+ }
181
+
182
+ return wanted;
183
+ };
184
+
185
+ export type ProviderRestrictionInput = {
186
+ payee_id: number;
187
+ mode: ProviderRestrictionMode | string;
188
+ values: unknown;
189
+ start_date?: string;
190
+ end_date?: string;
191
+ motivo?: string;
192
+ };
193
+
194
+ export type ProviderRestrictionInputError = {
195
+ field: string;
196
+ message: string;
197
+ };
198
+
199
+ export const validateProviderRestrictionInput = (
200
+ input: ProviderRestrictionInput,
201
+ ): ProviderRestrictionInputError[] => {
202
+ const errors: ProviderRestrictionInputError[] = [];
203
+ if (!Number(input.payee_id) || Number(input.payee_id) < 1) {
204
+ errors.push({ field: "payee_id", message: "Seleccione un proveedor." });
205
+ }
206
+ if (!isProviderRestrictionMode(input.mode)) {
207
+ errors.push({
208
+ field: "mode",
209
+ message: "Seleccione si se permiten o se bloquean esos valores.",
210
+ });
211
+ }
212
+ if (normalizeProviderRestrictionValues(input.values).length === 0) {
213
+ errors.push({
214
+ field: "values",
215
+ message: "Seleccione al menos un valor.",
216
+ });
217
+ }
218
+
219
+ const start = normalizeRestrictionValue(input.start_date);
220
+ const end = normalizeRestrictionValue(input.end_date);
221
+ if (start || end) {
222
+ if (!start || !end) {
223
+ errors.push({
224
+ field: "start_date",
225
+ message: "Si usa fechas, indique inicio y fin.",
226
+ });
227
+ } else {
228
+ if (!DATE_RE.test(start) || !DATE_RE.test(end)) {
229
+ errors.push({
230
+ field: "start_date",
231
+ message: "Las fechas deben ir en formato YYYY-MM-DD.",
232
+ });
233
+ } else if (start > end) {
234
+ errors.push({
235
+ field: "end_date",
236
+ message: "La fecha fin no puede ser anterior a la fecha inicio.",
237
+ });
238
+ }
239
+ }
240
+ if (!normalizeRestrictionValue(input.motivo)) {
241
+ errors.push({
242
+ field: "motivo",
243
+ message: "Indique el motivo de la excepción con rango de fechas.",
244
+ });
245
+ }
246
+ }
247
+
248
+ return errors;
249
+ };
250
+
251
+ const describeMode = (mode: ProviderRestrictionMode): string => {
252
+ switch (mode) {
253
+ case "include":
254
+ return "solo puede usar";
255
+ case "exclude":
256
+ return "no puede usar";
257
+ default:
258
+ return assertNever(mode);
259
+ }
260
+ };
261
+
262
+ export const checkProviderRestrictionValue = (input: {
263
+ payeeId: number;
264
+ date: string;
265
+ value: string | number;
266
+ rules: ProviderRestrictionRule[] | undefined;
267
+ universe: Array<string | number>;
268
+ label: string;
269
+ valueLabel?: string;
270
+ }): ProviderRestrictionCheckResult => {
271
+ const allowed = isRestrictionValueAllowed(input);
272
+ if (allowed) {
273
+ return { allowed: true };
274
+ }
275
+
276
+ const active = filterActiveProviderRestrictions(
277
+ input.rules,
278
+ input.payeeId,
279
+ input.date,
280
+ );
281
+ const includes = active.filter((rule) => rule.mode === "include");
282
+ const mode: ProviderRestrictionMode =
283
+ includes.length > 0 ? "include" : "exclude";
284
+ const valueLabel =
285
+ normalizeRestrictionValue(input.valueLabel) ||
286
+ normalizeRestrictionValue(input.value);
287
+
288
+ return {
289
+ allowed: false,
290
+ mode,
291
+ matchedValues: [valueLabel],
292
+ title: `${input.label} no permitido para este proveedor`,
293
+ description: `En la fecha ${input.date} este proveedor ${describeMode(mode)} ${input.label.toLowerCase()} "${valueLabel}".`,
294
+ };
295
+ };