Skip to content

PHPAY-163: Efí — multa, juros e itens da cobrança deixam de ser fixos no código - #164

Merged
mariolucasdev merged 1 commit into
developfrom
fix/phpay-163
Oct 26, 2026
Merged

mariolucasdev merged 1 commit into
developfrom
fix/phpay-163

Conversation

@mariolucasdev

Copy link
Copy Markdown
Collaborator

Closes #163.

O CLAUDE.md registrava que o Charge da Efí tinha $items e $configuration
sem setter. Fui conferir e eram três problemas, dois deles com dinheiro.
Confirmei os três contra um client mockado antes de mexer em nada.

1. Multa e juros eram fixos no código

$configurations = ['fine' => 200, 'interest' => 33];   // sem setter

Toda cobrança da Efí saía com isso. E o que significa não estava em lugar
nenhum — fui à documentação da Efí:

valor significa escala
fine 200 2% após o vencimento percentual × 100
interest 33 0,033% ao dia percentual × 1000

As duas escalas são diferentes, e é a própria Efí que documenta assim:
"neste caso, 200 equivale a 2%" e "33 equivale a 0,033%". É a armadilha que
o Money existe para fechar — errar não quebra a integração, cobra errado. Por
isso os dois são inteiros, e o validador recusa decimal repetindo as duas
escalas
na mensagem: 2,5% de multa é 250.

O default fica, e é decisão

Tirar seria mudança de comportamento numa minor: boleto de integração que
nunca configurou nada pararia de carregar encargo que o lojista pode estar
contando. Ele continua, mas agora é visível e sobrescrevível.

Sem encargo é ['fine' => 0, 'interest' => 0]. Array vazio é recusado — não
diz "sem encargo", e a Efí não documenta se o objeto configurations pode
ser omitido, então não vou shippar um caminho cujo comportamento no servidor não
confirmei. A mensagem aponta a saída que existe.

2. Boleto só podia ter um item

O payload da Efí não tem valor no nível da cobrança — tem items. O
setAmount() com a description é o atalho que monta um item, e era o único
caminho. Agora:

->setItems([
    ['name' => 'Camiseta', 'value' => 4990, 'amount' => 2],
    ['name' => 'Frete',    'value' => 1500],
])

Com itens explícitos, description e value deixam de ser exigidos: eles
pertencem ao atalho, não ao gateway, e exigi-los junto seria pedir campo que não
vai a lugar nenhum.

3. create() duas vezes mentia para quem chamou

O mais afiado. O getItems() mutava o estado:

unset($this->charge['description']);
unset($this->charge['value']);

E a segunda chamada falhava com "O campo description é obrigatório" — culpando
quem chamou por um campo que ele preencheu. Os unset() não serviam para nada: o
payload é montado a partir dos itens e de expire_at. Saíram, com teste de
regressão.

Uma assimetria de propósito

Chave de configurations que esta versão não modela passa adiante, ao
contrário do array de configuração do gateway, que recusa chave desconhecida. Lá
o array é superfície da biblioteca e chave estranha é typo; aqui é payload da
Efí, e recusar um campo que funciona quebraria quem o manda. Está comentado no
validador.

Duas coisas que vale dizer

Adicionei dois métodos ao ChargeInterface da Efí. Isso é tecnicamente
quebra para quem implementa a interface — o mesmo cuidado que manteve o
SupportsWebhookSignature devolvendo object no #146. A diferença: aquela é
interface de capacidade, que gateway de terceiro implementa; esta é interface
de Resource interna do gateway, e EfiGatewayInterface::charge() devolve a
classe concreta, então nada em prática depende de ela ser fechada. Segui o padrão
do repositório, em que todo setter de Resource está na interface dele.

Corrijo uma coisa que eu disse de passagem: cheguei a achar que a criação de
boleto da Efí estava sem cobertura. Está coberta — forma do payload e unidade do
valor, no EfiGatewayTest. O que não tinha teste era configurations, item
múltiplo e reuso do Resource, que é como o bug de mutação sobreviveu.

Fora de escopo, com o motivo

  • Converter percentual (setFine(2.0)): a escala da forma de objeto de
    interest (['value' => 330, 'type' => 'monthly']) não está documentada
    pela Efí. Por analogia pareceria ×1000, mas meia conversão segura é pior que
    nenhuma — então a lib aceita a forma de objeto e não converte nada.
  • discount: não vive em configurations, é irmão de banking_billet.

Verificação

composer test: lint, 770 testes (+27), PHPStan nível 9, 113 links. Smoke
com 97 verificações.

Os 27 cobrem o default que sai, a troca, o boleto sem encargo, o array vazio
recusado, decimal recusado nas duas escalas, a forma de objeto de interest e o
type, o days_to_write_off, campo desconhecido passando, item múltiplo, itens
dispensando o atalho, a validação de item em centavos — e o create() duas
vezes.

Three problems in the same resource, two of them about money. All three were
confirmed against a mocked client before anything was changed.

Fine and interest were hardcoded. getConfigurations() fell back to
['fine' => 200, 'interest' => 33] with no setter, so every billet this
library created carried 2% of fine and 0.033% of interest per day, chosen by
nobody. Efi's own documentation says the two fields use DIFFERENT scales:
fine is percent x 100 and interest is percent x 1000. setConfigurations()
now exists, and the validator refuses a decimal by repeating both scales
rather than letting a wrong value through.

The default stays. Removing it would be a behaviour change in a minor: a
billet from an integration that never configured anything would stop
carrying charges the merchant may be counting on. What changed is that it is
visible and overridable, and ['fine' => 0, 'interest' => 0] bills without
it. An empty array is refused, because it does not say "no charges" and Efi
does not document whether the object can be omitted at all.

A billet could only have one item. setItems() now takes the list, and with
explicit items description and value stop being required: they belong to the
single-item shortcut, not to the gateway, whose payload has no charge-level
amount.

create() twice on the same resource failed, and the message blamed the
caller for a field they had set: getItems() unset description and value
after building the item. The unsets served nothing, since the payload is
assembled from the items and from expire_at. They are gone, with a
regression test.

Unknown `configurations` keys pass through, unlike the gateway config array.
There the array is the library's own surface and a stray key is a typo; here
it is Efi's payload, and refusing a field that works would break whoever
sends one.

Out of scope, with the reason recorded: converting a percentage into Efi's
unit, because the scale of the monthly form of interest is not documented;
and `discount`, which is a sibling of banking_billet and not part of
configurations.
@mariolucasdev
mariolucasdev merged commit f0635ac into develop Oct 26, 2026
8 checks passed
@mariolucasdev
mariolucasdev deleted the fix/phpay-163 branch October 26, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Efí: multa e juros fixos no código, boleto de item único, e create() que muta o estado

1 participant