@develit-services/bank 0.0.2

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.
@@ -0,0 +1,1214 @@
1
+ import { uuidv4, useResult, createInternalError, develitWorker, first, RPCResponse, action } from '@develit-io/backend-sdk';
2
+ import { C as CURRENCY_CODES, P as PAYMENT_TYPES, t as tables, F as FinbricksConnector, M as MockConnector, a as MockCobsConnector, E as ErsteConnector, g as getPaymentDirection } from '../shared/bank._EHVypLw.mjs';
3
+ import { WorkerEntrypoint } from 'cloudflare:workers';
4
+ import { drizzle } from 'drizzle-orm/d1';
5
+ import { z } from 'zod';
6
+ import { eq, and, inArray, desc, sql } from 'drizzle-orm';
7
+ import superjson from 'superjson';
8
+ import '../shared/bank.BeH-ZCJJ.mjs';
9
+ import 'drizzle-orm/sqlite-core';
10
+ import 'drizzle-orm/relations';
11
+ import 'date-fns';
12
+ import 'jose';
13
+
14
+ const sendPaymentInputSchema = z.object({
15
+ refId: z.string(),
16
+ amount: z.number().positive(),
17
+ paymentType: z.enum(PAYMENT_TYPES),
18
+ currency: z.enum(CURRENCY_CODES),
19
+ vs: z.string().nullable().optional(),
20
+ ss: z.string().nullable().optional(),
21
+ ks: z.string().nullable().optional(),
22
+ message: z.string().nullable().optional(),
23
+ creditorHolderName: z.string(),
24
+ creditorAccountNumberWithBankCode: z.string(),
25
+ creditorIban: z.string(),
26
+ debtorHolderName: z.string(),
27
+ debtorAccountNumberWithBankCode: z.string(),
28
+ debtorIban: z.string()
29
+ });
30
+
31
+ const saveErsteCodeInputSchema = z.object({
32
+ code: z.string()
33
+ });
34
+
35
+ const simulateDepositInputSchema = z.object({
36
+ amount: z.number(),
37
+ currency: z.enum(CURRENCY_CODES),
38
+ bankingVs: z.string().max(10),
39
+ message: z.string(),
40
+ creditorIban: z.string().max(34),
41
+ creditorHolderName: z.string(),
42
+ debtorIban: z.string().max(34),
43
+ debtorHolderName: z.string()
44
+ });
45
+
46
+ const setLastSyncAtInputSchema = z.object({
47
+ iban: z.string(),
48
+ lastSyncedAt: z.coerce.date()
49
+ });
50
+
51
+ const initiateConnectorInputSchema = z.object({
52
+ connectorKey: z.string()
53
+ });
54
+
55
+ const getBatchesInputSchema = z.object({
56
+ limit: z.number().positive().optional().default(100),
57
+ offset: z.number().min(0).optional().default(0)
58
+ });
59
+
60
+ const monthAgo = new Date(
61
+ Date.now() - 30 * 24 * 60 * 60 * 1e3
62
+ // 30 days ago
63
+ );
64
+ const stagingAccounts = [
65
+ {
66
+ connectorKey: "MOCK_COBS",
67
+ id: "staging-czk",
68
+ identification: {
69
+ number: "1234567890",
70
+ bankCode: "5051",
71
+ iban: "CZ0350510000000000000449",
72
+ holderName: "Devizov\xE1 Burza a.s."
73
+ },
74
+ fallbackLastSync: monthAgo,
75
+ currency: "CZK",
76
+ production: false
77
+ },
78
+ {
79
+ connectorKey: "MOCK_COBS",
80
+ id: "staging-eur",
81
+ identification: {
82
+ number: "2345678901",
83
+ bankCode: "5051",
84
+ iban: "CZ0750510000000000000668",
85
+ holderName: "Devizov\xE1 Burza a.s."
86
+ },
87
+ fallbackLastSync: monthAgo,
88
+ currency: "EUR",
89
+ production: false
90
+ },
91
+ {
92
+ connectorKey: "FIO",
93
+ id: "kleinpetr-test",
94
+ identification: {
95
+ number: "2502130437",
96
+ bankCode: "2010",
97
+ iban: "CZ8520100000002502130437",
98
+ holderName: "Petr Klein"
99
+ },
100
+ fallbackLastSync: monthAgo,
101
+ currency: "CZK",
102
+ production: true
103
+ },
104
+ {
105
+ connectorKey: "MOCK",
106
+ id: "id-test",
107
+ identification: {
108
+ number: "777777777",
109
+ bankCode: "0800",
110
+ iban: "PL0000000000002444430437",
111
+ holderName: "Test Test"
112
+ },
113
+ fallbackLastSync: monthAgo,
114
+ currency: "PLN",
115
+ production: false
116
+ }
117
+ // {
118
+ // connectorKey: 'MOCK_COBS',
119
+ // id: 'staging-usd',
120
+ // identification: {
121
+ // iban: 'CZ3456789012345678901234',
122
+ // accountNumberWithBankCode: '3456789012/3456',
123
+ // },
124
+ // currency: 'USD',
125
+ // bankCode: '0800',
126
+ // accountNumber: '3456789012',
127
+ // production: false,
128
+ // },
129
+ ];
130
+ const productionAccounts = [
131
+ // Banka CREDITAS (2250)
132
+ {
133
+ connectorKey: "CREDITAS",
134
+ id: "2250-CZK",
135
+ identification: {
136
+ number: "100602391",
137
+ bankCode: "2250",
138
+ iban: "CZ5122500000000100602391",
139
+ holderName: "Devizov\xE1 Burza a.s."
140
+ },
141
+ fallbackLastSync: monthAgo,
142
+ currency: "CZK",
143
+ production: true
144
+ },
145
+ {
146
+ connectorKey: "CREDITAS",
147
+ id: "2250-EUR",
148
+ identification: {
149
+ number: "100602404",
150
+ bankCode: "2250",
151
+ iban: "CZ8822500000000100602404",
152
+ holderName: "Devizov\xE1 Burza a.s."
153
+ },
154
+ fallbackLastSync: monthAgo,
155
+ currency: "EUR",
156
+ production: true
157
+ },
158
+ {
159
+ connectorKey: "CREDITAS",
160
+ id: "2250-USD",
161
+ identification: {
162
+ number: "100602412",
163
+ bankCode: "2250",
164
+ iban: "CZ6622500000000100602412",
165
+ holderName: "Devizov\xE1 Burza a.s."
166
+ },
167
+ fallbackLastSync: monthAgo,
168
+ currency: "USD",
169
+ production: true
170
+ },
171
+ {
172
+ connectorKey: "CREDITAS",
173
+ id: "2250-GBP",
174
+ identification: {
175
+ number: "100602420",
176
+ bankCode: "2250",
177
+ iban: "CZ4422500000000100602420",
178
+ holderName: "Devizov\xE1 Burza a.s."
179
+ },
180
+ fallbackLastSync: monthAgo,
181
+ currency: "GBP",
182
+ production: true
183
+ },
184
+ {
185
+ connectorKey: "CREDITAS",
186
+ id: "2250-CHF",
187
+ identification: {
188
+ number: "100602439",
189
+ bankCode: "2250",
190
+ iban: "CZ1622500000000100602439",
191
+ holderName: "Devizov\xE1 Burza a.s."
192
+ },
193
+ fallbackLastSync: monthAgo,
194
+ currency: "CHF",
195
+ production: true
196
+ },
197
+ {
198
+ connectorKey: "CREDITAS",
199
+ id: "2250-PLN",
200
+ identification: {
201
+ number: "100602447",
202
+ bankCode: "2250",
203
+ iban: "CZ9122500000000100602447",
204
+ holderName: "Devizov\xE1 Burza a.s."
205
+ },
206
+ fallbackLastSync: monthAgo,
207
+ currency: "PLN",
208
+ production: true
209
+ },
210
+ // Fio banka (2010)
211
+ {
212
+ connectorKey: "FINBRICKS",
213
+ id: "2010-CZK",
214
+ identification: {
215
+ number: "2500845869",
216
+ bankCode: "2010",
217
+ iban: "CZ0420100000002500845869",
218
+ holderName: "Devizov\xE1 Burza a.s."
219
+ },
220
+ fallbackLastSync: monthAgo,
221
+ currency: "CZK",
222
+ production: true
223
+ },
224
+ {
225
+ connectorKey: "FINBRICKS",
226
+ id: "2010-EUR",
227
+ identification: {
228
+ number: "2600884128",
229
+ bankCode: "2010",
230
+ iban: "CZ0820100000002600884128",
231
+ holderName: "Devizov\xE1 Burza a.s."
232
+ },
233
+ fallbackLastSync: monthAgo,
234
+ currency: "EUR",
235
+ production: true
236
+ },
237
+ {
238
+ connectorKey: "FINBRICKS",
239
+ id: "2010-USD",
240
+ identification: {
241
+ number: "2700884133",
242
+ bankCode: "2010",
243
+ iban: "CZ1420100000002700884133",
244
+ holderName: "Devizov\xE1 Burza a.s."
245
+ },
246
+ fallbackLastSync: monthAgo,
247
+ currency: "USD",
248
+ production: true
249
+ },
250
+ {
251
+ connectorKey: "FINBRICKS",
252
+ id: "2010-CHF",
253
+ identification: {
254
+ number: "2401098351",
255
+ bankCode: "2010",
256
+ iban: "CZ0920100000002401098351",
257
+ holderName: "Devizov\xE1 Burza a.s."
258
+ },
259
+ fallbackLastSync: monthAgo,
260
+ currency: "CHF",
261
+ production: true
262
+ },
263
+ {
264
+ connectorKey: "FINBRICKS",
265
+ id: "2010-GBP",
266
+ identification: {
267
+ number: "2400884131",
268
+ bankCode: "2010",
269
+ iban: "CZ3320100000002400884131",
270
+ holderName: "Devizov\xE1 Burza a.s."
271
+ },
272
+ fallbackLastSync: monthAgo,
273
+ currency: "GBP",
274
+ production: true
275
+ },
276
+ {
277
+ connectorKey: "FINBRICKS",
278
+ id: "2010-PLN",
279
+ identification: {
280
+ number: "2701098353",
281
+ bankCode: "2010",
282
+ iban: "CZ8720100000002701098353",
283
+ holderName: "Devizov\xE1 Burza a.s."
284
+ },
285
+ fallbackLastSync: monthAgo,
286
+ currency: "PLN",
287
+ production: true
288
+ },
289
+ {
290
+ connectorKey: "FINBRICKS",
291
+ id: "2010-CAD",
292
+ identification: {
293
+ number: "2701478102",
294
+ bankCode: "2010",
295
+ iban: "CZ5520100000002701478102",
296
+ holderName: "Devizov\xE1 Burza a.s."
297
+ },
298
+ fallbackLastSync: monthAgo,
299
+ currency: "CAD",
300
+ production: true
301
+ },
302
+ // Komerční banka (0100)
303
+ {
304
+ connectorKey: "FINBRICKS",
305
+ id: "0100-CZK",
306
+ identification: {
307
+ number: "115-1483630297",
308
+ bankCode: "0100",
309
+ iban: "CZ5901000001151483630297",
310
+ holderName: "Devizov\xE1 Burza a.s."
311
+ },
312
+ fallbackLastSync: monthAgo,
313
+ currency: "CZK",
314
+ production: true
315
+ },
316
+ {
317
+ connectorKey: "FINBRICKS",
318
+ id: "0100-EUR",
319
+ identification: {
320
+ number: "115-1484940297",
321
+ bankCode: "0100",
322
+ iban: "CZ4201000001151484940297",
323
+ holderName: "Devizov\xE1 Burza a.s."
324
+ },
325
+ fallbackLastSync: monthAgo,
326
+ currency: "EUR",
327
+ production: true
328
+ },
329
+ {
330
+ connectorKey: "FINBRICKS",
331
+ id: "0100-USD",
332
+ identification: {
333
+ number: "115-1483960237",
334
+ bankCode: "0100",
335
+ iban: "CZ6201000001151483960237",
336
+ holderName: "Devizov\xE1 Burza a.s."
337
+ },
338
+ fallbackLastSync: monthAgo,
339
+ currency: "USD",
340
+ production: true
341
+ },
342
+ {
343
+ connectorKey: "FINBRICKS",
344
+ id: "0100-PLN",
345
+ identification: {
346
+ number: "115-6674290267",
347
+ bankCode: "0100",
348
+ iban: "CZ3501000001156674290267",
349
+ holderName: "Devizov\xE1 Burza a.s."
350
+ },
351
+ fallbackLastSync: monthAgo,
352
+ currency: "PLN",
353
+ production: true
354
+ },
355
+ {
356
+ connectorKey: "FINBRICKS",
357
+ id: "0100-SEK",
358
+ identification: {
359
+ number: "115-9578110207",
360
+ bankCode: "0100",
361
+ iban: "CZ2401000001159578110207",
362
+ holderName: "Devizov\xE1 Burza a.s."
363
+ },
364
+ fallbackLastSync: monthAgo,
365
+ currency: "SEK",
366
+ production: true
367
+ },
368
+ // Česká spořitelna (0800)
369
+ {
370
+ connectorKey: "ERSTE",
371
+ id: "0800-CZK",
372
+ identification: {
373
+ number: "4413926379",
374
+ bankCode: "0800",
375
+ iban: "CZ7908000000004413926379",
376
+ holderName: "Devizov\xE1 Burza a.s."
377
+ },
378
+ fallbackLastSync: monthAgo,
379
+ currency: "CZK",
380
+ production: true
381
+ },
382
+ {
383
+ connectorKey: "ERSTE",
384
+ id: "0800-EUR",
385
+ identification: {
386
+ number: "1966646293",
387
+ bankCode: "0800",
388
+ iban: "CZ2308000000001966646293",
389
+ holderName: "Devizov\xE1 Burza a.s."
390
+ },
391
+ fallbackLastSync: monthAgo,
392
+ currency: "EUR",
393
+ production: true
394
+ },
395
+ {
396
+ connectorKey: "ERSTE",
397
+ id: "0800-USD",
398
+ identification: {
399
+ number: "1978982243",
400
+ bankCode: "0800",
401
+ iban: "CZ1908000000001978982243",
402
+ holderName: "Devizov\xE1 Burza a.s."
403
+ },
404
+ fallbackLastSync: monthAgo,
405
+ currency: "USD",
406
+ production: true
407
+ },
408
+ {
409
+ connectorKey: "ERSTE",
410
+ id: "0800-HUF",
411
+ identification: {
412
+ number: "13461252",
413
+ bankCode: "0800",
414
+ iban: "CZ1908000000000013461252",
415
+ holderName: "Devizov\xE1 Burza a.s."
416
+ },
417
+ fallbackLastSync: monthAgo,
418
+ currency: "HUF",
419
+ production: true
420
+ },
421
+ // MONETA Money Bank (0600)
422
+ {
423
+ connectorKey: "FINBRICKS",
424
+ id: "0600-CZK",
425
+ identification: {
426
+ number: "224252186",
427
+ bankCode: "0600",
428
+ iban: "CZ5906000000000224252186",
429
+ holderName: "Devizov\xE1 Burza a.s."
430
+ },
431
+ fallbackLastSync: monthAgo,
432
+ currency: "CZK",
433
+ production: true
434
+ },
435
+ {
436
+ connectorKey: "FINBRICKS",
437
+ id: "0600-EUR",
438
+ identification: {
439
+ number: "224252231",
440
+ bankCode: "0600",
441
+ iban: "CZ0806000000000224252231",
442
+ holderName: "Devizov\xE1 Burza a.s."
443
+ },
444
+ fallbackLastSync: monthAgo,
445
+ currency: "EUR",
446
+ production: true
447
+ },
448
+ {
449
+ connectorKey: "FINBRICKS",
450
+ id: "0600-USD",
451
+ identification: {
452
+ number: "224252290",
453
+ bankCode: "0600",
454
+ iban: "CZ6406000000000224252290",
455
+ holderName: "Devizov\xE1 Burza a.s."
456
+ },
457
+ fallbackLastSync: monthAgo,
458
+ currency: "USD",
459
+ production: true
460
+ },
461
+ // NEY spořitelní družstvo (2260)
462
+ {
463
+ connectorKey: "FINBRICKS",
464
+ id: "2260-CZK",
465
+ identification: {
466
+ number: "1702700014",
467
+ bankCode: "2260",
468
+ iban: "CZ7422600000001702700014",
469
+ holderName: "Devizov\xE1 Burza a.s."
470
+ },
471
+ fallbackLastSync: monthAgo,
472
+ currency: "CZK",
473
+ production: true
474
+ }
475
+ ];
476
+
477
+ const createPaymentCommand = (db, { payment }) => {
478
+ return {
479
+ command: db.insert(tables.payment).values(payment).returning()
480
+ };
481
+ };
482
+
483
+ const updatePaymentCommand = (db, { payment }) => {
484
+ return {
485
+ command: db.update(tables.payment).set(payment).where(eq(tables.payment.id, payment.id)).returning()
486
+ };
487
+ };
488
+
489
+ const upsertBatchCommand = (db, { batch }) => {
490
+ const id = batch.id || uuidv4();
491
+ const command = db.insert(tables.batch).values({
492
+ ...batch,
493
+ id
494
+ }).onConflictDoUpdate({
495
+ target: tables.batch.id,
496
+ set: {
497
+ ...batch
498
+ }
499
+ }).returning();
500
+ return {
501
+ id,
502
+ command
503
+ };
504
+ };
505
+
506
+ const getPaymentByRefIdQuery = async (db, { refId }) => {
507
+ const payment = await db.select().from(tables.payment).where(eq(tables.payment.refId, refId)).limit(1).get();
508
+ return payment;
509
+ };
510
+
511
+ const getOpenBatchByAccountIdQuery = async (db, { accountId }) => {
512
+ const batch = await db.select().from(tables.batch).where(
513
+ and(
514
+ eq(tables.batch.accountId, accountId),
515
+ eq(tables.batch.status, "OPEN")
516
+ )
517
+ ).limit(1).get();
518
+ return batch;
519
+ };
520
+
521
+ const getAllOpenBatchesQuery = async (db) => {
522
+ return await db.select().from(tables.batch).where(eq(tables.batch.status, "OPEN"));
523
+ };
524
+
525
+ const getPaymentsByBankRefIdsQuery = async (db, { ids }) => {
526
+ return await db.select().from(tables.payment).where(inArray(tables.payment.bankRefId, ids));
527
+ };
528
+
529
+ class CreditasConnector extends FinbricksConnector {
530
+ constructor(config) {
531
+ super("CREDITAS", config);
532
+ }
533
+ }
534
+
535
+ class FioConnector extends FinbricksConnector {
536
+ constructor(config) {
537
+ super("FIO", config);
538
+ }
539
+ }
540
+
541
+ const initiateConnector = (bank, env) => {
542
+ switch (bank) {
543
+ case "ERSTE":
544
+ return new ErsteConnector({
545
+ API_KEY: env.ERSTE_API_KEY,
546
+ CLIENT_ID: env.ERSTE_CLIENT_ID,
547
+ CLIENT_SECRET: env.ERSTE_CLIENT_SECRET,
548
+ REDIRECT_URI: env.ERSTE_REDIRECT_URI,
549
+ AUTH_URI: env.ERSTE_AUTH_URI,
550
+ PAYMENTS_URI: env.ERSTE_PAYMENTS_URI,
551
+ ACCOUNTS_URI: env.ERSTE_ACCOUNTS_URI
552
+ });
553
+ case "CREDITAS":
554
+ return new CreditasConnector({
555
+ BASE_URI: env.FINBRICKS_BASE_URI,
556
+ MERCHANT_ID: env.FINBRICKS_MERCHANT_ID,
557
+ PRIVATE_KEY_PEM: env.FINBRICKS_PRIVATE_KEY_PEM
558
+ });
559
+ case "MOCK_COBS":
560
+ return new MockCobsConnector({
561
+ BASE_URI: env.FINBRICKS_BASE_URI,
562
+ MERCHANT_ID: env.FINBRICKS_MERCHANT_ID,
563
+ PRIVATE_KEY_PEM: env.FINBRICKS_PRIVATE_KEY_PEM
564
+ });
565
+ case "FIO":
566
+ return new FioConnector({
567
+ BASE_URI: env.FINBRICKS_BASE_URI,
568
+ MERCHANT_ID: env.FINBRICKS_MERCHANT_ID,
569
+ PRIVATE_KEY_PEM: env.FINBRICKS_PRIVATE_KEY_PEM
570
+ });
571
+ default:
572
+ return new MockConnector();
573
+ }
574
+ };
575
+
576
+ const useSync = (kv) => {
577
+ const getLastSync = async ({
578
+ account
579
+ }) => {
580
+ const [rawSync, rawSyncError] = await useResult(
581
+ kv.get(`sync-log:${account.identification.iban}:${account.currency}`)
582
+ );
583
+ if (rawSyncError) throw createInternalError(rawSyncError);
584
+ if (!rawSync) return null;
585
+ return superjson.parse(rawSync);
586
+ };
587
+ const setLastSync = async ({
588
+ account,
589
+ log
590
+ }) => {
591
+ const [_, error] = await useResult(
592
+ kv.put(
593
+ `sync-log:${account.identification.iban}:${account.currency}`,
594
+ superjson.stringify(log)
595
+ )
596
+ );
597
+ if (error) throw error;
598
+ };
599
+ return {
600
+ getLastSync,
601
+ setLastSync
602
+ };
603
+ };
604
+
605
+ const seperateSupportedPayments = (mappedPayments, accounts) => {
606
+ const ibans = new Set(accounts.map((acc) => acc.identification.iban));
607
+ const currencies = new Set(accounts.map((acc) => acc.currency));
608
+ const [supportedPayments, unsupportedPayments] = mappedPayments.reduce(
609
+ ([valid, invalid], payment) => {
610
+ const isValid = ibans.has(payment.debtorIban ?? "") && currencies.has(payment.currency);
611
+ if (isValid) {
612
+ valid.push(payment);
613
+ } else {
614
+ invalid.push(payment);
615
+ }
616
+ return [valid, invalid];
617
+ },
618
+ [[], []]
619
+ );
620
+ return {
621
+ supportedPayments,
622
+ unsupportedPayments
623
+ };
624
+ };
625
+
626
+ var __defProp = Object.defineProperty;
627
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
628
+ var __decorateClass = (decorators, target, key, kind) => {
629
+ var result = __getOwnPropDesc(target, key) ;
630
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
631
+ if (decorator = decorators[i])
632
+ result = (decorator(target, key, result) ) || result;
633
+ if (result) __defProp(target, key, result);
634
+ return result;
635
+ };
636
+ class BankServiceBase extends develitWorker(WorkerEntrypoint) {
637
+ constructor(ctx, env) {
638
+ super(ctx, env);
639
+ this.name = "bank-service";
640
+ this.db = drizzle(this.env.BANK_D1, { schema: tables });
641
+ }
642
+ get accounts() {
643
+ return this.env.ENVIRONMENT === "production" ? productionAccounts : stagingAccounts;
644
+ }
645
+ accountFetchInterval(connectorKey) {
646
+ switch (connectorKey) {
647
+ case "ERSTE":
648
+ return ErsteConnector.FETCH_INTERVAL;
649
+ case "FINBRICKS":
650
+ return FinbricksConnector.FETCH_INTERVAL;
651
+ case "MOCK":
652
+ return MockConnector.FETCH_INTERVAL;
653
+ default:
654
+ return 0;
655
+ }
656
+ }
657
+ async syncAccounts() {
658
+ return this.handleAction(null, {}, async () => {
659
+ const accountsToSync = [];
660
+ const { getLastSync } = useSync(this.env.BANK_KV);
661
+ for (const account of this.accounts.filter(
662
+ (acc) => !!!acc.connectorKey.includes("MOCK")
663
+ )) {
664
+ let lastSync = await getLastSync({ account });
665
+ if (!lastSync) {
666
+ await this.setLastSyncAt({
667
+ iban: account.identification.iban,
668
+ lastSyncedAt: account.fallbackLastSync
669
+ });
670
+ lastSync = await getLastSync({ account });
671
+ if (!lastSync) continue;
672
+ }
673
+ const accountFetchInterval = this.accountFetchInterval(
674
+ account.connectorKey
675
+ );
676
+ const now = Date.now();
677
+ const lastSyncTime = lastSync.lastSyncedAt.getTime();
678
+ const intervalMs = accountFetchInterval * 1e3;
679
+ const shouldFetch = now - lastSyncTime >= intervalMs;
680
+ if (!shouldFetch) continue;
681
+ accountsToSync.push({
682
+ ...account,
683
+ lastSyncAt: lastSync
684
+ });
685
+ }
686
+ console.log("ACCS TO SYNC", accountsToSync.length);
687
+ if (accountsToSync.length <= 0) return;
688
+ await this.saveOrUpdatePayments(accountsToSync);
689
+ });
690
+ }
691
+ async scheduled(controller) {
692
+ if (controller.cron === this.env.CRON_BATCHES_PROCESSING) {
693
+ console.log("Scheduled CRON batches processing");
694
+ const openBatches = await getAllOpenBatchesQuery(this.db);
695
+ if (!openBatches.length) return;
696
+ for (const batch of openBatches) {
697
+ const connectorKey = this.accounts.find(
698
+ (acc) => acc.id === batch.accountId
699
+ )?.connectorKey;
700
+ if (!connectorKey) {
701
+ this.logError({
702
+ message: "Failed to find connector key",
703
+ batch,
704
+ connectorKey
705
+ });
706
+ continue;
707
+ }
708
+ await this.processBatch({
709
+ batch,
710
+ connectorKey
711
+ });
712
+ }
713
+ }
714
+ if (controller.cron === this.env.CRON_PAYMENTS_PROCESSING) {
715
+ console.log("Scheduled CRON payments processing");
716
+ await this.syncAccounts();
717
+ }
718
+ }
719
+ // @action('initiate-bank-connector')
720
+ async initiateBankConnector(input) {
721
+ return this.handleAction(
722
+ { data: input, schema: initiateConnectorInputSchema },
723
+ { successMessage: "Bank connector initiated successfully" },
724
+ async ({ connectorKey }) => {
725
+ this.bankConnector = initiateConnector(
726
+ connectorKey,
727
+ this.env
728
+ );
729
+ await this.env.BANK_KV.put(
730
+ `${connectorKey}:token`,
731
+ "d32a83cf-d352-41f3-96d7-4d2961336ddc"
732
+ //hard-coded finbricks clientId (valid til 03/26)
733
+ );
734
+ const token = await this.env.BANK_KV.get(`${connectorKey}:token`);
735
+ if (!token) throw createInternalError("Provider not authorized");
736
+ await this.bankConnector.authenticate({
737
+ token
738
+ });
739
+ }
740
+ );
741
+ }
742
+ async addPaymentsToBatch({
743
+ paymentsToBatch
744
+ }) {
745
+ return this.handleAction(null, {}, async () => {
746
+ this.logInput({ paymentsToBatch });
747
+ const mappedPayments = paymentsToBatch.map(
748
+ (payment) => ({
749
+ id: uuidv4(),
750
+ refId: payment.refId,
751
+ amount: payment.amount,
752
+ direction: "OUTGOING",
753
+ paymentType: payment.paymentType,
754
+ currency: payment.currency,
755
+ status: "CREATED",
756
+ initiatedAt: /* @__PURE__ */ new Date(),
757
+ processedAt: null,
758
+ vs: payment.vs || null,
759
+ ss: payment.ss || null,
760
+ ks: payment.ks || null,
761
+ message: payment.message || null,
762
+ creditorHolderName: payment.creditorHolderName,
763
+ creditorAccountNumberWithBankCode: payment.creditorAccountNumberWithBankCode,
764
+ creditorIban: payment.creditorIban,
765
+ debtorHolderName: payment.debtorHolderName,
766
+ debtorAccountNumberWithBankCode: payment.debtorAccountNumberWithBankCode,
767
+ debtorIban: payment.debtorIban
768
+ })
769
+ );
770
+ const { supportedPayments, unsupportedPayments } = seperateSupportedPayments(mappedPayments, this.accounts);
771
+ if (unsupportedPayments.length > 0) {
772
+ this.logError({ unsupportedPayments });
773
+ await this.pushToQueue(
774
+ this.env.QUEUE_BUS_QUEUE,
775
+ unsupportedPayments.map((unsupported) => ({
776
+ eventType: "BANK_PAYMENT",
777
+ bankPayment: {
778
+ ...unsupported,
779
+ status: "FAILED",
780
+ statusReason: "UNSUPPORTED_ACCOUNT",
781
+ paymentType: unsupported.paymentType,
782
+ creditorAccountNumberWithBankCode: unsupported.creditorAccountNumberWithBankCode,
783
+ creditorIban: unsupported.creditorIban,
784
+ creditorHolderName: unsupported.creditorHolderName,
785
+ debtorAccountNumberWithBankCode: unsupported.debtorAccountNumberWithBankCode,
786
+ debtorIban: unsupported.debtorIban,
787
+ debtorHolderName: unsupported.debtorHolderName
788
+ },
789
+ metadata: {
790
+ correlationId: uuidv4(),
791
+ timestamp: (/* @__PURE__ */ new Date()).toDateString()
792
+ }
793
+ }))
794
+ );
795
+ }
796
+ for (const acc of this.accounts) {
797
+ const newPayments = supportedPayments.filter(
798
+ (payment) => payment.debtorIban === acc.identification.iban && payment.currency === acc.currency
799
+ );
800
+ if (newPayments.length === 0) {
801
+ continue;
802
+ }
803
+ this.log({
804
+ message: `\u{1F4B0} Processing ${newPayments.length} payments for account ${acc.id} (${acc.identification.iban}, ${acc.currency})`
805
+ });
806
+ const existingBatch = await getOpenBatchByAccountIdQuery(this.db, {
807
+ accountId: acc.id
808
+ });
809
+ if (existingBatch) {
810
+ this.log({
811
+ message: `\u{1F504} Found existing OPEN batch for account ${acc.id}, merging ${existingBatch.payments.length} existing + ${newPayments.length} new payments`
812
+ });
813
+ } else {
814
+ this.log({
815
+ message: `\u2728 Creating new batch for account ${acc.id} with ${newPayments.length} payments`
816
+ });
817
+ }
818
+ const { command } = upsertBatchCommand(this.db, {
819
+ batch: existingBatch ? {
820
+ ...existingBatch,
821
+ payments: [...existingBatch.payments, ...newPayments]
822
+ } : {
823
+ id: uuidv4(),
824
+ authorizationUrls: [],
825
+ accountId: acc.id,
826
+ payments: newPayments,
827
+ status: "OPEN"
828
+ }
829
+ });
830
+ const upsertedBatch = await command.execute();
831
+ this.log({
832
+ message: `\u2705 Batch upserted successfully for account ${acc.id}`
833
+ });
834
+ return upsertedBatch;
835
+ }
836
+ });
837
+ }
838
+ async processBatch({
839
+ batch,
840
+ connectorKey
841
+ }) {
842
+ return this.handleAction(null, {}, async () => {
843
+ this.logInput({ batch, connectorKey });
844
+ await upsertBatchCommand(this.db, {
845
+ batch: {
846
+ ...batch,
847
+ status: "PROCESSING"
848
+ }
849
+ }).command.execute();
850
+ await this.initiateBankConnector({ connectorKey });
851
+ if (!this.bankConnector) {
852
+ throw createInternalError(null, {
853
+ message: `\u274C Failed to initialize ${connectorKey} bank connector`
854
+ });
855
+ }
856
+ this.log({
857
+ message: `\u2705 Bank connector initialized successfully for account ${connectorKey}`
858
+ });
859
+ const preparedBatch = {
860
+ retriedPayments: [],
861
+ newlyPreparedPayments: [],
862
+ failedPayments: []
863
+ };
864
+ for (const payment of batch.payments) {
865
+ const existingPayment = await getPaymentByRefIdQuery(this.db, {
866
+ refId: payment.refId
867
+ });
868
+ if (existingPayment) {
869
+ preparedBatch.retriedPayments.push({
870
+ ...existingPayment,
871
+ status: "PREPARED"
872
+ });
873
+ this.log({
874
+ message: `\u2705 Payment ${existingPayment.id} already exists`
875
+ });
876
+ continue;
877
+ }
878
+ const newlyPreparedPayment = await this.bankConnector.preparePayment(payment);
879
+ if (!newlyPreparedPayment) {
880
+ preparedBatch.failedPayments.push({ ...payment, status: "FAILED" });
881
+ continue;
882
+ }
883
+ preparedBatch.newlyPreparedPayments.push(newlyPreparedPayment);
884
+ }
885
+ await Promise.all([
886
+ [
887
+ ...preparedBatch.failedPayments,
888
+ ...preparedBatch.newlyPreparedPayments
889
+ ].map(
890
+ (p) => createPaymentCommand(this.db, { payment: p }).command.execute()
891
+ )
892
+ ]);
893
+ const { authorizationUrls, payments, metadata } = await this.bankConnector.initiateBatchFromPayments({
894
+ payments: [
895
+ ...preparedBatch.newlyPreparedPayments,
896
+ ...preparedBatch.retriedPayments
897
+ ]
898
+ });
899
+ if (!authorizationUrls) {
900
+ this.logError({ message: "Failed to retrieve signing URI" });
901
+ return;
902
+ }
903
+ const { command: upsertBatch } = upsertBatchCommand(this.db, {
904
+ batch: {
905
+ ...batch,
906
+ payments,
907
+ metadata,
908
+ status: "READY_TO_SIGN",
909
+ authorizationUrls,
910
+ batchPaymentInitiatedAt: /* @__PURE__ */ new Date()
911
+ }
912
+ });
913
+ const updatePayments = payments.map(
914
+ (payment) => updatePaymentCommand(this.db, {
915
+ payment: { ...payment, status: "INITIALIZED" }
916
+ }).command
917
+ );
918
+ await this.db.batch([upsertBatch, ...updatePayments]);
919
+ this.log({
920
+ message: "Authorization Urls for batch to create",
921
+ authorizationUrl: authorizationUrls[0]
922
+ });
923
+ await this.pushToQueue(this.env.NOTIFICATIONS_QUEUE, {
924
+ type: "email",
925
+ payload: {
926
+ email: {
927
+ to: ["petr@develit.io"],
928
+ subject: "Payment Authorization",
929
+ text: authorizationUrls[0] || "No Authorization Url"
930
+ }
931
+ },
932
+ metadata: {
933
+ initiator: {
934
+ service: this.name
935
+ }
936
+ }
937
+ });
938
+ this.logOutput({ message: "Batch successfully processed" });
939
+ });
940
+ }
941
+ async queue(b) {
942
+ await this.handleAction(
943
+ null,
944
+ { successMessage: "Queue handler executed successfully" },
945
+ async () => {
946
+ this.logQueuePull(b);
947
+ const queueHandlerResponse = await this.addPaymentsToBatch({
948
+ paymentsToBatch: b.messages.map(({ body }) => body)
949
+ });
950
+ this.logOutput(queueHandlerResponse);
951
+ }
952
+ );
953
+ }
954
+ async getErsteCodeURI() {
955
+ return this.handleAction(
956
+ null,
957
+ { successMessage: "Erste URI code obtained." },
958
+ async () => {
959
+ this.bankConnector = initiateConnector("ERSTE", this.env);
960
+ const uri = this.bankConnector instanceof ErsteConnector ? this.bankConnector.adminCodeCreationURI : "nothing";
961
+ return { uri };
962
+ }
963
+ );
964
+ }
965
+ async saveErsteCode(input) {
966
+ return this.handleAction(
967
+ { data: input, schema: saveErsteCodeInputSchema },
968
+ { successMessage: "Erste code saved." },
969
+ async ({ code }) => {
970
+ this.bankConnector = initiateConnector("ERSTE", this.env);
971
+ const token = await this.bankConnector.authenticate({
972
+ token: code
973
+ });
974
+ await this.env.BANK_KV.put("ERSTE:refreshToken", token);
975
+ }
976
+ );
977
+ }
978
+ async saveOrUpdatePayments(accounts) {
979
+ const { setLastSync } = useSync(this.env.BANK_KV);
980
+ return this.handleAction(null, {}, async () => {
981
+ const allFetchedPayments = [];
982
+ const connectorKeys = new Set(
983
+ accounts.map(({ connectorKey }) => connectorKey)
984
+ );
985
+ for (const connectorKey of connectorKeys) {
986
+ await this.initiateBankConnector({ connectorKey });
987
+ for (const account of accounts.filter(
988
+ (acc) => acc.connectorKey === connectorKey
989
+ )) {
990
+ const { lastSyncAt } = account;
991
+ const payments = await this.bankConnector.getAllAccountPayments({
992
+ db: this.db,
993
+ env: this.env.ENVIRONMENT,
994
+ account,
995
+ lastSync: lastSyncAt
996
+ });
997
+ if (!payments || payments.length === 0) continue;
998
+ this.log(payments);
999
+ payments.forEach((payment) => {
1000
+ payment.direction = getPaymentDirection(
1001
+ payment,
1002
+ account.identification.iban
1003
+ );
1004
+ });
1005
+ allFetchedPayments.push(...payments);
1006
+ }
1007
+ }
1008
+ if (allFetchedPayments.length < 1) return;
1009
+ const bankRefIds = allFetchedPayments.map((payment) => payment.bankRefId).filter(Boolean);
1010
+ const alreadyExistingPayments = await getPaymentsByBankRefIdsQuery(
1011
+ this.db,
1012
+ { ids: bankRefIds }
1013
+ );
1014
+ const paymentsCommands = allFetchedPayments.map((payment) => {
1015
+ const isAlreadyExisting = alreadyExistingPayments.some(
1016
+ (existingPayment) => existingPayment.bankRefId === payment.bankRefId
1017
+ );
1018
+ if (isAlreadyExisting)
1019
+ return updatePaymentCommand(this.db, { payment }).command;
1020
+ return createPaymentCommand(this.db, { payment }).command;
1021
+ });
1022
+ await this.db.batch([paymentsCommands[0], ...paymentsCommands.slice(1)]);
1023
+ console.log("FETCHEDPAYMANETS TO SYNC", allFetchedPayments.length);
1024
+ for (const account of accounts) {
1025
+ const paymentsForAccount = allFetchedPayments.filter(
1026
+ (payment) => (payment.direction === "OUTGOING" ? payment.debtorIban : payment.creditorIban) === account.identification.iban
1027
+ );
1028
+ let lastSyncPayment;
1029
+ lastSyncPayment = paymentsForAccount.filter(({ status }) => status !== "COMPLETED").sort(
1030
+ (a, b) => (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0)
1031
+ )[0];
1032
+ if (!lastSyncPayment) {
1033
+ lastSyncPayment = paymentsForAccount.sort(
1034
+ (a, b) => (b.createdAt?.getTime() || 0) - (a.createdAt?.getTime() || 0)
1035
+ )[0];
1036
+ }
1037
+ if (lastSyncPayment.createdAt) {
1038
+ await setLastSync({
1039
+ account,
1040
+ log: {
1041
+ lastSyncedAt: lastSyncPayment.createdAt
1042
+ }
1043
+ });
1044
+ }
1045
+ }
1046
+ await this.pushToQueue(
1047
+ this.env.QUEUE_BUS_QUEUE,
1048
+ allFetchedPayments.map((payment) => ({
1049
+ eventType: "BANK_PAYMENT",
1050
+ bankPayment: payment,
1051
+ metadata: {
1052
+ correlationId: uuidv4(),
1053
+ timestamp: (/* @__PURE__ */ new Date()).toDateString()
1054
+ }
1055
+ }))
1056
+ );
1057
+ });
1058
+ }
1059
+ async simulateDeposit(input) {
1060
+ return this.handleAction(
1061
+ { data: input, schema: simulateDepositInputSchema },
1062
+ { successMessage: "Deposit saved." },
1063
+ async ({
1064
+ amount,
1065
+ currency,
1066
+ bankingVs,
1067
+ message,
1068
+ creditorIban,
1069
+ creditorHolderName,
1070
+ debtorIban,
1071
+ debtorHolderName
1072
+ }) => {
1073
+ const payment = {
1074
+ id: uuidv4(),
1075
+ amount,
1076
+ direction: "INCOMING",
1077
+ paymentType: "DOMESTIC",
1078
+ currency,
1079
+ status: "COMPLETED",
1080
+ batchId: uuidv4(),
1081
+ initiatedAt: /* @__PURE__ */ new Date(),
1082
+ processedAt: /* @__PURE__ */ new Date(),
1083
+ vs: bankingVs,
1084
+ message,
1085
+ creditorIban,
1086
+ creditorHolderName,
1087
+ debtorIban,
1088
+ debtorHolderName
1089
+ };
1090
+ const { command } = createPaymentCommand(this.db, { payment });
1091
+ const createdPayment = await command.execute().then(first);
1092
+ this.logQueuePush({ payment, isPaymentExecuted: true });
1093
+ await this.pushToQueue(this.env.QUEUE_BUS_QUEUE, {
1094
+ eventType: "BANK_PAYMENT",
1095
+ bankPayment: createdPayment,
1096
+ metadata: {
1097
+ correlationId: createdPayment.id,
1098
+ timestamp: (/* @__PURE__ */ new Date()).toDateString()
1099
+ }
1100
+ });
1101
+ return createdPayment;
1102
+ }
1103
+ );
1104
+ }
1105
+ async setLastSyncAt(input) {
1106
+ return this.handleAction(
1107
+ { data: input, schema: setLastSyncAtInputSchema },
1108
+ { successMessage: "Last sync set." },
1109
+ async ({ iban, lastSyncedAt }) => {
1110
+ const { setLastSync } = useSync(this.env.BANK_KV);
1111
+ const configAccount = this.accounts.find(
1112
+ (acc) => acc.identification.iban === iban
1113
+ );
1114
+ if (!configAccount) {
1115
+ throw createInternalError(null, {
1116
+ status: 404,
1117
+ message: "Account for this organization does not exist."
1118
+ });
1119
+ }
1120
+ await setLastSync({
1121
+ log: {
1122
+ lastSyncedAt
1123
+ },
1124
+ account: configAccount
1125
+ });
1126
+ return {};
1127
+ }
1128
+ );
1129
+ }
1130
+ async sendPayment(input) {
1131
+ return this.handleAction(
1132
+ { data: input, schema: sendPaymentInputSchema },
1133
+ { successMessage: "Payment queued successfully" },
1134
+ async (data) => {
1135
+ await this.pushToQueue(
1136
+ this.env.PAYMENTS_READY_TO_BATCH_QUEUE,
1137
+ data
1138
+ );
1139
+ }
1140
+ );
1141
+ }
1142
+ async getBankAccounts() {
1143
+ return RPCResponse.ok("Bank accounts retrieved successfully", {
1144
+ data: this.accounts
1145
+ });
1146
+ }
1147
+ async getBatches(input) {
1148
+ return this.handleAction(
1149
+ { data: input, schema: getBatchesInputSchema },
1150
+ { successMessage: "Batches retrieved successfully" },
1151
+ async ({ limit = 100, offset = 0 }) => {
1152
+ const [batchesResult, countResult] = await Promise.all([
1153
+ this.db.select().from(tables.batch).limit(limit).offset(offset).orderBy(desc(tables.batch.createdAt)),
1154
+ this.db.select({ count: sql`count(*)`.as("count") }).from(tables.batch)
1155
+ ]);
1156
+ const totalCount = Number(countResult[0]?.count) || 0;
1157
+ return {
1158
+ batches: batchesResult,
1159
+ totalCount
1160
+ };
1161
+ }
1162
+ );
1163
+ }
1164
+ }
1165
+ __decorateClass([
1166
+ action("synchronize-accounts")
1167
+ ], BankServiceBase.prototype, "syncAccounts");
1168
+ __decorateClass([
1169
+ action("scheduled")
1170
+ ], BankServiceBase.prototype, "scheduled");
1171
+ __decorateClass([
1172
+ action("add-payments-to-batch")
1173
+ ], BankServiceBase.prototype, "addPaymentsToBatch");
1174
+ __decorateClass([
1175
+ action("process-batch")
1176
+ ], BankServiceBase.prototype, "processBatch");
1177
+ __decorateClass([
1178
+ action("queue-handler")
1179
+ ], BankServiceBase.prototype, "queue");
1180
+ __decorateClass([
1181
+ action("get-erste-code-uri")
1182
+ ], BankServiceBase.prototype, "getErsteCodeURI");
1183
+ __decorateClass([
1184
+ action("save-erste-code")
1185
+ ], BankServiceBase.prototype, "saveErsteCode");
1186
+ __decorateClass([
1187
+ action("save-or-update-payments")
1188
+ ], BankServiceBase.prototype, "saveOrUpdatePayments");
1189
+ __decorateClass([
1190
+ action("simulate-deposit")
1191
+ ], BankServiceBase.prototype, "simulateDeposit");
1192
+ __decorateClass([
1193
+ action("set-last-sync-at")
1194
+ ], BankServiceBase.prototype, "setLastSyncAt");
1195
+ __decorateClass([
1196
+ action("send-payment")
1197
+ ], BankServiceBase.prototype, "sendPayment");
1198
+ __decorateClass([
1199
+ action("get-bank-accounts")
1200
+ ], BankServiceBase.prototype, "getBankAccounts");
1201
+ __decorateClass([
1202
+ action("getBatches")
1203
+ ], BankServiceBase.prototype, "getBatches");
1204
+ function defineBankService() {
1205
+ return class BankService extends BankServiceBase {
1206
+ constructor(ctx, env) {
1207
+ super(ctx, env);
1208
+ }
1209
+ };
1210
+ }
1211
+
1212
+ const BankService = defineBankService();
1213
+
1214
+ export { BankService as default, defineBankService };