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âmetro | Descrição |
|---|---|
operatorTaxId | CPF/CNPJ de quem autorizou o estorno |
paymentMethod | Tipo 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 |
metadata | Dados do app que seguem até o gateway. Veja Configuração |
Fluxo de callbacks:
onStartGetCard()— aguardando cartãoonRefundTransactionsAvailable(transactions, ..., confirmRefund)— o app chamaconfirmRefund(indice, valor):valor = nullestorna o saldo total;indice = nullcancelaonRefundCompleted(result)ouonRefundError(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 selecionada | Como estornar |
|---|---|
paymentMethod == PIX | startPixRefundFlow(tx.id, saldo, operatorTaxId). Não há leitura nem onRefundTransactionsAvailable |
| Crédito, débito ou voucher | startRefundFlow(operatorTaxId, tx.paymentMethod). Depois da leitura, localize a venda pelo id em onRefundTransactionsAvailable e confirme pelo índice |
Sequência de chamadas
| # | Quem chama | Chamada | Cartão | PIX |
|---|---|---|---|---|
| 1 | App | PosPinpad.create(context, callback) e terminalInitialization(...), uma vez, na abertura do app | ✓ | ✓ |
| 2 | App | getTransactionSummaries(filtros): monta o relatório | ✓ | ✓ |
| 3 | App | getTransactionSummary(id): recarrega a venda escolhida | ✓ | ✓ |
| 4 | App | startRefundFlow(...) ou startPixRefundFlow(...) | ✓ | ✓ |
| 5 | SDK | onStartGetCard() e onUiNotify(...): aguardando o cartão | ✓ | |
| 6 | SDK | onRefundTransactionsAvailable(..., confirmRefund) | ✓ | |
| 7 | App | confirmRefund(indice, valor), em até 30 s | ✓ | |
| 8 | SDK | onRefundCompleted(result), onRefundError(...) ou onAppError(...) | ✓ | ✓ |
| 9 | App | print(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 emonRefundTransactionsAvailable. - Status
approved, oupartial_refundedpara estornar o saldo restante (só cartão). PIX só comapproved. - 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:
errorCode | O que fazer |
|---|---|
TIMEOUT, CANCELLED | Voltar ao relatório. Também chega onAppError(PP_TIMEOUT) ou onAppError(PP_CANCEL) |
CARD_READ_ERROR, NO_DATA | Pedir para tentar de novo |
NO_TRANSACTIONS | O cartão lido não tem venda estornável hoje: é outro cartão |
NO_TRANSACTIONS_FOR_METHOD | O 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; chegaonAppError(PP_CANCEL).- O primeiro parâmetro é a posição em
transactions, não oid. Índice fora da lista geraonRefundError("INVALID_SELECTION", ...). refundAmounté em centavos. Valor acima do saldo geraonRefundError("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.