mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 18:43:18 +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>
604 lines
21 KiB
PL/PgSQL
604 lines
21 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · RPC proyectos (obras)
|
|
|
|
--changeset panel:core-009a-fn-next-project-code splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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 splitStatements:false
|
|
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;
|