Configurar Pagos
Section titled “Configurar Pagos”TalentBricksAI usa Stripe para procesar pagos de cursos y suscripciones.
Modelos de Pago
Section titled “Modelos de Pago”| Modelo | Descripcion | Precio Sugerido |
|---|---|---|
| Curso Individual | Compra unica de un curso | $29-99 USD |
| Suscripcion Mensual | Acceso a todos los cursos | $19/mes |
| Suscripcion Anual | Acceso a todos los cursos | $149/ano (~35% descuento) |
Configuracion de Stripe
Section titled “Configuracion de Stripe”1. Crear Cuenta Stripe
Section titled “1. Crear Cuenta Stripe”- Ir a stripe.com
- Crear cuenta
- Completar verificacion de negocio
2. Obtener API Keys
Section titled “2. Obtener API Keys”En el Dashboard de Stripe:
- Ir a Developers > API Keys
- Copiar Publishable key y Secret key
Para desarrollo, usar las keys de Test mode.
3. Configurar Variables de Entorno
Section titled “3. Configurar Variables de Entorno”.env.server
STRIPE_SECRET_KEY=sk_test_...STRIPE_WEBHOOK_SECRET=whsec_....env.client
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_...Crear Productos en Stripe
Section titled “Crear Productos en Stripe”Opcion 1: Dashboard de Stripe
Section titled “Opcion 1: Dashboard de Stripe”- Ir a Products > Add Product
- Crear productos:
| Producto | Tipo | Precio |
|---|---|---|
| Suscripcion Mensual | Recurring | $19/mes |
| Suscripcion Anual | Recurring | $149/ano |
Para cursos individuales, los productos se crean dinamicamente.
Opcion 2: Stripe CLI
Section titled “Opcion 2: Stripe CLI”# Crear producto de suscripcionstripe products create \ --name="Suscripcion Mensual TalentBricksAI" \ --description="Acceso a todos los cursos"
# Crear preciostripe prices create \ --product=prod_xxx \ --unit-amount=1900 \ --currency=usd \ --recurring[interval]=monthConfigurar Planes en la Aplicacion
Section titled “Configurar Planes en la Aplicacion”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' ] }};Flujo de Checkout
Section titled “Flujo de Checkout”Compra de Curso Individual
Section titled “Compra de Curso Individual”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 };};Suscripcion
Section titled “Suscripcion”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 };};Webhook de Stripe
Section titled “Webhook de Stripe”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 }); }}Configurar Webhook en Stripe
Section titled “Configurar Webhook en Stripe”Desarrollo (Local)
Section titled “Desarrollo (Local)”# Instalar Stripe CLIbrew install stripe/stripe-cli/stripe
# Loginstripe login
# Escuchar webhooks (dejar corriendo)stripe listen --forward-to localhost:3001/stripe-webhookProduccion
Section titled “Produccion”En Stripe Dashboard:
- Ir a Developers > Webhooks
- Add endpoint:
https://tu-dominio.com/stripe-webhook - Seleccionar eventos:
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deleted
Verificar Acceso a Cursos
Section titled “Verificar Acceso a Cursos”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' };};Pagina de Precios
Section titled “Pagina de Precios”Crear una pagina de precios atractiva:
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> );}Siguiente Paso
Section titled “Siguiente Paso”- Despliegue - Deploy a produccion