diff --git a/README.md b/README.md new file mode 100644 index 0000000..27bd2b7 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# FRASA Ecosystem + +Ecosistema comercial de Frasa Desarrollos S.A.P.I. de C.V. + +## Componentes + +| Componente | Ruta | Descripción | +|---|---|---| +| `apps/landing` | `/` | Entrada al ecosistema | +| `apps/cotizador` | `/quote/` | Cotizador financiero (4 propuestas, PDF ANEXO A) | +| `apps/alta-clientes` | `/client/` | Alta de clientes (wizard, firma digital, PDFs PLD) | +| `apps/cobranza` | `/pay/` | Planes de enganche, vencidos, estados de cuenta | +| `api-gateway` | `/api/` | Express 5 + Prisma + integración Monday.com | + +## Desarrollo local + +Cada app es un proyecto Vite independiente: `npm install && npm run dev` (o `npm run preview` tras `npm run build`). Tests: `npx vitest run` en cada paquete. El API: `cd api-gateway && npm install && npx prisma migrate dev && npm run dev`. + +Variables de entorno: copiar `.env.example` a `.env` y completar (el `.env` nunca se commitea). + +## Deploy (servidor Ubuntu + Docker) + +```bash +git clone https://git.consultoria-as.com/consultoria-as/FRASA-CAS.git /opt/frasa-ecosystem +cd /opt/frasa-ecosystem +cp .env.example .env && nano .env # completar secretos + +# SSL: webroot (funciona con nginx corriendo y renueva sin downtime) +docker compose up -d nginx # primer arranque SIN ssl: ver nota abajo +certbot certonly --webroot -w /var/www/certbot -d frasadesarrollos.ddns.net + +docker compose build && docker compose up -d +docker compose logs -f api +curl https://frasadesarrollos.ddns.net/api/health +``` + +Nota primer arranque: mientras no existan los certs, comenta el bloque `server 443` de +`nginx/nginx.conf` (o usa un cert autofirmado temporal); después de expedir, restaura y +`docker compose restart nginx`. Renovación: `certbot renew --deploy-hook "docker compose -f /opt/frasa-ecosystem/docker-compose.yml restart nginx"`. + +Antes del deploy, cambiar en `apps/landing/src/data/modulos.ts` las URLs absolutas de +preview por rutas relativas (`/quote/`, `/client/`, `/pay/`) y rehacer el build. + +Backup: `scripts/backup.sh` (cron sugerido `17 2 * * *`). + +## Monday.com + +Workspace `Frasa-CAS`. Tableros: Contactos, Desarrollos, Unidades (inventario importado), Leads, Oportunidades, Apartado, Ventas. Los board IDs están en `.env.example`. Webhook: `POST /api/webhooks/monday` (responde el challenge de activación). + +## Pendientes conocidos + +- Migrar Prisma de SQLite a Postgres 16 (dinero a `Decimal`). +- Auth JWT, helmet, rate-limit, CORS restringido (hoy: red interna). +- Self-hostear worker/wasm de tesseract.js (OCR sin internet). +- Automatizaciones Monday (round-robin, recordatorios 10 días) y cron de cobranza en el API. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3aff82b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,78 @@ +# FRASA Ecosystem — orquestación de producción +# Nota: el API usa SQLite (volumen api_data) hasta la migración de Prisma a Postgres. +# Postgres y Redis quedan listos pero SOLO en la red interna (sin puertos expuestos). + +services: + nginx: + image: nginx:alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - /etc/letsencrypt:/etc/letsencrypt:ro # certs siempre al día tras certbot renew + - certbot_webroot:/var/www/certbot # challenge HTTP-01 + depends_on: + - api + - landing + - cotizador + - alta-clientes + - cobranza + + api: + build: ./api-gateway + restart: unless-stopped + env_file: .env + environment: + NODE_ENV: production + PORT: 3000 + DATABASE_URL: "file:/data/dev.db" # TODO: postgres cuando se migre el provider + UPLOADS_DIR: "/data/uploads" + volumes: + - api_data:/data + # Sin puerto expuesto: solo se alcanza via nginx en /api + + landing: + build: ./apps/landing + restart: unless-stopped + + cotizador: + build: ./apps/cotizador + restart: unless-stopped + + alta-clientes: + build: ./apps/alta-clientes + restart: unless-stopped + + cobranza: + build: ./apps/cobranza + restart: unless-stopped + + postgres: + image: postgres:16-alpine + restart: unless-stopped + env_file: .env + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + # Sin puerto expuesto al host + + redis: + image: redis:7-alpine + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + # Sin puerto expuesto al host + +volumes: + api_data: + postgres_data: + certbot_webroot: diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..4f4046c --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,63 @@ +# FRASA Ecosystem — reverse proxy +# Las apps se construyeron con base path (/quote/, /client/, /pay/); +# el proxy conserva la URI completa (sin slash final en proxy_pass). + +# HTTP → HTTPS (excepto el challenge de Let's Encrypt) +server { + listen 80; + server_name frasadesarrollos.ddns.net; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + server_name frasadesarrollos.ddns.net; + + ssl_certificate /etc/letsencrypt/live/frasadesarrollos.ddns.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/frasadesarrollos.ddns.net/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1.2 TLSv1.3; + + # Uploads de hasta 10MB (expedientes de clientes) + client_max_body_size 10m; + + # API Gateway + location /api/ { + proxy_pass http://api:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Webhooks de Monday (mismo upstream, ruta explícita para claridad) + location = /api/webhooks/monday { + proxy_pass http://api:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Apps web (cada contenedor sirve su SPA con su base path) + location /quote/ { proxy_pass http://cotizador:80; proxy_set_header Host $host; } + location /client/ { proxy_pass http://alta-clientes:80; proxy_set_header Host $host; } + location /pay/ { proxy_pass http://cobranza:80; proxy_set_header Host $host; } + + # Landing (raíz) + location / { + proxy_pass http://landing:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..fc02d4a --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Backup diario FRASA — cron: 17 2 * * * /opt/frasa-ecosystem/scripts/backup.sh +set -euo pipefail + +BACKUP_DIR="/backups" +FECHA=$(date +%Y%m%d) +RETENCION_DIAS=30 +mkdir -p "$BACKUP_DIR" + +cd "$(dirname "$0")/.." + +# 1. Base de datos SQLite del API (mientras no se migre a Postgres) +docker compose exec -T api sh -c 'cat /data/dev.db' > "$BACKUP_DIR/frasa_db_$FECHA.sqlite" || true + +# 2. Postgres (cuando el API migre; hoy falla en silencio si no hay datos) +docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-frasa_user}" "${POSTGRES_DB:-frasa_db}" \ + > "$BACKUP_DIR/frasa_pg_$FECHA.sql" 2>/dev/null || rm -f "$BACKUP_DIR/frasa_pg_$FECHA.sql" + +# 3. Expedientes subidos (INE, firmas, comprobantes) +docker compose exec -T api sh -c 'cd /data && tar czf - uploads' > "$BACKUP_DIR/uploads_$FECHA.tar.gz" || true + +# 4. Retención +find "$BACKUP_DIR" -name "frasa_*" -mtime +$RETENCION_DIAS -delete +find "$BACKUP_DIR" -name "uploads_*" -mtime +$RETENCION_DIAS -delete + +echo "[$(date -Is)] backup completo en $BACKUP_DIR"