From 36242dc0f3e40cac028f16a08201867b78e9db16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Lucas?= Date: Thu, 22 Oct 2026 11:42:52 -0300 Subject: [PATCH 1/4] PHPAY-113: fix(doc): close the code fence that swallowed the Rede section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PHP block in the Cielo recurrence example was never closed. It came in with the Cielo gateway (PHPAY-83), which appended three lines to the end of that example and left the closing fence out. The damage was not limited to one block: fences toggle, so from that point the whole document was inverted. The `### Rede` heading and the prose under it rendered as code, the real examples after it rendered as prose, and everything down through AbacatePay, Woovi and Efí was scrambled on GitHub — which is the second half of the README of a package being downloaded 593 times a month. Found while mapping the file to split it: a parity check counted 113 fences, which cannot be right, and the heading map showed `### Rede` sitting inside a block. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f33a6e9..3d604a3 100644 --- a/README.md +++ b/README.md @@ -889,6 +889,8 @@ $id = $recorrencia['Payment']['RecurrentPayment']['RecurrentPaymentId']; $phpay->updateAmount($id, 19900); $phpay->deactivate($id); $phpay->reactivate($id); +``` + ### Rede Adquirente, e a forma mais estreita da biblioteca: **só From 7a8044e4332c6d3effa91af69ba8d561eb8b85dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Lucas?= Date: Thu, 22 Oct 2026 11:46:16 -0300 Subject: [PATCH 2/4] PHPAY-113: fix(doc): point the SUPPORT link at the repository README From inside .github/, ./README.md resolves to .github/README.md, which does not exist, so the first link of the support page was dead. Found by the link checker written for the split. --- .github/SUPPORT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 61102d1..7d7b166 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -8,7 +8,7 @@ Se você encontrar problemas, dúvidas ou precisa de ajuda, siga estas etapas: 1. **Consulte a documentação**: - - Leia o [README.md](./README.md) do repositório para instruções de uso e soluções comuns. + - Leia o [README.md](../README.md) do repositório para instruções de uso e soluções comuns. 2. **Pesquise nas issues**: From 3ce16238d1853b28357d875332a566d8cb36b5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Lucas?= Date: Thu, 22 Oct 2026 11:46:16 -0300 Subject: [PATCH 3/4] PHPAY-113: doc: split the README into docs/, one page per concept and per gateway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README was 1338 lines and 46 KB. Someone arriving from Packagist had to scroll past 500 lines of concepts to reach the gateway they use, and nine gateways shared one page. It is now 308 lines and 11 KB: what it is, the capability matrix, requirements, install, quickstart, and an index. 1036 lines moved into 16 files plus an index, nothing rewritten in the move: seven concept pages in docs/, nine gateway pages in docs/gateways/, with headings promoted — a section that was ### became # in its own file, and its #### became ##. Five anchor links became relative paths, each resolved against where the source file ended up: README to docs/gateways/efi.md, docs/valores-monetarios.md to gateways/efi.md#api-pix, and so on. A checker extracts every relative link and anchor from the 29 markdown files of the repository, confirms the file exists and that some heading produces that anchor, and reports 67 links with none broken. Two things did change text. The capability matrix gained its sixth column: webhook signature shipped in v2.3.0 and the table still showed five, so a reader was told the library did less than it does. And the two section intros that said "three things" and pointed at an anchor that no longer exists were replaced by the index. --- CLAUDE.md | 2 +- README.md | 1113 ++----------------------------- docs/README.md | 27 + docs/ambientes-e-credenciais.md | 35 + docs/capacidades.md | 41 ++ docs/cliente.md | 43 ++ docs/erros.md | 32 + docs/gateways/abacatepay.md | 51 ++ docs/gateways/asaas.md | 154 +++++ docs/gateways/cielo.md | 61 ++ docs/gateways/efi.md | 152 +++++ docs/gateways/mercado-pago.md | 51 ++ docs/gateways/pagarme.md | 64 ++ docs/gateways/pagbank.md | 55 ++ docs/gateways/rede.md | 49 ++ docs/gateways/woovi.md | 56 ++ docs/retry-e-idempotencia.md | 65 ++ docs/valores-monetarios.md | 56 ++ docs/webhooks.md | 47 ++ 19 files changed, 1084 insertions(+), 1070 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/ambientes-e-credenciais.md create mode 100644 docs/capacidades.md create mode 100644 docs/cliente.md create mode 100644 docs/erros.md create mode 100644 docs/gateways/abacatepay.md create mode 100644 docs/gateways/asaas.md create mode 100644 docs/gateways/cielo.md create mode 100644 docs/gateways/efi.md create mode 100644 docs/gateways/mercado-pago.md create mode 100644 docs/gateways/pagarme.md create mode 100644 docs/gateways/pagbank.md create mode 100644 docs/gateways/rede.md create mode 100644 docs/gateways/woovi.md create mode 100644 docs/retry-e-idempotencia.md create mode 100644 docs/valores-monetarios.md create mode 100644 docs/webhooks.md diff --git a/CLAUDE.md b/CLAUDE.md index 0e82d60..de173bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -316,7 +316,7 @@ a notificação veio do gateway é `SupportsWebhookSignature`, e são coisas dif header, então o middleware não o vê. - `SupportsWebhookSignature` fica em cinco gateways: Asaas, PagBank, Mercado Pago, Woovi e AbacatePay. Efí (mTLS), Cielo, Rede e Pagar.me ficam fora, cada um pelo - seu motivo, e os motivos estão no README. O HMAC-SHA1 da Woovi é aceito, mas é + seu motivo, e os motivos estão em `docs/webhooks.md`. O HMAC-SHA1 da Woovi é aceito, mas é depreciado pela própria Woovi. - `SupportsWebhooks` e `SupportsPixKeys` só no Asaas, no Woovi e na Efí — os três são PSP. Mercado Pago, PagBank e Pagar.me registram endpoints por painel, e Pix neles é diff --git a/README.md b/README.md index 3d604a3..03a4833 100644 --- a/README.md +++ b/README.md @@ -21,23 +21,7 @@ - [Requisitos](#requisitos) - [Instalação](#instalação) - [Início rápido](#início-rápido) -- [Conceitos](#conceitos) - - [Capacidades](#capacidades) - - [Tratamento de erros](#tratamento-de-erros) - - [Retry e idempotência](#retry-e-idempotência) - - [Ambientes e credenciais](#ambientes-e-credenciais) - - [Unidade monetária](#unidade-monetária) - - [Verificando o webhook que chega](#verificando-o-webhook-que-chega) -- [Gateways](#gateways) - - [Asaas](#asaas) - - [Mercado Pago](#mercado-pago) - - [PagBank](#pagbank) - - [Pagar.me](#pagarme) - - [Cielo](#cielo) - - [Rede](#rede) - - [AbacatePay](#abacatepay) - - [Woovi/OpenPix](#wooviopenpix) - - [Efí](#efí) +- [Documentação](#documentação) - [Exemplos executáveis](#exemplos-executáveis) - [Migrando da v1](#migrando-da-v1) - [Roadmap](#roadmap) @@ -73,34 +57,42 @@ Trocar de gateway é trocar a linha do construtor. ## Gateways suportados -| Gateway | Clientes | Cobranças | Assinaturas | Webhooks | Chaves Pix | -| --- | :---: | :---: | :---: | :---: | :---: | -| **Asaas** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Woovi/OpenPix** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Efí** | — | ✅ | ✅ | ✅ | ✅ | -| **Mercado Pago** | ✅ | ✅ | ✅ | — | — | -| **PagBank** | ✅ | ✅ | ✅ | — | — | -| **Pagar.me** | ✅ | ✅ | ✅ | — | — | -| **AbacatePay** | ✅ | ✅ | — | — | — | -| **Cielo** | — | ✅ | ✅ | — | — | -| **Rede** | — | ✅ | — | — | — | +| Gateway | Clientes | Cobranças | Assinaturas | Webhooks | Webhook assinado | Chaves Pix | +| --- | :---: | :---: | :---: | :---: | :---: | :---: | +| **Asaas** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Woovi/OpenPix** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Efí** | — | ✅ | ✅ | ✅ | — | ✅ | +| **Mercado Pago** | ✅ | ✅ | ✅ | — | ✅ | — | +| **PagBank** | ✅ | ✅ | ✅ | — | ✅ | — | +| **Pagar.me** | ✅ | ✅ | ✅ | — | — | — | +| **AbacatePay** | ✅ | ✅ | — | — | ✅ | — | +| **Cielo** | — | ✅ | ✅ | — | — | — | +| **Rede** | — | ✅ | — | — | — | — | As interfaces correspondentes são `SupportsCustomers`, `SupportsCharges`, -`SupportsSubscriptions`, `SupportsWebhooks` e `SupportsPixKeys`. +`SupportsSubscriptions`, `SupportsWebhooks`, `SupportsWebhookSignature` e +`SupportsPixKeys`. -Duas colunas merecem explicação, porque a ausência de ✅ **não** quer dizer que o -gateway não aceita Pix ou não manda webhook: +Três colunas merecem explicação, porque a ausência de ✅ **não** quer dizer que o +gateway não aceita Pix, não manda webhook ou não assina nada: - **`SupportsPixKeys`** significa *gerenciar chaves Pix e QR Code estático*, o que só um PSP que emite chave própria oferece. Nos outros gateways, Pix é forma de pagamento de uma cobrança — e todos aceitam. As duas metades não andam juntas em todo PSP: o QR Code estático está no Asaas e na Woovi, que - expõem endpoint para ele, e **não** na [Efí](#efí), cuja API Pix não tem + expõem endpoint para ele, e **não** na [Efí](docs/gateways/efi.md), cuja API Pix não tem essa rota. - **`SupportsWebhooks`** significa *cadastrar endpoints pela API*. Nos outros, o cadastro é no painel; a notificação vai por cobrança, no campo `notification_url`. O Pagar.me ainda deixa **consultar e reenviar entregas**, - através de [`webhookDeliveries()`](#consultando-entregas-de-webhook). + através de [`webhookDeliveries()`](docs/gateways/pagarme.md#consultando-entregas-de-webhook). +- **`SupportsWebhookSignature`** significa *provar que a notificação recebida + veio do gateway*. É outra coisa que cadastrar endpoint, e as duas não andam + juntas: Mercado Pago, PagBank e AbacatePay assinam sem deixar cadastrar pela + API, enquanto Efí, Cielo, Rede e Pagar.me não dão o que verificar — a Efí + notifica por mTLS, Cielo e Rede não mandam assinatura, e a v5 do Pagar.me não + documenta esquema. O motivo de cada um está em + [Verificando o webhook que chega](docs/webhooks.md). --- @@ -156,1052 +148,35 @@ try { ``` Um array devolvido é **sempre** uma resposta de sucesso. Qualquer falha vira -exceção — veja [Tratamento de erros](#tratamento-de-erros). +exceção — veja [Tratamento de erros](docs/erros.md). --- -## Conceitos - -Três coisas valem entender uma vez; depois todo gateway se comporta igual. - -### Capacidades - -`GatewayInterface` carrega só a identidade do gateway. Cada recurso é uma -interface que o gateway implementa **se, e só se,** oferecer: - -```php -use PHPay\Contracts\Capability; - -$phpay = PHPay::gateway(new RedeGateway(REDE_PV, REDE_TOKEN)); - -$phpay->name(); // 'Rede' -$phpay->supports(Capability::SUBSCRIPTIONS); // false -$phpay->capabilities(); // [Capability::CHARGES] -``` - -Chamar um recurso que o gateway não oferece lança uma exceção que diz o que ele -oferece: - -```php -$phpay->pix(); -// NotImplementedException: Rede não suporta chaves Pix. -// Capacidades disponíveis: cobranças. -``` - -Se você segurar o **gateway concreto** em vez da facade, o erro sobe para tempo -de análise — o PHPStan acusa que o método não existe naquele tipo: - -```php -$rede = new RedeGateway(REDE_PV, REDE_TOKEN); - -$rede->charge(); // ✅ -$rede->pix(); // ❌ o método não existe nesse gateway -``` - -Para injeção de dependência, tipe a capacidade em vez do gateway: - -```php -use PHPay\Contracts\SupportsCharges; - -public function __construct(private SupportsCharges $gateway) {} -``` - -### Tratamento de erros - -Toda exceção da biblioteca implementa `PHPay\Exceptions\PHPayException`, então -um `catch` cobre a integração inteira: - -| Exceção | Estende | Quando acontece | -| --- | --- | --- | -| `ValidationException` | `InvalidArgumentException` | Payload inválido, **antes** de qualquer HTTP | -| `ApiException` | `RuntimeException` | O gateway recusou, ou está inacessível | -| `NotImplementedException` | `BadMethodCallException` | O gateway não oferece o recurso | - -```php -use PHPay\Exceptions\ApiException; -use PHPay\Exceptions\PHPayException; -use PHPay\Exceptions\ValidationException; - -try { - $cobranca = $phpay->charge()->setCharge($dados)->create(); -} catch (ValidationException $e) { - // payload inválido: nenhuma requisição foi feita -} catch (ApiException $e) { - $e->getStatusCode(); // 400, 401, 404… ou 0 se nem chegou ao gateway - $e->getResponse(); // corpo devolvido pelo gateway - $e->getGateway(); // 'Asaas', 'Pagar.me', … - $e->isConnectionError(); // true em timeout, DNS, TLS — vale retry -} catch (PHPayException $e) { - // qualquer outra falha do PHPay -} -``` - -`ApiException` já resume os formatos de erro de cada gateway, então -`getMessage()` traz a descrição legível, não um dump. +## Documentação -### Retry e idempotência +A documentação completa está em [`docs/`](docs/). O que vale para todos os +gateways está nos conceitos; cada página de gateway cobre só o que é +específico dele. -A biblioteca repete uma requisição **só quando repetir não pode cobrar de novo**. -Vale para os dez gateways, porque o middleware vive na -`PHPay\Http\ClientFactory`, o único lugar onde um client nasce. +**Conceitos** -| Situação | Repete | +| | | | --- | --- | -| Erro antes de a requisição sair (DNS, connect, handshake TLS) | sempre — nada chegou ao gateway | -| Timeout sem conexão estabelecida | sempre — nada chegou | -| Timeout com conexão estabelecida | só leitura, ou com chave de idempotência | -| `429` | sempre, respeitando `Retry-After` | -| `5xx` | só leitura, ou com chave de idempotência | -| `4xx` fora do 429 | nunca — o gateway entendeu e recusou | - -Leitura é `GET`, `HEAD` e `OPTIONS`. `PUT`, `PATCH` e `DELETE` **não** são -repetidos, apesar de o HTTP chamar `PUT` e `DELETE` de idempotentes: a -documentação do Asaas pede para -[não tratar a exclusão de cobrança como idempotente](https://docs.asaas.com/reference/excluir-cobranca) -sem conferir a resposta, e esse aviso vale mais que a teoria. - -São 3 tentativas no total — 200ms, 400ms, teto de 2s, com jitter. Se o gateway -mandar um `Retry-After` maior que o teto, a biblioteca **desiste** em vez de -repetir antes da janela pedida, porque repetir cedo só seria recusado de novo. -E lembre que retry multiplica o pior caso de tempo de parede: 3 tentativas com -`timeout` de 60s é o limite superior. - -Para mudar a política, passe uma à fábrica: - -```php -use PHPay\Http\{ClientFactory, RetryPolicy}; - -$maisTentativas = ClientFactory::make(['base_uri' => $uri], new RetryPolicy(5)); -$semRetry = ClientFactory::make(['base_uri' => $uri], RetryPolicy::disabled()); -``` - -#### Chave de idempotência +| [Capacidades](docs/capacidades.md) | o que cada gateway declara suportar, e o que acontece quando você pede o que ele não tem | +| [Tratamento de erros](docs/erros.md) | array é sucesso, falha é exceção — e qual exceção | +| [Retry e idempotência](docs/retry-e-idempotencia.md) | o que a biblioteca repete, o que ela recusa a repetir, e por quê | +| [Ambientes e credenciais](docs/ambientes-e-credenciais.md) | sandbox e produção em cada gateway, e como a credencial fica fora de dump | +| [Cliente](docs/cliente.md) | o VO `Customer`, porque o mesmo campo tem seis grafias entre os gateways | +| [Valores monetários](docs/valores-monetarios.md) | `Money`, porque reais e centavos não se misturam | +| [Verificando o webhook que chega](docs/webhooks.md) | provar que a notificação veio do gateway | -Com uma chave, o `POST` passa a ser repetível: o gateway reconhece a segunda -requisição como a mesma operação e devolve o resultado da primeira em vez de -cobrar outra vez. +**Gateways** -| Gateway | Header | Como | +| | | | | --- | --- | --- | -| Mercado Pago | `X-Idempotency-Key` | sempre enviada — a API de pagamentos exige | -| Pagar.me | `Idempotency-Key` | você informa; vale 24h | -| PagBank | `x-idempotency-key` | você informa; vale 48h | - -```php -$pedido = $pagbank->charge() - ->setCustomer($cliente) - ->addItem('Assinatura PHPay', 10050) - ->setIdempotencyKey($idDoSeuPedido) - ->create(); -``` - -**Use uma chave sua, guardada junto do registro pendente.** Chave gerada pela -biblioteca cobre só as tentativas de dentro da mesma chamada — o retry reenvia -a mesma requisição, com o mesmo header. O retry do seu lado, em outro processo -ou em outra chamada a `create()`, chega com chave nova e cobra de novo. - -Os outros gateways não documentam chave de idempotência. Neles um `POST` só é -repetido quando o erro provou que a requisição não saiu da máquina — e no Asaas -a orientação da própria documentação é -[consultar a cobrança antes de repetir](https://docs.asaas.com/docs/cobrancas-via-cartao-de-credito). - ---- - -### Ambientes e credenciais - -Os gateways discordam sobre como separar teste de produção, e o PHPay segue o -que cada um faz em vez de inventar um padrão: - -| Gateway | Como o ambiente é decidido | -| --- | --- | -| **Asaas** | `$sandbox` no construtor — troca a URL | -| **PagBank** | `$sandbox` no construtor — troca a URL | -| **Cielo** | `$sandbox` no construtor — troca **as duas** URLs | -| **Efí** | `$sandbox` no construtor — troca **as duas** URLs (Cobranças e Pix) | -| **Rede** | `$sandbox` no construtor — troca **as duas** URLs e o caminho do token | -| **Mercado Pago** | Prefixo do token (`TEST-`); host único, sem `$sandbox` | -| **Pagar.me** | Prefixo da chave (`sk_test_`); host único, sem `$sandbox` | -| **Woovi** | `$sandbox` no construtor — o sandbox tem **domínio próprio** | -| **AbacatePay** | Pela chave usada; host único, **sem prefixo** — a cobrança informa em `devMode` | - -Nos dois últimos, `isSandbox()` diz em qual ambiente você está: - -```php -(new MercadoPagoGateway($token))->isSandbox(); -(new PagarMeGateway($secretKey))->isSandbox(); -``` - -> **Nunca** versione credenciais. Os arquivos `examples/*/credentials.php` são -> ignorados pelo git por padrão. - -`var_dump()` e `print_r()` de um gateway ou recurso mostram `********` no lugar -da credencial, e o client HTTP, que a carrega nos headers, aparece oculto. O -`dd()` do Symfony VarDumper lê as propriedades por conta própria e **não** passa -por essa máscara: não despeje o gateway em log. - -Toda requisição tem prazo: 60 segundos no total e 10 para conectar. Sem isso, um -gateway que trava prenderia o worker indefinidamente. Um estouro de prazo chega -como `ApiException` com `isConnectionError()` verdadeiro. - -### Cliente - -O mesmo campo tem **seis grafias** entre os gateways: `cpfCnpj` no Asaas, -`tax_id` no PagBank, `document` no Pagar.me, `taxId` no AbacatePay, `taxID` no -Woovi, `cpf_cnpj` no Efí. Código escrito para um não migra para outro, e nada -no tipo avisa. - -`Customer` é a forma única. Cada gateway mapeia para o formato dele: - -```php -use PHPay\Support\Customer; - -$cliente = Customer::make( - name: 'Mário Lucas', - document: '123.456.789-01', // pontuação é limpa - email: 'fale@phpay.io', - phone: '(11) 94002-8922', -); - -$phpay->charge()->setCustomer($cliente); // funciona nos nove -``` - -Ele também carrega o que cada gateway deriva do cliente, e que antes ficava -espalhado: - -```php -$cliente->isIndividual(); // CPF: o Pagar.me precisa como type: 'individual' -$cliente->documentType(); // 'CPF' | 'CNPJ': a Cielo quer em IdentityType -$cliente->firstName(); // o Mercado Pago quer nome e sobrenome separados -$cliente->phoneParts(); // ['country' => '55', 'area' => '11', ...] para o PagBank -``` - -Para um cliente que já existe no gateway, ou para campos que só aquele gateway -tem: - -```php -$cliente->withId('cus_000006337812'); // reaproveita em vez de criar -$cliente->withExtra(['externalReference' => 'x']); // vai junto no payload -``` - -> O `withExtra()` existe para o que é específico de um gateway — endereço, -> data de nascimento, referência externa. **Nenhum campo obrigatório de -> nenhum dos nove gateways precisa dele**: o value object cobre todos. - -### Unidade monetária - -Os gateways discordam sobre a unidade, e **errar não quebra a integração — ela -cobra o valor errado**. Mandar `100.50` num gateway de centavos cobra R$ 1,00, -e você só descobre na conciliação. - -Use `Money` e o problema deixa de existir: você diz a unidade que tem, o -gateway pede a unidade que precisa, e nenhum dos dois pode errar. - -```php -use PHPay\Support\Money; - -$valor = Money::reais(100.50); // ou Money::centavos(10050) - -$asaas->charge()->setAmount($valor); // vira 100.50 -$pagbank->charge()->addItem('Item', $valor); // vira 10050 -``` - -Também aceita string, que é o que um campo de formulário costuma entregar: - -```php -Money::reais('100,50'); // notação brasileira -Money::reais('1.234,56'); // com separador de milhar -Money::reais('100.50'); // notação com ponto -``` - -E tem o que um total precisa: - -```php -$unitario = Money::reais(59.90); - -$unitario->multiply(2); // R$ 119,80 -$unitario->plus(Money::reais(10)); // R$ 69,90 -$unitario->format(); // 'R$ 59,90' -``` - -> `Money::reais(10 / 3)` **lança exceção** em vez de arredondar. Arredondamento -> silencioso é como nascem erros de um centavo na conciliação — arredonde você -> mesmo, ou use `Money::centavos()` para ser exato. - -#### Passando número cru - -Continua funcionando, e cada gateway lê na unidade que sempre esperou: - -| Gateway | Unidade do número cru | R$ 100,50 | -| --- | --- | --- | -| **Asaas**, **Mercado Pago** | Reais (decimal) | `100.50` | -| **PagBank**, **Pagar.me**, **Cielo**, **Rede**, **AbacatePay**, **Woovi**, **Efí** | Centavos (inteiro) | `10050` | - -Nos que usam centavos, o PHPay recusa decimal na validação. Mas é justamente -essa tabela que o `Money` torna desnecessária — **prefira o value object**. - -A exceção é a [API Pix do Efí](#api-pix), que **não aceita número cru, só -`Money`**. Ela quer reais (`"100.50"`) enquanto a API de Cobranças do mesmo -gateway quer centavos, e um número solto ali seria a ambiguidade que o value -object existe para eliminar. - ---- - -### Verificando o webhook que chega - -Cadastrar o endpoint é metade do caminho. A outra é provar que a notificação -veio do gateway: sem isso, quem descobrir a URL confirma uma cobrança. Cada -gateway assina de um jeito, e `webhookSignature()` dá a todos a mesma forma. - -```php -$ok = $gateway->webhookSignature() - ->setSecret($segredoDoWebhook) - ->setPayload(file_get_contents('php://input')) - ->setHeaders(getallheaders()) - ->verify(); - -if (!$ok) { - http_response_code(401); - exit; -} -``` - -`verify()` devolve `bool` — assinatura que não bate é resposta, não erro. O que -levanta `ValidationException` é uso errado: segredo ausente, corpo vazio ou -header de assinatura que não chegou. - -**O corpo precisa ser o cru.** `file_get_contents('php://input')`, nunca o array -decodificado e reserializado: um espaço a mais muda o hash. Em Laravel, -`$request->getContent()`. - -| Gateway | O que é verificado | O que isso prova | -| --- | --- | --- | -| Asaas | token no header `asaas-access-token` | que o remetente conhece o token — o corpo não é assinado | -| PagBank | `x-authenticity-token`: SHA-256 de `{token}-{corpo}` | que o corpo chegou inteiro | -| Mercado Pago | `x-signature`: HMAC-SHA256 de `data.id` + `x-request-id` + `ts` | que a notificação é da sua aplicação e aponta para aquele recurso | -| Woovi/OpenPix | `x-webhook-signature`: RSA-SHA256 do corpo, conferido com a chave pública da Woovi | que o corpo veio da Woovi | -| AbacatePay | `webhookSecret` na query e, com a chave, `X-Webhook-Signature` | que a URL é a sua e, com a chave, que o corpo não mudou | - -No Mercado Pago, informe o `data.id` da query com `setDataId()`: ele entra no -cálculo, e omiti-lo muda o manifesto. Na Woovi, a chave pública é buscada em -`api/v1/webhook/public-keys` na primeira verificação — passe `setPublicKeys()` -para evitar a chamada — e a lista inteira é aceita, porque durante uma rotação -ainda chegam notificações assinadas pela chave anterior. - -Quem **não** declara a capacidade: **Efí**, porque a notificação Pix chega por -mTLS e o certificado já cobre isso; **Cielo** e **Rede**, porque a notificação -não traz assinatura e o caminho que a própria documentação indica é consultar a -transação de volta; e **Pagar.me**, porque a v5 não documenta esquema de -assinatura. Nos quatro, `webhookSignature()` levanta `NotImplementedException` -em vez de devolver um verificador que sempre diz sim. - ---- - -## Gateways - -Cada seção cobre só o que é específico daquele gateway. Tudo que vale para -todos está em [Conceitos](#conceitos). - -### Asaas - -Um dos dois com as cinco capacidades, ao lado do Woovi. É PSP, então emite -chave Pix própria e gerencia webhooks por API. - -```php -use PHPay\Asaas\AsaasGateway; - -$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX)); -``` - -#### Cobranças - -```php -$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->charge(); - -/* cria a cobrança, criando também o cliente */ -$phpay->setCharge($cobranca)->setCustomer($cliente)->create(); - -/* reaproveita um cliente que já existe — evita cadastro duplicado */ -$phpay->setCharge($cobranca)->setCustomerId('cus_000006337812')->create(); - -$phpay->find($id); -$phpay->getAll(); -$phpay->setQueryParams(['limit' => 2])->getAll(); -$phpay->update($id, $dados); -$phpay->destroy($id); -$phpay->restore($id); - -$phpay->getStatus($id); -$phpay->getDigitableLine($id); -$phpay->getQrCodePix($id); - -$phpay->confirmReceipt($id, [ - 'paymentDate' => date('Y-m-d'), - 'value' => 100.00, - 'notifyCustomer' => true, -]); -$phpay->undoConfirmReceipt($id); -``` - -#### Clientes - -```php -$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX)); - -$cliente = $phpay->customer(['name' => 'Mário Lucas', 'cpfCnpj' => '12345678901'])->create(); - -$phpay->customer()->find($cliente['id']); -$phpay->customer()->setFilter(['cpfCnpj' => '12345678901'])->getAll(); -$phpay->customer(['name' => 'Novo Nome'])->update($cliente['id']); -$phpay->customer()->getNotifications($cliente['id']); -$phpay->customer()->destroy($cliente['id']); -$phpay->customer()->restore($cliente['id']); -``` - -#### Assinaturas - -A assinatura gera uma cobrança por ciclo, e cada uma é uma cobrança comum: -aparece em `getPayments()` e é tratada pelo recurso de cobrança. - -```php -use PHPay\Asaas\Enums\SubscriptionCycleEnum; - -$assinaturas = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->subscription(); - -$assinatura = $assinaturas - ->setCustomer($cliente) // ou setCustomerId('cus_...') - ->setAmount(Money::reais('49,90')) - ->setCycle(SubscriptionCycleEnum::MONTHLY) - ->setSubscription([ - 'billingType' => 'BOLETO', - 'nextDueDate' => '2026-10-10', - 'description' => 'Plano mensal', - ]) - ->create(); -``` - -> O `create([...])` com o payload inteiro continua funcionando, e o array -> sobrescreve o que os setters montaram. O `cycle` é obrigatório: sem ele o -> Asaas recusa, e o PHPay barra antes. - -Consulta e ciclo de vida: - -```php -$assinaturas->setQueryParams(['customer' => 'cus_...', 'status' => 'ACTIVE'])->getAll(); -$assinaturas->find($id); - -/* muda as próximas cobranças; com updatePendingPayments, as pendentes também */ -$assinaturas->update($id, ['description' => 'Plano anual', 'updatePendingPayments' => true]); - -/* pausa: para de gerar cobranças e mantém as que existem */ -$assinaturas->deactivate($id); -$assinaturas->reactivate($id, '2026-11-10'); // o Asaas exige um novo vencimento - -/* remove: as cobranças pendentes e vencidas vão junto; as pagas ficam */ -$assinaturas->destroy($id); -``` - -Cobranças, carnê e cartão: - -```php -$assinaturas->getPayments($id, ['status' => 'PENDING']); - -/* o carnê vem como os bytes do PDF */ -file_put_contents('carne.pdf', $assinaturas->paymentBook($id, month: 12, year: 2026)); - -/* troca o cartão sem cobrar — as cobranças pendentes passam para o novo */ -$assinaturas->updateCreditCard($id, [ - 'creditCardToken' => $token, // ou creditCard + creditCardHolderInfo - 'remoteIp' => $ipDoComprador, -]); -``` - -Nota fiscal emitida automaticamente para cada cobrança: - -```php -$assinaturas->createInvoiceSettings($id, [ - 'municipalServiceName' => 'Desenvolvimento de software', - 'effectiveDatePeriod' => 'ON_PAYMENT_CONFIRMATION', - 'taxes' => [ // os sete são obrigatórios; 0 quando não houver - 'retainIss' => false, - 'iss' => 2, - 'pis' => 0.65, - 'cofins' => 3, - 'csll' => 0, - 'inss' => 0, - 'ir' => 0, - ], -]); - -$assinaturas->getInvoiceSettings($id); -$assinaturas->updateInvoiceSettings($id, [...]); -$assinaturas->destroyInvoiceSettings($id); -$assinaturas->getInvoices($id); // as notas já emitidas -``` - -#### Webhooks e chaves Pix - -```php -$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX)); - -/* webhooks com CRUD completo — exclusividade do Asaas */ -$phpay->webhook(WEBHOOK)->create(); -$phpay->webhook()->getAll(); -$phpay->webhook()->update($id, $dados); -$phpay->webhook()->destroy($id); - -/* chaves Pix e QR Code estático */ -$chave = $phpay->pix()->createKey(); -$phpay->pix()->getAll(); -$phpay->pix()->staticQrCode(['addressKey' => $chave['key'], 'value' => 25.00]); -$phpay->pix()->destroy($chave['id']); -``` - -### Mercado Pago - -Ambiente pelo prefixo do token. `POST /v1/payments` exige o header -`X-Idempotency-Key`: o PHPay gera uma chave por chamada, e -`setIdempotencyKey()` deixa você fixar a sua — assim um retry da mesma operação -de negócio não cria duas cobranças. - -```php -use PHPay\MercadoPago\Enums\PaymentMethodEnum; -use PHPay\MercadoPago\MercadoPagoGateway; - -$gateway = new MercadoPagoGateway(ACCESS_TOKEN); - -$cobranca = PHPay::gateway($gateway)->charge() - ->setCharge([ - 'transaction_amount' => 100.50, - 'payment_method_id' => PaymentMethodEnum::PIX->value, - 'notification_url' => 'https://exemplo.test/webhook/mercadopago', - ]) - ->setPayer(['email' => 'comprador@exemplo.test']) - ->setIdempotencyKey('pedido-123456') - ->create(); - -$phpay->getPixCode($cobranca['id']); -``` - -Assinaturas usam `/preapproval`, com ou sem plano associado: - -```php -$phpay = PHPay::gateway($gateway)->subscription(); - -$phpay->setPayerEmail('comprador@exemplo.test')->create([ - 'reason' => 'Assinatura PHPay', - 'back_url' => 'https://exemplo.test/retorno', - 'auto_recurring' => [ - 'frequency' => 1, - 'frequency_type' => 'months', - 'transaction_amount' => 100.50, - 'currency_id' => 'BRL', - ], -]); - -/* com plano, a recorrência vem do plano */ -$phpay->setPayerEmail('comprador@exemplo.test') - ->setPlan('2c938084726fca480172750000000000') - ->create(['back_url' => 'https://exemplo.test/retorno']); -``` - -> O recurso `Customer` do Mercado Pago existe para cartões salvos — **não** é -> pré-requisito para cobrar, já que o pagamento carrega `payer.email` direto. A -> API também não oferece exclusão de cliente. - -### PagBank - -Duas particularidades, ambas resolvidas pela biblioteca. - -**Duas APIs em hosts diferentes.** Pedidos vivem em `api.pagseguro.com`, -assinaturas em `api.assinaturas.pagseguro.com`. Cada recurso boota o client da -API certa — você não precisa saber disso. - -**Pix não é uma cobrança.** Entra como `qr_codes` do pedido, e só um por pedido. -A conta precisa ter uma chave Pix ativa. - -```php -use PHPay\PagBank\PagBankGateway; - -$phpay = PHPay::gateway(new PagBankGateway(TOKEN_PAGBANK_SANDBOX))->charge(); - -$pedido = $phpay - ->setCustomer(['name' => 'Mário', 'email' => 'fale@phpay.io', 'tax_id' => '12345678901']) - ->addItem('Assinatura PHPay', 10050) // R$ 100,50 - ->setQrCode(10050) - ->setNotificationUrls(['https://exemplo.test/webhook/pagbank']) - ->create(); - -$phpay->getPixCode($pedido['id']); // de qr_codes[0].text -``` - -Cartão e boleto, aí sim, vão em `charges`: - -```php -$phpay - ->setCustomer($cliente) - ->addItem('Camiseta', 5990, 2) - ->setCharges([[ - 'reference_id' => 'cobranca-1', - 'amount' => ['value' => 11980, 'currency' => 'BRL'], - 'payment_method' => ['type' => 'CREDIT_CARD', 'installments' => 1, 'capture' => true], - ]]) - ->create(); -``` - -Assinaturas sempre pertencem a um plano, e o assinante pode nascer junto: - -```php -$phpay = PHPay::gateway(new PagBankGateway(TOKEN_PAGBANK_SANDBOX))->subscription(); - -$plano = $phpay->createPlan([ - 'name' => 'Plano PHPay Mensal', - 'amount' => ['value' => 4990, 'currency' => 'BRL'], // R$ 49,90 - 'interval' => ['unit' => 'MONTHS', 'length' => 1], -]); - -$phpay->setPlan($plano['id']) - ->setCustomer(['name' => 'Mário', 'email' => 'fale@phpay.io', 'tax_id' => '12345678901']) - ->create(); -``` - -### Pagar.me - -Autenticação Basic com a secret key, ambiente pelo prefixo da chave, e Pix como -forma de pagamento do pedido. - -```php -use PHPay\PagarMe\PagarMeGateway; - -$gateway = new PagarMeGateway(SECRET_KEY_PAGARME); - -$pedido = PHPay::gateway($gateway)->charge() - ->setCustomer([ - 'name' => 'Mário Lucas', - 'email' => 'fale@phpay.io', - 'document' => '12345678901', - ]) - ->addItem('Assinatura PHPay', 10050) // R$ 100,50 - ->setPix(1800) // expira em 30 minutos - ->create(); - -$phpay->getPixCode($pedido['id']); // de charges[0].last_transaction.qr_code -``` - -O cancelamento é `DELETE`, com valor opcional para estorno parcial: - -```php -$phpay->cancel($cobrancaId, 2500); // estorna R$ 25,00 -$phpay->cancel($cobrancaId); // estorna tudo -``` - -Assinaturas aceitam um plano ou a recorrência no próprio payload: - -```php -$phpay = PHPay::gateway($gateway)->subscription(); - -$plano = $phpay->createPlan([ - 'name' => 'Plano PHPay Mensal', - 'interval' => 'month', - 'interval_count' => 1, - 'items' => [[ - 'name' => 'Mensalidade', - 'quantity' => 1, - 'pricing_scheme' => ['price' => 4990], // R$ 49,90 - ]], -]); - -$phpay->setPlan($plano['id']) - ->setCustomerId($clienteId) - ->create(['payment_method' => 'pix']); -``` - -#### Consultando entregas de webhook - -O Pagar.me deixa ler e reenviar os eventos que já despachou. Isso **não** é a -capacidade `SupportsWebhooks` — o cadastro dos endpoints é no dashboard — então -vive no gateway concreto, não na facade: - -```php -$gateway->webhookDeliveries()->setFilter(['size' => 10])->getAll(); -$gateway->webhookDeliveries()->resend($hookId); -``` - -É assim que o modelo de capacidades abre espaço para o que só um gateway -oferece: quem segura `PagarMeGateway` alcança, quem tipa uma capacidade não. - -### Cielo - -A primeira **adquirente** da biblioteca, e a forma mostra: não há recurso de -cliente — ele é um campo da venda. Daí as duas capacidades. - -A particularidade é que a Cielo separa **dois hosts por tipo de operação**: -escritas vão para `api.cieloecommerce...`, consultas para -`apiquery.cieloecommerce...`. O mesmo recurso usa os dois, e o PHPay roteia -sozinho — `create()` vai num, `find()` no outro. - -```php -use PHPay\Cielo\CieloGateway; - -$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->charge(); - -$venda = $phpay - ->setOrderId('pedido-1') - ->setCustomer(['Name' => 'Mário Lucas']) - ->setPix(15700) // R$ 157,00 - ->setRequestId('pedido-1') // idempotência - ->create(); - -$phpay->getPixCode($venda['Payment']['PaymentId']); -``` - -Cartão em duas etapas — autoriza agora, captura depois: - -```php -$phpay - ->setCustomer(['Name' => 'Mário Lucas']) - ->setCreditCard(15700, $cartao, installments: 3) // capture: false por padrão - ->create(); - -$phpay->capture($paymentId); -$phpay->cancel($paymentId, 2500); // estorna R$ 25,00 -``` - -#### Recorrência - -A Cielo **não tem endpoint de criar assinatura**: a recorrência nasce de uma -venda com um bloco `RecurrentPayment`, e só então ganha um `RecurrentPaymentId` -próprio. Sempre cobra cartão. - -```php -use PHPay\Cielo\Enums\RecurrentIntervalEnum; - -$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->subscription(); - -$recorrencia = $phpay - ->setCustomer(['Name' => 'Mário Lucas']) - ->setCard($cartao) - ->setInterval(RecurrentIntervalEnum::MONTHLY) - ->setEndDate('2027-12-31') - ->create(15700); - -$id = $recorrencia['Payment']['RecurrentPayment']['RecurrentPaymentId']; - -$phpay->updateAmount($id, 19900); -$phpay->deactivate($id); -$phpay->reactivate($id); -``` - -### Rede - -Adquirente, e a forma mais estreita da biblioteca: **só -cobranças**. Não há recurso de cliente nem assinatura gerenciável — a -transação tem um campo `subscription`, mas é uma flag para a adquirente, não -algo que você liste ou cancele. - -A particularidade é a autenticação: **OAuth2 `client_credentials` num host -separado do de API**, com o caminho do token diferente em cada ambiente. E o -token **expira** — o PHPay renegocia sozinho quando isso acontece. - -```php -use PHPay\Rede\Enums\TransactionKindEnum; -use PHPay\Rede\RedeGateway; - -/* nenhuma chamada de rede aqui: o token é negociado no primeiro uso */ -$gateway = new RedeGateway(REDE_PV, REDE_TOKEN); - -$transacao = PHPay::gateway($gateway)->charge() - ->setReference('pedido-1') - ->setCard('5448280000000007', 'MARIO LUCAS', '12', '2030', '123') - ->setPayment(2099, TransactionKindEnum::CREDIT) // R$ 20,99 - ->setSoftDescriptor('PHPAY') - ->create(); -``` - -Fluxo em duas etapas — autoriza agora, captura quando o pedido for separado: - -```php -$phpay->setPayment(5000, capture: false)->create(); - -$phpay->capture($tid); -$phpay->refund($tid, 1000); // estorna R$ 10,00 -``` - -O código de retorno `"00"` significa aprovada: - -```php -use PHPay\Rede\Enums\TransactionStatusEnum; - -TransactionStatusEnum::approved($phpay->getStatus($tid)); -``` - -Num processo longo, dá para inspecionar ou descartar o token em mãos: - -```php -$gateway->authorization()->hasValidToken(); -$gateway->authorization()->forget(); -``` - -### AbacatePay - -Pix nativo, e **mesmo assim não declara `SupportsPixKeys`** — aquela capacidade -é sobre gerenciar chaves e QR Code estático, coisa de PSP. Aqui Pix é o método -de pagamento da cobrança, e o único aceito. - -Também não declara assinaturas: a API documenta `ONE_TIME` como a única -frequência aceita. - -A cobrança é um **link de pagamento montado a partir de produtos**, não de um -valor solto — o total vem calculado em `amount`. Preço em centavos, com -**mínimo de 100** (R$ 1,00) por produto. - -```php -use PHPay\AbacatePay\AbacatePayGateway; - -$gateway = new AbacatePayGateway(ABACATEPAY_TOKEN); - -$cobranca = PHPay::gateway($gateway)->charge() - ->setCustomer([ - 'name' => 'Mário Lucas', - 'email' => 'fale@phpay.io', - 'cellphone' => '(11) 4002-8922', - 'taxId' => '12345678901', - ]) - ->addProduct('prod-1234', 'Assinatura PHPay', 2000) // R$ 20,00 - ->setUrls( - completionUrl: 'https://exemplo.test/obrigado', - returnUrl: 'https://exemplo.test/loja' - ) - ->create(); - -$phpay->getPaymentUrl($cobranca); // o link para onde mandar o cliente -$phpay->isDevMode($cobranca); // em qual ambiente a cobrança nasceu -``` - -O `externalId` do produto é o id **no seu sistema** — o AbacatePay cria o -produto do lado dele a partir dele, então precisa ser único. - -#### Cupons de desconto - -Nenhum outro gateway da biblioteca tem isso, então não é capacidade: vive no -gateway concreto, como o `webhookDeliveries()` do Pagar.me. - -```php -$gateway->coupons()->create([ - 'code' => 'PHPAY10', - 'discountKind' => 'PERCENTAGE', - 'discount' => 10, -]); -``` - -### Woovi/OpenPix - -**O segundo gateway com as cinco capacidades**, ao lado do Asaas — e o que -confirma que o modelo descreve o domínio, não um fornecedor: são duas empresas -independentes, com APIs independentes, preenchendo o mesmo contrato. - -Sendo PSP Pix-nativo, ele gerencia chaves e QR Code estático de verdade. - -```php -use PHPay\Woovi\Enums\PixKeyTypeEnum; -use PHPay\Woovi\WooviGateway; - -$phpay = PHPay::gateway(new WooviGateway(WOOVI_APP_ID)); - -/* chaves Pix da conta */ -$phpay->pix()->createKey(PixKeyTypeEnum::RANDOM); -$phpay->pix()->getAll(); - -/* consulta uma chave antes de pagar */ -$phpay->pix()->verifyKey('fale@phpay.io'); - -/* QR Code estático, com ou sem valor */ -$phpay->pix()->staticQrCode('Caixa 1'); -$phpay->pix()->staticQrCode('Caixa 2', 2500); -``` - -Três particularidades: - -**O AppID vai cru no `Authorization`** — sem `Bearer`, sem `Basic`. - -**O sandbox tem domínio próprio**: `api.woovi-sandbox.com` contra -`api.openpix.com.br`. - -**Todo objeto é endereçável pelo `correlationID`**, o id no *seu* sistema — -nenhum outro gateway da biblioteca oferece isso: - -```php -$cobranca = $phpay->charge() - ->setCorrelationId('pedido-1') - ->setCustomer(['name' => 'Mário Lucas', 'email' => 'fale@phpay.io']) - ->create(10050); // R$ 100,50 - -$phpay->charge()->getPixCode($cobranca); -$phpay->charge()->find('pedido-1'); // pelo SEU id, não pelo do gateway -``` - -Webhooks têm CRUD por API — junto com o Asaas, os únicos: - -```php -$phpay->webhook(['name' => 'PHPay', 'url' => 'https://exemplo.test/webhook'])->create(); -$phpay->webhook()->getAll(); -``` - -> Repare que o webhook fica em `api/openpix/v1/`, enquanto os demais recursos -> ficam em `api/v1/` — herança da fusão das duas marcas. O PHPay trata isso -> internamente. - -### Efí - -**Duas APIs com as mesmas credenciais**, e é isso que dá ao Efí quatro das -cinco capacidades: - -| API | Host | Autenticação | Recursos | -| --- | --- | --- | --- | -| **Cobranças** | `cobrancas.api.efipay.com.br` | OAuth2 | `charge()` — boleto | -| **Pix** | `pix.api.efipay.com.br` | OAuth2 **+ mTLS** | `pix()`, `webhook()`, `subscription()`, `pixCharge()` | - -Clientes ficam de fora: nenhuma das duas APIs mantém cadastro de cliente. - -O gateway **não faz chamada de rede no construtor**. Cada API tem o seu token, -pedido na primeira vez que é necessário e **renovado sozinho quando expira** — -um gateway vivo num worker de fila não passa a tomar 401. - -#### Cobranças (boleto) - -```php -use PHPay\Efi\EfiGateway; -use PHPay\Support\{Customer, Money}; - -$gateway = new EfiGateway(CLIENT_ID, CLIENT_SECRET); - -$cobranca = PHPay::gateway($gateway)->charge([ - 'description' => 'Assinatura PHPay', - 'expire_at' => date('Y-m-d', strtotime('+3 days')), -]) - ->setAmount(Money::reais('100,50')) - ->setCustomer(new Customer('Mário Lucas', '12345678909')) - ->create(); -``` - -#### API Pix - -**Toda requisição da API Pix é por mTLS**, inclusive a do token. Passe o -certificado `.p12` (ou `.pem`) da aplicação, que você baixa no painel do Efí: - -```php -$gateway = new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: '/caminho/certificado.p12'); -``` - -Com senha, ou vindo de uma variável de ambiente — o comum em container e -serverless: - -```php -use PHPay\Http\Certificate; - -new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: new Certificate('/caminho/certificado.p12', 'senha')); - -new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: Certificate::fromBase64(getenv('EFI_CERTIFICATE_BASE64'))); -``` - -> O `fromBase64()` grava o certificado num arquivo temporário com permissão -> `0600`, apagado quando o processo termina. A senha nunca aparece num -> `var_dump()`. - -Certificado de homologação só funciona com `$sandbox = true`, e o de produção, -com `false`. Sem certificado, a API de Cobranças continua funcionando e a API -Pix responde com uma `ValidationException` clara, não com um erro de TLS. - -**Cobrança Pix** — imediata por padrão, com vencimento quando há `setDueDate()`: - -```php -$cobranca = $gateway->pixCharge() - ->setAmount(Money::reais('123,45')) - ->setKey('sua-chave-pix') // chave da conta Efí que recebe - ->setCustomer(new Customer('Mário Lucas', '12345678909')) - ->setDescription('Pedido 1234') - ->setExpiration(3600) // segundos - ->create(); - -$qr = $gateway->pixCharge()->qrCode($cobranca['loc']['id']); -$qr['qrcode']; // copia e cola -$qr['imagemQrcode']; // PNG em base64 - -/* com vencimento: multa, juros e desconto como num boleto */ -$gateway->pixCharge() - ->setAmount(Money::reais(250)) - ->setKey('sua-chave-pix') - ->setCustomer(new Customer('Sixtec LTDA', '12345678000199')) - ->setDueDate('2026-12-31', validityAfterDue: 15) - ->create(); - -/* devolução, total ou parcial */ -$gateway->pixCharge()->refund($endToEndId, Money::reais(10)); -``` - -`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 quem está na v2. - -**Chaves Pix** — só chaves aleatórias (EVP) são gerenciáveis pela API: - -```php -$chave = PHPay::gateway($gateway)->pix()->createKey()['chave']; - -PHPay::gateway($gateway)->pix()->getAll(); -PHPay::gateway($gateway)->pix()->destroy($chave); -``` - -> **Sem QR Code estático.** A API Pix da Efí não expõe endpoint para criar um, -> então aqui não existe o `staticQrCode()` do Asaas e da Woovi. Pelo padrão do -> BACEN, um QR Code estático é um BR Code composto a partir da chave, de um -> valor opcional e de um identificador livre — quem oferece endpoint faz por -> conveniência, não por necessidade. O `qrCode()` do `pixCharge()`, acima, é o -> QR Code **de uma cobrança**, que é outra coisa. - -**Webhooks** — um por chave Pix, endereçado pela própria chave: - -```php -PHPay::gateway($gateway) - ->webhook(['chave' => $chave, 'webhookUrl' => 'https://loja.com/webhook/pix']) - ->create(); -``` - -> O mTLS vale **nos dois sentidos**: por padrão o Efí só entrega para um -> servidor que valide o certificado dele. Se o seu não consegue (hospedagem -> compartilhada, balanceador que termina o TLS), `skipMtlsChecking()` desliga a -> checagem — e aí valide a origem de outro jeito, como um `hmac` na URL. - -**Pix Automático** — o pagador autoriza uma vez no app do banco, e cada ciclo -é debitado sem nova aprovação: - -```php -use PHPay\Efi\Enums\{AccountTypeEnum, PeriodicityEnum}; - -$assinaturas = PHPay::gateway($gateway)->subscription(); - -/* 1. o location que o QR Code de autorização aponta */ -$location = $assinaturas->createLocation(); - -/* 2. a recorrência: o que o pagador autoriza */ -$recorrencia = $assinaturas - ->setCustomer(new Customer('Mário Lucas', '12345678909')) - ->setContract('CONTRATO-2026-001') // até 35 caracteres - ->setDescription('Plano mensal') - ->setAmount(Money::reais('49,90')) // ou setMinimumAmount(), para valor variável - ->setPeriodicity(PeriodicityEnum::MONTHLY, '2026-10-01') - ->allowRetries() // até 3 tentativas em 7 dias - ->setLocation($location['id']) - ->create(); - -$assinaturas->find($recorrencia['idRec'])['dadosQR']; // copia e cola para o pagador autorizar - -/* 3. a cobrança de cada ciclo */ -$assinaturas - ->setReceiver('12345-6', AccountTypeEnum::CHECKING, '0001') - ->createCharge($recorrencia['idRec'], Money::reais('49,90'), '2026-11-05'); - -$assinaturas->cancel($recorrencia['idRec']); -``` +| [Asaas](docs/gateways/asaas.md) | [Woovi/OpenPix](docs/gateways/woovi.md) | [Efí](docs/gateways/efi.md) | +| [Mercado Pago](docs/gateways/mercado-pago.md) | [PagBank](docs/gateways/pagbank.md) | [Pagar.me](docs/gateways/pagarme.md) | +| [AbacatePay](docs/gateways/abacatepay.md) | [Cielo](docs/gateways/cielo.md) | [Rede](docs/gateways/rede.md) | --- diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..683bb7a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,27 @@ +# Documentação do PHPay + +O que vale para todos os gateways está nos conceitos: capacidade, erro, +unidade monetária, retry. Cada página de gateway cobre só o que é +específico dele. + +[← voltar ao README](../README.md) + +## Conceitos + +| | | +| --- | --- | +| [Capacidades](capacidades.md) | o que cada gateway declara suportar, e o que acontece quando você pede o que ele não tem | +| [Tratamento de erros](erros.md) | array é sucesso, falha é exceção — e qual exceção | +| [Retry e idempotência](retry-e-idempotencia.md) | o que a biblioteca repete, o que ela recusa a repetir, e por quê | +| [Ambientes e credenciais](ambientes-e-credenciais.md) | sandbox e produção em cada gateway, e como a credencial fica fora de dump | +| [Cliente](cliente.md) | o VO `Customer`, porque o mesmo campo tem seis grafias entre os gateways | +| [Valores monetários](valores-monetarios.md) | `Money`, porque reais e centavos não se misturam | +| [Verificando o webhook que chega](webhooks.md) | provar que a notificação veio do gateway | + +## Gateways + +| | | | +| --- | --- | --- | +| [Asaas](gateways/asaas.md) | [Woovi/OpenPix](gateways/woovi.md) | [Efí](gateways/efi.md) | +| [Mercado Pago](gateways/mercado-pago.md) | [PagBank](gateways/pagbank.md) | [Pagar.me](gateways/pagarme.md) | +| [AbacatePay](gateways/abacatepay.md) | [Cielo](gateways/cielo.md) | [Rede](gateways/rede.md) | diff --git a/docs/ambientes-e-credenciais.md b/docs/ambientes-e-credenciais.md new file mode 100644 index 0000000..3722f1c --- /dev/null +++ b/docs/ambientes-e-credenciais.md @@ -0,0 +1,35 @@ +# Ambientes e credenciais + +Os gateways discordam sobre como separar teste de produção, e o PHPay segue o +que cada um faz em vez de inventar um padrão: + +| Gateway | Como o ambiente é decidido | +| --- | --- | +| **Asaas** | `$sandbox` no construtor — troca a URL | +| **PagBank** | `$sandbox` no construtor — troca a URL | +| **Cielo** | `$sandbox` no construtor — troca **as duas** URLs | +| **Efí** | `$sandbox` no construtor — troca **as duas** URLs (Cobranças e Pix) | +| **Rede** | `$sandbox` no construtor — troca **as duas** URLs e o caminho do token | +| **Mercado Pago** | Prefixo do token (`TEST-`); host único, sem `$sandbox` | +| **Pagar.me** | Prefixo da chave (`sk_test_`); host único, sem `$sandbox` | +| **Woovi** | `$sandbox` no construtor — o sandbox tem **domínio próprio** | +| **AbacatePay** | Pela chave usada; host único, **sem prefixo** — a cobrança informa em `devMode` | + +Nos dois últimos, `isSandbox()` diz em qual ambiente você está: + +```php +(new MercadoPagoGateway($token))->isSandbox(); +(new PagarMeGateway($secretKey))->isSandbox(); +``` + +> **Nunca** versione credenciais. Os arquivos `examples/*/credentials.php` são +> ignorados pelo git por padrão. + +`var_dump()` e `print_r()` de um gateway ou recurso mostram `********` no lugar +da credencial, e o client HTTP, que a carrega nos headers, aparece oculto. O +`dd()` do Symfony VarDumper lê as propriedades por conta própria e **não** passa +por essa máscara: não despeje o gateway em log. + +Toda requisição tem prazo: 60 segundos no total e 10 para conectar. Sem isso, um +gateway que trava prenderia o worker indefinidamente. Um estouro de prazo chega +como `ApiException` com `isConnectionError()` verdadeiro. diff --git a/docs/capacidades.md b/docs/capacidades.md new file mode 100644 index 0000000..445e841 --- /dev/null +++ b/docs/capacidades.md @@ -0,0 +1,41 @@ +# Capacidades + +`GatewayInterface` carrega só a identidade do gateway. Cada recurso é uma +interface que o gateway implementa **se, e só se,** oferecer: + +```php +use PHPay\Contracts\Capability; + +$phpay = PHPay::gateway(new RedeGateway(REDE_PV, REDE_TOKEN)); + +$phpay->name(); // 'Rede' +$phpay->supports(Capability::SUBSCRIPTIONS); // false +$phpay->capabilities(); // [Capability::CHARGES] +``` + +Chamar um recurso que o gateway não oferece lança uma exceção que diz o que ele +oferece: + +```php +$phpay->pix(); +// NotImplementedException: Rede não suporta chaves Pix. +// Capacidades disponíveis: cobranças. +``` + +Se você segurar o **gateway concreto** em vez da facade, o erro sobe para tempo +de análise — o PHPStan acusa que o método não existe naquele tipo: + +```php +$rede = new RedeGateway(REDE_PV, REDE_TOKEN); + +$rede->charge(); // ✅ +$rede->pix(); // ❌ o método não existe nesse gateway +``` + +Para injeção de dependência, tipe a capacidade em vez do gateway: + +```php +use PHPay\Contracts\SupportsCharges; + +public function __construct(private SupportsCharges $gateway) {} +``` diff --git a/docs/cliente.md b/docs/cliente.md new file mode 100644 index 0000000..fc43824 --- /dev/null +++ b/docs/cliente.md @@ -0,0 +1,43 @@ +# Cliente + +O mesmo campo tem **seis grafias** entre os gateways: `cpfCnpj` no Asaas, +`tax_id` no PagBank, `document` no Pagar.me, `taxId` no AbacatePay, `taxID` no +Woovi, `cpf_cnpj` no Efí. Código escrito para um não migra para outro, e nada +no tipo avisa. + +`Customer` é a forma única. Cada gateway mapeia para o formato dele: + +```php +use PHPay\Support\Customer; + +$cliente = Customer::make( + name: 'Mário Lucas', + document: '123.456.789-01', // pontuação é limpa + email: 'fale@phpay.io', + phone: '(11) 94002-8922', +); + +$phpay->charge()->setCustomer($cliente); // funciona nos nove +``` + +Ele também carrega o que cada gateway deriva do cliente, e que antes ficava +espalhado: + +```php +$cliente->isIndividual(); // CPF: o Pagar.me precisa como type: 'individual' +$cliente->documentType(); // 'CPF' | 'CNPJ': a Cielo quer em IdentityType +$cliente->firstName(); // o Mercado Pago quer nome e sobrenome separados +$cliente->phoneParts(); // ['country' => '55', 'area' => '11', ...] para o PagBank +``` + +Para um cliente que já existe no gateway, ou para campos que só aquele gateway +tem: + +```php +$cliente->withId('cus_000006337812'); // reaproveita em vez de criar +$cliente->withExtra(['externalReference' => 'x']); // vai junto no payload +``` + +> O `withExtra()` existe para o que é específico de um gateway — endereço, +> data de nascimento, referência externa. **Nenhum campo obrigatório de +> nenhum dos nove gateways precisa dele**: o value object cobre todos. diff --git a/docs/erros.md b/docs/erros.md new file mode 100644 index 0000000..0bc3f53 --- /dev/null +++ b/docs/erros.md @@ -0,0 +1,32 @@ +# Tratamento de erros + +Toda exceção da biblioteca implementa `PHPay\Exceptions\PHPayException`, então +um `catch` cobre a integração inteira: + +| Exceção | Estende | Quando acontece | +| --- | --- | --- | +| `ValidationException` | `InvalidArgumentException` | Payload inválido, **antes** de qualquer HTTP | +| `ApiException` | `RuntimeException` | O gateway recusou, ou está inacessível | +| `NotImplementedException` | `BadMethodCallException` | O gateway não oferece o recurso | + +```php +use PHPay\Exceptions\ApiException; +use PHPay\Exceptions\PHPayException; +use PHPay\Exceptions\ValidationException; + +try { + $cobranca = $phpay->charge()->setCharge($dados)->create(); +} catch (ValidationException $e) { + // payload inválido: nenhuma requisição foi feita +} catch (ApiException $e) { + $e->getStatusCode(); // 400, 401, 404… ou 0 se nem chegou ao gateway + $e->getResponse(); // corpo devolvido pelo gateway + $e->getGateway(); // 'Asaas', 'Pagar.me', … + $e->isConnectionError(); // true em timeout, DNS, TLS — vale retry +} catch (PHPayException $e) { + // qualquer outra falha do PHPay +} +``` + +`ApiException` já resume os formatos de erro de cada gateway, então +`getMessage()` traz a descrição legível, não um dump. diff --git a/docs/gateways/abacatepay.md b/docs/gateways/abacatepay.md new file mode 100644 index 0000000..a4d42ec --- /dev/null +++ b/docs/gateways/abacatepay.md @@ -0,0 +1,51 @@ +# AbacatePay + +Pix nativo, e **mesmo assim não declara `SupportsPixKeys`** — aquela capacidade +é sobre gerenciar chaves e QR Code estático, coisa de PSP. Aqui Pix é o método +de pagamento da cobrança, e o único aceito. + +Também não declara assinaturas: a API documenta `ONE_TIME` como a única +frequência aceita. + +A cobrança é um **link de pagamento montado a partir de produtos**, não de um +valor solto — o total vem calculado em `amount`. Preço em centavos, com +**mínimo de 100** (R$ 1,00) por produto. + +```php +use PHPay\AbacatePay\AbacatePayGateway; + +$gateway = new AbacatePayGateway(ABACATEPAY_TOKEN); + +$cobranca = PHPay::gateway($gateway)->charge() + ->setCustomer([ + 'name' => 'Mário Lucas', + 'email' => 'fale@phpay.io', + 'cellphone' => '(11) 4002-8922', + 'taxId' => '12345678901', + ]) + ->addProduct('prod-1234', 'Assinatura PHPay', 2000) // R$ 20,00 + ->setUrls( + completionUrl: 'https://exemplo.test/obrigado', + returnUrl: 'https://exemplo.test/loja' + ) + ->create(); + +$phpay->getPaymentUrl($cobranca); // o link para onde mandar o cliente +$phpay->isDevMode($cobranca); // em qual ambiente a cobrança nasceu +``` + +O `externalId` do produto é o id **no seu sistema** — o AbacatePay cria o +produto do lado dele a partir dele, então precisa ser único. + +## Cupons de desconto + +Nenhum outro gateway da biblioteca tem isso, então não é capacidade: vive no +gateway concreto, como o `webhookDeliveries()` do Pagar.me. + +```php +$gateway->coupons()->create([ + 'code' => 'PHPAY10', + 'discountKind' => 'PERCENTAGE', + 'discount' => 10, +]); +``` diff --git a/docs/gateways/asaas.md b/docs/gateways/asaas.md new file mode 100644 index 0000000..9ad9f25 --- /dev/null +++ b/docs/gateways/asaas.md @@ -0,0 +1,154 @@ +# Asaas + +Um dos dois com as cinco capacidades, ao lado do Woovi. É PSP, então emite +chave Pix própria e gerencia webhooks por API. + +```php +use PHPay\Asaas\AsaasGateway; + +$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX)); +``` + +## Cobranças + +```php +$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->charge(); + +/* cria a cobrança, criando também o cliente */ +$phpay->setCharge($cobranca)->setCustomer($cliente)->create(); + +/* reaproveita um cliente que já existe — evita cadastro duplicado */ +$phpay->setCharge($cobranca)->setCustomerId('cus_000006337812')->create(); + +$phpay->find($id); +$phpay->getAll(); +$phpay->setQueryParams(['limit' => 2])->getAll(); +$phpay->update($id, $dados); +$phpay->destroy($id); +$phpay->restore($id); + +$phpay->getStatus($id); +$phpay->getDigitableLine($id); +$phpay->getQrCodePix($id); + +$phpay->confirmReceipt($id, [ + 'paymentDate' => date('Y-m-d'), + 'value' => 100.00, + 'notifyCustomer' => true, +]); +$phpay->undoConfirmReceipt($id); +``` + +## Clientes + +```php +$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX)); + +$cliente = $phpay->customer(['name' => 'Mário Lucas', 'cpfCnpj' => '12345678901'])->create(); + +$phpay->customer()->find($cliente['id']); +$phpay->customer()->setFilter(['cpfCnpj' => '12345678901'])->getAll(); +$phpay->customer(['name' => 'Novo Nome'])->update($cliente['id']); +$phpay->customer()->getNotifications($cliente['id']); +$phpay->customer()->destroy($cliente['id']); +$phpay->customer()->restore($cliente['id']); +``` + +## Assinaturas + +A assinatura gera uma cobrança por ciclo, e cada uma é uma cobrança comum: +aparece em `getPayments()` e é tratada pelo recurso de cobrança. + +```php +use PHPay\Asaas\Enums\SubscriptionCycleEnum; + +$assinaturas = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->subscription(); + +$assinatura = $assinaturas + ->setCustomer($cliente) // ou setCustomerId('cus_...') + ->setAmount(Money::reais('49,90')) + ->setCycle(SubscriptionCycleEnum::MONTHLY) + ->setSubscription([ + 'billingType' => 'BOLETO', + 'nextDueDate' => '2026-10-10', + 'description' => 'Plano mensal', + ]) + ->create(); +``` + +> O `create([...])` com o payload inteiro continua funcionando, e o array +> sobrescreve o que os setters montaram. O `cycle` é obrigatório: sem ele o +> Asaas recusa, e o PHPay barra antes. + +Consulta e ciclo de vida: + +```php +$assinaturas->setQueryParams(['customer' => 'cus_...', 'status' => 'ACTIVE'])->getAll(); +$assinaturas->find($id); + +/* muda as próximas cobranças; com updatePendingPayments, as pendentes também */ +$assinaturas->update($id, ['description' => 'Plano anual', 'updatePendingPayments' => true]); + +/* pausa: para de gerar cobranças e mantém as que existem */ +$assinaturas->deactivate($id); +$assinaturas->reactivate($id, '2026-11-10'); // o Asaas exige um novo vencimento + +/* remove: as cobranças pendentes e vencidas vão junto; as pagas ficam */ +$assinaturas->destroy($id); +``` + +Cobranças, carnê e cartão: + +```php +$assinaturas->getPayments($id, ['status' => 'PENDING']); + +/* o carnê vem como os bytes do PDF */ +file_put_contents('carne.pdf', $assinaturas->paymentBook($id, month: 12, year: 2026)); + +/* troca o cartão sem cobrar — as cobranças pendentes passam para o novo */ +$assinaturas->updateCreditCard($id, [ + 'creditCardToken' => $token, // ou creditCard + creditCardHolderInfo + 'remoteIp' => $ipDoComprador, +]); +``` + +Nota fiscal emitida automaticamente para cada cobrança: + +```php +$assinaturas->createInvoiceSettings($id, [ + 'municipalServiceName' => 'Desenvolvimento de software', + 'effectiveDatePeriod' => 'ON_PAYMENT_CONFIRMATION', + 'taxes' => [ // os sete são obrigatórios; 0 quando não houver + 'retainIss' => false, + 'iss' => 2, + 'pis' => 0.65, + 'cofins' => 3, + 'csll' => 0, + 'inss' => 0, + 'ir' => 0, + ], +]); + +$assinaturas->getInvoiceSettings($id); +$assinaturas->updateInvoiceSettings($id, [...]); +$assinaturas->destroyInvoiceSettings($id); +$assinaturas->getInvoices($id); // as notas já emitidas +``` + +## Webhooks e chaves Pix + +```php +$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX)); + +/* webhooks com CRUD completo — exclusividade do Asaas */ +$phpay->webhook(WEBHOOK)->create(); +$phpay->webhook()->getAll(); +$phpay->webhook()->update($id, $dados); +$phpay->webhook()->destroy($id); + +/* chaves Pix e QR Code estático */ +$chave = $phpay->pix()->createKey(); +$phpay->pix()->getAll(); +$phpay->pix()->staticQrCode(['addressKey' => $chave['key'], 'value' => 25.00]); +$phpay->pix()->destroy($chave['id']); +``` diff --git a/docs/gateways/cielo.md b/docs/gateways/cielo.md new file mode 100644 index 0000000..00fa65f --- /dev/null +++ b/docs/gateways/cielo.md @@ -0,0 +1,61 @@ +# Cielo + +A primeira **adquirente** da biblioteca, e a forma mostra: não há recurso de +cliente — ele é um campo da venda. Daí as duas capacidades. + +A particularidade é que a Cielo separa **dois hosts por tipo de operação**: +escritas vão para `api.cieloecommerce...`, consultas para +`apiquery.cieloecommerce...`. O mesmo recurso usa os dois, e o PHPay roteia +sozinho — `create()` vai num, `find()` no outro. + +```php +use PHPay\Cielo\CieloGateway; + +$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->charge(); + +$venda = $phpay + ->setOrderId('pedido-1') + ->setCustomer(['Name' => 'Mário Lucas']) + ->setPix(15700) // R$ 157,00 + ->setRequestId('pedido-1') // idempotência + ->create(); + +$phpay->getPixCode($venda['Payment']['PaymentId']); +``` + +Cartão em duas etapas — autoriza agora, captura depois: + +```php +$phpay + ->setCustomer(['Name' => 'Mário Lucas']) + ->setCreditCard(15700, $cartao, installments: 3) // capture: false por padrão + ->create(); + +$phpay->capture($paymentId); +$phpay->cancel($paymentId, 2500); // estorna R$ 25,00 +``` + +## Recorrência + +A Cielo **não tem endpoint de criar assinatura**: a recorrência nasce de uma +venda com um bloco `RecurrentPayment`, e só então ganha um `RecurrentPaymentId` +próprio. Sempre cobra cartão. + +```php +use PHPay\Cielo\Enums\RecurrentIntervalEnum; + +$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->subscription(); + +$recorrencia = $phpay + ->setCustomer(['Name' => 'Mário Lucas']) + ->setCard($cartao) + ->setInterval(RecurrentIntervalEnum::MONTHLY) + ->setEndDate('2027-12-31') + ->create(15700); + +$id = $recorrencia['Payment']['RecurrentPayment']['RecurrentPaymentId']; + +$phpay->updateAmount($id, 19900); +$phpay->deactivate($id); +$phpay->reactivate($id); +``` diff --git a/docs/gateways/efi.md b/docs/gateways/efi.md new file mode 100644 index 0000000..10d3553 --- /dev/null +++ b/docs/gateways/efi.md @@ -0,0 +1,152 @@ +# Efí + +**Duas APIs com as mesmas credenciais**, e é isso que dá ao Efí quatro das +cinco capacidades: + +| API | Host | Autenticação | Recursos | +| --- | --- | --- | --- | +| **Cobranças** | `cobrancas.api.efipay.com.br` | OAuth2 | `charge()` — boleto | +| **Pix** | `pix.api.efipay.com.br` | OAuth2 **+ mTLS** | `pix()`, `webhook()`, `subscription()`, `pixCharge()` | + +Clientes ficam de fora: nenhuma das duas APIs mantém cadastro de cliente. + +O gateway **não faz chamada de rede no construtor**. Cada API tem o seu token, +pedido na primeira vez que é necessário e **renovado sozinho quando expira** — +um gateway vivo num worker de fila não passa a tomar 401. + +## Cobranças (boleto) + +```php +use PHPay\Efi\EfiGateway; +use PHPay\Support\{Customer, Money}; + +$gateway = new EfiGateway(CLIENT_ID, CLIENT_SECRET); + +$cobranca = PHPay::gateway($gateway)->charge([ + 'description' => 'Assinatura PHPay', + 'expire_at' => date('Y-m-d', strtotime('+3 days')), +]) + ->setAmount(Money::reais('100,50')) + ->setCustomer(new Customer('Mário Lucas', '12345678909')) + ->create(); +``` + +## API Pix + +**Toda requisição da API Pix é por mTLS**, inclusive a do token. Passe o +certificado `.p12` (ou `.pem`) da aplicação, que você baixa no painel do Efí: + +```php +$gateway = new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: '/caminho/certificado.p12'); +``` + +Com senha, ou vindo de uma variável de ambiente — o comum em container e +serverless: + +```php +use PHPay\Http\Certificate; + +new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: new Certificate('/caminho/certificado.p12', 'senha')); + +new EfiGateway(CLIENT_ID, CLIENT_SECRET, certificate: Certificate::fromBase64(getenv('EFI_CERTIFICATE_BASE64'))); +``` + +> O `fromBase64()` grava o certificado num arquivo temporário com permissão +> `0600`, apagado quando o processo termina. A senha nunca aparece num +> `var_dump()`. + +Certificado de homologação só funciona com `$sandbox = true`, e o de produção, +com `false`. Sem certificado, a API de Cobranças continua funcionando e a API +Pix responde com uma `ValidationException` clara, não com um erro de TLS. + +**Cobrança Pix** — imediata por padrão, com vencimento quando há `setDueDate()`: + +```php +$cobranca = $gateway->pixCharge() + ->setAmount(Money::reais('123,45')) + ->setKey('sua-chave-pix') // chave da conta Efí que recebe + ->setCustomer(new Customer('Mário Lucas', '12345678909')) + ->setDescription('Pedido 1234') + ->setExpiration(3600) // segundos + ->create(); + +$qr = $gateway->pixCharge()->qrCode($cobranca['loc']['id']); +$qr['qrcode']; // copia e cola +$qr['imagemQrcode']; // PNG em base64 + +/* com vencimento: multa, juros e desconto como num boleto */ +$gateway->pixCharge() + ->setAmount(Money::reais(250)) + ->setKey('sua-chave-pix') + ->setCustomer(new Customer('Sixtec LTDA', '12345678000199')) + ->setDueDate('2026-12-31', validityAfterDue: 15) + ->create(); + +/* devolução, total ou parcial */ +$gateway->pixCharge()->refund($endToEndId, Money::reais(10)); +``` + +`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 quem está na v2. + +**Chaves Pix** — só chaves aleatórias (EVP) são gerenciáveis pela API: + +```php +$chave = PHPay::gateway($gateway)->pix()->createKey()['chave']; + +PHPay::gateway($gateway)->pix()->getAll(); +PHPay::gateway($gateway)->pix()->destroy($chave); +``` + +> **Sem QR Code estático.** A API Pix da Efí não expõe endpoint para criar um, +> então aqui não existe o `staticQrCode()` do Asaas e da Woovi. Pelo padrão do +> BACEN, um QR Code estático é um BR Code composto a partir da chave, de um +> valor opcional e de um identificador livre — quem oferece endpoint faz por +> conveniência, não por necessidade. O `qrCode()` do `pixCharge()`, acima, é o +> QR Code **de uma cobrança**, que é outra coisa. + +**Webhooks** — um por chave Pix, endereçado pela própria chave: + +```php +PHPay::gateway($gateway) + ->webhook(['chave' => $chave, 'webhookUrl' => 'https://loja.com/webhook/pix']) + ->create(); +``` + +> O mTLS vale **nos dois sentidos**: por padrão o Efí só entrega para um +> servidor que valide o certificado dele. Se o seu não consegue (hospedagem +> compartilhada, balanceador que termina o TLS), `skipMtlsChecking()` desliga a +> checagem — e aí valide a origem de outro jeito, como um `hmac` na URL. + +**Pix Automático** — o pagador autoriza uma vez no app do banco, e cada ciclo +é debitado sem nova aprovação: + +```php +use PHPay\Efi\Enums\{AccountTypeEnum, PeriodicityEnum}; + +$assinaturas = PHPay::gateway($gateway)->subscription(); + +/* 1. o location que o QR Code de autorização aponta */ +$location = $assinaturas->createLocation(); + +/* 2. a recorrência: o que o pagador autoriza */ +$recorrencia = $assinaturas + ->setCustomer(new Customer('Mário Lucas', '12345678909')) + ->setContract('CONTRATO-2026-001') // até 35 caracteres + ->setDescription('Plano mensal') + ->setAmount(Money::reais('49,90')) // ou setMinimumAmount(), para valor variável + ->setPeriodicity(PeriodicityEnum::MONTHLY, '2026-10-01') + ->allowRetries() // até 3 tentativas em 7 dias + ->setLocation($location['id']) + ->create(); + +$assinaturas->find($recorrencia['idRec'])['dadosQR']; // copia e cola para o pagador autorizar + +/* 3. a cobrança de cada ciclo */ +$assinaturas + ->setReceiver('12345-6', AccountTypeEnum::CHECKING, '0001') + ->createCharge($recorrencia['idRec'], Money::reais('49,90'), '2026-11-05'); + +$assinaturas->cancel($recorrencia['idRec']); +``` diff --git a/docs/gateways/mercado-pago.md b/docs/gateways/mercado-pago.md new file mode 100644 index 0000000..16f765a --- /dev/null +++ b/docs/gateways/mercado-pago.md @@ -0,0 +1,51 @@ +# Mercado Pago + +Ambiente pelo prefixo do token. `POST /v1/payments` exige o header +`X-Idempotency-Key`: o PHPay gera uma chave por chamada, e +`setIdempotencyKey()` deixa você fixar a sua — assim um retry da mesma operação +de negócio não cria duas cobranças. + +```php +use PHPay\MercadoPago\Enums\PaymentMethodEnum; +use PHPay\MercadoPago\MercadoPagoGateway; + +$gateway = new MercadoPagoGateway(ACCESS_TOKEN); + +$cobranca = PHPay::gateway($gateway)->charge() + ->setCharge([ + 'transaction_amount' => 100.50, + 'payment_method_id' => PaymentMethodEnum::PIX->value, + 'notification_url' => 'https://exemplo.test/webhook/mercadopago', + ]) + ->setPayer(['email' => 'comprador@exemplo.test']) + ->setIdempotencyKey('pedido-123456') + ->create(); + +$phpay->getPixCode($cobranca['id']); +``` + +Assinaturas usam `/preapproval`, com ou sem plano associado: + +```php +$phpay = PHPay::gateway($gateway)->subscription(); + +$phpay->setPayerEmail('comprador@exemplo.test')->create([ + 'reason' => 'Assinatura PHPay', + 'back_url' => 'https://exemplo.test/retorno', + 'auto_recurring' => [ + 'frequency' => 1, + 'frequency_type' => 'months', + 'transaction_amount' => 100.50, + 'currency_id' => 'BRL', + ], +]); + +/* com plano, a recorrência vem do plano */ +$phpay->setPayerEmail('comprador@exemplo.test') + ->setPlan('2c938084726fca480172750000000000') + ->create(['back_url' => 'https://exemplo.test/retorno']); +``` + +> O recurso `Customer` do Mercado Pago existe para cartões salvos — **não** é +> pré-requisito para cobrar, já que o pagamento carrega `payer.email` direto. A +> API também não oferece exclusão de cliente. diff --git a/docs/gateways/pagarme.md b/docs/gateways/pagarme.md new file mode 100644 index 0000000..57d0d42 --- /dev/null +++ b/docs/gateways/pagarme.md @@ -0,0 +1,64 @@ +# Pagar.me + +Autenticação Basic com a secret key, ambiente pelo prefixo da chave, e Pix como +forma de pagamento do pedido. + +```php +use PHPay\PagarMe\PagarMeGateway; + +$gateway = new PagarMeGateway(SECRET_KEY_PAGARME); + +$pedido = PHPay::gateway($gateway)->charge() + ->setCustomer([ + 'name' => 'Mário Lucas', + 'email' => 'fale@phpay.io', + 'document' => '12345678901', + ]) + ->addItem('Assinatura PHPay', 10050) // R$ 100,50 + ->setPix(1800) // expira em 30 minutos + ->create(); + +$phpay->getPixCode($pedido['id']); // de charges[0].last_transaction.qr_code +``` + +O cancelamento é `DELETE`, com valor opcional para estorno parcial: + +```php +$phpay->cancel($cobrancaId, 2500); // estorna R$ 25,00 +$phpay->cancel($cobrancaId); // estorna tudo +``` + +Assinaturas aceitam um plano ou a recorrência no próprio payload: + +```php +$phpay = PHPay::gateway($gateway)->subscription(); + +$plano = $phpay->createPlan([ + 'name' => 'Plano PHPay Mensal', + 'interval' => 'month', + 'interval_count' => 1, + 'items' => [[ + 'name' => 'Mensalidade', + 'quantity' => 1, + 'pricing_scheme' => ['price' => 4990], // R$ 49,90 + ]], +]); + +$phpay->setPlan($plano['id']) + ->setCustomerId($clienteId) + ->create(['payment_method' => 'pix']); +``` + +## Consultando entregas de webhook + +O Pagar.me deixa ler e reenviar os eventos que já despachou. Isso **não** é a +capacidade `SupportsWebhooks` — o cadastro dos endpoints é no dashboard — então +vive no gateway concreto, não na facade: + +```php +$gateway->webhookDeliveries()->setFilter(['size' => 10])->getAll(); +$gateway->webhookDeliveries()->resend($hookId); +``` + +É assim que o modelo de capacidades abre espaço para o que só um gateway +oferece: quem segura `PagarMeGateway` alcança, quem tipa uma capacidade não. diff --git a/docs/gateways/pagbank.md b/docs/gateways/pagbank.md new file mode 100644 index 0000000..2f4a356 --- /dev/null +++ b/docs/gateways/pagbank.md @@ -0,0 +1,55 @@ +# PagBank + +Duas particularidades, ambas resolvidas pela biblioteca. + +**Duas APIs em hosts diferentes.** Pedidos vivem em `api.pagseguro.com`, +assinaturas em `api.assinaturas.pagseguro.com`. Cada recurso boota o client da +API certa — você não precisa saber disso. + +**Pix não é uma cobrança.** Entra como `qr_codes` do pedido, e só um por pedido. +A conta precisa ter uma chave Pix ativa. + +```php +use PHPay\PagBank\PagBankGateway; + +$phpay = PHPay::gateway(new PagBankGateway(TOKEN_PAGBANK_SANDBOX))->charge(); + +$pedido = $phpay + ->setCustomer(['name' => 'Mário', 'email' => 'fale@phpay.io', 'tax_id' => '12345678901']) + ->addItem('Assinatura PHPay', 10050) // R$ 100,50 + ->setQrCode(10050) + ->setNotificationUrls(['https://exemplo.test/webhook/pagbank']) + ->create(); + +$phpay->getPixCode($pedido['id']); // de qr_codes[0].text +``` + +Cartão e boleto, aí sim, vão em `charges`: + +```php +$phpay + ->setCustomer($cliente) + ->addItem('Camiseta', 5990, 2) + ->setCharges([[ + 'reference_id' => 'cobranca-1', + 'amount' => ['value' => 11980, 'currency' => 'BRL'], + 'payment_method' => ['type' => 'CREDIT_CARD', 'installments' => 1, 'capture' => true], + ]]) + ->create(); +``` + +Assinaturas sempre pertencem a um plano, e o assinante pode nascer junto: + +```php +$phpay = PHPay::gateway(new PagBankGateway(TOKEN_PAGBANK_SANDBOX))->subscription(); + +$plano = $phpay->createPlan([ + 'name' => 'Plano PHPay Mensal', + 'amount' => ['value' => 4990, 'currency' => 'BRL'], // R$ 49,90 + 'interval' => ['unit' => 'MONTHS', 'length' => 1], +]); + +$phpay->setPlan($plano['id']) + ->setCustomer(['name' => 'Mário', 'email' => 'fale@phpay.io', 'tax_id' => '12345678901']) + ->create(); +``` diff --git a/docs/gateways/rede.md b/docs/gateways/rede.md new file mode 100644 index 0000000..f9f94ee --- /dev/null +++ b/docs/gateways/rede.md @@ -0,0 +1,49 @@ +# Rede + +Adquirente, e a forma mais estreita da biblioteca: **só +cobranças**. Não há recurso de cliente nem assinatura gerenciável — a +transação tem um campo `subscription`, mas é uma flag para a adquirente, não +algo que você liste ou cancele. + +A particularidade é a autenticação: **OAuth2 `client_credentials` num host +separado do de API**, com o caminho do token diferente em cada ambiente. E o +token **expira** — o PHPay renegocia sozinho quando isso acontece. + +```php +use PHPay\Rede\Enums\TransactionKindEnum; +use PHPay\Rede\RedeGateway; + +/* nenhuma chamada de rede aqui: o token é negociado no primeiro uso */ +$gateway = new RedeGateway(REDE_PV, REDE_TOKEN); + +$transacao = PHPay::gateway($gateway)->charge() + ->setReference('pedido-1') + ->setCard('5448280000000007', 'MARIO LUCAS', '12', '2030', '123') + ->setPayment(2099, TransactionKindEnum::CREDIT) // R$ 20,99 + ->setSoftDescriptor('PHPAY') + ->create(); +``` + +Fluxo em duas etapas — autoriza agora, captura quando o pedido for separado: + +```php +$phpay->setPayment(5000, capture: false)->create(); + +$phpay->capture($tid); +$phpay->refund($tid, 1000); // estorna R$ 10,00 +``` + +O código de retorno `"00"` significa aprovada: + +```php +use PHPay\Rede\Enums\TransactionStatusEnum; + +TransactionStatusEnum::approved($phpay->getStatus($tid)); +``` + +Num processo longo, dá para inspecionar ou descartar o token em mãos: + +```php +$gateway->authorization()->hasValidToken(); +$gateway->authorization()->forget(); +``` diff --git a/docs/gateways/woovi.md b/docs/gateways/woovi.md new file mode 100644 index 0000000..a60ef3c --- /dev/null +++ b/docs/gateways/woovi.md @@ -0,0 +1,56 @@ +# Woovi/OpenPix + +**O segundo gateway com as cinco capacidades**, ao lado do Asaas — e o que +confirma que o modelo descreve o domínio, não um fornecedor: são duas empresas +independentes, com APIs independentes, preenchendo o mesmo contrato. + +Sendo PSP Pix-nativo, ele gerencia chaves e QR Code estático de verdade. + +```php +use PHPay\Woovi\Enums\PixKeyTypeEnum; +use PHPay\Woovi\WooviGateway; + +$phpay = PHPay::gateway(new WooviGateway(WOOVI_APP_ID)); + +/* chaves Pix da conta */ +$phpay->pix()->createKey(PixKeyTypeEnum::RANDOM); +$phpay->pix()->getAll(); + +/* consulta uma chave antes de pagar */ +$phpay->pix()->verifyKey('fale@phpay.io'); + +/* QR Code estático, com ou sem valor */ +$phpay->pix()->staticQrCode('Caixa 1'); +$phpay->pix()->staticQrCode('Caixa 2', 2500); +``` + +Três particularidades: + +**O AppID vai cru no `Authorization`** — sem `Bearer`, sem `Basic`. + +**O sandbox tem domínio próprio**: `api.woovi-sandbox.com` contra +`api.openpix.com.br`. + +**Todo objeto é endereçável pelo `correlationID`**, o id no *seu* sistema — +nenhum outro gateway da biblioteca oferece isso: + +```php +$cobranca = $phpay->charge() + ->setCorrelationId('pedido-1') + ->setCustomer(['name' => 'Mário Lucas', 'email' => 'fale@phpay.io']) + ->create(10050); // R$ 100,50 + +$phpay->charge()->getPixCode($cobranca); +$phpay->charge()->find('pedido-1'); // pelo SEU id, não pelo do gateway +``` + +Webhooks têm CRUD por API — junto com o Asaas, os únicos: + +```php +$phpay->webhook(['name' => 'PHPay', 'url' => 'https://exemplo.test/webhook'])->create(); +$phpay->webhook()->getAll(); +``` + +> Repare que o webhook fica em `api/openpix/v1/`, enquanto os demais recursos +> ficam em `api/v1/` — herança da fusão das duas marcas. O PHPay trata isso +> internamente. diff --git a/docs/retry-e-idempotencia.md b/docs/retry-e-idempotencia.md new file mode 100644 index 0000000..5b55911 --- /dev/null +++ b/docs/retry-e-idempotencia.md @@ -0,0 +1,65 @@ +# Retry e idempotência + +A biblioteca repete uma requisição **só quando repetir não pode cobrar de novo**. +Vale para os dez gateways, porque o middleware vive na +`PHPay\Http\ClientFactory`, o único lugar onde um client nasce. + +| Situação | Repete | +| --- | --- | +| Erro antes de a requisição sair (DNS, connect, handshake TLS) | sempre — nada chegou ao gateway | +| Timeout sem conexão estabelecida | sempre — nada chegou | +| Timeout com conexão estabelecida | só leitura, ou com chave de idempotência | +| `429` | sempre, respeitando `Retry-After` | +| `5xx` | só leitura, ou com chave de idempotência | +| `4xx` fora do 429 | nunca — o gateway entendeu e recusou | + +Leitura é `GET`, `HEAD` e `OPTIONS`. `PUT`, `PATCH` e `DELETE` **não** são +repetidos, apesar de o HTTP chamar `PUT` e `DELETE` de idempotentes: a +documentação do Asaas pede para +[não tratar a exclusão de cobrança como idempotente](https://docs.asaas.com/reference/excluir-cobranca) +sem conferir a resposta, e esse aviso vale mais que a teoria. + +São 3 tentativas no total — 200ms, 400ms, teto de 2s, com jitter. Se o gateway +mandar um `Retry-After` maior que o teto, a biblioteca **desiste** em vez de +repetir antes da janela pedida, porque repetir cedo só seria recusado de novo. +E lembre que retry multiplica o pior caso de tempo de parede: 3 tentativas com +`timeout` de 60s é o limite superior. + +Para mudar a política, passe uma à fábrica: + +```php +use PHPay\Http\{ClientFactory, RetryPolicy}; + +$maisTentativas = ClientFactory::make(['base_uri' => $uri], new RetryPolicy(5)); +$semRetry = ClientFactory::make(['base_uri' => $uri], RetryPolicy::disabled()); +``` + +## Chave de idempotência + +Com uma chave, o `POST` passa a ser repetível: o gateway reconhece a segunda +requisição como a mesma operação e devolve o resultado da primeira em vez de +cobrar outra vez. + +| Gateway | Header | Como | +| --- | --- | --- | +| Mercado Pago | `X-Idempotency-Key` | sempre enviada — a API de pagamentos exige | +| Pagar.me | `Idempotency-Key` | você informa; vale 24h | +| PagBank | `x-idempotency-key` | você informa; vale 48h | + +```php +$pedido = $pagbank->charge() + ->setCustomer($cliente) + ->addItem('Assinatura PHPay', 10050) + ->setIdempotencyKey($idDoSeuPedido) + ->create(); +``` + +**Use uma chave sua, guardada junto do registro pendente.** Chave gerada pela +biblioteca cobre só as tentativas de dentro da mesma chamada — o retry reenvia +a mesma requisição, com o mesmo header. O retry do seu lado, em outro processo +ou em outra chamada a `create()`, chega com chave nova e cobra de novo. + +Os outros gateways não documentam chave de idempotência. Neles um `POST` só é +repetido quando o erro provou que a requisição não saiu da máquina — e no Asaas +a orientação da própria documentação é +[consultar a cobrança antes de repetir](https://docs.asaas.com/docs/cobrancas-via-cartao-de-credito). diff --git a/docs/valores-monetarios.md b/docs/valores-monetarios.md new file mode 100644 index 0000000..7c7fcdf --- /dev/null +++ b/docs/valores-monetarios.md @@ -0,0 +1,56 @@ +# Unidade monetária + +Os gateways discordam sobre a unidade, e **errar não quebra a integração — ela +cobra o valor errado**. Mandar `100.50` num gateway de centavos cobra R$ 1,00, +e você só descobre na conciliação. + +Use `Money` e o problema deixa de existir: você diz a unidade que tem, o +gateway pede a unidade que precisa, e nenhum dos dois pode errar. + +```php +use PHPay\Support\Money; + +$valor = Money::reais(100.50); // ou Money::centavos(10050) + +$asaas->charge()->setAmount($valor); // vira 100.50 +$pagbank->charge()->addItem('Item', $valor); // vira 10050 +``` + +Também aceita string, que é o que um campo de formulário costuma entregar: + +```php +Money::reais('100,50'); // notação brasileira +Money::reais('1.234,56'); // com separador de milhar +Money::reais('100.50'); // notação com ponto +``` + +E tem o que um total precisa: + +```php +$unitario = Money::reais(59.90); + +$unitario->multiply(2); // R$ 119,80 +$unitario->plus(Money::reais(10)); // R$ 69,90 +$unitario->format(); // 'R$ 59,90' +``` + +> `Money::reais(10 / 3)` **lança exceção** em vez de arredondar. Arredondamento +> silencioso é como nascem erros de um centavo na conciliação — arredonde você +> mesmo, ou use `Money::centavos()` para ser exato. + +## Passando número cru + +Continua funcionando, e cada gateway lê na unidade que sempre esperou: + +| Gateway | Unidade do número cru | R$ 100,50 | +| --- | --- | --- | +| **Asaas**, **Mercado Pago** | Reais (decimal) | `100.50` | +| **PagBank**, **Pagar.me**, **Cielo**, **Rede**, **AbacatePay**, **Woovi**, **Efí** | Centavos (inteiro) | `10050` | + +Nos que usam centavos, o PHPay recusa decimal na validação. Mas é justamente +essa tabela que o `Money` torna desnecessária — **prefira o value object**. + +A exceção é a [API Pix do Efí](gateways/efi.md#api-pix), que **não aceita número cru, só +`Money`**. Ela quer reais (`"100.50"`) enquanto a API de Cobranças do mesmo +gateway quer centavos, e um número solto ali seria a ambiguidade que o value +object existe para eliminar. diff --git a/docs/webhooks.md b/docs/webhooks.md new file mode 100644 index 0000000..0cab7be --- /dev/null +++ b/docs/webhooks.md @@ -0,0 +1,47 @@ +# Verificando o webhook que chega + +Cadastrar o endpoint é metade do caminho. A outra é provar que a notificação +veio do gateway: sem isso, quem descobrir a URL confirma uma cobrança. Cada +gateway assina de um jeito, e `webhookSignature()` dá a todos a mesma forma. + +```php +$ok = $gateway->webhookSignature() + ->setSecret($segredoDoWebhook) + ->setPayload(file_get_contents('php://input')) + ->setHeaders(getallheaders()) + ->verify(); + +if (!$ok) { + http_response_code(401); + exit; +} +``` + +`verify()` devolve `bool` — assinatura que não bate é resposta, não erro. O que +levanta `ValidationException` é uso errado: segredo ausente, corpo vazio ou +header de assinatura que não chegou. + +**O corpo precisa ser o cru.** `file_get_contents('php://input')`, nunca o array +decodificado e reserializado: um espaço a mais muda o hash. Em Laravel, +`$request->getContent()`. + +| Gateway | O que é verificado | O que isso prova | +| --- | --- | --- | +| Asaas | token no header `asaas-access-token` | que o remetente conhece o token — o corpo não é assinado | +| PagBank | `x-authenticity-token`: SHA-256 de `{token}-{corpo}` | que o corpo chegou inteiro | +| Mercado Pago | `x-signature`: HMAC-SHA256 de `data.id` + `x-request-id` + `ts` | que a notificação é da sua aplicação e aponta para aquele recurso | +| Woovi/OpenPix | `x-webhook-signature`: RSA-SHA256 do corpo, conferido com a chave pública da Woovi | que o corpo veio da Woovi | +| AbacatePay | `webhookSecret` na query e, com a chave, `X-Webhook-Signature` | que a URL é a sua e, com a chave, que o corpo não mudou | + +No Mercado Pago, informe o `data.id` da query com `setDataId()`: ele entra no +cálculo, e omiti-lo muda o manifesto. Na Woovi, a chave pública é buscada em +`api/v1/webhook/public-keys` na primeira verificação — passe `setPublicKeys()` +para evitar a chamada — e a lista inteira é aceita, porque durante uma rotação +ainda chegam notificações assinadas pela chave anterior. + +Quem **não** declara a capacidade: **Efí**, porque a notificação Pix chega por +mTLS e o certificado já cobre isso; **Cielo** e **Rede**, porque a notificação +não traz assinatura e o caminho que a própria documentação indica é consultar a +transação de volta; e **Pagar.me**, porque a v5 não documenta esquema de +assinatura. Nos quatro, `webhookSignature()` levanta `NotImplementedException` +em vez de devolver um verificador que sempre diz sim. From 885f90ab31eaa9d9c765b59aa275c1f7b3c5f021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Lucas?= Date: Thu, 22 Oct 2026 12:00:30 -0300 Subject: [PATCH 4/4] PHPAY-113: fix(doc): the library covers nine gateways, not ten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retry documentation said ten in both the README section and CLAUDE.md. There are nine: Asaas, Woovi, Efí, Mercado Pago, PagBank, Pagar.me, AbacatePay, Cielo and Rede — nine namespaces in composer.json, nine rows in the capability matrix. Caught by the PHP 8.1 smoke test from PHPAY-111, which says it instantiates the nine gateways. --- CLAUDE.md | 2 +- docs/retry-e-idempotencia.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 961a460..6f9eb3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -187,7 +187,7 @@ quebra a integração, cobra o valor errado. ## Retry e idempotência A `PHPay\Http\RetryPolicy` decide o que pode ser repetido, e o middleware entra na -`ClientFactory` — então os dez gateways herdam a política de uma vez. +`ClientFactory` — então os nove gateways herdam a política de uma vez. - **Repetir é sobre dinheiro, não sobre resiliência.** Um `POST` repetido que o gateway já processou cobra o cliente duas vezes. Só é repetido: leitura diff --git a/docs/retry-e-idempotencia.md b/docs/retry-e-idempotencia.md index 5b55911..9174e1c 100644 --- a/docs/retry-e-idempotencia.md +++ b/docs/retry-e-idempotencia.md @@ -1,7 +1,7 @@ # Retry e idempotência A biblioteca repete uma requisição **só quando repetir não pode cobrar de novo**. -Vale para os dez gateways, porque o middleware vive na +Vale para os nove gateways, porque o middleware vive na `PHPay\Http\ClientFactory`, o único lugar onde um client nasce. | Situação | Repete |