mt940-mbank-ts/src/index.ts

187 lines
4.3 KiB
TypeScript

import {
addLineNumbers,
chunks,
fillWithEmpty,
removeRepeatingSpace,
} from "./utils";
class Account {
constructor(
public number: string,
public initial_balance: number,
public closing_balance: number,
public currency: string
) {}
}
class Transaction {
constructor(
public acc_date: Date,
public op_date: Date,
public description: string,
public title: string,
public person: string,
public account_number: string,
public amount: number,
public balance_after: number
) {}
public static counter = (function* () {
let c = 1;
while (true) {
yield c.toString().padStart(11, "0");
c++;
}
})();
formatTitle() {
return addLineNumbers(
fillWithEmpty(chunks(this.title.trim(), 27), 9),
20
).join("\n");
}
formatPerson() {
return addLineNumbers(
fillWithEmpty(
chunks(removeRepeatingSpace(this.person).trim(), 27),
2
).slice(0, 2),
32
).join("\n");
}
toMT940() {
const prefixes = { incoming: "150", outcoming: "169" };
const prefix = this.amount > 0 ? prefixes.incoming : prefixes.outcoming;
return `:61:${mtDate(this.acc_date)}${mtDate(this.op_date).slice(
2
)}C${mtAmount(this.amount)}S${prefix}${Transaction.counter.next().value}
:86:${prefix}
:86:${prefix}~00B${prefix}${this.description.slice(0, 23)}
${this.formatTitle()}
~29${this.account_number}
~30${this.account_number.slice(2, 10)}
~31${this.account_number.slice(10)}
${this.formatPerson()}
~34${prefix}
~38PL${this.account_number}
~62
~63`;
}
}
class Range {
constructor(public date_start: Date, public date_end: Date) {}
}
abstract class CSVParser {
abstract read_csv(content_utf8: string): {
account: Account;
range: Range;
transactions: Transaction[];
};
}
class mBankParser extends CSVParser {
read_csv(content_utf8: string): {
account: Account;
range: Range;
transactions: Transaction[];
} {
const lines = content_utf8.split("\r\n").map((line) => line.split(";"));
let last = 0;
while (!lines[last][0].startsWith("Niniejszy dokument sporządzono")) {
last++;
}
const account = new Account(
lines[20][0],
this.parseAmount(lines[35][1]),
this.parseAmount(lines[last - 2][7]),
lines[18][0]
);
const range = new Range(
this.parseDate(lines[14][0]),
this.parseDate(lines[14][1])
);
const transactions = [];
for (let i = 38; i <= last - 5; i++) {
const line = lines[i];
if (line.length != 9) {
throw new Error(
"Wrong amount of columns! maybe a semicolon got stuck in a transaction description?"
);
}
const date_acc = new Date(line[0]);
const date_op = new Date(line[1]);
const [description, title, person, account_number] = line
.slice(2)
.map(this.trimString);
const [amount, balance_after] = line
.slice(6)
.map((s) => this.parseAmount(s));
transactions.push(
new Transaction(
date_acc,
date_op,
description,
title,
person,
account_number,
amount,
balance_after
)
);
}
return { account, range, transactions };
}
parseAmount(s: string): number {
return parseFloat(s.replace(/[^0-9,-]/g, "").replace(",", "."));
}
parseDate(s: string): Date {
const [day, month, year] = s.split(".").map((s) => parseInt(s));
const d = new Date();
d.setHours(0);
d.setMinutes(0);
d.setSeconds(0);
d.setMilliseconds(0);
d.setDate(day);
d.setMonth(month - 1);
d.setFullYear(year);
return d;
}
trimString(s: string): string {
//removes redundant quotes at the beginning or end of the transaction
return s.replaceAll(/(^["']|["']$)/g, "");
}
}
function mtDate(d: Date) {
return `${d.getFullYear() - 2000}${(d.getMonth() + 1)
.toString()
.padStart(2, "0")}${d.getDate().toString().padStart(2, "0")}`;
}
function mtAmount(n: number) {
return Math.abs(n).toString().replace(".", ",");
}
export function convert(csv_utf8: string): { output: string; range: Range } {
const result = new mBankParser().read_csv(csv_utf8);
const { account, transactions, range } = result;
const string = `:20:MT940
:25:/PL${account.number.replaceAll(/[^0-9]/g, "")}
:28C:${mtDate(range.date_start)}
:60F:D${mtDate(range.date_start)}${account.currency}${mtAmount(
account.initial_balance
)}
${transactions.map((t) => t.toMT940()).join("\n")}
:62F:D${mtDate(range.date_end)}${account.currency}${mtAmount(
account.closing_balance
)}`;
return { output: string, range };
}