commit 8ae3b44bc930d0cd46a53258e6c23bd0b1dd2459 Author: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Wed Jun 17 13:22:39 2026 -0500 Initial commit: RIPS Manager web system diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d16bbfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +venv/ +__pycache__/ +*.pyc +*.pyo +*.db-shm +*.db-wal +.env +python/ +*.zip diff --git a/DOCUMENTACION.md b/DOCUMENTACION.md new file mode 100644 index 0000000..978ea03 --- /dev/null +++ b/DOCUMENTACION.md @@ -0,0 +1,347 @@ +# RIPS Manager — Documentación Técnica + +Sistema web para generar y enviar JSON de RIPS (Res. 2275/2023) a una API, conectándose a una base de datos Firebird. + +--- + +## Arquitectura + +``` +┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Navegador │────▶│ FastAPI (8080) │────▶│ Firebird BD │ +│ (Tailwind) │◀────│ + Jinja2 │◀────│ (datos) │ +└─────────────┘ └──────────────────┘ └──────────────┘ + │ + ▼ + ┌──────────────┐ + │ SQLite │ + │ (app.db) │ + └──────────────┘ + │ + ▼ + ┌──────────────┐ + │ API externa │ + │ (envío RIPS)│ + └──────────────┘ +``` + +## Stack técnico + +| Componente | Tecnología | +|---|---| +| Backend | Python 3.9+ / FastAPI | +| Frontend | Jinja2 + Tailwind CSS (CDN) + FontAwesome | +| Base de datos app | SQLite (usuarios, config, logs, queries) | +| Base de datos datos | Firebird (vía `fdb`) | +| Autenticación | JWT + bcrypt + HttpOnly cookies | +| Cliente HTTP | httpx (async) | +| Servidor | uvicorn | + +--- + +## Endpoints (20 totales) + +### Autenticación (6 endpoints) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/auth/login` | Página de inicio de sesión | Pública | +| POST | `/auth/login` | Procesa login, devuelve cookie JWT | Pública | +| GET | `/auth/register` | Página de registro | Pública | +| POST | `/auth/register` | Registra nuevo usuario | Pública | +| GET | `/auth/logout` | Cierra sesión (elimina cookie) | Pública | +| POST | `/auth/api/login` | Login vía API (devuelve token JSON) | Pública | + +### Dashboard (1 endpoint) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/dashboard` | Estadísticas de envíos, últimos registros | Requerida | + +### Configuración (2 endpoints) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/config` | Página de configuración (Firebird + API + prestador) | Requerida | +| POST | `/config/save` | Guarda toda la configuración | Requerida | + +### Consultas SQL (3 endpoints) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/queries` | Página de gestión de consultas SQL | Requerida | +| POST | `/queries/create` | Crea nueva consulta SQL | Requerida | +| POST | `/queries/delete/{id}` | Elimina consulta SQL | Requerida | +| POST | `/queries/update/{id}` | Actualiza consulta SQL | Requerida | + +### Terceros (3 endpoints) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/terceros` | Página de envío de terceros | Requerida | +| POST | `/terceros/test-connection` | Prueba conexión Firebird | Requerida | +| POST | `/terceros/preview` | Vista previa del JSON de terceros | Requerida | +| POST | `/terceros/send` | Genera y envía JSON de terceros a la API | Requerida | + +### Transacción RIPS (3 endpoints) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/transaccion` | Página de envío de transacción RIPS | Requerida | +| POST | `/transaccion/preview` | Vista previa del JSON de transacción | Requerida | +| POST | `/transaccion/send` | Genera y envía JSON de transacción a la API | Requerida | + +### Automatización (2 endpoints) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/automation` | Página de automatización 2 pasos | Requerida | +| POST | `/automation/run` | Ejecuta Paso 1 (terceros) + Paso 2 (transacción) | Requerida | + +### Logs (1 endpoint) + +| Método | Ruta | Descripción | Autenticación | +|---|---|---|---| +| GET | `/logs` | Historial de envíos con filtros | Requerida | + +--- + +## Flujo de autenticación + +``` +Navegador Servidor + │ │ + │── GET /auth/login ───────▶│ + │◀──── HTML login page ─────│ + │ │ + │── POST /auth/login ───────▶│ (username + password) + │ │── verifica credenciales + │ │── genera JWT + │◀── 302 /dashboard ────────│ (Set-Cookie: token=JWT; HttpOnly) + │ │ + │── GET /dashboard ────────▶│ (Cookie: token=JWT) + │ │── middleware verifica JWT + │ │── setea request.state.user + │◀──── HTML dashboard ──────│ +``` + +- **Cookie HttpOnly**: no accesible desde JavaScript (seguridad XSS) +- **JWT expira en 12 horas** +- **Bearer token** también soportado para llamadas API (`Authorization: Bearer `) + +--- + +## Flujo de envío de RIPS + +### Manual (página Terceros + Transacción) + +``` +1. Configurar conexión Firebird y API (una vez) +2. Definir consultas SQL en "Consultas SQL" +3. Ir a "Terceros" → seleccionar consulta → "Vista Previa" → "Enviar a API" +4. Ir a "Transacción RIPS" → seleccionar consulta + fechas → "Enviar a API" +``` + +### Automatizado (página Automatización) + +``` +1. Seleccionar consulta de terceros (Paso 1) +2. Seleccionar consulta de transacción (Paso 2) +3. Definir rango de fechas +4. "Ejecutar Automatización Completa" + └── Paso 1: Envía todos los terceros del período + └── Paso 2: Envía todas las transacciones del período +``` + +### Estructura de los JSON generados + +#### JSON Terceros +```json +{ + "tipoDocumentoIdentificacion": "CC", + "numDocumentoIdentificacion": "27765610", + "primerNombre": "MARIA", + "segundoNombre": "ELENA", + "primerApellido": "GOMEZ", + "segundoApellido": "RUIZ", + "fechaNacimiento": "1954-01-19", + "codSexo": "F", + "codEntidadAdministradora": "EPS010", + "tipoUsuario": "12", + "codPaisResidencia": "170", + "codMunicipioResidencia": "54001", + "codZonaTerritorialResidencia": "02", + "incapacidad": "NO", + "codPaisOrigen": "170", + "direccionResidencia": "CRA 5 #10-20", + "codZonaResidencia": "02" +} +``` + +#### JSON Transacción RIPS +```json +{ + "numDocumentoIdObligado": "900278729", + "numFactura": "LHXC03404", + "tipoNota": null, + "numNota": null, + "usuarios": [{ + "tipoDocumentoIdentificacion": "CC", + "numDocumentoIdentificacion": "27765610", + "codEntidadAdministradora": "EPS010", + "tipoUsuario": "12", + "fechaNacimiento": "1954-01-19", + "codSexo": "F", + "codPaisResidencia": "170", + "codMunicipioResidencia": "54001", + "codZonaTerritorialResidencia": "02", + "incapacidad": "NO", + "consecutivo": 1, + "codPaisOrigen": "170", + "servicios": { + "procedimientos": [{ + "consecutivo": 1, + "codProcedimiento": "903841", + "fechaInicioAtencion": "2026-06-11 00:00", + "codDiagnosticoPrincipal": "R790", + "codDiagnosticoRelacionado": null, + "finalidadTecnologiaSalud": "23", + "viaIngresoServicioSalud": "02", + "modalidadGrupoServicioTecSal": "01", + "grupoServicios": "02", + "codServicio": 706, + "codPrestador": "540010152002", + "tipoDocumentoIdentificacion": "CC", + "numDocumentoIdentificacion": "27898369", + "vrServicio": 8000, + "valorPagoModerador": 0, + "conceptoRecaudo": "05", + "numAutorizacion": null, + "idMIPRES": null, + "codComplicacion": null, + "numFEVPagoModerador": null + }] + } + }] +} +``` + +--- + +## Base de datos SQLite (app) + +### Tabla `users` +| Columna | Tipo | Descripción | +|---|---|---| +| id | INTEGER PK | Auto-incrementable | +| username | TEXT UNIQUE | Nombre de usuario | +| email | TEXT UNIQUE | Correo electrónico | +| password_hash | TEXT | Hash bcrypt de la contraseña | +| created_at | TEXT | Fecha de creación | + +### Tabla `config` +| Columna | Tipo | Descripción | +|---|---|---| +| id | INTEGER PK | Auto-incrementable | +| key | TEXT UNIQUE | Clave de configuración | +| value | TEXT | Valor | + +Claves predefinidas: +- `firebird_host`, `firebird_port`, `firebird_database`, `firebird_user`, `firebird_password` +- `api_url`, `api_method`, `api_key`, `api_timeout` +- `num_documento_obligado`, `cod_prestador` + +### Tabla `queries` +| Columna | Tipo | Descripción | +|---|---|---| +| id | INTEGER PK | Auto-incrementable | +| name | TEXT | Nombre descriptivo | +| query_type | TEXT | `terceros` o `transaccion` | +| query_text | TEXT | Sentencia SQL con parámetros | +| description | TEXT | Descripción | +| created_at | TEXT | Fecha de creación | + +### Tabla `envios` +| Columna | Tipo | Descripción | +|---|---|---| +| id | INTEGER PK | Auto-incrementable | +| user_id | INTEGER FK | Usuario que realizó el envío | +| tipo | TEXT | `terceros` o `transaccion` | +| factura | TEXT | Número de factura | +| fecha_inicio / fecha_fin | TEXT | Rango de fechas del envío | +| pacientes_count / servicios_count | INTEGER | Cantidades | +| status | TEXT | `success` o `error` | +| json_enviado | TEXT | JSON completo enviado | +| respuesta_api | TEXT | Respuesta de la API | +| codigo_cuv | TEXT | Código CUV (si aplica) | +| created_at | TEXT | Fecha del envío | + +--- + +## Parámetros en consultas SQL + +Las consultas pueden usar estos parámetros que el sistema reemplaza automáticamente: + +| Parámetro | Ejemplo | Uso | +|---|---|---| +| `:doc_num` | `WHERE doc = :doc_num` | Filtrar por documento de paciente | +| `:factura` | `WHERE fact = :factura` | Filtrar por número de factura | +| `:fecha_ini` | `WHERE fecha >= :fecha_ini` | Fecha inicio del rango | +| `:fecha_fin` | `WHERE fecha <= :fecha_fin` | Fecha fin del rango | + +--- + +## Estructura del proyecto + +``` +rips_manager/ +├── main.py # Entry point, middleware, rutas +├── requirements.txt # Dependencias Python +├── rips_manager.db # SQLite (autogenerado) +├── DOCUMENTACION.md # Este archivo +├── README.md # Documentación de RIPS +├── ejemplo_terceros.json # JSON de ejemplo terceros +├── ejemplo_transaccion_rips.json # JSON de ejemplo transacción +├── app/ +│ ├── __init__.py +│ ├── auth.py # JWT, bcrypt, get_current_user +│ ├── database.py # SQLite init y conexión +│ ├── models.py # Pydantic models +│ ├── routes/ +│ │ ├── auth.py # Login, register, logout +│ │ ├── dashboard.py # Página principal con stats +│ │ ├── config.py # Configuración Firebird + API +│ │ ├── queries.py # CRUD de consultas SQL +│ │ ├── terceros.py # Generar y enviar JSON terceros +│ │ ├── transaccion.py # Generar y enviar JSON transacción +│ │ ├── automation.py # Paso 1 + Paso 2 automático +│ │ └── logs.py # Historial de envíos +│ ├── services/ +│ │ ├── firebird_service.py # Conexión y consultas Firebird +│ │ ├── json_generator.py # Construcción de JSON RIPS +│ │ └── api_client.py # Envío HTTP a API externa +│ └── templates/ # 10 plantillas Jinja2 + Tailwind +``` + +--- + +## Cómo ejecutar + +```bash +cd /Users/lizandro/Documents/proyectos/rips_manager +pip3 install -r requirements.txt +python3 main.py +``` + +Abrir en el navegador: **http://localhost:8080** + +--- + +## Secuencia para primer uso + +1. Abrir http://localhost:8080/auth/register +2. Crear usuario y contraseña +3. Iniciar sesión +4. Ir a **Configuración** → configurar Firebird, API, datos del prestador +5. Ir a **Consultas SQL** → ajustar las queries a los nombres reales de tablas +6. Usar **Terceros**, **Transacción RIPS** o **Automatización** diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..a4cdb88 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,66 @@ +import bcrypt +from datetime import datetime, timedelta +from jose import JWTError, jwt +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +SECRET_KEY = "rips-manager-secret-key-change-in-production" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_HOURS = 12 + +security = HTTPBearer(auto_error=False) + + +def hash_password(password: str) -> str: + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() + + +def verify_password(password: str, password_hash: str) -> bool: + return bcrypt.checkpw(password.encode(), password_hash.encode()) + + +def create_token(user_id: int, username: str) -> str: + payload = { + "user_id": user_id, + "username": username, + "exp": datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS), + } + return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) + + +from typing import Optional + +def decode_token(token: str) -> Optional[dict]: + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except JWTError: + return None + + +from fastapi import Request + + +def get_current_user(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)): + if hasattr(request.state, "user") and request.state.user: + return request.state.user + if credentials is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + ) + payload = decode_token(credentials.credentials) + if payload is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + ) + return payload + + +def optional_user(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)): + if hasattr(request.state, "user") and request.state.user: + return request.state.user + if credentials is None: + return None + return decode_token(credentials.credentials) diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..a0a1f0f --- /dev/null +++ b/app/database.py @@ -0,0 +1,73 @@ +import sqlite3 +import os +from datetime import datetime + +DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "rips_manager.db") + + +def get_connection(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA foreign_keys=ON") + return conn + + +def init_db(): + conn = get_connection() + conn.executescript(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT UNIQUE NOT NULL, + value TEXT + ); + + CREATE TABLE IF NOT EXISTS queries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + query_type TEXT NOT NULL CHECK(query_type IN ('terceros','transaccion')), + query_text TEXT NOT NULL, + description TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + action TEXT NOT NULL, + step TEXT, + status TEXT NOT NULL CHECK(status IN ('success','error')), + payload TEXT, + response TEXT, + error_message TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS envios ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + tipo TEXT NOT NULL CHECK(tipo IN ('terceros','transaccion')), + factura TEXT, + fecha_inicio TEXT, + fecha_fin TEXT, + pacientes_count INTEGER DEFAULT 0, + servicios_count INTEGER DEFAULT 0, + status TEXT NOT NULL DEFAULT 'pendiente', + json_enviado TEXT, + respuesta_api TEXT, + codigo_cuv TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) + ); + """) + conn.commit() + conn.close() diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..2672773 --- /dev/null +++ b/app/models.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel +from typing import Optional + + +class UserCreate(BaseModel): + username: str + email: str + password: str + + +class UserLogin(BaseModel): + username: str + password: str + + +class ConfigUpdate(BaseModel): + key: str + value: str + + +class QueryCreate(BaseModel): + name: str + query_type: str + query_text: str + description: Optional[str] = None + + +class QueryUpdate(BaseModel): + name: Optional[str] = None + query_text: Optional[str] = None + description: Optional[str] = None + + +class SendRequest(BaseModel): + tipo: str + fecha_inicio: str + fecha_fin: str + factura: Optional[str] = None diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/auth.py b/app/routes/auth.py new file mode 100644 index 0000000..9c1acc5 --- /dev/null +++ b/app/routes/auth.py @@ -0,0 +1,96 @@ +from fastapi import APIRouter, Request, Form, Depends, HTTPException +from fastapi.responses import RedirectResponse, JSONResponse +from app.database import get_connection +from app.auth import hash_password, verify_password, create_token, get_current_user + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.get("/login") +async def login_page(request: Request): + return request.app.state.templates.TemplateResponse("login.html", {"request": request}) + + +@router.get("/register") +async def register_page(request: Request): + return request.app.state.templates.TemplateResponse("register.html", {"request": request}) + + +@router.post("/register") +async def register( + request: Request, + username: str = Form(...), + email: str = Form(...), + password: str = Form(...), + confirm_password: str = Form(...), +): + if password != confirm_password: + return request.app.state.templates.TemplateResponse("register.html", { + "request": request, "error": "Las contraseñas no coinciden" + }) + + conn = get_connection() + try: + existing = conn.execute( + "SELECT id FROM users WHERE username = ? OR email = ?", + (username, email) + ).fetchone() + if existing: + return request.app.state.templates.TemplateResponse("register.html", { + "request": request, "error": "Usuario o email ya registrado" + }) + + conn.execute( + "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)", + (username, email, hash_password(password)) + ) + conn.commit() + return RedirectResponse("/auth/login", status_code=302) + finally: + conn.close() + + +@router.post("/login") +async def login( + request: Request, + username: str = Form(...), + password: str = Form(...), +): + conn = get_connection() + try: + user = conn.execute( + "SELECT * FROM users WHERE username = ?", (username,) + ).fetchone() + if not user or not verify_password(password, user["password_hash"]): + return request.app.state.templates.TemplateResponse("login.html", { + "request": request, "error": "Usuario o contraseña incorrectos" + }) + + token = create_token(user["id"], user["username"]) + resp = RedirectResponse("/dashboard", status_code=302) + resp.set_cookie(key="token", value=token, httponly=True, max_age=43200) + return resp + finally: + conn.close() + + +@router.get("/logout") +async def logout(): + resp = RedirectResponse("/auth/login", status_code=302) + resp.delete_cookie("token") + return resp + + +@router.post("/api/login") +async def api_login(username: str = Form(...), password: str = Form(...)): + conn = get_connection() + try: + user = conn.execute( + "SELECT * FROM users WHERE username = ?", (username,) + ).fetchone() + if not user or not verify_password(password, user["password_hash"]): + raise HTTPException(status_code=401, detail="Invalid credentials") + token = create_token(user["id"], user["username"]) + return {"access_token": token, "token_type": "bearer"} + finally: + conn.close() diff --git a/app/routes/automation.py b/app/routes/automation.py new file mode 100644 index 0000000..218465c --- /dev/null +++ b/app/routes/automation.py @@ -0,0 +1,204 @@ +from fastapi import APIRouter, Request, Form, Depends +from fastapi.responses import JSONResponse +from app.database import get_connection +from app.auth import get_current_user +from app.services.firebird_service import FirebirdService +from app.services.json_generator import generar_terceros, generar_transaccion + +router = APIRouter(prefix="/automation", tags=["automation"]) + + +@router.get("") +async def automation_page(request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + queries = conn.execute("SELECT * FROM queries ORDER BY query_type, name").fetchall() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + return request.app.state.templates.TemplateResponse("automation.html", { + "request": request, "user": user, + "queries": queries, "configs": configs, + }) + + +@router.post("/run") +async def run_automation( + request: Request, + user: dict = Depends(get_current_user), + query_terceros_id: int = Form(...), + query_transaccion_id: int = Form(...), + fecha_inicio: str = Form(...), + fecha_fin: str = Form(...), + factura: str = Form(""), +): + import json as json_lib + import httpx + from datetime import datetime + from collections import defaultdict + + conn = get_connection() + q_terceros = conn.execute("SELECT * FROM queries WHERE id = ?", (query_terceros_id,)).fetchone() + q_trans = conn.execute("SELECT * FROM queries WHERE id = ?", (query_transaccion_id,)).fetchone() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + if not q_terceros or not q_trans: + return JSONResponse({"success": False, "message": "Consultas no encontradas"}) + + fb = FirebirdService() + fb_success, fb_msg = fb.connect( + configs.get("firebird_host", "localhost"), + int(configs.get("firebird_port", 3050)), + configs.get("firebird_database", ""), + configs.get("firebird_user", "SYSDBA"), + configs.get("firebird_password", "masterkey"), + ) + if not fb_success: + return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"}) + + api_url = configs.get("api_url", "") + api_key = configs.get("api_key", "") + api_method = configs.get("api_method", "POST") + headers = {"Content-Type": "application/json"} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + resultado = {"paso1_terceros": {"status": "pendiente"}, "paso2_transaccion": {"status": "pendiente"}} + + # --------------------------------------------------------------- + # PASO 1: Enviar TERCEROS + # --------------------------------------------------------------- + params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} + if ":factura" in q_terceros["query_text"] and factura: + params["factura"] = factura + if ":doc_num" in q_terceros["query_text"]: + params["doc_num"] = "" + + success, error, rows = fb.execute_query(q_terceros["query_text"], params if ":fecha_ini" in q_terceros["query_text"] else None) + + if not success: + resultado["paso1_terceros"] = {"status": "error", "message": error} + elif not rows: + resultado["paso1_terceros"] = {"status": "error", "message": "No hay pacientes para enviar"} + else: + terceros_enviados = 0 + terceros_errores = 0 + pacientes_enviados = [] + + for row in rows: + tercero_json = generar_terceros(row) + doc_id = tercero_json["numDocumentoIdentificacion"] + if doc_id in pacientes_enviados: + continue + pacientes_enviados.append(doc_id) + + try: + async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: + if api_method == "POST": + resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers) + else: + resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers) + + if resp.is_success: + terceros_enviados += 1 + else: + terceros_errores += 1 + except Exception as e: + terceros_errores += 1 + + conn = get_connection() + conn.execute(""" + INSERT INTO envios (user_id, tipo, factura, status, json_enviado, respuesta_api, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + user["user_id"], "terceros", factura or "AUTO", + "success" if resp.is_success else "error", + json_lib.dumps(tercero_json, indent=2, ensure_ascii=False), + resp.text[:1000] if resp.is_success else str(e), + datetime.now().isoformat(), + )) + conn.commit() + conn.close() + + resultado["paso1_terceros"] = { + "status": "success" if terceros_errores == 0 else "partial", + "enviados": terceros_enviados, + "errores": terceros_errores, + } + + # --------------------------------------------------------------- + # PASO 2: Enviar TRANSACCION + # --------------------------------------------------------------- + params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} + if ":factura" in q_trans["query_text"] and factura: + params["factura"] = factura + + success, error, rows = fb.execute_query(q_trans["query_text"], params) + + if not success: + resultado["paso2_transaccion"] = {"status": "error", "message": error} + elif not rows: + resultado["paso2_transaccion"] = {"status": "error", "message": "No hay servicios para enviar"} + else: + grupos = defaultdict(lambda: {"factura": "", "procedimientos": [], "paciente": {}}) + for row in rows: + doc_key = (row.get("tipo_doc_paciente", "CC"), row.get("num_doc_paciente", "")) + fact = row.get("num_factura", factura) + grupos[(fact, doc_key)]["factura"] = fact + grupos[(fact, doc_key)]["procedimientos"].append(dict(row)) + + trans_enviados = 0 + trans_errores = 0 + + for (fact, doc_key), grupo in grupos.items(): + paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} + trans_json = generar_transaccion( + fact, + configs.get("num_documento_obligado", ""), + paciente_data, + grupo["procedimientos"], + ) + + try: + async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: + if api_method == "POST": + resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers) + else: + resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers) + + status_ok = resp.is_success + resp_text = resp.text[:1000] + except Exception as e: + status_ok = False + resp_text = str(e) + + if status_ok: + trans_enviados += 1 + else: + trans_errores += 1 + + conn = get_connection() + conn.execute(""" + INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin, + pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + user["user_id"], "transaccion", fact, + fecha_inicio, fecha_fin, + 1, len(grupo["procedimientos"]), + "success" if status_ok else "error", + json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000], + resp_text, + datetime.now().isoformat(), + )) + conn.commit() + conn.close() + + resultado["paso2_transaccion"] = { + "status": "success" if trans_errores == 0 else "partial", + "enviados": trans_enviados, + "errores": trans_errores, + } + + fb.disconnect() + return JSONResponse({"success": True, "resultado": resultado}) diff --git a/app/routes/config.py b/app/routes/config.py new file mode 100644 index 0000000..f817ff5 --- /dev/null +++ b/app/routes/config.py @@ -0,0 +1,57 @@ +from fastapi import APIRouter, Request, Form, Depends +from fastapi.responses import RedirectResponse +from app.database import get_connection +from app.auth import get_current_user + +router = APIRouter(prefix="/config", tags=["config"]) + +DEFAULT_KEYS = [ + ("firebird_host", "localhost"), + ("firebird_port", "3050"), + ("firebird_database", "/path/to/database.fdb"), + ("firebird_user", "SYSDBA"), + ("firebird_password", "masterkey"), + ("api_url", "https://api.example.com/rips"), + ("api_method", "POST"), + ("api_key", ""), + ("api_timeout", "30"), + ("num_documento_obligado", ""), + ("cod_prestador", ""), +] + + +def ensure_defaults(): + conn = get_connection() + for key, default in DEFAULT_KEYS: + exists = conn.execute("SELECT id FROM config WHERE key = ?", (key,)).fetchone() + if not exists: + conn.execute("INSERT INTO config (key, value) VALUES (?, ?)", (key, default)) + conn.commit() + conn.close() + + +@router.get("") +async def config_page(request: Request, user: dict = Depends(get_current_user)): + ensure_defaults() + conn = get_connection() + configs = conn.execute("SELECT * FROM config ORDER BY key").fetchall() + conn.close() + return request.app.state.templates.TemplateResponse("config.html", { + "request": request, "user": user, "configs": configs + }) + + +@router.post("/save") +async def config_save(request: Request, user: dict = Depends(get_current_user)): + form = await request.form() + conn = get_connection() + for key, value in form.multi_items(): + if key.startswith("config_"): + real_key = key.replace("config_", "", 1) + conn.execute( + "UPDATE config SET value = ? WHERE key = ?", + (value, real_key) + ) + conn.commit() + conn.close() + return RedirectResponse("/config", status_code=302) diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py new file mode 100644 index 0000000..d75d15b --- /dev/null +++ b/app/routes/dashboard.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Request, Depends +from app.database import get_connection +from app.auth import get_current_user + +router = APIRouter(tags=["dashboard"]) + + +@router.get("/dashboard") +async def dashboard(request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + try: + stats = conn.execute(""" + SELECT + (SELECT COUNT(*) FROM envios WHERE status = 'success') as total_exitosos, + (SELECT COUNT(*) FROM envios WHERE status = 'error') as total_errores, + (SELECT COUNT(*) FROM envios WHERE tipo = 'terceros') as total_terceros, + (SELECT COUNT(*) FROM envios WHERE tipo = 'transaccion') as total_transacciones, + (SELECT COUNT(DISTINCT factura) FROM envios WHERE factura IS NOT NULL) as total_facturas + """).fetchone() + + ultimos = conn.execute(""" + SELECT e.*, u.username FROM envios e + LEFT JOIN users u ON e.user_id = u.id + ORDER BY e.created_at DESC LIMIT 10 + """).fetchall() + + return request.app.state.templates.TemplateResponse("dashboard.html", { + "request": request, "user": user, "stats": stats, "ultimos": ultimos + }) + finally: + conn.close() diff --git a/app/routes/logs.py b/app/routes/logs.py new file mode 100644 index 0000000..d35580c --- /dev/null +++ b/app/routes/logs.py @@ -0,0 +1,55 @@ +from fastapi import APIRouter, Request, Depends +from app.database import get_connection +from app.auth import get_current_user + +router = APIRouter(prefix="/logs", tags=["logs"]) + + +@router.get("") +async def logs_page( + request: Request, + user: dict = Depends(get_current_user), + tipo: str = "", + status: str = "", + factura: str = "", +): + conn = get_connection() + + where = ["1=1"] + params = [] + + if tipo: + where.append("e.tipo = ?") + params.append(tipo) + if status: + where.append("e.status = ?") + params.append(status) + if factura: + where.append("e.factura LIKE ?") + params.append(f"%{factura}%") + + envios = conn.execute(f""" + SELECT e.*, u.username FROM envios e + LEFT JOIN users u ON e.user_id = u.id + WHERE {' AND '.join(where)} + ORDER BY e.created_at DESC LIMIT 100 + """, params).fetchall() + + stats = conn.execute(""" + SELECT + tipo, + status, + COUNT(*) as total, + SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as exitosos, + SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as fallidos + FROM envios + GROUP BY tipo, status + """).fetchall() + + conn.close() + + return request.app.state.templates.TemplateResponse("logs.html", { + "request": request, "user": user, + "envios": envios, "stats": stats, + "filtro_tipo": tipo, "filtro_status": status, "filtro_factura": factura, + }) diff --git a/app/routes/queries.py b/app/routes/queries.py new file mode 100644 index 0000000..0814e79 --- /dev/null +++ b/app/routes/queries.py @@ -0,0 +1,152 @@ +from fastapi import APIRouter, Request, Form, Depends +from fastapi.responses import RedirectResponse, JSONResponse +from app.database import get_connection +from app.auth import get_current_user +from app.models import QueryCreate + +router = APIRouter(prefix="/queries", tags=["queries"]) + +QUERY_DEFAULTS = [ + { + "name": "Terceros - Datos del paciente", + "query_type": "terceros", + "query_text": """SELECT + p.TIPO_DOCUMENTO as tipo_documento, + p.NUMERO_DOCUMENTO as numero_documento, + p.PRIMER_NOMBRE as primer_nombre, + p.SEGUNDO_NOMBRE as segundo_nombre, + p.PRIMER_APELLIDO as primer_apellido, + p.SEGUNDO_APELLIDO as segundo_apellido, + p.FECHA_NACIMIENTO as fecha_nacimiento, + p.SEXO as cod_sexo, + p.COD_ENTIDAD as cod_entidad, + p.TIPO_USUARIO as tipo_usuario, + p.COD_MUNICIPIO as cod_municipio, + p.ZONA as cod_zona, + p.DIRECCION as direccion +FROM USUAHOS p +WHERE p.NUMERO_DOCUMENTO = :doc_num""", + "description": "Consulta datos maestros del paciente por documento" + }, + { + "name": "Procedimientos por factura", + "query_type": "transaccion", + "query_text": """SELECT + s.CODIGO_CUP as cod_procedimiento, + s.FECHA_ATENCION as fecha_atencion, + s.COD_DIAGNOSTICO as cod_diagnostico, + s.FINALIDAD as finalidad, + s.VIA_INGRESO as via_ingreso, + s.MODALIDAD as modalidad, + s.GRUPO_SERVICIO as grupo_servicio, + s.COD_SERVICIO as cod_servicio, + s.COD_PRESTADOR as cod_prestador, + s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional, + s.NUM_DOC_PROFESIONAL as num_doc_profesional, + s.VR_SERVICIO as vr_servicio, + s.VALOR_PAGO_MODERADOR as valor_pago_moderador, + s.CONCEPTO_RECAUDO as concepto_recaudo, + s.NUM_AUTORIZACION as num_autorizacion +FROM SERVICIOS s +WHERE s.NUM_FACTURA = :factura + AND s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""", + "description": "Consulta procedimientos por factura y rango de fechas" + }, + { + "name": "Procedimientos por fecha", + "query_type": "transaccion", + "query_text": """SELECT + s.FACTURA as num_factura, + s.CODIGO_CUP as cod_procedimiento, + s.FECHA_ATENCION as fecha_atencion, + s.COD_DIAGNOSTICO as cod_diagnostico, + s.FINALIDAD as finalidad, + s.VIA_INGRESO as via_ingreso, + s.MODALIDAD as modalidad, + s.GRUPO_SERVICIO as grupo_servicio, + s.COD_SERVICIO as cod_servicio, + s.COD_PRESTADOR as cod_prestador, + s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional, + s.NUM_DOC_PROFESIONAL as num_doc_profesional, + s.VR_SERVICIO as vr_servicio, + p.TIPO_DOCUMENTO as tipo_doc_paciente, + p.NUMERO_DOCUMENTO as num_doc_paciente +FROM SERVICIOS s +JOIN USUAHOS p ON s.COD_PACIENTE = p.COD_PACIENTE +WHERE s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""", + "description": "Consulta todos los procedimientos en rango de fechas" + }, +] + + +def ensure_defaults(): + conn = get_connection() + for q in QUERY_DEFAULTS: + exists = conn.execute( + "SELECT id FROM queries WHERE name = ?", (q["name"],) + ).fetchone() + if not exists: + conn.execute( + "INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)", + (q["name"], q["query_type"], q["query_text"], q["description"]), + ) + conn.commit() + conn.close() + + +@router.get("") +async def queries_page(request: Request, user: dict = Depends(get_current_user)): + ensure_defaults() + conn = get_connection() + queries = conn.execute("SELECT * FROM queries ORDER BY query_type, name").fetchall() + conn.close() + return request.app.state.templates.TemplateResponse("queries.html", { + "request": request, "user": user, "queries": queries + }) + + +@router.post("/create") +async def query_create( + request: Request, + user: dict = Depends(get_current_user), + name: str = Form(...), + query_type: str = Form(...), + query_text: str = Form(...), + description: str = Form(""), +): + conn = get_connection() + conn.execute( + "INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)", + (name, query_type, query_text, description), + ) + conn.commit() + conn.close() + return RedirectResponse("/queries", status_code=302) + + +@router.post("/update/{query_id}") +async def query_update( + query_id: int, + request: Request, + user: dict = Depends(get_current_user), + name: str = Form(...), + query_text: str = Form(...), + description: str = Form(""), +): + conn = get_connection() + conn.execute( + "UPDATE queries SET name = ?, query_text = ?, description = ? WHERE id = ?", + (name, query_text, description, query_id), + ) + conn.commit() + conn.close() + return RedirectResponse("/queries", status_code=302) + + +@router.post("/delete/{query_id}") +async def query_delete(query_id: int, user: dict = Depends(get_current_user)): + conn = get_connection() + conn.execute("DELETE FROM queries WHERE id = ?", (query_id,)) + conn.commit() + conn.close() + return RedirectResponse("/queries", status_code=302) diff --git a/app/routes/terceros.py b/app/routes/terceros.py new file mode 100644 index 0000000..ad60154 --- /dev/null +++ b/app/routes/terceros.py @@ -0,0 +1,191 @@ +from fastapi import APIRouter, Request, Form, Depends +from fastapi.responses import RedirectResponse, JSONResponse +from app.database import get_connection +from app.auth import get_current_user +from app.services.firebird_service import FirebirdService +from app.services.json_generator import generar_terceros + +router = APIRouter(prefix="/terceros", tags=["terceros"]) + + +@router.get("") +async def terceros_page(request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + envios = conn.execute(""" + SELECT * FROM envios WHERE tipo = 'terceros' + ORDER BY created_at DESC LIMIT 20 + """).fetchall() + queries = conn.execute( + "SELECT * FROM queries WHERE query_type = 'terceros' ORDER BY name" + ).fetchall() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + return request.app.state.templates.TemplateResponse("terceros.html", { + "request": request, "user": user, + "envios": envios, "queries": queries, + "configs": configs, + "firebird_host": configs.get("firebird_host", "localhost"), + "firebird_port": configs.get("firebird_port", "3050"), + "firebird_database": configs.get("firebird_database", ""), + }) + + +@router.post("/test-connection") +async def test_connection( + request: Request, + user: dict = Depends(get_current_user), + host: str = Form(...), + port: int = Form(...), + database: str = Form(...), + fb_user: str = Form(...), + fb_password: str = Form(...), +): + fb = FirebirdService() + success, msg = fb.connect(host, port, database, fb_user, fb_password) + if success: + fb.disconnect() + return JSONResponse({"success": success, "message": msg}) + + +@router.post("/preview") +async def preview_query( + request: Request, + user: dict = Depends(get_current_user), + query_id: int = Form(...), + doc_num: str = Form(""), +): + conn = get_connection() + q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + if not q: + return JSONResponse({"success": False, "message": "Consulta no encontrada"}) + + fb = FirebirdService() + fb_success, fb_msg = fb.connect( + configs.get("firebird_host", "localhost"), + int(configs.get("firebird_port", 3050)), + configs.get("firebird_database", ""), + configs.get("firebird_user", "SYSDBA"), + configs.get("firebird_password", "masterkey"), + ) + if not fb_success: + return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"}) + + params = {} + if ":doc_num" in q["query_text"] and doc_num: + params["doc_num"] = doc_num + if ":fecha_ini" in q["query_text"]: + params["fecha_ini"] = "1900-01-01" + params["fecha_fin"] = "2100-12-31" + if ":factura" in q["query_text"]: + params["factura"] = doc_num if doc_num else "" + + success, error, rows = fb.execute_query(q["query_text"], params if params else None) + fb.disconnect() + + if not success: + return JSONResponse({"success": False, "message": error}) + + json_result = None + if rows: + json_result = generar_terceros(rows[0]) + + return JSONResponse({ + "success": True, + "rows_count": len(rows), + "columns": list(rows[0].keys()) if rows else [], + "preview": rows[:5], + "generated_json": json_result, + }) + + +@router.post("/send") +async def send_terceros( + request: Request, + user: dict = Depends(get_current_user), + query_id: int = Form(...), + doc_num: str = Form(""), +): + import json + import httpx + from datetime import datetime + + conn = get_connection() + q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + if not q: + return JSONResponse({"success": False, "message": "Consulta no encontrada"}) + + fb = FirebirdService() + fb_success, fb_msg = fb.connect( + configs.get("firebird_host", "localhost"), + int(configs.get("firebird_port", 3050)), + configs.get("firebird_database", ""), + configs.get("firebird_user", "SYSDBA"), + configs.get("firebird_password", "masterkey"), + ) + if not fb_success: + return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"}) + + params = {} + if ":doc_num" in q["query_text"]: + params["doc_num"] = doc_num or configs.get("doc_num_default", "") + + success, error, rows = fb.execute_query(q["query_text"], params if params else None) + fb.disconnect() + + if not success: + return JSONResponse({"success": False, "message": error}) + + if not rows: + return JSONResponse({"success": False, "message": "No se encontraron datos"}) + + # Generar JSON terceros + tercero_json = generar_terceros(rows[0]) + + # Enviar a API + api_url = configs.get("api_url", "") + api_key = configs.get("api_key", "") + api_method = configs.get("api_method", "POST") + + headers = {"Content-Type": "application/json"} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + try: + async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: + if api_method == "POST": + resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers) + else: + resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers) + + result = resp.status_code, resp.is_success, resp.text + except Exception as e: + result = (0, False, str(e)) + + # Guardar log + conn = get_connection() + conn.execute(""" + INSERT INTO envios (user_id, tipo, status, json_enviado, respuesta_api, created_at) + VALUES (?, ?, ?, ?, ?, ?) + """, ( + user["user_id"], "terceros", + "success" if result[1] else "error", + json.dumps(tercero_json, indent=2, ensure_ascii=False), + str(result[2])[:1000], + datetime.now().isoformat(), + )) + conn.commit() + conn.close() + + return JSONResponse({ + "success": result[1], + "status_code": result[0], + "message": "Envío exitoso" if result[1] else f"Error: {result[2]}", + "cuv": result[2][:200] if result[1] else None, + }) diff --git a/app/routes/transaccion.py b/app/routes/transaccion.py new file mode 100644 index 0000000..600b8ce --- /dev/null +++ b/app/routes/transaccion.py @@ -0,0 +1,218 @@ +from fastapi import APIRouter, Request, Form, Depends +from fastapi.responses import JSONResponse +from app.database import get_connection +from app.auth import get_current_user +from app.services.firebird_service import FirebirdService +from app.services.json_generator import generar_terceros, generar_transaccion + +router = APIRouter(prefix="/transaccion", tags=["transaccion"]) + + +@router.get("") +async def transaccion_page(request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + envios = conn.execute(""" + SELECT * FROM envios WHERE tipo = 'transaccion' + ORDER BY created_at DESC LIMIT 20 + """).fetchall() + queries = conn.execute( + "SELECT * FROM queries WHERE query_type = 'transaccion' ORDER BY name" + ).fetchall() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + return request.app.state.templates.TemplateResponse("transaccion.html", { + "request": request, "user": user, + "envios": envios, "queries": queries, + "configs": configs, + }) + + +@router.post("/preview") +async def preview_transaccion( + request: Request, + user: dict = Depends(get_current_user), + query_id: int = Form(...), + factura: str = Form(""), + fecha_inicio: str = Form(...), + fecha_fin: str = Form(...), +): + conn = get_connection() + q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + if not q: + return JSONResponse({"success": False, "message": "Consulta no encontrada"}) + + fb = FirebirdService() + fb_success, fb_msg = fb.connect( + configs.get("firebird_host", "localhost"), + int(configs.get("firebird_port", 3050)), + configs.get("firebird_database", ""), + configs.get("firebird_user", "SYSDBA"), + configs.get("firebird_password", "masterkey"), + ) + if not fb_success: + return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"}) + + params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} + if ":factura" in q["query_text"] and factura: + params["factura"] = factura + + success, error, rows = fb.execute_query(q["query_text"], params) + fb.disconnect() + + if not success: + return JSONResponse({"success": False, "message": error}) + + # Agrupar por paciente y factura + from collections import defaultdict + grupos = defaultdict(lambda: {"factura": "", "procedimientos": [], "paciente": {}}) + + for row in rows: + doc_key = (row.get("tipo_doc_paciente", "CC"), row.get("num_doc_paciente", "")) + fact = row.get("num_factura", factura) + grupos[(fact, doc_key)]["factura"] = fact + grupos[(fact, doc_key)]["procedimientos"].append(dict(row)) + + json_result = [] + for (fact, doc_key), grupo in grupos.items(): + paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} + trans = generar_transaccion( + fact, + configs.get("num_documento_obligado", ""), + paciente_data, + grupo["procedimientos"], + ) + json_result.append(trans) + + return JSONResponse({ + "success": True, + "rows_count": len(rows), + "grupos_count": len(grupos), + "columns": list(rows[0].keys()) if rows else [], + "preview": rows[:5], + "generated_json": json_result[0] if json_result else None, + "total_json": len(json_result), + }) + + +@router.post("/send") +async def send_transaccion( + request: Request, + user: dict = Depends(get_current_user), + query_id: int = Form(...), + factura: str = Form(""), + fecha_inicio: str = Form(...), + fecha_fin: str = Form(...), +): + import json as json_lib + import httpx + from datetime import datetime + from collections import defaultdict + + conn = get_connection() + q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + if not q: + return JSONResponse({"success": False, "message": "Consulta no encontrada"}) + + fb = FirebirdService() + fb_success, fb_msg = fb.connect( + configs.get("firebird_host", "localhost"), + int(configs.get("firebird_port", 3050)), + configs.get("firebird_database", ""), + configs.get("firebird_user", "SYSDBA"), + configs.get("firebird_password", "masterkey"), + ) + if not fb_success: + return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"}) + + params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} + if ":factura" in q["query_text"] and factura: + params["factura"] = factura + + success, error, rows = fb.execute_query(q["query_text"], params) + fb.disconnect() + + if not success: + return JSONResponse({"success": False, "message": error}) + if not rows: + return JSONResponse({"success": False, "message": "No se encontraron datos"}) + + # Agrupar + grupos = defaultdict(lambda: {"factura": "", "procedimientos": [], "paciente": {}}) + for row in rows: + doc_key = (row.get("tipo_doc_paciente", "CC"), row.get("num_doc_paciente", "")) + fact = row.get("num_factura", factura) + grupos[(fact, doc_key)]["factura"] = fact + grupos[(fact, doc_key)]["procedimientos"].append(dict(row)) + + api_url = configs.get("api_url", "") + api_key = configs.get("api_key", "") + api_method = configs.get("api_method", "POST") + headers = {"Content-Type": "application/json"} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + total_enviados = 0 + total_errores = 0 + resultados = [] + + for (fact, doc_key), grupo in grupos.items(): + paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} + trans_json = generar_transaccion( + fact, + configs.get("num_documento_obligado", ""), + paciente_data, + grupo["procedimientos"], + ) + + try: + async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: + if api_method == "POST": + resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers) + else: + resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers) + + status_ok = resp.is_success + response_text = resp.text[:1000] + except Exception as e: + status_ok = False + response_text = str(e) + + if status_ok: + total_enviados += 1 + else: + total_errores += 1 + + resultados.append({"factura": fact, "success": status_ok}) + + # Guardar log + conn = get_connection() + conn.execute(""" + INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin, + pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + user["user_id"], "transaccion", fact, + fecha_inicio, fecha_fin, + 1, len(grupo["procedimientos"]), + "success" if status_ok else "error", + json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000], + response_text, + datetime.now().isoformat(), + )) + conn.commit() + conn.close() + + return JSONResponse({ + "success": total_errores == 0, + "total_enviados": total_enviados, + "total_errores": total_errores, + "resultados": resultados, + "message": f"Enviados: {total_enviados}, Errores: {total_errores}", + }) diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/api_client.py b/app/services/api_client.py new file mode 100644 index 0000000..730770f --- /dev/null +++ b/app/services/api_client.py @@ -0,0 +1,35 @@ +import httpx +import json +from typing import Optional + + +async def send_json( + url: str, + json_data: dict, + method: str = "POST", + headers: Optional[dict] = None, + timeout: int = 30, +) -> dict: + default_headers = {"Content-Type": "application/json"} + if headers: + default_headers.update(headers) + + try: + async with httpx.AsyncClient(timeout=timeout) as client: + if method == "POST": + resp = await client.post(url, json=json_data, headers=default_headers) + elif method == "PUT": + resp = await client.put(url, json=json_data, headers=default_headers) + else: + resp = await client.get(url, headers=default_headers) + + return { + "status_code": resp.status_code, + "success": resp.is_success, + "body": resp.text, + "headers": dict(resp.headers), + } + except httpx.TimeoutException: + return {"status_code": 0, "success": False, "body": "Timeout", "headers": {}} + except Exception as e: + return {"status_code": 0, "success": False, "body": str(e), "headers": {}} diff --git a/app/services/firebird_service.py b/app/services/firebird_service.py new file mode 100644 index 0000000..ac00bf6 --- /dev/null +++ b/app/services/firebird_service.py @@ -0,0 +1,55 @@ +import fdb +from typing import Optional + + +class FirebirdService: + def __init__(self): + self.conn = None + + def connect(self, host: str, port: int, database: str, user: str, password: str): + try: + self.conn = fdb.connect( + host=host, + port=port, + database=database, + user=user, + password=password, + charset="UTF8", + ) + return True, "Conexión exitosa" + except Exception as e: + return False, str(e) + + def disconnect(self): + if self.conn: + self.conn.close() + self.conn = None + + def is_connected(self) -> bool: + return self.conn is not None + + def test_connection(self) -> tuple: + if not self.conn: + return False, "No hay conexión activa" + try: + cur = self.conn.cursor() + cur.execute("SELECT 1 FROM RDB$DATABASE") + cur.fetchone() + return True, "Conexión OK" + except Exception as e: + return False, str(e) + + def execute_query(self, query: str, params: Optional[dict] = None) -> tuple: + if not self.conn: + return False, "No hay conexión activa", [] + try: + cur = self.conn.cursor() + if params: + cur.execute(query, params) + else: + cur.execute(query) + columns = [desc[0] for desc in cur.description] if cur.description else [] + rows = cur.fetchall() + return True, "", [dict(zip(columns, row)) for row in rows] + except Exception as e: + return False, str(e), [] diff --git a/app/services/json_generator.py b/app/services/json_generator.py new file mode 100644 index 0000000..b62a8fc --- /dev/null +++ b/app/services/json_generator.py @@ -0,0 +1,89 @@ +from datetime import datetime +from typing import Optional + + +def generar_terceros(row: dict) -> dict: + return { + "tipoDocumentoIdentificacion": row.get("tipo_documento", "CC"), + "numDocumentoIdentificacion": row.get("numero_documento", ""), + "primerNombre": (row.get("primer_nombre") or "").upper(), + "segundoNombre": (row.get("segundo_nombre") or "").upper(), + "primerApellido": (row.get("primer_apellido") or "").upper(), + "segundoApellido": (row.get("segundo_apellido") or "").upper(), + "fechaNacimiento": str(row.get("fecha_nacimiento", ""))[:10], + "codSexo": row.get("cod_sexo", "M"), + "codEntidadAdministradora": row.get("cod_entidad", ""), + "tipoUsuario": row.get("tipo_usuario", "01"), + "codPaisResidencia": row.get("cod_pais", "170"), + "codMunicipioResidencia": row.get("cod_municipio", ""), + "codZonaTerritorialResidencia": row.get("cod_zona", "01"), + "incapacidad": row.get("incapacidad", "NO"), + "codPaisOrigen": row.get("cod_pais_origen", "170"), + "direccionResidencia": row.get("direccion", ""), + "codZonaResidencia": row.get("cod_zona", "01"), + } + + +def generar_procedimiento(row: dict, consecutivo: int) -> dict: + return { + "consecutivo": consecutivo, + "codProcedimiento": row.get("cod_procedimiento", ""), + "fechaInicioAtencion": str(row.get("fecha_atencion", datetime.now().strftime("%Y-%m-%d %H:%M")))[:16], + "codDiagnosticoPrincipal": row.get("cod_diagnostico", "R790"), + "codDiagnosticoRelacionado": row.get("cod_diagnostico_rel", None), + "finalidadTecnologiaSalud": row.get("finalidad", "23"), + "viaIngresoServicioSalud": row.get("via_ingreso", "02"), + "modalidadGrupoServicioTecSal": row.get("modalidad", "01"), + "grupoServicios": row.get("grupo_servicio", "02"), + "codServicio": int(row.get("cod_servicio", 706)), + "codPrestador": row.get("cod_prestador", ""), + "tipoDocumentoIdentificacion": row.get("tipo_doc_profesional", "CC"), + "numDocumentoIdentificacion": row.get("num_doc_profesional", ""), + "vrServicio": int(row.get("vr_servicio", 0)), + "valorPagoModerador": int(row.get("valor_pago_moderador", 0)), + "conceptoRecaudo": row.get("concepto_recaudo", "05"), + "numAutorizacion": row.get("num_autorizacion", None), + "idMIPRES": row.get("id_mipres", None), + "codComplicacion": row.get("cod_complicacion", None), + "numFEVPagoModerador": row.get("num_fev_pago_moderador", None), + } + + +def generar_transaccion( + factura: str, + num_doc_obligado: str, + rows_tercero: Optional[dict], + rows_procedimientos: list, +) -> dict: + transaccion = { + "numDocumentoIdObligado": num_doc_obligado, + "numFactura": factura, + "tipoNota": None, + "numNota": None, + "usuarios": [], + } + + if rows_tercero and rows_procedimientos: + usuario = { + "tipoDocumentoIdentificacion": rows_tercero.get("tipoDocumentoIdentificacion", "CC"), + "numDocumentoIdentificacion": rows_tercero.get("numDocumentoIdentificacion", ""), + "codEntidadAdministradora": rows_tercero.get("codEntidadAdministradora", ""), + "tipoUsuario": rows_tercero.get("tipoUsuario", "01"), + "fechaNacimiento": rows_tercero.get("fechaNacimiento", ""), + "codSexo": rows_tercero.get("codSexo", "M"), + "codPaisResidencia": rows_tercero.get("codPaisResidencia", "170"), + "codMunicipioResidencia": rows_tercero.get("codMunicipioResidencia", ""), + "codZonaTerritorialResidencia": rows_tercero.get("codZonaTerritorialResidencia", "01"), + "incapacidad": rows_tercero.get("incapacidad", "NO"), + "consecutivo": 1, + "codPaisOrigen": rows_tercero.get("codPaisOrigen", "170"), + "servicios": { + "procedimientos": [ + generar_procedimiento(p, i + 1) + for i, p in enumerate(rows_procedimientos) + ] + }, + } + transaccion["usuarios"].append(usuario) + + return transaccion diff --git a/app/templates/automation.html b/app/templates/automation.html new file mode 100644 index 0000000..9229bfc --- /dev/null +++ b/app/templates/automation.html @@ -0,0 +1,163 @@ +{% extends "base.html" %} +{% block title %}Automatización{% endblock %} +{% block header %}Automatización Completa{% endblock %} +{% block content %} +
+
+
+

