@develit-services/bank 0.8.9 → 0.8.10

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