@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.
@@ -1,8 +1,8 @@
1
1
  import { first, uuidv4, asNonEmpty } from '@develit-io/backend-sdk';
2
- import { G as tables, H as relations, v as toBatchedPaymentFromPaymentRequest, z as toPreparedPayment, m as isPaymentCompleted } from '../shared/bank.DI_Q2OtU.mjs';
2
+ import { G as tables, H as relations, v as toBatchedPaymentFromPaymentRequest, z as toPreparedPayment, m as isPaymentCompleted } from '../shared/bank.BVXtqHnq.mjs';
3
3
  import { i as isBatchAuthorized, b as isBatchFailed, d as isBatchProcessing } from '../shared/bank.XqSw509X.mjs';
4
- import { eq, and, inArray } from 'drizzle-orm';
5
- import { g as getBatchByIdQuery, a as getPaymentRequestsByBatchIdQuery, c as checksum, u as upsertBatchCommand, b as getAccountByIdQuery, d as createCredentialsResolver, i as initiateConnector, e as updatePaymentRequestStatusCommand, f as createPaymentCommand } from '../shared/bank.BTsBHRsn.mjs';
4
+ import { sql, and, eq, inArray } from 'drizzle-orm';
5
+ import { g as getBatchByIdQuery, a as getPaymentRequestsByBatchIdQuery, c as checksum, u as upsertBatchCommand, b as getAccountByIdQuery, d as createCredentialsResolver, i as initiateConnector, e as updatePaymentRequestStatusCommand, f as isSupersededBy, h as createPaymentCommand, r as resolveIterationBudget } from '../shared/bank.D6gBgL07.mjs';
6
6
  import { WorkflowEntrypoint } from 'cloudflare:workers';
7
7
  import { NonRetryableError } from 'cloudflare:workflows';
8
8
  import { drizzle } from 'drizzle-orm/d1';
@@ -18,12 +18,17 @@ import 'drizzle-orm/zod';
18
18
  const updateAccountLastSyncCommand = (db, {
19
19
  lastSyncAt,
20
20
  accountId,
21
- lastSyncMetadata
21
+ lastSyncMetadata,
22
+ seenInstanceId
22
23
  }) => {
24
+ const markerUnchanged = seenInstanceId === void 0 ? void 0 : (
25
+ // IS instead of = so a null marker compares equal to null.
26
+ sql`json_extract(${tables.account.lastSyncMetadata}, '$.instanceId') IS ${seenInstanceId}`
27
+ );
23
28
  const command = db.update(tables.account).set({
24
29
  lastSyncAt,
25
30
  lastSyncMetadata
26
- }).where(eq(tables.account.id, accountId)).returning();
31
+ }).where(and(eq(tables.account.id, accountId), markerUnchanged)).returning();
27
32
  return {
28
33
  command
29
34
  };
@@ -338,6 +343,72 @@ function getStepCount(ctx) {
338
343
  return ctx.step?.count ?? 0;
339
344
  }
340
345
 
