@develit-services/bank 5.7.0 → 5.8.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/base.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { uuidv4, first, buildMultiFilterConditions as buildMultiFilterConditions$1, bankAccountMetadataSchema, structuredAddressSchema, workflowInstanceStatusSchema, develitWorker, createInternalError, action, service } from '@develit-io/backend-sdk';
2
- import { G as tables, g as accountInsertSchema, H as relations, o as isProcessedStatus, p as isTerminalStatus, L as getNonTerminalPaymentRequestsQuery, x as toIncomingPayment, N as calculateCzechIban, j as assignAccount, u as toBatchedPayment, y as toPaymentRequestInsert, a as FinbricksClient, F as FINBRICKS_ENDPOINTS } from './shared/bank.DI_Q2OtU.mjs';
2
+ import { G as tables, g as accountInsertSchema, H as relations, o as isProcessedStatus, p as isTerminalStatus, L as getNonTerminalPaymentRequestsQuery, x as toIncomingPayment, N as calculateCzechIban, j as assignAccount, u as toBatchedPayment, y as toPaymentRequestInsert, a as FinbricksClient, F as FINBRICKS_ENDPOINTS } from './shared/bank.BVXtqHnq.mjs';
3
+ import { j as encrypt, d as createCredentialsResolver, i as initiateConnector, p as parseIterationBudgetParam, e as updatePaymentRequestStatusCommand, a as getPaymentRequestsByBatchIdQuery, g as getBatchByIdQuery, u as upsertBatchCommand, b as getAccountByIdQuery, k as resolveCurrentSyncInstanceId, l as importAesKey, h as createPaymentCommand } from './shared/bank.D6gBgL07.mjs';
3
4
  import { eq, sql, and, like, asc, desc, inArray, gte, lte, isNull, count } from 'drizzle-orm';
4
5
  import { WorkerEntrypoint } from 'cloudflare:workers';
5
6
  import { drizzle } from 'drizzle-orm/d1';
6
7
  import 'jose';
7
- import { h as encrypt, d as createCredentialsResolver, i as initiateConnector, e as updatePaymentRequestStatusCommand, a as getPaymentRequestsByBatchIdQuery, g as getBatchByIdQuery, u as upsertBatchCommand, j as importAesKey, f as createPaymentCommand, b as getAccountByIdQuery } from './shared/bank.BTsBHRsn.mjs';
8
8
  import { z } from 'zod';
9
9
  import { I as INSTRUCTION_PRIORITIES, C as CHARGE_BEARERS, g as PAYMENT_TYPES, b as CONNECTOR_KEYS, a as BATCH_STATUSES, f as PAYMENT_STATUSES, P as PAYMENT_DIRECTIONS, e as PAYMENT_REQUEST_STATUSES } from './shared/bank.BzDNLxB_.mjs';
10
10
  import { CURRENCY_CODES } from '@develit-io/general-codes';
@@ -13,21 +13,37 @@ import 'node:crypto';
13
13
  import 'drizzle-orm/zod';
14
14
  import 'drizzle-orm/sqlite-core';
15
15
 
16
+ const STALE_SYNC_INTERVAL_MULTIPLIER = 3;
17
+ function assessSyncStaleness({ syncIntervalS, lastSyncAt, createdAt }, nowMs, minGraceMs) {
18
+ const baseline = lastSyncAt ?? createdAt;
19
+ if (baseline == null) return null;
20
+ return {
21
+ staleForMs: nowMs - baseline.getTime(),
22
+ thresholdMs: Math.max(
23
+ syncIntervalS * STALE_SYNC_INTERVAL_MULTIPLIER * 1e3,
24
+ minGraceMs
25
+ )
26
+ };
27
+ }
28
+
16
29
  const DEAD_STATUSES = /* @__PURE__ */ new Set([
17
30
  "complete",
18
31
  "terminated",
19
32
  "errored",
20
33
  "unknown"
21
34
  ]);
