@develit-services/bank 0.8.9 → 0.8.12

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 +425 -557
  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 +425 -557
  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)
931
828
  );
829
+ const allAuthorizedOrHigher = allPRs.every(
830
+ (pr) => [...authorizedOrHigher, ...terminalStatuses].includes(
831
+ pr.status
832
+ )
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,20 +1209,32 @@ 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);
1215
+ this._validatePaymentTypeAndCurrency(
1216
+ data.paymentType,
1217
+ data.currency,
1218
+ data.creditor
1219
+ );
1494
1220
  const { accounts } = await this._getAccounts();
1495
1221
  const account = accounts.find(
1496
- (acc) => acc.iban === incomingPayment.debtorIban && acc.currency === incomingPayment.currency
1222
+ (acc) => acc.iban === incomingPayment.debtorIban
1497
1223
  );
1498
1224
  if (!account) {
1499
1225
  throw backendSdk.createInternalError(null, {
1500
- message: `No account found for IBAN ${incomingPayment.debtorIban} with currency ${incomingPayment.currency}`,
1226
+ message: `No account found for IBAN ${incomingPayment.debtorIban}`,
1501
1227
  code: "VALID-B-004",
1502
1228
  status: 422
1503
1229
  });
1504
1230
  }
1231
+ if (account.currency !== data.currency) {
1232
+ throw backendSdk.createInternalError(null, {
1233
+ message: `Account ${account.iban} has currency ${account.currency}, but payment requires ${data.currency}`,
1234
+ code: "VALID-B-010",
1235
+ status: 422
1236
+ });
1237
+ }
1505
1238
  if (account.status !== "AUTHORIZED") {
1506
1239
  throw backendSdk.createInternalError(null, {
1507
1240
  message: `Account ${account.iban} is not authorized (status: ${account.status})`,
@@ -1519,42 +1252,107 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
1519
1252
  status: 422
1520
1253
  });
1521
1254
  }
1522
- this._validatePaymentTypeAndCurrency(
1523
- data.paymentType,
1524
- data.currency,
1525
- data.creditor
1526
- );
1527
1255
  const accountAssigned = mock_connector.assignAccount(incomingPayment, account);
1256
+ const batchedPayment = mock_connector.toBatchedPayment(accountAssigned);
1528
1257
  const { command: insertPaymentRequest } = createPaymentRequestCommand(
1529
1258
  this.db,
1530
1259
  { paymentRequest: mock_connector.toPaymentRequestInsert(accountAssigned, null) }
1531
1260
  );
1532
1261
  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 };
1262
+ const initiate = () => {
1263
+ switch (data.paymentType) {
1264
+ case "DOMESTIC":
1265
+ return connector.initiateDomesticPayment(batchedPayment);
1266
+ case "SEPA":
1267
+ return connector.initiateSEPAPayment(batchedPayment);
1268
+ case "SWIFT":
1269
+ return connector.initiateForeignPayment(batchedPayment);
1270
+ default:
1271
+ throw backendSdk.createInternalError(null, {
1272
+ message: `Unsupported payment type: ${data.paymentType}`,
1273
+ code: "VALID-B-005",
1274
+ status: 400
1275
+ });
1276
+ }
1277
+ };
1278
+ let initiated;
1279
+ try {
1280
+ initiated = await initiate();
1281
+ } catch (err) {
1282
+ await drizzle.updatePaymentRequestStatusCommand(this.db, {
1283
+ id: incomingPayment.id,
1284
+ deletedAt: /* @__PURE__ */ new Date()
1285
+ }).command.execute();
1286
+ throw err;
1287
+ }
1288
+ await drizzle.updatePaymentRequestStatusCommand(this.db, {
1289
+ id: incomingPayment.id,
1290
+ bankRefId: initiated.payment.bankRefId,
1291
+ initiatedAt: initiated.payment.initiatedAt,
1292
+ authorizationUrl: initiated.authorizationUrl
1293
+ }).command.execute();
1294
+ return {
1295
+ paymentRequestId: incomingPayment.id,
1296
+ authorizationUrl: initiated.authorizationUrl
1297
+ };
1538
1298
  }
1539
1299
  );
1540
1300
  }
1541
1301
  async sendPaymentSync(input) {
1302
+ const result = await this.sendPayment(input);
1303
+ return {
1304
+ ...result,
1305
+ data: result.data ? { authorizationUrl: result.data.authorizationUrl } : void 0
1306
+ };
1307
+ }
1308
+ async sendBatch(input) {
1542
1309
  return this.handleAction(
1543
- { data: input, schema: sendPaymentSyncInputSchema },
1544
- { successMessage: "Payment initiated successfully" },
1545
- async (data) => {
1546
- const incomingPayment = mock_connector.toIncomingPayment(data);
1310
+ { data: input, schema: sendBatchInputSchema },
1311
+ { successMessage: "Batch initiated successfully" },
1312
+ async ({ payments: paymentInputs }) => {
1313
+ const firstPayment = paymentInputs[0];
1314
+ const debtorIban = firstPayment.debtor.iban;
1315
+ const paymentType = firstPayment.paymentType;
1316
+ const currency = firstPayment.currency;
1317
+ for (const p of paymentInputs) {
1318
+ if (p.debtor.iban !== debtorIban || p.currency !== currency) {
1319
+ throw backendSdk.createInternalError(null, {
1320
+ message: "All payments in a batch must have the same debtor IBAN and currency",
1321
+ code: "VALID-B-011",
1322
+ status: 422
1323
+ });
1324
+ }
1325
+ if (p.paymentType !== paymentType) {
1326
+ throw backendSdk.createInternalError(null, {
1327
+ message: "All payments in a batch must have the same payment type",
1328
+ code: "VALID-B-012",
1329
+ status: 422
1330
+ });
1331
+ }
1332
+ }
1333
+ for (const p of paymentInputs) {
1334
+ this._validatePaymentTypeAndCurrency(
1335
+ p.paymentType,
1336
+ p.currency,
1337
+ p.creditor
1338
+ );
1339
+ }
1547
1340
  const { accounts } = await this._getAccounts();
1548
- const account = accounts.find(
1549
- (acc) => acc.iban === incomingPayment.debtorIban && acc.currency === incomingPayment.currency
1550
- );
1341
+ const account = accounts.find((acc) => acc.iban === debtorIban);
1551
1342
  if (!account) {
1552
1343
  throw backendSdk.createInternalError(null, {
1553
- message: `No account found for IBAN ${incomingPayment.debtorIban} with currency ${incomingPayment.currency}`,
1344
+ message: `No account found for IBAN ${debtorIban}`,
1554
1345
  code: "VALID-B-004",
1555
1346
  status: 422
1556
1347
  });
1557
1348
  }
1349
+ if (account.currency !== currency) {
1350
+ throw backendSdk.createInternalError(null, {
1351
+ message: `Account ${account.iban} has currency ${account.currency}, but payments require ${currency}`,
1352
+ code: "VALID-B-010",
1353
+ status: 422
1354
+ });
1355
+ }
1558
1356
  if (account.status !== "AUTHORIZED") {
1559
1357
  throw backendSdk.createInternalError(null, {
1560
1358
  message: `Account ${account.iban} is not authorized (status: ${account.status})`,
@@ -1562,44 +1360,106 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
1562
1360
  status: 422
1563
1361
  });
1564
1362
  }
1565
- const batchedPayment = mock_connector.toBatchedPayment(
1566
- mock_connector.assignAccount(incomingPayment, account)
1567
- );
1363
+ if (paymentInputs.length > account.batchSizeLimit) {
1364
+ throw backendSdk.createInternalError(null, {
1365
+ message: `Batch size ${paymentInputs.length} exceeds limit ${account.batchSizeLimit}`,
1366
+ code: "VALID-B-013",
1367
+ status: 422
1368
+ });
1369
+ }
1568
1370
  const connector = await this._initiateBankConnector({
1569
1371
  connectorKey: account.connectorKey
1570
1372
  });
1571
- if (!connector.supportsPaymentType(data.paymentType)) {
1373
+ if (!connector.supportsPaymentType(paymentType)) {
1572
1374
  throw backendSdk.createInternalError(null, {
1573
- message: `Connector ${account.connectorKey} does not support ${data.paymentType} payments yet`,
1375
+ message: `Connector ${account.connectorKey} does not support ${paymentType} payments yet`,
1574
1376
  code: "VALID-B-006",
1575
1377
  status: 422
1576
1378
  });
1577
1379
  }
1578
- this._validatePaymentTypeAndCurrency(
1579
- data.paymentType,
1580
- data.currency,
1581
- data.creditor
1380
+ const incomingPayments = paymentInputs.map(mock_connector.toIncomingPayment);
1381
+ const batchedPayments = incomingPayments.map(
1382
+ (p) => mock_connector.toBatchedPayment(mock_connector.assignAccount(p, account))
1582
1383
  );
1583
- const initiate = () => {
1584
- switch (data.paymentType) {
1585
- case "DOMESTIC":
1586
- return connector.initiateDomesticPayment(batchedPayment);
1384
+ const batchId = backendSdk.uuidv4();
1385
+ const batchMode = connector.supportsBatch(paymentType) ? "NATIVE" : "SINGLE";
1386
+ const batchCmd = drizzle.upsertBatchCommand(this.db, {
1387
+ batch: {
1388
+ id: batchId,
1389
+ authorizationUrls: [],
1390
+ accountId: account.id,
1391
+ paymentType,
1392
+ status: "PROCESSING",
1393
+ metadata: { sizeLimit: account.batchSizeLimit },
1394
+ batchMode
1395
+ }
1396
+ }).command;
1397
+ const prCmds = incomingPayments.map(
1398
+ (p) => createPaymentRequestCommand(this.db, {
1399
+ paymentRequest: mock_connector.toPaymentRequestInsert(
1400
+ mock_connector.assignAccount(p, account),
1401
+ batchId
1402
+ )
1403
+ }).command
1404
+ );
1405
+ await this.db.batch([batchCmd, ...prCmds]);
1406
+ const initiateBatch = () => {
1407
+ const args = { batchId, payments: batchedPayments };
1408
+ switch (paymentType) {
1587
1409
  case "SEPA":
1588
- return connector.initiateSEPAPayment(batchedPayment);
1410
+ return connector.initiateSEPABatch(args);
1589
1411
  case "SWIFT":
1590
- return connector.initiateForeignPayment(batchedPayment);
1412
+ return connector.initiateForeignBatch(args);
1413
+ case "DOMESTIC":
1591
1414
  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
- });
1415
+ return connector.initiateDomesticBatch(args);
1597
1416
  }
1598
1417
  };
1599
- const initiated = await initiate();
1600
- return {
1601
- authorizationUrl: initiated.authorizationUrl
1602
- };
1418
+ let result;
1419
+ try {
1420
+ result = await initiateBatch();
1421
+ } catch (err) {
1422
+ const deletePrCmds = incomingPayments.map(
1423
+ (p) => drizzle.updatePaymentRequestStatusCommand(this.db, {
1424
+ id: p.id,
1425
+ deletedAt: /* @__PURE__ */ new Date()
1426
+ }).command
1427
+ );
1428
+ const deleteBatchCmd = drizzle.upsertBatchCommand(this.db, {
1429
+ batch: {
1430
+ id: batchId,
1431
+ accountId: account.id,
1432
+ paymentType,
1433
+ status: "FAILED",
1434
+ deletedAt: /* @__PURE__ */ new Date()
1435
+ }
1436
+ }).command;
1437
+ await this.db.batch([deleteBatchCmd, ...deletePrCmds]);
1438
+ throw err;
1439
+ }
1440
+ const { authorizationUrls, payments: preparedPayments } = result;
1441
+ const isPerPaymentFallback = authorizationUrls.length === preparedPayments.length;
1442
+ const updatePrCmds = preparedPayments.map(
1443
+ (pp, i) => drizzle.updatePaymentRequestStatusCommand(this.db, {
1444
+ id: pp.id,
1445
+ bankRefId: pp.bankRefId,
1446
+ initiatedAt: pp.initiatedAt,
1447
+ authorizationUrl: isPerPaymentFallback ? authorizationUrls[i] : authorizationUrls[0]
1448
+ }).command
1449
+ );
1450
+ const updateBatchCmd = drizzle.upsertBatchCommand(this.db, {
1451
+ batch: {
1452
+ id: batchId,
1453
+ accountId: account.id,
1454
+ paymentType,
1455
+ authorizationUrls,
1456
+ metadata: result.metadata,
1457
+ status: "READY_TO_SIGN",
1458
+ batchPaymentInitiatedAt: /* @__PURE__ */ new Date()
1459
+ }
1460
+ }).command;
1461
+ await this.db.batch([updateBatchCmd, ...updatePrCmds]);
1462
+ return { batchId, authorizationUrls };
1603
1463
  }
1604
1464
  );
1605
1465
  }
@@ -1612,6 +1472,7 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
1612
1472
  limit,
1613
1473
  includeWorkflow,
1614
1474
  includeBatchCounts,
1475
+ includePendingPaymentRequestCount,
1615
1476
  filterIbans,
1616
1477
  filterCurrencies,
1617
1478
  filterBankRefIds
@@ -1629,7 +1490,8 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
1629
1490
  ...a,
1630
1491
  expiresAt: a.expiresAt || null,
1631
1492
  workflow: null,
1632
- batches: null
1493
+ batches: null,
1494
+ pendingPaymentRequestCount: null
1633
1495
  };
1634
1496
  if (includeWorkflow) {
1635
1497
  let status;
@@ -1650,6 +1512,18 @@ let BankServiceBase = class extends backendSdk.develitWorker(cloudflare_workers.
1650
1512
  });
1651
1513
  result.batches = batchCounts;
1652
1514
  }
1515
+ if (includePendingPaymentRequestCount) {
1516
+ const [row] = await this.db.select({
1517
+ count: drizzleOrm.sql`count(*)`
1518
+ }).from(drizzle.tables.paymentRequest).where(
1519
+ drizzleOrm.and(
1520
+ drizzleOrm.eq(drizzle.tables.paymentRequest.accountId, a.id),
1521
+ drizzleOrm.eq(drizzle.tables.paymentRequest.status, "OPENED"),
1522
+ drizzleOrm.isNull(drizzle.tables.paymentRequest.deletedAt)
1523
+ )
1524
+ );
1525
+ result.pendingPaymentRequestCount = row?.count ?? 0;
1526
+ }
1653
1527
  return result;
1654
1528
  })
