Referencia completa de código TypeScript / Next.js 14 App Router para suscripciones, pagos, webhooks idempotentes, portal de cliente y facturación por uso.
| Plan | Mensual | Anual | Eventos/mes | Proyectos | Trial | Stripe Price ID (env) |
|---|---|---|---|---|---|---|
| Starter | 29 € | 290 € (-17%) | 100.000 | 3 | ✓ 14 días | STRIPE_STARTER_*_PRICE_ID |
| Pro | 79 € | 790 € (-17%) | 1.000.000 | Ilimitados | ✓ 14 días | STRIPE_PRO_*_PRICE_ID |
| Enterprise | Factura a medida | 10M+ | Ilimitados | — | STRIPE_ENT_PRICE_ID | |
// lib/stripe.ts — FlowMetrics Stripe client import Stripe from "stripe" export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-04-10", typescript: true, appInfo: { name: "flowmetrics", version: "1.0.0" }, }) export const PLANS = { starter: { monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID!, yearly: process.env.STRIPE_STARTER_YEARLY_PRICE_ID!, eventsLimit: 100_000, projects: 3, label: "Starter", }, pro: { monthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!, yearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID!, eventsLimit: 1_000_000, projects: Infinity, label: "Pro", }, } as const // Metered price for events over quota export const METERED_EVENTS_PRICE_ID = process.env.STRIPE_METERED_EVENTS_PRICE_ID! // ───────────────────────────────────────────── // app/api/billing/checkout/route.ts // ───────────────────────────────────────────── import { NextResponse } from "next/server" import { stripe, PLANS } from "@/lib/stripe" import { getAuthUser } from "@/lib/auth" import { db } from "@/lib/db" export async function POST(req: Request) { const user = await getAuthUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) const { planKey, interval } = await req.json() // interval: "monthly" | "yearly" const plan = PLANS[planKey as keyof typeof PLANS] if (!plan) return NextResponse.json({ error: "Invalid plan" }, { status: 400 }) // Get or create Stripe customer let customerId = user.stripeCustomerId if (!customerId) { const customer = await stripe.customers.create({ email: user.email, name: user.name, metadata: { userId: user.id }, }) customerId = customer.id await db.user.update({ where: { id: user.id }, data: { stripeCustomerId: customerId } }) } const session = await stripe.checkout.sessions.create({ customer: customerId, mode: "subscription", payment_method_types: ["card"], line_items: [{ price: plan[interval], quantity: 1 }], allow_promotion_codes: true, subscription_data: { // Solo primer trial por workspace trial_period_days: user.hasHadTrial ? undefined : 14, metadata: { userId: user.id, planKey: planKey }, }, success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?cancelled=1`, metadata: { userId: user.id }, }) return NextResponse.json({ url: session.url }) }
// lib/billing.ts — Upgrade/Downgrade + Prorateamiento FlowMetrics import { stripe } from "@/lib/stripe" /** * Cambia el plan de suscripción. * immediate=true → upgrade inmediato con prorateamiento (genera factura) * immediate=false → downgrade al final del período, sin cargo adicional */ export async function changeSubscriptionPlan( subscriptionId: string, newPriceId: string, immediate = false, ) { const sub = await stripe.subscriptions.retrieve(subscriptionId) const item = sub.items.data[0] return stripe.subscriptions.update(subscriptionId, { items: [{ id: item.id, price: newPriceId }], proration_behavior: immediate ? "always_invoice" : "none", billing_cycle_anchor: "unchanged", }) } /** Preview del cargo de prorateamiento antes de confirmar upgrade */ export async function previewProration(subscriptionId: string, newPriceId: string) { const sub = await stripe.subscriptions.retrieve(subscriptionId) const prorationDate = Math.floor(Date.now() / 1000) const preview = await stripe.invoices.retrieveUpcoming({ customer: sub.customer as string, subscription: subscriptionId, subscription_items: [{ id: sub.items.data[0].id, price: newPriceId }], subscription_proration_date: prorationDate, }) return { amountDueCents: preview.amount_due, amountDueEur: (preview.amount_due / 100).toFixed(2), prorationDate, lines: preview.lines.data.map((l) => ({ description: l.description, amount: (l.amount / 100).toFixed(2) + " €", })), } } /** Reportar eventos para facturación metered (over-quota) */ export async function reportEvents(subscriptionItemId: string, quantity: number) { await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, { quantity, timestamp: Math.floor(Date.now() / 1000), action: "increment", }) } /** Feature gate — acceso activo si active|trialing o grace period past_due */ export function isSubscriptionActive(user: { subscriptionStatus: string | null stripeCurrentPeriodEnd: Date | null }): boolean { if (!user.subscriptionStatus) return false if (["active", "trialing"].includes(user.subscriptionStatus)) return true // Grace period: past_due pero dentro del período actual if (user.subscriptionStatus === "past_due" && user.stripeCurrentPeriodEnd) { return user.stripeCurrentPeriodEnd > new Date() } return false }
| Evento Stripe | Acción en FlowMetrics DB | Prioridad |
|---|---|---|
| checkout.session.completed | Vincula stripeSubscriptionId al user, marca hasHadTrial=true, sincroniza plan y period_end | CRÍTICO |
| customer.subscription.updated | Actualiza priceId, status, period_end y cancelAtPeriodEnd | CRÍTICO |
| customer.subscription.deleted | Anula subscriptionId/priceId/periodEnd; status → "canceled" | CRÍTICO |
| invoice.payment_succeeded | Restaura status → "active", actualiza period_end para el nuevo ciclo | IMPORTANTE |
| invoice.payment_failed | Status → "past_due". Si attempt_count ≥ 3: envío email dunning final | IMPORTANTE |
| customer.subscription.trial_will_end | Envía email recordatorio 3 días antes del fin de trial con CTA de pago | INFO |
| Variable | Descripción | |
|---|---|---|
| STRIPE_SECRET_KEY | API key privada (sk_live_…) | REQ |
| STRIPE_WEBHOOK_SECRET | Secreto de endpoint webhook (whsec_…) | REQ |
| STRIPE_STARTER_MONTHLY_PRICE_ID | Price Starter mensual 29€ | REQ |
| STRIPE_STARTER_YEARLY_PRICE_ID | Price Starter anual 290€ | REQ |
| STRIPE_PRO_MONTHLY_PRICE_ID | Price Pro mensual 79€ | REQ |
| STRIPE_PRO_YEARLY_PRICE_ID | Price Pro anual 790€ | REQ |
| STRIPE_METERED_EVENTS_PRICE_ID | Precio metered 0,005€/1k ev. | REQ |
| NEXT_PUBLIC_APP_URL | URL base (success/cancel redirect) | REQ |