Sistema de licitaciones con verificación automática de PDFs, revisión por niveles y chat asíncrono entre postores (BIDDER) y personal interno (WORKER).
Última rama base:
mainVideo entrega 1: https://youtu.be/5adWhOlFJDo
Video entrega 2: https://youtu.be/NIh8oXUAUA8
Video entrega 3, parte 1: https://youtu.be/bwoR_Vqz4xM
Video entrega 3, parte 2: https://www.youtube.com/watch?v=nSC2gh13e7M
- Resumen
- Arquitectura
- Tecnologías usadas
- Requisitos
- Configuración de entorno
- Ejecución con Docker
- Pruebas (Cypress)
- CI/CD con Jenkins
- Solución de problemas (FAQ)
LicitAgil gestiona el ciclo completo de una licitación:
- Carga y verificación automática de PDF (estructura, tamaño/páginas).
- Revisión por niveles (aprobación +1/rechazo) por WORKER/ADMIN con tracking de estado y nivel requerido.
- Descarga segura mediante URLs prefirmadas (S3-compatible).
- Autenticación y rutas protegidas por rol (
ADMIN,BIDDER,WORKER). - Chat asíncrono:
- El BIDDER abre un chat (botón flotante con badge de no leídos).
- El WORKER responde desde una pantalla dedicada (lista + conversación).
- Contadores de no leídos por participante, polling liviano, preview del último mensaje.
- Seed de roles/departamentos al arrancar.
frontend (Vite/React)
├─ rutas por rol (RequireRole / AuthedLayout)
├─ módulo bidder (aplicar, listar, chat flotante)
└─ módulo worker (pantalla de chats)
backend (FastAPI)
├─ auth + roles + departamentos (seed)
├─ tender-requests + archivos + presign (S3)
└─ chats + messages (BIDDER/WORKER)
infra
├─ docker-compose (mongo, api, front, mongo-express)
└─ Jenkins (pipeline desde SCM, build + up + smoke)
Frontend — Vite + React (TypeScript)
react-router-dom, hooks, CSS simple.- Descarga por presigned URL.
- Botón flotante de chat con polling a
/chats/unread_count.
Backend — FastAPI + MongoDB
- Uvicorn/FastAPI async.
- Pydantic para modelos/validación.
- MongoDB async (usuarios, roles, departamentos, tenders, chats y mensajes).
- Presign GET/PUT a S3.
Pruebas — Cypress
- E2E de login, navegación por rol, creación/validación de tender y chat.
- Docker Desktop (recomendado para ejecución local)
- (Modo dev opcional) Node 18+ y Python 3.11+
- Puertos:
- Frontend: 8080
- Backend: 8000
- Mongo Express: 8081
VITE_API_URL=http://localhost:8000Archivo en la raíz del proyecto (donde está
docker-compose.yml).
# Mongo (el servicio del compose se llama "mongo")
MONGODB_URI=mongodb://mongo:27017
MONGODB_DB=miapp
# S3 (o compatible)
AWS_REGION=us-east-2
S3_BUCKET=ppdsfiles
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxNota: en CI (Jenkins) estas variables se inyectan desde credenciales (ver sección Jenkins).
Build + up:
docker compose up --buildBajar y borrar volúmenes (p. ej., limpiar DB):
docker compose down -vRevisión manual
- Frontend: http://localhost:8080
- Backend (docs): http://localhost:8000/docs
- Mongo Express: http://localhost:8081
En cypress/support/commands.ts:
cy.loginUI(): login por UI.cy.seedSession(user)/cy.seedSessionWithRole(roleKey, user?): “inyecta” sesión enlocalStorage.cy.loginAsBidder()/cy.loginAsAdmin(): sesión Cypress “cacheada” por correo.
Estos comandos usan
Cypress.env('API')(default:http://localhost:8000).
| Spec | Cobertura | Notas |
|---|---|---|
cypress/e2e/bidder.nav-permissions.cy.ts |
Navegación BIDDER + bloqueo rutas admin | Usa loginAsBidder() |
cypress/e2e/bidder.smoke.cy.ts |
Smoke full-stack BIDDER (login real + wizard) | Intercepta /departments |
cypress/e2e/bidder.apply.stub.cy.ts |
Flujo postulación con stubs (presign → PUT → validate → create → attach) | Éxito determinista |
cypress/e2e/bidder.apply.validation.cy.ts |
Validación PDF falla → NO crear tender | Asegura 0 llamadas a /tender-requests |
cypress/e2e/bidder.apply.errors.cy.ts |
Errores presign/PUT/validate/create/attach | Verifica mensajes de error y flujo |
cypress/e2e/admin.review.cy.ts |
ADMIN revisa: aprueba dos veces la primera tender / otra la rechaza | Stubs de prompt/confirm con cy.on('window:...') |
cypress/e2e/chat.flow.cy.ts |
Chat BIDDER ↔ WORKER (startChat, envío y badge unread) | Usa el FAB en BIDDER y la pantalla en WORKER |
Ejecutar:
# UI interactiva
npx cypress open
# Headless
npx cypress run --e2ePara correr en CI, setear
CYPRESS_BASE_URL=http://localhost:8080(si aplica) yCypress.env('API').
- Jenkins LTS (JDK17 ok).
- El contenedor/servidor Jenkins debe tener acceso al Docker daemon del host (montar
/var/run/docker.socksi Jenkins corre en contenedor). - Docker 24+ y
docker compose v2.
Compose de Jenkins (independiente):
version: "3.8"
services:
jenkins:
image: jenkins/jenkins:lts-jdk17
container_name: jenkins
user: "0:0"
ports:
- "8082:8080"
- "50000:50000"
volumes:
- jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
volumes:
jenkins_home:Crear Secret text globales (Manage Jenkins → Credentials):
| ID (exacto) | Contenido |
|---|---|
aws-access-key-id |
AWS_ACCESS_KEY_ID |
aws-secret-access-key |
AWS_SECRET_ACCESS_KEY |
aws-region |
AWS_REGION (ej: us-east-2) |
s3-bucket |
S3_BUCKET (ej: ppdsfiles) |
mongo-uri |
MONGODB_URI (ej: mongodb://mongo:27017) |
mongo-db |
MONGODB_DB (ej: miapp) |
vite-api-url |
VITE_API_URL (ej: http://localhost:8000) |
Los IDs deben coincidir con lo que usa el
Jenkinsfile.
- New Item → Pipeline.
- Definition: Pipeline script from SCM.
- SCM: Git →
https://github.com/Waracho/project_licit - Branch:
*/main(o la rama que uses). - Script Path:
Jenkinsfile - (Opcional) Triggers: GitHub hook trigger o Build periodically.
- Checkout del repo.
- withCredentials: exporta secrets a
.env(backend) y variables de build del front. docker compose build+docker compose up -d.- (Opcional) cleanup previo si usas
container_nameen compose. - Smoke: ping a
/departmentsy a la raíz del front.
Esqueleto (ya funcional en tu setup):
pipeline {
agent any
environment {
COMPOSE_FILE = 'docker-compose.yml'
COMPOSE_PROJECT_NAME = 'licit'
}
options { timestamps() }
stages {
stage('Checkout') { steps { checkout scm } }
stage('Preparar .env (backend) y variables (frontend)') {
steps {
withCredentials([
string(credentialsId: 'mongo-uri', variable: 'MONGODB_URI'),
string(credentialsId: 'mongo-db', variable: 'MONGODB_DB'),
string(credentialsId: 'aws-region', variable: 'AWS_REGION'),
string(credentialsId: 's3-bucket', variable: 'S3_BUCKET'),
string(credentialsId: 'aws-access-key-id', variable: 'AWS_ACCESS_KEY_ID'),
string(credentialsId: 'aws-secret-access-key',variable: 'AWS_SECRET_ACCESS_KEY'),
string(credentialsId: 'vite-api-url', variable: 'VITE_API_URL'),
]) {
sh '''
set -euo pipefail
cat > .env <<EOF
MONGODB_URI=${MONGODB_URI}
MONGODB_DB=${MONGODB_DB}
AWS_REGION=${AWS_REGION}
S3_BUCKET=${S3_BUCKET}
AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
EOF
cat > .env.front.exp <<EOF
VITE_API_URL=${VITE_API_URL}
AWS_REGION=${AWS_REGION}
S3_BUCKET=${S3_BUCKET}
AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
EOF
echo "🔎 Front args:"; grep -E '^(VITE_API_URL|AWS_REGION|S3_BUCKET|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY)=' .env.front.exp
'''
}
}
}
stage('docker compose build') {
steps {
sh '''
set -euxo pipefail
set -a; [ -f .env.front.exp ] && . ./.env.front.exp; set +a
docker compose -f ${COMPOSE_FILE} config
docker compose -f ${COMPOSE_FILE} build
'''
}
}
stage('docker compose up') {
steps {
sh '''
set -euxo pipefail
set -a; [ -f .env.front.exp ] && . ./.env.front.exp; set +a
docker compose -f ${COMPOSE_FILE} up -d
docker ps --format "table {{.Names}}\\t{{.Ports}}\\t{{.Status}}"
'''
}
}
stage('Smoke check') {
steps {
sh '''
set -e
curl -fsS http://localhost:8000/departments >/dev/null
curl -fsS http://localhost:8080 >/dev/null
'''
}
}
}
post {
always {
sh 'rm -f .env .env.front.exp || true'
echo '✅ Pipeline terminado.'
}
}
}- Front:
GET http://localhost:8080devuelve HTML de Nginx. - API:
GET http://localhost:8000/departmentsresponde 200/JSON. - Mongo: el servicio
mongoapareceUpy elbackendresponde.
| Problema | Causa | Solución |
|---|---|---|
docker: not found |
Jenkins sin acceso a Docker | Montar /var/run/docker.sock o instalar Docker en el host/agent |
env file .env not found |
.env no creado en pipeline |
Revisa “Preparar .env” y credenciales |
Conflict: container name ... already in use |
container_name fijos |
Eliminar container_name o cleanup antes de up |
additional properties 'args' not allowed |
Indentación de build.args |
Poner args: dentro de build: |
Could not find credentials entry with ID ... |
ID distinto en Jenkins | Verificar tabla de credenciales (IDs exactos) |
Para este proyecto se implementó un conjunto de pruebas end-to-end utilizando Playwright con el objetivo de validar los flujos críticos de negocio desde la perspectiva de los distintos tipos de usuario (ADMIN, WORKER y BIDDER).
Las pruebas se ejecutan contra la aplicación real levantada con docker-compose, por lo que cubren tanto frontend como backend y la integración entre ambos.
Todas las pruebas E2E se encuentran en frontend/playwright/e2e/*.spec.ts.
1. Login y redirección por rol
Archivo: login.spec.ts
- Verifica que la pantalla de login se renderiza correctamente.
- Comprueba que:
admin@local.cl→ se autentica y es redirigido a/admin(pantalla Inicio de admin).worker@local.cl→ se autentica y es redirigido a/worker(pantalla Inicio de worker).bidder@local.cl→ se autentica y es redirigido a/bidder(pantalla Encuentra y postula tu licitación en minutos).
Esto asegura que el control de acceso y la navegación inicial por rol funcionan después de cualquier cambio.
2. Flujos del BIDDER (postulación a licitaciones)
Archivo: bidder.tenders.spec.ts
- Login como
BIDDER. - Navegación a Mis licitaciones (
/bidder/tenders). - Apertura de la tarjeta Crear una nueva licitación.
- Flujo completo de creación de postulación:
- Seleccionar un departamento.
- Subir un archivo PDF (
playwright/fixtures/sample.pdf). - Enviar la postulación y validar el mensaje ¡Postulación enviada!.
- Ir a Mis postulaciones y comprobar que aparece una licitación con código
TR-AG-....
Este test garantiza que un proveedor puede crear correctamente una postulación con archivo adjunto y verla listada después.
3. Flujos de chat BIDDER ↔ WORKER
Archivos:
chat.bidder.start-and-send.spec.tschat.worker.reply.spec.ts
Escenarios:
-
BIDDER abre el chat y envía un mensaje:
- Desde
/bidder, abre el botón flotante de chat (FAB). - Crea/conecta un chat con el botón Conectar.
- Selecciona un chat tipo
Chat #.... - Envía el mensaje "Hola" y valida que aparece en la conversación.
- Desde
-
WORKER ve el chat existente y responde:
- En un contexto separado, un
BIDDERcrea el chat y envía "Hola". - Luego, un
WORKERinicia sesión y entra a/worker/chats. - Ve en la lista un chat cuyo preview contiene "Hola".
- Abre el chat, ve el mensaje del BIDDER y responde "Qué tal", verificando que el mensaje queda registrado.
- En un contexto separado, un
Estos tests validan el ciclo completo de mensajería entre proveedor y trabajador, incluyendo la sincronización entre sesiones distintas.
4. Flujos del ADMIN sobre licitaciones
Archivos:
admin.approve-download.spec.tsadmin.reject.spec.ts
Escenarios:
-
Descarga y aprobación progresiva de una licitación:
- Login como
ADMINy navegación a Mis departamentos (/admin/departments). - Selección de una fila en estado
IN_REVIEWque tenga botones Descargar y Aprobar. - Verificación de la descarga: se hace stub de
window.openpara comprobar que efectivamente se abre una URL de descarga al pulsar Descargar. - Doble aprobación:
- Primera aprobación: se muestra un
promptpara comentario, se envía un mensaje desde el test y el campo Nivel cambia (por ejemplo de0/2a1/2). - Segunda aprobación: se vuelve a escribir un comentario y el Nivel vuelve a cambiar (por ejemplo a
2/2).
- Primera aprobación: se muestra un
- Login como
-
Rechazo de una licitación:
- Login como
ADMINy navegación a Mis departamentos. - Selección de una fila en estado
IN_REVIEWcon botón Rechazar. - Manejo de los diálogos del navegador:
promptcon el motivo de rechazo.confirmde confirmación.
- Espera de la llamada
POSTal endpoint/tender-requests/{id}/review. - Verificación de que el chip de estado deja de mostrar
IN_REVIEWy pasa a un estado equivalente aREJECTED.
- Login como
Con esto se cubren los dos caminos principales de decisión del administrador: aprobar (uno o varios niveles) o rechazar una licitación.
Las pruebas E2E se ejecutan automáticamente en el pipeline de Jenkins junto con el resto de etapas de build y despliegue.
A nivel general, el pipeline hace lo siguiente:
- Checkout del repositorio.
- Preparación de variables y
.envpara backend y frontend. - Levantamiento del stack completo con
docker-compose(API, base de datos y frontend enhttp://localhost:8080). - Ejecución de Playwright dentro de un contenedor dedicado, montando el código del frontend.
La etapa de Playwright dentro del Jenkinsfile queda conceptualmente así:
stage('E2E - Playwright') {
steps {
sh '''
set -euxo pipefail
docker run --rm --network host \
-e BASE_URL="http://localhost:8080" \
-v "$PWD/frontend":/app \
-w /app \
mcr.microsoft.com/playwright:v1.57.0-jammy bash -lc '
npm ci &&
npx playwright install --with-deps &&
npm run test:e2e
'
'''
}
}500 en /roles
Usa un response_model flexible (keys string) y proyecta campos mínimos en el find.
CORS
Incluye http://localhost:8080 y/o http://localhost:5173 en CORS_ORIGINS del backend.
Signup (crear cuenta)
El front resuelve rolId de BIDDER consultando /roles. Verifica que el router esté montado y el seed haya corrido.
Descarga de PDFs (presigned GET)
Revisa AWS_* y S3_BUCKET. Si es S3 compatible/MinIO, valida el endpoint y política.
Chat no conecta
Confirma que /chats/* esté montado, que exista al menos un usuario WORKER semillado, y que el polling de unread_count responda.