1655
1529
  );
@@ -1829,27 +1703,18 @@ __decorateClass([
1829
1703
  __decorateClass([
1830
1704
  backendSdk.action("synchronize-accounts")
1831
1705
  ], BankServiceBase.prototype, "syncAccounts", 1);
1706
+ __decorateClass([
1707
+ backendSdk.action("update-payment-request-statuses")
1708
+ ], BankServiceBase.prototype, "updatePaymentRequestStatuses", 1);
1832
1709
  __decorateClass([
1833
1710
  backendSdk.action("scheduled")
1834
1711
  ], 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
1712
  __decorateClass([
1842
1713
  backendSdk.action("handle-authorization-callback")
1843
1714
  ], BankServiceBase.prototype, "handleAuthorizationCallback", 1);
1844
- __decorateClass([
1845
- backendSdk.action("add-payments-to-batch")
1846
- ], BankServiceBase.prototype, "addPaymentsToBatch", 1);
1847
1715
  __decorateClass([
1848
1716
  backendSdk.action("process-batch")
1849
1717
  ], BankServiceBase.prototype, "processBatch", 1);
1850
- __decorateClass([
1851
- backendSdk.action("queue-handler")
1852
- ], BankServiceBase.prototype, "queue", 1);
1853
1718
  __decorateClass([
1854
1719
  backendSdk.action("get-auth-uri")
1855
1720
  ], BankServiceBase.prototype, "getAuthUri", 1);
@@ -1865,6 +1730,9 @@ __decorateClass([
1865
1730
  __decorateClass([
1866
1731
  backendSdk.action("send-payment-sync")
1867
1732
  ], BankServiceBase.prototype, "sendPaymentSync", 1);
1733
+ __decorateClass([
1734
+ backendSdk.action("send-batch")
1735
+ ], BankServiceBase.prototype, "sendBatch", 1);
1868
1736
  __decorateClass([
1869
1737
  backendSdk.action("get-bank-accounts")
1870
1738
  ], BankServiceBase.prototype, "getBankAccounts", 1);