Commit 9ca0772c by Antonio Rueda

Resuelto merge con modelo y uso de notaciones jpa de jakarta

parents 1c879729 9ef7b48f
Showing with 549 additions and 223 deletions
## Proyecto UJACoin con persistencia mediante JPA/Hibernate/MySQL
### Conexión con la base de datos
La forma más sencilla de preparar la base de datos es utilizando la imagen
`mysql` de docker, para ello hay que tener instalado Docker Desktop
(https://www.docker.com/products/docker-desktop). Después basta con ejecutar
los siguientes comandos:
```
docker run -d -p 33060:3306 --name mysql-db -e MYSQL_ROOT_PASSWORD=secret mysql
```
Esto descarga e instala la imagen oficial de mysql (última versión).
Después arranca el contenedor, define _secret_ como clave de root y
asocia MySQL al puerto de la máquina anfitrión 33060.
```
docker exec mysql-db mysql -psecret -e "create database ujacoin; use ujacoin; create user 'ujacoin' identified by 'secret'; grant all privileges on ujacoin.* to 'ujacoin'@'%'"
```
Este comando ejecuta la utilidad de administración `mysql` dentro del contenedor,
crea la base de datos *ujacoin*, un usuario con el mismo nombre y clave _secret_
y finalmente le otorga los permisos necesarios para trabajar con la base
de datos.
Para el testing, crear una nueva base de datos ujacoin_test y dar permisos al usuario creado anteriormente.
```
docker exec mysql-db mysql -psecret -e "create database ujacoin_test; use ujacon_test; grant all privileges on ujacoin_test.* to 'ujacoin'@'%'"
```
......@@ -7,8 +7,8 @@
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<parent>
......@@ -25,10 +25,27 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
......@@ -40,18 +57,6 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!--
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>3.0.3</version>
</dependency>
-->
</dependencies>
<build>
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.boot.autoconfigure.domain.EntityScan;
/**
*
* @author admin
* Clase principal
* @author ajrueda
*/
@SpringBootApplication(scanBasePackages="es.ujaen.dae.ujacoin.servicios")
@SpringBootApplication(scanBasePackages={
"es.ujaen.dae.ujacoin.servicios",
"es.ujaen.dae.ujacoin.repositorios"
})
@EntityScan(basePackages="es.ujaen.dae.ujacoin.entidades")
public class UjaCoinApp {
public static void main(String[] args) throws Exception {
// Creación de servidor
SpringApplication servidor = new SpringApplication(UjaCoinApp.class);
ApplicationContext context = servidor.run(args);
SpringApplication.run(UjaCoinApp.class, args);
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades;
import es.ujaen.dae.ujacoin.util.ExprReg;
import es.ujaen.dae.ujacoin.util.CodificadorMd5;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
......@@ -21,8 +25,12 @@ import java.util.Optional;
* Cliente del banco virtual UjaCoin
* @author ajrueda
*/
public class Cliente {
@Entity
public class Cliente implements Serializable {
/** DNI del cliente*/
@Id
@NotNull
@Size(min=9, max=9)
@Pattern(regexp=ExprReg.DNI)
String dni;
/** Nombre completo */
......@@ -30,27 +38,39 @@ public class Cliente {
String nombre;
/** Fecha de nacimiento */
@NotNull
@Past
LocalDate fNacimiento;
/** Dirección del domicilio */
@NotBlank
String direccion;
/** Teléfono */
@NotNull
@Size(min=9, max=13)
@Pattern(regexp=ExprReg.TLF)
String tlf;
/** Email */
@NotNull
@Email
String email;
/** Clave de acceso al sistema */
@NotNull
String clave;
/** Tarjetas asociadas al cliente (no tiene por qué ser el titular */
@OneToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
@JoinColumn(name="cliente_dni")
List<Tarjeta> tarjetas;
/** Cuentas asociadas al cliente */
@OneToMany(mappedBy="titular")
List<Cuenta> cuentas;
public Cliente() {
}
public Cliente(String dni, String nombre, LocalDate fNacimiento, String direccion, String tlf, String email, String clave) {
this.dni = dni;
......@@ -60,7 +80,7 @@ public class Cliente {
this.tlf = tlf;
this.email = email;
this.clave = CodificadorMd5.codificar(clave);
this.clave = (clave != null ? CodificadorMd5.codificar(clave) : null);
tarjetas = new ArrayList<>();
cuentas = new ArrayList<>();
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades;
import es.ujaen.dae.ujacoin.excepciones.SaldoInsuficienteParaOperacion;
import es.ujaen.dae.ujacoin.entidades.movimientos.Movimiento;
import es.ujaen.dae.ujacoin.util.ExprReg;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
......@@ -21,12 +23,16 @@ import java.util.stream.Collectors;
* Clase para representar cuentas de moneda virtual UjaCoin
* @author ajrueda
*/
@Entity
public class Cuenta {
static final LocalDateTime PASADO_DISTANTE = LocalDateTime.of(1970, 1, 1, 0, 0);
static final LocalDateTime FUTURO_DISTANTE = LocalDateTime.of(2100, 1, 1, 0, 0);
/** Número de cuenta */
@Id
@NotNull
@Size(min=10, max=10)
@Pattern(regexp=ExprReg.NUM_CUENTA)
String num;
/** Saldo de la cuenta en Ujacoins */
......@@ -34,11 +40,20 @@ public class Cuenta {
float saldo;
/** Titular de la cuenta */
@NotNull
@ManyToOne
Cliente titular;
/** Lista de movimientos */
@OneToMany(cascade=CascadeType.ALL)
@JoinColumn(name="cuenta_num")
List<Movimiento> movimientos;
// @Version
// int version;
public Cuenta() {
}
public Cuenta(String num, Cliente titular) {
this.num = num;
this.titular = titular;
......@@ -81,9 +96,10 @@ public class Cuenta {
* @return la lista de movimientos dentro del intervalo de fechas indicado
*/
public List<Movimiento> listarMovimientosDesdeHasta(LocalDateTime fechaHoraDesde, LocalDateTime fechaHoraHasta) {
LocalDateTime fechaHoraDesdeConsulta = Optional.ofNullable(fechaHoraDesde).orElse(PASADO_DISTANTE);
LocalDateTime fechaHoraHastaConsulta = Optional.ofNullable(fechaHoraHasta).orElse(FUTURO_DISTANTE);
return movimientos.stream().filter(m ->
m.getFechaHora().isAfter(fechaHoraDesdeConsulta) &&
m.getFechaHora().isBefore(fechaHoraHastaConsulta)
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades;
import es.ujaen.dae.ujacoin.util.ExprReg;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
import java.time.LocalDate;
/**
* Tarjeta para la realización de ingresos o reintegros en moneda real
* @author ajrueda
*/
public class Tarjeta {
@Entity
public class Tarjeta implements Serializable {
/** Número de tarjeta */
@Id
@NotNull
@Size(min=16, max=16)
@Pattern(regexp=ExprReg.NUM_TARJETA)
String num;
/** Titular de la tarjeta (puede ser diferente al cliente que la usa */
@NotBlank
String titular;
/** Fecha de caducidad */
@NotNull
@Future
LocalDate fechaCaducidad;
/** Código de seguridad (CVC) */
@NotNull
@Size(min=3, max=3)
@Pattern(regexp=ExprReg.CVC)
String cvc;
public Tarjeta() {
}
public Tarjeta(String num, String titular, LocalDate fechaCaducidad, String cvc) {
this.num = num;
this.titular = titular;
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades.movimientos;
import es.ujaen.dae.ujacoin.entidades.Tarjeta;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotNull;
/**
* Ingreso de dinero en cuenta desde una tarjeta
* @author ajrueda
*/
@Entity
public class Ingreso extends Movimiento {
@NotNull
@ManyToOne
Tarjeta tarjeta;
public Ingreso() {
}
/**
* Constructor del igreso
* @param tarjeta la tarjeta desde donde se transfiere el dinero
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades.movimientos;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import jakarta.validation.constraints.Positive;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Clase que representa un movimiento en cuenta
* @author ajrueda
*/
public abstract class Movimiento {
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Movimiento implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
int id;
/** Fecha del movimiento */
@NotNull
@PastOrPresent
LocalDateTime fechaHora;
/** Importe del movimiento. Los valores negativos representan retiradas de dinero */
@Positive
float importe;
public Movimiento() {
}
/**
* Constructor del movimiento
* @param importe el importe asociado al movimiento
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades.movimientos;
import es.ujaen.dae.ujacoin.entidades.Tarjeta;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotNull;
/**
* Reintegro desde la cuenta a una tarjeta destino
* @author ajrueda
*/
@Entity
public class Reintegro extends Movimiento {
@NotNull
@ManyToOne
Tarjeta tarjeta;
public Reintegro() {
}
/**
* Constructor del reintegro
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades.movimientos;
import es.ujaen.dae.ujacoin.entidades.Cuenta;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotNull;
/**
* Transferencia emitida a otra cuenta
* @author ajrueda
*/
@Entity
public class TransferenciaEmitida extends Movimiento {
@NotNull
@ManyToOne
// Columna renombrada para que no colisione con la cuenta del movimiento
@JoinColumn(name="cuenta_dest_num")
Cuenta cuenta;
public TransferenciaEmitida() {
}
public TransferenciaEmitida(Cuenta cuenta, float importe) {
super(-importe);
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades.movimientos;
import es.ujaen.dae.ujacoin.entidades.Cuenta;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotNull;
/**
* Transferencia recibida de otra cuenta
* @author ajrueda
*/
@Entity
public class TransferenciaRecibida extends Movimiento {
@NotNull
@ManyToOne
// Columna renombrada para que no coincida con la cuenta del movimiento
@JoinColumn(name="cuenta_orig_num")
Cuenta cuenta;
public TransferenciaRecibida() {
}
public TransferenciaRecibida(Cuenta cuenta, float importe) {
super(importe);
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.excepciones;
/**
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.excepciones;
/**
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.excepciones;
/**
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.excepciones;
/**
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.excepciones;
/**
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.excepciones;
/**
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.objetosvalor;
/**
* Simple random token for autorization
* NO SE USA EN EL PROYECTO POR EL MOMENTO
* @author ajrueda
*/
/*
public class Token {
public final long id;
public Token() { id = 0; }
private Token(long id) {
this.id = id;
}
public static Token generarAleatorio() {
return new Token(new Random().nextLong());
}
}
*/
\ No newline at end of file
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.repositorios;
import es.ujaen.dae.ujacoin.entidades.Cliente;
import es.ujaen.dae.ujacoin.entidades.Tarjeta;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
*
* @author ajrueda
*/
@Repository
@Transactional(propagation = Propagation.REQUIRED)
public class RepositorioClientes {
@PersistenceContext
EntityManager em;
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Optional<Cliente> buscar(String dni) {
return Optional.ofNullable(em.find(Cliente.class, dni));
}
public void guardar(Cliente cliente) {
em.persist(cliente);
}
public void nuevaTarjeta(Cliente cliente, Tarjeta tarjeta) {
em.persist(tarjeta);
cliente = em.merge(cliente);
cliente.nuevaTarjeta(tarjeta);
}
public void actualizar(Cliente cliente) {
em.merge(cliente);
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.repositorios;
import es.ujaen.dae.ujacoin.entidades.Cuenta;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.PersistenceContext;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
*
* @author ajrueda
*/
@Repository
@Transactional(propagation = Propagation.REQUIRED)
public class RepositorioCuentas {
@PersistenceContext
EntityManager em;
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Optional<Cuenta> buscar(String num) {
return Optional.ofNullable(em.find(Cuenta.class, num));
}
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Optional<Cuenta> buscarYBloquear(String num) {
return Optional.ofNullable(em.find(Cuenta.class, num, LockModeType.PESSIMISTIC_WRITE));
}
public void guardar(Cuenta cuenta) {
em.persist(cuenta);
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.servicios;
import es.ujaen.dae.ujacoin.excepciones.TarjetaYaRegistrada;
......@@ -12,6 +8,7 @@ import es.ujaen.dae.ujacoin.entidades.Cliente;
import es.ujaen.dae.ujacoin.entidades.Cuenta;
import es.ujaen.dae.ujacoin.entidades.Tarjeta;
import es.ujaen.dae.ujacoin.entidades.movimientos.Ingreso;
import es.ujaen.dae.ujacoin.entidades.movimientos.Movimiento;
import es.ujaen.dae.ujacoin.entidades.movimientos.Reintegro;
import es.ujaen.dae.ujacoin.entidades.movimientos.TransferenciaEmitida;
import es.ujaen.dae.ujacoin.entidades.movimientos.TransferenciaRecibida;
......@@ -21,12 +18,15 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import es.ujaen.dae.ujacoin.repositorios.RepositorioClientes;
import es.ujaen.dae.ujacoin.repositorios.RepositorioCuentas;
import jakarta.validation.constraints.Past;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
/**
......@@ -38,14 +38,13 @@ import org.springframework.validation.annotation.Validated;
@Service
@Validated
public class ServicioUjaCoin {
/** Mapa con la lista de clientes ordenada por DNI */
Map<String, Cliente> clientes;
/** Mapa con la lista de cuentas ordenada por número */
Map<String, Cuenta> cuentas;
@Autowired
RepositorioClientes repositorioClientes;
@Autowired
RepositorioCuentas repositorioCuentas;
public ServicioUjaCoin() {
clientes = new TreeMap<>();
cuentas = new TreeMap<>();
}
/**
......@@ -54,16 +53,15 @@ public class ServicioUjaCoin {
* @return la cuenta asociada al cliente
*/
public Cuenta altaCliente(@NotNull @Valid Cliente cliente) {
if (clientes.containsKey(cliente.getDni())) {
throw new ClienteYaRegistrado();
if (repositorioClientes.buscar(cliente.getDni()).isPresent()) {
throw new ClienteYaRegistrado();
}
// Registrar cliente
clientes.put(cliente.getDni(), cliente);
repositorioClientes.guardar(cliente);
// Crear y registrar cuenta
Cuenta cuenta = crearCuenta(cliente);
cuentas.put(cuenta.getNum(), cuenta);
Cuenta cuenta = crearCuenta(cliente);
repositorioCuentas.guardar(cuenta);
return cuenta;
}
......@@ -74,8 +72,14 @@ public class ServicioUjaCoin {
* @param clave la clave de acceso
* @return el objeto de la clase Cliente asociado
*/
@Transactional
public Optional<Cliente> loginCliente(@NotBlank String dni, @NotBlank String clave) {
return Optional.ofNullable(clientes.get(dni)).filter((cliente)->cliente.claveValida(clave));
Optional<Cliente> clienteLogin = repositorioClientes.buscar(dni)
.filter((cliente)->cliente.claveValida(clave));
// Asegurarnos de que se devuelve el cliente con los datos precargados
clienteLogin.ifPresent(c -> c.verCuentas().size());
return clienteLogin;
}
/**
......@@ -84,8 +88,12 @@ public class ServicioUjaCoin {
* @return la cuenta creada
*/
public Cuenta crearCuenta(@NotBlank String dni) {
Cliente cliente = Optional.ofNullable(clientes.get(dni)).orElseThrow(ClienteNoRegistrado::new);
return crearCuenta(cliente);
Cliente cliente = repositorioClientes.buscar(dni).orElseThrow(ClienteNoRegistrado::new);
Cuenta cuenta = crearCuenta(cliente);
repositorioCuentas.guardar(cuenta);
return cuenta;
}
/**
......@@ -93,31 +101,40 @@ public class ServicioUjaCoin {
* @param dni el DNI del cliente
* @param tarjeta la tarjeta a registrar
*/
// @Transactional
public void registrarTarjeta(@NotBlank String dni, @NotNull @Valid Tarjeta tarjeta) {
Cliente cliente = Optional.ofNullable(clientes.get(dni)).orElseThrow(ClienteNoRegistrado::new);
Cliente cliente = repositorioClientes.buscar(dni).orElseThrow(ClienteNoRegistrado::new);
cliente.verTarjeta(tarjeta.getNum()).ifPresent(x -> { throw new TarjetaYaRegistrada(); } );
cliente.nuevaTarjeta(tarjeta);
repositorioClientes.actualizar(cliente);
}
/**
* Devolver las cuentas de un cliente dado
* No es una operación imprescindible puesto que el cliente ya
* tiene la lista de cuentas
* @param dni el DNI del cliente
* @return la lista de cuentas
*/
@Transactional
public List<Cuenta> verCuentas(@NotBlank String dni) {
Cliente cliente = Optional.ofNullable(clientes.get(dni)).orElseThrow(ClienteNoRegistrado::new);
Cliente cliente = repositorioClientes.buscar(dni).orElseThrow(ClienteNoRegistrado::new);
// Precargar a memoria la relación lazy de cuentas del cliente antes de devolver
cliente.verCuentas().size();
return cliente.verCuentas();
}
/**
* Devolver las tarjetas registradas por el usuario
* Devolver las tarjetas registradas por el usuario. No es una operación
* realmente necesaria puesto que el cliente ya contiene esta lista
* @param dni el DNI del cliente
* @return la lista de tarjetas
*/
public List<Tarjeta> verTarjetas(@NotBlank String dni) {
Cliente cliente = Optional.ofNullable(clientes.get(dni)).orElseThrow(ClienteNoRegistrado::new);
return cliente.verTarjetas();
public List<Tarjeta> verTarjetas(@NotBlank String dni) {
Cliente cliente = repositorioClientes.buscar(dni).orElseThrow(ClienteNoRegistrado::new);
return cliente.verTarjetas(); // Relación eager, no hay que hacer nada
}
/**
......@@ -126,13 +143,14 @@ public class ServicioUjaCoin {
* @param numTarjeta el número de la tarjeta desde donde se retira el dinero
* @param importe el importe a ingresar en euros (se tiene en cuenta el cambio euro-UJACoin actual)
*/
@Transactional
public void ingreso(@NotBlank String numCuenta, @NotBlank String numTarjeta, @Positive float importe) {
Cuenta cuenta = Optional.ofNullable(cuentas.get(numCuenta))
Cuenta cuenta = repositorioCuentas.buscarYBloquear(numCuenta)
.orElseThrow(CuentaNoRegistrada::new);
Tarjeta tarjeta = cuenta.getTitular().verTarjeta(numTarjeta)
.orElseThrow(TarjetaNoRegistrada::new);
cuenta.nuevoMovimiento(new Ingreso(tarjeta, importe));
}
......@@ -142,9 +160,10 @@ public class ServicioUjaCoin {
* @param numTarjeta el número de la tarjeta donde se hace el ingreso
* @param importe el importe a retirar
*/
@Transactional
public void reintegro(@NotBlank String numCuenta, @NotBlank String numTarjeta, @Positive float importe) {
Cuenta cuenta = Optional.ofNullable(cuentas.get(numCuenta))
.orElseThrow(CuentaNoRegistrada::new);
Cuenta cuenta = repositorioCuentas.buscarYBloquear(numCuenta)
.orElseThrow(CuentaNoRegistrada::new);
Tarjeta tarjeta = cuenta.getTitular().verTarjeta(numTarjeta)
.orElseThrow(TarjetaNoRegistrada::new);
......@@ -158,27 +177,61 @@ public class ServicioUjaCoin {
* @param numCuentaDestino el número de la cuenta destino
* @param importe el importe a transferir
*/
@Transactional
public void transferencia(@NotBlank String numCuentaOrigen, @NotBlank String numCuentaDestino, @Positive float importe) {
Cuenta cuentaOrigen = Optional.ofNullable(cuentas.get(numCuentaOrigen))
Cuenta cuentaOrigen = repositorioCuentas.buscarYBloquear(numCuentaOrigen)
.orElseThrow(CuentaNoRegistrada::new);
Cuenta cuentaDestino = Optional.ofNullable(cuentas.get(numCuentaDestino))
Cuenta cuentaDestino = repositorioCuentas.buscarYBloquear(numCuentaDestino)
.orElseThrow(CuentaNoRegistrada::new);
cuentaOrigen.nuevoMovimiento(new TransferenciaEmitida(cuentaDestino, importe));
cuentaDestino.nuevoMovimiento(new TransferenciaRecibida(cuentaOrigen, importe));
}
/**
* Devolver la lista de movimientos de la cuenta, filtrada por fechas
* @param numCuenta el número de la cuenta
* @param fechaHoraDesde la fecha inicial
* @param fechaHoraHasta la fecha final
* @return la lista de los movimientos entre las fechas indicadas
*/
@Transactional
List<Movimiento> listarMovimientosCuentaDesdeHasta(@NotBlank String numCuenta, @Past LocalDateTime fechaHoraDesde, @Past LocalDateTime fechaHoraHasta) {
Cuenta cuenta = repositorioCuentas.buscar(numCuenta)
.orElseThrow(CuentaNoRegistrada::new);
return cuenta.listarMovimientosDesdeHasta(fechaHoraDesde, fechaHoraHasta);
}
/**
* Listar movimientos desde una fecha dada
* @param numCuenta el número de la cuenta
* @param fechaHoraDesde fecha de primer movimiento
* @return el listado de movimientos
*/
@Transactional
public List<Movimiento> listarMovimientosDesde(@NotBlank String numCuenta, @Past LocalDateTime fechaHoraDesde) {
Cuenta cuenta = repositorioCuentas.buscar(numCuenta)
.orElseThrow(CuentaNoRegistrada::new);
return cuenta.listarMovimientosDesde(fechaHoraDesde);
}
// Crear una cuenta para el cliente indicado con un número no asignado
private Cuenta crearCuenta(Cliente cliente) {
// Generar número de cuenta de 10 dígitos aleatorio y no usado previamente
String numCuenta;
long min = 1000000000L;
long max = 8999999999L;
do {
numCuenta = Long.toString((new Random().nextLong() % 9000000000L) + 1000000000L);
} while (cuentas.containsKey(numCuenta));
numCuenta = Long.toString(min + (long) (Math.random() * (max - min)));
} while (repositorioCuentas.buscar(numCuenta).isPresent());
Cuenta cuenta = new Cuenta(numCuenta, cliente);
cliente.nuevaCuenta(cuenta);
// No es necesario al ser una relación bidireccional gestionada por JPA
// cliente.nuevaCuenta(cuenta);
return cuenta;
}
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.util;
import java.security.MessageDigest;
......@@ -10,8 +5,8 @@ import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
*
* @author admin
* Codificador sencillo para contraseñas basado en Md5 (no seguro)
* @author ajrueda
*/
public class CodificadorMd5 {
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.util;
/**
*
* Recopilación de expresiones regulares para validación
* @author ajrueda
*/
public class ExprReg {
private ExprReg() {}
private ExprReg() {
}
public static final String DNI = "\\d{8}[A-HJ-NP-TV-Z]";
public static final String TLF = "^(\\+34|0034|34)?[6789]\\d{8}$";
......
## Fichero de configuración para UjaCoin durante testing
# spring.datasource.url: jdbc:mysql://localhost:33060/ujacoin_test
spring.datasource.url: jdbc:h2:mem:ujacoin_test;MODE=MYSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1
spring.jpa.properties.javax.persistence.schema-generation.database.action: drop-and-create
## Fichero de configuración para UjaCoin
spring.datasource.url: jdbc:mysql://localhost:33060/ujacoin
spring.datasource.username: ujacoin
spring.datasource.password: secret
spring.jpa.properties.javax.persistence.schema-generation.database.action: none
spring.data.jpa.repositories.bootstrap-mode: default
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades;
import jakarta.validation.ConstraintViolation;
......@@ -15,7 +10,7 @@ import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* Test para clase Cliente
* Test unitario para clase Cliente
* @author ajrueda
*/
public class ClienteTest {
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.entidades;
import es.ujaen.dae.ujacoin.entidades.movimientos.Ingreso;
......@@ -15,7 +10,7 @@ import org.junit.jupiter.api.Test;
/**
*
* Test unitario para clase Cuenta
* @author ajrueda
*/
public class CuentaTest {
......
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package es.ujaen.dae.ujacoin.servicios;
import es.ujaen.dae.ujacoin.entidades.Cliente;
import es.ujaen.dae.ujacoin.entidades.Cuenta;
import es.ujaen.dae.ujacoin.entidades.Tarjeta;
import es.ujaen.dae.ujacoin.entidades.movimientos.Ingreso;
import es.ujaen.dae.ujacoin.entidades.movimientos.Movimiento;
import es.ujaen.dae.ujacoin.entidades.movimientos.TransferenciaEmitida;
import es.ujaen.dae.ujacoin.entidades.movimientos.TransferenciaRecibida;
import jakarta.validation.ConstraintViolationException;
import es.ujaen.dae.ujacoin.excepciones.SaldoInsuficienteParaOperacion;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.MethodMode;
import org.springframework.test.context.ActiveProfiles;
/**
*
* Test de integración de la aplicación
* @author ajrueda
*/
//@Disabled
// @Disabled
@SpringBootTest(classes = es.ujaen.dae.ujacoin.app.UjaCoinApp.class)
@ActiveProfiles(profiles = {"test"})
public class ServicioUjaCoinTest {
@Autowired
ServicioUjaCoin servicioUjaCoin;
@Test
public void testAccesoServicioUjaCoin() {
Assertions.assertThat(servicioUjaCoin).isNotNull();
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testAltaClienteInvalido() {
// Cliente con e-mail incorrecto!!!
Cliente cliente = new Cliente(
......@@ -57,7 +60,7 @@ public class ServicioUjaCoinTest {
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testAltaYLoginClienteCuenta() {
Cliente cliente = new Cliente(
"11995667D",
......@@ -68,15 +71,63 @@ public class ServicioUjaCoinTest {
"jee@gmail.com",
"clave");
Cuenta cuenta = servicioUjaCoin.altaCliente(cliente);
servicioUjaCoin.altaCliente(cliente);
Optional<Cliente> clienteLogin = servicioUjaCoin.loginCliente(cliente.getDni(), "clave");
Assertions.assertThat(clienteLogin.isPresent()).isTrue();
Assertions.assertThat(clienteLogin.get()).isEqualTo(cliente);
Assertions.assertThat(clienteLogin.get().getDni()).isEqualTo(cliente.getDni());
Assertions.assertThat(clienteLogin.get().verCuentas()).isNotEmpty();
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testCreacionCuentaAdicional() {
Cliente cliente = new Cliente(
"11995667D",
"Juan España España",
LocalDate.of(1990, 11, 1),
"Cl La Luz, 13 - Jaén",
"988674533",
"jee@gmail.com",
"clave");
servicioUjaCoin.altaCliente(cliente);
// Crear cuenta adicional
servicioUjaCoin.crearCuenta(cliente.getDni());
Optional<Cliente> clienteLogin = servicioUjaCoin.loginCliente(cliente.getDni(), "clave");
Assertions.assertThat(clienteLogin.get().verCuentas()).hasSize(2);
Assertions.assertThat(clienteLogin.get().verCuentas().get(0).getNum())
.isNotEqualTo(clienteLogin.get().verCuentas().get(1).getNum());
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testAnadirTarjetaACliente() {
// Registrar cliente y realizar login
Cliente cliente = new Cliente(
"11995667D",
"Juan España España",
LocalDate.of(1990, 11, 1),
"Cl La Luz, 13 - Jaén",
"988674533",
"jee@gmail.com",
"clave");
// Crear cliente
servicioUjaCoin.altaCliente(cliente);
// Añadir una tarjeta
Tarjeta tarjeta = new Tarjeta("4111111111111111", cliente.getNombre(), LocalDate.of(2030, 12, 1), "365");
servicioUjaCoin.registrarTarjeta(cliente.getDni(), tarjeta);
Cliente clienteLogin = servicioUjaCoin.loginCliente(cliente.getDni(), "clave").get();
Assertions.assertThat(clienteLogin.verTarjeta(tarjeta.getNum())).isNotEmpty();
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testIngreso() {
// Registrar cliente y realizar login
Cliente cliente = new Cliente(
......@@ -102,13 +153,15 @@ public class ServicioUjaCoinTest {
// Realizar ingreso y comprobar estado de la cuenta
servicioUjaCoin.ingreso(cuentas.get(0).getNum(), tarjeta.getNum(), 1000);
Assertions.assertThat(cuentas.get(0).listarMovimientos()).hasSize(1);
Assertions.assertThat(cuentas.get(0).listarMovimientos().get(0)).isInstanceOf(Ingreso.class);
Assertions.assertThat(cuentas.get(0).getSaldo()).isEqualTo(1000);
List<Movimiento> movimientos = servicioUjaCoin.listarMovimientosDesde(cuentas.get(0).getNum(), LocalDateTime.MIN);
Assertions.assertThat(movimientos).hasSize(1);
Assertions.assertThat(movimientos.get(0)).isInstanceOf(Ingreso.class);
Assertions.assertThat(movimientos.get(0).getImporte()).isEqualTo(1000);
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testTransferencia() {
// Registrar cliente
Cliente cliente = new Cliente(
......@@ -120,7 +173,7 @@ public class ServicioUjaCoinTest {
"jee@gmail.com",
"claveyyy");
Cuenta cuenta = servicioUjaCoin.altaCliente(cliente);
Cuenta cuentaOrigen = servicioUjaCoin.altaCliente(cliente);
// Añadir una tarjeta
Tarjeta tarjeta = new Tarjeta("4111111111111111", cliente.getNombre(), LocalDate.of(2030, 12, 1), "365");
......@@ -128,7 +181,6 @@ public class ServicioUjaCoinTest {
cliente.nuevaTarjeta(tarjeta);
// Obtener cuenta y realizar ingreso en cuenta
Cuenta cuentaOrigen = servicioUjaCoin.verCuentas(cliente.getDni()).get(0);
servicioUjaCoin.ingreso(cuentaOrigen.getNum(), tarjeta.getNum(), 1000);
// Crear segundo cliente
......@@ -146,12 +198,100 @@ public class ServicioUjaCoinTest {
// Realizar transferencia y comprobar movimientos
servicioUjaCoin.transferencia(cuentaOrigen.getNum(), cuentaDestino.getNum(), 500);
Assertions.assertThat(cuentaOrigen.listarMovimientos()).hasSize(2);
Assertions.assertThat(cuentaOrigen.listarMovimientos().get(1)).isInstanceOf(TransferenciaEmitida.class);
// Refrescar cuenta origen y destino
cuentaOrigen = servicioUjaCoin.verCuentas(cliente.getDni()).get(0);
cuentaDestino = servicioUjaCoin.verCuentas(cliente2.getDni()).get(0);
// Listar movimientos de la cuenta origen
List<Movimiento> movimientos = servicioUjaCoin.listarMovimientosDesde(cuentaOrigen.getNum(), LocalDateTime.MIN);
Assertions.assertThat(movimientos).hasSize(2);
Assertions.assertThat(movimientos.get(1)).isInstanceOf(TransferenciaEmitida.class);
Assertions.assertThat(cuentaOrigen.getSaldo()).isEqualTo(500);
Assertions.assertThat(cuentaDestino.listarMovimientos()).hasSize(1);
Assertions.assertThat(cuentaDestino.listarMovimientos().get(0)).isInstanceOf(TransferenciaRecibida.class);
// Listar movimientos de la cuenta destino
movimientos = servicioUjaCoin.listarMovimientosDesde(cuentaDestino.getNum(), LocalDateTime.MIN);
Assertions.assertThat(movimientos).hasSize(1);
Assertions.assertThat(movimientos.get(0)).isInstanceOf(TransferenciaRecibida.class);
Assertions.assertThat(cuentaDestino.getSaldo()).isEqualTo(500);
}
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testReintegroDoble() {
// Registrar cliente
Cliente cliente = new Cliente(
"11995667D",
"Juan España España",
LocalDate.of(1990, 11, 1),
"Cl La Luz, 13 - Jaén",
"988674533",
"jee@gmail.com",
"claveyyy");
Cuenta cuenta = servicioUjaCoin.altaCliente(cliente);
// Añadir una tarjeta
Tarjeta tarjeta = new Tarjeta("4111111111111111", cliente.getNombre(), LocalDate.of(2030, 12, 1), "365");
servicioUjaCoin.registrarTarjeta(cliente.getDni(), tarjeta);
cliente.nuevaTarjeta(tarjeta);
// Obtener cuenta y realizar ingreso en cuenta
servicioUjaCoin.ingreso(cuenta.getNum(), tarjeta.getNum(), 1000);
Assertions.assertThatThrownBy(() -> {
servicioUjaCoin.reintegro(cuenta.getNum(), tarjeta.getNum(), 1000);
servicioUjaCoin.reintegro(cuenta.getNum(), tarjeta.getNum(), 1000);
}).isInstanceOfAny(SaldoInsuficienteParaOperacion.class);
}
@Test
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
public void testReintegroDobleParalelo() {
// Registrar cliente
Cliente cliente = new Cliente(
"11995667D",
"Juan España España",
LocalDate.of(1990, 11, 1),
"Cl La Luz, 13 - Jaén",
"988674533",
"jee@gmail.com",
"claveyyy");
Cuenta cuenta = servicioUjaCoin.altaCliente(cliente);
// Añadir una tarjeta
Tarjeta tarjeta = new Tarjeta("4111111111111111", cliente.getNombre(), LocalDate.of(2030, 12, 1), "365");
servicioUjaCoin.registrarTarjeta(cliente.getDni(), tarjeta);
cliente.nuevaTarjeta(tarjeta);
// Obtener cuenta y realizar ingreso en cuenta
servicioUjaCoin.ingreso(cuenta.getNum(), tarjeta.getNum(), 1000);
Assertions.assertThatThrownBy(() -> {
// Lanzar un reintegro en un thread secundario
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> result = executor.submit(
() -> {
servicioUjaCoin.reintegro(cuenta.getNum(), tarjeta.getNum(), 1000);
}
);
executor.shutdown();
// Con una pausa siempre funciona :-)
// Thread.sleep(1000);
// Ejecutar otro reintegro en el thread principal
servicioUjaCoin.reintegro(cuenta.getNum(), tarjeta.getNum(), 1000);
// Esperar finalización del reintegro en el thread secundario
// Y obtener excepciones si las hay
try {
result.get();
}
catch(ExecutionException e) {
throw e.getCause();
}
}).isInstanceOfAny(SaldoInsuficienteParaOperacion.class);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment