Add bank synchronization features with FinTS support and update dependencies
This commit is contained in:
161
convex/bank/fintsSession.ts
Normal file
161
convex/bank/fintsSession.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user