feat(billing): Stripe + abbonamento Pro con trial 30gg (ADR 0004)
Chiude la decisione "payment provider" aperta in CLAUDE.md. **Modello**: free 30gg post-signup (no carta richiesta) → Pro mensile €9,90 / annuale €89. Allinea il paywall al confine fra fase INTENSIVE e TRANSITION (PRD §5.1), quando l'utente ha già visto i primi risultati. Dopo la scadenza l'app non si "spegne": storico restano consultabili (sola lettura), ma generazione piani / nuove pesate / digiuni / foto / export richiedono abbonamento attivo. **Schema**: nuova tabella `subscriptions` (1:1 con users) con stati TRIALING/ACTIVE/PAST_DUE/CANCEL_AT_PERIOD_END/CANCELED/EXPIRED + `billing_webhook_events` per idempotenza dei retry Stripe. **Backend** (`apps/api/src/modules/billing/`): - `GET /me/billing/status` — snapshot + derived (kind, isPro, trialDaysRemaining) - `POST /me/billing/checkout` — crea Stripe Checkout Session (subscription mode + Stripe Tax + tax_id_collection) - `POST /me/billing/portal` — Customer Portal Session - `POST /webhooks/stripe` — raw body, firma HMAC, idempotenza per `event.id`, dispatch su `customer.subscription.*`, `checkout.session.completed`, `invoice.payment_failed` - Plugin `requirePro()` (402 payment_required) applicato a 9 rotte: meal-plans CRUD, weight-entries POST, check-ins POST, fast-events POST/PATCH, fasting/pause POST, export.pdf (plan+tracking) **Shared** (`@ketopath/shared/billing/pro-status`): - `isProActive(snap)` — verifica live (gestisce anche TRIALING con `trialEndsAt` passato in caso di cron in ritardo) - `deriveProStatus(snap)` — kind + isPro + trialDaysRemaining + accessEndsAt per l'UI - `computeTrialEndsAt(signupAt, days=30)` - 11 unit test **Frontend** (`apps/web/src/app/[locale]/billing/`): - Pagina `/billing` editoriale (capitolo VIII) con StatusBlock per ogni kind, BillingActionsBar client (transitions, redirect a Stripe), 3 benefits - `<TrialBanner>` in SignedInDashboard (3 stati: trial in corso oro / scaduto pomodoro / past_due pomodoro) - Nav item "Abbonamento" (chapter VI) nel grid asimmetrico - i18n IT completo (`Billing` namespace) **Soft-degradation**: env Stripe (`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID_*`, `BILLING_RETURN_URL`) sono tutte opzionali. Senza configurazione i route billing rispondono 503 e il banner trial mostra "pagamenti non ancora attivati" — utenti in trial continuano a usare l'app. 99/99 test verdi, lint pulito su tutto il monorepo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
41b3646325
commit
f455cbe499
28 files changed
+2035
-486
No files matched your search
@@ -15,6 +15,7 @@
|
||||
"viewShopping": "Lista della spesa",
|
||||
"viewTracking": "Pesata & misure",
|
||||
"viewFasting": "Digiuno intermittente",
|
||||
"viewBilling": "Abbonamento & fatture",
|
||||
"lettura": "Continua la lettura",
|
||||
"inizia": "Inizia",
|
||||
"greeting": "Buongiorno, <name>{name}</name>.",
|
||||
@@ -710,5 +711,57 @@
|
||||
"email_required": "Email assente sul tuo account.",
|
||||
"api_error": "Errore di rete. Riprova fra poco."
|
||||
}
|
||||
},
|
||||
"Billing": {
|
||||
"title": "Abbonamento",
|
||||
"subtitle": "Trenta giorni di prova gratuiti, senza carta. Poi un piano semplice — mensile o annuale — che si gestisce in autonomia dal portale Stripe.",
|
||||
"currentStatus": "Stato attuale",
|
||||
"actions": "Azioni",
|
||||
"trialActive": "Sei in prova ({days} giorni rimasti).",
|
||||
"trialActiveBody": "Hai accesso completo a piani, tracking, digiuno e foto progress fino al {endsAt}. Allo scadere puoi continuare a leggere lo storico — ma per nuove pesate, piani e foto serve l'abbonamento.",
|
||||
"trialExpired": "La prova è scaduta.",
|
||||
"trialExpiredBody": "Lo storico resta consultabile. Per ricominciare a generare piani, registrare pesate e completare digiuni, attiva un abbonamento.",
|
||||
"proActive": "Abbonamento attivo.",
|
||||
"proActiveBody": "Piano {interval}, prossimo rinnovo il {renewsAt}. Tutto sbloccato.",
|
||||
"pastDue": "Pagamento in sospeso.",
|
||||
"pastDueBody": "L'ultimo addebito è fallito. Stripe sta riprovando: aggiorna la carta dal portale per evitare interruzioni.",
|
||||
"canceling": "Disdetta in corso.",
|
||||
"cancelingBody": "L'abbonamento resta valido fino al {endsAt}. Puoi riattivarlo in qualsiasi momento dal portale.",
|
||||
"canceled": "Abbonamento disdetto.",
|
||||
"canceledBody": "Lo storico resta tuo e consultabile. Quando vuoi rientrare, basta un nuovo upgrade.",
|
||||
"noSubscription": "Nessun abbonamento attivo.",
|
||||
"intervalMonth": "mensile",
|
||||
"intervalYear": "annuale",
|
||||
"upgradeMonthly": "Abbonati — €9,90 / mese",
|
||||
"upgradeYearly": "Abbonati — €89 / anno (≈25% di sconto)",
|
||||
"managePortal": "Gestisci abbonamento",
|
||||
"opening": "Apertura…",
|
||||
"checkoutHint": "Verrai reindirizzato al pagamento sicuro su Stripe.",
|
||||
"notConfigured": "Pagamenti non ancora attivati. La tua prova continua: nessuna interruzione.",
|
||||
"errorLoading": "Impossibile caricare lo stato dell'abbonamento.",
|
||||
"whatYouGet": "Cosa è incluso",
|
||||
"benefit1Title": "Piani settimanali infiniti",
|
||||
"benefit1Body": "Genera quanti piani vuoi, riadattati alla fase, alle preferenze e alle esclusioni alimentari. Con riadattamento automatico se cambi peso o protocollo.",
|
||||
"benefit2Title": "Tracking completo",
|
||||
"benefit2Body": "Pesate quotidiane, check-in soggettivi, foto progress, digiuno con timer e milestone. Tutto cifrato a riposo (art. 9 GDPR).",
|
||||
"benefit3Title": "Export PDF per il medico",
|
||||
"benefit3Body": "Un documento sintetico — peso, misure, aderenza — pronto da inviare al nutrizionista o consultare in studio.",
|
||||
"trialBannerTitle": "Sei in prova",
|
||||
"trialBannerBody": "{days} giorni rimasti. Tutto sbloccato fino al {endsAt}.",
|
||||
"trialBannerCta": "Vai all'abbonamento",
|
||||
"trialEndedBannerTitle": "La prova è scaduta",
|
||||
"trialEndedBannerBody": "Per generare nuovi piani e registrare pesate serve l'abbonamento.",
|
||||
"trialEndedBannerCta": "Abbonati",
|
||||
"paywallTitle": "Funzione Pro",
|
||||
"paywallBody": "Questa azione richiede un abbonamento attivo.",
|
||||
"paywallCta": "Vai all'abbonamento",
|
||||
"navLabel": "Abbonamento",
|
||||
"error": {
|
||||
"billing_not_configured": "Il sistema di pagamento non è ancora configurato. Riprova più tardi.",
|
||||
"no_stripe_customer": "Non hai ancora un abbonamento attivo. Avvia prima il checkout.",
|
||||
"api_error_400": "Richiesta non valida.",
|
||||
"api_error_401": "Devi accedere per gestire l'abbonamento.",
|
||||
"api_error_500": "Errore del server. Riprova fra poco."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user