Ir al contenido

Cancelar suscripción

Cancela tu suscripción actual. Mantendrás acceso hasta el final del período de facturación.

  1. En la pestaña “Facturación”, desplázate hasta el final
  2. Clic en “Cancelar suscripción” (enlace en rojo)
  3. 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
  4. Confirma escribiendo “CANCELAR” en el campo de texto
  5. Clic en “Sí, cancelar suscripción”
  6. El estado cambia a “Se cancelará el [fecha]”
  7. Al finalizar el período, pierdes acceso al sistema

“No se pudo cancelar la suscripción”

Causa: Error de comunicación con Stripe.

Solución:

  1. Intenta de nuevo
  2. Si persiste, usa el Stripe Customer Portal: “Gestionar pago” → “Cancelar plan”
  3. 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
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 activa
const { 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 datos
await supabase
.from('business_subscriptions')
.update({
cancel_at_period_end: true,
updated_at: new Date().toISOString()
})
.eq('id', subscription.id);
// 4. Programar notificación de recordatorio
await 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: CancelSubscriptionDialog
const 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ón
export 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');
}
}