Spaces:
Sleeping
Sleeping
Adzacam commited on
Commit 路
1f20541
1
Parent(s): 68485d3
feat: implement financial unpivot ingestion endpoint and add new dimension and fact tables to schema
Browse files- app.py +81 -1
- database.py +57 -0
app.py
CHANGED
|
@@ -4,7 +4,10 @@ import logging
|
|
| 4 |
import re
|
| 5 |
from fastapi import FastAPI, Depends, HTTPException, status, Query
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
-
from pydantic import BaseModel,
|
|
|
|
|
|
|
|
|
|
| 8 |
from sqlalchemy.orm import Session
|
| 9 |
from database import (
|
| 10 |
get_db,
|
|
@@ -584,6 +587,83 @@ def procesar_registro_financiero(payload: FinancePayload, db: Session = Depends(
|
|
| 584 |
db.rollback()
|
| 585 |
raise HTTPException(status_code=500, detail=str(e))
|
| 586 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
@app.post("/api/v1/ingesta/financiera/bulk", status_code=status.HTTP_201_CREATED)
|
| 588 |
def procesar_lote_financiero(payloads: List[FinancePayload], db: Session = Depends(get_db), current_user: Users = Depends(get_current_user)):
|
| 589 |
if current_user.role not in ["analista_datos_marketing", "admin"]:
|
|
|
|
| 4 |
import re
|
| 5 |
from fastapi import FastAPI, Depends, HTTPException, status, Query
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from pydantic import BaseModel, ConfigDict
|
| 8 |
+
import rapidfuzz
|
| 9 |
+
import pandas as pd
|
| 10 |
+
from typing import List, Optional, Dict, Any
|
| 11 |
from sqlalchemy.orm import Session
|
| 12 |
from database import (
|
| 13 |
get_db,
|
|
|
|
| 587 |
db.rollback()
|
| 588 |
raise HTTPException(status_code=500, detail=str(e))
|
| 589 |
|
| 590 |
+
class UnpivotFinancePayload(BaseModel):
|
| 591 |
+
id_estudiante: int
|
| 592 |
+
raw_data: Dict[str, Any]
|
| 593 |
+
|
| 594 |
+
@app.post("/api/v1/ingest/finance_unpivot", status_code=status.HTTP_201_CREATED)
|
| 595 |
+
def procesar_lote_financiero_unpivot(payloads: List[UnpivotFinancePayload], db: Session = Depends(get_db)):
|
| 596 |
+
"""
|
| 597 |
+
Recibe un lote de datos financieros con columnas de meses (ej. 'MONTO ENERO 2024')
|
| 598 |
+
y utiliza pandas para despivotarlos antes de insertarlos en FactCobranzasProyectadas.
|
| 599 |
+
"""
|
| 600 |
+
try:
|
| 601 |
+
from database import FactCobranzasProyectadas, DimTiempo
|
| 602 |
+
|
| 603 |
+
# 1. Convertir payloads a DataFrame
|
| 604 |
+
df_list = []
|
| 605 |
+
for p in payloads:
|
| 606 |
+
row = p.raw_data.copy()
|
| 607 |
+
row['id_estudiante'] = p.id_estudiante
|
| 608 |
+
df_list.append(row)
|
| 609 |
+
|
| 610 |
+
if not df_list:
|
| 611 |
+
return {"status": "success", "inserted": 0}
|
| 612 |
+
|
| 613 |
+
df = pd.DataFrame(df_list)
|
| 614 |
+
|
| 615 |
+
# 2. Identificar columnas de meses (Empiezan con 'MONTO ')
|
| 616 |
+
monto_cols = [c for c in df.columns if c.startswith('MONTO ')]
|
| 617 |
+
id_cols = [c for c in df.columns if c not in monto_cols]
|
| 618 |
+
|
| 619 |
+
# 3. Despivotar (Melt)
|
| 620 |
+
df_melted = df.melt(id_vars=id_cols, value_vars=monto_cols, var_name='mes_anio', value_name='monto_esperado')
|
| 621 |
+
|
| 622 |
+
# Filtrar nulos o ceros si no son necesarios
|
| 623 |
+
df_melted['monto_esperado'] = pd.to_numeric(df_melted['monto_esperado'], errors='coerce')
|
| 624 |
+
df_melted = df_melted.dropna(subset=['monto_esperado'])
|
| 625 |
+
|
| 626 |
+
# 4. Insertar en base de datos
|
| 627 |
+
inserted_count = 0
|
| 628 |
+
for index, row in df_melted.iterrows():
|
| 629 |
+
mes_raw = str(row['mes_anio']).replace('MONTO ', '').strip() # Ej 'ENERO 2024'
|
| 630 |
+
parts = mes_raw.split()
|
| 631 |
+
gestion = int(parts[1]) if len(parts) > 1 else 2024
|
| 632 |
+
mes_str = parts[0] if len(parts) > 0 else 'Enero'
|
| 633 |
+
|
| 634 |
+
# Buscar o crear tiempo
|
| 635 |
+
tiempo = db.query(DimTiempo).filter(DimTiempo.gestion == gestion, DimTiempo.mes == mes_str).first()
|
| 636 |
+
if not tiempo:
|
| 637 |
+
tiempo = DimTiempo(gestion=gestion, mes=mes_str)
|
| 638 |
+
db.add(tiempo)
|
| 639 |
+
db.commit()
|
| 640 |
+
db.refresh(tiempo)
|
| 641 |
+
|
| 642 |
+
nuevo_cobro = FactCobranzasProyectadas(
|
| 643 |
+
id_estudiante=row['id_estudiante'],
|
| 644 |
+
id_tiempo=tiempo.id_tiempo,
|
| 645 |
+
monto_esperado=row['monto_esperado'],
|
| 646 |
+
estado_pago='PROYECTADO'
|
| 647 |
+
)
|
| 648 |
+
db.add(nuevo_cobro)
|
| 649 |
+
inserted_count += 1
|
| 650 |
+
|
| 651 |
+
db.commit()
|
| 652 |
+
return {"status": "success", "inserted": inserted_count}
|
| 653 |
+
except Exception as e:
|
| 654 |
+
db.rollback()
|
| 655 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 656 |
+
|
| 657 |
+
@app.post("/api/v1/ingest/surveys", status_code=status.HTTP_201_CREATED)
|
| 658 |
+
def procesar_lote_encuestas(payloads: List[Dict[str, Any]], db: Session = Depends(get_db)):
|
| 659 |
+
"""Ruta para encuestas (Placeholder para l贸gica de NLP sobre comentarios)"""
|
| 660 |
+
return {"status": "success", "message": "Ruta de encuestas lista para implementaci贸n"}
|
| 661 |
+
|
| 662 |
+
@app.post("/api/v1/ingest/marketing", status_code=status.HTTP_201_CREATED)
|
| 663 |
+
def procesar_lote_marketing(payloads: List[Dict[str, Any]], db: Session = Depends(get_db)):
|
| 664 |
+
"""Ruta para OKRs de Marketing (Placeholder)"""
|
| 665 |
+
return {"status": "success", "message": "Ruta de marketing lista para implementaci贸n"}
|
| 666 |
+
|
| 667 |
@app.post("/api/v1/ingesta/financiera/bulk", status_code=status.HTTP_201_CREATED)
|
| 668 |
def procesar_lote_financiero(payloads: List[FinancePayload], db: Session = Depends(get_db), current_user: Users = Depends(get_current_user)):
|
| 669 |
if current_user.role not in ["analista_datos_marketing", "admin"]:
|
database.py
CHANGED
|
@@ -55,6 +55,13 @@ class DimEstudiante(Base):
|
|
| 55 |
id_estudiante = Column(Integer, primary_key=True, index=True)
|
| 56 |
nombre_completo = Column(String(200), nullable=False)
|
| 57 |
codigo_estudiante = Column(String(50))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
class DimDocente(Base):
|
|
@@ -101,6 +108,13 @@ class LogAuditoriaNlp(Base):
|
|
| 101 |
usuario_auditor = Column(Integer, ForeignKey("users.id"))
|
| 102 |
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
# =============================================================================
|
| 106 |
# TABLAS DE HECHOS
|
|
@@ -134,6 +148,49 @@ class FactSituacionFinanciera(Base):
|
|
| 134 |
tipo_alerta = Column(String(20))
|
| 135 |
fecha_registro = Column(DateTime, default=datetime.datetime.utcnow)
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
def get_db():
|
| 139 |
"""
|
|
|
|
| 55 |
id_estudiante = Column(Integer, primary_key=True, index=True)
|
| 56 |
nombre_completo = Column(String(200), nullable=False)
|
| 57 |
codigo_estudiante = Column(String(50))
|
| 58 |
+
genero = Column(String(20))
|
| 59 |
+
fecha_nacimiento = Column(DateTime)
|
| 60 |
+
pais = Column(String(100))
|
| 61 |
+
ciudad = Column(String(100))
|
| 62 |
+
nivel_academico = Column(String(100))
|
| 63 |
+
carrera = Column(String(200))
|
| 64 |
+
institucion_origen= Column(String(200))
|
| 65 |
|
| 66 |
|
| 67 |
class DimDocente(Base):
|
|
|
|
| 108 |
usuario_auditor = Column(Integer, ForeignKey("users.id"))
|
| 109 |
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 110 |
|
| 111 |
+
class DimCategoriaFinanciera(Base):
|
| 112 |
+
"""Nueva dimension para Egresos, Ingresos y OKRs Financieros"""
|
| 113 |
+
__tablename__ = "dim_categoria_financiera"
|
| 114 |
+
id_categoria = Column(Integer, primary_key=True)
|
| 115 |
+
nombre_categoria = Column(String(200), nullable=False)
|
| 116 |
+
tipo = Column(String(50)) # 'INGRESO', 'EGRESO', 'RENTABILIDAD', 'EBITDA'
|
| 117 |
+
|
| 118 |
|
| 119 |
# =============================================================================
|
| 120 |
# TABLAS DE HECHOS
|
|
|
|
| 148 |
tipo_alerta = Column(String(20))
|
| 149 |
fecha_registro = Column(DateTime, default=datetime.datetime.utcnow)
|
| 150 |
|
| 151 |
+
class FactCobranzasProyectadas(Base):
|
| 152 |
+
"""Almacena montos despivotados de ingresos mensuales proyectados."""
|
| 153 |
+
__tablename__ = "fact_cobranzas_proyectadas"
|
| 154 |
+
id_hecho_cobro = Column(Integer, primary_key=True, index=True)
|
| 155 |
+
id_estudiante = Column(Integer, ForeignKey("dim_estudiante.id_estudiante"))
|
| 156 |
+
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo")) # Referencia al mes proyectado
|
| 157 |
+
monto_esperado = Column(Numeric(10, 2))
|
| 158 |
+
estado_pago = Column(String(50)) # Ej. PAGADO, PENDIENTE
|
| 159 |
+
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 160 |
+
|
| 161 |
+
class FactEvaluacionDocente(Base):
|
| 162 |
+
"""Respuestas a encuestas de satisfacci贸n docente y NPS."""
|
| 163 |
+
__tablename__ = "fact_evaluacion_docente"
|
| 164 |
+
id_hecho_eval = Column(Integer, primary_key=True)
|
| 165 |
+
id_docente = Column(Integer, ForeignKey("dim_docente.id_docente"))
|
| 166 |
+
id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
|
| 167 |
+
id_estudiante = Column(Integer, ForeignKey("dim_estudiante.id_estudiante"), nullable=True) # A veces anonimo
|
| 168 |
+
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
|
| 169 |
+
pregunta_bloque = Column(String(500))
|
| 170 |
+
puntuacion = Column(Numeric(4, 2)) # Ej. 1 al 5
|
| 171 |
+
comentario = Column(String(1000))
|
| 172 |
+
|
| 173 |
+
class FactMarketingInscripciones(Base):
|
| 174 |
+
"""M茅tricas de OKRs de Marketing y ventas."""
|
| 175 |
+
__tablename__ = "fact_marketing_inscripciones"
|
| 176 |
+
id_hecho_mkt = Column(Integer, primary_key=True)
|
| 177 |
+
id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
|
| 178 |
+
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
|
| 179 |
+
leads = Column(Integer, default=0)
|
| 180 |
+
reservas = Column(Integer, default=0)
|
| 181 |
+
inscritos = Column(Integer, default=0)
|
| 182 |
+
costo_programa= Column(Numeric(12, 2))
|
| 183 |
+
|
| 184 |
+
class FactRentabilidadPresupuesto(Base):
|
| 185 |
+
"""Indicadores de Rentabilidad, Egresos y EBITDA (Ejecutado vs Meta)."""
|
| 186 |
+
__tablename__ = "fact_rentabilidad_presupuesto"
|
| 187 |
+
id_hecho_rent = Column(Integer, primary_key=True)
|
| 188 |
+
id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
|
| 189 |
+
id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
|
| 190 |
+
id_categoria = Column(Integer, ForeignKey("dim_categoria_financiera.id_categoria"))
|
| 191 |
+
monto_ejecutado = Column(Numeric(14, 2))
|
| 192 |
+
monto_meta = Column(Numeric(14, 2))
|
| 193 |
+
|
| 194 |
|
| 195 |
def get_db():
|
| 196 |
"""
|