Paso 1 + Paso 2 Automático

+

Selecciona las consultas y el rango de fechas. El sistema enviará primero los terceros y luego las transacciones RIPS automáticamente.

+
+
+
+
+
+
+
1
+ Paso 1: Terceros +
+ +
+
+
+
2
+ Paso 2: Transacción RIPS +
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+ + +{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..cccf082 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,122 @@ + + + + + + RIPS Manager - {% block title %}Dashboard{% endblock %} + + + + + +
+ {% if user %} + +
+
+ + RIPS Manager +
+ +
+
+
+ {{ user.username[0]|upper }} +
+
+

{{ user.username }}

+

Sesión activa

+
+
+
+
+ + +
+
+
+

{% block header %}Dashboard{% endblock %}

+
+ + + + +
+
+
+
+ {% block content %}{% endblock %} +
+
+ {% else %} +
+ {% block auth_content %}{% endblock %} +
+ {% endif %} +
+ + + + diff --git a/app/templates/config.html b/app/templates/config.html new file mode 100644 index 0000000..73f183a --- /dev/null +++ b/app/templates/config.html @@ -0,0 +1,137 @@ +{% extends "base.html" %} +{% block title %}Configuración{% endblock %} +{% block header %}Configuración{% endblock %} +{% block content %} +
+
+
+

Conexión Firebird

+
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +

API de Envío

+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +

Datos del Prestador

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +{% endblock %} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..f46a6a7 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,139 @@ +{% extends "base.html" %} +{% block title %}Dashboard{% endblock %} +{% block header %}Dashboard{% endblock %} +{% block content %} +
+
+
+
+ +
+
+

Envíos Exitosos

+

{{ stats.total_exitosos or 0 }}

+
+
+
+
+
+
+ +
+
+

Envíos Fallidos

+

{{ stats.total_errores or 0 }}

+
+
+
+
+
+
+ +
+
+

Terceros

+

{{ stats.total_terceros or 0 }}

+
+
+
+
+
+
+ +
+
+

Transacciones

+

{{ stats.total_transacciones or 0 }}

+
+
+
+
+ +
+
+
+

Últimos Envíos

+
+
+ {% if ultimos %} +
+ + + + + + + + + + + + {% for e in ultimos %} + + + + + + + + {% endfor %} + +
TipoFacturaEstadoFechaUsuario
+ + {{ e.tipo }} + + {{ e.factura or '-' }} + + {{ 'Exitoso' if e.status == 'success' else 'Error' }} + + {{ e.created_at[:19] }}{{ e.username }}
+
+ {% else %} +
+ +

No hay envíos registrados

+

Usa el menú lateral para comenzar

+
+ {% endif %} +
+
+ + +
+{% endblock %} diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..e86e306 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% block title %}Iniciar Sesión{% endblock %} +{% block auth_content %} +
+
+
+
+
+ +
+

