Skip to main content

13 - Payments and Subscriptions with Stripe

Payments and Subscriptions with Stripe In a SaaS platform like Play The Event, payment management is not just an accessory module: it's...

What do you think about this article?

At a Glance

Payments and Subscriptions with Stripe In a SaaS platform like Play The Event, payment management is not just an accessory module: it's...

Payments and Subscriptions with Stripe

Monetization is a critical pillar of any SaaS platform. In Play The Event, the entire billing system is built on Stripe, the industry-standard payment processor, and is deeply integrated into the domain model following Domain-Driven Design principles. The result is a subscription engine that handles free tiers, premium upgrades, billing cycles, payment failures, promotional codes, and one-time purchases, all through a clean, testable architecture.

This article explores the complete payment and subscription layer: from the Abbonamento aggregate and its state machine, to the Stripe checkout flow, webhook handling, promotional code system, and crowdfunding support for events.

What You'll Find in This Article

  • Stripe integration architecture: Customer, Subscription, and Price ID mapping
  • Subscription plans across three account types (Private, Business, Association)
  • Billing cycles: monthly and annual with automatic savings calculation
  • Subscription state machine: PENDING, ACTIVE, CANCELLED, SUSPENDED, EXPIRED
  • Payment failure handling with 3 automatic retries before suspension
  • Factory methods for different subscription creation scenarios
  • Promotional code system with payment bypass and percentage/fixed discounts
  • Stripe webhooks for real-time subscription lifecycle updates
  • Event crowdfunding with donations, sponsorships, and contributions

Stripe Integration Architecture

The integration with Stripe follows a layered approach. The domain model (Abbonamento, TipoPiano, StatoAbbonamento) is completely independent of Stripe's SDK. An infrastructure service (StripeService) encapsulates all direct interactions with the Stripe API, while the application service (SubscriptionService) orchestrates the business logic between the two layers.

Integration Architecture
STRIPE INTEGRATION LAYERS

DOMAIN LAYER (Pure Business Logic)
├── Abbonamento          → Subscription aggregate root
├── TipoPiano            → Plan types with pricing
├── StatoAbbonamento     → State machine (5 states)
├── CicloPagamento       → Billing cycle (MENSILE/ANNUALE)
├── CodicePromozionale   → Promo code entity
└── UtilizzoCodicePromozionale → Usage tracking

INFRASTRUCTURE LAYER (Stripe SDK)
└── StripeService
    ├── creaCustomer()           → Creates Stripe Customer
    ├── creaCheckoutSession()    → Creates Checkout Session
    ├── annullaAbbonamento()     → Cancels subscription
    ├── creaPortalSession()      → Customer Portal access
    ├── getPriceId()             → Plan → Price ID mapping
    └── getPlanKeyFromPriceId()  → Price ID → Plan reverse mapping

APPLICATION LAYER (Orchestration)
└── SubscriptionService
    ├── creaCheckoutSession()           → Checkout flow
    ├── creaAbbonamentoDaStripe()       → From webhook
    ├── creaAbbonamentoGratuito()       → Free tier
    ├── gestisciPagamentoRiuscito()     → Webhook handler
    ├── gestisciPagamentoFallito()      → Webhook handler
    ├── gestisciAggiornaAbbonamento()   → Webhook handler
    └── puoAccedereFunzionalitaPremium()→ Access check

Stripe Entity Mapping

Each user in Play The Event is mapped to a Stripe Customer. The Abbonamento entity stores three Stripe identifiers that link the internal subscription to Stripe's billing system.

Stripe ID Fields in Abbonamento
// Stripe IDs stored in the Abbonamento entity
@Column(name = "id_cliente_stripe")
private String idClienteStripe;        // Stripe Customer ID (cus_...)

@Column(name = "id_abbonamento_stripe")
private String idAbbonamentoStripe;    // Stripe Subscription ID (sub_...)

@Column(name = "id_prezzo_stripe")
private String idPrezzoStripe;         // Stripe Price ID (price_...)

The StripeService maintains a bidirectional mapping between internal plan types and Stripe Price IDs. When a user selects a plan, the service resolves the correct Price ID. When a webhook arrives, the reverse mapping determines which internal plan corresponds to the Stripe price.

