UNPKG

n8n-nodes-aqbanking

Version:

n8n community node to interact with German banks using FinTS/HBCI protocol for account balance queries and transaction retrieval - Development/Testing version without aqbanking-cli dependency

464 lines (452 loc) 19 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.executeDockerFinTS = exports.executePythonFinTS = exports.NativeFinTSClient = void 0; const FinTS = __importStar(require("fints")); const n8n_workflow_1 = require("n8n-workflow"); class NativeFinTSClient { constructor(credentials) { this.credentials = credentials; } async initializeClient() { if (this.client) { return; } try { const FinTSClient = FinTS.default || FinTS.FinTSClient || FinTS; this.client = new FinTSClient(this.credentials.bankCode, this.credentials.userId, this.credentials.pin, this.credentials.serverUrl); // Set product ID for identification if (this.client.productId !== undefined) { this.client.productId = 'n8n-aqbanking-node'; } if (this.credentials.enableDebugLogging) { // Enable debug logging if requested if (this.client.debug !== undefined) { this.client.debug = true; } } } catch (error) { throw new Error(`Failed to initialize FinTS client: ${error.message}`); } } async getBalance(accountNumber) { var _a, _b; await this.initializeClient(); try { // Get accounts first const accounts = await this.client.accounts(); // Find the requested account const account = accounts.find((acc) => { var _a; return acc.accountNumber === accountNumber || acc.iban === accountNumber || ((_a = acc.iban) === null || _a === void 0 ? void 0 : _a.replace(/\s/g, '')) === accountNumber.replace(/\s/g, ''); }); if (!account) { throw new Error(`Account ${accountNumber} not found. Available accounts: ${accounts.map((a) => a.iban || a.accountNumber).join(', ')}`); } // Get balance for the account const balance = await this.client.balance(account); return { balance: parseFloat(((_a = balance.amount) === null || _a === void 0 ? void 0 : _a.toString()) || '0'), currency: balance.currency || 'EUR', accountNumber: account.accountNumber, iban: account.iban, bankCode: account.bankCode, accountName: account.name, timestamp: new Date().toISOString(), date: ((_b = balance.date) === null || _b === void 0 ? void 0 : _b.toISOString()) || new Date().toISOString(), }; } catch (error) { throw new Error(`Failed to get balance: ${error.message}`); } } async getTransactions(accountNumber, startDate, endDate) { await this.initializeClient(); try { // Get accounts first const accounts = await this.client.accounts(); // Find the requested account const account = accounts.find((acc) => { var _a; return acc.accountNumber === accountNumber || acc.iban === accountNumber || ((_a = acc.iban) === null || _a === void 0 ? void 0 : _a.replace(/\s/g, '')) === accountNumber.replace(/\s/g, ''); }); if (!account) { throw new Error(`Account ${accountNumber} not found`); } // Get transactions for the account const transactions = await this.client.transactions(account, startDate, endDate); // Transform transactions to standardized format const transformedTransactions = transactions.map((transaction) => { var _a, _b, _c; return ({ amount: parseFloat(((_a = transaction.amount) === null || _a === void 0 ? void 0 : _a.toString()) || '0'), currency: transaction.currency || 'EUR', date: ((_b = transaction.date) === null || _b === void 0 ? void 0 : _b.toISOString()) || '', valutaDate: ((_c = transaction.valutaDate) === null || _c === void 0 ? void 0 : _c.toISOString()) || '', remoteName: transaction.remoteName || '', purpose: transaction.purpose || transaction.text || '', remoteIBAN: transaction.remoteIban || '', remoteBIC: transaction.remoteBic || '', transactionCode: transaction.transactionCode || '', reference: transaction.reference || '', bookingText: transaction.bookingText || '', primaNotaNumber: transaction.primaNotaNumber || '', }); }); return { transactions: transformedTransactions, count: transformedTransactions.length, accountNumber: account.accountNumber, iban: account.iban, bankCode: account.bankCode, timestamp: new Date().toISOString(), }; } catch (error) { throw new Error(`Failed to get transactions: ${error.message}`); } } async getAccounts() { await this.initializeClient(); try { const accounts = await this.client.accounts(); return accounts.map((account) => ({ accountNumber: account.accountNumber, iban: account.iban, bic: account.bic, bankCode: account.bankCode, accountName: account.name || account.accountName, bankName: account.bankName, accountType: account.type, })); } catch (error) { throw new Error(`Failed to get accounts: ${error.message}`); } } async close() { if (this.client) { try { await this.client.close(); } catch (error) { // Ignore close errors } this.client = null; } } } exports.NativeFinTSClient = NativeFinTSClient; // Python FinTS implementation async function executePythonFinTS(executeFunctions, operation, params) { const { spawn } = require('child_process'); const credentials = params.credentials; return new Promise((resolve, reject) => { const pythonScript = generatePythonScript(operation, params); const pythonPath = credentials.pythonPath || 'python'; const pythonProcess = spawn(pythonPath, ['-c', pythonScript], { stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; pythonProcess.stdout.on('data', (data) => { stdout += data.toString(); }); pythonProcess.stderr.on('data', (data) => { stderr += data.toString(); }); pythonProcess.on('close', (code) => { if (code !== 0) { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Python FinTS process failed with code ${code}. Error: ${stderr}`)); return; } try { const result = JSON.parse(stdout); resolve(result); } catch (error) { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Failed to parse Python FinTS response: ${error.message}`)); } }); pythonProcess.on('error', (error) => { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Failed to execute Python FinTS: ${error.message}. Make sure Python and python-fints are installed.`)); }); }); } exports.executePythonFinTS = executePythonFinTS; function generatePythonScript(operation, params) { const credentials = params.credentials; const baseScript = ` import sys import json from datetime import datetime, date try: from fints import FinTS3PinTanClient except ImportError: print(json.dumps({"error": "python-fints package not installed. Run: pip install python-fints"})) sys.exit(1) def json_serial(obj): if isinstance(obj, (datetime, date)): return obj.isoformat() raise TypeError(f"Type {type(obj)} not serializable") try: client = FinTS3PinTanClient( "${credentials.bankCode}", "${credentials.userId}", "${credentials.pin}", "${credentials.serverUrl}" ) `; switch (operation) { case 'getBalance': return baseScript + ` accounts = client.get_sepa_accounts() account = None for acc in accounts: if acc.iban == "${params.accountNumber}" or acc.accountnumber == "${params.accountNumber}": account = acc break if not account: result = {"error": f"Account ${params.accountNumber} not found"} else: balance = client.get_balance(account) result = { "balance": float(balance.amount.amount), "currency": balance.amount.currency, "accountNumber": account.accountnumber, "iban": account.iban, "timestamp": datetime.now().isoformat() } print(json.dumps(result, default=json_serial)) except Exception as e: print(json.dumps({"error": str(e)})) `; case 'getTransactions': return baseScript + ` accounts = client.get_sepa_accounts() account = None for acc in accounts: if acc.iban == "${params.accountNumber}" or acc.accountnumber == "${params.accountNumber}": account = acc break if not account: result = {"error": f"Account ${params.accountNumber} not found"} else: start_date = ${params.startDate ? `datetime.fromisoformat("${params.startDate}")` : 'None'} end_date = ${params.endDate ? `datetime.fromisoformat("${params.endDate}")` : 'None'} transactions = client.get_transactions(account, start_date, end_date) trans_list = [] for trans in transactions: trans_list.append({ "amount": float(trans.data["amount"].amount), "currency": trans.data["amount"].currency, "date": trans.data["booking_date"].isoformat() if trans.data.get("booking_date") else "", "remoteName": trans.data.get("applicant_name", ""), "purpose": trans.data.get("purpose", ""), "reference": trans.data.get("end_to_end_reference", "") }) result = { "transactions": trans_list, "count": len(trans_list), "accountNumber": account.accountnumber, "iban": account.iban, "timestamp": datetime.now().isoformat() } print(json.dumps(result, default=json_serial)) except Exception as e: print(json.dumps({"error": str(e)})) `; default: return baseScript + ` print(json.dumps({"error": f"Unknown operation: ${operation}"})) `; } } // Docker FinTS implementation async function executeDockerFinTS(executeFunctions, operation, params) { const { spawn } = require('child_process'); const credentials = params.credentials; const os = require('os'); return new Promise((resolve, reject) => { const dockerImage = credentials.dockerImage || 'ghcr.io/larsux/aqbanking-docker'; const configVolume = `${os.homedir()}/.aqbanking:/root/.aqbanking`; const dockerArgs = [ 'run', '--rm', '-v', configVolume, dockerImage, 'aqbanking-cli', '--noninteractive', ...buildDockerOperationArgs(operation, params) ]; const dockerProcess = spawn('docker', dockerArgs, { stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; dockerProcess.stdout.on('data', (data) => { stdout += data.toString(); }); dockerProcess.stderr.on('data', (data) => { stderr += data.toString(); }); dockerProcess.on('close', (code) => { if (code !== 0) { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Docker FinTS process failed with code ${code}. Error: ${stderr}`)); return; } try { // Parse the AqBanking CLI output const result = parseAqBankingOutput(operation, stdout); resolve(result); } catch (error) { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Failed to parse Docker FinTS response: ${error.message}`)); } }); dockerProcess.on('error', (error) => { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Failed to execute Docker FinTS: ${error.message}. Make sure Docker is installed and running.`)); }); }); } exports.executeDockerFinTS = executeDockerFinTS; function buildDockerOperationArgs(operation, params) { const args = []; // Add common arguments first if (params.credentials.nonInteractive) { args.push('--noninteractive'); } // Add server URL if provided if (params.credentials.serverUrl) { args.push('--url', params.credentials.serverUrl); } switch (operation) { case 'getBalance': args.push('request', '--balance'); if (params.bankCode) { args.push('-b', params.bankCode); } args.push('-a', params.accountNumber); break; case 'getTransactions': args.push('request', '--transactions'); if (params.bankCode) { args.push('-b', params.bankCode); } args.push('-a', params.accountNumber); if (params.startDate) { args.push('--fromdate', params.startDate); } if (params.endDate) { args.push('--todate', params.endDate); } break; } return args; } function parseAqBankingOutput(operation, output) { // Reuse existing parsing functions from the main node switch (operation) { case 'getBalance': return parseBalanceOutputForDocker(output); case 'getTransactions': return parseTransactionOutputForDocker(output); default: return { rawOutput: output }; } } function parseBalanceOutputForDocker(output) { const patterns = [ /(?:Wert|Value|Balance)\s*:\s*([\d,.-]+)\s*([A-Z]{3})/i, /Saldo\s*:\s*([\d,.-]+)\s*([A-Z]{3})/i, /([\d,.-]+)\s*([A-Z]{3})\s*(?:Saldo|Balance)/i, ]; for (const pattern of patterns) { const match = output.match(pattern); if (match) { const balanceString = match[1].replace(/\./g, '').replace(',', '.'); const balance = parseFloat(balanceString); if (!isNaN(balance)) { return { balance, currency: match[2] }; } } } throw new Error(`Could not parse balance from Docker output: ${output}`); } function parseTransactionOutputForDocker(output) { const transactions = []; const lines = output.split('\n'); let currentTransaction = {}; for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('transaction ') || trimmedLine.startsWith('Transaction ')) { if (Object.keys(currentTransaction).length > 0) { transactions.push(currentTransaction); } currentTransaction = {}; continue; } const colonIndex = trimmedLine.indexOf(':'); if (colonIndex === -1) continue; const key = trimmedLine.substring(0, colonIndex).trim(); const value = trimmedLine.substring(colonIndex + 1).trim(); mapTransactionFieldForDocker(currentTransaction, key, value); } if (Object.keys(currentTransaction).length > 0) { transactions.push(currentTransaction); } return transactions; } function mapTransactionFieldForDocker(transaction, key, value) { const keyLower = key.toLowerCase(); if (keyLower.includes('value_value') || keyLower.includes('amount')) { transaction.amount = parseFloat(value.replace(',', '.')); } else if (keyLower.includes('value_currency') || keyLower.includes('currency')) { transaction.currency = value; } else if (keyLower.includes('date') || keyLower.includes('valutadate')) { transaction.date = value; } else if (keyLower.includes('remotename') || keyLower.includes('name')) { transaction.remoteName = value; } else if (keyLower.includes('purpose') || keyLower.includes('memo') || keyLower.includes('reference')) { transaction.purpose = value; } else if (keyLower.includes('remoteiban') || keyLower.includes('iban')) { transaction.remoteIBAN = value; } else if (keyLower.includes('remotebic') || keyLower.includes('bic')) { transaction.remoteBIC = value; } else if (keyLower.includes('transactioncode') || keyLower.includes('code')) { transaction.transactionCode = value; } }