panels-origin/db/core/changesets/016-rpc-worker-needs-badge.sql
Cursor Agent 3171cddd68
fix(core): RPC jsonb payload, Liquibase splits, worker needs_badge
- Pass JS objects to postgres jsonb params in callCoreFn (not JSON.stringify)
- Use splitStatements:false on RPC function changesets for Liquibase
- Fix needs_badge default in _worker_normalize when field is omitted
- Add scripts/crud-smoke-test.sh for API CRUD verification
- Fix findExistingWorker return type in main.ts

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-04 01:20:49 +00:00

200 lines
6.7 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · fix needs_badge cast en fn_worker_create/update
--changeset panel:core-016a-fn-worker-needs-badge splitStatements:false
CREATE OR REPLACE FUNCTION core.fn_worker_create(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_tid integer := COALESCE(
NULLIF(payload->>'tenant_id', '')::integer,
NULLIF(current_setting('app.tenant_id', true), '')::integer
);
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
v_errors jsonb;
v_norm jsonb;
v_existing workers%ROWTYPE;
v_row workers%ROWTYPE;
v_project_status text;
v_refresh jsonb;
BEGIN
v_errors := core._worker_validate_fields(payload);
IF v_errors <> '{}'::jsonb THEN
RETURN core.rpc_err(
'VALIDATION',
'fn_worker_create: datos del trabajador incompletos o inválidos',
jsonb_build_object('fn', 'fn_worker_create', 'field', 'payload'),
v_errors
);
END IF;
v_norm := core._worker_normalize(payload);
SELECT * INTO v_existing
FROM workers
WHERE curp = v_norm->>'curp'
OR rfc = v_norm->>'rfc'
OR nss = v_norm->>'nss'
LIMIT 1;
IF FOUND THEN
RETURN core.rpc_err(
'CONFLICT',
core._worker_conflict_message(v_existing, v_norm->>'curp', v_norm->>'rfc', v_norm->>'nss'),
jsonb_build_object('fn', 'fn_worker_create', 'worker_id', v_existing.id)
);
END IF;
IF v_project_id IS NOT NULL THEN
SELECT status INTO v_project_status FROM projects WHERE id = v_project_id;
IF NOT FOUND THEN
RETURN core.rpc_err(
'NOT_FOUND',
format('fn_worker_create: proyecto id=%s no encontrado', v_project_id),
jsonb_build_object('fn', 'fn_worker_create', 'project_id', v_project_id)
);
END IF;
IF v_project_status <> 'activo' THEN
RETURN core.rpc_err(
'VALIDATION',
'fn_worker_create: solo se asigna personal a proyectos activos',
jsonb_build_object('fn', 'fn_worker_create', 'project_id', v_project_id, 'status', v_project_status)
);
END IF;
END IF;
INSERT INTO workers (
first_name, middle_name, last_name_p, last_name_m, curp, rfc, nss, phone, email, address,
blood_type, hire_type, company_id, position, risk_code, work_type, daily_wage, needs_badge, status, tenant_id
) VALUES (
v_norm->>'first_name',
v_norm->>'middle_name',
v_norm->>'last_name_p',
v_norm->>'last_name_m',
v_norm->>'curp',
v_norm->>'rfc',
v_norm->>'nss',
v_norm->>'phone',
v_norm->>'email',
v_norm->>'address',
v_norm->>'blood_type',
'',
NULL,
v_norm->>'position',
v_norm->>'risk_code',
v_norm->>'work_type',
(v_norm->>'daily_wage')::numeric(12,2),
COALESCE((v_norm->'needs_badge')::boolean, true),
v_norm->>'status',
v_tid
)
RETURNING * INTO v_row;
IF v_project_id IS NOT NULL THEN
INSERT INTO assignments (worker_id, project_id, active, start_date)
VALUES (v_row.id, v_project_id, true, current_date)
ON CONFLICT (worker_id, project_id) DO UPDATE SET
active = true,
start_date = CASE
WHEN assignments.active = false THEN excluded.start_date
ELSE assignments.start_date
END,
end_date = NULL;
END IF;
v_refresh := core.fn_worker_refresh_pipeline(jsonb_build_object('worker_id', v_row.id, 'tenant_id', v_tid));
RETURN core.rpc_created(
jsonb_build_object('worker', to_jsonb(v_row), 'pipeline', v_refresh->'data'),
format('Trabajador %s %s creado (id=%s)', v_row.first_name, v_row.last_name_p, v_row.id),
jsonb_build_object('fn', 'fn_worker_create', 'id', v_row.id)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_worker_create', SQLSTATE, SQLERRM);
END;
$$;
CREATE OR REPLACE FUNCTION core.fn_worker_update(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_id bigint := NULLIF(payload->>'id', '')::bigint;
v_tid integer := COALESCE(
NULLIF(payload->>'tenant_id', '')::integer,
NULLIF(current_setting('app.tenant_id', true), '')::integer
);
v_cur workers%ROWTYPE;
v_merged jsonb;
v_errors jsonb;
v_norm jsonb;
v_existing workers%ROWTYPE;
v_refresh jsonb;
BEGIN
IF v_id IS NULL THEN
RETURN core.rpc_err(
'VALIDATION',
'fn_worker_update: id es obligatorio para actualizar un trabajador',
jsonb_build_object('fn', 'fn_worker_update', 'field', 'id')
);
END IF;
SELECT * INTO v_cur FROM workers WHERE id = v_id;
IF NOT FOUND THEN
RETURN core.rpc_err(
'NOT_FOUND',
format('fn_worker_update: trabajador id=%s no encontrado', v_id),
jsonb_build_object('fn', 'fn_worker_update', 'id', v_id)
);
END IF;
v_merged := to_jsonb(v_cur) || payload;
v_errors := core._worker_validate_fields(v_merged);
IF v_errors <> '{}'::jsonb THEN
RETURN core.rpc_err(
'VALIDATION',
format('fn_worker_update: datos del trabajador inválidos (id=%s)', v_id),
jsonb_build_object('fn', 'fn_worker_update', 'id', v_id),
v_errors
);
END IF;
v_norm := core._worker_normalize(v_merged);
SELECT * INTO v_existing
FROM workers
WHERE (curp = v_norm->>'curp' OR rfc = v_norm->>'rfc' OR nss = v_norm->>'nss')
AND id <> v_id
LIMIT 1;
IF FOUND THEN
RETURN core.rpc_err(
'CONFLICT',
core._worker_conflict_message(v_existing, v_norm->>'curp', v_norm->>'rfc', v_norm->>'nss'),
jsonb_build_object('fn', 'fn_worker_update', 'id', v_id, 'worker_id', v_existing.id)
);
END IF;
UPDATE workers SET
first_name = v_norm->>'first_name',
middle_name = v_norm->>'middle_name',
last_name_p = v_norm->>'last_name_p',
last_name_m = v_norm->>'last_name_m',
curp = v_norm->>'curp',
rfc = v_norm->>'rfc',
nss = v_norm->>'nss',
phone = v_norm->>'phone',
email = v_norm->>'email',
address = v_norm->>'address',
blood_type = v_norm->>'blood_type',
hire_type = v_cur.hire_type,
company_id = v_cur.company_id,
position = v_norm->>'position',
risk_code = v_norm->>'risk_code',
work_type = v_norm->>'work_type',
daily_wage = (v_norm->>'daily_wage')::numeric(12,2),
needs_badge = COALESCE((v_norm->'needs_badge')::boolean, v_cur.needs_badge, true),
status = v_norm->>'status'
WHERE id = v_id
RETURNING * INTO v_cur;
v_refresh := core.fn_worker_refresh_pipeline(jsonb_build_object('worker_id', v_id, 'tenant_id', v_tid));
RETURN core.rpc_ok(
jsonb_build_object('worker', to_jsonb(v_cur), 'pipeline', v_refresh->'data'),
format('Trabajador %s %s actualizado (id=%s)', v_cur.first_name, v_cur.last_name_p, v_id),
jsonb_build_object('fn', 'fn_worker_update', 'id', v_id)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_worker_update', SQLSTATE, SQLERRM);
END;
$$;