11 de outubro de 2026
TRNAMT: -150.00. O sinal indica se é débito ou crédito.Valor: 150,00 e uma coluna Tipo: Débito. Cabeçalhos próprios e vírgula decimal.PAGAMENTO FORNECEDOR -150,00. Texto livre que precisa ser extraído.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.TransactionImportService.TransactionFileAdapter.OfxAdapter, ExcelAdapter e PdfAdapter.Transaction.type TransactionType = "CREDIT" | "DEBIT";
interface Transaction {
date: string;
description: string;
amount: number;
type: TransactionType;
externalId?: string;
}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.interface TransactionFileAdapter {
parse(content: Buffer): Promise<Transaction[]>;
}DTPOSTED → date, MEMO → description, TRNAMT → amount, o sinal de TRNAMT vira type, e FITID → externalId.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,
}));
}
}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"]),
}));
}
}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",
}));
}
}Transaction[].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;
}
}const adapters: Record<string, TransactionFileAdapter> = {
ofx: new OfxAdapter(),
xlsx: new ExcelAdapter(),
pdf: new PdfAdapter(),
};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);
}$ importFile("ofx", ofxBuffer)
✓ OfxAdapter → Transaction[]
$ importFile("xlsx", excelBuffer)
✓ ExcelAdapter → Transaction[]
$ importFile("csv", csvBuffer)
✗ Error: Formato não suportadoCsvAdapter e registrar no mapa adapters. O serviço, a validação e as regras de negócio não mudam uma linha.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.tsFITID do OFX, mas deduplique no serviço, nunca no adaptador.adapters.