Ir al contenido

Cambiar plan

Actualiza tu suscripción a un plan superior (upgrade) o inferior (downgrade) con prorrateo automático.

  1. En la tarjeta de resumen, clic en “Cambiar plan”
  2. Se abre un diálogo con los planes disponibles en grid
  3. Selecciona el nuevo plan (el actual aparece con badge “Actual”)
  4. El sistema muestra automáticamente:
    • Crédito por tiempo no usado del plan actual
    • Cargo por el nuevo plan (prorrateado)
    • Total a pagar o reembolsar
  5. Revisa el preview de prorrateo
  6. Clic en “Confirmar Cambio”
  7. Si tus datos de facturación están completos:
    • Upgrade: Se aplica el cambio inmediatamente
    • Downgrade: Se programa para el fin del período actual
  8. Si faltan datos, se muestra diálogo de validación con campos faltantes
  9. Completa los datos requeridos y vuelve a intentar

“Datos de facturación incompletos”

Causa: Faltan campos obligatorios en tu perfil de negocio.

Solución:

  1. El diálogo de validación muestra exactamente qué campos faltan
  2. Ve a Configuración > Negocio
  3. Completa los campos marcados:
    • ✅ CIF/NIF válido (con dígito de control correcto)
    • ✅ Razón social completa
    • ✅ Dirección completa
    • ✅ Código postal y ciudad
    • ✅ Teléfono en formato +34 XXX XXX XXX
    • ✅ Email de contacto
  4. Guarda los cambios
  5. Vuelve a Suscripción e intenta el cambio de plan

“Ya estás suscrito a este plan”

Causa: Intentas seleccionar tu plan actual.

Solución:

  • El plan actual aparece con un badge “Actual” y no es seleccionable
  • Elige un plan diferente

“No se pudo procesar el cambio”

Causa: Error en la comunicación con Stripe.

Solución:

  1. Verifica tu conexión a internet
  2. Intenta de nuevo en unos minutos
  3. Si persiste, contacta a soporte con el mensaje de error
Implementación del cambio de plan

Componente: ChangePlanDialog

Ubicación: src/panels/owner/subscriptions/components/management/ChangePlanDialog.tsx

Flujo de validación:

// 1. Validar datos de facturación
const { data, error } = await supabase.functions.invoke('create-checkout-session', {
body: {
business_id: currentBusinessId,
plan_id: selectedPlanId,
success_url: `${window.location.origin}/subscription/success`,
cancel_url: `${window.location.origin}/subscription/cancel`,
}
});
// 2. Si hay errores de validación estructurados
if (error?.data?.validation) {
setValidationErrors(error.data.validation.errors);
setValidationWarnings(error.data.validation.warnings);
setValidatedFields(error.data.validation.validated_fields);
setShowValidationDialog(true);
return;
}
// 3. Tipos de respuesta:
// Caso A: Cambio de plan con Subscription API
{
type: 'plan_change',
subscription_updated: true,
message: 'Plan actualizado exitosamente',
invoice_url: 'https://invoice.stripe.com/...'
}
// Caso B: Nueva suscripción (requiere checkout)
{
type: 'checkout',
checkout_url: 'https://checkout.stripe.com/...'
}

Cálculo de prorrateo:

// Edge function: get-proration-preview
const { data } = await supabase.functions.invoke('get-proration-preview', {
body: {
business_id: currentBusinessId,
new_plan_id: selectedPlanId,
user_id: user?.id,
}
});
// Respuesta:
{
proration: {
credit_amount: 15.50, // Crédito por 15 días no usados
charge_amount: 49.00, // Cargo del plan Premium
tax_amount: 10.29, // IVA 21%
total_amount: 43.79 // Total a pagar
}
}

Algoritmo de prorrateo de Stripe:

  1. Calcula días restantes del período actual
  2. Convierte a crédito proporcional: (días_restantes / días_totales) * precio_plan_actual
  3. Calcula cargo del nuevo plan para días restantes: (días_restantes / días_totales) * precio_plan_nuevo
  4. Diferencia: cargo_nuevo - crédito_actual = total_a_pagar

