-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathservices.py
More file actions
247 lines (190 loc) · 11.5 KB
/
Copy pathservices.py
File metadata and controls
247 lines (190 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
from typing import List, Optional
from models import Presupuesto, Movimiento, ResumenPresupuesto
from database import DatabaseInterface
from gmail_adapter import GmailAdapter
from email_parser import EmailParser
from datetime import datetime
class PresupuestoService:
"""Servicio para gestión de presupuestos (Single Responsibility)"""
def __init__(self, repository: DatabaseInterface):
self._repository = repository
def crear_presupuesto(self, usuario_id: str, categoria: str, monto: float, periodicidad: str = "mensual") -> tuple[bool, str]:
"""Crea un nuevo presupuesto con validaciones"""
# Validaciones
if not categoria.strip():
return False, "La categoría no puede estar vacía"
if monto <= 0:
return False, "El monto debe ser mayor a 0"
if periodicidad.lower() not in ["diario", "semanal", "mensual", "anual"]:
return False, "Periodicidad debe ser: diario, semanal, mensual, anual"
# Crear presupuesto
presupuesto = Presupuesto(
usuario_id=usuario_id,
categoria=categoria,
monto=monto,
periodicidad=periodicidad.lower()
)
success = self._repository.crear_presupuesto(presupuesto)
if success:
return True, f"Presupuesto '{categoria}' creado exitosamente"
else:
return False, f"Ya existe un presupuesto para '{categoria}'"
def actualizar_presupuesto(self, usuario_id: str, categoria: str, monto: float, periodicidad: Optional[str] = None) -> tuple[bool, str]:
"""Actualiza un presupuesto existente"""
if monto <= 0:
return False, "El monto debe ser mayor a 0"
if periodicidad and periodicidad.lower() not in ["diario", "semanal", "mensual", "anual"]:
return False, "Periodicidad debe ser: diario, semanal, mensual, anual"
success = self._repository.actualizar_presupuesto(usuario_id, categoria, monto, periodicidad)
if success:
return True, f"Presupuesto '{categoria}' actualizado exitosamente"
else:
return False, f"No existe presupuesto '{categoria}'"
def eliminar_presupuesto(self, usuario_id: str, categoria: str) -> tuple[bool, str]:
"""Elimina un presupuesto y todos sus movimientos"""
success = self._repository.eliminar_presupuesto(usuario_id, categoria)
if success:
return True, f"Presupuesto '{categoria}' eliminado exitosamente"
else:
return False, f"No existe presupuesto '{categoria}'"
def obtener_resumen(self, usuario_id: str) -> List[ResumenPresupuesto]:
"""Obtiene el resumen de todos los presupuestos"""
return self._repository.obtener_resumen(usuario_id)
def presupuesto_existe(self, usuario_id: str, categoria: str) -> bool:
"""Verifica si existe un presupuesto"""
return self._repository.presupuesto_existe(usuario_id, categoria)
class MovimientoService:
"""Servicio para gestión de movimientos (Single Responsibility)"""
def __init__(self, repository: DatabaseInterface, presupuesto_service: PresupuestoService):
self._repository = repository
self._presupuesto_service = presupuesto_service
def registrar_gasto(self, usuario_id: str, categoria: str, monto: float, concepto: str = "") -> tuple[bool, str]:
"""Registra un gasto"""
return self._registrar_movimiento(usuario_id, categoria, "gasto", monto, concepto)
def registrar_ingreso(self, usuario_id: str, categoria: str, monto: float, concepto: str = "") -> tuple[bool, str]:
"""Registra un ingreso"""
return self._registrar_movimiento(usuario_id, categoria, "ingreso", monto, concepto)
def _registrar_movimiento(self, usuario_id: str, categoria: str, tipo: str, monto: float, concepto: str = "") -> tuple[bool, str]:
"""Registra un movimiento con validaciones"""
# Validaciones
if not self._presupuesto_service.presupuesto_existe(usuario_id, categoria):
return False, f"No existe presupuesto '{categoria}'. Créalo primero con /crear"
if monto <= 0:
return False, "El monto debe ser mayor a 0"
# Crear movimiento
try:
movimiento = Movimiento(
usuario_id=usuario_id,
categoria=categoria,
tipo=tipo,
monto=monto,
concepto=concepto
)
success = self._repository.registrar_movimiento(movimiento)
if success:
accion = "Gasto registrado" if tipo == "gasto" else "Ingreso registrado"
return True, f"💸 {accion}: ${monto:,.0f} en {categoria}" + (f" - {concepto}" if concepto else "")
else:
return False, "Error al registrar el movimiento"
except ValueError as e:
return False, str(e)
def obtener_historial(self, usuario_id: str, categoria: str) -> tuple[bool, str, List[Movimiento]]:
"""Obtiene el historial de movimientos de una categoría"""
if not self._presupuesto_service.presupuesto_existe(usuario_id, categoria):
return False, f"No existe presupuesto '{categoria}'", []
movimientos = self._repository.obtener_historial(usuario_id, categoria)
if not movimientos:
return False, f"No hay movimientos registrados para '{categoria}'", []
return True, "Historial obtenido exitosamente", movimientos
class GmailSyncService:
"""Servicio para sincronización con Gmail"""
def __init__(self, repository: DatabaseInterface, presupuesto_service: PresupuestoService):
self._repository = repository
self._presupuesto_service = presupuesto_service
self._adapter = GmailAdapter()
self._parser = EmailParser()
def sincronizar_movimientos(self, usuario_id: str) -> tuple[bool, str]:
"""Sincroniza movimientos desde Gmail"""
# Asegurar categoría por defecto para importaciones
cat_import = "importados_gmail"
if not self._presupuesto_service.presupuesto_existe(usuario_id, cat_import):
self._presupuesto_service.crear_presupuesto(usuario_id, cat_import, 1000000, "mensual")
try:
# Calcular fecha de inicio: últimos 62 días
hoy = datetime.now()
from datetime import timedelta
inicio_filtro = hoy - timedelta(days=62)
query_date = inicio_filtro.strftime("%Y/%m/%d")
# Query: de Bancolombia Y posterior al inicio de mes
# Nota: El usuario reportó que el remitente es an.notificacionesbancolombia.com
sender = "alertasynotificaciones@an.notificacionesbancolombia.com"
query = f"from:{sender} after:{query_date}"
emails = self._adapter.get_messages(query=query, max_results=100)
if not emails:
return True, f"No se encontraron correos nuevos desde {query_date}."
count = 0
for email in emails:
body = self._adapter.get_message_body(email)
parsed = self._parser.parse_body(body)
if parsed:
# Usar ID del mensaje como external_id para evitar duplicados
external_id = email['id']
movimiento = Movimiento(
usuario_id=usuario_id,
categoria=cat_import, # Por defecto
tipo=parsed.tipo,
monto=parsed.monto,
concepto=f"{parsed.comercio} (Importado)",
fecha=parsed.fecha,
external_id=external_id,
origen="gmail"
)
try:
if self._repository.registrar_movimiento(movimiento):
count += 1
except Exception:
pass # Probablemente duplicado (Unique Constraint)
return True, f"Sincronización completada. {count} movimientos nuevos importados en '{cat_import}'."
except Exception as e:
return False, f"Error en sincronización: {str(e)}"
class StatisticsService:
"""Servicio de estadísticas"""
def __init__(self, repository: DatabaseInterface):
self._repository = repository
def obtener_estadisticas_mes(self, usuario_id: str, mes: int, anio: int) -> dict:
return self._repository.obtener_estadisticas_mes(usuario_id, mes, anio)
def obtener_movimientos_filtro(self, usuario_id: str, mes: int, anio: int) -> List[Movimiento]:
return self._repository.obtener_movimientos_filtro(usuario_id, mes, anio)
class MonevoFacade:
"""Facade que unifica todos los servicios (Facade Pattern)"""
def __init__(self, repository: DatabaseInterface):
self.presupuesto_service = PresupuestoService(repository)
self.movimiento_service = MovimientoService(repository, self.presupuesto_service)
self.gmail_service = GmailSyncService(repository, self.presupuesto_service)
self.stats_service = StatisticsService(repository)
# Métodos de presupuesto
def crear_presupuesto(self, usuario_id: str, categoria: str, monto: float, periodicidad: str = "mensual") -> tuple[bool, str]:
return self.presupuesto_service.crear_presupuesto(usuario_id, categoria, monto, periodicidad)
def actualizar_presupuesto(self, usuario_id: str, categoria: str, monto: float, periodicidad: Optional[str] = None) -> tuple[bool, str]:
return self.presupuesto_service.actualizar_presupuesto(usuario_id, categoria, monto, periodicidad)
def eliminar_presupuesto(self, usuario_id: str, categoria: str) -> tuple[bool, str]:
return self.presupuesto_service.eliminar_presupuesto(usuario_id, categoria)
def obtener_resumen(self, usuario_id: str) -> List[ResumenPresupuesto]:
return self.presupuesto_service.obtener_resumen(usuario_id)
# Métodos de movimientos
def registrar_gasto(self, usuario_id: str, categoria: str, monto: float, concepto: str = "") -> tuple[bool, str]:
return self.movimiento_service.registrar_gasto(usuario_id, categoria, monto, concepto)
def registrar_ingreso(self, usuario_id: str, categoria: str, monto: float, concepto: str = "") -> tuple[bool, str]:
return self.movimiento_service.registrar_ingreso(usuario_id, categoria, monto, concepto)
def obtener_historial(self, usuario_id: str, categoria: str) -> tuple[bool, str, List[Movimiento]]:
return self.movimiento_service.obtener_historial(usuario_id, categoria)
def presupuesto_existe(self, usuario_id: str, categoria: str) -> bool:
return self.presupuesto_service.presupuesto_existe(usuario_id, categoria)
# Métodos de Gmail
def sincronizar_gmail(self, usuario_id: str) -> tuple[bool, str]:
return self.gmail_service.sincronizar_movimientos(usuario_id)
# Métodos de Estadísticas
def obtener_estadisticas_mes(self, usuario_id: str, mes: int, anio: int) -> dict:
return self.stats_service.obtener_estadisticas_mes(usuario_id, mes, anio)
def obtener_movimientos_filtro(self, usuario_id: str, mes: int, anio: int) -> List[Movimiento]:
return self.stats_service.obtener_movimientos_filtro(usuario_id, mes, anio)