Agrego carpeta del source sql de la Base de Datos
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
-- ============================================================================
|
||||
-- fc_anular_pago
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Anula un pago previamente registrado (soft-delete). El pago sigue
|
||||
-- existiendo y consultable, marcado con quién y cuándo lo anuló, y un
|
||||
-- motivo opcional. Genera un evento auditable.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Implementa la operación "anular" descrita en
|
||||
-- Documentation/DominioPagos.md §5.4 y §8. La anulación es terminal:
|
||||
-- un pago anulado no se desanula ni se vuelve a corregir; si la
|
||||
-- situación cambia se registra un pago nuevo (§5.4). Por ser
|
||||
-- transparente (visible + auditable), el rol con autoridad plena puede
|
||||
-- anular incluso fuera de la ventana de corrección (§9).
|
||||
--
|
||||
-- ARCHIVADO (US-R11)
|
||||
-- Un pago cuyo mes ya fue archivado al XLSX histórico es inmutable
|
||||
-- (DominioPagos §14): se rechaza la anulación antes de cualquier otra
|
||||
-- regla, incluso para la autoridad plena. El criterio lo resuelve
|
||||
-- internal.pagos_mes_archivado (consulta internal.archivo_exports).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_id UUID del pago a anular.
|
||||
-- p_motivo TEXT motivo libre. NULL, cadena vacía o sólo whitespace se
|
||||
-- persisten como NULL para no contaminar el campo.
|
||||
-- p_token UUID sesión del actor.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'agregar_pagos'.
|
||||
-- - Sin 'gestionar_cualquier_pago': debe ser el creador del pago y
|
||||
-- estar dentro de la ventana de corrección.
|
||||
-- - Con 'gestionar_cualquier_pago': sin restricción de ownership ni
|
||||
-- ventana.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'Sesión inválida o usuario no encontrado.'
|
||||
-- - 'No se encontró el pago.'
|
||||
-- - 'Pago archivado. No se puede modificar.' (US-R11)
|
||||
-- - 'Este pago ya estaba anulado.'
|
||||
-- - 'No tenés permiso para anular este pago.'
|
||||
-- - 'La ventana de anulación de este pago ya venció.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con el pago ya anulado, mismo shape que un ítem de
|
||||
-- fc_obtener_pagos.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - Setea anulado_at, anulado_por y motivo_anulacion del pago.
|
||||
-- - Registra evento 'pago_anulado' en public.eventos con un snapshot
|
||||
-- mínimo del pago (tipo, monto, mes, método, plan) como valor_anterior
|
||||
-- y el motivo como descripción.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_anular_pago(
|
||||
p_id UUID,
|
||||
p_motivo TEXT,
|
||||
p_token UUID
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
VOLATILE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_actor_id UUID;
|
||||
v_actor_rol TEXT;
|
||||
v_has_gestion BOOLEAN;
|
||||
v_ventana_minutos INT;
|
||||
v_pago public.pagos%ROWTYPE;
|
||||
v_motivo_limpio TEXT;
|
||||
v_snapshot_min JSONB;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'agregar_pagos');
|
||||
|
||||
SELECT s.usuario_id, u.rol
|
||||
INTO v_actor_id, v_actor_rol
|
||||
FROM internal.sesiones s
|
||||
JOIN usuarios u ON u.id = s.usuario_id
|
||||
WHERE s.token = p_token;
|
||||
|
||||
IF v_actor_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Sesión inválida o usuario no encontrado.';
|
||||
END IF;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM internal.permisos
|
||||
WHERE accion = 'gestionar_cualquier_pago'
|
||||
AND v_actor_rol = ANY(roles_permitidos)
|
||||
) INTO v_has_gestion;
|
||||
|
||||
SELECT * INTO v_pago FROM public.pagos WHERE id = p_id FOR UPDATE;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'No se encontró el pago.';
|
||||
END IF;
|
||||
|
||||
-- Pago archivado: inmutable (US-R11, DominioPagos §14). Bloquea incluso a
|
||||
-- la autoridad plena, que normalmente puede anular fuera de ventana: una
|
||||
-- vez en el archivo, anular descoordinaría base y XLSX. Se chequea antes
|
||||
-- que el resto porque es la condición de inmutabilidad más fuerte.
|
||||
IF internal.pagos_mes_archivado(v_pago.anio_mes_pagado) THEN
|
||||
RAISE EXCEPTION 'Pago archivado. No se puede modificar.';
|
||||
END IF;
|
||||
|
||||
IF v_pago.anulado_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Este pago ya estaba anulado.';
|
||||
END IF;
|
||||
|
||||
IF NOT v_has_gestion THEN
|
||||
IF v_pago.created_by IS DISTINCT FROM v_actor_id THEN
|
||||
RAISE EXCEPTION 'No tenés permiso para anular este pago.';
|
||||
END IF;
|
||||
v_ventana_minutos := internal.get_config_int('pagos.ventana_edicion_minutos', 30);
|
||||
IF now() > v_pago.created_at + (v_ventana_minutos || ' minutes')::interval THEN
|
||||
RAISE EXCEPTION 'La ventana de anulación de este pago ya venció.';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
v_motivo_limpio := NULLIF(trim(p_motivo), '');
|
||||
|
||||
UPDATE public.pagos
|
||||
SET anulado_at = now(),
|
||||
anulado_por = v_actor_id,
|
||||
motivo_anulacion = v_motivo_limpio
|
||||
WHERE id = p_id;
|
||||
|
||||
v_snapshot_min := jsonb_build_object(
|
||||
'tipo', v_pago.tipo,
|
||||
'monto_total', v_pago.monto_total,
|
||||
'anio_mes_pagado', v_pago.anio_mes_pagado,
|
||||
'metodo_id', v_pago.metodo_id,
|
||||
'tipo_cuota_id', v_pago.tipo_cuota_id
|
||||
);
|
||||
|
||||
PERFORM internal.log_evento(
|
||||
p_tipo => 'pago_anulado',
|
||||
p_tabla => 'pagos',
|
||||
p_referencia_id => p_id,
|
||||
p_cliente_id => v_pago.cliente_id,
|
||||
p_actor_id => v_actor_id,
|
||||
p_valor_anterior => v_snapshot_min,
|
||||
p_valor_actual => NULL,
|
||||
p_descripcion => v_motivo_limpio
|
||||
);
|
||||
|
||||
RETURN (
|
||||
SELECT jsonb_build_object(
|
||||
'id', p.id,
|
||||
'tipo', p.tipo,
|
||||
'anio_mes_pagado', p.anio_mes_pagado,
|
||||
'fecha_pago', p.fecha_pago,
|
||||
'monto_total', p.monto_total,
|
||||
'detalle', p.detalle,
|
||||
'metodo', m.descripcion,
|
||||
'created_at', p.created_at,
|
||||
'created_by', p.created_by,
|
||||
'created_by_nombre', cu.nombre,
|
||||
'updated_at', p.updated_at,
|
||||
'updated_by_nombre', uu.nombre,
|
||||
'anulado_at', p.anulado_at,
|
||||
'anulado_por_nombre', au.nombre,
|
||||
'motivo_anulacion', p.motivo_anulacion,
|
||||
'cliente', jsonb_build_object(
|
||||
'nombre', cl.nombre,
|
||||
'apellido', cl.apellido,
|
||||
'dni', cl.dni
|
||||
)
|
||||
)
|
||||
FROM public.pagos p
|
||||
JOIN public.usuarios cl ON p.cliente_id = cl.id
|
||||
LEFT JOIN public.metodos_pago m ON p.metodo_id = m.id
|
||||
LEFT JOIN public.usuarios cu ON p.created_by = cu.id
|
||||
LEFT JOIN public.usuarios uu ON p.updated_by = uu.id
|
||||
LEFT JOIN public.usuarios au ON p.anulado_por = au.id
|
||||
WHERE p.id = p_id
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,272 @@
|
||||
-- ============================================================================
|
||||
-- fc_editar_pago
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Corrige un pago previamente registrado, dentro de la ventana de
|
||||
-- corrección. Genera un evento auditable con los cambios reales producidos.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Implementa la operación "corregir" descrita en
|
||||
-- Documentation/DominioPagos.md §5.3 y §8. La ventana aplica a TODOS los
|
||||
-- actores (incluso al rol con autoridad plena): pasada la ventana, la
|
||||
-- única vía para enmendar un pago es anular y registrar uno nuevo (§9).
|
||||
--
|
||||
-- ARCHIVADO (US-R11)
|
||||
-- Un pago cuyo mes ya fue archivado al XLSX histórico es inmutable
|
||||
-- (DominioPagos §14): se rechaza la edición antes de cualquier otra
|
||||
-- regla. El criterio de "archivado" lo resuelve
|
||||
-- internal.pagos_mes_archivado (consulta internal.archivo_exports).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_id UUID del pago a editar.
|
||||
-- p_datos JSONB con los campos a actualizar. Semántica PATCH: sólo se
|
||||
-- modifican las claves presentes en el JSON. Editables:
|
||||
-- metodo_id, anio_mes_pagado, fecha_pago, monto_total,
|
||||
-- detalle, tipo_cuota_id. cliente_id y tipo NO son editables.
|
||||
-- p_token UUID sesión del actor.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'agregar_pagos'.
|
||||
-- - Sin 'gestionar_cualquier_pago': debe ser el creador del pago y estar
|
||||
-- dentro de la ventana de corrección.
|
||||
-- - Con 'gestionar_cualquier_pago': sin restricción de ownership, pero
|
||||
-- igualmente dentro de la ventana.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'Sesión inválida o usuario no encontrado.'
|
||||
-- - 'No se encontró el pago.'
|
||||
-- - 'Pago archivado. No se puede modificar.' (US-R11)
|
||||
-- - 'No se puede editar un pago anulado.'
|
||||
-- - 'Sólo se pueden editar pagos de tipo cuota_mensual'
|
||||
-- - 'No tenés permiso para editar este pago.'
|
||||
-- - 'La ventana de edición de este pago ya venció.'
|
||||
-- - 'cliente_id no es editable. Anulá el pago y cargá uno nuevo.'
|
||||
-- - 'tipo no es editable. Anulá el pago y cargá uno nuevo.'
|
||||
-- - 'metodo_id no puede ser nulo en un pago de cuota_mensual.'
|
||||
-- - 'El método de pago ID X no existe o no está activo.'
|
||||
-- - 'anio_mes_pagado no puede ser nulo en un pago de cuota_mensual.'
|
||||
-- - 'El monto total es inválido para un pago de cuota_mensual.'
|
||||
-- - 'El tipo de cuota ID X no existe.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con el pago actualizado, mismo shape que un ítem de
|
||||
-- fc_obtener_pagos.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - UPDATE de la fila en public.pagos, setea updated_at = now() y
|
||||
-- updated_by = actor.
|
||||
-- - Registra evento 'pago_editado' en public.eventos con el diff campo
|
||||
-- a campo, SÓLO si hubo cambios reales. Las ediciones no-op no generan
|
||||
-- evento.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_editar_pago(
|
||||
p_id UUID,
|
||||
p_datos JSONB,
|
||||
p_token UUID
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
VOLATILE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_actor_id UUID;
|
||||
v_actor_rol TEXT;
|
||||
v_has_gestion BOOLEAN;
|
||||
v_ventana_minutos INT;
|
||||
v_pago public.pagos%ROWTYPE;
|
||||
v_after public.pagos%ROWTYPE;
|
||||
v_diff_anterior JSONB := '{}'::jsonb;
|
||||
v_diff_actual JSONB := '{}'::jsonb;
|
||||
v_new_metodo_id SMALLINT;
|
||||
v_new_anio_mes DATE;
|
||||
v_new_tipo_cuota UUID;
|
||||
v_new_monto NUMERIC;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'agregar_pagos');
|
||||
|
||||
SELECT s.usuario_id, u.rol
|
||||
INTO v_actor_id, v_actor_rol
|
||||
FROM internal.sesiones s
|
||||
JOIN usuarios u ON u.id = s.usuario_id
|
||||
WHERE s.token = p_token;
|
||||
|
||||
IF v_actor_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Sesión inválida o usuario no encontrado.';
|
||||
END IF;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM internal.permisos
|
||||
WHERE accion = 'gestionar_cualquier_pago'
|
||||
AND v_actor_rol = ANY(roles_permitidos)
|
||||
) INTO v_has_gestion;
|
||||
|
||||
SELECT * INTO v_pago FROM public.pagos WHERE id = p_id FOR UPDATE;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'No se encontró el pago.';
|
||||
END IF;
|
||||
|
||||
-- Pago archivado: inmutable (US-R11, DominioPagos §14). Se chequea antes
|
||||
-- que cualquier otra regla porque es la condición de inmutabilidad más
|
||||
-- fuerte: si el mes ya viajó al archivo, no hay edición posible para
|
||||
-- nadie. (Normalmente el pipeline ya borró la fila, pero el guard cubre
|
||||
-- la ventana entre marcar 'ok' y el DELETE, y deja la regla explícita.)
|
||||
IF internal.pagos_mes_archivado(v_pago.anio_mes_pagado) THEN
|
||||
RAISE EXCEPTION 'Pago archivado. No se puede modificar.';
|
||||
END IF;
|
||||
|
||||
IF v_pago.anulado_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'No se puede editar un pago anulado.';
|
||||
END IF;
|
||||
|
||||
IF v_pago.tipo <> 'cuota_mensual' THEN
|
||||
RAISE EXCEPTION 'Sólo se pueden editar pagos de tipo cuota_mensual';
|
||||
END IF;
|
||||
|
||||
IF NOT v_has_gestion THEN
|
||||
IF v_pago.created_by IS DISTINCT FROM v_actor_id THEN
|
||||
RAISE EXCEPTION 'No tenés permiso para editar este pago.';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
v_ventana_minutos := internal.get_config_int('pagos.ventana_edicion_minutos', 30);
|
||||
IF now() > v_pago.created_at + (v_ventana_minutos || ' minutes')::interval THEN
|
||||
RAISE EXCEPTION 'La ventana de edición de este pago ya venció.';
|
||||
END IF;
|
||||
|
||||
IF p_datos ? 'cliente_id' THEN
|
||||
RAISE EXCEPTION 'cliente_id no es editable. Anulá el pago y cargá uno nuevo.';
|
||||
END IF;
|
||||
IF p_datos ? 'tipo' THEN
|
||||
RAISE EXCEPTION 'tipo no es editable. Anulá el pago y cargá uno nuevo.';
|
||||
END IF;
|
||||
|
||||
IF p_datos ? 'metodo_id' THEN
|
||||
v_new_metodo_id := (p_datos->>'metodo_id')::SMALLINT;
|
||||
IF v_new_metodo_id IS NULL THEN
|
||||
RAISE EXCEPTION 'metodo_id no puede ser nulo en un pago de cuota_mensual.';
|
||||
END IF;
|
||||
PERFORM 1 FROM metodos_pago WHERE id = v_new_metodo_id AND activo = true;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'El método de pago ID % no existe o no está activo.', v_new_metodo_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF p_datos ? 'anio_mes_pagado' THEN
|
||||
v_new_anio_mes := date_trunc('month', (p_datos->>'anio_mes_pagado')::DATE)::DATE;
|
||||
IF v_new_anio_mes IS NULL THEN
|
||||
RAISE EXCEPTION 'anio_mes_pagado no puede ser nulo en un pago de cuota_mensual.';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF p_datos ? 'monto_total' THEN
|
||||
v_new_monto := (p_datos->>'monto_total')::NUMERIC;
|
||||
IF v_new_monto IS NULL OR v_new_monto < 0 THEN
|
||||
RAISE EXCEPTION 'El monto total es inválido para un pago de cuota_mensual.';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF p_datos ? 'tipo_cuota_id' THEN
|
||||
IF (p_datos->>'tipo_cuota_id') IS NOT NULL THEN
|
||||
v_new_tipo_cuota := (p_datos->>'tipo_cuota_id')::UUID;
|
||||
PERFORM 1 FROM tipos_cuota WHERE id = v_new_tipo_cuota;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'El tipo de cuota ID % no existe.', v_new_tipo_cuota;
|
||||
END IF;
|
||||
ELSE
|
||||
v_new_tipo_cuota := NULL;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
UPDATE public.pagos p
|
||||
SET metodo_id = CASE WHEN p_datos ? 'metodo_id'
|
||||
THEN v_new_metodo_id ELSE p.metodo_id END,
|
||||
anio_mes_pagado = CASE WHEN p_datos ? 'anio_mes_pagado'
|
||||
THEN v_new_anio_mes ELSE p.anio_mes_pagado END,
|
||||
fecha_pago = CASE WHEN p_datos ? 'fecha_pago'
|
||||
THEN (p_datos->>'fecha_pago')::TIMESTAMPTZ
|
||||
ELSE p.fecha_pago END,
|
||||
monto_total = CASE WHEN p_datos ? 'monto_total'
|
||||
THEN v_new_monto ELSE p.monto_total END,
|
||||
detalle = CASE WHEN p_datos ? 'detalle'
|
||||
THEN p_datos->'detalle' ELSE p.detalle END,
|
||||
tipo_cuota_id = CASE WHEN p_datos ? 'tipo_cuota_id'
|
||||
THEN v_new_tipo_cuota ELSE p.tipo_cuota_id END,
|
||||
updated_at = now(),
|
||||
updated_by = v_actor_id
|
||||
WHERE p.id = p_id
|
||||
RETURNING * INTO v_after;
|
||||
|
||||
IF v_pago.metodo_id IS DISTINCT FROM v_after.metodo_id THEN
|
||||
v_diff_anterior := v_diff_anterior || jsonb_build_object('metodo_id', v_pago.metodo_id);
|
||||
v_diff_actual := v_diff_actual || jsonb_build_object('metodo_id', v_after.metodo_id);
|
||||
END IF;
|
||||
IF v_pago.anio_mes_pagado IS DISTINCT FROM v_after.anio_mes_pagado THEN
|
||||
v_diff_anterior := v_diff_anterior || jsonb_build_object('anio_mes_pagado', v_pago.anio_mes_pagado);
|
||||
v_diff_actual := v_diff_actual || jsonb_build_object('anio_mes_pagado', v_after.anio_mes_pagado);
|
||||
END IF;
|
||||
IF v_pago.fecha_pago IS DISTINCT FROM v_after.fecha_pago THEN
|
||||
v_diff_anterior := v_diff_anterior || jsonb_build_object('fecha_pago', v_pago.fecha_pago);
|
||||
v_diff_actual := v_diff_actual || jsonb_build_object('fecha_pago', v_after.fecha_pago);
|
||||
END IF;
|
||||
IF v_pago.monto_total IS DISTINCT FROM v_after.monto_total THEN
|
||||
v_diff_anterior := v_diff_anterior || jsonb_build_object('monto_total', v_pago.monto_total);
|
||||
v_diff_actual := v_diff_actual || jsonb_build_object('monto_total', v_after.monto_total);
|
||||
END IF;
|
||||
IF v_pago.detalle IS DISTINCT FROM v_after.detalle THEN
|
||||
v_diff_anterior := v_diff_anterior || jsonb_build_object('detalle', v_pago.detalle);
|
||||
v_diff_actual := v_diff_actual || jsonb_build_object('detalle', v_after.detalle);
|
||||
END IF;
|
||||
IF v_pago.tipo_cuota_id IS DISTINCT FROM v_after.tipo_cuota_id THEN
|
||||
v_diff_anterior := v_diff_anterior || jsonb_build_object('tipo_cuota_id', v_pago.tipo_cuota_id);
|
||||
v_diff_actual := v_diff_actual || jsonb_build_object('tipo_cuota_id', v_after.tipo_cuota_id);
|
||||
END IF;
|
||||
|
||||
IF v_diff_anterior <> '{}'::jsonb THEN
|
||||
PERFORM internal.log_evento(
|
||||
p_tipo => 'pago_editado',
|
||||
p_tabla => 'pagos',
|
||||
p_referencia_id => p_id,
|
||||
p_cliente_id => v_after.cliente_id,
|
||||
p_actor_id => v_actor_id,
|
||||
p_valor_anterior => v_diff_anterior,
|
||||
p_valor_actual => v_diff_actual,
|
||||
p_descripcion => NULL
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT jsonb_build_object(
|
||||
'id', p.id,
|
||||
'tipo', p.tipo,
|
||||
'anio_mes_pagado', p.anio_mes_pagado,
|
||||
'fecha_pago', p.fecha_pago,
|
||||
'monto_total', p.monto_total,
|
||||
'detalle', p.detalle,
|
||||
'metodo', m.descripcion,
|
||||
'created_at', p.created_at,
|
||||
'created_by', p.created_by,
|
||||
'created_by_nombre', cu.nombre,
|
||||
'updated_at', p.updated_at,
|
||||
'updated_by_nombre', uu.nombre,
|
||||
'anulado_at', p.anulado_at,
|
||||
'anulado_por_nombre', au.nombre,
|
||||
'motivo_anulacion', p.motivo_anulacion,
|
||||
'cliente', jsonb_build_object(
|
||||
'nombre', cl.nombre,
|
||||
'apellido', cl.apellido,
|
||||
'dni', cl.dni
|
||||
)
|
||||
)
|
||||
FROM public.pagos p
|
||||
JOIN public.usuarios cl ON p.cliente_id = cl.id
|
||||
LEFT JOIN public.metodos_pago m ON p.metodo_id = m.id
|
||||
LEFT JOIN public.usuarios cu ON p.created_by = cu.id
|
||||
LEFT JOIN public.usuarios uu ON p.updated_by = uu.id
|
||||
LEFT JOIN public.usuarios au ON p.anulado_por = au.id
|
||||
WHERE p.id = p_id
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,210 @@
|
||||
-- ============================================================================
|
||||
-- fc_insertar_pago
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Registra un pago nuevo de tipo cuota_mensual para un cliente identificado
|
||||
-- por DNI. Opcionalmente actualiza el plan por defecto del cliente. Como
|
||||
-- efecto colateral, activa al cliente.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Implementa la operación "registrar" descrita en
|
||||
-- Documentation/DominioPagos.md §7. Hoy sólo soporta el concepto
|
||||
-- cuota_mensual (§5.1); el dominio admite otros conceptos en el futuro,
|
||||
-- pero la función los rechaza explícitamente por ahora.
|
||||
--
|
||||
-- RETENCIÓN (US-R10)
|
||||
-- Rechaza pagos cuyo `anio_mes_pagado` cae por debajo de la ventana
|
||||
-- operativa de pagos (`retencion.pagos_meses`, default 12). Sin esto
|
||||
-- se podrían crear pagos que el pipeline mensual archivaría y
|
||||
-- purgaría a los pocos días (Documentation/PoliticaRetencion.md
|
||||
-- §4, §7). La validación es sólo del borde inferior; los meses
|
||||
-- futuros no se restringen acá.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_datos JSONB con el payload del pago. Claves:
|
||||
-- dni TEXT (obligatorio) DNI del cliente.
|
||||
-- metodo_id SMALLINT (obligatorio) método de pago activo.
|
||||
-- anio_mes_pagado DATE (obligatorio) mes que cubre el pago.
|
||||
-- Se trunca al día 1 del mes.
|
||||
-- fecha_pago TIMESTAMPTZ (opcional) fecha declarada del
|
||||
-- cobro. Default: now() en zona
|
||||
-- America/Argentina/Buenos_Aires.
|
||||
-- monto_total NUMERIC (obligatorio) ≥ 0.
|
||||
-- tipo_cuota_id UUID (opcional) plan que paga el cliente.
|
||||
-- set_default_tipo_cuota BOOLEAN (opcional, default false). Si true y
|
||||
-- tipo_cuota_id no nulo, actualiza el
|
||||
-- plan por defecto del cliente.
|
||||
-- detalle JSONB (opcional, default '{}'). Si se pasa
|
||||
-- tipo_cuota_id, se enriquece con
|
||||
-- cuota_nombre, cuota_precio,
|
||||
-- cuota_dias_semana (snapshot del plan
|
||||
-- al momento del cobro).
|
||||
-- tipo TEXT (opcional) debe ser 'cuota_mensual'
|
||||
-- si se pasa; otros valores se rechazan.
|
||||
-- p_token UUID de sesión del actor.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'agregar_pagos'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'Tipo de pago no soportado'
|
||||
-- - 'Sesión inválida o usuario no encontrado.'
|
||||
-- - 'No existe un usuario con el DNI X'
|
||||
-- - 'El campo metodo_id es obligatorio'
|
||||
-- - 'El método de pago ID X no existe o no está activo'
|
||||
-- - 'El tipo de cuota ID X no existe'
|
||||
-- - 'El monto total es inválido.'
|
||||
-- - 'No se puede registrar un pago para el mes X: está fuera de la
|
||||
-- ventana de retención de pagos (N meses). ...' (US-R10)
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con {id, cliente_id, anio_mes_pagado, fecha_pago, monto_total,
|
||||
-- status: 'success'}.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - INSERT en public.pagos con created_by = actor, tipo = 'cuota_mensual'.
|
||||
-- - SIEMPRE activa al cliente (usuarios.isactive = true), independiente
|
||||
-- de set_default_tipo_cuota.
|
||||
-- - Si set_default_tipo_cuota = true Y tipo_cuota_id no nulo: actualiza
|
||||
-- usuarios.tipo_cuota del cliente.
|
||||
-- - NO genera evento en public.eventos. La trazabilidad de creación está
|
||||
-- cubierta por created_at + created_by en la propia fila.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_insertar_pago(
|
||||
p_datos JSONB,
|
||||
p_token UUID
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
VOLATILE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_actor_id UUID;
|
||||
v_cliente_id UUID;
|
||||
v_metodo_id SMALLINT;
|
||||
v_anio_mes DATE;
|
||||
v_fecha_pago TIMESTAMPTZ;
|
||||
v_monto NUMERIC;
|
||||
v_detalle JSONB;
|
||||
v_tipo_cuota_id UUID;
|
||||
v_set_default BOOLEAN;
|
||||
v_info_cuota JSONB;
|
||||
v_pago_insertado RECORD;
|
||||
v_meses_retencion INT;
|
||||
v_mes_limite DATE;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'agregar_pagos');
|
||||
|
||||
-- Actualmente la app sólo crea pagos de tipo 'cuota_mensual'.
|
||||
-- En un futuro, se podrían registrar pagos de otras cosas (nutricionista, pago anualizado, o cosas así)
|
||||
-- y las tablas ya lo soportan, sólo se cambiaría esta función.
|
||||
|
||||
IF p_datos ? 'tipo' AND (p_datos->>'tipo') IS DISTINCT FROM 'cuota_mensual' THEN
|
||||
RAISE EXCEPTION 'Tipo de pago no soportado';
|
||||
END IF;
|
||||
|
||||
-- Autor del registro de pago, lo queremos para auditoría (created_by).
|
||||
SELECT usuario_id INTO v_actor_id FROM internal.sesiones WHERE token = p_token;
|
||||
IF v_actor_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Sesión inválida o usuario no encontrado.';
|
||||
END IF;
|
||||
|
||||
SELECT id INTO v_cliente_id FROM usuarios WHERE dni = (p_datos->>'dni');
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'No existe un usuario con el DNI %', p_datos->>'dni';
|
||||
END IF;
|
||||
|
||||
v_metodo_id := (p_datos->>'metodo_id')::SMALLINT;
|
||||
IF v_metodo_id IS NULL THEN
|
||||
RAISE EXCEPTION 'El campo metodo_id es obligatorio';
|
||||
END IF;
|
||||
|
||||
PERFORM 1 FROM metodos_pago WHERE id = v_metodo_id AND activo = true;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'El método de pago ID % no existe o no está activo', v_metodo_id;
|
||||
END IF;
|
||||
|
||||
v_tipo_cuota_id := (p_datos->>'tipo_cuota_id')::UUID;
|
||||
v_set_default := COALESCE((p_datos->>'set_default_tipo_cuota')::BOOLEAN, false);
|
||||
v_detalle := COALESCE(p_datos->'detalle', '{}'::jsonb);
|
||||
|
||||
IF v_tipo_cuota_id IS NOT NULL THEN
|
||||
SELECT jsonb_build_object(
|
||||
'cuota_nombre', nombre,
|
||||
'cuota_precio', precio,
|
||||
'cuota_dias_semana', dias_semana
|
||||
) INTO v_info_cuota
|
||||
FROM tipos_cuota
|
||||
WHERE id = v_tipo_cuota_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'El tipo de cuota ID % no existe', v_tipo_cuota_id;
|
||||
END IF;
|
||||
|
||||
v_detalle := v_detalle || v_info_cuota;
|
||||
END IF;
|
||||
|
||||
v_anio_mes := date_trunc('month', (p_datos->>'anio_mes_pagado')::DATE)::DATE;
|
||||
v_fecha_pago := COALESCE((p_datos->>'fecha_pago')::TIMESTAMPTZ, NOW());
|
||||
v_monto := (p_datos->>'monto_total')::NUMERIC;
|
||||
|
||||
-- cuota_mensual: monto positivo o cero. Los correctivos (que aún no se
|
||||
-- pueden crear acá) podrán llevar negativo cuando tengan sus funciones.
|
||||
IF v_monto IS NULL OR v_monto < 0 THEN
|
||||
RAISE EXCEPTION 'El monto total es inválido.';
|
||||
END IF;
|
||||
|
||||
-- Ventana de retención (US-R10): no permitir registrar un pago cuyo mes
|
||||
-- objetivo ya quedó fuera de la ventana operativa de pagos. De aceptarse,
|
||||
-- el pipeline mensual lo archivaría y purgaría casi de inmediato (ver
|
||||
-- Documentation/PoliticaRetencion.md §4, §7). Sólo se valida el borde
|
||||
-- inferior: los meses futuros (pago adelantado) no son un problema de
|
||||
-- retención y quedan deliberadamente fuera de esta validación.
|
||||
v_meses_retencion := internal.get_config_int('retencion.pagos_meses', 12);
|
||||
v_mes_limite := date_trunc('month', CURRENT_DATE)::DATE
|
||||
- (v_meses_retencion * INTERVAL '1 month');
|
||||
IF v_anio_mes < v_mes_limite THEN
|
||||
RAISE EXCEPTION
|
||||
'No se puede registrar un pago para el mes %: está fuera de la ventana de retención de pagos (% meses). El mes más antiguo admitido es %.',
|
||||
to_char(v_anio_mes, 'YYYY-MM'), v_meses_retencion, to_char(v_mes_limite, 'YYYY-MM');
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.pagos (
|
||||
id, cliente_id, anio_mes_pagado, fecha_pago, monto_total,
|
||||
metodo_id, detalle, tipo_cuota_id, tipo, created_by
|
||||
) VALUES (
|
||||
gen_random_uuid(), v_cliente_id, v_anio_mes, v_fecha_pago, v_monto,
|
||||
v_metodo_id, v_detalle, v_tipo_cuota_id, 'cuota_mensual', v_actor_id
|
||||
)
|
||||
RETURNING id, anio_mes_pagado, fecha_pago, monto_total, detalle
|
||||
INTO v_pago_insertado;
|
||||
|
||||
|
||||
IF v_set_default AND v_tipo_cuota_id IS NOT NULL THEN
|
||||
UPDATE usuarios
|
||||
SET tipo_cuota = v_tipo_cuota_id,
|
||||
isactive = true
|
||||
WHERE id = v_cliente_id;
|
||||
ELSE
|
||||
UPDATE usuarios
|
||||
SET isactive = true
|
||||
WHERE id = v_cliente_id;
|
||||
END IF;
|
||||
|
||||
-- No loggeamos creación en eventos (decisión de scope): created_at/created_by
|
||||
-- ya cubren la trazabilidad de la inserción.
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'id', v_pago_insertado.id,
|
||||
'cliente_id', v_cliente_id,
|
||||
'anio_mes_pagado', v_pago_insertado.anio_mes_pagado,
|
||||
'fecha_pago', v_pago_insertado.fecha_pago,
|
||||
'monto_total', v_pago_insertado.monto_total,
|
||||
'status', 'success'
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,90 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_mis_pagos
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve los pagos del cliente autenticado, paginados.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Versión "self-service" de la consulta descrita en
|
||||
-- Documentation/DominioPagos.md §7. Incluye los pagos anulados del propio
|
||||
-- cliente: él pudo haberlos visto cuando estaban vigentes, así que también
|
||||
-- debe poder consultar el rastro de la anulación
|
||||
-- (Documentation/DominioPagos.md §12).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del cliente.
|
||||
-- p_pagina INT (default 1) página, base 1.
|
||||
-- p_cantidad INT (default 20) ítems por página.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_mis_pagos'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'Sesión inválida o usuario no encontrado.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB array (puede ser vacío). Cada ítem: id, tipo, anio_mes_pagado,
|
||||
-- fecha_pago, monto_total, detalle, metodo (descripción), metadata de
|
||||
-- auditoría reducida (created_at, created_by, updated_at,
|
||||
-- updated_by_nombre, anulado_at, anulado_por_nombre) y motivo_anulacion.
|
||||
-- NO incluye el objeto cliente porque es siempre el del token.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_obtener_mis_pagos(
|
||||
p_token UUID,
|
||||
p_pagina INT DEFAULT 1,
|
||||
p_cantidad INT DEFAULT 20
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_user_id UUID;
|
||||
v_offset INT;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'ver_mis_pagos');
|
||||
|
||||
SELECT usuario_id INTO v_user_id FROM internal.sesiones WHERE token = p_token;
|
||||
IF v_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Sesión inválida o usuario no encontrado.';
|
||||
END IF;
|
||||
|
||||
v_offset := (p_pagina - 1) * p_cantidad;
|
||||
|
||||
RETURN (
|
||||
SELECT COALESCE(jsonb_agg(pago_json), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT jsonb_build_object(
|
||||
'id', p.id,
|
||||
'tipo', p.tipo,
|
||||
'anio_mes_pagado', p.anio_mes_pagado,
|
||||
'fecha_pago', p.fecha_pago,
|
||||
'monto_total', p.monto_total,
|
||||
'detalle', p.detalle,
|
||||
'metodo', m.descripcion,
|
||||
'created_at', p.created_at,
|
||||
'created_by', p.created_by,
|
||||
'updated_at', p.updated_at,
|
||||
'updated_by_nombre', uu.nombre,
|
||||
'anulado_at', p.anulado_at,
|
||||
'anulado_por_nombre', au.nombre,
|
||||
'motivo_anulacion', p.motivo_anulacion
|
||||
) AS pago_json
|
||||
FROM public.pagos p
|
||||
LEFT JOIN public.metodos_pago m ON p.metodo_id = m.id
|
||||
LEFT JOIN public.usuarios uu ON p.updated_by = uu.id
|
||||
LEFT JOIN public.usuarios au ON p.anulado_por = au.id
|
||||
WHERE p.cliente_id = v_user_id
|
||||
ORDER BY p.fecha_pago DESC
|
||||
LIMIT p_cantidad
|
||||
OFFSET v_offset
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,106 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_pagos
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve un lote paginado de pagos, filtrable por cliente.
|
||||
-- Opcionalmente incluye los pagos anulados.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Sirve a la operación "consultar pagos" descrita en
|
||||
-- Documentation/DominioPagos.md §7. Cada ítem trae la metadata necesaria
|
||||
-- para que la UI pueda distinguir pagos vigentes de anulados, identificar
|
||||
-- autoría de registro / corrección / anulación y mostrar motivo (§12).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_dni TEXT (opcional) DNI del cliente para filtrar.
|
||||
-- NULL = todos los clientes.
|
||||
-- p_pagina INT (default 1) página, base 1.
|
||||
-- p_cantidad INT (default 50) ítems por página.
|
||||
-- p_incluir_anulados BOOLEAN (default FALSE) si TRUE, incluye pagos
|
||||
-- anulados en el resultado.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_pagos'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'No existe un usuario con el DNI X' cuando p_dni se pasa y no
|
||||
-- coincide con ningún cliente.
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB array (puede ser vacío). Cada ítem: id, tipo, anio_mes_pagado,
|
||||
-- fecha_pago, monto_total, detalle, metodo (descripción), metadata de
|
||||
-- auditoría (created/updated/anulado: at + by_nombre), motivo_anulacion
|
||||
-- y un objeto cliente {nombre, apellido, dni}.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_obtener_pagos(
|
||||
p_token UUID,
|
||||
p_dni TEXT DEFAULT NULL,
|
||||
p_pagina INT DEFAULT 1,
|
||||
p_cantidad INT DEFAULT 50,
|
||||
p_incluir_anulados BOOLEAN DEFAULT FALSE
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_cliente_id UUID := NULL;
|
||||
v_offset INT;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'ver_pagos');
|
||||
v_offset := (p_pagina - 1) * p_cantidad;
|
||||
|
||||
IF p_dni IS NOT NULL THEN
|
||||
SELECT id INTO v_cliente_id FROM usuarios WHERE dni = p_dni;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'No existe un usuario con el DNI %', p_dni;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT COALESCE(jsonb_agg(pago_json), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT jsonb_build_object(
|
||||
'id', p.id,
|
||||
'tipo', p.tipo,
|
||||
'anio_mes_pagado', p.anio_mes_pagado,
|
||||
'fecha_pago', p.fecha_pago,
|
||||
'monto_total', p.monto_total,
|
||||
'detalle', p.detalle,
|
||||
'metodo', m.descripcion,
|
||||
'created_at', p.created_at,
|
||||
'created_by', p.created_by,
|
||||
'created_by_nombre', cu.nombre,
|
||||
'updated_at', p.updated_at,
|
||||
'updated_by_nombre', uu.nombre,
|
||||
'anulado_at', p.anulado_at,
|
||||
'anulado_por_nombre', au.nombre,
|
||||
'motivo_anulacion', p.motivo_anulacion,
|
||||
'cliente', jsonb_build_object(
|
||||
'nombre', cl.nombre,
|
||||
'apellido', cl.apellido,
|
||||
'dni', cl.dni
|
||||
)
|
||||
) AS pago_json
|
||||
FROM public.pagos p
|
||||
JOIN public.usuarios cl ON p.cliente_id = cl.id
|
||||
LEFT JOIN public.metodos_pago m ON p.metodo_id = m.id
|
||||
LEFT JOIN public.usuarios cu ON p.created_by = cu.id
|
||||
LEFT JOIN public.usuarios uu ON p.updated_by = uu.id
|
||||
LEFT JOIN public.usuarios au ON p.anulado_por = au.id
|
||||
WHERE (v_cliente_id IS NULL OR p.cliente_id = v_cliente_id)
|
||||
AND (p_incluir_anulados OR p.anulado_at IS NULL)
|
||||
ORDER BY p.fecha_pago DESC
|
||||
LIMIT p_cantidad
|
||||
OFFSET v_offset
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,96 @@
|
||||
-- ============================================================================
|
||||
-- fc_modificar_metodos_pago
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Modifica los campos editables de un método de pago existente. Semántica
|
||||
-- PATCH: sólo se actualizan las claves presentes en p_datos.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Mantenimiento de los métodos de pago como configuración del módulo.
|
||||
-- No crea ni elimina; el alta lógica/baja lógica se maneja con el flag
|
||||
-- `activo` del propio método.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_datos JSONB con:
|
||||
-- id INTEGER (obligatorio) ID del método a modificar.
|
||||
-- descripcion TEXT (opcional) nuevo nombre / etiqueta.
|
||||
-- activo BOOLEAN (opcional) baja lógica (false) o reactivación
|
||||
-- (true).
|
||||
-- icono TEXT (opcional) identificador del ícono asociado.
|
||||
-- p_token UUID sesión del actor.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'modificar_metodos_pago'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'Debe indicar el ID del tipo de cuota a modificar.'
|
||||
-- (NOTA: el mensaje refiere a "tipo de cuota" por un error histórico;
|
||||
-- en realidad valida que se pase el ID del método de pago.)
|
||||
-- - 'El método de pago con ID X no existe.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con la fila completa del método de pago tras la modificación.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- UPDATE sobre metodos_pago. No genera evento.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION fc_modificar_metodos_pago(
|
||||
p_datos JSONB,
|
||||
p_token UUID
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
VOLATILE -- Checklist #2: Modifica datos
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_resultado JSONB;
|
||||
v_id INTEGER;
|
||||
BEGIN
|
||||
-- -----------------------------------------------------------
|
||||
-- Checklist #3: Validar permisos
|
||||
-- -----------------------------------------------------------
|
||||
PERFORM internal.validate_permission(p_token, 'modificar_metodos_pago');
|
||||
|
||||
|
||||
-- 3. Validar que venga el ID para saber qué modificar
|
||||
IF NOT p_datos ? 'id' OR p_datos->>'id' IS NULL THEN
|
||||
RAISE EXCEPTION 'Debe indicar el ID del tipo de cuota a modificar.';
|
||||
END IF;
|
||||
|
||||
v_id := (p_datos->>'id')::INTEGER;
|
||||
-- -----------------------------------------------------------
|
||||
-- 1. UPDATE Dinámico y Seguro
|
||||
-- -----------------------------------------------------------
|
||||
UPDATE metodos_pago
|
||||
SET
|
||||
descripcion = CASE
|
||||
WHEN p_datos ? 'descripcion' THEN p_datos->>'descripcion'
|
||||
ELSE descripcion
|
||||
END,
|
||||
|
||||
activo = CASE
|
||||
WHEN p_datos ? 'activo' THEN (p_datos->>'activo')::BOOL
|
||||
ELSE activo
|
||||
END,
|
||||
|
||||
-- BUG CORREGIDO: Ahora si no viene el icono, no lo tocamos.
|
||||
icono = CASE
|
||||
WHEN p_datos ? 'icono' THEN p_datos->>'icono'
|
||||
ELSE icono
|
||||
END
|
||||
WHERE id = v_id
|
||||
RETURNING to_jsonb(metodos_pago.*) INTO v_resultado; -- Retorno atómico
|
||||
|
||||
-- -----------------------------------------------------------
|
||||
-- 2. Verificar si se actualizó algo
|
||||
-- -----------------------------------------------------------
|
||||
IF v_resultado IS NULL THEN
|
||||
RAISE EXCEPTION 'El método de pago con ID % no existe.', v_id;
|
||||
END IF;
|
||||
|
||||
RETURN v_resultado;
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,55 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_metodos_pago
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve todos los métodos de pago configurados (activos e inactivos).
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Los métodos de pago (efectivo, transferencia, billetera digital, etc.)
|
||||
-- son entidades de configuración del módulo de pagos. Se consultan al
|
||||
-- registrar o corregir un pago para que el responsable elija cómo se cobró
|
||||
-- (Documentation/DominioPagos.md §5.1).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_metodos_pago'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- Ninguno propio (sólo los del validador de permiso).
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB array de filas completas de metodos_pago, ordenadas por id.
|
||||
-- Cada ítem: id, descripcion, activo, icono. Array vacío si no hay
|
||||
-- registros.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION fc_obtener_metodos_pago(p_token UUID)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE
|
||||
AS $function$
|
||||
DECLARE
|
||||
-- No necesitamos variables extra si usamos internal.validate_permission
|
||||
BEGIN
|
||||
-- 1. Validar permisos (Checklist #3)
|
||||
-- Asumimos acción 'ver_configuracion' o 'ver_metodos_pago'
|
||||
-- Esto te permite mañana darle permiso al 'cliente' sin tocar este código.
|
||||
PERFORM internal.validate_permission(p_token, 'ver_metodos_pago');
|
||||
|
||||
-- 2. Retorno directo y sin vueltas
|
||||
RETURN (
|
||||
SELECT COALESCE(
|
||||
jsonb_agg(to_jsonb(mp) ORDER BY mp.id),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM public.metodos_pago mp
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
Reference in New Issue
Block a user