Back to the blog
Adapter Pattern na prática: importando extratos OFX, Excel e PDF com um único contrato em TypeScript
Como o padrão Adapter (GoF) permite importar extratos bancários em formatos diferentes para um modelo único de transação, sem mexer nas regras de negócio a cada formato novo.
Related project: FlyUp BPOThis article is only available in Portuguese.
Todo sistema financeiro chega no mesmo ponto: o usuário quer importar o extrato do banco. Um banco exporta OFX, outro manda uma planilha Excel, e o terceiro só oferece PDF. A transação é a mesma, mas cada arquivo descreve ela de um jeito diferente.
Neste artigo eu mostro como o Adapter, um padrão estrutural do catálogo GoF (Gang of Four), resolve esse problema com uma ideia simples: o sistema define um contrato, e cada formato ganha um tradutor que cumpre esse contrato. As regras de negócio não precisam saber de onde a transação veio.
Este conteúdo nasceu de uma apresentação que fiz na faculdade (TADS, UDESC) sobre padrões de projeto.
O problema
Olha como a mesma saída de R$ 150,00 aparece em cada formato:
- OFX:
TRNAMT: -150.00. O sinal indica se é débito ou crédito. - Excel:
Valor: 150,00e uma colunaTipo: Débito. Cabeçalhos próprios e vírgula decimal. - PDF:
PAGAMENTO FORNECEDOR -150,00. Texto livre que precisa ser extraído.
Sem um padrão, o serviço de importação vira uma sequência de
if/else por formato. Pior: cada formato novo obriga a mexer nas regras centrais do sistema, justamente o código que deveria ser o mais estável.O conceito
O Adapter faz uma interface incompatível funcionar onde o sistema espera outra. São quatro peças:
- Client: quem usa o resultado. Aqui, o
TransactionImportService. - Target: o contrato que o sistema entende. Aqui, a interface
TransactionFileAdapter. - Adapter: o tradutor.
OfxAdapter,ExcelAdapterePdfAdapter. - Adaptee: o que tem formato próprio. Os arquivos e seus parsers.
O Client usa o Target, o Adapter cumpre o Target e traduz o Adaptee. Todo formato termina no mesmo lugar: uma lista de
Transaction.1. Defina o que o sistema entende
Antes de qualquer adaptador, vem o modelo de domínio. Repare que ele não menciona OFX, Excel nem PDF.
transaction.ts
type TransactionType = "CREDIT" | "DEBIT";
interface Transaction {
date: string;
description: string;
amount: number;
type: TransactionType;
externalId?: string;
}Uma decisão importante:
amount guarda sempre o valor absoluto, e type diz se é crédito ou débito. Isso é uma convenção do domínio, não do padrão, mas é ela que permite que três formatos tão diferentes produzam exatamente o mesmo objeto.2. Defina a regra que todo adaptador cumpre
transaction-file-adapter.ts (Target)
interface TransactionFileAdapter {
parse(content: Buffer): Promise<Transaction[]>;
}É o contrato inteiro: recebe o conteúdo do arquivo e devolve transações normalizadas. Qualquer coisa que cumpra isso pode ser importada.
3. Um adaptador por formato
O adaptador de OFX mapeia campo a campo:
DTPOSTED → date, MEMO → description, TRNAMT → amount, o sinal de TRNAMT vira type, e FITID → externalId.ofx.adapter.ts
class OfxAdapter implements TransactionFileAdapter {
async parse(content: Buffer): Promise<Transaction[]> {
const entries = await parseOfxFile(content);
return entries.map((entry) => ({
date: normalizeOfxDate(entry.DTPOSTED),
description: entry.MEMO,
amount: Math.abs(Number(entry.TRNAMT)),
type: Number(entry.TRNAMT) < 0 ? "DEBIT" : "CREDIT",
externalId: entry.FITID,
}));
}
}O de Excel lê as colunas da planilha e trata a vírgula decimal:
excel.adapter.ts
class ExcelAdapter implements TransactionFileAdapter {
async parse(content: Buffer): Promise<Transaction[]> {
const rows = await parseExcelFile(content);
return rows.map((row) => ({
date: normalizeExcelDate(row["Data"]),
description: row["Histórico"],
amount: Math.abs(parseMoney(row["Valor"])),
type: normalizeTransactionType(row["Tipo"]),
}));
}
}E o de PDF primeiro extrai o texto e depois interpreta as linhas do extrato:
pdf.adapter.ts
class PdfAdapter implements TransactionFileAdapter {
async parse(content: Buffer): Promise<Transaction[]> {
const text = await extractPdfText(content);
const entries = parseStatementLines(text);
return entries.map((entry) => ({
date: normalizePdfDate(entry.date),
description: entry.description,
amount: Math.abs(parseMoney(entry.amount)),
type: parseMoney(entry.amount) < 0 ? "DEBIT" : "CREDIT",
}));
}
}Três entradas completamente diferentes, a mesma saída:
Transaction[].4. Um serviço que não conhece arquivos
Validação, deduplicação e persistência ficam no serviço, fora dos adaptadores. O detalhe que faz tudo funcionar: ele recebe a interface, não uma classe concreta. Por isso aceita qualquer adaptador.
transaction-import-service.ts
class TransactionImportService {
async import(
content: Buffer,
adapter: TransactionFileAdapter,
): Promise<Transaction[]> {
const transactions = await adapter.parse(content);
this.validate(transactions);
// deduplicação, regras de negócio, persistência
return transactions;
}
}5. Escolhendo o adaptador
Para selecionar o adaptador pela extensão do arquivo, um objeto simples resolve. Não é preciso uma Factory completa:
adapter-resolver.ts
const adapters: Record<string, TransactionFileAdapter> = {
ofx: new OfxAdapter(),
xlsx: new ExcelAdapter(),
pdf: new PdfAdapter(),
};import-file.ts
async function importFile(ext: string, content: Buffer) {
const adapter = adapters[ext.toLowerCase()];
if (!adapter) {
throw new Error("Formato não suportado");
}
const service = new TransactionImportService();
return service.import(content, adapter);
}terminal
$ importFile("ofx", ofxBuffer)
✓ OfxAdapter → Transaction[]
$ importFile("xlsx", excelBuffer)
✓ ExcelAdapter → Transaction[]
$ importFile("csv", csvBuffer)
✗ Error: Formato não suportadoE aqui está o ganho real: para aceitar CSV, basta criar um
CsvAdapter e registrar no mapa adapters. O serviço, a validação e as regras de negócio não mudam uma linha.O fluxo completo
- Upload e validação do arquivo
- Resolver o adaptador pela extensão
- O adaptador traduz o formato
- Modelo normalizado (Transaction[])
- Validação, duplicidades e gravação no serviço
Uma organização de pastas que deixa essas fronteiras claras:
src/transactions/
import/parsers/ → lê o arquivo (SheetJS, PDF.js...)
ofx.parser.ts · excel.parser.ts · pdf.parser.ts
import/adapters/ → traduz cada formato para o modelo comum
ofx.adapter.ts · excel.adapter.ts · pdf.adapter.ts
domain/ → validação, deduplicação e gravação
transaction-import-service.tsCuidados que fazem diferença em produção
O Adapter só faz a tradução técnica. Os problemas reais de importação aparecem em outros lugares:
- Duplicidades são regra do domínio: use o
FITIDdo OFX, mas deduplique no serviço, nunca no adaptador. - Valores: trabalhe com centavos inteiros ou uma biblioteca decimal. Ponto flutuante e dinheiro não combinam.
- Datas: deixe explícitos o fuso horário e os formatos aceitos.
- PDFs: a extração é frágil. Mostre ao usuário o que foi lido para ele revisar antes de gravar.
- Erros parciais: diga quais linhas falharam e por quê, em vez de rejeitar o arquivo inteiro.
- Segurança: valide o conteúdo real do arquivo (não só a extensão) e limite o tamanho.
Adapter, Factory e Strategy
Esses três padrões costumam aparecer juntos, e vale separar o papel de cada um:
- Adapter: converte um formato para o modelo comum. No projeto, os adaptadores de OFX, Excel e PDF.
- Factory: cria ou seleciona o adaptador certo. No projeto, o mapa
adapters. - Strategy: troca um algoritmo por outro. No projeto, as regras de duplicidade.
Conclusão
Uma interface, um adaptador por formato e um serviço que não sabe nada sobre arquivos. Com isso, o sistema aceita formatos novos sem que as regras centrais mudem.
O Adapter não é sofisticado, e esse é o ponto. Ele coloca a sujeira de cada formato na borda do sistema, onde ela pode ser isolada e testada, e mantém o domínio falando uma língua só.
Se este artigo te ajudou, compartilhe com quem está lutando com importação de arquivos. Dúvidas ou comentários, é só me chamar! 🚀