@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.
Files changed (289) hide show
  1. package/CHANGELOG.md +519 -1
  2. package/README.md +138 -40
  3. package/dist/client.d.ts +49 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +242 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/account.d.ts +46 -7
  8. package/dist/core/account.d.ts.map +1 -1
  9. package/dist/core/account.js +143 -24
  10. package/dist/core/account.js.map +1 -1
  11. package/dist/core/amount.d.ts +15 -4
  12. package/dist/core/amount.d.ts.map +1 -1
  13. package/dist/core/amount.js +46 -9
  14. package/dist/core/amount.js.map +1 -1
  15. package/dist/core/balance.d.ts +4 -0
  16. package/dist/core/balance.d.ts.map +1 -1
  17. package/dist/core/balance.js +11 -5
  18. package/dist/core/balance.js.map +1 -1
  19. package/dist/core/bank-info.d.ts +21 -4
  20. package/dist/core/bank-info.d.ts.map +1 -1
  21. package/dist/core/bank-info.js +26 -5
  22. package/dist/core/bank-info.js.map +1 -1
  23. package/dist/core/client.d.ts +81 -19
  24. package/dist/core/client.d.ts.map +1 -1
  25. package/dist/core/config.d.ts +40 -20
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +85 -30
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/errors.d.ts +100 -26
  30. package/dist/core/errors.d.ts.map +1 -1
  31. package/dist/core/errors.js +136 -53
  32. package/dist/core/errors.js.map +1 -1
  33. package/dist/core/feedback.d.ts +11 -1
  34. package/dist/core/feedback.d.ts.map +1 -1
  35. package/dist/core/feedback.js +14 -2
  36. package/dist/core/feedback.js.map +1 -1
  37. package/dist/core/transaction.d.ts +11 -1
  38. package/dist/core/transaction.d.ts.map +1 -1
  39. package/dist/core/transaction.js +21 -4
  40. package/dist/core/transaction.js.map +1 -1
  41. package/dist/core/transport.d.ts +48 -6
  42. package/dist/core/transport.d.ts.map +1 -1
  43. package/dist/core/transport.js +126 -29
  44. package/dist/core/transport.js.map +1 -1
  45. package/dist/formats/camt.d.ts +28 -5
  46. package/dist/formats/camt.d.ts.map +1 -1
  47. package/dist/formats/camt.js +132 -50
  48. package/dist/formats/camt.js.map +1 -1
  49. package/dist/formats/mt940.d.ts +29 -5
  50. package/dist/formats/mt940.d.ts.map +1 -1
  51. package/dist/formats/mt940.js +209 -113
  52. package/dist/formats/mt940.js.map +1 -1
  53. package/dist/formats/readers.d.ts +6 -2
  54. package/dist/formats/readers.d.ts.map +1 -1
  55. package/dist/formats/readers.js +28 -29
  56. package/dist/formats/readers.js.map +1 -1
  57. package/dist/formats/record-budget.d.ts +7 -0
  58. package/dist/formats/record-budget.d.ts.map +1 -0
  59. package/dist/formats/record-budget.js +17 -0
  60. package/dist/formats/record-budget.js.map +1 -0
  61. package/dist/formats/sepa-purpose.d.ts +9 -0
  62. package/dist/formats/sepa-purpose.d.ts.map +1 -1
  63. package/dist/formats/sepa-purpose.js +32 -2
  64. package/dist/formats/sepa-purpose.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/session/app-approval.d.ts +11 -5
  70. package/dist/session/app-approval.d.ts.map +1 -1
  71. package/dist/session/app-approval.js +23 -12
  72. package/dist/session/app-approval.js.map +1 -1
  73. package/dist/session/dialog-runner.d.ts +11 -11
  74. package/dist/session/dialog-runner.d.ts.map +1 -1
  75. package/dist/session/dialog-runner.js +34 -19
  76. package/dist/session/dialog-runner.js.map +1 -1
  77. package/dist/session/login-guard.d.ts +54 -49
  78. package/dist/session/login-guard.d.ts.map +1 -1
  79. package/dist/session/login-guard.js +170 -83
  80. package/dist/session/login-guard.js.map +1 -1
  81. package/dist/session/sepa-accounts.d.ts +20 -14
  82. package/dist/session/sepa-accounts.d.ts.map +1 -1
  83. package/dist/session/sepa-accounts.js +62 -46
  84. package/dist/session/sepa-accounts.js.map +1 -1
  85. package/dist/session/session-setup.d.ts +41 -24
  86. package/dist/session/session-setup.d.ts.map +1 -1
  87. package/dist/session/session-setup.js +106 -63
  88. package/dist/session/session-setup.js.map +1 -1
  89. package/dist/session/session-state.d.ts +10 -6
  90. package/dist/session/session-state.d.ts.map +1 -1
  91. package/dist/session/session-state.js +72 -18
  92. package/dist/session/session-state.js.map +1 -1
  93. package/dist/session/signed-dialog.d.ts +83 -0
  94. package/dist/session/signed-dialog.d.ts.map +1 -0
  95. package/dist/session/signed-dialog.js +185 -0
  96. package/dist/session/signed-dialog.js.map +1 -0
  97. package/dist/session/tan-selection.d.ts +26 -18
  98. package/dist/session/tan-selection.d.ts.map +1 -1
  99. package/dist/session/tan-selection.js +44 -33
  100. package/dist/session/tan-selection.js.map +1 -1
  101. package/dist/session/user-prompts.d.ts +14 -5
  102. package/dist/session/user-prompts.d.ts.map +1 -1
  103. package/dist/session/user-prompts.js +53 -18
  104. package/dist/session/user-prompts.js.map +1 -1
  105. package/dist/tan/challenge.d.ts +47 -24
  106. package/dist/tan/challenge.d.ts.map +1 -1
  107. package/dist/tan/challenge.js +56 -51
  108. package/dist/tan/challenge.js.map +1 -1
  109. package/dist/tan/flicker.d.ts +11 -1
  110. package/dist/tan/flicker.d.ts.map +1 -1
  111. package/dist/tan/flicker.js +70 -24
  112. package/dist/tan/flicker.js.map +1 -1
  113. package/dist/tan/tan-image.d.ts +6 -2
  114. package/dist/tan/tan-image.d.ts.map +1 -1
  115. package/dist/tan/tan-image.js +14 -6
  116. package/dist/tan/tan-image.js.map +1 -1
  117. package/dist/tan/tan-models.d.ts +14 -2
  118. package/dist/tan/tan-models.d.ts.map +1 -1
  119. package/dist/tan/tan-models.js +24 -7
  120. package/dist/tan/tan-models.js.map +1 -1
  121. package/dist/util/arguments.d.ts +13 -0
  122. package/dist/util/arguments.d.ts.map +1 -0
  123. package/dist/util/arguments.js +40 -0
  124. package/dist/util/arguments.js.map +1 -0
  125. package/dist/util/async.d.ts +4 -3
  126. package/dist/util/async.d.ts.map +1 -1
  127. package/dist/util/async.js +6 -7
  128. package/dist/util/async.js.map +1 -1
  129. package/dist/util/bytes.d.ts +3 -4
  130. package/dist/util/bytes.d.ts.map +1 -1
  131. package/dist/util/bytes.js +13 -21
  132. package/dist/util/bytes.js.map +1 -1
  133. package/dist/util/call-context.d.ts.map +1 -1
  134. package/dist/util/call-context.js +3 -2
  135. package/dist/util/call-context.js.map +1 -1
  136. package/dist/util/dates.d.ts +6 -20
  137. package/dist/util/dates.d.ts.map +1 -1
  138. package/dist/util/dates.js +58 -28
  139. package/dist/util/dates.js.map +1 -1
  140. package/dist/util/errors.d.ts +13 -3
  141. package/dist/util/errors.d.ts.map +1 -1
  142. package/dist/util/errors.js +19 -6
  143. package/dist/util/errors.js.map +1 -1
  144. package/dist/util/immutable-map.d.ts +5 -2
  145. package/dist/util/immutable-map.d.ts.map +1 -1
  146. package/dist/util/immutable-map.js +15 -12
  147. package/dist/util/immutable-map.js.map +1 -1
  148. package/dist/util/inspect.d.ts +11 -0
  149. package/dist/util/inspect.d.ts.map +1 -0
  150. package/dist/util/inspect.js +14 -0
  151. package/dist/util/inspect.js.map +1 -0
  152. package/dist/util/json.d.ts +4 -0
  153. package/dist/util/json.d.ts.map +1 -1
  154. package/dist/util/json.js +18 -0
  155. package/dist/util/json.js.map +1 -1
  156. package/dist/util/text.d.ts +38 -5
  157. package/dist/util/text.d.ts.map +1 -1
  158. package/dist/util/text.js +90 -16
  159. package/dist/util/text.js.map +1 -1
  160. package/dist/util/xml.d.ts +9 -6
  161. package/dist/util/xml.d.ts.map +1 -1
  162. package/dist/util/xml.js +129 -56
  163. package/dist/util/xml.js.map +1 -1
  164. package/dist/v3/account-extension.d.ts.map +1 -1
  165. package/dist/v3/account-extension.js +7 -10
  166. package/dist/v3/account-extension.js.map +1 -1
  167. package/dist/v3/bank-connection.d.ts +54 -0
  168. package/dist/v3/bank-connection.d.ts.map +1 -0
  169. package/dist/v3/bank-connection.js +118 -0
  170. package/dist/v3/bank-connection.js.map +1 -0
  171. package/dist/v3/dialog.d.ts +84 -46
  172. package/dist/v3/dialog.d.ts.map +1 -1
  173. package/dist/v3/dialog.js +174 -60
  174. package/dist/v3/dialog.js.map +1 -1
  175. package/dist/v3/message.d.ts +32 -11
  176. package/dist/v3/message.d.ts.map +1 -1
  177. package/dist/v3/message.js +144 -83
  178. package/dist/v3/message.js.map +1 -1
  179. package/dist/v3/parameter-store.d.ts +111 -0
  180. package/dist/v3/parameter-store.d.ts.map +1 -0
  181. package/dist/v3/parameter-store.js +197 -0
  182. package/dist/v3/parameter-store.js.map +1 -0
  183. package/dist/v3/parameters.d.ts +44 -17
  184. package/dist/v3/parameters.d.ts.map +1 -1
  185. package/dist/v3/parameters.js +136 -60
  186. package/dist/v3/parameters.js.map +1 -1
  187. package/dist/v3/protocol.d.ts +50 -50
  188. package/dist/v3/protocol.d.ts.map +1 -1
  189. package/dist/v3/protocol.js +91 -210
  190. package/dist/v3/protocol.js.map +1 -1
  191. package/dist/v3/queries.d.ts +51 -0
  192. package/dist/v3/queries.d.ts.map +1 -0
  193. package/dist/v3/queries.js +128 -0
  194. package/dist/v3/queries.js.map +1 -0
  195. package/dist/v3/segments.d.ts +43 -19
  196. package/dist/v3/segments.d.ts.map +1 -1
  197. package/dist/v3/segments.js +84 -33
  198. package/dist/v3/segments.js.map +1 -1
  199. package/dist/v3/syntax.d.ts +68 -13
  200. package/dist/v3/syntax.d.ts.map +1 -1
  201. package/dist/v3/syntax.js +183 -102
  202. package/dist/v3/syntax.js.map +1 -1
  203. package/package.json +10 -6
  204. package/src/client.ts +293 -0
  205. package/src/core/account.ts +153 -24
  206. package/src/core/amount.ts +45 -10
  207. package/src/core/balance.ts +12 -5
  208. package/src/core/bank-info.ts +29 -5
  209. package/src/core/client.ts +83 -19
  210. package/src/core/config.ts +95 -35
  211. package/src/core/errors.ts +148 -54
  212. package/src/core/feedback.ts +16 -2
  213. package/src/core/transaction.ts +22 -4
  214. package/src/core/transport.ts +153 -32
  215. package/src/formats/camt.ts +153 -45
  216. package/src/formats/mt940.ts +244 -122
  217. package/src/formats/readers.ts +28 -31
  218. package/src/formats/record-budget.ts +18 -0
  219. package/src/formats/sepa-purpose.ts +29 -2
  220. package/src/index.ts +4 -2
  221. package/src/session/app-approval.ts +51 -38
  222. package/src/session/dialog-runner.ts +61 -31
  223. package/src/session/login-guard.ts +189 -87
  224. package/src/session/sepa-accounts.ts +63 -45
  225. package/src/session/session-setup.ts +105 -68
  226. package/src/session/session-state.ts +74 -23
  227. package/src/session/signed-dialog.ts +222 -0
  228. package/src/session/tan-selection.ts +50 -39
  229. package/src/session/user-prompts.ts +62 -18
  230. package/src/tan/challenge.ts +87 -64
  231. package/src/tan/flicker.ts +72 -23
  232. package/src/tan/tan-image.ts +15 -6
  233. package/src/tan/tan-models.ts +26 -7
  234. package/src/util/arguments.ts +40 -0
  235. package/src/util/async.ts +10 -9
  236. package/src/util/bytes.ts +14 -20
  237. package/src/util/call-context.ts +3 -2
  238. package/src/util/dates.ts +64 -33
  239. package/src/util/errors.ts +22 -6
  240. package/src/util/immutable-map.ts +15 -12
  241. package/src/util/inspect.ts +14 -0
  242. package/src/util/json.ts +16 -0
  243. package/src/util/text.ts +87 -15
  244. package/src/util/xml.ts +130 -52
  245. package/src/v3/account-extension.ts +7 -12
  246. package/src/v3/bank-connection.ts +126 -0
  247. package/src/v3/dialog.ts +186 -92
  248. package/src/v3/message.ts +140 -79
  249. package/src/v3/parameter-store.ts +226 -0
  250. package/src/v3/parameters.ts +166 -66
  251. package/src/v3/protocol.ts +103 -253
  252. package/src/v3/queries.ts +198 -0
  253. package/src/v3/segments.ts +86 -31
  254. package/src/v3/syntax.ts +172 -93
  255. package/dist/session/bank-connection.d.ts +0 -28
  256. package/dist/session/bank-connection.d.ts.map +0 -1
  257. package/dist/session/bank-connection.js +0 -48
  258. package/dist/session/bank-connection.js.map +0 -1
  259. package/dist/session/parameter-data.d.ts +0 -83
  260. package/dist/session/parameter-data.d.ts.map +0 -1
  261. package/dist/session/parameter-data.js +0 -68
  262. package/dist/session/parameter-data.js.map +0 -1
  263. package/dist/session/pin-tan-client.d.ts +0 -54
  264. package/dist/session/pin-tan-client.d.ts.map +0 -1
  265. package/dist/session/pin-tan-client.js +0 -153
  266. package/dist/session/pin-tan-client.js.map +0 -1
  267. package/dist/session/pin-tan-dialog.d.ts +0 -71
  268. package/dist/session/pin-tan-dialog.d.ts.map +0 -1
  269. package/dist/session/pin-tan-dialog.js +0 -124
  270. package/dist/session/pin-tan-dialog.js.map +0 -1
  271. package/dist/session/protocol.d.ts +0 -209
  272. package/dist/session/protocol.d.ts.map +0 -1
  273. package/dist/session/protocol.js +0 -75
  274. package/dist/session/protocol.js.map +0 -1
  275. package/dist/session/response-handler.d.ts +0 -48
  276. package/dist/session/response-handler.d.ts.map +0 -1
  277. package/dist/session/response-handler.js +0 -121
  278. package/dist/session/response-handler.js.map +0 -1
  279. package/dist/v3/client.d.ts +0 -24
  280. package/dist/v3/client.d.ts.map +0 -1
  281. package/dist/v3/client.js +0 -16
  282. package/dist/v3/client.js.map +0 -1
  283. package/src/session/bank-connection.ts +0 -51
  284. package/src/session/parameter-data.ts +0 -137
  285. package/src/session/pin-tan-client.ts +0 -186
  286. package/src/session/pin-tan-dialog.ts +0 -160
  287. package/src/session/protocol.ts +0 -311
  288. package/src/session/response-handler.ts +0 -130
  289. package/src/v3/client.ts +0 -29
@@ -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
- /** Parser for ISO 20022 camt.052 and camt.053 account reports. */
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 all entries; throws `FormatError` for malformed XML, and for an entry without amount, currency, credit/debit
12
- * indicator or date.
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
- const root = parseXml(xml.trim());
18
- return root.descendantsNamed('Ntry').flatMap((entry) => new CamtEntry(entry, status).transactions());
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 every
27
- * detail states its amount.
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 = Amount.parseDecimal(this.amountElement.innerText, this.currency);
65
- const details = this.entry.descendantsNamed('TxDtls');
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
- // Without the amount of every detail the batch cannot be split. It stays one booking then, without the details of
69
- // any one of them, which would describe the others wrongly.
70
- if (amounts.includes(undefined)) return [this.transaction(undefined, total, this.debit)];
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
- Amount.parseDecimal(amount.innerText, amount.getAttribute('Ccy') ?? this.currency),
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?.descendantsNamed('Ustrd') ?? [])
91
- .map((line) => line.innerText.trim())
92
- .filter((text) => text !== '');
93
- const purposeParts = lines.map((line, i) => (i > 0 && lines[i - 1]!.length !== cutLineLength ? ` ${line}` : line));
94
- const purpose = purposeParts.join('');
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 = new Map(keywords);
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.entry.textAt(['AddtlNtryInf']) ?? details?.textAt(['AddtlTxInf']),
113
- purpose: keywords.get('SVWZ') ?? (purpose === '' ? details?.textAt(['AddtlTxInf']) : 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.entry.textAt(['AcctSvcrRef']),
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
- function fail(message: string): never {
134
- throw new FormatError(message);
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
- return parseIsoDate(e?.textAt(['Dt']) ?? e?.textAt(['DtTm']));
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
- /** The SEPA creditor identifier: the `Othr` identification in the scheme `SEPA`. */
164
- function creditorIdOf(parties: XmlElement | undefined): string | undefined {
165
- for (const other of parties?.descendantsNamed('Othr') ?? []) {
166
- if (other.textAt(['SchmeNm', 'Prtry']) === 'SEPA') return other.textAt(['Id']);
167
- }
168
- return undefined;
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
  }