Cambiar plan
Exportar contenido
Actualiza tu suscripción a un plan superior (upgrade) o inferior (downgrade) con prorrateo automático.
- En la tarjeta de resumen, clic en “Cambiar plan”
- Se abre un diálogo con los planes disponibles en grid
- Selecciona el nuevo plan (el actual aparece con badge “Actual”)
- 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
- Revisa el preview de prorrateo
- Clic en “Confirmar Cambio”
- 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
- Si faltan datos, se muestra diálogo de validación con campos faltantes
- Completa los datos requeridos y vuelve a intentar
Errores comunes
Sección titulada «Errores comunes»“Datos de facturación incompletos”
Causa: Faltan campos obligatorios en tu perfil de negocio.
Solución:
- El diálogo de validación muestra exactamente qué campos faltan
- Ve a Configuración > Negocio
- 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
- Guarda los cambios
- 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:
- Verifica tu conexión a internet
- Intenta de nuevo en unos minutos
- Si persiste, contacta a soporte con el mensaje de error
Detalles técnicos
Sección titulada «Detalles técnicos»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ónconst { 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 estructuradosif (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-previewconst { 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:
- Calcula días restantes del período actual
- Convierte a crédito proporcional:
(días_restantes / días_totales) * precio_plan_actual - Calcula cargo del nuevo plan para días restantes:
(días_restantes / días_totales) * precio_plan_nuevo - Diferencia:
cargo_nuevo - crédito_actual = total_a_pagar
Validación de campos obligatorios:
// Función: validateBusinessBillingInfoconst 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 visualtype 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 completadoEdge Function: create-checkout-session
Ubicación: supabase/functions/create-checkout-session/index.ts
Responsabilidades:
- 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 } };}- 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 checkoutif (!existingSubscriptions || existingSubscriptions.length === 0) { return createStripeCheckout();}
// Caso B: Con suscripción activa → cambiar planif (existingSubscriptions.length === 1) { return updateExistingSubscription();}
// Caso C: Múltiples suscripciones → errorif (existingSubscriptions.length > 1) { throw new Error('Multiple active subscriptions');}- 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, }],});- Crear Checkout Session o actualizar suscripción:
// Opción A: Nueva suscripciónconst 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' };}