Skip to content

PHPAY-95: Efí — API Pix com mTLS, de 1 para 4 capacidades - #96

Merged
mariolucasdev merged 7 commits into
developfrom
feat/phpay-95
Sep 21, 2026
Merged

mariolucasdev merged 7 commits into
developfrom
feat/phpay-95

Conversation

@mariolucasdev

Copy link
Copy Markdown
Collaborator

Closes #95.

O que muda

O Efí passa de 1 para 4 capacidades com a API Pix, que roda em outro host e só aceita mTLS:

Capacidade Recurso Endpoints
Chaves Pix pix() /v2/gn/evp: criar, listar e remover EVP
Webhooks webhook() /v2/webhook/:chave, um por chave, com skipMtlsChecking()
Assinaturas subscription() Pix Automático: /v2/rec, /v2/locrec e /v2/cobr
pixCharge() cob (imediata) e cobv (com vencimento), QR Code e devolução

Clientes continuam fora, porque nenhuma das duas APIs do Efí mantém cadastro de cliente.

$efi = new EfiGateway($id, $secret, certificate: '/caminho/certificado.p12');

$cobranca = $efi->pixCharge()
    ->setAmount(Money::reais('123,45'))
    ->setKey('sua-chave-pix')
    ->setCustomer(new Customer('Mário Lucas', '12345678909'))
    ->create();

$efi->pixCharge()->qrCode($cobranca['loc']['id'])['qrcode'];   // copia e cola

Commits

  1. feat(money): Money::toDecimal() devolve "123.45", o formato do padrão Pix do BACEN, montado com aritmética inteira.
  2. feat(http): PHPay\Http\Certificate, genérico, porque Inter, BB, Itaú, Sicoob e Sicredi usam o mesmo esquema de mTLS. Aceita .p12/.pem, fromBase64() para container e serverless (temporário 0600, apagado ao fim do processo), e __debugInfo() mascara a senha. O HasHttpClient ganha patch() e headers no put().
  3. fix(efi): o token da API de Cobranças ficava em cache para sempre. Um gateway vivo num worker de fila passava a tomar 401 quando o Efí o expirava, e agora o expires_in é respeitado.
  4. feat(efi): a API Pix em si.
  5. doc(efi): README, CLAUDE.md, examples/efi/pix.php e .gitignore para certificados.
  6. refactor(efi): @phpstan-assert no validador de webhook, no lugar de @var inline.

Decisões que merecem revisão

  • pixCharge() é um extra do gateway concreto, como o webhookDeliveries() do Pagar.me. O charge() da facade já é o boleto, e mudar o retorno dele quebraria a v2.
  • Na API Pix, valor só como Money, sem int. Ela quer reais e a API de Cobranças do mesmo gateway quer centavos, então aceitar número cru criaria exatamente a ambiguidade que o Money existe para eliminar. Como a API é nova, não há compatibilidade a manter.
  • Guzzle ^7.0^7.3, a primeira versão que entrega .p12 ao cURL pela extensão. Não uso CURLOPT_SSLCERTTYPE em curl porque o Guzzle recente recusa opção cURL que conflite com as dele. É um piso de dependência que sobe; convém citar na release.
  • O certificado só é exigido quando o recurso monta o próprio client. Sem ele, a API de Cobranças continua funcionando e a API Pix responde com uma ValidationException clara, em vez de uma falha de handshake TLS.

Fontes

  • Rotas, métodos e hosts: SDK oficial efipay/sdk-php-apis-efi, arquivo src/Efi/Endpoints/Pix.php
  • Payloads: exemplos do mesmo SDK, em examples/pix/
  • Status de cancelamento (REMOVIDA_PELO_USUARIO_RECEBEDOR, CANCELADA), periodicidades e campos obrigatórios do Pix Automático: especificação do BACEN bacen/pix-api, arquivo openapi.yaml

Como testar

composer test roda 331 testes, 910 asserções, todos com HTTP mockado. Os 56 testes novos cobrem:

  • que o client montado apresenta o certificado e o Bearer (inspecionando a config do Guzzle), e que a autorização usa Basic + mTLS no host certo de cada ambiente
  • que os tokens das duas APIs são separados, reaproveitados e renovados ao expirar
  • os payloads de cob, cobv, rec e cobr no formato do BACEN, e as validações que barram antes de qualquer chamada HTTP

Ainda não foi exercitado contra o sandbox real: o teste mockado prova o payload que decidimos, não que o Efí o aceita. Para isso é preciso o certificado de homologação da conta. examples/efi/pix.php já está pronto para esse teste.