RIPS Manager

+

Gestión de envío de RIPS

+
+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+

+ ¿No tienes cuenta? Regístrate +

+
+

RIPS Manager v1.0 — Resolución 2275/2023

+
+
+{% endblock %} diff --git a/app/templates/logs.html b/app/templates/logs.html new file mode 100644 index 0000000..619235a --- /dev/null +++ b/app/templates/logs.html @@ -0,0 +1,132 @@ +{% extends "base.html" %} +{% block title %}Historial{% endblock %} +{% block header %}Historial de Envíos{% endblock %} +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ {% if envios %} +
+ + + + + + + + + + + + + + + + + {% for e in envios %} + + + + + + + + + + + + + {% endfor %} + +
IDTipoFacturaEstadoPacientesServiciosRespuesta APIFechaUsuario
{{ e.id }} + + {{ e.tipo }} + + {{ e.factura or '-' }} + + {{ 'Exitoso' if e.status == 'success' else 'Error' }} + + {{ e.pacientes_count or 0 }}{{ e.servicios_count or 0 }}{{ e.respuesta_api[:80] if e.respuesta_api else '-' }}{{ e.created_at[:19] }}{{ e.username }} + +
+
+ {% else %} +
+ +

No hay registros con los filtros seleccionados

+
+ {% endif %} +
+
+ + + + + +{% endblock %} diff --git a/app/templates/queries.html b/app/templates/queries.html new file mode 100644 index 0000000..7e6853e --- /dev/null +++ b/app/templates/queries.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} +{% block title %}Consultas SQL{% endblock %} +{% block header %}Consultas SQL{% endblock %} +{% block content %} +
+
+
+
+

