Aller au contenu

TP Banque Fonctionnelle avec VAVR & Either

Niveau : Intermédiaire
Technologies : Java 17+, Spring Boot 3.x, VAVR 0.10.4, Spring Data JPA (H2)


Contexte

Vous êtes développeur.euse dans une startup FinTech appelée VavrBank. Vous devez implémenter le backend d’une application de gestion de comptes bancaires en appliquant les concepts vus en formation : Either, Option, Try, Validation.

L’objectif est de ne jamais utiliser d’exceptions pour les erreurs métier, mais uniquement pour les erreurs techniques non prévisibles.


Architecture du projet

vavrbank/
├── src/main/java/com/vavrbank/
│   ├── VavrBankApplication.java
│   ├── config/
│   │   └── JacksonConfig.java
│   ├── domain/
│   │   ├── Compte.java          (entité JPA)
│   │   ├── Transaction.java     (entité JPA)
│   │   └── TypeTransaction.java (enum : CREDIT, DEBIT)
│   ├── error/
│   │   └── BanqueError.java      (interface — à compléter)
│   ├── repository/
│   │   ├── CompteRepository.java
│   │   └── TransactionRepository.java
│   ├── service/
│   │   ├── CompteService.java  (à implémenter)
│   │   └── ValidationService.java (à implémenter)
│   ├── controller/
│   │   └── CompteController.java (à implémenter)
│   └── dto/
│       ├── CreateCompteDTO.java
│       ├── VirementDTO.java
│       └── CompteResponseDTO.java
└── src/main/resources/
    └── application.properties ou application.yml

Étape 1 — Mise en place du projet

1.1 Créez un projet Spring Boot avec les dépendances suivantes

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.vavr</groupId>
        <artifactId>vavr</artifactId>
        <version>0.10.4</version>
    </dependency>
    <dependency>
        <groupId>io.vavr</groupId>
        <artifactId>vavr-jackson</artifactId>
        <version>0.10.4</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

1.2 Configurez application.properties

Pas besoin de base de données postgres

spring.datasource.url=jdbc:h2:mem:vavrbank
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true
spring.jpa.show-sql=true

1.2.1 Ajoutez le code du JacksonConfig.java

package com.vavrbank.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.vavr.jackson.datatype.VavrModule;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * configuration Jackson pour supporter les types VAVR (Option, Either,...)
 * dans la sérialisation/désérialisation JSON.
 */
@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer vavrCustomizer() {
        return builder -> builder.modulesToInstall(new VavrModule());
    }
}

Étape 2 — Le domaine

2.1 Créez l’entité Compte

L’entité Compte doit avoir :

2.2 Créez l’entité Transaction

L’entité Transaction doit avoir :

2.3 Complétez BanqueError

Implémentez la l’interface BanqueError avec les implémentations suivantes (records Java) :

public sealed interface BanqueError {
    // À implémenter avec record :
    // - CompteInexistant(String numero)
    // - SoldeInsuffisant(BigDecimal solde, BigDecimal montant)
    // - MontantInvalide(String raison)
    // - CompteBloque(String numero)
    // - NumeroDejaExistant(String numero)
    // - VirementMemeCompte(String numero)
}

Indice : Utilisez record pour chaque implémentation. Exemple : record CompteInexistant(String numero) implements BanqueError {}


Étape 3 — Service de validation

Implémentez ValidationService avec les méthodes suivantes. Chaque méthode doit utiliser Validation<String, T> de VAVR.

@Service
public class ValidationService {

    /**
     * Valide le numéro de compte.
     * Règles : non null, non vide, correspond au format "FR76-XXXX"
     * (commence par "FR76-" et est suivi de chiffres)
     */
    public Validation<String, String> validerNumero(String numero) {
        // À implémenter
    }

    /**
     * Valide le nom du propriétaire.
     * Règles : non null, non vide, au moins 2 caractères, max 100 caractères
     */
    public Validation<String, String> validerProprietaire(String proprietaire) {
        // À implémenter
    }