346
+ const MAX_CATCH_UP_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
347
+ function resolveSyncWindow(lastSyncAt, nowMs, maxWindowMs) {
348
+ const openEnded = {
349
+ dateFrom: lastSyncAt,
350
+ dateTo: new Date(Math.max(nowMs, lastSyncAt.getTime())),
351
+ isCatchUp: false
352
+ };
353
+ if (!Number.isFinite(maxWindowMs) || maxWindowMs <= 0) return openEnded;
354
+ const clippedToMs = lastSyncAt.getTime() + maxWindowMs;
355
+ if (clippedToMs >= nowMs) return openEnded;
356
+ return {
357
+ dateFrom: lastSyncAt,
358
+ dateTo: new Date(clippedToMs),
359
+ isCatchUp: true
360
+ };
361
+ }
362
+
363
+ const STEP_RESULT_BYTE_CEILING = 800 * 1024;
364
+ class StepResultTooLargeError extends NonRetryableError {
365
+ }
366
+ function guardStepResultSize(value, ceilingBytes = STEP_RESULT_BYTE_CEILING) {
367
+ const bytes = new TextEncoder().encode(JSON.stringify(value)).byteLength;
368
+ if (bytes > ceilingBytes) {
369
+ const count = Array.isArray(value) ? value.length : 1;
370
+ throw new StepResultTooLargeError(
371
+ `Fetched ${count} payments (${bytes} bytes), over the ${ceilingBytes} byte step result ceiling`
372
+ );
373
+ }
374
+ }
375
+ function rethrowFetchError(err) {
376
+ if (err instanceof NonRetryableError) throw err;
377
+ const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : JSON.stringify(err);
378
+ throw new Error(message);
379
+ }
380
+
381
+ const MIN_CATCH_UP_WINDOW_MS = 6 * 60 * 60 * 1e3;
382
+ async function fetchBoundedByWindow({
383
+ window,
384
+ fetchWindow,
385
+ guardResult,
386
+ minWindowMs,
387
+ onNarrowed
388
+ }) {
389
+ let current = window;
390
+ while (true) {
391
+ const result = await fetchWindow(current);
392
+ try {
393
+ guardResult(result);
394
+ return { result, window: current };
395
+ } catch (tooLarge) {
396
+ if (!(tooLarge instanceof StepResultTooLargeError)) throw tooLarge;
397
+ const halvedSpanMs = Math.floor(
398
+ (current.dateTo.getTime() - current.dateFrom.getTime()) / 2
399
+ );
400
+ if (halvedSpanMs < minWindowMs) throw tooLarge;
401
+ current = {
402
+ dateFrom: current.dateFrom,
403
+ dateTo: new Date(current.dateFrom.getTime() + halvedSpanMs),
404
+ // Stops short of the original target, so another pass is needed.
405
+ isCatchUp: true
406
+ };
407
+ onNarrowed?.(current);
408
+ }
409
+ }
410
+ }
411
+
341
412
  async function pushToQueue(queue, message) {
342
413
  if (!Array.isArray(message)) {
343
414
  await queue.send(message, { contentType: "v8" });
@@ -352,7 +423,8 @@ async function pushToQueue(queue, message) {
352
423
  }
353
424
  class BankSyncAccountPayments extends WorkflowEntrypoint {
354
425
  async run(event, step) {
355
- const { accountId } = event.payload;
426
+ const { accountId, maxIterations } = event.payload;
427
+ const iterationBudget = resolveIterationBudget(maxIterations);
356
428
  const db = drizzle(this.env.BANK_D1, { schema: tables, relations });
357
429
  const logger = createWorkflowLogger(event.instanceId);
358
430
  if (!accountId) {
@@ -362,8 +434,7 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
362
434
  "capture workflow start time",
363
435
  async () => Date.now()
364
436
  );
365
- while (true) {
366
- const now = /* @__PURE__ */ new Date();
437
+ for (let iteration = 0; iteration < iterationBudget; iteration++) {
367
438
  const account = await step.do("load account", async () => {
368
439
  const account2 = await getAccountByIdQuery(db, { accountId });
369
440
  if (!account2) {
@@ -374,7 +445,38 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
374
445
  if (!account.lastSyncAt) {
375
446
  throw new Error(`lastSyncedAt is not set for account: ${accountId}`);
376
447
  }
377
- const payments = await step.do(
448
+ if (isSupersededBy(
449
+ event.instanceId,
450
+ account.lastSyncMetadata?.instanceId,
451
+ accountId
452
+ )) {
453
+ logger.info("sync.superseded", {
454
+ accountId,
455
+ instanceId: event.instanceId,
456
+ supersededBy: account.lastSyncMetadata?.instanceId,
457
+ iteration
458
+ });
459
+ return;
460
+ }
461
+ const lastSyncAtMs = account.lastSyncAt.getTime();
462
+ const windowMs = await step.do("resolve sync window", async () => {
463
+ const window = resolveSyncWindow(
464
+ new Date(lastSyncAtMs),
465
+ Date.now(),
466
+ MAX_CATCH_UP_WINDOW_MS
467
+ );
468
+ return {
469
+ dateFromMs: window.dateFrom.getTime(),
470
+ dateToMs: window.dateTo.getTime(),
471
+ isCatchUp: window.isCatchUp
472
+ };
473
+ });
474
+ const syncWindow = {
475
+ dateFrom: new Date(windowMs.dateFromMs),
476
+ dateTo: new Date(windowMs.dateToMs),
477
+ isCatchUp: windowMs.isCatchUp
478
+ };
479
+ const fetched = await step.do(
378
480
  "fetch bank payments",
379
481
  {
380
482
  retries: { limit: 5, delay: "2 minutes", backoff: "exponential" },
@@ -403,42 +505,88 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
403
505
  }
404
506
  ]
405
507
  });
406
- const result = await connector.getAllAccountPayments({
407
- account,
408
- filter: { dateFrom: account.lastSyncAt }
409
- });
508
+ const fetchWindow = async (window) => {
509
+ const result = await connector.getAllAccountPayments({
510
+ account,
511
+ filter: {
512
+ dateFrom: window.dateFrom,
513
+ dateTo: window.dateTo
514
+ }
515
+ });
516
+ const identifiable = result.filter(
517
+ (p) => p.parsed.bankRefId?.trim()
518
+ );
519
+ const skipped = result.length - identifiable.length;
520
+ if (skipped > 0) {
521
+ logger.warn("payments.fetch.skipped_blank_bank_ref", {
522
+ accountId,
523
+ connectorKey: account.connectorKey,
524
+ skipped
525
+ });
526
+ }
527
+ return Promise.all(
528
+ identifiable.map(async (p) => ({
529
+ parsed: {
530
+ ...p.parsed,
531
+ id: await deriveV4PaymentId(p.parsed)
532
+ }
533
+ }))
534
+ );
535
+ };
536
+ let bounded;
537
+ try {
538
+ bounded = await fetchBoundedByWindow({
539
+ window: syncWindow,
540
+ fetchWindow,
541
+ guardResult: guardStepResultSize,
542
+ minWindowMs: MIN_CATCH_UP_WINDOW_MS,
543
+ onNarrowed: (narrowed) => logger.warn("payments.fetch.window-narrowed", {
544
+ accountId,
545
+ connectorKey: account.connectorKey,
546
+ dateFrom: narrowed.dateFrom.toISOString(),
547
+ dateTo: narrowed.dateTo.toISOString()
548
+ })
549
+ });
550
+ } catch (err) {
551
+ if (err instanceof StepResultTooLargeError) {
552
+ logger.error("payments.fetch.result-too-large", {
553
+ accountId,
554
+ connectorKey: account.connectorKey,
555
+ dateFrom: syncWindow.dateFrom.toISOString(),
556
+ dateTo: syncWindow.dateTo.toISOString()
557
+ });
558
+ }
559
+ throw err;
560
+ }
410
561
  logger.info("payments.fetch.completed", {
411
562
  accountId,
412
563
  connectorKey: account.connectorKey,
413
- paymentsCount: result.length
564
+ paymentsCount: bounded.result.length
414
565
  });
415
- const identifiable = result.filter(
416
- (p) => p.parsed.bankRefId?.trim()
417
- );
418
- const skipped = result.length - identifiable.length;
419
- if (skipped > 0) {
420
- logger.warn("payments.fetch.skipped_blank_bank_ref", {
421
- accountId,
422
- connectorKey: account.connectorKey,
423
- skipped
424
- });
425
- }
426
- return Promise.all(
427
- identifiable.map(async (p) => ({
428
- parsed: { ...p.parsed, id: await deriveV4PaymentId(p.parsed) }
429
- }))
430
- );
566
+ return {
567
+ payments: bounded.result,
568
+ windowMs: {
569
+ dateFromMs: bounded.window.dateFrom.getTime(),
570
+ dateToMs: bounded.window.dateTo.getTime(),
571
+ isCatchUp: bounded.window.isCatchUp
572
+ }
573
+ };
431
574
  } catch (err) {
432
- const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : JSON.stringify(err);
433
575
  logger.error("payments.fetch.failed", {
434
576
  accountId,
435
577
  connectorKey: account.connectorKey,
436
- error: message
578
+ error: err instanceof Error ? err.message : String(err)
437
579
  });
438
- throw new Error(message);
580
+ rethrowFetchError(err);
439
581
  }
440
582
  }
441
583
  );
584
+ const payments = fetched.payments;
585
+ const effectiveWindow = {
586
+ dateFrom: new Date(fetched.windowMs.dateFromMs),
587
+ dateTo: new Date(fetched.windowMs.dateToMs),
588
+ isCatchUp: fetched.windowMs.isCatchUp
589
+ };
442
590
  const paymentsToProcess = payments.filter(
443
591
  (p) => isPaymentCompleted(p.parsed)
444
592
  );
@@ -513,6 +661,7 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
513
661
  }))
514
662
  );
515
663
  const lastSyncMetadata = {
664
+ instanceId: event.instanceId,
516
665
  payments: payments.length,
517
666
  paymentsToProcess: paymentsToProcess.length,
518
667
  paymentsInserted: paymentsToInsert.length,
@@ -529,15 +678,27 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
529
678
  };
530
679
  const updateLastSyncCommand = updateAccountLastSyncCommand(db, {
531
680
  accountId: account.id,
532
- lastSyncAt: now,
533
- lastSyncMetadata
681
+ // End of the window just fetched, not `now`: while catching up the
682
+ // window stops short of now, and advancing past it would skip every
683
+ // payment in the gap.
684
+ lastSyncAt: effectiveWindow.dateTo,
685
+ lastSyncMetadata,
686
+ seenInstanceId: account.lastSyncMetadata?.instanceId ?? null
534
687
  }).command;
688
+ let syncStateRows;
535
689
  if (createCommands.length) {
536
- await db.batch(
690
+ const [updateResult] = await db.batch(
537
691
  asNonEmpty([updateLastSyncCommand, ...createCommands])
538
692
  );
693
+ syncStateRows = updateResult;
539
694
  } else {
540
- await updateLastSyncCommand;
695
+ syncStateRows = await updateLastSyncCommand;
696
+ }
697
+ if (syncStateRows.length === 0) {
698
+ logger.info("sync.write-superseded", {
699
+ accountId,
700
+ instanceId: event.instanceId
701
+ });
541
702
  }
542
703
  if (eventsToEmit.length) {
543
704
  await pushToQueue(
@@ -547,14 +708,16 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
547
708
  }
548
709
  return {
549
710
  ...lastSyncMetadata,
550
- newLastSyncAt: now
711
+ newLastSyncAt: effectiveWindow.dateTo
551
712
  };
552
713
  }
553
714
  );
554
- await step.sleep(
555
- "Sleep for next sync",
556
- `${account.syncIntervalS} seconds`
557
- );
715
+ if (!effectiveWindow.isCatchUp) {
716
+ await step.sleep(
717
+ "Sleep for next sync",
718
+ `${account.syncIntervalS} seconds`
719
+ );
720
+ }
558
721
  }
