Uma interface, quatro parceiros, uma resposta só
Toda integração de parceiro começa igual: “é só um cliente HTTP, faço num service e pronto”. Aí entra o segundo parceiro. Depois o terceiro. E quando você percebe, o Controller virou um if/elseif gigante decidindo qual API chamar, qual campo ler, qual formato devolver. Cada parceiro novo é uma cirurgia de risco no código que já existe.
Eu vivo isso num projeto de cashback que conversa com quatro parceiros diferentes ao mesmo tempo (vou chamá-los aqui de FidelidadeA, FidelidadeB, FidelidadeC e FidelidadeD). Quatro APIs, quatro autenticações, quatro formatos de resposta — e um único frontend de PDV que não quer saber de nada disso. Ele manda um cupom e espera uma resposta só, sempre no mesmo shape.
A solução não tem nada de mágico. É interface + factory + um normalizador de saída, tudo amarrado pelo container do Laravel. Mas a diferença entre fazer isso e não fazer é a diferença entre adicionar um parceiro novo em uma tarde ou em uma sprint inteira. Bora destrinchar.
O problema real: cada ponta de entrada é um mundo
Pensa no que cada parceiro exige na prática:
- FidelidadeA usa OAuth (
client_credentials), devolveredeemables, tem PIN criptografado em AES que precisa ser descriptografado. - FidelidadeB separa resposta em
discountseproducts, temredeemType, identifica cliente poruniqueId. - FidelidadeC trabalha com
gift, percentual vs. valor fixo, lógica de cashback acumulado. - FidelidadeD tem
balance,card_number, data de expiração em outro formato.
Cada um manda CPF num campo diferente, chama valor de um jeito, estrutura “o que o cliente pode usar” de outra forma. Se isso vaza pro resto do sistema, todo lugar que consome cashback precisa conhecer os quatro. É o famoso acoplamento que te persegue: muda a API de um parceiro e você reza pra não ter esquecido nenhum if.
O segredo é traçar uma linha. De um lado, o caos específico de cada parceiro. Do outro, o sistema que só fala uma língua. A interface é essa linha.
O contrato: uma interface que define a língua comum
namespace App\Cashback\V1\Contracts;
use App\Models\CashBackAuth;
interface CashbackInterface
{
public function auth(): ?CashBackAuth;
public function search(array $data): ?array;
public function validate(array $data): ?array;
public function confirm(array $data): ?array;
public function refund(array $data): ?array;
public function addCashback(array $data): ?array;
public function benefits(array $data): ?array;
}
Sete métodos. É o ciclo de vida inteiro de um cashback: autentica, busca, valida, confirma, estorna. Não importa se por baixo é OAuth da FidelidadeA ou token simples da FidelidadeD — quem consome só conhece esses sete verbos.
Cada parceiro implementa o contrato do seu jeito:
class FidelidadeACashback implements CashbackInterface
{
use NormalizeResultCashback;
public function __construct(
private CashBackConfig $cashBackConfig,
private CashBackAuthRepositoryInterface $authRepository
) {}
public function search(array $data): ?array
{
// ... chama a API do parceiro, com retry em timeout
return $this->prepareResponseData($resultData, $data);
}
// confirm(), refund(), auth()... cada um com a regra da FidelidadeA
}
O FidelidadeBCashback, FidelidadeCCashback e FidelidadeDCashback seguem a mesma assinatura, cada um com a doideira da própria API encapsulada lá dentro. O caos não some — ele só fica trancado em uma classe, sem vazar.
A factory: traduzindo “tipo” em implementação
Beleza, mas quem decide se hoje é FidelidadeA ou FidelidadeB? Essa decisão tem que morar em um lugar só. Esse lugar é a factory:
class CashbackFactory
{
public function __construct(
private CashBackAuthRepositoryInterface $authRepository
) {}
public function make(CashBackConfig $cashBackConfig): CashbackInterface
{
return match (strtolower($cashBackConfig->tipo)) {
'fidelidade_a' => new FidelidadeACashback($cashBackConfig, $this->authRepository),
'fidelidade_b' => new FidelidadeBCashback($cashBackConfig, $this->authRepository),
'fidelidade_c' => new FidelidadeCCashback($cashBackConfig, $this->authRepository),
'fidelidade_d' => new FidelidadeDCashback($cashBackConfig, $this->authRepository),
default => throw new InvalidArgumentException("Empresa '{$cashBackConfig->tipo}' não suportada."),
};
}
}
Repara que o retorno é CashbackInterface, não a classe concreta. Quem chama make() recebe “um cashback” e ponto — não sabe nem quer saber qual. E aquele default com exceção é ouro: parceiro não configurado falha na hora, alto e claro, em vez de devolver null silencioso que vira bug três camadas adiante.
O match do PHP 8 deixa isso lindo de ler. Adicionar parceiro? Uma linha nova. É o único ponto do sistema que conhece todos os tipos.
Onde o Laravel brilha: o container fazendo o trabalho sujo
Aqui é onde quem vem de Laravel sorri. No AppServiceProvider, você amarra os contratos às implementações:
// Repository bindings
$this->app->bind(CashBackAuthRepositoryInterface::class, CashBackAuthRepository::class);
// Service bindings
$this->app->bind(SearchCupomServiceInterface::class, SearchCupomService::class);
// Factory
$this->app->bind('App\Cashback\V1\CashbackFactory', CashbackFactory::class);
Olha o detalhe esperto: a CashbackFactory depende de CashBackAuthRepositoryInterface no construtor. Você nunca instancia o repository na mão — o container resolve sozinho quando pede a factory. É a injeção de dependência trabalhando de graça pra você.
E no controller, a coisa fica fluida:
protected function buildSearchCupomUseCase(CashBackConfig $cashbackConfig, string $token): SearchCupomUseCase
{
$cashbackRepository = app()
->make('App\Cashback\V1\CashbackFactory')
->make($cashbackConfig);
$searchCupomService = app()->make(SearchCupomServiceInterface::class, [
'repository' => $cashbackRepository,
'token' => $token,
]);
return new SearchCupomUseCase($searchCupomService);
}
O service recebe um CashbackInterface via repository e processa. Ele não tem ideia se é FidelidadeA ou FidelidadeB — só chama search(). O parâmetro tipo veio da config do lojista no banco; o resto se monta sozinho. Esse é o ponto que vale gravar: a ponta de entrada (o tipo do parceiro) é dado, não é código. Trocar de parceiro é mudar uma linha numa tabela, não fazer deploy.
A saída normalizada: o NormalizeResultCashback
Falta a outra ponta. As quatro APIs respondem em formatos diferentes, mas o PDV precisa de um shape só. É aí que entra a trait que cada implementação usa:
public function normalizeCashbackResult(array $normalizedData, $originalData): array
{
$tipo = $normalizedData['tipo'] ?? 'unknown';
$data = array_merge($custom, $normalizedData, $originalData);
return [
'original_data' => $myOriginalData, // resposta crua, pra debug e auditoria
'normalized_data' => match ($tipo) {
'fidelidade_a' => $this->normalizeFidelidadeAResult($data),
'fidelidade_b' => $this->normalizeFidelidadeBResult($data),
'fidelidade_c' => $this->normalizeFidelidadeCResult($data),
'fidelidade_d' => $this->normalizeFidelidadeDResult($data),
default => $this->normalizeDefaultResult($data),
},
'type' => $tipo,
'processed_at' => now()->toISOString(),
];
}
Cada normalizeXResult() pega a bagunça do parceiro e devolve sempre as mesmas chaves: customer_data, balance, redeemables, provider_data, card_data, store_data. O normalizeFidelidadeAResult, por exemplo, ainda descriptografa o PIN AES antes de montar a saída — toda a sujeira mora aqui, isolada.
Dois detalhes que valem por si só:
original_datasempre junto. Você normaliza pra fora, mas guarda o cru. Quando o parceiro jura que mandou o valor certo e o cliente diz que não recebeu, você tem a resposta original pra provar quem está certo. Em integração de pagamento isso não é luxo, é sobrevivência.defaultnomatch. Parceiro novo que ainda não tem normalizador não quebra a aplicação — cai num formato padrão. Degrada com elegância em vez de explodir.
Os ganhos, sem enrolação
Vale a pena listar o que essa estrutura compra na prática:
- Parceiro novo = um arquivo + duas linhas. Cria a classe
implements CashbackInterface, adiciona umcasena factory e umnormalizeXResult(). Zero alteração no que já roda. - O resto do sistema é imutável. Controllers, services e use cases falam com a interface. Mudou a API da FidelidadeA? O estrago fica dentro de
FidelidadeACashback. - Testável de verdade. Como tudo depende de
CashbackInterface, você dá mock nela e testa o fluxo sem bater em nenhuma API externa. (Temtests/Unit/NormalizeResultCashbackTest.phpjustamente garantindo a saída.) - Onboarding mais rápido. Dev novo entende “tem um contrato, tem implementações, tem uma factory” em cinco minutos. Não precisa ler 2 mil linhas de
if. - Saída previsível. Quem consome a API confia que
normalized_dataé sempre igual. Contrato de saída estável é o que deixa o frontend dormir tranquilo.
Não tem padrão exótico aqui. É interface (o contrato), factory (a escolha em um lugar só) e um normalizador (a saída única), costurados pelo container do Laravel. A graça é o que isso elimina: o acoplamento entre “de onde o dado vem” e “como o sistema usa o dado”.
Quando você tem várias pontas de entrada possíveis — parceiros de pagamento, gateways, provedores de qualquer coisa —, o reflexo certo é desenhar o contrato primeiro e empurrar a bagunça pra dentro das implementações. O dia em que chegar o quinto parceiro, você vai agradecer: vai ser uma tarde, não uma sprint. E o melhor, sem medo de quebrar o que já está em produção.
A pergunta que eu sempre faço antes de codar uma integração: “se amanhã aparecer mais um desses, quanto do meu código eu vou ter que mexer?”. Se a resposta for “muito”, a abstração ainda não está no lugar.