    /**
     * Valide un montant de transaction.
     * Règles : non null, strictement positif, max 10 000 €
     */
    public Validation<String, BigDecimal> validerMontant(BigDecimal montant) {
        // À implémenter
    }

    /**
     * Valide un DTO de création de compte complet.
     * Doit utiliser Validation.combine() pour accumuler toutes les erreurs.
     * Retourne Either<List<String>, CreateCompteDTO>
     */
    public Either<List<String>, CreateCompteDTO> validerCreateCompte(CreateCompteDTO dto) {
        // À implémenter
    }
}

Étape 4 — Service métier

Implémentez CompteService. Toutes les méthodes doivent retourner Either<BanqueError, T>.

@Service
@Transactional
public class CompteService {

    /**
     * Crée un nouveau compte bancaire.
     * - Valider le DTO (utiliser ValidationService)
     * - Vérifier que le numéro n'existe pas déjà
     * - Sauvegarder et retourner le compte créé
     */
    public Either<BanqueError, Compte> creerCompte(CreateCompteDTO dto) {
        // À implémenter
    }

    /**
     * Trouve un compte par son numéro.
     * - Utiliser Option.ofNullable() et .toEither()
     */
    public Either<BanqueError, Compte> trouverParNumero(String numero) {
        // À implémenter
    }

    /**
     * Effectue un virement entre deux comptes.
     * Étapes à chaîner avec flatMap :
     * 1. Valider le montant
     * 2. Vérifier que les deux numéros sont différents
     * 3. Trouver le compte source
     * 4. Vérifier que le compte source est actif
     * 5. Vérifier que le solde source est suffisant
     * 6. Trouver le compte destination
     * 7. Vérifier que le compte destination est actif
     * 8. Effectuer le débit/crédit (avec Try pour les erreurs techniques)
     * 9. Sauvegarder la transaction
     */
    public Either<BanqueError, Transaction> effectuerVirement(VirementDTO dto) {
        // À implémenter
    }

    /**
     * Bloque un compte.
     */
    public Either<BanqueError, Compte> bloquerCompte(String numero) {
        // À implémenter
    }

    /**
     * Retourne l'historique des transactions d'un compte.
     */
    public Either<BanqueError, List<Transaction>> getHistorique(String numero) {
        // À implémenter
    }
}

Étape 5 — Controller REST

Implémentez CompteController. Utilisez fold() pour transformer les Either en ResponseEntity.

Endpoint Méthode Description
POST /api/comptes POST Créer un compte
GET /api/comptes/{numero} GET Récupérer un compte
POST /api/comptes/virement POST Effectuer un virement
PUT /api/comptes/{numero}/bloquer PUT Bloquer un compte
GET /api/comptes/{numero}/historique GET Historique des transactions

Codes HTTP attendus


Étape 6 — Tests avec les URL et le JSON

Une fois l’application lancée, testez avec les commandes suivantes directement avec Swagger ou Bruno (ici j’ai mis ce dont vous avez besoin).

Créer des comptes

Créer le compte d’Alicia

{
    "numero": "FR76-0001",
    "proprietaire": "Alicia Duponti"
}

Créer le compte de Boby

{
    "numero": "FR76-0002",
    "proprietaire": "Boby Lapointe"
}

Tenter de créer un compte avec un numéro déjà existant (409)

{
    "numero": "FR76-0001",
    "proprietaire": "Charlie"
}

Créditer un compte (virement depuis compte fictif)

Créditer Alicia depuis un compte externe

{
    "numeroSource": "FR76-0001",
    "numeroDestination": "FR76-0002",
    "montant": 500
}

Tester les erreurs

Virement avec montant négatif : 400

{
    "numeroSource": "FR76-0001",
    "numeroDestination": "FR76-0002",
    "montant": -100
}

Virement vers compte inexistant : 404

{
    "numeroSource": "FR76-0001",
    "numeroDestination": "FR76-9999",
    "montant": 100
}

Compte inexistant : 404

Bloquer un compte

Tenter un virement depuis un compte bloqué : 403

{
    "numeroSource": "FR76-0001",
    "numeroDestination": "FR76-0002",
    "montant": 10
}

