@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.
Files changed (43) hide show
  1. package/README.md +150 -275
  2. package/dist/database/schema.cjs +1 -1
  3. package/dist/database/schema.d.cts +2 -2
  4. package/dist/database/schema.d.mts +2 -2
  5. package/dist/database/schema.d.ts +2 -2
  6. package/dist/database/schema.mjs +1 -1
  7. package/dist/export/worker.cjs +405 -549
  8. package/dist/export/worker.d.cts +855 -86
  9. package/dist/export/worker.d.mts +855 -86
  10. package/dist/export/worker.d.ts +855 -86
  11. package/dist/export/worker.mjs +405 -549
  12. package/dist/export/workflows.cjs +16 -16
  13. package/dist/export/workflows.mjs +16 -16
  14. package/dist/export/wrangler.cjs +0 -12
  15. package/dist/export/wrangler.d.cts +1 -8
  16. package/dist/export/wrangler.d.mts +1 -8
  17. package/dist/export/wrangler.d.ts +1 -8
  18. package/dist/export/wrangler.mjs +0 -12
  19. package/dist/shared/{bank.JVlyPAAb.cjs → bank.BBXoZ5QU.cjs} +26 -13
  20. package/dist/shared/{bank.DRTuKO8S.d.ts → bank.BCop1cDT.d.mts} +4 -15
  21. package/dist/shared/{bank.Bg3Pdwm4.cjs → bank.BsIiXsFH.cjs} +5 -13
  22. package/dist/shared/{bank.BoZtXQpG.mjs → bank.CR0UlyRi.mjs} +1 -1
  23. package/dist/shared/{bank.CtnsGHM8.cjs → bank.CUvVxlHy.cjs} +126 -152
  24. package/dist/shared/{bank.DJnDSYqE.cjs → bank.CVi6R7fr.cjs} +1 -1
  25. package/dist/shared/{bank.C6jjS1Pl.mjs → bank.CXBeULUL.mjs} +25 -14
  26. package/dist/shared/{bank.DT6bg8k5.cjs → bank.Cev1E9sk.cjs} +2 -2
  27. package/dist/shared/{bank.B-NJB8GB.d.cts → bank.Cj2Goq7s.d.cts} +104 -176
  28. package/dist/shared/{bank.B-NJB8GB.d.mts → bank.Cj2Goq7s.d.mts} +104 -176
  29. package/dist/shared/{bank.B-NJB8GB.d.ts → bank.Cj2Goq7s.d.ts} +104 -176
  30. package/dist/shared/{bank.BzobShUU.d.cts → bank.CjTfEd1Q.d.cts} +4 -15
  31. package/dist/shared/{bank.BtszLapg.mjs → bank.D-O_gmmZ.mjs} +127 -152
  32. package/dist/shared/{bank.BP_3WMIF.d.cts → bank.DMjtitKo.d.cts} +0 -1
  33. package/dist/shared/{bank.BP_3WMIF.d.mts → bank.DMjtitKo.d.mts} +0 -1
  34. package/dist/shared/{bank.BP_3WMIF.d.ts → bank.DMjtitKo.d.ts} +0 -1
  35. package/dist/shared/{bank.CAVvvZZO.d.mts → bank.OlDt7dpb.d.ts} +4 -15
  36. package/dist/shared/{bank.CbAwwIhZ.mjs → bank.vz1uqEYa.mjs} +5 -11
  37. package/dist/shared/{bank.B5bZRvgq.mjs → bank.xB9eTN77.mjs} +2 -2
  38. package/dist/types.cjs +6 -6
  39. package/dist/types.d.cts +26 -45
  40. package/dist/types.d.mts +26 -45
  41. package/dist/types.d.ts +26 -45
  42. package/dist/types.mjs +3 -3
  43. package/package.json +1 -1