Bidirectional Price ID Mapping
PRICE ID MAPPING (Configured at startup)

Forward: Plan + Cycle → Stripe Price ID
  "PRIVATE_STARTER_MENSILE"  → "price_1Abc..."
  "PRIVATE_STARTER_ANNUALE"  → "price_1Def..."
  "PRIVATE_PRO_MENSILE"      → "price_1Ghi..."
  "BUSINESS_STARTER_ANNUALE" → "price_1Jkl..."

Reverse: Stripe Price ID → Plan + Cycle
  "price_1Abc..." → TipoPiano.PRIVATE_STARTER + MENSILE
  "price_1Def..." → TipoPiano.PRIVATE_STARTER + ANNUALE
  "price_1Ghi..." → TipoPiano.PRIVATE_PRO + MENSILE
  "price_1Jkl..." → TipoPiano.BUSINESS_STARTER + ANNUALE

Subscription Plans (TipoPiano)

The TipoPiano enum is one of the richest Value Objects in the domain. Each plan variant encodes its display name, the required account type, monthly and annual pricing, and resource limits (maximum events and participants). Plans are organized into three categories based on account type.

Private Plans (Individual Users)

Plan Monthly Annual Max Events Max Participants
PRIVATE_FREE Free Free 3 50
PRIVATE_STARTER 4.99 EUR 49.90 EUR 10 100
PRIVATE_PRO 9.99 EUR 99.90 EUR Unlimited 300
PRIVATE_PREMIUM 19.99 EUR 199.90 EUR Unlimited 1,000

Business Plans (Companies)

Plan Monthly Annual Max Events Max Participants
BUSINESS_STARTER 29.00 EUR 290.00 EUR Unlimited 200
BUSINESS_PROFESSIONAL 79.00 EUR 790.00 EUR Unlimited 500
BUSINESS_ENTERPRISE 199.00 EUR 1,990.00 EUR Unlimited Unlimited

Association Plans (Non-Profit)

Plan Price Max Events Max Participants
ASSOCIAZIONE_ILLIMITATO 24.00 EUR/year Unlimited Unlimited

Non-profit associations receive a single, deeply discounted plan with unlimited access. This reflects the platform's commitment to supporting community organizations.

Each plan variant is type-safe and carries built-in validation. The enum enforces compatibility between account types and plans: an individual user cannot subscribe to a business plan, and a company cannot select a private plan. The isCompatibileCon() method performs this check before checkout is initiated.

Billing Cycles (CicloPagamento)

Play The Event supports two billing cycles: MENSILE (monthly) and ANNUALE (annual). Annual plans offer approximately two months free, which the TipoPiano enum can calculate dynamically through its calcolaRisparmioAnnuale() method.

Annual Savings Calculation
// Dynamic annual savings calculation in TipoPiano
public BigDecimal calcolaRisparmioAnnuale() {
    if (isGratuito()) {
        return BigDecimal.ZERO;
    }
    BigDecimal costoAnnualeMensile =
        prezzoMensile.multiply(new BigDecimal("12"));
    BigDecimal risparmio =
        costoAnnualeMensile.subtract(prezzoAnnuale);
    return risparmio
        .divide(costoAnnualeMensile, 4, RoundingMode.HALF_UP)
        .multiply(new BigDecimal("100"));
}

EXAMPLE: PRIVATE_STARTER
  Monthly cost x12 = 4.99 x 12  = 59.88 EUR
  Annual cost       =              49.90 EUR
  Savings           =               9.98 EUR (~16.7%)

The billing cycle, together with the plan type, forms the lookup key for the Stripe Price ID. The key format is PLAN_NAME_CYCLE (e.g., PRIVATE_PRO_ANNUALE), ensuring a one-to-one mapping between every plan+cycle combination and its corresponding Stripe product.

Subscription State Machine

The StatoAbbonamento enum defines five states that govern the complete lifecycle of a subscription in Play The Event. Each state carries behavioral methods that the domain uses for authorization decisions.

