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.
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 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.
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.
// 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
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.
// 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 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 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 (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
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 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.
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
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 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 (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 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
@Versionfor 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.
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