@@ -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, u as upsertBatchCommand, a as getPaymentRequestsByBatchIdQuery, e as updatePaymentRequestStatusCommand, g as getBatchByIdQuery, i as importAesKey, f as createPaymentCommand, b as getAccountByIdQuery } from '../shared/bank.B5bZRvgq.mjs';
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, m as mapFinbricksTransactionStatus, g as toIncomingPayment, d as assignAccount, t as toBatchedPayment, a as FinbricksClient, F as FINBRICKS_ENDPOINTS, h as toPaymentRequestInsert } from '../shared/bank.BtszLapg.mjs';
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, e as PAYMENT_TYPES, a as CONNECTOR_KEYS, B as BATCH_STATUSES, d as PAYMENT_STATUSES, P as PAYMENT_DIRECTIONS, i as accountInsertSchema } from '../shared/bank.C6jjS1Pl.mjs';
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.BoZtXQpG.mjs';
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
- openCount: sql`SUM(CASE WHEN ${tables.batch.status} = 'OPEN' THEN 1 ELSE 0 END)`.as(
109
- "openCount"
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
- openCount: 0,
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(inArray(tables.paymentRequest.batchId, batchIds)) : [];
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 NON_BATCH_CONNECTOR_KEYS = ["DBU", "CREDITAS"];
348
- const getPendingNonBatchPaymentRequestsQuery = (db) => db.select().from(tables.paymentRequest).where(
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
- "PREPARED",
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
- paymentId: z.string().uuid().optional(),
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([z.enum(PAYMENT_STATUSES), z.enum(PAYMENT_STATUSES).array()]).optional(),
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
- async scheduled(controller) {
747
- if (controller.cron === this.env.CRON_BATCH_STATUSES) {
748
- console.log("Scheduled CRON batch statuses");
749
- await this.updateBatchStatuses();
750
- }
751
- if (controller.cron === this.env.CRON_PAYMENT_STATUSES) {
752
- console.log("Scheduled CRON payment statuses");
753
- await this.updatePaymentStatuses();
754
- }
755
- }
756
- async _resolveSingleBatch(batch, connector) {
757
- const previousStatus = batch.status;
758
- const { status: currentStatus, apiResponse } = await connector.getBatchStatus({
759
- batchId: batch.id
760
- });
761
- let statusChanged = false;
762
- if (previousStatus !== currentStatus || !batch.statusResponse) {
763
- await upsertBatchCommand(this.db, {
764
- batch: {
765
- ...batch,
766
- status: currentStatus,
767
- statusResponse: apiResponse
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
- const allTerminal = freshPRs.every(
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
- return {
838
- batchId: batch.id,
839
- previousStatus,
840
- currentStatus,
841
- statusChanged
842
- };
843
- }
844
- async updateBatchStatuses(input) {
845
- return this.handleAction(
846
- { data: input, schema: updateBatchStatusesInputSchema },
847
- { successMessage: "Batch statuses updated" },
848
- async (validatedInput) => {
849
- let pendingBatches;
850
- if (validatedInput?.batchId) {
851
- const batch = await getBatchByIdQuery(this.db, {
852
- batchId: validatedInput.batchId
853
- });
854
- if (!batch) {
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
- }).command;
892
- const allCmds = [batchCmd, ...prCmds];
893
- if (allCmds.length > 0) {
894
- await this.db.batch(allCmds);
895
- }
896
- continue;
776
+ });
897
777
  }
898
- const batches = batchesByConnector.get(account.connectorKey) || [];
899
- batches.push(batch);
900
- batchesByConnector.set(account.connectorKey, batches);
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 updatePaymentStatuses() {
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 pendingRequests = await getPendingNonBatchPaymentRequestsQuery(
937
- this.db
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 requestsByConnector = /* @__PURE__ */ new Map();
943
- for (const pr of pendingRequests) {
944
- const list = requestsByConnector.get(pr.connectorKey) || [];
945
- list.push(pr);
946
- requestsByConnector.set(pr.connectorKey, list);
947
- }
948
- let processed = 0;
949
- let statusChanged = 0;
950
- for (const [connectorKey, requests] of requestsByConnector) {
951
- let connector;
952
- try {
953
- connector = await this._initiateBankConnector({ connectorKey });
954
- } catch (err) {
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
- const affectedBatchIds = [
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 ({ paymentId, batchId }) => {
1022
- let targetBatchId = null;
1023
- let paymentsUpdated = 0;
1024
- if (paymentId) {
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: paymentId
898
+ paymentRequestId
1027
899
  });
1028
- if (!pr) {
1029
- throw createInternalError(null, {
1030
- message: `Payment request not found: ${paymentId}`,
1031
- code: "DB-B-008",
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
- const allDone = allPayments.every(
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
- return {
1083
- paymentsUpdated,
1084
- batchId: targetBatchId,
1085
- batchSigned
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 singlePayments = paymentsOfType.filter(
1125
- (p) => p.sendAsSinglePayment === true
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
- for (const singlePayment of singlePayments) {
1134
- const batchId = uuidv4();
1135
- const { command: upsertBatch } = upsertBatchCommand(this.db, {
1136
- batch: {
1137
- id: batchId,
1138
- authorizationUrls: [],
1139
- accountId: acc.id,
1140
- paymentType: singlePayment.paymentType,
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
- if (regularPayments.length > 0) {
1155
- const openBatches = await getAccountAccumulatingBatchesQuery(
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
- let batchId;
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: "COMPLETED",
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: "paymentCompleted",
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 queued successfully" },
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
- await this.pushToQueue(
1532
- this.env.PAYMENTS_READY_TO_BATCH_QUEUE,
1533
- { paymentId: incomingPayment.id }
1534
- );
1535
- return { paymentId: incomingPayment.id };
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: sendPaymentSyncInputSchema },
1542
- { successMessage: "Payment initiated successfully" },
1543
- async (data) => {
1544
- const incomingPayment = toIncomingPayment(data);
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 === incomingPayment.debtorIban && acc.currency === incomingPayment.currency
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 ${incomingPayment.debtorIban} with currency ${incomingPayment.currency}`,
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
- const batchedPayment = toBatchedPayment(
1564
- assignAccount(incomingPayment, account)
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(data.paymentType)) {
1352
+ if (!connector.supportsPaymentType(paymentType)) {
1570
1353
  throw createInternalError(null, {
1571
- message: `Connector ${account.connectorKey} does not support ${data.paymentType} payments yet`,
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
- this._validatePaymentTypeAndCurrency(
1577
- data.paymentType,
1578
- data.currency,
1579
- data.creditor
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 initiate = () => {
1582
- switch (data.paymentType) {
1583
- case "DOMESTIC":
1584
- return connector.initiateDomesticPayment(batchedPayment);
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.initiateSEPAPayment(batchedPayment);
1396
+ return connector.initiateSEPABatch(args);
1587
1397
  case "SWIFT":
1588
- return connector.initiateForeignPayment(batchedPayment);
1398
+ return connector.initiateForeignBatch(args);
1399
+ case "DOMESTIC":
1589
1400
  default:
1590
- throw createInternalError(null, {
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
- const initiated = await initiate();
1598
- return {
1599
- authorizationUrl: initiated.authorizationUrl
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);