Mis Consultas

+ +
+
+ {% for q in queries %} +
+
+ {{ q.name }} + + {{ q.query_type }} + +
+

{{ q.description or 'Sin descripción' }}

+
+ {% endfor %} +
+
+
+ +
+
+
+

Editor SQL

+
+
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+

Parámetros disponibles

+

+ Usa :doc_num para filtro por documento, + :factura para filtro por factura, + :fecha_ini y :fecha_fin para rango de fechas. +

+
+
+
+ + +{% endblock %} diff --git a/app/templates/register.html b/app/templates/register.html new file mode 100644 index 0000000..07476e9 --- /dev/null +++ b/app/templates/register.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% block title %}Registro{% endblock %} +{% block auth_content %} +
+
+
+
+
+ +
+

Crear Cuenta

+

Regístrate para gestionar RIPS

+
+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+

+ ¿Ya tienes cuenta? Inicia Sesión +

+
+
+
+{% endblock %} diff --git a/app/templates/terceros.html b/app/templates/terceros.html new file mode 100644 index 0000000..bc45398 --- /dev/null +++ b/app/templates/terceros.html @@ -0,0 +1,130 @@ +{% extends "base.html" %} +{% block title %}Terceros{% endblock %} +{% block header %}Envío de Terceros{% endblock %} +{% block content %} +
+
+
+
+

