Compare commits

15 Commits

Author SHA1 Message Date
minguezsanzjuanjose
1a53cd3918 feat: add postrequest script to save access_token in active environment
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 14:25:24 +02:00
minguezsanzjuanjose
35e1d38859 fix: use native Postman auth field and declare collection variables
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 14:11:39 +02:00
minguezsanzjuanjose
a064895689 fix: add missing initial migration for promociones app
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:53:14 +02:00
minguezsanzjuanjose
05e95f8880 chore: remove legacy postman_environments.json (replaced by environments/ folder)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:43:35 +02:00
minguezsanzjuanjose
af38d88876 fix: add Postman-compatible environment files (split by env, correct format)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:35:57 +02:00
minguezsanzjuanjose
02fa6247f1 fix: replace external saas_network with named network to auto-create on compose up
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:27:06 +02:00
minguezsanzjuanjose
428b745700 chore: add Makefile to auto-create saas_network before docker compose up
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:25:18 +02:00
minguezsanzjuanjose
f6892b2166 feat: expand Request_API with all engine endpoints
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Adds get_dataComplex, set_data2, set_data_batch methods for all 3 specialized APIs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:30:48 +02:00
minguezsanzjuanjose
d2c91d5196 feat: add Request_API dispatcher and internal API URLs
Replaces urllib http_client with proper Request_API class in general/request_api.py
using the requests library. Adds API_BACKOFFICE, API_COMUNICACIONES, API_DOCUMENTACION
settings pointing to Docker service names. Adds requests==2.32.3 to requirements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:24:46 +02:00
minguezsanzjuanjose
d735d73322 feat: add saas_network to docker-compose and http_client for internal API calls
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:05:08 +02:00
minguezsanzjuanjose
197cff011f docs: add createsuperuser step for Docker setup
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 00:10:09 +02:00
minguezsanzjuanjose
590e18e994 fix: forzar LF en entrypoint.sh + environments postman + limpiar variables coleccion
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- .gitattributes: añadir '*.sh text eol=lf' para evitar CRLF en Windows
- deployments/entrypoint.sh: renormalizado a LF
- postman_environments.json: entornos LOCAL/DEV/PROD en un único fichero
- postman_collection.json: eliminadas variables por defecto (vienen del entorno)
2026-04-17 23:01:36 +02:00
juanjo
c692ce2a61 sync
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-17 01:15:05 +02:00
juanjo
152e9c14ef Merge: docs flujo ramas 2026-04-17 01:15:05 +02:00
juanjo
99de5f06b5 docs: corregir flujo de ramas en CLAUDE.md (pre-dev → dev → master)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:15:04 +02:00
15 changed files with 319 additions and 37 deletions

3
.gitattributes vendored
View File

@@ -1,2 +1,5 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto
# Shell scripts: forzar LF siempre (evita CRLF en Windows que rompe Docker)
*.sh text eol=lf

View File

@@ -80,10 +80,17 @@ class MiVista(APIView):
pre-dev → dev → master pre-dev → dev → master
``` ```
- `pre-dev`: desarrollo activo - `pre-dev`: desarrollo activo — aquí entran todos los cambios
- `dev`: validación previa a producción - `dev`: validación previa a producción
- `master`: producción estable - `master`: producción estable
- Merges siempre con `--no-ff` - Merges siempre con `--no-ff`
- **NUNCA** propagar en sentido inverso (master → dev o dev → pre-dev)
- Secuencia correcta:
```bash
git checkout pre-dev && git merge <mi-cambio> --no-ff
git checkout dev && git merge pre-dev --no-ff && git push origin dev
git checkout master && git merge dev --no-ff && git push origin master
```
## Estructura de app Django ## Estructura de app Django

View File

@@ -140,9 +140,11 @@ python manage.py migrate <app_name> zero
--- ---
## Docker — Producción ## Docker — Local y Producción
```bash ```bash
cd deployments
# Construir e iniciar # Construir e iniciar
docker-compose up --build -d docker-compose up --build -d
@@ -158,6 +160,16 @@ docker-compose down -v
El contenedor expone el puerto **8000**. El contenedor expone el puerto **8000**.
### Crear superusuario (primera vez con Docker)
Una vez los contenedores estén corriendo:
```bash
docker-compose exec web sh -c "cd /app/app && python manage.py createsuperuser"
```
Introduce usuario, email y contraseña cuando lo pida. Estas credenciales son las que usarás para entrar en el panel web (`web_interno`) y en `/admin/`.
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). 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).
--- ---

View File

@@ -175,6 +175,11 @@ LOGGING = {
}, },
} }
# --- URLs de APIs internas (via red Docker saas_network) ---
API_BACKOFFICE = os.getenv('API_BACKOFFICE', 'http://api_backoffice:8001')
API_COMUNICACIONES = os.getenv('API_COMUNICACIONES', 'http://api_comunicaciones:8002')
API_DOCUMENTACION = os.getenv('API_DOCUMENTACION', 'http://api_documentacion:8003')
# --- CONFIGURACIONES PERSONALIZADAS DE LA APP --- # --- CONFIGURACIONES PERSONALIZADAS DE LA APP ---
# Leemos la variable del .env (cargado previamente con load_dotenv) # Leemos la variable del .env (cargado previamente con load_dotenv)