Bonus (si vous avez le temps)

Bonus 1 — DataLoader

Ajoutez un CommandLineRunner qui initialise des comptes et des transactions au démarrage pour faciliter les tests. L’interface CommandeLineRunner vous oblige à implémenter la maéthode run() pour y mettre votre code d’initialisation des entités destinées à la persistance. Spring comprend avec l’annotation @Configuration

Je vous donne :

package com.vavrbank.config;

import com.vavrbank.dto.CreateCompteDTO;
import com.vavrbank.dto.VirementDTO;
import com.vavrbank.service.CompteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.math.BigDecimal;

/**
 * initialisation des données de démonstration au démarrage.
 * Utilise les méthodes du service qui retournent Either —
 * on utilise peek() pour logger les succès et left() pour logger les erreurs à titre d'exemple.
 * Souvenez-vous que peek() ne change rien au valeur, il ne sert qu'à générer de l'information pour nos logs.
 */
@Configuration
@RequiredArgsConstructor
@Slf4j
public class DataLoader {

    @Bean
    CommandLineRunner initData(CompteService compteService) {
        return args -> {
            log.info("== Initialisation des données de démonstration ==");

            // Créer des comptes avec dépôt initial
            compteService.creerCompte(new CreateCompteDTO("FR76-0001", "Alicia Duponti", new BigDecimal("5000")))
                .peek(c -> log.info("Compte créé : {} (solde: {}€)", c.getNumero(), c.getSolde()))
                .peekLeft(e -> log.warn("Erreur : {}", e));

            compteService.creerCompte(new CreateCompteDTO("FR76-0002", "Boby Lapointe", new BigDecimal("3000")))
                .peek(c -> log.info("Compte créé : {} (solde: {}€)", c.getNumero(), c.getSolde()))
                .peekLeft(e -> log.warn(" Erreur : {}", e));

            compteService.creerCompte(new CreateCompteDTO("FR76-0003", "Charlie Chaplin", new BigDecimal("1500")))
                .peek(c -> log.info("Compte créé : {} (solde: {}€)", c.getNumero(), c.getSolde()))
                .peekLeft(e -> log.warn("️ Erreur : {}", e));

            // Effectuer quelques virements de démonstration
            compteService.effectuerVirement(new VirementDTO("FR76-0001", "FR76-0002", new BigDecimal("500"), "Remboursement déjeuner"))
                .peek(t -> log.info(" Virement effectué : {} → {} pour {}€", t.getCompteSource(), t.getCompteDestination(), t.getMontant()))
                .peekLeft(e -> log.warn("️ Erreur virement : {}", e));

            compteService.effectuerVirement(new VirementDTO("FR76-0002", "FR76-0003", new BigDecimal("200"), "Cadeau anniversaire"))
                .peek(t -> log.info(" Virement effectué : {} → {} pour {}€", t.getCompteSource(), t.getCompteDestination(), t.getMontant()))
                .peekLeft(e -> log.warn("️ Erreur virement : {}", e));

            log.info("=== Données de démonstration initialisées ===");
            log.info("Console H2 : http://localhost:8080/h2-console (JDBC URL: jdbc:h2:mem:vavrbank)");
        };
    }
}

Bonus 2 — Rapport de comptes

Implémentez un endpoint GET /api/comptes qui retourne tous les comptes avec leur solde, en utilisant les collections VAVR.

Bonus 3 — Dépôt initial

Ajoutez une option dans CreateCompteDTO pour un dépôt initial optionnel (montant de départ). Utilisez Option<BigDecimal> dans le DTO.

Bonus 4 — Endpoint de statistiques

Implémentez GET /api/stats retournant :


Rappel des imports VAVR utiles

import io.vavr.control.Either;
import io.vavr.control.Option;
import io.vavr.control.Try;
import io.vavr.control.Validation;
import io.vavr.collection.List;
import io.vavr.collection.Seq;

Bon courage ! N’oubliez pas : Right = succès (c’est la “bonne” réponse), Left = erreur.