@zauru-sdk/utils 6.19.1 → 6.21.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/dist/esm/index.js CHANGED
@@ -26,6 +26,7 @@ export * from "./zauru/rejectionPercentageAdjustmentRules.utils.js";
26
26
  export * from "./zauru/weeklyItemPrice.utils.js";
27
27
  export * from "./zauru/templates.utils.js";
28
28
  export * from "./zauru/tiposMuestra.utils.js";
29
+ export * from "./zauru/rbac/index.js";
29
30
  export * from "./zauru/webapp-tables.utils.js";
30
31
  export * from "./zauru/4pinos-po-discount-history.utils.js";
31
32
  export * from "./zauru/programaciones.utils.js";
@@ -41,7 +41,7 @@ export function createLoginAction(extras) {
41
41
  return redirect(authorizeUrl(request));
42
42
  }
43
43
  if (session.has("code")) {
44
- if (isDenied(extras?.requireAccess?.(session))) {
44
+ if (isDenied(await extras?.requireAccess?.(session))) {
45
45
  return redirect("/");
46
46
  }
47
47
  return redirect("/home");
@@ -50,7 +50,7 @@ export function createLoginAction(extras) {
50
50
  if (loginResponse.error || !loginResponse.data) {
51
51
  return Response.json(loginErrorPayload(loginResponse.userMsg?.toString() ?? ""));
52
52
  }
53
- if (isDenied(extras?.requireAccess?.(session))) {
53
+ if (isDenied(await extras?.requireAccess?.(session))) {
54
54
  return Response.json(loginErrorPayload("No tienes permiso para acceder a esta aplicación. Si crees que esto es un error, contacta al administrador.", "Acceso denegado"));
55
55
  }
56
56
  if (extras?.afterLogin) {
@@ -78,7 +78,7 @@ export function createSessionGuardLoader(extras) {
78
78
  if (!session.has("username")) {
79
79
  return redirect("/");
80
80
  }
81
- if (isDenied(extras?.requireAccess?.(session))) {
81
+ if (isDenied(await extras?.requireAccess?.(session))) {
82
82
  return redirect("/");
83
83
  }
84
84
  return Response.json({});
@@ -104,7 +104,7 @@ export function createIndexLoader(extras) {
104
104
  return async ({ request }) => {
105
105
  const session = await getSession(request.headers.get("Cookie"));
106
106
  if (session.has("username")) {
107
- if (isDenied(extras?.requireAccess?.(session))) {
107
+ if (isDenied(await extras?.requireAccess?.(session))) {
108
108
  return Response.json({});
109
109
  }
110
110
  return redirect("/home");
@@ -121,7 +121,7 @@ export function createReloadCatalogsLoader(extras) {
121
121
  if (!session.has("username")) {
122
122
  return redirect("/");
123
123
  }
124
- if (isDenied(extras?.requireAccess?.(session))) {
124
+ if (isDenied(await extras?.requireAccess?.(session))) {
125
125
  return redirect("/");
126
126
  }
127
127
  try {
@@ -1,6 +1,7 @@
1
1
  import { handlePossibleAxiosErrors } from "@zauru-sdk/common";
2
2
  import { createWebAppTableRegister, getVariablesByName, getWebAppTableRegisters, updateWebAppTableRegister, } from "@zauru-sdk/services";
3
3
  const TABLE_VAR = "price_adjustment_rules_web_app_table_id";
4
+ export const PRICE_ADJUSTMENT_REVERSAL_RULES_TABLE_VAR = "price_adjustment_reversal_rules_web_app_table_id";
4
5
  const DEFAULT_FILTERS = {
5
6
  itemMode: "all",
6
7
  itemIds: [],
@@ -44,6 +45,29 @@ export const updatePriceAdjustmentRule = (headers, session, id, body) => {
44
45
  return updateWebAppTableRegister(headers, tableId, Number(id), body);
45
46
  });
46
47
  };
48
+ const getReversalTableId = async (headers, session) => {
49
+ const vars = await getVariablesByName(headers, session, [
50
+ PRICE_ADJUSTMENT_REVERSAL_RULES_TABLE_VAR,
51
+ ]);
52
+ return vars[PRICE_ADJUSTMENT_REVERSAL_RULES_TABLE_VAR];
53
+ };
54
+ export const filterActivePriceAdjustmentReversalRules = (rules) => (rules ?? []).filter((rule) => !rule.data?.fechaEliminacion && rule.data?.activa !== false);
55
+ export const getPriceAdjustmentReversalRules = (headers, session) => handlePossibleAxiosErrors(async () => {
56
+ const tableId = await getReversalTableId(headers, session);
57
+ const response = await getWebAppTableRegisters(session, tableId);
58
+ if (response.error) {
59
+ throw new Error(`Ocurrió un error al consultar las reglas de reversión de precio: ${response.userMsg}`);
60
+ }
61
+ return response.data ?? [];
62
+ });
63
+ export const createPriceAdjustmentReversalRule = (headers, session, body) => handlePossibleAxiosErrors(async () => {
64
+ const tableId = await getReversalTableId(headers, session);
65
+ return createWebAppTableRegister(headers, tableId, body);
66
+ });
67
+ export const updatePriceAdjustmentReversalRule = (headers, session, id, body) => handlePossibleAxiosErrors(async () => {
68
+ const tableId = await getReversalTableId(headers, session);
69
+ return updateWebAppTableRegister(headers, tableId, Number(id), body);
70
+ });
47
71
  export const normalizeComparableValue = (value) => String(value ?? "").replace(/\s+/g, " ").trim();
48
72
  export const formatReceptionTypeValue = (type) => [type?.Nombre, type?.Codigo]
49
73
  .map((part) => String(part ?? "").trim())
@@ -104,11 +128,15 @@ export const applyPriceAdjustmentRules = (basePrice, rules, ctx) => {
104
128
  let runningTotal = safeBase;
105
129
  const steps = [];
106
130
  const normalized = rules
107
- .map((rule) => ("data" in rule ? rule.data : rule))
108
- .filter((rule) => Boolean(rule))
109
- .filter((rule) => !rule.fechaEliminacion && rule.activa !== false)
110
- .sort((a, b) => (a.prioridad ?? 0) - (b.prioridad ?? 0));
111
- for (const rule of normalized) {
131
+ .map((rule) => ({
132
+ rule: "data" in rule ? rule.data : rule,
133
+ ruleId: "data" in rule ? rule.id : undefined,
134
+ }))
135
+ .filter((entry) => Boolean(entry.rule));
136
+ const activeRules = normalized
137
+ .filter(({ rule }) => !rule.fechaEliminacion && rule.activa !== false)
138
+ .sort((a, b) => (a.rule.prioridad ?? 0) - (b.rule.prioridad ?? 0));
139
+ for (const { rule, ruleId } of activeRules) {
112
140
  if (!priceAdjustmentRuleMatches(rule, ctx)) {
113
141
  continue;
114
142
  }
@@ -125,6 +153,7 @@ export const applyPriceAdjustmentRules = (basePrice, rules, ctx) => {
125
153
  }
126
154
  runningTotal = roundMoney(runningTotal);
127
155
  steps.push({
156
+ ...(ruleId !== undefined ? { ruleId } : {}),
128
157
  ruleName: rule.nombre,
129
158
  operation: rule.operacion,
130
159
  valueType: rule.tipoValor,
@@ -142,3 +171,78 @@ export const applyPriceAdjustmentRules = (basePrice, rules, ctx) => {
142
171
  result.description = formatPriceAdjustmentDescription(result);
143
172
  return result;
144
173
  };
174
+ const sameMoney = (left, right) => Math.abs(roundMoney(left, 2) - roundMoney(right, 2)) < 0.000001;
175
+ export const buildPriceAdjustmentReversalPlan = ({ calculation, currentPrice, reversalRules, history, }) => {
176
+ const base = {
177
+ calculationId: calculation.calculationId,
178
+ detailId: calculation.detailId,
179
+ itemId: calculation.itemId,
180
+ previousPrice: roundMoney(currentPrice, 2),
181
+ finalPrice: roundMoney(currentPrice, 2),
182
+ penaltyAmount: 0,
183
+ matches: [],
184
+ };
185
+ if (!calculation.calculationId || calculation.detailId == null) {
186
+ return {
187
+ ...base,
188
+ status: "ineligible",
189
+ reason: "El cálculo no contiene calculationId y detailId.",
190
+ };
191
+ }
192
+ const activeRules = filterActivePriceAdjustmentReversalRules(reversalRules);
193
+ const reversalBySource = new Map(activeRules.map((entry) => [entry.data.sourceRuleId, entry]));
194
+ const applicable = calculation.steps.flatMap((step) => {
195
+ if (step.ruleId == null ||
196
+ step.operation !== "add" ||
197
+ step.appliedAmount <= 0) {
198
+ return [];
199
+ }
200
+ const reversal = reversalBySource.get(step.ruleId);
201
+ if (!reversal)
202
+ return [];
203
+ return [
204
+ {
205
+ sourceRuleId: step.ruleId,
206
+ sourceRuleName: step.ruleName,
207
+ reversalRuleId: reversal.id,
208
+ reversalRuleName: reversal.data.nombre,
209
+ appliedAmount: roundMoney(step.appliedAmount),
210
+ },
211
+ ];
212
+ });
213
+ if (applicable.length === 0) {
214
+ return {
215
+ ...base,
216
+ status: "ineligible",
217
+ reason: "Ningún paso del cálculo tiene una regla de reversión activa.",
218
+ };
219
+ }
220
+ const calculationHistory = (history ?? []).filter((entry) => entry.calculationId === calculation.calculationId &&
221
+ entry.detailId === calculation.detailId);
222
+ const alreadyAppliedSourceIds = new Set(calculationHistory.map((entry) => entry.sourceRuleId));
223
+ const pending = applicable.filter((match) => !alreadyAppliedSourceIds.has(match.sourceRuleId));
224
+ if (pending.length === 0) {
225
+ return {
226
+ ...base,
227
+ status: "already_applied",
228
+ reason: "Todas las reglas vinculadas ya fueron penalizadas.",
229
+ };
230
+ }
231
+ const expectedCurrentPrice = roundMoney(Number(calculation.finalPrice) +
232
+ calculationHistory.reduce((sum, entry) => sum + entry.amount, 0), 2);
233
+ if (!sameMoney(currentPrice, expectedCurrentPrice)) {
234
+ return {
235
+ ...base,
236
+ status: "price_changed",
237
+ reason: `El precio actual ${roundMoney(currentPrice, 2)} no coincide con el precio esperado ${expectedCurrentPrice}.`,
238
+ };
239
+ }
240
+ const penaltyAmount = roundMoney(-pending.reduce((sum, match) => sum + match.appliedAmount, 0));
241
+ return {
242
+ ...base,
243
+ status: "eligible",
244
+ finalPrice: roundMoney(currentPrice + penaltyAmount, 2),
245
+ penaltyAmount,
246
+ matches: pending,
247
+ };
248
+ };
@@ -0,0 +1,36 @@
1
+ import { DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES } from "./catalog.js";
2
+ export function isSuperAdminEmail(email, suffixes = DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES) {
3
+ if (!email) {
4
+ return false;
5
+ }
6
+ const normalized = email.toLowerCase().trim();
7
+ return suffixes.some((suffix) => normalized.endsWith(suffix.toLowerCase()));
8
+ }
9
+ export function hasPermissionKey(superAdmin, permissionKeys, key) {
10
+ if (superAdmin) {
11
+ return true;
12
+ }
13
+ return (permissionKeys ?? []).includes(key);
14
+ }
15
+ export function hasPermission(context, key) {
16
+ if (!context) {
17
+ return false;
18
+ }
19
+ return hasPermissionKey(context.superAdmin, context.permissionKeys, key);
20
+ }
21
+ export function isActiveWebappRow(row) {
22
+ return Boolean(row?.data) && !row?.data?.fechaEliminacion;
23
+ }
24
+ export function emptyAccessContext(employeeId, email) {
25
+ return {
26
+ employeeId,
27
+ email,
28
+ superAdmin: false,
29
+ roleId: null,
30
+ roleName: null,
31
+ assignmentId: null,
32
+ permissionKeys: [],
33
+ allowedItemIds: [],
34
+ allowedPayeeIds: [],
35
+ };
36
+ }
@@ -0,0 +1,67 @@
1
+ export const DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES = [
2
+ "@zauru.com",
3
+ "@intuitiva.biz",
4
+ ];
5
+ export function flattenPermissionRows(groups) {
6
+ return groups.flatMap((group) => group.actions.map((action) => ({
7
+ groupId: group.id,
8
+ groupLabel: group.label,
9
+ key: action.key,
10
+ label: action.label,
11
+ })));
12
+ }
13
+ export function allPermissionKeysFromGroups(groups) {
14
+ return flattenPermissionRows(groups).map((row) => row.key);
15
+ }
16
+ export function isValidPermissionKey(key, validKeys) {
17
+ if (Array.isArray(validKeys)) {
18
+ return validKeys.includes(key);
19
+ }
20
+ return validKeys.has(key);
21
+ }
22
+ export function normalizePermissionKeys(value) {
23
+ if (Array.isArray(value)) {
24
+ return value.filter((key) => typeof key === "string");
25
+ }
26
+ if (typeof value === "string") {
27
+ const trimmed = value.trim();
28
+ if (!trimmed) {
29
+ return [];
30
+ }
31
+ try {
32
+ const parsed = JSON.parse(trimmed);
33
+ if (Array.isArray(parsed)) {
34
+ return parsed.filter((key) => typeof key === "string");
35
+ }
36
+ }
37
+ catch {
38
+ return [trimmed];
39
+ }
40
+ }
41
+ return [];
42
+ }
43
+ export function normalizeIdList(value) {
44
+ if (!Array.isArray(value)) {
45
+ return [];
46
+ }
47
+ return value
48
+ .map((item) => Number(item))
49
+ .filter((item) => Number.isFinite(item) && item > 0);
50
+ }
51
+ export function togglePermissionKey(keys, key, granted) {
52
+ const next = new Set(keys);
53
+ if (granted) {
54
+ next.add(key);
55
+ }
56
+ else {
57
+ next.delete(key);
58
+ }
59
+ return [...next];
60
+ }
61
+ export function addPermissionKeys(keys, add) {
62
+ return [...new Set([...keys, ...add])];
63
+ }
64
+ export function removePermissionKeys(keys, remove) {
65
+ const drop = new Set(remove);
66
+ return keys.filter((key) => !drop.has(key));
67
+ }
@@ -0,0 +1,252 @@
1
+ import { redirect } from "@remix-run/node";
2
+ import { handlePossibleAxiosErrors } from "@zauru-sdk/common";
3
+ import { createWebAppTableRegister, getVariablesByName, getWebAppRow, getWebAppTableRegisters, updateWebAppTableRegister, } from "@zauru-sdk/services";
4
+ import { hasPermission as hasPermissionInContext, isActiveWebappRow, isSuperAdminEmail, } from "./access.js";
5
+ import { DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES, isValidPermissionKey, normalizeIdList, normalizePermissionKeys, } from "./catalog.js";
6
+ const deletedAt = () => new Date().toISOString();
7
+ const toNumber = (value) => {
8
+ const parsed = Number(value);
9
+ return Number.isFinite(parsed) ? parsed : 0;
10
+ };
11
+ export function createWebappRbac(config) {
12
+ const suffixes = config.superAdminEmailSuffixes ?? DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES;
13
+ const validKeys = new Set(config.allPermissionKeys);
14
+ const getTableIds = async (headers, session) => {
15
+ const vars = await getVariablesByName(headers, session, [
16
+ config.rolesTableVar,
17
+ config.employeeRolesTableVar,
18
+ ]);
19
+ return {
20
+ rolesTableId: vars[config.rolesTableVar],
21
+ employeeRolesTableId: vars[config.employeeRolesTableVar],
22
+ };
23
+ };
24
+ const sanitizeKeys = (keys) => normalizePermissionKeys(keys).filter((key) => isValidPermissionKey(key, validKeys));
25
+ const listRoles = (headers, session) => handlePossibleAxiosErrors(async () => {
26
+ const { rolesTableId } = await getTableIds(headers, session);
27
+ const response = await getWebAppTableRegisters(session, rolesTableId);
28
+ if (response.error) {
29
+ throw new Error(`Ocurrió un error al consultar los roles: ${response.userMsg}`);
30
+ }
31
+ return (response.data ?? [])
32
+ .filter(isActiveWebappRow)
33
+ .map((row) => ({
34
+ ...row,
35
+ data: {
36
+ ...row.data,
37
+ permission_keys: sanitizeKeys(row.data.permission_keys),
38
+ },
39
+ }));
40
+ });
41
+ const getRoleById = (session, roleId) => handlePossibleAxiosErrors(async () => {
42
+ const response = await getWebAppRow(session, roleId);
43
+ if (response.error) {
44
+ throw new Error(`Ocurrió un error al consultar el rol: ${response.userMsg}`);
45
+ }
46
+ if (!response.data || response.data.fechaEliminacion) {
47
+ return null;
48
+ }
49
+ return {
50
+ ...response.data,
51
+ permission_keys: sanitizeKeys(response.data.permission_keys),
52
+ };
53
+ });
54
+ const listEmployeeRoles = (headers, session, employeeId) => handlePossibleAxiosErrors(async () => {
55
+ const { employeeRolesTableId } = await getTableIds(headers, session);
56
+ const response = await getWebAppTableRegisters(session, employeeRolesTableId, employeeId
57
+ ? { data: { employee_id: employeeId } }
58
+ : undefined);
59
+ if (response.error) {
60
+ throw new Error(`Ocurrió un error al consultar las asignaciones de rol: ${response.userMsg}`);
61
+ }
62
+ const rows = (response.data ?? []).filter(isActiveWebappRow);
63
+ if (employeeId == null) {
64
+ return rows;
65
+ }
66
+ return rows.filter((row) => Number(row.data.employee_id) === Number(employeeId));
67
+ });
68
+ const createRole = (headers, session, body) => handlePossibleAxiosErrors(async () => {
69
+ const name = body.name.trim();
70
+ if (!name) {
71
+ throw new Error("El nombre del rol es requerido.");
72
+ }
73
+ const { rolesTableId } = await getTableIds(headers, session);
74
+ return createWebAppTableRegister(headers, rolesTableId, {
75
+ name,
76
+ description: body.description?.trim() ?? "",
77
+ permission_keys: sanitizeKeys(body.permission_keys),
78
+ fechaEliminacion: "",
79
+ });
80
+ });
81
+ const updateRole = (headers, session, roleId, body) => handlePossibleAxiosErrors(async () => {
82
+ const { rolesTableId } = await getTableIds(headers, session);
83
+ const current = await getWebAppRow(session, roleId);
84
+ if (current.error || !current.data || current.data.fechaEliminacion) {
85
+ throw new Error("El rol no existe o fue eliminado.");
86
+ }
87
+ const next = {
88
+ name: body.name?.trim() || current.data.name,
89
+ description: body.description !== undefined
90
+ ? body.description.trim()
91
+ : (current.data.description ?? ""),
92
+ permission_keys: body.permission_keys !== undefined
93
+ ? sanitizeKeys(body.permission_keys)
94
+ : sanitizeKeys(current.data.permission_keys),
95
+ fechaEliminacion: current.data.fechaEliminacion ?? "",
96
+ };
97
+ return updateWebAppTableRegister(headers, rolesTableId, roleId, next);
98
+ });
99
+ const deleteRole = (headers, session, roleId) => handlePossibleAxiosErrors(async () => {
100
+ const assignments = await listEmployeeRoles(headers, session);
101
+ if (assignments.error) {
102
+ throw new Error(assignments.userMsg ?? "No se pudieron consultar las asignaciones.");
103
+ }
104
+ const inUse = (assignments.data ?? []).some((row) => Number(row.data.role_id) === Number(roleId));
105
+ if (inUse) {
106
+ throw new Error("No se puede eliminar el rol: hay empleados asignados. Reasígnalos primero.");
107
+ }
108
+ const { rolesTableId } = await getTableIds(headers, session);
109
+ const current = await getWebAppRow(session, roleId);
110
+ if (current.error || !current.data) {
111
+ throw new Error("El rol no existe.");
112
+ }
113
+ return updateWebAppTableRegister(headers, rolesTableId, roleId, {
114
+ ...current.data,
115
+ permission_keys: sanitizeKeys(current.data.permission_keys),
116
+ fechaEliminacion: deletedAt(),
117
+ });
118
+ });
119
+ const syncRolePermissionKeys = (headers, session, roleId, keys) => updateRole(headers, session, roleId, { permission_keys: keys });
120
+ const assignEmployeeRole = (headers, session, body) => handlePossibleAxiosErrors(async () => {
121
+ const employeeId = toNumber(body.employee_id);
122
+ if (!employeeId) {
123
+ throw new Error("El empleado es requerido.");
124
+ }
125
+ const existing = await listEmployeeRoles(headers, session, employeeId);
126
+ if (existing.error) {
127
+ throw new Error(existing.userMsg ?? "No se pudieron consultar las asignaciones.");
128
+ }
129
+ const current = existing.data?.[0];
130
+ const { employeeRolesTableId } = await getTableIds(headers, session);
131
+ if (body.role_id == null) {
132
+ if (!current) {
133
+ return {
134
+ id: 0,
135
+ webapp_table_id: Number(employeeRolesTableId),
136
+ data: { Nombre: "" },
137
+ creator_id: 0,
138
+ updater_id: null,
139
+ created_at: "",
140
+ updated_at: "",
141
+ };
142
+ }
143
+ return updateWebAppTableRegister(headers, employeeRolesTableId, current.id, {
144
+ ...current.data,
145
+ allowed_item_ids: normalizeIdList(body.allowed_item_ids ?? current.data.allowed_item_ids),
146
+ allowed_payee_ids: normalizeIdList(body.allowed_payee_ids ?? current.data.allowed_payee_ids),
147
+ fechaEliminacion: deletedAt(),
148
+ });
149
+ }
150
+ const payload = {
151
+ employee_id: employeeId,
152
+ role_id: toNumber(body.role_id),
153
+ allowed_item_ids: normalizeIdList(body.allowed_item_ids),
154
+ allowed_payee_ids: normalizeIdList(body.allowed_payee_ids),
155
+ fechaEliminacion: "",
156
+ };
157
+ if (current) {
158
+ return updateWebAppTableRegister(headers, employeeRolesTableId, current.id, payload);
159
+ }
160
+ return createWebAppTableRegister(headers, employeeRolesTableId, payload);
161
+ });
162
+ const resolveAccess = async (headers, session) => {
163
+ const email = String(session.get("email") ?? "");
164
+ const employeeId = toNumber(session.get("employee_id"));
165
+ const superAdmin = isSuperAdminEmail(email, suffixes);
166
+ if (superAdmin) {
167
+ return {
168
+ employeeId,
169
+ email,
170
+ superAdmin: true,
171
+ roleId: null,
172
+ roleName: null,
173
+ assignmentId: null,
174
+ permissionKeys: [...config.allPermissionKeys],
175
+ allowedItemIds: [],
176
+ allowedPayeeIds: [],
177
+ };
178
+ }
179
+ if (!employeeId) {
180
+ return null;
181
+ }
182
+ const assignments = await listEmployeeRoles(headers, session, employeeId);
183
+ if (assignments.error) {
184
+ throw new Error(assignments.userMsg ?? "No se pudieron consultar las asignaciones.");
185
+ }
186
+ const assignment = assignments.data?.[0];
187
+ if (!assignment) {
188
+ return null;
189
+ }
190
+ const roleId = Number(assignment.data.role_id);
191
+ const role = await getRoleById(session, roleId);
192
+ if (role.error) {
193
+ throw new Error(role.userMsg ?? "No se pudo consultar el rol.");
194
+ }
195
+ if (!role.data) {
196
+ return null;
197
+ }
198
+ return {
199
+ employeeId,
200
+ email,
201
+ superAdmin: false,
202
+ roleId,
203
+ roleName: role.data.name,
204
+ assignmentId: assignment.id,
205
+ permissionKeys: sanitizeKeys(role.data.permission_keys),
206
+ allowedItemIds: normalizeIdList(assignment.data.allowed_item_ids),
207
+ allowedPayeeIds: normalizeIdList(assignment.data.allowed_payee_ids),
208
+ };
209
+ };
210
+ const hasAppAccess = async (headers, session) => {
211
+ if (isSuperAdminEmail(session.get("email"), suffixes)) {
212
+ return true;
213
+ }
214
+ const context = await resolveAccess(headers, session);
215
+ return context != null;
216
+ };
217
+ const requireAppAccess = async (headers, session) => {
218
+ if (!session.has("username")) {
219
+ throw redirect("/");
220
+ }
221
+ const context = await resolveAccess(headers, session);
222
+ if (!context) {
223
+ throw redirect("/");
224
+ }
225
+ return context;
226
+ };
227
+ const requirePermission = async (headers, session, key) => {
228
+ const context = await requireAppAccess(headers, session);
229
+ if (!hasPermissionInContext(context, key)) {
230
+ throw redirect("/");
231
+ }
232
+ return context;
233
+ };
234
+ return {
235
+ config,
236
+ getTableIds,
237
+ listRoles,
238
+ getRoleById,
239
+ createRole,
240
+ updateRole,
241
+ deleteRole,
242
+ syncRolePermissionKeys,
243
+ listEmployeeRoles,
244
+ assignEmployeeRole,
245
+ resolveAccess,
246
+ hasAppAccess,
247
+ requireAppAccess,
248
+ requirePermission,
249
+ hasPermission: hasPermissionInContext,
250
+ isSuperAdminEmail: (email) => isSuperAdminEmail(email, suffixes),
251
+ };
252
+ }
@@ -0,0 +1,3 @@
1
+ export { addPermissionKeys, allPermissionKeysFromGroups, DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES, flattenPermissionRows, isValidPermissionKey, normalizeIdList, normalizePermissionKeys, removePermissionKeys, togglePermissionKey, } from "./catalog.js";
2
+ export { emptyAccessContext, hasPermission, hasPermissionKey, isActiveWebappRow, isSuperAdminEmail, } from "./access.js";
3
+ export { createWebappRbac, } from "./createWebappRbac.js";
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export * from "./zauru/rejectionPercentageAdjustmentRules.utils.js";
26
26
  export * from "./zauru/weeklyItemPrice.utils.js";
27
27
  export * from "./zauru/templates.utils.js";
28
28
  export * from "./zauru/tiposMuestra.utils.js";
29
+ export * from "./zauru/rbac/index.js";
29
30
  export * from "./zauru/webapp-tables.utils.js";
30
31
  export * from "./zauru/4pinos-po-discount-history.utils.js";
31
32
  export * from "./zauru/programaciones.utils.js";
@@ -7,7 +7,7 @@ export type AuthAccessExtras = {
7
7
  * Return `false` to deny. Throw `redirect(...)` to send the user elsewhere.
8
8
  * Any other return (including `void`) allows access.
9
9
  */
10
- requireAccess?: (session: AuthSession) => boolean | void;
10
+ requireAccess?: (session: AuthSession) => boolean | void | Promise<boolean | void>;
11
11
  };
12
12
  export type LoginActionExtras = AuthAccessExtras & {
13
13
  afterLogin?: (ctx: {
@@ -1,10 +1,15 @@
1
1
  import type { Session } from "@remix-run/node";
2
- import { AxiosUtilsResponse, PriceAdjustmentContext, PriceAdjustmentFilters, PriceAdjustmentResult, PriceAdjustmentRule, WebAppRowGraphQL, WebAppTableUpdateResponse } from "@zauru-sdk/types";
2
+ import { AxiosUtilsResponse, PriceAdjustmentContext, PriceAdjustmentFilters, PriceAdjustmentResult, PriceAdjustmentRule, PriceAdjustmentReversalPlan, PriceAdjustmentReversalRule, PriceAdjustmentStep, CertificationPenaltyHistoryEntry, WebAppRowGraphQL, WebAppTableUpdateResponse } from "@zauru-sdk/types";
3
+ export declare const PRICE_ADJUSTMENT_REVERSAL_RULES_TABLE_VAR = "price_adjustment_reversal_rules_web_app_table_id";
3
4
  export declare const defaultPriceAdjustmentFilters: () => PriceAdjustmentFilters;
4
5
  export declare const filterActivePriceAdjustmentRules: (rules?: WebAppRowGraphQL<PriceAdjustmentRule>[]) => WebAppRowGraphQL<PriceAdjustmentRule>[];
5
6
  export declare const getPriceAdjustmentRules: (headers: any, session: Session) => Promise<AxiosUtilsResponse<WebAppRowGraphQL<PriceAdjustmentRule>[]>>;
6
7
  export declare const createPriceAdjustmentRule: (headers: any, session: Session, body: PriceAdjustmentRule) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
7
8
  export declare const updatePriceAdjustmentRule: (headers: any, session: Session, id: string, body: Partial<PriceAdjustmentRule>) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
9
+ export declare const filterActivePriceAdjustmentReversalRules: (rules?: WebAppRowGraphQL<PriceAdjustmentReversalRule>[]) => WebAppRowGraphQL<PriceAdjustmentReversalRule>[];
10
+ export declare const getPriceAdjustmentReversalRules: (headers: any, session: Session) => Promise<AxiosUtilsResponse<WebAppRowGraphQL<PriceAdjustmentReversalRule>[]>>;
11
+ export declare const createPriceAdjustmentReversalRule: (headers: any, session: Session, body: PriceAdjustmentReversalRule) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
12
+ export declare const updatePriceAdjustmentReversalRule: (headers: any, session: Session, id: string | number, body: Partial<PriceAdjustmentReversalRule>) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
8
13
  export declare const normalizeComparableValue: (value: string | number | undefined | null) => string;
9
14
  export declare const formatReceptionTypeValue: (type?: {
10
15
  Nombre?: string;
@@ -13,3 +18,15 @@ export declare const formatReceptionTypeValue: (type?: {
13
18
  export declare const priceAdjustmentRuleMatches: (rule: PriceAdjustmentRule, ctx: PriceAdjustmentContext) => boolean;
14
19
  export declare const formatPriceAdjustmentDescription: (result: Pick<PriceAdjustmentResult, "basePrice" | "finalPrice" | "steps">, baseLabel?: string) => string;
15
20
  export declare const applyPriceAdjustmentRules: (basePrice: number, rules: Array<PriceAdjustmentRule | WebAppRowGraphQL<PriceAdjustmentRule>>, ctx: PriceAdjustmentContext) => PriceAdjustmentResult;
21
+ export declare const buildPriceAdjustmentReversalPlan: ({ calculation, currentPrice, reversalRules, history, }: {
22
+ calculation: {
23
+ calculationId?: string;
24
+ detailId?: number;
25
+ itemId: number;
26
+ finalPrice: number;
27
+ steps: PriceAdjustmentStep[];
28
+ };
29
+ currentPrice: number;
30
+ reversalRules: WebAppRowGraphQL<PriceAdjustmentReversalRule>[];
31
+ history?: CertificationPenaltyHistoryEntry[];
32
+ }) => PriceAdjustmentReversalPlan;
@@ -0,0 +1,8 @@
1
+ import type { WebappAccessContext, WebAppRowGraphQL } from "@zauru-sdk/types";
2
+ export declare function isSuperAdminEmail(email: string | null | undefined, suffixes?: readonly string[]): boolean;
3
+ export declare function hasPermissionKey(superAdmin: boolean | undefined, permissionKeys: readonly string[] | undefined, key: string): boolean;
4
+ export declare function hasPermission(context: WebappAccessContext | null | undefined, key: string): boolean;
5
+ export declare function isActiveWebappRow<T extends {
6
+ fechaEliminacion?: string;
7
+ }>(row: WebAppRowGraphQL<T> | null | undefined): row is WebAppRowGraphQL<T>;
8
+ export declare function emptyAccessContext(employeeId: number, email: string): WebappAccessContext;
@@ -0,0 +1,15 @@
1
+ import type { PermissionGroup } from "@zauru-sdk/types";
2
+ export declare const DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES: readonly ["@zauru.com", "@intuitiva.biz"];
3
+ export declare function flattenPermissionRows(groups: PermissionGroup[]): Array<{
4
+ groupId: string;
5
+ groupLabel: string;
6
+ key: string;
7
+ label: string;
8
+ }>;
9
+ export declare function allPermissionKeysFromGroups(groups: PermissionGroup[]): string[];
10
+ export declare function isValidPermissionKey(key: string, validKeys: ReadonlySet<string> | readonly string[]): boolean;
11
+ export declare function normalizePermissionKeys(value: unknown): string[];
12
+ export declare function normalizeIdList(value: unknown): number[];
13
+ export declare function togglePermissionKey(keys: readonly string[], key: string, granted: boolean): string[];
14
+ export declare function addPermissionKeys(keys: readonly string[], add: readonly string[]): string[];
15
+ export declare function removePermissionKeys(keys: readonly string[], remove: readonly string[]): string[];
@@ -0,0 +1,39 @@
1
+ import { type Session } from "@remix-run/node";
2
+ import type { AxiosUtilsResponse, WebappAccessContext, WebappEmployeeRole, WebappRbacConfig, WebappRole, WebAppRowGraphQL, WebAppTableUpdateResponse } from "@zauru-sdk/types";
3
+ import { hasPermission as hasPermissionInContext } from "./access.js";
4
+ type TableIds = {
5
+ rolesTableId: string;
6
+ employeeRolesTableId: string;
7
+ };
8
+ export type CreateRoleInput = {
9
+ name: string;
10
+ description?: string;
11
+ permission_keys?: string[];
12
+ };
13
+ export type UpdateRoleInput = Partial<Pick<WebappRole, "name" | "description" | "permission_keys">>;
14
+ export type AssignEmployeeRoleInput = {
15
+ employee_id: number;
16
+ role_id: number | null;
17
+ allowed_item_ids?: number[];
18
+ allowed_payee_ids?: number[];
19
+ };
20
+ export declare function createWebappRbac(config: WebappRbacConfig): {
21
+ config: WebappRbacConfig;
22
+ getTableIds: (headers: any, session: Session) => Promise<TableIds>;
23
+ listRoles: (headers: any, session: Session) => Promise<AxiosUtilsResponse<WebAppRowGraphQL<WebappRole>[]>>;
24
+ getRoleById: (session: Session, roleId: number) => Promise<AxiosUtilsResponse<WebappRole | null>>;
25
+ createRole: (headers: any, session: Session, body: CreateRoleInput) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
26
+ updateRole: (headers: any, session: Session, roleId: number, body: UpdateRoleInput) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
27
+ deleteRole: (headers: any, session: Session, roleId: number) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
28
+ syncRolePermissionKeys: (headers: any, session: Session, roleId: number, keys: string[]) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
29
+ listEmployeeRoles: (headers: any, session: Session, employeeId?: number) => Promise<AxiosUtilsResponse<WebAppRowGraphQL<WebappEmployeeRole>[]>>;
30
+ assignEmployeeRole: (headers: any, session: Session, body: AssignEmployeeRoleInput) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
31
+ resolveAccess: (headers: any, session: Session) => Promise<WebappAccessContext | null>;
32
+ hasAppAccess: (headers: any, session: Session) => Promise<boolean>;
33
+ requireAppAccess: (headers: any, session: Session) => Promise<WebappAccessContext>;
34
+ requirePermission: (headers: any, session: Session, key: string) => Promise<WebappAccessContext>;
35
+ hasPermission: typeof hasPermissionInContext;
36
+ isSuperAdminEmail: (email: string | null | undefined) => boolean;
37
+ };
38
+ export type WebappRbac = ReturnType<typeof createWebappRbac>;
39
+ export {};
@@ -0,0 +1,3 @@
1
+ export { addPermissionKeys, allPermissionKeysFromGroups, DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES, flattenPermissionRows, isValidPermissionKey, normalizeIdList, normalizePermissionKeys, removePermissionKeys, togglePermissionKey, } from "./catalog.js";
2
+ export { emptyAccessContext, hasPermission, hasPermissionKey, isActiveWebappRow, isSuperAdminEmail, } from "./access.js";
3
+ export { createWebappRbac, type AssignEmployeeRoleInput, type CreateRoleInput, type UpdateRoleInput, type WebappRbac, } from "./createWebappRbac.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/utils",
3
- "version": "6.19.1",
3
+ "version": "6.21.0",
4
4
  "description": "Utilidades, parseo de request y demás para desarrollo de webapps de Zauru",
5
5
  "main": "./dist/esm/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -71,12 +71,12 @@
71
71
  "dependencies": {
72
72
  "@aws-sdk/client-cloudwatch-logs": "^3.1123.0",
73
73
  "@remix-run/node": "^2.8.1",
74
- "@zauru-sdk/common": "^6.8.2",
74
+ "@zauru-sdk/common": "^6.8.4",
75
75
  "@zauru-sdk/config": "^6.0.0",
76
76
  "@zauru-sdk/graphql": "^6.8.0",
77
- "@zauru-sdk/services": "^6.11.5",
78
- "@zauru-sdk/types": "^6.20.0",
77
+ "@zauru-sdk/services": "^6.11.7",
78
+ "@zauru-sdk/types": "^6.22.0",
79
79
  "moment": "^2.30.1"
80
80
  },
81
- "gitHead": "2da58dd263ca61a03f1e4779df9244b94050008d"
81
+ "gitHead": "800600a8cdb22f1c49293fbc3a48d26aa03771c9"
82
82
  }