Skip to content

TalentBricksAI usa Stripe para procesar pagos de cursos y suscripciones.

ModeloDescripcionPrecio Sugerido
Curso IndividualCompra unica de un curso$29-99 USD
Suscripcion MensualAcceso a todos los cursos$19/mes
Suscripcion AnualAcceso a todos los cursos$149/ano (~35% descuento)
  1. Ir a stripe.com
  2. Crear cuenta
  3. Completar verificacion de negocio

En el Dashboard de Stripe:

  1. Ir a Developers > API Keys
  2. Copiar Publishable key y Secret key

Para desarrollo, usar las keys de Test mode.

.env.server

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

.env.client

REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_...
  1. Ir a Products > Add Product
  2. Crear productos:
ProductoTipoPrecio
Suscripcion MensualRecurring$19/mes
Suscripcion AnualRecurring$149/ano

Para cursos individuales, los productos se crean dinamicamente.

Terminal window
# Crear producto de suscripcion
stripe products create \
--name="Suscripcion Mensual TalentBricksAI" \
--description="Acceso a todos los cursos"
# Crear precio
stripe prices create \
--product=prod_xxx \
--unit-amount=1900 \
--currency=usd \
--recurring[interval]=month

Editar app/src/payment/plans.ts:

export enum PaymentPlanId {
// Existentes
Hobby = 'hobby',
Pro = 'pro',
// Nuevos para cursos
SingleCourse = 'single-course',
MonthlySubscription = 'monthly-subscription',
AnnualSubscription = 'annual-subscription',
}
export const paymentPlans: Record<PaymentPlanId, PaymentPlan> = {
[PaymentPlanId.MonthlySubscription]: {
name: 'Suscripcion Mensual',
price: 19,
interval: 'month',
stripePriceId: 'price_xxx', // ID del precio en Stripe
features: [
'Acceso a todos los cursos',
'Nuevos cursos cada mes',
'Certificados de completacion',
'Soporte prioritario'
]
},
[PaymentPlanId.AnnualSubscription]: {
name: 'Suscripcion Anual',
price: 149,
interval: 'year',
stripePriceId: 'price_yyy',
features: [
'Todo lo de mensual',
'Ahorra 35%',
'Acceso anticipado a cursos'
]
},
[PaymentPlanId.SingleCourse]: {
name: 'Curso Individual',
price: 0, // Precio dinamico por curso
interval: 'one_time',
stripePriceId: null, // Se crea dinamicamente
features: [
'Acceso de por vida al curso',
'Certificado de completacion'
]
}
};
courses/operations.ts
export const createCourseCheckout: CreateCourseCheckout = async (
{ courseId },
context
) => {
if (!context.user) throw new HttpError(401);
const course = await context.entities.Course.findUnique({
where: { id: courseId }
});
if (!course) throw new HttpError(404, 'Curso no encontrado');
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: course.title,
description: course.description.substring(0, 500),
images: course.thumbnail ? [course.thumbnail] : []
},
unit_amount: course.price // en centavos
},
quantity: 1
}],
mode: 'payment',
success_url: `${process.env.CLIENT_URL}/curso/${course.slug}/aprender?success=true`,
cancel_url: `${process.env.CLIENT_URL}/curso/${course.slug}?canceled=true`,
metadata: {
userId: context.user.id.toString(),
courseId: course.id.toString(),
type: 'course_purchase'
}
});
return { url: session.url };
};
export const createSubscriptionCheckout: CreateSubscriptionCheckout = async (
{ planId },
context
) => {
if (!context.user) throw new HttpError(401);
const plan = paymentPlans[planId as PaymentPlanId];
if (!plan || !plan.stripePriceId) {
throw new HttpError(400, 'Plan invalido');
}
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price: plan.stripePriceId,
quantity: 1
}],
mode: 'subscription',
success_url: `${process.env.CLIENT_URL}/mis-cursos?success=true`,
cancel_url: `${process.env.CLIENT_URL}/precios?canceled=true`,
metadata: {
userId: context.user.id.toString(),
type: 'subscription'
}
});
return { url: session.url };
};

Configurar el webhook en app/src/payment/stripe/webhook.ts:

export const stripeWebhook = async (req: Request, res: Response) => {
const sig = req.headers['stripe-signature'] as string;
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object);
break;
case 'customer.subscription.updated':
case 'customer.subscription.deleted':
await handleSubscriptionChange(event.data.object);
break;
}
res.json({ received: true });
};
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const { userId, courseId, type } = session.metadata!;
if (type === 'course_purchase') {
// Crear enrollment
await prisma.enrollment.create({
data: {
userId: parseInt(userId),
courseId: parseInt(courseId)
}
});
}
if (type === 'subscription') {
// Actualizar estado de suscripcion
await prisma.user.update({
where: { id: parseInt(userId) },
data: {
subscriptionStatus: 'active',
subscriptionPlan: session.metadata!.planId
}
});
// Enrollar en todos los cursos publicados
const courses = await prisma.course.findMany({
where: { isPublished: true }
});
await prisma.enrollment.createMany({
data: courses.map(course => ({
userId: parseInt(userId),
courseId: course.id
})),
skipDuplicates: true
});
}
}
Terminal window
# Instalar Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Escuchar webhooks (dejar corriendo)
stripe listen --forward-to localhost:3001/stripe-webhook

En Stripe Dashboard:

  1. Ir a Developers > Webhooks
  2. Add endpoint: https://tu-dominio.com/stripe-webhook
  3. Seleccionar eventos:
    • checkout.session.completed
    • customer.subscription.updated
    • customer.subscription.deleted
courses/operations.ts
export const getCourseAccess: GetCourseAccess = async (
{ courseId },
context
) => {
if (!context.user) return { hasAccess: false, reason: 'not_logged_in' };
// Verificar suscripcion activa
if (context.user.subscriptionStatus === 'active') {
return { hasAccess: true, reason: 'subscription' };
}
// Verificar enrollment individual
const enrollment = await context.entities.Enrollment.findUnique({
where: {
userId_courseId: {
userId: context.user.id,
courseId
}
}
});
if (enrollment) {
return { hasAccess: true, reason: 'purchased' };
}
return { hasAccess: false, reason: 'not_purchased' };
};

Crear una pagina de precios atractiva:

PricingPage.tsx
export function PricingPage() {
return (
<div className="container mx-auto py-12">
<h1 className="text-4xl font-bold text-center mb-4">
Invierte en tu Carrera
</h1>
<p className="text-center text-muted-foreground mb-12">
Accede a todos nuestros cursos de Data Engineering e IA
</p>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<PricingCard plan={paymentPlans.monthlySubscription} />
<PricingCard plan={paymentPlans.annualSubscription} featured />
</div>
</div>
);
}