Subscription State Machine
SUBSCRIPTION STATE MACHINE

               checkout.session.completed
    [START] ──────────────────────────────────► PENDING
                                                   │
                                   first payment   │
                                   succeeded       │
                                                   ▼
                                               ACTIVE ◄─────────────┐
                                              /  |   \              │
                                             /   |    \    payment  │
                          user cancels      /    |     \  succeeds  │
                                           /     |      \           │
                                          ▼      │       ▼          │
                                    CANCELLED    │    SUSPENDED ────┘
                                                 │    (3 failed
                                                 │     payments)
                                                 │
                                   period ends   │
                                                 ▼
                                              EXPIRED

STATE BEHAVIORS:
┌──────────────┬────────────┬──────────────┬──────────────┐
│ State        │ Premium    │ Needs        │ Can Be       │
│              │ Access     │ Payment      │ Reactivated  │
├──────────────┼────────────┼──────────────┼──────────────┤
│ PENDING      │ No         │ Yes          │ No           │
│ ACTIVE       │ Yes        │ No           │ No           │
│ CANCELLED    │ No*        │ No           │ Yes          │
│ SUSPENDED    │ No         │ Yes          │ Yes          │
│ EXPIRED      │ No         │ Yes          │ Yes          │
└──────────────┴────────────┴──────────────┴──────────────┘
* CANCELLED is "potentially active" until the billing period ends

The state machine is enforced at the domain level. The StatoAbbonamento enum provides three key query methods: consenteFunzionalitaPremium() (only ACTIVE returns true), richiedePagamento() (PENDING, SUSPENDED, EXPIRED), and puoEssereRiattivato() (CANCELLED, SUSPENDED, EXPIRED). These methods drive feature gating across the entire platform without requiring the calling code to know about specific state values.

Payment Failure Handling

One of the most critical aspects of subscription management is graceful degradation when payments fail. Play The Event implements a three-strike policy: each failed payment increments a counter, and after three consecutive failures the subscription is automatically suspended.

Payment Failure Logic
// In Abbonamento.java - Domain logic for payment failures
public void registraPagamentoFallito(String errore) {
    this.ultimoErrorePagamento = errore;
    this.tentativiPagamentoFalliti++;

    if (this.tentativiPagamentoFalliti >= 3) {
        this.stato = StatoAbbonamento.SUSPENDED;
    }
}

// Successful payment resets everything
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;
    }
}
Payment Failure Timeline
PAYMENT FAILURE SCENARIO

Day 0:  Invoice generated by Stripe
        ├── Payment attempt #1: FAILED (card declined)
        ├── tentativiPagamentoFalliti = 1
        └── Status: ACTIVE (still active)

Day 3:  Stripe retries automatically
        ├── Payment attempt #2: FAILED (insufficient funds)
        ├── tentativiPagamentoFalliti = 2
        └── Status: ACTIVE (still active)

Day 7:  Stripe retries again
        ├── Payment attempt #3: FAILED (card expired)
        ├── tentativiPagamentoFalliti = 3
        ├── Status: SUSPENDED ← Automatic suspension
        └── User loses premium access

Day 14: User updates payment method
        ├── Payment attempt #4: SUCCESS
        ├── tentativiPagamentoFalliti = 0 (reset)
        ├── ultimoErrorePagamento = null (cleared)
        └── Status: ACTIVE ← Automatic reactivation

Why 3 Retries?

Three attempts balance user experience with business reality. Temporary issues (bank maintenance, daily limits) often resolve within a few days. Suspending too early loses customers; waiting too long provides free service. The three-retry window, combined with Stripe's automatic retry schedule, gives users approximately one week to resolve payment issues before losing access.

Factory Methods: Four Ways to Create a Subscription

The Abbonamento entity provides four static factory methods, each encoding a distinct business scenario. This pattern replaces complex constructors with intention-revealing creation logic.

Factory Method Overview
FACTORY METHODS IN ABBONAMENTO

1. creaConStripe(user, customerId, subscriptionId,
                 priceId, tipoPiano, ciclo, importo)
   ├── Standard paid subscription
   ├── Links to Stripe Customer + Subscription
   ├── Initial state: PENDING (awaiting first payment)
   └── Used when: Stripe webhook confirms subscription created

