Salta al contenuto

GET/it/projects/travel-agency

Gestionale per agenzia viaggi

Cercare un viaggio, prenotarlo per più persone, pagarlo con PayPal e scaricare il biglietto: il gestionale di una piccola agenzia, in Spring Boot.

stack
Java 25, Spring Boot 3.5, Spring Security, Spring Data JPA, MariaDB
pagamenti
PayPal, provato fino in fondo nella sandbox
codice
repository privato
stato
Concluso

Il punto di partenza

Il mio primo progetto è stato una biblioteca in Java puro, con i libri in un file di testo. L’agenzia riprende la stessa divisione in entità e service, in un altro dominio.

L’obiettivo era fare tutto il percorso di un’applicazione Spring: dai dati in memoria a un database relazionale, dalla console al web, fino al pagamento e al biglietto. Le scelte e il backend sono miei; le pagine HTML le ha generate l’AI.

La persistenza, un passo alla volta

Dopo la biblioteca sono arrivato a JPA un passo alla volta, e ogni passo risolve un problema di quello prima.

  1. maggio 2025 un altro progetto La biblioteca Java puro, i libri in un file di testo il punto di partenza
  2. 16 ottobre 2025 Liste in memoria viaggi e prenotazioni in liste in memoria al riavvio si perdono
  3. 24 ottobre 2025 JDBC a mano SQL nel codice, ogni service apre la sua connessione i dati sopravvivono al riavvio
  4. 29 ottobre 2025 DataSource e HikariCP Spring inietta un DataSource con il pool connessioni restituite al pool e riusate
  5. 1 dicembre 2025 JPA e Spring Data entità e repository il codice JDBC ripetuto sparisce

MariaDB lo stesso database, cambia come ci si arriva

Come è fatto

La console è venuta prima, con le regole già nei service. Il web si è aggiunto sopra gli stessi service, nello stesso processo, senza toccare la logica delle prenotazioni.

  • Console l’interfaccia di partenza
    Menu nessuna regola
  • Browser Thymeleaf e JavaScript
    Spring Security form login, sessione
    Controller sottili
service le regole e le transazioni; verso l’alto escono solo DTO
  • utenti
  • viaggi
  • prenotazioni
  • biglietti
  • pagamenti
  • email
  • token
Repository Spring Data JPA
HikariCP → MariaDB lo stesso database per la console e per il web

i service parlano anche con

  • PayPal API REST
  • Gmail email

Il menu della console e la cattura di PayPal creano la prenotazione con lo stesso metodo del service.

Dalla ricerca al biglietto

Ordine e cattura passano dal server, con le sue credenziali, e sul web la prenotazione nasce solo quando PayPal gli risponde che il pagamento è completato.

  1. browser → server la ricerca destinazione, prezzo e posti, in una query con filtri opzionali
  2. browser → server la conferma prezzo, sconto, numero di persone
  3. server → PayPal crea l’ordine con le credenziali del server
  4. browser → PayPal il cliente approva nel popup di PayPal
  5. server → PayPal capture → COMPLETED su richiesta del browser
  6. server → MariaDB prenotazione e posti una transazione sola
  7. browser → server il biglietto PDF con QR, solo se la prenotazione è tua

Il modello dei dati

Solo le colonne che raccontano una scelta: i posti come contatore sul viaggio, il prezzo congelato nella prenotazione, il token che scade e vale una volta.

users

  • email
  • password solo l’hash
  • enabled dopo la conferma dell’email

booked_trips

  • people_count persone
  • final_price totale pagato, congelato
  • applied_discount sconto di allora, congelato

trips

  • price prezzo di oggi
  • discount_percentage sconto di oggi
  • available_seats contatore dei posti

e intorno a users

followed_trips

  • user_id
  • trip_id

confirmation_token

  • token casuale
  • expires_at scadenza breve
  • confirmed_at vale una volta
  • token_type conferma o reset

Decisioni

  • ADR 001 Accettata

    La sessione, non un token

    contesto
    Le pagine HTML le genera lo stesso server, con Thymeleaf: non c’è un frontend separato.
    decisione
    Login con il form di Spring Security e la sessione sul server, non con un JWT.
    conseguenze
    Login, logout e redirect li gestisce Spring Security senza codice in più, e il logout chiude davvero la sessione. Nel gestionale per autoscuole, con un client separato, ho usato invece un JWT.
  • ADR 002 Accettata

    Il prezzo si congela nella prenotazione

    contesto
    Prezzo e sconto di un viaggio possono cambiare dopo l’acquisto. I posti non sono assegnati: conta solo quanti ne restano.
    decisione
    La prenotazione salva il totale pagato, lo sconto applicato in quel momento e il numero di persone. Il viaggio tiene un contatore dei posti liberi.
    conseguenze
    Lo storico resta quello che il cliente ha pagato, anche se poi l’agenzia cambia lo sconto. Una riga per posto non serviva: senza posti assegnati basta un contatore.
  • ADR 003 Accettata

    Un token nel database per conferma e reset

    contesto
    In due momenti serve provare che un’email è di chi la usa: alla registrazione e quando si dimentica la password.
    decisione
    Una tabella sola: un token casuale che scade presto e vale una volta sola, con un tipo, conferma o reset. L’account resta disattivato finché l’email non è confermata.
    conseguenze
    Il token si invalida scrivendo una data, cosa che un token firmato e senza stato non permette senza una lista di revoca. Il tipo impedisce che un link di conferma cambi una password, e il reset è arrivato dopo con un tipo in più, non con un’altra tabella.

Le password

  • Dal testo in chiaro all’hash

    All’inizio le password erano salvate in chiaro, e il login le confrontava così com’erano. Poi ho studiato come funziona l’hashing e cos’è BCrypt, e l’ho adottato.

    prima
    in chiaro salvata e confrontata così com’era
    dopo
    BCrypt nel database solo l’hash, verificato al login

Lo stato

Completo, con PayPal in sandbox. Il progetto successivo è il gestionale per autoscuole.

Cosa ho imparato

Scrivere a mano JDBC, le connessioni e il passaggio dalle righe agli oggetti, prima di usare JPA, mi ha fatto capire cosa fa un ORM al posto mio e dove guardare quando fa qualcosa che non mi aspetto. Con PayPal ho visto che un pagamento non è una chiamata sola ma un ordine che passa per più stati, e che la prenotazione deve nascere solo dalla cattura confermata.

Rifacendolo, terrei i posti prima di chiedere il pagamento: oggi si scalano dopo la cattura, e se finiscono nel frattempo il cliente ha pagato per una prenotazione che non nasce.