Skip to main content
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:
  1. Credito do plano atual: valor proporcional aos dias nao utilizados
  2. Custo do novo plano: valor proporcional aos dias restantes
  3. 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.