panels-origin/scripts/crud-smoke-test.sh
Alberto Martinez b9bf7044b2 refactor(core): migrate core CRUD to PostgreSQL RPC + unified HTTP errors
<!-- 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>&nbsp;<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>&nbsp;</div>
2026-09-04 02:34:43 +00:00

168 lines
6.2 KiB
Bash
Executable file

#!/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 <<EOF
{
"first_name": "Juan",
"last_name_p": "Perez",
"last_name_m": "Lopez",
"curp": "ROCL900215HDFRPM${WORKER_TAG}",
"rfc": "ROCL900215A${WORKER_TAG}",
"nss": "12345679000",
"phone": "5559876543",
"email": "worker${SUFFIX}@test.local",
"address": "Calle Worker 1",
"hire_type": "ARCT2608",
"position": "Oficial",
"risk_code": "medio",
"work_type": "N",
"daily_wage": 450,
"project_id": ${PROJECT_ID}
}
EOF
)
R=$(curl -s -w "\n%{http_code}" -b "$JAR" -X POST "$BASE/v1/workers" \
-H "Content-Type: application/json" \
-d "$WORKER_JSON")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check_status "worker create" "201" "$CODE" "$BODY"
WORKER_ID=$(echo "$BODY" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
[[ -n "$WORKER_ID" ]] || fail "no worker id"
echo "=== Workers GET ==="
R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/workers/$WORKER_ID")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check_status "worker get" "200" "$CODE" "$BODY"
echo "=== Workers LIST ==="
R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/workers")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check_status "workers list" "200" "$CODE" "$BODY"
echo "=== Workers PATCH ==="
R=$(curl -s -w "\n%{http_code}" -b "$JAR" -X PATCH "$BASE/v1/workers/$WORKER_ID" \
-H "Content-Type: application/json" \
-d '{"position":"Residente","daily_wage":500}')
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check_status "worker patch" "200" "$CODE" "$BODY"
echo "=== Catalogs ==="
R=$(curl -s -w "\n%{http_code}" -b "$JAR" "$BASE/v1/catalogs")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check_status "catalogs" "200" "$CODE" "$BODY"
echo "$BODY" | grep -q '"risks"' || fail "catalogs missing risks"
echo ""
echo "All CRUD smoke tests passed."