Add bank synchronization features with FinTS support and update dependencies

This commit is contained in:
Matthias
2026-06-15 13:56:32 +02:00
parent fc0a6fb975
commit d65e7681ac
23 changed files with 2609 additions and 150 deletions

161
convex/bank/fintsSession.ts Normal file
View File

@@ -0,0 +1,161 @@
"use node";
import {
FinTSClient,
FinTSConfig,
type BankingInformation,
type ClientResponse,
} from "lib-fints";
import { resolveFintsEnvFields } from "./fintsConfig";
export type FintsInteractionKind =
| { type: "sync"; syncSystemId: boolean }
| { type: "balance"; accountNumber: string }
| { type: "statements"; accountNumber: string; from?: string; to?: string; preferCamt: boolean };
export type SerializedFintsSession = {
bankingInformation: BankingInformation;
tanMethodId?: number;
tanMediaName?: string;
tanReference: string;
tanContinuation: "sync" | "balance" | "statements";
};
export type FintsEnvConfig = {
productId: string;
productVersion: string;
url: string;
blz: string;
login: string;
pin: string;
tanMethodId?: number;
tanMediaName?: string;
bankingInformation?: BankingInformation;
};
/** @deprecated Import from ./fintsConfig */
export { FINTS_PRODUCT_ID_PLACEHOLDER, getFintsConfigStatus } from "./fintsConfig";
export type { FintsConfigStatus } from "./fintsConfig";
export function resolveFintsEnv(overrides?: {
blz?: string;
url?: string;
login?: string;
productId?: string;
productVersion?: string;
tanMethodId?: number;
tanMediaName?: string;
bankingInformationJson?: string;
pin?: string;
}): FintsEnvConfig {
const fields = resolveFintsEnvFields(overrides);
if (fields.usesProductIdPlaceholder) {
console.warn("[fints] FINTS_PRODUCT_ID fehlt Platzhalter wird verwendet");
}
let bankingInformation: BankingInformation | undefined;
if (overrides?.bankingInformationJson) {
bankingInformation = JSON.parse(overrides.bankingInformationJson) as BankingInformation;
}
return {
productId: fields.productId,
productVersion: fields.productVersion,
url: fields.url,
blz: fields.blz,
login: fields.login,
pin: fields.pin,
tanMethodId: overrides?.tanMethodId,
tanMediaName: overrides?.tanMediaName,
bankingInformation,
};
}
export function createFinTsClient(config: FintsEnvConfig): FinTSClient {
const fintsConfig = config.bankingInformation
? FinTSConfig.fromBankingInformation(
config.productId,
config.productVersion,
config.bankingInformation,
config.login,
config.pin,
config.tanMethodId,
config.tanMediaName,
)
: FinTSConfig.forFirstTimeUse(
config.productId,
config.productVersion,
config.url,
config.blz,
config.login,
config.pin,
);
const client = new FinTSClient(fintsConfig);
if (config.tanMethodId) {
client.selectTanMethod(config.tanMethodId);
}
if (config.tanMediaName) {
client.selectTanMedia(config.tanMediaName);
}
return client;
}
export function buildSessionSnapshot(
client: FinTSClient,
tanReference: string,
tanContinuation: SerializedFintsSession["tanContinuation"],
): SerializedFintsSession {
return {
bankingInformation: client.config.bankingInformation,
tanMethodId: client.config.selectedTanMethod?.id,
tanMediaName: client.config.selectedTanMethod?.activeTanMedia?.[0],
tanReference,
tanContinuation,
};
}
export async function continueWithTan(
client: FinTSClient,
session: SerializedFintsSession,
tan?: string,
): Promise<ClientResponse> {
switch (session.tanContinuation) {
case "sync":
return await client.synchronizeWithTan(session.tanReference, tan);
case "balance":
return await client.getAccountBalanceWithTan(session.tanReference, tan);
case "statements":
return await client.getAccountStatementsWithTan(session.tanReference, tan);
}
}
export function encodePhotoTan(response: ClientResponse): {
mimeType?: string;
base64?: string;
} {
if (!response.tanPhoto?.image) return {};
const bytes =
response.tanPhoto.image instanceof Uint8Array
? response.tanPhoto.image
: new Uint8Array(response.tanPhoto.image);
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return {
mimeType: response.tanPhoto.mimeType,
base64: btoa(binary),
};
}
export function pickDecoupledTanMethod(client: FinTSClient): number | undefined {
const methods = client.config.availableTanMethods;
const decoupled = methods.find((m) => m.isDecoupled);
if (decoupled) return decoupled.id;
return methods[0]?.id;
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}