Pagamenti e abbonamenti
Il kit include un'integrazione Stripe completa: abbonamenti ricorrenti con prove gratuite facoltative, pagamenti una tantum (le offerte a vita), un esempio a consumo, codici promozionali, fatture con il loro PDF, e Stripe Tax facoltativo. Tutto gira sul tuo account Stripe e sui tuoi prezzi; i piani inseriti dal seed sono segnaposto da sostituire col listino del tuo prodotto.
Il modello dei dati
Tre modelli Prisma reggono la fatturazione (vedi prisma/schema.prisma):
| Modello | Cosa contiene |
|---|---|
Plan | L'unica fonte di verità per ogni livello vendibile: nome, prezzo, interval (MONTH, YEAR o ONE_TIME), l'ID del prezzo su Stripe, le funzioni mostrate sulle schede, isActive, e trialDays quando il piano offre una prova gratuita. |
Subscription | Una riga per utente abbonato, tenuta allineata dal webhook: stato, periodo corrente, flag di disdetta, e trialEndsAt quando l'abbonamento è partito con una prova. |
Purchase | Una riga per pagamento una tantum, creata dal webhook. L'ID del PaymentIntent di Stripe è unico, il che rende innocui i tentativi ripetuti del webhook. |
Per sapere cosa ha un utente, chiama getEntitlement(userId) da src/lib/billing.ts. Restituisce lifetime, subscription o free (a vita vince quando ci sono entrambi) ed è lo schema da copiare quando devi proteggere una tua funzione:
import { getEntitlement } from "@/lib/billing"
const entitlement = await getEntitlement(session.user.id)
if (entitlement.kind === "free") {
// mostra l'invito a passare a un piano
}Abbonamenti
Il flusso: l'utente sceglie un piano su /dashboard/billing, POST /api/checkout valida il prezzo contro la tabella Plan e apre Stripe Checkout in modalità subscription, e il webhook (/api/webhooks/stripe) crea o aggiorna la riga Subscription quando arriva checkout.session.completed. Cambi di piano, disdette e metodi di pagamento li gestisce il Customer Portal di Stripe (POST /api/billing/portal): il kit non include logica di rateo dentro l'app, di proposito.
Un utente con un abbonamento attivo non può avviare un secondo checkout; l'API risponde 400 e lo manda al portale.
Prove gratuite
Imposta trialDays su un Plan (il piano Pro mensile del seed ne ha 14) e il checkout offre quei giorni gratis, tramite subscription_data.trial_period_days.
- Una prova per cliente.
trialDaysFor()insrc/lib/billing.tsnon dà la prova a chi ha già una rigaSubscription, e un abbonamento disdetto la sua riga la conserva. Senza questa regola, disdire e rifare il checkout farebbe ripartire la prova ogni volta. Le schede dei piani applicano la stessa regola, quindi promettono una prova solo a chi il checkout la darà davvero - La carta si inserisce subito, che è il comportamento predefinito del Checkout. Il primo addebito arriva quando la prova finisce, senza un secondo passaggio per il cliente
- Durante la prova l'abbonamento è
TRIALING, che pergetEntitlementvale come accesso. La pagina dei pagamenti mostra la data di fine, e l'email di conferma dice quando arriva il primo addebito - Le email di promemoria prima della fine della prova le manda Stripe: attivale nel pannello Stripe, nelle impostazioni di Billing, invece di scriverne di tue
- I piani una tantum non hanno mai la prova
Codici promozionali
Imposta STRIPE_ALLOW_PROMOTION_CODES="true" e il Checkout mostra il campo per il codice. Crea i coupon e i loro codici nel pannello Stripe (Product catalog → Coupons). L'interruttore è spento di default perché il campo compare anche quando non esiste nessun codice, e un campo vuoto manda i clienti a cercare uno sconto che non c'è.
Uno sconto attivo compare nella pagina dei pagamenti: la percentuale o l'importo, quanto dura, e il codice che l'ha applicato. Viene letto da Stripe quando la pagina si carica, quindi un coupon che chiudi su Stripe sparisce dalla pagina senza webhook e senza migrazioni.
Un coupon che dura un certo numero di mesi li conta da quando viene applicato, prova compresa: tre mesi applicati all'inizio di una prova di 14 giorni coprono circa due mesi e mezzo pagati.
Pagamenti una tantum
Un piano con interval: "ONE_TIME" (il piano «Lifetime» del seed) passa invece dal checkout in modalità payment:
POST /api/checkoutcrea la sessione coninvoice_creationattivo, così il pagamento compare nello storico delle fatture e nel portale come qualsiasi fattura di abbonamento.- Su
checkout.session.completedil webhook crea una rigaPurchase. Gli eventi ripetuti trovano la riga già presente e non fanno nulla. - Parte un'email di conferma tramite Resend, quando è configurato.
Rimborsi: su charge.refunded il webhook segna l'acquisto come REFUNDED, il che toglie il diritto d'accesso. I rimborsi parziali lasciano l'acquisto intatto; solo un rimborso totale lo revoca.
Se chi ha comprato a vita ha anche un vecchio abbonamento, la pagina dei pagamenti glielo segnala suggerendo di disdirlo dal portale. Il kit non lo disdice da solo: è una decisione di prodotto che resta tua.
Più livelli di prezzo
Le schede su /dashboard/billing rendono ogni riga Plan attiva, con l'interruttore mensile/annuale quando esistono entrambi gli intervalli e un'etichetta «Pay once» sui piani una tantum. Per cambiare il listino modifichi dati, non componenti: aggiorni prisma/seed.ts (o le righe direttamente) e l'interfaccia segue.
Accanto a queste c'è una scheda facoltativa per la vendita assistita, src/components/billing/enterprise-card.ts, che non è una riga Plan e non avvia nessun checkout. Segue anche lei l'interruttore: imposta il suo blocco yearly per mostrare una cifra diversa sul lato annuale, per esempio $500 «al mese» contro $5.000 «all'anno». Lascia fuori yearly e la scheda dice la stessa cosa da entrambe le parti.
Fatturazione a consumo
Il kit include un esempio a metrica ridotto all'osso: un aiuto recordUsage(), un piano «Pay as you go» disattivo nel seed, e questa guida. Per accenderlo:
-
Crea un Billing Meter nel pannello Stripe (Billing → Meters → Create meter). Imposta il nome dell'evento su
api_request, o uno tuo, e l'aggregazione su Sum. -
Crea un prezzo a consumo: sul tuo prodotto aggiungi un prezzo ricorrente, scegli «Usage-based» e seleziona il meter. Copia l'ID del prezzo in
STRIPE_METERED_PRICE_ID. -
Riesegui il seed e attiva: lancia
npm run db:seed, poi impostaisActive: truesul pianometered-example(modifica il seed o la riga). IlmeterEventNamedel piano deve coincidere col nome dell'evento del meter. -
Registra il consumo dal codice lato server, dove avviene la cosa che fatturi:
import { recordUsage } from "@/lib/usage" // per esempio dentro una server action o una route API await recordUsage(session.user.id, "api_request")Passa un
identifierunico quando il chiamante potrebbe riprovare: Stripe lo usa per non contare due volte lo stesso evento. L'aiuto non fa nulla, con un avviso in console, quando Stripe non è configurato o l'utente non ha ancora un cliente Stripe, così il codice strumentato è sicuro ovunque.
Il checkout gestisce i prezzi a consumo da solo (vengono inviati senza quantità). Stripe fattura il consumo accumulato alla fine di ogni periodo.
Fatture
La pagina dei pagamenti elenca le ultime dieci fatture da Stripe, ognuna con il totale nella sua valuta, lo stato, un link alla fattura online e un link al PDF.
Stripe Tax
Imposta STRIPE_AUTOMATIC_TAX="true" e il Checkout attiva il calcolo automatico delle tasse, chiede un indirizzo di fatturazione e raccoglie le partite IVA. L'indirizzo e la ragione sociale inseriti nel Checkout vengono salvati sul cliente Stripe, cosa che Stripe richiede per entrambe le funzioni.
Configura Stripe Tax prima di accenderlo: nel pannello Stripe attiva Tax con l'indirizzo della sede e aggiungi una registrazione per ogni luogo in cui riscuoti le tasse. È la parte da leggere due volte, per come fallisce: con Stripe Tax non attivo, Stripe non rifiuta il checkout. Crea la sessione, raccoglie l'indirizzo e non applica nessuna tassa, e la fattura registra il motivo come not_collecting. Per questo al checkout il kit chiede a Stripe le impostazioni di Tax e, finché non sono attive, scrive nei log un avviso con quello che manca. Non blocca mai un pagamento.
Cosa non fa:
- Non è gratis: Stripe fa pagare Tax a transazione, in aggiunta alle sue commissioni abituali. La cifra aggiornata è sulla loro pagina dei prezzi, e vale la pena leggerla prima di accenderlo
- Non ti registra da nessuna parte. Se devi registrarti, e dove, è una domanda per il tuo commercialista
- Riscuote solo dove hai aggiunto una registrazione; altrove la tassa è zero
- Se un prezzo include la tassa o la aggiunge sopra si decide sul prezzo in Stripe. Un prezzo senza comportamento fiscale impostato viene trattato come tassa esclusa
Eventi del webhook
Il gestore su /api/webhooks/stripe tratta questi eventi; selezionali quando crei l'endpoint di produzione (vedi Deployment):
checkout.session.completed(abbonamenti e pagamenti una tantum)customer.subscription.updatedcustomer.subscription.deletedinvoice.payment_failedcharge.refunded
Provare in locale
Con le chiavi di test in .env.local e la Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripeUsa la carta 4242 4242 4242 4242 nel checkout. Controlli utili: compra il piano Lifetime e verifica la riga Purchase e la fattura nella pagina dei pagamenti; rimanda l'evento (stripe events resend <event_id>) e verifica che non si duplichi niente; rimborsa il pagamento dal pannello e verifica che il piano torni Free.
Per vedere una prova gratuita, registrati con un account nuovo e scegli il piano Pro mensile del seed: il Checkout mostra i giorni gratis, e dopo aver pagato con la carta di test la pagina dei pagamenti mostra quando finisce la prova.
Senza Stripe
Tutto degrada con eleganza quando STRIPE_SECRET_KEY non è impostata: la pagina dei pagamenti rende con i pulsanti di checkout disattivati, la lista delle fatture si spiega da sola, recordUsage() non fa nulla, e /api/checkout risponde con un 503 chiaro. Gli interruttori dei codici promozionali e di Stripe Tax non hanno effetto senza una chiave. Anche la modalità demo (DEMO_MODE="true") disattiva il checkout, così la demo pubblica può mostrare l'interfaccia dei pagamenti su dati di esempio senza un account Stripe.