@coderoftime/fints 0.0.0-stage → 0.1.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 (254) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +28 -0
  3. package/README.md +156 -2
  4. package/dist/core/account.d.ts +140 -0
  5. package/dist/core/account.d.ts.map +1 -0
  6. package/dist/core/account.js +174 -0
  7. package/dist/core/account.js.map +1 -0
  8. package/dist/core/amount.d.ts +62 -0
  9. package/dist/core/amount.d.ts.map +1 -0
  10. package/dist/core/amount.js +189 -0
  11. package/dist/core/amount.js.map +1 -0
  12. package/dist/core/balance.d.ts +54 -0
  13. package/dist/core/balance.d.ts.map +1 -0
  14. package/dist/core/balance.js +53 -0
  15. package/dist/core/balance.js.map +1 -0
  16. package/dist/core/bank-info.d.ts +49 -0
  17. package/dist/core/bank-info.d.ts.map +1 -0
  18. package/dist/core/bank-info.js +52 -0
  19. package/dist/core/bank-info.js.map +1 -0
  20. package/dist/core/client.d.ts +126 -0
  21. package/dist/core/client.d.ts.map +1 -0
  22. package/dist/core/client.js +2 -0
  23. package/dist/core/client.js.map +1 -0
  24. package/dist/core/config.d.ts +68 -0
  25. package/dist/core/config.d.ts.map +1 -0
  26. package/dist/core/config.js +113 -0
  27. package/dist/core/config.js.map +1 -0
  28. package/dist/core/errors.d.ts +91 -0
  29. package/dist/core/errors.d.ts.map +1 -0
  30. package/dist/core/errors.js +182 -0
  31. package/dist/core/errors.js.map +1 -0
  32. package/dist/core/feedback.d.ts +69 -0
  33. package/dist/core/feedback.d.ts.map +1 -0
  34. package/dist/core/feedback.js +124 -0
  35. package/dist/core/feedback.js.map +1 -0
  36. package/dist/core/transaction.d.ts +98 -0
  37. package/dist/core/transaction.d.ts.map +1 -0
  38. package/dist/core/transaction.js +91 -0
  39. package/dist/core/transaction.js.map +1 -0
  40. package/dist/core/transport.d.ts +37 -0
  41. package/dist/core/transport.d.ts.map +1 -0
  42. package/dist/core/transport.js +92 -0
  43. package/dist/core/transport.js.map +1 -0
  44. package/dist/formats/camt.d.ts +12 -0
  45. package/dist/formats/camt.d.ts.map +1 -0
  46. package/dist/formats/camt.js +159 -0
  47. package/dist/formats/camt.js.map +1 -0
  48. package/dist/formats/mt940.d.ts +42 -0
  49. package/dist/formats/mt940.d.ts.map +1 -0
  50. package/dist/formats/mt940.js +297 -0
  51. package/dist/formats/mt940.js.map +1 -0
  52. package/dist/formats/readers.d.ts +11 -0
  53. package/dist/formats/readers.d.ts.map +1 -0
  54. package/dist/formats/readers.js +47 -0
  55. package/dist/formats/readers.js.map +1 -0
  56. package/dist/formats/sepa-purpose.d.ts +22 -0
  57. package/dist/formats/sepa-purpose.d.ts.map +1 -0
  58. package/dist/formats/sepa-purpose.js +68 -0
  59. package/dist/formats/sepa-purpose.js.map +1 -0
  60. package/dist/index.d.ts +28 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +27 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/session/app-approval.d.ts +60 -0
  65. package/dist/session/app-approval.d.ts.map +1 -0
  66. package/dist/session/app-approval.js +215 -0
  67. package/dist/session/app-approval.js.map +1 -0
  68. package/dist/session/bank-connection.d.ts +28 -0
  69. package/dist/session/bank-connection.d.ts.map +1 -0
  70. package/dist/session/bank-connection.js +48 -0
  71. package/dist/session/bank-connection.js.map +1 -0
  72. package/dist/session/dialog-runner.d.ts +34 -0
  73. package/dist/session/dialog-runner.d.ts.map +1 -0
  74. package/dist/session/dialog-runner.js +112 -0
  75. package/dist/session/dialog-runner.js.map +1 -0
  76. package/dist/session/login-guard.d.ts +73 -0
  77. package/dist/session/login-guard.d.ts.map +1 -0
  78. package/dist/session/login-guard.js +109 -0
  79. package/dist/session/login-guard.js.map +1 -0
  80. package/dist/session/parameter-data.d.ts +83 -0
  81. package/dist/session/parameter-data.d.ts.map +1 -0
  82. package/dist/session/parameter-data.js +68 -0
  83. package/dist/session/parameter-data.js.map +1 -0
  84. package/dist/session/pin-tan-client.d.ts +54 -0
  85. package/dist/session/pin-tan-client.d.ts.map +1 -0
  86. package/dist/session/pin-tan-client.js +153 -0
  87. package/dist/session/pin-tan-client.js.map +1 -0
  88. package/dist/session/pin-tan-dialog.d.ts +71 -0
  89. package/dist/session/pin-tan-dialog.d.ts.map +1 -0
  90. package/dist/session/pin-tan-dialog.js +124 -0
  91. package/dist/session/pin-tan-dialog.js.map +1 -0
  92. package/dist/session/protocol.d.ts +209 -0
  93. package/dist/session/protocol.d.ts.map +1 -0
  94. package/dist/session/protocol.js +75 -0
  95. package/dist/session/protocol.js.map +1 -0
  96. package/dist/session/response-handler.d.ts +48 -0
  97. package/dist/session/response-handler.d.ts.map +1 -0
  98. package/dist/session/response-handler.js +121 -0
  99. package/dist/session/response-handler.js.map +1 -0
  100. package/dist/session/sepa-accounts.d.ts +34 -0
  101. package/dist/session/sepa-accounts.d.ts.map +1 -0
  102. package/dist/session/sepa-accounts.js +96 -0
  103. package/dist/session/sepa-accounts.js.map +1 -0
  104. package/dist/session/session-setup.d.ts +72 -0
  105. package/dist/session/session-setup.d.ts.map +1 -0
  106. package/dist/session/session-setup.js +160 -0
  107. package/dist/session/session-setup.js.map +1 -0
  108. package/dist/session/session-state.d.ts +47 -0
  109. package/dist/session/session-state.d.ts.map +1 -0
  110. package/dist/session/session-state.js +147 -0
  111. package/dist/session/session-state.js.map +1 -0
  112. package/dist/session/tan-selection.d.ts +88 -0
  113. package/dist/session/tan-selection.d.ts.map +1 -0
  114. package/dist/session/tan-selection.js +178 -0
  115. package/dist/session/tan-selection.js.map +1 -0
  116. package/dist/session/user-prompts.d.ts +61 -0
  117. package/dist/session/user-prompts.d.ts.map +1 -0
  118. package/dist/session/user-prompts.js +164 -0
  119. package/dist/session/user-prompts.js.map +1 -0
  120. package/dist/tan/challenge.d.ts +114 -0
  121. package/dist/tan/challenge.d.ts.map +1 -0
  122. package/dist/tan/challenge.js +185 -0
  123. package/dist/tan/challenge.js.map +1 -0
  124. package/dist/tan/flicker.d.ts +36 -0
  125. package/dist/tan/flicker.d.ts.map +1 -0
  126. package/dist/tan/flicker.js +262 -0
  127. package/dist/tan/flicker.js.map +1 -0
  128. package/dist/tan/tan-image.d.ts +21 -0
  129. package/dist/tan/tan-image.d.ts.map +1 -0
  130. package/dist/tan/tan-image.js +41 -0
  131. package/dist/tan/tan-image.js.map +1 -0
  132. package/dist/tan/tan-models.d.ts +145 -0
  133. package/dist/tan/tan-models.d.ts.map +1 -0
  134. package/dist/tan/tan-models.js +148 -0
  135. package/dist/tan/tan-models.js.map +1 -0
  136. package/dist/util/async.d.ts +24 -0
  137. package/dist/util/async.d.ts.map +1 -0
  138. package/dist/util/async.js +49 -0
  139. package/dist/util/async.js.map +1 -0
  140. package/dist/util/bytes.d.ts +23 -0
  141. package/dist/util/bytes.d.ts.map +1 -0
  142. package/dist/util/bytes.js +136 -0
  143. package/dist/util/bytes.js.map +1 -0
  144. package/dist/util/call-context.d.ts +12 -0
  145. package/dist/util/call-context.d.ts.map +1 -0
  146. package/dist/util/call-context.js +29 -0
  147. package/dist/util/call-context.js.map +1 -0
  148. package/dist/util/dates.d.ts +62 -0
  149. package/dist/util/dates.d.ts.map +1 -0
  150. package/dist/util/dates.js +169 -0
  151. package/dist/util/dates.js.map +1 -0
  152. package/dist/util/errors.d.ts +9 -0
  153. package/dist/util/errors.d.ts.map +1 -0
  154. package/dist/util/errors.js +15 -0
  155. package/dist/util/errors.js.map +1 -0
  156. package/dist/util/immutable-map.d.ts +16 -0
  157. package/dist/util/immutable-map.d.ts.map +1 -0
  158. package/dist/util/immutable-map.js +38 -0
  159. package/dist/util/immutable-map.js.map +1 -0
  160. package/dist/util/json.d.ts +13 -0
  161. package/dist/util/json.d.ts.map +1 -0
  162. package/dist/util/json.js +44 -0
  163. package/dist/util/json.js.map +1 -0
  164. package/dist/util/text.d.ts +20 -0
  165. package/dist/util/text.d.ts.map +1 -0
  166. package/dist/util/text.js +78 -0
  167. package/dist/util/text.js.map +1 -0
  168. package/dist/util/xml.d.ts +35 -0
  169. package/dist/util/xml.d.ts.map +1 -0
  170. package/dist/util/xml.js +183 -0
  171. package/dist/util/xml.js.map +1 -0
  172. package/dist/v3/account-extension.d.ts +25 -0
  173. package/dist/v3/account-extension.d.ts.map +1 -0
  174. package/dist/v3/account-extension.js +176 -0
  175. package/dist/v3/account-extension.js.map +1 -0
  176. package/dist/v3/client.d.ts +24 -0
  177. package/dist/v3/client.d.ts.map +1 -0
  178. package/dist/v3/client.js +16 -0
  179. package/dist/v3/client.js.map +1 -0
  180. package/dist/v3/dialog.d.ts +78 -0
  181. package/dist/v3/dialog.d.ts.map +1 -0
  182. package/dist/v3/dialog.js +138 -0
  183. package/dist/v3/dialog.js.map +1 -0
  184. package/dist/v3/message.d.ts +72 -0
  185. package/dist/v3/message.d.ts.map +1 -0
  186. package/dist/v3/message.js +259 -0
  187. package/dist/v3/message.js.map +1 -0
  188. package/dist/v3/parameters.d.ts +79 -0
  189. package/dist/v3/parameters.d.ts.map +1 -0
  190. package/dist/v3/parameters.js +396 -0
  191. package/dist/v3/parameters.js.map +1 -0
  192. package/dist/v3/protocol.d.ts +68 -0
  193. package/dist/v3/protocol.d.ts.map +1 -0
  194. package/dist/v3/protocol.js +263 -0
  195. package/dist/v3/protocol.js.map +1 -0
  196. package/dist/v3/segments.d.ts +89 -0
  197. package/dist/v3/segments.d.ts.map +1 -0
  198. package/dist/v3/segments.js +283 -0
  199. package/dist/v3/segments.js.map +1 -0
  200. package/dist/v3/syntax.d.ts +76 -0
  201. package/dist/v3/syntax.d.ts.map +1 -0
  202. package/dist/v3/syntax.js +301 -0
  203. package/dist/v3/syntax.js.map +1 -0
  204. package/package.json +63 -4
  205. package/src/core/account.ts +250 -0
  206. package/src/core/amount.ts +209 -0
  207. package/src/core/balance.ts +84 -0
  208. package/src/core/bank-info.ts +66 -0
  209. package/src/core/client.ts +149 -0
  210. package/src/core/config.ts +141 -0
  211. package/src/core/errors.ts +203 -0
  212. package/src/core/feedback.ts +137 -0
  213. package/src/core/transaction.ts +156 -0
  214. package/src/core/transport.ts +114 -0
  215. package/src/formats/camt.ts +169 -0
  216. package/src/formats/mt940.ts +333 -0
  217. package/src/formats/readers.ts +56 -0
  218. package/src/formats/sepa-purpose.ts +75 -0
  219. package/src/index.ts +68 -0
  220. package/src/session/app-approval.ts +265 -0
  221. package/src/session/bank-connection.ts +51 -0
  222. package/src/session/dialog-runner.ts +119 -0
  223. package/src/session/login-guard.ts +135 -0
  224. package/src/session/parameter-data.ts +137 -0
  225. package/src/session/pin-tan-client.ts +186 -0
  226. package/src/session/pin-tan-dialog.ts +160 -0
  227. package/src/session/protocol.ts +311 -0
  228. package/src/session/response-handler.ts +130 -0
  229. package/src/session/sepa-accounts.ts +97 -0
  230. package/src/session/session-setup.ts +165 -0
  231. package/src/session/session-state.ts +170 -0
  232. package/src/session/tan-selection.ts +192 -0
  233. package/src/session/user-prompts.ts +174 -0
  234. package/src/tan/challenge.ts +252 -0
  235. package/src/tan/flicker.ts +270 -0
  236. package/src/tan/tan-image.ts +36 -0
  237. package/src/tan/tan-models.ts +247 -0
  238. package/src/util/async.ts +53 -0
  239. package/src/util/bytes.ts +135 -0
  240. package/src/util/call-context.ts +40 -0
  241. package/src/util/dates.ts +193 -0
  242. package/src/util/errors.ts +16 -0
  243. package/src/util/immutable-map.ts +47 -0
  244. package/src/util/json.ts +51 -0
  245. package/src/util/text.ts +82 -0
  246. package/src/util/xml.ts +172 -0
  247. package/src/v3/account-extension.ts +190 -0
  248. package/src/v3/client.ts +29 -0
  249. package/src/v3/dialog.ts +199 -0
  250. package/src/v3/message.ts +321 -0
  251. package/src/v3/parameters.ts +451 -0
  252. package/src/v3/protocol.ts +331 -0
  253. package/src/v3/segments.ts +331 -0
  254. package/src/v3/syntax.ts +307 -0
@@ -0,0 +1,156 @@
1
+ import { formatIsoDate } from '../util/dates.js';
2
+ import { ImmutableMap } from '../util/immutable-map.js';
3
+ import type { Amount } from './amount.js';
4
+
5
+ /** Whether a transaction is booked or still pending. */
6
+ export type TransactionStatus = 'booked' | 'pending';
7
+
8
+ /** Transaction format to request; `auto` prefers camt.052 when the bank supports it. */
9
+ export type TransactionFormat =
10
+ /** camt.052 where the bank offers it, otherwise MT940. */
11
+ | 'auto'
12
+ /** SWIFT MT940 (booked) and MT942 (pending). */
13
+ | 'mt940'
14
+ /** ISO 20022 camt.052. */
15
+ | 'camt';
16
+
17
+ /** What `Transaction` takes; the parsers fill in what the bank states. */
18
+ export interface TransactionInit {
19
+ status?: TransactionStatus;
20
+ bookingDate?: Date | undefined;
21
+ valueDate: Date;
22
+ amount: Amount;
23
+ isReversal?: boolean;
24
+ bookingText?: string | undefined;
25
+ purpose?: string | undefined;
26
+ counterpartyName?: string | undefined;
27
+ counterpartyIban?: string | undefined;
28
+ counterpartyBic?: string | undefined;
29
+ counterpartyAccountNumber?: string | undefined;
30
+ counterpartyBankCode?: string | undefined;
31
+ endToEndId?: string | undefined;
32
+ mandateId?: string | undefined;
33
+ creditorId?: string | undefined;
34
+ customerReference?: string | undefined;
35
+ bankReference?: string | undefined;
36
+ transactionCode?: string | undefined;
37
+ transactionTypeId?: string | undefined;
38
+ primaNota?: string | undefined;
39
+ ultimateCreditor?: string | undefined;
40
+ ultimateDebtor?: string | undefined;
41
+ purposeCodes?: ReadonlyMap<string, string>;
42
+ }
43
+
44
+ /** A single account transaction, parsed from camt.052 or MT940/MT942. */
45
+ export class Transaction {
46
+ /** Whether the transaction is booked or pending. */
47
+ readonly status: TransactionStatus;
48
+
49
+ /**
50
+ * Booking date; `undefined` where the bank states none, as for some pending transactions and MT940 entries without an
51
+ * entry date. A local `Date` at midnight, like every calendar date.
52
+ */
53
+ readonly bookingDate: Date | undefined;
54
+
55
+ /** Value date (Valuta); for a camt entry that states its booking date only, the booking date. */
56
+ readonly valueDate: Date;
57
+
58
+ /** Signed amount; negative for debits. */
59
+ readonly amount: Amount;
60
+
61
+ /** Whether this is a reversal (Storno) of an earlier booking. */
62
+ readonly isReversal: boolean;
63
+
64
+ /** Booking type text, for example `SEPA-UEBERWEISUNG`. */
65
+ readonly bookingText: string | undefined;
66
+
67
+ /** Remittance information; the `SVWZ+` part when the bank sends SEPA keywords. */
68
+ readonly purpose: string | undefined;
69
+
70
+ /** Payee for debits, payer for credits; for a reversal, the counterparty of the booking it reverses. */
71
+ readonly counterpartyName: string | undefined;
72
+
73
+ /** IBAN of the counterparty. */
74
+ readonly counterpartyIban: string | undefined;
75
+
76
+ /** BIC of the counterparty's bank. */
77
+ readonly counterpartyBic: string | undefined;
78
+
79
+ /** National account number of the counterparty, where the bank does not state an IBAN. */
80
+ readonly counterpartyAccountNumber: string | undefined;
81
+
82
+ /** German bank code of the counterparty's bank, where the bank does not state a BIC. */
83
+ readonly counterpartyBankCode: string | undefined;
84
+
85
+ /** SEPA end-to-end reference (EREF). */
86
+ readonly endToEndId: string | undefined;
87
+
88
+ /** SEPA direct debit mandate reference (MREF). */
89
+ readonly mandateId: string | undefined;
90
+
91
+ /** SEPA creditor identifier (CRED). */
92
+ readonly creditorId: string | undefined;
93
+
94
+ /** Reference the customer gave the order. */
95
+ readonly customerReference: string | undefined;
96
+
97
+ /** Reference the bank gave the booking. */
98
+ readonly bankReference: string | undefined;
99
+
100
+ /** German business transaction code (Geschäftsvorfallcode, GVC). */
101
+ readonly transactionCode: string | undefined;
102
+
103
+ /** SWIFT transaction type, for example `NTRF`. */
104
+ readonly transactionTypeId: string | undefined;
105
+
106
+ /** Prima nota number of the booking. */
107
+ readonly primaNota: string | undefined;
108
+
109
+ /** The party the payee acts for, where that is not the payee itself. */
110
+ readonly ultimateCreditor: string | undefined;
111
+
112
+ /** The party the payer acts for, where that is not the payer itself. */
113
+ readonly ultimateDebtor: string | undefined;
114
+
115
+ /** All SEPA keywords found in the purpose, keyed by keyword (EREF, KREF, MREF, CRED, SVWZ, …). */
116
+ readonly purposeCodes: ReadonlyMap<string, string>;
117
+
118
+ constructor(init: TransactionInit) {
119
+ this.status = init.status ?? 'booked';
120
+ this.bookingDate = init.bookingDate;
121
+ this.valueDate = init.valueDate;
122
+ this.amount = init.amount;
123
+ this.isReversal = init.isReversal ?? false;
124
+ this.bookingText = init.bookingText;
125
+ this.purpose = init.purpose;
126
+ this.counterpartyName = init.counterpartyName;
127
+ this.counterpartyIban = init.counterpartyIban;
128
+ this.counterpartyBic = init.counterpartyBic;
129
+ this.counterpartyAccountNumber = init.counterpartyAccountNumber;
130
+ this.counterpartyBankCode = init.counterpartyBankCode;
131
+ this.endToEndId = init.endToEndId;
132
+ this.mandateId = init.mandateId;
133
+ this.creditorId = init.creditorId;
134
+ this.customerReference = init.customerReference;
135
+ this.bankReference = init.bankReference;
136
+ this.transactionCode = init.transactionCode;
137
+ this.transactionTypeId = init.transactionTypeId;
138
+ this.primaNota = init.primaNota;
139
+ this.ultimateCreditor = init.ultimateCreditor;
140
+ this.ultimateDebtor = init.ultimateDebtor;
141
+ this.purposeCodes = new ImmutableMap(init.purposeCodes ?? []);
142
+ }
143
+
144
+ /** Whether the transaction is still pending. */
145
+ get isPending(): boolean {
146
+ return this.status === 'pending';
147
+ }
148
+
149
+ /**
150
+ * The date and status only, without amount, counterparty or purpose, so that logs and crash reports do not collect
151
+ * account data.
152
+ */
153
+ toString(): string {
154
+ return `Transaction(${formatIsoDate(this.bookingDate ?? this.valueDate)}, ${this.status}${this.isReversal ? ', reversal' : ''})`;
155
+ }
156
+ }
@@ -0,0 +1,114 @@
1
+ import { concatBytes } from '../util/bytes.js';
2
+ import { FinTsTransportError } from './errors.js';
3
+
4
+ /** Sends one FinTS message and returns the bank's reply; replace it for a relay, proxies, pinning or tests. */
5
+ export interface FinTsTransport {
6
+ /**
7
+ * Posts `body` to `url` and returns the response body.
8
+ *
9
+ * Implementations report failures as `FinTsTransportError`.
10
+ */
11
+ post(url: URL, body: Uint8Array, contentType: string): Promise<Uint8Array>;
12
+
13
+ /** Releases the connection; the client calls it when it is closed. */
14
+ close(): void;
15
+ }
16
+
17
+ /** What `HttpTransport` takes; all optional. */
18
+ export interface HttpTransportOptions {
19
+ /** The `fetch` to send with, for example one with a proxy agent; the global `fetch` by default. */
20
+ fetch?: typeof fetch;
21
+ /** Time for the whole exchange in milliseconds, from sending the request to the last byte of the response; 90 s. */
22
+ timeoutMs?: number;
23
+ /** Largest response accepted, so that a faulty server cannot exhaust the memory; 16 MiB. */
24
+ maxResponseBytes?: number;
25
+ }
26
+
27
+ /**
28
+ * Default `FinTsTransport`: HTTP POST with `fetch`.
29
+ *
30
+ * In a browser it only reaches servers that allow the page's origin (CORS), which bank servers do not: a web app needs
31
+ * a server of its own between browser and bank, reached with a `FinTsTransport` of its own.
32
+ */
33
+ export class HttpTransport implements FinTsTransport {
34
+ readonly timeoutMs: number;
35
+ readonly maxResponseBytes: number;
36
+ private readonly fetchFn: typeof fetch;
37
+
38
+ constructor(options: HttpTransportOptions = {}) {
39
+ this.fetchFn = options.fetch ?? ((input, init) => fetch(input, init));
40
+ this.timeoutMs = options.timeoutMs ?? 90_000;
41
+ this.maxResponseBytes = options.maxResponseBytes ?? 16 << 20;
42
+ }
43
+
44
+ async post(url: URL, body: Uint8Array, contentType: string): Promise<Uint8Array> {
45
+ const controller = new AbortController();
46
+ let timer: ReturnType<typeof setTimeout> | undefined;
47
+ // Waiting for the timeout as well covers a `fetch` that cannot abort a request.
48
+ const timedOut = new Promise<never>((_, reject) => {
49
+ timer = setTimeout(() => {
50
+ controller.abort();
51
+ reject(new FinTsTransportError('Request timed out'));
52
+ }, this.timeoutMs);
53
+ });
54
+ try {
55
+ return await Promise.race([this.exchange(url, body, contentType, controller.signal), timedOut]);
56
+ } catch (error) {
57
+ if (error instanceof FinTsTransportError) throw error;
58
+ if (controller.signal.aborted) throw new FinTsTransportError('Request timed out');
59
+ throw new FinTsTransportError(describe(error));
60
+ } finally {
61
+ clearTimeout(timer);
62
+ }
63
+ }
64
+
65
+ private async exchange(url: URL, body: Uint8Array, contentType: string, signal: AbortSignal): Promise<Uint8Array> {
66
+ const response = await this.fetchFn(url, {
67
+ method: 'POST',
68
+ body: body as Uint8Array<ArrayBuffer>,
69
+ headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache', Pragma: 'no-cache' },
70
+ // The PIN travels in the body: never let the request be repeated at another address.
71
+ redirect: 'manual',
72
+ signal,
73
+ });
74
+ if (response.type === 'opaqueredirect') {
75
+ throw new FinTsTransportError('The server redirects the request, which the client does not follow');
76
+ }
77
+ if (response.status < 200 || response.status > 299) {
78
+ await response.body?.cancel().catch(() => undefined);
79
+ throw new FinTsTransportError(`HTTP ${response.status} ${response.statusText}`.trim(), {
80
+ statusCode: response.status,
81
+ });
82
+ }
83
+ return this.readBody(response);
84
+ }
85
+
86
+ private async readBody(response: Response): Promise<Uint8Array> {
87
+ const reader = response.body?.getReader();
88
+ if (reader === undefined) return new Uint8Array(await response.arrayBuffer());
89
+ const chunks: Uint8Array[] = [];
90
+ let size = 0;
91
+ for (;;) {
92
+ const { done, value } = await reader.read();
93
+ if (done) break;
94
+ chunks.push(value);
95
+ size += value.length;
96
+ if (size > this.maxResponseBytes) {
97
+ await reader.cancel().catch(() => undefined);
98
+ throw new FinTsTransportError(`Response larger than ${this.maxResponseBytes} bytes`);
99
+ }
100
+ }
101
+ return concatBytes(chunks);
102
+ }
103
+
104
+ close(): void {
105
+ // `fetch` keeps no connection the transport could release.
106
+ }
107
+ }
108
+
109
+ /** The message of a `fetch` failure, with its cause: Node.js reports `fetch failed` and the reason in the cause. */
110
+ function describe(error: unknown): string {
111
+ if (!(error instanceof Error)) return String(error);
112
+ const cause: unknown = error.cause;
113
+ return cause instanceof Error ? `${error.message}: ${cause.message}` : error.message;
114
+ }
@@ -0,0 +1,169 @@
1
+ import { Amount } from '../core/amount.js';
2
+ import { Transaction, type TransactionStatus } from '../core/transaction.js';
3
+ import { parseIsoDate } from '../util/dates.js';
4
+ import { FormatError } from '../util/errors.js';
5
+ import { parseXml, type XmlElement } from '../util/xml.js';
6
+ import { providedReference, SepaPurpose } from './sepa-purpose.js';
7
+
8
+ /** Parser for ISO 20022 camt.052 and camt.053 account reports. */
9
+ export const Camt = Object.freeze({
10
+ /**
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.
15
+ */
16
+ parseTransactions(xml: string, status?: TransactionStatus): Transaction[] {
17
+ const root = parseXml(xml.trim());
18
+ return root.descendantsNamed('Ntry').flatMap((entry) => new CamtEntry(entry, status).transactions());
19
+ },
20
+ });
21
+
22
+ /** Width of the purpose lines banks cut the remittance information into, as in MT940 and DTAUS. */
23
+ const cutLineLength = 35;
24
+
25
+ /**
26
+ * One booking (`Ntry`) of a report; a batch booking yields one transaction per transaction detail (`TxDtls`) when every
27
+ * detail states its amount.
28
+ *
29
+ * Nothing is guessed: what the entry lacks, and what the reader needs, is a `FormatError`. The one exception is the
30
+ * value date, which `Transaction.valueDate` requires: an entry with a booking date only takes that.
31
+ */
32
+ class CamtEntry {
33
+ private readonly amountElement: XmlElement;
34
+ private readonly currency: string;
35
+ private readonly debit: boolean;
36
+ private readonly reversal: boolean;
37
+ private readonly status: TransactionStatus;
38
+ private readonly bookingDate: Date | undefined;
39
+ private readonly valueDate: Date;
40
+
41
+ /** German banks send `transaction type+GVC+prima nota`, for example `NDDT+105+9310`. */
42
+ private readonly proprietaryCode: readonly string[];
43
+
44
+ constructor(
45
+ private readonly entry: XmlElement,
46
+ forcedStatus: TransactionStatus | undefined,
47
+ ) {
48
+ this.amountElement = entry.child('Amt') ?? fail('camt entry without amount');
49
+ this.currency = this.amountElement.getAttribute('Ccy') ?? fail('camt amount without currency');
50
+ this.debit = debitIndicator(entry.textAt(['CdtDbtInd'])) ?? fail('camt entry without credit/debit indicator');
51
+ this.reversal = ['true', '1'].includes(entry.textAt(['RvslInd']) ?? '');
52
+ this.status = forcedStatus ?? this.statusOfEntry();
53
+ this.bookingDate = dateOf(entry.child('BookgDt'));
54
+ this.valueDate = dateOf(entry.child('ValDt')) ?? this.bookingDate ?? fail('camt entry without date');
55
+ this.proprietaryCode = entry.textAt(['BkTxCd', 'Prtry', 'Cd'])?.split('+') ?? [];
56
+ }
57
+
58
+ private statusOfEntry(): TransactionStatus {
59
+ const code = this.entry.textAt(['Sts', 'Cd']) ?? this.entry.textAt(['Sts']);
60
+ return code === 'PDNG' || code === 'INFO' ? 'pending' : 'booked';
61
+ }
62
+
63
+ transactions(): Transaction[] {
64
+ const total = Amount.parseDecimal(this.amountElement.innerText, this.currency);
65
+ const details = this.entry.descendantsNamed('TxDtls');
66
+ if (details.length <= 1) return [this.transaction(details[0], total, this.debit)];
67
+ 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(
74
+ detail,
75
+ Amount.parseDecimal(amount.innerText, amount.getAttribute('Ccy') ?? this.currency),
76
+ // A detail may book in the other direction than the batch as a whole.
77
+ debitIndicator(detail.textAt(['CdtDbtInd'])) ?? this.debit,
78
+ );
79
+ });
80
+ }
81
+
82
+ /** A transaction of `amount`, booked as a debit if `debit`, with what `details` tell about it. */
83
+ private transaction(details: XmlElement | undefined, amount: Amount, debit: boolean): Transaction {
84
+ const parties = details?.child('RltdPties');
85
+ const agents = details?.child('RltdAgts');
86
+ // The related parties describe the original operation, which a reversal books the other way round (ISO 20022
87
+ // `RvslInd`: with `CRDT` the original operation was a debit, with `DBIT` a credit).
88
+ const originalDebit = debit !== this.reversal;
89
+ 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('');
95
+ const keywords = SepaPurpose.parseParts(purposeParts);
96
+ const endToEndId = providedReference(details?.textAt(['Refs', 'EndToEndId']));
97
+ const mandateId = details?.textAt(['Refs', 'MndtId']);
98
+ const creditorId = creditorIdOf(parties);
99
+ const purposeCode = details?.textAt(['Purp', 'Cd']);
100
+ 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);
105
+ if (purposeCode !== undefined) purposeCodes.set('PURP', purposeCode);
106
+ return new Transaction({
107
+ status: this.status,
108
+ bookingDate: this.bookingDate,
109
+ valueDate: this.valueDate,
110
+ amount: debit ? amount.negate() : amount,
111
+ isReversal: this.reversal,
112
+ bookingText: this.entry.textAt(['AddtlNtryInf']) ?? details?.textAt(['AddtlTxInf']),
113
+ purpose: keywords.get('SVWZ') ?? (purpose === '' ? details?.textAt(['AddtlTxInf']) : purpose),
114
+ counterpartyName: partyName(parties?.child(counterparty)),
115
+ counterpartyIban: parties?.child(`${counterparty}Acct`)?.textAt(['Id', 'IBAN']),
116
+ counterpartyAccountNumber: parties?.child(`${counterparty}Acct`)?.textAt(['Id', 'Othr', 'Id']),
117
+ counterpartyBic: bicOf(agents?.child(`${counterparty}Agt`)),
118
+ endToEndId,
119
+ mandateId,
120
+ creditorId,
121
+ customerReference: details?.textAt(['Refs', 'InstrId']) ?? details?.textAt(['Refs', 'PmtInfId']),
122
+ bankReference: details?.textAt(['Refs', 'AcctSvcrRef']) ?? this.entry.textAt(['AcctSvcrRef']),
123
+ transactionCode: this.proprietaryCode[1],
124
+ transactionTypeId: typeId,
125
+ primaNota: this.proprietaryCode[2],
126
+ ultimateCreditor: partyName(parties?.child('UltmtCdtr')),
127
+ ultimateDebtor: partyName(parties?.child('UltmtDbtr')),
128
+ purposeCodes,
129
+ });
130
+ }
131
+ }
132
+
133
+ function fail(message: string): never {
134
+ throw new FormatError(message);
135
+ }
136
+
137
+ /** `true` for `DBIT`, `false` for `CRDT`, `undefined` if `indicator` is missing; throws for any other value. */
138
+ function debitIndicator(indicator: string | undefined): boolean | undefined {
139
+ switch (indicator) {
140
+ case 'DBIT':
141
+ return true;
142
+ case 'CRDT':
143
+ return false;
144
+ case undefined:
145
+ return undefined;
146
+ default:
147
+ throw new FormatError('camt credit/debit indicator is neither CRDT nor DBIT', indicator);
148
+ }
149
+ }
150
+
151
+ function dateOf(e: XmlElement | undefined): Date | undefined {
152
+ return parseIsoDate(e?.textAt(['Dt']) ?? e?.textAt(['DtTm']));
153
+ }
154
+
155
+ function partyName(party: XmlElement | undefined): string | undefined {
156
+ return party?.textAt(['Nm']) ?? party?.textAt(['Pty', 'Nm']);
157
+ }
158
+
159
+ function bicOf(agent: XmlElement | undefined): string | undefined {
160
+ return agent?.textAt(['FinInstnId', 'BIC']) ?? agent?.textAt(['FinInstnId', 'BICFI']);
161
+ }
162
+
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;
169
+ }