Commit graph

31 commits

Author SHA1 Message Date
Alberto Martinez
90bcd48f95 Facturas, nómina, dispersión e IMSS (#28)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Resumen

Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían.

## Qué queda disponible

- Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC.
- Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada.
- Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios.
- Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos.
- Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar.
- Dispersión: corte semanal, corrección antes de definitiva y comprobante.
- Historial de alta, baja y reingreso en la ficha, más movimientos IMSS.
- Reporte por periodo en Excel y PDF.
- Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel.

## Verificación

`deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?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-5e23dc3b-921e-4091-9650-7091d3a52bbf&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-25 03:26:30 +00:00
Alberto Martinez
34c0da30d8 fix(padron): vista previa de documentos, eliminar y reemplazar por tipo (#26)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema
En el expediente del padrón, al ver un documento el navegador lo trataba como descarga (cabeceras `application/octet-stream` + `attachment` y blob sin MIME). No había forma de eliminar archivos cargados y al subir otro del mismo tipo algunos entornos fallaban o no refrescaban bien la lista vigente.

## Cambios
- **API:** los GET de documentos (trabajador, proyecto, empresa) envían el `Content-Type` real del archivo y `Content-Disposition: inline` por defecto (`?disposition=attachment` para forzar descarga).
- **UI:** `DocumentPreviewDialog` pide `disposition=inline` y reconstruye el blob con MIME correcto para que PDF/imagen se vean en el diálogo.
- **Eliminar:** RPC `core.fn_worker_document_delete`, `DELETE /v1/workers/:id/documents/:docId`, permiso `documents.delete` (incluido en `manage_documents`), botón de papelera en vigentes e histórico.
- **Reemplazar:** `fn_worker_document_store` marca como no vigentes **todas** las filas del mismo tipo antes del INSERT; se limpia el `FileUpload` tras subir; filtro `is_current` tolera booleano o `1`.

## Migraciones
- Core: `040-fn-worker-document-delete.sql`, `041-fn-worker-document-store-replace.sql`
- IAM: `006-documents-delete-permission.sql`

## Verificación
Tras aplicar Liquibase, en un trabajador del padrón: abrir ojo en PDF/JPG (debe verse en modal), eliminar un doc de prueba, subir dos veces el mismo tipo (debe quedar una vigente y la anterior en histórico).
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?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-d8a5d34a-ead9-4912-8296-322cff799783&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-16 21:17:39 +00:00
Alberto Martinez
cb04a9a04c fix: alta de proyecto sin choque en projects_code_key (PRY duplicado) (#25)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Causa

El error `duplicate key value violates unique constraint "projects_code_key"` aparece porque:

1. La columna `projects.code` es **UNIQUE en toda la base** (todos los tenants).
2. Al dar de alta, `fn_project_create` llama a `fn_next_project_code`, que hacía `MAX(PRY…)+1` **solo sobre las filas que el tenant ve** (RLS).
3. Si en la base ya existe `PRY-0001` (seed de dev, otra empresa cliente, u otra obra que este tenant no ve), el sistema vuelve a proponer `PRY-0001` → el `INSERT` falla con CONFLICT.

No es el nombre del proyecto ni el presupuesto adjunto: es el **código autogenerado**.

## Cambio (Liquibase 039)

- `fn_next_project_code`: `SECURITY DEFINER` para leer la secuencia **global** de códigos PRY/OBR.
- `fn_project_create`: `pg_advisory_xact_lock` al asignar código (evita duplicado si envían dos veces seguidas).

## Cómo probar

1. Aplicar migraciones (039).
2. Con un tenant que aún no tenga obras (o en un ambiente donde ya exista `PRY-0001` de otro tenant), dar de alta un proyecto nuevo.
3. Debe crearse con `PRY-0002` (o el siguiente libre), sin toast de CONFLICT.

Script de demostración RLS: `scripts/verify-project-code-next.sql` + `verify-project-code-next-run.sql`.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?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-d8a5d34a-ead9-4912-8296-322cff799783&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-16 20:56:22 +00:00
Alberto Martinez
0045ccd8ed fix: programado vs costo (warehouse_movements.project_id no existe)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema

Tras cargar el programa, la pantalla pide en paralelo get / concepts / partidas / **vs-cost**. El vs-cost fallaba:

`fn_work_program_vs_cost (sqlstate=42703): column m.project_id does not exist`

`warehouse_movements` no tiene `project_id`. El proyecto está en `warehouses.project_id`.

## Cambio (changeset 038)

- Salidas de almacén por `JOIN warehouses w ON w.id = m.warehouse_id WHERE w.project_id = …`
- Ejecutado por partida **sin duplicar** al cruzar varias semanas (el join anterior multiplicaba el monto)

## Prueba local (hecha antes de pedir merge)

En Postgres 16 corrí las **4 RPCs** que usa la página, con presupuesto, 2 semanas, gasto $50 y salida de almacén $25:

- `fn_work_program_get` — curva 400 / 600
- `fn_work_program_list_concepts` — items
- `fn_work_program_list_partidas` — A001
- `fn_work_program_vs_cost` — ejecutado semanal **75**, por partida **75** (no 150)

Script: `scripts/verify-work-program-load.sh`

## Cómo probar en tu ambiente

1. Mergear y aplicar Liquibase (038).
2. Recargar Programa de obra en la obra ya importada. Sin toast rojo.
3. La pestaña / bloque de programado vs costo debe abrir.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?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-d8a5d34a-ead9-4912-8296-322cff799783&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-08 21:55:34 +00:00
Alberto Martinez
7ffdd9f7bf fix: recargar programa de obra (v_row no es una tabla)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema

Tras el #21 el GET ya no choca por agregados anidados, pero al recargar aparece:

`fn_work_program_get (sqlstate=42P01): missing FROM-clause entry for table "v_row"`

`v_row` es `jsonb`. Escribir `v_row.value` lo toma Postgres como `tabla.columna`, no como campo del JSON.

El #21 destapó este bug: el 42803 cortaba antes de llegar al loop.

## Cambio

Changeset **037** (no se edita el 036, ya corrido). La curva se arma en SQL (subquery + `SUM() OVER`) sin el `FOR` ni `v_row.value`.

## Cómo probar

1. Mergear este PR y aplicar Liquibase (037).
2. Abrir Programa de obra en la obra que ya importaste. Debe cargar sin toast rojo.
3. Recargar la página: igual.
4. Si hace falta, volver a Importar el Excel por partida y confirmar que se ve el programa.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?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-d8a5d34a-ead9-4912-8296-322cff799783&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-08 21:46:48 +00:00
Alberto Martinez
1a3fb49272 fix: guardar programa de obra (GET fallaba por agregados SQL anidados)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema

La preview del Excel por partida ya funciona. Al pulsar **Importar** el programa sí se guarda (`Programa importado`), pero al recargar `GET /v1/projects/:id/work-program` aparece:

`Error inesperado en fn_work_program_get (sqlstate=42803): aggregate function calls cannot be nested`

Postgres no permite `jsonb_agg(... SUM(...))` en el mismo SELECT. Eso arma la curva semanal después del import.

## Cambio

Changeset `036`: `SUM` va en una subquery y `jsonb_agg` queda afuera. El import no cambia.

## Cómo probar

1. Aplicar migraciones Liquibase (changeset 036).
2. En una obra con presupuesto, importar el Excel por partida (el de 8 partidas / 9 semanas).
3. Tras **Importar**, la pantalla debe mostrar el programa (semanas y curva), sin el toast rojo.
4. Actualizar / volver a entrar a Programa de obra: sigue cargando.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?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-d8a5d34a-ead9-4912-8296-322cff799783&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-08 21:36:05 +00:00
Cursor Agent
ad152d46f4
merge: #18 gafete job y empresa del tenant
Añade 032-fn-badge-job-get al changelog junto a los demás 032.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:13:45 +00:00
Cursor Agent
5e4d9a6710
merge: #17 documentos sin fechas de vigencia
Añade 032-document-validity-from-upload junto a los demás 032.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:13:33 +00:00
Cursor Agent
15ffb3ca29
merge: #15+#16 gastos, proveedores y almacén
Conserva los changesets 032 ya unidos y añade 033–035.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:13:02 +00:00
Cursor Agent
2f2d23004e
merge: #13 preview programa de obra
Incluye ambos changesets 032 (document-get y work-program) en el changelog.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:12:47 +00:00
Cursor Agent
016bc0bcee
merge: #11 ver PDF de proyecto
Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:12:37 +00:00
Cursor Agent
31a7c1cbf5
fix: gafete usa la empresa del tenant y corrige fn_badge_job_get
El ORDER BY del listado de personas refería la tabla w fuera del
subquery (42P01). El frente del gafete ya no imprime Construcciones
Arctec/Zendala: usa el nombre comercial de la empresa del proyecto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:52:35 +00:00
Cursor Agent
e89b51bf43
fix: subir documentos sin fecha de emisión ni vencimiento
La vigencia de 3 meses para alta o reingreso cuenta desde la fecha de
carga. Quien recibe el expediente revisa si el documento es válido.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:47:16 +00:00
Cursor Agent
e528d217d3
feat: almacén global, abrir/cerrar por obra y catálogo de materiales
El árbol de Almacén deja de listar todas las obras. El almacén global
siempre está disponible y consolida el stock de los almacenes abiertos.
Las obras abren almacén a demanda y al cerrarlo pueden devolver el
remanente al global. Entradas y traspasos usan el catálogo de materiales
con unidades de medida y uno o varios proveedores.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:41:18 +00:00
Cursor Agent
61e36696b9
feat: reclasificar gastos sin concepto o cambiar de partida
Si un gasto se guardó sin clasificar, ahora se edita desde la lista (clic o Reclasificar) y se le asigna o cambia el concepto del presupuesto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:19:13 +00:00
Cursor Agent
1e5bc22936
fix: catálogo de proveedores y unidades en la captura de gastos
Proveedor y unidad pasan a Select de catálogo (no texto libre). Subtotal, IVA y total se calculan al vuelo. El comprobante usa FileUpload de PrimeVue.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:06:45 +00:00
Cursor Agent
b7bd167e08
fix: importar programa de obra sin jsonb_object_length
Postgres no tiene jsonb_object_length; fn_work_program_replace
tronaba al confirmar el Excel. El conteo de semanas ahora usa
un contador en el loop de periodos.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:49:26 +00:00
Cursor Agent
de07975d4a
fix: abrir documentos de proyecto enviando doc_id a la RPC
La API pedía el PDF con { id } y fn_project_document_get leía
payload->>'doc_id', así que el id llegaba vacío aunque el archivo
ya estuviera en el bucket. Mismo desajuste en empresa y trabajador.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 19:27:24 +00:00
Cursor Agent
a9fbca232d
Fix IAM v2 migrate: split DO block and harden role backfill
The migrate job failed at changeset 005h during Coolify deploy. Liquibase
was splitting the DO $$ block on inner semicolons (splitStatements:true),
which breaks PL/pgSQL. Move the unique-index DO block to 005h2 with
splitStatements:false.

Add 005g2 to create missing operativo roles per tenant, backfill any
remaining role_id values, demote duplicate tenant_admin users to operativo,
and remove orphan users without tenant_id before SET NOT NULL.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 18:57:19 +00:00
Cursor Agent
25ceb6a6bd
Add IAM v2 roles/permissions migration and project doc types
Introduce Liquibase changeset 005 for IAM schema v2: CRUD permission codes,
tenant-scoped roles with system tenant_admin, role_permissions by role_id,
user scope fields and user_projects, RLS policies, and full RPC suite for
roles and users. Add core changeset 031 for technical project document types.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 18:10:45 +00:00
Alberto Martinez
496273b41c feat: Programa de obra por proyecto
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Implementa el módulo **Programa de obra** por proyecto, vinculado al presupuesto ya existente.

### Flujo
1. Cargar presupuesto (requisito)
2. Definir fechas de inicio/término en el proyecto
3. Crear programa: **importar Excel Neodata/Opus**, **generar automático** o **crear vacío**
4. Editar en cuadrícula (conceptos con `Barra=` y partidas con erogaciones semanales)
5. Visualizar Gantt resumido y comparativo **programado vs ejecutado**

### Backend
- Tablas: `work_programs`, `work_program_periods`, `work_program_concept_slots`, `work_program_partida_amounts`, `work_program_progress`
- RPC: list/get, replace (import), generate (wizard), update celdas, recalculate partidas, vs-cost
- Parser Excel para formatos **por concepto** (`Barra=`) y **por partida** (montos semanales)
- Tests con fixtures reales del usuario

### Frontend
- Nueva página `/programa-obra` con pestañas: Conceptos, Partidas, Gantt, Programado vs costos
- Diálogos de importación y generación automática
- Menú principal, enlace en ficha de proyecto, banner en presupuesto

### Permisos
Reutiliza `manage_budget` en MVP.

## Test plan
- [x] `deno test api/work_program_excel_test.ts` (5 tests)
- [x] `deno check` API modules
- [x] `npm run build` web-panel
- [ ] Migrar DB (`029`, `030`) en entorno con Postgres
- [ ] Importar Excel por concepto y por partida en obra con presupuesto
- [ ] Generar automático con estrategia keyword
- [ ] Recalcular partidas desde conceptos
- [ ] Verificar tab Programado vs costos con gastos registrados
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-3985fe74-d26b-4506-bd4d-f9ad57374141?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-3985fe74-d26b-4506-bd4d-f9ad57374141&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-08 04:51:09 +00:00
Alberto Martinez
41273dfb04 Gastos, almacén y control presupuestal
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Validación de endpoints y SPs

Se probaron todos los endpoints nuevos (gastos, almacén, control presupuestal, IAM) y sus RPCs asociados.

### Bugs corregidos en migraciones/SQL

1. **IAM grants en schema core** — `iam-004e` intentaba `GRANT` sobre `core` con rol `iam_owner` (sin permiso). Los grants cruzados se movieron a `core-027-iam-rpc-cross-grants.sql`.
2. **`_cost_settings` ambiguo** — columnas `budget_warn_pct` etc. colisionaban con `RETURNS TABLE` en PL/pgSQL, rompiendo `fn_expense_create` y control presupuestal. Corregido en `core-028-fix-cost-settings-ambiguous.sql`.

### Tests añadidos

- `api/cost_modules_test.ts` — CRUD RPC gastos, flujo almacén completo, control presupuestal, IAM permisos
- `scripts/crud-smoke-test.sh` — smoke HTTP de todos los endpoints nuevos

### Resultados

- `deno test cost_modules_test.ts` — 5/5 OK
- `./scripts/crud-smoke-test.sh` — todos los checks OK (gastos CRUD, IVA, cost-settings, almacén, transferencias, cost-control, IAM)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?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-eeec3b5c-f789-43e8-a353-0755e4706377&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 04:29:02 +00:00
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
Cursor Agent
42bba34469
api: --allow-sys para el SDK de AWS/R2 en Deno
El cliente S3 lee osRelease; sin --allow-sys el ping a R2 falla
con NotCapable aunque endpoint y bucket estén bien.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:49:14 +00:00
Cursor Agent
05d1a8c133
migrate: rechazar CONTAINER ID y fallar si el host no resuelve
Coolify no imprime stdout de Liquibase; el job migrate seguía
saliendo 1 por DNS. Preflight deja el host JDBC en los logs del
contenedor y corta si parece un ID de 12 caracteres.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:24:51 +00:00
Cursor Agent
a45675b6a6
deploy: Liquibase automático en cada deploy (job migrate)
Coolify usa docker-compose.yml: migrate aplica schema (!dev) y la API
espera a que termine. Postgres/Redis locales quedan en docker-compose.local.yml.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 04:54:15 +00:00
Cursor Agent
1ac04996b6
ops: generar accesos por ambiente y verificar Postgres, Redis y Contabo
Scripts create-accesses / verify-connectivity para un Postgres+Redis+bucket
por ambiente. /v1/health y el arranque de la API incluyen sonda de storage.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-02 21:22:39 +00:00
Cursor Agent
d36c287a82
db/infra: ETL de datos, backups y limpieza de despliegue (fase 5-7)
Fase 5 (migracion de datos):
- api/scripts/migrate-sqlite-to-postgres.ts: ETL unico SQLite -> Postgres.
  Pre-flight (CURP/RFC duplicados case-insensitive, FKs huerfanas,
  tenant_id invalido, fechas mal formateadas) -> carga por base/esquema
  con credenciales _owner (setval de secuencias, OVERRIDING SYSTEM VALUE,
  ON CONFLICT DO NOTHING idempotente) -> verificacion de conteos y sumas
  de dinero con tolerancia. Probado end-to-end contra un dataset SQLite
  legacy sintetico y un Postgres limpio (solo schema+catalogos, sin
  contexto dev): 100% de las filas migradas, sumas de dinero exactas,
  snapshot de uploaded_by/created_by resuelto correctamente.
- db/RUNBOOK-corte.md: checklist go/no-go para el corte por ambiente.
- Fix de bug real encontrado al probar: sin --context-filter explicito,
  Liquibase corre TAMBIEN los changesets context=dev (comportamiento por
  defecto, no "modo seguro") -- documentado en db/README.md con el
  ejemplo correcto (--context-filter='!dev' para staging/produccion).

Fase 5b: patron de compensacion para createTenant ya resuelto en el
rewrite de saas.ts (fase 3/4).

Fase 6 (backups): db/backups/ con plantillas pgBackRest por base,
politica de retencion, nota de persistencia minima de Redis (RDB, sin
retencion de negocio), checklist de simulacro de restauracion mensual,
y la validacion pendiente de si Coolify permite WAL archiving antes de
comprometerse a PITR real.

Fase 7 (limpieza y CI):
- Dockerfile.api simplificado (sin JRE/Liquibase/FFI). Dockerfile.migrate
  y Dockerfile.provision nuevos, de un solo uso, para el paso explicito
  de deploy (nunca sirven trafico).
- docker-compose.yml: postgres+redis+provision+migrate para dev local
  end-to-end; api ya no arranca hasta que migrate termina bien.
- docs/coolify.md y .env.example actualizados al modelo de 2 bases +
  Redis + Contabo.
- api/schema.sql eliminado (desincronizado, competia con Liquibase como
  fuente de verdad).
- .github/workflows/ci.yml: Postgres+Redis de servicio, aprovisiona
  roles/ACLs, dry-run de Liquibase (updateSQL) antes de aplicar,
  verify-isolation.sh, deno check + deno test, build de los dos frontends.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-02 21:05:07 +00:00
Cursor Agent
b87b0205a8
db: migrar infraestructura de BD a Postgres (fase 0-1)
- Fase 0: scripts de aprovisionamiento (db/provision/) para roles, dos
  bases de datos separadas (panels_platform / panels_product con
  esquemas iam+core) y ACLs de Redis por modulo, con verificacion
  automatizada de aislamiento (verify-isolation.sh) y setup local
  reproducible (dev-local.sh).
- Fase 1: changelogs de Liquibase reescritos para Postgres
  (db/platform, db/iam, db/core reemplazan db/app + los changesets
  SQLite de platform). Baseline como estado final (no replay literal),
  tipos traducidos (IDENTITY, TIMESTAMPTZ/DATE, NUMERIC, BOOLEAN,
  CITEXT), contexts dev vs. schema/catalogos, RLS por tenant_id como
  defensa en profundidad, uploaded_by/created_by como snapshot
  desnormalizado (sin FK hacia iam).
- Migraciones ya no corren en el arranque de la API: paso explicito de
  deploy via db/update.sh con credenciales _owner.

Verificado end-to-end contra Postgres 16 + Redis local.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-02 20:00:49 +00:00
085366c287 Nominas 2026-08-25 20:21:49 -05:00
70d89609f4 first commit 2026-08-21 16:55:01 -05:00