mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 21:43:17 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
Migrates the `core` schema business logic from inline `db.prepare()` calls in the API to PostgreSQL RPC functions (`core.fn_*`) with a unified JSON envelope for errors and HTTP status mapping.
### Database (Liquibase changesets 006–017)
- **006** — RPC infra: `rpc_ok`, `rpc_err`, `rpc_created`, `rpc_from_exception`
- **007** — Catalogs and tenant settings
- **008** — Companies CRUD
- **009** — Projects, checklists, document lists
- **010** — Workers CRUD, pipeline, checklist, assign
- **011** — Budget CRUD + `fn_budget_replace`
- **012** — Badge jobs
- **013** — Payroll (settings, attendance, loans, weeks, destajo)
- **014** — Worker import batch + document store
- **015** — Project/company/worker document metadata RPCs
- **016–017** — Fixes: `needs_badge` default on worker create; Liquibase `splitStatements:false` on function changesets
### API
- `api/rpc.ts` — `callCoreFn()`, `RpcCallError` (jsonb payload fix: pass JS object, not `JSON.stringify`)
- `api/http_errors.ts` — `mapRpcToStatus()`, `respondRpc()`, `respondApiError()`, `onAppError()`
- Refactored: `main.ts`, `companies.ts`, `db.ts`, `budget.ts`, `excel.ts`, `payroll.ts`, `payroll_http.ts`
- Front helpers: `web-panel/composables/api-response.ts`, `web-saas/composables/api-response.ts`
### Envelope contract
DB functions return `{ ok, code, layer: "db", message, context, data, errors }`. The API adds `status` (HTTP code) via `respondRpc()` / `respondApiError()`.
### Out of scope
`iam`, `platform`, `saas.ts`, auth/sessions, S3, PDF generation, Excel parsing, and bootstrap scripts still use direct SQL where appropriate.
## Test plan
- [x] `deno check main.ts` — compila sin errores de tipos
- [x] `npm run build` — web-panel y web-saas compilan
- [x] `deno test` — 25 tests unitarios (http_errors, companies, budget, mx, document_validity)
- [x] Liquibase migrations `006`–`017` aplicadas en Postgres local (`--context-filter=dev`)
- [x] API levantada localmente; `/v1/health` OK
- [x] Smoke CRUD vía `scripts/crud-smoke-test.sh`: empresas, proyectos, trabajadores, catálogos (create/get/list/patch)
- [ ] Import Excel de trabajadores (flujo multipart + S3/local storage)
- [ ] Import presupuesto desde Excel
- [ ] Flujo nómina: asistencia → cerrar semana
- [ ] CI en el remoto (sin checks reportados aún)
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-06667c14-38e8-42a8-9ed9-6b1322f12ae7?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-06667c14-38e8-42a8-9ed9-6b1322f12ae7&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
566 lines
20 KiB
PL/PgSQL
566 lines
20 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · RPC importación de personal y metadatos documentales
|
|
|
|
--changeset panel:core-014a-import-helpers splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core._import_tenant_id(payload jsonb)
|
|
RETURNS integer
|
|
LANGUAGE sql
|
|
STABLE
|
|
AS $$
|
|
SELECT COALESCE(
|
|
NULLIF(payload->>'tenant_id', '')::integer,
|
|
NULLIF(current_setting('app.tenant_id', true), '')::integer,
|
|
0
|
|
);
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_norm_upper(p_value text)
|
|
RETURNS text
|
|
LANGUAGE sql
|
|
IMMUTABLE
|
|
AS $$
|
|
SELECT upper(regexp_replace(btrim(COALESCE(p_value, '')), '\s+', '', 'g'));
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_norm_name(p_value text)
|
|
RETURNS text
|
|
LANGUAGE sql
|
|
IMMUTABLE
|
|
AS $$
|
|
SELECT regexp_replace(btrim(COALESCE(p_value, '')), '\s+', ' ', 'g');
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_format_nss(p_value text)
|
|
RETURNS text
|
|
LANGUAGE sql
|
|
IMMUTABLE
|
|
AS $$
|
|
SELECT regexp_replace(COALESCE(p_value, ''), '\D', '', 'g');
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_canonical_risk(p_value text)
|
|
RETURNS text
|
|
LANGUAGE sql
|
|
IMMUTABLE
|
|
AS $$
|
|
SELECT CASE lower(btrim(COALESCE(p_value, '')))
|
|
WHEN 'alto' THEN 'rojo'
|
|
WHEN 'medio' THEN 'amarillo'
|
|
WHEN 'bajo' THEN 'verde'
|
|
ELSE lower(btrim(COALESCE(p_value, '')))
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_validate_worker_row(p_row jsonb)
|
|
RETURNS text[]
|
|
LANGUAGE plpgsql
|
|
IMMUTABLE
|
|
AS $$
|
|
DECLARE
|
|
v_errors text[] := ARRAY[]::text[];
|
|
v_email text := lower(btrim(COALESCE(p_row->>'email', '')));
|
|
v_wt text := upper(btrim(COALESCE(p_row->>'work_type', '')));
|
|
v_nss text := core._import_format_nss(p_row->>'nss');
|
|
BEGIN
|
|
IF core._import_norm_name(p_row->>'first_name') = '' THEN
|
|
v_errors := array_append(v_errors, 'Nombre obligatorio');
|
|
END IF;
|
|
IF core._import_norm_name(p_row->>'last_name_p') = '' THEN
|
|
v_errors := array_append(v_errors, 'Apellido paterno obligatorio');
|
|
END IF;
|
|
IF core._import_norm_name(p_row->>'last_name_m') = '' THEN
|
|
v_errors := array_append(v_errors, 'Apellido materno obligatorio');
|
|
END IF;
|
|
IF length(core._import_norm_upper(p_row->>'curp')) <> 18 THEN
|
|
v_errors := array_append(v_errors, 'CURP inválida (18 caracteres, formato oficial)');
|
|
END IF;
|
|
IF length(core._import_norm_upper(p_row->>'rfc')) NOT IN (12, 13) THEN
|
|
v_errors := array_append(v_errors, 'RFC inválido (12 o 13 caracteres, formato SAT)');
|
|
END IF;
|
|
IF length(v_nss) <> 11 THEN
|
|
v_errors := array_append(v_errors, 'NSS inválido (11 dígitos)');
|
|
END IF;
|
|
IF core._import_norm_name(p_row->>'phone') = '' THEN
|
|
v_errors := array_append(v_errors, 'Teléfono obligatorio');
|
|
END IF;
|
|
IF v_email = '' THEN
|
|
v_errors := array_append(v_errors, 'Correo obligatorio');
|
|
ELSIF v_email !~ '^[^\s@]+@[^\s@]+\.[^\s@]+$' THEN
|
|
v_errors := array_append(v_errors, 'Correo inválido');
|
|
END IF;
|
|
IF core._import_norm_name(p_row->>'address') = '' THEN
|
|
v_errors := array_append(v_errors, 'Dirección obligatoria');
|
|
END IF;
|
|
IF core._import_norm_name(p_row->>'position') = '' THEN
|
|
v_errors := array_append(v_errors, 'Cargo obligatorio');
|
|
END IF;
|
|
IF core._import_canonical_risk(p_row->>'risk_code') = '' THEN
|
|
v_errors := array_append(v_errors, 'Riesgo obligatorio');
|
|
END IF;
|
|
IF v_wt NOT IN ('N', 'D') THEN
|
|
v_errors := array_append(v_errors, 'Tipo de trabajo N o D');
|
|
END IF;
|
|
IF p_row->>'daily_wage' IS NULL OR (p_row->>'daily_wage')::numeric IS NULL THEN
|
|
v_errors := array_append(v_errors, 'Jornal obligatorio');
|
|
END IF;
|
|
RETURN v_errors;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_find_worker(p_curp text, p_rfc text, p_nss text)
|
|
RETURNS TABLE(id bigint, curp text, rfc text, nss text)
|
|
LANGUAGE sql
|
|
STABLE
|
|
SET search_path = core
|
|
AS $$
|
|
SELECT w.id, w.curp::text, w.rfc::text, w.nss
|
|
FROM workers w
|
|
WHERE w.curp = p_curp OR w.rfc = p_rfc OR w.nss = p_nss
|
|
LIMIT 1;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_assign_worker(p_worker_id bigint, p_project_id bigint)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SET search_path = core
|
|
AS $$
|
|
BEGIN
|
|
IF p_project_id IS NULL THEN
|
|
RETURN;
|
|
END IF;
|
|
INSERT INTO assignments (worker_id, project_id, active, start_date)
|
|
VALUES (p_worker_id, p_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;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core._import_upsert_worker(
|
|
p_row jsonb,
|
|
p_company_id bigint,
|
|
p_tenant_id integer
|
|
)
|
|
RETURNS TABLE(worker_id bigint, action text, matched text)
|
|
LANGUAGE plpgsql
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_curp text := core._import_norm_upper(p_row->>'curp');
|
|
v_rfc text := core._import_norm_upper(p_row->>'rfc');
|
|
v_nss text := core._import_format_nss(p_row->>'nss');
|
|
v_existing record;
|
|
v_status text := CASE WHEN lower(btrim(COALESCE(p_row->>'status', 'activo'))) = 'baja' THEN 'baja' ELSE 'activo' END;
|
|
v_needs_badge boolean := CASE
|
|
WHEN lower(COALESCE(p_row->>'needs_badge', 'true')) IN ('false', '0', 'no') THEN false
|
|
ELSE true
|
|
END;
|
|
v_id bigint;
|
|
BEGIN
|
|
SELECT * INTO v_existing FROM core._import_find_worker(v_curp, v_rfc, v_nss);
|
|
IF FOUND THEN
|
|
UPDATE workers SET
|
|
first_name = initcap(lower(core._import_norm_name(p_row->>'first_name'))),
|
|
middle_name = NULLIF(initcap(lower(core._import_norm_name(p_row->>'middle_name'))), ''),
|
|
last_name_p = initcap(lower(core._import_norm_name(p_row->>'last_name_p'))),
|
|
last_name_m = initcap(lower(core._import_norm_name(p_row->>'last_name_m'))),
|
|
curp = v_curp,
|
|
rfc = v_rfc,
|
|
nss = v_nss,
|
|
phone = core._import_norm_name(p_row->>'phone'),
|
|
email = lower(btrim(COALESCE(p_row->>'email', ''))),
|
|
address = core._import_norm_name(p_row->>'address'),
|
|
blood_type = NULLIF(core._import_norm_upper(p_row->>'blood_type'), ''),
|
|
hire_type = upper(btrim(COALESCE(p_row->>'hire_type', ''))),
|
|
company_id = p_company_id,
|
|
tenant_id = COALESCE(p_tenant_id, tenant_id),
|
|
position = initcap(lower(core._import_norm_name(p_row->>'position'))),
|
|
risk_code = core._import_canonical_risk(p_row->>'risk_code'),
|
|
work_type = upper(btrim(COALESCE(p_row->>'work_type', ''))),
|
|
daily_wage = COALESCE((p_row->>'daily_wage')::numeric, 0),
|
|
needs_badge = v_needs_badge,
|
|
status = v_status,
|
|
updated_at = now()
|
|
WHERE id = v_existing.id;
|
|
worker_id := v_existing.id;
|
|
action := 'existed';
|
|
matched := CASE
|
|
WHEN v_existing.curp = v_curp THEN 'CURP'
|
|
WHEN v_existing.rfc = v_rfc THEN 'RFC'
|
|
ELSE 'NSS'
|
|
END;
|
|
RETURN NEXT;
|
|
RETURN;
|
|
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 (
|
|
initcap(lower(core._import_norm_name(p_row->>'first_name'))),
|
|
NULLIF(initcap(lower(core._import_norm_name(p_row->>'middle_name'))), ''),
|
|
initcap(lower(core._import_norm_name(p_row->>'last_name_p'))),
|
|
initcap(lower(core._import_norm_name(p_row->>'last_name_m'))),
|
|
v_curp, v_rfc, v_nss,
|
|
core._import_norm_name(p_row->>'phone'),
|
|
lower(btrim(COALESCE(p_row->>'email', ''))),
|
|
core._import_norm_name(p_row->>'address'),
|
|
NULLIF(core._import_norm_upper(p_row->>'blood_type'), ''),
|
|
upper(btrim(COALESCE(p_row->>'hire_type', ''))),
|
|
p_company_id,
|
|
initcap(lower(core._import_norm_name(p_row->>'position'))),
|
|
core._import_canonical_risk(p_row->>'risk_code'),
|
|
upper(btrim(COALESCE(p_row->>'work_type', ''))),
|
|
COALESCE((p_row->>'daily_wage')::numeric, 0),
|
|
v_needs_badge,
|
|
v_status,
|
|
p_tenant_id
|
|
)
|
|
RETURNING id INTO v_id;
|
|
worker_id := v_id;
|
|
action := 'inserted';
|
|
matched := NULL;
|
|
RETURN NEXT;
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-014b-fn-workers-import-batch splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_workers_import_batch(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._import_tenant_id(payload);
|
|
v_default_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
|
v_rows jsonb := COALESCE(payload->'rows', '[]'::jsonb);
|
|
v_row jsonb;
|
|
v_idx integer := 0;
|
|
v_sheet text;
|
|
v_row_no integer;
|
|
v_nombre text;
|
|
v_errors text[];
|
|
v_company record;
|
|
v_company_code text;
|
|
v_company_id bigint;
|
|
v_project_id bigint;
|
|
v_status text;
|
|
v_curp text;
|
|
v_rfc text;
|
|
v_nss text;
|
|
v_seen_curp jsonb := '{}'::jsonb;
|
|
v_seen_rfc jsonb := '{}'::jsonb;
|
|
v_seen_nss jsonb := '{}'::jsonb;
|
|
v_dup_row integer;
|
|
v_upsert record;
|
|
v_inserted integer := 0;
|
|
v_existed integer := 0;
|
|
v_error_count integer := 0;
|
|
v_error_rows jsonb := '[]'::jsonb;
|
|
v_existed_rows jsonb := '[]'::jsonb;
|
|
BEGIN
|
|
IF jsonb_typeof(v_rows) <> 'array' THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_workers_import_batch: rows debe ser un arreglo JSON',
|
|
jsonb_build_object('fn', 'fn_workers_import_batch', 'field', 'rows')
|
|
);
|
|
END IF;
|
|
IF jsonb_array_length(v_rows) = 0 THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_workers_import_batch: el arreglo rows está vacío',
|
|
jsonb_build_object('fn', 'fn_workers_import_batch', 'field', 'rows')
|
|
);
|
|
END IF;
|
|
|
|
FOR v_row IN SELECT value FROM jsonb_array_elements(v_rows) LOOP
|
|
v_idx := v_idx + 1;
|
|
v_sheet := COALESCE(NULLIF(btrim(v_row->>'sheet'), ''), 'IMPORT');
|
|
v_row_no := COALESCE((v_row->>'row')::integer, v_idx);
|
|
v_nombre := trim(concat_ws(' ', v_row->>'first_name', v_row->>'last_name_p', v_row->>'last_name_m'));
|
|
v_errors := core._import_validate_worker_row(v_row);
|
|
|
|
v_company_code := upper(btrim(COALESCE(v_row->>'hire_type', v_row->>'company_code', '')));
|
|
v_company_id := NULLIF(v_row->>'company_id', '')::bigint;
|
|
IF v_company_id IS NOT NULL THEN
|
|
SELECT id, code, tenant_id INTO v_company FROM companies WHERE id = v_company_id;
|
|
ELSIF v_company_code <> '' THEN
|
|
SELECT id, code, tenant_id INTO v_company FROM companies WHERE upper(code) = v_company_code;
|
|
ELSE
|
|
v_company := NULL;
|
|
END IF;
|
|
IF v_company.id IS NULL THEN
|
|
v_errors := array_append(
|
|
v_errors,
|
|
format('Empresa no encontrada (%s)', COALESCE(NULLIF(v_company_code, ''), '—'))
|
|
);
|
|
ELSE
|
|
v_company_id := v_company.id;
|
|
IF v_tid = 0 AND v_company.tenant_id IS NOT NULL THEN
|
|
v_tid := v_company.tenant_id;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM risk_levels WHERE code = core._import_canonical_risk(v_row->>'risk_code')
|
|
) THEN
|
|
v_errors := array_append(
|
|
v_errors,
|
|
format('Riesgo inválido (%s). Use alto, medio o bajo', COALESCE(v_row->>'risk_code', '—'))
|
|
);
|
|
END IF;
|
|
|
|
v_curp := core._import_norm_upper(v_row->>'curp');
|
|
v_rfc := core._import_norm_upper(v_row->>'rfc');
|
|
v_nss := core._import_format_nss(v_row->>'nss');
|
|
|
|
IF cardinality(v_errors) = 0 THEN
|
|
IF v_seen_curp ? v_curp THEN
|
|
v_dup_row := (v_seen_curp->>v_curp)::integer;
|
|
v_errors := array_append(v_errors, format('CURP duplicada en el lote (fila %s)', v_dup_row));
|
|
END IF;
|
|
IF v_seen_rfc ? v_rfc THEN
|
|
v_dup_row := (v_seen_rfc->>v_rfc)::integer;
|
|
v_errors := array_append(v_errors, format('RFC duplicado en el lote (fila %s)', v_dup_row));
|
|
END IF;
|
|
IF v_seen_nss ? v_nss THEN
|
|
v_dup_row := (v_seen_nss->>v_nss)::integer;
|
|
v_errors := array_append(v_errors, format('NSS duplicado en el lote (fila %s)', v_dup_row));
|
|
END IF;
|
|
END IF;
|
|
|
|
IF cardinality(v_errors) > 0 THEN
|
|
v_error_count := v_error_count + 1;
|
|
v_error_rows := v_error_rows || jsonb_build_array(jsonb_build_object(
|
|
'sheet', v_sheet,
|
|
'row', v_row_no,
|
|
'nombre', v_nombre,
|
|
'curp', COALESCE(v_row->>'curp', ''),
|
|
'messages', to_jsonb(v_errors)
|
|
));
|
|
CONTINUE;
|
|
END IF;
|
|
|
|
v_seen_curp := v_seen_curp || jsonb_build_object(v_curp, v_row_no);
|
|
v_seen_rfc := v_seen_rfc || jsonb_build_object(v_rfc, v_row_no);
|
|
v_seen_nss := v_seen_nss || jsonb_build_object(v_nss, v_row_no);
|
|
|
|
v_status := CASE WHEN lower(btrim(COALESCE(v_row->>'status', 'activo'))) = 'baja' THEN 'baja' ELSE 'activo' END;
|
|
v_project_id := COALESCE(NULLIF(v_row->>'project_id', '')::bigint, v_default_project_id);
|
|
IF v_status <> 'activo' THEN
|
|
v_project_id := NULL;
|
|
END IF;
|
|
|
|
SELECT * INTO v_upsert
|
|
FROM core._import_upsert_worker(
|
|
v_row || jsonb_build_object('hire_type', v_company.code),
|
|
v_company_id,
|
|
COALESCE(v_tid, v_company.tenant_id, 1)
|
|
);
|
|
|
|
IF v_project_id IS NOT NULL THEN
|
|
PERFORM core._import_assign_worker(v_upsert.worker_id, v_project_id);
|
|
END IF;
|
|
|
|
IF v_status = 'baja' THEN
|
|
UPDATE workers SET pipeline_status = 'baja' WHERE id = v_upsert.worker_id;
|
|
END IF;
|
|
|
|
IF v_upsert.action = 'inserted' THEN
|
|
v_inserted := v_inserted + 1;
|
|
ELSE
|
|
v_existed := v_existed + 1;
|
|
v_existed_rows := v_existed_rows || jsonb_build_array(jsonb_build_object(
|
|
'sheet', v_sheet,
|
|
'row', v_row_no,
|
|
'nombre', v_nombre,
|
|
'curp', v_curp,
|
|
'matched', COALESCE(v_upsert.matched, 'CURP')
|
|
));
|
|
END IF;
|
|
END LOOP;
|
|
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object(
|
|
'inserted', v_inserted,
|
|
'existed', v_existed,
|
|
'errors', v_error_count,
|
|
'existed_rows', v_existed_rows,
|
|
'error_rows', v_error_rows
|
|
),
|
|
format('Importación por lote: %s nuevo(s), %s existente(s), %s error(es)',
|
|
v_inserted, v_existed, v_error_count),
|
|
jsonb_build_object(
|
|
'fn', 'fn_workers_import_batch',
|
|
'tenant_id', v_tid,
|
|
'inserted', v_inserted,
|
|
'existed', v_existed,
|
|
'errors', v_error_count
|
|
)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_workers_import_batch', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-014c-fn-worker-document-store splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_worker_document_store(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
|
|
v_type_code text := btrim(COALESCE(payload->>'type_code', payload->>'type', ''));
|
|
v_original_name text := btrim(COALESCE(payload->>'original_name', payload->>'filename', ''));
|
|
v_mime text := btrim(COALESCE(payload->>'mime', ''));
|
|
v_size_bytes bigint := NULLIF(payload->>'size_bytes', '')::bigint;
|
|
v_sha256 text := btrim(COALESCE(payload->>'sha256', ''));
|
|
v_iv text := btrim(COALESCE(payload->>'iv', ''));
|
|
v_storage_name text := btrim(COALESCE(payload->>'storage_name', ''));
|
|
v_uploaded_by_id integer := NULLIF(payload->>'uploaded_by_id', '')::integer;
|
|
v_uploaded_by_name text := COALESCE(NULLIF(btrim(payload->>'uploaded_by_name'), ''), '');
|
|
v_issued_at date := NULLIF(btrim(payload->>'issued_at'), '')::date;
|
|
v_expires_at date := NULLIF(btrim(payload->>'expires_at'), '')::date;
|
|
v_imss_company_id integer := NULLIF(payload->>'imss_company_id', '')::integer;
|
|
v_imss_alta_at date := NULLIF(btrim(payload->>'imss_alta_at'), '')::date;
|
|
v_imss_baja_at date := NULLIF(btrim(payload->>'imss_baja_at'), '')::date;
|
|
v_today date := current_date;
|
|
v_movement_date date;
|
|
v_doc_id bigint;
|
|
v_company record;
|
|
BEGIN
|
|
IF v_worker_id IS NULL THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_worker_document_store: worker_id es obligatorio',
|
|
jsonb_build_object('fn', 'fn_worker_document_store', 'field', 'worker_id')
|
|
);
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM workers WHERE id = v_worker_id) THEN
|
|
RETURN core.rpc_err(
|
|
'NOT_FOUND',
|
|
format('fn_worker_document_store: trabajador id=%s no encontrado', v_worker_id),
|
|
jsonb_build_object('fn', 'fn_worker_document_store', 'worker_id', v_worker_id)
|
|
);
|
|
END IF;
|
|
IF v_type_code = '' THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_worker_document_store: type_code es obligatorio',
|
|
jsonb_build_object('fn', 'fn_worker_document_store', 'field', 'type_code')
|
|
);
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM document_types WHERE code = v_type_code) THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
format('fn_worker_document_store: tipo de documento no válido (%s)', v_type_code),
|
|
jsonb_build_object('fn', 'fn_worker_document_store', 'type_code', v_type_code)
|
|
);
|
|
END IF;
|
|
IF v_original_name = '' OR v_mime = '' OR v_sha256 = '' OR v_iv = '' OR v_storage_name = '' THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_worker_document_store: original_name, mime, sha256, iv y storage_name son obligatorios tras la carga a almacenamiento',
|
|
jsonb_build_object('fn', 'fn_worker_document_store')
|
|
);
|
|
END IF;
|
|
IF v_size_bytes IS NULL OR v_size_bytes < 0 THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_worker_document_store: size_bytes debe ser un entero no negativo',
|
|
jsonb_build_object('fn', 'fn_worker_document_store', 'field', 'size_bytes')
|
|
);
|
|
END IF;
|
|
|
|
v_imss_alta_at := COALESCE(v_imss_alta_at, CASE WHEN v_type_code = 'alta_imss' THEN v_today END);
|
|
v_imss_baja_at := COALESCE(v_imss_baja_at, CASE WHEN v_type_code = 'baja_imss' THEN v_today END);
|
|
v_movement_date := CASE WHEN v_type_code = 'baja_imss' THEN v_imss_baja_at ELSE v_imss_alta_at END;
|
|
|
|
UPDATE documents
|
|
SET is_current = false
|
|
WHERE worker_id = v_worker_id
|
|
AND type_code = v_type_code
|
|
AND is_current = true;
|
|
|
|
INSERT INTO documents (
|
|
worker_id, type_code, original_name, mime, size_bytes, sha256, iv, storage_name,
|
|
is_current, parse_status, issued_at, expires_at, imss_company_id, imss_alta_at,
|
|
uploaded_by_id, uploaded_by_name
|
|
) VALUES (
|
|
v_worker_id, v_type_code, v_original_name, v_mime, v_size_bytes, v_sha256, v_iv, v_storage_name,
|
|
true, 'manual',
|
|
COALESCE(v_issued_at, v_movement_date),
|
|
v_expires_at,
|
|
v_imss_company_id,
|
|
v_movement_date,
|
|
v_uploaded_by_id,
|
|
v_uploaded_by_name
|
|
)
|
|
RETURNING id INTO v_doc_id;
|
|
|
|
IF v_type_code = 'alta_imss' THEN
|
|
v_imss_company_id := COALESCE(
|
|
v_imss_company_id,
|
|
(SELECT company_id FROM workers WHERE id = v_worker_id)
|
|
);
|
|
SELECT id, code INTO v_company FROM companies WHERE id = v_imss_company_id;
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_worker_document_store: empresa patrón no válida para alta IMSS',
|
|
jsonb_build_object('fn', 'fn_worker_document_store', 'worker_id', v_worker_id)
|
|
);
|
|
END IF;
|
|
UPDATE workers SET
|
|
imss_status = 'alta',
|
|
imss_company_id = v_company.id,
|
|
imss_alta_at = v_imss_alta_at,
|
|
imss_baja_at = NULL,
|
|
company_id = v_company.id,
|
|
hire_type = v_company.code,
|
|
updated_at = now()
|
|
WHERE id = v_worker_id;
|
|
END IF;
|
|
|
|
IF v_type_code = 'baja_imss' THEN
|
|
UPDATE workers SET
|
|
imss_status = 'baja_imss',
|
|
imss_baja_at = v_imss_baja_at,
|
|
imss_company_id = NULL,
|
|
company_id = NULL,
|
|
hire_type = '',
|
|
updated_at = now()
|
|
WHERE id = v_worker_id;
|
|
END IF;
|
|
|
|
IF (SELECT status FROM workers WHERE id = v_worker_id) = 'baja' THEN
|
|
UPDATE workers SET pipeline_status = 'baja' WHERE id = v_worker_id;
|
|
END IF;
|
|
|
|
RETURN core.rpc_created(
|
|
jsonb_build_object(
|
|
'id', v_doc_id,
|
|
'worker_id', v_worker_id,
|
|
'type_code', v_type_code,
|
|
'is_current', true,
|
|
'storage_name', v_storage_name
|
|
),
|
|
format('Metadatos del documento %s guardados para trabajador %s (versión actual)', v_type_code, v_worker_id),
|
|
jsonb_build_object('fn', 'fn_worker_document_store', 'id', v_doc_id, 'worker_id', v_worker_id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_worker_document_store', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-014d-fn-import-grants endDelimiter:; splitStatements:true
|
|
GRANT EXECUTE ON FUNCTION core.fn_workers_import_batch(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_worker_document_store(jsonb) TO panels_core_app;
|