Pagamenti e Abbonamenti con Stripe
In una piattaforma SaaS come Play The Event, la gestione dei pagamenti non è semplicemente un modulo accessorio: è il cuore del modello di business. Ogni utente deve poter scegliere un piano tariffario, sottoscrivere un abbonamento, ricevere fatture automatiche e gestire il proprio metodo di pagamento in completa autonomia. Per raggiungere questo obiettivo, la piattaforma si integra con Stripe, il provider di pagamenti più utilizzato nel mondo SaaS, sfruttando le sue API per Checkout, Subscription, Customer Portal e Webhook.
In questo articolo analizzeremo in dettaglio l'architettura del sistema di pagamenti, dalla definizione dei piani tariffari alla macchina a stati degli abbonamenti, passando per la gestione dei fallimenti di pagamento, i codici promozionali e il sistema di finanziamento eventi.
Cosa Troverai in Questo Articolo
- L'architettura di integrazione con Stripe (Customer, Subscription, Price IDs)
- I piani di abbonamento organizzati per categoria (Private, Business, Associazione)
- I cicli di fatturazione mensile e annuale
- La macchina a stati dell'abbonamento (PENDING → ACTIVE → CANCELLED/SUSPENDED/EXPIRED)
- La gestione dei fallimenti di pagamento con 3 retry automatici
- I Factory Methods per la creazione di abbonamenti (Stripe, gratuiti, bypass)
- Il sistema di codici promozionali con bypass pagamento e bypass permanente
- La gestione dei webhook Stripe per aggiornamenti in tempo reale
- Gli acquisti evento e il sistema di finanziamento/crowdfunding
Architettura dell'Integrazione Stripe
L'integrazione con Stripe segue un pattern architetturale preciso: il backend funge da orchestratore tra l'utente e le API Stripe, senza mai esporre le chiavi segrete al frontend. Il flusso di comunicazione si basa su tre concetti fondamentali di Stripe che vengono mappati nel dominio applicativo.
I Tre Pilastri dell'Integrazione
- Customer: ogni utente registrato sulla piattaforma viene associato a un oggetto
Customersu Stripe, identificato daidClienteStripe. Questo permette a Stripe di tracciare i metodi di pagamento e la cronologia delle transazioni - Subscription: quando un utente sottoscrive un piano a pagamento, viene creata una
Subscriptionsu Stripe, identificata daidAbbonamentoStripe. La subscription gestisce il ciclo di fatturazione automatica - Price ID: ogni piano tariffario è collegato a un
Pricesu Stripe tramiteidPrezzoStripe. Il Price ID definisce l'importo, la valuta e l'intervallo di fatturazione
@Entity
@Table(name = "abbonamenti")
public class Abbonamento {
// Collegamento utente
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "utente_id", nullable = false)
private User utente;
// Dettagli Stripe
@Column(name = "id_cliente_stripe")
private String idClienteStripe; // cus_xxxxxxxxxxxx
@Column(name = "id_abbonamento_stripe")
private String idAbbonamentoStripe; // sub_xxxxxxxxxxxx
@Column(name = "id_prezzo_stripe")
private String idPrezzoStripe; // price_xxxxxxxxxxxx
// Piano e fatturazione
@Enumerated(EnumType.STRING)
private TipoPiano tipoPiano;
@Enumerated(EnumType.STRING)
private CicloPagamento cicloFatturazione;
private BigDecimal importo;
private String valuta = "EUR";
}
Servizio StripeService: il Ponte con le API
Il StripeService è il componente infrastrutturale che incapsula tutte le
interazioni con le API Stripe. Gestisce la mappatura bidirezionale tra i Price ID di Stripe e
i piani tariffari interni, permettendo sia la creazione di sessioni di checkout sia la risoluzione
inversa dei piani a partire dai webhook.
@Service
public class StripeService {
// Mappa: "PRIVATE_STARTER_MENSILE" -> "price_xxxxx"
private final Map<String, String> stripePriceIds = new HashMap<>();
// Mappa inversa: "price_xxxxx" -> "PRIVATE_STARTER_MENSILE"
private final Map<String, String> priceIdToPlanMap = new HashMap<>();
public String getPriceId(TipoPiano tipoPiano, CicloPagamento ciclo) {
String key = tipoPiano.name() + "_" + ciclo.name();
return stripePriceIds.get(key);
}
public TipoPiano getTipoPianoFromPriceId(String priceId) {
String planKey = priceIdToPlanMap.get(priceId);
// Parsing: "PRIVATE_STARTER_MENSILE" -> "PRIVATE_STARTER"
String tipoPianoStr = planKey.endsWith("_MENSILE")
? planKey.substring(0, planKey.length() - "_MENSILE".length())
: planKey.substring(0, planKey.length() - "_ANNUALE".length());
return TipoPiano.valueOf(tipoPianoStr);
}
}
Piani di Abbonamento
Play The Event
offre una struttura tariffaria articolata su tre categorie di account, ciascuna con piani
progressivamente più ricchi di funzionalità. L'enum TipoPiano definisce
tutti i piani disponibili con i relativi prezzi, limiti e compatibilità con il tipo di account.
Piani Private (Individui)
- PRIVATE_FREE: piano gratuito con limiti base (3 eventi, 50 partecipanti). Ideale per chi vuole provare la piattaforma
- PRIVATE_STARTER: €4.99/mese o €49.90/anno — 10 eventi, 100 partecipanti
- PRIVATE_PRO: €9.99/mese o €99.90/anno — eventi illimitati, 300 partecipanti, AI analytics
- PRIVATE_PREMIUM: €19.99/mese o €199.90/anno — eventi illimitati, 1000 partecipanti, tutte le funzionalità
Piani Business (Aziende)
- BUSINESS_STARTER: €29/mese o €290/anno — eventi illimitati, 200 partecipanti, team 5 membri
- BUSINESS_PROFESSIONAL: €79/mese o €790/anno — eventi illimitati, 500 partecipanti, team 15 membri
- BUSINESS_ENTERPRISE: €199/mese o €1990/anno — eventi illimitati, partecipanti illimitati, supporto prioritario
Piano Associazioni (No-Profit)
- ASSOCIAZIONE_ILLIMITATO: €24/anno — piano unico con eventi e partecipanti illimitati, tutto incluso. Una scelta etica per supportare il terzo settore
public enum TipoPiano {
PRIVATE_FREE("Private Free", TipoAccount.INDIVIDUO,
BigDecimal.ZERO, BigDecimal.ZERO, 3, 50),
PRIVATE_STARTER("Private Starter", TipoAccount.INDIVIDUO,
new BigDecimal("4.99"), new BigDecimal("49.90"), 10, 100),
PRIVATE_PRO("Private Pro", TipoAccount.INDIVIDUO,
new BigDecimal("9.99"), new BigDecimal("99.90"), -1, 300),
BUSINESS_ENTERPRISE("Business Enterprise", TipoAccount.AZIENDA,
new BigDecimal("199.00"), new BigDecimal("1990.00"), -1, -1),
ASSOCIAZIONE_ILLIMITATO("Associazione Illimitato", TipoAccount.ASSOCIAZIONE,
new BigDecimal("24.00"), new BigDecimal("24.00"), -1, -1);
// -1 = illimitati
private final int maxEventi;
private final int maxPartecipanti;
public boolean isCompatibileCon(TipoAccount tipoAccount) {
return this.tipoAccountRichiesto == tipoAccount;
}
public BigDecimal calcolaRisparmioAnnuale() {
BigDecimal costoAnnualeMensile = prezzoMensile.multiply(new BigDecimal("12"));
BigDecimal risparmio = costoAnnualeMensile.subtract(prezzoAnnuale);
return risparmio.divide(costoAnnualeMensile, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
}
Risparmio con il Piano Annuale
Tutti i piani a pagamento offrono l'equivalente di 2 mesi gratuiti con la
fatturazione annuale. Il metodo calcolaRisparmioAnnuale() calcola dinamicamente
la percentuale di risparmio (~16.7%) confrontando il costo annuale con 12 mensilità.
Cicli di Fatturazione
Il sistema supporta due cicli di fatturazione, definiti dall'enum CicloPagamento:
MENSILE e ANNUALE. La scelta del ciclo influenza direttamente
il Price ID associato su Stripe e la data della prossima fatturazione.
public enum CicloPagamento {
MENSILE("Mensile"),
ANNUALE("Annuale");
private final String displayName;
}
// La chiave di lookup per il Price ID Stripe
// combina TipoPiano + CicloPagamento:
// Es: "PRIVATE_STARTER_MENSILE" -> "price_1Abc..."
// Es: "PRIVATE_STARTER_ANNUALE" -> "price_2Def..."
Macchina a Stati dell'Abbonamento
Ogni abbonamento attraversa una serie di stati ben definiti durante il suo ciclo di vita.
L'enum StatoAbbonamento modella questa macchina a stati con cinque valori possibili,
ciascuno con regole precise su quali transizioni sono ammesse e quali funzionalità sono
accessibili.
┌──────────────────────────────────────────────────┐
│ │
│ MACCHINA A STATI ABBONAMENTO │
│ │
└──────────────────────────────────────────────────┘
┌─────────────┐ pagamento riuscito ┌──────────────┐
│ PENDING │ ──────────────────────────▶ │ ACTIVE │
│ (In Attesa) │ │ (Attivo) │
└─────────────┘ └──────┬───────┘
│
┌─────────────────────────────────┼───────────────────┐
│ │ │
annulla() 3 pagamenti falliti segnaScaduto()
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ CANCELLED │ │ SUSPENDED │ │ EXPIRED │
│ (Annullato) │ │ (Sospeso) │ │ (Scaduto) │
└──────────────┘ └───────┬───────┘ └──────────────┘
│ │ │
│ riattiva() │ riattiva() │
└──────────────────────────────┼───────────────────┘
│
▼
┌──────────────┐
│ ACTIVE │
│ (Attivo) │
└──────────────┘
Dettaglio degli Stati
- PENDING (In Attesa): stato iniziale dopo la creazione con Stripe. L'abbonamento attende la conferma del primo pagamento
- ACTIVE (Attivo): l'utente ha accesso completo alle funzionalità del piano. Unico stato che consente l'accesso premium
- CANCELLED (Annullato): l'utente ha richiesto la cancellazione. L'accesso può rimanere attivo fino alla fine del periodo già pagato
- SUSPENDED (Sospeso): pagamenti falliti hanno causato la sospensione. L'accesso alle funzionalità premium è bloccato
- EXPIRED (Scaduto): l'abbonamento ha superato la data di scadenza. Richiede una nuova sottoscrizione
public enum StatoAbbonamento {
ACTIVE("Attivo"),
CANCELLED("Annullato"),
EXPIRED("Scaduto"),
SUSPENDED("Sospeso"),
PENDING("In Attesa");
// Solo ACTIVE consente funzionalità premium
public boolean consenteFunzionalitaPremium() {
return this == ACTIVE;
}
// PENDING, SUSPENDED, EXPIRED richiedono pagamento
public boolean richiedePagamento() {
return this == PENDING || this == SUSPENDED || this == EXPIRED;
}
// ACTIVE e CANCELLED sono potenzialmente attivi
public boolean ePotenzialmenteAttivo() {
return this == ACTIVE || this == CANCELLED;
}
// CANCELLED, SUSPENDED, EXPIRED possono essere riattivati
public boolean puoEssereRiattivato() {
return this == CANCELLED || this == SUSPENDED || this == EXPIRED;
}
}
Gestione dei Fallimenti di Pagamento
La gestione dei pagamenti falliti è uno degli aspetti più critici di un sistema di abbonamenti. Un utente potrebbe avere la carta scaduta, fondi insufficienti o un problema temporaneo con la banca. Il sistema implementa una strategia di 3 retry automatici prima di sospendere l'abbonamento.
public class Abbonamento {
private int tentativiPagamentoFalliti = 0;
private String ultimoErrorePagamento;
/**
* Registra un pagamento fallito.
* Dopo 3 tentativi falliti, l'abbonamento viene sospeso.
*/
public void registraPagamentoFallito(String errore) {
this.ultimoErrorePagamento = errore;
this.tentativiPagamentoFalliti++;
if (this.tentativiPagamentoFalliti >= 3) {
this.stato = StatoAbbonamento.SUSPENDED;
}
}
/**
* Registra un pagamento riuscito.
* Resetta il contatore e riattiva se sospeso.
*/
public void registraPagamentoRiuscito() {
this.ultimoPagamentoIl = Instant.now();
this.ultimoErrorePagamento = null;
this.tentativiPagamentoFalliti = 0;
if (this.stato == StatoAbbonamento.SUSPENDED
|| this.stato == StatoAbbonamento.PENDING) {
this.stato = StatoAbbonamento.ACTIVE;
}
}
}
Strategia di Dunning (Recupero Crediti)
La strategia di dunning è configurata sia lato Stripe che lato applicativo. Stripe ritenta automaticamente il pagamento secondo la sua policy Smart Retries, mentre il backend traccia ogni tentativo fallito ricevuto via webhook. Al terzo fallimento consecutivo, la sospensione è automatica e l'utente viene notificato via email. Un pagamento riuscito in qualsiasi momento resetta completamente il contatore e riattiva l'abbonamento.
Factory Methods: Creare Abbonamenti nel Modo Giusto
La classe Abbonamento implementa il pattern Factory Method per
garantire che ogni tipo di abbonamento venga creato con lo stato iniziale corretto. Questo
approccio elimina la possibilità di creare abbonamenti in stati incoerenti e centralizza
la logica di inizializzazione.
creaConStripe() — Abbonamento Standard
Il metodo principale per creare un abbonamento a pagamento tramite Stripe. L'abbonamento parte
in stato PENDING e diventa ACTIVE solo dopo la conferma del pagamento
ricevuta via webhook.
public static Abbonamento creaConStripe(
User utente,
String idClienteStripe,
String idAbbonamentoStripe,
String idPrezzoStripe,
TipoPiano tipoPiano,
CicloPagamento cicloFatturazione,
BigDecimal importo) {
return Abbonamento.builder()
.utente(utente)
.idClienteStripe(idClienteStripe)
.idAbbonamentoStripe(idAbbonamentoStripe)
.idPrezzoStripe(idPrezzoStripe)
.tipoPiano(tipoPiano)
.cicloFatturazione(cicloFatturazione)
.importo(importo)
.valuta("EUR")
.stato(StatoAbbonamento.PENDING) // Attesa conferma pagamento
.dataInizio(Instant.now())
.tentativiPagamentoFalliti(0)
.build();
}
creaGratuito() — Piano Free
Per il piano PRIVATE_FREE, non è necessaria alcuna interazione con Stripe.
L'abbonamento viene creato direttamente in stato ACTIVE con importo zero.
public static Abbonamento creaGratuito(User utente) {
return Abbonamento.builder()
.utente(utente)
.tipoPiano(TipoPiano.PRIVATE_FREE)
.cicloFatturazione(CicloPagamento.MENSILE)
.importo(BigDecimal.ZERO)
.valuta("EUR")
.stato(StatoAbbonamento.ACTIVE) // Attivo immediatamente
.dataInizio(Instant.now())
.tentativiPagamentoFalliti(0)
.build();
}
creaBypass() — Codice Promo con Scadenza
Quando un utente applica un codice promozionale di tipo BYPASS_PAGAMENTO, ottiene
l'accesso a un piano premium senza pagare, ma con una scadenza precisa calcolata in base alla
durata configurata nel codice.
public static Abbonamento creaBypass(
User utente,
TipoPiano tipoPiano,
CodicePromozionale codicePromozionale,
int durataMesi) {
return Abbonamento.builder()
.utente(utente)
.tipoPiano(tipoPiano)
.cicloFatturazione(CicloPagamento.MENSILE)
.importo(BigDecimal.ZERO)
.valuta("EUR")
.stato(StatoAbbonamento.ACTIVE)
.dataInizio(Instant.now())
.scadenzaBypass(Instant.now().plus(
durataMesi * 30L, ChronoUnit.DAYS))
.codicePromozionale(codicePromozionale)
.tentativiPagamentoFalliti(0)
.build();
}
creaBypassPermanente() — Accesso a Vita
Per collaborazioni speciali, partner e dipendenti, esiste la possibilità di creare
abbonamenti permanenti che non scadono mai. La scadenzaBypass impostata a
null indica esplicitamente che l'abbonamento non ha scadenza.
public static Abbonamento creaBypassPermanente(
User utente,
TipoPiano tipoPiano,
CodicePromozionale codicePromozionale) {
return Abbonamento.builder()
.utente(utente)
.tipoPiano(tipoPiano)
.cicloFatturazione(CicloPagamento.MENSILE)
.importo(BigDecimal.ZERO)
.valuta("EUR")
.stato(StatoAbbonamento.ACTIVE)
.dataInizio(Instant.now())
.scadenzaBypass(null) // Null = permanente, mai scade
.codicePromozionale(codicePromozionale)
.tentativiPagamentoFalliti(0)
.build();
}
// Metodi di verifica
public boolean isAbbonamentoBypass() {
return scadenzaBypass != null;
}
public boolean isPermanente() {
return codicePromozionale != null
&& scadenzaBypass == null
&& importo.compareTo(BigDecimal.ZERO) == 0;
}
Sistema di Codici Promozionali
Il sistema di codici promozionali di Play The Event è progettato per supportare diversi scenari commerciali: dalle campagne marketing con sconti percentuali o fissi, fino al bypass completo del pagamento per partner strategici. Ogni codice promozionale è un'entity con regole di validazione, limiti di utilizzo e tracciamento completo.
Tipi di Codice Promozionale
- SCONTO_PERCENTUALE: applica uno sconto in percentuale (es: 30%) tramite Stripe Promotion Codes. Richiede un
idPromotionCodeStripeconfigurato - SCONTO_FISSO: applica uno sconto fisso in EUR (es: €10) tramite Stripe Promotion Codes. Anche questo richiede configurazione su Stripe
- BYPASS_PAGAMENTO: bypass completo del pagamento con durata limitata. Attiva un piano premium senza richiedere pagamento Stripe, con scadenza configurabile
- BYPASS_PERMANENTE: bypass a vita per collaborazioni speciali, partner e dipendenti. L'abbonamento non ha scadenza
public enum TipoCodicePromozionale {
BYPASS_PAGAMENTO,
BYPASS_PERMANENTE,
SCONTO_PERCENTUALE,
SCONTO_FISSO;
// Solo SCONTO_* richiedono configurazione Stripe
public boolean richiedeStripe() {
return this == SCONTO_PERCENTUALE || this == SCONTO_FISSO;
}
// BYPASS_* saltano completamente il pagamento
public boolean bypassaPagamento() {
return this == BYPASS_PAGAMENTO || this == BYPASS_PERMANENTE;
}
public boolean isPermanente() {
return this == BYPASS_PERMANENTE;
}
}
Validazione del Codice
Prima di essere applicato, ogni codice promozionale viene sottoposto a una serie di verifiche: deve essere attivo, nel periodo di validità, con utilizzi disponibili e compatibile con il piano selezionato dall'utente.
@Entity
@Table(name = "codici_promozionali")
public class CodicePromozionale {
private String codice; // "WELCOME50", "PARTNER2024"
private TipoCodicePromozionale tipo;
private BigDecimal valore; // Sconto % o fisso (null per bypass)
private TipoPiano tipoPianoApplicabile; // Null = tutti i piani
private Integer durataMesiBypass; // Solo per BYPASS_PAGAMENTO
private Integer maxUtilizzi; // Null = illimitati
private int utilizziCorrenti;
private boolean attivo;
public boolean isValido() {
if (!attivo) return false;
Instant now = Instant.now();
if (validoDa != null && now.isBefore(validoDa)) return false;
if (validoFino != null && now.isAfter(validoFino)) return false;
if (maxUtilizzi != null && utilizziCorrenti >= maxUtilizzi)
return false;
return true;
}
public boolean isApplicabileAPiano(TipoPiano piano) {
if (tipoPianoApplicabile == null) return true;
return tipoPianoApplicabile == piano;
}
}
Tracciamento degli Utilizzi
Ogni utilizzo di un codice promozionale viene registrato nell'entity
UtilizzoCodicePromozionale, che traccia chi ha usato il codice, quando, in quale
contesto (registrazione o checkout) e quale abbonamento è stato creato o modificato.
@Entity
@Table(name = "utilizzi_codici_promozionali")
public class UtilizzoCodicePromozionale {
private CodicePromozionale codicePromozionale;
private User utente;
private Abbonamento abbonamento;
private Instant utilizzatoIl;
private String contesto; // "REGISTRAZIONE" o "CHECKOUT"
// Factory methods per i due contesti
public static UtilizzoCodicePromozionale perRegistrazione(
CodicePromozionale codice, User utente, Abbonamento abb) {
return UtilizzoCodicePromozionale.builder()
.codicePromozionale(codice)
.utente(utente)
.abbonamento(abb)
.utilizzatoIl(Instant.now())
.contesto("REGISTRAZIONE")
.build();
}
public static UtilizzoCodicePromozionale perchèckout(
CodicePromozionale codice, User utente, Abbonamento abb) {
// ... stesso pattern con contesto "CHECKOUT"
}
}
Webhook Stripe: Aggiornamenti in Tempo Reale
Il StripeWebhookController è l'endpoint che riceve gli eventi push da Stripe
ogni volta che lo stato di un abbonamento o di un pagamento cambia. Questo approccio
event-driven garantisce che il database locale sia sempre sincronizzato con
lo stato reale dei pagamenti su Stripe, senza bisogno di polling periodici.
Eventi Gestiti
customer.subscription.created— Nuovo abbonamento creato: sincronizza l'abbonamento nel database localecustomer.subscription.updated— Abbonamento aggiornato: riflette cambi di piano, stato o prossima fatturazionecustomer.subscription.deleted— Abbonamento cancellato: segna l'abbonamento come CANCELLEDinvoice.paid— Fattura pagata: registra il pagamento riuscito, riattiva se sospesoinvoice.payment_failed— Pagamento fallito: incrementa il contatore di fallimenti, sospende dopo 3 tentativi
@RestController
@RequestMapping("/api/v1/webhooks/stripe")
public class StripeWebhookController {
@PostMapping
public ResponseEntity<String> handleWebhook(
@RequestBody String payload,
@RequestHeader("Stripe-Signature") String stripeSignature) {
// 1. Verifica firma con webhook secret
Event event = Webhook.constructEvent(
payload, stripeSignature, webhookSecret);
// 2. Dispatch in base al tipo di evento
switch (event.getType()) {
case "customer.subscription.created"
-> handleSubscriptionCreated(event);
case "customer.subscription.updated"
-> handleSubscriptionUpdated(event);
case "customer.subscription.deleted"
-> handleSubscriptionDeleted(event);
case "invoice.paid"
-> handleInvoicePaid(event);
case "invoice.payment_failed"
-> handleInvoicePaymentFailed(event);
}
// 3. Ritorna sempre 200 per evitare retry di Stripe
return ResponseEntity.ok("Evento ricevuto");
}
}
Sicurezza dei Webhook
Ogni webhook ricevuto viene verificato tramite la firma Stripe-Signature con il
webhookSecret configurato. Questa verifica garantisce che l'evento proviene
effettivamente da Stripe e non da un attaccante che tenta di manipolare lo stato degli
abbonamenti. In modalità sviluppo, la verifica della firma può essere
disabilitata per facilitare il testing con Stripe CLI.
Checkout e Customer Portal
Il flusso di checkout utilizza Stripe Checkout, una pagina di pagamento ospitata da Stripe che gestisce automaticamente la raccolta dei dati della carta, la conformità PCI-DSS, il 3D Secure e le conversioni valutarie. Il backend crea una sessione di checkout e restituisce l'URL al frontend.
public Session creaCheckoutSession(
String customerId,
String priceId,
String successUrl,
String cancelUrl,
String promotionCodeId) throws StripeException {
var paramsBuilder = SessionCreateParams.builder()
.setCustomer(customerId)
.setMode(SessionCreateParams.Mode.SUBSCRIPTION)
.addLineItem(SessionCreateParams.LineItem.builder()
.setPrice(priceId)
.setQuantity(1L)
.build())
.setSuccessUrl(successUrl + "?session_id={CHECKOUT_SESSION_ID}")
.setCancelUrl(cancelUrl);
if (promotionCodeId != null) {
// Codice promo predefinito
paramsBuilder.addDiscount(
SessionCreateParams.Discount.builder()
.setPromotionCode(promotionCodeId)
.build());
} else {
// Permetti inserimento manuale
paramsBuilder.setAllowPromotionCodes(true);
}
return Session.create(paramsBuilder.build());
}
Per la gestione post-acquisto, Stripe Customer Portal permette all'utente di aggiornare il metodo di pagamento, visualizzare le fatture, cambiare piano o annullare l'abbonamento, il tutto senza che il backend debba implementare queste funzionalità.
Acquisti Evento
Oltre al sistema di abbonamenti, la piattaforma supporta acquisti singoli
associati a un evento specifico tramite l'entity AcquistoEvento. Questa
funzionalità permette agli organizzatori di tracciare gli articoli necessari per un
evento, gestendone lo stato di completamento.
@Entity
@Table(name = "acquisto_evento")
public class AcquistoEvento {
private Evento evento;
private String nome;
private Integer quantità;
private String note;
private Boolean completato;
private User completatoDa;
private Instant completatoIl;
private User creatoDa;
public static AcquistoEvento crea(
Evento evento, String nome, Integer quantità,
String note, User creatoDa) {
// Validazioni e creazione
}
public void completaAcquisto(User utente) {
this.completato = true;
this.completatoDa = utente;
this.completatoIl = Instant.now();
}
public void toggleCompletamento(User utente) {
if (this.completato) annullaCompletamento();
else completaAcquisto(utente);
}
}
Finanziamento e Crowdfunding Eventi
Play The Event
include un sistema di finanziamento eventi che permette a donatori e sponsor
di contribuire economicamente alla realizzazione di un evento. L'entity Finanziamento
modella diversi tipi di contributi finanziari, ciascuno con un flusso di conferma e
tracciamento completo.
Tipi di Finanziamento
- DONAZIONE: contributo volontario senza aspettative di ritorno
- SPONSORIZZAZIONE: contributo aziendale con visibilità del brand durante l'evento
- FINANZIAMENTO: investimento strutturato con possibili condizioni di rimborso
- CONTRIBUTO: partecipazione economica generica (es: quote di partecipazione suddivise)
@Entity
@Table(name = "finanziamenti")
public class Finanziamento {
private Long eventoId;
private Long donatoreId;
private String donatoreNome;
private String donatoreEmail;
private String titolo;
private TipoFinanziamento tipo;
@Embedded
private Money importo; // Value Object immutabile
private LocalDate dataFinanziamento;
private Boolean confermato;
public static Finanziamento crea(
Long eventoId, String titolo, Money importo,
TipoFinanziamento tipo, String donatoreNome,
Long registratoDaId, LocalDate dataFinanziamento) {
// Validazioni rigorose: importo positivo,
// titolo e donatore obbligatori
}
public void conferma() {
this.confermato = true;
this.confermatoIl = Instant.now();
this.dataRicezione = LocalDate.now();
}
}
// Il Value Object Money garantisce operazioni
// monetarie sicure con valuta esplicita
Money donazione = Money.euro(150.00);
Money sponsor = Money.of(new BigDecimal("500.00"), "EUR");
Money totale = donazione.add(sponsor); // EUR 650.00
Riepilogo dell'Architettura Pagamenti
- Integrazione Stripe completa: Customer, Subscription, Checkout, Customer Portal, Webhook
- 11 piani tariffari organizzati per Private, Business e Associazione
- Macchina a stati con 5 stati e transizioni controllate
- 3 retry automatici prima della sospensione per pagamenti falliti
- 4 Factory Methods per creare abbonamenti in modo coerente
- 4 tipi di codici promozionali con tracciamento utilizzo
- Webhook event-driven per sincronizzazione in tempo reale
- Sistema di finanziamento con 4 tipi di contributo e conferma manuale
- Value Object Money per operazioni monetarie type-safe
Il codice sorgente relativo ai pagamenti è disponibile su GitHub. Nel prossimo articolo approfondiremo un altro aspetto fondamentale della piattaforma, continuando il nostro viaggio nell'architettura di Play The Event.







