Skip to content

PHPAY-173: feat: describe a charge once, for more than one gateway - #175

Merged
mariolucasdev merged 1 commit into
developfrom
feat/phpay-173
Oct 26, 2026
Merged

mariolucasdev merged 1 commit into
developfrom
feat/phpay-173

Conversation

@mariolucasdev

Copy link
Copy Markdown
Collaborator

Fase 1 de #173, com as três decisões do desenho fechadas como combinado.

O que entra

peça onde
Method (pix, boleto, crédito, débito) src/Support/Method.php
ChargeIntent src/Support/ChargeIntent.php
SupportsChargeIntents src/Contracts/SupportsChargeIntents.php
Capability::CHARGE_INTENTS src/Contracts/Capability.php
createCharge() e supportedMethods() na facade src/PHPay.php
Asaas, Woovi, Mercado Pago, Inter fromIntent() + os dois métodos

A prova

O mesmo ChargeIntent nos três primeiros, e o corpo que de fato sai:

gateway value
Asaas 149.9 — reais
Woovi 14990 — centavos
Inter {"original": "149.90"} — reais em string, o terceiro formato

E o mesmo Customer vira cpfCnpj, taxID, payer.identification.number e
devedor.cpf. Tudo asseverado sobre o corpo da requisição, não sobre o
retorno.

As três decisões, implementadas

1. supportedMethods() numa interface nova. SupportsCharges fica
intacta — acrescentar método a interface publicada quebraria quem a implementa
fora do repositório. A forma é a do SupportsRefunds: estende
SupportsCharges, declara operação, tem case na Capability e guard na
facade.

2. A Efí fica para a fase 3, e entra roteando por dentro: lá charge() é
boleto e pixCharge() é a cobrança Pix, e será o createCharge() a decidir.
Confirmei por que a alternativa está fora: Efi::charge(): Charge é tipado
concreto, então despachar por método tornaria o retorno Charge|PixCharge e
quebraria todo Charge $c = $efi->charge(); existente.

3. A chave Pix do Inter foi para a configuração (pixKey em
Gateway::INTER->options()), no construtor depois de $client. A posição
é feia e é deliberada: $client é o sétimo parâmetro, e inserir antes
quebraria construção posicional. setKey() continua para quem atende mais de
uma chave.

Uma decisão que o desenho não explicitou

supportedMethods() devolve o que createCharge() aceita hoje, não o
catálogo do gateway. O Asaas vende boleto e cartão por setCharge(), e eles
não aparecem ali — declarar um meio que a chamada recusaria é o mesmo erro
que declarar capacidade e lançar de dentro dela, que é a primeira regra do
CLAUDE.md. Está no PHPDoc da interface e na documentação.

Vencimento: recusar em vez de converter

A Woovi expira por expiresIn (segundos) e o Mercado Pago por
date_of_expiration (data e hora com fuso). Converter um dueDate exigiria
assumir um fuso horário, e a biblioteca não assume nenhum em lugar nenhum —
então os dois recusam, com a mensagem apontando o campo nativo via
withExtra(). O Asaas é o inverso: exige dueDate mesmo no Pix, e a
recusa diz que é limite do gateway.

O que não foi tocado

setCharge(array) segue intacto e segue sendo o caminho do payload cru. São
dois caminhos convivendo, e isso é custo permanente de documentação — é o
preço de não quebrar a v2, e está dito em docs/cobranca-portavel.md.

Portão

  • 800 testes (+27), 2219 asserções
  • PHPStan nível 9 limpo
  • a matriz do README se regerou sozinha (10 gateways, 9 capacidades) — foi
    para isso que o gerador existe
  • o smoke test do job minimum ganhou o ramo da capacidade nova: 105
    verificações, e ele exercita os dois sentidos sem rede
  • quatro asserções de contagem foram atualizadas (toHaveCount(8) → 9, a
    lista do Mercado Pago, as options() do Inter). Nenhuma era defeito: são as
    afirmações que o projeto planta de propósito para pegar case novo, e
    pegaram.

Fase 2 e 3

Ficam em #173. A 2 é comItens() para PagBank, Pagar.me e AbacatePay, que
montam cobrança por itens e não por valor; a 3 é boleto(), cartao() e um VO
Card, que traz PAN e merece revisão própria.

ChargeIntent is to a charge what Customer is to the payer: one
description, and each gateway translates it in its own
*ChargeRequest::fromIntent(), where the knowledge of that schema already
lives.

SupportsChargeIntents declares an operation rather than a resource
getter, the shape SupportsRefunds already uses. It carries two methods
because SupportsCharges answers 'does this gateway charge?' and never
'does it charge that way?' - all ten declare charges and even so the
Rede only does credit, so supportedMethods() is what keeps a Pix charge
from being handed to a gateway that cannot do Pix.

supportedMethods() describes the portable path, not the gateway's
catalogue: the Asaas sells billets through setCharge() and they are
absent here, because declaring a method the call would refuse is the
same mistake as declaring a capability and throwing from inside it.

Where a gateway expires a charge by duration instead of by date, a due
date is refused rather than converted. The Woovi takes expiresIn in
seconds and the Mercado Pago takes date_of_expiration with an offset;
converting would mean assuming a timezone, and the library assumes none
anywhere.

The Inter's Pix key moved to the gateway configuration, since the same
key answers for every charge of that clientId. It sits after $client in
the constructor because inserting it earlier would break positional
construction.

setCharge(array) is untouched and stays the way to send that gateway's
raw payload.
@mariolucasdev
mariolucasdev merged commit 6a92da1 into develop Oct 26, 2026
8 checks passed
@mariolucasdev
mariolucasdev deleted the feat/phpay-173 branch October 26, 2026 21:52
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.

1 participant