@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.cjs +222 -18
- package/dist/base.d.cts +14 -7
- package/dist/base.d.mts +14 -7
- package/dist/base.d.ts +14 -7
- package/dist/base.mjs +222 -18
- 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/export/workflows.cjs +204 -41
- 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 +205 -42
- 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.0zYbe2ZB.d.ts → bank.BOeuHdjy.d.ts} +1 -1
- package/dist/shared/{bank.DI_Q2OtU.mjs → bank.BVXtqHnq.mjs} +47 -9
- package/dist/shared/{bank.B1Xa5U2u.cjs → bank.CQjuudum.cjs} +47 -9
- package/dist/shared/{bank.DmZly4Hz.d.cts → bank.CefGTNH1.d.cts} +6 -4
- package/dist/shared/{bank.DmZly4Hz.d.mts → bank.CefGTNH1.d.mts} +6 -4
- package/dist/shared/{bank.DmZly4Hz.d.ts → bank.CefGTNH1.d.ts} +6 -4
- package/dist/shared/{bank.sVKKxDLm.d.mts → bank.CtIkqQG9.d.cts} +1 -1
- package/dist/shared/{bank.BcAwr2OG.cjs → bank.Cy7xa566.cjs} +46 -1
- package/dist/shared/{bank.BTsBHRsn.mjs → bank.D6gBgL07.mjs} +43 -2
- package/dist/shared/{bank.Dyt5JZy1.d.cts → bank.oNDZ44du.d.mts} +1 -1
- package/dist/types.cjs +1 -1
- package/dist/types.d.cts +12 -5
- package/dist/types.d.mts +12 -5
- package/dist/types.d.ts +12 -5
- package/dist/types.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
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.CQjuudum.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.Cy7xa566.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');
|
|
@@ -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) {
|
|
@@ -376,7 +447,38 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
376
447
|
if (!account.lastSyncAt) {
|
|
377
448
|
throw new Error(`lastSyncedAt is not set for account: ${accountId}`);
|
|
378
449
|
}
|
|
379
|
-
|
|
450
|
+
if (bank.isSupersededBy(
|
|
451
|
+
event.instanceId,
|
|
452
|
+
account.lastSyncMetadata?.instanceId,
|
|
453
|
+
accountId
|
|
454
|
+
)) {
|
|
455
|
+
logger.info("sync.superseded", {
|
|
456
|
+
accountId,
|
|
457
|
+
instanceId: event.instanceId,
|
|
458
|
+
supersededBy: account.lastSyncMetadata?.instanceId,
|
|
459
|
+
iteration
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const lastSyncAtMs = account.lastSyncAt.getTime();
|
|
464
|
+
const windowMs = await step.do("resolve sync window", async () => {
|
|
465
|
+
const window = resolveSyncWindow(
|
|
466
|
+
new Date(lastSyncAtMs),
|
|
467
|
+
Date.now(),
|
|
468
|
+
MAX_CATCH_UP_WINDOW_MS
|
|
469
|
+
);
|
|
470
|
+
return {
|
|
471
|
+
dateFromMs: window.dateFrom.getTime(),
|
|
472
|
+
dateToMs: window.dateTo.getTime(),
|
|
473
|
+
isCatchUp: window.isCatchUp
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
const syncWindow = {
|
|
477
|
+
dateFrom: new Date(windowMs.dateFromMs),
|
|
478
|
+
dateTo: new Date(windowMs.dateToMs),
|
|
479
|
+
isCatchUp: windowMs.isCatchUp
|
|
480
|
+
};
|
|
481
|
+
const fetched = await step.do(
|
|
380
482
|
"fetch bank payments",
|
|
381
483
|
{
|
|
382
484
|
retries: { limit: 5, delay: "2 minutes", backoff: "exponential" },
|
|
@@ -405,42 +507,88 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
405
507
|
}
|
|
406
508
|
]
|
|
407
509
|
});
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
510
|
+
const fetchWindow = async (window) => {
|
|
511
|
+
const result = await connector.getAllAccountPayments({
|
|
512
|
+
account,
|
|
513
|
+
filter: {
|
|
514
|
+
dateFrom: window.dateFrom,
|
|
515
|
+
dateTo: window.dateTo
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
const identifiable = result.filter(
|
|
519
|
+
(p) => p.parsed.bankRefId?.trim()
|
|
520
|
+
);
|
|
521
|
+
const skipped = result.length - identifiable.length;
|
|
522
|
+
if (skipped > 0) {
|
|
523
|
+
logger.warn("payments.fetch.skipped_blank_bank_ref", {
|
|
524
|
+
accountId,
|
|
525
|
+
connectorKey: account.connectorKey,
|
|
526
|
+
skipped
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return Promise.all(
|
|
530
|
+
identifiable.map(async (p) => ({
|
|
531
|
+
parsed: {
|
|
532
|
+
...p.parsed,
|
|
533
|
+
id: await deriveV4PaymentId(p.parsed)
|
|
534
|
+
}
|
|
535
|
+
}))
|
|
536
|
+
);
|
|
537
|
+
};
|
|
538
|
+
let bounded;
|
|
539
|
+
try {
|
|
540
|
+
bounded = await fetchBoundedByWindow({
|
|
541
|
+
window: syncWindow,
|
|
542
|
+
fetchWindow,
|
|
543
|
+
guardResult: guardStepResultSize,
|
|
544
|
+
minWindowMs: MIN_CATCH_UP_WINDOW_MS,
|
|
545
|
+
onNarrowed: (narrowed) => logger.warn("payments.fetch.window-narrowed", {
|
|
546
|
+
accountId,
|
|
547
|
+
connectorKey: account.connectorKey,
|
|
548
|
+
dateFrom: narrowed.dateFrom.toISOString(),
|
|
549
|
+
dateTo: narrowed.dateTo.toISOString()
|
|
550
|
+
})
|
|
551
|
+
});
|
|
552
|
+
} catch (err) {
|
|
553
|
+
if (err instanceof StepResultTooLargeError) {
|
|
554
|
+
logger.error("payments.fetch.result-too-large", {
|
|
555
|
+
accountId,
|
|
556
|
+
connectorKey: account.connectorKey,
|
|
557
|
+
dateFrom: syncWindow.dateFrom.toISOString(),
|
|
558
|
+
dateTo: syncWindow.dateTo.toISOString()
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
throw err;
|
|
562
|
+
}
|
|
412
563
|
logger.info("payments.fetch.completed", {
|
|
413
564
|
accountId,
|
|
414
565
|
connectorKey: account.connectorKey,
|
|
415
|
-
paymentsCount: result.length
|
|
566
|
+
paymentsCount: bounded.result.length
|
|
416
567
|
});
|
|
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
|
-
);
|
|
568
|
+
return {
|
|
569
|
+
payments: bounded.result,
|
|
570
|
+
windowMs: {
|
|
571
|
+
dateFromMs: bounded.window.dateFrom.getTime(),
|
|
572
|
+
dateToMs: bounded.window.dateTo.getTime(),
|
|
573
|
+
isCatchUp: bounded.window.isCatchUp
|
|
574
|
+
}
|
|
575
|
+
};
|
|
433
576
|
} 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
577
|
logger.error("payments.fetch.failed", {
|
|
436
578
|
accountId,
|
|
437
579
|
connectorKey: account.connectorKey,
|
|
438
|
-
error: message
|
|
580
|
+
error: err instanceof Error ? err.message : String(err)
|
|
439
581
|
});
|
|
440
|
-
|
|
582
|
+
rethrowFetchError(err);
|
|
441
583
|
}
|
|
442
584
|
}
|
|
443
585
|
);
|
|
586
|
+
const payments = fetched.payments;
|
|
587
|
+
const effectiveWindow = {
|
|
588
|
+
dateFrom: new Date(fetched.windowMs.dateFromMs),
|
|
589
|
+
dateTo: new Date(fetched.windowMs.dateToMs),
|
|
590
|
+
isCatchUp: fetched.windowMs.isCatchUp
|
|
591
|
+
};
|
|
444
592
|
const paymentsToProcess = payments.filter(
|
|
445
593
|
(p) => paymentDirection.isPaymentCompleted(p.parsed)
|
|
446
594
|
);
|
|
@@ -515,6 +663,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
515
663
|
}))
|
|
516
664
|
);
|
|
517
665
|
const lastSyncMetadata = {
|
|
666
|
+
instanceId: event.instanceId,
|
|
518
667
|
payments: payments.length,
|
|
519
668
|
paymentsToProcess: paymentsToProcess.length,
|
|
520
669
|
paymentsInserted: paymentsToInsert.length,
|
|
@@ -531,15 +680,27 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
531
680
|
};
|
|
532
681
|
const updateLastSyncCommand = updateAccountLastSyncCommand(db, {
|
|
533
682
|
accountId: account.id,
|
|
534
|
-
|
|
535
|
-
|
|
683
|
+
// End of the window just fetched, not `now`: while catching up the
|
|
684
|
+
// window stops short of now, and advancing past it would skip every
|
|
685
|
+
// payment in the gap.
|
|
686
|
+
lastSyncAt: effectiveWindow.dateTo,
|
|
687
|
+
lastSyncMetadata,
|
|
688
|
+
seenInstanceId: account.lastSyncMetadata?.instanceId ?? null
|
|
536
689
|
}).command;
|
|
690
|
+
let syncStateRows;
|
|
537
691
|
if (createCommands.length) {
|
|
538
|
-
await db.batch(
|
|
692
|
+
const [updateResult] = await db.batch(
|
|
539
693
|
backendSdk.asNonEmpty([updateLastSyncCommand, ...createCommands])
|
|
540
694
|
);
|
|
695
|
+
syncStateRows = updateResult;
|
|
541
696
|
} else {
|
|
542
|
-
await updateLastSyncCommand;
|
|
697
|
+
syncStateRows = await updateLastSyncCommand;
|
|
698
|
+
}
|
|
699
|
+
if (syncStateRows.length === 0) {
|
|
700
|
+
logger.info("sync.write-superseded", {
|
|
701
|
+
accountId,
|
|
702
|
+
instanceId: event.instanceId
|
|
703
|
+
});
|
|
543
704
|
}
|
|
544
705
|
if (eventsToEmit.length) {
|
|
545
706
|
await pushToQueue(
|
|
@@ -549,14 +710,16 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
|
|
|
549
710
|
}
|
|
550
711
|
return {
|
|
551
712
|
...lastSyncMetadata,
|
|
552
|
-
newLastSyncAt:
|
|
713
|
+
newLastSyncAt: effectiveWindow.dateTo
|
|
553
714
|
};
|
|
554
715
|
}
|
|
555
716
|
);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
717
|
+
if (!effectiveWindow.isCatchUp) {
|
|
718
|
+
await step.sleep(
|
|
719
|
+
"Sleep for next sync",
|
|
720
|
+
`${account.syncIntervalS} seconds`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
560
723
|
}
|
|
561
724
|
}
|
|
562
725
|
}
|
|
@@ -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>;
|