2. creaGratuito(user)
   ├── Free tier subscription (PRIVATE_FREE)
   ├── No Stripe IDs needed
   ├── importo = 0, ciclo = MENSILE
   ├── Initial state: ACTIVE (immediate access)
   └── Used when: New user registration

3. creaBypass(user, tipoPiano, codicePromozionale, durataMesi)
   ├── Premium access without payment (time-limited)
   ├── No Stripe IDs needed
   ├── importo = 0, scadenzaBypass = now + (durataMesi * 30 days)
   ├── Initial state: ACTIVE (immediate access)
   └── Used when: Promo code of type BYPASS_PAGAMENTO at registration

4. creaBypassPermanente(user, tipoPiano, codicePromozionale)
   ├── Lifetime premium access without payment
   ├── No Stripe IDs needed
   ├── importo = 0, scadenzaBypass = null (never expires)
   ├── Initial state: ACTIVE (immediate access)
   └── Used when: Promo code of type BYPASS_PERMANENTE
       (partners, employees, special collaborations)

The distinction between creaBypass and creaBypassPermanente is subtle but important. A bypass subscription has a scadenzaBypass timestamp set to a future date; when that date passes, the subscription is considered expired. A permanent bypass sets scadenzaBypass to null, which the domain interprets as "never expires." The isPermanente() method checks all three conditions: a promo code is present, no bypass expiration is set, and the amount is zero.

Promotional Code System

The promotional code system in Play The Event supports four distinct types, covering both Stripe-integrated discounts and payment-bypassing scenarios. The CodicePromozionale entity is a full-featured model with validation periods, usage limits, and plan-specific applicability.

Promotional Code Types
PROMOTIONAL CODE TYPES (TipoCodicePromozionale)

1. BYPASS_PAGAMENTO
   ├── Bypasses payment entirely (time-limited)
   ├── Requires: durataMesiBypass (e.g., 3 months)
   ├── Does NOT require Stripe integration
   ├── Creates subscription via: creaBypass()
   └── Used at: REGISTRATION only

2. BYPASS_PERMANENTE
   ├── Bypasses payment forever (lifetime)
   ├── No expiration date
   ├── Does NOT require Stripe integration
   ├── Creates subscription via: creaBypassPermanente()
   └── Used for: Partners, employees, special collaborations

3. SCONTO_PERCENTUALE
   ├── Percentage discount (e.g., 20% off)
   ├── Requires: idPromotionCodeStripe
   ├── Applied through Stripe Checkout Session
   └── Used at: CHECKOUT

4. SCONTO_FISSO
   ├── Fixed amount discount (e.g., 10 EUR off)
   ├── Requires: idPromotionCodeStripe
   ├── Applied through Stripe Checkout Session
   └── Used at: CHECKOUT

Promo Code Validation

Before a promotional code can be applied, it passes through a multi-layered validation process. The CodicePromozionale entity performs self-validation through its isValido() method, checking active status, date range, and usage limits.

Promo Code Validation Chain
PROMO CODE VALIDATION

Input: codice = "WELCOME50", piano = PRIVATE_PRO

Step 1: isValido()
  ├── Check: attivo == true          ✓
  ├── Check: now >= validoDa         ✓ (or validoDa is null)
  ├── Check: now <= validoFino       ✓ (or validoFino is null)
  └── Check: utilizziCorrenti < maxUtilizzi  ✓ (or maxUtilizzi is null)

Step 2: isApplicabileAPiano(PRIVATE_PRO)
  ├── If tipoPianoApplicabile is null → applicable to ALL plans ✓
  └── If tipoPianoApplicabile == PRIVATE_PRO → match ✓

Step 3: Context-specific checks (in SubscriptionService)
  ├── BYPASS codes → Only allowed at REGISTRATION
  │   └── Rejected if used at CHECKOUT
  ├── SCONTO codes → Require Stripe Promotion Code ID
  │   └── Rejected if idPromotionCodeStripe is null
  └── All valid → Apply to Stripe Checkout Session

