From 67bb02f8c70fc683a456acdbf2e134f3ebe4afe8 Mon Sep 17 00:00:00 2001 From: satyam-19 Date: Tue, 29 Apr 2025 18:44:08 +0530 Subject: [PATCH] feat: Add/update bank processor for HDFC --- .../bankStatementProcessor-HDFC.js | 300 ++++++++++++++++++ src/BANK.js | 1 + 2 files changed, 301 insertions(+) create mode 100644 Services/BankStatementProcessor/bankStatementProcessor-HDFC.js diff --git a/Services/BankStatementProcessor/bankStatementProcessor-HDFC.js b/Services/BankStatementProcessor/bankStatementProcessor-HDFC.js new file mode 100644 index 0000000..80ce7f8 --- /dev/null +++ b/Services/BankStatementProcessor/bankStatementProcessor-HDFC.js @@ -0,0 +1,300 @@ +/** + * Generated by Gemini AI - Attempt 2 + * Processes bank statement data from Excel + * @param {Array} rawData - Array of objects from bank statement Excel + * @returns {Object} Processed bank statement data + */ +function processBankStatement(rawData) { + const bank_details = { + bank_name: null, + opening_balance: 0, + ifsc: null, + address: null, + city: null, + account_no: null, + account_holder_name: null, + branch_name: null, + branch_code: null + }; + const transactions = []; + + let headerRowIndex = -1; + const headerKeys = {}; + + // Helper function to parse dates + function parseDate(dateString) { + if (!dateString) return null; + + if (typeof dateString !== 'string') { + if (typeof dateString === 'number') { + const excelEpoch = new Date(Date.UTC(1899, 11, 30)); + const jsDate = new Date(excelEpoch.getTime() + (dateString * 24 * 60 * 60 * 1000)); + const year = jsDate.getFullYear(); + const month = String(jsDate.getMonth() + 1).padStart(2, '0'); + const day = String(jsDate.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + return null; + } + + const excelSerialDateRegex = /^\d+$/; + if (excelSerialDateRegex.test(dateString)) { + const excelSerial = parseInt(dateString, 10); + const excelEpoch = new Date(Date.UTC(1899, 11, 30)); + const jsDate = new Date(excelEpoch.getTime() + (excelSerial * 24 * 60 * 60 * 1000)); + const year = jsDate.getFullYear(); + const month = String(jsDate.getMonth() + 1).padStart(2, '0'); + const day = String(jsDate.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + const dateFormats = [ + [/^(\d{2})\/(\d{2})\/(\d{4})$/, (m, d, M, Y) => `${Y}-${M}-${d}`], // DD/MM/YYYY + [/^(\d{2})-(\w{3})-(\d{4})$/, (m, d, Mon, Y) => { + const monthMap = { + 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', + 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' + }; + return `${Y}-${monthMap[Mon] || '00'}-${d}`; + }], // DD-Mon-YYYY + [/^(\d{2})\/(\d{2})\/(\d{2})$/, (m, d, M, Y) => { + const fullYear = `20${Y}`; + return `${fullYear}-${M}-${d}` + }], // DD/MM/YY + [/^(\d{2})-(\w{3})-(\d{2})$/, (m, d, Mon, Y) => { + const monthMap = { + 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', + 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' + }; + const fullYear = `20${Y}`; + return `${fullYear}-${monthMap[Mon] || '00'}-${d}`; + }], // DD-Mon-YY + [/^(\d{2})\/(\d{2})\/(\d{4})$/, (m, M, d, Y) => `${Y}-${M}-${d}`], // MM/DD/YYYY + [/^(\d{4})-(\d{2})-(\d{2})$/, (m, Y, M, d) => `${Y}-${M}-${d}`], // YYYY-MM-DD + ]; + + for (const [regex, formatter] of dateFormats) { + if (typeof dateString === 'string') { + const match = dateString.match(regex); + if (match) { + return formatter(match[0], match[1], match[2], match[3]); + } + } + } + + return null; // Indicate failure to parse + } + + // Helper function to convert to Number + function convertToNumber(value) { + if (value === null || value === undefined) return null; + if (typeof value === 'number') return value; + if (typeof value !== 'string') return NaN; + + const cleanedValue = value.replace(/[^0-9.-]+/g, ''); + const num = Number(cleanedValue); + return isNaN(num) ? null : num; + } + + + // 1. Extract Bank Details + for (let i = 0; i < Math.min(rawData.length, 15); i++) { + const row = rawData[i]; + for (const key in row) { + if (row.hasOwnProperty(key)) { + const value = row[key]; + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + if (lowerValue.includes("bank name")) { + const bankNameKey = Object.keys(row).find(k => k !== key); + if (bankNameKey && row[bankNameKey]) { + bank_details.bank_name = String(row[bankNameKey]).trim(); + } + } else if (lowerValue.includes("account holder name")) { + const accountHolderNameKey = Object.keys(row).find(k => k !== key); + if (accountHolderNameKey && row[accountHolderNameKey]) { + bank_details.account_holder_name = String(row[accountHolderNameKey]).trim(); + } + } else if (lowerValue.includes("account no") || lowerValue.includes("account number")) { + const accountNoKey = Object.keys(row).find(k => k !== key); + if (accountNoKey && row[accountNoKey]) { + bank_details.account_no = String(row[accountNoKey]).trim(); + } + } else if (lowerValue.includes("ifsc") || lowerValue.includes("ifs")) { + const ifscKey = Object.keys(row).find(k => k !== key); + if (ifscKey && row[ifscKey]) { + bank_details.ifsc = String(row[ifscKey]).trim(); + } + } else if (lowerValue.includes("branch name")) { + const branchNameKey = Object.keys(row).find(k => k !== key); + if(branchNameKey && row[branchNameKey]) { + bank_details.branch_name = String(row[branchNameKey]).trim(); + } + + } else if (lowerValue.includes("address")) { + const addressKey = Object.keys(row).find(k => k !== key); + if (addressKey && row[addressKey]) { + bank_details.address = String(row[addressKey]).trim(); + } + } + } + } + } + } + + // Extract opening balance (simplified - assuming it is in one of the rows close to bank details) + for (let i = 0; i < Math.min(rawData.length, 15); i++) { + const row = rawData[i]; + for (const key in row) { + if (row.hasOwnProperty(key)) { + const value = row[key]; + if (typeof value === 'string' && value.toLowerCase().includes("opening balance")) { + const openingBalanceKey = Object.keys(row).find(k => k !== key); + if (openingBalanceKey) { + const potentialBalance = convertToNumber(row[openingBalanceKey]); + if(potentialBalance !== null) + { + bank_details.opening_balance = potentialBalance; + } + + } + } + } + } + } + + if(bank_details.ifsc === null) + { + for (let i = 0; i < Math.min(rawData.length, 15); i++) { + const row = rawData[i]; + for (const key in row) { + if (row.hasOwnProperty(key)) { + const value = row[key]; + if (typeof value === 'string') { + const ifscRegex = /IFSC *: *([A-Za-z0-9]+)/i; + const match = value.match(ifscRegex); + if (match) { + bank_details.ifsc = match[1].trim(); + break; + } + } + } + if(bank_details.ifsc !== null) break; + } + if(bank_details.ifsc !== null) break; + } + } + + + // 2. Identify Header Row & Create Key Mapping + for (let i = 0; i < rawData.length; i++) { + const row = rawData[i]; + let dateFound = false; + let narrationFound = false; + let debitFound = false; + let creditFound = false; + let balanceFound = false; + + for (const key in row) { + if (row.hasOwnProperty(key) && typeof row[key] === 'string') { + const value = row[key].toLowerCase(); + + if (value.includes("date") || value.includes("txn date")) { + headerKeys.dateKey = key; + dateFound = true; + } + if (value.includes("narration") || value.includes("description") || value.includes("details")) { + headerKeys.narrationKey = key; + narrationFound = true; + } + if (value.includes("debit") || value.includes("withdrawal")) { + headerKeys.debitKey = key; + debitFound = true; + } + if (value.includes("credit") || value.includes("deposit")) { + headerKeys.creditKey = key; + creditFound = true; + } + if (value.includes("balance") || value.includes("closing balance")) { + headerKeys.balanceKey = key; + balanceFound = true; + } + } + } + + if (dateFound && narrationFound && (debitFound || creditFound) && balanceFound) { + headerRowIndex = i; + break; + } + } + + // 3. Process Transactions + if (headerRowIndex !== -1) { + let voucherNumber = 1; + for (let i = headerRowIndex + 1; i < rawData.length; i++) { + const row = rawData[i]; + if (!row) continue; // Skip empty rows + + const dateString = row[headerKeys.dateKey]; + const desc = row[headerKeys.narrationKey] ? String(row[headerKeys.narrationKey]).trim() : null; + const debitValue = convertToNumber(row[headerKeys.debitKey]); + const creditValue = convertToNumber(row[headerKeys.creditKey]); + const balanceValue = convertToNumber(row[headerKeys.balanceKey]); + + if (!dateString && (!debitValue && !creditValue) && balanceValue === null) continue; + + const date = parseDate(dateString); + if (!date) continue; //Skip if date parse fails + + let type = null; + let amount = null; + + if (debitValue !== null) { + type = "withdrawal"; + amount = Math.abs(debitValue); // Store amount as positive + } else if (creditValue !== null) { + type = "deposit"; + amount = Math.abs(creditValue); // Store amount as positive + } else { + continue; // Skip transactions with neither debit nor credit + } + + const transaction = { + date: date, + voucher_number: voucherNumber++, + amount: amount, + desc: desc, + from: null, + to: null, + type: type, + balance: balanceValue + }; + + // Basic from/to parsing from desc (example, can be extended) + if (desc) { + const upiRegex = /([a-zA-Z0-9.-]+@[a-zA-Z0-9.-]+)/; //Basic UPI ID pattern + const upiMatch = desc.match(upiRegex); + + if (upiMatch && upiMatch[1]) { + if (transaction.type === 'withdrawal') { + transaction.to = upiMatch[1]; + } else { + transaction.from = upiMatch[1]; + } + } + } + transactions.push(transaction); + } + } + + if(bank_details.ifsc === null) { + bank_details.ifsc = ""; + } + + return { + bank_details: bank_details, + transactions: transactions + }; +} + +module.exports = processBankStatement; \ No newline at end of file diff --git a/src/BANK.js b/src/BANK.js index 3da59a8..f375135 100644 --- a/src/BANK.js +++ b/src/BANK.js @@ -1,6 +1,7 @@ const BANK = { BOB: "bob", IOB: "iob", + HDFC: "hdfc", } module.exports = BANK;