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

877 lines (876 loc) 36.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AqBanking = void 0; const n8n_workflow_1 = require("n8n-workflow"); const child_process_1 = require("child_process"); const FinTSClient_1 = require("./FinTSClient"); class AqBanking { constructor() { this.description = { displayName: 'AqBanking', name: 'aqBanking', icon: 'fa:university', group: ['productivity'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Interact with German banks using AqBanking (FinTS/HBCI protocol)', defaults: { name: 'AqBanking', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], credentials: [ { name: 'aqBankingCredentials', required: true, }, ], usableAsTool: true, properties: [ { displayName: 'Resource', name: 'resource', type: 'options', noDataExpression: true, options: [ { name: 'Account', value: 'account', }, { name: 'Transaction', value: 'transaction', }, { name: 'User Management', value: 'user', }, ], default: 'account', }, // Account Operations { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['account'], }, }, options: [ { name: 'Get Balance', value: 'getBalance', action: 'Get the balance of an account', description: 'Retrieve the current balance of a bank account', }, { name: 'List Accounts', value: 'listAccounts', action: 'List all configured accounts', description: 'Get a list of all configured bank accounts', }, { name: 'Get Account Info', value: 'getAccountInfo', action: 'Get detailed account information', description: 'Retrieve detailed information about a specific account', }, ], default: 'getBalance', }, // Transaction Operations { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['transaction'], }, }, options: [ { name: 'Get Transactions', value: 'getTransactions', action: 'Get account transactions', description: 'Retrieve transaction history for an account', }, { name: 'Export Transactions', value: 'exportTransactions', action: 'Export transactions to CSV', description: 'Export transactions in CSV format for further processing', }, ], default: 'getTransactions', }, // User Management Operations { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, displayOptions: { show: { resource: ['user'], }, }, options: [ { name: 'List Users', value: 'listUsers', action: 'List configured users', description: 'Get a list of all configured FinTS/HBCI users', }, { name: 'Get System ID', value: 'getSystemId', action: 'Get system ID from bank', description: 'Retrieve system ID from the bank server', }, { name: 'Get TAN Methods', value: 'getTanMethods', action: 'Get available TAN methods', description: 'Retrieve available TAN methods from the bank', }, ], default: 'listUsers', }, // Common Parameters { displayName: 'Account Number/IBAN', name: 'accountNumber', type: 'string', default: '', displayOptions: { show: { operation: ['getBalance', 'getTransactions', 'exportTransactions', 'getAccountInfo'], }, }, description: 'The bank account number or IBAN to query', placeholder: 'DE89370400440532013000 or 532013000', }, { displayName: 'Bank Code (BLZ)', name: 'bankCode', type: 'string', default: '', displayOptions: { show: { operation: ['getBalance', 'getTransactions', 'exportTransactions', 'getAccountInfo'], }, }, description: 'Bank code (BLZ) - only required if using account number instead of IBAN', placeholder: '37040044', }, // Transaction specific parameters { displayName: 'Date Range', name: 'dateRange', type: 'options', displayOptions: { show: { operation: ['getTransactions', 'exportTransactions'], }, }, options: [ { name: 'All Available', value: 'all', }, { name: 'Last 30 Days', value: 'last30Days', }, { name: 'Last 90 Days', value: 'last90Days', }, { name: 'Custom Range', value: 'custom', }, ], default: 'last30Days', }, { displayName: 'Start Date', name: 'startDate', type: 'dateTime', default: '', displayOptions: { show: { operation: ['getTransactions', 'exportTransactions'], dateRange: ['custom'], }, }, description: 'The start date for the transaction query', }, { displayName: 'End Date', name: 'endDate', type: 'dateTime', default: '', displayOptions: { show: { operation: ['getTransactions', 'exportTransactions'], dateRange: ['custom'], }, }, description: 'The end date for the transaction query', }, // Advanced Options { displayName: 'Additional Options', name: 'additionalOptions', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { displayName: 'Simplify Output', name: 'simplifyOutput', type: 'boolean', default: true, description: 'Whether to simplify the output format for easier processing', }, { displayName: 'Include Raw Output', name: 'includeRawOutput', type: 'boolean', default: false, description: 'Whether to include the raw AqBanking output in the result', }, { displayName: 'Max Results', name: 'maxResults', type: 'number', default: 0, description: 'Maximum number of results to return (0 = no limit)', }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', displayOptions: { show: { '/operation': ['exportTransactions'], }, }, options: [ { name: 'CSV', value: 'csv', }, { name: 'JSON', value: 'json', }, ], default: 'csv', description: 'The format for exported transaction data', }, ], }, ], }; } // eslint-disable-next-line no-unused-vars async execute() { const items = this.getInputData(); const returnData = []; for (let i = 0; i < items.length; i++) { const resource = this.getNodeParameter('resource', i); const operation = this.getNodeParameter('operation', i); const credentials = await this.getCredentials('aqBankingCredentials'); try { let result; if (resource === 'account') { result = await handleAccountOperations(this, operation, i, credentials); } else if (resource === 'transaction') { result = await handleTransactionOperations(this, operation, i, credentials); } else if (resource === 'user') { result = await handleUserOperations(this, operation, i, credentials); } returnData.push({ json: result, pairedItem: { item: i }, }); } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { error: error.message, operation, resource, }, pairedItem: { item: i }, }); continue; } throw error; } } return [returnData]; } } exports.AqBanking = AqBanking; // Helper functions async function handleAccountOperations(executeFunctions, operation, itemIndex, credentials) { switch (operation) { case 'getBalance': return getBalance(executeFunctions, itemIndex, credentials); case 'listAccounts': return listAccounts(executeFunctions, credentials); case 'getAccountInfo': return getAccountInfo(executeFunctions, itemIndex, credentials); default: throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Unknown account operation: ${operation}`); } } async function handleTransactionOperations(executeFunctions, operation, itemIndex, credentials) { switch (operation) { case 'getTransactions': return getTransactions(executeFunctions, itemIndex, credentials); case 'exportTransactions': return exportTransactions(executeFunctions, itemIndex, credentials); default: throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Unknown transaction operation: ${operation}`); } } async function handleUserOperations(executeFunctions, operation, itemIndex, credentials) { switch (operation) { case 'listUsers': return listUsers(executeFunctions, credentials); case 'getSystemId': return getSystemId(executeFunctions, credentials); case 'getTanMethods': return getTanMethods(executeFunctions, credentials); default: throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Unknown user operation: ${operation}`); } } async function getBalance(executeFunctions, itemIndex, credentials) { const accountNumber = executeFunctions.getNodeParameter('accountNumber', itemIndex, ''); const bankCode = executeFunctions.getNodeParameter('bankCode', itemIndex, ''); const additionalOptions = executeFunctions.getNodeParameter('additionalOptions', itemIndex, {}); if (!accountNumber) { throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), 'Account number is required for balance query'); } const implementation = credentials.fintsImplementation || 'system'; switch (implementation) { case 'native': return getBalanceNative(executeFunctions, itemIndex, credentials, accountNumber, additionalOptions); case 'python': return (0, FinTSClient_1.executePythonFinTS)(executeFunctions, 'getBalance', { credentials, accountNumber, bankCode, ...additionalOptions }); case 'docker': return (0, FinTSClient_1.executeDockerFinTS)(executeFunctions, 'getBalance', { credentials, accountNumber, bankCode, ...additionalOptions }); case 'system': default: return getBalanceSystem(executeFunctions, itemIndex, credentials, accountNumber, bankCode, additionalOptions); } } async function getBalanceNative(executeFunctions, itemIndex, credentials, accountNumber, additionalOptions) { const client = new FinTSClient_1.NativeFinTSClient(credentials); try { const result = await client.getBalance(accountNumber); return { ...result, ...(additionalOptions.includeRawOutput && { rawOutput: 'Native FinTS implementation - no raw output available' }), }; } finally { await client.close(); } } async function getBalanceSystem(executeFunctions, itemIndex, credentials, accountNumber, bankCode, additionalOptions) { const args = buildBaseArgs(credentials); args.push('request', '--balance'); if (bankCode) { args.push('-b', bankCode); } args.push('-a', accountNumber); const result = await executeAqBankingCommand(executeFunctions, 'aqbanking-cli', args, credentials); const parsed = parseBalanceOutput(executeFunctions, result.stdout); return { ...parsed, accountNumber, bankCode: bankCode || credentials.bankCode, timestamp: new Date().toISOString(), ...(additionalOptions.includeRawOutput && { rawOutput: result.stdout }), }; } async function getTransactions(executeFunctions, itemIndex, credentials) { const accountNumber = executeFunctions.getNodeParameter('accountNumber', itemIndex, ''); const bankCode = executeFunctions.getNodeParameter('bankCode', itemIndex, ''); const dateRange = executeFunctions.getNodeParameter('dateRange', itemIndex, 'last30Days'); const additionalOptions = executeFunctions.getNodeParameter('additionalOptions', itemIndex, {}); if (!accountNumber) { throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), 'Account number is required for transaction query'); } const implementation = credentials.fintsImplementation || 'system'; const { startDate, endDate } = getDateRange(executeFunctions, dateRange, itemIndex); switch (implementation) { case 'native': return getTransactionsNative(executeFunctions, itemIndex, credentials, accountNumber, startDate, endDate, additionalOptions); case 'python': return (0, FinTSClient_1.executePythonFinTS)(executeFunctions, 'getTransactions', { credentials, accountNumber, bankCode, startDate, endDate, ...additionalOptions }); case 'docker': return (0, FinTSClient_1.executeDockerFinTS)(executeFunctions, 'getTransactions', { credentials, accountNumber, bankCode, startDate, endDate, ...additionalOptions }); case 'system': default: return getTransactionsSystem(executeFunctions, itemIndex, credentials, accountNumber, bankCode, dateRange, additionalOptions); } } async function getTransactionsNative(executeFunctions, itemIndex, credentials, accountNumber, startDate, endDate, additionalOptions) { const client = new FinTSClient_1.NativeFinTSClient(credentials); try { const startDateObj = startDate ? new Date(startDate.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')) : undefined; const endDateObj = endDate ? new Date(endDate.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')) : undefined; const result = await client.getTransactions(accountNumber, startDateObj, endDateObj); // Apply max results limit if (additionalOptions.maxResults && additionalOptions.maxResults > 0) { result.transactions = result.transactions.slice(0, additionalOptions.maxResults); result.count = result.transactions.length; } return { ...result, dateRange: { startDate, endDate }, ...(additionalOptions.includeRawOutput && { rawOutput: 'Native FinTS implementation - no raw output available' }), }; } finally { await client.close(); } } async function getTransactionsSystem(executeFunctions, itemIndex, credentials, accountNumber, bankCode, dateRange, additionalOptions) { const args = buildBaseArgs(credentials); args.push('request', '--transactions'); if (bankCode) { args.push('-b', bankCode); } args.push('-a', accountNumber); // Handle date range const { startDate, endDate } = getDateRange(executeFunctions, dateRange, itemIndex); if (startDate) { args.push('--fromdate', startDate); } if (endDate) { args.push('--todate', endDate); } const result = await executeAqBankingCommand(executeFunctions, 'aqbanking-cli', args, credentials); let transactions = parseTransactionOutput(result.stdout); // Apply max results limit if (additionalOptions.maxResults && additionalOptions.maxResults > 0) { transactions = transactions.slice(0, additionalOptions.maxResults); } return { transactions, count: transactions.length, accountNumber, bankCode: bankCode || credentials.bankCode, dateRange: { startDate, endDate }, timestamp: new Date().toISOString(), ...(additionalOptions.includeRawOutput && { rawOutput: result.stdout }), }; } async function exportTransactions(executeFunctions, itemIndex, credentials) { const additionalOptions = executeFunctions.getNodeParameter('additionalOptions', itemIndex, {}); const outputFormat = additionalOptions.outputFormat || 'csv'; // Get transactions first const transactionData = await getTransactions(executeFunctions, itemIndex, credentials); if (outputFormat === 'csv') { const csvData = convertTransactionsToCsv(transactionData.transactions); return { ...transactionData, csvData, format: 'csv', }; } return { ...transactionData, format: 'json', }; } async function listAccounts(executeFunctions, credentials) { const args = ['listaccounts']; const result = await executeAqBankingCommand(executeFunctions, 'aqhbci-tool4', args, credentials); return { accounts: parseAccountListOutput(result.stdout), timestamp: new Date().toISOString(), }; } async function getAccountInfo(executeFunctions, itemIndex, credentials) { const accountNumber = executeFunctions.getNodeParameter('accountNumber', itemIndex, ''); if (!accountNumber) { throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), 'Account number is required'); } // Get account list and find the specific account const accountsResult = await listAccounts(executeFunctions, credentials); const account = accountsResult.accounts.find((acc) => acc.accountNumber === accountNumber || acc.iban === accountNumber); if (!account) { throw new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Account ${accountNumber} not found`); } return { ...account, timestamp: new Date().toISOString(), }; } async function listUsers(executeFunctions, credentials) { const args = ['listusers']; const result = await executeAqBankingCommand(executeFunctions, 'aqhbci-tool4', args, credentials); return { users: parseUserListOutput(result.stdout), timestamp: new Date().toISOString(), }; } async function getSystemId(executeFunctions, credentials) { const args = buildHbciArgs(credentials); args.push('getsysid'); const result = await executeAqBankingCommand(executeFunctions, 'aqhbci-tool4', args, credentials); return { systemId: parseSystemIdOutput(result.stdout), timestamp: new Date().toISOString(), }; } async function getTanMethods(executeFunctions, credentials) { const args = buildHbciArgs(credentials); args.push('getitanmodes'); const result = await executeAqBankingCommand(executeFunctions, 'aqhbci-tool4', args, credentials); return { tanMethods: parseTanMethodsOutput(result.stdout), timestamp: new Date().toISOString(), }; } // Helper methods for building command arguments function buildBaseArgs(credentials) { const args = []; if (credentials.nonInteractive) { args.push('--noninteractive'); } // Add server URL if provided if (credentials.serverUrl) { args.push('--url', credentials.serverUrl); } return args; } function buildHbciArgs(credentials) { const args = []; if (credentials.userId) { args.push('-u', credentials.userId); } // Add server URL if provided if (credentials.serverUrl) { args.push('--url', credentials.serverUrl); } return args; } // Date range calculation function getDateRange(executeFunctions, dateRange, itemIndex) { const now = new Date(); switch (dateRange) { case 'all': return {}; case 'last30Days': { const thirtyDaysAgo = new Date(now); thirtyDaysAgo.setDate(now.getDate() - 30); return { startDate: formatDate(thirtyDaysAgo), endDate: formatDate(now), }; } case 'last90Days': { const ninetyDaysAgo = new Date(now); ninetyDaysAgo.setDate(now.getDate() - 90); return { startDate: formatDate(ninetyDaysAgo), endDate: formatDate(now), }; } case 'custom': { const startDate = executeFunctions.getNodeParameter('startDate', itemIndex, ''); const endDate = executeFunctions.getNodeParameter('endDate', itemIndex, ''); return { startDate: startDate ? formatDate(new Date(startDate)) : undefined, endDate: endDate ? formatDate(new Date(endDate)) : undefined, }; } default: return {}; } } function formatDate(date) { return date.toISOString().split('T')[0].replace(/-/g, ''); } // Command execution async function executeAqBankingCommand(executeFunctions, command, args, credentials) { return new Promise((resolve, reject) => { if (credentials.enableDebugLogging) { process.env.GWEN_LOGLEVEL = 'info'; process.env.AQBANKING_LOGLEVEL = 'info'; process.env.AQHBCI_LOGLEVEL = 'info'; } const childProcess = (0, child_process_1.spawn)(command, args, { stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; childProcess.stdout.on('data', (data) => { stdout += data.toString(); }); childProcess.stderr.on('data', (data) => { stderr += data.toString(); }); childProcess.on('close', (code) => { if (code !== 0) { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `${command} exited with code ${code}. Error: ${stderr}`)); return; } resolve({ stdout, stderr }); }); childProcess.on('error', (error) => { reject(new n8n_workflow_1.NodeOperationError(executeFunctions.getNode(), `Failed to execute ${command}: ${error.message}`)); }); // Send PIN if needed (for non-interactive mode, this should be set up via mkpinlist) if (credentials.pin && !credentials.nonInteractive) { childProcess.stdin.write(credentials.pin + '\n'); } childProcess.stdin.end(); }); } // Output parsing methods function parseBalanceOutput(_executeFunctions, output) { // Try multiple regex patterns for different output formats 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 n8n_workflow_1.NodeOperationError(_executeFunctions.getNode(), `Could not parse balance from AqBanking output. Output: ${output}`); } function parseTransactionOutput(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(cleanTransaction(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(); // Map various field names to standardized properties mapTransactionField(currentTransaction, key, value); } if (Object.keys(currentTransaction).length > 0) { transactions.push(cleanTransaction(currentTransaction)); } return transactions; } function mapTransactionField(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; } } function cleanTransaction(transaction) { // Ensure required fields have default values return { amount: transaction.amount || 0, currency: transaction.currency || 'EUR', date: transaction.date || '', remoteName: transaction.remoteName || '', purpose: transaction.purpose || '', remoteIBAN: transaction.remoteIBAN || '', remoteBIC: transaction.remoteBIC || '', transactionCode: transaction.transactionCode || '', ...transaction, }; } function parseAccountListOutput(output) { const accounts = []; const lines = output.split('\n'); let currentAccount = {}; for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('Account ') || trimmedLine.startsWith('Konto ')) { if (Object.keys(currentAccount).length > 0) { accounts.push(currentAccount); } currentAccount = {}; continue; } const colonIndex = trimmedLine.indexOf(':'); if (colonIndex === -1) continue; const key = trimmedLine.substring(0, colonIndex).trim().toLowerCase(); const value = trimmedLine.substring(colonIndex + 1).trim(); if (key.includes('number') || key.includes('nummer')) { currentAccount.accountNumber = value; } else if (key.includes('iban')) { currentAccount.iban = value; } else if (key.includes('bic') || key.includes('blz')) { currentAccount.bic = value; } else if (key.includes('name') || key.includes('owner')) { currentAccount.accountName = value; } else if (key.includes('bank')) { currentAccount.bankName = value; } } if (Object.keys(currentAccount).length > 0) { accounts.push(currentAccount); } return accounts; } function parseUserListOutput(output) { const users = []; const lines = output.split('\n'); let currentUser = {}; for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('User ') || trimmedLine.startsWith('Benutzer ')) { if (Object.keys(currentUser).length > 0) { users.push(currentUser); } currentUser = {}; continue; } const colonIndex = trimmedLine.indexOf(':'); if (colonIndex === -1) continue; const key = trimmedLine.substring(0, colonIndex).trim().toLowerCase(); const value = trimmedLine.substring(colonIndex + 1).trim(); if (key.includes('id') || key.includes('userid')) { currentUser.userId = value; } else if (key.includes('name')) { currentUser.userName = value; } else if (key.includes('bank') || key.includes('blz')) { currentUser.bankCode = value; } } if (Object.keys(currentUser).length > 0) { users.push(currentUser); } return users; } function parseSystemIdOutput(output) { const match = output.match(/System.*ID.*:\s*([^\s\n]+)/i); return match ? match[1] : ''; } function parseTanMethodsOutput(output) { const methods = []; const lines = output.split('\n'); for (const line of lines) { const match = line.match(/(\d+):\s*(.+)/); if (match) { methods.push({ id: match[1], name: match[2].trim(), }); } } return methods; } function convertTransactionsToCsv(transactions) { if (transactions.length === 0) { return 'Date,Amount,Currency,Remote Name,Purpose,Remote IBAN,Remote BIC,Transaction Code\n'; } const headers = ['Date', 'Amount', 'Currency', 'Remote Name', 'Purpose', 'Remote IBAN', 'Remote BIC', 'Transaction Code']; const csvRows = [headers.join(',')]; for (const transaction of transactions) { const row = [ transaction.date || '', transaction.amount || 0, transaction.currency || '', escapeCsvField(transaction.remoteName || ''), escapeCsvField(transaction.purpose || ''), transaction.remoteIBAN || '', transaction.remoteBIC || '', transaction.transactionCode || '', ]; csvRows.push(row.join(',')); } return csvRows.join('\n'); } function escapeCsvField(field) { if (field.includes(',') || field.includes('"') || field.includes('\n')) { return `"${field.replace(/"/g, '""')}"`; } return field; }