diff --git a/cmd/mcpcurl/coverage_test.go b/cmd/mcpcurl/coverage_test.go new file mode 100644 index 0000000000..f9c5afbf2e --- /dev/null +++ b/cmd/mcpcurl/coverage_test.go @@ -0,0 +1,199 @@ +package main + +import ( + "encoding/json" + "io" + "os" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +// sampleTool returns a Tool exercising every property type handled by +// addCommandFromTool / buildArgumentsMap. +func sampleTool() *Tool { + return &Tool{ + Name: "sample_tool", + Description: "A sample tool", + InputSchema: InputSchema{ + Type: "object", + Properties: map[string]Property{ + "name": {Type: "string", Description: "the name"}, + "status": {Type: "string", Description: "state", Enum: []string{"open", "closed"}}, + "count": {Type: "integer", Description: "how many"}, + "ratio": {Type: "number", Description: "a ratio"}, + "active": {Type: "boolean", Description: "is active"}, + "labels": {Type: "array", Description: "labels", Items: &PropertyItem{Type: "string"}}, + "items": {Type: "array", Description: "items", Items: &PropertyItem{Type: "object"}}, + }, + Required: []string{"name"}, + }, + } +} + +// newToolCommand builds the cobra subcommand for a tool via addCommandFromTool +// and returns it, so tests can set flags and call the pure helpers. +func newToolCommand(t *testing.T, tool *Tool) *cobra.Command { + t.Helper() + parent := &cobra.Command{Use: "tools"} + addCommandFromTool(parent, tool, false) + cmds := parent.Commands() + require.Len(t, cmds, 1) + return cmds[0] +} + +func TestAddCommandFromTool_RegistersFlags(t *testing.T) { + cmd := newToolCommand(t, sampleTool()) + + require.Equal(t, "sample_tool", cmd.Use) + require.Equal(t, "A sample tool", cmd.Short) + + // A flag is registered for each scalar/array property. + for _, name := range []string{"name", "status", "count", "ratio", "active", "labels"} { + require.NotNil(t, cmd.Flags().Lookup(name), "expected flag %q", name) + } + // Object-array properties get a "-json" flag. + require.NotNil(t, cmd.Flags().Lookup("items-json")) + + // Optional flags advertise themselves as optional in the usage text. + require.Contains(t, cmd.Flags().Lookup("status").Usage, "(optional)") + require.NotContains(t, cmd.Flags().Lookup("name").Usage, "(optional)") +} + +func TestAddCommandFromTool_EnumValidation(t *testing.T) { + cmd := newToolCommand(t, sampleTool()) + require.NotNil(t, cmd.PreRunE, "enum property should install a PreRunE validator") + + require.NoError(t, cmd.Flags().Set("status", "open")) + require.NoError(t, cmd.PreRunE(cmd, nil)) + + require.NoError(t, cmd.Flags().Set("status", "bogus")) + err := cmd.PreRunE(cmd, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "status must be one of") +} + +func TestBuildArgumentsMap_AllTypes(t *testing.T) { + tool := sampleTool() + cmd := newToolCommand(t, tool) + + require.NoError(t, cmd.Flags().Set("name", "hello")) + require.NoError(t, cmd.Flags().Set("count", "5")) + require.NoError(t, cmd.Flags().Set("ratio", "1.5")) + require.NoError(t, cmd.Flags().Set("active", "true")) + require.NoError(t, cmd.Flags().Set("labels", "a,b")) + require.NoError(t, cmd.Flags().Set("items-json", `[{"x":1}]`)) + + args, err := buildArgumentsMap(cmd, tool) + require.NoError(t, err) + + require.Equal(t, "hello", args["name"]) + require.Equal(t, int64(5), args["count"]) + require.InEpsilon(t, 1.5, args["ratio"], 1e-9) + require.Equal(t, true, args["active"]) + require.Equal(t, []string{"a", "b"}, args["labels"]) + require.Equal(t, []any{map[string]any{"x": float64(1)}}, args["items"]) + + // Unset, zero-valued, and empty fields are omitted. + _, ok := args["status"] + require.False(t, ok, "unset string should be omitted") +} + +func TestBuildArgumentsMap_OmitsZeroAndUnset(t *testing.T) { + tool := sampleTool() + cmd := newToolCommand(t, tool) + + // Only set the boolean to false explicitly; it must still be included + // because Changed() is true, while zero-valued numbers stay omitted. + require.NoError(t, cmd.Flags().Set("active", "false")) + + args, err := buildArgumentsMap(cmd, tool) + require.NoError(t, err) + + require.Equal(t, false, args["active"]) + _, hasCount := args["count"] + require.False(t, hasCount, "zero integer should be omitted") + _, hasName := args["name"] + require.False(t, hasName, "unset string should be omitted") +} + +func TestBuildArgumentsMap_InvalidObjectJSON(t *testing.T) { + tool := sampleTool() + cmd := newToolCommand(t, tool) + + require.NoError(t, cmd.Flags().Set("items-json", "{not valid json")) + + _, err := buildArgumentsMap(cmd, tool) + require.Error(t, err) + require.Contains(t, err.Error(), "error parsing JSON for items") +} + +func TestBuildJSONRPCRequest(t *testing.T) { + out, err := buildJSONRPCRequest("tools/call", "my_tool", map[string]any{"a": "b"}) + require.NoError(t, err) + + var req JSONRPCRequest + require.NoError(t, json.Unmarshal([]byte(out), &req)) + require.Equal(t, "2.0", req.JSONRPC) + require.Equal(t, "tools/call", req.Method) + require.Equal(t, "my_tool", req.Params.Name) + require.Equal(t, "b", req.Params.Arguments["a"]) +} + +// captureStdout runs fn and returns everything it wrote to os.Stdout. +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + orig := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + defer func() { os.Stdout = orig }() + + fn() + require.NoError(t, w.Close()) + out, err := io.ReadAll(r) + require.NoError(t, err) + return string(out) +} + +func TestPrintResponse_NoPrettyPrint(t *testing.T) { + raw := `{"jsonrpc":"2.0","id":1,"result":{}}` + out := captureStdout(t, func() { + require.NoError(t, printResponse(raw, false)) + }) + require.Equal(t, raw, strings.TrimSpace(out)) +} + +func TestPrintResponse_PrettyObjectContent(t *testing.T) { + // content.text holds a JSON object string that should be re-indented. + resp := `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"hello\":\"world\"}"}]}}` + out := captureStdout(t, func() { + require.NoError(t, printResponse(resp, true)) + }) + require.Contains(t, out, "\"hello\": \"world\"") +} + +func TestPrintResponse_PrettyArrayContent(t *testing.T) { + // content.text holds a JSON array string -> JSONL fallback path. + resp := `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"[{\"n\":1}]"}]}}` + out := captureStdout(t, func() { + require.NoError(t, printResponse(resp, true)) + }) + require.Contains(t, out, "\"n\": 1") +} + +func TestPrintResponse_PrettyEmptyContent(t *testing.T) { + resp := `{"jsonrpc":"2.0","id":1,"result":{"content":[]}}` + out := captureStdout(t, func() { + require.NoError(t, printResponse(resp, true)) + }) + require.Contains(t, out, resp) +} + +func TestPrintResponse_PrettyInvalidJSON(t *testing.T) { + err := printResponse("not json", true) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse JSON") +} diff --git a/docs/PENDIENTES_DE_EMPEZAR.md b/docs/PENDIENTES_DE_EMPEZAR.md new file mode 100644 index 0000000000..1fd2bb47dc --- /dev/null +++ b/docs/PENDIENTES_DE_EMPEZAR.md @@ -0,0 +1,29 @@ +# Pendientes de empezar (sin comenzar) + +> Fecha: junio 2026. Cobertura global actual: **69,3 %** (sobre `main` = upstream). + +## Paquetes con menor cobertura (objetivos) + +| Paquete | Cobertura | Notas | +|---|---:|---| +| `internal/ghmcp` | **0,0 %** | Arranque del servidor, hosts GHES/GHEC, transporte HTTP. Mayor hueco e impacto. | +| `cmd/mcpcurl` | ~55 % | **En progreso** (este trabajo). Resto sin cubrir: rutas con subproceso (`executeServerCommand`, `Run`). | +| `internal/githubv4mock` | 18,1 % | Utilidad de test; cobertura indirecta. | +| `pkg/http` | 37,4 % | Capa del servidor HTTP remoto. | +| `pkg/http/transport` | 46,7 % | Transporte HTTP. | +| `pkg/log` | 47,4 % | Logging E/S. | +| `pkg/http/middleware` | 50,6 % | Middleware del servidor. | +| `pkg/utils` | 54,0 % | Helpers de resultados. | + +## Tareas concretas sin empezar +1. **`internal/ghmcp`**: tests de derivación de hosts GHES/GHEC, wrappers de + transporte/`RoundTrip`, middleware. Mayor impacto en el total. +2. **`pkg/http` y submódulos** (`transport`, `middleware`, `oauth`): rutas del + servidor remoto. +3. **`cmd/mcpcurl`** (rematar): cubrir `executeServerCommand` y el `Run` de los + comandos generados (requiere un binario/servidor de prueba por stdio). + +## Recomendación de orden +Confirmar primero la estabilidad vs upstream (ver PENDIENTES_DE_RESOLVER). Si se +mantiene: `internal/ghmcp` → `pkg/http*`. Re-medir la cobertura del paquete antes +de cada tarea, porque el repo cambia con frecuencia. diff --git a/docs/PENDIENTES_DE_RESOLVER.md b/docs/PENDIENTES_DE_RESOLVER.md new file mode 100644 index 0000000000..589933ce54 --- /dev/null +++ b/docs/PENDIENTES_DE_RESOLVER.md @@ -0,0 +1,18 @@ +# Pendientes de resolver (en curso / decisión pendiente) + +> Fecha: junio 2026. + +## 1. ¿Abrir PR con los tests de `cmd/mcpcurl`? +- Estado: el fichero `cmd/mcpcurl/coverage_test.go` está hecho y verde en local, + commiteado en la rama. Falta decidir si se abre PR contra `main`. + +## 2. Estabilidad del repo frente al upstream — RIESGO +- `main` ya se ha reseteado al upstream una vez, borrando trabajo previo. +- Mientras no se aclare la política del fork (¿se sigue sincronizando con + `github/github-mcp-server`?), **cualquier test nuevo puede volver a perderse**. +- Pendiente: confirmar con el responsable si el fork va a divergir del upstream + o seguir sincronizándose, para decidir cuánto esfuerzo invertir. + +## 3. Documentos de seguimiento +- Recuperados en `docs/` y puestos al día (este conjunto). Pendiente: mantenerlos + vivos según avance el trabajo. diff --git a/docs/TRABAJO_REALIZADO.md b/docs/TRABAJO_REALIZADO.md new file mode 100644 index 0000000000..d686d3ff10 --- /dev/null +++ b/docs/TRABAJO_REALIZADO.md @@ -0,0 +1,34 @@ +# Trabajo realizado + +> Proyecto: **github-mcp-server** (servidor MCP en Go). Fecha: junio 2026. +> Rama de trabajo: `claude/test-coverage-analysis-s33ri0`. + +## Nota de contexto importante +El `main` del fork se **sincronizó/reseteó con el upstream oficial +`github/github-mcp-server`**. Eso **eliminó de `main`** todo el trabajo previo +del fork (tests de `pkg/lockdown` del PR #5, documentos de estado, los arreglos +de workflows, etc.). El historial actual de `main` es el del upstream. Por eso +este documento se ha reescrito para reflejar la realidad actual. + +## Histórico (referencia) +- Análisis inicial de cobertura del repo. +- **PR #5** (fusionado en su momento): tests de `pkg/lockdown` → perdidos en el reset. +- **PR #10** (cerrado sin fusionar): arreglo de CI mezclado con docs. +- **PR #12** (cerrado): reintento de arreglo de CI; quedó obsoleto porque el + reset a upstream eliminó los workflows rotos (`python-package.yml`, + `go-build.yml`) que pretendía borrar. +- Diagnósticos de CI hechos por el camino (colisión de símbolos por refactor + paralelo, crash de golangci-lint por Go 1.26 vs golangci 1.25, workflow de + Python sobre repo Go). + +## Trabajo vigente sobre el `main` nuevo (upstream) +- **Re-medición de cobertura** desde cero: total **69,3 %**. +- **Tests para `cmd/mcpcurl`**: subido de **8,9 % → ~55 %** añadiendo + `cmd/mcpcurl/coverage_test.go` (sin tocar el test existente). Cubre: + - `buildArgumentsMap` → 100 % (todos los tipos: string/number/integer/boolean/ + array de strings/array de objetos vía `-json`, y error de JSON inválido). + - `printResponse` → 88,5 % (sin pretty, objeto, array/JSONL, vacío, JSON inválido). + - `buildJSONRPCRequest` → 75 %. + - `addCommandFromTool` → 63,5 % (registro de flags, flags requeridos, validación + de `enum` vía `PreRunE`). +- Verificado en local: `gofmt`, `go vet ./...`, `go test ./...` → **todo verde**. diff --git a/docs/erp-grupo-tesela/ARQUITECTURA.md b/docs/erp-grupo-tesela/ARQUITECTURA.md new file mode 100644 index 0000000000..ce42640885 --- /dev/null +++ b/docs/erp-grupo-tesela/ARQUITECTURA.md @@ -0,0 +1,309 @@ +# Arquitectura del ERP — Grupo Tesela + +> Documento vivo. **v3 — reconciliada** con la realidad operativa (Holded en vivo). +> Sector: **construcción · arquitectura · promoción inmobiliaria** (en ese orden). +> Stack basado en los conectores (MCP) disponibles en el entorno de trabajo. + +--- + +## 0. Qué ha cambiado respecto a la v1 (registro de correcciones) + +Esta versión corrige los problemas detectados en la revisión del diseño inicial: + +- **Coherencia arquitectónica:** las automatizaciones (Make/Zapier) **ya no + escriben directamente a Postgres**; pasan por la API / Edge Functions de + Supabase, para no saltarse las reglas de negocio ni romper la "fuente única de + verdad". (En la v1 el diagrama conectaba Make/Zapier directos a la BD.) +- **Priorización por sector:** **Obras/Proyectos y Promoción inmobiliaria pasan + a ser el núcleo (Fase 1-2)**, no módulos tardíos. En construcción/promoción el + negocio es el control de costes por obra y la comercialización de unidades. +- **Fiscalidad española añadida:** IVA con **inversión del sujeto pasivo** + (ejecuciones de obra), retenciones, **SII / Verifactu**, certificaciones. +- **Build-vs-buy explícito** para el núcleo Finanzas/Contabilidad (Holded/Odoo o + software de construcción tipo Presto vs desarrollo a medida). +- **Recortes:** se marca el solapamiento Make+Zapier y Airtable+Notion+Supabase; + Supermetrics se mueve a fase tardía; el "Inventario/Almacén" genérico se + sustituye por "Aprovisionamiento y subcontratas por obra". +- **Secciones nuevas:** modelo de datos inicial, seguridad/RGPD/backups, KPIs. +- **Aviso de disponibilidad:** **Coupler.io no tiene conector MCP** en este + entorno hoy; su rol de ETL queda como "a confirmar / alternativa". + +### Reconciliación v3 (realidad operativa) + +Según la coordinación entre sesiones (`COORDINACION_SESIONES_HOLDED_2026-06-19`): + +- **Holded ya está EN VIVO** y es la **fuente canónica** de finanzas/contabilidad. + Deja de ser una decisión "build-vs-buy" pendiente: **está decidido y operativo**. + No se reimplementa contabilidad a medida. +- **Una sola integración de Holded canónica = la API directa en vivo.** Cualquier + puente de automatización (p. ej. Holded↔Make) es **opcional y solo para + automatizar** (sync/alertas), **nunca** una segunda fuente de verdad. +- **Reglas comunes de coste/margen** (única forma de calcular): coste imputado por + **`projectid`** (no por tags), **excluir confirming**, **mano de obra propia = + coste directo**, y **un único cálculo de margen** (nada de dos pipelines). +- **Gobierno de la información:** **Drive = memoria** (notas/estado entre sesiones), + **repo "Cerebro" = código**. Evitar duplicados (índices/carpetas repetidos). + +> ⚠️ **Riesgo de ubicación:** este documento vive en un *fork* de +> `github-mcp-server`. Una sincronización con el upstream lo borra (ya pasó). Lo +> antes posible debería moverse a un **repositorio propio del ERP** (¿el "Cerebro"?). +> +> 🔒 **Seguridad pendiente:** la nota menciona claves de Holded *expuestas* que +> debían rotarse. Confirmar **rotación** y que viven solo en secretos/`.env`, +> nunca en Drive ni en el repo. + +--- + +## 1. Principios de diseño + +1. **Fuente única de verdad:** todos los datos del negocio viven en una base de + datos central (Supabase/Postgres). El resto de herramientas leen/escriben + **a través de la API**, nunca directamente a las tablas. +2. **Modular y por fases:** cada área es un módulo activable de forma + independiente. No se construye todo de golpe. +3. **Automatización orquestada:** las tareas repetitivas se automatizan **contra + la API**, no contra la BD ni a mano. +4. **API-first:** todo expone API para conectarse con cualquier herramienta. +5. **Coste escalado:** plan gratuito primero; premium cuando el módulo lo + justifique. +6. **Cumplimiento desde el día 1:** RGPD, copias de seguridad y control de + accesos no son una fase posterior. + +--- + +## 2. Stack elegido + +| Capa | Herramienta | Notas | +|------|-------------|-------| +| **Núcleo de datos / Backend** | **Supabase** | Postgres + Auth + API + Storage + Edge Functions. El corazón. | +| **Núcleo fiscal/contable** *(EN VIVO · canónico)* | **Holded** | Ya operativo y fuente única de finanzas. Integración por **API directa**. Ver §6. | +| **Código y CI/CD** | **GitHub** | Repos, ramas, PRs, despliegues. | +| **Automatización principal** | **Make.com** | Orquestador de flujos contra la API. | +| **Automatización secundaria** | **Zapier** | Solo "larga cola" que Make no cubra (evitar solape con Make). | +| **Diseño de interfaz** | **Figma** | Frontend del ERP y design-to-code. | +| **CRM / Ventas** | **Attio** | Pipeline comercial. Para venta de inmuebles, evaluar CRM inmobiliario específico. | +| **Vistas operativas ligeras** | **Airtable** | Solo donde Supabase+frontend aún no llegue (evitar duplicar la BD). | +| **Documentación / Wiki** | **Notion** | Manual y procesos internos. | +| **Ofimática y correo** | **Google Workspace** | Gmail + Calendar + Drive. | +| **Reuniones** | **Zoom** | Grabaciones y resúmenes. | +| **Gestión dev** | **Linear** | Roadmap del propio ERP. | +| **Marketing (tardío)** | **Supermetrics** | Solo con inversión publicitaria seria (Fase 3). | +| **ETL / BI** *(a confirmar)* | **Coupler.io** | Sin conector MCP aquí hoy; alternativa: Edge Functions + Supabase. | + +--- + +## 3. Arquitectura por capas (corregida) + +```mermaid +flowchart TB + subgraph PRES["🖥️ Presentación"] + UI["Frontend ERP (Figma→código)"] + AIR["Airtable (vistas operativas)"] + NOT["Notion (wiki)"] + end + + subgraph APP["⚙️ Aplicación / Lógica"] + API["API REST/GraphQL (auto)"] + EF["Edge Functions (reglas de negocio)"] + end + + subgraph DATA["🗄️ Datos — FUENTE ÚNICA DE VERDAD"] + PG[("Supabase PostgreSQL")] + ST["Storage (planos, facturas, licencias)"] + AUTH["Auth (usuarios y roles)"] + end + + subgraph AUTO["🔄 Automatización"] + MAKE["Make.com"] + ZAP["Zapier (larga cola)"] + end + + subgraph FIN["🧾 Núcleo fiscal/contable (EN VIVO)"] + HOLDED["Holded (canónico, API directa)"] + end + + subgraph COLAB["👥 Colaboración"] + GW["Google Workspace"] + ZOOM["Zoom"] + ATT["Attio (CRM)"] + end + + subgraph DEV["🛠️ Desarrollo"] + GH["GitHub"] + LIN["Linear"] + end + + UI --> API + AIR --> API + NOT --> API + API --> EF + EF --> PG + EF --> ST + EF --> AUTH + %% Las automatizaciones pasan por la API, no por la BD directamente + MAKE --> API + ZAP --> API + HOLDED <--> API + ATT <--> MAKE + COLAB <--> MAKE + GH --> EF + LIN -.gestiona.-> GH +``` + +--- + +## 4. Módulos del ERP (reordenados por sector) + +```mermaid +flowchart LR + CORE[("Núcleo Supabase\nDatos + Auth")] + CORE --- M1["🏗️ Obras / Proyectos\n(núcleo)"] + CORE --- M2["🏠 Promoción y\nVentas inmobiliarias"] + CORE --- M3["💰 Finanzas analítica\npor obra + fiscal ES"] + CORE --- M4["🧱 Compras y\nsubcontratas"] + CORE --- M5["📁 Documentación\n(planos, licencias)"] + CORE --- M6["🤝 CRM (Attio)"] + CORE --- M7["📣 Marketing\n(tardío)"] + CORE --- M8["📈 BI / Reporting"] + CORE --- M9["👤 RRHH y Nóminas"] +``` + +| # | Módulo | Conectores | Fase | +|---|--------|-----------|------| +| 1 | **Obras / Proyectos** (presupuesto vs real, certificaciones, planificación) | Supabase + Linear/Airtable + Make | **Fase 1** | +| 2 | **Promoción y ventas inmobiliarias** (promociones, unidades, reservas/arras, estado comercial) | Supabase + Attio/CRM inmobiliario + Make | **Fase 2** | +| 3 | **Finanzas analítica por obra + fiscalidad ES** | **Holded (en vivo)** + Supabase | **Fase 1** | +| 4 | **Compras y subcontratas** (aprovisionamiento por obra) | Supabase + Gmail + Make | Fase 2 | +| 5 | **Documentación de proyecto** (planos, licencias, CAD/BIM) | Drive + Notion + Supabase Storage | **Fase 1** | +| 6 | CRM comercial | Attio + Supabase | Fase 2 | +| 7 | Marketing | Supermetrics | Fase 3 | +| 8 | BI y reporting | Supabase + (Coupler.io a confirmar) | Fase 3 | +| 9 | RRHH y nóminas | Supabase + Google Workspace + gestoría | Fase 3 | + +--- + +## 5. Hoja de ruta por fases (corregida) + +- **Fase 0 — Cimientos:** Supabase (BD + Auth + RLS) + GitHub + estructura de + datos central + diseño en Figma + **backups y política RGPD**. +- **Fase 1 — Núcleo de obra:** Obras/Proyectos (costes por obra, certificaciones) + + Finanzas analítica + Documentación de proyecto + automatizaciones core. +- **Fase 2 — Comercial y compras:** Promoción/ventas inmobiliarias + CRM + + Compras/subcontratas. +- **Fase 3 — Avanzado:** Marketing (Supermetrics), BI/reporting, RRHH/nóminas. + +--- + +## 6. Núcleo fiscal/contable: DECIDIDO → Holded en vivo + +> **Decisión tomada (no pendiente).** Holded ya está **operativo** y es la +> **fuente canónica** de finanzas/contabilidad. No se reimplementa a medida. + +Motivo (sigue vigente): en construcción/promoción la contabilidad es +**analítica por obra** y la fiscalidad ES es exigente (IVA con **inversión del +sujeto pasivo** en ejecuciones de obra, retenciones, **SII/Verifactu**, +certificaciones). Holded lo resuelve sin reinventarlo. + +**Cómo encaja:** +- **Holded = sistema de registro fiscal/contable** (canónico), integrado por + **API directa**. +- **Supabase** = capa operativa del ERP a medida (Obras, Promoción, Compras, + Documentación) y, donde haga falta, espejo analítico para BI — **leyendo de + Holded, sin convertirse en segunda contabilidad**. +- **Automatización (Make/Zapier)** = opcional, solo para sync/alertas contra la + API; **nunca** una segunda fuente ni un segundo cálculo. + +### Reglas de coste y margen (únicas y comunes) +Para que haya **un solo número** por obra (sin pipelines divergentes): +- Coste imputado por **`projectid`** de Holded (**no** por tags). +- **Excluir confirming** del cómputo de coste. +- **Mano de obra propia = coste directo** de la obra. +- **Un único cálculo de margen** (venta − coste) por obra. +- Validación de referencia: cuadrar cifras con la obra **Palmeres** + (venta ≈ 1.385.494, compras ≈ 1.257.796). + +--- + +## 7. Modelo de datos inicial (esqueleto, a detallar) + +Entidades mínimas para arrancar Fase 0-1 (nombres orientativos): + +- `obras` (proyecto, cliente, presupuesto, fechas, estado) +- `partidas` / `capitulos` (presupuesto por obra) +- `certificaciones` (avance/facturación por obra) +- `proveedores` y `subcontratas` +- `pedidos_compra` (ligados a obra) +- `promociones` y `unidades` (inmuebles: estado comercial, precio, reserva) +- `clientes` / `contactos` +- `documentos` (planos, licencias; referencia a Storage) +- `usuarios` y `roles` (Auth + RLS) + +> Cada tabla con **Row Level Security** y acceso solo vía API/Edge Functions. + +--- + +## 8. Seguridad y cumplimiento (nuevo) + +- **RGPD:** base legal del tratamiento, registro de actividades, derechos ARCO, + encargados de tratamiento (proveedores cloud) y DPA firmados. +- **Backups:** copias automáticas de Supabase + retención definida + prueba de + restauración periódica. +- **Accesos:** roles por módulo, principio de mínimo privilegio, RLS en todas las + tablas, 2FA en cuentas críticas. +- **Entornos:** separación dev / producción. + +--- + +## 9. KPIs por fase (nuevo) + +- **Fase 1:** % obras con presupuesto vs real al día; nº certificaciones + gestionadas; tiempo de cierre mensual. +- **Fase 2:** unidades comercializadas; conversión de leads; plazo medio de + reserva→venta. +- **Fase 3:** coste de adquisición de cliente; cuadros de mando en uso. + +--- + +## 10. Plan de cuentas premium (qué pagar y cuándo) + +> Precios **aproximados** y orientativos por mes; confirmar en cada web. + +| Prioridad | Servicio | Plan | Coste aprox. | ¿Premium? | +|-----------|----------|------|--------------|-----------| +| 🔴 1 (ya) | **Supabase** | Pro | ~25 $/mes | SÍ. Es el núcleo. | +| 🔴 1 (ya) | **Make.com** | Core/Pro | ~9–16 $/mes | SÍ. Motor de automatización. | +| 🔴 1 (ya) | **Google Workspace** | Business Starter | ~6–7 €/usuario | SÍ. Correo + Drive. | +| 🔴 1 (ya) | **Holded** (núcleo fiscal, EN VIVO) | según módulos | ~consultar | SÍ. Ya en uso, es la fuente canónica (§6). | +| 🟠 2 | **Attio** | Plus/Pro | ~29 $/usuario | Al arrancar lo comercial. | +| 🟠 2 | **Airtable** | Team | ~20 $/usuario | Solo si hay vistas operativas. | +| 🟡 3 | **Supermetrics** | según fuentes | ~100–200+ €/mes | Solo con inversión publicitaria. | +| 🟡 3 | **Coupler.io** | según volumen | ~49–99 $/mes | Solo si se confirma su uso (sin MCP aquí). | +| 🟡 3 | **Notion / Figma** | Plus / Pro | ~10–15 $/usuario | Opcional. | +| 🟢 4 | **GitHub / Linear** | Free→Team | 0 → ~4–8 $ | Gratis basta al inicio. | +| 🟢 4 | **Zapier / Zoom** | Starter / Pro | ~14–30 $ | Solo si hacen falta. | + +### Inversión mínima para arrancar (Fase 0-1) +**Supabase Pro + Make Core + Google Workspace** (+ núcleo fiscal si se compra) → +del orden de **~40–50 €/mes** sin el ERP fiscal; con Holded/Odoo, sumar su plan. + +--- + +## 11. Cómo se gestiona + +- Se conecta cada herramienta **por módulo y por fase**, no todas el día 1. +- Cada servicio que requiera login se pide **solo al activarlo**. +- Código y configuración versionados en **GitHub** (idealmente, repo propio del + ERP — ver aviso de §0). + +--- + +## 12. Pendiente de confirmar + +- ✅ **Sector:** construcción · arquitectura · promoción inmobiliaria (resuelto). +- ✅ **Núcleo fiscal:** Holded en vivo, canónico (resuelto — §6). +- **Confirmar la fuente canónica única de Holded** (API directa en vivo) y dejar + el puente Make solo para automatización, o cancelarlo si no aporta. +- 🔒 **Rotación de claves de Holded** (las expuestas) y dónde viven (secretos/`.env`). +- **Nº de usuarios** previstos (para dimensionar planes por usuario). +- **Software actual** del que migrar datos (contabilidad, presupuestos de obra). +- **Repo propio** del ERP / "Cerebro" (sacarlo del fork de github-mcp-server).