Pular para o conteúdo principal

Estorno

startRefundFlow()​

@RunAsync
fun startRefundFlow(
operatorTaxId: String? = null,
paymentMethod: PaymentMethod? = null,
metadata: TransactionMetadata = TransactionMetadata.EMPTY
)

Inicia o fluxo de estorno com cartão: captura o cartão, busca as transações estornáveis daquele cartão e chama onRefundTransactionsAvailable(...). O app mostra a lista e confirma pela função recebida no callback. O resultado chega por onRefundCompleted(result) ou onRefundError(errorCode, message, ...).

ParâmetroDescrição
operatorTaxIdCPF/CNPJ de quem autorizou o estorno
paymentMethodTipo escolhido pelo operador. VOUCHER orienta a leitura em cartões multi-aplicação e lista só vendas voucher; CREDIT/DEBIT listam crédito e débito; null não filtra
metadataDados do app que seguem até o gateway. Veja Configuração

Fluxo de callbacks:

  1. onStartGetCard() — aguardando cartão
  2. onRefundTransactionsAvailable(transactions, ..., confirmRefund) — o app chama confirmRefund(indice, valor): valor = null estorna o saldo total; indice = null cancela
  3. onRefundCompleted(result) ou onRefundError(errorCode, message, ...) — resultado

O estorno volta pelo adquirente da venda original. A seleção da transação tem 30 s; sem resposta, o fluxo termina por tempo esgotado (onAppError(PP_TIMEOUT)). Os valores de errorCode estão em Callbacks — Estorno.

Como os demais fluxos start*, sem terminal inicializado só emite onAppError(SdkErrorCode.TERMINAL_NOT_INITIALIZED, ...) — não lança exceção.

startPixRefundFlow() e startPixRefundListFlow()​

Estorno de PIX, sem leitura de cartão.

// Estorno de uma transação PIX conhecida (valor total, em centavos)
fun startPixRefundFlow(transactionId: String, amount: Long, operatorTaxId: String? = null)

// Estorno a partir da lista de PIX do dia
fun startPixRefundListFlow(operatorTaxId: String? = null)

PIX não tem estorno parcial. No fluxo por lista, as transações chegam em onRefundTransactionsAvailable e o app confirma com confirmRefund(indice, null). O desfecho chega em onRefundCompleted ou onRefundError.

Estorno a partir da listagem de vendas​

Caso comum: o app mostra o relatório de vendas, o operador toca numa venda e pede o estorno. O SDK não tem estorno de cartão por id: só PIX pode ser estornado direto pelo id. Venda com cartão sempre exige a leitura do mesmo cartão, que é o que comprova que quem pede o estorno tem o cartão da venda.

Venda selecionadaComo estornar
paymentMethod == PIXstartPixRefundFlow(tx.id, saldo, operatorTaxId). Não há leitura nem onRefundTransactionsAvailable
Crédito, débito ou voucherstartRefundFlow(operatorTaxId, tx.paymentMethod). Depois da leitura, localize a venda pelo id em onRefundTransactionsAvailable e confirme pelo índice

Sequência de chamadas​

#Quem chamaChamadaCartãoPIX
1AppPosPinpad.create(context, callback) e terminalInitialization(...), uma vez, na abertura do app✓✓
2AppgetTransactionSummaries(filtros): monta o relatório✓✓
3AppgetTransactionSummary(id): recarrega a venda escolhida✓✓
4AppstartRefundFlow(...) ou startPixRefundFlow(...)✓✓
5SDKonStartGetCard() e onUiNotify(...): aguardando o cartão✓
6SDKonRefundTransactionsAvailable(..., confirmRefund)✓
7AppconfirmRefund(indice, valor), em até 30 s✓
8SDKonRefundCompleted(result), onRefundError(...) ou onAppError(...)✓✓
9Appprint(result.slip) (opcional) e nova chamada a getTransactionSummaries✓✓

Passo 1 — Criar e inicializar o PosPinpad​

O estorno usa a mesma instância e o mesmo PosPinpadCallback das vendas. A implementação do callback precisa tratar onRefundTransactionsAvailable, onRefundCompleted e onRefundError, além de onStartGetCard, onUiNotify e onAppError, que também aparecem no estorno.

val posPinpad = PosPinpad.create(context, callback)
// ... autenticação e terminalInitialization(...), como em Inicialização do terminal

