Skip to content

ERP Grupo Tesela — sistema completo (BD, app, BI e integración Holded)#11

Merged
israel2606 merged 30 commits into
mainfrom
claude/connector-recommendations-hzwhi4
Jun 17, 2026
Merged

ERP Grupo Tesela — sistema completo (BD, app, BI e integración Holded)#11
israel2606 merged 30 commits into
mainfrom
claude/connector-recommendations-hzwhi4

Conversation

@israel2606

Copy link
Copy Markdown
Owner

ERP a medida para Grupo Tesela (construcción · arquitectura · promoción inmobiliaria)

Sistema de gestión completo, en producción, construido sobre Supabase con integración real a Holded.

🗄️ Base de datos y seguridad

  • 16+ tablas (sociedades, promociones, fases, unidades, clientes, proveedores, reservas, compraventas, hitos de pago, presupuestos, partidas, contratos de obra, certificaciones, documental, perfiles, facturas).
  • Usuarios y roles (dirección / obra / comercial) con RLS (seguridad por fila) por promoción.
  • Almacén documental (Supabase Storage) con permisos por promoción.

📊 BI y automatización

  • Vistas: v_rentabilidad_promocion, v_comercializacion_promocion, v_tesoreria_promocion, v_resumen_grupo.
  • Triggers: al vender → unidad vendida + factura encolada; al certificar → coste real/margen actualizados.

🔄 Integración Holded (real)

  • 47 contactos + 56 facturas reales importados vía API de Holded.
  • Key cifrada en Vault + sincronización automática diaria (pg_cron + Edge Function sync-holded).

🖥️ Aplicación web

  • Publicada como Edge Function app (login Supabase Auth).
  • Pantallas: Dashboard del grupo, Promociones (listado + detalle), Contactos, Facturas.
  • Operativa: registrar ventas y certificaciones desde la app.

📚 Documentación

  • docs/erp-grupo-tesela/: INDICE, README, ARQUITECTURA, MODELO-DATOS, ESTADO + prototipos.
  • 20 migraciones SQL versionadas en supabase/migrations/.

⚠️ Notas

  • Las credenciales (API keys, secretos) no están en el repo: viven en Supabase Vault / paneles.
  • Pendiente: añadir las API keys vigentes de las otras 4 sociedades de Holded.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PKkvWSji6Gz89BK3N15Z5u


Generated by Claude Code

claude added 28 commits June 8, 2026 20:58
- Sector: construccion, arquitectura y promocion inmobiliaria (10 usuarios, meta Fase 2)
- Holded como motor contable/fiscal integrado via API (no se reemplaza)
- Modulos sectoriales: promociones/obras, comercializacion, proyectos, subcontratas, postventa
- Decision de base de datos: PostgreSQL recomendado, opciones de soberania del dato (UE/Barcelona)
- Base de datos: PostgreSQL en Supabase Cloud (region UE), MySQL descartado
- Holded: se mantiene como motor contable, integrado por API
- Nuevo MODELO-DATOS.md: esquema SQL Postgres + diagrama ER (promociones,
  obra, comercializacion, terceros, documental) con enlaces a Holded y Attio
