From e2d3189f55fb4775ffa0b35406d6fbdd388d0f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Mon, 14 Apr 2025 13:43:57 +0200 Subject: [PATCH] Add support for custom transfers in pl.ing --- src/backend/index.ts | 19 ++++++++++-- src/parser/pl/amex/index.ts | 2 ++ src/parser/pl/ing/index.ts | 61 ++++++++++++++++++++++++++++++++++--- src/types/transaction.ts | 2 ++ src/util/code.ts | 10 ++++-- 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index fb8b5b8..9d37eec 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -53,7 +53,7 @@ export class Actual { date: transaction.date, amount: utils.amountToInteger(transaction.amount), notes: transaction.title, - imported_payee: transaction.to, + imported_payee: transaction.toDetails, cleared: false }; @@ -129,8 +129,21 @@ export class Actual { const output: ActualImportResult = { added: [], updated: [], errors: [] }; - if (this.#dryRun) for (const transaction of toImport ){ - console.log(`${transaction.imported_payee} ::: ${transaction.notes} ::: ${transaction.amount}`); + if (this.#dryRun) { + const data: { value: (t: ActualTransaction) => string|undefined, header: string, pad: number }[] = [ + { value: t => t.payee !== undefined ? "Yes" : "No", header: "Known payee", pad: 12 }, + { value: t => t.imported_payee, header: "Imported payee", pad: 70 }, + { value: t => t.amount?.toString(), header: "Amount", pad: 10 }, + { value: t => t.notes, header: "Notes", pad: 100 } + ]; + + const header = data.map(d => d.header.padEnd(d.pad)).join(" "); + const rows = toImport.map(transaction => data.map(d => (d.value(transaction) ?? "").padEnd(d.pad)).join(" ")); + + console.log(header); + console.log("-".repeat(Math.max(...rows.map(x => x.length)) ?? header.length)); + rows.forEach(x => console.log(x)); + } else for (const transaction of toImport) { diff --git a/src/parser/pl/amex/index.ts b/src/parser/pl/amex/index.ts index fbc1e41..658e219 100644 --- a/src/parser/pl/amex/index.ts +++ b/src/parser/pl/amex/index.ts @@ -84,7 +84,9 @@ export default class extends BaseTransactionParser { return { kind: 'regular', from: transaction.accountName, + fromDetails: transaction.accountName, to, + toDetails: to, date: transaction.posted, title: `${transaction.mCCDescription} (${transaction.originalAmount} ${transaction.currencyDesc}, conv. rate: ${transaction.conversionRate})`, amount: -amount, diff --git a/src/parser/pl/ing/index.ts b/src/parser/pl/ing/index.ts index 3e2ae39..1b4e450 100644 --- a/src/parser/pl/ing/index.ts +++ b/src/parser/pl/ing/index.ts @@ -1,7 +1,21 @@ import { Transaction } from "@/types/transaction"; -import { ParserConfig } from "@/types/config"; +import { ParserConfig, ProfileConfig, ServerConfig } from "@/types/config"; import { BaseTransactionParser } from "../.."; import { mapCombine, parseAmount } from "@/util/parser"; +import { js1 } from "@/util/code"; + +type CustomTransferDefinition = { + to?: string; + when?: string; +}; + +type CustomTransfer = Required & { + fn: (transaction: Transaction) => boolean; +}; + +type IngParserConfig = ParserConfig & { + transfers?: CustomTransferDefinition[]; +}; type IngTransaction = { [K in typeof headers[number]]: string; @@ -36,9 +50,26 @@ const headers = [ 'unknown4', ]; -export default class extends BaseTransactionParser { +export default class extends BaseTransactionParser { protected requiredFields = []; #transactions: ValidatedIngTransaction[] = []; + #transfers: CustomTransfer[] = []; + + protected configure(config: Omit & { config: IngParserConfig }, serverConfig: ServerConfig): void { + this.#transfers = config.config.transfers?.map(t => { + if (!t.to) { + throw new Error(`Undefined 'to' attribute for pl.ing transfer: ${JSON.stringify(t)}`); + } + + if (!t.when) { + throw new Error(`Undefined 'when' condition for pl.ing transfer: ${JSON.stringify(t)}`); + } + + const fn = js1(t.when); + + return ({ ...t, fn }) as CustomTransfer; + }) ?? []; + } async pushTransaction(data: string[]): Promise { if (data.length !== headers.length) { @@ -76,8 +107,24 @@ export default class extends BaseTransactionParser { } async reconcile(): Promise { - const transactions = mapCombine(this.#transactions, "transactionNumber", this.#map, this.#combine); - return transactions.reverse(); + return mapCombine(this.#transactions, "transactionNumber", this.#map, this.#combine) + .map(this.#mapCustomTransfers.bind(this)) + .reverse(); + } + + #mapCustomTransfers(transaction: Transaction): Transaction { + const transfer = this.#transfers.find(transfer => transfer.fn(transaction)); + + if (transfer) { + return { + ...transaction, + kind: 'transfer', + to: transfer.to, + amount: -transaction.amount, + }; + } + + return transaction; } #map(transaction: ValidatedIngTransaction): Transaction { @@ -90,7 +137,9 @@ export default class extends BaseTransactionParser { return { kind: 'regular', from: transaction.account, + fromDetails: transaction.account, to: transaction.contrahentData, + toDetails: transaction.contrahentData, date: transaction.transactionDate, title: transaction.title, id: transaction.transactionNumber, @@ -130,14 +179,18 @@ export default class extends BaseTransactionParser { // Transaction B => A if (aAmount > 0) { transaction.from = b.account; + transaction.fromDetails = b.account; transaction.to = a.account; + transaction.toDetails = a.account; transaction.amount = aAmount; } // Transaction A => B else { transaction.from = a.account; + transaction.fromDetails = a.account; transaction.to = b.account; + transaction.toDetails = b.account; transaction.amount = bAmount; } diff --git a/src/types/transaction.ts b/src/types/transaction.ts index f9c2ac0..d7ce3c4 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -3,6 +3,8 @@ export type Transaction = { date: string; from: string; to: string; + fromDetails: string; + toDetails: string; amount: number; id?: string; title?: string; diff --git a/src/util/code.ts b/src/util/code.ts index 66f49fb..e92ff99 100644 --- a/src/util/code.ts +++ b/src/util/code.ts @@ -2,8 +2,14 @@ const standardContext = { }; +export function js1(code: string, i1: string = "$", context: Record = {}): (i: I) => O { + const ctx = { ...standardContext, ...context }; + const fn = new Function(i1, ...Object.keys(ctx), code); + return (i: I) => fn(i, ...Object.values(ctx)); +} + export function js2(code: string, i1: string, i2: string, context: Record = {}): (i1: I1, i2: I2) => O { const ctx = { ...standardContext, ...context }; - const filter = new Function(i1, i2, ...Object.keys(ctx), code); - return (i1: I1, i2: I2) => filter(i1, i2, ...Object.values(ctx)); + const fn = new Function(i1, i2, ...Object.keys(ctx), code); + return (i1: I1, i2: I2) => fn(i1, i2, ...Object.values(ctx)); } \ No newline at end of file