Step 4: Track usage
  ├── codicePromozionale.incrementaUtilizzi()
  └── Create UtilizzoCodicePromozionale record
      ├── contesto: "REGISTRAZIONE" or "CHECKOUT"
      ├── utente: the user
      └── abbonamento: the resulting subscription

Usage Tracking

Every promotional code usage is tracked through the UtilizzoCodicePromozionale entity, which records the user, the subscription created, the timestamp, and the context (REGISTRATION or CHECKOUT). This provides a complete audit trail for marketing analysis and fraud detection.

Stripe Checkout Flow

When a user decides to upgrade to a premium plan, the system orchestrates a multi-step checkout flow that bridges the internal domain with Stripe's hosted checkout page.

Checkout Session Creation Flow
CHECKOUT FLOW

1. USER SELECTS PLAN
   Frontend: POST /api/subscriptions/checkout
   Body: {
     tipoPiano: "PRIVATE_PRO",
     cicloFatturazione: "ANNUALE",
     codicePromozionale: "WELCOME50",  // optional
     successUrl: "https://playtheevent.com/success",
     cancelUrl: "https://playtheevent.com/pricing"
   }

2. VALIDATE REQUEST (SubscriptionService)
   ├── Load User from database
   ├── Parse TipoPiano from string
   ├── Verify plan compatibility with account type
   │   └── Individual user + Business plan → REJECTED
   ├── Resolve Stripe Price ID for plan + cycle
   │   └── PRIVATE_PRO_ANNUALE → "price_1Ghi..."
   └── Validate promo code if present
       ├── BYPASS type → REJECTED (registration only)
       └── SCONTO type → Extract Stripe Promotion Code ID

3. GET OR CREATE STRIPE CUSTOMER
   ├── Search existing subscriptions for customer ID
   ├── If found → reuse existing customer
   └── If not found → stripeService.creaCustomer(user)
       └── Metadata: { user_id: "123" }

4. CREATE STRIPE CHECKOUT SESSION
   ├── Mode: SUBSCRIPTION
   ├── Line item: price_id + quantity=1
   ├── Promotion code: applied if present
   │   └── Or allowPromotionCodes=true (manual entry)
   ├── Success URL with session_id parameter
   └── Cancel URL

5. RETURN CHECKOUT URL → User redirected to Stripe

6. AFTER PAYMENT → Stripe sends webhook events

Stripe Webhooks

Webhooks are the backbone of the Stripe integration. Rather than polling for payment status, Play The Event registers webhook endpoints that Stripe calls in real-time when events occur. The SubscriptionService provides dedicated handlers for each webhook event type.

Webhook Event Handlers
STRIPE WEBHOOK EVENTS HANDLED

1. customer.subscription.created
   ├── Handler: creaAbbonamentoDaStripe()
   ├── Extract: customerId, priceId, amount, status
   ├── Reverse-map: priceId → TipoPiano + CicloPagamento
   ├── Check: Cancel existing active subscription
   ├── Create: Abbonamento.creaConStripe(...)
   ├── Set trial end date if applicable
   └── If Stripe status is "active" → set ACTIVE immediately

2. customer.subscription.updated
   ├── Handler: gestisciAggiornaAbbonamento()
   ├── Find subscription by Stripe subscription ID
   ├── Map Stripe status → StatoAbbonamento
   │   ├── "active" / "trialing" → ACTIVE
   │   ├── "past_due"            → SUSPENDED
   │   ├── "canceled" / "unpaid" → EXPIRED
   │   └── "incomplete"          → PENDING
   ├── Update: prossimaDataFatturazione
   └── Update: scadeIl (if cancelAt is set)

3. customer.subscription.deleted
   ├── Handler: gestisciCancellazioneAbbonamento()
   ├── Find subscription by Stripe subscription ID
   └── Set state: EXPIRED + scadeIl = now

4. invoice.payment_succeeded
   ├── Handler: gestisciPagamentoRiuscito()
   ├── Find subscription by Stripe subscription ID
   ├── Reset: tentativiPagamentoFalliti = 0
   ├── Clear: ultimoErrorePagamento = null
   ├── Set: ultimoPagamentoIl = now
   └── Reactivate if SUSPENDED or PENDING → ACTIVE

