¡Descarga economicallyapppara personas y más Apuntes en PDF de Tecnología solo en Docsity!
EconomicallyE
Impulsa tus ahorros con el poder de la IA
Trabajo final de grado.
Tutor: Carlos Gonzales.
Integrantes: José Angel López, Ramiro Cueva.
Curso: 2ºDAW 2024-
Índice
- Ciclo: 2 DAW
- 1 Contenido del documento - 1.1 Introducción..................................................................... - 1.2 Justificación del proyecto y objetivos............................. - 1.3 Planificación..................................................................... - 1.4 Parte experimental......................................................... - 1.4.1 Análisis:¿Qué hará la aplicación?......................... - 1.4.2 Diseño. ¿Cómo se hará la aplicación?................... - 1.4.3 Implementación y pruebas..................................... - 1.4.4 Implantación y documentación.............................. - 1.4.5 Resultados y discusión........................................... - 1.5 Conclusiones..................................................................... - 1.6 Bibliografía y referencias................................................... - 1.7 Anexos............................................................................
Ciclo: 2 DAW 2025
proyecto nace con la necesidad de ayudar a aquellas personas que siempre
planean ahorrar algo a fin de mes y se les complica seguir sus gastos para lograr
sus cometidos.
Estado del arte:
Mercado:
Actualmente el sector económico ha decaído mucho después de la pandemia, lo
cual ha generado cierta necesidad de la población por ahorrar influenciada
también por las redes sociales en cuanto a la atención sobre la economía
personal.
Necesidades:
Falta de educación financiera en la población y el querer empezar a tener un plan
ahorrativo
Productos existentes:
Calculadoras financieras
Calculadora N
Fintonic
Mint
A pesar de los productos existentes, ninguno ofrece recomendaciones
personalizadas y la mayoría de las aplicaciones requiere de un cierto
conocimiento en finanzas. Nuestra diferenciación es que nuestra aplicación busca
ayudar y a planificar un plan de ahorro a nuestros clientes de manera
personalizada
Objetivos:
Ayudar a nuestros usuarios a tener un incremento de ahorro mediante
control de gastos
Seguimiento personalizado aconsejado mediante la IA
Interfaz fácil de usar permitiendo al cliente actualizar datos
Ciclo: 2 DAW 2025
Perfil público al que se dirige:
A pesar de que la aplicación está pensada en ser dirigida a cualquier tipo de
publico que busque ahorrar, mayormente va enfocada en jóvenes los cuales están
teniendo su primer trabajo y tienen poca experiencia en cómo poder manejar sus
ingresos.
1.3. Planificación
Historias de usuario:
1. Como usuario nuevo:
Quiero registrarme en la aplicación con mi correo y una contraseña propia,
acceder a mi perfil y poder agregar y gestionar mis datos personales
2. Como usuario ya registrado:
Quiero poder ver mis datos y poder cambiar las fechas, parámetros, cantidades
según mis intereses y preferencias
3. Como usuario registrado:
Quiero establecer objetivos a futuro(vacaciones, fondo de emergencia, coche,
teléfono móvil). Para tener metas establecidas
4. Como usuario registrado:
Quiero recibir recomendaciones de ahorro personalizadas en base a mis datos
previamente introducidos
5. Como usuario registrado:
Quiero poder ver mis consejos personalizados con fecha y orden para ir
gestionando mi avance
6. Como usuario registrado:
Quiero ver graficas, estadísticas y diagramas sobre mi progreso, en el que se
muestren mis ahorros contrastados con mis gastos para evaluar mis decisiones, si
estoy cumpliendo metas y adecuar mi comportamiento financiero en caso de que
sea necesario
Ciclo: 2 DAW 2025
Generación de consejos financieros con IA:
Mediante la base de ingresos, gastos, metas del usuario la app haría una
petición a la API para luego devolver el consejo financiero
Estas recomendaciones pueden ser consejos sobre cómo ajustar gastos,
cuánto ahorrar mensualmente, o cómo priorizar objetivos.
7.1. Parte experimental
1.4.1 Análisis. ¿Qué hará la aplicación?
La aplicación será un consejero financiero en el sector ahorrativo y personal de
cada usuario, mediante formulario previo el cual se almacenara en una base
de datos utilizada para generar un prompt, la IA obtendrá los datos enviando
este prompt hacia la API de OpenAI, en este caso (Chatgpt4-turbo) la cual se
encargara de dar los consejos personalizados usando los datos de los
usuarios y contexto de los mismos para así ser más precisa.
Dichos consejos también serán guardados en la base de datos para poder
darle al usuario una recomendación basada en su progreso de una manera
más amigable ya que contara con la información necesaria para darle soporte
de manera ahorrativa a nuestros usuarios
La aplicación será un ayudante. En este punto nos referimos a que la
aplicación no controlara tus finanzas ni te dirá en donde invertir, será
meramente un consejero el cual te dirá cuáles son tus posibilidades de ahorro
dependiendo de tus circunstancias económicas
Ciclo: 2 DAW 2025
Diagrama de la base de datos Entidad Relación:
Modelo relacional y tablas a utilizar:
Usuarios: PK id, nombre, email, contraseña, fecha_registro, ingreso_mensual
Recomendaciones: PK id, FK id_usuario, resultadoIA, fecha_recomendacion
Gastos fijos: PK id, FK id_usuario, nombre, monto, frecuencia, descripción
Meta Ahorros: PK id, FK id_usuario, descripción, monto_objetivo,
monto_ahorrado, fecha_meta
Ciclo: 2 DAW 2025
Resumen de perfil:
1.4.2 Diseño. ¿Como se hará la aplicación?
Estructura de base de datos
La aplicación usara una base de datos relacional que almacenara la información
que permitirá utilizar las principales funciones de la aplicación, como la gestión de
usuarios, gastos, ingresos y objetivos financieros.
Las tablas principales serán:
Usuarios (users): almacena información básica del usuario e inicio de
sesión
Gastos fijos (fixed expenses): registros de gastos fijos del usuario,
guardando el id del mismo.
Ciclo: 2 DAW 2025
Gastos variables (variable expenses): aquí se registraran los gastos
variables del usuario.
Metas de Ahorro (goals): aquí se incluyen las metas de ahorro de cada
usuario junto a su fecha estimada para dicho objetivo.
Consejos (advices): esta tabla guardara el id del consejo que nos
proporcione la IA mediante los datos del usuario previamente enviados
junto al prompt.
Arquitectura del proyecto
La aplicación seguirá una arquitectura de capas basada en el patrón MVC
(Modelo, Vista, Controlador) porque hemos llegado a la conclusión de que es lo
mejor para la escalabilidad del proyecto:
Ciclo: 2 DAW 2025
Scripts más relevantes:
El flujo de información es importante en nuestra aplicación, por ende nuestras
entidades tienen que estar bien estructuradas con los métodos crud para poder
enviarle el prompt lo mas especificado con los datos del usuario a la IA
Algunos de los servicios con mas lógica y con mayor importancia de nuestra
aplicación:
OpenAiService
Utilizando RestTemplate, se guardan los valores desde el properties con la
anotación @Value, ApiKey, ApiURL, y el modelo que se le asignara a nuestra
inteligencia artificial, en este caso la API de OpenAi Gpt4-Turbo, el cual fue
escogido por su relación entre calidad-precio y su eficiencia en la conversación
mediante texto, también se manejan las excepciones en caso de fallos en la
comunicación con la API
Este servicio permite obtener una respuesta textual desde la IA que se utilizara
como la recomendación personalizada para el usuario
@Service @RequiredArgsConstructor public class OpenAIServiceImpl implements AIService { private final RestTemplate restTemplate; @Value("${openai.api.url}") private String apiUrl; @Value("${openai.api.key}") private String apiKey; @Value("${openai.model}") private String model; @Override public String getAIRecommendation(String prompt) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType. APPLICATION_JSON ); headers.setBearerAuth(apiKey); Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", model); requestBody.put("messages", List. of ( Map. of ("role", "user", "content", prompt) )); requestBody.put("temperature", 0.7); requestBody.put("max_tokens", 1000 ); HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers); try {
Ciclo: 2 DAW 2025
ResponseEntity
Ciclo: 2 DAW 2025
// Validación: El usuario debe tener ingresos mensuales if (user.getMonthlyIncome() == null || user.getMonthlyIncome() <= 0 ) { return buildErrorAdvice(user.getId(), localeResolver.resolveLocale(request).getLanguage().equals("es") ? "Debes configurar tus ingresos mensuales antes de generar un consejo financiero." : "You must configure your monthly income before generating financial advice."); } Optional lastAdvice = adviceRepository.findTopByUserIdOrderByRecommendationDateDesc(user.getId( )); // Validación 1: Verificar si hay cambios en los datos del usuario boolean dataChanged = lastAdvice.isEmpty() || hasUserDataChanged(lastAdvice.get(), user, currentFixedExpenses, currentVariableExpenses, currentGoals); // Validación 2: Verificar si el último consejo fue hace menos de 1 semana boolean isTooRecent = lastAdvice.isPresent() && lastAdvice.get().getRecommendationDate() .isAfter(LocalDateTime. now ().minusWeeks( 1 )); if (lastAdvice.isPresent()) { if (!dataChanged) { return buildErrorAdvice(user.getId(), localeResolver.resolveLocale(request).getLanguage().equals("es") ? "Debes actualizar tu información financiera antes de recibir un nuevo consejo." : "You must update your financial information before receiving new advice."); } if (isTooRecent) { return buildErrorAdvice(user.getId(), localeResolver.resolveLocale(request).getLanguage().equals("es") ? "Debes esperar al menos una semana desde tu último consejo para recibir uno nuevo." : "You must wait at least one week from your last advice to receive a new one."); } } // Generar el consejo Locale locale = localeResolver.resolveLocale(request); String prompt = buildPrompt(user, currentFixedExpenses, currentVariableExpenses, currentGoals, questionnaire.getPlannedSavings(), locale); // Log del prompt que se envía a la IA logPromptDetails(prompt, locale); String iaResult; try { iaResult = aiService.getAIRecommendation(prompt);
Ciclo: 2 DAW 2025
} catch (Exception e) { iaResult = locale.getLanguage().equals("es") ? "No se pudo generar un consejo en este momento. Inténtalo más tarde." : "Could not generate advice at this time. Please try again later."; } String dataHash = calculateUserDataHash(user, currentFixedExpenses, currentVariableExpenses, currentGoals); String adviceWithHash = "[DATAHASH:" + dataHash + "]\n" + iaResult; Advice advice = Advice. builder () .user(user) .iaResult(iaResult) .recommendationDate(LocalDateTime. now ()) .dataHash(dataHash) .build(); advice = adviceRepository.save(advice); return adviceMapper.toDto(advice); } private String buildPrompt(User user, List fixedExpenses, List variableExpenses, List goals, Double plannedSavings, Locale locale) { StringBuilder prompt = new StringBuilder(); String dataHash = calculateUserDataHash(user, fixedExpenses, variableExpenses, goals); if (locale.getLanguage().equals(new Locale("es").getLanguage())) { prompt.append("Por favor, responde completamente en español.\n\n"); } else { prompt.append(" (IMPORTANT) Please respond entirely in English.\n\n"); } prompt.append("Eres un asesor financiero experto con un enfoque en coaching financiero. Tu objetivo es proporcionar recomendaciones extremadamente prácticas y personalizadas que ayuden al usuario a optimizar sus finanzas sin caer en recortes innecesarios.\n\n"); prompt.append("### CONTEXTO CLAVE QUE DEBES CONSIDERAR ###\n"); prompt.append("- El usuario tiene ingresos mensuales estables de ").append(user.getMonthlyIncome()).append(" €.\n"); prompt.append("- Después de cubrir sus gastos fijos y variables conocidos, le quedan aproximadamente ").append(calculateRemainingMoney(user, fixedExpenses, variableExpenses)).append(" € mensuales libres.\n"); prompt.append("- Esto significa que, si bien hay margen para pequeños ajustes, el usuario ya está en una buena posición para ahorrar sin tener que hacer grandes sacrificios.\n"); prompt.append("- No insistas en eliminar todos los gastos si no es necesario. En su lugar, enseña cómo organizar y aprovechar mejor su dinero libre para alcanzar sus metas.\n"); prompt.append("- El objetivo principal no es solo reducir, sino equilibrar: mantener calidad de vida, reducir gastos innecesarios solo cuando tenga sentido y planificar activamente el ahorro.\n"); prompt.append("- Ten en cuenta que hay gastos que no están detallados (como comida o transporte), por lo que no asumas que el dinero restante
Ciclo: 2 DAW 2025
Documentación de las pruebas realizadas:
Test de comprobación usando Mock
package es.jose.economicallye; @ExtendWith(MockitoExtension.class) public class AdviceServiceImplUnitTest { @Test void generateAdvice_validQuestionnaire_shouldReturnAdviceDTO() { Long userId = 1L; FinancialQuestionnaireDTO questionnaire = FinancialQuestionnaireDTO. builder ().userId(userId).plannedSavings(100.0). build(); User user = User. builder ().id(userId).name("Test User").monthlyIncome(3000.0).build(); List fixedExpenses = Collections. singletonList (FixedExpense. builder ().id(1L).user(user).name(" Rent").amount(1000.0).frequency("Monthly").build()); List variableExpenses = Collections. singletonList (VariableExpense. builder ().id(1L).user(user).nam e("Groceries").amount(300.0).expenseDate(LocalDate. now ()).build()); List goals = Collections. singletonList (Goal. builder ().id(1L).user(user).description("N ew Laptop").targetAmount(1500.0).savedAmount(500.0).deadline(LocalDate. now () .plusMonths( 6 )).build()); String aiRecommendation = "Genial consejo"; Advice savedAdvice = Advice. builder ().id(1L).user(user).iaResult(aiRecommendation).recommendat ionDate(LocalDateTime. now ()).build(); AdviceDTO expectedAdviceDTO = AdviceDTO. builder ().id(1L).userId(userId).iaResult(aiRecommendation).reco mmendationDate(savedAdvice.getRecommendationDate()).build(); when (userRepository.findById(userId)).thenReturn(Optional. of (user)); when (fixedExpenseRepository.findByUserId(userId)).thenReturn(fixedExpense s); when (variableExpenseRepository.findByUserId(userId)).thenReturn(variableE xpenses); when (goalRepository.findByUserId(userId)).thenReturn(goals); when (aiService.getAIRecommendation( anyString ())).thenReturn(aiRecommendat ion); when (adviceRepository.save( any (Advice.class))).thenReturn(savedAdvice); when (adviceMapper.toDto(savedAdvice)).thenReturn(expectedAdviceDTO); AdviceDTO actualAdviceDTO = adviceService.generateAdvice(questionnaire);
Ciclo: 2 DAW 2025
assertNotNull (actualAdviceDTO); assertEquals (expectedAdviceDTO.getId(), actualAdviceDTO.getId()); assertEquals (expectedAdviceDTO.getUserId(), actualAdviceDTO.getUserId()); assertEquals (expectedAdviceDTO.getIaResult(), actualAdviceDTO.getIaResult()); assertEquals (expectedAdviceDTO.getRecommendationDate().toLocalDate(), actualAdviceDTO.getRecommendationDate().toLocalDate()); verify (userRepository, times ( 1 )).findById(userId); verify (fixedExpenseRepository, times ( 1 )).findByUserId(userId); verify (variableExpenseRepository, times ( 1 )).findByUserId(userId); verify (goalRepository, times ( 1 )).findByUserId(userId); verify (aiService, times ( 1 )).getAIRecommendation( anyString ()); verify (adviceRepository, times ( 1 )).save( any (Advice.class)); verify (adviceMapper, times ( 1 )).toDto(savedAdvice); } @Test void generateAdvice_nullUserId_shouldThrowIllegalArgumentException() { FinancialQuestionnaireDTO questionnaire = FinancialQuestionnaireDTO. builder ().build(); assertThrows (IllegalArgumentException.class, () - > adviceService.generateAdvice(questionnaire)); verifyNoInteractions (userRepository); verifyNoInteractions (fixedExpenseRepository); verifyNoInteractions (variableExpenseRepository); verifyNoInteractions (goalRepository); verifyNoInteractions (aiService); verifyNoInteractions (adviceRepository); verifyNoInteractions (adviceMapper); } @Test void generateAdvice_userNotFound_shouldThrowUserNotFoundException() { Long userId = 1L; FinancialQuestionnaireDTO questionnaire = FinancialQuestionnaireDTO. builder ().userId(userId).plannedSavings(100.0). build(); when (userRepository.findById(userId)).thenReturn(Optional. empty ()); assertThrows (UserNotFoundException.class, () - > adviceService.generateAdvice(questionnaire)); verify (userRepository, times ( 1 )).findById(userId); verifyNoInteractions (fixedExpenseRepository); verifyNoInteractions (variableExpenseRepository); verifyNoInteractions (goalRepository); verifyNoInteractions (aiService); verifyNoInteractions (adviceRepository); verifyNoInteractions (adviceMapper); } @Test void getAdviceHistory_nullUserId_shouldThrowIllegalArgumentException() {