Cancelar suscripción
Exportar contenido
Cancela tu suscripción actual. Mantendrás acceso hasta el final del período de facturación.
- En la pestaña “Facturación”, desplázate hasta el final
- Clic en “Cancelar suscripción” (enlace en rojo)
- Se abre diálogo de confirmación explicando:
- Mantendrás acceso hasta el fin del período
- No se harán más cobros
- Tus datos se conservan
- Puedes reactivar en cualquier momento
- Confirma escribiendo “CANCELAR” en el campo de texto
- Clic en “Sí, cancelar suscripción”
- El estado cambia a “Se cancelará el [fecha]”
- Al finalizar el período, pierdes acceso al sistema
Errores comunes
Sección titulada «Errores comunes»“No se pudo cancelar la suscripción”
Causa: Error de comunicación con Stripe.
Solución:
- Intenta de nuevo
- Si persiste, usa el Stripe Customer Portal: “Gestionar pago” → “Cancelar plan”
- Contacta a soporte si no puedes cancelar por ninguna vía
“Ya tienes una cancelación programada”
Causa: Tu suscripción ya está marcada para cancelarse al fin del período.
Solución:
- Si quieres reactivarla, clic en “Reactivar suscripción”
- Si quieres cancelar inmediatamente, contacta a soporte
Detalles técnicos
Sección titulada «Detalles técnicos»Implementación de cancelación
Edge Function: cancel-subscription
Ubicación: supabase/functions/cancel-subscription/index.ts
Flujo completo:
// 1. Validar que existe suscripción activaconst { data: subscription } = await supabase .from('business_subscriptions') .select('*') .eq('business_id', businessId) .in('status', ['active', 'trialing']) .order('created_at', { ascending: false }) .limit(1) .single();
if (!subscription?.stripe_subscription_id) { throw new Error('No active subscription found');}
// 2. Cancelar en Stripe (al final del período)await stripe.subscriptions.update(subscription.stripe_subscription_id, { cancel_at_period_end: true, // NO cancela inmediatamente});
// 3. Actualizar en base de datosawait supabase .from('business_subscriptions') .update({ cancel_at_period_end: true, updated_at: new Date().toISOString() }) .eq('id', subscription.id);
// 4. Programar notificación de recordatorioawait supabase .from('subscription_notifications') .insert({ business_id: businessId, notification_type: 'subscription_ending', scheduled_for: new Date(subscription.current_period_end), metadata: { plan_name: subscription.plan_name, end_date: subscription.current_period_end } });
console.log(`Subscription cancelled for business ${businessId}`);Respuesta de la función:
{ success: true, message: 'Subscription will be cancelled at the end of the billing period', currentPeriodEnd: '2026-03-15T23:59:59.000Z'}Webhook: customer.subscription.deleted
Cuando llega la fecha de fin del período, Stripe envía este webhook:
stripe.webhook.on('customer.subscription.deleted', async (subscription) => { // 1. Actualizar estado a canceled await supabase .from('business_subscriptions') .update({ status: 'canceled', canceled_at: new Date().toISOString() }) .eq('stripe_subscription_id', subscription.id);
// 2. Registrar en audit log await supabase .from('subscription_audit_log') .insert({ business_id: subscription.metadata.business_id, old_plan_id: subscription.metadata.plan_id, new_plan_id: null, change_type: 'canceled', effective_date: new Date().toISOString(), metadata: { reason: 'User requested cancellation', cancel_at_period_end: true } });
// 3. Enviar email de confirmación await sendEmail({ to: business.email, template: 'subscription_canceled', data: { business_name: business.name, plan_name: subscription.plan.name, canceled_at: new Date().toLocaleDateString('es-ES') } });});Reactivación de suscripción cancelada:
Si el usuario cambia de opinión antes de que finalice el período:
// Componente: CancelSubscriptionDialogconst reactivateSubscription = async () => { // 1. Reactivar en Stripe await stripe.subscriptions.update(subscription.stripe_subscription_id, { cancel_at_period_end: false, });
// 2. Actualizar en base de datos await supabase .from('business_subscriptions') .update({ cancel_at_period_end: false, updated_at: new Date().toISOString() }) .eq('id', subscription.id);
// 3. Mostrar confirmación toast({ title: "Suscripción reactivada", description: "Tu suscripción continuará normalmente", });};Control de acceso post-cancelación:
// Middleware de autenticaciónexport function checkSubscriptionAccess(businessId: string) { const { data: subscription } = await supabase .from('business_subscriptions') .select('status, current_period_end') .eq('business_id', businessId) .single();
// Permitir acceso si: // 1. Está activa // 2. Está cancelada pero aún en período de gracia const now = new Date(); const periodEnd = new Date(subscription.current_period_end);
const hasAccess = subscription.status === 'active' || subscription.status === 'trialing' || (subscription.status === 'canceled' && now < periodEnd);
if (!hasAccess) { throw new Error('Subscription expired or canceled'); }}