En vez de la separación clásica en capas (config / controller / service /
repository / dto regados por todo el proyecto), todo el módulo vive en
un solo paquete, transferencias, pensado como un Modelo-Controlador
(sin Vista, porque esto es solo backend):
src/main/java/edu/eci/byteprogramming/bankify/
├── BankifyApplication.java (bootstrap + titulo de swagger)
├── SecurityConfig.java (abre todos los endpoints, sin login)
└── transferencias/
├── Cuenta.java [Modelo] documento Mongo: numeroCuenta, titular, saldo
├── Transaccion.java [Modelo] documento Mongo: registro de cada transferencia
├── TransferenciaRequest.java [Modelo] cuerpo de la petición POST
├── CuentaRepository.java acceso a Mongo (Spring Data, sin código propio)
├── TransaccionRepository.java acceso a Mongo (Spring Data, sin código propio)
├── CuentaSeeder.java crea 2 cuentas de prueba al iniciar la app
└── TransferenciaController.java [Controlador] los 2 endpoints + la lógica de negocio
La lógica de la transferencia (la que contiene el error) vive directamente en
el controlador, sin una capa de "Service" intermedia, para que sea muy fácil
de encontrar y de leer durante el reto: todo el comportamiento de la
transferencia está en un solo método, TransferenciaController.transferir(...).
Los repositorios (CuentaRepository, TransaccionRepository) son interfaces de
Spring Data Mongo sin ningún método escrito a mano (salvo un findByNumeroCuenta):
es la forma más simple de hablar con MongoDB, no una capa de arquitectura a
explicar.
Las clases Cuenta, Transaccion y TransferenciaRequest son POJOs comunes
(con constructor, getters y setters escritos a mano, sin Lombok) para que se
pueda leer exactamente qué hace cada método con solo abrir el archivo.
El proyecto ya traía configurado spring-boot-starter-data-mongodb y lee la
conexión desde variables de entorno (application.properties):
spring.data.mongodb.database=${MONGO_DATABASE}
spring.data.mongodb.uri=${MONGO_URI}Nota:
.env.exampleteníaMONGODB_URI/MONGODB_DATABASE, peroapplication.propertiesleeMONGO_URI/MONGO_DATABASE(sin elDB). Con el nombre original la app no arrancaba (placeholder no resuelto). Se corrigió.env.examplepara que coincida con lo que realmente usa la app.
Para correr el proyecto local, crea un .env en la raíz (no se sube a git)
basado en .env.example, por ejemplo apuntando a un Mongo en Docker:
docker run -d --name bankify-mongo -p 27017:27017 mongo:7MONGO_URI=mongodb://localhost:27017
MONGO_DATABASE=bankify_demo
JWT_SECRET=no-se-usa-en-este-modulo-pero-la-propiedad-debe-existir
JWT_EXPIRATION=86400000Al iniciar, CuentaSeeder crea automáticamente 2 cuentas de prueba si la
colección cuentas está vacía:
| numeroCuenta | titular | saldo |
|---|---|---|
| 1001 | Ana Perez | 1.000.000 |
| 1002 | Carlos Gomez | 500.000 |
Así no se necesita un tercer endpoint solo para crear cuentas de prueba.
El pom.xml ya incluía spring-boot-starter-security y dependencias de JWT
(seguramente para otras fallas del reto). Si se deja tal cual, Spring Security
bloquea todo con un login por defecto. Como este módulo no debe pedir
autenticación, se agregó SecurityConfig.java que desactiva CSRF, HTTP
Basic y el login, y deja todas las rutas en permitAll(). No hay JWT, no hay
usuarios, no hay tokens en este módulo.
Lista todas las cuentas con su saldo actual. Se usa para "ver" el saldo antes y después de la transferencia.
Recibe:
{ "cuentaOrigen": "1001", "cuentaDestino": "1002", "monto": 100000 }Descuenta el monto de la cuenta origen, lo acredita en la cuenta destino y registra la transacción. Esta es la operación que en el estado inicial del repositorio tiene el error.
Ambos endpoints están documentados con anotaciones de springdoc-openapi y se
pueden ver/probar en Swagger UI: http://localhost:8080/swagger-ui/index.html
(JSON crudo en /v3/api-docs). No requieren login.
Todo el problema está en TransferenciaController.transferir(...)
(src/main/java/edu/eci/byteprogramming/bankify/transferencias/TransferenciaController.java,
líneas 44 a 53):
// Descuenta el monto de la cuenta origen
origen.setSaldo(origen.getSaldo() + monto);
cuentaRepository.save(origen);
// Acredita el monto en la cuenta destino
origen.setSaldo(origen.getSaldo() + monto);
cuentaRepository.save(origen);
Transaccion transaccion = new Transaccion(null, origen.getNumeroCuenta(), destino.getNumeroCuenta(),
monto, "PENDIENTE", LocalDateTime.now());
return transaccion;Hay 4 errores, calcados de los 4 que pide el enunciado:
- Descuenta mal el saldo: usa
+en vez de-sobreorigen. En lugar de restar, el monto se suma a la cuenta que está enviando el dinero. - Suma el valor a la cuenta equivocada: la segunda actualización (la que
debería acreditar
destino) vuelve a tocarorigenpor error de copiar y pegar la variable.destinonunca se guarda ni cambia. - No valida fondos suficientes: no existe ningún
ifque compareorigen.getSaldo()contramontoantes de operar, así que se puede transferir más dinero del que hay en la cuenta. - No registra adecuadamente la transacción: se crea el objeto
Transaccion, pero nunca se llama atransaccionRepository.save(...), y el estado se deja en"PENDIENTE"(nunca pasa a"COMPLETADA"). El objeto se devuelve igual al cliente aunque nunca quedó guardado en Mongo (por eso suidsiempre salenull).
Los comentarios (// Descuenta..., // Acredita...) describen lo que el
código debería hacer; la lógica de abajo no cumple eso. Es justo lo que se
le pide encontrar al aspirante durante el reto: a simple vista el código
"parece" hacer lo correcto, pero al leerlo con cuidado no coincide con lo que
dice el comentario.
GET /api/cuentas -> 1001 = 1.000.000 1002 = 500.000
POST /api/transferencias {cuentaOrigen:1001, cuentaDestino:1002, monto:100000}
-> {"id":null,...,"estado":"PENDIENTE"}
GET /api/cuentas -> 1001 = 1.200.000 (¡subió!) 1002 = 500.000 (no cambió)
Y una transferencia de 50.000.000 (mucho más que el saldo) se acepta sin ningún error, porque no se valida nada.
Dentro del mismo método, reemplazar esas líneas por:
if (origen.getSaldo() < monto) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Fondos insuficientes en la cuenta origen");
}
// Descuenta el monto de la cuenta origen
origen.setSaldo(origen.getSaldo() - monto);
cuentaRepository.save(origen);
// Acredita el monto en la cuenta destino
destino.setSaldo(destino.getSaldo() + monto);
cuentaRepository.save(destino);
Transaccion transaccion = new Transaccion(null, origen.getNumeroCuenta(), destino.getNumeroCuenta(),
monto, "COMPLETADA", LocalDateTime.now());
transaccionRepository.save(transaccion);
return transaccion;Los 4 cambios, uno por cada error:
-en vez de+paraorigen.destino.setSaldo(...)(ycuentaRepository.save(destino)) en vez de volver a tocarorigen.- Se agrega la validación de fondos suficientes antes de mover el dinero.
- Se llama a
transaccionRepository.save(transaccion)y el estado se marca"COMPLETADA".
GET /api/cuentas -> 1001 = 1.000.000 1002 = 500.000
POST /api/transferencias {cuentaOrigen:1001, cuentaDestino:1002, monto:100000}
-> {"id":"<generado>",...,"estado":"COMPLETADA"}
GET /api/cuentas -> 1001 = 900.000 (-100.000) 1002 = 600.000 (+100.000)
src/test/java/.../transferencias/TransferenciaControllerTest.java usa
JUnit 5 + Mockito (sin levantar Spring ni Mongo: se simulan los dos
repositorios) y traduce el criterio de aceptación a 7 pruebas:
listarCuentas_debeRetornarLasCuentasDelRepositoriotransferir_debeRechazarMontosMenoresOIgualesACerotransferir_debeRechazarCuentaOrigenInexistentetransferir_debeDescontarElMontoDeLaCuentaOrigentransferir_debeAcreditarElMontoEnLaCuentaDestinotransferir_debeRechazarSiNoHayFondosSuficientestransferir_debeRegistrarLaTransaccionComoCompletada
Importante: con el código tal como está hoy en el repo (con el bug), al correr:
./mvnw test4 de esas 7 pruebas fallan a propósito (descuento de saldo, acreditación en destino, validación de fondos y registro de la transacción). Eso es correcto y esperado: es la evidencia automatizada de la falla, además de la evidencia manual por HTTP. Las otras 3 (monto inválido, cuenta inexistente, listar cuentas) ya pasan porque esa parte no tiene el bug.
Cuando se aplica la corrección de la sección anterior, las 7 pruebas pasan (se verificó localmente antes de entregar este módulo, aplicando el fix de forma temporal).
- Levantar Mongo y crear el
.env(ver sección de Persistencia). ./mvnw spring-boot:run- Abrir Swagger en
http://localhost:8080/swagger-ui/index.htmlo usarcurl:curl http://localhost:8080/api/cuentas curl -X POST http://localhost:8080/api/transferencias \ -H "Content-Type: application/json" \ -d '{"cuentaOrigen":"1001","cuentaDestino":"1002","monto":100000}' curl http://localhost:8080/api/cuentas
- Confirmar que el saldo queda mal (paso "Falla").
- Abrir
TransferenciaController.java, aplicar la corrección indicada arriba. - Detener la app, volver a correr
./mvnw spring-boot:run. - Repetir el mismo
curlde transferencia. - Confirmar que ahora el saldo de origen baja, el de destino sube y la
transacción queda registrada con
estado: "COMPLETADA"y unidreal. ./mvnw testdebería mostrar las 7 pruebas en verde.