Generar y Enviar

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+

JSON Generado

+
+
+
 Haz clic en "Vista Previa" para ver el JSON
+
+
+
+ +
+
+
+

Últimos Envíos

+
+
+ {% if envios %} +
+ {% for e in envios %} +
+
+ + {{ 'Exitoso' if e.status == 'success' else 'Error' }} + + {{ e.created_at[:19] }} +
+

{{ e.respuesta_api[:100] if e.respuesta_api else 'Sin respuesta' }}

+
+ {% endfor %} +
+ {% else %} +

No hay envíos de terceros aún

+ {% endif %} +
+
+
+
+ + +{% endblock %} diff --git a/app/templates/transaccion.html b/app/templates/transaccion.html new file mode 100644 index 0000000..86661ef --- /dev/null +++ b/app/templates/transaccion.html @@ -0,0 +1,148 @@ +{% extends "base.html" %} +{% block title %}Transacción RIPS{% endblock %} +{% block header %}Envío de Transacción RIPS{% endblock %} +{% block content %} +
+
+
+
+

Generar y Enviar

+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ +
+
+

JSON Generado

+
+
+
+
 Haz clic en "Vista Previa" para ver el JSON
+
+
+
+ +
+
+
+

Últimos Envíos

+
+
+ {% if envios %} +
+ {% for e in envios %} +
+
+ {{ e.factura or 'N/A' }} + + {{ 'Exitoso' if e.status == 'success' else 'Error' }} + +
+
+ {{ e.pacientes_count or 0 }} pacientes / {{ e.servicios_count or 0 }} servicios + {{ e.created_at[:19] }} +
+
+ {% endfor %} +
+ {% else %} +

