@develit-services/bank 5.7.1 → 5.9.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.cjs +296 -32
- package/dist/base.d.cts +38 -11
- package/dist/base.d.mts +38 -11
- package/dist/base.d.ts +38 -11
- package/dist/base.mjs +296 -32
- package/dist/database/schema.cjs +1 -1
- package/dist/database/schema.d.cts +1 -1
- package/dist/database/schema.d.mts +1 -1
- package/dist/database/schema.d.ts +1 -1
- package/dist/database/schema.mjs +1 -1
- package/dist/export/workflows.cjs +213 -42
- package/dist/export/workflows.d.cts +1 -0
- package/dist/export/workflows.d.mts +1 -0
- package/dist/export/workflows.d.ts +1 -0
- package/dist/export/workflows.mjs +214 -43
- package/dist/service.d.cts +3 -0
- package/dist/service.d.mts +3 -0
- package/dist/service.d.ts +3 -0
- package/dist/shared/{bank.BzDNLxB_.mjs → bank.BT7HayCV.mjs} +4 -0
- package/dist/shared/{bank.BUzoc8p6.d.cts → bank.Bhh_O_5-.d.mts} +1 -1
- package/dist/shared/{bank.DZ3Ow4bP.d.mts → bank.BhyIbhvK.d.cts} +1 -1
- package/dist/shared/{bank.CBpXmaTL.d.ts → bank.Bnc_DoG4.d.ts} +1 -1
- package/dist/shared/{bank.CQjuudum.cjs → bank.BsWBG0gb.cjs} +1 -1
- package/dist/shared/{bank.9Yw4KHyl.cjs → bank.Ca3jzmIb.cjs} +4 -0
- package/dist/shared/{bank.DLU1sOBm.mjs → bank.DAjqsBQN.mjs} +30 -3
- package/dist/shared/{bank.B0DNtuUM.cjs → bank.DBTtRCcf.cjs} +33 -2
- package/dist/shared/{bank.BVXtqHnq.mjs → bank.kz-PKVi5.mjs} +1 -1
- package/dist/shared/{bank.BQwwtIR4.d.ts → bank.mkq4IlpC.d.cts} +68 -2
- package/dist/shared/{bank.BQwwtIR4.d.cts → bank.mkq4IlpC.d.mts} +68 -2
- package/dist/shared/{bank.BQwwtIR4.d.mts → bank.mkq4IlpC.d.ts} +68 -2
- package/dist/types.cjs +2 -2
- package/dist/types.d.cts +4 -4
- package/dist/types.d.mts +4 -4
- package/dist/types.d.ts +4 -4
- package/dist/types.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const backendSdk = require('@develit-io/backend-sdk');
|
|
4
|
-
const paymentDirection = require('../shared/bank.
|
|
4
|
+
const paymentDirection = require('../shared/bank.BsWBG0gb.cjs');
|
|
5
5
|
const batchLifecycle = require('../shared/bank.NF8bZBy0.cjs');
|
|
6
6
|
const drizzleOrm = require('drizzle-orm');
|
|
7
|
-
const bank = require('../shared/bank.
|
|
7
|
+
const bank = require('../shared/bank.DBTtRCcf.cjs');
|
|
8
8
|
const cloudflare_workers = require('cloudflare:workers');
|
|
9
9
|
const cloudflare_workflows = require('cloudflare:workflows');
|
|
10
10
|
const d1 = require('drizzle-orm/d1');
|
|
11
11
|
require('node:crypto');
|
|
12
12
|
require('date-fns');
|
|
13
|
-
require('../shared/bank.
|
|
13
|
+
require('../shared/bank.Ca3jzmIb.cjs');
|
|
14
14
|
require('drizzle-orm/sqlite-core');
|
|
15
15
|
require('@develit-io/general-codes');
|
|
16
16
|
require('jose');
|
|
@@ -20,12 +20,17 @@ require('drizzle-orm/zod');
|
|
|
20
20
|
const updateAccountLastSyncCommand = (db, {
|
|
21
21
|
lastSyncAt,
|
|
22
22
|
accountId,
|
|
23
|
-
lastSyncMetadata
|
|
23
|
+
lastSyncMetadata,
|
|
24
|
+
seenInstanceId
|
|
24
25
|
}) => {
|
|
26
|
+
const markerUnchanged = seenInstanceId === void 0 ? void 0 : (
|
|
27
|
+
// IS instead of = so a null marker compares equal to null.
|
|
28
|
+
drizzleOrm.sql`json_extract(${paymentDirection.tables.account.lastSyncMetadata}, '$.instanceId') IS ${seenInstanceId}`
|
|
29
|
+
);
|
|
25
30
|
const command = db.update(paymentDirection.tables.account).set({
|
|
26
31
|
lastSyncAt,
|
|
27
32
|
lastSyncMetadata
|
|
28
|
-
}).where(drizzleOrm.eq(paymentDirection.tables.account.id, accountId)).returning();
|
|
33
|
+
}).where(drizzleOrm.and(drizzleOrm.eq(paymentDirection.tables.account.id, accountId), markerUnchanged)).returning();
|
|
29
34
|
return {
|
|
30
35
|
command
|
|
31
36
|
};
|
|
@@ -340,6 +345,72 @@ function getStepCount(ctx) {
|
|
|
340
345
|
return ctx.step?.count ?? 0;
|
|
341
346
|
}
|
|
342
347
|
|
|
348
|
+
const MAX_CATCH_UP_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
349
|
+
function resolveSyncWindow(lastSyncAt, nowMs, maxWindowMs) {
|
|
350
|
+
const openEnded = {
|
|
351
|
+
dateFrom: lastSyncAt,
|
|
352
|
+
dateTo: new Date(Math.max(nowMs, lastSyncAt.getTime())),
|
|
353
|
+
isCatchUp: false
|
|
354
|
+
};
|
|
355
|
+
if (!Number.isFinite(maxWindowMs) || maxWindowMs <= 0) return openEnded;
|
|
356
|
+
const clippedToMs = lastSyncAt.getTime() + maxWindowMs;
|
|
357
|
+
if (clippedToMs >= nowMs) return openEnded;
|
|
358
|
+
return {
|
|
359
|
+
dateFrom: lastSyncAt,
|
|
360
|
+
dateTo: new Date(clippedToMs),
|
|
361
|
+
isCatchUp: true
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const STEP_RESULT_BYTE_CEILING = 800 * 1024;
|
|
366
|
+
class StepResultTooLargeError extends cloudflare_workflows.NonRetryableError {
|
|
367
|
+
}
|
|
368
|
+
function guardStepResultSize(value, ceilingBytes = STEP_RESULT_BYTE_CEILING) {
|
|
369
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
370
|
+
if (bytes > ceilingBytes) {
|
|
371
|
+
const count = Array.isArray(value) ? value.length : 1;
|
|
372
|
+
throw new StepResultTooLargeError(
|
|
373
|
+
`Fetched ${count} payments (${bytes} bytes), over the ${ceilingBytes} byte step result ceiling`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function rethrowFetchError(err) {
|
|
378
|
+
if (err instanceof cloudflare_workflows.NonRetryableError) throw err;
|
|
379
|
+
const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : JSON.stringify(err);
|
|
380
|
+
throw new Error(message);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const MIN_CATCH_UP_WINDOW_MS = 6 * 60 * 60 * 1e3;
|
|
384
|
+
async function fetchBoundedByWindow({
|
|
385
|
+
window,
|
|
386
|
+
fetchWindow,
|
|
387
|
+
guardResult,
|
|
388
|
+
minWindowMs,
|
|
389
|
+
onNarrowed
|
|
390
|
+
}) {
|
|
391
|
+
let current = window;
|
|
392
|
+
while (true) {
|
|
393
|
+
const result = await fetchWindow(current);
|
|
394
|
+
try {
|
|
395
|
+
guardResult(result);
|
|
396
|
+
return { result, window: current };
|
|
397
|
+
} catch (tooLarge) {
|
|
398
|
+
if (!(tooLarge instanceof StepResultTooLargeError)) throw tooLarge;
|
|
399
|
+
const halvedSpanMs = Math.floor(
|
|
400
|
+
(current.dateTo.getTime() - current.dateFrom.getTime()) / 2
|
|
401
|
+
);
|
|
402
|
+
if (halvedSpanMs < minWindowMs) throw tooLarge;
|
|
403
|
+
current = {
|
|
404
|
+
dateFrom: current.dateFrom,
|
|
405
|
+
dateTo: new Date(current.dateFrom.getTime() + halvedSpanMs),
|
|
406
|
+
// Stops short of the original target, so another pass is needed.
|
|
407
|
+
isCatchUp: true
|
|
408
|
+
};
|
|
409
|
+
onNarrowed?.(current);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
343
414
|
async function pushToQueue(queue, message) {
|
|
344
415
|
if (!Array.isArray(message)) {
|
|
345
416
|
await queue.send(message, { contentType: "v8" });
|
|
@@ -354,7 +425,8 @@ async function pushToQueue(queue, message) {
|
|
|
354
425
|
}
|
|
355
426
|
class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
356
427
|
async run(event, step) {
|
|
357
|
-
const { accountId } = event.payload;
|
|
428
|
+
const { accountId, maxIterations } = event.payload;
|
|
429
|
+
const iterationBudget = bank.resolveIterationBudget(maxIterations);
|
|
358
430
|
const db = d1.drizzle(this.env.BANK_D1, { schema: paymentDirection.tables, relations: paymentDirection.relations });
|
|
359
431
|
const logger = createWorkflowLogger(event.instanceId);
|
|
360
432
|
if (!accountId) {
|
|
@@ -364,8 +436,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
364
436
|
"capture workflow start time",
|
|
365
437
|
async () => Date.now()
|
|
366
438
|
);
|
|
367
|
-
|
|
368
|
-
const now = /* @__PURE__ */ new Date();
|
|
439
|
+
for (let iteration = 0; iteration < iterationBudget; iteration++) {
|
|
369
440
|
const account = await step.do("load account", async () => {
|
|
370
441
|
const account2 = await bank.getAccountByIdQuery(db, { accountId });
|
|
371
442
|
if (!account2) {
|
|
@@ -373,10 +444,49 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
373
444
|
}
|
|
374
445
|
return account2;
|
|
375
446
|
});
|
|
447
|
+
if (!account.syncEnabled) {
|
|
448
|
+
logger.info("sync.disabled", {
|
|
449
|
+
accountId,
|
|
450
|
+
instanceId: event.instanceId,
|
|
451
|
+
iteration
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
376
455
|
if (!account.lastSyncAt) {
|
|
377
456
|
throw new Error(`lastSyncedAt is not set for account: ${accountId}`);
|
|
378
457
|
}
|
|
379
|
-
|
|
458
|
+
if (bank.isSupersededBy(
|
|
459
|
+
event.instanceId,
|
|
460
|
+
account.lastSyncMetadata?.instanceId,
|
|
461
|
+
accountId
|
|
462
|
+
)) {
|
|
463
|
+
logger.info("sync.superseded", {
|
|
464
|
+
accountId,
|
|
465
|
+
instanceId: event.instanceId,
|
|
466
|
+
supersededBy: account.lastSyncMetadata?.instanceId,
|
|
467
|
+
iteration
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const lastSyncAtMs = account.lastSyncAt.getTime();
|
|
472
|
+
const windowMs = await step.do("resolve sync window", async () => {
|
|
473
|
+
const window = resolveSyncWindow(
|
|
474
|
+
new Date(lastSyncAtMs),
|
|
475
|
+
Date.now(),
|
|
476
|
+
MAX_CATCH_UP_WINDOW_MS
|
|
477
|
+
);
|
|
478
|
+
return {
|
|
479
|
+
dateFromMs: window.dateFrom.getTime(),
|
|
480
|
+
dateToMs: window.dateTo.getTime(),
|
|
481
|
+
isCatchUp: window.isCatchUp
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
const syncWindow = {
|
|
485
|
+
dateFrom: new Date(windowMs.dateFromMs),
|
|
486
|
+
dateTo: new Date(windowMs.dateToMs),
|
|
487
|
+
isCatchUp: windowMs.isCatchUp
|
|
488
|
+
};
|
|
489
|
+
const fetched = await step.do(
|
|
380
490
|
"fetch bank payments",
|
|
381
491
|
{
|
|
382
492
|
retries: { limit: 5, delay: "2 minutes", backoff: "exponential" },
|
|
@@ -405,42 +515,88 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
405
515
|
}
|
|
406
516
|
]
|
|
407
517
|
});
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
518
|
+
const fetchWindow = async (window) => {
|
|
519
|
+
const result = await connector.getAllAccountPayments({
|
|
520
|
+
account,
|
|
521
|
+
filter: {
|
|
522
|
+
dateFrom: window.dateFrom,
|
|
523
|
+
dateTo: window.dateTo
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
const identifiable = result.filter(
|
|
527
|
+
(p) => p.parsed.bankRefId?.trim()
|
|
528
|
+
);
|
|
529
|
+
const skipped = result.length - identifiable.length;
|
|
530
|
+
if (skipped > 0) {
|
|
531
|
+
logger.warn("payments.fetch.skipped_blank_bank_ref", {
|
|
532
|
+
accountId,
|
|
533
|
+
connectorKey: account.connectorKey,
|
|
534
|
+
skipped
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return Promise.all(
|
|
538
|
+
identifiable.map(async (p) => ({
|
|
539
|
+
parsed: {
|
|
540
|
+
...p.parsed,
|
|
541
|
+
id: await deriveV4PaymentId(p.parsed)
|
|
542
|
+
}
|
|
543
|
+
}))
|
|
544
|
+
);
|
|
545
|
+
};
|
|
546
|
+
let bounded;
|
|
547
|
+
try {
|
|
548
|
+
bounded = await fetchBoundedByWindow({
|
|
549
|
+
window: syncWindow,
|
|
550
|
+
fetchWindow,
|
|
551
|
+
guardResult: guardStepResultSize,
|
|
552
|
+
minWindowMs: MIN_CATCH_UP_WINDOW_MS,
|
|
553
|
+
onNarrowed: (narrowed) => logger.warn("payments.fetch.window-narrowed", {
|
|
554
|
+
accountId,
|
|
555
|
+
connectorKey: account.connectorKey,
|
|
556
|
+
dateFrom: narrowed.dateFrom.toISOString(),
|
|
557
|
+
dateTo: narrowed.dateTo.toISOString()
|
|
558
|
+
})
|
|
559
|
+
});
|
|
560
|
+
} catch (err) {
|
|
561
|
+
if (err instanceof StepResultTooLargeError) {
|
|
562
|
+
logger.error("payments.fetch.result-too-large", {
|
|
563
|
+
accountId,
|
|
564
|
+
connectorKey: account.connectorKey,
|
|
565
|
+
dateFrom: syncWindow.dateFrom.toISOString(),
|
|
566
|
+
dateTo: syncWindow.dateTo.toISOString()
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
412
571
|
logger.info("payments.fetch.completed", {
|
|
413
572
|
accountId,
|
|
414
573
|
connectorKey: account.connectorKey,
|
|
415
|
-
paymentsCount: result.length
|
|
574
|
+
paymentsCount: bounded.result.length
|
|
416
575
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
skipped
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
return Promise.all(
|
|
429
|
-
identifiable.map(async (p) => ({
|
|
430
|
-
parsed: { ...p.parsed, id: await deriveV4PaymentId(p.parsed) }
|
|
431
|
-
}))
|
|
432
|
-
);
|
|
576
|
+
return {
|
|
577
|
+
payments: bounded.result,
|
|
578
|
+
windowMs: {
|
|
579
|
+
dateFromMs: bounded.window.dateFrom.getTime(),
|
|
580
|
+
dateToMs: bounded.window.dateTo.getTime(),
|
|
581
|
+
isCatchUp: bounded.window.isCatchUp
|
|
582
|
+
}
|
|
583
|
+
};
|
|
433
584
|
} catch (err) {
|
|
434
|
-
const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : JSON.stringify(err);
|
|
435
585
|
logger.error("payments.fetch.failed", {
|
|
436
586
|
accountId,
|
|
437
587
|
connectorKey: account.connectorKey,
|
|
438
|
-
error: message
|
|
588
|
+
error: err instanceof Error ? err.message : String(err)
|
|
439
589
|
});
|
|
440
|
-
|
|
590
|
+
rethrowFetchError(err);
|
|
441
591
|
}
|
|
442
592
|
}
|
|
443
593
|
);
|
|
594
|
+
const payments = fetched.payments;
|
|
595
|
+
const effectiveWindow = {
|
|
596
|
+
dateFrom: new Date(fetched.windowMs.dateFromMs),
|
|
597
|
+
dateTo: new Date(fetched.windowMs.dateToMs),
|
|
598
|
+
isCatchUp: fetched.windowMs.isCatchUp
|
|
599
|
+
};
|
|
444
600
|
const paymentsToProcess = payments.filter(
|
|
445
601
|
(p) => paymentDirection.isPaymentCompleted(p.parsed)
|
|
446
602
|
);
|
|
@@ -515,6 +671,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
515
671
|
}))
|
|
516
672
|
);
|
|
517
673
|
const lastSyncMetadata = {
|
|
674
|
+
instanceId: event.instanceId,
|
|
518
675
|
payments: payments.length,
|
|
519
676
|
paymentsToProcess: paymentsToProcess.length,
|
|
520
677
|
paymentsInserted: paymentsToInsert.length,
|
|
@@ -531,15 +688,27 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
531
688
|
};
|
|
532
689
|
const updateLastSyncCommand = updateAccountLastSyncCommand(db, {
|
|
533
690
|
accountId: account.id,
|
|
534
|
-
|
|
535
|
-
|
|
691
|
+
// End of the window just fetched, not `now`: while catching up the
|
|
692
|
+
// window stops short of now, and advancing past it would skip every
|
|
693
|
+
// payment in the gap.
|
|
694
|
+
lastSyncAt: effectiveWindow.dateTo,
|
|
695
|
+
lastSyncMetadata,
|
|
696
|
+
seenInstanceId: account.lastSyncMetadata?.instanceId ?? null
|
|
536
697
|
}).command;
|
|
698
|
+
let syncStateRows;
|
|
537
699
|
if (createCommands.length) {
|
|
538
|
-
await db.batch(
|
|
700
|
+
const [updateResult] = await db.batch(
|
|
539
701
|
backendSdk.asNonEmpty([updateLastSyncCommand, ...createCommands])
|
|
540
702
|
);
|
|
703
|
+
syncStateRows = updateResult;
|
|
541
704
|
} else {
|
|
542
|
-
await updateLastSyncCommand;
|
|
705
|
+
syncStateRows = await updateLastSyncCommand;
|
|
706
|
+
}
|
|
707
|
+
if (syncStateRows.length === 0) {
|
|
708
|
+
logger.info("sync.write-superseded", {
|
|
709
|
+
accountId,
|
|
710
|
+
instanceId: event.instanceId
|
|
711
|
+
});
|
|
543
712
|
}
|
|
544
713
|
if (eventsToEmit.length) {
|
|
545
714
|
await pushToQueue(
|
|
@@ -549,14 +718,16 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
549
718
|
}
|
|
550
719
|
return {
|
|
551
720
|
...lastSyncMetadata,
|
|
552
|
-
newLastSyncAt:
|
|
721
|
+
newLastSyncAt: effectiveWindow.dateTo
|
|
553
722
|
};
|
|
554
723
|
}
|
|
555
724
|
);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
725
|
+
if (!effectiveWindow.isCatchUp) {
|
|
726
|
+
await step.sleep(
|
|
727
|
+
"Sleep for next sync",
|
|
728
|
+
`${account.syncIntervalS} seconds`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
560
731
|
}
|
|
561
732
|
}
|
|
562
733
|
}
|
|
@@ -9,6 +9,7 @@ declare class BankProcessBatch extends WorkflowEntrypoint<BankEnv, Params$1> {
|
|
|
9
9
|
|
|
10
10
|
type Params = {
|
|
11
11
|
accountId: string;
|
|
12
|
+
maxIterations?: number;
|
|
12
13
|
};
|
|
13
14
|
declare class BankSyncAccountPayments extends WorkflowEntrypoint<BankEnv, Params> {
|
|
14
15
|
run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<void>;
|
|
@@ -9,6 +9,7 @@ declare class BankProcessBatch extends WorkflowEntrypoint<BankEnv, Params$1> {
|
|
|
9
9
|
|
|
10
10
|
type Params = {
|
|
11
11
|
accountId: string;
|
|
12
|
+
maxIterations?: number;
|
|
12
13
|
};
|
|
13
14
|
declare class BankSyncAccountPayments extends WorkflowEntrypoint<BankEnv, Params> {
|
|
14
15
|
run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<void>;
|
|
@@ -9,6 +9,7 @@ declare class BankProcessBatch extends WorkflowEntrypoint<BankEnv, Params$1> {
|
|
|
9
9
|
|
|
10
10
|
type Params = {
|
|
11
11
|
accountId: string;
|
|
12
|
+
maxIterations?: number;
|
|
12
13
|
};
|
|
13
14
|
declare class BankSyncAccountPayments extends WorkflowEntrypoint<BankEnv, Params> {
|
|
14
15
|
run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<void>;
|