@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,211 @@
|
|
|
1
|
+
import { FormatError } from '../util/errors.js';
|
|
2
|
+
|
|
3
|
+
/** Most decimal places an amount has: ISO 20022, the widest of the bank formats, allows five. */
|
|
4
|
+
const maxScale = 5;
|
|
5
|
+
|
|
6
|
+
/** Largest `minorUnits` an amount holds: 2^53 - 1, which JavaScript numbers represent exactly. */
|
|
7
|
+
const maxMinorUnits = Number.MAX_SAFE_INTEGER;
|
|
8
|
+
const powersOfTen = [1, 10, 100, 1000, 10000, 100000];
|
|
9
|
+
|
|
10
|
+
/** Most digits a parsed amount has: few enough that the sum of two parsed amounts still fits. */
|
|
11
|
+
const maxDigits = 15;
|
|
12
|
+
|
|
13
|
+
const decimal = /^([+-]?)(\d*)(?:[.,](\d*))?$/;
|
|
14
|
+
|
|
15
|
+
/** Decimal places of the minor unit of `currency` (ISO 4217): two, except for the currencies listed here. */
|
|
16
|
+
function minorUnitDigits(currency: string): number {
|
|
17
|
+
switch (currency) {
|
|
18
|
+
case 'BIF':
|
|
19
|
+
case 'CLP':
|
|
20
|
+
case 'DJF':
|
|
21
|
+
case 'GNF':
|
|
22
|
+
case 'ISK':
|
|
23
|
+
case 'JPY':
|
|
24
|
+
case 'KMF':
|
|
25
|
+
case 'KRW':
|
|
26
|
+
case 'PYG':
|
|
27
|
+
case 'RWF':
|
|
28
|
+
case 'UGX':
|
|
29
|
+
case 'UYI':
|
|
30
|
+
case 'VND':
|
|
31
|
+
case 'VUV':
|
|
32
|
+
case 'XAF':
|
|
33
|
+
case 'XOF':
|
|
34
|
+
case 'XPF':
|
|
35
|
+
return 0;
|
|
36
|
+
case 'BHD':
|
|
37
|
+
case 'IQD':
|
|
38
|
+
case 'JOD':
|
|
39
|
+
case 'KWD':
|
|
40
|
+
case 'LYD':
|
|
41
|
+
case 'OMR':
|
|
42
|
+
case 'TND':
|
|
43
|
+
return 3;
|
|
44
|
+
case 'CLF':
|
|
45
|
+
case 'UYW':
|
|
46
|
+
return 4;
|
|
47
|
+
default:
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A money amount stored as an integer number of `scale`-digit minor units. */
|
|
53
|
+
export class Amount {
|
|
54
|
+
/** The amount in units of 10^-`scale` of the currency, negative for a debit: 1234 with scale 2 is 12.34. */
|
|
55
|
+
readonly minorUnits: number;
|
|
56
|
+
|
|
57
|
+
/** Number of decimal places `minorUnits` has, from 0 to 5. */
|
|
58
|
+
readonly scale: number;
|
|
59
|
+
|
|
60
|
+
/** ISO 4217 currency code, for example `EUR`. */
|
|
61
|
+
readonly currency: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An amount of `minorUnits` with `scale` decimal places; throws `RangeError` for a `scale` outside 0 to 5, and for
|
|
65
|
+
* `minorUnits` that are no integer or beyond 2^53 - 1 in either direction, about 90 trillion at two decimal places.
|
|
66
|
+
*/
|
|
67
|
+
constructor(minorUnits: number, scale: number, currency: string) {
|
|
68
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > maxScale) {
|
|
69
|
+
throw new RangeError(`scale must be an integer from 0 to ${maxScale}: ${scale}`);
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isSafeInteger(minorUnits)) throw new RangeError(`minorUnits out of range: ${minorUnits}`);
|
|
72
|
+
// No negative zero, which would format as `-0.00`.
|
|
73
|
+
this.minorUnits = minorUnits === 0 ? 0 : minorUnits;
|
|
74
|
+
this.scale = scale;
|
|
75
|
+
this.currency = currency;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parses a number as banks send it: digits with at most one decimal separator, `,` (FinTS 3.0, MT940) or `.` (camt),
|
|
80
|
+
* and no thousands separators.
|
|
81
|
+
*
|
|
82
|
+
* The amount has the decimal places of the `currency`'s minor unit (ISO 4217): two for most currencies, none for
|
|
83
|
+
* `JPY`, three for `KWD`. It has more where the value has more significant digits. So `100,` in `EUR` is `100.00`, in
|
|
84
|
+
* `JPY` `100`.
|
|
85
|
+
*
|
|
86
|
+
* Throws `FormatError` for anything else, so that `1.234,56` in bank data is an error, not a guess, and for more than
|
|
87
|
+
* five decimal places (trailing zeros aside) or more than 15 digits.
|
|
88
|
+
*/
|
|
89
|
+
static parseDecimal(value: string, currency: string): Amount {
|
|
90
|
+
const match = decimal.exec(value.trim());
|
|
91
|
+
const whole = match?.[2] ?? '';
|
|
92
|
+
let fraction = match?.[3] ?? '';
|
|
93
|
+
if (!match || (whole === '' && fraction === '')) throw new FormatError('Invalid amount', value);
|
|
94
|
+
const minorDigits = minorUnitDigits(currency);
|
|
95
|
+
while (fraction.length > minorDigits && fraction.endsWith('0')) {
|
|
96
|
+
fraction = fraction.substring(0, fraction.length - 1);
|
|
97
|
+
}
|
|
98
|
+
fraction = fraction.padEnd(minorDigits, '0');
|
|
99
|
+
const digits = `${whole.replace(/^0+/, '')}${fraction}`;
|
|
100
|
+
if (fraction.length > maxScale || digits.length > maxDigits) {
|
|
101
|
+
throw new FormatError(`Amount with more than ${maxScale} decimal places or ${maxDigits} digits`, value);
|
|
102
|
+
}
|
|
103
|
+
// Nothing is left of a zero without decimal places, such as `0` in `JPY`.
|
|
104
|
+
const units = digits === '' ? 0 : Number(digits);
|
|
105
|
+
return new Amount(match[1] === '-' ? -units : units, fraction.length, currency);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The amount as a floating-point number, which cannot hold every decimal fraction exactly; do not calculate with it. */
|
|
109
|
+
get value(): number {
|
|
110
|
+
return this.minorUnits / powersOfTen[this.scale]!;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether the amount is below zero. */
|
|
114
|
+
get isNegative(): boolean {
|
|
115
|
+
return this.minorUnits < 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether the amount is zero. */
|
|
119
|
+
get isZero(): boolean {
|
|
120
|
+
return this.minorUnits === 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** This amount with the opposite sign. */
|
|
124
|
+
negate(): Amount {
|
|
125
|
+
return new Amount(-this.minorUnits, this.scale, this.currency);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** This amount without its sign. */
|
|
129
|
+
abs(): Amount {
|
|
130
|
+
return this.isNegative ? this.negate() : this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The sum with `other`, at the larger of the two scales; throws `TypeError` for another currency and `RangeError` for
|
|
135
|
+
* a sum beyond what an amount holds.
|
|
136
|
+
*/
|
|
137
|
+
add(other: Amount): Amount {
|
|
138
|
+
this.checkCurrency(other);
|
|
139
|
+
const [mine, theirs] = this.atCommonScale(other);
|
|
140
|
+
const sum = mine + theirs;
|
|
141
|
+
const limit = BigInt(maxMinorUnits);
|
|
142
|
+
if (sum > limit || sum < -limit)
|
|
143
|
+
throw new RangeError(`The sum of ${this.toString()} and ${other.toString()} is too large for an Amount`);
|
|
144
|
+
return new Amount(Number(sum), Math.max(this.scale, other.scale), this.currency);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The difference to `other`; throws `TypeError` for another currency. */
|
|
148
|
+
subtract(other: Amount): Amount {
|
|
149
|
+
return this.add(other.negate());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Compares by value, whatever the scales; throws `TypeError` for another currency. */
|
|
153
|
+
compareTo(other: Amount): number {
|
|
154
|
+
this.checkCurrency(other);
|
|
155
|
+
return this.compareValue(other);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whether `other` is the same amount in the same currency, whatever the `scale`: 1.50 equals 1.500. */
|
|
159
|
+
equals(other: unknown): boolean {
|
|
160
|
+
return other instanceof Amount && other.currency === this.currency && this.compareValue(other) === 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Formats the amount without currency, for example `-12.50`. */
|
|
164
|
+
toDecimalString(decimalSeparator = '.'): string {
|
|
165
|
+
const digits = Math.abs(this.minorUnits)
|
|
166
|
+
.toString()
|
|
167
|
+
.padStart(this.scale + 1, '0');
|
|
168
|
+
const whole = digits.substring(0, digits.length - this.scale);
|
|
169
|
+
const fraction = digits.substring(digits.length - this.scale);
|
|
170
|
+
const sign = this.isNegative ? '-' : '';
|
|
171
|
+
return this.scale === 0 ? `${sign}${whole}` : `${sign}${whole}${decimalSeparator}${fraction}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toString(): string {
|
|
175
|
+
return `${this.toDecimalString()} ${this.currency}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The minor units of both amounts at the larger scale, as `bigint`s, since the factor could leave the safe range. */
|
|
179
|
+
private atCommonScale(other: Amount): [bigint, bigint] {
|
|
180
|
+
const target = Math.max(this.scale, other.scale);
|
|
181
|
+
return [this.rescale(target), other.rescale(target)];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private rescale(target: number): bigint {
|
|
185
|
+
return BigInt(this.minorUnits) * BigInt(powersOfTen[target - this.scale]!);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private compareValue(other: Amount): number {
|
|
189
|
+
if (this.scale === other.scale) return Math.sign(this.minorUnits - other.minorUnits);
|
|
190
|
+
const [mine, theirs] = this.atCommonScale(other);
|
|
191
|
+
return mine === theirs ? 0 : mine < theirs ? -1 : 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private checkCurrency(other: Amount): void {
|
|
195
|
+
if (other.currency !== this.currency)
|
|
196
|
+
throw new TypeError(`Currency mismatch: ${this.currency} vs ${other.currency}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* `amount` as a credit (unchanged) or a debit (negated), by the bank's credit/debit `mark`.
|
|
202
|
+
*
|
|
203
|
+
* Throws `FormatError` for any other `mark`, `undefined` included: a missing mark must not be read as a credit. It does
|
|
204
|
+
* as well for a negative `amount`, whose sign the mark would turn: the mark alone gives the direction.
|
|
205
|
+
*/
|
|
206
|
+
export function withMark(amount: Amount, mark: string | undefined, credit: string, debit: string): Amount {
|
|
207
|
+
if (amount.isNegative) throw new FormatError('Negative amount with a credit/debit mark', amount.toString());
|
|
208
|
+
if (mark === credit) return amount;
|
|
209
|
+
if (mark === debit) return amount.negate();
|
|
210
|
+
throw new FormatError(`Credit/debit mark is neither "${credit}" nor "${debit}"`, mark);
|
|
211
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { copyDate, formatIsoDate } from '../util/dates.js';
|
|
2
|
+
import type { Account } from './account.js';
|
|
3
|
+
import type { Amount } from './amount.js';
|
|
4
|
+
|
|
5
|
+
/** What `Balance` takes; the bank states the amounts it knows. */
|
|
6
|
+
export interface BalanceInit {
|
|
7
|
+
account: Account;
|
|
8
|
+
booked: Amount;
|
|
9
|
+
bookedAsOf: Date;
|
|
10
|
+
bookedAsOfTime?: Date | undefined;
|
|
11
|
+
pending?: Amount | undefined;
|
|
12
|
+
creditLine?: Amount | undefined;
|
|
13
|
+
available?: Amount | undefined;
|
|
14
|
+
used?: Amount | undefined;
|
|
15
|
+
overdraft?: Amount | undefined;
|
|
16
|
+
bookingDate?: Date | undefined;
|
|
17
|
+
bookingTime?: Date | undefined;
|
|
18
|
+
productName?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Account balance as reported by the bank. */
|
|
22
|
+
export class Balance {
|
|
23
|
+
/** The account the balance belongs to. */
|
|
24
|
+
readonly account: Account;
|
|
25
|
+
|
|
26
|
+
/** Booked balance; negative when overdrawn. */
|
|
27
|
+
readonly booked: Amount;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The calendar day the bank states with the booked balance: the day it transmitted the balance, which need not be the
|
|
31
|
+
* day the balance was booked (`bookingDate`). A local `Date` at midnight.
|
|
32
|
+
*/
|
|
33
|
+
readonly bookedAsOf: Date;
|
|
34
|
+
|
|
35
|
+
/** The exact moment of `bookedAsOf`, where the bank states the time of day as well; `undefined` otherwise. */
|
|
36
|
+
readonly bookedAsOfTime: Date | undefined;
|
|
37
|
+
|
|
38
|
+
/** Balance of pending (not yet booked) transactions, if the bank reports it. */
|
|
39
|
+
readonly pending: Amount | undefined;
|
|
40
|
+
|
|
41
|
+
/** Arranged overdraft limit. */
|
|
42
|
+
readonly creditLine: Amount | undefined;
|
|
43
|
+
|
|
44
|
+
/** Amount currently available for payments. */
|
|
45
|
+
readonly available: Amount | undefined;
|
|
46
|
+
|
|
47
|
+
/** Amount already drawn on the account's limit. */
|
|
48
|
+
readonly used: Amount | undefined;
|
|
49
|
+
|
|
50
|
+
/** Amount by which the account exceeds its limit. */
|
|
51
|
+
readonly overdraft: Amount | undefined;
|
|
52
|
+
|
|
53
|
+
/** The calendar day the booked balance was booked (Buchungszeitpunkt), where the bank states it. */
|
|
54
|
+
readonly bookingDate: Date | undefined;
|
|
55
|
+
|
|
56
|
+
/** The exact moment of `bookingDate`, where the bank states the time of day as well; `undefined` otherwise. */
|
|
57
|
+
readonly bookingTime: Date | undefined;
|
|
58
|
+
|
|
59
|
+
/** The bank's name for the account, for example `Girokonto`. */
|
|
60
|
+
readonly productName: string | undefined;
|
|
61
|
+
|
|
62
|
+
/** Takes copies of the dates of `init`: each balance has its own, which no other one shares. */
|
|
63
|
+
constructor(init: BalanceInit) {
|
|
64
|
+
this.account = init.account;
|
|
65
|
+
this.booked = init.booked;
|
|
66
|
+
this.bookedAsOf = copyDate(init.bookedAsOf);
|
|
67
|
+
this.bookedAsOfTime = copyDate(init.bookedAsOfTime);
|
|
68
|
+
this.pending = init.pending;
|
|
69
|
+
this.creditLine = init.creditLine;
|
|
70
|
+
this.available = init.available;
|
|
71
|
+
this.used = init.used;
|
|
72
|
+
this.overdraft = init.overdraft;
|
|
73
|
+
this.bookingDate = copyDate(init.bookingDate);
|
|
74
|
+
this.bookingTime = copyDate(init.bookingTime);
|
|
75
|
+
this.productName = init.productName;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The account, as `Account.toString` shows it, and the date; no amount, so that logs and crash reports do not collect
|
|
80
|
+
* account data.
|
|
81
|
+
*/
|
|
82
|
+
toString(): string {
|
|
83
|
+
return `Balance(${this.account.toString()}, ${formatIsoDate(this.bookedAsOf)})`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** General bank information from the bank parameter data (BPD). */
|
|
2
|
+
export class BankInfo {
|
|
3
|
+
/** German bank code (BLZ). */
|
|
4
|
+
readonly bankCode: string;
|
|
5
|
+
|
|
6
|
+
/** The bank's name. */
|
|
7
|
+
readonly name: string | undefined;
|
|
8
|
+
|
|
9
|
+
/** Version of the bank parameter data this information comes from. */
|
|
10
|
+
readonly bpdVersion: number;
|
|
11
|
+
|
|
12
|
+
/** FinTS versions the bank supports, as it names them. */
|
|
13
|
+
readonly supportedVersions: readonly string[];
|
|
14
|
+
|
|
15
|
+
/** Languages the bank's messages come in, as codes the bank defines. */
|
|
16
|
+
readonly languages: readonly string[];
|
|
17
|
+
|
|
18
|
+
/** Largest message in kilobytes (1024 bytes) the bank accepts; the client sends nothing larger. */
|
|
19
|
+
readonly maxMessageSize: number | undefined;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Time in seconds before which a client must not send a life-indicator message to keep an idle dialog open, `0` for
|
|
23
|
+
* none (HIBPA, Formals C.9). This client sends no such messages.
|
|
24
|
+
*/
|
|
25
|
+
readonly minTimeout: number | undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Time in seconds after which the bank will likely end a dialog without messages, `0` for no limit (HIBPA, Formals
|
|
29
|
+
* C.9). The client starts a new dialog for a call after that time, or after 5 minutes where the bank states none.
|
|
30
|
+
*/
|
|
31
|
+
readonly maxTimeout: number | undefined;
|
|
32
|
+
|
|
33
|
+
constructor(init: {
|
|
34
|
+
bankCode: string;
|
|
35
|
+
name?: string | undefined;
|
|
36
|
+
bpdVersion: number;
|
|
37
|
+
supportedVersions?: readonly string[];
|
|
38
|
+
languages?: readonly string[];
|
|
39
|
+
maxMessageSize?: number | undefined;
|
|
40
|
+
minTimeout?: number | undefined;
|
|
41
|
+
maxTimeout?: number | undefined;
|
|
42
|
+
}) {
|
|
43
|
+
this.bankCode = init.bankCode;
|
|
44
|
+
this.name = init.name;
|
|
45
|
+
this.bpdVersion = init.bpdVersion;
|
|
46
|
+
this.supportedVersions = Object.freeze([...(init.supportedVersions ?? [])]);
|
|
47
|
+
this.languages = Object.freeze([...(init.languages ?? [])]);
|
|
48
|
+
this.maxMessageSize = init.maxMessageSize;
|
|
49
|
+
this.minTimeout = init.minTimeout;
|
|
50
|
+
this.maxTimeout = init.maxTimeout;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Free-text message sent by the bank during login. */
|
|
55
|
+
export class BankMessage {
|
|
56
|
+
constructor(
|
|
57
|
+
/** What the message is about. */
|
|
58
|
+
readonly subject: string,
|
|
59
|
+
/** The message itself. */
|
|
60
|
+
readonly text: string,
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
toString(): string {
|
|
64
|
+
return `${this.subject}: ${this.text}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { TanCallback, TanMediumSelector, TanMethodSelector } from '../tan/challenge.js';
|
|
2
|
+
import type { TanMedium, TanMethod } from '../tan/tan-models.js';
|
|
3
|
+
import type { Account } from './account.js';
|
|
4
|
+
import type { Balance } from './balance.js';
|
|
5
|
+
import type { BankInfo, BankMessage } from './bank-info.js';
|
|
6
|
+
import type { FinTsLoginRefusedError } from './errors.js';
|
|
7
|
+
import type { FinTsFeedback } from './feedback.js';
|
|
8
|
+
import type { Transaction, TransactionFormat } from './transaction.js';
|
|
9
|
+
import type { FinTsTransport } from './transport.js';
|
|
10
|
+
|
|
11
|
+
/** Which way a FinTS message travels: from the client to the bank, or back. */
|
|
12
|
+
export type MessageDirection = 'outgoing' | 'incoming';
|
|
13
|
+
|
|
14
|
+
/** Receives every FinTS message with PIN and TAN masked. */
|
|
15
|
+
export type MessageLogger = (direction: MessageDirection, message: string) => void;
|
|
16
|
+
|
|
17
|
+
/** Receives free-text messages the bank sends during login. */
|
|
18
|
+
export type BankMessageHandler = (message: BankMessage) => void;
|
|
19
|
+
|
|
20
|
+
/** Receives a warning the bank sends, a return code from 3000 to 3999. */
|
|
21
|
+
export type WarningHandler = (warning: FinTsFeedback) => void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How a client reaches the user and the bank, apart from the login data in `FinTsConfig`.
|
|
25
|
+
*
|
|
26
|
+
* An error thrown by a callback that only informs the app (`onBankMessage`, `onWarning`, `logger`) fails the call once
|
|
27
|
+
* the call's exchange with the bank is over, so that it cannot break off a dialog half-way; an error of the call itself
|
|
28
|
+
* takes precedence.
|
|
29
|
+
*/
|
|
30
|
+
export interface FinTsClientOptions {
|
|
31
|
+
/** Called whenever the bank asks for a TAN or an app approval. */
|
|
32
|
+
tanCallback: TanCallback;
|
|
33
|
+
|
|
34
|
+
/** Picks the TAN method when the bank offers several; the first is used without it. */
|
|
35
|
+
tanMethodSelector?: TanMethodSelector;
|
|
36
|
+
|
|
37
|
+
/** Picks the TAN medium when the method needs one and the user has several; the first is used without it. */
|
|
38
|
+
tanMediumSelector?: TanMediumSelector;
|
|
39
|
+
|
|
40
|
+
/** Receives the free-text messages the bank sends during login. */
|
|
41
|
+
onBankMessage?: BankMessageHandler;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Receives the bank's warnings, such as 3951 (switch to another TAN method), but not those the client acts on itself,
|
|
45
|
+
* such as 3040 (more data follow). Show them to the user with code and text, as the specification asks.
|
|
46
|
+
*/
|
|
47
|
+
onWarning?: WarningHandler;
|
|
48
|
+
|
|
49
|
+
/** Receives every FinTS message with PIN and TAN masked, for diagnosis; the messages contain account data. */
|
|
50
|
+
logger?: MessageLogger;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `FinTsClient.state` of a previous session, which skips the bank setup steps. A refused login in it keeps the
|
|
54
|
+
* client from sending the PIN (`FinTsClient.loginRefusal`).
|
|
55
|
+
*
|
|
56
|
+
* A state belongs to one client at a time: restore the clients of one bank user one after another, each from the
|
|
57
|
+
* state the one before saved, never two at once from the same state. The state saved last wins, and could lift a
|
|
58
|
+
* refused login the other client saved.
|
|
59
|
+
*
|
|
60
|
+
* A state saved for another user or bank is ignored, and of one saved for another server of the bank, such as before
|
|
61
|
+
* the bank moved its FinTS URL, only the refused login holds.
|
|
62
|
+
*/
|
|
63
|
+
state?: string | undefined;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sends the messages; an `HttpTransport` by default.
|
|
67
|
+
*
|
|
68
|
+
* The client closes it on `FinTsClient.close`, so hand each client a transport of its own.
|
|
69
|
+
*/
|
|
70
|
+
transport?: FinTsTransport;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** What `getTransactions` takes besides the account. */
|
|
74
|
+
export interface TransactionQuery {
|
|
75
|
+
/** The first day, a day in the future counting as today; banks typically ask for a TAN beyond 90 days ago. */
|
|
76
|
+
from?: Date | undefined;
|
|
77
|
+
/** The last day, a day in the future counting as today. */
|
|
78
|
+
to?: Date | undefined;
|
|
79
|
+
/** The format to ask for; `auto` by default. */
|
|
80
|
+
format?: TransactionFormat;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read access to a bank account over FinTS.
|
|
85
|
+
*
|
|
86
|
+
* A call logs in where it needs to, which sends the PIN and may ask the callbacks for a TAN method, a TAN medium and an
|
|
87
|
+
* approval. Calls share one bank dialog while it is open, so that the approval of its login serves them all. The next
|
|
88
|
+
* call logs in anew, which may ask for another approval, once the dialog has idled longer than `BankInfo.maxTimeout`,
|
|
89
|
+
* after `selectTanMethod` and `clearLoginRefusal`, once the bank has ended the dialog, and after a message failed with
|
|
90
|
+
* another error than the bank's (`FinTsBankError`), such as a cancelled approval or a broken connection. Calls run one
|
|
91
|
+
* after another; call `close` when done.
|
|
92
|
+
*
|
|
93
|
+
* A callback in `FinTsClientOptions` must not wait for another call of its client, which would wait for the running
|
|
94
|
+
* call. On Node.js, Deno 2.1 and Bun 1.2.6 or later such a call fails with `FinTsStateError`; elsewhere it waits
|
|
95
|
+
* forever. `close` works from anywhere.
|
|
96
|
+
*/
|
|
97
|
+
export interface FinTsClient {
|
|
98
|
+
/**
|
|
99
|
+
* Serialized session data: bank and user parameters, customer system ID, the TAN methods the user may use, the chosen
|
|
100
|
+
* TAN method and medium, the SEPA account list, and a refused login (`loginRefusal`).
|
|
101
|
+
*
|
|
102
|
+
* Contains no PIN or TAN, but account details, so store it encrypted. It holds what this client knows, and replaces
|
|
103
|
+
* the state it was restored from: one client at a time per state (`FinTsClientOptions.state`).
|
|
104
|
+
*/
|
|
105
|
+
readonly state: string;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The refused login that keeps the client from sending the PIN again (`FinTsLoginRefusedError`); `undefined` while
|
|
109
|
+
* there is none. It is kept in `state` until `clearLoginRefusal`.
|
|
110
|
+
*/
|
|
111
|
+
readonly loginRefusal: FinTsLoginRefusedError | undefined;
|
|
112
|
+
|
|
113
|
+
/** Security function code of the TAN method in use, for example `942`; `undefined` before the first login. */
|
|
114
|
+
readonly selectedTanMethod: string | undefined;
|
|
115
|
+
|
|
116
|
+
/** Name of the TAN medium sent to the bank, if the method needs one. */
|
|
117
|
+
readonly selectedTanMedium: string | undefined;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Switches to another TAN method, and optionally TAN medium, for the following calls.
|
|
121
|
+
*
|
|
122
|
+
* Ends the current dialog, so the next call logs in again with the new method. If the bank does not offer
|
|
123
|
+
* `securityFunction`, the client picks another method at that login.
|
|
124
|
+
*/
|
|
125
|
+
selectTanMethod(securityFunction: string, tanMedium?: string): Promise<void>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Lifts `loginRefusal` and signs with `pin` from now on. Ends the current dialog, so that the next call logs in with
|
|
129
|
+
* it, and forgets the TAN medium, which the bank may have refused, such as that of a phone the user has replaced: where
|
|
130
|
+
* the TAN method needs one, the next call looks the media up anew and asks the `tanMediumSelector`.
|
|
131
|
+
*
|
|
132
|
+
* Pass a PIN the user has entered after the refusal, never a stored one unasked: banks lock the access after a few
|
|
133
|
+
* failed logins. Rejects with `TypeError` for a PIN the client cannot send, as its constructor throws, and with the
|
|
134
|
+
* refusal where it is a `FinTsLoginDataChangedError`: only a new client with the new login data can log in.
|
|
135
|
+
*/
|
|
136
|
+
clearLoginRefusal(pin: string): Promise<void>;
|
|
137
|
+
|
|
138
|
+
/** General information about the bank, from the bank parameter data. */
|
|
139
|
+
getBankInfo(): Promise<BankInfo>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* TAN procedures the bank allows for this user, which the synchronization reports. Where the session has no customer
|
|
143
|
+
* system ID yet, as without a saved state, this call logs in for it: it may ask `tanMethodSelector` where the bank
|
|
144
|
+
* refuses the one-step method, and `tanCallback` for an approval.
|
|
145
|
+
*/
|
|
146
|
+
getTanMethods(): Promise<TanMethod[]>;
|
|
147
|
+
|
|
148
|
+
/** TAN devices registered for this user. */
|
|
149
|
+
getTanMedia(): Promise<TanMedium[]>;
|
|
150
|
+
|
|
151
|
+
/** Accounts of the user, completed with IBAN and BIC where the bank provides them. */
|
|
152
|
+
getAccounts(): Promise<Account[]>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The balance of `account`.
|
|
156
|
+
*
|
|
157
|
+
* Where the user parameter data carry the complete balance (account extension), possibly from the saved `state`, it
|
|
158
|
+
* comes from there without a query; `Balance.bookedAsOf` tells the day it states.
|
|
159
|
+
*
|
|
160
|
+
* Rejects with `FinTsUnsupportedError` where the parameter data rule the query out, without sending it; the login,
|
|
161
|
+
* which delivers them, may have gone out.
|
|
162
|
+
*/
|
|
163
|
+
getBalance(account: Account): Promise<Balance>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Transactions from the day of `from` to the day of `to`, both included, booked and pending, sorted by date; those of
|
|
167
|
+
* the same day in the bank's order.
|
|
168
|
+
*
|
|
169
|
+
* Only the local calendar day counts, and a day in the future counts as today. Banks typically ask for a TAN when
|
|
170
|
+
* `from` is more than 90 days ago; see also `Account.dataStatus`. Rejects with `TypeError`, before it waits for other
|
|
171
|
+
* calls, if `from` or `to` is no valid `Date` or `from` lies on a day after `to`, and with `FinTsUnsupportedError`,
|
|
172
|
+
* without sending the query, where the parameter data rule it out; the login may have gone out.
|
|
173
|
+
*/
|
|
174
|
+
getTransactions(account: Account, query?: TransactionQuery): Promise<Transaction[]>;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Ends the bank dialog, releases the connection and forgets the PIN.
|
|
178
|
+
*
|
|
179
|
+
* A running call stops at its next step, also while it waits for a TAN, an app approval or a selection, and fails with
|
|
180
|
+
* `FinTsStateError`, as do all later calls. A request already on its way is aborted where the transport supports it,
|
|
181
|
+
* as `HttpTransport` does: the call fails with `FinTsTransportError` then, as after a broken connection, and an aborted
|
|
182
|
+
* login counts as one whose answer was lost. Another transport is awaited, at most for its timeout; after the
|
|
183
|
+
* request, the call sends nothing but the end of its dialogs.
|
|
184
|
+
*/
|
|
185
|
+
close(): Promise<void>;
|
|
186
|
+
}
|