Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9487194
armo tabla de fichadas recibidas y script con usuarios externos, falt…
manueldelapenna Dec 12, 2025
560cd8f
permito vacio en campos text, menos tipo fichada
manueldelapenna Dec 12, 2025
2e8bd16
quito script sobrante
manueldelapenna Dec 15, 2025
4ed6c34
intercepto cambios de usuarios para guardar en cola y procesar luego …
manueldelapenna Jan 5, 2026
0c0abd1
primera version, no anda desde cron por tema de permisos
manueldelapenna Jan 12, 2026
bdee516
quito archivo que no corresponde
manueldelapenna Jan 12, 2026
2235f13
ajustes para que funcione cron
manueldelapenna Jan 12, 2026
febdcdf
corrijo query
manueldelapenna Jan 12, 2026
1779d53
ajustes finales
manueldelapenna Jan 12, 2026
6550e7d
ajustes finales
manueldelapenna Jan 12, 2026
0bde5d3
ajuste activo
manueldelapenna Jan 12, 2026
c7fc7b0
agrego fecha a sp
manueldelapenna Jan 12, 2026
6543475
ajusto query que guarda estado
manueldelapenna Jan 12, 2026
3e9f38c
reutilizo pool
manueldelapenna Jan 13, 2026
f3a0c12
ajustes usuarios bbdd
manueldelapenna Jan 13, 2026
104de50
actualizamos con main con martin
manueldelapenna Jan 13, 2026
536654c
agrego parametros a cola de usuarios
manueldelapenna Jan 13, 2026
6355e5f
ajustes
manueldelapenna Jan 13, 2026
ed52db3
agrego log
manueldelapenna Jan 13, 2026
5b2e684
ajustes usuario en vez de idper
manueldelapenna Jan 13, 2026
43c9775
quito fk en fichadas recibidas
manueldelapenna Jan 13, 2026
4d19cc2
ajusto doc
manueldelapenna Jan 14, 2026
5a58516
permisos que faltaban en fichadas recibidas de test
manueldelapenna Jan 14, 2026
b5ac787
renombramos idper a fichador en fichadas_recibidas
manueldelapenna Jan 14, 2026
00b81f3
renombro tabla de cola a sinc_fichadores
manueldelapenna Jan 15, 2026
3bc6b8c
trigger que pasa fichadas recibidas a fichadas, loguea errores y exitos
manueldelapenna Jan 15, 2026
169c350
muevo sinc_fichadas_recibidas a pre adapt en def config
fernquiros Jan 15, 2026
b2a33fa
ajustes grilla y default
manueldelapenna Jan 15, 2026
b41c3ec
ajustes finales
manueldelapenna Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/encolado_usuarios.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Especificación Técnica: Tabla siper.sinc_fichadores

## 1. Propósito
Gestionar la sincronización de identidades entre Siper y el módulo externo. La solución asegura que solo los usuarios designados como "Principales" sean replicados, administrando cambios de identidad, bajas lógicas y físicas, y garantizando la entrega del estado más reciente mediante una cola de eventos persistente.

## 2. Componentes de la Solución
- **Tabla de Usuarios (siper.usuarios)**: Origen de datos. Incorpora el campo principal (booleano) para determinar la identidad activa por cada idper.
- **Tabla de Sincronizaciones (siper.sinc_fichadores)**: Registro persistente de eventos pendientes de procesar.
- **Índice Único Parcial**: Restricción a nivel de base de datos que impide la existencia de más de un usuario principal = true por cada idper.
- **Trigger de Sincronización (tr_sincro_usuarios_modulo_global)**: El "agente" que detecta cambios y gestiona la cola.

## 3. Flujo de Encolado
El proceso se activa automáticamente ante las siguientes operaciones en la tabla de usuarios:
- **INSERT o DELETE** de un usuario.
- **UPDATE** exclusivamente de los campos: usuario, activo, hashpass e idper.

**Nota:** Cambios en otros campos (como teléfono o mail) no dispararán la sincronización para optimizar el rendimiento.

### Condiciones de Entrada
Para que un evento sea efectivamente encolado, deben cumplirse estas condiciones:
- **Algoritmo Específico**: El campo algoritmo_pass debe ser estrictamente igual a 'PG-SHA256'.

