@coderoftime/fints 0.0.0-stage → 0.2.0
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/CHANGELOG.md +152 -0
- package/LICENSE +28 -0
- package/README.md +246 -2
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +168 -0
- package/dist/core/account.d.ts.map +1 -0
- package/dist/core/account.js +245 -0
- package/dist/core/account.js.map +1 -0
- package/dist/core/amount.d.ts +63 -0
- package/dist/core/amount.d.ts.map +1 -0
- package/dist/core/amount.js +192 -0
- package/dist/core/amount.js.map +1 -0
- package/dist/core/balance.d.ts +55 -0
- package/dist/core/balance.d.ts.map +1 -0
- package/dist/core/balance.js +54 -0
- package/dist/core/balance.js.map +1 -0
- package/dist/core/bank-info.d.ts +49 -0
- package/dist/core/bank-info.d.ts.map +1 -0
- package/dist/core/bank-info.js +52 -0
- package/dist/core/bank-info.js.map +1 -0
- package/dist/core/client.d.ts +161 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +2 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/config.d.ts +80 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +153 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/errors.d.ts +153 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +247 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/feedback.d.ts +72 -0
- package/dist/core/feedback.d.ts.map +1 -0
- package/dist/core/feedback.js +127 -0
- package/dist/core/feedback.js.map +1 -0
- package/dist/core/transaction.d.ts +103 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/core/transaction.js +96 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/core/transport.d.ts +64 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +149 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/formats/camt.d.ts +32 -0
- package/dist/formats/camt.d.ts.map +1 -0
- package/dist/formats/camt.js +242 -0
- package/dist/formats/camt.js.map +1 -0
- package/dist/formats/mt940.d.ts +54 -0
- package/dist/formats/mt940.d.ts.map +1 -0
- package/dist/formats/mt940.js +376 -0
- package/dist/formats/mt940.js.map +1 -0
- package/dist/formats/readers.d.ts +15 -0
- package/dist/formats/readers.d.ts.map +1 -0
- package/dist/formats/readers.js +42 -0
- package/dist/formats/readers.js.map +1 -0
- package/dist/formats/record-budget.d.ts +7 -0
- package/dist/formats/record-budget.d.ts.map +1 -0
- package/dist/formats/record-budget.js +17 -0
- package/dist/formats/record-budget.js.map +1 -0
- package/dist/formats/sepa-purpose.d.ts +28 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -0
- package/dist/formats/sepa-purpose.js +89 -0
- package/dist/formats/sepa-purpose.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/session/app-approval.d.ts +66 -0
- package/dist/session/app-approval.d.ts.map +1 -0
- package/dist/session/app-approval.js +226 -0
- package/dist/session/app-approval.js.map +1 -0
- package/dist/session/dialog-runner.d.ts +31 -0
- package/dist/session/dialog-runner.d.ts.map +1 -0
- package/dist/session/dialog-runner.js +111 -0
- package/dist/session/dialog-runner.js.map +1 -0
- package/dist/session/login-guard.d.ts +78 -0
- package/dist/session/login-guard.d.ts.map +1 -0
- package/dist/session/login-guard.js +181 -0
- package/dist/session/login-guard.js.map +1 -0
- package/dist/session/sepa-accounts.d.ts +40 -0
- package/dist/session/sepa-accounts.d.ts.map +1 -0
- package/dist/session/sepa-accounts.js +112 -0
- package/dist/session/sepa-accounts.js.map +1 -0
- package/dist/session/session-setup.d.ts +80 -0
- package/dist/session/session-setup.d.ts.map +1 -0
- package/dist/session/session-setup.js +182 -0
- package/dist/session/session-setup.js.map +1 -0
- package/dist/session/session-state.d.ts +50 -0
- package/dist/session/session-state.d.ts.map +1 -0
- package/dist/session/session-state.js +182 -0
- package/dist/session/session-state.js.map +1 -0
- package/dist/session/signed-dialog.d.ts +77 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +170 -0
- package/dist/session/signed-dialog.js.map +1 -0
- package/dist/session/tan-selection.d.ts +96 -0
- package/dist/session/tan-selection.d.ts.map +1 -0
- package/dist/session/tan-selection.js +189 -0
- package/dist/session/tan-selection.js.map +1 -0
- package/dist/session/user-prompts.d.ts +65 -0
- package/dist/session/user-prompts.d.ts.map +1 -0
- package/dist/session/user-prompts.js +184 -0
- package/dist/session/user-prompts.js.map +1 -0
- package/dist/tan/challenge.d.ts +135 -0
- package/dist/tan/challenge.d.ts.map +1 -0
- package/dist/tan/challenge.js +184 -0
- package/dist/tan/challenge.js.map +1 -0
- package/dist/tan/flicker.d.ts +42 -0
- package/dist/tan/flicker.d.ts.map +1 -0
- package/dist/tan/flicker.js +296 -0
- package/dist/tan/flicker.js.map +1 -0
- package/dist/tan/tan-image.d.ts +22 -0
- package/dist/tan/tan-image.d.ts.map +1 -0
- package/dist/tan/tan-image.js +43 -0
- package/dist/tan/tan-image.js.map +1 -0
- package/dist/tan/tan-models.d.ts +149 -0
- package/dist/tan/tan-models.d.ts.map +1 -0
- package/dist/tan/tan-models.js +153 -0
- package/dist/tan/tan-models.js.map +1 -0
- package/dist/util/async.d.ts +22 -0
- package/dist/util/async.d.ts.map +1 -0
- package/dist/util/async.js +45 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/bytes.d.ts +22 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +128 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/call-context.d.ts +12 -0
- package/dist/util/call-context.d.ts.map +1 -0
- package/dist/util/call-context.js +30 -0
- package/dist/util/call-context.js.map +1 -0
- package/dist/util/dates.d.ts +55 -0
- package/dist/util/dates.d.ts.map +1 -0
- package/dist/util/dates.js +189 -0
- package/dist/util/dates.js.map +1 -0
- package/dist/util/errors.d.ts +17 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +24 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/immutable-map.d.ts +16 -0
- package/dist/util/immutable-map.d.ts.map +1 -0
- package/dist/util/immutable-map.js +38 -0
- package/dist/util/immutable-map.js.map +1 -0
- package/dist/util/json.d.ts +17 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +62 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/text.d.ts +37 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +107 -0
- package/dist/util/text.js.map +1 -0
- package/dist/util/xml.d.ts +36 -0
- package/dist/util/xml.d.ts.map +1 -0
- package/dist/util/xml.js +194 -0
- package/dist/util/xml.js.map +1 -0
- package/dist/v3/account-extension.d.ts +25 -0
- package/dist/v3/account-extension.d.ts.map +1 -0
- package/dist/v3/account-extension.js +173 -0
- package/dist/v3/account-extension.js.map +1 -0
- package/dist/v3/bank-connection.d.ts +34 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +58 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +112 -0
- package/dist/v3/dialog.d.ts.map +1 -0
- package/dist/v3/dialog.js +241 -0
- package/dist/v3/dialog.js.map +1 -0
- package/dist/v3/message.d.ts +85 -0
- package/dist/v3/message.d.ts.map +1 -0
- package/dist/v3/message.js +303 -0
- package/dist/v3/message.js.map +1 -0
- package/dist/v3/parameter-store.d.ts +75 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +143 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +105 -0
- package/dist/v3/parameters.d.ts.map +1 -0
- package/dist/v3/parameters.js +450 -0
- package/dist/v3/parameters.js.map +1 -0
- package/dist/v3/protocol.d.ts +61 -0
- package/dist/v3/protocol.d.ts.map +1 -0
- package/dist/v3/protocol.js +113 -0
- package/dist/v3/protocol.js.map +1 -0
- package/dist/v3/queries.d.ts +51 -0
- package/dist/v3/queries.d.ts.map +1 -0
- package/dist/v3/queries.js +120 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +106 -0
- package/dist/v3/segments.d.ts.map +1 -0
- package/dist/v3/segments.js +325 -0
- package/dist/v3/segments.js.map +1 -0
- package/dist/v3/syntax.d.ts +127 -0
- package/dist/v3/syntax.d.ts.map +1 -0
- package/dist/v3/syntax.js +367 -0
- package/dist/v3/syntax.js.map +1 -0
- package/package.json +67 -4
- package/src/client.ts +252 -0
- package/src/core/account.ts +323 -0
- package/src/core/amount.ts +211 -0
- package/src/core/balance.ts +85 -0
- package/src/core/bank-info.ts +66 -0
- package/src/core/client.ts +186 -0
- package/src/core/config.ts +182 -0
- package/src/core/errors.ts +277 -0
- package/src/core/feedback.ts +140 -0
- package/src/core/transaction.ts +161 -0
- package/src/core/transport.ts +189 -0
- package/src/formats/camt.ts +266 -0
- package/src/formats/mt940.ts +419 -0
- package/src/formats/readers.ts +49 -0
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +94 -0
- package/src/index.ts +70 -0
- package/src/session/app-approval.ts +278 -0
- package/src/session/dialog-runner.ts +122 -0
- package/src/session/login-guard.ts +221 -0
- package/src/session/sepa-accounts.ts +115 -0
- package/src/session/session-setup.ts +184 -0
- package/src/session/session-state.ts +204 -0
- package/src/session/signed-dialog.ts +192 -0
- package/src/session/tan-selection.ts +203 -0
- package/src/session/user-prompts.ts +193 -0
- package/src/tan/challenge.ts +261 -0
- package/src/tan/flicker.ts +308 -0
- package/src/tan/tan-image.ts +38 -0
- package/src/tan/tan-models.ts +252 -0
- package/src/util/async.ts +48 -0
- package/src/util/bytes.ts +129 -0
- package/src/util/call-context.ts +41 -0
- package/src/util/dates.ts +210 -0
- package/src/util/errors.ts +27 -0
- package/src/util/immutable-map.ts +47 -0
- package/src/util/json.ts +67 -0
- package/src/util/text.ts +112 -0
- package/src/util/xml.ts +183 -0
- package/src/v3/account-extension.ts +185 -0
- package/src/v3/bank-connection.ts +67 -0
- package/src/v3/dialog.ts +283 -0
- package/src/v3/message.ts +373 -0
- package/src/v3/parameter-store.ts +160 -0
- package/src/v3/parameters.ts +524 -0
- package/src/v3/protocol.ts +148 -0
- package/src/v3/queries.ts +190 -0
- package/src/v3/segments.ts +374 -0
- package/src/v3/syntax.ts +369 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
|
|
2
|
+
|
|
3
|
+
import type { Account } from '../core/account.js';
|
|
4
|
+
import type { Balance } from '../core/balance.js';
|
|
5
|
+
import { FinTsBankError, FinTsProtocolError, FinTsUnsupportedError } from '../core/errors.js';
|
|
6
|
+
import type { FinTsFeedback } from '../core/feedback.js';
|
|
7
|
+
import type { Transaction, TransactionFormat } from '../core/transaction.js';
|
|
8
|
+
import { camtTransactions, mt940Transactions } from '../formats/readers.js';
|
|
9
|
+
import type { TanMedium } from '../tan/tan-models.js';
|
|
10
|
+
import type { IncomingMessage3 } from './message.js';
|
|
11
|
+
import type { ParameterStore } from './parameter-store.js';
|
|
12
|
+
import { lists, offeredVersion, requireBankParameters } from './parameters.js';
|
|
13
|
+
import { balanceAccount, camtAccount, parseBalance, parseSepaAccounts, parseTanMedia, Segments } from './segments.js';
|
|
14
|
+
import type { Segment, SegmentSpec } from './syntax.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A business transaction: the order to send for each page, and how to read all pages the bank sent.
|
|
18
|
+
*
|
|
19
|
+
* Where the bank does not offer the transaction, the builders of the lookups the session makes for itself, of the TAN
|
|
20
|
+
* media and the SEPA accounts, return `undefined`, and those of the app's queries, of a balance and of transactions,
|
|
21
|
+
* throw `FinTsUnsupportedError`.
|
|
22
|
+
*/
|
|
23
|
+
export interface Query<T> {
|
|
24
|
+
/** The order for the page at `continuation`; `undefined` for the first page. */
|
|
25
|
+
readonly order: (continuation: string | undefined) => SegmentSpec;
|
|
26
|
+
|
|
27
|
+
/** Reads the result; throws `FormatError` for malformed data. */
|
|
28
|
+
readonly read: (pages: readonly IncomingMessage3[]) => T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The segments of `type` in all `pages`. */
|
|
32
|
+
function all(pages: readonly IncomingMessage3[], type: string): Segment[] {
|
|
33
|
+
return pages.flatMap((p) => p.findAll(type));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function version(
|
|
37
|
+
parameters: ParameterStore,
|
|
38
|
+
transaction: string,
|
|
39
|
+
implemented: readonly number[],
|
|
40
|
+
options: { account?: Account; requiresEntry?: boolean } = {},
|
|
41
|
+
): number | undefined {
|
|
42
|
+
return offeredVersion(
|
|
43
|
+
parameters.bankParameters,
|
|
44
|
+
parameters.userParameters,
|
|
45
|
+
transaction,
|
|
46
|
+
new Set(implemented),
|
|
47
|
+
options,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Lists the TAN media, and keeps the names as the bank sent them in `parameters`; `undefined` if the bank does not. */
|
|
52
|
+
export function tanMediaQuery(parameters: ParameterStore): Query<TanMedium[]> | undefined {
|
|
53
|
+
const offered = version(parameters, 'HKTAB', [4, 5]);
|
|
54
|
+
if (offered === undefined) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
order: () => Segments.tanMedia(offered),
|
|
57
|
+
read: (pages) => {
|
|
58
|
+
const names = new Map<string, string>();
|
|
59
|
+
const media = all(pages, 'HITAB').flatMap((s) => parseTanMedia(s, names));
|
|
60
|
+
parameters.keepTanMediumNames(names);
|
|
61
|
+
return media;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Lists IBAN and BIC of the accounts; `undefined` if the bank does not offer it. */
|
|
67
|
+
export function sepaAccountsQuery(parameters: ParameterStore): Query<Account[]> | undefined {
|
|
68
|
+
const offered = version(parameters, 'HKSPA', [1, 2, 3], { requiresEntry: true });
|
|
69
|
+
if (offered === undefined) return undefined;
|
|
70
|
+
const { countryCode, bankCode } = parameters.config;
|
|
71
|
+
return {
|
|
72
|
+
order: (continuation) => Segments.sepaAccounts(offered, continuation),
|
|
73
|
+
read: (pages) => all(pages, 'HISPA').flatMap((s) => parseSepaAccounts(s, countryCode, bankCode)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The balance (HISAL) of `account` in `records`: the one whose account matches. A single record must match as well,
|
|
79
|
+
* though the query asked for this one account only: it names the account it is for.
|
|
80
|
+
*
|
|
81
|
+
* Throws `FinTsBankError` with the bank's `feedback` if there is none, and `FinTsProtocolError` if none matches.
|
|
82
|
+
*
|
|
83
|
+
* @internal Exported for the tests.
|
|
84
|
+
*/
|
|
85
|
+
export function balanceRecord(
|
|
86
|
+
records: readonly Segment[],
|
|
87
|
+
account: Account,
|
|
88
|
+
feedback: readonly FinTsFeedback[],
|
|
89
|
+
): Segment {
|
|
90
|
+
if (records.length === 0) throw new FinTsBankError(feedback, 'No balance returned');
|
|
91
|
+
const match = records.find((record) => balanceAccount(record).matches(account));
|
|
92
|
+
if (match === undefined) {
|
|
93
|
+
throw new FinTsProtocolError('The bank returned balances, but none for the requested account');
|
|
94
|
+
}
|
|
95
|
+
return match;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The balance of `account`, which the session has taken as the bank knows it, with the transactions the bank permits
|
|
100
|
+
* for it (`SepaAccounts.complete`); throws `FinTsUnsupportedError` if the bank does not report balances.
|
|
101
|
+
*/
|
|
102
|
+
export function balanceQuery(parameters: ParameterStore, account: Account): Query<Balance> {
|
|
103
|
+
const offered = version(parameters, 'HKSAL', [6, 7, 8], { account });
|
|
104
|
+
if (offered === undefined) {
|
|
105
|
+
throw new FinTsUnsupportedError('The bank reports no balance (HKSAL) for this account');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
order: (continuation) => Segments.balance(offered, account, continuation),
|
|
109
|
+
read: (pages) =>
|
|
110
|
+
parseBalance(
|
|
111
|
+
balanceRecord(
|
|
112
|
+
all(pages, 'HISAL'),
|
|
113
|
+
account,
|
|
114
|
+
pages.flatMap((p) => p.feedback),
|
|
115
|
+
),
|
|
116
|
+
account,
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The transactions of `account` in `format`, from the day of `from` to that of `to`. The session has taken `account` as
|
|
123
|
+
* the bank knows it, with the transactions the bank permits for it and its currency (`SepaAccounts.complete`).
|
|
124
|
+
*
|
|
125
|
+
* The client reads camt.052 only, so camt counts as offered only where the bank offers a camt.052 format. With `auto`
|
|
126
|
+
* the client takes camt where the bank offers it, unless the user parameter data list only MT940 transactions for the
|
|
127
|
+
* account.
|
|
128
|
+
*
|
|
129
|
+
* Throws `FinTsUnsupportedError` if the bank does not offer the chosen format for the account.
|
|
130
|
+
*/
|
|
131
|
+
export function transactionsQuery(
|
|
132
|
+
parameters: ParameterStore,
|
|
133
|
+
account: Account,
|
|
134
|
+
format: TransactionFormat,
|
|
135
|
+
from: Date | undefined,
|
|
136
|
+
to: Date | undefined,
|
|
137
|
+
): Query<Transaction[]> {
|
|
138
|
+
const camtFormats = requireBankParameters(parameters.bankParameters).camtFormats.filter((f) =>
|
|
139
|
+
f.includes('camt.052'),
|
|
140
|
+
);
|
|
141
|
+
const camtVersion = camtFormats.length === 0 ? undefined : version(parameters, 'HKCAZ', [1], { account });
|
|
142
|
+
const camt =
|
|
143
|
+
format === 'camt' ||
|
|
144
|
+
(format === 'auto' && camtVersion !== undefined && (lists(account, 'HKCAZ') || !lists(account, 'HKKAZ')));
|
|
145
|
+
const offered = camt ? camtVersion : version(parameters, 'HKKAZ', [6, 7], { account });
|
|
146
|
+
if (offered === undefined) {
|
|
147
|
+
throw new FinTsUnsupportedError(
|
|
148
|
+
`The bank offers no ${camt ? 'camt ' : ''}transactions (${camt ? 'HKCAZ' : 'HKKAZ'}) for this account`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (camt) {
|
|
152
|
+
return {
|
|
153
|
+
order: (continuation) => Segments.transactionsCamt(offered, account, camtFormats, { from, to, continuation }),
|
|
154
|
+
// The first element names the account, the third element group holds the booked documents, the fourth
|
|
155
|
+
// element the pending one.
|
|
156
|
+
read: (pages) => {
|
|
157
|
+
const segments = all(pages, 'HICAZ');
|
|
158
|
+
if (!segments.every((s) => camtAccount(s).matches(account))) {
|
|
159
|
+
throw new FinTsProtocolError('The bank returned the transactions of another account than the one asked for');
|
|
160
|
+
}
|
|
161
|
+
return camtTransactions(
|
|
162
|
+
segments.flatMap((s) =>
|
|
163
|
+
s
|
|
164
|
+
.group(3)
|
|
165
|
+
.filter((d) => !d.isEmpty)
|
|
166
|
+
.map((d) => d.bytes),
|
|
167
|
+
),
|
|
168
|
+
segments.map((s) => s.binary(4)).filter((d) => d !== undefined),
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
order: (continuation) => Segments.transactionsMt940(offered, account, { from, to, continuation }),
|
|
175
|
+
// The first element holds the booked transactions, the second the pending ones. Unlike HICAZ, HIKAZ names no
|
|
176
|
+
// account, and banks write the account line of a statement (`:25:`) in many ways, so it is not compared with
|
|
177
|
+
// `account`: a strict comparison would fail every query of such a bank.
|
|
178
|
+
read: (pages) =>
|
|
179
|
+
mt940Transactions(
|
|
180
|
+
all(pages, 'HIKAZ')
|
|
181
|
+
.map((s) => s.binary(1))
|
|
182
|
+
.filter((d) => d !== undefined),
|
|
183
|
+
all(pages, 'HIKAZ')
|
|
184
|
+
.map((s) => s.binary(2))
|
|
185
|
+
.filter((d) => d !== undefined),
|
|
186
|
+
// The currency of statements that state none.
|
|
187
|
+
account.currency,
|
|
188
|
+
),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// Internal to the package, and named after the FinTS specification. Documentation is where the meaning is not evident.
|
|
2
|
+
|
|
3
|
+
import { Account } from '../core/account.js';
|
|
4
|
+
import { Amount, withMark } from '../core/amount.js';
|
|
5
|
+
import { Balance } from '../core/balance.js';
|
|
6
|
+
import { TanMedium, tanMediumStatusOf } from '../tan/tan-models.js';
|
|
7
|
+
import { formatFintsDate, parseFintsDate, parseFintsDateTime } from '../util/dates.js';
|
|
8
|
+
import { FormatError } from '../util/errors.js';
|
|
9
|
+
import { type Segment, SegmentSpec, type SegmentValue, Verbatim } from './syntax.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The filler the specification names for a reference that does not exist.
|
|
13
|
+
*
|
|
14
|
+
* The bank puts it into a dummy answer when no approval is needed. The client sends it as the TAN medium name in the
|
|
15
|
+
* dialog that looks up the TAN media, before it knows a medium; the bank ignores it there (PIN/TAN B.4.3.1.3).
|
|
16
|
+
*/
|
|
17
|
+
export const noReference = 'noref';
|
|
18
|
+
|
|
19
|
+
/** Bank identification (KIK): country code and bank code. */
|
|
20
|
+
function bankIdentification(countryCode: string, bankCode: string): SegmentValue {
|
|
21
|
+
return [countryCode, bankCode];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** National account (KTV): account number, sub-account and bank identification. */
|
|
25
|
+
function nationalAccount(a: Account): SegmentValue[] {
|
|
26
|
+
return [a.accountNumber, a.subAccount, a.countryCode, a.bankCode];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** International account (KTI): IBAN, BIC, then the national account. */
|
|
30
|
+
function internationalAccount(a: Account): SegmentValue[] {
|
|
31
|
+
return [a.iban, a.bic, ...nationalAccount(a)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dateOrEmpty(date: Date | undefined): string | undefined {
|
|
35
|
+
return date === undefined ? undefined : formatFintsDate(date);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// HKTAN fields: order reference (Auftragsreferenz), "more TANs follow" and the TAN medium name.
|
|
39
|
+
const orderReferenceField = 5;
|
|
40
|
+
const moreTansField = 6;
|
|
41
|
+
const tanMediumField = 11;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builders for the request segments the client sends. Values the bank issued, such as the customer system ID, the TAN
|
|
45
|
+
* order reference and the position to continue at, go back as `Verbatim`.
|
|
46
|
+
*/
|
|
47
|
+
export const Segments = Object.freeze({
|
|
48
|
+
/** HKIDN: who is logging in, from which customer system. */
|
|
49
|
+
identification(parts: {
|
|
50
|
+
countryCode: string;
|
|
51
|
+
bankCode: string;
|
|
52
|
+
customerId: string;
|
|
53
|
+
systemId: string;
|
|
54
|
+
systemIdRequired: boolean;
|
|
55
|
+
}): SegmentSpec {
|
|
56
|
+
return new SegmentSpec('HKIDN', 2, [
|
|
57
|
+
bankIdentification(parts.countryCode, parts.bankCode),
|
|
58
|
+
parts.customerId,
|
|
59
|
+
new Verbatim(parts.systemId),
|
|
60
|
+
parts.systemIdRequired ? 1 : 0,
|
|
61
|
+
]);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/** HKVVB: the parameter data versions the client knows, the dialog language and the registered product. */
|
|
65
|
+
processingPreparation(parts: {
|
|
66
|
+
bpdVersion: number;
|
|
67
|
+
updVersion: number;
|
|
68
|
+
language: number;
|
|
69
|
+
productId: string;
|
|
70
|
+
productVersion: string;
|
|
71
|
+
}): SegmentSpec {
|
|
72
|
+
return new SegmentSpec('HKVVB', 3, [
|
|
73
|
+
parts.bpdVersion,
|
|
74
|
+
parts.updVersion,
|
|
75
|
+
parts.language,
|
|
76
|
+
parts.productId,
|
|
77
|
+
parts.productVersion,
|
|
78
|
+
]);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/** HKSYN: requests a new customer system ID. */
|
|
82
|
+
synchronization(): SegmentSpec {
|
|
83
|
+
return new SegmentSpec('HKSYN', 3, [0]);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
dialogEnd(dialogId: string): SegmentSpec {
|
|
87
|
+
return new SegmentSpec('HKEND', 1, [new Verbatim(dialogId)]);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* HKTAN process 4: asks for strong customer authentication of the `segmentType` sent in the same message, with the TAN
|
|
92
|
+
* medium `mediumName`, as the bank sent it.
|
|
93
|
+
*/
|
|
94
|
+
tanProcess4(version: number, segmentType: string, mediumName?: string): SegmentSpec {
|
|
95
|
+
return SegmentSpec.numbered('HKTAN', version, {
|
|
96
|
+
1: '4',
|
|
97
|
+
2: segmentType,
|
|
98
|
+
[tanMediumField]: Verbatim.of(mediumName),
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** HKTAN process 2: submits the TAN (carried in the signature) for the order with `reference`. */
|
|
103
|
+
tanProcess2(version: number, reference: string): SegmentSpec {
|
|
104
|
+
return SegmentSpec.numbered('HKTAN', version, {
|
|
105
|
+
1: '2',
|
|
106
|
+
[orderReferenceField]: new Verbatim(reference),
|
|
107
|
+
[moreTansField]: false,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/** HKTAN process S: asks whether the order with `reference` was approved in the banking app. */
|
|
112
|
+
tanProcessStatus(version: number, reference: string): SegmentSpec {
|
|
113
|
+
return SegmentSpec.numbered('HKTAN', version, {
|
|
114
|
+
1: 'S',
|
|
115
|
+
[orderReferenceField]: new Verbatim(reference),
|
|
116
|
+
[moreTansField]: false,
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/** HKSAL: the balance of one account (not of all accounts). */
|
|
121
|
+
balance(version: number, account: Account, continuation?: string): SegmentSpec {
|
|
122
|
+
return new SegmentSpec('HKSAL', version, [
|
|
123
|
+
version >= 7 ? internationalAccount(account) : nationalAccount(account),
|
|
124
|
+
false,
|
|
125
|
+
null,
|
|
126
|
+
Verbatim.of(continuation),
|
|
127
|
+
]);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/** HKKAZ: MT940 transactions of one account. */
|
|
131
|
+
transactionsMt940(
|
|
132
|
+
version: number,
|
|
133
|
+
account: Account,
|
|
134
|
+
period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
|
|
135
|
+
): SegmentSpec {
|
|
136
|
+
return new SegmentSpec('HKKAZ', version, [
|
|
137
|
+
version >= 7 ? internationalAccount(account) : nationalAccount(account),
|
|
138
|
+
false,
|
|
139
|
+
dateOrEmpty(period.from),
|
|
140
|
+
dateOrEmpty(period.to),
|
|
141
|
+
null,
|
|
142
|
+
Verbatim.of(period.continuation),
|
|
143
|
+
]);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/** HKCAZ: camt transactions of one account in one of `formats`. */
|
|
147
|
+
transactionsCamt(
|
|
148
|
+
version: number,
|
|
149
|
+
account: Account,
|
|
150
|
+
formats: readonly string[],
|
|
151
|
+
period: { from?: Date | undefined; to?: Date | undefined; continuation?: string | undefined },
|
|
152
|
+
): SegmentSpec {
|
|
153
|
+
return new SegmentSpec('HKCAZ', version, [
|
|
154
|
+
internationalAccount(account),
|
|
155
|
+
formats,
|
|
156
|
+
false,
|
|
157
|
+
dateOrEmpty(period.from),
|
|
158
|
+
dateOrEmpty(period.to),
|
|
159
|
+
null,
|
|
160
|
+
Verbatim.of(period.continuation),
|
|
161
|
+
]);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/** HKSPA: IBAN and BIC of all accounts. */
|
|
165
|
+
sepaAccounts(version: number, continuation?: string): SegmentSpec {
|
|
166
|
+
return new SegmentSpec('HKSPA', version, [null, ...(version >= 2 ? [null, Verbatim.of(continuation)] : [])]);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/** HKTAB: all TAN media of all classes. */
|
|
170
|
+
tanMedia(version: number): SegmentSpec {
|
|
171
|
+
return new SegmentSpec('HKTAB', version, [0, 'A']);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/** Positions of the data elements in the TAN part of a response (HITAN versions 6 and 7), after the segment header. */
|
|
176
|
+
const Hitan = { process: 1, reference: 3, challenge: 4, hhdUc: 5, validUntil: 6, mediumName: 7 } as const;
|
|
177
|
+
|
|
178
|
+
/** The TAN part of a response (HITAN). */
|
|
179
|
+
export class TanResponse {
|
|
180
|
+
constructor(
|
|
181
|
+
/** The TAN process the segment answers: `4` asks for an approval, `S` answers a status request. */
|
|
182
|
+
readonly process: string | undefined,
|
|
183
|
+
/** The order reference, as `Segment.verbatim` reads it, to send back byte for byte. */
|
|
184
|
+
readonly reference: string | undefined,
|
|
185
|
+
readonly challenge: string | undefined,
|
|
186
|
+
readonly hhdUc: Uint8Array | undefined,
|
|
187
|
+
readonly validUntil: Date | undefined,
|
|
188
|
+
readonly mediumName: string | undefined,
|
|
189
|
+
) {}
|
|
190
|
+
|
|
191
|
+
static parse(s: Segment): TanResponse {
|
|
192
|
+
return new TanResponse(
|
|
193
|
+
s.text(Hitan.process),
|
|
194
|
+
s.verbatim(Hitan.reference),
|
|
195
|
+
s.text(Hitan.challenge),
|
|
196
|
+
s.binary(Hitan.hhdUc),
|
|
197
|
+
parseFintsDateTime(s.text(Hitan.validUntil, 0), s.text(Hitan.validUntil, 1)),
|
|
198
|
+
s.text(Hitan.mediumName),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Whether the bank needs no approval although it answered with a TAN segment. */
|
|
203
|
+
get isDummy(): boolean {
|
|
204
|
+
return this.reference === undefined || this.reference === noReference;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Positions of the data elements in a balance (HISAL), after the segment header. */
|
|
209
|
+
const Hisal = {
|
|
210
|
+
account: 1,
|
|
211
|
+
productName: 2,
|
|
212
|
+
accountCurrency: 3,
|
|
213
|
+
bookedBalance: 4,
|
|
214
|
+
pendingBalance: 5,
|
|
215
|
+
creditLine: 6,
|
|
216
|
+
availableAmount: 7,
|
|
217
|
+
usedAmount: 8,
|
|
218
|
+
overdraft: 9,
|
|
219
|
+
bookingTime: 10,
|
|
220
|
+
} as const;
|
|
221
|
+
|
|
222
|
+
/** Positions inside a balance (SDO): credit/debit mark, value, currency, date and time. */
|
|
223
|
+
const Sdo = { mark: 0, value: 1, currency: 2, date: 3, time: 4 } as const;
|
|
224
|
+
|
|
225
|
+
/** An amount with currency (BTG) at `index`, or `undefined` if empty. */
|
|
226
|
+
function amountAt(s: Segment, index: number, fallbackCurrency: string): Amount | undefined {
|
|
227
|
+
const value = s.text(index, 0);
|
|
228
|
+
if (value === undefined) return undefined;
|
|
229
|
+
return Amount.parseDecimal(value, s.text(index, 1) ?? fallbackCurrency);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Parses a balance (HISAL); throws `FormatError` if the booked balance is missing or malformed, or a balance has no
|
|
234
|
+
* credit/debit mark.
|
|
235
|
+
*/
|
|
236
|
+
export function parseBalance(s: Segment, account: Account): Balance {
|
|
237
|
+
const currency = s.text(Hisal.accountCurrency) ?? account.currency;
|
|
238
|
+
const signedBalance = (index: number): Amount | undefined => {
|
|
239
|
+
const value = s.text(index, Sdo.value);
|
|
240
|
+
if (value === undefined) return undefined;
|
|
241
|
+
const amountCurrency = s.text(index, Sdo.currency) ?? currency;
|
|
242
|
+
if (amountCurrency === undefined) throw new FormatError('Balance without currency');
|
|
243
|
+
return withMark(Amount.parseDecimal(value, amountCurrency), s.text(index, Sdo.mark), 'C', 'D');
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const booked = signedBalance(Hisal.bookedBalance);
|
|
247
|
+
if (booked === undefined) throw new FormatError('Balance without booked amount');
|
|
248
|
+
const bookedDate = s.text(Hisal.bookedBalance, Sdo.date);
|
|
249
|
+
const bookedAsOf = parseFintsDate(bookedDate);
|
|
250
|
+
if (bookedAsOf === undefined) throw new FormatError('Balance without date');
|
|
251
|
+
return new Balance({
|
|
252
|
+
account,
|
|
253
|
+
productName: s.text(Hisal.productName),
|
|
254
|
+
booked,
|
|
255
|
+
bookedAsOf,
|
|
256
|
+
bookedAsOfTime: parseFintsDateTime(bookedDate, s.text(Hisal.bookedBalance, Sdo.time)),
|
|
257
|
+
pending: signedBalance(Hisal.pendingBalance),
|
|
258
|
+
creditLine: amountAt(s, Hisal.creditLine, booked.currency),
|
|
259
|
+
available: amountAt(s, Hisal.availableAmount, booked.currency),
|
|
260
|
+
used: amountAt(s, Hisal.usedAmount, booked.currency),
|
|
261
|
+
overdraft: amountAt(s, Hisal.overdraft, booked.currency),
|
|
262
|
+
bookingDate: parseFintsDate(s.text(Hisal.bookingTime, 0)),
|
|
263
|
+
bookingTime: parseFintsDateTime(s.text(Hisal.bookingTime, 0), s.text(Hisal.bookingTime, 1)),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The account a balance (HISAL) belongs to: international (IBAN, BIC, then national) from version 7, national before.
|
|
269
|
+
*/
|
|
270
|
+
export function balanceAccount(s: Segment): Account {
|
|
271
|
+
return accountAt(s, Hisal.account, s.version >= 7);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The account camt transactions (HICAZ) belong to, which the first element names (international). */
|
|
275
|
+
export function camtAccount(s: Segment): Account {
|
|
276
|
+
return accountAt(s, 1, true);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The account at `index` of `s`: an `international` one (KTI), else a national one (KTV). */
|
|
280
|
+
function accountAt(s: Segment, index: number, international: boolean): Account {
|
|
281
|
+
const fields = s.texts(index);
|
|
282
|
+
const national = international ? 2 : 0;
|
|
283
|
+
return new Account({
|
|
284
|
+
iban: international ? fields[0] : undefined,
|
|
285
|
+
bic: international ? fields[1] : undefined,
|
|
286
|
+
accountNumber: fields[national],
|
|
287
|
+
subAccount: fields[national + 1],
|
|
288
|
+
countryCode: fields[national + 2] ?? '',
|
|
289
|
+
bankCode: fields[national + 3] ?? '',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Positions inside an account of the SEPA account list (HISPA "Kontoverbindung SEPA"). */
|
|
294
|
+
const Hispa = {
|
|
295
|
+
isSepaAccount: 0,
|
|
296
|
+
iban: 1,
|
|
297
|
+
bic: 2,
|
|
298
|
+
accountNumber: 3,
|
|
299
|
+
subAccount: 4,
|
|
300
|
+
countryCode: 5,
|
|
301
|
+
bankCode: 6,
|
|
302
|
+
} as const;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Parses the SEPA account list (HISPA), one account per data element. An account the bank marks as no payment account
|
|
306
|
+
* ("Kontoverwendung SEPA" N) is left out: it has no SEPA connection.
|
|
307
|
+
*/
|
|
308
|
+
export function parseSepaAccounts(s: Segment, defaultCountry: string, defaultBank: string): Account[] {
|
|
309
|
+
const accounts: Account[] = [];
|
|
310
|
+
for (let i = 1; i < s.length; i++) {
|
|
311
|
+
if (s.text(i, Hispa.isSepaAccount) === 'N') continue;
|
|
312
|
+
if (s.text(i, Hispa.iban) === undefined && s.text(i, Hispa.accountNumber) === undefined) continue;
|
|
313
|
+
accounts.push(
|
|
314
|
+
new Account({
|
|
315
|
+
iban: s.text(i, Hispa.iban),
|
|
316
|
+
bic: s.text(i, Hispa.bic),
|
|
317
|
+
accountNumber: s.text(i, Hispa.accountNumber),
|
|
318
|
+
subAccount: s.text(i, Hispa.subAccount),
|
|
319
|
+
countryCode: s.text(i, Hispa.countryCode) ?? defaultCountry,
|
|
320
|
+
bankCode: s.text(i, Hispa.bankCode) ?? defaultBank,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return accounts;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Elements of the account a TAN medium belongs to, a national account (`ktv`: account number, sub-account, country
|
|
329
|
+
* code, bank code), which shifts every field behind it.
|
|
330
|
+
*/
|
|
331
|
+
const accountWidth = 4;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Parses the TAN media list (HITAB versions 4 and 5, PIN/TAN "TAN-Medium-Liste").
|
|
335
|
+
*
|
|
336
|
+
* Each name whose text is not what `Segment.verbatim` reads, such as a name in UTF-8, goes into `namesAsSent`, keyed
|
|
337
|
+
* by its text: it has to go back as the bank sent it, which its text does not tell where it fits ISO 8859-1.
|
|
338
|
+
*/
|
|
339
|
+
export function parseTanMedia(s: Segment, namesAsSent?: Map<string, string>): TanMedium[] {
|
|
340
|
+
// Version 5 adds the security function in front of the card number.
|
|
341
|
+
const version5 = s.version >= 5;
|
|
342
|
+
const cardNumber = version5 ? 3 : 2;
|
|
343
|
+
// The card number, card sequence number and card type are followed by the account. Behind it come: valid from, valid
|
|
344
|
+
// until, TAN list number, name, masked and full mobile number, SMS charge account (six elements), number of free TANs,
|
|
345
|
+
// last use, activation.
|
|
346
|
+
const after = cardNumber + 3 + accountWidth;
|
|
347
|
+
const media: TanMedium[] = [];
|
|
348
|
+
// The first data element is the TAN usage option; the media follow.
|
|
349
|
+
for (let i = 2; i < s.length; i++) {
|
|
350
|
+
const fields = s.texts(i);
|
|
351
|
+
const mediumClass = fields[0];
|
|
352
|
+
if (mediumClass === undefined) continue;
|
|
353
|
+
const name = fields[after + 3];
|
|
354
|
+
const nameAsSent = s.verbatim(i, after + 3);
|
|
355
|
+
if (name !== undefined && nameAsSent !== undefined && nameAsSent !== name) namesAsSent?.set(name, nameAsSent);
|
|
356
|
+
media.push(
|
|
357
|
+
new TanMedium({
|
|
358
|
+
mediumClass,
|
|
359
|
+
status: tanMediumStatusOf(fields[1]),
|
|
360
|
+
securityFunction: version5 ? fields[2] : undefined,
|
|
361
|
+
cardNumber: fields[cardNumber],
|
|
362
|
+
cardSequenceNumber: fields[cardNumber + 1],
|
|
363
|
+
validFrom: parseFintsDate(fields[after]),
|
|
364
|
+
validUntil: parseFintsDate(fields[after + 1]),
|
|
365
|
+
name,
|
|
366
|
+
mobileNumberMasked: fields[after + 4],
|
|
367
|
+
mobileNumber: fields[after + 5],
|
|
368
|
+
lastUsed: parseFintsDate(fields[after + 13]),
|
|
369
|
+
activatedOn: parseFintsDate(fields[after + 14]),
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return media;
|
|
374
|
+
}
|