559
722
  }
560
723
  }
@@ -17,6 +17,9 @@ interface BankServiceVariables {
17
17
  DBUCS_TX_AUTH_URI: string;
18
18
  REDIRECT_URI: string;
19
19
  SYNC_WORKFLOW_RESET_AFTER_ITERATIONS: string;
20
+ CRON_SYNC_WORKFLOW_DISPATCH: string;
21
+ SYNC_DISPATCH_ACCOUNT_IDS: string;
22
+ SYNC_WORKFLOW_MAX_ITERATIONS: string;
20
23
  [key: string]: string | number | boolean;
21
24
  }
22
25
  declare const BANK_SERVICE_BINDINGS: {
@@ -17,6 +17,9 @@ interface BankServiceVariables {
17
17
  DBUCS_TX_AUTH_URI: string;
18
18
  REDIRECT_URI: string;
19
19
  SYNC_WORKFLOW_RESET_AFTER_ITERATIONS: string;
20
+ CRON_SYNC_WORKFLOW_DISPATCH: string;
21
+ SYNC_DISPATCH_ACCOUNT_IDS: string;
22
+ SYNC_WORKFLOW_MAX_ITERATIONS: string;
20
23
  [key: string]: string | number | boolean;
21
24
  }
22
25
  declare const BANK_SERVICE_BINDINGS: {
package/dist/service.d.ts CHANGED
@@ -17,6 +17,9 @@ interface BankServiceVariables {
17
17
  DBUCS_TX_AUTH_URI: string;
18
18
  REDIRECT_URI: string;
19
19
  SYNC_WORKFLOW_RESET_AFTER_ITERATIONS: string;
20
+ CRON_SYNC_WORKFLOW_DISPATCH: string;
21
+ SYNC_DISPATCH_ACCOUNT_IDS: string;
22
+ SYNC_WORKFLOW_MAX_ITERATIONS: string;
20
23
  [key: string]: string | number | boolean;
21
24
  }
22
25
  declare const BANK_SERVICE_BINDINGS: {
@@ -1,4 +1,4 @@
1
- import { e as CurrencyCode, R as BankCode, a1 as CountryCode, P as PaymentRequestSelectType } from './bank.DmZly4Hz.js';
1
+ import { e as CurrencyCode, S as BankCode, a2 as CountryCode, P as PaymentRequestSelectType } from './bank.CefGTNH1.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  type ReferenceType = `${'VS' | 'SS' | 'KS'}:${number}`;
@@ -803,6 +803,31 @@ const calculateCzechIban = (accountNumber, bankCode) => {
803
803
  return `CZ${checkDigits}${basicIban}`;
804
804
  };
805
805
 
806
+ const unsupported = (priority, rail, supported) => {
807
+ throw createInternalError(null, {
808
+ message: `instructionPriority=${priority} is not supported on the ${rail} rail (supported: ${supported}).`,
809
+ code: "UNSUPPORTED_INSTRUCTION_PRIORITY",
810
+ status: 422
811
+ });
812
+ };
813
+ const toFinbricksPriority = (priority) => {
814
+ if (!priority || priority === "NORM") return "NORM";
815
+ if (priority === "INST") return "INST";
816
+ return unsupported(priority, "Finbricks SEPA/domestic", "NORM, INST");
817
+ };
818
+ const toFinbricksForeignPriority = (priority) => {
819
+ if (!priority || priority === "NORM") return "NORM";
820
+ return unsupported(priority, "Finbricks foreign (SWIFT)", "NORM");
821
+ };
822
+ const toErstePriority = (priority) => {
823
+ if (!priority || priority === "NORM") return "NORM";
824
+ if (priority === "HIGH") return "HIGH";
825
+ return unsupported(priority, "Erste ISO 20022", "NORM, HIGH");
826
+ };
827
+ const assertDbuPriority = (priority) => {
828
+ if (priority === "HIGH") unsupported(priority, "DBU domestic", "NORM, INST");
829
+ };
830
+
806
831
  function autoVariableSymbol(paymentId) {
807
832
  let hash = 0;
808
833
  for (let i = 0; i < paymentId.length; i++) {
@@ -1090,7 +1115,7 @@ class FinbricksConnector extends IBankConnector {
1090
1115
  description: this.PROVIDER === "CSOB" ? composeWithUltimateCreditor(p.message, p.creditor, {
1091
1116
  stripDiacritics: p.currency !== "CZK"
1092
1117
  }) : p.message,
1093
- instructionPriority: p.instructionPriority ?? "NORM"
1118
+ instructionPriority: toFinbricksPriority(p.instructionPriority)
1094
1119
  }))
1095
1120
  }
1096
1121
  })
@@ -1114,6 +1139,15 @@ class FinbricksConnector extends IBankConnector {
1114
1139
  initiateForeignBatchImpl(_args) {
1115
1140
  throw new Error("Finbricks: Foreign batch not implemented");
1116
1141
  }
1142
+ /**
1143
+ * endToEndIdentification for the SEPA rail. Optional per the Finbricks spec —
1144
+ * return `undefined` to omit the field entirely, which some executing banks
1145
+ * need because they overwrite the remittance text with it (see
1146
+ * `CreditasConnector`).
1147
+ */
1148
+ buildSepaEndToEndId(payment) {
1149
+ return this.buildEndToEndId(payment);
1150
+ }
1117
1151
  async initiateForeignPayment(payment) {
1118
1152
  const debtorAccount = this.connectedAccounts.find(
1119
1153
  (acc) => acc.iban === payment.debtorIban
@@ -1171,6 +1205,9 @@ class FinbricksConnector extends IBankConnector {
1171
1205
  },
1172
1206
  ...creditorAgent && { creditorAgent },
1173
1207
  ...unstructured && { remittanceInformation: { unstructured } },
1208
+ instructionPriority: toFinbricksForeignPriority(
1209
+ payment.instructionPriority
1210
+ ),
1174
1211
  callbackUrl: `${this.finbricks.REDIRECT_URI}?type=paymentRequest&paymentRequestId=${payment.id}`
1175
1212
  }
1176
1213
  })
