Programação Por Equipe DevBras

Capa do artigo: SDK de pagamento integrado: o que gravar no pedido, no log e no sandbox (sem o cartão)

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

  1. 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.
  2. Monte o payload do SDK só com o que ele precisa. Nada de repassar $_POST inteiro. Liste campo por campo.
  3. 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.
  4. Passe tudo que vai para log por um filtro. Chaves sensíveis viram [removido] e qualquer sequência longa de dígitos é mascarada.
  5. 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.
  6. 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?

Avalie este artigo

Referência: https://github.com/pagseguro/pagseguro-php-sdk