- Proyecto Supabase creado (erp-grupo-tesela, region UE) con 14 tablas
- RLS base aplicado (acceso anonimo bloqueado)
- Prueba end-to-end del modelo de datos superada
- Edge Function que lee contactos de Holded y los vuelca en cliente/proveedor
- Upsert por holded_id (indice unico parcial) para no duplicar
- Usa secreto HOLDED_API_KEY; pendiente de activar
- Documentado el proceso de activacion en ESTADO.md
- 19 acciones de Holded activas en Zapier (find_contact, create_invoice, etc.)
- Documentadas las dos vias: Zapier (transaccional) + Edge Function (carga masiva)
- Lee las API keys desde el secret HOLDED_SOCIEDADES (JSON {nombre,key}), no del repo
- Crea/actualiza cada sociedad y asocia sus contactos via sociedad_id
- Upsert por holded_id para no duplicar
- Migracion: columna sociedad_id en cliente/proveedor + unique en sociedad.nombre
- Documentada la plantilla del secret HOLDED_SOCIEDADES (sin claves)
- Tablas perfil + acceso_promocion vinculadas a Supabase Auth
- Trigger de alta automatica de perfil y funciones helper RLS
- RLS fino: direccion=todo; obra/comercial=lectura por promocion + escritura en su dominio
- Endurecimiento: funciones helper retiradas del API REST publico
- Migraciones SQL versionadas en supabase/migrations/
- Bucket privado 'documentos' con RLS por promocion (ruta <promocion_id>/...)
- Funcion promocion_de_path para aplicar permisos a los archivos
- Vista v_rentabilidad_promocion (ingresos vs coste real) con security_invoker
- v_comercializacion_promocion: unidades por estado, % colocado, importe vendido
- security_invoker para respetar RLS
- Pantalla del panel con KPIs, comercializacion, avance de obra y control presupuestario
- Datos de la promocion demo; base para el frontend real del ERP
- Login, Dashboard, Listado de promociones, Panel comercial y Ficha de vivienda
- Mismo diseno previsto para Figma, con datos de la promocion demo
- Tabla factura_pendiente + trigger encolar_factura al escriturar compraventa
- Backfill de compraventas ya escrituradas; RLS por promocion
- Lista para que Make/Zapier emita las facturas en Holded (create_invoice)
- v_tesoreria_promocion: contratado en hitos, cobrado y pendiente de cobro
- README que ata toda la documentacion del ERP para el equipo
- Login con Supabase Auth + dashboard de promociones en vivo
- Carga v_comercializacion_promocion y v_rentabilidad_promocion (RLS por rol)
- Sin framework ni build; clave publishable (publica, seguridad por RLS)
- Edge Function app (verify_jwt=false) sirve la web del ERP
- Documentada la URL y el acceso de prueba
…ntos)

