@develit-services/bank 0.3.60 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/schema.cjs +9 -9
- package/dist/database/schema.d.cts +4 -5
- package/dist/database/schema.d.mts +4 -5
- package/dist/database/schema.d.ts +4 -5
- package/dist/database/schema.mjs +3 -3
- package/dist/export/worker.cjs +91 -85
- package/dist/export/worker.d.cts +23 -766
- package/dist/export/worker.d.mts +23 -766
- package/dist/export/worker.d.ts +23 -766
- package/dist/export/worker.mjs +72 -66
- package/dist/export/workflows.cjs +215 -211
- package/dist/export/workflows.d.cts +4 -4
- package/dist/export/workflows.d.mts +4 -4
- package/dist/export/workflows.d.ts +4 -4
- package/dist/export/workflows.mjs +210 -206
- package/dist/export/wrangler.d.cts +3 -1
- package/dist/export/wrangler.d.mts +3 -1
- package/dist/export/wrangler.d.ts +3 -1
- package/dist/shared/bank.B51e8oDT.mjs +104 -0
- package/dist/shared/{bank.Dh_H_5rC.d.cts → bank.BJvgLwyZ.d.cts} +2 -0
- package/dist/shared/{bank.Dh_H_5rC.d.mts → bank.BJvgLwyZ.d.mts} +2 -0
- package/dist/shared/{bank.Dh_H_5rC.d.ts → bank.BJvgLwyZ.d.ts} +2 -0
- package/dist/shared/bank.BUSlmr6r.mjs +13 -0
- package/dist/shared/{bank.CMbfx0u7.cjs → bank.BeIpkWR-.cjs} +3 -14
- package/dist/shared/bank.Bg3Pdwm4.cjs +44 -0
- package/dist/shared/bank.CDoYUKBx.d.cts +748 -0
- package/dist/shared/bank.CDoYUKBx.d.mts +748 -0
- package/dist/shared/bank.CDoYUKBx.d.ts +748 -0
- package/dist/shared/{bank.CNEv3Rac.cjs → bank.CfSZTfWS.cjs} +634 -179
- package/dist/shared/{bank.XJJV8p4b.d.cts → bank.CsJuzqZH.d.cts} +2401 -2117
- package/dist/shared/{bank.XJJV8p4b.d.mts → bank.CsJuzqZH.d.mts} +2401 -2117
- package/dist/shared/{bank.XJJV8p4b.d.ts → bank.CsJuzqZH.d.ts} +2401 -2117
- package/dist/shared/bank.DEvSNsEs.cjs +114 -0
- package/dist/shared/{bank.DgGcjhmM.mjs → bank.DinCwx0P.mjs} +627 -179
- package/dist/shared/bank.Dpx6PvkA.mjs +33 -0
- package/dist/shared/bank.c38V_FCq.cjs +15 -0
- package/dist/shared/{bank.BqGzJNC6.mjs → bank.qc8ALZwm.mjs} +4 -14
- package/dist/types.cjs +51 -33
- package/dist/types.d.cts +164 -39
- package/dist/types.d.mts +164 -39
- package/dist/types.d.ts +164 -39
- package/dist/types.mjs +4 -3
- package/package.json +7 -10
- package/dist/shared/bank.CXpc8168.cjs +0 -212
- package/dist/shared/bank.DSsVeR3F.mjs +0 -202
package/dist/export/worker.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { uuidv4, first, bankAccountMetadataSchema, workflowInstanceStatusSchema, develitWorker, createInternalError, action, service } from '@develit-io/backend-sdk';
|
|
2
2
|
import { WorkerEntrypoint } from 'cloudflare:workers';
|
|
3
3
|
import { drizzle } from 'drizzle-orm/d1';
|
|
4
|
-
import { t as tables } from '../shared/bank.
|
|
4
|
+
import { t as tables, e as encrypt, i as importAesKey, b as getCredentialsByAccountId, u as upsertBatchCommand, g as getBatchByIdQuery, d as createPaymentCommand, a as getAccountByIdQuery } from '../shared/bank.B51e8oDT.mjs';
|
|
5
|
+
import { i as initiateConnector, t as toIncomingPayment, d as assignAccount, e as toBatchedPayment } from '../shared/bank.DinCwx0P.mjs';
|
|
5
6
|
import { z } from 'zod';
|
|
6
|
-
import { I as INSTRUCTION_PRIORITIES, C as CHARGE_BEARERS, P as PAYMENT_TYPES, d as CONNECTOR_KEYS, B as BATCH_STATUSES, a as PAYMENT_STATUSES, b as PAYMENT_DIRECTIONS, f as accountInsertSchema } from '../shared/bank.
|
|
7
|
+
import { I as INSTRUCTION_PRIORITIES, C as CHARGE_BEARERS, P as PAYMENT_TYPES, d as CONNECTOR_KEYS, B as BATCH_STATUSES, a as PAYMENT_STATUSES, b as PAYMENT_DIRECTIONS, f as accountInsertSchema } from '../shared/bank.qc8ALZwm.mjs';
|
|
7
8
|
import { CURRENCY_CODES } from '@develit-io/general-codes';
|
|
8
9
|
import 'date-fns';
|
|
9
|
-
import { eq, sql, inArray, and, asc, desc, gte, lte } from 'drizzle-orm';
|
|
10
10
|
import 'jose';
|
|
11
|
+
import { eq, sql, inArray, and, asc, desc, gte, lte } from 'drizzle-orm';
|
|
11
12
|
import 'node:crypto';
|
|
12
|
-
import
|
|
13
|
-
import 'drizzle-orm/sqlite-core';
|
|
13
|
+
import '../shared/bank.BUSlmr6r.mjs';
|
|
14
14
|
import 'drizzle-orm/relations';
|
|
15
|
+
import 'drizzle-orm/sqlite-core';
|
|
15
16
|
import 'drizzle-zod';
|
|
16
17
|
|
|
17
18
|
const upsertAccountCommand = (db, { account }) => {
|
|
@@ -234,8 +235,8 @@ const getOttQuery = async (db, { ott }) => {
|
|
|
234
235
|
return await db.select().from(tables.ott).where(eq(tables.ott.oneTimeToken, ott)).get();
|
|
235
236
|
};
|
|
236
237
|
|
|
237
|
-
const seperateSupportedPayments = (
|
|
238
|
-
const [supportedPayments, unsupportedPayments] =
|
|
238
|
+
const seperateSupportedPayments = (incomingPayments, accounts) => {
|
|
239
|
+
const [supportedPayments, unsupportedPayments] = incomingPayments.reduce(
|
|
239
240
|
([valid, invalid], payment) => {
|
|
240
241
|
const isValid = accounts.some(
|
|
241
242
|
(acc) => acc.iban === payment.debtorIban && acc.currency === payment.currency
|
|
@@ -270,7 +271,8 @@ const sendPaymentInputSchema = z.object({
|
|
|
270
271
|
message: z.string().optional(),
|
|
271
272
|
creditor: bankAccountMetadataSchema,
|
|
272
273
|
debtor: bankAccountMetadataSchema,
|
|
273
|
-
purpose: z.string().optional()
|
|
274
|
+
purpose: z.string().optional(),
|
|
275
|
+
sendAsSinglePayment: z.boolean().optional()
|
|
274
276
|
});
|
|
275
277
|
|
|
276
278
|
const getAuthUriInputSchema = z.object({
|
|
@@ -673,32 +675,9 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
|
|
|
673
675
|
}) {
|
|
674
676
|
return this.handleAction(null, {}, async () => {
|
|
675
677
|
this.logInput({ paymentsToBatch });
|
|
676
|
-
const
|
|
677
|
-
(payment) => {
|
|
678
|
-
return {
|
|
679
|
-
id: uuidv4(),
|
|
680
|
-
correlationId: payment.correlationId,
|
|
681
|
-
refId: payment.refId,
|
|
682
|
-
amount: payment.amount,
|
|
683
|
-
direction: "OUTGOING",
|
|
684
|
-
paymentType: payment.paymentType,
|
|
685
|
-
currency: payment.currency,
|
|
686
|
-
status: "CREATED",
|
|
687
|
-
initiatedAt: /* @__PURE__ */ new Date(),
|
|
688
|
-
processedAt: null,
|
|
689
|
-
vs: payment.vs || null,
|
|
690
|
-
ss: payment.ss || null,
|
|
691
|
-
ks: payment.ks || null,
|
|
692
|
-
message: payment.message || null,
|
|
693
|
-
creditor: payment.creditor,
|
|
694
|
-
creditorIban: payment.creditor.iban || null,
|
|
695
|
-
debtor: payment.debtor,
|
|
696
|
-
debtorIban: payment.debtor.iban || null
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
);
|
|
678
|
+
const incomingPayments = paymentsToBatch.map(toIncomingPayment);
|
|
700
679
|
const accounts = await this._getAccounts();
|
|
701
|
-
const { supportedPayments, unsupportedPayments } = seperateSupportedPayments(
|
|
680
|
+
const { supportedPayments, unsupportedPayments } = seperateSupportedPayments(incomingPayments, accounts);
|
|
702
681
|
if (unsupportedPayments.length > 0) {
|
|
703
682
|
this.logError({ unsupportedPayments });
|
|
704
683
|
await this.pushToQueue(
|
|
@@ -720,45 +699,72 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
|
|
|
720
699
|
);
|
|
721
700
|
}
|
|
722
701
|
for (const acc of accounts) {
|
|
723
|
-
const
|
|
702
|
+
const accountAssignedPayments = supportedPayments.filter(
|
|
724
703
|
(payment) => payment.debtorIban === acc.iban && payment.currency === acc.currency
|
|
725
|
-
).map((payment) => (
|
|
726
|
-
|
|
727
|
-
connectorKey: acc.connectorKey,
|
|
728
|
-
accountId: acc.id
|
|
729
|
-
}));
|
|
730
|
-
if (newPayments.length === 0) {
|
|
704
|
+
).map((payment) => assignAccount(payment, acc));
|
|
705
|
+
if (accountAssignedPayments.length === 0) {
|
|
731
706
|
continue;
|
|
732
707
|
}
|
|
733
708
|
this.log({
|
|
734
|
-
message: `\u{1F4B0} Processing ${
|
|
735
|
-
});
|
|
736
|
-
const openBatches = await getAccountOpenBatchesQuery(this.db, {
|
|
737
|
-
accountId: acc.id
|
|
709
|
+
message: `\u{1F4B0} Processing ${accountAssignedPayments.length} payments for account (${acc.iban}, ${acc.currency})`
|
|
738
710
|
});
|
|
739
|
-
const
|
|
740
|
-
(
|
|
711
|
+
const singlePayments = accountAssignedPayments.filter(
|
|
712
|
+
(p) => p.sendAsSinglePayment === true
|
|
741
713
|
);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
714
|
+
const regularPayments = accountAssignedPayments.filter(
|
|
715
|
+
(p) => p.sendAsSinglePayment !== true
|
|
716
|
+
);
|
|
717
|
+
for (const singlePayment of singlePayments) {
|
|
718
|
+
const batchedPayment = toBatchedPayment(singlePayment);
|
|
719
|
+
const { command } = upsertBatchCommand(this.db, {
|
|
720
|
+
batch: {
|
|
721
|
+
id: uuidv4(),
|
|
722
|
+
authorizationUrls: [],
|
|
723
|
+
accountId: acc.id,
|
|
724
|
+
payments: [batchedPayment],
|
|
725
|
+
status: "OPEN",
|
|
726
|
+
metadata: {
|
|
727
|
+
sizeLimit: 1
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
await command.execute();
|
|
732
|
+
this.log({
|
|
733
|
+
message: `\u2728 Created single payment batch for account ${acc.id}`
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (regularPayments.length > 0) {
|
|
737
|
+
const openBatches = await getAccountOpenBatchesQuery(this.db, {
|
|
738
|
+
accountId: acc.id
|
|
739
|
+
});
|
|
740
|
+
const availableBatch = openBatches.find(
|
|
741
|
+
(batch) => batch.payments.length < (batch.metadata?.sizeLimit ?? acc.batchSizeLimit)
|
|
742
|
+
);
|
|
743
|
+
this.log({
|
|
744
|
+
message: availableBatch ? `\u{1F504} Found existing OPEN batch for account ${acc.id}, merging ${availableBatch.payments.length} existing + ${regularPayments.length} new payments` : `\u2728 Creating new batch for account ${acc.id} with ${regularPayments.length} payments`
|
|
745
|
+
});
|
|
746
|
+
const batchedPayments = regularPayments.map(toBatchedPayment);
|
|
747
|
+
const { command } = upsertBatchCommand(this.db, {
|
|
748
|
+
batch: availableBatch ? {
|
|
749
|
+
...availableBatch,
|
|
750
|
+
payments: [...availableBatch.payments, ...batchedPayments]
|
|
751
|
+
} : {
|
|
752
|
+
id: uuidv4(),
|
|
753
|
+
authorizationUrls: [],
|
|
754
|
+
accountId: acc.id,
|
|
755
|
+
payments: batchedPayments,
|
|
756
|
+
status: "OPEN",
|
|
757
|
+
metadata: {
|
|
758
|
+
sizeLimit: acc.batchSizeLimit
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
const upsertedBatch = await command.execute();
|
|
763
|
+
this.log({
|
|
764
|
+
message: `\u2705 Batch upserted successfully for account ${acc.id}`
|
|
765
|
+
});
|
|
766
|
+
return upsertedBatch;
|
|
767
|
+
}
|
|
762
768
|
}
|
|
763
769
|
});
|
|
764
770
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const backendSdk = require('@develit-io/backend-sdk');
|
|
3
4
|
const cloudflare_workers = require('cloudflare:workers');
|
|
4
5
|
const cloudflare_workflows = require('cloudflare:workflows');
|
|
5
6
|
const d1 = require('drizzle-orm/d1');
|
|
6
|
-
const drizzle = require('../shared/bank.
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const drizzle = require('../shared/bank.DEvSNsEs.cjs');
|
|
8
|
+
const batchLifecycle = require('../shared/bank.Bg3Pdwm4.cjs');
|
|
9
|
+
const mock_connector = require('../shared/bank.CfSZTfWS.cjs');
|
|
9
10
|
const drizzleOrm = require('drizzle-orm');
|
|
10
|
-
require('
|
|
11
|
-
require('../shared/bank.
|
|
12
|
-
require('drizzle-orm/sqlite-core');
|
|
11
|
+
require('../shared/bank.c38V_FCq.cjs');
|
|
12
|
+
require('../shared/bank.BeIpkWR-.cjs');
|
|
13
13
|
require('drizzle-orm/relations');
|
|
14
|
+
require('drizzle-orm/sqlite-core');
|
|
15
|
+
require('date-fns');
|
|
14
16
|
require('@develit-io/general-codes');
|
|
15
17
|
require('jose');
|
|
16
18
|
require('node:crypto');
|
|
@@ -39,6 +41,206 @@ function pushToQueue$1(queue, message) {
|
|
|
39
41
|
}))
|
|
40
42
|
);
|
|
41
43
|
}
|
|
44
|
+
class BankProcessBatch extends cloudflare_workers.WorkflowEntrypoint {
|
|
45
|
+
async run(event, step) {
|
|
46
|
+
const { batchId } = event.payload;
|
|
47
|
+
const db = d1.drizzle(this.env.BANK_D1, { schema: drizzle.tables });
|
|
48
|
+
const batch = await step.do("load batch", async () => {
|
|
49
|
+
const batch2 = await drizzle.getBatchByIdQuery(db, { batchId });
|
|
50
|
+
if (!batch2) {
|
|
51
|
+
throw new cloudflare_workflows.NonRetryableError(`Batch not found`);
|
|
52
|
+
}
|
|
53
|
+
if (batchLifecycle.isBatchSigned(batch2))
|
|
54
|
+
throw new cloudflare_workflows.NonRetryableError(`Batch already signed`);
|
|
55
|
+
return batch2;
|
|
56
|
+
});
|
|
57
|
+
await step.do("lock batch", async () => {
|
|
58
|
+
const paymentsChecksum = drizzle.checksum(batch.payments);
|
|
59
|
+
if (batch.paymentsChecksum && batch.paymentsChecksum !== paymentsChecksum)
|
|
60
|
+
throw new cloudflare_workflows.NonRetryableError(
|
|
61
|
+
`Batch payments have been modified externally, manual review needed`
|
|
62
|
+
);
|
|
63
|
+
if (batchLifecycle.isBatchFailed(batch))
|
|
64
|
+
throw new cloudflare_workflows.NonRetryableError(
|
|
65
|
+
`Batch is in FAILED status and cannot be processed, manual review needed`
|
|
66
|
+
);
|
|
67
|
+
if (batchLifecycle.isBatchOpen(batch)) {
|
|
68
|
+
const updatedBatch = await drizzle.upsertBatchCommand(db, {
|
|
69
|
+
batch: {
|
|
70
|
+
...batch,
|
|
71
|
+
status: "PROCESSING",
|
|
72
|
+
paymentsChecksum
|
|
73
|
+
}
|
|
74
|
+
}).command.execute().then(backendSdk.first);
|
|
75
|
+
return { status: updatedBatch.status };
|
|
76
|
+
}
|
|
77
|
+
return { status: batch.status };
|
|
78
|
+
});
|
|
79
|
+
const batchResult = await step.do(
|
|
80
|
+
"initiate batch with payments",
|
|
81
|
+
{
|
|
82
|
+
retries: { limit: 5, delay: 2e3, backoff: "constant" },
|
|
83
|
+
timeout: "30 seconds"
|
|
84
|
+
},
|
|
85
|
+
async () => {
|
|
86
|
+
if (batch.batchPaymentInitiatedAt) {
|
|
87
|
+
return {
|
|
88
|
+
authorizationUrls: batch.authorizationUrls,
|
|
89
|
+
preparedPayments: batch.payments.map(
|
|
90
|
+
(p) => mock_connector.toPreparedPayment(p, void 0, batch.batchPaymentInitiatedAt)
|
|
91
|
+
),
|
|
92
|
+
metadata: batch.metadata
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const account = await drizzle.getAccountByIdQuery(db, {
|
|
96
|
+
accountId: batch.accountId
|
|
97
|
+
});
|
|
98
|
+
if (!account) {
|
|
99
|
+
throw new cloudflare_workflows.NonRetryableError(`Account not found: ${batch.accountId}`);
|
|
100
|
+
}
|
|
101
|
+
const encryptionKey = await drizzle.importAesKey(this.env.ENCRYPTION_KEY);
|
|
102
|
+
const credentials = await drizzle.getCredentialsByAccountId(db, encryptionKey, {
|
|
103
|
+
accountId: account.id
|
|
104
|
+
});
|
|
105
|
+
if (!credentials) {
|
|
106
|
+
throw new cloudflare_workflows.NonRetryableError(
|
|
107
|
+
`No credentials found for account: ${account.id}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (credentials.expiresAt < /* @__PURE__ */ new Date()) {
|
|
111
|
+
throw new cloudflare_workflows.NonRetryableError(
|
|
112
|
+
`Credentials have expired for account: ${account.id}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const connector = mock_connector.initiateConnector({
|
|
116
|
+
env: this.env,
|
|
117
|
+
bank: account.connectorKey,
|
|
118
|
+
connectedAccounts: [
|
|
119
|
+
{
|
|
120
|
+
...account,
|
|
121
|
+
iban: account.iban,
|
|
122
|
+
token: credentials.value
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
});
|
|
126
|
+
const result = await connector.initiateBatch({
|
|
127
|
+
batchId: batch.id,
|
|
128
|
+
payments: batch.payments
|
|
129
|
+
});
|
|
130
|
+
const {
|
|
131
|
+
authorizationUrls,
|
|
132
|
+
payments: preparedPayments,
|
|
133
|
+
metadata
|
|
134
|
+
} = result;
|
|
135
|
+
if (!authorizationUrls || authorizationUrls.length === 0) {
|
|
136
|
+
throw new Error("Failed to retrieve authorization URLs");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
authorizationUrls,
|
|
140
|
+
preparedPayments,
|
|
141
|
+
metadata
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
await step.do("update batch to ready to sign", async () => {
|
|
146
|
+
const upsertBatch = drizzle.upsertBatchCommand(db, {
|
|
147
|
+
batch: {
|
|
148
|
+
...batch,
|
|
149
|
+
authorizationUrls: batchResult.authorizationUrls,
|
|
150
|
+
metadata: batchResult.metadata,
|
|
151
|
+
status: "READY_TO_SIGN",
|
|
152
|
+
batchPaymentInitiatedAt: /* @__PURE__ */ new Date()
|
|
153
|
+
}
|
|
154
|
+
}).command;
|
|
155
|
+
await upsertBatch.execute();
|
|
156
|
+
return {
|
|
157
|
+
status: "READY_TO_SIGN",
|
|
158
|
+
paymentsPrepared: batchResult.preparedPayments.length
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
const isMockConnector = batchResult.preparedPayments.some(
|
|
162
|
+
(p) => p.connectorKey === "MOCK"
|
|
163
|
+
);
|
|
164
|
+
if (isMockConnector) {
|
|
165
|
+
await step.do(
|
|
166
|
+
"mock: create completed payments and update batch",
|
|
167
|
+
async () => {
|
|
168
|
+
const processedAt = /* @__PURE__ */ new Date();
|
|
169
|
+
const completedPayments = batchResult.preparedPayments.map(
|
|
170
|
+
(p) => mock_connector.toCompletedPayment(p, "COMPLETED", `MOCK-${backendSdk.uuidv4()}`, processedAt)
|
|
171
|
+
);
|
|
172
|
+
const createCommands = completedPayments.map(
|
|
173
|
+
(payment) => drizzle.createPaymentCommand(db, { payment }).command
|
|
174
|
+
);
|
|
175
|
+
const updateBatchCommand = drizzle.upsertBatchCommand(db, {
|
|
176
|
+
batch: {
|
|
177
|
+
...batch,
|
|
178
|
+
status: "SIGNED",
|
|
179
|
+
authorizationUrls: batchResult.authorizationUrls,
|
|
180
|
+
metadata: batchResult.metadata,
|
|
181
|
+
batchPaymentInitiatedAt: /* @__PURE__ */ new Date()
|
|
182
|
+
}
|
|
183
|
+
}).command;
|
|
184
|
+
if (createCommands.length > 0) {
|
|
185
|
+
await db.batch([updateBatchCommand, ...createCommands]);
|
|
186
|
+
} else {
|
|
187
|
+
await updateBatchCommand.execute();
|
|
188
|
+
}
|
|
189
|
+
const eventsToEmit = completedPayments.map(
|
|
190
|
+
(payment) => ({
|
|
191
|
+
eventType: "BANK_PAYMENT",
|
|
192
|
+
eventSignal: "paymentCompleted",
|
|
193
|
+
bankPayment: payment,
|
|
194
|
+
metadata: {
|
|
195
|
+
correlationId: backendSdk.uuidv4(),
|
|
196
|
+
entityId: payment.id,
|
|
197
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
await pushToQueue$1(
|
|
202
|
+
this.env.QUEUE_BUS_QUEUE,
|
|
203
|
+
eventsToEmit
|
|
204
|
+
);
|
|
205
|
+
return {
|
|
206
|
+
paymentsCreated: completedPayments.length,
|
|
207
|
+
eventsEmitted: completedPayments.length
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (!isMockConnector) {
|
|
213
|
+
await step.do("enqueue authorization email", async () => {
|
|
214
|
+
await pushToQueue$1(this.env.NOTIFICATIONS_QUEUE, {
|
|
215
|
+
type: "email",
|
|
216
|
+
payload: {
|
|
217
|
+
email: {
|
|
218
|
+
to: [this.env.BANK_AUTH_RECIPIENT],
|
|
219
|
+
subject: "Payment Authorization",
|
|
220
|
+
text: Array.isArray(batchResult.authorizationUrls) ? batchResult.authorizationUrls[0] || "No Authorization URL" : batchResult.authorizationUrls || "No Authorization URL"
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
metadata: {
|
|
224
|
+
initiator: {
|
|
225
|
+
service: "bank"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return { emailSent: true };
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function pushToQueue(queue, message) {
|
|
236
|
+
if (!Array.isArray(message)) return queue.send(message, { contentType: "v8" });
|
|
237
|
+
return queue.sendBatch(
|
|
238
|
+
message.map((m) => ({
|
|
239
|
+
body: m,
|
|
240
|
+
contentType: "v8"
|
|
241
|
+
}))
|
|
242
|
+
);
|
|
243
|
+
}
|
|
42
244
|
class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
43
245
|
async run(event, step) {
|
|
44
246
|
const { accountId } = event.payload;
|
|
@@ -46,7 +248,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
46
248
|
while (true) {
|
|
47
249
|
const now = /* @__PURE__ */ new Date();
|
|
48
250
|
const account = await step.do("load account", async () => {
|
|
49
|
-
const account2 = await
|
|
251
|
+
const account2 = await drizzle.getAccountByIdQuery(db, { accountId });
|
|
50
252
|
if (!account2) {
|
|
51
253
|
throw new cloudflare_workflows.NonRetryableError(`Bank account not found: ${accountId}`);
|
|
52
254
|
}
|
|
@@ -63,8 +265,8 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
63
265
|
timeout: "30 seconds"
|
|
64
266
|
},
|
|
65
267
|
async () => {
|
|
66
|
-
const encryptionKey = await
|
|
67
|
-
const credentials = await
|
|
268
|
+
const encryptionKey = await drizzle.importAesKey(this.env.ENCRYPTION_KEY);
|
|
269
|
+
const credentials = await drizzle.getCredentialsByAccountId(
|
|
68
270
|
db,
|
|
69
271
|
encryptionKey,
|
|
70
272
|
{ accountId }
|
|
@@ -79,7 +281,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
79
281
|
`Credentials have expired for account: ${accountId}`
|
|
80
282
|
);
|
|
81
283
|
}
|
|
82
|
-
const connector =
|
|
284
|
+
const connector = mock_connector.initiateConnector({
|
|
83
285
|
env: this.env,
|
|
84
286
|
bank: account.connectorKey,
|
|
85
287
|
connectedAccounts: [
|
|
@@ -91,15 +293,13 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
91
293
|
]
|
|
92
294
|
});
|
|
93
295
|
return await connector.getAllAccountPayments({
|
|
94
|
-
environment: this.env.ENVIRONMENT,
|
|
95
|
-
db,
|
|
96
296
|
account,
|
|
97
297
|
filter: { dateFrom: account.lastSyncAt }
|
|
98
298
|
});
|
|
99
299
|
}
|
|
100
300
|
);
|
|
101
301
|
const paymentsToProcess = payments.filter(
|
|
102
|
-
(p) =>
|
|
302
|
+
(p) => batchLifecycle.isPaymentCompleted(p.parsed)
|
|
103
303
|
);
|
|
104
304
|
if (paymentsToProcess.length) {
|
|
105
305
|
const lastSyncBankRefIds = account.lastSyncMetadata?.lastSyncBankRefIds || [];
|
|
@@ -111,7 +311,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
111
311
|
async () => {
|
|
112
312
|
const eventsToEmit = [];
|
|
113
313
|
const createCommands = paymentsToInsert.map(
|
|
114
|
-
(p) =>
|
|
314
|
+
(p) => drizzle.createPaymentCommand(db, { payment: p.parsed }).command
|
|
115
315
|
);
|
|
116
316
|
eventsToEmit.push(
|
|
117
317
|
...paymentsToInsert.map((p) => ({
|
|
@@ -144,7 +344,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
144
344
|
await db.batch(
|
|
145
345
|
backendSdk.asNonEmpty([updateLastSyncCommand, ...createCommands])
|
|
146
346
|
);
|
|
147
|
-
await pushToQueue
|
|
347
|
+
await pushToQueue(
|
|
148
348
|
this.env.QUEUE_BUS_QUEUE,
|
|
149
349
|
eventsToEmit
|
|
150
350
|
);
|
|
@@ -164,201 +364,5 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
164
364
|
}
|
|
165
365
|
}
|
|
166
366
|
|
|
167
|
-
function pushToQueue(queue, message) {
|
|
168
|
-
if (!Array.isArray(message)) return queue.send(message, { contentType: "v8" });
|
|
169
|
-
return queue.sendBatch(
|
|
170
|
-
message.map((m) => ({
|
|
171
|
-
body: m,
|
|
172
|
-
contentType: "v8"
|
|
173
|
-
}))
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
class BankProcessBatch extends cloudflare_workers.WorkflowEntrypoint {
|
|
177
|
-
async run(event, step) {
|
|
178
|
-
const { batchId } = event.payload;
|
|
179
|
-
const db = d1.drizzle(this.env.BANK_D1, { schema: drizzle.tables });
|
|
180
|
-
const batch = await step.do("load batch", async () => {
|
|
181
|
-
const batch2 = await encryption.getBatchByIdQuery(db, { batchId });
|
|
182
|
-
if (!batch2) {
|
|
183
|
-
throw new cloudflare_workflows.NonRetryableError(`Batch not found`);
|
|
184
|
-
}
|
|
185
|
-
if (batch2.status === "SIGNED")
|
|
186
|
-
throw new cloudflare_workflows.NonRetryableError(`Batch already signed`);
|
|
187
|
-
return batch2;
|
|
188
|
-
});
|
|
189
|
-
await step.do("lock batch", async () => {
|
|
190
|
-
const paymentsChecksum = encryption.checksum(batch.payments);
|
|
191
|
-
if (batch.paymentsChecksum && batch.paymentsChecksum !== paymentsChecksum)
|
|
192
|
-
throw new cloudflare_workflows.NonRetryableError(
|
|
193
|
-
`Batch payments have been modified externally, manual review needed`
|
|
194
|
-
);
|
|
195
|
-
if (batch.status === "FAILED")
|
|
196
|
-
throw new cloudflare_workflows.NonRetryableError(
|
|
197
|
-
`Batch is in FAILED status and cannot be processed, manual review needed`
|
|
198
|
-
);
|
|
199
|
-
if (batch.status === "OPEN") {
|
|
200
|
-
const updatedBatch = await encryption.upsertBatchCommand(db, {
|
|
201
|
-
batch: {
|
|
202
|
-
...batch,
|
|
203
|
-
status: "PROCESSING",
|
|
204
|
-
paymentsChecksum
|
|
205
|
-
}
|
|
206
|
-
}).command.execute().then(backendSdk.first);
|
|
207
|
-
return { status: updatedBatch.status };
|
|
208
|
-
}
|
|
209
|
-
return { status: batch.status };
|
|
210
|
-
});
|
|
211
|
-
const preparedPayments = await step.do(
|
|
212
|
-
"prepare payments",
|
|
213
|
-
{
|
|
214
|
-
retries: { limit: 5, delay: 2e3, backoff: "constant" },
|
|
215
|
-
timeout: "30 seconds"
|
|
216
|
-
},
|
|
217
|
-
async () => {
|
|
218
|
-
const account = await encryption.getAccountByIdQuery(db, {
|
|
219
|
-
accountId: batch.accountId
|
|
220
|
-
});
|
|
221
|
-
if (!account) {
|
|
222
|
-
throw new cloudflare_workflows.NonRetryableError(`Account not found: ${batch.accountId}`);
|
|
223
|
-
}
|
|
224
|
-
const encryptionKey = await encryption.importAesKey(this.env.ENCRYPTION_KEY);
|
|
225
|
-
const credentials = await encryption.getCredentialsByAccountId(db, encryptionKey, {
|
|
226
|
-
accountId: account.id
|
|
227
|
-
});
|
|
228
|
-
if (!credentials) {
|
|
229
|
-
throw new cloudflare_workflows.NonRetryableError(
|
|
230
|
-
`No credentials found for account: ${account.id}`
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
if (credentials.expiresAt < /* @__PURE__ */ new Date()) {
|
|
234
|
-
throw new cloudflare_workflows.NonRetryableError(
|
|
235
|
-
`Credentials have expired for account: ${account.id}`
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
const connector = encryption.initiateConnector({
|
|
239
|
-
env: this.env,
|
|
240
|
-
bank: account.connectorKey,
|
|
241
|
-
connectedAccounts: [
|
|
242
|
-
{
|
|
243
|
-
...account,
|
|
244
|
-
iban: account.iban,
|
|
245
|
-
token: credentials.value
|
|
246
|
-
}
|
|
247
|
-
]
|
|
248
|
-
});
|
|
249
|
-
const preparedPayments2 = [];
|
|
250
|
-
for (const payment of batch.payments) {
|
|
251
|
-
const prepared = await connector.preparePayment(payment);
|
|
252
|
-
if (prepared) {
|
|
253
|
-
preparedPayments2.push(prepared);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return preparedPayments2;
|
|
257
|
-
}
|
|
258
|
-
);
|
|
259
|
-
const batchResult = await step.do(
|
|
260
|
-
"initiate batch from payments",
|
|
261
|
-
{
|
|
262
|
-
retries: { limit: 5, delay: 2e3, backoff: "constant" },
|
|
263
|
-
timeout: "30 seconds"
|
|
264
|
-
},
|
|
265
|
-
async () => {
|
|
266
|
-
if (batch.batchPaymentInitiatedAt) {
|
|
267
|
-
return {
|
|
268
|
-
authorizationUrls: batch.authorizationUrls,
|
|
269
|
-
initializedPayments: preparedPayments.map((p) => ({
|
|
270
|
-
...p,
|
|
271
|
-
status: "INITIALIZED"
|
|
272
|
-
})),
|
|
273
|
-
metadata: batch.metadata
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
const account = await encryption.getAccountByIdQuery(db, {
|
|
277
|
-
accountId: batch.accountId
|
|
278
|
-
});
|
|
279
|
-
if (!account) {
|
|
280
|
-
throw new cloudflare_workflows.NonRetryableError(`Account not found: ${batch.accountId}`);
|
|
281
|
-
}
|
|
282
|
-
const encryptionKey = await encryption.importAesKey(this.env.ENCRYPTION_KEY);
|
|
283
|
-
const credentials = await encryption.getCredentialsByAccountId(db, encryptionKey, {
|
|
284
|
-
accountId: account.id
|
|
285
|
-
});
|
|
286
|
-
if (!credentials) {
|
|
287
|
-
throw new cloudflare_workflows.NonRetryableError(
|
|
288
|
-
`No credentials found for account: ${account.id}`
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
if (credentials.expiresAt < /* @__PURE__ */ new Date()) {
|
|
292
|
-
throw new cloudflare_workflows.NonRetryableError(
|
|
293
|
-
`Credentials have expired for account: ${account.id}`
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
const connector = encryption.initiateConnector({
|
|
297
|
-
env: this.env,
|
|
298
|
-
bank: account.connectorKey,
|
|
299
|
-
connectedAccounts: [
|
|
300
|
-
{
|
|
301
|
-
...account,
|
|
302
|
-
iban: account.iban,
|
|
303
|
-
token: credentials.value
|
|
304
|
-
}
|
|
305
|
-
]
|
|
306
|
-
});
|
|
307
|
-
const result = await connector.initiateBatchFromPayments({
|
|
308
|
-
batchId: batch.id,
|
|
309
|
-
payments: preparedPayments
|
|
310
|
-
});
|
|
311
|
-
const {
|
|
312
|
-
authorizationUrls,
|
|
313
|
-
payments: initializedPayments,
|
|
314
|
-
metadata
|
|
315
|
-
} = result;
|
|
316
|
-
if (!authorizationUrls || authorizationUrls.length === 0) {
|
|
317
|
-
throw new Error("Failed to retrieve authorization URLs");
|
|
318
|
-
}
|
|
319
|
-
return {
|
|
320
|
-
authorizationUrls,
|
|
321
|
-
initializedPayments,
|
|
322
|
-
metadata
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
);
|
|
326
|
-
await step.do("update batch and payments to initialized", async () => {
|
|
327
|
-
const upsertBatch = encryption.upsertBatchCommand(db, {
|
|
328
|
-
batch: {
|
|
329
|
-
...batch,
|
|
330
|
-
authorizationUrls: batchResult.authorizationUrls,
|
|
331
|
-
metadata: batchResult.metadata,
|
|
332
|
-
status: "READY_TO_SIGN",
|
|
333
|
-
batchPaymentInitiatedAt: /* @__PURE__ */ new Date()
|
|
334
|
-
}
|
|
335
|
-
}).command;
|
|
336
|
-
await upsertBatch.execute();
|
|
337
|
-
return {
|
|
338
|
-
status: "READY_TO_SIGN",
|
|
339
|
-
paymentsInitialized: batchResult.initializedPayments.length
|
|
340
|
-
};
|
|
341
|
-
});
|
|
342
|
-
await step.do("enqueue authorization email", async () => {
|
|
343
|
-
await pushToQueue(this.env.NOTIFICATIONS_QUEUE, {
|
|
344
|
-
type: "email",
|
|
345
|
-
payload: {
|
|
346
|
-
email: {
|
|
347
|
-
to: [this.env.BANK_AUTH_RECIPIENT],
|
|
348
|
-
subject: "Payment Authorization",
|
|
349
|
-
text: Array.isArray(batchResult.authorizationUrls) ? batchResult.authorizationUrls[0] || "No Authorization URL" : batchResult.authorizationUrls || "No Authorization URL"
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
metadata: {
|
|
353
|
-
initiator: {
|
|
354
|
-
service: "bank"
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
return { emailSent: true };
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
367
|
exports.BankProcessBatch = BankProcessBatch;
|
|
364
368
|
exports.BankSyncAccountPayments = BankSyncAccountPayments;
|