@develit-services/bank 0.8.9 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -275
- package/dist/database/schema.cjs +1 -1
- package/dist/database/schema.d.cts +2 -2
- package/dist/database/schema.d.mts +2 -2
- package/dist/database/schema.d.ts +2 -2
- package/dist/database/schema.mjs +1 -1
- package/dist/export/worker.cjs +404 -548
- package/dist/export/worker.d.cts +848 -84
- package/dist/export/worker.d.mts +848 -84
- package/dist/export/worker.d.ts +848 -84
- package/dist/export/worker.mjs +404 -548
- package/dist/export/workflows.cjs +16 -16
- package/dist/export/workflows.mjs +16 -16
- package/dist/export/wrangler.cjs +0 -12
- package/dist/export/wrangler.d.cts +1 -8
- package/dist/export/wrangler.d.mts +1 -8
- package/dist/export/wrangler.d.ts +1 -8
- package/dist/export/wrangler.mjs +0 -12
- package/dist/shared/{bank.JVlyPAAb.cjs → bank.BBXoZ5QU.cjs} +26 -13
- package/dist/shared/{bank.CHQ3VSEh.d.ts → bank.BCop1cDT.d.mts} +4 -15
- package/dist/shared/{bank.Bg3Pdwm4.cjs → bank.BsIiXsFH.cjs} +5 -13
- package/dist/shared/{bank.BoZtXQpG.mjs → bank.CR0UlyRi.mjs} +1 -1
- package/dist/shared/{bank.CtnsGHM8.cjs → bank.CUvVxlHy.cjs} +126 -152
- package/dist/shared/{bank.DJnDSYqE.cjs → bank.CVi6R7fr.cjs} +1 -1
- package/dist/shared/{bank.C6jjS1Pl.mjs → bank.CXBeULUL.mjs} +25 -14
- package/dist/shared/{bank.DT6bg8k5.cjs → bank.Cev1E9sk.cjs} +2 -2
- package/dist/shared/{bank.pgyk4j94.d.cts → bank.Cj2Goq7s.d.cts} +104 -176
- package/dist/shared/{bank.pgyk4j94.d.mts → bank.Cj2Goq7s.d.mts} +104 -176
- package/dist/shared/{bank.pgyk4j94.d.ts → bank.Cj2Goq7s.d.ts} +104 -176
- package/dist/shared/{bank.vPWD7Ce4.d.cts → bank.CjTfEd1Q.d.cts} +4 -15
- package/dist/shared/{bank.BtszLapg.mjs → bank.D-O_gmmZ.mjs} +127 -152
- package/dist/shared/{bank.BP_3WMIF.d.cts → bank.DMjtitKo.d.cts} +0 -1
- package/dist/shared/{bank.BP_3WMIF.d.mts → bank.DMjtitKo.d.mts} +0 -1
- package/dist/shared/{bank.BP_3WMIF.d.ts → bank.DMjtitKo.d.ts} +0 -1
- package/dist/shared/{bank.CwuH4spB.d.mts → bank.OlDt7dpb.d.ts} +4 -15
- package/dist/shared/{bank.CbAwwIhZ.mjs → bank.vz1uqEYa.mjs} +5 -11
- package/dist/shared/{bank.B5bZRvgq.mjs → bank.xB9eTN77.mjs} +2 -2
- package/dist/types.cjs +6 -6
- package/dist/types.d.cts +26 -45
- package/dist/types.d.mts +26 -45
- package/dist/types.d.ts +26 -45
- package/dist/types.mjs +3 -3
- package/package.json +1 -1
package/dist/export/worker.cjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const backendSdk = require('@develit-io/backend-sdk');
|
|
4
|
-
const drizzle = require('../shared/bank.
|
|
4
|
+
const drizzle = require('../shared/bank.Cev1E9sk.cjs');
|
|
5
|
+
const drizzleOrm = require('drizzle-orm');
|
|
5
6
|
const cloudflare_workers = require('cloudflare:workers');
|
|
6
7
|
const d1 = require('drizzle-orm/d1');
|
|
7
|
-
const mock_connector = require('../shared/bank.
|
|
8
|
+
const mock_connector = require('../shared/bank.CUvVxlHy.cjs');
|
|
8
9
|
require('jose');
|
|
9
10
|
const zod = require('zod');
|
|
10
|
-
const paymentRequest_schema = require('../shared/bank.
|
|
11
|
+
const paymentRequest_schema = require('../shared/bank.BBXoZ5QU.cjs');
|
|
11
12
|
const generalCodes = require('@develit-io/general-codes');
|
|
12
13
|
require('date-fns');
|
|
13
|
-
const drizzleOrm = require('drizzle-orm');
|
|
14
14
|
require('node:crypto');
|
|
15
|
-
require('../shared/bank.
|
|
15
|
+
require('../shared/bank.CVi6R7fr.cjs');
|
|
16
16
|
require('drizzle-orm/relations');
|
|
17
17
|
require('drizzle-orm/sqlite-core');
|
|
18
18
|
require('drizzle-zod');
|
|
@@ -107,8 +107,8 @@ const createPaymentRequestCommand = (db, { paymentRequest }) => {
|
|
|
107
107
|
const getAccountBatchCountsQuery = async (db, { accountId }) => {
|
|
108
108
|
const result = await db.select({
|
|
109
109
|
totalCount: drizzleOrm.sql`COUNT(*)`.as("totalCount"),
|
|
110
|
-
|
|
111
|
-
"
|
|
110
|
+
processingCount: drizzleOrm.sql`SUM(CASE WHEN ${drizzle.tables.batch.status} = 'PROCESSING' THEN 1 ELSE 0 END)`.as(
|
|
111
|
+
"processingCount"
|
|
112
112
|
),
|
|
113
113
|
readyToSignCount: drizzleOrm.sql`SUM(CASE WHEN ${drizzle.tables.batch.status} = 'READY_TO_SIGN' THEN 1 ELSE 0 END)`.as(
|
|
114
114
|
"readyToSignCount"
|
|
@@ -119,7 +119,7 @@ const getAccountBatchCountsQuery = async (db, { accountId }) => {
|
|
|
119
119
|
}).from(drizzle.tables.batch).where(drizzleOrm.eq(drizzle.tables.batch.accountId, accountId)).then(backendSdk.first);
|
|
120
120
|
return result || {
|
|
121
121
|
totalCount: 0,
|
|
122
|
-
|
|
122
|
+
processingCount: 0,
|
|
123
123
|
readyToSignCount: 0,
|
|
124
124
|
failedCount: 0
|
|
125
125
|
};
|
|
@@ -162,10 +162,6 @@ const getAllAccountsQuery = async (db, filters) => {
|
|
|
162
162
|
};
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
-
const getAllPendingBatchesQuery = (db) => {
|
|
166
|
-
return db.select().from(drizzle.tables.batch).where(drizzleOrm.inArray(drizzle.tables.batch.status, ["READY_TO_SIGN", "SIGNED"]));
|
|
167
|
-
};
|
|
168
|
-
|
|
169
165
|
const buildMultiFilterConditions = (column, value) => {
|
|
170
166
|
if (value === void 0) return void 0;
|
|
171
167
|
if (Array.isArray(value)) {
|
|
@@ -265,6 +261,7 @@ const getBatchesQuery = async (db, {
|
|
|
265
261
|
filterBatchStatus
|
|
266
262
|
}) => {
|
|
267
263
|
const whereConditions = drizzleOrm.and(
|
|
264
|
+
drizzleOrm.isNull(drizzle.tables.batch.deletedAt),
|
|
268
265
|
buildMultiFilterConditions(drizzle.tables.batch.accountId, filterBatchAccountId),
|
|
269
266
|
buildMultiFilterConditions(drizzle.tables.batch.status, filterBatchStatus)
|
|
270
267
|
);
|
|
@@ -274,7 +271,12 @@ const getBatchesQuery = async (db, {
|
|
|
274
271
|
}).from(drizzle.tables.batch).where(whereConditions);
|
|
275
272
|
const batches = await db.select().from(drizzle.tables.batch).where(whereConditions).limit(limit).offset((page - 1) * limit).orderBy(sort.direction === "asc" ? drizzleOrm.asc(sortColumn) : drizzleOrm.desc(sortColumn));
|
|
276
273
|
const batchIds = batches.map((b) => b.id);
|
|
277
|
-
const paymentRequests = batchIds.length > 0 ? await db.select().from(drizzle.tables.paymentRequest).where(
|
|
274
|
+
const paymentRequests = batchIds.length > 0 ? await db.select().from(drizzle.tables.paymentRequest).where(
|
|
275
|
+
drizzleOrm.and(
|
|
276
|
+
drizzleOrm.inArray(drizzle.tables.paymentRequest.batchId, batchIds),
|
|
277
|
+
drizzleOrm.isNull(drizzle.tables.paymentRequest.deletedAt)
|
|
278
|
+
)
|
|
279
|
+
) : [];
|
|
278
280
|
const paymentRequestsByBatchId = Map.groupBy(
|
|
279
281
|
paymentRequests,
|
|
280
282
|
(pr) => pr.batchId
|
|
@@ -289,17 +291,6 @@ const getBatchesQuery = async (db, {
|
|
|
289
291
|
};
|
|
290
292
|
};
|
|
291
293
|
|
|
292
|
-
const getAccountAccumulatingBatchesQuery = async (db, { accountId, paymentType }) => {
|
|
293
|
-
return await db.select().from(drizzle.tables.batch).where(
|
|
294
|
-
drizzleOrm.and(
|
|
295
|
-
drizzleOrm.eq(drizzle.tables.batch.accountId, accountId),
|
|
296
|
-
drizzleOrm.eq(drizzle.tables.batch.status, "OPEN"),
|
|
297
|
-
drizzleOrm.isNull(drizzle.tables.batch.batchPaymentInitiatedAt),
|
|
298
|
-
paymentType ? drizzleOrm.eq(drizzle.tables.batch.paymentType, paymentType) : void 0
|
|
299
|
-
)
|
|
300
|
-
);
|
|
301
|
-
};
|
|
302
|
-
|
|
303
294
|
const getOttQuery = async (db, { ott }) => {
|
|
304
295
|
return await db.select().from(drizzle.tables.ott).where(drizzleOrm.eq(drizzle.tables.ott.oneTimeToken, ott)).get();
|
|
305
296
|
};
|
|
@@ -346,15 +337,15 @@ const getPaymentRequestsQuery = async (db, params) => {
|
|
|
346
337
|
return { paymentRequests, totalCount };
|
|
347
338
|
};
|
|
348
339
|
|
|
349
|
-
const
|
|
350
|
-
|
|
340
|
+
const TERMINAL_STATUSES = [
|
|
341
|
+
"SETTLED",
|
|
342
|
+
"REJECTED",
|
|
343
|
+
"CLOSED"
|
|
344
|
+
];
|
|
345
|
+
const getNonTerminalPaymentRequestsQuery = (db) => db.select().from(drizzle.tables.paymentRequest).where(
|
|
351
346
|
drizzleOrm.and(
|
|
352
|
-
drizzleOrm.inArray(drizzle.tables.paymentRequest.status,
|
|
353
|
-
|
|
354
|
-
"SIGNED",
|
|
355
|
-
"PENDING"
|
|
356
|
-
]),
|
|
357
|
-
drizzleOrm.inArray(drizzle.tables.paymentRequest.connectorKey, NON_BATCH_CONNECTOR_KEYS)
|
|
347
|
+
drizzleOrm.not(drizzleOrm.inArray(drizzle.tables.paymentRequest.status, TERMINAL_STATUSES)),
|
|
348
|
+
drizzleOrm.isNull(drizzle.tables.paymentRequest.deletedAt)
|
|
358
349
|
)
|
|
359
350
|
);
|
|
360
351
|
|
|
@@ -375,6 +366,10 @@ const sendPaymentInputSchema = zod.z.object({
|
|
|
375
366
|
sendAsSinglePayment: zod.z.boolean().optional()
|
|
376
367
|
});
|
|
377
368
|
|
|
369
|
+
const sendBatchInputSchema = zod.z.object({
|
|
370
|
+
payments: zod.z.array(sendPaymentInputSchema).min(1)
|
|
371
|
+
});
|
|
372
|
+
|
|
378
373
|
const getAuthUriInputSchema = zod.z.object({
|
|
379
374
|
connectorKey: zod.z.enum(paymentRequest_schema.CONNECTOR_KEYS)
|
|
380
375
|
});
|
|
@@ -439,14 +434,6 @@ zod.z.object({
|
|
|
439
434
|
details: backendSdk.workflowInstanceStatusSchema
|
|
440
435
|
});
|
|
441
436
|
|
|
442
|
-
const updateBatchStatusesInputSchema = zod.z.object({
|
|
443
|
-
batchId: zod.z.uuid().optional()
|
|
444
|
-
}).optional();
|
|
445
|
-
zod.z.object({
|
|
446
|
-
processed: zod.z.number(),
|
|
447
|
-
statusChanged: zod.z.number()
|
|
448
|
-
});
|
|
449
|
-
|
|
450
437
|
const ALLOWED_PAYMENT_FILTERS = {
|
|
451
438
|
ACCOUNT_ID: "filterPaymentAccountId",
|
|
452
439
|
AMOUNT: "filterPaymentAmount",
|
|
@@ -535,6 +522,7 @@ const getBankAccountsInputSchema = zod.z.object({
|
|
|
535
522
|
limit: zod.z.number().positive().optional(),
|
|
536
523
|
includeWorkflow: zod.z.boolean().optional(),
|
|
537
524
|
includeBatchCounts: zod.z.boolean().optional(),
|
|
525
|
+
includePendingPaymentRequestCount: zod.z.boolean().optional(),
|
|
538
526
|
filterIbans: zod.z.array(zod.z.string()).optional(),
|
|
539
527
|
filterCurrencies: zod.z.array(zod.z.string()).optional(),
|
|
540
528
|
filterBankRefIds: zod.z.array(zod.z.string()).optional()
|
|
@@ -545,14 +533,9 @@ const disconnectAccountInputSchema = zod.z.object({
|
|
|
545
533
|
});
|
|
546
534
|
|
|
547
535
|
const handleAuthorizationCallbackInputSchema = zod.z.object({
|
|
548
|
-
|
|
549
|
-
batchId: zod.z.string().uuid().optional()
|
|
550
|
-
}).refine((data) => data.paymentId || data.batchId, {
|
|
551
|
-
message: "Either paymentId or batchId is required"
|
|
536
|
+
callbackUrl: zod.z.string().url()
|
|
552
537
|
});
|
|
553
538
|
|
|
554
|
-
const sendPaymentSyncInputSchema = sendPaymentInputSchema;
|
|
555
|
-
|
|
556
539
|
const getPaymentRequestsInputSchema = zod.z.object({
|
|
557
540
|
page: zod.z.number().positive(),
|
|
558
541
|
limit: zod.z.number().positive(),
|
|
@@ -561,7 +544,10 @@ const getPaymentRequestsInputSchema = zod.z.object({
|
|
|
561
544
|
direction: zod.z.enum(["asc", "desc"])
|
|
562
545
|
}),
|
|
563
546
|
filterAccountId: zod.z.union([zod.z.uuid(), zod.z.uuid().array()]).optional(),
|
|
564
|
-
filterStatus: zod.z.union([
|
|
547
|
+
filterStatus: zod.z.union([
|
|
548
|
+
zod.z.enum(paymentRequest_schema.PAYMENT_REQUEST_STATUSES),
|
|
549
|
+
zod.z.enum(paymentRequest_schema.PAYMENT_REQUEST_STATUSES).array()
|
|
550
|
+
]).optional(),
|
|
565
551
|
filterPaymentType: zod.z.union([zod.z.enum(paymentRequest_schema.PAYMENT_TYPES), zod.z.enum(paymentRequest_schema.PAYMENT_TYPES).array()]).optional(),
|
|
566
552
|
filterCurrency: zod.z.union([zod.z.enum(generalCodes.CURRENCY_CODES), zod.z.enum(generalCodes.CURRENCY_CODES).array()]).optional(),
|
|
567
553
|
filterMinAmount: zod.z.number().positive().optional(),
|
|
@@ -581,11 +567,6 @@ const getPaymentRequestsInputSchema = zod.z.object({
|
|
|
581
567
|
// filterDirection: removed — payment_request is always OUTGOING
|
|
582
568
|
});
|
|
583
569
|
|
|
584
|
-
zod.z.object({
|
|
585
|
-
processed: zod.z.number(),
|
|
586
|
-
statusChanged: zod.z.number()
|
|
587
|
-
});
|
|
588
|
-
|
|
589
570
|
var __defProp = Object.defineProperty;
|
|
590
571
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
591
572
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
@@ -596,11 +577,14 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
596
577
|
if (kind && result) __defProp(target, key, result);
|
|
597
578
|
return result;
|
|
598
579
|
};
|
|
599
|
-
const POLLING_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
600
580
|
let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.WorkerEntrypoint) {
|
|
601
581
|
constructor(ctx, env, config) {
|
|
602
582
|
super(ctx, env);
|
|
603
583
|
this.allowedProviders = [];
|
|
584
|
+
// ── Unified status resolution ──────────────────────────────────────
|
|
585
|
+
this.POLLING_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
586
|
+
// 14 days
|
|
587
|
+
this.COMPLETED_POLLING_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
604
588
|
this.allowedProviders = config.allowedProviders;
|
|
605
589
|
this.db = d1.drizzle(this.env.BANK_D1, { schema: drizzle.tables });
|
|
606
590
|
}
|
|
@@ -745,472 +729,222 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
745
729
|
}
|
|
746
730
|
);
|
|
747
731
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}).command.execute();
|
|
772
|
-
statusChanged = true;
|
|
773
|
-
}
|
|
774
|
-
if (statusChanged) {
|
|
775
|
-
const paymentRequests = await drizzle.getPaymentRequestsByBatchIdQuery(this.db, {
|
|
776
|
-
batchId: batch.id
|
|
777
|
-
});
|
|
778
|
-
const perPaymentStatuses = "payments" in apiResponse ? apiResponse.payments : null;
|
|
779
|
-
const batchStatusToPaymentStatus = {
|
|
780
|
-
SIGNED: "SIGNED",
|
|
781
|
-
COMPLETED: "COMPLETED",
|
|
782
|
-
FAILED: "FAILED",
|
|
783
|
-
SIGNATURE_FAILED: "FAILED"
|
|
784
|
-
};
|
|
785
|
-
for (const pr of paymentRequests) {
|
|
786
|
-
if (pr.status !== "COMPLETED" && pr.status !== "FAILED" && pr.createdAt != null && Date.now() - pr.createdAt.getTime() > POLLING_TIMEOUT_MS) {
|
|
787
|
-
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
788
|
-
id: pr.id,
|
|
789
|
-
status: "FAILED",
|
|
790
|
-
statusReason: "Polling timeout: no final status received after 14 days",
|
|
791
|
-
processedAt: /* @__PURE__ */ new Date()
|
|
792
|
-
}).command.execute();
|
|
793
|
-
continue;
|
|
794
|
-
}
|
|
795
|
-
let newStatus = pr.status;
|
|
796
|
-
let match;
|
|
797
|
-
if (perPaymentStatuses) {
|
|
798
|
-
match = perPaymentStatuses.find(
|
|
799
|
-
(p) => p.merchantTransactionId === pr.id
|
|
800
|
-
);
|
|
801
|
-
if (match) {
|
|
802
|
-
newStatus = mock_connector.mapFinbricksTransactionStatus(
|
|
803
|
-
match.resultCode,
|
|
804
|
-
match.finalBankStatus ?? false
|
|
805
|
-
);
|
|
806
|
-
}
|
|
807
|
-
} else {
|
|
808
|
-
newStatus = batchStatusToPaymentStatus[currentStatus] ?? pr.status;
|
|
809
|
-
}
|
|
810
|
-
if (newStatus !== pr.status) {
|
|
811
|
-
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
812
|
-
id: pr.id,
|
|
813
|
-
status: newStatus,
|
|
814
|
-
...newStatus === "FAILED" && {
|
|
815
|
-
statusReason: perPaymentStatuses ? match?.resultCode ?? currentStatus : currentStatus
|
|
816
|
-
},
|
|
817
|
-
processedAt: newStatus === "COMPLETED" || newStatus === "FAILED" ? /* @__PURE__ */ new Date() : void 0
|
|
818
|
-
}).command.execute();
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (paymentRequests.length > 0) {
|
|
822
|
-
const freshPRs = await drizzle.getPaymentRequestsByBatchIdQuery(this.db, {
|
|
823
|
-
batchId: batch.id
|
|
732
|
+
// 7 days
|
|
733
|
+
/**
|
|
734
|
+
* Core status resolution logic. Polls connector for each PR and updates status.
|
|
735
|
+
* Called from CRON and from handleAuthorizationCallback.
|
|
736
|
+
*/
|
|
737
|
+
async _resolvePaymentRequestStatuses(prIds) {
|
|
738
|
+
if (prIds.length === 0) return { processed: 0, statusChanged: 0 };
|
|
739
|
+
const allPRs = (await Promise.all(
|
|
740
|
+
prIds.map(
|
|
741
|
+
(id) => getPaymentRequestByIdQuery(this.db, { paymentRequestId: id })
|
|
742
|
+
)
|
|
743
|
+
)).filter((p) => p !== void 0);
|
|
744
|
+
const byConnector = Map.groupBy(allPRs, (pr) => pr.connectorKey);
|
|
745
|
+
let processed = 0;
|
|
746
|
+
let statusChanged = 0;
|
|
747
|
+
const eventsToEmit = [];
|
|
748
|
+
for (const [connectorKey, requests] of byConnector) {
|
|
749
|
+
let connector;
|
|
750
|
+
try {
|
|
751
|
+
connector = await this._initiateBankConnector({ connectorKey });
|
|
752
|
+
} catch (err) {
|
|
753
|
+
this.logError({
|
|
754
|
+
message: `[_resolvePaymentRequestStatuses] Failed to init connector ${connectorKey}: ${err}`
|
|
824
755
|
});
|
|
825
|
-
|
|
826
|
-
(pr) => pr.status === "COMPLETED" || pr.status === "FAILED"
|
|
827
|
-
);
|
|
828
|
-
if (allTerminal && currentStatus !== "COMPLETED" && currentStatus !== "FAILED") {
|
|
829
|
-
const finalBatchStatus = freshPRs.some((pr) => pr.status === "FAILED") ? "FAILED" : "COMPLETED";
|
|
830
|
-
await drizzle.upsertBatchCommand(this.db, {
|
|
831
|
-
batch: { ...batch, status: finalBatchStatus }
|
|
832
|
-
}).command.execute();
|
|
833
|
-
console.log(
|
|
834
|
-
`Batch ${batch.id} auto-closed with status ${finalBatchStatus}`
|
|
835
|
-
);
|
|
836
|
-
}
|
|
756
|
+
continue;
|
|
837
757
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
throw backendSdk.createInternalError(null, {
|
|
858
|
-
message: `Batch not found: ${validatedInput.batchId}`,
|
|
859
|
-
code: "DB-B-007",
|
|
860
|
-
status: 404
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
pendingBatches = [batch];
|
|
864
|
-
} else {
|
|
865
|
-
pendingBatches = await getAllPendingBatchesQuery(this.db);
|
|
866
|
-
}
|
|
867
|
-
const accounts = await this._getConnectedAccounts();
|
|
868
|
-
console.log(`Processing ${pendingBatches.length} pending batche(s)`);
|
|
869
|
-
const batchesByConnector = /* @__PURE__ */ new Map();
|
|
870
|
-
for (const batch of pendingBatches) {
|
|
871
|
-
const account = accounts.find((acc) => acc.id === batch.accountId);
|
|
872
|
-
if (!account) {
|
|
873
|
-
this.logError({
|
|
874
|
-
message: `Account not found for batch ${batch.id}, skipping`
|
|
875
|
-
});
|
|
876
|
-
const prs = await drizzle.getPaymentRequestsByBatchIdQuery(this.db, {
|
|
877
|
-
batchId: batch.id
|
|
878
|
-
});
|
|
879
|
-
const prCmds = prs.filter((p) => p.status !== "FAILED").map(
|
|
880
|
-
(p) => drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
881
|
-
id: p.id,
|
|
882
|
-
status: "FAILED",
|
|
883
|
-
statusReason: "ACCOUNT_NOT_FOUND",
|
|
884
|
-
processedAt: /* @__PURE__ */ new Date()
|
|
885
|
-
}).command
|
|
886
|
-
);
|
|
887
|
-
const batchCmd = drizzle.upsertBatchCommand(this.db, {
|
|
888
|
-
batch: {
|
|
889
|
-
...batch,
|
|
890
|
-
status: "FAILED",
|
|
891
|
-
statusReason: "ACCOUNT_NOT_FOUND"
|
|
758
|
+
for (const pr of requests) {
|
|
759
|
+
try {
|
|
760
|
+
const paymentId = pr.bankRefId ?? pr.id;
|
|
761
|
+
const newStatus = await connector.getPaymentStatus({ paymentId });
|
|
762
|
+
if (newStatus !== pr.status) {
|
|
763
|
+
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
764
|
+
id: pr.id,
|
|
765
|
+
status: newStatus,
|
|
766
|
+
processedAt: newStatus === "COMPLETED" ? /* @__PURE__ */ new Date() : void 0
|
|
767
|
+
}).command.execute();
|
|
768
|
+
statusChanged++;
|
|
769
|
+
eventsToEmit.push({
|
|
770
|
+
eventType: "BANK_PAYMENT_REQUEST",
|
|
771
|
+
eventSignal: "paymentRequestStatusChanged",
|
|
772
|
+
paymentRequest: { ...pr, status: newStatus },
|
|
773
|
+
metadata: {
|
|
774
|
+
correlationId: pr.correlationId,
|
|
775
|
+
entityId: pr.id,
|
|
776
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
892
777
|
}
|
|
893
|
-
})
|
|
894
|
-
const allCmds = [batchCmd, ...prCmds];
|
|
895
|
-
if (allCmds.length > 0) {
|
|
896
|
-
await this.db.batch(allCmds);
|
|
897
|
-
}
|
|
898
|
-
continue;
|
|
778
|
+
});
|
|
899
779
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const allResults = [];
|
|
905
|
-
for (const [connectorKey, batches] of batchesByConnector) {
|
|
906
|
-
console.log(
|
|
907
|
-
`Initializing ${connectorKey} connector for ${batches.length} batches`
|
|
908
|
-
);
|
|
909
|
-
const connector = await this._initiateBankConnector({
|
|
910
|
-
connectorKey
|
|
780
|
+
processed++;
|
|
781
|
+
} catch (err) {
|
|
782
|
+
this.logError({
|
|
783
|
+
message: `Failed to resolve status for PR ${pr.id}: ${err}`
|
|
911
784
|
});
|
|
912
|
-
if (connector.lifecycleMode === "per-payment") {
|
|
913
|
-
continue;
|
|
914
|
-
}
|
|
915
|
-
for (const batch of batches) {
|
|
916
|
-
const result = await this._resolveSingleBatch(batch, connector);
|
|
917
|
-
if (result) {
|
|
918
|
-
allResults.push(result);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
785
|
}
|
|
922
|
-
const changedCount = allResults.filter((r) => r.statusChanged).length;
|
|
923
|
-
console.log(
|
|
924
|
-
`Batch update completed: ${changedCount} status change(s) detected`
|
|
925
|
-
);
|
|
926
|
-
return {
|
|
927
|
-
processed: allResults.length,
|
|
928
|
-
statusChanged: changedCount
|
|
929
|
-
};
|
|
930
786
|
}
|
|
787
|
+
}
|
|
788
|
+
if (eventsToEmit.length > 0) {
|
|
789
|
+
await this.pushToQueue(
|
|
790
|
+
this.env.QUEUE_BUS_QUEUE,
|
|
791
|
+
eventsToEmit
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const affectedBatchIds = [
|
|
795
|
+
...new Set(
|
|
796
|
+
allPRs.map((pr) => pr.batchId).filter((id) => id != null)
|
|
797
|
+
)
|
|
798
|
+
];
|
|
799
|
+
for (const batchId of affectedBatchIds) {
|
|
800
|
+
await this._deriveBatchStatus(batchId);
|
|
801
|
+
}
|
|
802
|
+
return { processed, statusChanged };
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Derives batch status from its payment requests.
|
|
806
|
+
*/
|
|
807
|
+
async _deriveBatchStatus(batchId) {
|
|
808
|
+
const [allPRs, batch] = await Promise.all([
|
|
809
|
+
drizzle.getPaymentRequestsByBatchIdQuery(this.db, { batchId }),
|
|
810
|
+
drizzle.getBatchByIdQuery(this.db, { batchId })
|
|
811
|
+
]);
|
|
812
|
+
if (!batch || batch.status === "COMPLETED" || batch.status === "FAILED")
|
|
813
|
+
return;
|
|
814
|
+
if (allPRs.length === 0) return;
|
|
815
|
+
const terminalStatuses = [
|
|
816
|
+
"SETTLED",
|
|
817
|
+
"REJECTED",
|
|
818
|
+
"CLOSED"
|
|
819
|
+
];
|
|
820
|
+
const authorizedOrHigher = [
|
|
821
|
+
"AUTHORIZED",
|
|
822
|
+
"COMPLETED",
|
|
823
|
+
"BOOKED",
|
|
824
|
+
"SETTLED"
|
|
825
|
+
];
|
|
826
|
+
const allTerminal = allPRs.every(
|
|
827
|
+
(pr) => terminalStatuses.includes(pr.status)
|
|
828
|
+
);
|
|
829
|
+
const allAuthorizedOrHigher = allPRs.every(
|
|
830
|
+
(pr) => [...authorizedOrHigher, ...terminalStatuses].includes(
|
|
831
|
+
pr.status
|
|
832
|
+
)
|
|
931
833
|
);
|
|
834
|
+
if (allTerminal) {
|
|
835
|
+
const hasFailed = allPRs.some(
|
|
836
|
+
(pr) => pr.status === "REJECTED" || pr.status === "CLOSED"
|
|
837
|
+
);
|
|
838
|
+
await drizzle.upsertBatchCommand(this.db, {
|
|
839
|
+
batch: { ...batch, status: hasFailed ? "FAILED" : "COMPLETED" }
|
|
840
|
+
}).command.execute();
|
|
841
|
+
} else if (allAuthorizedOrHigher && batch.status !== "AUTHORIZED") {
|
|
842
|
+
await drizzle.upsertBatchCommand(this.db, {
|
|
843
|
+
batch: { ...batch, status: "AUTHORIZED" }
|
|
844
|
+
}).command.execute();
|
|
845
|
+
}
|
|
932
846
|
}
|
|
933
|
-
async
|
|
847
|
+
async updatePaymentRequestStatuses() {
|
|
934
848
|
return this.handleAction(
|
|
935
849
|
null,
|
|
936
|
-
{ successMessage: "Payment statuses updated" },
|
|
850
|
+
{ successMessage: "Payment request statuses updated" },
|
|
937
851
|
async () => {
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
);
|
|
941
|
-
if (pendingRequests.length === 0) {
|
|
852
|
+
const nonTerminalPRs = await getNonTerminalPaymentRequestsQuery(this.db);
|
|
853
|
+
if (nonTerminalPRs.length === 0) {
|
|
942
854
|
return { processed: 0, statusChanged: 0 };
|
|
943
855
|
}
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
this.logError({
|
|
958
|
-
message: `[updatePaymentStatuses] Failed to init connector ${connectorKey}: ${err}`
|
|
959
|
-
});
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
for (const pr of requests) {
|
|
963
|
-
try {
|
|
964
|
-
if (pr.createdAt != null && Date.now() - pr.createdAt.getTime() > POLLING_TIMEOUT_MS) {
|
|
965
|
-
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
966
|
-
id: pr.id,
|
|
967
|
-
status: "FAILED",
|
|
968
|
-
statusReason: "Polling timeout: no final status received after 14 days",
|
|
969
|
-
processedAt: /* @__PURE__ */ new Date()
|
|
970
|
-
}).command.execute();
|
|
971
|
-
statusChanged++;
|
|
972
|
-
processed++;
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
const paymentId = pr.bankRefId ?? pr.id;
|
|
976
|
-
const newStatus = await connector.getPaymentStatus({ paymentId });
|
|
977
|
-
if (newStatus !== pr.status) {
|
|
978
|
-
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
979
|
-
id: pr.id,
|
|
980
|
-
status: newStatus,
|
|
981
|
-
processedAt: newStatus === "COMPLETED" || newStatus === "FAILED" ? /* @__PURE__ */ new Date() : void 0
|
|
982
|
-
}).command.execute();
|
|
983
|
-
statusChanged++;
|
|
984
|
-
}
|
|
985
|
-
processed++;
|
|
986
|
-
} catch (err) {
|
|
987
|
-
this.logError({
|
|
988
|
-
message: `Failed to get status for payment ${pr.id}: ${err}`
|
|
989
|
-
});
|
|
856
|
+
const now = Date.now();
|
|
857
|
+
const pollableIds = [];
|
|
858
|
+
for (const pr of nonTerminalPRs) {
|
|
859
|
+
const status = pr.status;
|
|
860
|
+
if (status === "OPENED" || status === "AUTHORIZED") {
|
|
861
|
+
if (pr.createdAt != null && now - pr.createdAt.getTime() > this.POLLING_TIMEOUT_MS) {
|
|
862
|
+
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
863
|
+
id: pr.id,
|
|
864
|
+
status: "CLOSED",
|
|
865
|
+
statusReason: "Polling timeout: no final status received after 14 days",
|
|
866
|
+
processedAt: /* @__PURE__ */ new Date()
|
|
867
|
+
}).command.execute();
|
|
868
|
+
continue;
|
|
990
869
|
}
|
|
870
|
+
pollableIds.push(pr.id);
|
|
871
|
+
} else if (status === "COMPLETED" || status === "BOOKED") {
|
|
872
|
+
if (pr.processedAt != null && now - pr.processedAt.getTime() > this.COMPLETED_POLLING_WINDOW_MS) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
pollableIds.push(pr.id);
|
|
991
876
|
}
|
|
992
877
|
}
|
|
993
|
-
|
|
994
|
-
...new Set(
|
|
995
|
-
pendingRequests.map((pr) => pr.batchId).filter((id) => id != null)
|
|
996
|
-
)
|
|
997
|
-
];
|
|
998
|
-
for (const batchId of affectedBatchIds) {
|
|
999
|
-
const [allPRs, batch] = await Promise.all([
|
|
1000
|
-
drizzle.getPaymentRequestsByBatchIdQuery(this.db, { batchId }),
|
|
1001
|
-
drizzle.getBatchByIdQuery(this.db, { batchId })
|
|
1002
|
-
]);
|
|
1003
|
-
if (!batch || batch.status === "COMPLETED" || batch.status === "FAILED")
|
|
1004
|
-
continue;
|
|
1005
|
-
const allTerminal = allPRs.every(
|
|
1006
|
-
(pr) => pr.status === "COMPLETED" || pr.status === "FAILED"
|
|
1007
|
-
);
|
|
1008
|
-
if (allTerminal && allPRs.length > 0) {
|
|
1009
|
-
const finalStatus = allPRs.some((pr) => pr.status === "FAILED") ? "FAILED" : "COMPLETED";
|
|
1010
|
-
await drizzle.upsertBatchCommand(this.db, {
|
|
1011
|
-
batch: { ...batch, status: finalStatus }
|
|
1012
|
-
}).command.execute();
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return { processed, statusChanged };
|
|
878
|
+
return this._resolvePaymentRequestStatuses(pollableIds);
|
|
1016
879
|
}
|
|
1017
880
|
);
|
|
1018
881
|
}
|
|
882
|
+
async scheduled(controller) {
|
|
883
|
+
if (controller.cron === this.env.CRON_PAYMENT_STATUSES) {
|
|
884
|
+
console.log("Scheduled CRON payment request statuses");
|
|
885
|
+
await this.updatePaymentRequestStatuses();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
1019
888
|
async handleAuthorizationCallback(input) {
|
|
1020
889
|
return this.handleAction(
|
|
1021
890
|
{ data: input, schema: handleAuthorizationCallbackInputSchema },
|
|
1022
891
|
{ successMessage: "Authorization callback processed" },
|
|
1023
|
-
async ({
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
892
|
+
async ({ callbackUrl }) => {
|
|
893
|
+
const url = new URL(callbackUrl);
|
|
894
|
+
const params = url.searchParams;
|
|
895
|
+
const paymentRequestId = params.get("paymentRequestId");
|
|
896
|
+
const batchId = params.get("batchId");
|
|
897
|
+
let connectorKey;
|
|
898
|
+
if (paymentRequestId) {
|
|
1027
899
|
const pr = await getPaymentRequestByIdQuery(this.db, {
|
|
1028
|
-
paymentRequestId
|
|
900
|
+
paymentRequestId
|
|
1029
901
|
});
|
|
1030
|
-
if (
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
status: 404
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
if (pr.status !== "SIGNED" && pr.status !== "COMPLETED") {
|
|
1038
|
-
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1039
|
-
id: paymentId,
|
|
1040
|
-
status: "SIGNED"
|
|
1041
|
-
}).command.execute();
|
|
1042
|
-
paymentsUpdated = 1;
|
|
1043
|
-
}
|
|
1044
|
-
targetBatchId = pr.batchId;
|
|
1045
|
-
}
|
|
1046
|
-
if (batchId) {
|
|
1047
|
-
targetBatchId = batchId;
|
|
1048
|
-
const batchPayments = await drizzle.getPaymentRequestsByBatchIdQuery(
|
|
1049
|
-
this.db,
|
|
1050
|
-
{
|
|
1051
|
-
batchId
|
|
1052
|
-
}
|
|
1053
|
-
);
|
|
1054
|
-
for (const pr of batchPayments) {
|
|
1055
|
-
if (pr.status !== "SIGNED" && pr.status !== "COMPLETED") {
|
|
1056
|
-
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1057
|
-
id: pr.id,
|
|
1058
|
-
status: "SIGNED"
|
|
1059
|
-
}).command.execute();
|
|
1060
|
-
paymentsUpdated++;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
let batchSigned = false;
|
|
1065
|
-
if (targetBatchId) {
|
|
1066
|
-
const allPayments = await drizzle.getPaymentRequestsByBatchIdQuery(this.db, {
|
|
1067
|
-
batchId: targetBatchId
|
|
902
|
+
if (pr) connectorKey = pr.connectorKey;
|
|
903
|
+
} else if (batchId) {
|
|
904
|
+
const batchPrs = await drizzle.getPaymentRequestsByBatchIdQuery(this.db, {
|
|
905
|
+
batchId
|
|
1068
906
|
});
|
|
1069
|
-
|
|
1070
|
-
(p) => p.status === "SIGNED" || p.status === "COMPLETED"
|
|
1071
|
-
);
|
|
1072
|
-
if (allDone) {
|
|
1073
|
-
const batch = await drizzle.getBatchByIdQuery(this.db, {
|
|
1074
|
-
batchId: targetBatchId
|
|
1075
|
-
});
|
|
1076
|
-
if (batch && batch.status !== "SIGNED") {
|
|
1077
|
-
await drizzle.upsertBatchCommand(this.db, {
|
|
1078
|
-
batch: { ...batch, status: "SIGNED" }
|
|
1079
|
-
}).command.execute();
|
|
1080
|
-
batchSigned = true;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
907
|
+
if (batchPrs.length > 0) connectorKey = batchPrs[0].connectorKey;
|
|
1083
908
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
async addPaymentsToBatch({ paymentIds }) {
|
|
1093
|
-
return this.handleAction(null, {}, async () => {
|
|
1094
|
-
this.logInput({ paymentIds });
|
|
1095
|
-
const paymentRequests = (await Promise.all(
|
|
1096
|
-
paymentIds.map(
|
|
1097
|
-
(id) => getPaymentRequestByIdQuery(this.db, { paymentRequestId: id })
|
|
1098
|
-
)
|
|
1099
|
-
)).filter((p) => p !== void 0);
|
|
1100
|
-
const foundIds = new Set(paymentRequests.map((p) => p.id));
|
|
1101
|
-
const missingIds = paymentIds.filter((id) => !foundIds.has(id));
|
|
1102
|
-
if (missingIds.length > 0) {
|
|
1103
|
-
this.logError({ missingIds });
|
|
1104
|
-
}
|
|
1105
|
-
const byAccount = Map.groupBy(
|
|
1106
|
-
paymentRequests,
|
|
1107
|
-
(p) => `${p.accountId}:${p.paymentType}`
|
|
1108
|
-
);
|
|
1109
|
-
const { accounts } = await this._getAccounts();
|
|
1110
|
-
for (const [, paymentsOfType] of byAccount) {
|
|
1111
|
-
const first2 = paymentsOfType[0];
|
|
1112
|
-
const acc = accounts.find((a) => a.id === first2.accountId);
|
|
1113
|
-
if (!acc) {
|
|
1114
|
-
this.logError({ message: `Account not found: ${first2.accountId}` });
|
|
1115
|
-
await Promise.all(
|
|
1116
|
-
paymentsOfType.map(
|
|
1117
|
-
(p) => drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1118
|
-
id: p.id,
|
|
1119
|
-
status: "FAILED",
|
|
1120
|
-
statusReason: "ACCOUNT_NOT_FOUND"
|
|
1121
|
-
}).command.execute()
|
|
1122
|
-
)
|
|
1123
|
-
);
|
|
1124
|
-
continue;
|
|
909
|
+
if (!connectorKey) {
|
|
910
|
+
throw backendSdk.createInternalError(null, {
|
|
911
|
+
message: `Could not resolve connector from callback URL`,
|
|
912
|
+
code: "DB-B-008",
|
|
913
|
+
status: 400
|
|
914
|
+
});
|
|
1125
915
|
}
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
);
|
|
1129
|
-
const regularPayments = paymentsOfType.filter(
|
|
1130
|
-
(p) => p.sendAsSinglePayment !== true
|
|
1131
|
-
);
|
|
1132
|
-
this.log({
|
|
1133
|
-
message: `\u{1F4B0} Processing ${paymentsOfType.length} payments for account (${acc.iban}, ${acc.currency})`
|
|
916
|
+
const connector = await this._initiateBankConnector({
|
|
917
|
+
connectorKey
|
|
1134
918
|
});
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const { command: updateRequest } = drizzle.updatePaymentRequestStatusCommand(
|
|
1148
|
-
this.db,
|
|
1149
|
-
{ id: singlePayment.id, status: "CREATED", batchId }
|
|
1150
|
-
);
|
|
1151
|
-
await this.db.batch([upsertBatch, updateRequest]);
|
|
1152
|
-
this.log({
|
|
1153
|
-
message: `\u2728 Created single payment batch (${singlePayment.paymentType}) for account ${acc.id}`
|
|
1154
|
-
});
|
|
919
|
+
const result = connector.parseAuthorizationCallback(callbackUrl);
|
|
920
|
+
if (!result.success) {
|
|
921
|
+
return {
|
|
922
|
+
paymentsUpdated: 0,
|
|
923
|
+
batchId: null,
|
|
924
|
+
batchAuthorized: false,
|
|
925
|
+
error: result.error
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
const prIds = [];
|
|
929
|
+
if (result.type === "paymentRequest") {
|
|
930
|
+
prIds.push(result.paymentRequestId);
|
|
1155
931
|
}
|
|
1156
|
-
if (
|
|
1157
|
-
const
|
|
932
|
+
if (result.type === "batch") {
|
|
933
|
+
const batchPayments = await drizzle.getPaymentRequestsByBatchIdQuery(
|
|
1158
934
|
this.db,
|
|
1159
|
-
{
|
|
1160
|
-
accountId: acc.id,
|
|
1161
|
-
paymentType: first2.paymentType
|
|
1162
|
-
}
|
|
935
|
+
{ batchId: result.batchId }
|
|
1163
936
|
);
|
|
1164
|
-
|
|
1165
|
-
let availableCount = 0;
|
|
1166
|
-
let availableBatch;
|
|
1167
|
-
for (const ob of openBatches) {
|
|
1168
|
-
const existingPayments = await drizzle.getPaymentRequestsByBatchIdQuery(
|
|
1169
|
-
this.db,
|
|
1170
|
-
{ batchId: ob.id }
|
|
1171
|
-
);
|
|
1172
|
-
const limit = ob.metadata?.sizeLimit ?? acc.batchSizeLimit;
|
|
1173
|
-
if (existingPayments.length < limit) {
|
|
1174
|
-
availableBatch = ob;
|
|
1175
|
-
availableCount = existingPayments.length;
|
|
1176
|
-
break;
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
if (availableBatch) {
|
|
1180
|
-
batchId = availableBatch.id;
|
|
1181
|
-
this.log({
|
|
1182
|
-
message: `\u{1F504} Found existing OPEN batch (${first2.paymentType}) for account ${acc.id}, merging ${availableCount} existing + ${regularPayments.length} new payments`
|
|
1183
|
-
});
|
|
1184
|
-
} else {
|
|
1185
|
-
batchId = backendSdk.uuidv4();
|
|
1186
|
-
this.log({
|
|
1187
|
-
message: `\u2728 Creating new batch (${first2.paymentType}) for account ${acc.id} with ${regularPayments.length} payments`
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
const { command: upsertBatch } = drizzle.upsertBatchCommand(this.db, {
|
|
1191
|
-
batch: availableBatch ? { ...availableBatch } : {
|
|
1192
|
-
id: batchId,
|
|
1193
|
-
authorizationUrls: [],
|
|
1194
|
-
accountId: acc.id,
|
|
1195
|
-
paymentType: first2.paymentType,
|
|
1196
|
-
status: "OPEN",
|
|
1197
|
-
metadata: { sizeLimit: acc.batchSizeLimit }
|
|
1198
|
-
}
|
|
1199
|
-
});
|
|
1200
|
-
const updateCommands = regularPayments.map(
|
|
1201
|
-
(p) => drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1202
|
-
id: p.id,
|
|
1203
|
-
status: "CREATED",
|
|
1204
|
-
batchId
|
|
1205
|
-
}).command
|
|
1206
|
-
);
|
|
1207
|
-
await this.db.batch([upsertBatch, ...updateCommands]);
|
|
1208
|
-
this.log({
|
|
1209
|
-
message: `\u2705 Batch (${first2.paymentType}) upserted successfully for account ${acc.id}`
|
|
1210
|
-
});
|
|
937
|
+
prIds.push(...batchPayments.map((pr) => pr.id));
|
|
1211
938
|
}
|
|
939
|
+
const { statusChanged } = await this._resolvePaymentRequestStatuses(prIds);
|
|
940
|
+
return {
|
|
941
|
+
paymentsUpdated: statusChanged,
|
|
942
|
+
batchId: result.type === "batch" ? result.batchId : null,
|
|
943
|
+
batchAuthorized: false,
|
|
944
|
+
error: null
|
|
945
|
+
};
|
|
1212
946
|
}
|
|
1213
|
-
|
|
947
|
+
);
|
|
1214
948
|
}
|
|
1215
949
|
async processBatch(input) {
|
|
1216
950
|
return this.handleAction(
|
|
@@ -1257,19 +991,6 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1257
991
|
}
|
|
1258
992
|
);
|
|
1259
993
|
}
|
|
1260
|
-
async queue(b) {
|
|
1261
|
-
await this.handleAction(
|
|
1262
|
-
null,
|
|
1263
|
-
{ successMessage: "Queue handler executed successfully" },
|
|
1264
|
-
async () => {
|
|
1265
|
-
this.logQueuePull(b);
|
|
1266
|
-
const queueHandlerResponse = await this.addPaymentsToBatch({
|
|
1267
|
-
paymentIds: b.messages.map(({ body }) => body.paymentId)
|
|
1268
|
-
});
|
|
1269
|
-
this.logOutput(queueHandlerResponse);
|
|
1270
|
-
}
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
994
|
async getAuthUri(input) {
|
|
1274
995
|
return this.handleAction(
|
|
1275
996
|
{ data: input, schema: getAuthUriInputSchema },
|
|
@@ -1455,7 +1176,7 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1455
1176
|
direction: "INCOMING",
|
|
1456
1177
|
paymentType: "DOMESTIC",
|
|
1457
1178
|
currency,
|
|
1458
|
-
status: "
|
|
1179
|
+
status: "BOOKED",
|
|
1459
1180
|
batchId: backendSdk.uuidv4(),
|
|
1460
1181
|
initiatedAt: /* @__PURE__ */ new Date(),
|
|
1461
1182
|
processedAt: /* @__PURE__ */ new Date(),
|
|
@@ -1473,7 +1194,7 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1473
1194
|
this.logQueuePush({ payment, isPaymentExecuted: true });
|
|
1474
1195
|
await this.pushToQueue(this.env.QUEUE_BUS_QUEUE, {
|
|
1475
1196
|
eventType: "BANK_PAYMENT",
|
|
1476
|
-
eventSignal: "
|
|
1197
|
+
eventSignal: "paymentFetched",
|
|
1477
1198
|
bankPayment: createdPayment,
|
|
1478
1199
|
metadata: {
|
|
1479
1200
|
correlationId: createdPayment.correlationId,
|
|
@@ -1488,7 +1209,7 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1488
1209
|
async sendPayment(input) {
|
|
1489
1210
|
return this.handleAction(
|
|
1490
1211
|
{ data: input, schema: sendPaymentInputSchema },
|
|
1491
|
-
{ successMessage: "Payment
|
|
1212
|
+
{ successMessage: "Payment initiated successfully" },
|
|
1492
1213
|
async (data) => {
|
|
1493
1214
|
const incomingPayment = mock_connector.toIncomingPayment(data);
|
|
1494
1215
|
const { accounts } = await this._getAccounts();
|
|
@@ -1525,32 +1246,90 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1525
1246
|
data.creditor
|
|
1526
1247
|
);
|
|
1527
1248
|
const accountAssigned = mock_connector.assignAccount(incomingPayment, account);
|
|
1249
|
+
const batchedPayment = mock_connector.toBatchedPayment(accountAssigned);
|
|
1528
1250
|
const { command: insertPaymentRequest } = createPaymentRequestCommand(
|
|
1529
1251
|
this.db,
|
|
1530
1252
|
{ paymentRequest: mock_connector.toPaymentRequestInsert(accountAssigned, null) }
|
|
1531
1253
|
);
|
|
1532
1254
|
await insertPaymentRequest.execute();
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1255
|
+
const initiate = () => {
|
|
1256
|
+
switch (data.paymentType) {
|
|
1257
|
+
case "DOMESTIC":
|
|
1258
|
+
return connector.initiateDomesticPayment(batchedPayment);
|
|
1259
|
+
case "SEPA":
|
|
1260
|
+
return connector.initiateSEPAPayment(batchedPayment);
|
|
1261
|
+
case "SWIFT":
|
|
1262
|
+
return connector.initiateForeignPayment(batchedPayment);
|
|
1263
|
+
default:
|
|
1264
|
+
throw backendSdk.createInternalError(null, {
|
|
1265
|
+
message: `Unsupported payment type: ${data.paymentType}`,
|
|
1266
|
+
code: "VALID-B-005",
|
|
1267
|
+
status: 400
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
let initiated;
|
|
1272
|
+
try {
|
|
1273
|
+
initiated = await initiate();
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1276
|
+
id: incomingPayment.id,
|
|
1277
|
+
deletedAt: /* @__PURE__ */ new Date()
|
|
1278
|
+
}).command.execute();
|
|
1279
|
+
throw err;
|
|
1280
|
+
}
|
|
1281
|
+
await drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1282
|
+
id: incomingPayment.id,
|
|
1283
|
+
bankRefId: initiated.payment.bankRefId,
|
|
1284
|
+
initiatedAt: initiated.payment.initiatedAt,
|
|
1285
|
+
authorizationUrl: initiated.authorizationUrl
|
|
1286
|
+
}).command.execute();
|
|
1287
|
+
return {
|
|
1288
|
+
paymentRequestId: incomingPayment.id,
|
|
1289
|
+
authorizationUrl: initiated.authorizationUrl
|
|
1290
|
+
};
|
|
1538
1291
|
}
|
|
1539
1292
|
);
|
|
1540
1293
|
}
|
|
1541
1294
|
async sendPaymentSync(input) {
|
|
1295
|
+
const result = await this.sendPayment(input);
|
|
1296
|
+
return {
|
|
1297
|
+
...result,
|
|
1298
|
+
data: result.data ? { authorizationUrl: result.data.authorizationUrl } : void 0
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
async sendBatch(input) {
|
|
1542
1302
|
return this.handleAction(
|
|
1543
|
-
{ data: input, schema:
|
|
1544
|
-
{ successMessage: "
|
|
1545
|
-
async (
|
|
1546
|
-
const
|
|
1303
|
+
{ data: input, schema: sendBatchInputSchema },
|
|
1304
|
+
{ successMessage: "Batch initiated successfully" },
|
|
1305
|
+
async ({ payments: paymentInputs }) => {
|
|
1306
|
+
const firstPayment = paymentInputs[0];
|
|
1307
|
+
const debtorIban = firstPayment.debtor.iban;
|
|
1308
|
+
const paymentType = firstPayment.paymentType;
|
|
1309
|
+
const currency = firstPayment.currency;
|
|
1310
|
+
for (const p of paymentInputs) {
|
|
1311
|
+
if (p.debtor.iban !== debtorIban || p.currency !== currency) {
|
|
1312
|
+
throw backendSdk.createInternalError(null, {
|
|
1313
|
+
message: "All payments in a batch must have the same debtor IBAN and currency",
|
|
1314
|
+
code: "VALID-B-011",
|
|
1315
|
+
status: 422
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
if (p.paymentType !== paymentType) {
|
|
1319
|
+
throw backendSdk.createInternalError(null, {
|
|
1320
|
+
message: "All payments in a batch must have the same payment type",
|
|
1321
|
+
code: "VALID-B-012",
|
|
1322
|
+
status: 422
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1547
1326
|
const { accounts } = await this._getAccounts();
|
|
1548
1327
|
const account = accounts.find(
|
|
1549
|
-
(acc) => acc.iban ===
|
|
1328
|
+
(acc) => acc.iban === debtorIban && acc.currency === currency
|
|
1550
1329
|
);
|
|
1551
1330
|
if (!account) {
|
|
1552
1331
|
throw backendSdk.createInternalError(null, {
|
|
1553
|
-
message: `No account found for IBAN ${
|
|
1332
|
+
message: `No account found for IBAN ${debtorIban} with currency ${currency}`,
|
|
1554
1333
|
code: "VALID-B-004",
|
|
1555
1334
|
status: 422
|
|
1556
1335
|
});
|
|
@@ -1562,44 +1341,113 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1562
1341
|
status: 422
|
|
1563
1342
|
});
|
|
1564
1343
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1344
|
+
if (paymentInputs.length > account.batchSizeLimit) {
|
|
1345
|
+
throw backendSdk.createInternalError(null, {
|
|
1346
|
+
message: `Batch size ${paymentInputs.length} exceeds limit ${account.batchSizeLimit}`,
|
|
1347
|
+
code: "VALID-B-013",
|
|
1348
|
+
status: 422
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1568
1351
|
const connector = await this._initiateBankConnector({
|
|
1569
1352
|
connectorKey: account.connectorKey
|
|
1570
1353
|
});
|
|
1571
|
-
if (!connector.supportsPaymentType(
|
|
1354
|
+
if (!connector.supportsPaymentType(paymentType)) {
|
|
1572
1355
|
throw backendSdk.createInternalError(null, {
|
|
1573
|
-
message: `Connector ${account.connectorKey} does not support ${
|
|
1356
|
+
message: `Connector ${account.connectorKey} does not support ${paymentType} payments yet`,
|
|
1574
1357
|
code: "VALID-B-006",
|
|
1575
1358
|
status: 422
|
|
1576
1359
|
});
|
|
1577
1360
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1361
|
+
for (const p of paymentInputs) {
|
|
1362
|
+
this._validatePaymentTypeAndCurrency(
|
|
1363
|
+
p.paymentType,
|
|
1364
|
+
p.currency,
|
|
1365
|
+
p.creditor
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
const incomingPayments = paymentInputs.map(mock_connector.toIncomingPayment);
|
|
1369
|
+
const batchedPayments = incomingPayments.map(
|
|
1370
|
+
(p) => mock_connector.toBatchedPayment(mock_connector.assignAccount(p, account))
|
|
1582
1371
|
);
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1372
|
+
const batchId = backendSdk.uuidv4();
|
|
1373
|
+
const batchMode = connector.supportsBatch(paymentType) ? "NATIVE" : "SINGLE";
|
|
1374
|
+
const batchCmd = drizzle.upsertBatchCommand(this.db, {
|
|
1375
|
+
batch: {
|
|
1376
|
+
id: batchId,
|
|
1377
|
+
authorizationUrls: [],
|
|
1378
|
+
accountId: account.id,
|
|
1379
|
+
paymentType,
|
|
1380
|
+
status: "PROCESSING",
|
|
1381
|
+
metadata: { sizeLimit: account.batchSizeLimit },
|
|
1382
|
+
batchMode
|
|
1383
|
+
}
|
|
1384
|
+
}).command;
|
|
1385
|
+
const prCmds = incomingPayments.map(
|
|
1386
|
+
(p) => createPaymentRequestCommand(this.db, {
|
|
1387
|
+
paymentRequest: mock_connector.toPaymentRequestInsert(
|
|
1388
|
+
mock_connector.assignAccount(p, account),
|
|
1389
|
+
batchId
|
|
1390
|
+
)
|
|
1391
|
+
}).command
|
|
1392
|
+
);
|
|
1393
|
+
await this.db.batch([batchCmd, ...prCmds]);
|
|
1394
|
+
const initiateBatch = () => {
|
|
1395
|
+
const args = { batchId, payments: batchedPayments };
|
|
1396
|
+
switch (paymentType) {
|
|
1587
1397
|
case "SEPA":
|
|
1588
|
-
return connector.
|
|
1398
|
+
return connector.initiateSEPABatch(args);
|
|
1589
1399
|
case "SWIFT":
|
|
1590
|
-
return connector.
|
|
1400
|
+
return connector.initiateForeignBatch(args);
|
|
1401
|
+
case "DOMESTIC":
|
|
1591
1402
|
default:
|
|
1592
|
-
|
|
1593
|
-
message: `Unsupported payment type for single payment: ${data.paymentType}`,
|
|
1594
|
-
code: "VALID-B-005",
|
|
1595
|
-
status: 400
|
|
1596
|
-
});
|
|
1403
|
+
return connector.initiateDomesticBatch(args);
|
|
1597
1404
|
}
|
|
1598
1405
|
};
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1406
|
+
let result;
|
|
1407
|
+
try {
|
|
1408
|
+
result = await initiateBatch();
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
const deletePrCmds = incomingPayments.map(
|
|
1411
|
+
(p) => drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1412
|
+
id: p.id,
|
|
1413
|
+
deletedAt: /* @__PURE__ */ new Date()
|
|
1414
|
+
}).command
|
|
1415
|
+
);
|
|
1416
|
+
const deleteBatchCmd = drizzle.upsertBatchCommand(this.db, {
|
|
1417
|
+
batch: {
|
|
1418
|
+
id: batchId,
|
|
1419
|
+
accountId: account.id,
|
|
1420
|
+
paymentType,
|
|
1421
|
+
status: "FAILED",
|
|
1422
|
+
deletedAt: /* @__PURE__ */ new Date()
|
|
1423
|
+
}
|
|
1424
|
+
}).command;
|
|
1425
|
+
await this.db.batch([deleteBatchCmd, ...deletePrCmds]);
|
|
1426
|
+
throw err;
|
|
1427
|
+
}
|
|
1428
|
+
const { authorizationUrls, payments: preparedPayments } = result;
|
|
1429
|
+
const isPerPaymentFallback = authorizationUrls.length === preparedPayments.length;
|
|
1430
|
+
const updatePrCmds = preparedPayments.map(
|
|
1431
|
+
(pp, i) => drizzle.updatePaymentRequestStatusCommand(this.db, {
|
|
1432
|
+
id: pp.id,
|
|
1433
|
+
bankRefId: pp.bankRefId,
|
|
1434
|
+
initiatedAt: pp.initiatedAt,
|
|
1435
|
+
authorizationUrl: isPerPaymentFallback ? authorizationUrls[i] : authorizationUrls[0]
|
|
1436
|
+
}).command
|
|
1437
|
+
);
|
|
1438
|
+
const updateBatchCmd = drizzle.upsertBatchCommand(this.db, {
|
|
1439
|
+
batch: {
|
|
1440
|
+
id: batchId,
|
|
1441
|
+
accountId: account.id,
|
|
1442
|
+
paymentType,
|
|
1443
|
+
authorizationUrls,
|
|
1444
|
+
metadata: result.metadata,
|
|
1445
|
+
status: "READY_TO_SIGN",
|
|
1446
|
+
batchPaymentInitiatedAt: /* @__PURE__ */ new Date()
|
|
1447
|
+
}
|
|
1448
|
+
}).command;
|
|
1449
|
+
await this.db.batch([updateBatchCmd, ...updatePrCmds]);
|
|
1450
|
+
return { batchId, authorizationUrls };
|
|
1603
1451
|
}
|
|
1604
1452
|
);
|
|
1605
1453
|
}
|
|
@@ -1612,6 +1460,7 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1612
1460
|
limit,
|
|
1613
1461
|
includeWorkflow,
|
|
1614
1462
|
includeBatchCounts,
|
|
1463
|
+
includePendingPaymentRequestCount,
|
|
1615
1464
|
filterIbans,
|
|
1616
1465
|
filterCurrencies,
|
|
1617
1466
|
filterBankRefIds
|
|
@@ -1629,7 +1478,8 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1629
1478
|
...a,
|
|
1630
1479
|
expiresAt: a.expiresAt || null,
|
|
1631
1480
|
workflow: null,
|
|
1632
|
-
batches: null
|
|
1481
|
+
batches: null,
|
|
1482
|
+
pendingPaymentRequestCount: null
|
|
1633
1483
|
};
|
|
1634
1484
|
if (includeWorkflow) {
|
|
1635
1485
|
let status;
|
|
@@ -1650,6 +1500,18 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
|
|
|
1650
1500
|
});
|
|
1651
1501
|
result.batches = batchCounts;
|
|
1652
1502
|
}
|
|
1503
|
+
if (includePendingPaymentRequestCount) {
|
|
1504
|
+
const [row] = await this.db.select({
|
|
1505
|
+
count: drizzleOrm.sql`count(*)`
|
|
1506
|
+
}).from(drizzle.tables.paymentRequest).where(
|
|
1507
|
+
drizzleOrm.and(
|
|
1508
|
+
drizzleOrm.eq(drizzle.tables.paymentRequest.accountId, a.id),
|
|
1509
|
+
drizzleOrm.eq(drizzle.tables.paymentRequest.status, "OPENED"),
|
|
1510
|
+
drizzleOrm.isNull(drizzle.tables.paymentRequest.deletedAt)
|
|
1511
|
+
)
|
|
1512
|
+
);
|
|
1513
|
+
result.pendingPaymentRequestCount = row?.count ?? 0;
|
|
1514
|
+
}
|
|
1653
1515
|
return result;
|
|
1654
1516
|
})
|
|
1655
1517
|
);
|
|
@@ -1829,27 +1691,18 @@ __decorateClass([
|
|
|
1829
1691
|
__decorateClass([
|
|
1830
1692
|
backendSdk.action("synchronize-accounts")
|
|
1831
1693
|
], BankServiceBase.prototype, "syncAccounts", 1);
|
|
1694
|
+
__decorateClass([
|
|
1695
|
+
backendSdk.action("update-payment-request-statuses")
|
|
1696
|
+
], BankServiceBase.prototype, "updatePaymentRequestStatuses", 1);
|
|
1832
1697
|
__decorateClass([
|
|
1833
1698
|
backendSdk.action("scheduled")
|
|
1834
1699
|
], BankServiceBase.prototype, "scheduled", 1);
|
|
1835
|
-
__decorateClass([
|
|
1836
|
-
backendSdk.action("update-batch-statuses")
|
|
1837
|
-
], BankServiceBase.prototype, "updateBatchStatuses", 1);
|
|
1838
|
-
__decorateClass([
|
|
1839
|
-
backendSdk.action("update-payment-statuses")
|
|
1840
|
-
], BankServiceBase.prototype, "updatePaymentStatuses", 1);
|
|
1841
1700
|
__decorateClass([
|
|
1842
1701
|
backendSdk.action("handle-authorization-callback")
|
|
1843
1702
|
], BankServiceBase.prototype, "handleAuthorizationCallback", 1);
|
|
1844
|
-
__decorateClass([
|
|
1845
|
-
backendSdk.action("add-payments-to-batch")
|
|
1846
|
-
], BankServiceBase.prototype, "addPaymentsToBatch", 1);
|
|
1847
1703
|
__decorateClass([
|
|
1848
1704
|
backendSdk.action("process-batch")
|
|
1849
1705
|
], BankServiceBase.prototype, "processBatch", 1);
|
|
1850
|
-
__decorateClass([
|
|
1851
|
-
backendSdk.action("queue-handler")
|
|
1852
|
-
], BankServiceBase.prototype, "queue", 1);
|
|
1853
1706
|
__decorateClass([
|
|
1854
1707
|
backendSdk.action("get-auth-uri")
|
|
1855
1708
|
], BankServiceBase.prototype, "getAuthUri", 1);
|
|
@@ -1865,6 +1718,9 @@ __decorateClass([
|
|
|
1865
1718
|
__decorateClass([
|
|
1866
1719
|
backendSdk.action("send-payment-sync")
|
|
1867
1720
|
], BankServiceBase.prototype, "sendPaymentSync", 1);
|
|
1721
|
+
__decorateClass([
|
|
1722
|
+
backendSdk.action("send-batch")
|
|
1723
|
+
], BankServiceBase.prototype, "sendBatch", 1);
|
|
1868
1724
|
__decorateClass([
|
|
1869
1725
|
backendSdk.action("get-bank-accounts")
|
|
1870
1726
|
], BankServiceBase.prototype, "getBankAccounts", 1);
|