Files
ketopath/apps/api/.env.example
T
lucianoandClaude Opus 4.7 f455cbe499 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>
2026-05-07 11:27:06 +02:00

47 lines
1.8 KiB
Bash

NODE_ENV=development
PORT=4000
HOST=127.0.0.1
LOG_LEVEL=info
# CORS — origini consentite (separate da virgola)
CORS_ORIGINS=http://localhost:3000
DATABASE_URL=postgresql://luciano:luciano%40@localhost:5432/ketopath?schema=public
# Cifratura at-rest dei campi sanitari (vedi ADR 0002).
# Genera con: node -e "console.log('k1.aesgcm256.'+require('crypto').generateKeySync('aes',{length:256}).export().toString('base64url'))"
# DEVE coincidere con apps/web/.env e con tutti i deploy che leggono il DB.
PRISMA_FIELD_ENCRYPTION_KEY=
# Better Auth — DEVE coincidere con apps/web/.env (sessione condivisa)
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=http://localhost:3000
# Google OAuth (configurato a fine progetto)
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# Sentry — error tracking (lascia vuoto per disabilitare)
# SENTRY_DSN=
# Web Push — VAPID (vedi ADR 0003).
# Genera con: cd apps/api && node -e "console.log(require('web-push').generateVAPIDKeys())"
# La PUBLIC_KEY va anche in apps/web/.env come NEXT_PUBLIC_VAPID_PUBLIC_KEY.
# Lascia vuoto per disabilitare le push (il cron e gli endpoint rispondono 503).
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:hello@ketopath.app
# Stripe — abbonamenti (vedi ADR 0004). Tutti opzionali: senza queste env il
# webhook e gli endpoint /me/billing/* rispondono 503; le route gated (genera
# piano, pesata, fast, export PDF) continuano a funzionare per gli utenti in
# trial o non gated, ma chi è EXPIRED riceve 402 a prescindere.
# Usa le keys "test mode" per sviluppo (sk_test_..., whsec_...).
# I price ID si creano da dashboard Stripe → Products.
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_ID_MONTHLY=
STRIPE_PRICE_ID_YEARLY=
# URL di base usato nelle redirect post-checkout (success/cancel).
BILLING_RETURN_URL=http://localhost:3000