Ficou de fora

Pix Automático pela jornada 1 (solicrec), webhooks de recorrência (webhookrec, webhookcobr), envio de Pix e split. Está registrado no CLAUDE.md.

Checklist

  • composer test passa (Pint, Pest e PHPStan nível 9)
  • Cobri a mudança com testes, usando HTTP mockado (nenhum teste acessa a rede)
  • Atualizei README, CLAUDE.md ou UPGRADE.md, se a mudança afeta quem usa a biblioteca
  • Nenhuma credencial, token ou dado pessoal real foi commitado
  • Li e aceito o Contributor License Agreement

Nada quebra: o certificado e o pixClient são parâmetros novos e opcionais no fim do construtor.

A API Pix do BACEN quer o valor em reais como string com ponto e duas casas ("123.45"), que não é nem toReais() (float) nem toCentavos() (int). Montado com aritmética inteira, sem float no caminho.
O padrão do BACEN para API Pix exige mTLS em toda requisição, inclusive na de token. A Efí é a primeira a precisar disso aqui, e Inter, BB, Itaú, Sicoob e Sicredi usam o mesmo esquema, por isso o certificado mora em Http e não dentro de um gateway.

- Certificate valida existência e formato (.p12/.pem, que o Guzzle entrega ao cURL pela extensão)
- fromBase64() para containers e serverless, gravando num temporário 0600 removido ao fim do processo
- __debugInfo() mascara a senha
- HasHttpClient ganha patch() e headers por requisição no put()
O token ficava em cache para sempre, mas a Efí o expira (expires_in). Um gateway vivo num worker de fila passava a tomar 401 depois de alguns minutos. Agora o prazo anunciado é respeitado, com a mesma margem de 30s da Rede. Sem expires_in, o comportamento antigo se mantém.
…ico e cobrança Pix

A Efí tem uma segunda API, a API Pix, em outro host (pix.api.efipay.com.br / pix-h.api.efipay.com.br) e só por mTLS. Com ela o gateway vai de 1 para 4 capacidades:

- SupportsPixKeys: chaves aleatórias (EVP) em /v2/gn/evp
- SupportsWebhooks: um webhook por chave Pix, com skipMtlsChecking() para servidor que não valida o certificado da Efí
- SupportsSubscriptions: Pix Automático, com a recorrência (/v2/rec), o location da jornada de QR Code (/v2/locrec) e a cobrança de cada ciclo (/v2/cobr)
- pixCharge(): cobrança imediata (cob) e com vencimento (cobv), QR Code e devolução. É um extra do gateway concreto, porque charge() já é o boleto e mudar o retorno quebraria a v2

Decisões:

- O mesmo EfiGateway, com as mesmas credenciais. O certificado entra como parâmetro opcional novo (caminho ou Certificate), então nada quebra
- Cada API tem o seu token, em cache e renovado ao expirar
- Valor só como Money: a API Pix quer reais ("123.45") e a de Cobranças do mesmo gateway quer centavos, então número cru aqui seria justamente a ambiguidade que o Money existe para eliminar
- O certificado só é exigido quando a biblioteca monta o próprio client. Sem ele, o erro é claro em vez de uma falha de handshake TLS
- Guzzle ^7.3, a primeira versão que entrega .p12 ao cURL pela extensão

Rotas e métodos conferidos no SDK oficial (efipay/sdk-php-apis-efi, src/Efi/Endpoints/Pix.php). Status de cancelamento, periodicidades e campos obrigatórios conferidos na especificação do BACEN (bacen/pix-api, openapi.yaml).
- README: o Efí sobe para 4 capacidades nas tabelas, ganha uma seção com as duas APIs, o certificado (caminho, senha e base64) e exemplos de cobrança Pix, chaves, webhooks e Pix Automático. O exemplo de gateway restrito em Capacidades passa a ser a Rede
- CLAUDE.md: as particularidades do Efí, uma seção de mTLS e os itens que ficaram de fora (jornada 1, webhookrec/webhookcobr, envio de Pix). A visão geral também passa a citar o Woovi, que estava faltando
- examples/efi/pix.php, com CERTIFICATE e PIX_KEY no credentials.example.php
- .gitignore passa a ignorar certificados (.p12, .pfx, .pem)
… @phpstan-assert

Tira o @var inline do Webhook::create(): quem valida é quem garante o tipo, e o PHPStan passa a saber disso pelo contrato do validador.
@mariolucasdev
mariolucasdev merged commit 9c63f59 into develop Sep 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Efí: API Pix com mTLS — de 1 para 4 capacidades

1 participant