Il Modulo Centrale: Gestione Eventi e Partecipanti
Il cuore pulsante di Play The Event è il modulo di gestione eventi e partecipanti. Ogni funzionalità della piattaforma ruota attorno a questo nucleo: dalla creazione di un evento alla suddivisione delle spese, dalla pianificazione dei viaggi all'analisi dei dati, tutto parte dall'evento e dai suoi partecipanti.
In questo articolo esploreremo in profondità come il dominio modella il ciclo di vita di un evento attraverso una macchina a stati, come i partecipanti vengono gestiti con ruoli differenziati e un sistema RSVP completo, e come le API REST espongono tutte queste funzionalità in modo coerente e sicuro.
Cosa Troverai in Questo Articolo
- Il ciclo di vita dell'evento con la macchina a stati: da DRAFT a COMPLETED
- I ruoli dei partecipanti: ORGANIZER, CO_ORGANIZER, ATTENDEE e VIP
- Il sistema RSVP con stati IN_ATTESA, ACCETTATO, RIFIUTATO, FORSE, ANNULLATO e SCADUTO
- Il meccanismo di check-in e checkout con token pubblici e QR code
- La visibilità dell'evento e la condivisione tramite link
- La gestione della capacità e il conteggio real-time dei partecipanti
- Le API REST principali per eventi e partecipanti
L'Aggregato Evento nel Domain-Driven Design
L'entità Evento è l'Aggregate Root principale del sistema. Seguendo i principi del DDD, tutte le modifiche allo stato dell'evento e dei suoi partecipanti passano attraverso questa root, garantendo la consistenza delle regole di business.
L'aggregato incapsula una ricca struttura di dati: titolo, descrizione, date di inizio e fine, luogo con coordinate geografiche, budget con valuta, tipo di ripartizione spese, capacità massima e l'insieme dei partecipanti. Ogni campo è validato attraverso regole di business esplicite definite nel dominio.
AGGREGATE ROOT: Evento
┌────────────────────────────────────────┐
│ Campi Principali │
│ - id (Long, auto-generated) │
│ - titolo (max 200 caratteri) │
│ - descrizione (TEXT, max 2000) │
│ - stato (StatoEvento enum) │
│ - organizzatoreId (FK utente) │
│ - dataInizio / dataFine (Instant) │
│ - budget (Money Value Object) │
│ - luogo + coordinate GPS │
│ - maxPartecipanti (opzionale) │
├────────────────────────────────────────┤
│ Token di Condivisione │
│ - tokenCondivisione (8 chars) │
│ - tokenCheckin (8 chars) │
│ - tokenCheckout (8 chars) │
├────────────────────────────────────────┤
│ Relazioni │
│ - partecipanti: Set<Partecipante> │
│ (1:N, lazy loading) │
└────────────────────────────────────────┘
Il pattern Factory Method viene utilizzato per la creazione: il costruttore
è privato e un metodo statico Evento.crea() si occupa della validazione
e dell'inizializzazione. L'evento nasce sempre in stato DRAFT,
garantendo un punto di partenza coerente.
Il Lifecycle dell'Evento: Macchina a Stati
Ogni evento in Play The Event attraversa un ciclo di vita ben definito, modellato come una macchina a stati finiti. Ogni transizione è protetta da regole di business che impediscono passaggi non validi.
┌───────────┐
│ DRAFT │ (Evento creato, in bozza)
└─────┬─────┘
│
pubblica()
│
▼
┌─────────────┐
│ PUBLISHED │ (Visibile, inviti aperti)
└──────┬──────┘
│
conferma()
│
▼
┌─────────────┐
│ CONFIRMED │ (Evento confermato)
└──────┬──────┘
│
avvia()
│
▼
┌───────────────┐
│ IN_PROGRESS │ (Evento in corso)
└───────┬───────┘
│
iniziaDivisioneSpese()
│
▼
┌───────────────────┐
│ EXPENSE_SPLITTING │ (Divisione spese attiva)
└─────────┬─────────┘
│
completa()
│
▼
┌─────────────┐
│ COMPLETED │ (Evento concluso)
└─────────────┘
NOTA: Da qualsiasi stato (tranne COMPLETED e CANCELLED)
si può passare a CANCELLED tramite annulla(motivo)
Dettaglio degli Stati
Ogni stato ha un significato preciso e determina quali operazioni sono consentite:
- DRAFT: L'evento è stato creato ma non è ancora visibile ai partecipanti. Le date sono opzionali, il titolo e la descrizione possono essere modificati liberamente. È lo stato iniziale di ogni evento.
- PUBLISHED: L'evento è stato pubblicato ed è visibile. Per pubblicare, sono richiesti titolo e data di inizio. Gli inviti possono essere inviati e i partecipanti possono rispondere.
- CONFIRMED: L'evento è confermato e si terrà. La transizione avviene da PUBLISHED quando l'organizzatore conferma che l'evento avrà luogo.
- IN_PROGRESS: L'evento è in corso. I check-in possono essere effettuati, le attività sono live.
- EXPENSE_SPLITTING: L'evento è terminato e le spese vengono suddivise tra i partecipanti. Questa fase è obbligatoria prima del completamento.
- COMPLETED: Tutto è stato finalizzato: spese divise, saldi calcolati. L'evento è in archivio.
- CANCELLED: L'evento è stato annullato. L'annullamento richiede un motivo e genera notifiche a tutti i partecipanti.
Regole di Transizione
Le transizioni di stato sono protette da metodi sull'enum StatoEvento che
determinano cosa è consentito in ogni momento:
canAcceptParticipants()
DRAFT, PUBLISHED, INVITATIONS_SENT, CONFIRMED → true
isModifiable()
DRAFT, PUBLISHED, INVITATIONS_SENT → true
isActive()
Tutti tranne CANCELLED e COMPLETED → true
canSplitExpenses()
IN_PROGRESS, EXPENSE_SPLITTING → true
canComplete()
Solo EXPENSE_SPLITTING → true
Questo approccio garantisce che ogni tentativo di modifica illegale venga intercettato
a livello di dominio, lanciando un'eccezione IllegalStateException con
un messaggio descrittivo, prima ancora di raggiungere il database.
I Ruoli dei Partecipanti
Ogni partecipante a un evento in Play The Event ha un ruolo specifico che determina i suoi permessi e le sue capacità all'interno dell'evento. Il sistema definisce quattro ruoli distinti.
ORGANIZER ("Organizzatore")
└─ Ha creato l'evento
└─ Permessi completi di modifica
└─ Può aggiungere co-organizzatori
└─ Può promuovere/retrocedere partecipanti
└─ Non può essere rimosso dall'evento
└─ Plus-one consentito automaticamente
CO_ORGANIZER ("Co-Organizzatore")
└─ Può modificare l'evento
└─ Può invitare nuovi partecipanti
└─ NON può eliminare l'evento
└─ Plus-one consentito automaticamente
└─ Può essere retrocesso solo dal creatore
ATTENDEE ("Partecipante")
└─ Partecipante standard
└─ Accesso in sola lettura all'evento
└─ Può rispondere all'invito (RSVP)
└─ Può aggiungere restrizioni alimentari
└─ Può invitare altri partecipanti
VIP ("VIP")
└─ Partecipante con status speciale
└─ Stessi permessi di ATTENDEE
└─ Visibilità prioritaria nelle liste
Promozione e Retrocessione
L'aggregato Evento espone metodi dedicati per gestire le promozioni.
Il metodo promuoviACoorganizzatore() verifica che il partecipante non
sia già organizzatore o co-organizzatore prima di eseguire la promozione.
La retrocessione tramite retroceidiCoorganizzatore() riporta il co-organizzatore
al ruolo di ATTENDEE.
Una regola di business importante: quando un partecipante viene promosso a un ruolo organizzativo, il sistema abilita automaticamente il plus-one, permettendo all'organizzatore di portare un accompagnatore all'evento.
Il Sistema RSVP
Il sistema RSVP (Répondez s'il vous plaît) gestisce l'intero flusso delle risposte agli inviti. Ogni partecipante ha uno stato RSVP che evolve nel tempo in base alle sue azioni.
┌───────────────┐ accettaInvito() ┌─────────────┐
│ IN_ATTESA │ ───────────────────▶ │ ACCETTATO │
└───────┬───────┘ └─────────────┘
│
├── rifiutaInvito() ──▶ RIFIUTATO
│
├── rispondiForse() ──▶ FORSE
│
└── (scadenza) ─────▶ SCADUTO
Da ACCETTATO:
annullaPartecipazione() ──▶ ANNULLATO
NOTA: Gli organizzatori vengono impostati
automaticamente come ACCETTATO alla creazione.
Funzionamento degli Inviti
Quando un partecipante viene aggiunto a un evento, il sistema:
- Crea l'entità
Partecipantecon stato IN_ATTESA (gli organizzatori sono automaticamente ACCETTATO) - Registra chi ha effettuato l'invito (
invitatoDaUtenteId) per tracciabilità - Calcola una data di scadenza: se non specificata, il default è il giorno prima dell'evento alle 23:59:59
- Invia una notifica in-app al destinatario con il nome dell'invitante
- Se l'email dell'utente è verificata, invia anche una notifica email
L'invitato può quindi rispondere con tre opzioni: Accetto, Rifiuto o Forse. Ogni risposta registra il timestamp e una nota opzionale. Se l'invito scade senza risposta, un job schedulato lo marca come SCADUTO.
Conteggio Intelligente
Il sistema distingue tra partecipanti certi e potenziali:
contaPartecipantiConfermati()conta solo chi ha stato ACCETTATOcontaPartecipantiPotenziali()include anche chi ha risposto FORSE- Il limite di capacità (
maxPartecipanti) si basa sui confermati
Check-in e Check-out: Controllo Accessi Live
Il sistema di check-in e checkout permette di gestire gli accessi all'evento in tempo
reale, raccogliendo dati preziosi per l'analytics. Entrambi funzionano tramite
token pubblici di 8 caratteri alfanumerici, generati con
SecureRandom e accessibili senza autenticazione.
Check-in degli Invitati
Il check-in raccoglie dati demografici e di sondaggio all'arrivo dei partecipanti.
L'organizzatore può abilitare il check-in generando un link pubblico
(esempio: /c/Ab3kL9xZ) condivisibile tramite QR code.
CheckinInvitato
├─ nome, cognome, età
├─ città e stato di provenienza
├─ orarioArrivo (Instant.now() automatico)
├─ frequenzaPartecipazione
│ (PRIMA_VOLTA, OCCASIONALE, REGOLARE, ABITUALE)
├─ fonteEvento
│ (SOCIAL_MEDIA, AMICI, SITO_WEB, EMAIL, PASSAPAROLA...)
├─ accompagnatori
│ (DA_SOLO, COPPIA, PICCOLO_GRUPPO, GRANDE_GRUPPO)
├─ iscrizioneNewsletter (boolean)
└─ email (opzionale, richiesta se newsletter)
Questi dati alimentano il modulo analytics e permettono di calcolare statistiche come la distribuzione per fascia d'età, le fonti di acquisizione più efficaci, il numero stimato di persone nel gruppo e la percentuale di partecipanti alla prima esperienza.
Checkout (Feedback Post-Evento)
Il checkout è il complemento del check-in: raccoglie il feedback dei partecipanti
dopo l'evento. Accessibile tramite un link separato (esempio: /o/Xy7mN2pQ),
include:
- Valutazione generale: da 1 a 5 stelle, classificata come positiva (4-5), neutra (3) o negativa (1-2)
- Aspetti positivi e da migliorare: selezione multipla tra opzioni predefinite (organizzazione, cibo, location, intrattenimento...)
- Suggerimenti: campo di testo libero fino a 2000 caratteri
- Intenzione di ritorno: SI, FORSE, NO
- Net Promoter Score (NPS): punteggio da 1 a 10, classificato come Promoter (9-10), Passive (7-8) o Detractor (1-6)
Il calcolo del punteggio NPS segue la formula standard: NPS = % Promoters - % Detractors, fornendo una metrica chiave sulla soddisfazione complessiva dei partecipanti.
Visibilità e Condivisione dell'Evento
La piattaforma supporta tre meccanismi di condivisione, ciascuno con il proprio token di 8 caratteri generato in modo sicuro:
1. LINK DI CONDIVISIONE (/e/{token})
└─ Permette di visualizzare i dettagli dell'evento
└─ Attivabile/disattivabile dall'organizzatore
└─ Token rigenerabile (invalida il link precedente)
└─ Flag: condivisioneAttiva (boolean)
2. LINK DI CHECK-IN (/c/{token})
└─ Form pubblico per check-in senza autenticazione
└─ Raccoglie dati demografici e sondaggio
└─ Ideale per QR code stampati all'ingresso
└─ Flag: checkinAttivo (boolean)
3. LINK DI CHECKOUT (/o/{token})
└─ Form pubblico per feedback post-evento
└─ Raccoglie valutazione, NPS, suggerimenti
└─ Condivisibile via email o QR code
└─ Flag: checkoutAttivo (boolean)
Inoltre, l'evento può essere reso visibile sulla mappa pubblica
della piattaforma tramite il flag visibileInMappa. Questa funzionalità
richiede che le coordinate geografiche siano impostate ed è utile per eventi
pubblici che vogliono attirare partecipanti dalla zona.
Capacità e Gestione Real-Time
La gestione della capacità è implementata direttamente nell'aggregato
Evento. Il campo maxPartecipanti è opzionale: se non
impostato, l'evento accetta un numero illimitato di partecipanti.
// Verifica posti disponibili
public boolean haPostiDisponibili() {
if (this.maxPartecipanti == null) {
return true; // Nessun limite
}
return contaPartecipantiConfermati() < this.maxPartecipanti;
}
// Posti rimanenti
public Integer getPostiRimanenti() {
if (this.maxPartecipanti == null) {
return null; // Nessun limite
}
return this.maxPartecipanti - contaPartecipantiConfermati();
}
// Controllo all'aggiunta di un partecipante
public void aggiungiPartecipante(Long utenteId, RuoloPartecipante ruolo) {
// ... validazioni ...
if (this.maxPartecipanti != null &&
contaPartecipantiConfermati() >= this.maxPartecipanti) {
throw new IllegalStateException(
"Raggiunto il numero massimo di partecipanti"
);
}
Partecipante partecipante = Partecipante.crea(this.id, utenteId, ruolo);
this.partecipanti.add(partecipante);
}
Il conteggio si basa esclusivamente sui partecipanti con RSVP ACCETTATO, non includendo chi ha risposto FORSE o non ha ancora risposto. Questo garantisce che il limite di capacità rifletta le conferme effettive.
L'aggregato implementa anche l'optimistic locking tramite il campo
@Version, prevenendo conflitti quando più organizzatori modificano
l'evento contemporaneamente.
Informazioni Aggiuntive del Partecipante
Oltre al ruolo e allo stato RSVP, ogni partecipante può specificare informazioni aggiuntive utili per l'organizzazione:
- Restrizioni alimentari: campo di testo fino a 500 caratteri per allergie, intolleranze o preferenze
- Plus-one: possibilità di portare un accompagnatore, con nome indicato. Il plus-one è automaticamente consentito per organizzatori e co-organizzatori
- Nota di risposta: messaggio libero allegato alla risposta RSVP
- Stato di registrazione: flag
registratoe timestampregistratoIlper il check-in fisico all'evento
Il check-in fisico del partecipante registrato (diverso dal check-in pubblico per invitati esterni) è protetto da una regola: solo chi ha accettato l'invito può effettuare il check-in, e non è possibile farlo due volte.
Le API REST
La piattaforma Play The Event espone API RESTful complete per la gestione di eventi e partecipanti, documentate con OpenAPI/Swagger e protette tramite JWT Bearer Authentication.
Endpoint Evento
POST /api/v1/events Crea evento (stato DRAFT)
GET /api/v1/events/{id} Dettaglio evento
GET /api/v1/events?organizzatoreId={id} Lista eventi per organizzatore
GET /api/v1/events/all Tutti gli eventi dell'utente
?status=DRAFT&sortBy=date&sortDirection=desc
PUT /api/v1/events/{id} Aggiorna dettagli evento
DELETE /api/v1/events/{id} Elimina evento
GET /api/v1/events/statistiche Statistiche aggregate utente
--- Transizioni di Stato ---
POST /api/v1/events/{id}/publish DRAFT → PUBLISHED
POST /api/v1/events/{id}/confirm PUBLISHED → CONFIRMED
POST /api/v1/events/{id}/start CONFIRMED → IN_PROGRESS
POST /api/v1/events/{id}/start-expense-splitting
IN_PROGRESS → EXPENSE_SPLITTING
POST /api/v1/events/{id}/complete EXPENSE_SPLITTING → COMPLETED
POST /api/v1/events/{id}/cancel?motivo=... * → CANCELLED
--- Condivisione ---
POST /api/v1/events/{id}/share Genera link condivisione
GET /api/v1/events/{id}/share Stato condivisione
DELETE /api/v1/events/{id}/share Disattiva condivisione
POST /api/v1/events/{id}/share/regenerate Rigenera token
--- Check-in ---
POST /api/v1/events/{id}/checkin/enable Abilita check-in
DELETE /api/v1/events/{id}/checkin/disable Disabilita check-in
POST /api/v1/events/{id}/checkin/regenerate Rigenera token
GET /api/v1/events/{id}/checkin/status Stato check-in
GET /api/v1/events/{id}/checkin/list Lista check-in (paginata)
GET /api/v1/events/{id}/checkin/stats Statistiche check-in
--- Checkout (Feedback) ---
POST /api/v1/events/{id}/checkout/enable Abilita checkout
DELETE /api/v1/events/{id}/checkout/disable Disabilita checkout
POST /api/v1/events/{id}/checkout/regenerate Rigenera token
GET /api/v1/events/{id}/checkout/status Stato checkout
GET /api/v1/events/{id}/checkout/list Lista feedback (paginata)
GET /api/v1/events/{id}/checkout/stats Statistiche feedback
Endpoint Partecipanti
GET /api/v1/events/{eventId}/participants
Lista partecipanti (arricchita con dati utente)
GET /api/v1/events/{eventId}/participants/{participantId}
Dettaglio singolo partecipante
POST /api/v1/events/{eventId}/participants
Aggiungi partecipante (con notifica e tracciamento invitante)
DELETE /api/v1/events/{eventId}/participants/{participantId}
Rimuovi partecipante (solo organizzatori)
PATCH /api/v1/events/{eventId}/participants/{participantId}/role
?nuovoRuolo=CO_ORGANIZER
Cambia ruolo partecipante
Endpoint Pubblici (Senza Autenticazione)
--- Check-in Pubblico ---
GET /api/v1/public/checkin/{token}/info
Info evento per form check-in
POST /api/v1/public/checkin/{token}
Invia check-in (dati demografici + sondaggio)
--- Checkout Pubblico ---
GET /api/v1/public/checkout/{token}/info
Info evento per form feedback
POST /api/v1/public/checkout/{token}
Invia feedback (valutazione + NPS + suggerimenti)
--- Evento Pubblico ---
GET /api/v1/public/events/{token}
Visualizza dettagli evento condiviso
Notifiche e Comunicazione
Ogni azione significativa sugli eventi genera notifiche attraverso un doppio canale:
- Notifiche in-app: create come entità
NotificaUtentecon riferimento all'evento, tipo di notifica e link diretto - Notifiche email: inviate solo agli utenti con email verificata, usando template specifici per ogni tipo di evento
I tipi di notifica includono: INVITO_EVENTO (quando si viene invitati), EVENTO_MODIFICATO (quando i dettagli cambiano), EVENTO_ANNULLATO (con il motivo dell'annullamento) e NUOVO_CO_ORGANIZZATORE (quando si viene promossi). In tutti i casi, l'autore dell'azione viene escluso dalla lista dei destinatari.
Validazioni e Regole di Business
L'aggregato Evento implementa numerose validazioni a livello di dominio, seguendo il principio DDD secondo cui le regole di business vivono nel modello, non nei controller:
- Titolo: obbligatorio, massimo 200 caratteri
- Date: la data di fine non può precedere la data di inizio; in DRAFT le date sono opzionali
- Coordinate: se fornite, devono essere entrambe presenti; latitudine tra -90 e 90, longitudine tra -180 e 180
- Budget: deve essere positivo, modificabile solo in stati modificabili
- Pubblicazione: richiede titolo e data di inizio
- Partecipanti: unicita per coppia evento-utente, impossibile aggiungere un secondo ORGANIZER
- Capacità: controllo automatico al raggiungimento del limite
Pattern Chiave in Questo Modulo
- Aggregate Root: Evento come punto di accesso unico per tutte le modifiche
- Factory Method:
Evento.crea()ePartecipante.crea()per la creazione controllata - State Machine: transizioni di stato esplicite con validazione
- Optimistic Locking: campo
@Versionper gestione concorrenza - Value Object:
Moneyper il budget,StatoEventoeRuoloPartecipantecome enum ricchi - Token-based Access: condivisione e check-in con token crittograficamente sicuri
Nel prossimo articolo della serie esploreremo il sistema di suddivisione spese, con le sue quattro modalità di split e la gestione multi-valuta che permette ai partecipanti di un evento di dividere equamente i costi sostenuti.







