From eb934d750b02b62620b9fcadcf8c44c9e4049806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Tue, 1 Apr 2025 13:53:58 +0200 Subject: [PATCH] =?UTF-8?q?Implement=20scaffolding=20of=20parsers=20and=20?= =?UTF-8?q?ING=20Bank=20=C5=9Al=C4=85ski=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/base.ts | 35 +++++++++++++++++++++ src/parser/index.ts | 24 ++++++++++++++ src/parser/pl/ing/index.ts | 64 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 src/parser/base.ts create mode 100644 src/parser/index.ts create mode 100644 src/parser/pl/ing/index.ts diff --git a/src/parser/base.ts b/src/parser/base.ts new file mode 100644 index 0000000..198afcd --- /dev/null +++ b/src/parser/base.ts @@ -0,0 +1,35 @@ +import { Transaction } from "@/types/transaction"; +import { ProfileConfig, ParserConfig } from "@/types/config"; + +export abstract class BaseTransactionParser { + public readonly name: string; + protected abstract requiredFields: readonly (keyof C)[]; + abstract parse(config: C, data: string[]): Promise; + + constructor(name: string) { + this.name = name; + } + + #validate(config: Partial|undefined): C { + for (const field of this.requiredFields) { + if (config?.[field] === undefined) { + throw new Error(`The '${String(field)}' configuration field of '${this.name}' parser is required`) + } + } + + return config as C; + } + + public async parseTransaction(config: ProfileConfig, data: string[]): Promise { + const cfg = config?.config as Partial | undefined; + return this.parse(this.#validate(cfg), data); + } + + protected parseAmount(input?: string): number|undefined { + if (input === undefined) { + return undefined; + } + + return Number.parseFloat(input.replaceAll(",", ".")); + } +} \ No newline at end of file diff --git a/src/parser/index.ts b/src/parser/index.ts new file mode 100644 index 0000000..a44e61c --- /dev/null +++ b/src/parser/index.ts @@ -0,0 +1,24 @@ +export { BaseTransactionParser } from "./base"; +import { ProfileConfig, ParserConfig } from "@/types/config"; +import { BaseTransactionParser } from "./base"; +import { default as PlIng } from "./pl/ing"; +import { Transaction } from "@/types/transaction"; + +type Constructor> = new (name: string) => T; + +const PARSERS: Record>> = { + "pl.ing": PlIng +}; + +export async function parseTransaction(parserName: string, config: ProfileConfig, data: string[]): Promise { + const Parser = PARSERS[parserName]; + + if (!Parser) { + throw new Error(`Unknown parser: ${parserName}`); + } + + const parser = new Parser(parserName); + + return parser.parseTransaction(config, data); +} + diff --git a/src/parser/pl/ing/index.ts b/src/parser/pl/ing/index.ts new file mode 100644 index 0000000..3a10762 --- /dev/null +++ b/src/parser/pl/ing/index.ts @@ -0,0 +1,64 @@ +import { Transaction } from "@/types/transaction"; +import { ParserConfig } from "@/types/config"; +import { BaseTransactionParser } from "../.."; +import { Parser } from "papaparse"; + +const headers = [ + 'transactionDate', + 'accountingDate', + 'contrahentData', + 'title', + 'accountNumber', + 'bankName', + 'details', + 'transactionNumber', + 'transactionAmount', + 'transactionCurrency', + 'lockAmount', + 'lockCurrency', + 'foreignAmount', + 'foreignCurrency', + 'account', + 'bilanceAfterTransaction', + 'accountCurrency', + 'unknown1', + 'unknown2', + 'unknown3', + 'unknown4', +]; + +type IngTransaction = { + [K in typeof headers[number]]: string; +}; + +const readIngTransaction = (data: string[]): IngTransaction|undefined => { + if (data.length !== headers.length) { + return; + } + + const transaction: IngTransaction = {}; + headers.forEach((key, index) => { + transaction[key] = data[index]; + }); + + return transaction; +}; + +export default class extends BaseTransactionParser { + protected requiredFields = []; + + async parse(config: ParserConfig, data: string[]): Promise { + const ing = readIngTransaction(data); + + if (!ing) { + return undefined; + } + + return { + account: "TODO: unknown account (not supported yet)", + date: ing.transactionDate, + amount: this.parseAmount(ing.transactionAmount), + imported_payee: ing.contrahentData?.trim()?.replaceAll(/\s+/g, " ") + } + } +} \ No newline at end of file