@@ -1218,6 +1255,10 @@ class FinbricksConnector extends IBankConnector {
1218
1255
  0,
1219
1256
  140
1220
1257
  );
1258
+ const endToEndIdentification = this.buildSepaEndToEndId({
1259
+ ...payment,
1260
+ id: payment.id
1261
+ });
1221
1262
  const bankRefId = uuidv4();
1222
1263
  const [response, error] = await useResult(
1223
1264
  this.finbricks.request({
@@ -1231,10 +1272,7 @@ class FinbricksConnector extends IBankConnector {
1231
1272
  },
1232
1273
  paymentIdentification: {
1233
1274
  merchantTransactionId: bankRefId,
1234
- endToEndIdentification: this.buildEndToEndId({
1235
- ...payment,
1236
- id: payment.id
1237
- })
1275
+ ...endToEndIdentification && { endToEndIdentification }
1238
1276
  },
1239
1277
  amount: payment.amount,
1240
1278
  debtor: {
@@ -1249,8 +1287,7 @@ class FinbricksConnector extends IBankConnector {
1249
1287
  ...creditorAddress && { postalAddress: creditorAddress }
1250
1288
  },
1251
1289
  remittanceInformation: { unstructured },
1252
- // Finbricks SEPA init accepts only NORM/INST — internal HIGH maps to NORM.
1253
- instructionPriority: payment.instructionPriority === "INST" ? "INST" : "NORM",
1290
+ instructionPriority: toFinbricksPriority(payment.instructionPriority),
1254
1291
  callbackUrl: `${this.finbricks.REDIRECT_URI}?type=paymentRequest&paymentRequestId=${payment.id}`
1255
1292
  }
1256
1293
  })
@@ -1303,7 +1340,7 @@ class FinbricksConnector extends IBankConnector {
1303
1340
  // TEMP: clientId disabled per client request — do not send.
1304
1341
  // clientId,
1305
1342
  paymentProvider: this.PROVIDER,
1306
- instructionPriority: payment.instructionPriority ?? "NORM"
1343
+ instructionPriority: toFinbricksPriority(payment.instructionPriority)
1307
1344
  }
1308
1345
  })
1309
1346
  );
@@ -1825,6 +1862,7 @@ class DbuConnector extends IBankConnector {
1825
1862
  };
1826
1863
  }