5. invoice.payment_failed
   ├── Handler: gestisciPagamentoFallito()
   ├── Find subscription by Stripe subscription ID
   ├── Increment: tentativiPagamentoFalliti++
   ├── Store: ultimoErrorePagamento = error message
   └── If attempts >= 3 → SUSPENDED

Webhook Security

All incoming webhook requests are verified using Stripe's webhook signing secret. The StripeService stores the webhookSecret configured at deployment, and every incoming event's signature is validated before processing. This prevents malicious actors from forging webhook events to grant themselves free subscriptions or manipulate billing state.

Organization Plan Inheritance

A distinctive feature of the subscription system is plan inheritance through organizations. When a user is an active member of an organization that holds a premium subscription, that user inherits premium access without needing a personal paid plan.

Premium Access Check with Organization Inheritance
PREMIUM ACCESS CHECK

puoAccedereFunzionalitaPremium(userId):

Step 1: Check personal subscription
  ├── Find active subscription for user
  ├── If exists AND consenteFunzionalitaPremium() → return TRUE
  └── Otherwise → continue to Step 2

Step 2: Check organization memberships
  ├── Find all active memberships for user
  ├── For each organization:
  │   ├── Find organization's active subscription
  │   ├── If exists AND consenteFunzionalitaPremium()
  │   │   └── return TRUE (inherited access)
  │   └── Continue to next organization
  └── No premium org found → return FALSE

RESULT:
  User has premium access if:
  - Personal premium subscription is active, OR
  - Any organization they belong to has premium active

This inheritance model means that a company purchasing a BUSINESS_ENTERPRISE plan automatically grants premium access to all its active members, without requiring each member to purchase an individual plan.

Stripe Customer Portal

Instead of building a custom billing management interface, Play The Event leverages Stripe's Customer Portal. The portal provides a pre-built, PCI-compliant interface where users can update payment methods, view invoices, change plans, and cancel subscriptions.

Customer Portal Session
CUSTOMER PORTAL FLOW

1. User clicks "Manage Subscription" in profile
   → POST /api/subscriptions/portal

2. SubscriptionService.creaPortalSession(userId, returnUrl)
   ├── Load user and find Stripe Customer ID
   ├── If no customer ID → create new Stripe Customer
   └── Call stripeService.creaPortalSession(customerId)
       └── Return URL: profile page with account tab

3. Redirect user to Stripe-hosted portal URL
   ├── Update payment method (PCI-compliant)
   ├── View billing history and invoices
   ├── Change subscription plan
   └── Cancel subscription

4. User finishes → Redirected back to returnUrl

Event Crowdfunding

Beyond subscriptions, Play The Event supports financial contributions to individual events through the Finanziamento entity. This allows events to receive donations, sponsorships, and other funding types.

Funding Types
FUNDING TYPES (TipoFinanziamento)

1. FINANZIAMENTO (Funding)
   └── General financial contribution to an event

2. DONAZIONE (Donation)
   └── No-strings-attached contribution from supporters

3. SPONSORIZZAZIONE (Sponsorship)
   └── Brand-linked contribution with visibility expectations

4. CONTRIBUTO (Contribution)
   └── Targeted contribution for specific event aspects

The Finanziamento entity tracks the full lifecycle of each contribution: donor information (name, email, optional user ID), the amount using the Money Value Object for currency safety, the funding date, a confirmation workflow, and an audit trail. Each contribution must be explicitly confirmed by the event organizer before it is considered received.

Funding Confirmation Workflow
FUNDING LIFECYCLE

1. CREATION
   Finanziamento.crea(eventoId, titolo, importo,
                      tipo, donatoreNome, registratoDaId,
                      dataFinanziamento)
   ├── Validates: eventoId, titolo, importo > 0
   ├── Sets: confermato = false
   └── Tracks: registratoDaId (who recorded it)

2. PENDING CONFIRMATION
   ├── Organizer reviews the contribution
   ├── Optionally links donor user account
   └── Optionally adds description

3. CONFIRMATION
   finanziamento.conferma()
   ├── confermato = true
   ├── confermatoIl = now
   └── dataRicezione = today