Antes de mostrar a ação "Estornar", confira posPinpad.isTerminalInitialized(). Sem terminal inicializado, os fluxos start* só emitem onAppError(TERMINAL_NOT_INITIALIZED). Veja Inicialização do terminal.

Passo 2 — Listar as vendas​

Use getTransactionSummaries() com preloads = true. Ele traz os ciclos da venda (NSU, autorização e estornos já feitos), dos quais sai o refundedAmountInCents.

val br = ZoneId.of("America/Sao_Paulo")
val inicioDoDia = LocalDate.now(br).atStartOfDay(br).toInstant().toString()

val filtros = QueryFilterBuilder()
.pagination(cursor = null, limit = 20, preloads = true)
.add(FilterField.CREATED_AT, FilterOperator.GE, inicioDoDia)

val pagina = try {
posPinpad.getTransactionSummaries(filtros) // suspend: chame de uma corrotina
} catch (e: Exception) {
// Falha de rede ou HTTP. O SDK também chama onNetworkConnectionError() no callback
showError("Não foi possível carregar as vendas")
return
}
val vendas = pagina.data // próxima página: pagina.metadata.nextCursor

O relatório pode cobrir qualquer período. O filtro do dia acima só reflete que apenas vendas do dia são estornáveis.

Passo 3 — Mostrar "Estornar" só nas vendas estornáveis​

O SDK só estorna vendas que atendem às regras abaixo. Desabilite a ação nas demais, para o operador não passar o cartão à toa:

  • Do dia (D0, fuso America/Sao_Paulo). Vendas de dias anteriores não aparecem em onRefundTransactionsAvailable.
  • Status approved, ou partial_refunded para estornar o saldo restante (só cartão). PIX só com approved.
  • PIX de adquirente diferente de PagSeguro: isPixRefundableAcquirer(tx.acquirer).
  • Saldo maior que zero: tx.amountInCents - tx.refundedAmountInCents.
private val BR = ZoneId.of("America/Sao_Paulo")

fun TransactionSummary.saldoEstornavel(): Long = amountInCents - refundedAmountInCents

fun TransactionSummary.podeEstornar(): Boolean {
val doDia = OffsetDateTime.parse(createdAt).atZoneSameInstant(BR).toLocalDate() == LocalDate.now(BR)
val statusOk = if (paymentMethod == PaymentMethod.PIX) {
isApproved && isPixRefundableAcquirer(acquirer)
} else {
isApproved || isPartialRefunded
}
return doDia && statusOk && saldoEstornavel() > 0
}

A busca de estornáveis não filtra por terminal: uma venda feita em outro terminal do mesmo estabelecimento pode ser estornada neste.

Passo 4 — Recarregar a venda escolhida​

Quando o operador tocar em "Estornar", busque a venda de novo com getTransactionSummary(id). A listagem pode estar desatualizada (a venda pode ter sido estornada em outro terminal), e o saldo precisa estar correto para o estorno parcial e para o PIX.

val venda = try {
posPinpad.getTransactionSummary(tx.id)
} catch (e: Exception) {
showError("Não foi possível consultar a venda")
return
}
if (!venda.podeEstornar()) {
showError("Esta venda não pode mais ser estornada")
return
}

Para oferecer estorno parcial (só cartão), peça o valor ao operador aqui, limitado a venda.saldoEstornavel().

Passo 5 — Iniciar o estorno​

Guarde a venda escolhida e o valor, no caso do parcial: os dois são usados no passo 7, dentro do callback.

// Estado do estorno em andamento
private var alvo: TransactionSummary? = null
private var valorParcial: Long? = null // null = saldo total

fun estornar(venda: TransactionSummary, valor: Long?, operatorTaxId: String?) {
if (venda.paymentMethod == PaymentMethod.PIX) {
// PIX: sem leitura e sem estorno parcial; o próximo evento já é o resultado (passo 8)
posPinpad.startPixRefundFlow(venda.id, venda.saldoEstornavel(), operatorTaxId)
return
}
alvo = venda
valorParcial = valor
// O método da venda orienta a leitura: em cartão multi-aplicação, VOUCHER faz o
// pinpad usar a aplicação de benefício, que tem PAN próprio
posPinpad.startRefundFlow(operatorTaxId = operatorTaxId, paymentMethod = venda.paymentMethod)
}

