--liquibase formatted sql -- PANELS · core · RPC empresas (CRUD) --changeset panel:core-008a-company-helpers endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core._company_trim_text(p_value text) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT btrim(COALESCE(p_value, '')); $$; CREATE OR REPLACE FUNCTION core._company_norm_code(p_value text) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT upper(regexp_replace(btrim(COALESCE(p_value, '')), '\s+', '', 'g')); $$; CREATE OR REPLACE FUNCTION core._company_norm_rfc(p_value text) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT upper(regexp_replace(btrim(COALESCE(p_value, '')), '\s+', '', 'g')); $$; CREATE OR REPLACE FUNCTION core._company_code_from_name(p_name text) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT COALESCE( NULLIF( left( upper( regexp_replace( translate( COALESCE(p_name, ''), 'áàäâãåéèëêíìïîóòöôõúùüûñÁÀÄÂÃÅÉÈËÊÍÌÏÎÓÒÖÔÕÚÙÜÛÑ', 'aaaaaaeeeeiiiiooooouuuunAAAAAAEEEEIIIIOOOOOUUUUN' ), '[^A-Z0-9]+', '', 'g' ) ), 12 ), '' ), 'EMP' ); $$; CREATE OR REPLACE FUNCTION core._company_validate_code(p_code text) RETURNS text LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN IF p_code IS NULL OR p_code = '' THEN RETURN 'Código de 2 a 16 caracteres (letras, números, _ o -)'; END IF; IF p_code !~ '^[A-Z0-9][A-Z0-9_-]{1,15}$' THEN RETURN 'Código de 2 a 16 caracteres (letras, números, _ o -)'; END IF; RETURN NULL; END; $$; CREATE OR REPLACE FUNCTION core._company_validate_profile( p_payload jsonb, p_require_legal boolean DEFAULT false ) RETURNS text LANGUAGE plpgsql IMMUTABLE AS $$ DECLARE v_rfc text := core._company_norm_rfc(p_payload->>'rfc'); v_clase text := upper(core._company_trim_text(p_payload->>'clase_riesgo')); BEGIN IF v_rfc <> '' AND v_rfc !~ '^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$' THEN RETURN 'RFC inválido (formato mexicano de 12 o 13 caracteres)'; END IF; IF p_require_legal THEN IF core._company_trim_text(p_payload->>'razon_social') = '' AND core._company_trim_text(p_payload->>'name') = '' AND core._company_trim_text(p_payload->>'nombre_comercial') = '' THEN RETURN 'Indique razón social o nombre comercial'; END IF; END IF; IF v_clase <> '' AND v_clase NOT IN ('I', 'II', 'III', 'IV', 'V') THEN RETURN 'Clase de riesgo IMSS debe ser I, II, III, IV o V'; END IF; RETURN NULL; END; $$; CREATE OR REPLACE FUNCTION core._company_normalize_profile( p_payload jsonb, p_fallback_name text DEFAULT '' ) RETURNS jsonb LANGUAGE plpgsql IMMUTABLE AS $$ DECLARE v_nombre_comercial text; v_razon_social text; v_name text; BEGIN v_nombre_comercial := COALESCE( NULLIF(core._company_trim_text(p_payload->>'nombre_comercial'), ''), NULLIF(core._company_trim_text(p_payload->>'name'), ''), p_fallback_name ); v_razon_social := COALESCE( NULLIF(core._company_trim_text(p_payload->>'razon_social'), ''), NULLIF(v_nombre_comercial, ''), p_fallback_name ); v_name := COALESCE( NULLIF(core._company_trim_text(p_payload->>'name'), ''), NULLIF(v_nombre_comercial, ''), NULLIF(v_razon_social, ''), p_fallback_name ); RETURN jsonb_build_object( 'name', v_name, 'nombre_comercial', v_nombre_comercial, 'razon_social', v_razon_social, 'rfc', core._company_norm_rfc(p_payload->>'rfc'), 'regimen_fiscal', core._company_trim_text(p_payload->>'regimen_fiscal'), 'registro_patronal', upper(core._company_trim_text(p_payload->>'registro_patronal')), 'clase_riesgo', upper(core._company_trim_text(p_payload->>'clase_riesgo')), 'domicilio_fiscal', core._company_trim_text(p_payload->>'domicilio_fiscal'), 'codigo_postal', core._company_trim_text(p_payload->>'codigo_postal'), 'ciudad', core._company_trim_text(p_payload->>'ciudad'), 'estado', core._company_trim_text(p_payload->>'estado'), 'telefono', core._company_trim_text(p_payload->>'telefono'), 'email', lower(core._company_trim_text(p_payload->>'email')), 'representante_legal', core._company_trim_text(p_payload->>'representante_legal'), 'giro', core._company_trim_text(p_payload->>'giro') ); END; $$; --changeset panel:core-008b-fn-company-list endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_company_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := NULLIF(payload->>'tenant_id', '')::integer; v_companies jsonb; v_count integer; BEGIN SELECT COALESCE( jsonb_agg(row_to_json(x)::jsonb ORDER BY CASE x.kind WHEN 'principal' THEN 0 ELSE 1 END, lower(x.name)), '[]'::jsonb ), COUNT(*)::integer INTO v_companies, v_count FROM ( SELECT c.*, p.code AS parent_code, p.name AS parent_name, (SELECT COUNT(*) FROM workers w WHERE w.company_id = c.id) AS worker_count FROM companies c LEFT JOIN companies p ON p.id = c.parent_id WHERE v_tid IS NULL OR c.tenant_id = v_tid ) x; RETURN core.rpc_ok( jsonb_build_object('companies', v_companies), format('Listado de empresas: %s registro(s)%s', v_count, CASE WHEN v_tid IS NOT NULL THEN format(' para tenant_id=%s', v_tid) ELSE '' END), jsonb_build_object('fn', 'fn_company_list', 'tenant_id', v_tid, 'count', v_count) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_company_list', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-008c-fn-company-get endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_company_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_id bigint := NULLIF(payload->>'id', '')::bigint; v_tid integer := NULLIF(payload->>'tenant_id', '')::integer; v_company jsonb; BEGIN IF v_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_company_get: id es obligatorio para consultar una empresa', jsonb_build_object('fn', 'fn_company_get', 'field', 'id') ); END IF; SELECT to_jsonb(c) INTO v_company FROM companies c WHERE c.id = v_id AND (v_tid IS NULL OR c.tenant_id IS NULL OR c.tenant_id = v_tid); IF v_company IS NULL THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_company_get: empresa id=%s no encontrada%s', v_id, CASE WHEN v_tid IS NOT NULL THEN format(' en tenant_id=%s', v_tid) ELSE '' END), jsonb_build_object('fn', 'fn_company_get', 'id', v_id, 'tenant_id', v_tid) ); END IF; RETURN core.rpc_ok( jsonb_build_object('company', v_company), format('Empresa id=%s cargada (%s)', v_id, v_company->>'code'), jsonb_build_object('fn', 'fn_company_get', 'id', v_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_company_get', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-008d-fn-company-get-by-code endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_company_get_by_code(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_code text := core._company_norm_code(payload->>'code'); v_company jsonb; BEGIN IF v_code = '' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_company_get_by_code: code es obligatorio para consultar una empresa', jsonb_build_object('fn', 'fn_company_get_by_code', 'field', 'code') ); END IF; SELECT to_jsonb(c) INTO v_company FROM companies c WHERE c.code = v_code; IF v_company IS NULL THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_company_get_by_code: empresa con código %s no encontrada', v_code), jsonb_build_object('fn', 'fn_company_get_by_code', 'code', v_code) ); END IF; RETURN core.rpc_ok( jsonb_build_object('company', v_company), format('Empresa con código %s cargada (id=%s)', v_code, v_company->>'id'), jsonb_build_object('fn', 'fn_company_get_by_code', 'code', v_code) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_company_get_by_code', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-008e-fn-next-company-code endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_next_company_code(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_name text := core._company_trim_text(payload->>'name'); v_base text; v_n integer; v_code text; BEGIN IF v_name = '' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_next_company_code: name es obligatorio para generar un código de empresa', jsonb_build_object('fn', 'fn_next_company_code', 'field', 'name') ); END IF; v_base := core._company_code_from_name(v_name); IF NOT EXISTS (SELECT 1 FROM companies WHERE code = v_base) THEN v_code := v_base; ELSE SELECT COALESCE(MAX(substring(code FROM 5)::integer), 0) + 1 INTO v_n FROM companies WHERE code ~ '^EMP-[0-9]{4}'; v_code := 'EMP-' || lpad(v_n::text, 4, '0'); END IF; RETURN core.rpc_ok( jsonb_build_object('code', v_code), format('Código sugerido para "%s": %s', v_name, v_code), jsonb_build_object('fn', 'fn_next_company_code', 'name', v_name, 'code', v_code) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_next_company_code', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-008f-fn-company-create endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_company_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_tid integer := NULLIF(payload->>'tenant_id', '')::integer; v_profile_err text; v_profile jsonb; v_principal companies%ROWTYPE; v_parent_id bigint; v_parent companies%ROWTYPE; v_code text; v_code_err text; v_new_id bigint; v_n integer; v_company jsonb; BEGIN v_profile_err := core._company_validate_profile(payload, true); IF v_profile_err IS NOT NULL THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_create: %s', v_profile_err), jsonb_build_object('fn', 'fn_company_create', 'field', 'profile') ); END IF; v_profile := core._company_normalize_profile(payload, ''); IF core._company_trim_text(v_profile->>'name') = '' THEN RETURN core.rpc_err( 'VALIDATION', 'fn_company_create: nombre de empresa obligatorio', jsonb_build_object('fn', 'fn_company_create', 'field', 'name') ); END IF; IF v_tid IS NOT NULL THEN SELECT * INTO v_principal FROM companies WHERE kind = 'principal' AND tenant_id = v_tid ORDER BY id LIMIT 1; ELSE SELECT * INTO v_principal FROM companies WHERE kind = 'principal' ORDER BY id LIMIT 1; END IF; IF NOT FOUND THEN RETURN core.rpc_err( 'VALIDATION', 'fn_company_create: no hay empresa principal configurada en el tenant', jsonb_build_object('fn', 'fn_company_create', 'tenant_id', v_tid) ); END IF; v_parent_id := COALESCE(NULLIF(payload->>'parent_id', '')::bigint, v_principal.id); SELECT * INTO v_parent FROM companies WHERE id = v_parent_id; IF NOT FOUND THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_create: empresa padre id=%s no encontrada', v_parent_id), jsonb_build_object('fn', 'fn_company_create', 'parent_id', v_parent_id) ); END IF; IF v_tid IS NOT NULL AND v_parent.tenant_id IS NOT NULL AND v_parent.tenant_id <> v_tid THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_create: empresa padre id=%s pertenece a tenant_id=%s, distinto del solicitado (%s)', v_parent_id, v_parent.tenant_id, v_tid), jsonb_build_object('fn', 'fn_company_create', 'parent_id', v_parent_id, 'tenant_id', v_tid) ); END IF; v_code := core._company_norm_code(payload->>'code'); IF v_code = '' THEN v_code := core._company_code_from_name(v_profile->>'name'); IF EXISTS (SELECT 1 FROM companies WHERE code = v_code) THEN SELECT COALESCE(MAX(substring(code FROM 5)::integer), 0) + 1 INTO v_n FROM companies WHERE code ~ '^EMP-[0-9]{4}'; v_code := 'EMP-' || lpad(v_n::text, 4, '0'); END IF; END IF; v_code_err := core._company_validate_code(v_code); IF v_code_err IS NOT NULL THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_create: %s (código=%s)', v_code_err, v_code), jsonb_build_object('fn', 'fn_company_create', 'field', 'code', 'code', v_code) ); END IF; IF EXISTS (SELECT 1 FROM companies WHERE code = v_code) THEN RETURN core.rpc_err( 'CONFLICT', format('fn_company_create: ya existe una empresa con el código %s', v_code), jsonb_build_object('fn', 'fn_company_create', 'code', v_code) ); END IF; INSERT INTO companies ( code, name, parent_id, kind, status, tenant_id, registro_patronal, razon_social, nombre_comercial, rfc, regimen_fiscal, clase_riesgo, domicilio_fiscal, codigo_postal, ciudad, estado, telefono, email, representante_legal, giro ) VALUES ( v_code, v_profile->>'name', v_parent.id, 'sub', 'activo', COALESCE(v_tid, v_principal.tenant_id), v_profile->>'registro_patronal', v_profile->>'razon_social', v_profile->>'nombre_comercial', v_profile->>'rfc', v_profile->>'regimen_fiscal', v_profile->>'clase_riesgo', v_profile->>'domicilio_fiscal', v_profile->>'codigo_postal', v_profile->>'ciudad', v_profile->>'estado', v_profile->>'telefono', v_profile->>'email', v_profile->>'representante_legal', v_profile->>'giro' ) RETURNING id INTO v_new_id; SELECT to_jsonb(c) INTO v_company FROM companies c WHERE c.id = v_new_id; RETURN core.rpc_created( jsonb_build_object('company', v_company), format('Subempresa creada: %s (%s), id=%s', v_company->>'name', v_code, v_new_id), jsonb_build_object('fn', 'fn_company_create', 'id', v_new_id, 'code', v_code, 'parent_id', v_parent.id) ); EXCEPTION WHEN unique_violation THEN RETURN core.rpc_err( 'CONFLICT', format('fn_company_create: ya existe una empresa con el código %s', v_code), jsonb_build_object('fn', 'fn_company_create', 'code', v_code) ); WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_company_create', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-008g-fn-company-update endDelimiter:; splitStatements:true CREATE OR REPLACE FUNCTION core.fn_company_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 := NULLIF(payload->>'tenant_id', '')::integer; v_current companies%ROWTYPE; v_merged jsonb; v_profile_err text; v_profile jsonb; v_code text; v_code_err text; v_status text; v_clash_id bigint; v_company jsonb; BEGIN IF v_id IS NULL THEN RETURN core.rpc_err( 'VALIDATION', 'fn_company_update: id es obligatorio para actualizar una empresa', jsonb_build_object('fn', 'fn_company_update', 'field', 'id') ); END IF; SELECT * INTO v_current FROM companies WHERE id = v_id; IF NOT FOUND THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_company_update: empresa id=%s no encontrada', v_id), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'tenant_id', v_tid) ); END IF; IF v_tid IS NOT NULL AND v_current.tenant_id IS NOT NULL AND v_current.tenant_id <> v_tid THEN RETURN core.rpc_err( 'NOT_FOUND', format('fn_company_update: empresa id=%s no encontrada en tenant_id=%s', v_id, v_tid), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'tenant_id', v_tid) ); END IF; v_merged := jsonb_build_object( 'name', CASE WHEN payload ? 'name' THEN payload->>'name' ELSE v_current.name END, 'code', CASE WHEN payload ? 'code' THEN payload->>'code' ELSE v_current.code END, 'status', CASE WHEN payload ? 'status' THEN payload->>'status' ELSE v_current.status END, 'registro_patronal', CASE WHEN payload ? 'registro_patronal' THEN payload->>'registro_patronal' ELSE v_current.registro_patronal END, 'razon_social', CASE WHEN payload ? 'razon_social' THEN payload->>'razon_social' ELSE v_current.razon_social END, 'nombre_comercial', CASE WHEN payload ? 'nombre_comercial' THEN payload->>'nombre_comercial' ELSE v_current.nombre_comercial END, 'rfc', CASE WHEN payload ? 'rfc' THEN payload->>'rfc' ELSE v_current.rfc END, 'regimen_fiscal', CASE WHEN payload ? 'regimen_fiscal' THEN payload->>'regimen_fiscal' ELSE v_current.regimen_fiscal END, 'clase_riesgo', CASE WHEN payload ? 'clase_riesgo' THEN payload->>'clase_riesgo' ELSE v_current.clase_riesgo END, 'domicilio_fiscal', CASE WHEN payload ? 'domicilio_fiscal' THEN payload->>'domicilio_fiscal' ELSE v_current.domicilio_fiscal END, 'codigo_postal', CASE WHEN payload ? 'codigo_postal' THEN payload->>'codigo_postal' ELSE v_current.codigo_postal END, 'ciudad', CASE WHEN payload ? 'ciudad' THEN payload->>'ciudad' ELSE v_current.ciudad END, 'estado', CASE WHEN payload ? 'estado' THEN payload->>'estado' ELSE v_current.estado END, 'telefono', CASE WHEN payload ? 'telefono' THEN payload->>'telefono' ELSE v_current.telefono END, 'email', CASE WHEN payload ? 'email' THEN payload->>'email' ELSE v_current.email END, 'representante_legal', CASE WHEN payload ? 'representante_legal' THEN payload->>'representante_legal' ELSE v_current.representante_legal END, 'giro', CASE WHEN payload ? 'giro' THEN payload->>'giro' ELSE v_current.giro END ); v_profile_err := core._company_validate_profile(v_merged, false); IF v_profile_err IS NOT NULL THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_update: %s (id=%s)', v_profile_err, v_id), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'field', 'profile') ); END IF; v_profile := core._company_normalize_profile(v_merged, v_current.name); IF core._company_trim_text(v_profile->>'name') = '' THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_update: nombre de empresa obligatorio (id=%s)', v_id), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'field', 'name') ); END IF; v_code := v_current.code; IF payload ? 'code' THEN v_code := core._company_norm_code(payload->>'code'); v_code_err := core._company_validate_code(v_code); IF v_code_err IS NOT NULL THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_update: %s (id=%s, código=%s)', v_code_err, v_id, v_code), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'field', 'code', 'code', v_code) ); END IF; SELECT id INTO v_clash_id FROM companies WHERE code = v_code AND id <> v_id; IF FOUND THEN RETURN core.rpc_err( 'CONFLICT', format('fn_company_update: ya existe otra empresa con el código %s (id=%s)', v_code, v_clash_id), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'code', v_code, 'conflict_id', v_clash_id) ); END IF; END IF; v_status := v_current.status; IF payload ? 'status' THEN IF (payload->>'status') NOT IN ('activo', 'inactivo') THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_update: estatus debe ser activo o inactivo (recibido=%s, id=%s)', payload->>'status', v_id), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'field', 'status', 'status', payload->>'status') ); END IF; IF v_current.kind = 'principal' AND payload->>'status' = 'inactivo' THEN RETURN core.rpc_err( 'VALIDATION', format('fn_company_update: la empresa principal id=%s no se puede inactivar', v_id), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'kind', v_current.kind) ); END IF; v_status := payload->>'status'; END IF; UPDATE companies SET name = v_profile->>'name', code = v_code, status = v_status, registro_patronal = v_profile->>'registro_patronal', razon_social = v_profile->>'razon_social', nombre_comercial = v_profile->>'nombre_comercial', rfc = v_profile->>'rfc', regimen_fiscal = v_profile->>'regimen_fiscal', clase_riesgo = v_profile->>'clase_riesgo', domicilio_fiscal = v_profile->>'domicilio_fiscal', codigo_postal = v_profile->>'codigo_postal', ciudad = v_profile->>'ciudad', estado = v_profile->>'estado', telefono = v_profile->>'telefono', email = v_profile->>'email', representante_legal = v_profile->>'representante_legal', giro = v_profile->>'giro' WHERE id = v_id; IF v_code <> v_current.code THEN UPDATE workers SET hire_type = v_code WHERE company_id = v_id; END IF; SELECT to_jsonb(c) INTO v_company FROM companies c WHERE c.id = v_id; RETURN core.rpc_ok( jsonb_build_object('company', v_company), format('Empresa id=%s actualizada (%s)', v_id, v_code), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'code', v_code) ); EXCEPTION WHEN unique_violation THEN RETURN core.rpc_err( 'CONFLICT', format('fn_company_update: ya existe una empresa con el código %s (id=%s)', v_code, v_id), jsonb_build_object('fn', 'fn_company_update', 'id', v_id, 'code', v_code) ); WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_company_update', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-008h-fn-companies-grants endDelimiter:; splitStatements:true GRANT EXECUTE ON FUNCTION core._company_trim_text(text) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._company_norm_code(text) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._company_norm_rfc(text) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._company_code_from_name(text) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._company_validate_code(text) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._company_validate_profile(jsonb, boolean) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._company_normalize_profile(jsonb, text) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_company_list(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_company_get(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_company_get_by_code(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_next_company_code(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_company_create(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_company_update(jsonb) TO panels_core_app;