- Click en una promocion abre su detalle con KPIs y secciones cargadas en vivo
- Re-desplegada la Edge Function app (v2) con lista + detalle
- Router simple lista<->detalle en app/app.js
- Boton Vender en el detalle: crea contrato_venta (modal con cliente y precio)
- Trigger marca la unidad vendida automaticamente al registrar la venta
- Cierra el ciclo: venta -> unidad vendida -> factura encolada
- Re-desplegada la Edge Function app (v3)
- Vista v_resumen_grupo: KPIs agregados (cartera, vendido, margen, caja)
- Banda de resumen en la pantalla de inicio + promociones ordenadas por margen
- Re-desplegada la Edge Function app (v4)
- Boton '+ Cert.' por contrato en el detalle (rol direccion/obra)
- Modal con numero, importe y fecha -> inserta certificacion
- El coste real y el margen se actualizan solos (verificado)
- RLS obra_certificacion + Edge Function app v5
- Sincronizacion via API directa de Holded (pg_net) en Supabase
- 28 clientes + 19 proveedores reales volcados en la BD
- 4 de 5 keys invalidas (rotadas); pendientes las vigentes
- Rastro temporal de pg_net eliminado por seguridad
- Navegacion Promociones/Contactos en la topbar
- Tablas de clientes y proveedores (incluye los 47 reales de Holded)
- Edge Function app v6
- Tabla factura_holded (ventas y compras), RLS solo direccion
- 56 facturas reales importadas (30 ventas + 26 compras) via API Holded
- Rastro temporal de pg_net eliminado
- Navegacion Facturas con KPIs (ventas/compras/pendiente cobro/pago) y tabla
- Lee factura_holded (56 facturas reales); visible solo para Direccion
- Edge Function app v7
- Key cifrada en Supabase Vault (RPC get_holded_keys, solo service_role)
- sync-holded reescrita: lee del vault, sincroniza contactos + facturas
- Cron diario (pg_cron) a las 06:00 que invoca la funcion via pg_net
- Indices UNIQUE en holded_id para upsert idempotente
- Probada end-to-end: 28 clientes, 19 proveedores, 56 facturas

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00da5b57ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20260608_05_usuarios_y_roles.sql
'0 6 * * *',
$$ select net.http_post(
url := 'https://jpojckqnhepiuwefyvdr.supabase.co/functions/v1/sync-holded',
headers := '{"Content-Type":"application/json"}'::jsonb,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authenticate the scheduled Edge Function call

This cron job posts only Content-Type, and the repo does not add a supabase/config.toml entry disabling JWT verification for sync-holded; Supabase documents that verify_jwt is enabled by default and returns 401 when the Authorization header is missing, while pg_net/service calls should authenticate with an apikey/secret pattern. As written, the daily Holded sync will be rejected before index.ts runs unless it was manually deployed outside the repo with different auth settings.

Useful? React with 👍 / 👎.

Comment thread app/app.js
@@ -0,0 +1,13 @@
# Edge Function `app` — publicación de la web del ERP

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the actual app Edge Function entrypoint

The committed supabase/functions/app directory contains only this README (repo-wide search shows no supabase/functions/app/index.ts), so the documented /functions/v1/app Edge Function cannot be built or redeployed from the repository. Please include the function source/entrypoint or change the deployment instructions to the static app/ assets so the published web app is reproducible.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ce7da0566

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20260608_10_vista_comercializacion.sql

create or replace function public.get_holded_keys()
returns text language sql security definer set search_path = public, vault
as $$ select decrypted_secret from vault.decrypted_secrets where name = 'holded_sociedades' limit 1 $$;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align the Holded secret lookup with setup docs

The committed setup instructions tell operators to create an Edge Function secret named HOLDED_SOCIEDADES (docs/erp-grupo-tesela/ESTADO.md:83), but this RPC reads a database Vault secret named lowercase holded_sociedades. When setup follows the docs, sync-holded gets no keys and returns its 400 error before importing contacts or invoices; either read the Edge Function env var or align the Vault secret name/instructions.

Useful? React with 👍 / 👎.

@israel2606 israel2606 self-assigned this Jun 17, 2026
- P1: anadir migraciones base Fase 0 (01-04) -> el esquema se recrea desde cero
- P1: supabase/config.toml con verify_jwt=false para app y sync-holded (cron)
- P2: anadir supabase/functions/app/index.ts (función reproducible desde el repo)
- P2: escapar (anti-XSS) los datos importados de Holded antes de render (app + edge fn)
- P2: v_comercializacion_promocion devuelve 'estado' (la app ya no muestra '—')
- P2: documentar que las keys van en Vault ('holded_sociedades'), no en Edge secret

Copy link
Copy Markdown
Owner Author

He abordado los comentarios de la revisión (commit 74527b4):

  • P1 — Migraciones base: añadidas 20260608_01..04 (esquema de 14 tablas + RLS base + índices + multi-sociedad). El esquema ahora se recrea desde cero antes de las migraciones de roles/RLS/vistas.
  • P1 — Auth del cron: añadido supabase/config.toml con verify_jwt = false para app y sync-holded, de modo que el cron (pg_net sin Authorization) no recibe 401 al recrear desde el repo.
  • P2 — Entrypoint de la función app: añadido supabase/functions/app/index.ts (la web ya es reproducible/redeployable desde el repo).
  • P2 — XSS: los campos importados de Holded (contactos, facturas, comprador…) ahora se escapan con esc() antes de renderizarse, tanto en app/app.js como en la Edge Function.
  • P2 — Estado de promoción: v_comercializacion_promocion ahora devuelve estado (la app dejaba de mostrar ).
  • P2 — Secret de Holded: alineada la documentación: las keys viven cifradas en Vault (holded_sociedades, leído por la RPC get_holded_keys), no como secreto de Edge Functions.

Nota: el fallo previo de CI (lint/build en pkg/lockdown/safety_coverage_test.go) era de un test preexistente ya eliminado en main; se resolvió al actualizar la rama con main.


Generated by Claude Code

@israel2606 israel2606 merged commit 65f5e7c into main Jun 17, 2026
13 of 14 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74527b468e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,16 @@
-- Fase 0 — RLS base: bloquea el acceso anónimo, permite a usuarios autenticados.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use unique Supabase migration timestamps

This file name has the same leading timestamp as _01 and the rest of the new migrations. Supabase CLI parses migration versions from only the leading digits before the first underscore (^([0-9]+)_(.*)\.sql$) and stores that version as the primary key in supabase_migrations.schema_migrations, so after _01 records version 20260608, applying this second file fails with a duplicate migration version. Rename these files to unique full timestamps, or combine the ordered steps into one migration, so supabase db reset/db push can apply the schema.

Useful? React with 👍 / 👎.

Comment thread supabase/config.toml
# por eso se despliega con verify_jwt = false. La función no expone datos por GET:
# lee las keys del Vault y solo escribe en las tablas del ERP.
[functions.sync-holded]
verify_jwt = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Protect the service-role Holded sync endpoint

The fresh evidence is that this config explicitly disables JWT verification while sync-holded/index.ts accepts every request and immediately creates a Supabase client with SUPABASE_SERVICE_ROLE_KEY. Anyone who can reach /functions/v1/sync-holded can trigger reads against all configured Holded accounts, upsert ERP data, and receive the sociedad/count summary; use a scheduled-call secret/API key check instead of making the service-role sync function public.

Useful? React with 👍 / 👎.

create table contrato_venta (
id uuid primary key default gen_random_uuid(),
reserva_id uuid references reserva(id),
unidad_id uuid references unidad(id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce one sale contract per unit

When a commercial double-clicks the sale confirmation or posts two REST inserts for the same assigned unidad_id, both rows satisfy the RLS policy and the trigger only marks the unit as vendida; nothing on this table prevents multiple active contracts for one unit. The rentabilidad/comercialización views then sum both contracts, corrupting revenue and allowing the same unit to be sold to multiple clients, so unidad_id needs an appropriate unique/partial unique constraint or equivalent transactional guard.

Useful? React with 👍 / 👎.

Comment on lines +19 to +20
join public.fase f on f.promocion_id = pr.id
join public.unidad u on u.fase_id = f.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep empty promotions visible in the dashboard

With these inner joins, any promotion that has no phase yet, or has phases but no units, is omitted from v_comercializacion_promocion. The frontend uses this view as the source for the promotions list, so a newly created/assigned promotion in suelo or proyecto can disappear entirely and the user may see “No tienes promociones visibles”; use left joins and zero counts to keep accessible promotions visible before units are loaded.

Useful? React with 👍 / 👎.

Comment thread app/app.js
${ud.map((u) => {
const comp = esc(u.contrato_venta?.[0]?.cliente?.nombre) || "—";
const accion = (puedeVender && u.estado !== "vendida")
? `<button class="mini" onclick="registrarVenta('${u.id}',${u.precio_venta || 0},'${esc(u.referencia)}')">Vender</button>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid interpolating unit refs into inline handlers

For a unit reference containing a single quote, esc() emits &#39;, but browsers decode character references in an onclick attribute before compiling the handler, so the decoded quote terminates the JavaScript string and the rest of the reference can execute when a user clicks “Vender”. Use an event listener with data stored outside inline JavaScript, or JavaScript-string escaping such as JSON.stringify, instead of HTML escaping for this handler argument.

Useful? React with 👍 / 👎.

@israel2606

Copy link
Copy Markdown
Owner Author

🔍 Revisión — bloqueantes pendientes antes de fusionar

Resumen de los puntos detectados (incluye los comentarios de Codex aún sin resolver). Marcados por prioridad.

🔴 Mayores (bloqueantes)

1. XSS en app/app.js (Facturas/Contactos). Los campos importados de Holded se insertan con innerHTML sin escapar. Un nombre de contacto o nº de documento manipulado puede ejecutar script en la sesión del ERP. → Renderizar con textContent/APIs del DOM, o escapar todas las cadenas interpoladas antes de construir la tabla.

2. Faltan las migraciones base (Fase 0). La primera migración versionada en supabase/migrations/ ya referencia promocion, pero no hay migraciones 01–04 que creen esas tablas. Aplicar el repo en un Supabase limpio falla con relation "promocion" does not exist. → Commitear las migraciones de creación de tablas de Fase 0 antes de las de roles/RLS.

3. El cron de sync-holded no se autentica. 20260608_19_cron_sync_holded.sql hace http_post solo con Content-Type. Con verify_jwt activo por defecto, Supabase devuelve 401 y la sync diaria nunca corre. → Añadir cabecera de autorización (apikey/secret) o desactivar verify_jwt para esa función en config.toml.

4. Falta el entrypoint de la Edge Function app. supabase/functions/app/ solo contiene el README; no hay index.ts. La web /functions/v1/app no se puede construir ni redesplegar desde el repo. → Incluir el código de la función, o cambiar las instrucciones a los assets estáticos de app/.

🟡 Menores / inconsistencias

5. Estado de promoción vacío. El frontend muestra p.estado (app/app.js:133) desde v_comercializacion_promocion, pero esa vista no selecciona estado. → Añadir pr.estado al SELECT/GROUP BY o leerlo de otra fuente.

6. Desajuste en el nombre del secreto de Holded. La doc (ESTADO.md:83) indica HOLDED_SOCIEDADES, pero el RPC get_holded_keys() lee holded_sociedades (minúsculas) del Vault. → Alinear nombre del secreto y/o instrucciones.

🟢 Positivo

Alcance funcional muy completo y bien documentado (INDICE/ARQUITECTURA/MODELO-DATOS). Credenciales fuera del repo (Vault). Buen uso de RLS por promoción y security_invoker en vistas.

Recomendación: Request changes — abordar 1–4 antes de fusionar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants