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