Permitir que clientes facam upgrade de plano e fundamental para maximizar receita. A Chargefy automatiza o calculo de prorata, cobrando apenas a diferenca proporcional ao periodo restante e atualizando o valor recorrente a partir do proximo ciclo.
Como funciona o upgrade
Calculo de prorata
Quando o cliente faz upgrade no meio do ciclo, a Chargefy calcula:
Credito do plano atual : valor proporcional aos dias nao utilizados
Custo do novo plano : valor proporcional aos dias restantes
Diferenca cobrada : custo do novo plano - credito do plano atual
Exemplo pratico:
Plano atual: Basic (R$ 49,90/mes) — contratado dia 01/03
Upgrade para: Pro (R$ 99,90/mes) — no dia 16/03
Dias no ciclo: 31 (marco)
Dias restantes: 15
Credito Basic: R$ 49,90 x (15/31) = R$ 24,15
Custo Pro: R$ 99,90 x (15/31) = R$ 48,34
Prorata: R$ 48,34 - R$ 24,15 = R$ 24,19
Cobranca imediata: R$ 24,19
Proximo ciclo (01/04): R$ 99,90/mes
Passo 1: Identificar a assinatura atual
Antes de processar o upgrade, busque a assinatura ativa do cliente para apresentar as opcoes disponiveis.
Via SDK
const subscription = await chargefy . subscriptions . get ( 'sub_xxxx' )
console . log ({
planoAtual: subscription . product . name ,
precoAtual: `R$ ${ ( subscription . amount / 100 ). toFixed ( 2 ) } ` ,
intervalo: subscription . recurringInterval ,
proximoCiclo: subscription . currentPeriodEnd ,
})
Via cURL
curl -X GET https://api.chargefy.io/v1/subscriptions/sub_xxxx \
-H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN "
Passo 2: Executar o upgrade
Atualize a assinatura com o novo productPriceId e habilite a prorata.
Via SDK
// Upgrade de Basic (R$ 49,90/mes) para Pro (R$ 99,90/mes)
const upgraded = await chargefy . subscriptions . update ( 'sub_xxxx' , {
productPriceId: 'price_pro_mensal_xxxx' , // ID do novo preco
proration: true , // cobrar diferenca proporcional
})
console . log ( 'Upgrade realizado!' )
console . log ( 'Novo plano:' , upgraded . product . name )
console . log ( 'Novo valor: R$' , ( upgraded . amount / 100 ). toFixed ( 2 ))
console . log ( 'Credito prorata: R$' , ( upgraded . prorationCredit / 100 ). toFixed ( 2 ))
console . log ( 'Cobranca prorata: R$' , ( upgraded . prorationAmount / 100 ). toFixed ( 2 ))
Via cURL
curl -X PATCH https://api.chargefy.io/v1/subscriptions/sub_xxxx \
-H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN " \
-H "Content-Type: application/json" \
-d '{
"productPriceId": "price_pro_mensal_xxxx",
"proration": true
}'
O upgrade e aplicado imediatamente . O cliente recebe acesso ao novo plano assim que a cobranca de prorata e confirmada.
Passo 3: Processar webhooks do upgrade
Dois eventos sao disparados durante um upgrade:
subscription.updated
Enviado quando a assinatura e alterada para o novo plano.
// Handler de webhook
case 'subscription.updated' :
const { data } = event
// Verificar se foi upgrade
if ( data . previousAmount && data . amount > data . previousAmount ) {
console . log ( 'Upgrade detectado!' )
console . log ( `De R$ ${ ( data . previousAmount / 100 ). toFixed ( 2 ) } para R$ ${ ( data . amount / 100 ). toFixed ( 2 ) } ` )
// Atualizar banco de dados
await db . subscriptions . update (
{ chargefy_id: data . id },
{
product_id: data . product . id ,
amount: data . amount ,
tier: data . product . metadata ?. tier ,
}
)
// Liberar recursos do novo plano
await upgradeFeatures ( data . customer . id , data . product . metadata ?. tier )
// Notificar cliente
await sendEmail ( data . customer . email , 'plan_upgraded' , {
newPlan: data . product . name ,
newAmount: `R$ ${ ( data . amount / 100 ). toFixed ( 2 ) } ` ,
})
}
break
subscription.active
Enviado quando a cobranca de prorata e confirmada.
case 'subscription.active' :
if ( event . data . metadata ?. type === 'proration' ) {
console . log ( 'Prorata de upgrade paga:' , `R$ ${ ( event . data . amount / 100 ). toFixed ( 2 ) } ` )
}
break
Passo 4: Upgrade sem prorata (opcional)
Em alguns casos, voce pode querer aplicar o upgrade sem cobrar prorata — o novo valor so entra no proximo ciclo.
const upgraded = await chargefy . subscriptions . update ( 'sub_xxxx' , {
productPriceId: 'price_pro_mensal_xxxx' ,
proration: false , // novo valor apenas no proximo ciclo
})
console . log ( 'Upgrade agendado' )
console . log ( 'Plano atual ate:' , upgraded . currentPeriodEnd )
console . log ( 'Novo plano a partir de:' , upgraded . scheduledChange ?. effectiveDate )
Sem prorata, o cliente ja tem acesso aos recursos do novo plano, mas paga o preco antigo ate o fim do ciclo. Isso pode causar perda de receita em ciclos longos (ex: plano anual).
Upgrade de intervalo (mensal para anual)
Alem de trocar de tier, o cliente pode querer migrar de cobranca mensal para anual.
// Upgrade de Pro Mensal (R$ 99,90/mes) para Pro Anual (R$ 959,00/ano)
const upgraded = await chargefy . subscriptions . update ( 'sub_xxxx' , {
productPriceId: 'price_pro_anual_xxxx' ,
proration: true ,
})
console . log ( 'Migrado para plano anual!' )
console . log ( 'Credito mensal restante: R$' , ( upgraded . prorationCredit / 100 ). toFixed ( 2 ))
console . log ( 'Valor anual cobrado: R$' , ( upgraded . prorationAmount / 100 ). toFixed ( 2 ))
Exemplo completo: Pagina de upgrade
src/components/UpgradePlan.tsx
import { useState , useEffect } from 'react'
interface Plan {
id : string
name : string
priceId : string
amount : number
interval : string
tier : string
}
interface UpgradePreview {
currentPlan : string
newPlan : string
prorationCredit : number
prorationCharge : number
newMonthlyAmount : number
}
export function UpgradePlan ({ subscriptionId } : { subscriptionId : string }) {
const [ plans , setPlans ] = useState < Plan []>([])
const [ preview , setPreview ] = useState < UpgradePreview | null >( null )
const [ selectedPriceId , setSelectedPriceId ] = useState < string | null >( null )
const [ processing , setProcessing ] = useState ( false )
useEffect (() => {
fetch ( '/api/subscriptions/plans' )
. then ( r => r . json ())
. then ( setPlans )
}, [])
async function handlePreview ( priceId : string ) {
setSelectedPriceId ( priceId )
const res = await fetch ( `/api/subscriptions/ ${ subscriptionId } /preview-upgrade` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ newPriceId: priceId }),
})
const data = await res . json ()
setPreview ( data )
}
async function handleConfirmUpgrade () {
if ( ! selectedPriceId ) return
setProcessing ( true )
const res = await fetch ( `/api/subscriptions/ ${ subscriptionId } /change-plan` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ newPriceId: selectedPriceId }),
})
if ( res . ok ) {
alert ( 'Upgrade realizado com sucesso!' )
window . location . href = '/dashboard'
} else {
alert ( 'Erro ao processar upgrade.' )
setProcessing ( false )
}
}
const formatCurrency = ( cents : number ) => `R$ ${ ( cents / 100 ). toFixed ( 2 ) } `
return (
< div style = { { maxWidth: '600px' } } >
< h2 > Fazer upgrade </ h2 >
< div style = { { display: 'flex' , flexDirection: 'column' , gap: '0.75rem' } } >
{ plans . map ( plan => (
< button
key = { plan . priceId }
onClick = { () => handlePreview ( plan . priceId ) }
style = { {
padding: '1rem' ,
border: selectedPriceId === plan . priceId ? '2px solid #2563eb' : '1px solid #e5e7eb' ,
borderRadius: '8px' ,
textAlign: 'left' ,
cursor: 'pointer' ,
background: 'white' ,
} }
>
< strong > { plan . name } </ strong >
< span style = { { float: 'right' } } >
{ formatCurrency ( plan . amount ) } / { plan . interval === 'month' ? 'mes' : 'ano' }
</ span >
</ button >
)) }
</ div >
{ preview && (
< div style = { { marginTop: '1.5rem' , border: '1px solid #e5e7eb' , borderRadius: '8px' , padding: '1.5rem' } } >
< h3 > Resumo do upgrade </ h3 >
< table style = { { width: '100%' } } >
< tbody >
< tr >
< td > Plano atual </ td >
< td style = { { textAlign: 'right' } } > { preview . currentPlan } </ td >
</ tr >
< tr >
< td > Novo plano </ td >
< td style = { { textAlign: 'right' } } > { preview . newPlan } </ td >
</ tr >
< tr >
< td > Credito restante </ td >
< td style = { { textAlign: 'right' , color: '#16a34a' } } > - { formatCurrency ( preview . prorationCredit ) } </ td >
</ tr >
< tr style = { { fontWeight: 'bold' , borderTop: '1px solid #e5e7eb' } } >
< td > Cobranca hoje </ td >
< td style = { { textAlign: 'right' } } > { formatCurrency ( preview . prorationCharge ) } </ td >
</ tr >
< tr >
< td > Novo valor recorrente </ td >
< td style = { { textAlign: 'right' } } > { formatCurrency ( preview . newMonthlyAmount ) } /mes </ td >
</ tr >
</ tbody >
</ table >
< button
onClick = { handleConfirmUpgrade }
disabled = { processing }
style = { {
marginTop: '1rem' , width: '100%' , padding: '0.75rem' ,
background: '#2563eb' , color: 'white' , border: 'none' ,
borderRadius: '6px' , cursor: processing ? 'not-allowed' : 'pointer' ,
} }
>
{ processing ? 'Processando...' : 'Confirmar upgrade' }
</ button >
</ div >
) }
</ div >
)
}
Boas praticas
Sempre mostre um preview do calculo de prorata antes de confirmar o upgrade
Libere acesso imediatamente apos o upgrade ser confirmado — nao espere o webhook
Envie email de confirmacao informando o novo plano e o valor cobrado
Registre logs de todas as mudancas de plano para auditoria
Proximos passos
Downgrade de Assinatura Implemente downgrades com controle de periodo.
Variantes de Produto Configure os planos disponiveis para upgrade.