diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a57cd00 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,103 @@ +# Convenciones del Ecosistema V-Encore Lab + +## Ecosistema de microservicios + +| Repo | Rol | Puerto | +|------|-----|--------| +| `django-core-base` | Hub orquestador principal | 8000 | +| `api_backoffice` | Consulta y gestión de BD | 8001 | +| `api_comunicaciones` | Emails, notificaciones, webhooks | 8002 | +| `api_documentacion` | Generación y gestión de documentos | 8003 | +| `web_interno` | Panel de gestión (React + Ant Design) | 3000 | + +## Stack Django + +- Django 5.0 + DRF + SimpleJWT +- Apps bajo `app/`. Prefijo `/api/` en todas las URLs salvo `admin/`. +- Patrón 3 capas: **URL → View → Action** +- SQL con `connection.cursor()` y placeholders (`%s`). Nunca concatenar strings. + +## Patrón de vistas — 4 bloques obligatorios + +```python +from general.utilidades.acciones import LogService + +class MiVista(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + path = '/mi-app/mi-endpoint/' + + # Bloque 1 — Inicio Log + log_id = LogService.gestionar_log(self, request, path=path) + + try: + # Bloque 2 — Data Cleaning + data = request.data + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_request=data, status_code=100) + params = {'campo': data.get('campo')} + except Exception as error: + response = {'error': str(error)} + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_response=response, status_code=400) + return JsonResponse(response, status=400) + + try: + # Bloque 3 — Action Call + resultado = mi_accion(params) + # Bloque 4 — Cierre Log + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_response=resultado, status_code=200) + return JsonResponse(resultado, status=200) + except Exception as error: + response = {'error': str(error)} + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_response=response, status_code=500) + return JsonResponse(response, status=500) +``` + +## LogService — reglas + +- Siempre llamar como `LogService.gestionar_log(self, request, ...)` — pasar `self` de la vista. +- Primera llamada (sin `log_id`): crea el registro, devuelve el `log_id`. +- Llamadas siguientes: actualizar con `log_id=log_id`. +- `body_request` y `body_response` se serializan con `DjangoJSONEncoder` — soporta `datetime.date`, `Decimal`, etc. +- `status_code` como entero. Usar `is not None` para comprobar (0 es válido). + +## Base de datos + +- `DB_HOST` definido → PostgreSQL +- `DB_HOST` no definido → SQLite en `app/data/db.sqlite3` +- Migraciones siempre desde `app/`: `cd app && python manage.py migrate` +- SQL INSERT con psycopg2: usar `INSERT ... RETURNING id` + `cursor.fetchone()`. **Nunca `cursor.lastrowid`**. +- Booleans en psycopg2: pasar `True`/`False`, no `1`/`0`. + +## Flujo de ramas + +``` +pre-dev → dev → master +``` + +- `pre-dev`: desarrollo activo +- `dev`: validación previa a producción +- `master`: producción estable +- Merges siempre con `--no-ff` + +## Estructura de app Django + +``` +app/ +├── api_config/ # settings.py, urls.py, wsgi.py +├── general/ # LogService, utils — NO tiene modelos propios +├── backend_admin/ # Modelo Log (audit_logs), admin panel +├── common/ # Modelos y utilidades compartidas entre apps +├── / # Una app por dominio de negocio +├── data/ # SQLite (gitignored, solo .gitkeep versionado) +└── manage.py +``` + +## Fixtures + +- `loaddata` ignora `auto_now_add=True` — incluir fechas explícitas en el JSON o falla con NOT NULL en PostgreSQL. diff --git a/README.md b/README.md index 2e8bb40..0c1e763 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,312 @@ -# django-core-base +# django-core-base · API Hub Orquestador -// V-Encore Lab: Sistema Automatizado v1.0.4 +> V-Encore Lab — Microservicio principal. Actúa como orquestador y punto de entrada central del ecosistema SaaS. +> Puerto por defecto: **8000** -## 🚀 Inicio Rápido (Desarrollo Local) +--- + +## Tabla de contenidos + +1. [Requisitos previos](#requisitos-previos) +2. [Inicio rápido — Desarrollo local](#inicio-rápido--desarrollo-local) +3. [Variables de entorno](#variables-de-entorno) +4. [Base de datos](#base-de-datos) +5. [Docker — Producción](#docker--producción) +6. [Estructura del proyecto](#estructura-del-proyecto) +7. [Endpoints principales](#endpoints-principales) +8. [Patrón LogService](#patrón-logservice) +9. [Flujo de ramas](#flujo-de-ramas) +10. [Mantenimiento](#mantenimiento) + +--- + +## Requisitos previos + +| Herramienta | Versión mínima | +|-------------|---------------| +| Python | 3.11 | +| pip | 23+ | +| Docker | 24+ | +| Docker Compose | v2 | +| Git | 2.40+ | + +--- + +## Inicio rápido — Desarrollo local -### 1. Clonar y Configurar ```bash +# 1. Clonar git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git cd django-core-base -cp .env.example .env -``` -### 2. Instalar Dependencias -```bash +# 2. Entorno virtual +python -m venv .venv +source .venv/bin/activate # Linux/Mac +.venv\Scripts\activate # Windows + +# 3. Dependencias pip install -r deployments/requirements.txt -``` -### 3. Migraciones de Base de Datos -```bash -# Crear y aplicar migraciones para todos los modelos -python manage.py makemigrations +# 4. Variables de entorno +cp .env.example .env +# Editar .env según el entorno (ver sección Variables de entorno) + +# 5. Migraciones (SQLite en local) +cd app python manage.py migrate -# Opcional: Crear superusuario +# 6. Superusuario (primera vez) +python manage.py createsuperuser + +# 7. Servidor de desarrollo +python manage.py runserver 0.0.0.0:8000 +``` + +Accesos: +- API: http://localhost:8000/api/ +- Admin: http://localhost:8000/admin/ + +--- + +## Variables de entorno + +Copia `.env.example` a `.env` y ajusta los valores: + +| Variable | Descripción | Ejemplo / Default | +|----------------|--------------------------------------------------|----------------------------| +| `SECRET_KEY` | Clave secreta Django | `django-insecure-...` | +| `DEBUG` | Modo debug | `True` (dev) / `False` (prod) | +| `ALLOWED_HOSTS`| Hosts permitidos (separados por coma) | `localhost,127.0.0.1` | +| `DB_HOST` | Host PostgreSQL. **Si no se define → SQLite** | `localhost` / vacío | +| `DB_PORT` | Puerto PostgreSQL | `5432` | +| `DB_NAME` | Nombre de la base de datos | `vencorelab` | +| `DB_USER` | Usuario PostgreSQL | `postgres` | +| `DB_PASSWORD` | Contraseña PostgreSQL | `postgres` | + +--- + +## Base de datos + +### Desarrollo local — SQLite + +Si `DB_HOST` **no** está definido en `.env`, Django usa automáticamente SQLite: + +``` +app/data/db.sqlite3 +``` + +```bash +cd app +python manage.py migrate # crea app/data/db.sqlite3 python manage.py createsuperuser ``` -### 4. Correr el Servidor -```bash -python manage.py runserver +La carpeta `app/data/` está en `.gitignore` (solo se versiona `.gitkeep`). + +### Producción — PostgreSQL + +Define `DB_HOST` en `.env` para activar el driver PostgreSQL: + +```env +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=vencorelab +DB_USER=postgres +DB_PASSWORD=supersecret ``` -Abrir http://localhost:8000 - -## 🐳 Docker (Producción/Desarrollo) - ```bash -docker-compose up --build +cd app +python manage.py migrate ``` -Acceder a: -- App: http://localhost:8000 -- Admin: http://localhost:8000/admin/ -- DB: localhost:5432 (Postgres) - -## 📋 Comandos Django Comunes +### Comandos útiles de migración ```bash -# Verificar configuración -python manage.py check +cd app -# Recopilar static files -python manage.py collectstatic --noinput +# Ver migraciones pendientes +python manage.py showmigrations -# Test -python manage.py test +# Crear migraciones de una app +python manage.py makemigrations + +# Aplicar todas las migraciones +python manage.py migrate + +# Revertir migraciones de una app al estado inicial +python manage.py migrate zero ``` -## 🔧 Estructura del Proyecto +--- -``` -├── apps/ # Aplicaciones Django -│ ├── backend_admin/ -│ ├── common/ -│ └── promociones/ -├── core/ # Configuración principal -├── deployments/ # Docker, requirements prod -└── manage.py +## Docker — Producción + +```bash +# Construir e iniciar +docker-compose up --build -d + +# Ver logs +docker-compose logs -f + +# Parar +docker-compose down + +# Parar y eliminar volúmenes (¡cuidado en producción!) +docker-compose down -v ``` -## .env Variables -Ver `.env.example` para configuración. \ No newline at end of file +El contenedor expone el puerto **8000**. + +Para producción con PostgreSQL, asegúrate de que `.env` tenga `DB_HOST` apuntando al host correcto (puede ser el nombre del servicio en la red Docker). + +--- + +## Estructura del proyecto + +``` +django-core-base/ +├── app/ +│ ├── api_config/ # Configuración Django (settings, urls, wsgi) +│ ├── general/ # App transversal +│ │ └── utilidades/ +│ │ ├── acciones.py # LogService — auditoría centralizada +│ │ └── utils.py # Utilidades HTTP (get_client_ip, etc.) +│ ├── backend_admin/ # App admin: modelo Log, endpoints de gestión +│ ├── common/ # Modelos y utilidades compartidas +│ ├── promociones/ # App de ejemplo +│ ├── automatizados/ # Endpoints para Jenkins/automatizaciones +│ ├── data/ # Directorio de la BD SQLite (gitignored) +│ │ └── .gitkeep +│ └── manage.py +├── deployments/ +│ ├── requirements.txt +│ └── Dockerfile +├── docker-compose.yml +├── .env.example +└── README.md +``` + +--- + +## Endpoints principales + +| Método | Ruta | Descripción | Auth | +|--------|-------------------------------|-----------------------------------|--------------| +| POST | `/api/token/` | Obtener JWT (login) | No | +| POST | `/api/token/refresh/` | Renovar access token | No | +| GET | `/admin/` | Panel de administración Django | Session | +| POST | `/api/promociones/obtener/` | Consultar promociones | JWT Bearer | +| GET | `/api/general/health/` | Health check | No | + +Autenticación: `Authorization: Bearer ` + +--- + +## Patrón LogService + +Todas las vistas deben usar `LogService` para auditoría: + +```python +from general.utilidades.acciones import LogService + +class MiVista(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + path = '/mi-app/mi-endpoint/' + + # Bloque 1 — Inicio log + log_id = LogService.gestionar_log(self, request, path=path) + + try: + # Bloque 2 — Limpieza de datos + data = request.data + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_request=data, status_code=100) + params = {'campo': data.get('campo')} + except Exception as error: + response = {'error': str(error)} + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_response=response, status_code=400) + return JsonResponse(response, status=400) + + try: + # Bloque 3 — Acción + resultado = mi_accion(params) + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_response=resultado, status_code=200) + return JsonResponse(resultado, status=200) + except Exception as error: + response = {'error': str(error)} + LogService.gestionar_log(self, request, log_id=log_id, + path=path, body_response=response, status_code=500) + return JsonResponse(response, status=500) +``` + +`LogService` registra automáticamente en la tabla `audit_logs` el usuario, IP, path, request/response y status code. + +--- + +## Flujo de ramas + +``` +pre-dev → dev → master +``` + +- `pre-dev`: desarrollo activo, integración de features +- `dev`: validación previa a producción +- `master`: rama de producción estable + +```bash +# Crear feature +git checkout pre-dev +git checkout -b feature/mi-feature + +# Mergear a pre-dev +git checkout pre-dev +git merge feature/mi-feature --no-ff + +# Promover a dev +git checkout dev +git merge pre-dev --no-ff + +# Promover a master +git checkout master +git merge dev --no-ff +``` + +--- + +## Mantenimiento + +```bash +# Limpiar sesiones expiradas +cd app && python manage.py clearsessions + +# Ver estado de la BD +cd app && python manage.py dbshell + +# Backup SQLite (desarrollo) +cp app/data/db.sqlite3 app/data/db.sqlite3.bak + +# Backup PostgreSQL (producción) +pg_dump -U postgres vencorelab > backup_$(date +%Y%m%d).sql + +# Actualizar dependencias +pip list --outdated +pip install -U +pip freeze > deployments/requirements.txt + +# Reiniciar contenedor Docker +docker-compose restart web + +# Ver logs del contenedor +docker-compose logs -f web --tail=100 +``` + +--- + +> **V-Encore Lab** — Sistema Automatizado v1.0 +> Repositorio: `Proyecto-SaaS/django-core-base` diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml index 5b4ed6c..3cc71ee 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -1,45 +1,41 @@ version: '3.8' services: - gitea-db: - image: postgres:15 - # Usará el nombre de tu .env (django_db_local) - container_name: ${DB_CONTAINER_NAME:-django_db_dev} - restart: always + db: + image: postgres:15-alpine + container_name: ${DB_CONTAINER_NAME:-django_core_db} + restart: unless-stopped environment: - POSTGRES_DB: ${DB_NAME:-gitea} - POSTGRES_USER: ${DB_USER:-gitea} - POSTGRES_PASSWORD: ${DB_PASSWORD:-gitea} + POSTGRES_DB: ${DB_NAME:-django_core_db} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} volumes: - postgres_data:/var/lib/postgresql/data - # --- ESTO ES LO QUE FALTA --- ports: - "${DATABASE_EXPOSE_PORT:-5432}:5432" - # ---------------------------- healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gitea} -d ${DB_NAME:-gitea}"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-django_core_db}"] interval: 5s timeout: 5s - retries: 5 + retries: 10 + start_period: 10s web: build: context: .. dockerfile: deployments/Dockerfile - container_name: ${APP_CONTAINER_NAME:-django_app_dev} - restart: always + container_name: ${APP_CONTAINER_NAME:-django_core_app} + restart: unless-stopped env_file: - - .env + - ../.env environment: - - DEBUG=${DEBUG_MODE:-1} - # IMPORTANTE: Este nombre debe coincidir con el nombre del servicio arriba (gitea-db) - - DB_HOST=gitea-db + - DB_HOST=db - DB_PORT=5432 ports: - "${PORT:-8000}:8000" depends_on: - gitea-db: + db: condition: service_healthy volumes: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/deployments/entrypoint.sh b/deployments/entrypoint.sh index fc401c5..6d8e257 100644 --- a/deployments/entrypoint.sh +++ b/deployments/entrypoint.sh @@ -3,16 +3,39 @@ # Salir inmediatamente si un comando falla set -e +# --- Esperar a PostgreSQL si estamos en modo BD remota --- +if [ -n "$DB_HOST" ]; then + echo "--> Esperando a PostgreSQL en $DB_HOST:${DB_PORT:-5432}..." + until python -c " +import sys, psycopg2, os +try: + psycopg2.connect( + host=os.environ['DB_HOST'], + port=os.environ.get('DB_PORT', 5432), + user=os.environ['DB_USER'], + password=os.environ['DB_PASSWORD'], + dbname=os.environ['DB_NAME'] + ) + sys.exit(0) +except Exception: + sys.exit(1) +" 2>/dev/null; do + echo " PostgreSQL no disponible, reintentando en 2s..." + sleep 2 + done + echo "--> PostgreSQL listo." +fi + echo "--> Ejecutando migraciones..." -# Esto asegura que si hay cambios en models.py, se generen y apliquen las tablas -python manage.py makemigrations --noinput python manage.py migrate --noinput -echo "--> Cargando datos de prueba..." -# Este comando busca archivos JSON en las carpetas 'fixtures' de tus apps -# Usamos || true para que si el archivo no existe o ya están cargados, el contenedor no se detenga -python manage.py loaddata semillas || echo "Aviso: No se pudieron cargar las semillas (fichero no encontrado o error de formato)." +echo "--> Cargando semillas (si existen)..." +python manage.py loaddata semillas 2>/dev/null || echo " Sin semillas, continuando." -echo "--> Arrancando el servidor Django..." -# Usamos exec para que Django sea el proceso principal (PID 1) y reciba señales de Docker -exec python manage.py runserver 0.0.0.0:8000 \ No newline at end of file +echo "--> Arrancando servidor con Gunicorn..." +exec gunicorn api_config.wsgi:application \ + --bind 0.0.0.0:8000 \ + --workers 2 \ + --timeout 120 \ + --access-logfile - \ + --error-logfile -