60
app/common/http_client.py Normal file
View File

@@ -0,0 +1,60 @@
import urllib.request
import json
import os
BACKOFFICE_URL = os.getenv('BACKOFFICE_URL', 'http://django_app_backoffice:8000')
COMUNICACIONES_URL = os.getenv('COMUNICACIONES_URL', 'http://django_app_comunicaciones:8000')
DOCUMENTACION_URL = os.getenv('DOCUMENTACION_URL', 'http://django_app_documentacion:8000')
def _post(url, payload):
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(
url, data=data,
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
# --- Backoffice ---
def backoffice_obtener_usuario(params):
return _post(f'{BACKOFFICE_URL}/api/usuarios/obtener/', params)
def backoffice_crear_usuario(params):
return _post(f'{BACKOFFICE_URL}/api/usuarios/guardar/', params)
def backoffice_obtener_cliente(params):
return _post(f'{BACKOFFICE_URL}/api/clientes/obtener/', params)
def backoffice_crear_cliente(params):
return _post(f'{BACKOFFICE_URL}/api/clientes/guardar/', params)
def backoffice_obtener_contrato(params):
return _post(f'{BACKOFFICE_URL}/api/contratos/obtener/', params)
def backoffice_crear_contrato(params):
return _post(f'{BACKOFFICE_URL}/api/contratos/guardar/', params)
# --- Comunicaciones ---
def comunicaciones_enviar_email(params):
return _post(f'{COMUNICACIONES_URL}/api/email/enviar/', params)
def comunicaciones_enviar_sms(params):
return _post(f'{COMUNICACIONES_URL}/api/sms/enviar/', params)
def comunicaciones_registrar_webhook(params):
return _post(f'{COMUNICACIONES_URL}/api/webhooks/registrar/', params)
# --- Documentacion ---
def documentacion_generar_pdf(params):
return _post(f'{DOCUMENTACION_URL}/api/generation/pdf/', params)
def documentacion_obtener_template(params):
return _post(f'{DOCUMENTACION_URL}/api/templates/obtener/', params)
def documentacion_guardar_documento(params):
return _post(f'{DOCUMENTACION_URL}/api/storage/guardar/', params)

100
app/general/request_api.py Normal file
View File

@@ -0,0 +1,100 @@
import json
import requests
from django.conf import settings
class Request_API:
"""Dispatcher de llamadas HTTP internas hacia las APIs especializadas."""
def _post(self, base_url, url_path, data):
url = base_url.rstrip('/') + '/' + url_path.lstrip('/')
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json.dumps(data), headers=headers, timeout=30)
return response.json(), response.status_code
# ------------------------------------------------------------------ #
# api_backoffice #
# ------------------------------------------------------------------ #
def backoffice_get_parameterized(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/get_parameterized/', data)
def backoffice_get_dataComplex(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/get_dataComplex/', data)
def backoffice_set_parameterized(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_parameterized/', data)
def backoffice_set_data2(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_data2/', data)
def backoffice_set_data_batch(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_data_batch/', data)
def backoffice_get_BBDD(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/get_BBDD/', data)
def backoffice_set_BBDD(self, data):
return self._post(settings.API_BACKOFFICE, 'api/general/set_BBDD/', data)
# ------------------------------------------------------------------ #
# api_comunicaciones #
# ------------------------------------------------------------------ #
def comunicaciones_get_parameterized(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/get_parameterized/', data)
def comunicaciones_get_dataComplex(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/get_dataComplex/', data)
def comunicaciones_set_parameterized(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_parameterized/', data)
def comunicaciones_set_data2(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_data2/', data)
def comunicaciones_set_data_batch(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_data_batch/', data)
def comunicaciones_get_BBDD(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/get_BBDD/', data)
def comunicaciones_set_BBDD(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/general/set_BBDD/', data)
def comunicaciones_enviar_email(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/email/enviar/', data)
def comunicaciones_enviar_sms(self, data):
return self._post(settings.API_COMUNICACIONES, 'api/sms/enviar/', data)
# ------------------------------------------------------------------ #
# api_documentacion #
# ------------------------------------------------------------------ #
def documentacion_get_parameterized(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/get_parameterized/', data)
def documentacion_get_dataComplex(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/get_dataComplex/', data)
def documentacion_set_parameterized(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_parameterized/', data)
def documentacion_set_data2(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_data2/', data)
def documentacion_set_data_batch(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_data_batch/', data)
def documentacion_get_BBDD(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/get_BBDD/', data)
def documentacion_set_BBDD(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/general/set_BBDD/', data)
def documentacion_generar_pdf(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/generation/pdf/', data)
def documentacion_guardar(self, data):
return self._post(settings.API_DOCUMENTACION, 'api/storage/guardar/', data)

View File

@@ -0,0 +1,26 @@
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Promocion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=255)),
('fecha_inicio', models.DateField(blank=True, null=True)),
('fecha_modificacion', models.DateField(blank=True, null=True)),
('descripcion', models.TextField(blank=True, null=True)),
('activo', models.BooleanField(default=True)),
('categoria_id', models.IntegerField(blank=True, null=True)),
],
options={
'db_table': 'promociones',
},
),
]

View File

@@ -5,3 +5,4 @@ python-dotenv==1.0.1
djangorestframework djangorestframework
django-cors-headers django-cors-headers
djangorestframework-simplejwt djangorestframework-simplejwt
requests==2.32.3

15
deployments/Makefile Normal file
View File

@@ -0,0 +1,15 @@
NETWORK = saas_network
.PHONY: network up down logs
network:
docker network inspect $(NETWORK) >/dev/null 2>&1 || docker network create $(NETWORK)
up: network
docker compose up --build -d
down:
docker compose down
logs:
docker compose logs -f

View File

@@ -36,6 +36,13 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
networks:
- default
- saas_network
volumes: volumes:
postgres_data: postgres_data:
networks:
saas_network:
name: saas_network

View File

@@ -5,3 +5,4 @@ python-dotenv==1.0.1
djangorestframework djangorestframework
django-cors-headers django-cors-headers
djangorestframework-simplejwt djangorestframework-simplejwt
requests==2.32.3

View File

@@ -0,0 +1,11 @@
{
"id": "x8jh10qysfo",
"name": "DEV",
"values": [
{ "key": "base_url", "value": "https://dev.v-encore-lab.com", "type": "default", "enabled": true },
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
],
"_postman_variable_scope": "environment"
}

View File

@@ -0,0 +1,11 @@
{
"id": "ftt19d5vxuj",
"name": "LOCAL",
"values": [
{ "key": "base_url", "value": "http://localhost:8000", "type": "default", "enabled": true },
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
],
"_postman_variable_scope": "environment"
}

View File

@@ -0,0 +1,11 @@
{
"id": "3ckrklvkf27",
"name": "PROD",
"values": [
{ "key": "base_url", "value": "https://v-encore-lab.com", "type": "default", "enabled": true },
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
],
"_postman_variable_scope": "environment"
}

View File

@@ -6,31 +6,8 @@
"_postman_id": "django-core-base-collection" "_postman_id": "django-core-base-collection"
}, },
"variable": [ "variable": [
{ { "key": "access_token", "value": "", "type": "string" },
"key": "base_url", { "key": "refresh_token", "value": "", "type": "string" }
"value": "http://localhost:8000",
"type": "string"
},
{
"key": "access_token",
"value": "",
"type": "string"
},
{
"key": "refresh_token",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "admin",
"type": "string"
},
{
"key": "password",
"value": "admin",
"type": "string"
}
], ],
"item": [ "item": [
{ {
@@ -51,6 +28,29 @@
"}" "}"
] ]
} }
},
{
"listen": "postrequest",
"script": {
"type": "text/javascript",
"exec": [
"// Comprobamos si la respuesta es exitosa",
"if (pw.response.status === 200) {",
" // Si el body ya es un objeto no hace falta JSON.parse",
" const body = typeof pw.response.body === 'string'",
" ? JSON.parse(pw.response.body)",
" : pw.response.body;",
"",
" if (body.access) {",
" // Guardamos el token en el entorno actual",
" pw.env.set(\"access_token\", body.access);",
" console.log(\"✅ Token guardado correctamente\");",
" } else {",
" console.log(\"❌ No se encontró el campo 'access' en la respuesta\");",
" }",
"}"
]
}
} }
], ],
"request": { "request": {
@@ -77,10 +77,13 @@
{ {
"name": "Obtener Promoción", "name": "Obtener Promoción",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" }, { "key": "Content-Type", "value": "application/json" }
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
@@ -101,10 +104,13 @@
{ {
"name": "Ejecutar Automatizaciones", "name": "Ejecutar Automatizaciones",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" }, { "key": "Content-Type", "value": "application/json" }
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
@@ -120,10 +126,13 @@
{ {
"name": "Historial de Ejecuciones", "name": "Historial de Ejecuciones",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" }, { "key": "Content-Type", "value": "application/json" }
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
@@ -139,10 +148,13 @@
{ {
"name": "Estado del Módulo", "name": "Estado del Módulo",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}", "type": "string" }]
},
"method": "POST", "method": "POST",
"header": [ "header": [
{ "key": "Content-Type", "value": "application/json" }, { "key": "Content-Type", "value": "application/json" }
{ "key": "Authorization", "value": "Bearer {{access_token}}" }
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",