startRefundFlow e startPixRefundFlow retornam na hora e rodam em segundo plano (@RunAsync). Tudo o que segue chega pelos callbacks, na main thread.

Passo 6 — Leitura do cartão (só cartão)​

O SDK chama onStartGetCard() e envia as mensagens do pinpad, como "Aproxime ou insira o cartão", por onUiNotify(text, type). Mostre-as ao operador como na venda.

Se a leitura falhar, o fluxo termina aqui com onRefundError:

errorCodeO que fazer
TIMEOUT, CANCELLEDVoltar ao relatório. Também chega onAppError(PP_TIMEOUT) ou onAppError(PP_CANCEL)
CARD_READ_ERROR, NO_DATAPedir para tentar de novo
NO_TRANSACTIONSO cartão lido não tem venda estornável hoje: é outro cartão
NO_TRANSACTIONS_FOR_METHODO cartão tem vendas hoje, mas de outro tipo (cartão × voucher)

Falha de rede ao buscar as vendas do cartão chega como onAppError(GATEWAY_NETWORK_ERROR).

Passo 7 — Confirmar a venda escolhida (só cartão)​

Depois da leitura, o SDK busca as vendas estornáveis daquele cartão e chama onRefundTransactionsAvailable. No estorno a partir do relatório, o app não mostra essa lista: procura nela a venda escolhida pelo id e confirma pelo índice.

override fun onRefundTransactionsAvailable(
transactions: List<TransactionSummary>, cardLastDigits: String, cardBrand: String, cardBin: String,
confirmRefund: (transactionIndex: Int?, refundAmount: Long?) -> Unit
) {
val venda = alvo
val valor = valorParcial
alvo = null
valorParcial = null
if (venda == null) {
confirmRefund(null, null) // o app não pediu estorno
return
}
val indice = transactions.indexOfFirst { it.id == venda.id }
if (indice < 0) {
// O cartão lido tem vendas estornáveis, mas não esta. Cancele;
// nunca estorne outra venda da lista
confirmRefund(null, null)
showError("O cartão final $cardLastDigits não corresponde à venda selecionada")
return
}
confirmRefund(indice, valor) // valor null = saldo total
}

Regras de confirmRefund:

  • Sempre chame, inclusive nos caminhos de erro. Sem resposta em 30 s, o fluxo termina com onAppError(PP_TIMEOUT).
  • confirmRefund(null, null) cancela; chega onAppError(PP_CANCEL).
  • O primeiro parâmetro é a posição em transactions, não o id. Índice fora da lista gera onRefundError("INVALID_SELECTION", ...).
  • refundAmount é em centavos. Valor acima do saldo gera onRefundError("REFUND_EXCEEDS_BALANCE", ...).

Passo 8 — Tratar o resultado​

override fun onRefundCompleted(result: PaymentResult) {
showSuccess("Estorno aprovado")
result.slip?.let { posPinpad.print(it) } // o SDK não imprime o comprovante sozinho
recarregarRelatorio()
}

override fun onRefundError(errorCode: String, message: String, cardLastDigits: String, cardBrand: String, cardBin: String) {
// Erros de leitura (passo 6) e de seleção (passo 7), DETAIL_FETCH_FAILED,
// UNSUPPORTED_METHOD ou o código do gateway quando o estorno é negado
showError("Estorno não realizado [$errorCode]: $message")
}

override fun onAppError(error: SdkErrorCode, message: String) {
when (error) {
SdkErrorCode.PP_CANCEL, SdkErrorCode.PP_TIMEOUT -> voltarAoRelatorio()
else -> showError(message) // ex.: GATEWAY_NETWORK_ERROR, GATEWAY_TIMEOUT
}
}

No estorno parcial, result.receipt traz em amount o valor estornado e em originalAmount o valor da venda. Os códigos de erro estão em Callbacks — Estorno.

Passo 9 — Atualizar o relatório​

Depois de onRefundCompleted, chame getTransactionSummaries de novo. A venda passa a refunded (estorno total) ou partial_refunded (parcial), e refundedAmountInCents passa a incluir o valor estornado. Uma venda partial_refunded do dia continua estornável pelo saldo.

Veja Callbacks — Estorno para implementação detalhada.