Salta al contenuto principale

13 - Pagamenti e Abbonamenti con Stripe

Pagamenti e Abbonamenti con Stripe In una piattaforma SaaS come Play The Event , la gestione dei pagamenti non è semplicemente un modulo accessorio: è il...

Cosa ne pensi di questo articolo?

13 - Pagamenti e Abbonamenti con Stripe
Play the EventFoto:Brett SaylesPexels

In Breve

Pagamenti e Abbonamenti con Stripe In una piattaforma SaaS come Play The Event , la gestione dei pagamenti non è semplicemente un modulo accessorio: è il...

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 Customer su Stripe, identificato da idClienteStripe. 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 Subscription su Stripe, identificata da idAbbonamentoStripe. La subscription gestisce il ciclo di fatturazione automatica
  • Price ID: ogni piano tariffario è collegato a un Price su Stripe tramite idPrezzoStripe. Il Price ID definisce l'importo, la valuta e l'intervallo di fatturazione
Mappatura Stripe nell'Entity Abbonamento
@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.

Mappatura Bidirezionale dei Price ID
@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
Definizione Enum TipoPiano (estratto)
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.

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

Diagramma della Macchina a Stati
                    ┌──────────────────────────────────────────────────┐
                    │                                                  │
                    │         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
Metodi di Interrogazione dello Stato
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.

Logica di Retry e Sospensione
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.

Factory Method: creaConStripe()
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.

Factory Method: creaGratuito()
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.

Factory Method: creaBypass()
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.

Factory Method: creaBypassPermanente()
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 idPromotionCodeStripe configurato
  • 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
Enum TipoCodicePromozionale
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.

Logica di Validazione del CodicePromozionale
@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 UtilizzoCodicePromozionale
@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 locale
  • customer.subscription.updated - Abbonamento aggiornato: riflette cambi di piano, stato o prossima fatturazione
  • customer.subscription.deleted - Abbonamento cancellato: segna l'abbonamento come CANCELLED
  • invoice.paid - Fattura pagata: registra il pagamento riuscito, riattiva se sospeso
  • invoice.payment_failed - Pagamento fallito: incrementa il contatore di fallimenti, sospende dopo 3 tentativi
StripeWebhookController
@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.

Creazione Sessione di Checkout con Codice Promo
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 AcquistoEvento
@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 Finanziamento e Value Object Money
@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.

Domande frequenti

Come funziona la gestione dei pagamenti in una piattaforma SaaS come Play The Event?

La gestione dei pagamenti è un modulo fondamentale della piattaforma, che si integra con Stripe per offrire funzionalità di pagamento sicure e efficienti.

Quali sono i tre pilastri dell'integrazione con Stripe in Play The Event?

I tre pilastri sono il Servizio StripeService, che gestisce le interazioni con le API Stripe; i Piani di Abbonamento, che definiscono le opzioni tariffarie disponibili per gli utenti; e la Macchina a Stati dell'Abbonamento, che modella lo stato degli abbonamenti durante il loro ciclo di vita.

Come funziona la gestione dei fallimenti di pagamento in Play The Event?

La piattaforma implementa una strategia di 3 retry automatici prima di sospendere l'abbonamento, e configura sia lato Stripe che lato applicativo per garantire che il pagamento venga effettuato correttamente.

Come funziona la creazione degli abbonamenti in Play The Event?

La classe implementa il pattern Factory Method per garantire che ogni tipo di abbonamento venga creato con lo stato iniziale corretto, utilizzando metodi come creaConStripe(), creaGratuito() e creaBypass().

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