35
+ const STALE_MIN_GRACE_MS = 15 * 60 * 1e3;
22
36
  async function heartbeatSyncWorkflows({
23
37
  entities,
24
38
  resetAfterIterations,
25
39
  getInstance,
26
40
  createInstance,
27
- logger
41
+ logger,
42
+ now = Date.now
28
43
  }) {
29
44
  await Promise.all(
30
- entities.map(async ({ id, iterationCount }) => {
45
+ entities.map(async (entity) => {
46
+ const { id, iterationCount, lastSyncAt } = entity;
31
47
  try {
32
48
  const instance = await getInstance(id);
33
49
  const { status } = await instance.status();
@@ -39,6 +55,24 @@ async function heartbeatSyncWorkflows({
39
55
  });
40
56
  return;
41
57
  }
58
+ const staleness = assessSyncStaleness(entity, now(), STALE_MIN_GRACE_MS);
59
+ if (staleness == null) {
60
+ logger.warn("sync-workflow.heartbeat.sync-age-unknown", {
61
+ id,
62
+ status
63
+ });
64
+ } else if (staleness.staleForMs > staleness.thresholdMs) {
65
+ await instance.restart();
66
+ logger.info("sync-workflow.heartbeat.stale-recovery", {
67
+ id,
68
+ status,
69
+ iterationCount,
70
+ lastSyncAt: lastSyncAt?.toISOString() ?? null,
71
+ staleForS: Math.round(staleness.staleForMs / 1e3),
72
+ thresholdS: Math.round(staleness.thresholdMs / 1e3)
73
+ });
74
+ return;
75
+ }
42
76
  if (iterationCount == null) {
43
77
  logger.warn("sync-workflow.heartbeat.iteration-count-missing", {
44
78
  id,
@@ -71,6 +105,88 @@ async function heartbeatSyncWorkflows({
71
105
  );
72
106
  }
73
107
 
108
+ const DISPATCH_RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
109
+ function buildDispatchInstanceId(accountId, nowMs, recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS) {
110
+ return `${accountId}-${Math.floor(nowMs / recoveryWindowMs)}`;
111
+ }
112
+ function buildDispatchCreateOptions(instanceId, accountId, maxIterations) {
113
+ return {
114
+ id: instanceId,
115
+ // Omitted (not undefined) when unresolved: the workflow treats a missing
116
+ // budget as unbounded — degraded but syncing.
117
+ params: maxIterations == null ? { accountId } : { accountId, maxIterations },
118
+ // Dispatch mints ~24 instances per account per day and step outputs carry
119
+ // bank payloads — the default 30-day retention would pile up billable
120
+ // storage for state nobody reads after success.
121
+ retention: {
122
+ successRetention: "1 day",
123
+ errorRetention: "7 days"
124
+ }
125
+ };
126
+ }
127
+ function isAlreadyExists(err) {
128
+ const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : "";
129
+ return /already exists/i.test(message);
130
+ }
131
+
132
+ const DEFAULT_MIN_GRACE_MS = 3 * 60 * 1e3;
133
+ async function dispatchSyncWorkflows({
134
+ entities,
135
+ createInstance,
136
+ logger,
137
+ now = Date.now,
138
+ minGraceMs = DEFAULT_MIN_GRACE_MS,
139
+ recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS
140
+ }) {
141
+ const at = now();
142
+ await Promise.all(
143
+ entities.map(async (entity) => {
144
+ const { id, lastSyncAt } = entity;
145
+ const staleness = assessSyncStaleness(entity, at, minGraceMs);
146
+ if (staleness == null) {
147
+ logger.warn("sync-workflow.dispatch.sync-age-unknown", { id });
148
+ return;
149
+ }
150
+ if (staleness.staleForMs <= staleness.thresholdMs) return;
151
+ const instanceId = buildDispatchInstanceId(id, at, recoveryWindowMs);
152
+ try {
153
+ await createInstance(instanceId, id);
154
+ logger.info("sync-workflow.dispatch.created", {
155
+ id,
156
+ instanceId,
157
+ lastSyncAt: lastSyncAt?.toISOString() ?? null,
158
+ staleForS: Math.round(staleness.staleForMs / 1e3),
159
+ thresholdS: Math.round(staleness.thresholdMs / 1e3)
160
+ });
161
+ } catch (err) {
162
+ if (isAlreadyExists(err)) {
163
+ logger.info("sync-workflow.dispatch.already-running", {
164
+ id,
165
+ instanceId
166
+ });
167
+ return;
168
+ }
169
+ logger.error("sync-workflow.dispatch.failed", {
170
+ id,
171
+ instanceId,
172
+ error: err instanceof Error ? err.message : String(err)
173
+ });
174
+ }
175
+ })
176
+ );
177
+ }
178
+
179
+ function isDispatchEnabled(accountId, selection) {
180
+ const raw = selection?.trim();
181
+ if (!raw) return false;
182
+ if (raw.toLowerCase() === "all") return true;
183
+ return raw.split(",").map((id) => id.trim()).includes(accountId);
184
+ }
185
+ function isDispatchActive(accountId, selection, dispatchCron) {
186
+ if (!dispatchCron?.trim()) return false;
187
+ return isDispatchEnabled(accountId, selection);
188
+ }
189
+
74
190
  const FINITE_STATUSES = /* @__PURE__ */ new Set([
75
191
  "complete",
76
192
  "errored",
@@ -748,6 +864,11 @@ var __decorateClass = (decorators, target, key, kind) => {
748
864
  if (kind && result) __defProp(target, key, result);
749
865
  return result;
750
866
  };
867
+ const syncEventConsoleLogger = {
868
+ info: (event, data) => console.log(JSON.stringify({ level: "info", event, ...data })),
869
+ warn: (event, data) => console.warn(JSON.stringify({ level: "warn", event, ...data })),
870
+ error: (event, data) => console.error(JSON.stringify({ level: "error", event, ...data }))
871
+ };
751
872
  let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
752
873
  constructor(ctx, env, config) {
753
874
  super(ctx, env);
@@ -850,6 +971,29 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
850
971
  { data: input, schema: syncAccountInputSchema },
851
972
  { successMessage: "Account sync workflow started" },
852
973
  async ({ accountId }) => {
974
+ if (isDispatchActive(
975
+ accountId,
976
+ this.env.SYNC_DISPATCH_ACCOUNT_IDS,
977
+ this.env.CRON_SYNC_WORKFLOW_DISPATCH
978
+ )) {
979
+ const instanceId = buildDispatchInstanceId(accountId, Date.now());
980
+ const maxIterations = parseIterationBudgetParam(
981
+ this.env.SYNC_WORKFLOW_MAX_ITERATIONS
982
+ );
983
+ let instance2;
984
+ try {
985
+ instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
986
+ buildDispatchCreateOptions(instanceId, accountId, maxIterations)
987
+ );
988
+ } catch (err) {
989
+ if (!isAlreadyExists(err)) throw err;
990
+ instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(instanceId);
991
+ }
992
+ return {
993
+ instanceId: instance2.id,
994
+ details: await instance2.status()
995
+ };
996
+ }
853
997
  const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create({
854
998
  id: accountId,
855
999
  params: {
@@ -868,7 +1012,7 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
868
1012
  { data: input, schema: syncAccountStatusInputSchema },
869
1013
  { successMessage: "Account sync workflow status retrieved" },
870
1014
  async ({ accountId }) => {
871
- const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(accountId);
1015
+ const instance = await this.getCurrentSyncInstance(accountId);
872
1016
  return {
873
1017
  instanceId: instance.id,
874
1018
  details: await instance.status()
@@ -881,7 +1025,7 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
881
1025
  { data: input, schema: syncAccountRestartInputSchema },
882
1026
  { successMessage: "Account sync workflow restarted" },
883
1027
  async ({ accountId }) => {
884
- const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(accountId);
1028
+ const instance = await this.getCurrentSyncInstance(accountId);
885
1029
  await instance.restart();
886
1030
  return {
887
1031
  instanceId: instance.id,
@@ -895,7 +1039,7 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
895
1039
  { data: input, schema: syncAccountTerminateInputSchema },
896
1040
  { successMessage: "Account sync workflow terminated" },
897
1041
  async ({ accountId }) => {
898
- const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(accountId);
1042
+ const instance = await this.getCurrentSyncInstance(accountId);
899
1043
  await terminateSyncWorkflow(instance);
900
1044
  return {
901
1045
  instanceId: instance.id
@@ -1179,17 +1323,62 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
1179
1323
  if (controller.cron === this.env.CRON_PAYMENT_STATUSES) {
1180
1324
  console.log("Scheduled CRON payment request statuses");
1181
1325
  await this.updatePaymentRequestStatuses();
1182
- return;
1326
+ }
1327
+ if (controller.cron === this.env.CRON_SYNC_WORKFLOW_DISPATCH) {
1328
+ await this.dispatchSyncWorkflows();
1183
1329
  }
1184
1330
  if (controller.cron === this.env.CRON_SYNC_WORKFLOW_HEARTBEAT) {
1185
1331
  console.log("Scheduled CRON sync workflow heartbeat");
1186
1332
  await this.heartbeatSyncWorkflows();
1187
1333
  }
1188
1334
  }
1335
+ /**
1336
+ * Lifecycle actions must target the instance that actually syncs the
1337
+ * account — under dispatch that is the windowed instance recorded with the
1338
+ * last sync write, not the legacy canonical id.
1339
+ */
1340
+ async getCurrentSyncInstance(accountId) {
1341
+ const account = await getAccountByIdQuery(this.db, { accountId });
1342
+ return this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(
1343
+ resolveCurrentSyncInstanceId(
1344
+ accountId,
1345
+ account?.lastSyncMetadata?.instanceId
1346
+ )
1347
+ );
1348
+ }
1349
+ async dispatchSyncWorkflows() {
1350
+ const selection = this.env.SYNC_DISPATCH_ACCOUNT_IDS;
1351
+ if (!selection?.trim()) return;
1352
+ const accounts = await this.db.select({
1353
+ id: tables.account.id,
1354
+ syncIntervalS: tables.account.syncIntervalS,
1355
+ lastSyncAt: tables.account.lastSyncAt,
1356
+ createdAt: tables.account.createdAt
1357
+ }).from(tables.account).where(eq(tables.account.status, "AUTHORIZED")).all();
1358
+ const maxIterations = parseIterationBudgetParam(
1359
+ this.env.SYNC_WORKFLOW_MAX_ITERATIONS
1360
+ );
1361
+ if (maxIterations == null) {
1362
+ syncEventConsoleLogger.error(
1363
+ "sync-workflow.dispatch.invalid-max-iterations",
1364
+ { value: this.env.SYNC_WORKFLOW_MAX_ITERATIONS }
1365
+ );
1366
+ }
1367
+ await dispatchSyncWorkflows({
1368
+ entities: accounts.filter((a) => isDispatchEnabled(a.id, selection)),
1369
+ createInstance: (instanceId, accountId) => this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
1370
+ buildDispatchCreateOptions(instanceId, accountId, maxIterations)
1371
+ ),
1372
+ logger: syncEventConsoleLogger
1373
+ });
1374
+ }
1189
1375
  async heartbeatSyncWorkflows() {
1190
1376
  const accounts = await this.db.select({
1191
1377
  id: tables.account.id,
1192
- lastSyncMetadata: tables.account.lastSyncMetadata
1378
+ lastSyncMetadata: tables.account.lastSyncMetadata,
1379
+ syncIntervalS: tables.account.syncIntervalS,
1380
+ lastSyncAt: tables.account.lastSyncAt,
1381
+ createdAt: tables.account.createdAt
1193
1382
  }).from(tables.account).where(eq(tables.account.status, "AUTHORIZED")).all();
1194
1383
  const resetAfterIterations = Number(
1195
1384
  this.env.SYNC_WORKFLOW_RESET_AFTER_ITERATIONS
@@ -1202,9 +1391,18 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
1202
1391
  return;
1203
1392
  }
1204
1393
  await heartbeatSyncWorkflows({
1205
- entities: accounts.map((a) => ({
1394
+ entities: accounts.filter(
1395
+ (a) => !isDispatchActive(
1396
+ a.id,
1397
+ this.env.SYNC_DISPATCH_ACCOUNT_IDS,
1398
+ this.env.CRON_SYNC_WORKFLOW_DISPATCH
1399
+ )
1400
+ ).map((a) => ({
1206
1401
  id: a.id,
1207
- iterationCount: a.lastSyncMetadata?.iterationCount
1402
+ iterationCount: a.lastSyncMetadata?.iterationCount,
1403
+ syncIntervalS: a.syncIntervalS,
1404
+ lastSyncAt: a.lastSyncAt,
1405
+ createdAt: a.createdAt
1208
1406
  })),
1209
1407
  resetAfterIterations,
1210
1408
  getInstance: (id) => this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(id),
@@ -1212,11 +1410,7 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
1212
1410
  id,
1213
1411
  params: { accountId: id }
1214
1412
  }),
1215
- logger: {
1216
- info: (event, data) => console.log(JSON.stringify({ level: "info", event, ...data })),
1217
- warn: (event, data) => console.warn(JSON.stringify({ level: "warn", event, ...data })),
1218
- error: (event, data) => console.error(JSON.stringify({ level: "error", event, ...data }))
1219
- }
1413
+ logger: syncEventConsoleLogger
1220
1414
  });
1221
1415
  }
1222
1416
  async handleAuthorizationCallback(input) {
@@ -1975,7 +2169,12 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
1975
2169
  if (includeWorkflow) {
1976
2170
  let status;
1977
2171
  try {
1978
- const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(a.id);
2172
+ const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(
2173
+ resolveCurrentSyncInstanceId(
2174
+ a.id,
2175
+ a.lastSyncMetadata?.instanceId
2176
+ )
2177
+ );
1979
2178
  status = await instance.status();
1980
2179
  } catch (_) {
1981
2180
  status = null;
@@ -2079,7 +2278,12 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
2079
2278
  });
2080
2279
  }
2081
2280
  try {
2082
- const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(accountId);
2281
+ const instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(
2282
+ resolveCurrentSyncInstanceId(
2283
+ accountId,
2284
+ account.lastSyncMetadata?.instanceId
2285
+ )
2286
+ );
2083
2287
  await terminateSyncWorkflow(instance);
2084
2288
  } catch (error) {
2085
2289
  this.log({
@@ -1,4 +1,4 @@
1
- export { aB as account, aC as accountCredentials, aD as batch, aE as ott, aF as payment, aG as paymentRequest } from '../shared/bank.DmZly4Hz.cjs';
1
+ export { aC as account, aD as accountCredentials, aE as batch, aF as ott, aG as payment, aH as paymentRequest } from '../shared/bank.CefGTNH1.cjs';
2
2
  import 'drizzle-orm/sqlite-core';
3
3
  import 'drizzle-orm';
4
4
  import '@develit-io/general-codes';
@@ -1,4 +1,4 @@
1
- export { aB as account, aC as accountCredentials, aD as batch, aE as ott, aF as payment, aG as paymentRequest } from '../shared/bank.DmZly4Hz.mjs';
1
+ export { aC as account, aD as accountCredentials, aE as batch, aF as ott, aG as payment, aH as paymentRequest } from '../shared/bank.CefGTNH1.mjs';
2
2
  import 'drizzle-orm/sqlite-core';
3
3
  import 'drizzle-orm';
4
4
  import '@develit-io/general-codes';
@@ -1,4 +1,4 @@
1
- export { aB as account, aC as accountCredentials, aD as batch, aE as ott, aF as payment, aG as paymentRequest } from '../shared/bank.DmZly4Hz.js';
1
+ export { aC as account, aD as accountCredentials, aE as batch, aF as ott, aG as payment, aH as paymentRequest } from '../shared/bank.CefGTNH1.js';
2
2
  import 'drizzle-orm/sqlite-core';
3
3
  import 'drizzle-orm';
4
4
  import '@develit-io/general-codes';