From a73090bd11ada4b6ead8b027dff84fc9f5904440 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 23:48:40 +0000 Subject: [PATCH] Add PostgreSQL RPC functions for projects domain (009-rpc-projects) Co-authored-by: alberto.martinez --- db/core/changesets/009-rpc-projects.sql | 604 ++++++++++++++++++++++++ 1 file changed, 604 insertions(+) create mode 100644 db/core/changesets/009-rpc-projects.sql diff --git a/db/core/changesets/009-rpc-projects.sql b/db/core/changesets/009-rpc-projects.sql new file mode 100644 index 0000000..d24ef02 --- /dev/null +++ b/db/core/changesets/009-rpc-projects.sql @@ -0,0 +1,604 @@ +--liquibase formatted sql +-- PANELS · core · RPC proyectos (obras) + +--changeset panel:core-009a-fn-next-project-code endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_next_project_code(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_n integer; + v_code text; +BEGIN + SELECT COALESCE(MAX(substring(code FROM 5)::integer), 0) + 1 + INTO v_n + FROM projects + WHERE code ~ '^PRY-[0-9]{4}' OR code ~ '^OBR-[0-9]{4}'; + v_code := 'PRY-' || lpad(v_n::text, 4, '0'); + RETURN core.rpc_ok( + jsonb_build_object('code', v_code), + format('Siguiente código de proyecto asignado: %s', v_code), + jsonb_build_object('fn', 'fn_next_project_code', 'sequence', v_n) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_next_project_code', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009b-fn-project-assert-status endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_project_assert_status(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_id bigint := (payload->>'project_id')::bigint; + v_allowed text[]; + v_proj record; +BEGIN + IF v_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_assert_status: project_id es obligatorio', + jsonb_build_object('fn', 'fn_project_assert_status', 'field', 'project_id') + ); + END IF; + IF payload->'allowed_statuses' IS NULL OR jsonb_typeof(payload->'allowed_statuses') <> 'array' THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_assert_status: allowed_statuses debe ser un arreglo JSON', + jsonb_build_object('fn', 'fn_project_assert_status', 'field', 'allowed_statuses') + ); + END IF; + SELECT array_agg(btrim(elem)) + INTO v_allowed + FROM jsonb_array_elements_text(payload->'allowed_statuses') AS elem + WHERE btrim(elem) IN ('activo', 'pausado', 'concluido', 'cancelado'); + IF v_allowed IS NULL OR cardinality(v_allowed) = 0 THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_assert_status: allowed_statuses no contiene estados válidos', + jsonb_build_object('fn', 'fn_project_assert_status', 'field', 'allowed_statuses') + ); + END IF; + SELECT p.id, p.code, p.name, p.status + INTO v_proj + FROM projects p + WHERE p.id = v_id; + IF NOT FOUND THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_project_assert_status: proyecto id=%s no encontrado', v_id), + jsonb_build_object('fn', 'fn_project_assert_status', 'project_id', v_id) + ); + END IF; + IF NOT (v_proj.status = ANY (v_allowed)) THEN + RETURN core.rpc_err( + 'VALIDATION', + format('fn_project_assert_status: proyecto %s (%s) está en estado %s, no permitido para esta operación', + v_proj.code, v_id, v_proj.status), + jsonb_build_object( + 'fn', 'fn_project_assert_status', + 'project_id', v_id, + 'status', v_proj.status, + 'allowed_statuses', to_jsonb(v_allowed) + ) + ); + END IF; + RETURN core.rpc_ok( + jsonb_build_object( + 'project_id', v_proj.id, + 'code', v_proj.code, + 'name', v_proj.name, + 'status', v_proj.status + ), + format('Proyecto %s en estado %s válido para la operación', v_proj.code, v_proj.status), + jsonb_build_object('fn', 'fn_project_assert_status', 'project_id', v_id) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_project_assert_status', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009c-fn-project-list endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_project_list(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_status_raw text := nullif(btrim(payload->>'status'), ''); + v_statuses text[]; + v_projects jsonb; +BEGIN + IF v_status_raw IS NOT NULL THEN + SELECT array_agg(DISTINCT btrim(s)) + INTO v_statuses + FROM unnest(string_to_array(v_status_raw, ',')) AS s + WHERE btrim(s) IN ('activo', 'pausado', 'concluido', 'cancelado'); + IF v_statuses IS NULL OR cardinality(v_statuses) = 0 THEN + RETURN core.rpc_err( + 'VALIDATION', + format('fn_project_list: ningún estado válido en status=%s', v_status_raw), + jsonb_build_object('fn', 'fn_project_list', 'field', 'status', 'value', v_status_raw) + ); + END IF; + END IF; + SELECT COALESCE(jsonb_agg(row_to_json(x)::jsonb ORDER BY x.sort_key, x.id), '[]'::jsonb) + INTO v_projects + FROM ( + SELECT + p.*, + c.name AS company_name, + c.code AS company_code, + (SELECT COUNT(*) FROM assignments a WHERE a.project_id = p.id AND a.active = true) AS active_count, + (SELECT COUNT(*) FROM project_document_types t + WHERE t.required = true AND NOT EXISTS ( + SELECT 1 FROM project_documents d + WHERE d.project_id = p.id AND d.type_code = t.code AND d.is_current = true + )) AS missing_docs, + (SELECT COUNT(*) FROM budget_items b WHERE b.project_id = p.id) AS budget_count, + CASE p.status + WHEN 'activo' THEN 0 + WHEN 'pausado' THEN 1 + WHEN 'concluido' THEN 2 + ELSE 3 + END AS sort_key + FROM projects p + LEFT JOIN companies c ON c.id = p.company_id + WHERE v_statuses IS NULL OR p.status = ANY (v_statuses) + ) x; + RETURN core.rpc_ok( + jsonb_build_object('projects', v_projects), + format('Listado de proyectos: %s registro(s)', jsonb_array_length(v_projects)), + jsonb_build_object('fn', 'fn_project_list', 'count', jsonb_array_length(v_projects)) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_project_list', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009d-fn-project-get endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_project_get(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_id bigint := (payload->>'id')::bigint; + v_project jsonb; +BEGIN + IF v_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_get: id es obligatorio', + jsonb_build_object('fn', 'fn_project_get', 'field', 'id') + ); + END IF; + SELECT row_to_json(x)::jsonb + INTO v_project + FROM ( + SELECT + p.*, + c.name AS company_name, + c.code AS company_code, + (SELECT COUNT(*) FROM assignments a WHERE a.project_id = p.id AND a.active = true) AS active_count + FROM projects p + LEFT JOIN companies c ON c.id = p.company_id + WHERE p.id = v_id + ) x; + IF v_project IS NULL THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_project_get: proyecto id=%s no encontrado', v_id), + jsonb_build_object('fn', 'fn_project_get', 'id', v_id) + ); + END IF; + RETURN core.rpc_ok( + jsonb_build_object('project', v_project), + format('Proyecto %s cargado', v_project->>'code'), + jsonb_build_object('fn', 'fn_project_get', 'id', v_id) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_project_get', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009e-fn-project-create endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_project_create(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_tid integer := COALESCE( + (payload->>'tenant_id')::integer, + NULLIF(current_setting('app.tenant_id', true), '')::integer + ); + v_name text := nullif(btrim(payload->>'name'), ''); + v_address text := coalesce(nullif(btrim(payload->>'address'), ''), ''); + v_theme_id text := coalesce(nullif(btrim(payload->>'theme_id'), ''), 'arctec-dos-logos-fold'); + v_status text := coalesce(nullif(btrim(payload->>'status'), ''), 'activo'); + v_company_id integer := (payload->>'company_id')::integer; + v_contract_amount numeric(14,2) := (payload->>'contract_amount')::numeric(14,2); + v_start_date date := nullif(btrim(payload->>'start_date'), '')::date; + v_end_date date := nullif(btrim(payload->>'end_date'), '')::date; + v_resident_name text := coalesce(nullif(btrim(payload->>'resident_name'), ''), ''); + v_siroc text := upper(coalesce(nullif(btrim(payload->>'siroc'), ''), '')); + v_payroll_tax_pct numeric(5,2) := coalesce((payload->>'payroll_tax_pct')::numeric(5,2), 4); + v_code_env jsonb; + v_code text; + v_row projects%ROWTYPE; +BEGIN + IF v_name IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_create: nombre de proyecto obligatorio', + jsonb_build_object('fn', 'fn_project_create', 'field', 'name') + ); + END IF; + IF v_company_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_create: company_id es obligatorio', + jsonb_build_object('fn', 'fn_project_create', 'field', 'company_id') + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_company_id) THEN + RETURN core.rpc_err( + 'VALIDATION', + format('fn_project_create: empresa id=%s no encontrada', v_company_id), + jsonb_build_object('fn', 'fn_project_create', 'company_id', v_company_id) + ); + END IF; + IF v_status NOT IN ('activo', 'pausado', 'concluido', 'cancelado') THEN + RETURN core.rpc_err( + 'VALIDATION', + format('fn_project_create: estado inválido %s', v_status), + jsonb_build_object('fn', 'fn_project_create', 'field', 'status', 'value', v_status) + ); + END IF; + IF v_start_date IS NOT NULL AND v_end_date IS NOT NULL AND v_end_date < v_start_date THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_create: la fecha de término no puede ser anterior al inicio', + jsonb_build_object('fn', 'fn_project_create', 'field', 'end_date') + ); + END IF; + v_code_env := core.fn_next_project_code('{}'::jsonb); + IF NOT COALESCE((v_code_env->>'ok')::boolean, false) THEN + RETURN v_code_env; + END IF; + v_code := v_code_env->'data'->>'code'; + INSERT INTO projects ( + code, name, address, theme_id, status, company_id, contract_amount, + start_date, end_date, resident_name, siroc, payroll_tax_pct, tenant_id + ) VALUES ( + v_code, v_name, v_address, v_theme_id, v_status, v_company_id, v_contract_amount, + v_start_date, v_end_date, v_resident_name, v_siroc, v_payroll_tax_pct, v_tid + ) + RETURNING * INTO v_row; + RETURN core.rpc_created( + jsonb_build_object('id', v_row.id, 'code', v_row.code, 'status', v_row.status), + format('Proyecto %s (%s) creado', v_row.code, v_row.name), + jsonb_build_object('fn', 'fn_project_create', 'id', v_row.id, 'code', v_row.code) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_project_create', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009f-fn-project-update endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_project_update(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_id bigint := (payload->>'id')::bigint; + v_cur projects%ROWTYPE; + v_name text; + v_address text; + v_theme_id text; + v_status text; + v_company_id integer; + v_contract_amount numeric(14,2); + v_start_date date; + v_end_date date; + v_resident_name text; + v_siroc text; + v_payroll_tax_pct numeric(5,2); +BEGIN + IF v_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_update: id es obligatorio', + jsonb_build_object('fn', 'fn_project_update', 'field', 'id') + ); + END IF; + SELECT * INTO v_cur FROM projects WHERE id = v_id; + IF NOT FOUND THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_project_update: proyecto id=%s no encontrado', v_id), + jsonb_build_object('fn', 'fn_project_update', 'id', v_id) + ); + END IF; + v_name := coalesce(nullif(btrim(payload->>'name'), ''), v_cur.name); + v_address := CASE WHEN payload ? 'address' THEN coalesce(nullif(btrim(payload->>'address'), ''), '') ELSE v_cur.address END; + v_theme_id := coalesce(nullif(btrim(payload->>'theme_id'), ''), v_cur.theme_id); + v_status := CASE + WHEN payload ? 'status' AND btrim(payload->>'status') IN ('activo', 'pausado', 'concluido', 'cancelado') + THEN btrim(payload->>'status') + ELSE v_cur.status + END; + v_company_id := coalesce((payload->>'company_id')::integer, v_cur.company_id); + v_contract_amount := CASE + WHEN payload ? 'contract_amount' THEN (payload->>'contract_amount')::numeric(14,2) + ELSE v_cur.contract_amount + END; + v_start_date := CASE + WHEN payload ? 'start_date' THEN nullif(btrim(payload->>'start_date'), '')::date + ELSE v_cur.start_date + END; + v_end_date := CASE + WHEN payload ? 'end_date' THEN nullif(btrim(payload->>'end_date'), '')::date + ELSE v_cur.end_date + END; + v_resident_name := CASE + WHEN payload ? 'resident_name' THEN coalesce(nullif(btrim(payload->>'resident_name'), ''), '') + ELSE v_cur.resident_name + END; + v_siroc := CASE + WHEN payload ? 'siroc' THEN upper(coalesce(nullif(btrim(payload->>'siroc'), ''), '')) + ELSE v_cur.siroc + END; + v_payroll_tax_pct := coalesce((payload->>'payroll_tax_pct')::numeric(5,2), v_cur.payroll_tax_pct); + IF v_company_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_update: company_id es obligatorio', + jsonb_build_object('fn', 'fn_project_update', 'field', 'company_id', 'id', v_id) + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_company_id) THEN + RETURN core.rpc_err( + 'VALIDATION', + format('fn_project_update: empresa id=%s no encontrada', v_company_id), + jsonb_build_object('fn', 'fn_project_update', 'company_id', v_company_id, 'id', v_id) + ); + END IF; + IF v_start_date IS NOT NULL AND v_end_date IS NOT NULL AND v_end_date < v_start_date THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_update: la fecha de término no puede ser anterior al inicio', + jsonb_build_object('fn', 'fn_project_update', 'field', 'end_date', 'id', v_id) + ); + END IF; + UPDATE projects SET + name = v_name, + address = v_address, + theme_id = v_theme_id, + status = v_status, + company_id = v_company_id, + contract_amount = v_contract_amount, + start_date = v_start_date, + end_date = v_end_date, + resident_name = v_resident_name, + siroc = v_siroc, + payroll_tax_pct = v_payroll_tax_pct + WHERE id = v_id; + RETURN core.rpc_ok( + jsonb_build_object('id', v_id, 'code', v_cur.code, 'status', v_status), + format('Proyecto %s actualizado', v_cur.code), + jsonb_build_object('fn', 'fn_project_update', 'id', v_id, 'code', v_cur.code) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_project_update', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009g-fn-project-document-list endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_project_document_list(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_project_id bigint := (payload->>'project_id')::bigint; + v_documents jsonb; +BEGIN + IF v_project_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_document_list: project_id es obligatorio', + jsonb_build_object('fn', 'fn_project_document_list', 'field', 'project_id') + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM projects WHERE id = v_project_id) THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_project_document_list: proyecto id=%s no encontrado', v_project_id), + jsonb_build_object('fn', 'fn_project_document_list', 'project_id', v_project_id) + ); + END IF; + SELECT COALESCE(jsonb_agg(to_jsonb(d) ORDER BY d.uploaded_at DESC), '[]'::jsonb) + INTO v_documents + FROM ( + SELECT id, type_code, original_name, mime, size_bytes, is_current, uploaded_at + FROM project_documents + WHERE project_id = v_project_id + ) d; + RETURN core.rpc_ok( + jsonb_build_object('documents', v_documents), + format('Documentos del proyecto %s: %s archivo(s)', v_project_id, jsonb_array_length(v_documents)), + jsonb_build_object('fn', 'fn_project_document_list', 'project_id', v_project_id, 'count', jsonb_array_length(v_documents)) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_project_document_list', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009h-fn-company-document-list endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_company_document_list(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_company_id integer := (payload->>'company_id')::integer; + v_documents jsonb; +BEGIN + IF v_company_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_company_document_list: company_id es obligatorio', + jsonb_build_object('fn', 'fn_company_document_list', 'field', 'company_id') + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_company_id) THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_company_document_list: empresa id=%s no encontrada', v_company_id), + jsonb_build_object('fn', 'fn_company_document_list', 'company_id', v_company_id) + ); + END IF; + SELECT COALESCE(jsonb_agg(to_jsonb(d) ORDER BY d.uploaded_at DESC), '[]'::jsonb) + INTO v_documents + FROM ( + SELECT id, type_code, original_name, mime, size_bytes, is_current, uploaded_at + FROM company_documents + WHERE company_id = v_company_id + ) d; + RETURN core.rpc_ok( + jsonb_build_object('documents', v_documents), + format('Documentos de la empresa %s: %s archivo(s)', v_company_id, jsonb_array_length(v_documents)), + jsonb_build_object('fn', 'fn_company_document_list', 'company_id', v_company_id, 'count', jsonb_array_length(v_documents)) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_company_document_list', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009i-fn-project-checklist endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_project_checklist(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_project_id bigint := (payload->>'project_id')::bigint; + v_checklist jsonb; +BEGIN + IF v_project_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_project_checklist: project_id es obligatorio', + jsonb_build_object('fn', 'fn_project_checklist', 'field', 'project_id') + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM projects WHERE id = v_project_id) THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_project_checklist: proyecto id=%s no encontrado', v_project_id), + jsonb_build_object('fn', 'fn_project_checklist', 'project_id', v_project_id) + ); + END IF; + SELECT COALESCE(jsonb_agg( + jsonb_build_object( + 'code', t.code, + 'label', t.label, + 'required', t.required, + 'category', coalesce(t.category, 'otro'), + 'present', EXISTS ( + SELECT 1 FROM project_documents d + WHERE d.project_id = v_project_id + AND d.type_code = t.code + AND d.is_current = true + ) + ) ORDER BY t.code + ), '[]'::jsonb) + INTO v_checklist + FROM project_document_types t; + RETURN core.rpc_ok( + jsonb_build_object('checklist', v_checklist), + format('Checklist documental del proyecto %s: %s tipo(s)', v_project_id, jsonb_array_length(v_checklist)), + jsonb_build_object('fn', 'fn_project_checklist', 'project_id', v_project_id, 'count', jsonb_array_length(v_checklist)) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_project_checklist', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009j-fn-company-checklist endDelimiter:; splitStatements:true +CREATE OR REPLACE FUNCTION core.fn_company_checklist(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_company_id integer := (payload->>'company_id')::integer; + v_checklist jsonb; +BEGIN + IF v_company_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_company_checklist: company_id es obligatorio', + jsonb_build_object('fn', 'fn_company_checklist', 'field', 'company_id') + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_company_id) THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_company_checklist: empresa id=%s no encontrada', v_company_id), + jsonb_build_object('fn', 'fn_company_checklist', 'company_id', v_company_id) + ); + END IF; + SELECT COALESCE(jsonb_agg( + jsonb_build_object( + 'code', t.code, + 'label', t.label, + 'required', t.required, + 'category', coalesce(t.category, 'otro'), + 'present', EXISTS ( + SELECT 1 FROM company_documents d + WHERE d.company_id = v_company_id + AND d.type_code = t.code + AND d.is_current = true + ) + ) ORDER BY t.code + ), '[]'::jsonb) + INTO v_checklist + FROM company_document_types t; + RETURN core.rpc_ok( + jsonb_build_object('checklist', v_checklist), + format('Checklist documental de la empresa %s: %s tipo(s)', v_company_id, jsonb_array_length(v_checklist)), + jsonb_build_object('fn', 'fn_company_checklist', 'company_id', v_company_id, 'count', jsonb_array_length(v_checklist)) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_company_checklist', SQLSTATE, SQLERRM); +END; +$$; + +--changeset panel:core-009k-fn-projects-grants endDelimiter:; splitStatements:true +GRANT EXECUTE ON FUNCTION core.fn_next_project_code(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_project_assert_status(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_project_list(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_project_get(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_project_create(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_project_update(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_project_document_list(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_company_document_list(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_project_checklist(jsonb) TO panels_core_app; +GRANT EXECUTE ON FUNCTION core.fn_company_checklist(jsonb) TO panels_core_app;