panels-origin/db/core/changesets/015-rpc-documents.sql
Cursor Agent b58791aea4
refactor(core): finish RPC migration for budget, excel import, and docs
- Replace remaining budget.ts prepare() with fn_budget_replace and fn_project_update
- Migrate excel.ts findExisting, document store, assign, and catalogs to RPC
- Add 015-rpc-documents.sql for project/company/worker document metadata
- Document RPC envelope conventions in db/README.md
- Add http_errors_test and front api-response composables

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

164 lines
7.4 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · RPC documentos (metadata post-S3)
--changeset panel:core-015a-fn-project-document-store endDelimiter:; splitStatements:true
CREATE OR REPLACE FUNCTION core.fn_project_document_store(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_project_id bigint := (payload->>'project_id')::bigint;
v_type text := btrim(payload->>'type_code');
v_doc_id bigint;
BEGIN
IF v_project_id IS NULL OR v_type IS NULL OR v_type = '' THEN
RETURN core.rpc_err('VALIDATION', 'fn_project_document_store: project_id y type_code son obligatorios',
jsonb_build_object('fn', 'fn_project_document_store'));
END IF;
IF NOT EXISTS (SELECT 1 FROM project_document_types WHERE code = v_type) THEN
RETURN core.rpc_err('VALIDATION',
format('fn_project_document_store: tipo de documento %s no válido para proyectos', v_type),
jsonb_build_object('fn', 'fn_project_document_store', 'type_code', v_type));
END IF;
UPDATE project_documents SET is_current = false WHERE project_id = v_project_id AND type_code = v_type;
INSERT INTO project_documents (
project_id, type_code, original_name, mime, size_bytes, sha256, iv, storage_name,
is_current, parse_status, uploaded_by_id, uploaded_by_name
) VALUES (
v_project_id, v_type, payload->>'original_name', payload->>'mime',
(payload->>'size_bytes')::integer, payload->>'sha256', payload->>'iv', payload->>'storage_name',
true, 'manual', NULLIF(payload->>'uploaded_by_id', '')::integer, coalesce(payload->>'uploaded_by_name', '')
) RETURNING id INTO v_doc_id;
RETURN core.rpc_ok(jsonb_build_object('id', v_doc_id),
format('Documento %s guardado para proyecto id=%s (doc id=%s)', v_type, v_project_id, v_doc_id),
jsonb_build_object('fn', 'fn_project_document_store', 'project_id', v_project_id, 'id', v_doc_id));
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_project_document_store', SQLSTATE, SQLERRM);
END;
$$;
--changeset panel:core-015b-fn-company-document-store endDelimiter:; splitStatements:true
CREATE OR REPLACE FUNCTION core.fn_company_document_store(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_company_id bigint := (payload->>'company_id')::bigint;
v_type text := btrim(payload->>'type_code');
v_doc_id bigint;
BEGIN
IF v_company_id IS NULL OR v_type IS NULL OR v_type = '' THEN
RETURN core.rpc_err('VALIDATION', 'fn_company_document_store: company_id y type_code son obligatorios',
jsonb_build_object('fn', 'fn_company_document_store'));
END IF;
IF NOT EXISTS (SELECT 1 FROM company_document_types WHERE code = v_type) THEN
RETURN core.rpc_err('VALIDATION',
format('fn_company_document_store: tipo de documento %s no válido para empresas', v_type),
jsonb_build_object('fn', 'fn_company_document_store', 'type_code', v_type));
END IF;
UPDATE company_documents SET is_current = false WHERE company_id = v_company_id AND type_code = v_type;
INSERT INTO company_documents (
company_id, type_code, original_name, mime, size_bytes, sha256, iv, storage_name,
is_current, parse_status, uploaded_by_id, uploaded_by_name
) VALUES (
v_company_id, v_type, payload->>'original_name', payload->>'mime',
(payload->>'size_bytes')::integer, payload->>'sha256', payload->>'iv', payload->>'storage_name',
true, 'manual', NULLIF(payload->>'uploaded_by_id', '')::integer, coalesce(payload->>'uploaded_by_name', '')
) RETURNING id INTO v_doc_id;
RETURN core.rpc_ok(jsonb_build_object('id', v_doc_id),
format('Documento %s guardado para empresa id=%s (doc id=%s)', v_type, v_company_id, v_doc_id),
jsonb_build_object('fn', 'fn_company_document_store', 'company_id', v_company_id, 'id', v_doc_id));
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_company_document_store', SQLSTATE, SQLERRM);
END;
$$;
--changeset panel:core-015c-fn-document-get endDelimiter:; splitStatements:true
CREATE OR REPLACE FUNCTION core.fn_project_document_get(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_project_id bigint := (payload->>'project_id')::bigint;
v_doc_id bigint := (payload->>'doc_id')::bigint;
v_doc jsonb;
BEGIN
SELECT to_jsonb(d) INTO v_doc FROM project_documents d
WHERE d.id = v_doc_id AND d.project_id = v_project_id;
IF v_doc IS NULL THEN
RETURN core.rpc_err('NOT_FOUND',
format('fn_project_document_get: documento id=%s no encontrado en proyecto %s', v_doc_id, v_project_id),
jsonb_build_object('fn', 'fn_project_document_get', 'doc_id', v_doc_id, 'project_id', v_project_id));
END IF;
RETURN core.rpc_ok(jsonb_build_object('document', v_doc),
format('Documento id=%s del proyecto %s obtenido', v_doc_id, v_project_id),
jsonb_build_object('fn', 'fn_project_document_get', 'doc_id', v_doc_id));
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_project_document_get', SQLSTATE, SQLERRM);
END;
$$;
CREATE OR REPLACE FUNCTION core.fn_company_document_get(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_company_id bigint := (payload->>'company_id')::bigint;
v_doc_id bigint := (payload->>'doc_id')::bigint;
v_doc jsonb;
BEGIN
SELECT to_jsonb(d) INTO v_doc FROM company_documents d
WHERE d.id = v_doc_id AND d.company_id = v_company_id;
IF v_doc IS NULL THEN
RETURN core.rpc_err('NOT_FOUND',
format('fn_company_document_get: documento id=%s no encontrado en empresa %s', v_doc_id, v_company_id),
jsonb_build_object('fn', 'fn_company_document_get', 'doc_id', v_doc_id, 'company_id', v_company_id));
END IF;
RETURN core.rpc_ok(jsonb_build_object('document', v_doc),
format('Documento id=%s de la empresa %s obtenido', v_doc_id, v_company_id),
jsonb_build_object('fn', 'fn_company_document_get', 'doc_id', v_doc_id));
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_company_document_get', SQLSTATE, SQLERRM);
END;
$$;
CREATE OR REPLACE FUNCTION core.fn_worker_document_get(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_worker_id bigint := (payload->>'worker_id')::bigint;
v_doc_id bigint := (payload->>'doc_id')::bigint;
v_doc jsonb;
BEGIN
SELECT to_jsonb(d) INTO v_doc FROM documents d
WHERE d.id = v_doc_id AND d.worker_id = v_worker_id;
IF v_doc IS NULL THEN
RETURN core.rpc_err('NOT_FOUND',
format('fn_worker_document_get: documento id=%s no encontrado para trabajador %s', v_doc_id, v_worker_id),
jsonb_build_object('fn', 'fn_worker_document_get', 'doc_id', v_doc_id, 'worker_id', v_worker_id));
END IF;
RETURN core.rpc_ok(jsonb_build_object('document', v_doc),
format('Documento id=%s del trabajador %s obtenido', v_doc_id, v_worker_id),
jsonb_build_object('fn', 'fn_worker_document_get', 'doc_id', v_doc_id));
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_worker_document_get', SQLSTATE, SQLERRM);
END;
$$;
--changeset panel:core-015d-fn-documents-grants endDelimiter:; splitStatements:true
GRANT EXECUTE ON FUNCTION core.fn_project_document_store(jsonb) TO panels_core_app;
GRANT EXECUTE ON FUNCTION core.fn_company_document_store(jsonb) TO panels_core_app;
GRANT EXECUTE ON FUNCTION core.fn_project_document_get(jsonb) TO panels_core_app;
GRANT EXECUTE ON FUNCTION core.fn_company_document_get(jsonb) TO panels_core_app;
GRANT EXECUTE ON FUNCTION core.fn_worker_document_get(jsonb) TO panels_core_app;