SDK de pagamento integrado: o que gravar no pedido, no log e no sandbox (sem o cartão)
Defina o que o seu banco guarda depois de integrar um SDK de pagamento: id da transação e status, nunca o número do cartão. Limpe o log, separe sandbox de produção e concilie pelo retorno do provedor.
Terça-feira, 10h. O suporte pede para "refazer a cobrança do pedido 4812" e alguém descobre que dá: a tabela pedidos tem uma coluna cartao_numero, preenchida desde o primeiro dia da integração, porque o exemplo do SDK recebia o número no PHP e o desenvolvedor achou útil guardar. A resposta curta: o SDK de pagamento facilita a chamada ao provedor, mas não autoriza você a guardar o cartão. Depois da integração, o seu banco deve ter o identificador da transação no provedor, o status e o valor. O número do cartão (PAN), a validade e o código de segurança não entram em tabela, log, fila nem backup.
Este guia não repete o fluxo de checkout em si; para a parte de o número nem passar pelo seu servidor, veja o guia de pagamento sem o cartão passar pelo servidor. Aqui o foco é o que acontece em volta do SDK: o modelo de dados do pedido, o log, os ambientes e a conciliação. É onde o cartão vaza sem ninguém perceber.
Por que o cartão acaba gravado mesmo sem ninguém decidir isso
SDK de pagamento costuma vir com exemplos que montam um objeto com tudo: comprador, endereço, itens e, em alguns fluxos, dados do cartão. Quando esse objeto passa pelo seu código, três caminhos de vazamento se abrem sozinhos. O primeiro é o ORM: alguém faz $pedido->fill($request->all()) e qualquer campo com nome parecido vai para o banco. O segundo é o log: o handler de erro serializa o request inteiro "para ajudar no debug". O terceiro é a fila: o job de pagamento recebe o payload completo e o broker guarda a mensagem em disco.
A partir do momento em que um número de cartão completo está gravado em qualquer um desses lugares, o seu sistema passa a armazenar dados de cartão. Isso puxa as exigências do padrão PCI DSS para o banco, para o servidor de log, para o backup e para quem tem acesso a eles. O padrão também proíbe guardar o código de segurança depois da autorização, em qualquer hipótese. Você ganhou um dever que não pediu, por causa de uma coluna "para refazer".
E a coluna nem resolve o problema que motivou a ideia. Cobrança recorrente ou "refazer pagamento" se faz com o recurso do próprio provedor: token de cartão salvo no cofre dele, assinatura ou novo link de pagamento. O provedor guarda o cartão porque é o negócio dele e ele tem a certificação para isso.
O que o pedido precisa guardar de verdade
Pense no exemplo da loja que vende cursos e cobra pelo SDK em PHP. Para operar, atender o suporte e conciliar o financeiro, ela precisa de pouco:
- Identificador da transação no provedor, que é a chave para consultar, estornar e conciliar.
- Uma referência sua enviada ao provedor (o id do pedido), para casar o retorno com a linha certa.
- Status e data da última mudança, vindos do provedor, não da tela.
- Valor em centavos e moeda, para conferir com o que o provedor diz ter cobrado.
- Bandeira e últimos quatro dígitos, se o provedor devolver, para o atendimento identificar o cartão sem conhecê-lo.
- Ambiente (sandbox ou produção), para nunca misturar teste com cobrança real.
CREATE TABLE pagamentos (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
pedido_id BIGINT NOT NULL,
ambiente ENUM('sandbox','producao') NOT NULL,
provedor_tx_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
valor_centavos INT NOT NULL,
moeda CHAR(3) NOT NULL DEFAULT 'BRL',
bandeira VARCHAR(20) NULL,
ultimos4 CHAR(4) NULL,
atualizado_em DATETIME NOT NULL,
UNIQUE KEY uk_tx (ambiente, provedor_tx_id)
);
Repare no que não existe: nenhuma coluna de número, validade ou código. Se alguém precisar de um campo desses no futuro, a revisão do schema já vira a conversa certa. Valor em centavos evita o arredondamento de ponto flutuante; o porquê está em dinheiro em centavos.
Como integrar o SDK sem deixar rastro do cartão
- Use o fluxo do provedor para capturar o cartão. Checkout hospedado, campos seguros ou tokenização no navegador com o script do provedor. O seu PHP recebe um token ou um hash, não o número.
- Monte o payload do SDK só com o que ele precisa. Nada de repassar
$_POSTinteiro. Liste campo por campo. - Grave só a resposta útil. Id da transação, status, bandeira, últimos quatro dígitos. Descarte o resto da resposta em vez de guardar o JSON cru.
- Passe tudo que vai para log por um filtro. Chaves sensíveis viram
[removido]e qualquer sequência longa de dígitos é mascarada. - Separe as credenciais por ambiente. A chave do sandbox e a de produção vêm de variáveis diferentes, e o banco de teste não é o mesmo da produção.
- Concilie pelo retorno do provedor. O status oficial chega pela notificação ou pela consulta à API, nunca pelo "obrigado" da tela.
Filtro de log que pega o que escapou
Mesmo com o fluxo certo, alguém vai logar um array grande algum dia. O filtro abaixo remove chaves conhecidas e mascara sequências de 13 a 19 dígitos, que é a faixa de tamanho de números de cartão.
<?php
function limparParaLog(array $dados): array
{
$proibidas = ['card_number', 'cardnumber', 'numero_cartao', 'cvv', 'cvc',
'security_code', 'expiration', 'validade', 'token', 'senha'];
foreach ($dados as $chave => $valor) {
if (in_array(strtolower((string) $chave), $proibidas, true)) {
$dados[$chave] = '[removido]';
} elseif (is_array($valor)) {
$dados[$chave] = limparParaLog($valor);
} elseif (is_string($valor)) {
$dados[$chave] = preg_replace('/\b\d{13,19}\b/', '[digitos-mascarados]', $valor);
}
}
return $dados;
}
error_log(json_encode(limparParaLog($respostaDoProvedor)));
O filtro é rede de segurança, não estratégia. A estratégia é o número nunca chegar ao PHP. Para o restante do que não pode ir para log (senha, token, documento), veja log sem senha.
Sandbox e produção sem mistura
O erro clássico é usar o mesmo banco para os dois, trocando só a chave no arquivo de configuração. Um pedido de teste aprovado no sandbox vira "pago" no relatório real, ou pior, a chave de produção vai parar no servidor de homologação e cobra cartão de verdade de quem estava testando.
<?php
$ambiente = getenv('PAGAMENTO_AMBIENTE'); // 'sandbox' ou 'producao'
if (!in_array($ambiente, ['sandbox', 'producao'], true)) {
throw new RuntimeException('PAGAMENTO_AMBIENTE invalido');
}
$credencial = getenv($ambiente === 'producao'
? 'PAGAMENTO_TOKEN_PRODUCAO'
: 'PAGAMENTO_TOKEN_SANDBOX');
A coluna ambiente na tabela completa a separação: o relatório financeiro filtra ambiente = 'producao' e nenhum teste contamina o fechamento do mês.
Conciliação: o status vem do provedor, não do botão
A tela de "pagamento aprovado" é só o navegador da pessoa voltando para o seu site. Ela pode fechar a aba antes, pode recarregar, pode forjar o retorno. O status que vale é o que o provedor informa. Alguns provedores enviam uma notificação com um código e você consulta a transação na API deles; outros mandam o evento completo com assinatura. Nos dois casos, a regra é a mesma: confira a origem (assinatura ou consulta autenticada), localize o pagamento pelo id da transação e atualize o status.
Essa rota de notificação precisa ser idempotente: o provedor pode reenviar o mesmo aviso várias vezes. O guia de webhook com segredo mostra como validar a assinatura, e o de idempotência no pagamento mostra como não cobrar nem liberar duas vezes.
Erros comuns
- Guardar o número "para refazer a cobrança". Coloca o banco inteiro, o backup e o servidor de log dentro do escopo do PCI DSS, e o recurso de recorrência do provedor já resolve o caso.
- Logar o request ou a resposta crua do SDK. O cartão passa a morar no arquivo de log, que costuma ter retenção longa e acesso amplo.
- Usar
fill($request->all())no pedido. Qualquer campo com nome parecido com coluna existente é gravado sem ninguém notar. - Mesmo banco para sandbox e produção. Pedido de teste aparece como venda real e a conciliação do mês não fecha.
- Liberar o produto no retorno do navegador. Quem manipula a URL de retorno recebe o curso sem pagar.
- Esquecer a fila e o backup. Você limpa a tabela, mas o payload antigo continua no broker e nos dumps dos últimos meses.
Checklist
- Nenhuma tabela tem coluna de número de cartão, validade ou código de segurança.
- Uma busca por sequências de 13 a 19 dígitos nos logs do último mês não encontra número de cartão.
- O payload enviado ao SDK é montado campo a campo, sem repassar o request inteiro.
- Sandbox e produção usam credenciais e bancos diferentes, e a tabela registra o ambiente.
- O status do pagamento só muda pela notificação conferida ou pela consulta à API do provedor.
- A rota de notificação aguenta o mesmo aviso repetido sem duplicar efeito.
- Filas e backups antigos foram revisados depois de qualquer limpeza.
Os requisitos detalhados vêm do padrão PCI DSS e do contrato com o seu provedor; este guia não substitui a avaliação de um profissional de conformidade. Agora abra o log do último pagamento processado em produção: ele contém algum dígito do cartão além dos quatro últimos?
Referência: https://github.com/pagseguro/pagseguro-php-sdk