Skip to content

ADR-002 — Adapter Pattern para Integrações Externas

CampoValor
Status✅ Aceito
Data2026-04-30
AutoresEquipe OLP
Implementaçãosupabase/functions/_shared/wasender-whatsapp.ts, payment-gateway.ts, ntfy-helper.ts, hibp-helper.ts, Twilio (via Lovable Connector Gateway)

Contexto

A plataforma integra múltiplos serviços de terceiros:

  • Wasender (WhatsApp) — envio principal de OTP, alertas, faturamento
  • Twilio (SMS) — fallback quando Wasender falha
  • Mercado Pago — checkout e webhooks de assinatura
  • ntfy — alertas push internos para equipe
  • HIBP (Have I Been Pwned) — checagem de senha vazada

Sem padrão, cada integração se espalharia: tokens lidos em cada função, formatos de erro diferentes, troca de provedor (já houve fallback Twilio→Wasender) exigiria editar dezenas de arquivos.

Decisão

Toda chamada HTTP a serviço externo passa por um adapter centralizado em supabase/functions/_shared/<provedor>.ts.

Anatomia obrigatória do adapter

  1. Funções puras — sem side-effect em banco. Persistência fica no consumidor.
  2. Credenciais lidas internamente via Deno.env.get(). Consumidor nunca manipula tokens.
  3. Retorno tipado Result: { success: boolean; data?: T; error?: string; errorCode?: number }.
  4. try/catch em toda chamada fetch — erros de rede nunca escapam como exceção não tratada.
  5. Sem imports de logging-helper ou supabase-client — adapter não sabe nada de banco/log.
  6. Fallback documentado — quando aplicável, no próprio adapter (ex.: enviarMensagemComLog tenta Wasender, cai pra Twilio).

Exemplo canônico

ts
// supabase/functions/_shared/wasender-whatsapp.ts
const WASENDER_API_BASE = "https://www.wasenderapi.com";

export interface EnviarMensagemResult {
  success: boolean;
  msgId?: string;
  provider?: 'wasender' | 'twilio';
  error?: string;
}

export async function enviarMensagemComLog(
  params: EnviarMensagemParams,
): Promise<EnviarMensagemResult> {
  const apiKey = Deno.env.get("WASENDER_API_KEY");
  if (!apiKey) return { success: false, error: "WASENDER_API_KEY ausente" };
  try {
    const res = await fetch(...);
    if (!res.ok) return { success: false, error: `HTTP ${res.status}` };
    return { success: true, msgId: data.id, provider: 'wasender' };
  } catch (err) {
    return { success: false, error: String(err) };
  }
}

Alternativas consideradas

A. Cada Edge Function chama fetch direto

Pros: zero indireção, fácil de ler localmente. Cons: troca de provedor = N PRs; tokens vazam por descuido; tratamento de erro vira inconsistente; impossível mockar para testes. Veredicto: rejeitado — payback do adapter já provado (migração SMS→WhatsApp).

B. SDK oficial do provedor por integração

Pros: ergonomia de tipos. Cons: aumenta bundle Deno; SDKs do MP/Wasender têm bugs em Deno; acopla à API do SDK em vez da nossa interface. Veredicto: rejeitado — fetch + tipos próprios é mais portável.

C. Camada de "Service Bus" interna (fila + worker dedicado)

Pros: resiliência via retry assíncrono. Cons: complexidade não justificada para volume atual; latência de OTP não permite fila. Veredicto: rejeitado por ora — reavaliar quando volume exigir.

Consequências

Positivas

  • Trocar Wasender→Z-API: editar 1 arquivo.
  • Mock de adapter em testes Deno é trivial (enviarMensagemComLog substituível).
  • Credenciais auditáveis: rg "Deno.env.get" _shared/ lista todos os pontos.
  • Rate limits centralizados (messaging-rate-limits.ts) consumidos por todos os adapters.

Negativas

  • Indireção extra para quem está debugando localmente.
  • Adapter cresce se receber muitas operações (mitigado por arquivo separado por provedor).

Trade-offs

EixoCustoBenefício
Indireção+1 arquivo por integraçãoTrocar provedor sem tocar consumidores
Cobertura de testesAdapter precisa ser mockávelMock sobe a 1 arquivo por integração
RastreabilidadeDeno.env.get confinado ao adapter
Curva de aprendizadoDevs precisam saber que existerg _shared/ lista todos

Conformidade

Como verificar:

bash
# Não pode haver fetch para domínio externo fora de _shared/
rg "fetch\(['\"]https?://" supabase/functions --type ts \
  -g '!**/_shared/**'

Sinais de violação:

  • fetch('https://api.<provedor>...') em qualquer função que não seja um adapter
  • Deno.env.get('<PROVIDER>_API_KEY') fora de _shared/

Débito técnico conhecido

Varredura em 2026-04-30:

  • supabase/functions/healthcheck-cron/index.ts:185 — chama https://api.mercadopago.com/v1/payment_methods direto para probe de saúde. Justificável (probe sintético sem credencial sensível) mas idealmente vira pingMercadoPago() no payment-gateway.ts para consistência. Backlog técnico.

Referências