feat(dashboard): RPCs Mirada Empresa v3 + fix códigos v2 en helper [dashboard]
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>
This commit is contained in:
327
supabase/tests/02_dashboard_mirada_empresa.test.sql
Normal file
327
supabase/tests/02_dashboard_mirada_empresa.test.sql
Normal file
@@ -0,0 +1,327 @@
|
||||
-- ============================================================
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user