4. CANCELLATION (if needed)
   finanziamento.annullaConferma()
   ├── confermato = false
   ├── confermatoIl = null
   └── dataRicezione = null

Funding Features

  • Four funding types for different scenarios
  • Money Value Object ensures currency safety
  • Donor tracking with optional user linking
  • Confirmation workflow for financial accountability
  • Database indexes on evento, donatore, tipo, and date
  • Optimistic locking via @Version for concurrency

Business Rules

  • Amount must be positive (validated at creation)
  • Title and donor name are required
  • Description limited to 1,000 characters
  • Only organizers can confirm contributions
  • Confirmation can be reversed if needed
  • Full audit trail with JPA auditing

Subscription Status API

The frontend needs a comprehensive view of the user's subscription status to drive UI decisions: which features to enable, whether to show upgrade prompts, and whether premium access comes from a personal plan or organization membership. The SubscriptionStatusResponse provides all of this in a single call.

Subscription Status Response
GET /api/subscriptions/status → SubscriptionStatusResponse

{
  "haPiano": true,
  "tipoPiano": "PRIVATE_STARTER",
  "tipoPianoDisplayName": "Private Starter",
  "stato": "ACTIVE",
  "statoDisplayName": "Attivo",
  "pagamentoValido": true,
  "errorePagamento": null,
  "prossimaDataFatturazione": "2026-04-07T00:00:00Z",

  // Premium access (personal OR inherited)
  "puoAccedereFunzionalitaPremium": true,

  // Organization inheritance info
  "accessoEreditatoDaOrganizzazione": false,
  "organizzazioneIdEredita": null,
  "organizzazioneNomeEredita": null,
  "pianoEreditato": null,
  "pianoEreditatoDisplayName": null
}

SCENARIO: Free user in premium organization
{
  "haPiano": false,
  "tipoPiano": "PRIVATE_FREE",
  "puoAccedereFunzionalitaPremium": true,
  "accessoEreditatoDaOrganizzazione": true,
  "organizzazioneNomeEredita": "EventCorp Srl",
  "pianoEreditato": "BUSINESS_ENTERPRISE",
  "pianoEreditatoDisplayName": "Business Enterprise"
}

In the Next Article

With the payment and subscription system in place, the platform has a complete monetization layer. The next article explores Organizations and Festivals: how companies and associations manage memberships with organizational roles, how multi-day festivals are modeled with rooms, stands, and bookings, and how the Money Value Object ensures financial accuracy across all budget operations. Visit www.playtheevent.com to see the subscription system in action.

Key Takeaways

  • Stripe integration is layered: domain model stays independent of the Stripe SDK
  • TipoPiano encodes pricing, limits, and account-type compatibility in a type-safe enum
  • Five subscription states (PENDING, ACTIVE, CANCELLED, SUSPENDED, EXPIRED) drive feature gating
  • Three failed payments trigger automatic suspension; successful payment auto-reactivates
  • Four factory methods cover paid, free, time-limited bypass, and permanent bypass scenarios
  • Promotional codes support percentage discounts, fixed discounts, and payment bypass
  • Webhooks handle real-time updates: creation, payment success/failure, updates, and deletion
  • Organization plan inheritance grants premium access to all active members
  • Event crowdfunding supports donations, sponsorships, and contributions with confirmation workflow

Frequently asked questions

What is the architecture of Stripe integration in Play The Event?

The integration with Stripe follows a layered approach, where the domain model is completely independent of Stripe's SDK, and an infrastructure service encapsulates all direct interactions with the Stripe API.

How does Play The Event handle payment failures?

Play The Event implements a three-strike policy, where each failed payment increments a counter, and after three consecutive failures the subscription is automatically suspended.

What is the purpose of the promotional code system in Play The Event?

The promotional code system supports four distinct types, covering both Stripe-integrated discounts and payment-bypassing scenarios, with features such as validation periods, usage limits, and plan-specific applicability.

How does Play The Event handle subscription status API calls from the frontend?

The provides a comprehensive view of the user's subscription status in a single call, including which features to enable, whether to show upgrade prompts, and whether premium access comes from a personal plan or organization membership.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects