@coderoftime/fints 0.1.0 → 0.3.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 +519 -1
- package/README.md +138 -40
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +242 -0
- package/dist/client.js.map +1 -0
- package/dist/core/account.d.ts +46 -7
- package/dist/core/account.d.ts.map +1 -1
- package/dist/core/account.js +143 -24
- package/dist/core/account.js.map +1 -1
- package/dist/core/amount.d.ts +15 -4
- package/dist/core/amount.d.ts.map +1 -1
- package/dist/core/amount.js +46 -9
- package/dist/core/amount.js.map +1 -1
- package/dist/core/balance.d.ts +4 -0
- package/dist/core/balance.d.ts.map +1 -1
- package/dist/core/balance.js +11 -5
- package/dist/core/balance.js.map +1 -1
- package/dist/core/bank-info.d.ts +21 -4
- package/dist/core/bank-info.d.ts.map +1 -1
- package/dist/core/bank-info.js +26 -5
- package/dist/core/bank-info.js.map +1 -1
- package/dist/core/client.d.ts +81 -19
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/config.d.ts +40 -20
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +85 -30
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +100 -26
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +136 -53
- package/dist/core/errors.js.map +1 -1
- package/dist/core/feedback.d.ts +11 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +14 -2
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/transaction.d.ts +11 -1
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +21 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/transport.d.ts +48 -6
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +126 -29
- package/dist/core/transport.js.map +1 -1
- package/dist/formats/camt.d.ts +28 -5
- package/dist/formats/camt.d.ts.map +1 -1
- package/dist/formats/camt.js +132 -50
- package/dist/formats/camt.js.map +1 -1
- package/dist/formats/mt940.d.ts +29 -5
- package/dist/formats/mt940.d.ts.map +1 -1
- package/dist/formats/mt940.js +209 -113
- package/dist/formats/mt940.js.map +1 -1
- package/dist/formats/readers.d.ts +6 -2
- package/dist/formats/readers.d.ts.map +1 -1
- package/dist/formats/readers.js +28 -29
- package/dist/formats/readers.js.map +1 -1
- 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 +9 -0
- package/dist/formats/sepa-purpose.d.ts.map +1 -1
- package/dist/formats/sepa-purpose.js +32 -2
- package/dist/formats/sepa-purpose.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/session/app-approval.d.ts +11 -5
- package/dist/session/app-approval.d.ts.map +1 -1
- package/dist/session/app-approval.js +23 -12
- package/dist/session/app-approval.js.map +1 -1
- package/dist/session/dialog-runner.d.ts +11 -11
- package/dist/session/dialog-runner.d.ts.map +1 -1
- package/dist/session/dialog-runner.js +34 -19
- package/dist/session/dialog-runner.js.map +1 -1
- package/dist/session/login-guard.d.ts +54 -49
- package/dist/session/login-guard.d.ts.map +1 -1
- package/dist/session/login-guard.js +170 -83
- package/dist/session/login-guard.js.map +1 -1
- package/dist/session/sepa-accounts.d.ts +20 -14
- package/dist/session/sepa-accounts.d.ts.map +1 -1
- package/dist/session/sepa-accounts.js +62 -46
- package/dist/session/sepa-accounts.js.map +1 -1
- package/dist/session/session-setup.d.ts +41 -24
- package/dist/session/session-setup.d.ts.map +1 -1
- package/dist/session/session-setup.js +106 -63
- package/dist/session/session-setup.js.map +1 -1
- package/dist/session/session-state.d.ts +10 -6
- package/dist/session/session-state.d.ts.map +1 -1
- package/dist/session/session-state.js +72 -18
- package/dist/session/session-state.js.map +1 -1
- package/dist/session/signed-dialog.d.ts +83 -0
- package/dist/session/signed-dialog.d.ts.map +1 -0
- package/dist/session/signed-dialog.js +185 -0
- package/dist/session/signed-dialog.js.map +1 -0
- package/dist/session/tan-selection.d.ts +26 -18
- package/dist/session/tan-selection.d.ts.map +1 -1
- package/dist/session/tan-selection.js +44 -33
- package/dist/session/tan-selection.js.map +1 -1
- package/dist/session/user-prompts.d.ts +14 -5
- package/dist/session/user-prompts.d.ts.map +1 -1
- package/dist/session/user-prompts.js +53 -18
- package/dist/session/user-prompts.js.map +1 -1
- package/dist/tan/challenge.d.ts +47 -24
- package/dist/tan/challenge.d.ts.map +1 -1
- package/dist/tan/challenge.js +56 -51
- package/dist/tan/challenge.js.map +1 -1
- package/dist/tan/flicker.d.ts +11 -1
- package/dist/tan/flicker.d.ts.map +1 -1
- package/dist/tan/flicker.js +70 -24
- package/dist/tan/flicker.js.map +1 -1
- package/dist/tan/tan-image.d.ts +6 -2
- package/dist/tan/tan-image.d.ts.map +1 -1
- package/dist/tan/tan-image.js +14 -6
- package/dist/tan/tan-image.js.map +1 -1
- package/dist/tan/tan-models.d.ts +14 -2
- package/dist/tan/tan-models.d.ts.map +1 -1
- package/dist/tan/tan-models.js +24 -7
- package/dist/tan/tan-models.js.map +1 -1
- package/dist/util/arguments.d.ts +13 -0
- package/dist/util/arguments.d.ts.map +1 -0
- package/dist/util/arguments.js +40 -0
- package/dist/util/arguments.js.map +1 -0
- package/dist/util/async.d.ts +4 -3
- package/dist/util/async.d.ts.map +1 -1
- package/dist/util/async.js +6 -7
- package/dist/util/async.js.map +1 -1
- package/dist/util/bytes.d.ts +3 -4
- package/dist/util/bytes.d.ts.map +1 -1
- package/dist/util/bytes.js +13 -21
- package/dist/util/bytes.js.map +1 -1
- package/dist/util/call-context.d.ts.map +1 -1
- package/dist/util/call-context.js +3 -2
- package/dist/util/call-context.js.map +1 -1
- package/dist/util/dates.d.ts +6 -20
- package/dist/util/dates.d.ts.map +1 -1
- package/dist/util/dates.js +58 -28
- package/dist/util/dates.js.map +1 -1
- package/dist/util/errors.d.ts +13 -3
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +19 -6
- package/dist/util/errors.js.map +1 -1
- package/dist/util/immutable-map.d.ts +5 -2
- package/dist/util/immutable-map.d.ts.map +1 -1
- package/dist/util/immutable-map.js +15 -12
- package/dist/util/immutable-map.js.map +1 -1
- package/dist/util/inspect.d.ts +11 -0
- package/dist/util/inspect.d.ts.map +1 -0
- package/dist/util/inspect.js +14 -0
- package/dist/util/inspect.js.map +1 -0
- package/dist/util/json.d.ts +4 -0
- package/dist/util/json.d.ts.map +1 -1
- package/dist/util/json.js +18 -0
- package/dist/util/json.js.map +1 -1
- package/dist/util/text.d.ts +38 -5
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +90 -16
- package/dist/util/text.js.map +1 -1
- package/dist/util/xml.d.ts +9 -6
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/util/xml.js +129 -56
- package/dist/util/xml.js.map +1 -1
- package/dist/v3/account-extension.d.ts.map +1 -1
- package/dist/v3/account-extension.js +7 -10
- package/dist/v3/account-extension.js.map +1 -1
- package/dist/v3/bank-connection.d.ts +54 -0
- package/dist/v3/bank-connection.d.ts.map +1 -0
- package/dist/v3/bank-connection.js +118 -0
- package/dist/v3/bank-connection.js.map +1 -0
- package/dist/v3/dialog.d.ts +84 -46
- package/dist/v3/dialog.d.ts.map +1 -1
- package/dist/v3/dialog.js +174 -60
- package/dist/v3/dialog.js.map +1 -1
- package/dist/v3/message.d.ts +32 -11
- package/dist/v3/message.d.ts.map +1 -1
- package/dist/v3/message.js +144 -83
- package/dist/v3/message.js.map +1 -1
- package/dist/v3/parameter-store.d.ts +111 -0
- package/dist/v3/parameter-store.d.ts.map +1 -0
- package/dist/v3/parameter-store.js +197 -0
- package/dist/v3/parameter-store.js.map +1 -0
- package/dist/v3/parameters.d.ts +44 -17
- package/dist/v3/parameters.d.ts.map +1 -1
- package/dist/v3/parameters.js +136 -60
- package/dist/v3/parameters.js.map +1 -1
- package/dist/v3/protocol.d.ts +50 -50
- package/dist/v3/protocol.d.ts.map +1 -1
- package/dist/v3/protocol.js +91 -210
- package/dist/v3/protocol.js.map +1 -1
- package/dist/v3/queries.d.ts +51 -0
- package/dist/v3/queries.d.ts.map +1 -0
- package/dist/v3/queries.js +128 -0
- package/dist/v3/queries.js.map +1 -0
- package/dist/v3/segments.d.ts +43 -19
- package/dist/v3/segments.d.ts.map +1 -1
- package/dist/v3/segments.js +84 -33
- package/dist/v3/segments.js.map +1 -1
- package/dist/v3/syntax.d.ts +68 -13
- package/dist/v3/syntax.d.ts.map +1 -1
- package/dist/v3/syntax.js +183 -102
- package/dist/v3/syntax.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +293 -0
- package/src/core/account.ts +153 -24
- package/src/core/amount.ts +45 -10
- package/src/core/balance.ts +12 -5
- package/src/core/bank-info.ts +29 -5
- package/src/core/client.ts +83 -19
- package/src/core/config.ts +95 -35
- package/src/core/errors.ts +148 -54
- package/src/core/feedback.ts +16 -2
- package/src/core/transaction.ts +22 -4
- package/src/core/transport.ts +153 -32
- package/src/formats/camt.ts +153 -45
- package/src/formats/mt940.ts +244 -122
- package/src/formats/readers.ts +28 -31
- package/src/formats/record-budget.ts +18 -0
- package/src/formats/sepa-purpose.ts +29 -2
- package/src/index.ts +4 -2
- package/src/session/app-approval.ts +51 -38
- package/src/session/dialog-runner.ts +61 -31
- package/src/session/login-guard.ts +189 -87
- package/src/session/sepa-accounts.ts +63 -45
- package/src/session/session-setup.ts +105 -68
- package/src/session/session-state.ts +74 -23
- package/src/session/signed-dialog.ts +222 -0
- package/src/session/tan-selection.ts +50 -39
- package/src/session/user-prompts.ts +62 -18
- package/src/tan/challenge.ts +87 -64
- package/src/tan/flicker.ts +72 -23
- package/src/tan/tan-image.ts +15 -6
- package/src/tan/tan-models.ts +26 -7
- package/src/util/arguments.ts +40 -0
- package/src/util/async.ts +10 -9
- package/src/util/bytes.ts +14 -20
- package/src/util/call-context.ts +3 -2
- package/src/util/dates.ts +64 -33
- package/src/util/errors.ts +22 -6
- package/src/util/immutable-map.ts +15 -12
- package/src/util/inspect.ts +14 -0
- package/src/util/json.ts +16 -0
- package/src/util/text.ts +87 -15
- package/src/util/xml.ts +130 -52
- package/src/v3/account-extension.ts +7 -12
- package/src/v3/bank-connection.ts +126 -0
- package/src/v3/dialog.ts +186 -92
- package/src/v3/message.ts +140 -79
- package/src/v3/parameter-store.ts +226 -0
- package/src/v3/parameters.ts +166 -66
- package/src/v3/protocol.ts +103 -253
- package/src/v3/queries.ts +198 -0
- package/src/v3/segments.ts +86 -31
- package/src/v3/syntax.ts +172 -93
- package/dist/session/bank-connection.d.ts +0 -28
- package/dist/session/bank-connection.d.ts.map +0 -1
- package/dist/session/bank-connection.js +0 -48
- package/dist/session/bank-connection.js.map +0 -1
- package/dist/session/parameter-data.d.ts +0 -83
- package/dist/session/parameter-data.d.ts.map +0 -1
- package/dist/session/parameter-data.js +0 -68
- package/dist/session/parameter-data.js.map +0 -1
- package/dist/session/pin-tan-client.d.ts +0 -54
- package/dist/session/pin-tan-client.d.ts.map +0 -1
- package/dist/session/pin-tan-client.js +0 -153
- package/dist/session/pin-tan-client.js.map +0 -1
- package/dist/session/pin-tan-dialog.d.ts +0 -71
- package/dist/session/pin-tan-dialog.d.ts.map +0 -1
- package/dist/session/pin-tan-dialog.js +0 -124
- package/dist/session/pin-tan-dialog.js.map +0 -1
- package/dist/session/protocol.d.ts +0 -209
- package/dist/session/protocol.d.ts.map +0 -1
- package/dist/session/protocol.js +0 -75
- package/dist/session/protocol.js.map +0 -1
- package/dist/session/response-handler.d.ts +0 -48
- package/dist/session/response-handler.d.ts.map +0 -1
- package/dist/session/response-handler.js +0 -121
- package/dist/session/response-handler.js.map +0 -1
- package/dist/v3/client.d.ts +0 -24
- package/dist/v3/client.d.ts.map +0 -1
- package/dist/v3/client.js +0 -16
- package/dist/v3/client.js.map +0 -1
- package/src/session/bank-connection.ts +0 -51
- package/src/session/parameter-data.ts +0 -137
- package/src/session/pin-tan-client.ts +0 -186
- package/src/session/pin-tan-dialog.ts +0 -160
- package/src/session/protocol.ts +0 -311
- package/src/session/response-handler.ts +0 -130
- package/src/v3/client.ts +0 -29
package/src/formats/camt.ts
CHANGED
|
@@ -1,30 +1,88 @@
|
|
|
1
|
-
import { Amount } from '../core/amount.js';
|
|
2
|
-
import { Transaction, type TransactionStatus } from '../core/transaction.js';
|
|
1
|
+
import { addsUpTo, Amount } from '../core/amount.js';
|
|
2
|
+
import { requireStatus, Transaction, type TransactionStatus } from '../core/transaction.js';
|
|
3
3
|
import { parseIsoDate } from '../util/dates.js';
|
|
4
|
-
import { FormatError } from '../util/errors.js';
|
|
4
|
+
import { fail, FormatError } from '../util/errors.js';
|
|
5
|
+
import { frozenWithInspect } from '../util/inspect.js';
|
|
6
|
+
import { counted, maskedId, nonBlank, trimBlanks } from '../util/text.js';
|
|
5
7
|
import { parseXml, type XmlElement } from '../util/xml.js';
|
|
8
|
+
import { maxFieldLength } from './mt940.js';
|
|
9
|
+
import { RecordBudget } from './record-budget.js';
|
|
6
10
|
import { providedReference, SepaPurpose } from './sepa-purpose.js';
|
|
7
11
|
|
|
8
|
-
/**
|
|
12
|
+
/** One report (camt.052), statement (camt.053) or notification (camt.054): the entries of one account. */
|
|
13
|
+
export interface CamtStatement {
|
|
14
|
+
/** The bank's identification of the statement (`Id`). */
|
|
15
|
+
readonly reference: string | undefined;
|
|
16
|
+
/** The account the statement is for: its IBAN, or the bank's other identification of it where it states none. */
|
|
17
|
+
readonly accountId: string | undefined;
|
|
18
|
+
/** The currency of the account, where the statement states it. */
|
|
19
|
+
readonly currency: string | undefined;
|
|
20
|
+
/** The transactions of the statement, in the order the bank lists them. */
|
|
21
|
+
readonly transactions: readonly Transaction[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Local names of the elements of a document that hold the entries of one account. */
|
|
25
|
+
const statementNames = new Set(['Rpt', 'Stmt', 'Ntfctn']);
|
|
26
|
+
|
|
27
|
+
/** Parser for ISO 20022 camt.052, camt.053 and camt.054 account reports, statements and notifications. */
|
|
9
28
|
export const Camt = Object.freeze({
|
|
10
29
|
/**
|
|
11
|
-
* Parses
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `status` overrides the booking status stated in the entries.
|
|
30
|
+
* Parses the transactions of all statements in one list, which does not tell the account of each: a document may hold
|
|
31
|
+
* statements of several accounts, which `parse` keeps apart. Throws as `parse` does.
|
|
15
32
|
*/
|
|
16
33
|
parseTransactions(xml: string, status?: TransactionStatus): Transaction[] {
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
return Camt.parse(xml, status).flatMap((s) => s.transactions);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses all statements, each with the account it is for. Throws `FormatError` for malformed XML, for XML nested more
|
|
39
|
+
* than 100 levels deep or of more than 500,000 nodes besides text or one per 16 characters, whichever is more, for a
|
|
40
|
+
* document without any statement, for an entry without amount, currency, credit/debit indicator or date, for a date
|
|
41
|
+
* it cannot read, for remittance information of more than 65,536 characters, and for more than 200,000 statements and
|
|
42
|
+
* transactions.
|
|
43
|
+
*
|
|
44
|
+
* `status` overrides the booking status stated in the entries, unless it is `undefined` or `null`; throws `TypeError`
|
|
45
|
+
* for one other than `booked` and `pending`.
|
|
46
|
+
*/
|
|
47
|
+
parse(xml: string, status?: TransactionStatus): CamtStatement[] {
|
|
48
|
+
return parseCamt(xml, forcedStatus(status), new RecordBudget());
|
|
19
49
|
},
|
|
20
50
|
});
|
|
21
51
|
|
|
52
|
+
/** The `status` of `Camt.parse`; `undefined` for `null`, the status the entries state. */
|
|
53
|
+
function forcedStatus(status: unknown): TransactionStatus | undefined {
|
|
54
|
+
return status === undefined || status === null ? undefined : requireStatus(status);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `Camt.parse`, which takes each statement and transaction from `budget`, before it builds it. */
|
|
58
|
+
export function parseCamt(xml: string, status: TransactionStatus | undefined, budget: RecordBudget): CamtStatement[] {
|
|
59
|
+
const root = parseXml(trimBlanks(xml));
|
|
60
|
+
const statements = root.childElements.flatMap((message) =>
|
|
61
|
+
message.childElements.filter((c) => statementNames.has(c.localName)),
|
|
62
|
+
);
|
|
63
|
+
if (statements.length === 0) throw new FormatError('camt document without report, statement or notification');
|
|
64
|
+
return statements.map((statement) => {
|
|
65
|
+
budget.take();
|
|
66
|
+
const account = statement.child('Acct');
|
|
67
|
+
const entries = statement.childrenNamed('Ntry');
|
|
68
|
+
return frozenWithInspect(
|
|
69
|
+
{
|
|
70
|
+
reference: statement.textAt(['Id']),
|
|
71
|
+
accountId: account?.textAt(['Id', 'IBAN']) ?? account?.textAt(['Id', 'Othr', 'Id']),
|
|
72
|
+
currency: account?.textAt(['Ccy']),
|
|
73
|
+
transactions: Object.freeze(entries.flatMap((entry) => new CamtEntry(entry, status, budget).transactions())),
|
|
74
|
+
},
|
|
75
|
+
(s) => `CamtStatement(${maskedId(s.accountId ?? '')}, ${counted(s.transactions.length, 'transaction')})`,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
22
80
|
/** Width of the purpose lines banks cut the remittance information into, as in MT940 and DTAUS. */
|
|
23
81
|
const cutLineLength = 35;
|
|
24
82
|
|
|
25
83
|
/**
|
|
26
|
-
* One booking (`Ntry`) of a report; a batch booking yields one transaction per transaction detail (`TxDtls`) when
|
|
27
|
-
*
|
|
84
|
+
* One booking (`Ntry`) of a report; a batch booking yields one transaction per transaction detail (`TxDtls`) when the
|
|
85
|
+
* details state their amounts and these add up to the booked amount.
|
|
28
86
|
*
|
|
29
87
|
* Nothing is guessed: what the entry lacks, and what the reader needs, is a `FormatError`. The one exception is the
|
|
30
88
|
* value date, which `Transaction.valueDate` requires: an entry with a booking date only takes that.
|
|
@@ -37,6 +95,8 @@ class CamtEntry {
|
|
|
37
95
|
private readonly status: TransactionStatus;
|
|
38
96
|
private readonly bookingDate: Date | undefined;
|
|
39
97
|
private readonly valueDate: Date;
|
|
98
|
+
private readonly bookingText: string | undefined;
|
|
99
|
+
private readonly bankReference: string | undefined;
|
|
40
100
|
|
|
41
101
|
/** German banks send `transaction type+GVC+prima nota`, for example `NDDT+105+9310`. */
|
|
42
102
|
private readonly proprietaryCode: readonly string[];
|
|
@@ -44,6 +104,7 @@ class CamtEntry {
|
|
|
44
104
|
constructor(
|
|
45
105
|
private readonly entry: XmlElement,
|
|
46
106
|
forcedStatus: TransactionStatus | undefined,
|
|
107
|
+
private readonly budget: RecordBudget,
|
|
47
108
|
) {
|
|
48
109
|
this.amountElement = entry.child('Amt') ?? fail('camt entry without amount');
|
|
49
110
|
this.currency = this.amountElement.getAttribute('Ccy') ?? fail('camt amount without currency');
|
|
@@ -52,6 +113,8 @@ class CamtEntry {
|
|
|
52
113
|
this.status = forcedStatus ?? this.statusOfEntry();
|
|
53
114
|
this.bookingDate = dateOf(entry.child('BookgDt'));
|
|
54
115
|
this.valueDate = dateOf(entry.child('ValDt')) ?? this.bookingDate ?? fail('camt entry without date');
|
|
116
|
+
this.bookingText = entry.textAt(['AddtlNtryInf']);
|
|
117
|
+
this.bankReference = entry.textAt(['AcctSvcrRef']);
|
|
55
118
|
this.proprietaryCode = entry.textAt(['BkTxCd', 'Prtry', 'Cd'])?.split('+') ?? [];
|
|
56
119
|
}
|
|
57
120
|
|
|
@@ -61,47 +124,56 @@ class CamtEntry {
|
|
|
61
124
|
}
|
|
62
125
|
|
|
63
126
|
transactions(): Transaction[] {
|
|
64
|
-
const total =
|
|
65
|
-
const details = this.entry.
|
|
127
|
+
const total = amountOf(this.amountElement, this.currency);
|
|
128
|
+
const details = this.entry.childrenNamed('NtryDtls').flatMap((d) => d.childrenNamed('TxDtls'));
|
|
66
129
|
if (details.length <= 1) return [this.transaction(details[0], total, this.debit)];
|
|
130
|
+
// A batch is split only into details that make up its booking: each states its amount, and they add up to the
|
|
131
|
+
// booked one, in its currency. It stays one booking otherwise, without the details of any one of them, which would
|
|
132
|
+
// describe the others wrongly.
|
|
133
|
+
const booking = this.transaction(undefined, total, this.debit);
|
|
67
134
|
const amounts = details.map((detail) => detail.at(['AmtDtls', 'TxAmt', 'Amt']) ?? detail.child('Amt'));
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return details.map((detail, i) => {
|
|
72
|
-
const amount = amounts[i]!;
|
|
73
|
-
return this.transaction(
|
|
135
|
+
if (amounts.includes(undefined)) return [booking];
|
|
136
|
+
const split = details.map((detail, i) =>
|
|
137
|
+
this.transaction(
|
|
74
138
|
detail,
|
|
75
|
-
|
|
139
|
+
amountOf(amounts[i]!, this.currency),
|
|
76
140
|
// A detail may book in the other direction than the batch as a whole.
|
|
77
141
|
debitIndicator(detail.textAt(['CdtDbtInd'])) ?? this.debit,
|
|
78
|
-
)
|
|
79
|
-
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
const parts = split.map((t) => t.amount);
|
|
145
|
+
return addsUpTo(parts, booking.amount) ? split : [booking];
|
|
80
146
|
}
|
|
81
147
|
|
|
82
148
|
/** A transaction of `amount`, booked as a debit if `debit`, with what `details` tell about it. */
|
|
83
149
|
private transaction(details: XmlElement | undefined, amount: Amount, debit: boolean): Transaction {
|
|
150
|
+
this.budget.take();
|
|
84
151
|
const parties = details?.child('RltdPties');
|
|
85
152
|
const agents = details?.child('RltdAgts');
|
|
86
153
|
// The related parties describe the original operation, which a reversal books the other way round (ISO 20022
|
|
87
154
|
// `RvslInd`: with `CRDT` the original operation was a debit, with `DBIT` a credit).
|
|
88
155
|
const originalDebit = debit !== this.reversal;
|
|
89
156
|
const counterparty = originalDebit ? 'Cdtr' : 'Dbtr';
|
|
90
|
-
const lines = (details?.
|
|
91
|
-
.map((line) => line.innerText
|
|
92
|
-
.filter((text) => text !== '');
|
|
93
|
-
|
|
94
|
-
|
|
157
|
+
const lines = (details?.child('RmtInf')?.childrenNamed('Ustrd') ?? [])
|
|
158
|
+
.map((line) => line.innerText)
|
|
159
|
+
.filter((text) => trimBlanks(text) !== '');
|
|
160
|
+
// A line of 35 characters goes on in the next one as it is, maybe in the middle of a word or before a blank; any
|
|
161
|
+
// other line ends a phrase, which the next one follows after a blank.
|
|
162
|
+
const cut = (i: number): boolean => lines[i]?.length === cutLineLength;
|
|
163
|
+
const purposeParts = lines.map((line, i) => {
|
|
164
|
+
const part = cut(i) ? line : trimBlanks(line, { start: false });
|
|
165
|
+
return i === 0 || cut(i - 1) ? part : ` ${trimBlanks(part, { end: false })}`;
|
|
166
|
+
});
|
|
167
|
+
const remittance = purposeParts.join('');
|
|
168
|
+
// As long as an MT940 field: the keywords of megabytes of text would take memory without limit.
|
|
169
|
+
if (remittance.length > maxFieldLength) throw new FormatError('camt remittance information too long');
|
|
95
170
|
const keywords = SepaPurpose.parseParts(purposeParts);
|
|
96
171
|
const endToEndId = providedReference(details?.textAt(['Refs', 'EndToEndId']));
|
|
97
172
|
const mandateId = details?.textAt(['Refs', 'MndtId']);
|
|
98
|
-
const creditorId = creditorIdOf(parties);
|
|
173
|
+
const creditorId = creditorIdOf(parties?.child('Cdtr'));
|
|
99
174
|
const purposeCode = details?.textAt(['Purp', 'Cd']);
|
|
100
175
|
const typeId = this.proprietaryCode[0]?.length === 4 ? this.proprietaryCode[0] : undefined;
|
|
101
|
-
const purposeCodes =
|
|
102
|
-
if (endToEndId !== undefined) purposeCodes.set('EREF', endToEndId);
|
|
103
|
-
if (mandateId !== undefined) purposeCodes.set('MREF', mandateId);
|
|
104
|
-
if (creditorId !== undefined) purposeCodes.set('CRED', creditorId);
|
|
176
|
+
const purposeCodes = withKeywordsStatedApart(keywords, { EREF: endToEndId, MREF: mandateId, CRED: creditorId });
|
|
105
177
|
if (purposeCode !== undefined) purposeCodes.set('PURP', purposeCode);
|
|
106
178
|
return new Transaction({
|
|
107
179
|
status: this.status,
|
|
@@ -109,8 +181,12 @@ class CamtEntry {
|
|
|
109
181
|
valueDate: this.valueDate,
|
|
110
182
|
amount: debit ? amount.negate() : amount,
|
|
111
183
|
isReversal: this.reversal,
|
|
112
|
-
bookingText: this.
|
|
113
|
-
purpose:
|
|
184
|
+
bookingText: this.bookingText ?? details?.textAt(['AddtlTxInf']),
|
|
185
|
+
purpose:
|
|
186
|
+
keywords.get('SVWZ') ??
|
|
187
|
+
nonBlank(remittance) ??
|
|
188
|
+
details?.textAt(['RmtInf', 'Strd', 'CdtrRefInf', 'Ref']) ??
|
|
189
|
+
details?.textAt(['AddtlTxInf']),
|
|
114
190
|
counterpartyName: partyName(parties?.child(counterparty)),
|
|
115
191
|
counterpartyIban: parties?.child(`${counterparty}Acct`)?.textAt(['Id', 'IBAN']),
|
|
116
192
|
counterpartyAccountNumber: parties?.child(`${counterparty}Acct`)?.textAt(['Id', 'Othr', 'Id']),
|
|
@@ -119,7 +195,7 @@ class CamtEntry {
|
|
|
119
195
|
mandateId,
|
|
120
196
|
creditorId,
|
|
121
197
|
customerReference: details?.textAt(['Refs', 'InstrId']) ?? details?.textAt(['Refs', 'PmtInfId']),
|
|
122
|
-
bankReference: details?.textAt(['Refs', 'AcctSvcrRef']) ?? this.
|
|
198
|
+
bankReference: details?.textAt(['Refs', 'AcctSvcrRef']) ?? this.bankReference,
|
|
123
199
|
transactionCode: this.proprietaryCode[1],
|
|
124
200
|
transactionTypeId: typeId,
|
|
125
201
|
primaNota: this.proprietaryCode[2],
|
|
@@ -130,8 +206,31 @@ class CamtEntry {
|
|
|
130
206
|
}
|
|
131
207
|
}
|
|
132
208
|
|
|
133
|
-
|
|
134
|
-
|
|
209
|
+
/**
|
|
210
|
+
* The amount `element` states, in its currency or else in `currency`; throws `FormatError` for a negative one, whose
|
|
211
|
+
* sign the credit/debit indicator would turn: the indicator alone gives the direction.
|
|
212
|
+
*/
|
|
213
|
+
function amountOf(element: XmlElement, currency: string): Amount {
|
|
214
|
+
const amount = Amount.parseDecimal(element.innerText, element.getAttribute('Ccy') ?? currency);
|
|
215
|
+
if (amount.isNegative) throw new FormatError('Negative camt amount', element.innerText);
|
|
216
|
+
return amount;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* `keywords` with the values of EREF, MREF and CRED that the bank `stated` apart, as `endToEndId`, `mandateId` and
|
|
221
|
+
* `creditorId`, in the place the text gives them, and without those it does not state: in the remittance text, which
|
|
222
|
+
* the payer or the payee writes, a creditor ID or mandate reference could be that of a creditor the app trusts.
|
|
223
|
+
*/
|
|
224
|
+
function withKeywordsStatedApart(
|
|
225
|
+
keywords: ReadonlyMap<string, string>,
|
|
226
|
+
stated: Readonly<Record<'EREF' | 'MREF' | 'CRED', string | undefined>>,
|
|
227
|
+
): Map<string, string> {
|
|
228
|
+
const codes = new Map(keywords);
|
|
229
|
+
for (const [keyword, value] of Object.entries(stated)) {
|
|
230
|
+
if (value === undefined) codes.delete(keyword);
|
|
231
|
+
else codes.set(keyword, value);
|
|
232
|
+
}
|
|
233
|
+
return codes;
|
|
135
234
|
}
|
|
136
235
|
|
|
137
236
|
/** `true` for `DBIT`, `false` for `CRDT`, `undefined` if `indicator` is missing; throws for any other value. */
|
|
@@ -148,8 +247,15 @@ function debitIndicator(indicator: string | undefined): boolean | undefined {
|
|
|
148
247
|
}
|
|
149
248
|
}
|
|
150
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The calendar date of `e`: its date (`Dt`), or else its date and time (`DtTm`), both read by `parseIsoDate`, so that
|
|
252
|
+
* a date and time, which may be stated in UTC or another offset, counts on its German day; `undefined` if it states
|
|
253
|
+
* neither. Throws `FormatError` for one it cannot read, which would pass for a missing one otherwise, such as a value
|
|
254
|
+
* date the booking date stands in for.
|
|
255
|
+
*/
|
|
151
256
|
function dateOf(e: XmlElement | undefined): Date | undefined {
|
|
152
|
-
|
|
257
|
+
const date = e?.textAt(['Dt']) ?? e?.textAt(['DtTm']);
|
|
258
|
+
return date === undefined ? undefined : (parseIsoDate(date) ?? fail('Unreadable camt date', date));
|
|
153
259
|
}
|
|
154
260
|
|
|
155
261
|
function partyName(party: XmlElement | undefined): string | undefined {
|
|
@@ -160,10 +266,12 @@ function bicOf(agent: XmlElement | undefined): string | undefined {
|
|
|
160
266
|
return agent?.textAt(['FinInstnId', 'BIC']) ?? agent?.textAt(['FinInstnId', 'BICFI']);
|
|
161
267
|
}
|
|
162
268
|
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
269
|
+
/**
|
|
270
|
+
* The SEPA creditor identifier of the `creditor`: its private `Othr` identification in the scheme `SEPA`. The other
|
|
271
|
+
* parties' identifications are the creditor's or the debtor's to choose, and may be in that scheme as well.
|
|
272
|
+
*/
|
|
273
|
+
function creditorIdOf(creditor: XmlElement | undefined): string | undefined {
|
|
274
|
+
const identifications = creditor?.at(['Id', 'PrvtId']) ?? creditor?.at(['Pty', 'Id', 'PrvtId']);
|
|
275
|
+
const sepa = identifications?.childrenNamed('Othr').find((other) => other.textAt(['SchmeNm', 'Prtry']) === 'SEPA');
|
|
276
|
+
return sepa?.textAt(['Id']);
|
|
169
277
|
}
|