From 3171cddd68145843277367c7ddf70a615ec649a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 01:20:49 +0000 Subject: [PATCH] fix(core): RPC jsonb payload, Liquibase splits, worker needs_badge - Pass JS objects to postgres jsonb params in callCoreFn (not JSON.stringify) - Use splitStatements:false on RPC function changesets for Liquibase - Fix needs_badge default in _worker_normalize when field is omitted - Add scripts/crud-smoke-test.sh for API CRUD verification - Fix findExistingWorker return type in main.ts Co-authored-by: alberto.martinez --- api/main.ts | 6 +- api/rpc.ts | 2 +- db/core/changelog-master.xml | 2 + db/core/changesets/006-rpc-infra.sql | 12 +- db/core/changesets/007-rpc-catalogs.sql | 6 +- db/core/changesets/008-rpc-companies.sql | 14 +- db/core/changesets/009-rpc-projects.sql | 20 +- db/core/changesets/010-rpc-workers.sql | 22 +- db/core/changesets/011-rpc-budget.sql | 16 +- db/core/changesets/012-rpc-badges.sql | 8 +- db/core/changesets/013-rpc-payroll.sql | 26 +-- db/core/changesets/014-rpc-import.sql | 6 +- db/core/changesets/015-rpc-documents.sql | 6 +- .../changesets/016-rpc-worker-needs-badge.sql | 200 ++++++++++++++++++ .../017-rpc-worker-normalize-badge.sql | 44 ++++ scripts/crud-smoke-test.sh | 168 +++++++++++++++ 16 files changed, 487 insertions(+), 71 deletions(-) create mode 100644 db/core/changesets/016-rpc-worker-needs-badge.sql create mode 100644 db/core/changesets/017-rpc-worker-normalize-badge.sql create mode 100755 scripts/crud-smoke-test.sh diff --git a/api/main.ts b/api/main.ts index ba5a988..23619b4 100644 --- a/api/main.ts +++ b/api/main.ts @@ -950,13 +950,15 @@ async function findExistingWorker( nss: string, excludeId = 0, route = "worker-duplicate-check", -) { +): Promise< + { id: number; first_name: string; last_name_p: string; curp: string; rfc: string; nss: string } | undefined +> { const env = await callCoreFn<{ worker?: { id: number; first_name: string; last_name_p: string; curp: string; rfc: string; nss: string } | null; found?: boolean; }>(db, "core.fn_worker_find_existing", { curp, rfc, nss, exclude_id: excludeId }, { route }); if (!env.ok || !env.data?.found) return undefined; - return env.data.worker; + return env.data.worker ?? undefined; } app.post("/v1/workers", ...requireCoreAuth, async (c) => { diff --git a/api/rpc.ts b/api/rpc.ts index 9855fce..e2e284d 100644 --- a/api/rpc.ts +++ b/api/rpc.ts @@ -38,7 +38,7 @@ async function invokeOnce( payload: Record, ): Promise> { const row = await db.prepare(`SELECT ${fn}($1::jsonb) AS result`).get( - JSON.stringify(payload), + payload, ) as { result: unknown } | undefined; const raw = row?.result; if (typeof raw === "string") { diff --git a/db/core/changelog-master.xml b/db/core/changelog-master.xml index d343ac7..f1eee80 100644 --- a/db/core/changelog-master.xml +++ b/db/core/changelog-master.xml @@ -22,5 +22,7 @@ + + diff --git a/db/core/changesets/006-rpc-infra.sql b/db/core/changesets/006-rpc-infra.sql index c669017..11164f2 100644 --- a/db/core/changesets/006-rpc-infra.sql +++ b/db/core/changesets/006-rpc-infra.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC infrastructure (envelope helpers) ---changeset panel:core-006a-rpc-generic-messages endDelimiter:; splitStatements:true +--changeset panel:core-006a-rpc-generic-messages splitStatements:false CREATE OR REPLACE FUNCTION core.rpc_generic_messages() RETURNS text[] LANGUAGE sql @@ -13,7 +13,7 @@ AS $$ ]::text[]; $$; ---changeset panel:core-006b-rpc-assert-message endDelimiter:; splitStatements:true +--changeset panel:core-006b-rpc-assert-message splitStatements:false CREATE OR REPLACE FUNCTION core.rpc_assert_message(p_message text) RETURNS void LANGUAGE plpgsql @@ -29,7 +29,7 @@ BEGIN END; $$; ---changeset panel:core-006c-rpc-ok endDelimiter:; splitStatements:true +--changeset panel:core-006c-rpc-ok splitStatements:false CREATE OR REPLACE FUNCTION core.rpc_ok( p_data jsonb, p_message text, @@ -53,7 +53,7 @@ BEGIN END; $$; ---changeset panel:core-006d-rpc-created endDelimiter:; splitStatements:true +--changeset panel:core-006d-rpc-created splitStatements:false CREATE OR REPLACE FUNCTION core.rpc_created( p_data jsonb, p_message text, @@ -77,7 +77,7 @@ BEGIN END; $$; ---changeset panel:core-006e-rpc-err endDelimiter:; splitStatements:true +--changeset panel:core-006e-rpc-err splitStatements:false CREATE OR REPLACE FUNCTION core.rpc_err( p_code text, p_message text, @@ -102,7 +102,7 @@ BEGIN END; $$; ---changeset panel:core-006f-rpc-map-exception endDelimiter:; splitStatements:true +--changeset panel:core-006f-rpc-map-exception splitStatements:false CREATE OR REPLACE FUNCTION core.rpc_from_exception( p_fn text, p_sqlstate text, diff --git a/db/core/changesets/007-rpc-catalogs.sql b/db/core/changesets/007-rpc-catalogs.sql index 49eade4..c68f79d 100644 --- a/db/core/changesets/007-rpc-catalogs.sql +++ b/db/core/changesets/007-rpc-catalogs.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC catálogos y configuración tenant ---changeset panel:core-007a-fn-catalogs endDelimiter:; splitStatements:true +--changeset panel:core-007a-fn-catalogs splitStatements:false CREATE OR REPLACE FUNCTION core.fn_catalogs(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -53,7 +53,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-007b-fn-tenant-timezone-get endDelimiter:; splitStatements:true +--changeset panel:core-007b-fn-tenant-timezone-get splitStatements:false CREATE OR REPLACE FUNCTION core.fn_tenant_timezone_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -83,7 +83,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-007c-fn-tenant-settings-upsert endDelimiter:; splitStatements:true +--changeset panel:core-007c-fn-tenant-settings-upsert splitStatements:false CREATE OR REPLACE FUNCTION core.fn_tenant_settings_upsert(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/008-rpc-companies.sql b/db/core/changesets/008-rpc-companies.sql index 09c7627..c6f5015 100644 --- a/db/core/changesets/008-rpc-companies.sql +++ b/db/core/changesets/008-rpc-companies.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC empresas (CRUD) ---changeset panel:core-008a-company-helpers endDelimiter:; splitStatements:true +--changeset panel:core-008a-company-helpers splitStatements:false CREATE OR REPLACE FUNCTION core._company_trim_text(p_value text) RETURNS text LANGUAGE sql @@ -148,7 +148,7 @@ BEGIN END; $$; ---changeset panel:core-008b-fn-company-list endDelimiter:; splitStatements:true +--changeset panel:core-008b-fn-company-list splitStatements:false CREATE OR REPLACE FUNCTION core.fn_company_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -189,7 +189,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-008c-fn-company-get endDelimiter:; splitStatements:true +--changeset panel:core-008c-fn-company-get splitStatements:false CREATE OR REPLACE FUNCTION core.fn_company_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -230,7 +230,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-008d-fn-company-get-by-code endDelimiter:; splitStatements:true +--changeset panel:core-008d-fn-company-get-by-code splitStatements:false CREATE OR REPLACE FUNCTION core.fn_company_get_by_code(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -268,7 +268,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-008e-fn-next-company-code endDelimiter:; splitStatements:true +--changeset panel:core-008e-fn-next-company-code splitStatements:false CREATE OR REPLACE FUNCTION core.fn_next_company_code(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -308,7 +308,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-008f-fn-company-create endDelimiter:; splitStatements:true +--changeset panel:core-008f-fn-company-create splitStatements:false CREATE OR REPLACE FUNCTION core.fn_company_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -458,7 +458,7 @@ EXCEPTION END; $$; ---changeset panel:core-008g-fn-company-update endDelimiter:; splitStatements:true +--changeset panel:core-008g-fn-company-update splitStatements:false CREATE OR REPLACE FUNCTION core.fn_company_update(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/009-rpc-projects.sql b/db/core/changesets/009-rpc-projects.sql index d24ef02..ad123d3 100644 --- a/db/core/changesets/009-rpc-projects.sql +++ b/db/core/changesets/009-rpc-projects.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC proyectos (obras) ---changeset panel:core-009a-fn-next-project-code endDelimiter:; splitStatements:true +--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 @@ -27,7 +27,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009b-fn-project-assert-status endDelimiter:; splitStatements:true +--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 @@ -103,7 +103,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009c-fn-project-list endDelimiter:; splitStatements:true +--changeset panel:core-009c-fn-project-list splitStatements:false CREATE OR REPLACE FUNCTION core.fn_project_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -162,7 +162,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009d-fn-project-get endDelimiter:; splitStatements:true +--changeset panel:core-009d-fn-project-get splitStatements:false CREATE OR REPLACE FUNCTION core.fn_project_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -209,7 +209,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009e-fn-project-create endDelimiter:; splitStatements:true +--changeset panel:core-009e-fn-project-create splitStatements:false CREATE OR REPLACE FUNCTION core.fn_project_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -294,7 +294,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009f-fn-project-update endDelimiter:; splitStatements:true +--changeset panel:core-009f-fn-project-update splitStatements:false CREATE OR REPLACE FUNCTION core.fn_project_update(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -405,7 +405,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009g-fn-project-document-list endDelimiter:; splitStatements:true +--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 @@ -447,7 +447,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009h-fn-company-document-list endDelimiter:; splitStatements:true +--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 @@ -489,7 +489,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009i-fn-project-checklist endDelimiter:; splitStatements:true +--changeset panel:core-009i-fn-project-checklist splitStatements:false CREATE OR REPLACE FUNCTION core.fn_project_checklist(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -540,7 +540,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-009j-fn-company-checklist endDelimiter:; splitStatements:true +--changeset panel:core-009j-fn-company-checklist splitStatements:false CREATE OR REPLACE FUNCTION core.fn_company_checklist(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/010-rpc-workers.sql b/db/core/changesets/010-rpc-workers.sql index 6224623..9fac86d 100644 --- a/db/core/changesets/010-rpc-workers.sql +++ b/db/core/changesets/010-rpc-workers.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC trabajadores (personal) ---changeset panel:core-010a-worker-helpers endDelimiter:; splitStatements:true +--changeset panel:core-010a-worker-helpers splitStatements:false CREATE OR REPLACE FUNCTION core._worker_trim_text(p_value text) RETURNS text LANGUAGE sql @@ -533,7 +533,7 @@ BEGIN END; $$; ---changeset panel:core-010b-fn-worker-list endDelimiter:; splitStatements:true +--changeset panel:core-010b-fn-worker-list splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -666,7 +666,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010c-fn-worker-get endDelimiter:; splitStatements:true +--changeset panel:core-010c-fn-worker-get splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -767,7 +767,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010d-fn-worker-create endDelimiter:; splitStatements:true +--changeset panel:core-010d-fn-worker-create splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -883,7 +883,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010e-fn-worker-update endDelimiter:; splitStatements:true +--changeset panel:core-010e-fn-worker-update splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_update(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -980,7 +980,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010f-fn-worker-set-pipeline endDelimiter:; splitStatements:true +--changeset panel:core-010f-fn-worker-set-pipeline splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_set_pipeline(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1066,7 +1066,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010g-fn-worker-assign endDelimiter:; splitStatements:true +--changeset panel:core-010g-fn-worker-assign splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_assign(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1148,7 +1148,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010h-fn-worker-unassign endDelimiter:; splitStatements:true +--changeset panel:core-010h-fn-worker-unassign splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_unassign(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1213,7 +1213,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010i-fn-worker-find-existing endDelimiter:; splitStatements:true +--changeset panel:core-010i-fn-worker-find-existing splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_find_existing(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1264,7 +1264,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010j-fn-worker-refresh-pipeline endDelimiter:; splitStatements:true +--changeset panel:core-010j-fn-worker-refresh-pipeline splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_refresh_pipeline(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1360,7 +1360,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-010k-fn-worker-checklist-raw endDelimiter:; splitStatements:true +--changeset panel:core-010k-fn-worker-checklist-raw splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_checklist_raw(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/011-rpc-budget.sql b/db/core/changesets/011-rpc-budget.sql index 62bb24b..b7824be 100644 --- a/db/core/changesets/011-rpc-budget.sql +++ b/db/core/changesets/011-rpc-budget.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC presupuesto (capítulos, partidas, importación atómica) ---changeset panel:core-011a-fn-budget-list endDelimiter:; splitStatements:true +--changeset panel:core-011a-fn-budget-list splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -90,7 +90,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-011b-fn-budget-chapter-create endDelimiter:; splitStatements:true +--changeset panel:core-011b-fn-budget-chapter-create splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_chapter_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -156,7 +156,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-011c-fn-budget-chapter-update endDelimiter:; splitStatements:true +--changeset panel:core-011c-fn-budget-chapter-update splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_chapter_update(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -238,7 +238,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-011d-fn-budget-chapter-delete endDelimiter:; splitStatements:true +--changeset panel:core-011d-fn-budget-chapter-delete splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_chapter_delete(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -294,7 +294,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-011e-fn-budget-item-create endDelimiter:; splitStatements:true +--changeset panel:core-011e-fn-budget-item-create splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_item_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -369,7 +369,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-011f-fn-budget-item-update endDelimiter:; splitStatements:true +--changeset panel:core-011f-fn-budget-item-update splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_item_update(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -457,7 +457,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-011g-fn-budget-item-delete endDelimiter:; splitStatements:true +--changeset panel:core-011g-fn-budget-item-delete splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_item_delete(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -507,7 +507,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-011h-fn-budget-replace endDelimiter:; splitStatements:true +--changeset panel:core-011h-fn-budget-replace splitStatements:false CREATE OR REPLACE FUNCTION core.fn_budget_replace(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/012-rpc-badges.sql b/db/core/changesets/012-rpc-badges.sql index 29db9f2..a453cf8 100644 --- a/db/core/changesets/012-rpc-badges.sql +++ b/db/core/changesets/012-rpc-badges.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC gafetes (jobs de impresión y entrega) ---changeset panel:core-012a-fn-badge-job-create endDelimiter:; splitStatements:true +--changeset panel:core-012a-fn-badge-job-create splitStatements:false CREATE OR REPLACE FUNCTION core.fn_badge_job_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -114,7 +114,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-012b-fn-badge-job-list endDelimiter:; splitStatements:true +--changeset panel:core-012b-fn-badge-job-list splitStatements:false CREATE OR REPLACE FUNCTION core.fn_badge_job_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -164,7 +164,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-012c-fn-badge-job-get endDelimiter:; splitStatements:true +--changeset panel:core-012c-fn-badge-job-get splitStatements:false CREATE OR REPLACE FUNCTION core.fn_badge_job_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -239,7 +239,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-012d-fn-badge-job-set-delivered endDelimiter:; splitStatements:true +--changeset panel:core-012d-fn-badge-job-set-delivered splitStatements:false CREATE OR REPLACE FUNCTION core.fn_badge_job_set_delivered(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/013-rpc-payroll.sql b/db/core/changesets/013-rpc-payroll.sql index 665919e..0e82325 100644 --- a/db/core/changesets/013-rpc-payroll.sql +++ b/db/core/changesets/013-rpc-payroll.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC nómina (port de api/payroll.ts) ---changeset panel:core-013a-payroll-helpers endDelimiter:; splitStatements:true +--changeset panel:core-013a-payroll-helpers splitStatements:false CREATE OR REPLACE FUNCTION core.round_money(p_value numeric) RETURNS numeric LANGUAGE sql @@ -206,7 +206,7 @@ BEGIN END; $$; ---changeset panel:core-013b-payroll-recalc endDelimiter:; splitStatements:true +--changeset panel:core-013b-payroll-recalc splitStatements:false CREATE OR REPLACE FUNCTION core._payroll_recalc_week(p_week_id bigint) RETURNS void LANGUAGE plpgsql @@ -615,7 +615,7 @@ BEGIN END; $$; ---changeset panel:core-013c-fn-payroll-settings endDelimiter:; splitStatements:true +--changeset panel:core-013c-fn-payroll-settings splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_settings_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -678,7 +678,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013d-fn-payroll-units endDelimiter:; splitStatements:true +--changeset panel:core-013d-fn-payroll-units splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_unit_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -737,7 +737,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013e-fn-attendance endDelimiter:; splitStatements:true +--changeset panel:core-013e-fn-attendance splitStatements:false CREATE OR REPLACE FUNCTION core.fn_attendance_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -859,7 +859,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013f-fn-loan-create endDelimiter:; splitStatements:true +--changeset panel:core-013f-fn-loan-create splitStatements:false CREATE OR REPLACE FUNCTION core.fn_loan_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -977,7 +977,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013g-fn-payroll-week endDelimiter:; splitStatements:true +--changeset panel:core-013g-fn-payroll-week splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_week_ensure(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1323,7 +1323,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013h-fn-destajo endDelimiter:; splitStatements:true +--changeset panel:core-013h-fn-destajo splitStatements:false CREATE OR REPLACE FUNCTION core.fn_destajo_job_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1449,7 +1449,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013i-fn-payroll-admin-jornal endDelimiter:; splitStatements:true +--changeset panel:core-013i-fn-payroll-admin-jornal splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_admin_line_add(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1636,7 +1636,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013i2-fn-payroll-week-list-open endDelimiter:; splitStatements:true +--changeset panel:core-013i2-fn-payroll-week-list-open splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_week_list_open(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1668,7 +1668,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013i3-fn-payroll-week-csv endDelimiter:; splitStatements:true +--changeset panel:core-013i3-fn-payroll-week-csv splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_week_csv(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1714,7 +1714,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013i4-fn-loan-read endDelimiter:; splitStatements:true +--changeset panel:core-013i4-fn-loan-read splitStatements:false CREATE OR REPLACE FUNCTION core.fn_loan_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -1829,7 +1829,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-013i5-fn-payroll-period endDelimiter:; splitStatements:true +--changeset panel:core-013i5-fn-payroll-period splitStatements:false CREATE OR REPLACE FUNCTION core.fn_payroll_period_create(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/014-rpc-import.sql b/db/core/changesets/014-rpc-import.sql index 38fbfc4..f518d72 100644 --- a/db/core/changesets/014-rpc-import.sql +++ b/db/core/changesets/014-rpc-import.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC importación de personal y metadatos documentales ---changeset panel:core-014a-import-helpers endDelimiter:; splitStatements:true +--changeset panel:core-014a-import-helpers splitStatements:false CREATE OR REPLACE FUNCTION core._import_tenant_id(payload jsonb) RETURNS integer LANGUAGE sql @@ -228,7 +228,7 @@ BEGIN END; $$; ---changeset panel:core-014b-fn-workers-import-batch endDelimiter:; splitStatements:true +--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 @@ -409,7 +409,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-014c-fn-worker-document-store endDelimiter:; splitStatements:true +--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 diff --git a/db/core/changesets/015-rpc-documents.sql b/db/core/changesets/015-rpc-documents.sql index 8f9d52b..52127d2 100644 --- a/db/core/changesets/015-rpc-documents.sql +++ b/db/core/changesets/015-rpc-documents.sql @@ -1,7 +1,7 @@ --liquibase formatted sql -- PANELS · core · RPC documentos (metadata post-S3) ---changeset panel:core-015a-fn-project-document-store endDelimiter:; splitStatements:true +--changeset panel:core-015a-fn-project-document-store splitStatements:false CREATE OR REPLACE FUNCTION core.fn_project_document_store(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -39,7 +39,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-015b-fn-company-document-store endDelimiter:; splitStatements:true +--changeset panel:core-015b-fn-company-document-store splitStatements:false CREATE OR REPLACE FUNCTION core.fn_company_document_store(payload jsonb) RETURNS jsonb LANGUAGE plpgsql @@ -77,7 +77,7 @@ EXCEPTION WHEN OTHERS THEN END; $$; ---changeset panel:core-015c-fn-document-get endDelimiter:; splitStatements:true +--changeset panel:core-015c-fn-document-get splitStatements:false CREATE OR REPLACE FUNCTION core.fn_project_document_get(payload jsonb) RETURNS jsonb LANGUAGE plpgsql diff --git a/db/core/changesets/016-rpc-worker-needs-badge.sql b/db/core/changesets/016-rpc-worker-needs-badge.sql new file mode 100644 index 0000000..0a9eaa7 --- /dev/null +++ b/db/core/changesets/016-rpc-worker-needs-badge.sql @@ -0,0 +1,200 @@ +--liquibase formatted sql +-- PANELS · core · fix needs_badge cast en fn_worker_create/update + +--changeset panel:core-016a-fn-worker-needs-badge splitStatements:false +CREATE OR REPLACE FUNCTION core.fn_worker_create(payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = core +AS $$ +DECLARE + v_tid integer := COALESCE( + NULLIF(payload->>'tenant_id', '')::integer, + NULLIF(current_setting('app.tenant_id', true), '')::integer + ); + v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; + v_errors jsonb; + v_norm jsonb; + v_existing workers%ROWTYPE; + v_row workers%ROWTYPE; + v_project_status text; + v_refresh jsonb; +BEGIN + v_errors := core._worker_validate_fields(payload); + IF v_errors <> '{}'::jsonb THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_worker_create: datos del trabajador incompletos o inválidos', + jsonb_build_object('fn', 'fn_worker_create', 'field', 'payload'), + v_errors + ); + END IF; + v_norm := core._worker_normalize(payload); + SELECT * INTO v_existing + FROM workers + WHERE curp = v_norm->>'curp' + OR rfc = v_norm->>'rfc' + OR nss = v_norm->>'nss' + LIMIT 1; + IF FOUND THEN + RETURN core.rpc_err( + 'CONFLICT', + core._worker_conflict_message(v_existing, v_norm->>'curp', v_norm->>'rfc', v_norm->>'nss'), + jsonb_build_object('fn', 'fn_worker_create', 'worker_id', v_existing.id) + ); + END IF; + IF v_project_id IS NOT NULL THEN + SELECT status INTO v_project_status FROM projects WHERE id = v_project_id; + IF NOT FOUND THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_worker_create: proyecto id=%s no encontrado', v_project_id), + jsonb_build_object('fn', 'fn_worker_create', 'project_id', v_project_id) + ); + END IF; + IF v_project_status <> 'activo' THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_worker_create: solo se asigna personal a proyectos activos', + jsonb_build_object('fn', 'fn_worker_create', 'project_id', v_project_id, 'status', v_project_status) + ); + END IF; + 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 ( + v_norm->>'first_name', + v_norm->>'middle_name', + v_norm->>'last_name_p', + v_norm->>'last_name_m', + v_norm->>'curp', + v_norm->>'rfc', + v_norm->>'nss', + v_norm->>'phone', + v_norm->>'email', + v_norm->>'address', + v_norm->>'blood_type', + '', + NULL, + v_norm->>'position', + v_norm->>'risk_code', + v_norm->>'work_type', + (v_norm->>'daily_wage')::numeric(12,2), + COALESCE((v_norm->'needs_badge')::boolean, true), + v_norm->>'status', + v_tid + ) + RETURNING * INTO v_row; + IF v_project_id IS NOT NULL THEN + INSERT INTO assignments (worker_id, project_id, active, start_date) + VALUES (v_row.id, v_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 IF; + v_refresh := core.fn_worker_refresh_pipeline(jsonb_build_object('worker_id', v_row.id, 'tenant_id', v_tid)); + RETURN core.rpc_created( + jsonb_build_object('worker', to_jsonb(v_row), 'pipeline', v_refresh->'data'), + format('Trabajador %s %s creado (id=%s)', v_row.first_name, v_row.last_name_p, v_row.id), + jsonb_build_object('fn', 'fn_worker_create', 'id', v_row.id) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_worker_create', SQLSTATE, SQLERRM); +END; +$$; + +CREATE OR REPLACE FUNCTION core.fn_worker_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 := COALESCE( + NULLIF(payload->>'tenant_id', '')::integer, + NULLIF(current_setting('app.tenant_id', true), '')::integer + ); + v_cur workers%ROWTYPE; + v_merged jsonb; + v_errors jsonb; + v_norm jsonb; + v_existing workers%ROWTYPE; + v_refresh jsonb; +BEGIN + IF v_id IS NULL THEN + RETURN core.rpc_err( + 'VALIDATION', + 'fn_worker_update: id es obligatorio para actualizar un trabajador', + jsonb_build_object('fn', 'fn_worker_update', 'field', 'id') + ); + END IF; + SELECT * INTO v_cur FROM workers WHERE id = v_id; + IF NOT FOUND THEN + RETURN core.rpc_err( + 'NOT_FOUND', + format('fn_worker_update: trabajador id=%s no encontrado', v_id), + jsonb_build_object('fn', 'fn_worker_update', 'id', v_id) + ); + END IF; + v_merged := to_jsonb(v_cur) || payload; + v_errors := core._worker_validate_fields(v_merged); + IF v_errors <> '{}'::jsonb THEN + RETURN core.rpc_err( + 'VALIDATION', + format('fn_worker_update: datos del trabajador inválidos (id=%s)', v_id), + jsonb_build_object('fn', 'fn_worker_update', 'id', v_id), + v_errors + ); + END IF; + v_norm := core._worker_normalize(v_merged); + SELECT * INTO v_existing + FROM workers + WHERE (curp = v_norm->>'curp' OR rfc = v_norm->>'rfc' OR nss = v_norm->>'nss') + AND id <> v_id + LIMIT 1; + IF FOUND THEN + RETURN core.rpc_err( + 'CONFLICT', + core._worker_conflict_message(v_existing, v_norm->>'curp', v_norm->>'rfc', v_norm->>'nss'), + jsonb_build_object('fn', 'fn_worker_update', 'id', v_id, 'worker_id', v_existing.id) + ); + END IF; + UPDATE workers SET + first_name = v_norm->>'first_name', + middle_name = v_norm->>'middle_name', + last_name_p = v_norm->>'last_name_p', + last_name_m = v_norm->>'last_name_m', + curp = v_norm->>'curp', + rfc = v_norm->>'rfc', + nss = v_norm->>'nss', + phone = v_norm->>'phone', + email = v_norm->>'email', + address = v_norm->>'address', + blood_type = v_norm->>'blood_type', + hire_type = v_cur.hire_type, + company_id = v_cur.company_id, + position = v_norm->>'position', + risk_code = v_norm->>'risk_code', + work_type = v_norm->>'work_type', + daily_wage = (v_norm->>'daily_wage')::numeric(12,2), + needs_badge = COALESCE((v_norm->'needs_badge')::boolean, v_cur.needs_badge, true), + status = v_norm->>'status' + WHERE id = v_id + RETURNING * INTO v_cur; + v_refresh := core.fn_worker_refresh_pipeline(jsonb_build_object('worker_id', v_id, 'tenant_id', v_tid)); + RETURN core.rpc_ok( + jsonb_build_object('worker', to_jsonb(v_cur), 'pipeline', v_refresh->'data'), + format('Trabajador %s %s actualizado (id=%s)', v_cur.first_name, v_cur.last_name_p, v_id), + jsonb_build_object('fn', 'fn_worker_update', 'id', v_id) + ); +EXCEPTION WHEN OTHERS THEN + RETURN core.rpc_from_exception('fn_worker_update', SQLSTATE, SQLERRM); +END; +$$; diff --git a/db/core/changesets/017-rpc-worker-normalize-badge.sql b/db/core/changesets/017-rpc-worker-normalize-badge.sql new file mode 100644 index 0000000..9f83ca1 --- /dev/null +++ b/db/core/changesets/017-rpc-worker-normalize-badge.sql @@ -0,0 +1,44 @@ +--liquibase formatted sql +-- PANELS · core · fix needs_badge default cuando el payload no lo envía + +--changeset panel:core-017a-worker-needs-badge-default splitStatements:false +CREATE OR REPLACE FUNCTION core._worker_normalize(p_payload jsonb) +RETURNS jsonb +LANGUAGE plpgsql +IMMUTABLE +AS $$ +DECLARE + v_middle text := core._worker_trim_text(p_payload->>'middle_name'); + v_blood text := core._worker_norm_upper(p_payload->>'blood_type'); + v_needs_badge boolean := COALESCE( + NOT ( + p_payload->>'needs_badge' = 'false' + OR p_payload->>'needs_badge' = '0' + OR p_payload->'needs_badge' = 'false'::jsonb + OR p_payload->'needs_badge' = '0'::jsonb + ), + true + ); +BEGIN + RETURN jsonb_build_object( + 'first_name', initcap(lower(core._worker_trim_text(p_payload->>'first_name'))), + 'middle_name', CASE WHEN v_middle = '' THEN NULL ELSE initcap(lower(v_middle)) END, + 'last_name_p', initcap(lower(core._worker_trim_text(p_payload->>'last_name_p'))), + 'last_name_m', initcap(lower(core._worker_trim_text(p_payload->>'last_name_m'))), + 'curp', core._worker_norm_upper(p_payload->>'curp'), + 'rfc', core._worker_norm_upper(p_payload->>'rfc'), + 'nss', core._worker_format_nss(p_payload->>'nss'), + 'phone', core._worker_trim_text(p_payload->>'phone'), + 'email', lower(core._worker_trim_text(p_payload->>'email')), + 'address', core._worker_trim_text(p_payload->>'address'), + 'blood_type', CASE WHEN v_blood = '' THEN NULL ELSE v_blood END, + 'hire_type', upper(core._worker_trim_text(p_payload->>'hire_type')), + 'position', initcap(lower(core._worker_trim_text(p_payload->>'position'))), + 'risk_code', core._worker_canonical_risk(p_payload->>'risk_code'), + 'work_type', upper(core._worker_trim_text(p_payload->>'work_type')), + 'daily_wage', COALESCE((p_payload->>'daily_wage')::numeric(12,2), 0), + 'needs_badge', v_needs_badge, + 'status', CASE WHEN p_payload->>'status' = 'baja' THEN 'baja' ELSE 'activo' END + ); +END; +$$; diff --git a/scripts/crud-smoke-test.sh b/scripts/crud-smoke-test.sh new file mode 100755 index 0000000..7d940a4 --- /dev/null +++ b/scripts/crud-smoke-test.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# Smoke test CRUD via RPC-backed API +set -euo pipefail +BASE="http://127.0.0.1:8000" +JAR="/tmp/panels-cookies.txt" +PASS="${SEED_PASSWORD:-TestAdmin123!}" +SUFFIX=$((RANDOM % 90000 + 10000)) +WORKER_TAG=$(printf '%02d' $((10 + SUFFIX % 80))) + +pass() { echo "✓ $1"; } +fail() { echo "✗ $1"; exit 1; } + +check_status() { + local name="$1" expected="$2" actual="$3" body="$4" + if [[ "$actual" != "$expected" ]]; then + echo "FAIL $name: expected HTTP $expected, got $actual" + echo "$body" | head -c 800 + exit 1 + fi + pass "$name (HTTP $actual)" +} + +echo "=== Health ===" +H=$(curl -s -w "\n%{http_code}" "$BASE/v1/health") +BODY=$(echo "$H" | head -n -1) +CODE=$(echo "$H" | tail -1) +check_status "health" "200" "$CODE" "$BODY" +echo "$BODY" | grep -q '"ok":true' || fail "health body not ok" + +echo "=== Login ===" +LOGIN=$(curl -s -w "\n%{http_code}" -c "$JAR" -X POST "$BASE/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"arct2608\",\"password\":\"$PASS\"}") +BODY=$(echo "$LOGIN" | head -n -1) +CODE=$(echo "$LOGIN" | tail -1) +check_status "login" "200" "$CODE" "$BODY" + +echo "=== Companies LIST ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/companies") +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "companies list" "200" "$CODE" "$BODY" +echo "$BODY" | grep -q '"ok":true' || fail "companies list envelope" +echo "$BODY" | grep -q 'ARCT2608' || fail "seed company missing" + +echo "=== Companies CREATE ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" -X POST "$BASE/v1/companies" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Subempresa Prueba RPC", + "code": "SUBRPC'"$SUFFIX"'", + "razon_social": "Subempresa Prueba SA de CV", + "rfc": "SPR010101ABC", + "regimen_fiscal": "601", + "registro_patronal": "A1234567890", + "domicilio_fiscal": "Calle Test 123", + "codigo_postal": "01000", + "ciudad": "Ciudad de México", + "estado": "CDMX" + }') +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "company create" "201" "$CODE" "$BODY" +COMPANY_ID=$(echo "$BODY" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) +[[ -n "$COMPANY_ID" ]] || fail "no company id in create response" + +echo "=== Companies GET ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/companies/$COMPANY_ID") +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "company get" "200" "$CODE" "$BODY" +echo "$BODY" | grep -qi "SUBRPC$SUFFIX" || fail "company code mismatch" + +echo "=== Companies PATCH ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" -X PATCH "$BASE/v1/companies/$COMPANY_ID" \ + -H "Content-Type: application/json" \ + -d '{"nombre_comercial":"Sub RPC Editada","telefono":"5551234567"}') +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "company patch" "200" "$CODE" "$BODY" +echo "$BODY" | grep -q 'Sub RPC Editada' || fail "company patch not applied" + +echo "=== Projects LIST ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/projects") +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "projects list" "200" "$CODE" "$BODY" + +echo "=== Projects CREATE ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" -X POST "$BASE/v1/projects" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Obra Prueba RPC", + "company_id": 1, + "address": "Av. Reforma 100", + "contract_amount": 1500000, + "start_date": "2026-01-01" + }') +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "project create" "201" "$CODE" "$BODY" +PROJECT_ID=$(echo "$BODY" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) +[[ -n "$PROJECT_ID" ]] || fail "no project id" + +echo "=== Projects GET ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/projects/$PROJECT_ID") +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "project get" "200" "$CODE" "$BODY" + +echo "=== Projects PATCH ===" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" -X PATCH "$BASE/v1/projects/$PROJECT_ID" \ + -H "Content-Type: application/json" \ + -d '{"name":"Obra Prueba RPC Editada","resident_name":"Ing. Test"}') +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "project patch" "200" "$CODE" "$BODY" +R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/projects/$PROJECT_ID") +BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1) +check_status "project get after patch" "200" "$CODE" "$BODY" +echo "$BODY" | grep -qi 'Obra Prueba RPC Editada' || fail "project patch not applied" + +echo "=== Workers CREATE ===" +WORKER_JSON=$(cat <