ADR-002 — Adapter Pattern para Integrações Externas
| Campo | Valor |
|---|---|
| Status | ✅ Aceito |
| Data | 2026-04-30 |
| Autores | Equipe OLP |
| Implementação | supabase/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
- Funções puras — sem side-effect em banco. Persistência fica no consumidor.
- Credenciais lidas internamente via
Deno.env.get(). Consumidor nunca manipula tokens. - Retorno tipado
Result:{ success: boolean; data?: T; error?: string; errorCode?: number }. try/catchem toda chamadafetch— erros de rede nunca escapam como exceção não tratada.- Sem imports de
logging-helperousupabase-client— adapter não sabe nada de banco/log. - Fallback documentado — quando aplicável, no próprio adapter (ex.:
enviarMensagemComLogtenta Wasender, cai pra Twilio).
Exemplo canônico
// 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 (
enviarMensagemComLogsubstituí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
| Eixo | Custo | Benefício |
|---|---|---|
| Indireção | +1 arquivo por integração | Trocar provedor sem tocar consumidores |
| Cobertura de testes | Adapter precisa ser mockável | Mock sobe a 1 arquivo por integração |
| Rastreabilidade | — | Deno.env.get confinado ao adapter |
| Curva de aprendizado | Devs precisam saber que existe | rg _shared/ lista todos |
Conformidade
Como verificar:
# 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 adapterDeno.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— chamahttps://api.mercadopago.com/v1/payment_methodsdireto para probe de saúde. Justificável (probe sintético sem credencial sensível) mas idealmente virapingMercadoPago()nopayment-gateway.tspara consistência. Backlog técnico.
Referências
supabase/functions/_shared/wasender-whatsapp.tssupabase/functions/_shared/payment-gateway.tsdocs/architecture/THIRD_PARTY_INTEGRATIONS.md— guia operacional completodocs/architecture/MESSAGING_RATE_LIMITS.mdmem://architecture/third-party-integration-standard- ADR-006 — separação adapter ↔ logging