Validación de campos obligatorios:

// Función: validateBusinessBillingInfo
const validationRules = {
tax_id: {
required: true,
pattern: /^[0-9]{8}[A-Z]$|^[ABCDEFGHJNPQRSUVW][0-9]{7}[0-9A-J]$|^[XYZ][0-9]{7}[A-Z]$/,
message: 'CIF/NIF inválido o con formato incorrecto'
},
legal_name: {
required: true,
minLength: 2,
message: 'La razón social es obligatoria'
},
address: {
required: true,
minLength: 5,
message: 'La dirección completa es obligatoria'
},
postal_code: {
required: true,
pattern: /^(0[1-9]|[1-4][0-9]|5[0-2])[0-9]{3}$/,
message: 'Código postal español inválido'
},
phone: {
required: true,
pattern: /^\+34[6-9][0-9]{8}$/,
message: 'Teléfono debe ser formato +34XXXXXXXXX'
}
};

Estados del diálogo:

// Indicador de progreso visual
type LoadingStage = 'validating' | 'sending' | 'processing' | 'success';
// validating (0-25%): Validando datos de negocio
// sending (25-90%): Enviando a Stripe
// processing (90-100%): Procesando cambio
// success (100%): Cambio completado
Edge Function: create-checkout-session

Ubicación: supabase/functions/create-checkout-session/index.ts

Responsabilidades:

  1. Validar datos del negocio:
const validationResult = validateBusinessBillingInfo(business);
if (!validationResult.isValid) {
return {
error: 'Datos de facturación incompletos',
validation: {
errors: validationResult.errors,
warnings: validationResult.warnings,
validated_fields: validationResult.validated_fields
}
};
}
  1. Verificar estado de suscripción:
const { data: existingSubscriptions } = await supabase
.from('business_subscriptions')
.select('*')
.eq('business_id', business_id)
.in('status', ['active', 'trialing', 'past_due']);
// Caso A: Sin suscripción activa → crear checkout
if (!existingSubscriptions || existingSubscriptions.length === 0) {
return createStripeCheckout();
}
// Caso B: Con suscripción activa → cambiar plan
if (existingSubscriptions.length === 1) {
return updateExistingSubscription();
}
// Caso C: Múltiples suscripciones → error
if (existingSubscriptions.length > 1) {
throw new Error('Multiple active subscriptions');
}
  1. Crear Stripe Customer (si no existe):
const customer = await stripe.customers.create({
email: business.email,
name: business.legal_name,
metadata: {
business_id: business.id,
tax_id: business.tax_id,
},
address: {
line1: business.address,
city: business.city,
postal_code: business.postal_code,
country: business.country || 'ES',
},
phone: business.phone,
tax_id_data: [{
type: taxIdValidation.type, // 'es_cif' o 'es_nif'
value: business.tax_id,
}],
});
  1. Crear Checkout Session o actualizar suscripción:
// Opción A: Nueva suscripción
const session = await stripe.checkout.sessions.create({
customer: customer.id,
mode: 'subscription',
line_items: [{
price: plan.stripe_price_id,
quantity: 1,
}],
subscription_data: {
trial_period_days: 14,
metadata: {
business_id: business.id,
plan_id: plan.id,
},
},
success_url,
cancel_url,
});
// Opción B: Cambio de plan (con prorrateo)
await stripe.subscriptions.update(existingSubscription.stripe_subscription_id, {
items: [{
id: subscriptionItem.id,
price: newPlan.stripe_price_id,
}],
proration_behavior: 'always_invoice', // Factura el prorrateo inmediatamente
billing_cycle_anchor: 'unchanged', // Mantiene la fecha de renovación
});

Manejo de errores:

try {
// Lógica de checkout/update
} catch (error) {
// Errores específicos de Stripe
if (error.type === 'StripeCardError') {
return { error: 'Tarjeta rechazada: ' + error.message };
}
if (error.type === 'StripeInvalidRequestError') {
return { error: 'Solicitud inválida a Stripe: ' + error.message };
}
// Error genérico
return { error: error.message || 'Error desconocido' };
}