11 RPCs de Mirada Empresa (E1-E10 + estrella Talento en riesgo) sobre instrumento v3, k≥5 en dos niveles, helper _dash_es_cuidador (criterio UNFPA). Fix de _dash_responses: A6/A7 → 2.5/2.7. Verificado contra 50 respuestas reales: métricas núcleo exactas, k-anonimato suprime segmentos n<5, umbral de la estrella parametrizable. Modalidad por JOIN (TECH-DEBT-010). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
328 lines
16 KiB
PL/PgSQL
328 lines
16 KiB
PL/PgSQL
-- ============================================================
|
|
-- Tests pgTAP: Dashboard v3 — RPCs de la Mirada Empresa
|
|
--
|
|
-- Ejecutar con: supabase test db
|
|
-- Requiere: Docker + supabase start + extensión pgtap
|
|
--
|
|
-- Cubre:
|
|
-- 1. Existencia de las 11 RPCs + el helper _dash_es_cuidador (firma correcta)
|
|
-- 2. _dash_es_cuidador: las 4 combinaciones de la definición UNFPA
|
|
-- (solo Ninguna / Ninguna+otro / solo "hijo sin discapacidad" / sin responder)
|
|
-- 3. k≥5 en dos niveles: n=4 → suppressed, n=5 → ok con pct correcto (E1)
|
|
-- 4. Fix de _dash_responses: el filtro de género ya encuentra '2.5' (no '"A6"')
|
|
-- 5. Umbral parametrizable de la métrica estrella: mismo dataset, dos
|
|
-- resultados distintos según p_umbral_senales (prueba que el parámetro
|
|
-- realmente cambia el comportamiento, no es decorativo)
|
|
-- 6. E9 (rpc_me_segmentacion) — bug real encontrado en VPS: por_nivel leía
|
|
-- answers/2.6 en vez de responses.qi_sector. Prueba que ahora lee
|
|
-- qi_sector, que por_modalidad sigue leyendo por JOIN a 2.7, y que el
|
|
-- k≥5 por segmento sigue omitiendo filas chicas.
|
|
--
|
|
-- NO escrito por mí pero sí ejecutable por el director: no hay Docker en el
|
|
-- entorno donde se generó este archivo — no se corrió contra Postgres real.
|
|
-- ============================================================
|
|
|
|
BEGIN;
|
|
|
|
SELECT plan(28);
|
|
|
|
-- ============================================================
|
|
-- SECCIÓN 1: las funciones existen con la firma esperada
|
|
-- ============================================================
|
|
SELECT has_function('public', '_dash_es_cuidador', ARRAY['uuid'],
|
|
'1.1 _dash_es_cuidador(uuid) existe');
|
|
SELECT has_function('public', 'rpc_me_prevalencia_cuidadores', ARRAY['uuid','jsonb'],
|
|
'1.2 rpc_me_prevalencia_cuidadores existe');
|
|
SELECT has_function('public', 'rpc_me_riesgo_rotacion', ARRAY['uuid','jsonb'],
|
|
'1.3 rpc_me_riesgo_rotacion existe');
|
|
SELECT has_function('public', 'rpc_me_ausentismo', ARRAY['uuid','jsonb'],
|
|
'1.4 rpc_me_ausentismo existe');
|
|
SELECT has_function('public', 'rpc_me_presentismo', ARRAY['uuid','jsonb'],
|
|
'1.5 rpc_me_presentismo existe');
|
|
SELECT has_function('public', 'rpc_me_punto_ciego', ARRAY['uuid','jsonb'],
|
|
'1.6 rpc_me_punto_ciego existe');
|
|
SELECT has_function('public', 'rpc_me_apoyos_demandados', ARRAY['uuid','jsonb'],
|
|
'1.7 rpc_me_apoyos_demandados existe');
|
|
SELECT has_function('public', 'rpc_me_talento_en_riesgo', ARRAY['uuid','jsonb','int4'],
|
|
'1.8 rpc_me_talento_en_riesgo(uuid,jsonb,int) existe — confirma el parámetro de umbral');
|
|
SELECT has_function('public', 'rpc_me_carrera_congelada', ARRAY['uuid','jsonb'],
|
|
'1.9 rpc_me_carrera_congelada existe');
|
|
SELECT has_function('public', 'rpc_me_intensidad_carga', ARRAY['uuid','jsonb'],
|
|
'1.10 rpc_me_intensidad_carga existe');
|
|
SELECT has_function('public', 'rpc_me_segmentacion', ARRAY['uuid','jsonb'],
|
|
'1.11 rpc_me_segmentacion existe');
|
|
SELECT has_function('public', 'rpc_me_disposicion_red', ARRAY['uuid','jsonb'],
|
|
'1.12 rpc_me_disposicion_red existe');
|
|
|
|
-- ============================================================
|
|
-- SECCIÓN 2: fixture — 1 org, 1 survey v3, preguntas mínimas necesarias
|
|
-- ============================================================
|
|
DO $$
|
|
DECLARE
|
|
v_org_id uuid := '10000000-1111-0000-0000-000000000001';
|
|
v_cv_id uuid := '20000000-1111-0000-0000-000000000001';
|
|
v_survey_id uuid := '30000000-1111-0000-0000-000000000001';
|
|
BEGIN
|
|
INSERT INTO public.organizations (id, name) VALUES (v_org_id, '_test_me_org');
|
|
INSERT INTO public.consent_versions (id, version, body, scopes)
|
|
VALUES (v_cv_id, '_test-cv-me', 'body', '["operativo"]'::jsonb);
|
|
INSERT INTO public.surveys (id, org_id, title, is_anonymous, data_sensitivity, security_class, consent_version_id)
|
|
VALUES (v_survey_id, v_org_id, '_test_me_survey', true, 'sensible', 'confidencial', v_cv_id);
|
|
|
|
INSERT INTO public.questions (id, survey_id, code, type, prompt, position) VALUES
|
|
('40000000-1111-0000-0000-000000000041', v_survey_id, '4.1', 'multiple_choice', 'vínculos', 41),
|
|
('40000000-1111-0000-0000-000000000072', v_survey_id, '7.2', 'single_choice', 'presentismo', 72),
|
|
('40000000-1111-0000-0000-000000000073', v_survey_id, '7.3', 'single_choice', 'ausentismo', 73),
|
|
('40000000-1111-0000-0000-000000000075', v_survey_id, '7.5', 'multiple_choice', 'carrera', 75),
|
|
('40000000-1111-0000-0000-000000000076', v_survey_id, '7.6', 'single_choice', 'riesgo rotación', 76),
|
|
('40000000-1111-0000-0000-000000000091', v_survey_id, '9.1', 'single_choice', 'conciencia', 91),
|
|
('40000000-1111-0000-0000-000000000025', v_survey_id, '2.5', 'single_choice', 'género', 25);
|
|
END;
|
|
$$;
|
|
|
|
-- ============================================================
|
|
-- SECCIÓN 3: _dash_es_cuidador — las 4 combinaciones de la definición UNFPA
|
|
-- ============================================================
|
|
DO $$
|
|
DECLARE
|
|
v_survey_id uuid := '30000000-1111-0000-0000-000000000001';
|
|
v_q41 uuid := '40000000-1111-0000-0000-000000000041';
|
|
v_cr_id uuid := '50000000-1111-0000-0000-000000000001';
|
|
v_r_ninguna uuid := '60000000-1111-0000-0000-000000000001'; -- solo "Ninguna..."
|
|
v_r_mixto uuid := '60000000-1111-0000-0000-000000000002'; -- "Ninguna..." + otra opción
|
|
v_r_solonin uuid := '60000000-1111-0000-0000-000000000003'; -- solo "Hijo menor sin discapacidad"
|
|
v_r_sinresp uuid := '60000000-1111-0000-0000-000000000004'; -- no respondió 4.1
|
|
BEGIN
|
|
INSERT INTO public.consent_records (id, consent_version_id, granted_scopes)
|
|
VALUES (v_cr_id, '20000000-1111-0000-0000-000000000001', '{"operativo":true}'::jsonb);
|
|
|
|
INSERT INTO public.responses (id, survey_id, company_id, consent_record_id) VALUES
|
|
(v_r_ninguna, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id),
|
|
(v_r_mixto, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id),
|
|
(v_r_solonin, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id),
|
|
(v_r_sinresp, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id);
|
|
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value) VALUES
|
|
(v_r_ninguna, v_q41, 'default', '["Ninguna de las anteriores / No tengo vinculación de cuidado"]'::jsonb),
|
|
(v_r_mixto, v_q41, 'default', '["Ninguna de las anteriores / No tengo vinculación de cuidado","Otro / Especificar"]'::jsonb),
|
|
(v_r_solonin, v_q41, 'default', '["Hijo menor sin discapacidad"]'::jsonb);
|
|
-- v_r_sinresp: sin fila en answers para 4.1 (a propósito)
|
|
END;
|
|
$$;
|
|
|
|
SELECT is(public._dash_es_cuidador('60000000-1111-0000-0000-000000000001'), false,
|
|
'3.1 solo "Ninguna..." → NO es cuidador');
|
|
SELECT is(public._dash_es_cuidador('60000000-1111-0000-0000-000000000002'), true,
|
|
'3.2 "Ninguna..." + otra opción → SÍ es cuidador (la otra opción cuenta)');
|
|
SELECT is(public._dash_es_cuidador('60000000-1111-0000-0000-000000000003'), true,
|
|
'3.3 solo "Hijo menor sin discapacidad" → SÍ es cuidador (criterio UNFPA)');
|
|
SELECT is(public._dash_es_cuidador('60000000-1111-0000-0000-000000000004'), false,
|
|
'3.4 sin respuesta a 4.1 → NO es cuidador');
|
|
|
|
-- ============================================================
|
|
-- SECCIÓN 4: k≥5 en rpc_me_prevalencia_cuidadores (E1)
|
|
-- 4 cuidadores → suppressed; al agregar 1 más (5) → ok, pct correcto
|
|
-- ============================================================
|
|
SELECT is(
|
|
(public.rpc_me_prevalencia_cuidadores('30000000-1111-0000-0000-000000000001'::uuid)->>'status'),
|
|
'suppressed',
|
|
'4.1 con 4 responses (n<5) → status=suppressed'
|
|
);
|
|
|
|
DO $$
|
|
DECLARE
|
|
v_survey_id uuid := '30000000-1111-0000-0000-000000000001';
|
|
v_q41 uuid := '40000000-1111-0000-0000-000000000041';
|
|
v_cr_id uuid := '50000000-1111-0000-0000-000000000001';
|
|
v_r5 uuid := '60000000-1111-0000-0000-000000000005'; -- 5ta response, NO cuidador
|
|
BEGIN
|
|
INSERT INTO public.responses (id, survey_id, company_id, consent_record_id)
|
|
VALUES (v_r5, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id);
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
VALUES (v_r5, v_q41, 'default', '["Ninguna de las anteriores / No tengo vinculación de cuidado"]'::jsonb);
|
|
END;
|
|
$$;
|
|
|
|
-- Ahora hay 5 responses: 3 cuidadores (mixto, solonin) + ... v_r_sinresp NO es
|
|
-- cuidador (sin 4.1), v_r_ninguna NO es cuidador, v_r5 NO es cuidador.
|
|
-- Cuidadores reales = {v_r_mixto, v_r_solonin} = 2 de 5 → 40.0%
|
|
SELECT is(
|
|
(public.rpc_me_prevalencia_cuidadores('30000000-1111-0000-0000-000000000001'::uuid)->>'status'),
|
|
'ok',
|
|
'4.2 con 5 responses (n>=5) → status=ok'
|
|
);
|
|
SELECT is(
|
|
((public.rpc_me_prevalencia_cuidadores('30000000-1111-0000-0000-000000000001'::uuid)->>'pct_cuidadores')::numeric),
|
|
40.0,
|
|
'4.3 pct_cuidadores = 2/5 = 40.0%'
|
|
);
|
|
|
|
-- ============================================================
|
|
-- SECCIÓN 5: fix de _dash_responses — filtro de género contra 2.5 (no A6)
|
|
-- ============================================================
|
|
DO $$
|
|
DECLARE
|
|
v_survey_id uuid := '30000000-1111-0000-0000-000000000001';
|
|
v_q25 uuid := '40000000-1111-0000-0000-000000000025';
|
|
v_cr_id uuid := '50000000-1111-0000-0000-000000000001';
|
|
v_rg uuid := '60000000-1111-0000-0000-000000000006';
|
|
BEGIN
|
|
INSERT INTO public.responses (id, survey_id, company_id, consent_record_id)
|
|
VALUES (v_rg, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id);
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
VALUES (v_rg, v_q25, 'default', '"Femenino"'::jsonb);
|
|
END;
|
|
$$;
|
|
|
|
SELECT is(
|
|
(SELECT count(*)::int FROM public._dash_responses(
|
|
'30000000-1111-0000-0000-000000000001'::uuid, NULL, ARRAY['Femenino'], NULL, NULL
|
|
) WHERE response_id = '60000000-1111-0000-0000-000000000006'),
|
|
1,
|
|
'5.1 filtro p_genero=[Femenino] encuentra la response con 2.5=Femenino (post-fix)'
|
|
);
|
|
SELECT is(
|
|
(SELECT count(*)::int FROM public._dash_responses(
|
|
'30000000-1111-0000-0000-000000000001'::uuid, NULL, ARRAY['Masculino'], NULL, NULL
|
|
) WHERE response_id = '60000000-1111-0000-0000-000000000006'),
|
|
0,
|
|
'5.2 filtro p_genero=[Masculino] NO encuentra esa response (control negativo)'
|
|
);
|
|
|
|
-- ============================================================
|
|
-- SECCIÓN 6: umbral parametrizable — métrica estrella
|
|
-- 1 cuidador con exactamente 1 de 3 señales (riesgo de rotación, vía 7.6)
|
|
-- ============================================================
|
|
DO $$
|
|
DECLARE
|
|
v_survey_id uuid := '30000000-1111-0000-0000-000000000001';
|
|
v_q41 uuid := '40000000-1111-0000-0000-000000000041';
|
|
v_q76 uuid := '40000000-1111-0000-0000-000000000076';
|
|
v_q73 uuid := '40000000-1111-0000-0000-000000000073';
|
|
v_q75 uuid := '40000000-1111-0000-0000-000000000075';
|
|
v_cr_id uuid := '50000000-1111-0000-0000-000000000001';
|
|
v_r1 uuid := '60000000-2222-0000-0000-000000000001';
|
|
v_r2 uuid := '60000000-2222-0000-0000-000000000002';
|
|
v_r3 uuid := '60000000-2222-0000-0000-000000000003';
|
|
v_r4 uuid := '60000000-2222-0000-0000-000000000004';
|
|
v_r5 uuid := '60000000-2222-0000-0000-000000000005';
|
|
BEGIN
|
|
INSERT INTO public.responses (id, survey_id, company_id, consent_record_id)
|
|
VALUES (v_r1, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id),
|
|
(v_r2, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id),
|
|
(v_r3, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id),
|
|
(v_r4, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id),
|
|
(v_r5, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id);
|
|
|
|
-- Las 5 son cuidadoras (4.1 con un vínculo real) y respondieron 7.6
|
|
-- (ancla de la población), pero NINGUNA cumple 2+ señales — cada una
|
|
-- cumple EXACTAMENTE 1 (riesgo de rotación), 0 en ausentismo/carrera.
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
SELECT r, v_q41, 'default', '["Otro / Especificar"]'::jsonb
|
|
FROM unnest(ARRAY[v_r1,v_r2,v_r3,v_r4,v_r5]) AS r;
|
|
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
SELECT r, v_q76, 'default', '"Muy probable"'::jsonb
|
|
FROM unnest(ARRAY[v_r1,v_r2,v_r3,v_r4,v_r5]) AS r;
|
|
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
SELECT r, v_q73, 'default', '"Nunca"'::jsonb
|
|
FROM unnest(ARRAY[v_r1,v_r2,v_r3,v_r4,v_r5]) AS r;
|
|
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
SELECT r, v_q75, 'default', '["Ninguna de las anteriores"]'::jsonb
|
|
FROM unnest(ARRAY[v_r1,v_r2,v_r3,v_r4,v_r5]) AS r;
|
|
END;
|
|
$$;
|
|
|
|
-- Con el umbral default (2 de 3): nadie llega → 0%
|
|
SELECT is(
|
|
((public.rpc_me_talento_en_riesgo('30000000-1111-0000-0000-000000000001'::uuid)->>'pct_talento_riesgo')::numeric),
|
|
0.0,
|
|
'6.1 umbral default (2 de 3): 0% — nadie acumula 2 señales'
|
|
);
|
|
-- Bajando el umbral a 1 de 3: las 5 cumplen (todas tienen la señal de riesgo) → 100%
|
|
SELECT is(
|
|
((public.rpc_me_talento_en_riesgo(
|
|
'30000000-1111-0000-0000-000000000001'::uuid, '{}'::jsonb, 1
|
|
)->>'pct_talento_riesgo')::numeric),
|
|
100.0,
|
|
'6.2 umbral=1 (parámetro explícito): 100% — el parámetro SÍ cambia el resultado'
|
|
);
|
|
SELECT is(
|
|
((public.rpc_me_talento_en_riesgo('30000000-1111-0000-0000-000000000001'::uuid)->>'criterios_promedio')::numeric),
|
|
1.00,
|
|
'6.3 criterios_promedio = 1.00 (cada cuidadora cumple exactamente 1 de 3 señales)'
|
|
);
|
|
|
|
-- ============================================================
|
|
-- SECCIÓN 7: E9 (rpc_me_segmentacion) — por_nivel via qi_sector,
|
|
-- por_modalidad via JOIN a 2.7, k≥5 por segmento
|
|
-- ============================================================
|
|
DO $$
|
|
DECLARE
|
|
v_survey_id uuid := '30000000-1111-0000-0000-000000000001';
|
|
v_q27 uuid := '40000000-1111-0000-0000-000000000027';
|
|
v_cr_id uuid := '50000000-1111-0000-0000-000000000001';
|
|
v_r uuid;
|
|
i int;
|
|
BEGIN
|
|
INSERT INTO public.questions (id, survey_id, code, type, prompt, position)
|
|
VALUES (v_q27, v_survey_id, '2.7', 'single_choice', 'modalidad', 27);
|
|
|
|
-- 6 responses "Operativo"/"Presencial" (n=6 >= 5 → visible)
|
|
FOR i IN 1..6 LOOP
|
|
v_r := ('60000000-3333-0000-0000-' || lpad(i::text, 12, '0'))::uuid;
|
|
INSERT INTO public.responses (id, survey_id, company_id, consent_record_id, qi_sector)
|
|
VALUES (v_r, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id, 'Operativo');
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
VALUES (v_r, v_q27, 'default', '"Presencial"'::jsonb);
|
|
END LOOP;
|
|
|
|
-- 3 responses "Gerencial"/"Remoto (home office)" (n=3 < 5 → omitido)
|
|
FOR i IN 1..3 LOOP
|
|
v_r := ('60000000-4444-0000-0000-' || lpad(i::text, 12, '0'))::uuid;
|
|
INSERT INTO public.responses (id, survey_id, company_id, consent_record_id, qi_sector)
|
|
VALUES (v_r, v_survey_id, '10000000-1111-0000-0000-000000000001', v_cr_id, 'Gerencial');
|
|
INSERT INTO public.answers (response_id, question_id, instance_id, value)
|
|
VALUES (v_r, v_q27, 'default', '"Remoto (home office)"'::jsonb);
|
|
END LOOP;
|
|
END;
|
|
$$;
|
|
|
|
SELECT is(
|
|
jsonb_path_query_first(
|
|
public.rpc_me_segmentacion('30000000-1111-0000-0000-000000000001'::uuid),
|
|
'$.por_nivel[*] ? (@.nivel == "Operativo")'
|
|
)->>'n',
|
|
'6',
|
|
'7.1 por_nivel lee qi_sector — "Operativo" aparece con n=6 (post-fix)'
|
|
);
|
|
SELECT is(
|
|
jsonb_path_query_first(
|
|
public.rpc_me_segmentacion('30000000-1111-0000-0000-000000000001'::uuid),
|
|
'$.por_nivel[*] ? (@.nivel == "Gerencial")'
|
|
),
|
|
NULL::jsonb,
|
|
'7.2 "Gerencial" (n=3 < 5) está omitido de por_nivel — k-anonimato por segmento'
|
|
);
|
|
SELECT is(
|
|
jsonb_path_query_first(
|
|
public.rpc_me_segmentacion('30000000-1111-0000-0000-000000000001'::uuid),
|
|
'$.por_modalidad[*] ? (@.modalidad == "Presencial")'
|
|
)->>'n',
|
|
'6',
|
|
'7.3 por_modalidad lee 2.7 por JOIN a answers — "Presencial" aparece con n=6'
|
|
);
|
|
SELECT is(
|
|
jsonb_path_query_first(
|
|
public.rpc_me_segmentacion('30000000-1111-0000-0000-000000000001'::uuid),
|
|
'$.por_modalidad[*] ? (@.modalidad == "Remoto (home office)")'
|
|
),
|
|
NULL::jsonb,
|
|
'7.4 "Remoto (home office)" (n=3 < 5) está omitido de por_modalidad'
|
|
);
|
|
|
|
SELECT * FROM finish();
|
|
ROLLBACK;
|