No hay envíos de transacciones aún

+ {% endif %} +
+
+
+
+ + +{% endblock %} diff --git a/arrancar.sh b/arrancar.sh new file mode 100644 index 0000000..6b97bed --- /dev/null +++ b/arrancar.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -e +python3 -m venv venv 2>/dev/null || python -m venv venv +source venv/bin/activate 2>/dev/null || source venv/Scripts/activate +pip install -q -r requirements.txt +uvicorn main:app --host 0.0.0.0 --port 8080 diff --git a/main.py b/main.py new file mode 100644 index 0000000..e93ba34 --- /dev/null +++ b/main.py @@ -0,0 +1,95 @@ +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from fastapi import FastAPI, Request, Depends +from fastapi.responses import RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.middleware.cors import CORSMiddleware +import uvicorn + +from app.database import init_db +from app.auth import decode_token + +app = FastAPI(title="RIPS Manager", version="1.0.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +templates = Jinja2Templates( + directory=os.path.join(os.path.dirname(__file__), "app", "templates") +) +app.state.templates = templates + + +def get_user_from_request(request: Request): + cookies = dict(request.cookies) + token = cookies.get("token") + if not token: + auth = request.headers.get("Authorization", "") + if auth.startswith("Bearer "): + token = auth[7:] + if token: + decoded = decode_token(token) + if decoded: + return decoded + print(f" [AUTH] Invalid token: {token[:30]}...", flush=True) + return None + + +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + public_paths = ["/auth/login", "/auth/register", "/auth/api/login"] + if request.url.path in public_paths or request.url.path.startswith("/static"): + return await call_next(request) + + if request.url.path.startswith("/auth"): + return await call_next(request) + + user = get_user_from_request(request) + print(f" [AUTH] path={request.url.path} user={user['username'] if user else None} cookies={dict(request.cookies)}", flush=True) + if not user: + if request.url.path.startswith("/api/"): + from fastapi.responses import JSONResponse + print(f" [AUTH] -> 401 JSON for /api/ path", flush=True) + return JSONResponse({"detail": "Not authenticated"}, status_code=401) + print(f" [AUTH] -> 307 redirect to /auth/login", flush=True) + return RedirectResponse(url="/auth/login") + + request.state.user = user + return await call_next(request) + + +@app.on_event("startup") +async def startup(): + init_db() + + +@app.get("/") +async def root(): + return RedirectResponse(url="/dashboard") + + +# Register routes +from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation + +app.include_router(auth.router) +app.include_router(dashboard.router) +app.include_router(config.router) +app.include_router(queries.router) +app.include_router(terceros.router) +app.include_router(transaccion.router) +app.include_router(logs.router) +app.include_router(automation.router) + + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..018f7c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +jinja2==3.1.5 +python-multipart==0.0.20 +aiofiles==24.1.0 +bcrypt==4.2.1 +python-jose[cryptography]==3.3.0 +httpx==0.28.1 +fdb>=2.0.0 diff --git a/rips_manager.db b/rips_manager.db new file mode 100644 index 0000000..8e7ced6 Binary files /dev/null and b/rips_manager.db differ diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..d78d503 --- /dev/null +++ b/run.bat @@ -0,0 +1,48 @@ +@echo off +chcp 65001 >nul +title RIPS Manager - Iniciando... + +:: Verificar si ya existe Python portable +if exist python\python.exe goto :start + +echo ========================================== +echo Descargando Python portable... +echo ========================================== +echo. + +:: Descargar Python embeddable (64-bit) +powershell -Command "& {Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.12.9/python-3.12.9-embed-amd64.zip' -OutFile 'python.zip'}" + +if not exist python.zip ( + echo ERROR: No se pudo descargar Python + pause + exit /b +) + +echo. +echo Extrayendo... +powershell -Command "& {Expand-Archive -Path 'python.zip' -DestinationPath 'python' -Force}" +del python.zip + +:: Habilitar pip (descomentar línea en el archivo de configuración) +set PYTHON_EXE=%~dp0python\python.exe +set PTH_FILE=%~dp0python\python312._pth +powershell -Command "& {(Get-Content '%PTH_FILE%') -replace '#import site','import site' | Set-Content '%PTH_FILE%'}" + +:: Instalar pip +echo. +echo Instalando pip... +%PYTHON_EXE% -c "import urllib.request; exec(urllib.request.urlopen('https://bootstrap.pypa.io/get-pip.py').read())" + +:start +echo. +echo Instalando dependencias... +python\python.exe -m pip install -q -r requirements.txt 2>nul + +echo. +echo ========================================== +echo Servidor iniciado en http://localhost:8080 +echo Presiona Ctrl+C para detener +echo ========================================== +python\python.exe main.py +pause