1827
1864
  async initiateDomesticPayment(payment) {
1865
+ assertDbuPriority(payment.instructionPriority);
1828
1866
  if (payment.instructionPriority === "INST") {
1829
1867
  return this.initiateInstantPayment(payment);
1830
1868
  }
@@ -2172,7 +2210,7 @@ class ErsteConnector extends IBankConnector {
2172
2210
  instructionIdentification: payment.id
2173
2211
  },
2174
2212
  paymentTypeInformation: {
2175
- instructionPriority: payment.instructionPriority ?? "NORM"
2213
+ instructionPriority: toErstePriority(payment.instructionPriority)
2176
2214
  },
2177
2215
  amount: {
2178
2216
  instructedAmount: { value: payment.amount, currency: payment.currency }
@@ -805,6 +805,31 @@ const calculateCzechIban = (accountNumber, bankCode) => {
805
805
  return `CZ${checkDigits}${basicIban}`;
806
806
  };
807
807
 
808
+ const unsupported = (priority, rail, supported) => {
809
+ throw backendSdk.createInternalError(null, {
810
+ message: `instructionPriority=${priority} is not supported on the ${rail} rail (supported: ${supported}).`,
811
+ code: "UNSUPPORTED_INSTRUCTION_PRIORITY",
812
+ status: 422
813
+ });
814
+ };
815
+ const toFinbricksPriority = (priority) => {
816
+ if (!priority || priority === "NORM") return "NORM";
817
+ if (priority === "INST") return "INST";
818
+ return unsupported(priority, "Finbricks SEPA/domestic", "NORM, INST");
819
+ };
820
+ const toFinbricksForeignPriority = (priority) => {
821
+ if (!priority || priority === "NORM") return "NORM";
822
+ return unsupported(priority, "Finbricks foreign (SWIFT)", "NORM");
823
+ };
824
+ const toErstePriority = (priority) => {
825
+ if (!priority || priority === "NORM") return "NORM";
826
+ if (priority === "HIGH") return "HIGH";
827
+ return unsupported(priority, "Erste ISO 20022", "NORM, HIGH");
828
+ };
829
+ const assertDbuPriority = (priority) => {
830
+ if (priority === "HIGH") unsupported(priority, "DBU domestic", "NORM, INST");
831
+ };
832
+
808
833
  function autoVariableSymbol(paymentId) {
809
834
  let hash = 0;
810
835
  for (let i = 0; i < paymentId.length; i++) {
@@ -1092,7 +1117,7 @@ class FinbricksConnector extends IBankConnector {
1092
1117
  description: this.PROVIDER === "CSOB" ? composeWithUltimateCreditor(p.message, p.creditor, {
1093
1118
  stripDiacritics: p.currency !== "CZK"
1094
1119
  }) : p.message,
1095
- instructionPriority: p.instructionPriority ?? "NORM"
1120
+ instructionPriority: toFinbricksPriority(p.instructionPriority)
1096
1121
  }))
1097
1122
  }
1098
1123
  })
@@ -1116,6 +1141,15 @@ class FinbricksConnector extends IBankConnector {
1116
1141
  initiateForeignBatchImpl(_args) {
1117
1142
  throw new Error("Finbricks: Foreign batch not implemented");
1118
1143
  }
1144
+ /**
1145
+ * endToEndIdentification for the SEPA rail. Optional per the Finbricks spec —
1146
+ * return `undefined` to omit the field entirely, which some executing banks
1147
+ * need because they overwrite the remittance text with it (see
1148
+ * `CreditasConnector`).
1149
+ */
1150
+ buildSepaEndToEndId(payment) {
1151
+ return this.buildEndToEndId(payment);
1152
+ }
1119
1153
  async initiateForeignPayment(payment) {
1120
1154
  const debtorAccount = this.connectedAccounts.find(
1121
1155
  (acc) => acc.iban === payment.debtorIban
@@ -1173,6 +1207,9 @@ class FinbricksConnector extends IBankConnector {
1173
1207
  },
1174
1208
  ...creditorAgent && { creditorAgent },
1175
1209
  ...unstructured && { remittanceInformation: { unstructured } },
1210
+ instructionPriority: toFinbricksForeignPriority(
1211
+ payment.instructionPriority
1212
+ ),
1176
1213
  callbackUrl: `${this.finbricks.REDIRECT_URI}?type=paymentRequest&paymentRequestId=${payment.id}`
1177
1214
  }
1178
1215
  })
@@ -1220,6 +1257,10 @@ class FinbricksConnector extends IBankConnector {
1220
1257
  0,
1221
1258
  140
1222
1259
  );
1260
+ const endToEndIdentification = this.buildSepaEndToEndId({
1261
+ ...payment,
1262
+ id: payment.id
1263
+ });
1223
1264
  const bankRefId = backendSdk.uuidv4();