### Lógica de Decisión
El trigger evalúa dos posibles acciones simultáneas:
- **Desactivación de Identidad Previa**: Si un UPDATE cambia el usuario o bien se hace un DELETE del mismo, se encola una orden de DESACTIVAR para el ID que quedó huérfano.
- **Sincronización de Registro Actual**: Si es un INSERT/UPDATE: Se encola ACTUALIZAR (solo si hubo cambios en campos críticos)


## 4. Acciones de Sincronización

| Acción | Descripción |
|-------------|---------------------------------------------------------------|
| ACTUALIZAR | El usuario es el principal activo. Se deben enviar sus datos actuales y estado activo. |
| DESACTIVAR | El ID ya no pertenece a un usuario principal o fue borrado. Se debe marcar como inactivo. |

## 5. Estados del Evento
La tabla de cola gestiona el ciclo de vida de cada sincronización mediante los siguientes estados:

| Estado | Descripción |
|-------------|---------------------------------------------------------------|
| PENDIENTE | Registro listo para ser tomado por el worker. |
| EN_PROCESO | El worker ha tomado el registro y está ejecutando el proceso externo. |
| PROCESADO | Sincronización exitosa confirmada. |
| ERROR | Falló la ejecución. El sistema permitirá reintentos automáticos.|
| AGOTADO | Se alcanzó el límite máximo de intentos (5) sin éxito. |

## 6. Lógica de Persistencia (Upsert)
El encolado utiliza una lógica de "Conflicto Inteligente" basada en los estados definidos anteriormente:
- Si el usuario no está en la cola (o ya fue PROCESADO): Se crea un nuevo registro con estado PENDIENTE.
- Si el usuario ya tiene un evento activo: Si existe un registro con cualquier estado distinto a PROCESADO (ya sea ERROR, AGOTADO o incluso EN_PROCESO), el sistema no crea una fila nueva. En su lugar, actualiza la existente, resetea el contador de intentos a 0 y devuelve el estado a PENDIENTE.

### Protección de Concurrencia
Si un registro cambia mientras está EN_PROCESO, el reseteo a PENDIENTE asegura que el worker, al finalizar su tarea actual, detecte que hay un nuevo cambio pendiente y vuelva a procesar al usuario con la información más reciente.

## 7. Consideraciones de Seguridad y Robustez
- **Security Definer**: La función del trigger se ejecuta con los privilegios del propietario de la base de datos, garantizando el éxito del encolado independientemente de los permisos del usuario que opera en la web/app.
- **Desacoplamiento**: Si el módulo externo está caído, el evento permanece en la cola de forma persistente hasta que el servicio se restablezca.
10 changes: 10 additions & 0 deletions example-local-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ db:
schema: siper
user: siper_admin
password: cambiar_esta_clave
modulo-fichadas-db:
user: usuario,
password: cambiar_esta_clave
server: host o ip
database: database
port: 1433
options:
encrypt: false
trustServerCertificate: true

install:
dump:
drop-his: false # poner en true en desarrollo
Expand Down
50 changes: 50 additions & 0 deletions install/sinc_fichadas_recibidas.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
CREATE OR REPLACE FUNCTION procesar_fichada_recibida_trg() RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
set search_path=siper
AS
$$
DECLARE
v_idper text;
v_tipo_mapeado text;
BEGIN
BEGIN
SELECT idper INTO v_idper
FROM usuarios
WHERE usuario = NEW.fichador;

IF v_idper IS NULL THEN
RAISE EXCEPTION 'Usuario "%" no encontrado en la tabla usuarios', NEW.fichador;
END IF;

CASE
WHEN lower(NEW.tipo) IN ('e', 'entrada') THEN v_tipo_mapeado := 'E';
WHEN lower(NEW.tipo) IN ('s', 'salida') THEN v_tipo_mapeado := 'S';
ELSE v_tipo_mapeado := 'O';
END CASE;

INSERT INTO fichadas (
idper, fecha, hora, tipo_fichada,
observaciones, punto, tipo_dispositivo
) VALUES (
v_idper, NEW.fecha, NEW.hora, v_tipo_mapeado,
NEW.texto, NEW.punto_gps, NEW.dispositivo
);

NEW.migrado_estado := 'OK';
NEW.migrado_log := 'Migrado exitosamente';

EXCEPTION WHEN OTHERS THEN
NEW.migrado_estado := 'ERROR';
NEW.migrado_log := SQLERRM;
END;

RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS procesar_fichada_recibida_trg ON fichadas_recibidas;
CREATE TRIGGER procesar_fichada_recibida_trg
BEFORE INSERT ON fichadas_recibidas
FOR EACH ROW
EXECUTE PROCEDURE procesar_fichada_recibida_trg();
84 changes: 84 additions & 0 deletions install/sinc_fichadores.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
CREATE UNIQUE INDEX "idx_usuario_activo_sincro"
ON "sinc_fichadores" ("usuario")
WHERE ("estado" != 'PROCESADO');

CREATE OR REPLACE FUNCTION siper.fn_trigger_sincro_usuarios_modulo()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
-- Banderas de decisión
v_debe_desactivar_previo BOOLEAN := false;
v_debe_procesar_actual BOOLEAN := false;
v_accion_actual TEXT; -- 'ACTUALIZAR' o 'DESACTIVAR'

-- Variables auxiliares
v_algoritmo_actual TEXT;
BEGIN
-- 1. Determinar algoritmo del registro que disparó el trigger
v_algoritmo_actual := CASE WHEN TG_OP = 'DELETE' THEN OLD.algoritmo_pass ELSE NEW.algoritmo_pass END;

-- Guard: Si no es el algoritmo requerido, ignoramos
IF (COALESCE(v_algoritmo_actual, '') != 'PG-SHA256') THEN
RETURN NULL;
END IF;

-- 2. LÓGICA DE DECISIÓN

-- Caso A: Cambio de nombre (Renombre)
-- Si es un UPDATE y el nombre de usuario cambió, hay que "apagar" el nombre anterior.
IF (TG_OP = 'UPDATE' AND OLD.usuario IS DISTINCT FROM NEW.usuario) THEN
v_debe_desactivar_previo := true;
END IF;

-- Caso B: Determinar qué hacer con el usuario "actual"
IF (TG_OP = 'DELETE') THEN
v_debe_procesar_actual := true;
v_accion_actual := 'DESACTIVAR';

ELSIF (NEW.idper IS NULL) THEN
-- Si el registro actual no tiene persona, solo nos interesa si antes SÍ tenía (Desvínculo)
IF (TG_OP = 'UPDATE' AND OLD.idper IS NOT NULL) THEN
v_debe_procesar_actual := true;
v_accion_actual := 'DESACTIVAR';
END IF;

ELSE
-- El registro tiene idper (está vinculado)
-- Sincronizamos si es nuevo o si cambiaron datos relevantes
IF (TG_OP = 'INSERT' OR OLD.* IS DISTINCT FROM NEW.*) THEN
v_debe_procesar_actual := true;
v_accion_actual := 'ACTUALIZAR';
END IF;
END IF;

-- 3. EJECUCIÓN DE ACCIONES EN LA COLA

