Add support for custom transfers in pl.ing

This commit is contained in:
2025-04-14 13:43:57 +02:00
parent f3e8bb9c9b
commit e2d3189f55
5 changed files with 85 additions and 9 deletions

View File

@@ -53,7 +53,7 @@ export class Actual {
date: transaction.date, date: transaction.date,
amount: utils.amountToInteger(transaction.amount), amount: utils.amountToInteger(transaction.amount),
notes: transaction.title, notes: transaction.title,
imported_payee: transaction.to, imported_payee: transaction.toDetails,
cleared: false cleared: false
}; };
@@ -129,8 +129,21 @@ export class Actual {
const output: ActualImportResult = { added: [], updated: [], errors: [] }; const output: ActualImportResult = { added: [], updated: [], errors: [] };
if (this.#dryRun) for (const transaction of toImport ){ if (this.#dryRun) {
console.log(`${transaction.imported_payee} ::: ${transaction.notes} ::: ${transaction.amount}`); 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) { else for (const transaction of toImport) {

View File

@@ -84,7 +84,9 @@ export default class extends BaseTransactionParser<ParserConfig> {
return { return {
kind: 'regular', kind: 'regular',
from: transaction.accountName, from: transaction.accountName,
fromDetails: transaction.accountName,
to, to,
toDetails: to,
date: transaction.posted, date: transaction.posted,
title: `${transaction.mCCDescription} (${transaction.originalAmount} ${transaction.currencyDesc}, conv. rate: ${transaction.conversionRate})`, title: `${transaction.mCCDescription} (${transaction.originalAmount} ${transaction.currencyDesc}, conv. rate: ${transaction.conversionRate})`,
amount: -amount, amount: -amount,

View File

@@ -1,7 +1,21 @@
import { Transaction } from "@/types/transaction"; import { Transaction } from "@/types/transaction";
import { ParserConfig } from "@/types/config"; import { ParserConfig, ProfileConfig, ServerConfig } from "@/types/config";
import { BaseTransactionParser } from "../.."; import { BaseTransactionParser } from "../..";
import { mapCombine, parseAmount } from "@/util/parser"; import { mapCombine, parseAmount } from "@/util/parser";
import { js1 } from "@/util/code";
type CustomTransferDefinition = {
to?: string;
when?: string;
};
type CustomTransfer = Required<CustomTransferDefinition> & {
fn: (transaction: Transaction) => boolean;
};
type IngParserConfig = ParserConfig & {
transfers?: CustomTransferDefinition[];
};
type IngTransaction = { type IngTransaction = {
[K in typeof headers[number]]: string; [K in typeof headers[number]]: string;
@@ -36,9 +50,26 @@ const headers = [
'unknown4', 'unknown4',
]; ];
export default class extends BaseTransactionParser<ParserConfig> { export default class extends BaseTransactionParser<IngParserConfig> {
protected requiredFields = []; protected requiredFields = [];
#transactions: ValidatedIngTransaction[] = []; #transactions: ValidatedIngTransaction[] = [];
#transfers: CustomTransfer[] = [];
protected configure(config: Omit<ProfileConfig, "config"> & { 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<Transaction, boolean>(t.when);
return ({ ...t, fn }) as CustomTransfer;
}) ?? [];
}
async pushTransaction(data: string[]): Promise<boolean> { async pushTransaction(data: string[]): Promise<boolean> {
if (data.length !== headers.length) { if (data.length !== headers.length) {
@@ -76,8 +107,24 @@ export default class extends BaseTransactionParser<ParserConfig> {
} }
async reconcile(): Promise<Transaction[]> { async reconcile(): Promise<Transaction[]> {
const transactions = mapCombine(this.#transactions, "transactionNumber", this.#map, this.#combine); return mapCombine(this.#transactions, "transactionNumber", this.#map, this.#combine)
return transactions.reverse(); .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 { #map(transaction: ValidatedIngTransaction): Transaction {
@@ -90,7 +137,9 @@ export default class extends BaseTransactionParser<ParserConfig> {
return { return {
kind: 'regular', kind: 'regular',
from: transaction.account, from: transaction.account,
fromDetails: transaction.account,
to: transaction.contrahentData, to: transaction.contrahentData,
toDetails: transaction.contrahentData,
date: transaction.transactionDate, date: transaction.transactionDate,
title: transaction.title, title: transaction.title,
id: transaction.transactionNumber, id: transaction.transactionNumber,
@@ -130,14 +179,18 @@ export default class extends BaseTransactionParser<ParserConfig> {
// Transaction B => A // Transaction B => A
if (aAmount > 0) { if (aAmount > 0) {
transaction.from = b.account; transaction.from = b.account;
transaction.fromDetails = b.account;
transaction.to = a.account; transaction.to = a.account;
transaction.toDetails = a.account;
transaction.amount = aAmount; transaction.amount = aAmount;
} }
// Transaction A => B // Transaction A => B
else { else {
transaction.from = a.account; transaction.from = a.account;
transaction.fromDetails = a.account;
transaction.to = b.account; transaction.to = b.account;
transaction.toDetails = b.account;
transaction.amount = bAmount; transaction.amount = bAmount;
} }

View File

@@ -3,6 +3,8 @@ export type Transaction = {
date: string; date: string;
from: string; from: string;
to: string; to: string;
fromDetails: string;
toDetails: string;
amount: number; amount: number;
id?: string; id?: string;
title?: string; title?: string;

View File

@@ -2,8 +2,14 @@ const standardContext = {
}; };
export function js1<I, O>(code: string, i1: string = "$", context: Record<string, unknown> = {}): (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<I1, I2, O>(code: string, i1: string, i2: string, context: Record<string, unknown> = {}): (i1: I1, i2: I2) => O { export function js2<I1, I2, O>(code: string, i1: string, i2: string, context: Record<string, unknown> = {}): (i1: I1, i2: I2) => O {
const ctx = { ...standardContext, ...context }; const ctx = { ...standardContext, ...context };
const filter = new Function(i1, i2, ...Object.keys(ctx), code); const fn = new Function(i1, i2, ...Object.keys(ctx), code);
return (i1: I1, i2: I2) => filter(i1, i2, ...Object.values(ctx)); return (i1: I1, i2: I2) => fn(i1, i2, ...Object.values(ctx));
} }