1224
1265
  const [response, error] = await backendSdk.useResult(
1225
1266
  this.finbricks.request({
@@ -1233,10 +1274,7 @@ class FinbricksConnector extends IBankConnector {
1233
1274
  },
1234
1275
  paymentIdentification: {
1235
1276
  merchantTransactionId: bankRefId,
1236
- endToEndIdentification: this.buildEndToEndId({
1237
- ...payment,
1238
- id: payment.id
1239
- })
1277
+ ...endToEndIdentification && { endToEndIdentification }
1240
1278
  },
1241
1279
  amount: payment.amount,
1242
1280
  debtor: {
@@ -1251,8 +1289,7 @@ class FinbricksConnector extends IBankConnector {
1251
1289
  ...creditorAddress && { postalAddress: creditorAddress }
1252
1290
  },
1253
1291
  remittanceInformation: { unstructured },
1254
- // Finbricks SEPA init accepts only NORM/INST — internal HIGH maps to NORM.
1255
- instructionPriority: payment.instructionPriority === "INST" ? "INST" : "NORM",
1292
+ instructionPriority: toFinbricksPriority(payment.instructionPriority),
1256
1293
  callbackUrl: `${this.finbricks.REDIRECT_URI}?type=paymentRequest&paymentRequestId=${payment.id}`
1257
1294
  }
1258
1295
  })
@@ -1305,7 +1342,7 @@ class FinbricksConnector extends IBankConnector {
1305
1342
  // TEMP: clientId disabled per client request — do not send.
1306
1343
  // clientId,
1307
1344
  paymentProvider: this.PROVIDER,
1308
- instructionPriority: payment.instructionPriority ?? "NORM"
1345
+ instructionPriority: toFinbricksPriority(payment.instructionPriority)
1309
1346
  }
1310
1347
  })
1311
1348
  );
@@ -1827,6 +1864,7 @@ class DbuConnector extends IBankConnector {
1827
1864
  };
1828
1865
  }