-- Acción para el usuario PREVIO (solo en renombres)
IF (v_debe_desactivar_previo) THEN
INSERT INTO siper.sinc_fichadores (usuario, accion, estado, creado_en, actualizado_en)
VALUES (OLD.usuario, 'DESACTIVAR', 'PENDIENTE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
ON CONFLICT (usuario) WHERE (estado != 'PROCESADO')
DO UPDATE SET accion = 'DESACTIVAR', estado = 'PENDIENTE', actualizado_en = NOW(), intentos = 0, respuesta_sp = null;
END IF;

-- Acción para el usuario ACTUAL (Alta, Modificación, Baja o Desvínculo)
IF (v_debe_procesar_actual) THEN
INSERT INTO siper.sinc_fichadores (usuario, accion, estado, creado_en, actualizado_en)
VALUES (CASE WHEN TG_OP = 'DELETE' THEN OLD.usuario ELSE NEW.usuario END, v_accion_actual, 'PENDIENTE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
ON CONFLICT (usuario) WHERE (estado != 'PROCESADO')
DO UPDATE SET accion = v_accion_actual, estado = 'PENDIENTE', actualizado_en = NOW(), intentos = 0, respuesta_sp = null;
END IF;

RETURN NULL;
END;
$$;

DROP TRIGGER IF EXISTS tr_sincro_usuarios_modulo_global ON "usuarios";

CREATE TRIGGER tr_sincro_usuarios_modulo_global
AFTER INSERT OR DELETE OR UPDATE OF usuario, activo, hashpass, idper
ON "usuarios"
FOR EACH ROW
EXECUTE FUNCTION siper.fn_trigger_sincro_usuarios_modulo();
39 changes: 39 additions & 0 deletions operaciones/cambios-20251212b-fichadas_recibidas.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
set search_path=siper;
SET ROLE siper_muleto_owner;
--SET ROLE siper_owner;
--ojo mirar grants de abajo (test y/o produc segun entorno)

create table "fichadas_recibidas" (
"idper" text,
"fecha" date,
"hora" time,
"tipo" text,
"texto" text,
"dispositivo" text,
"punto_gps" text,
"id_originen" text
, primary key ("idper", "fecha", "hora")
);

grant select, insert, update, delete on "fichadas_recibidas" to siper_admin;

grant all on "fichadas_recibidas" to siper_owner;

alter table "fichadas_recibidas" add constraint "idper<>''" check ("idper"<>'');
alter table "fichadas_recibidas" alter column "idper" set not null;
alter table "fichadas_recibidas" alter column "fecha" set not null;
alter table "fichadas_recibidas" alter column "hora" set not null;
alter table "fichadas_recibidas" add constraint "tipo<>''" check ("tipo"<>'');
alter table "fichadas_recibidas" alter column "tipo" set not null;

alter table "fichadas_recibidas" add constraint "fichadas_recibidas personas REL" foreign key ("idper") references "personas" ("idper") on update cascade;
create index "idper 4 fichadas_recibidas IDX" ON "fichadas_recibidas" ("idper");

GRANT USAGE ON SCHEMA siper TO siper_intelektron_test;
--GRANT USAGE ON SCHEMA siper TO siper_intelektron_produc;

grant select, insert on "fichadas_recibidas" to siper_intelektron_test;
--grant select, insert, on "fichadas_recibidas" to siper_intelektron_produc;

GRANT SELECT ON usuarios_habilitados_fichadas TO siper_intelektron_test;
--GRANT SELECT ON usuarios_habilitados_fichadas TO siper_intelektron_produc;
135 changes: 135 additions & 0 deletions operaciones/cambios-20251229-sincro-usuarios-modulo-fichadas.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
set search_path=siper;
SET ROLE siper_muleto_owner;
--SET ROLE siper_owner;

create table "sinc_fichadores" (
"num_sincro" bigint,
"usuario" text,
"accion" text,
"estado" text,
"intentos" integer default '0',
"parametros" text,
"respuesta_sp" text,
"creado_en" timestamp,
"actualizado_en" timestamp
, primary key ("num_sincro")
);
grant select, insert, update, delete on "sinc_fichadores" to siper_muleto_admin;
grant all on "sinc_fichadores" to siper_muleto_owner;


CREATE SEQUENCE "sinc_usuarios_seq" START 1;
ALTER TABLE "sinc_fichadores" ALTER COLUMN "num_sincro" SET DEFAULT nextval('sinc_usuarios_seq'::regclass);
GRANT USAGE, SELECT ON SEQUENCE "sinc_usuarios_seq" TO siper_muleto_admin;

alter table "sinc_fichadores" alter column "num_sincro" set not null;
alter table "sinc_fichadores" add constraint "usuario<>''" check ("usuario"<>'');
alter table "sinc_fichadores" alter column "usuario" set not null;
alter table "sinc_fichadores" add constraint "accion<>''" check ("accion"<>'');
alter table "sinc_fichadores" alter column "accion" set not null;
alter table "sinc_fichadores" add constraint "estado<>''" check ("estado"<>'');
alter table "sinc_fichadores" alter column "estado" set not null;
alter table "sinc_fichadores" alter column "intentos" set not null;
alter table "sinc_fichadores" add constraint "respuesta_sp<>''" check ("respuesta_sp"<>'');
alter table "sinc_fichadores" add constraint "parametros<>''" check ("parametros"<>'');
alter table "sinc_fichadores" add constraint "estados_cola" check (estado IN ('PENDIENTE', 'EN_PROCESO', 'PROCESADO', 'ERROR', 'AGOTADO'));
alter table "sinc_fichadores" add constraint "acciones_cola" check (accion IN ('DESACTIVAR', 'ACTUALIZAR'));

CREATE UNIQUE INDEX "idx_usuario_activo_sincro"
ON "sinc_fichadores" ("usuario")
WHERE ("estado" != 'PROCESADO');

CREATE OR REPLACE FUNCTION siper.fn_trigger_sincro_usuarios_modulo()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
-- Banderas de decisión
v_debe_desactivar_previo BOOLEAN := false;
v_debe_procesar_actual BOOLEAN := false;
v_accion_actual TEXT; -- 'ACTUALIZAR' o 'DESACTIVAR'

-- Variables auxiliares
v_algoritmo_actual TEXT;
BEGIN
-- 1. Determinar algoritmo del registro que disparó el trigger
v_algoritmo_actual := CASE WHEN TG_OP = 'DELETE' THEN OLD.algoritmo_pass ELSE NEW.algoritmo_pass END;

-- Guard: Si no es el algoritmo requerido, ignoramos
IF (COALESCE(v_algoritmo_actual, '') != 'PG-SHA256') THEN
RETURN NULL;
END IF;

-- 2. LÓGICA DE DECISIÓN

-- Caso A: Cambio de nombre (Renombre)
-- Si es un UPDATE y el nombre de usuario cambió, hay que "apagar" el nombre anterior.
IF (TG_OP = 'UPDATE' AND OLD.usuario IS DISTINCT FROM NEW.usuario) THEN
v_debe_desactivar_previo := true;
END IF;

-- Caso B: Determinar qué hacer con el usuario "actual"
IF (TG_OP = 'DELETE') THEN
v_debe_procesar_actual := true;
v_accion_actual := 'DESACTIVAR';

ELSIF (NEW.idper IS NULL) THEN
-- Si el registro actual no tiene persona, solo nos interesa si antes SÍ tenía (Desvínculo)
IF (TG_OP = 'UPDATE' AND OLD.idper IS NOT NULL) THEN
v_debe_procesar_actual := true;
v_accion_actual := 'DESACTIVAR';
END IF;

ELSE
-- El registro tiene idper (está vinculado)
-- Sincronizamos si es nuevo o si cambiaron datos relevantes
IF (TG_OP = 'INSERT' OR OLD.* IS DISTINCT FROM NEW.*) THEN
v_debe_procesar_actual := true;
v_accion_actual := 'ACTUALIZAR';
END IF;
END IF;

-- 3. EJECUCIÓN DE ACCIONES EN LA COLA

-- Acción para el usuario PREVIO (solo en renombres)
IF (v_debe_desactivar_previo) THEN
INSERT INTO siper.sinc_fichadores (usuario, accion, estado, creado_en, actualizado_en)
VALUES (OLD.usuario, 'DESACTIVAR', 'PENDIENTE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
ON CONFLICT (usuario) WHERE (estado != 'PROCESADO')
DO UPDATE SET accion = 'DESACTIVAR', estado = 'PENDIENTE', actualizado_en = NOW(), intentos = 0, respuesta_sp = null;
END IF;

-- Acción para el usuario ACTUAL (Alta, Modificación, Baja o Desvínculo)
IF (v_debe_procesar_actual) THEN
INSERT INTO siper.sinc_fichadores (usuario, accion, estado, creado_en, actualizado_en)
VALUES (CASE WHEN TG_OP = 'DELETE' THEN OLD.usuario ELSE NEW.usuario END, v_accion_actual, 'PENDIENTE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
ON CONFLICT (usuario) WHERE (estado != 'PROCESADO')
DO UPDATE SET accion = v_accion_actual, estado = 'PENDIENTE', actualizado_en = NOW(), intentos = 0, respuesta_sp = null;
END IF;

RETURN NULL;
END;
$$;


DROP TRIGGER IF EXISTS tr_sincro_usuarios_modulo_global ON "usuarios";

CREATE TRIGGER tr_sincro_usuarios_modulo_global
AFTER INSERT OR DELETE OR UPDATE OF usuario, activo, hashpass, idper
ON "usuarios"
FOR EACH ROW
EXECUTE FUNCTION siper.fn_trigger_sincro_usuarios_modulo();


alter table "fichadas_recibidas" drop constraint "fichadas_recibidas personas REL";
drop index "idper 4 fichadas_recibidas IDX" ;


alter table fichadas_recibidas rename column idper to fichador;

--permisos faltantes en grilla fichadas recibidas
SET ROLE postgres;

grant select, insert, update, delete on "fichadas_recibidas" to siper_muleto_admin;
grant all on "fichadas_recibidas" to siper_muleto_owner;
Loading