@develit-services/bank 5.0.1 → 5.1.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.
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const backendSdk = require('@develit-io/backend-sdk');
4
- const ott_zod = require('../shared/bank.BSX82jhx.cjs');
4
+ const bank = require('../shared/bank.CjEKRnVl.cjs');
5
5
  const batchLifecycle = require('../shared/bank.NF8bZBy0.cjs');
6
6
  const drizzleOrm = require('drizzle-orm');
7
- const credentialsResolver = require('../shared/bank.CibQRM2D.cjs');
7
+ const credentialsResolver = require('../shared/bank.Dji7yhN_.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');
@@ -22,10 +22,10 @@ const updateAccountLastSyncCommand = (db, {
22
22
  accountId,
23
23
  lastSyncMetadata
24
24
  }) => {
25
- const command = db.update(ott_zod.tables.account).set({
25
+ const command = db.update(bank.tables.account).set({
26
26
  lastSyncAt,
27
27
  lastSyncMetadata
28
- }).where(drizzleOrm.eq(ott_zod.tables.account.id, accountId)).returning();
28
+ }).where(drizzleOrm.eq(bank.tables.account.id, accountId)).returning();
29
29
  return {
30
30
  command
31
31
  };
@@ -63,7 +63,7 @@ async function failBatchAndPayments(db, batch, paymentRequests, reason) {
63
63
  class BankProcessBatch extends cloudflare_workers.WorkflowEntrypoint {
64
64
  async run(event, step) {
65
65
  const { batchId } = event.payload;
66
- const db = d1.drizzle(this.env.BANK_D1, { schema: ott_zod.tables, relations: ott_zod.relations });
66
+ const db = d1.drizzle(this.env.BANK_D1, { schema: bank.tables, relations: bank.relations });
67
67
  const batch = await step.do("load batch", async () => {
68
68
  const batch2 = await credentialsResolver.getBatchByIdQuery(db, { batchId });
69
69
  if (!batch2) {
@@ -109,14 +109,14 @@ class BankProcessBatch extends cloudflare_workers.WorkflowEntrypoint {
109
109
  },
110
110
  async () => {
111
111
  const batchedPayments = paymentRequests.map(
112
- ott_zod.toBatchedPaymentFromPaymentRequest
112
+ bank.toBatchedPaymentFromPaymentRequest
113
113
  );
114
114
  const currentBatch = await credentialsResolver.getBatchByIdQuery(db, { batchId });
115
115
  if (currentBatch?.batchPaymentInitiatedAt) {
116
116
  return {
117
117
  authorizationUrls: currentBatch.authorizationUrls,
118
118
  preparedPayments: batchedPayments.map(
119
- (p) => ott_zod.toPreparedPayment(
119
+ (p) => bank.toPreparedPayment(
120
120
  p,
121
121
  void 0,
122
122
  currentBatch.batchPaymentInitiatedAt
@@ -134,7 +134,7 @@ class BankProcessBatch extends cloudflare_workers.WorkflowEntrypoint {
134
134
  throw new cloudflare_workflows.NonRetryableError(msg);
135
135
  }
136
136
  const resolveCredentials = await credentialsResolver.createCredentialsResolver(db, this.env);
137
- const connector = await ott_zod.initiateConnector({
137
+ const connector = await bank.initiateConnector({
138
138
  env: this.env,
139
139
  bank: account.connectorKey,
140
140
  resolveCredentials,
@@ -306,6 +306,7 @@ function createWorkflowLogger(instanceId) {
306
306
  }
307
307
 
308
308
  function getStepCount(ctx) {
309
+ if (!ctx) return 0;
309
310
  return ctx.step?.count ?? 0;
310
311
  }
311
312
 
@@ -314,17 +315,21 @@ async function pushToQueue(queue, message) {
314
315
  await queue.send(message, { contentType: "v8" });
315
316
  return;
316
317
  }
317
- await queue.sendBatch(
318
- message.map((m) => ({
319
- body: m,
320
- contentType: "v8"
321
- }))
322
- );
318
+ const QUEUE_BATCH_SIZE = 100;
319
+ for (let i = 0; i < message.length; i += QUEUE_BATCH_SIZE) {
320
+ const chunk = message.slice(i, i + QUEUE_BATCH_SIZE);
321
+ await queue.sendBatch(
322
+ chunk.map((m) => ({
323
+ body: m,
324
+ contentType: "v8"
325
+ }))
326
+ );
327
+ }
323
328
  }
324
329
  class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
325
330
  async run(event, step) {
326
331
  const { accountId } = event.payload;
327
- const db = d1.drizzle(this.env.BANK_D1, { schema: ott_zod.tables, relations: ott_zod.relations });
332
+ const db = d1.drizzle(this.env.BANK_D1, { schema: bank.tables, relations: bank.relations });
328
333
  const logger = createWorkflowLogger(event.instanceId);
329
334
  if (!accountId) {
330
335
  throw new cloudflare_workflows.NonRetryableError(`Haven't obtained accountId to load.`);
@@ -345,13 +350,13 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
345
350
  if (!account.lastSyncAt) {
346
351
  throw new Error(`lastSyncedAt is not set for account: ${accountId}`);
347
352
  }
348
- const payments = await step.do(
349
- "fetch bank payments",
353
+ await step.do(
354
+ "fetch and process payments",
350
355
  {
351
356
  retries: { limit: 5, delay: "2 minutes", backoff: "exponential" },
352
- timeout: "2 minutes"
357
+ timeout: "5 minutes"
353
358
  },
354
- async () => {
359
+ async (ctx) => {
355
360
  try {
356
361
  logger.info("payments.fetch.started", {
357
362
  accountId,
@@ -362,7 +367,7 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
362
367
  db,
363
368
  this.env
364
369
  );
365
- const connector = await ott_zod.initiateConnector({
370
+ const connector = await bank.initiateConnector({
366
371
  env: this.env,
367
372
  bank: account.connectorKey,
368
373
  resolveCredentials,
@@ -374,19 +379,192 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
374
379
  }
375
380
  ]
376
381
  });
377
- const result = await connector.getAllAccountPayments({
382
+ const payments = await connector.getAllAccountPayments({
378
383
  account,
379
384
  filter: { dateFrom: account.lastSyncAt }
380
385
  });
381
386
  logger.info("payments.fetch.completed", {
382
387
  accountId,
383
388
  connectorKey: account.connectorKey,
384
- paymentsCount: result.length
389
+ paymentsCount: payments.length
390
+ });
391
+ const paymentsToProcess = payments.filter(
392
+ (p) => bank.isPaymentCompleted(p.parsed)
393
+ );
394
+ logger.info("payments.filtered.toProcess", {
395
+ accountId,
396
+ totalFetched: payments.length,
397
+ paymentsToProcess: paymentsToProcess.length,
398
+ sampleStatuses: payments.slice(0, 5).map((p) => ({
399
+ bankRefId: p.parsed.bankRefId,
400
+ status: p.parsed.status,
401
+ isCompleted: bank.isPaymentCompleted(p.parsed)
402
+ }))
403
+ });
404
+ const lastSyncBankRefIds = account.lastSyncMetadata?.lastSyncBankRefIds || [];
405
+ const paymentsToInsert = paymentsToProcess.filter(
406
+ (p) => !lastSyncBankRefIds.includes(p.parsed.bankRefId)
407
+ );
408
+ logger.info("payments.filtered.toInsert", {
409
+ accountId,
410
+ paymentsToProcess: paymentsToProcess.length,
411
+ paymentsToInsert: paymentsToInsert.length,
412
+ lastSyncBankRefIdsCount: lastSyncBankRefIds.length,
413
+ sampleLastSyncBankRefIds: lastSyncBankRefIds.slice(0, 10),
414
+ sampleToInsert: paymentsToInsert.slice(0, 5).map((p) => p.parsed.bankRefId)
415
+ });
416
+ const eventsToEmit = [];
417
+ const bankRefIds = paymentsToInsert.map((p) => p.parsed.bankRefId).filter((id) => id != null);
418
+ const BANK_REF_ID_CHUNK_SIZE = 90;
419
+ const matchingRequests = [];
420
+ for (let i = 0; i < bankRefIds.length; i += BANK_REF_ID_CHUNK_SIZE) {
421
+ const chunkIds = bankRefIds.slice(i, i + BANK_REF_ID_CHUNK_SIZE);
422
+ const rows = await db.select().from(bank.tables.paymentRequest).where(
423
+ drizzleOrm.and(
424
+ drizzleOrm.inArray(bank.tables.paymentRequest.bankRefId, chunkIds),
425
+ drizzleOrm.eq(bank.tables.paymentRequest.accountId, account.id),
426
+ drizzleOrm.eq(
427
+ bank.tables.paymentRequest.connectorKey,
428
+ account.connectorKey
429
+ )
430
+ )
431
+ );
432
+ matchingRequests.push(...rows);
433
+ }
434
+ const requestByBankRefId = Object.fromEntries(
435
+ matchingRequests.map((r) => [r.bankRefId, r])
436
+ );
437
+ const enrichedPayments = paymentsToInsert.map((p) => {
438
+ const req = p.parsed.bankRefId && requestByBankRefId[p.parsed.bankRefId] || null;
439
+ if (!req) return p;
440
+ return {
441
+ ...p,
442
+ parsed: {
443
+ ...p.parsed,
444
+ // queue-bus: transaction matching (DBU doesn't echo these in statements)
445
+ vs: p.parsed.vs ?? req.vs,
446
+ ss: p.parsed.ss ?? req.ss,
447
+ ks: p.parsed.ks ?? req.ks,
448
+ message: p.parsed.message ?? req.message,
449
+ // queue-bus: party creation
450
+ creditor: {
451
+ ...p.parsed.creditor,
452
+ holderName: p.parsed.creditor?.holderName ?? req.creditor?.holderName
453
+ },
454
+ debtor: {
455
+ ...p.parsed.debtor,
456
+ holderName: p.parsed.debtor?.holderName ?? req.debtor?.holderName
457
+ }
458
+ // NOT enriched: chargeBearer, instructionPriority, refId, batchId,
459
+ // createdAt, address, swiftBic — no downstream consumer in payment table
460
+ }
461
+ };
462
+ });
463
+ const createCommands = enrichedPayments.map(
464
+ (p) => credentialsResolver.createPaymentCommand(db, { payment: p.parsed }).command
465
+ );
466
+ logger.info("payments.commands.created", {
467
+ accountId,
468
+ createCommandsCount: createCommands.length,
469
+ enrichedPaymentsCount: enrichedPayments.length
470
+ });
471
+ eventsToEmit.push(
472
+ ...enrichedPayments.map((p) => ({
473
+ eventType: "BANK_PAYMENT",
474
+ eventSignal: "paymentFetched",
475
+ bankPayment: p.parsed,
476
+ metadata: {
477
+ correlationId: p.parsed.correlationId,
478
+ entityId: p.parsed.id,
479
+ timestamp: /* @__PURE__ */ new Date()
480
+ }
481
+ }))
482
+ );
483
+ const lastSyncMetadata = {
484
+ payments: payments.length,
485
+ paymentsToProcess: paymentsToProcess.length,
486
+ paymentsInserted: paymentsToInsert.length,
487
+ lastSyncBankRefIds: paymentsToProcess.filter((p) => p.parsed.status === "BOOKED").map((p) => p.parsed.bankRefId),
488
+ lastSyncPayments: lastSyncBankRefIds.length,
489
+ eventsEmitted: eventsToEmit.length,
490
+ iterationCount: getStepCount(ctx),
491
+ workflowStartedAt
492
+ };
493
+ const updateLastSyncCommand = updateAccountLastSyncCommand(db, {
494
+ accountId: account.id,
495
+ lastSyncAt: now,
496
+ lastSyncMetadata
497
+ }).command;
498
+ logger.info("payments.database.beforeBatch", {
499
+ accountId,
500
+ createCommandsCount: createCommands.length,
501
+ willUseBatch: createCommands.length > 0
385
502
  });
386
- return result;
503
+ if (createCommands.length) {
504
+ logger.info("payments.database.batchStart", {
505
+ accountId,
506
+ totalCommands: createCommands.length + 1,
507
+ paymentsToInsert: createCommands.length
508
+ });
509
+ const BATCH_CHUNK_SIZE = 90;
510
+ let totalInserted = 0;
511
+ for (let i = 0; i < createCommands.length; i += BATCH_CHUNK_SIZE) {
512
+ const chunkCommands = createCommands.slice(
513
+ i,
514
+ i + BATCH_CHUNK_SIZE
515
+ );
516
+ const isLastChunk = i + BATCH_CHUNK_SIZE >= createCommands.length;
517
+ const batchCommands = isLastChunk ? backendSdk.asNonEmpty([updateLastSyncCommand, ...chunkCommands]) : backendSdk.asNonEmpty(chunkCommands);
518
+ logger.info("payments.database.batchChunk", {
519
+ accountId,
520
+ chunkIndex: Math.floor(i / BATCH_CHUNK_SIZE),
521
+ chunkSize: chunkCommands.length,
522
+ isLastChunk
523
+ });
524
+ await db.batch(batchCommands);
525
+ totalInserted += chunkCommands.length;
526
+ }
527
+ logger.info("payments.database.batchComplete", {
528
+ accountId,
529
+ inserted: totalInserted,
530
+ chunks: Math.ceil(createCommands.length / BATCH_CHUNK_SIZE)
531
+ });
532
+ } else {
533
+ logger.info("payments.database.updateOnly", {
534
+ accountId,
535
+ reason: "no new payments to insert"
536
+ });
537
+ await updateLastSyncCommand.execute();
538
+ logger.info("payments.database.updateComplete", {
539
+ accountId
540
+ });
541
+ }
542
+ if (eventsToEmit.length) {
543
+ logger.info("payments.queue.sending", {
544
+ accountId,
545
+ eventsCount: eventsToEmit.length
546
+ });
547
+ await pushToQueue(
548
+ this.env.QUEUE_BUS_QUEUE,
549
+ eventsToEmit
550
+ );
551
+ logger.info("payments.queue.sent", {
552
+ accountId,
553
+ eventsCount: eventsToEmit.length
554
+ });
555
+ }
556
+ logger.info("payments.process.complete", {
557
+ accountId,
558
+ ...lastSyncMetadata,
559
+ newLastSyncAt: now
560
+ });
561
+ return {
562
+ ...lastSyncMetadata,
563
+ newLastSyncAt: now
564
+ };
387
565
  } catch (err) {
388
566
  const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : JSON.stringify(err);
389
- logger.error("payments.fetch.failed", {
567
+ logger.error("payments.process.failed", {
390
568
  accountId,
391
569
  connectorKey: account.connectorKey,
392
570
  error: message
@@ -395,109 +573,6 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
395
573
  }
396
574
  }
397
575
  );
398
- const paymentsToProcess = payments.filter(
399
- (p) => ott_zod.isPaymentCompleted(p.parsed)
400
- );
401
- const lastSyncBankRefIds = account.lastSyncMetadata?.lastSyncBankRefIds || [];
402
- const paymentsToInsert = paymentsToProcess.filter(
403
- (p) => !lastSyncBankRefIds.includes(p.parsed.bankRefId)
404
- );
405
- await step.do(
406
- "process new payments and update lastSyncAt",
407
- async (ctx) => {
408
- const eventsToEmit = [];
409
- const bankRefIds = paymentsToInsert.map((p) => p.parsed.bankRefId).filter((id) => id != null);
410
- const BANK_REF_ID_CHUNK_SIZE = 90;
411
- const matchingRequests = [];
412
- for (let i = 0; i < bankRefIds.length; i += BANK_REF_ID_CHUNK_SIZE) {
413
- const chunkIds = bankRefIds.slice(i, i + BANK_REF_ID_CHUNK_SIZE);
414
- const rows = await db.select().from(ott_zod.tables.paymentRequest).where(
415
- drizzleOrm.and(
416
- drizzleOrm.inArray(ott_zod.tables.paymentRequest.bankRefId, chunkIds),
417
- drizzleOrm.eq(ott_zod.tables.paymentRequest.accountId, account.id),
418
- drizzleOrm.eq(ott_zod.tables.paymentRequest.connectorKey, account.connectorKey)
419
- )
420
- );
421
- matchingRequests.push(...rows);
422
- }
423
- const requestByBankRefId = Object.fromEntries(
424
- matchingRequests.map((r) => [r.bankRefId, r])
425
- );
426
- const enrichedPayments = paymentsToInsert.map((p) => {
427
- const req = p.parsed.bankRefId && requestByBankRefId[p.parsed.bankRefId] || null;
428
- if (!req) return p;
429
- return {
430
- ...p,
431
- parsed: {
432
- ...p.parsed,
433
- // queue-bus: transaction matching (DBU doesn't echo these in statements)
434
- vs: p.parsed.vs ?? req.vs,
435
- ss: p.parsed.ss ?? req.ss,
436
- ks: p.parsed.ks ?? req.ks,
437
- message: p.parsed.message ?? req.message,
438
- // queue-bus: party creation
439
- creditor: {
440
- ...p.parsed.creditor,
441
- holderName: p.parsed.creditor?.holderName ?? req.creditor?.holderName
442
- },
443
- debtor: {
444
- ...p.parsed.debtor,
445
- holderName: p.parsed.debtor?.holderName ?? req.debtor?.holderName
446
- }
447
- // NOT enriched: chargeBearer, instructionPriority, refId, batchId,
448
- // createdAt, address, swiftBic — no downstream consumer in payment table
449
- }
450
- };
451
- });
452
- const createCommands = enrichedPayments.map(
453
- (p) => credentialsResolver.createPaymentCommand(db, { payment: p.parsed }).command
454
- );
455
- eventsToEmit.push(
456
- ...enrichedPayments.map((p) => ({
457
- eventType: "BANK_PAYMENT",
458
- eventSignal: "paymentFetched",
459
- bankPayment: p.parsed,
460
- metadata: {
461
- correlationId: p.parsed.correlationId,
462
- entityId: p.parsed.id,
463
- timestamp: /* @__PURE__ */ new Date()
464
- }
465
- }))
466
- );
467
- const lastSyncMetadata = {
468
- payments: payments.length,
469
- paymentsToProcess: paymentsToProcess.length,
470
- paymentsInserted: paymentsToInsert.length,
471
- lastSyncBankRefIds: paymentsToProcess.filter((p) => p.parsed.status === "BOOKED").map((p) => p.parsed.bankRefId),
472
- lastSyncPayments: lastSyncBankRefIds.length,
473
- eventsEmitted: eventsToEmit.length,
474
- iterationCount: getStepCount(ctx),
475
- workflowStartedAt
476
- };
477
- const updateLastSyncCommand = updateAccountLastSyncCommand(db, {
478
- accountId: account.id,
479
- lastSyncAt: now,
480
- lastSyncMetadata
481
- }).command;
482
- if (createCommands.length) {
483
- await db.batch(
484
- backendSdk.asNonEmpty([updateLastSyncCommand, ...createCommands])
485
- );
486
- } else {
487
- await updateLastSyncCommand;
488
- }
489
- if (eventsToEmit.length) {
490
- await pushToQueue(
491
- this.env.QUEUE_BUS_QUEUE,
492
- eventsToEmit
493
- );
494
- }
495
- return {
496
- ...lastSyncMetadata,
497
- newLastSyncAt: now
498
- };
499
- }
500
- );
501
576
  await step.sleep(
502
577
  "Sleep for next sync",
503
578
  `${account.syncIntervalS} seconds`