1829
1866
  async initiateDomesticPayment(payment) {
1867
+ assertDbuPriority(payment.instructionPriority);
1830
1868
  if (payment.instructionPriority === "INST") {
1831
1869
  return this.initiateInstantPayment(payment);
1832
1870
  }
@@ -2174,7 +2212,7 @@ class ErsteConnector extends IBankConnector {
2174
2212
  instructionIdentification: payment.id
2175
2213
  },
2176
2214
  paymentTypeInformation: {
2177
- instructionPriority: payment.instructionPriority ?? "NORM"
2215
+ instructionPriority: toErstePriority(payment.instructionPriority)
2178
2216
  },
2179
2217
  amount: {
2180
2218
  instructedAmount: { value: payment.amount, currency: payment.currency }
@@ -27,6 +27,8 @@ interface LastSyncMetadata {
27
27
  eventsEmitted: number;
28
28
  iterationCount?: number;
29
29
  workflowStartedAt?: number;
30
+ /** Which instance wrote this sync — lets an older sibling detect it was superseded. */
31
+ instanceId?: string;
30
32
  }
31
33
  type ConnectorConfig = Record<string, string | number | boolean | null>;
32
34
 
@@ -3606,7 +3608,7 @@ declare const payment: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
3606
3608
  name: string;
3607
3609
  tableName: "payment";
3608
3610
  dataType: "string enum";
3609
- data: "BOOKED" | "REJECTED" | "PENDING" | "PROCESSING" | "CANCELLED" | "SCHEDULED" | "HOLD" | "INFO";
3611
+ data: "BOOKED" | "REJECTED" | "PROCESSING" | "PENDING" | "CANCELLED" | "SCHEDULED" | "HOLD" | "INFO";
3610
3612
  driverParam: string;
3611
3613
  notNull: true;
3612
3614
  hasDefault: false;
@@ -4110,7 +4112,7 @@ declare const paymentRequest: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
4110
4112
  name: string;
4111
4113
  tableName: "payment_request";
4112
4114
  dataType: "string enum";
4113
- data: "AUTHORIZED" | "OPENED" | "COMPLETED" | "BOOKED" | "SETTLED" | "REJECTED" | "CLOSED";
4115
+ data: "OPENED" | "AUTHORIZED" | "COMPLETED" | "BOOKED" | "SETTLED" | "REJECTED" | "CLOSED";
4114
4116
  driverParam: string;
4115
4117
  notNull: true;
4116
4118
  hasDefault: false;
@@ -5391,5 +5393,5 @@ type ParsedBankPayment = {
5391
5393
  */
5392
5394
  type BatchPayment = Omit<PaymentInsertType, 'bankRefId'>;
5393
5395
 
5394
- export { IBankConnector as I, BASE_TERMINAL_STATUSES as M, BATCH_MODES as N, BATCH_STATUSES as O, CHARGE_BEARERS as X, CONNECTOR_KEYS as Y, COUNTRY_CODES as Z, CREDENTIALS_TYPES as _, INSTRUCTION_PRIORITIES as a3, PAYMENT_DIRECTIONS as a5, PAYMENT_REQUEST_STATUSES as a6, PAYMENT_STATUSES as a7, PAYMENT_TYPES as a8, isTerminalStatus as aA, account as aB, accountCredentials as aC, batch as aD, ott as aE, payment as aF, paymentRequest as aG, TOKEN_TYPES as ai, accountCredentialsInsertSchema as ak, accountCredentialsSelectSchema as al, accountCredentialsUpdateSchema as am, accountInsertSchema as an, accountSelectSchema as ao, accountUpdateSchema as ap, hasPaymentAccountAssigned as aq, isBatchAuthorized as ar, isBatchCompleted as as, isBatchFailed as at, isBatchInitiated as au, isBatchProcessing as av, isBatchReadyToSign as aw, isPaymentCompleted as ax, isPendingStatus as ay, isProcessedStatus as az, tables as t, ACCOUNT_STATUSES as y };
5395
- export type { ChargeBearer as $, AccountSelectType as A, BatchSelectType as B, ConnectorConfig as C, AccountCredentialsSelectType as D, AccountCredentialsUpdateType as E, AccountPatchType as F, AccountStatus as G, HandleAuthorizationCallbackInput as H, AccountUpdateType as J, AuthorizedBatch as K, LastSyncMetadata as L, PaymentRequestSelectType as P, BankAccountWithLastSync as Q, BankCode as R, BatchInsertType as S, BatchLifecycle as T, BatchMode as U, BatchPayment as V, BatchStatus as W, PaymentSelectType as a, CompletedBatch as a0, CountryCode as a1, CredentialsType as a2, InstructionPriority as a4, PaymentDirection as a9, PaymentFailedInsertType as aa, PaymentInsertType as ab, PaymentLifecycle as ac, PaymentPreparedInsertType as ad, PaymentStatus as ae, ProcessingBatch as af, ReadyToSignBatch as ag, ResolvedCredentials as ah, TokenType as aj, ConnectorKey as b, ConfigEnvironmentBank as c, PaymentType as d, CurrencyCode as e, HandleAuthorizationCallbackOutput as f, ConnectedAccount as g, CredentialsResolver as h, AccountCredentialsInsertType as i, AccountInsertType as j, BatchedPayment as k, InitiatedBatch as l, IncomingPayment as m, InitiatedPayment as n, ParsedBankPayment as o, PaymentRequestStatus as p, AuthorizationCallbackResult as q, BatchMetadata as r, Currency as s, AccountAssignedPayment as u, PreparedPayment as v, CompletedPayment as w, PaymentRequestInsertType as x, AccountCredentialsPatchType as z };
5396
+ export { CREDENTIALS_TYPES as $, IBankConnector as I, BASE_TERMINAL_STATUSES as N, BATCH_MODES as O, BATCH_STATUSES as Q, CHARGE_BEARERS as Y, CONNECTOR_KEYS as Z, COUNTRY_CODES as _, INSTRUCTION_PRIORITIES as a4, PAYMENT_DIRECTIONS as a6, PAYMENT_REQUEST_STATUSES as a7, PAYMENT_STATUSES as a8, PAYMENT_TYPES as a9, isProcessedStatus as aA, isTerminalStatus as aB, account as aC, accountCredentials as aD, batch as aE, ott as aF, payment as aG, paymentRequest as aH, TOKEN_TYPES as aj, accountCredentialsInsertSchema as al, accountCredentialsSelectSchema as am, accountCredentialsUpdateSchema as an, accountInsertSchema as ao, accountSelectSchema as ap, accountUpdateSchema as aq, hasPaymentAccountAssigned as ar, isBatchAuthorized as as, isBatchCompleted as at, isBatchFailed as au, isBatchInitiated as av, isBatchProcessing as aw, isBatchReadyToSign as ax, isPaymentCompleted as ay, isPendingStatus as az, tables as t, ACCOUNT_STATUSES as y };
5397
+ export type { AccountSelectType as A, BatchSelectType as B, ConnectorConfig as C, AccountCredentialsSelectType as D, EndToEndIdPayment as E, AccountCredentialsUpdateType as F, AccountPatchType as G, HandleAuthorizationCallbackInput as H, AccountStatus as J, AccountUpdateType as K, LastSyncMetadata as L, AuthorizedBatch as M, PaymentRequestSelectType as P, BankAccountWithLastSync as R, BankCode as S, BatchInsertType as T, BatchLifecycle as U, BatchMode as V, BatchPayment as W, BatchStatus as X, PaymentSelectType as a, ChargeBearer as a0, CompletedBatch as a1, CountryCode as a2, CredentialsType as a3, InstructionPriority as a5, PaymentDirection as aa, PaymentFailedInsertType as ab, PaymentInsertType as ac, PaymentLifecycle as ad, PaymentPreparedInsertType as ae, PaymentStatus as af, ProcessingBatch as ag, ReadyToSignBatch as ah, ResolvedCredentials as ai, TokenType as ak, ConnectorKey as b, ConfigEnvironmentBank as c, PaymentType as d, CurrencyCode as e, HandleAuthorizationCallbackOutput as f, ConnectedAccount as g, CredentialsResolver as h, AccountCredentialsInsertType as i, AccountInsertType as j, BatchedPayment as k, InitiatedBatch as l, IncomingPayment as m, InitiatedPayment as n, ParsedBankPayment as o, PaymentRequestStatus as p, AuthorizationCallbackResult as q, BatchMetadata as r, Currency as s, AccountAssignedPayment as u, PreparedPayment as v, CompletedPayment as w, PaymentRequestInsertType as x, AccountCredentialsPatchType as z };