Salta al contenuto principale

07 - Gestione Eventi e Partecipanti: Il Cuore della Piattaforma

Il Modulo Centrale: Gestione Eventi e Partecipanti Il cuore pulsante di Play The Event è il modulo di gestione eventi e partecipanti.

Cosa ne pensi di questo articolo?

07 - Gestione Eventi e Partecipanti: Il Cuore della Piattaforma
Play the EventFoto:panumas nikhomkhaiPexels

In Breve

Il Modulo Centrale: Gestione Eventi e Partecipanti Il cuore pulsante di Play The Event è il modulo di gestione eventi e partecipanti.

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.

Struttura dell'Aggregate Root Evento
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.

Diagramma degli Stati dell'Evento
                    ┌───────────┐
                    │   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:

Metodi di Controllo su StatoEvento
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.

Enum RuoloPartecipante
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.

Stati RSVP del Partecipante
┌───────────────┐     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à Partecipante con 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 ACCETTATO
  • contaPartecipantiPotenziali() 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.

Dati Raccolti al Check-in
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:

Meccanismi di Condivisione
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.

Logica di Gestione Capacità
// 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 registrato e timestamp registratoIl per 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

API Gestione Evento - /api/v1/events
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

API Gestione Partecipanti - /api/v1/events/{eventId}/participants
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)

API Pubbliche - Check-in e Checkout
--- 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à NotificaUtente con 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() e Partecipante.crea() per la creazione controllata
  • State Machine: transizioni di stato esplicite con validazione
  • Optimistic Locking: campo @Version per gestione concorrenza
  • Value Object: Money per il budget, StatoEvento e RuoloPartecipante come 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.

Domande frequenti

Come viene modellato il ciclo di vita di un evento all'interno del modulo di gestione eventi e partecipanti?

Il ciclo di vita di un evento è modellato come una macchina a stati finiti, con ogni stato protetto da regole di business che impediscono passaggi non validi.

Come vengono gestiti i ruoli dei partecipanti all'interno del sistema?

Ogni partecipante ha un ruolo specifico che determina i suoi permessi e le sue capacità all'interno dell'evento, con quattro ruoli distinti definiti dal sistema.

Come funziona il sistema RSVP per gestire le risposte agli inviti?

Il sistema RSVP registra lo stato RSVP di ogni partecipante, che può essere ACCETTATO, RIFIUTATO o FORSE, con un timestamp e una nota opzionale.

Come vengono esposte le API REST per la gestione di eventi e partecipanti?

Le API REST sono documentate con OpenAPI/Swagger e protette tramite JWT Bearer Authentication, con endpoint specifici per gli eventi, i partecipanti e le notifiche.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti