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
Files changed (2) hide show
  1. app.py +81 -1
  2. 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, Field, field_validator
 
 
 
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
  """