@things-factory/accounting 10.1.139 → 10.1.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/service/account/account-match.d.ts +32 -0
- package/dist-server/service/account/account-match.js +74 -0
- package/dist-server/service/account/account-match.js.map +1 -0
- package/dist-server/service/bank-statement/bank-import-mutation.d.ts +5 -0
- package/dist-server/service/bank-statement/bank-import-mutation.js +43 -0
- package/dist-server/service/bank-statement/bank-import-mutation.js.map +1 -0
- package/dist-server/service/bank-statement/bank-import-type.d.ts +31 -0
- package/dist-server/service/bank-statement/bank-import-type.js +122 -0
- package/dist-server/service/bank-statement/bank-import-type.js.map +1 -0
- package/dist-server/service/bank-statement/bank-import.d.ts +16 -0
- package/dist-server/service/bank-statement/bank-import.js +141 -0
- package/dist-server/service/bank-statement/bank-import.js.map +1 -0
- package/dist-server/service/bank-statement/bank-parser.d.ts +35 -0
- package/dist-server/service/bank-statement/bank-parser.js +89 -0
- package/dist-server/service/bank-statement/bank-parser.js.map +1 -0
- package/dist-server/service/bank-statement/bank-reconcile.d.ts +35 -0
- package/dist-server/service/bank-statement/bank-reconcile.js +71 -0
- package/dist-server/service/bank-statement/bank-reconcile.js.map +1 -0
- package/dist-server/service/bank-statement/bank-source.d.ts +25 -0
- package/dist-server/service/bank-statement/bank-source.js +55 -0
- package/dist-server/service/bank-statement/bank-source.js.map +1 -0
- package/dist-server/service/bank-statement/index.d.ts +2 -1
- package/dist-server/service/bank-statement/index.js +2 -1
- package/dist-server/service/bank-statement/index.js.map +1 -1
- package/dist-server/service/index.d.ts +8 -1
- package/dist-server/service/index.js +7 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/payment/document-targets.d.ts +16 -0
- package/dist-server/service/payment/document-targets.js +99 -0
- package/dist-server/service/payment/document-targets.js.map +1 -0
- package/dist-server/service/payment/payment-service.js +3 -0
- package/dist-server/service/payment/payment-service.js.map +1 -1
- package/dist-server/service/payment/payment-targets.d.ts +12 -0
- package/dist-server/service/payment/payment-targets.js +9 -0
- package/dist-server/service/payment/payment-targets.js.map +1 -1
- package/dist-server/service/transaction/transaction-query.js +13 -1
- package/dist-server/service/transaction/transaction-query.js.map +1 -1
- package/dist-server/service/transaction/transaction-service.d.ts +5 -0
- package/dist-server/service/transaction/transaction-service.js +24 -0
- package/dist-server/service/transaction/transaction-service.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -7
- package/translations/en.json +24 -1
- package/translations/ko.json +24 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseBankWorkbook = parseBankWorkbook;
|
|
4
|
+
exports.parseBankRows = parseBankRows;
|
|
5
|
+
const xlsx_1 = require("xlsx");
|
|
6
|
+
const bank_source_1 = require("./bank-source");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
function parseBankWorkbook(data, timezoneFor = () => 'Asia/Seoul') {
|
|
9
|
+
const workbook = (0, xlsx_1.read)(data, { type: 'buffer' });
|
|
10
|
+
if (workbook.SheetNames.length !== 1)
|
|
11
|
+
throw new shell_1.Refusal('bank-single-sheet', {}, 'Upload a statement with one sheet');
|
|
12
|
+
const rows = xlsx_1.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { header: 1, defval: null });
|
|
13
|
+
return parseBankRows(rows, timezoneFor);
|
|
14
|
+
}
|
|
15
|
+
function parseBankRows(rows, timezoneFor = () => 'Asia/Seoul') {
|
|
16
|
+
const headerIndex = rows.findIndex(row => row.includes('거래일시'));
|
|
17
|
+
if (headerIndex < 0)
|
|
18
|
+
throw new shell_1.Refusal('bank-unsupported-format', {}, 'Unsupported bank statement format');
|
|
19
|
+
const header = rows[headerIndex];
|
|
20
|
+
const metadata = rows.slice(0, headerIndex).map(row => row.join(' ')).join('\n');
|
|
21
|
+
const accountNumber = /계좌번호\s*:\s*([\d-]+)/.exec(metadata)?.[1];
|
|
22
|
+
if (!accountNumber)
|
|
23
|
+
throw new shell_1.Refusal('bank-missing-account', {}, 'The bank statement has no account number');
|
|
24
|
+
const woori = header.includes('지급(원)');
|
|
25
|
+
const foreign = header.includes('통화');
|
|
26
|
+
if (!woori && !foreign && !(header.includes('출금') && header.includes('입금') && header.includes('거래후 잔액')))
|
|
27
|
+
throw new shell_1.Refusal('bank-unsupported-format', {}, 'Unsupported bank statement format');
|
|
28
|
+
const bankName = woori ? '우리은행' : '기업은행';
|
|
29
|
+
const periods = metadata.match(/\d{4}[-.]\d{2}[-.]\d{2}/g) ?? [];
|
|
30
|
+
const text = (value) => value == null || value === '' ? null : String(value);
|
|
31
|
+
const money = (value) => {
|
|
32
|
+
if (value == null || value === '')
|
|
33
|
+
throw new shell_1.Refusal('bank-invalid-money', {}, 'Check the statement amounts and balances');
|
|
34
|
+
const n = Number(typeof value === 'string' ? value.replace(/,/g, '').trim() : value);
|
|
35
|
+
if (!Number.isFinite(n) || Math.abs(n * 100 - Math.round(n * 100)) > 0.0001)
|
|
36
|
+
throw new shell_1.Refusal('bank-invalid-money', {}, 'Check the statement amounts and balances');
|
|
37
|
+
return n;
|
|
38
|
+
};
|
|
39
|
+
const lines = [];
|
|
40
|
+
const occurrences = new Map();
|
|
41
|
+
const sourceRows = rows.slice(headerIndex + 1).map((raw, i) => ({ raw, row: i + headerIndex + 2 })).filter(({ raw }) => typeof raw[0] === 'number');
|
|
42
|
+
if (!sourceRows.length)
|
|
43
|
+
throw new shell_1.Refusal('bank-empty-statement', {}, 'The statement has no transactions');
|
|
44
|
+
for (const { raw, row } of sourceRows) {
|
|
45
|
+
const time = (0, bank_source_1.bankTimestamp)(String(raw[1]), timezoneFor(foreign ? String(raw[2]) : 'KRW'));
|
|
46
|
+
const deposit = money(raw[woori ? 5 : foreign ? 3 : 3]);
|
|
47
|
+
const withdrawal = money(raw[woori ? 4 : foreign ? 4 : 2]);
|
|
48
|
+
if (deposit < 0 || withdrawal < 0 || (deposit && withdrawal))
|
|
49
|
+
throw new shell_1.Refusal('bank-invalid-direction', { row }, 'Check the bank deposit and withdrawal direction');
|
|
50
|
+
const line = {
|
|
51
|
+
row, bankSequence: foreign ? raw[0] : sourceRows.length + 1 - raw[0],
|
|
52
|
+
currency: foreign ? String(raw[2]) : 'KRW', ...time,
|
|
53
|
+
type: deposit ? 'INCOME' : 'EXPENSE', amount: deposit || withdrawal,
|
|
54
|
+
balance: money(raw[woori ? 6 : foreign ? 5 : 4]),
|
|
55
|
+
bankDescription: text(raw[woori ? 3 : foreign ? 6 : 5]),
|
|
56
|
+
bankKind: foreign ? '외화' : text(raw[woori ? 2 : 9]),
|
|
57
|
+
bankCounterpartyName: text(raw[woori ? 3 : foreign ? 8 : 12]),
|
|
58
|
+
bankCounterpartyAccount: foreign || woori ? null : text(raw[6]),
|
|
59
|
+
bankCounterpartyBank: foreign || woori ? null : text(raw[7]),
|
|
60
|
+
bankMemo: foreign ? null : text(raw[8]), fingerprint: '', raw
|
|
61
|
+
};
|
|
62
|
+
if (!/^[A-Z]{3}$/.test(line.currency))
|
|
63
|
+
throw new shell_1.Refusal('bank-invalid-currency', { row }, 'Check the bank currency');
|
|
64
|
+
const base = (0, bank_source_1.bankFingerprint)({ ...line, accountNumber });
|
|
65
|
+
const occurrence = occurrences.get(base) ?? 0;
|
|
66
|
+
occurrences.set(base, occurrence + 1);
|
|
67
|
+
line.fingerprint = (0, bank_source_1.bankFingerprint)({ ...line, accountNumber }, occurrence);
|
|
68
|
+
lines.push(line);
|
|
69
|
+
}
|
|
70
|
+
// The bank's chronological row order resolves ties in second-resolution timestamps.
|
|
71
|
+
const sorted = [...lines].sort((a, b) => (a.transactionAt ?? a.transactionDate).localeCompare(b.transactionAt ?? b.transactionDate) || a.bankSequence - b.bankSequence);
|
|
72
|
+
const previous = new Map();
|
|
73
|
+
for (const line of sorted) {
|
|
74
|
+
const prior = previous.get(line.currency);
|
|
75
|
+
if (prior && Math.abs(line.balance - prior.balance - (line.type === 'INCOME' ? line.amount : -line.amount)) > 0.005)
|
|
76
|
+
throw new shell_1.Refusal('bank-balance-gap', { row: line.row }, 'The bank balance does not follow the previous transaction');
|
|
77
|
+
previous.set(line.currency, line);
|
|
78
|
+
}
|
|
79
|
+
const total = rows.find(row => row[1] === '합계');
|
|
80
|
+
if (total && !foreign && !woori) {
|
|
81
|
+
for (const [type, index] of [['EXPENSE', 2], ['INCOME', 3]]) {
|
|
82
|
+
const sum = lines.filter(line => line.type === type).reduce((n, line) => n + line.amount, 0);
|
|
83
|
+
if (Math.abs(sum - money(total[index])) > 0.005)
|
|
84
|
+
throw new shell_1.Refusal('bank-total-mismatch', {}, 'The bank transaction total does not match');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { bankName, accountNumber, periodStart: periods[0]?.replace(/\./g, '-') ?? null, periodEnd: periods[1]?.replace(/\./g, '-') ?? null, rawRows: rows, lines };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=bank-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bank-parser.js","sourceRoot":"","sources":["../../../server/service/bank-statement/bank-parser.ts"],"names":[],"mappings":"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","sourcesContent":["import { read, utils } from 'xlsx'\nimport { bankFingerprint, bankTimestamp } from './bank-source'\nimport { Refusal } from '@things-factory/shell'\n\nexport interface BankLine {\n row: number\n bankSequence: number\n currency: string\n transactionDate: string\n transactionAt: string | null\n bankLocalTime: string\n type: 'INCOME' | 'EXPENSE'\n amount: number\n balance: number\n bankDescription: string | null\n bankKind: string | null\n bankCounterpartyName: string | null\n bankCounterpartyAccount: string | null\n bankCounterpartyBank: string | null\n bankMemo: string | null\n fingerprint: string\n raw: any[]\n}\n\nexport function parseBankWorkbook(data: Buffer, timezoneFor: (currency: string) => string = () => 'Asia/Seoul') {\n const workbook = read(data, { type: 'buffer' })\n if (workbook.SheetNames.length !== 1) throw new Refusal('bank-single-sheet', {}, 'Upload a statement with one sheet')\n const rows = utils.sheet_to_json<any[]>(workbook.Sheets[workbook.SheetNames[0]], { header: 1, defval: null })\n return parseBankRows(rows, timezoneFor)\n}\n\nexport function parseBankRows(rows: any[][], timezoneFor: (currency: string) => string = () => 'Asia/Seoul') {\n const headerIndex = rows.findIndex(row => row.includes('거래일시'))\n if (headerIndex < 0) throw new Refusal('bank-unsupported-format', {}, 'Unsupported bank statement format')\n const header = rows[headerIndex]\n const metadata = rows.slice(0, headerIndex).map(row => row.join(' ')).join('\\n')\n const accountNumber = /계좌번호\\s*:\\s*([\\d-]+)/.exec(metadata)?.[1]\n if (!accountNumber) throw new Refusal('bank-missing-account', {}, 'The bank statement has no account number')\n const woori = header.includes('지급(원)')\n const foreign = header.includes('통화')\n if (!woori && !foreign && !(header.includes('출금') && header.includes('입금') && header.includes('거래후 잔액'))) throw new Refusal('bank-unsupported-format', {}, 'Unsupported bank statement format')\n const bankName = woori ? '우리은행' : '기업은행'\n const periods = metadata.match(/\\d{4}[-.]\\d{2}[-.]\\d{2}/g) ?? []\n const text = (value: any) => value == null || value === '' ? null : String(value)\n const money = (value: any) => {\n if (value == null || value === '') throw new Refusal('bank-invalid-money', {}, 'Check the statement amounts and balances')\n const n = Number(typeof value === 'string' ? value.replace(/,/g, '').trim() : value)\n if (!Number.isFinite(n) || Math.abs(n * 100 - Math.round(n * 100)) > 0.0001) throw new Refusal('bank-invalid-money', {}, 'Check the statement amounts and balances')\n return n\n }\n const lines: BankLine[] = []\n const occurrences = new Map<string, number>()\n const sourceRows = rows.slice(headerIndex + 1).map((raw, i) => ({ raw, row: i + headerIndex + 2 })).filter(({ raw }) => typeof raw[0] === 'number')\n if (!sourceRows.length) throw new Refusal('bank-empty-statement', {}, 'The statement has no transactions')\n for (const { raw, row } of sourceRows) {\n const time = bankTimestamp(String(raw[1]), timezoneFor(foreign ? String(raw[2]) : 'KRW'))\n const deposit = money(raw[woori ? 5 : foreign ? 3 : 3])\n const withdrawal = money(raw[woori ? 4 : foreign ? 4 : 2])\n if (deposit < 0 || withdrawal < 0 || (deposit && withdrawal)) throw new Refusal('bank-invalid-direction', { row }, 'Check the bank deposit and withdrawal direction')\n const line: BankLine = {\n row, bankSequence: foreign ? raw[0] : sourceRows.length + 1 - raw[0],\n currency: foreign ? String(raw[2]) : 'KRW', ...time,\n type: deposit ? 'INCOME' : 'EXPENSE', amount: deposit || withdrawal,\n balance: money(raw[woori ? 6 : foreign ? 5 : 4]),\n bankDescription: text(raw[woori ? 3 : foreign ? 6 : 5]),\n bankKind: foreign ? '외화' : text(raw[woori ? 2 : 9]),\n bankCounterpartyName: text(raw[woori ? 3 : foreign ? 8 : 12]),\n bankCounterpartyAccount: foreign || woori ? null : text(raw[6]),\n bankCounterpartyBank: foreign || woori ? null : text(raw[7]),\n bankMemo: foreign ? null : text(raw[8]), fingerprint: '', raw\n }\n if (!/^[A-Z]{3}$/.test(line.currency)) throw new Refusal('bank-invalid-currency', { row }, 'Check the bank currency')\n const base = bankFingerprint({ ...line, accountNumber })\n const occurrence = occurrences.get(base) ?? 0\n occurrences.set(base, occurrence + 1)\n line.fingerprint = bankFingerprint({ ...line, accountNumber }, occurrence)\n lines.push(line)\n }\n // The bank's chronological row order resolves ties in second-resolution timestamps.\n const sorted = [...lines].sort((a, b) => (a.transactionAt ?? a.transactionDate).localeCompare(b.transactionAt ?? b.transactionDate) || a.bankSequence - b.bankSequence)\n const previous = new Map<string, BankLine>()\n for (const line of sorted) {\n const prior = previous.get(line.currency)\n if (prior && Math.abs(line.balance - prior.balance - (line.type === 'INCOME' ? line.amount : -line.amount)) > 0.005)\n throw new Refusal('bank-balance-gap', { row: line.row }, 'The bank balance does not follow the previous transaction')\n previous.set(line.currency, line)\n }\n const total = rows.find(row => row[1] === '합계')\n if (total && !foreign && !woori) {\n for (const [type, index] of [['EXPENSE', 2], ['INCOME', 3]] as const) {\n const sum = lines.filter(line => line.type === type).reduce((n, line) => n + line.amount, 0)\n if (Math.abs(sum - money(total[index])) > 0.005) throw new Refusal('bank-total-mismatch', {}, 'The bank transaction total does not match')\n }\n }\n return { bankName, accountNumber, periodStart: periods[0]?.replace(/\\./g, '-') ?? null, periodEnd: periods[1]?.replace(/\\./g, '-') ?? null, rawRows: rows, lines }\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BankLine } from './bank-parser';
|
|
2
|
+
export interface LedgerCandidate {
|
|
3
|
+
id: string;
|
|
4
|
+
currency: string;
|
|
5
|
+
type: string;
|
|
6
|
+
amount: number;
|
|
7
|
+
transactionDate: string;
|
|
8
|
+
balance?: number;
|
|
9
|
+
source?: string;
|
|
10
|
+
bankFingerprint?: string;
|
|
11
|
+
bankAccountId?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
counterpartyName?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Resolve the entire statement before writing: one ledger row cannot be consumed twice. */
|
|
16
|
+
export declare function reconcileBankLines(lines: BankLine[], ledger: LedgerCandidate[]): {
|
|
17
|
+
line: BankLine;
|
|
18
|
+
candidates: LedgerCandidate[];
|
|
19
|
+
match: LedgerCandidate;
|
|
20
|
+
}[];
|
|
21
|
+
/**
|
|
22
|
+
* Place the held lines a person decided: on one of the line's own candidates (the row it is), or nowhere (a new
|
|
23
|
+
* transaction). No ledger row is taken twice, counting the ones the statement matched by itself. False when a
|
|
24
|
+
* decision names a line that was not held, or a row it may not take — then nothing is placed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function placeHeldLines(plan: {
|
|
27
|
+
line: {
|
|
28
|
+
fingerprint: string;
|
|
29
|
+
};
|
|
30
|
+
candidates: LedgerCandidate[];
|
|
31
|
+
match?: LedgerCandidate;
|
|
32
|
+
}[], decisions: readonly {
|
|
33
|
+
fingerprint: string;
|
|
34
|
+
transactionId?: string | null;
|
|
35
|
+
}[]): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reconcileBankLines = reconcileBankLines;
|
|
4
|
+
exports.placeHeldLines = placeHeldLines;
|
|
5
|
+
const cents = (value) => Math.round(Number(value) * 100);
|
|
6
|
+
const day = (value) => Date.parse(value + 'T00:00:00Z') / 86400000;
|
|
7
|
+
const normalized = (value) => (value ?? '').normalize('NFKC').replace(/[^\p{L}\p{N}]/gu, '').toLowerCase();
|
|
8
|
+
/** Resolve the entire statement before writing: one ledger row cannot be consumed twice. */
|
|
9
|
+
function reconcileBankLines(lines, ledger) {
|
|
10
|
+
const candidates = lines.map(line => {
|
|
11
|
+
const possible = ledger.filter(old => !old.bankFingerprint && old.source !== 'BANK_STATEMENT' &&
|
|
12
|
+
old.currency === line.currency && old.type === line.type && cents(old.amount) === cents(line.amount));
|
|
13
|
+
const exact = possible.filter(old => old.transactionDate === line.transactionDate);
|
|
14
|
+
if (exact.length)
|
|
15
|
+
return exact;
|
|
16
|
+
// Date correction requires independent evidence, never amount alone.
|
|
17
|
+
return possible.filter(old => Math.abs(day(old.transactionDate) - day(line.transactionDate)) <= 7 && ((old.balance != null && cents(old.balance) === cents(line.balance)) ||
|
|
18
|
+
[old.description, old.counterpartyName].some(value => normalized(value).length >= 4 &&
|
|
19
|
+
[line.bankDescription, line.bankCounterpartyName].some(bank => normalized(bank) === normalized(value)))));
|
|
20
|
+
});
|
|
21
|
+
const uses = new Map();
|
|
22
|
+
for (const group of candidates)
|
|
23
|
+
for (const old of group)
|
|
24
|
+
uses.set(old.id, (uses.get(old.id) ?? 0) + 1);
|
|
25
|
+
return lines.map((line, index) => {
|
|
26
|
+
let group = candidates[index];
|
|
27
|
+
let uncertain = false;
|
|
28
|
+
if (!group.length) {
|
|
29
|
+
group = ledger.filter(old => !old.bankFingerprint && old.source !== 'BANK_STATEMENT' && old.currency === line.currency &&
|
|
30
|
+
old.type === line.type && cents(old.amount) === cents(line.amount) && Math.abs(day(old.transactionDate) - day(line.transactionDate)) <= 7);
|
|
31
|
+
uncertain = group.length > 0;
|
|
32
|
+
}
|
|
33
|
+
// A ledger total may represent multiple bank movements. Keep all its constituent rows pending.
|
|
34
|
+
if (!group.length) {
|
|
35
|
+
const sameDay = lines.filter(other => other.transactionDate === line.transactionDate && other.currency === line.currency && other.type === line.type);
|
|
36
|
+
group = ledger.filter(old => !old.bankFingerprint && old.source !== 'BANK_STATEMENT' && old.transactionDate === line.transactionDate &&
|
|
37
|
+
old.currency === line.currency && old.type === line.type && cents(old.amount) > cents(line.amount) &&
|
|
38
|
+
(cents(old.amount) === sameDay.reduce((sum, other) => sum + cents(other.amount), 0) ||
|
|
39
|
+
sameDay.some(other => other !== line && cents(other.amount) + cents(line.amount) === cents(old.amount))));
|
|
40
|
+
}
|
|
41
|
+
return { line, candidates: group, match: !uncertain && group.length === 1 && uses.get(group[0].id) === 1 ? group[0] : undefined };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Place the held lines a person decided: on one of the line's own candidates (the row it is), or nowhere (a new
|
|
46
|
+
* transaction). No ledger row is taken twice, counting the ones the statement matched by itself. False when a
|
|
47
|
+
* decision names a line that was not held, or a row it may not take — then nothing is placed.
|
|
48
|
+
*/
|
|
49
|
+
function placeHeldLines(plan, decisions) {
|
|
50
|
+
const byLine = new Map(plan.map(item => [item.line.fingerprint, item]));
|
|
51
|
+
const taken = new Set(plan.filter(item => item.match).map(item => item.match.id));
|
|
52
|
+
const placed = [];
|
|
53
|
+
for (const decision of decisions) {
|
|
54
|
+
const item = byLine.get(decision.fingerprint);
|
|
55
|
+
if (!item || item.match || !item.candidates.length || placed.some(([one]) => one === item))
|
|
56
|
+
return false;
|
|
57
|
+
const id = decision.transactionId ?? null;
|
|
58
|
+
const row = id ? item.candidates.find(old => old.id === id) : undefined;
|
|
59
|
+
if (id && (!row || taken.has(id)))
|
|
60
|
+
return false;
|
|
61
|
+
if (row)
|
|
62
|
+
taken.add(row.id);
|
|
63
|
+
placed.push([item, row]);
|
|
64
|
+
}
|
|
65
|
+
for (const [item, row] of placed) {
|
|
66
|
+
item.match = row;
|
|
67
|
+
item.candidates = [];
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=bank-reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bank-reconcile.js","sourceRoot":"","sources":["../../../server/service/bank-statement/bank-reconcile.ts"],"names":[],"mappings":";;AAYA,gDAiCC;AAOD,wCAqBC;AAlED,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAA;AAChE,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,YAAY,CAAC,GAAG,QAAQ,CAAA;AAC1E,MAAM,UAAU,GAAG,CAAC,KAAqB,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;AAE1H,4FAA4F;AAC5F,SAAgB,kBAAkB,CAAC,KAAiB,EAAE,MAAyB;IAC7E,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAClC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,eAAe,IAAI,GAAG,CAAC,MAAM,KAAK,gBAAgB;YAC3F,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;QACvG,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,eAAe,KAAK,IAAI,CAAC,eAAe,CAAC,CAAA;QAClF,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO,KAAK,CAAA;QAC9B,qEAAqE;QACrE,OAAO,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CACnG,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACnE,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC;gBACjF,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAC1G,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAA;IACtC,KAAK,MAAM,KAAK,IAAI,UAAU;QAAE,KAAK,MAAM,GAAG,IAAI,KAAK;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACtG,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;QAC7B,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAClB,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,eAAe,IAAI,GAAG,CAAC,MAAM,KAAK,gBAAgB,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ;gBACpH,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;YAC5I,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;QAC9B,CAAC;QACD,+FAA+F;QAC/F,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,eAAe,KAAK,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAA;YACrJ,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,eAAe,IAAI,GAAG,CAAC,MAAM,KAAK,gBAAgB,IAAI,GAAG,CAAC,eAAe,KAAK,IAAI,CAAC,eAAe;gBAClI,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;gBAClG,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;oBACjF,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QAC/G,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;IACnI,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,cAAc,CAC5B,IAAiG,EACjG,SAA4E;IAE5E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IACvE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IAClF,MAAM,MAAM,GAAyD,EAAE,CAAA;IACvE,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;QAC7C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QACxG,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,IAAI,IAAI,CAAA;QACzC,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACvE,IAAI,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;QAC/C,IAAI,GAAG;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC1B,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;IAC1B,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,EAAE,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,GAAG,CAAA;QAChB,IAAI,CAAC,UAAU,GAAG,EAAE,CAAA;IACtB,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import type { BankLine } from './bank-parser'\n\nexport interface LedgerCandidate {\n id: string; currency: string; type: string; amount: number; transactionDate: string\n balance?: number; source?: string; bankFingerprint?: string; bankAccountId?: string\n description?: string; counterpartyName?: string\n}\nconst cents = (value: number) => Math.round(Number(value) * 100)\nconst day = (value: string) => Date.parse(value + 'T00:00:00Z') / 86400000\nconst normalized = (value?: string | null) => (value ?? '').normalize('NFKC').replace(/[^\\p{L}\\p{N}]/gu, '').toLowerCase()\n\n/** Resolve the entire statement before writing: one ledger row cannot be consumed twice. */\nexport function reconcileBankLines(lines: BankLine[], ledger: LedgerCandidate[]) {\n const candidates = lines.map(line => {\n const possible = ledger.filter(old => !old.bankFingerprint && old.source !== 'BANK_STATEMENT' &&\n old.currency === line.currency && old.type === line.type && cents(old.amount) === cents(line.amount))\n const exact = possible.filter(old => old.transactionDate === line.transactionDate)\n if (exact.length) return exact\n // Date correction requires independent evidence, never amount alone.\n return possible.filter(old => Math.abs(day(old.transactionDate) - day(line.transactionDate)) <= 7 && (\n (old.balance != null && cents(old.balance) === cents(line.balance)) ||\n [old.description, old.counterpartyName].some(value => normalized(value).length >= 4 &&\n [line.bankDescription, line.bankCounterpartyName].some(bank => normalized(bank) === normalized(value)))\n ))\n })\n const uses = new Map<string, number>()\n for (const group of candidates) for (const old of group) uses.set(old.id, (uses.get(old.id) ?? 0) + 1)\n return lines.map((line, index) => {\n let group = candidates[index]\n let uncertain = false\n if (!group.length) {\n group = ledger.filter(old => !old.bankFingerprint && old.source !== 'BANK_STATEMENT' && old.currency === line.currency &&\n old.type === line.type && cents(old.amount) === cents(line.amount) && Math.abs(day(old.transactionDate) - day(line.transactionDate)) <= 7)\n uncertain = group.length > 0\n }\n // A ledger total may represent multiple bank movements. Keep all its constituent rows pending.\n if (!group.length) {\n const sameDay = lines.filter(other => other.transactionDate === line.transactionDate && other.currency === line.currency && other.type === line.type)\n group = ledger.filter(old => !old.bankFingerprint && old.source !== 'BANK_STATEMENT' && old.transactionDate === line.transactionDate &&\n old.currency === line.currency && old.type === line.type && cents(old.amount) > cents(line.amount) &&\n (cents(old.amount) === sameDay.reduce((sum, other) => sum + cents(other.amount), 0) ||\n sameDay.some(other => other !== line && cents(other.amount) + cents(line.amount) === cents(old.amount))))\n }\n return { line, candidates: group, match: !uncertain && group.length === 1 && uses.get(group[0].id) === 1 ? group[0] : undefined }\n })\n}\n\n/**\n * Place the held lines a person decided: on one of the line's own candidates (the row it is), or nowhere (a new\n * transaction). No ledger row is taken twice, counting the ones the statement matched by itself. False when a\n * decision names a line that was not held, or a row it may not take — then nothing is placed.\n */\nexport function placeHeldLines(\n plan: { line: { fingerprint: string }; candidates: LedgerCandidate[]; match?: LedgerCandidate }[],\n decisions: readonly { fingerprint: string; transactionId?: string | null }[]\n): boolean {\n const byLine = new Map(plan.map(item => [item.line.fingerprint, item]))\n const taken = new Set(plan.filter(item => item.match).map(item => item.match!.id))\n const placed: [typeof plan[number], LedgerCandidate | undefined][] = []\n for (const decision of decisions) {\n const item = byLine.get(decision.fingerprint)\n if (!item || item.match || !item.candidates.length || placed.some(([one]) => one === item)) return false\n const id = decision.transactionId ?? null\n const row = id ? item.candidates.find(old => old.id === id) : undefined\n if (id && (!row || taken.has(id))) return false\n if (row) taken.add(row.id)\n placed.push([item, row])\n }\n for (const [item, row] of placed) {\n item.match = row\n item.candidates = []\n }\n return true\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Multipart headers may expose UTF-8 filename bytes as Latin-1 text. */
|
|
2
|
+
export declare function bankFilename(value: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Dates stay in the bank's local calendar; an unknown time is never invented. `bankLocalTime` is the time as the bank
|
|
5
|
+
* printed it — what identifies the line, whatever timezone the account is later given.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bankTimestamp(value: string, timezone?: string): {
|
|
8
|
+
transactionDate: string;
|
|
9
|
+
transactionAt: string | null;
|
|
10
|
+
bankLocalTime: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* File names and export row numbers change between overlapping statements; the account's timezone may change too.
|
|
14
|
+
* The line is known by what the bank printed: its local time string, not the instant it was read as (ADR-0120).
|
|
15
|
+
*/
|
|
16
|
+
export declare function bankFingerprint(line: {
|
|
17
|
+
accountNumber: string;
|
|
18
|
+
currency: string;
|
|
19
|
+
bankLocalTime: string;
|
|
20
|
+
type: string;
|
|
21
|
+
amount: number;
|
|
22
|
+
balance: number;
|
|
23
|
+
bankDescription: string | null;
|
|
24
|
+
bankKind: string | null;
|
|
25
|
+
}, occurrence?: number): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bankFilename = bankFilename;
|
|
4
|
+
exports.bankTimestamp = bankTimestamp;
|
|
5
|
+
exports.bankFingerprint = bankFingerprint;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const moment_timezone_1 = tslib_1.__importDefault(require("moment-timezone"));
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const shell_1 = require("@things-factory/shell");
|
|
10
|
+
/** Multipart headers may expose UTF-8 filename bytes as Latin-1 text. */
|
|
11
|
+
function bankFilename(value) {
|
|
12
|
+
if (/[^\x00-\xff]/.test(value))
|
|
13
|
+
return value.normalize('NFC');
|
|
14
|
+
const decoded = Buffer.from(value, 'latin1').toString('utf8');
|
|
15
|
+
return (decoded.includes('\ufffd') ? value : decoded).normalize('NFC');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Dates stay in the bank's local calendar; an unknown time is never invented. `bankLocalTime` is the time as the bank
|
|
19
|
+
* printed it — what identifies the line, whatever timezone the account is later given.
|
|
20
|
+
*/
|
|
21
|
+
function bankTimestamp(value, timezone = 'Asia/Seoul') {
|
|
22
|
+
const normalized = value.trim().replace(/\./g, '-').replace(/[()]/g, '').replace(/\s+/g, ' ');
|
|
23
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/.exec(normalized);
|
|
24
|
+
if (!match)
|
|
25
|
+
throw new shell_1.Refusal('bank-invalid-time', { value }, 'Check the bank transaction timestamp');
|
|
26
|
+
const [, y, m, d, h, min, sec] = match;
|
|
27
|
+
const date = new Date(Date.UTC(+y, +m - 1, +d));
|
|
28
|
+
if (date.getUTCFullYear() !== +y || date.getUTCMonth() !== +m - 1 || date.getUTCDate() !== +d || (h !== undefined && (+h > 23 || +min > 59 || +sec > 59)))
|
|
29
|
+
throw new shell_1.Refusal('bank-invalid-time', { value }, 'Check the bank transaction timestamp');
|
|
30
|
+
if (!moment_timezone_1.default.tz.zone(timezone))
|
|
31
|
+
throw new shell_1.Refusal('bank-invalid-time', { value: timezone }, 'Invalid bank account time zone');
|
|
32
|
+
let transactionAt = null;
|
|
33
|
+
if (h !== undefined) {
|
|
34
|
+
const local = `${y}-${m}-${d} ${h}:${min}:${sec}`;
|
|
35
|
+
const instant = moment_timezone_1.default.tz(local, 'YYYY-MM-DD HH:mm:ss', true, timezone);
|
|
36
|
+
if (instant.format('YYYY-MM-DD HH:mm:ss') !== local)
|
|
37
|
+
throw new shell_1.Refusal('bank-invalid-time', { value }, 'Nonexistent local bank time');
|
|
38
|
+
if ([-120, -60, 60, 120].some(minutes => instant.clone().add(minutes, 'minutes').format('YYYY-MM-DD HH:mm:ss') === local))
|
|
39
|
+
throw new shell_1.Refusal('bank-invalid-time', { value }, 'Ambiguous local bank time');
|
|
40
|
+
transactionAt = instant.format('YYYY-MM-DDTHH:mm:ssZ');
|
|
41
|
+
}
|
|
42
|
+
return { transactionDate: `${y}-${m}-${d}`, transactionAt, bankLocalTime: h === undefined ? `${y}-${m}-${d}` : `${y}-${m}-${d} ${h}:${min}:${sec}` };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* File names and export row numbers change between overlapping statements; the account's timezone may change too.
|
|
46
|
+
* The line is known by what the bank printed: its local time string, not the instant it was read as (ADR-0120).
|
|
47
|
+
*/
|
|
48
|
+
function bankFingerprint(line, occurrence = 0) {
|
|
49
|
+
return (0, crypto_1.createHash)('sha256').update(JSON.stringify([
|
|
50
|
+
line.accountNumber.replace(/\D/g, ''), line.currency, line.bankLocalTime,
|
|
51
|
+
line.type, line.amount.toFixed(2), line.balance.toFixed(2),
|
|
52
|
+
(line.bankDescription ?? '').normalize('NFKC').trim(), (line.bankKind ?? '').normalize('NFKC').trim(), occurrence
|
|
53
|
+
])).digest('hex');
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=bank-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bank-source.js","sourceRoot":"","sources":["../../../server/service/bank-statement/bank-source.ts"],"names":[],"mappings":";;AAKA,oCAIC;AAMD,sCAkBC;AAMD,0CAMC;;AA7CD,8EAAoC;AACpC,mCAAmC;AACnC,iDAA+C;AAE/C,yEAAyE;AACzE,SAAgB,YAAY,CAAC,KAAa;IACxC,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC7D,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AACxE,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,KAAa,EAAE,QAAQ,GAAG,YAAY;IAClE,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7F,MAAM,KAAK,GAAG,wDAAwD,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACvF,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,EAAE,sCAAsC,CAAC,CAAA;IACrG,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,KAAK,CAAA;IACtC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/C,IAAI,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,GAAG,EAAE,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;QACvJ,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,EAAE,sCAAsC,CAAC,CAAA;IAC3F,IAAI,CAAC,yBAAM,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,gCAAgC,CAAC,CAAA;IAC5H,IAAI,aAAa,GAAkB,IAAI,CAAA;IACvC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,GAAG,EAAE,CAAA;QACjD,MAAM,OAAO,GAAG,yBAAM,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;QACvE,IAAI,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,KAAK,KAAK;YAAE,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,EAAE,6BAA6B,CAAC,CAAA;QACrI,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,qBAAqB,CAAC,KAAK,KAAK,CAAC;YAAE,MAAM,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,EAAE,2BAA2B,CAAC,CAAA;QACzM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAA;IACxD,CAAC;IACD,OAAO,EAAE,eAAe,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,GAAG,EAAE,EAAE,CAAA;AACtJ,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe,CAAC,IAAgL,EAAE,UAAU,GAAG,CAAC;IAC9N,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QAChD,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa;QACxE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1D,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,UAAU;KAClH,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACnB,CAAC","sourcesContent":["import moment from 'moment-timezone'\nimport { createHash } from 'crypto'\nimport { Refusal } from '@things-factory/shell'\n\n/** Multipart headers may expose UTF-8 filename bytes as Latin-1 text. */\nexport function bankFilename(value: string): string {\n if (/[^\\x00-\\xff]/.test(value)) return value.normalize('NFC')\n const decoded = Buffer.from(value, 'latin1').toString('utf8')\n return (decoded.includes('\\ufffd') ? value : decoded).normalize('NFC')\n}\n\n/**\n * Dates stay in the bank's local calendar; an unknown time is never invented. `bankLocalTime` is the time as the bank\n * printed it — what identifies the line, whatever timezone the account is later given.\n */\nexport function bankTimestamp(value: string, timezone = 'Asia/Seoul'): { transactionDate: string; transactionAt: string | null; bankLocalTime: string } {\n const normalized = value.trim().replace(/\\./g, '-').replace(/[()]/g, '').replace(/\\s+/g, ' ')\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})(?: (\\d{2}):(\\d{2}):(\\d{2}))?$/.exec(normalized)\n if (!match) throw new Refusal('bank-invalid-time', { value }, 'Check the bank transaction timestamp')\n const [, y, m, d, h, min, sec] = match\n const date = new Date(Date.UTC(+y, +m - 1, +d))\n if (date.getUTCFullYear() !== +y || date.getUTCMonth() !== +m - 1 || date.getUTCDate() !== +d || (h !== undefined && (+h > 23 || +min > 59 || +sec > 59)))\n throw new Refusal('bank-invalid-time', { value }, 'Check the bank transaction timestamp')\n if (!moment.tz.zone(timezone)) throw new Refusal('bank-invalid-time', { value: timezone }, 'Invalid bank account time zone')\n let transactionAt: string | null = null\n if (h !== undefined) {\n const local = `${y}-${m}-${d} ${h}:${min}:${sec}`\n const instant = moment.tz(local, 'YYYY-MM-DD HH:mm:ss', true, timezone)\n if (instant.format('YYYY-MM-DD HH:mm:ss') !== local) throw new Refusal('bank-invalid-time', { value }, 'Nonexistent local bank time')\n if ([-120, -60, 60, 120].some(minutes => instant.clone().add(minutes, 'minutes').format('YYYY-MM-DD HH:mm:ss') === local)) throw new Refusal('bank-invalid-time', { value }, 'Ambiguous local bank time')\n transactionAt = instant.format('YYYY-MM-DDTHH:mm:ssZ')\n }\n return { transactionDate: `${y}-${m}-${d}`, transactionAt, bankLocalTime: h === undefined ? `${y}-${m}-${d}` : `${y}-${m}-${d} ${h}:${min}:${sec}` }\n}\n\n/**\n * File names and export row numbers change between overlapping statements; the account's timezone may change too.\n * The line is known by what the bank printed: its local time string, not the instant it was read as (ADR-0120).\n */\nexport function bankFingerprint(line: { accountNumber: string; currency: string; bankLocalTime: string; type: string; amount: number; balance: number; bankDescription: string | null; bankKind: string | null }, occurrence = 0): string {\n return createHash('sha256').update(JSON.stringify([\n line.accountNumber.replace(/\\D/g, ''), line.currency, line.bankLocalTime,\n line.type, line.amount.toFixed(2), line.balance.toFixed(2),\n (line.bankDescription ?? '').normalize('NFKC').trim(), (line.bankKind ?? '').normalize('NFKC').trim(), occurrence\n ])).digest('hex')\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BankStatement } from './bank-statement';
|
|
2
2
|
import { BankStatementQuery } from './bank-statement-query';
|
|
3
|
+
import { BankImportMutation } from './bank-import-mutation';
|
|
3
4
|
export declare const entities: (typeof BankStatement)[];
|
|
4
|
-
export declare const resolvers: (typeof BankStatementQuery)[];
|
|
5
|
+
export declare const resolvers: (typeof BankStatementQuery | typeof BankImportMutation)[];
|
|
5
6
|
export declare const subscribers: any[];
|
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.subscribers = exports.resolvers = exports.entities = void 0;
|
|
4
4
|
const bank_statement_1 = require("./bank-statement");
|
|
5
5
|
const bank_statement_query_1 = require("./bank-statement-query");
|
|
6
|
+
const bank_import_mutation_1 = require("./bank-import-mutation");
|
|
6
7
|
exports.entities = [bank_statement_1.BankStatement];
|
|
7
|
-
exports.resolvers = [bank_statement_query_1.BankStatementQuery];
|
|
8
|
+
exports.resolvers = [bank_statement_query_1.BankStatementQuery, bank_import_mutation_1.BankImportMutation];
|
|
8
9
|
exports.subscribers = [];
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/bank-statement/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAChD,iEAA2D;AAE9C,QAAA,QAAQ,GAAG,CAAC,8BAAa,CAAC,CAAA;AAC1B,QAAA,SAAS,GAAG,CAAC,yCAAkB,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/bank-statement/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAChD,iEAA2D;AAC3D,iEAA2D;AAE9C,QAAA,QAAQ,GAAG,CAAC,8BAAa,CAAC,CAAA;AAC1B,QAAA,SAAS,GAAG,CAAC,yCAAkB,EAAE,yCAAkB,CAAC,CAAA;AACpD,QAAA,WAAW,GAAG,EAAE,CAAA","sourcesContent":["import { BankStatement } from './bank-statement'\nimport { BankStatementQuery } from './bank-statement-query'\nimport { BankImportMutation } from './bank-import-mutation'\n\nexport const entities = [BankStatement]\nexport const resolvers = [BankStatementQuery, BankImportMutation]\nexport const subscribers = []\n"]}
|
|
@@ -17,12 +17,19 @@ export * from './transaction/transaction-service';
|
|
|
17
17
|
export * from './bank-account/bank-account-service';
|
|
18
18
|
export * from './payment/payment-targets';
|
|
19
19
|
export * from './payment/payment-service';
|
|
20
|
+
export * from './payment/document-targets';
|
|
20
21
|
export * from './financial-statement/financial-statement-line';
|
|
21
22
|
export * from './financial-statement/financial-statement-service';
|
|
22
23
|
export * from './recurring-cash-item/recurring-schedule';
|
|
23
24
|
export * from './recurring-cash-item/recurring-cash-item-service';
|
|
25
|
+
export * from './account/account-match';
|
|
26
|
+
export * from './bank-statement/bank-source';
|
|
27
|
+
export * from './bank-statement/bank-parser';
|
|
28
|
+
export * from './bank-statement/bank-reconcile';
|
|
29
|
+
export * from './bank-statement/bank-import-type';
|
|
30
|
+
export * from './bank-statement/bank-import';
|
|
24
31
|
export declare const entities: (typeof import("./financial-statement/financial-statement-line").FinancialStatementLine | typeof import("./financial-statement/financial-statement").FinancialStatement | typeof import("./accounting-category/accounting-category").AccountingCategory | typeof import("./account/account").Account | typeof import("./bank-account/bank-account").BankAccount | typeof import("./bank-statement/bank-statement").BankStatement | typeof import("./payment/payment").Payment | typeof import("./transaction/transaction").Transaction | typeof import("./accounting-document/accounting-document").AccountingDocument | typeof import("./recurring-cash-item/recurring-cash-item").RecurringCashItem | typeof import("./financial-institution/financial-institution").FinancialInstitution | typeof import("./fiscal-year/fiscal-year").FiscalYear | typeof import("./financial-statement/financial-statement-history").FinancialStatementHistory | typeof import("./transaction/transaction-history").TransactionHistory)[];
|
|
25
32
|
export declare const subscribers: any[];
|
|
26
33
|
export declare const schema: {
|
|
27
|
-
resolverClasses: (typeof import("./financial-statement/financial-statement-query").FinancialStatementQuery | typeof import("./financial-statement/financial-statement-query").FinancialStatementLineQuery | typeof import("./financial-statement/financial-statement-mutation").FinancialStatementMutation | typeof import("./accounting-category/accounting-category-query").AccountingCategoryQuery | typeof import("./accounting-category/accounting-category-mutation").AccountingCategoryMutation | typeof import("./transaction/transaction-query").TransactionQuery | typeof import("./transaction/transaction-mutation").TransactionMutation | typeof import("./bank-account/bank-account-query").BankAccountQuery | typeof import("./bank-account/bank-account-mutation").BankAccountMutation | typeof import("./bank-statement/bank-statement-query").BankStatementQuery | typeof import("./payment/payment-query").PaymentQuery | typeof import("./payment/payment-mutation").PaymentMutation | typeof import("./account/account-query").AccountQuery | typeof import("./account/account-mutation").AccountMutation | typeof import("./accounting-document/accounting-document-query").AccountingDocumentQuery | typeof import("./accounting-document/accounting-document-mutation").AccountingDocumentMutation | typeof import("./recurring-cash-item/recurring-cash-item-query").RecurringCashItemQuery | typeof import("./recurring-cash-item/recurring-cash-item-mutation").RecurringCashItemMutation | typeof import("./financial-institution/financial-institution-query").FinancialInstitutionQuery | typeof import("./financial-institution/financial-institution-mutation").FinancialInstitutionMutation | typeof import("./fiscal-month/fiscal-month-query").FiscalMonthQuery | typeof import("./fiscal-month/fiscal-month-mutation").FiscalMonthMutation | typeof import("./fiscal-quarter/fiscal-quarter-query").FiscalQuarterQuery | typeof import("./fiscal-quarter/fiscal-quarter-mutation").FiscalQuarterMutation | typeof import("./fiscal-year/fiscal-year-query").FiscalYearQuery | typeof import("./fiscal-year/fiscal-year-mutation").FiscalYearMutation)[];
|
|
34
|
+
resolverClasses: (typeof import("./financial-statement/financial-statement-query").FinancialStatementQuery | typeof import("./financial-statement/financial-statement-query").FinancialStatementLineQuery | typeof import("./financial-statement/financial-statement-mutation").FinancialStatementMutation | typeof import("./accounting-category/accounting-category-query").AccountingCategoryQuery | typeof import("./accounting-category/accounting-category-mutation").AccountingCategoryMutation | typeof import("./transaction/transaction-query").TransactionQuery | typeof import("./transaction/transaction-mutation").TransactionMutation | typeof import("./bank-account/bank-account-query").BankAccountQuery | typeof import("./bank-account/bank-account-mutation").BankAccountMutation | typeof import("./bank-statement/bank-statement-query").BankStatementQuery | typeof import("./bank-statement/bank-import-mutation").BankImportMutation | typeof import("./payment/payment-query").PaymentQuery | typeof import("./payment/payment-mutation").PaymentMutation | typeof import("./account/account-query").AccountQuery | typeof import("./account/account-mutation").AccountMutation | typeof import("./accounting-document/accounting-document-query").AccountingDocumentQuery | typeof import("./accounting-document/accounting-document-mutation").AccountingDocumentMutation | typeof import("./recurring-cash-item/recurring-cash-item-query").RecurringCashItemQuery | typeof import("./recurring-cash-item/recurring-cash-item-mutation").RecurringCashItemMutation | typeof import("./financial-institution/financial-institution-query").FinancialInstitutionQuery | typeof import("./financial-institution/financial-institution-mutation").FinancialInstitutionMutation | typeof import("./fiscal-month/fiscal-month-query").FiscalMonthQuery | typeof import("./fiscal-month/fiscal-month-mutation").FiscalMonthMutation | typeof import("./fiscal-quarter/fiscal-quarter-query").FiscalQuarterQuery | typeof import("./fiscal-quarter/fiscal-quarter-mutation").FiscalQuarterMutation | typeof import("./fiscal-year/fiscal-year-query").FiscalYearQuery | typeof import("./fiscal-year/fiscal-year-mutation").FiscalYearMutation)[];
|
|
28
35
|
};
|
|
@@ -23,10 +23,17 @@ tslib_1.__exportStar(require("./transaction/transaction-service"), exports);
|
|
|
23
23
|
tslib_1.__exportStar(require("./bank-account/bank-account-service"), exports);
|
|
24
24
|
tslib_1.__exportStar(require("./payment/payment-targets"), exports);
|
|
25
25
|
tslib_1.__exportStar(require("./payment/payment-service"), exports);
|
|
26
|
+
tslib_1.__exportStar(require("./payment/document-targets"), exports);
|
|
26
27
|
tslib_1.__exportStar(require("./financial-statement/financial-statement-line"), exports);
|
|
27
28
|
tslib_1.__exportStar(require("./financial-statement/financial-statement-service"), exports);
|
|
28
29
|
tslib_1.__exportStar(require("./recurring-cash-item/recurring-schedule"), exports);
|
|
29
30
|
tslib_1.__exportStar(require("./recurring-cash-item/recurring-cash-item-service"), exports);
|
|
31
|
+
tslib_1.__exportStar(require("./account/account-match"), exports);
|
|
32
|
+
tslib_1.__exportStar(require("./bank-statement/bank-source"), exports);
|
|
33
|
+
tslib_1.__exportStar(require("./bank-statement/bank-parser"), exports);
|
|
34
|
+
tslib_1.__exportStar(require("./bank-statement/bank-reconcile"), exports);
|
|
35
|
+
tslib_1.__exportStar(require("./bank-statement/bank-import-type"), exports);
|
|
36
|
+
tslib_1.__exportStar(require("./bank-statement/bank-import"), exports);
|
|
30
37
|
/* IMPORT ENTITIES AND RESOLVERS */
|
|
31
38
|
const financial_statement_1 = require("./financial-statement");
|
|
32
39
|
const accounting_category_1 = require("./accounting-category");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,2DAAgC;AAChC,oFAAyD;AACzD,oFAAyD;AACzD,oEAAyC;AACzC,sEAA2C;AAC3C,0EAA+C;AAC/C,4DAAiC;AACjC,4DAAiC;AACjC,oFAAyD;AACzD,oFAAyD;AACzD,wFAA6D;AAC7D,sEAA2C;AAC3C,0EAA+C;AAC/C,oEAAyC;AAEzC,qBAAqB;AACrB,2EAAgD;AAChD,4EAAiD;AACjD,8EAAmD;AACnD,oEAAyC;AACzC,oEAAyC;AACzC,yFAA8D;AAC9D,4FAAiE;AACjE,mFAAwD;AACxD,4FAAiE;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,2DAAgC;AAChC,oFAAyD;AACzD,oFAAyD;AACzD,oEAAyC;AACzC,sEAA2C;AAC3C,0EAA+C;AAC/C,4DAAiC;AACjC,4DAAiC;AACjC,oFAAyD;AACzD,oFAAyD;AACzD,wFAA6D;AAC7D,sEAA2C;AAC3C,0EAA+C;AAC/C,oEAAyC;AAEzC,qBAAqB;AACrB,2EAAgD;AAChD,4EAAiD;AACjD,8EAAmD;AACnD,oEAAyC;AACzC,oEAAyC;AACzC,qEAA0C;AAC1C,yFAA8D;AAC9D,4FAAiE;AACjE,mFAAwD;AACxD,4FAAiE;AACjE,kEAAuC;AACvC,uEAA4C;AAC5C,uEAA4C;AAC5C,0EAA+C;AAC/C,4EAAiD;AACjD,uEAA4C;AAE5C,mCAAmC;AACnC,+DAI8B;AAC9B,+DAI8B;AAC9B,+CAIsB;AACtB,iDAIuB;AACvB,qDAIyB;AACzB,uCAIkB;AAClB,uCAIkB;AAClB,+DAI8B;AAC9B,+DAI8B;AAC9B,mEAIgC;AAChC,iDAIuB;AACvB,qDAIyB;AACzB,+CAIsB;AAET,QAAA,QAAQ,GAAG;IACtB,GAAG,8BAA0B;IAC7B,GAAG,8BAA0B;IAC7B,GAAG,sBAAmB;IACtB,GAAG,uBAAmB;IACtB,GAAG,yBAAqB;IACxB,GAAG,kBAAe;IAClB,GAAG,kBAAe;IAClB,GAAG,8BAA0B;IAC7B,GAAG,8BAAyB;IAC5B,GAAG,gCAA4B;IAC/B,GAAG,uBAAmB;IACtB,GAAG,yBAAqB;IACxB,GAAG,sBAAkB;CACtB,CAAA;AAEY,QAAA,WAAW,GAAG;IACzB,GAAG,iCAA6B;IAChC,GAAG,iCAA6B;IAChC,GAAG,yBAAsB;IACzB,GAAG,0BAAsB;IACzB,GAAG,4BAAwB;IAC3B,GAAG,qBAAkB;IACrB,GAAG,qBAAkB;IACrB,GAAG,iCAA6B;IAChC,GAAG,iCAA4B;IAC/B,GAAG,mCAA+B;IAClC,GAAG,0BAAsB;IACzB,GAAG,4BAAwB;IAC3B,GAAG,yBAAqB;CACzB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,GAAG,+BAA2B;QAC9B,GAAG,+BAA2B;QAC9B,GAAG,uBAAoB;QACvB,GAAG,wBAAoB;QACvB,GAAG,0BAAsB;QACzB,GAAG,mBAAgB;QACnB,GAAG,mBAAgB;QACnB,GAAG,+BAA2B;QAC9B,GAAG,+BAA0B;QAC7B,GAAG,iCAA6B;QAChC,GAAG,wBAAoB;QACvB,GAAG,0BAAsB;QACzB,GAAG,uBAAmB;KACvB;CACF,CAAA","sourcesContent":["/* EXPORT ENTITY TYPES */\nexport * from './cash-flow-type'\nexport * from './financial-statement/financial-statement'\nexport * from './accounting-category/accounting-category'\nexport * from './transaction/transaction'\nexport * from './bank-account/bank-account'\nexport * from './bank-statement/bank-statement'\nexport * from './payment/payment'\nexport * from './account/account'\nexport * from './accounting-document/accounting-document'\nexport * from './recurring-cash-item/recurring-cash-item'\nexport * from './financial-institution/financial-institution'\nexport * from './fiscal-month/fiscal-month'\nexport * from './fiscal-quarter/fiscal-quarter'\nexport * from './fiscal-year/fiscal-year'\n\n/* EXPORT SERVICES */\nexport * from './transaction/transaction-period'\nexport * from './transaction/transaction-service'\nexport * from './bank-account/bank-account-service'\nexport * from './payment/payment-targets'\nexport * from './payment/payment-service'\nexport * from './payment/document-targets'\nexport * from './financial-statement/financial-statement-line'\nexport * from './financial-statement/financial-statement-service'\nexport * from './recurring-cash-item/recurring-schedule'\nexport * from './recurring-cash-item/recurring-cash-item-service'\nexport * from './account/account-match'\nexport * from './bank-statement/bank-source'\nexport * from './bank-statement/bank-parser'\nexport * from './bank-statement/bank-reconcile'\nexport * from './bank-statement/bank-import-type'\nexport * from './bank-statement/bank-import'\n\n/* IMPORT ENTITIES AND RESOLVERS */\nimport {\n entities as FinancialStatementEntities,\n resolvers as FinancialStatementResolvers,\n subscribers as FinancialStatementSubscribers\n} from './financial-statement'\nimport {\n entities as AccountingCategoryEntities,\n resolvers as AccountingCategoryResolvers,\n subscribers as AccountingCategorySubscribers\n} from './accounting-category'\nimport {\n entities as TransactionEntities,\n resolvers as TransactionResolvers,\n subscribers as TransactionSubscribers\n} from './transaction'\nimport {\n entities as BankAccountEntities,\n resolvers as BankAccountResolvers,\n subscribers as BankAccountSubscribers\n} from './bank-account'\nimport {\n entities as BankStatementEntities,\n resolvers as BankStatementResolvers,\n subscribers as BankStatementSubscribers\n} from './bank-statement'\nimport {\n entities as PaymentEntities,\n resolvers as PaymentResolvers,\n subscribers as PaymentSubscribers\n} from './payment'\nimport {\n entities as AccountEntities,\n resolvers as AccountResolvers,\n subscribers as AccountSubscribers\n} from './account'\nimport {\n entities as AccountingDocumentEntities,\n resolvers as AccountingDocumentResolvers,\n subscribers as AccountingDocumentSubscribers\n} from './accounting-document'\nimport {\n entities as RecurringCashItemEntities,\n resolvers as RecurringCashItemResolvers,\n subscribers as RecurringCashItemSubscribers\n} from './recurring-cash-item'\nimport {\n entities as FinancialInstitutionEntities,\n resolvers as FinancialInstitutionResolvers,\n subscribers as FinancialInstitutionSubscribers\n} from './financial-institution'\nimport {\n entities as FiscalMonthEntities,\n resolvers as FiscalMonthResolvers,\n subscribers as FiscalMonthSubscribers\n} from './fiscal-month'\nimport {\n entities as FiscalQuarterEntities,\n resolvers as FiscalQuarterResolvers,\n subscribers as FiscalQuarterSubscribers\n} from './fiscal-quarter'\nimport {\n entities as FiscalYearEntities,\n resolvers as FiscalYearResolvers,\n subscribers as FiscalYearSubscribers\n} from './fiscal-year'\n\nexport const entities = [\n ...FinancialStatementEntities,\n ...AccountingCategoryEntities,\n ...TransactionEntities,\n ...BankAccountEntities,\n ...BankStatementEntities,\n ...PaymentEntities,\n ...AccountEntities,\n ...AccountingDocumentEntities,\n ...RecurringCashItemEntities,\n ...FinancialInstitutionEntities,\n ...FiscalMonthEntities,\n ...FiscalQuarterEntities,\n ...FiscalYearEntities\n]\n\nexport const subscribers = [\n ...FinancialStatementSubscribers,\n ...AccountingCategorySubscribers,\n ...TransactionSubscribers,\n ...BankAccountSubscribers,\n ...BankStatementSubscribers,\n ...PaymentSubscribers,\n ...AccountSubscribers,\n ...AccountingDocumentSubscribers,\n ...RecurringCashItemSubscribers,\n ...FinancialInstitutionSubscribers,\n ...FiscalMonthSubscribers,\n ...FiscalQuarterSubscribers,\n ...FiscalYearSubscribers\n]\n\nexport const schema = {\n resolverClasses: [\n ...FinancialStatementResolvers,\n ...AccountingCategoryResolvers,\n ...TransactionResolvers,\n ...BankAccountResolvers,\n ...BankStatementResolvers,\n ...PaymentResolvers,\n ...AccountResolvers,\n ...AccountingDocumentResolvers,\n ...RecurringCashItemResolvers,\n ...FinancialInstitutionResolvers,\n ...FiscalMonthResolvers,\n ...FiscalQuarterResolvers,\n ...FiscalYearResolvers\n ]\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EntityManager } from 'typeorm';
|
|
2
|
+
import { Payment } from './payment';
|
|
3
|
+
export declare const INVOICE_TARGET = "Invoice";
|
|
4
|
+
export declare const TAX_INVOICE_TARGET = "TaxInvoice";
|
|
5
|
+
/** What a payment pays of its document, in the document's currency. */
|
|
6
|
+
export declare const paidOfDocument: (payment: Payment) => number;
|
|
7
|
+
/** The payments on some documents of a kind, or of some bank lines, with their line. */
|
|
8
|
+
export declare function documentPaymentsOf(tx: EntityManager, domain: {
|
|
9
|
+
id: string;
|
|
10
|
+
}, targetType: string, of: {
|
|
11
|
+
documentIds?: string[];
|
|
12
|
+
transactionIds?: string[];
|
|
13
|
+
}, options?: {
|
|
14
|
+
withDeleted?: boolean;
|
|
15
|
+
relations?: string[];
|
|
16
|
+
}): Promise<Payment[]>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paidOfDocument = exports.TAX_INVOICE_TARGET = exports.INVOICE_TARGET = void 0;
|
|
4
|
+
exports.documentPaymentsOf = documentPaymentsOf;
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const invoice_1 = require("@things-factory/invoice");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
const tax_invoice_1 = require("@things-factory/tax-invoice");
|
|
9
|
+
const transaction_1 = require("../transaction/transaction");
|
|
10
|
+
const payment_1 = require("./payment");
|
|
11
|
+
const payment_targets_1 = require("./payment-targets");
|
|
12
|
+
/*
|
|
13
|
+
* The bank lines that paid a commercial invoice or a tax invoice (ADR-0122). Both documents evidence the money - a
|
|
14
|
+
* contract round may settle it too - so they are EVIDENCE, each kind on its own: a local-L/C export's zero-rated tax
|
|
15
|
+
* invoice and its commercial invoice each evidence the whole deposit. The rules are the same in every application,
|
|
16
|
+
* so accounting registers both kinds; the document modules do not know accounting.
|
|
17
|
+
*/
|
|
18
|
+
exports.INVOICE_TARGET = 'Invoice';
|
|
19
|
+
exports.TAX_INVOICE_TARGET = 'TaxInvoice';
|
|
20
|
+
/** What a payment pays of its document, in the document's currency. */
|
|
21
|
+
const paidOfDocument = (payment) => Number(payment.targetAmount ?? payment.amount);
|
|
22
|
+
exports.paidOfDocument = paidOfDocument;
|
|
23
|
+
/** The payments on some documents of a kind, or of some bank lines, with their line. */
|
|
24
|
+
async function documentPaymentsOf(tx, domain, targetType, of, options = {}) {
|
|
25
|
+
return tx.getRepository(payment_1.Payment).find({
|
|
26
|
+
where: {
|
|
27
|
+
domain: { id: domain.id },
|
|
28
|
+
targetType,
|
|
29
|
+
...(of.documentIds ? { targetId: (0, typeorm_1.In)(of.documentIds) } : {}),
|
|
30
|
+
...(of.transactionIds ? { transaction: { id: (0, typeorm_1.In)(of.transactionIds) } } : {})
|
|
31
|
+
},
|
|
32
|
+
withDeleted: options.withDeleted,
|
|
33
|
+
relations: ['transaction', ...(options.relations ?? [])]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const fits = (amount, currency) => {
|
|
37
|
+
const scale = 10 ** (0, invoice_1.currencyDigits)(currency);
|
|
38
|
+
return Math.abs(amount * scale - Math.round(amount * scale)) <= 0.00001;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* A document is paid by bank lines on its side - money in for a sale, out for a purchase, the other way for a
|
|
42
|
+
* negative one - and by no more than its total, in its own currency. A line in another currency says what it paid of
|
|
43
|
+
* the document (targetAmount) and on what basis (notes); a line in the same currency says nothing more.
|
|
44
|
+
*/
|
|
45
|
+
async function checkDocuments(tx, domain, targetType, documents, codes) {
|
|
46
|
+
const payments = await documentPaymentsOf(tx, domain, targetType, { documentIds: documents.map(one => one.id) });
|
|
47
|
+
for (const document of documents) {
|
|
48
|
+
const ofDocument = payments.filter(one => one.targetId === document.id);
|
|
49
|
+
const incoming = document.sale === document.total >= 0;
|
|
50
|
+
for (const one of ofDocument) {
|
|
51
|
+
const line = one.transaction;
|
|
52
|
+
if (line.category !== transaction_1.TransactionCategory.CONFIRMED || line.type !== (incoming ? 'INCOME' : 'EXPENSE'))
|
|
53
|
+
throw new shell_1.Refusal(codes.side, {}, 'choose a deposit for a sale, a withdrawal for a purchase; the other way for a negative document');
|
|
54
|
+
const lineCurrency = line.currency ?? 'KRW';
|
|
55
|
+
if (lineCurrency === document.currency) {
|
|
56
|
+
if (one.targetAmount !== null && one.targetAmount !== undefined)
|
|
57
|
+
throw new shell_1.Refusal('document-payment-same-currency', {}, 'a line in the document currency says its amount once');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (one.targetAmount === null || one.targetAmount === undefined)
|
|
61
|
+
throw new shell_1.Refusal('document-payment-amount-required', { currency: document.currency }, 'record what the line paid in the document currency');
|
|
62
|
+
if (!one.notes?.trim())
|
|
63
|
+
throw new shell_1.Refusal('document-payment-basis-required', {}, 'record the exchange rate or settlement basis in notes');
|
|
64
|
+
}
|
|
65
|
+
if (!fits(Number(one.amount), lineCurrency) || !fits((0, exports.paidOfDocument)(one), document.currency))
|
|
66
|
+
throw new shell_1.Refusal('document-payment-precision', {}, 'an amount has more decimals than its currency');
|
|
67
|
+
}
|
|
68
|
+
if (ofDocument.reduce((sum, one) => sum + (0, exports.paidOfDocument)(one), 0) > Math.abs(document.total) + 0.00001)
|
|
69
|
+
throw new shell_1.Refusal(codes.over, {}, 'the payments are more than the document');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
(0, payment_targets_1.registerPaymentTarget)(exports.INVOICE_TARGET, {
|
|
73
|
+
purpose: payment_1.PaymentPurpose.EVIDENCE,
|
|
74
|
+
async label(tx, domain, ids) {
|
|
75
|
+
const invoices = await tx.getRepository(invoice_1.Invoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) } });
|
|
76
|
+
return new Map(invoices.map(one => [one.id, [one.number, one.direction === 'SALES' ? one.buyerName : one.supplierName].filter(Boolean).join(' ')]));
|
|
77
|
+
},
|
|
78
|
+
async validate(tx, domain, { targetIds }) {
|
|
79
|
+
const invoices = await tx.getRepository(invoice_1.Invoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
80
|
+
if (invoices.length !== new Set(targetIds).size)
|
|
81
|
+
throw new shell_1.Refusal('invoice-not-found', {}, 'the invoice was not found - reload and try again');
|
|
82
|
+
await checkDocuments(tx, domain, exports.INVOICE_TARGET, invoices.map(one => ({ id: one.id, currency: one.currency, total: Number(one.totalAmount), sale: one.direction === 'SALES' })), { side: 'invoice-payment-side', over: 'invoice-payment-over' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
(0, payment_targets_1.registerPaymentTarget)(exports.TAX_INVOICE_TARGET, {
|
|
86
|
+
purpose: payment_1.PaymentPurpose.EVIDENCE,
|
|
87
|
+
async label(tx, domain, ids) {
|
|
88
|
+
const invoices = await tx.getRepository(tax_invoice_1.TaxInvoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) } });
|
|
89
|
+
return new Map(invoices.map(one => [one.id, [one.issueDate, one.category === tax_invoice_1.Category.EXPORT ? one.buyerBusinessName : one.supplierBusinessName, one.approvalNo].filter(Boolean).join(' ')]));
|
|
90
|
+
},
|
|
91
|
+
/* A tax invoice is in won. */
|
|
92
|
+
async validate(tx, domain, { targetIds }) {
|
|
93
|
+
const invoices = await tx.getRepository(tax_invoice_1.TaxInvoice).find({ where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(targetIds) } });
|
|
94
|
+
if (invoices.length !== new Set(targetIds).size)
|
|
95
|
+
throw new shell_1.Refusal('tax-invoice-not-found', {}, 'the tax invoice was not found - reload and try again');
|
|
96
|
+
await checkDocuments(tx, domain, exports.TAX_INVOICE_TARGET, invoices.map(one => ({ id: one.id, currency: 'KRW', total: Number(one.totalAmount), sale: one.category === tax_invoice_1.Category.EXPORT })), { side: 'tax-invoice-payment-side', over: 'tax-invoice-payment-over' });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=document-targets.js.map
|