AkiraAkira.dev
3 min de leitura

O Laravel SISP 2.0 quase não trouxe features

Se o pacote não fizer o que a integração precisa, edita-se um array. Foi para isso que serviram 47 commits e 126 ficheiros.

também em EN FR

O adquirente liga a informar que passa a exigir uma verificação antifraude antes do redirect. O checkout está em produção, o pacote de pagamentos é meu, e a verificação tem de entrar entre a blacklist e o rate limiter.

Até à versão 1.x do Laravel SISP, esse telefonema custava uma semana e um fork.

Porque é que custava tanto

O controller de pagamento recebia oito actions:

// src/Http/Controllers/PaymentController.php (1.0.3)
public function __construct(
    private CreateIdempotentPaymentTransactionAction $createPayment,
    private PreparePaymentAction $preparePayment,
    private CreateAndStorePaymentTransactionAction $createTransaction,
    private RenderPaymentFormBasedOnConfigAction $renderForm,
    private CheckRateLimitAction $checkRateLimit,
    private CheckBlacklistAction $checkBlacklist,
    private StoreRequestMetadataAction $storeMetadata,
    private LoadConfig $config,
) {}

Cada uma delas é uma classe como deve ser. O problema está no que não aparece ali: a ordem por que correm. Essa estava escrita à mão dentro do __invoke(), em 81 linhas fechadas a sete chaves. Para meter uma verificação no meio, restava clonar o repositório ou pôr uma rota por cima da minha, à espera de que a idempotência aguentasse.

O que a 2.0 fez com isso

Tirou a ordem de dentro do controller e passou-a para um sítio editável:

// config/sisp.php
'pipelines' => [
    'payment' => [
        Akira\Sisp\Pipelines\Payment\Pipes\EnsureIpIsNotBlacklisted::class,
        Akira\Sisp\Pipelines\Payment\Pipes\EnforceRateLimits::class,
        Akira\Sisp\Pipelines\Payment\Pipes\ApplyPaymentIntent::class,
        Akira\Sisp\Pipelines\Payment\Pipes\BuildPaymentRequest::class,
        Akira\Sisp\Pipelines\Payment\Pipes\PersistTransaction::class,
        Akira\Sisp\Pipelines\Payment\Pipes\CaptureRequestMetadata::class,
    ],
    'callback' => [
        Akira\Sisp\Pipelines\Callback\Pipes\ResolveTransaction::class,
        Akira\Sisp\Pipelines\Callback\Pipes\ValidateFingerprint::class,
        Akira\Sisp\Pipelines\Callback\Pipes\EnsureCallbackMatchesTransaction::class,
        Akira\Sisp\Pipelines\Callback\Pipes\ApplyTransactionStatus::class,
        Akira\Sisp\Pipelines\Callback\Pipes\DispatchPaymentEvents::class,
    ],
],

Onze pipes, ida e volta do fluxo. A tal verificação antifraude passa a ser uma classe que implementa PaymentPipe e uma linha nesse array. O controller ficou em 51 linhas e duas dependências.

Foram 47 commits e 126 ficheiros mexidos, à volta de 4000 linhas acrescentadas e 1400 retiradas, para chegar aqui. Nenhum cliente final vai reparar em nada.

O preço também não se esconde. Aquele array passou a ser contrato público. Já não posso trocar a ordem das etapas numa versão menor, porque agora há pipes de outras equipas entaladas entre as minhas. Costura aberta ao público deixa de ser propriedade de quem a abriu.

A mesma ideia, do lado de quem chama

Pedir um pagamento não devia obrigar ninguém a saber como o pacote arruma as coisas cá dentro:

$paymentRequest = Sisp::payment()
    ->amount(1500.0)
    ->currency('132')
    ->customerEmail('comprador@exemplo.cv')
    ->locale('pt')
    ->build();

$transaction = Sisp::refund($transaction)
    ->amount(500.0)
    ->reason('partial_return')
    ->process();

No 1.x os value objects eram montados à mão, e cada campo renomeado do meu lado aparecia como diff do outro. O RefundBuilder tem quatro métodos: amount(), full(), reason(), process(). É a superfície de reembolso completa, e é pequena de propósito.

O que é que isto parte no upgrade

Abrir ao público uma costura que era privada estraga a vida a quem já tinha dado com ela. A validação de callback passava pela facade Sisp, e os testes trocavam o serviço no container:

// 1.x, deixou de funcionar
app()->instance(\Akira\Sisp\Sisp::class, new class {
    public function validateCallback($payload): bool { return true; }
});

Agora quem está ali é um contrato:

// 2.0
app()->instance(CallbackFingerprintValidator::class, new class implements CallbackFingerprintValidator {
    public function handle(CallbackPayload $payload): bool { return true; }
});

Quem fazia stub de callbacks apanhou a suite toda vermelha no dia do upgrade. Podia ter deixado o caminho antigo vivo ao lado do novo, e não deixei. Havendo duas maneiras de fingir uma validação de fingerprint num pacote de pagamentos, uma delas acaba esquecida em produção. Mais vale que rebente na integração contínua do que num extrato bancário.

”Isto é arquitetura a mais para um redirect”

É a objeção honesta, e seria a minha. O SISP é um formulário submetido e um callback que volta.

Acontece que um pipe é isto:

// src/Pipelines/Payment/Pipes/EnforceRateLimits.php
final readonly class EnforceRateLimits implements PaymentPipe
{
    public function __construct(private CheckRateLimitAction $checkRateLimit) {}

    public function handle(PaymentContext $context, Closure $next): PaymentContext
    {
        $this->checkRateLimit->handle(identifier: $context->request->ip());

        return $next($context);
    }
}

Vinte e duas linhas com namespace e imports. Uma dependência, uma chamada. A abstração pesa menos do que o parágrafo de controller que veio substituir. Não se acrescentou camada nenhuma. Mudou-se uma sequência de um sítio fechado para um sítio aberto.

O resto é manutenção corrente. PHP 8.5, Laravel 13, #[Bind] e #[Singleton] nos contratos, #[Fillable] nos modelos.

Voltando ao telefonema do adquirente: com a 2.0, a resposta sai nesse mesmo dia, com uma classe nova e uma linha de configuração.

partilhar