From f91e24d348d196bb8f2c00d2c7e1426d90aa41ea Mon Sep 17 00:00:00 2001 From: satyam-19 Date: Tue, 29 Apr 2025 18:42:39 +0530 Subject: [PATCH] feat: Add/update bank processor for HDFC --- .../bankStatementProcessor-HDFC.js | 273 ++++++++++++++++++ src/BANK.js | 1 + 2 files changed, 274 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..41c5ce4 --- /dev/null +++ b/Services/BankStatementProcessor/bankStatementProcessor-HDFC.js @@ -0,0 +1,273 @@ +/** + * Generated by Gemini AI - Attempt 5 + * 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; + let keyMap = {}; + let voucherNumber = 1; + + // Helper function to parse date strings + function parseDate(dateString) { + if (!dateString) return null; + + const excelSerialDate = parseInt(dateString, 10); + if (!isNaN(excelSerialDate)) { + const excelEpoch = new Date(Date.UTC(1899, 11, 30)); + const javascriptDate = new Date(excelEpoch.getTime() + excelSerialDate * 24 * 60 * 60 * 1000); + const year = javascriptDate.getFullYear(); + const month = String(javascriptDate.getMonth() + 1).padStart(2, '0'); + const day = String(javascriptDate.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + const dateFormats = [ + [/^(\d{2})\/(\d{2})\/(\d{4})$/, '$3-$2-$1'], // DD/MM/YYYY + [/^(\d{2})-(\w{3})-(\d{4})$/, (match) => { + 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 month = monthMap[match[2]]; + return `${match[3]}-${month}-${match[1]}`; + }], + [/^(\d{2})\/(\d{2})\/(\d{2})$/, (match) => { // DD/MM/YY + const year = `20${match[3]}`; + return `${year}-${match[2]}-${match[1]}`; + }], + [/^(\d{2})-(\d{2})-(\d{4})$/, '$3-$2-$1'], // DD-MM-YYYY + [/^(\d{4})-(\d{2})-(\d{2})$/, '$1-$2-$3'], // YYYY-MM-DD + [/^(\d{2})-(\d{2})-(\d{2})$/, (match) => { // DD-MM-YY + const year = `20${match[3]}`; + return `${year}-${match[2]}-${match[1]}`; + }], + [/^(\d{2})-(\w{3})-(\d{2})$/, (match) => { + 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 month = monthMap[match[2]]; + const year = `20${match[3]}`; + return `${year}-${month}-${match[1]}`; + }], + + [/^(\d{2})\.(\d{2})\.(\d{4})$/, '$3-$2-$1'], // DD.MM.YYYY + [/^(\d{2})\.(\d{2})\.(\d{2})$/, (match) => { // DD.MM.YY + const year = `20${match[3]}`; + return `${year}-${match[2]}-${match[1]}`; + }], + [/^(\d{4})\/(\d{2})\/(\d{2})$/, '$1-$2-$3'], // YYYY/MM/DD + [/^(\d{1})\/(\d{1})\/(\d{4})$/, (match) => { + const day = String(parseInt(match[1])).padStart(2, '0'); + const month = String(parseInt(match[2])).padStart(2, '0'); + return `${match[3]}-${month}-${day}` + }], + [/^(\d{1})\/(\d{2})\/(\d{4})$/, (match) => { + const day = String(parseInt(match[1])).padStart(2, '0'); + return `${match[3]}-${match[2]}-${day}` + }], + [/^(\d{2})\/(\d{1})\/(\d{4})$/, (match) => { + const month = String(parseInt(match[2])).padStart(2, '0'); + return `${match[3]}-${month}-${match[1]}` + }], + [/^(\d{1})-(\d{1})-(\d{4})$/, (match) => { + const day = String(parseInt(match[1])).padStart(2, '0'); + const month = String(parseInt(match[2])).padStart(2, '0'); + return `${match[3]}-${month}-${day}` + }], + [/^(\d{1})-(\d{2})-(\d{4})$/, (match) => { + const day = String(parseInt(match[1])).padStart(2, '0'); + return `${match[3]}-${match[2]}-${day}` + }], + [/^(\d{2})-(\d{1})-(\d{4})$/, (match) => { + const month = String(parseInt(match[2])).padStart(2, '0'); + return `${match[3]}-${month}-${match[1]}` + }], + ]; + + for (const [format, replacement] of dateFormats) { + const match = dateString.match(format); + if (match) { + return typeof replacement === 'string' ? dateString.replace(format, replacement) : replacement(match); + } + } + + return null; + } + + // Helper function to extract a number from a string, handling commas and currency symbols + function extractNumber(str) { + if (typeof str === 'number') return str; + if (!str) return null; + + const cleanedStr = str.toString().replace(/[^\d.-]/g, ''); // Remove currency symbols and commas, keeping only digits, dots, and hyphens + const num = parseFloat(cleanedStr); + return isNaN(num) ? null : num; + } + + // 1. Extract Bank Details + for (let i = 0; i < Math.min(15, rawData.length); i++) { + const row = rawData[i]; + for (const key in row) { + if (row.hasOwnProperty(key) && row[key] !== null) { + const value = row[key].toString().trim(); + const nextKey = Object.keys(row)[Object.keys(row).indexOf(key) + 1]; //get adjacent cell + + if (value.toLowerCase().includes("bank name")) { + bank_details.bank_name = row[nextKey] ? row[nextKey].toString().trim() : null; + } else if (value.toLowerCase().includes("account holder name") || value.toLowerCase().includes("account name")) { + bank_details.account_holder_name = row[nextKey] ? row[nextKey].toString().trim() : null; + } else if (value.toLowerCase().includes("account no") || value.toLowerCase().includes("account number")) { + bank_details.account_no = row[nextKey] ? row[nextKey].toString().trim() : null; + } else if (value.toLowerCase().includes("ifsc") || value.toLowerCase().includes("ifs")) { + bank_details.ifsc = row[nextKey] ? row[nextKey].toString().trim() : null; + } else if (value.toLowerCase().includes("branch name")) { + bank_details.branch_name = row[nextKey] ? row[nextKey].toString().trim() : null; + } + else if (value.toLowerCase().includes("branch code")) { + bank_details.branch_code = row[nextKey] ? row[nextKey].toString().trim() : null; + } + else if (value.toLowerCase().includes("address")) { + bank_details.address = (bank_details.address || "") + (row[nextKey] ? row[nextKey].toString().trim() : ""); + } else if (value.toLowerCase().includes("city")) { + bank_details.city = row[nextKey] ? row[nextKey].toString().trim() : null; + } + else if (value.toLowerCase().includes("opening balance")) { + const balanceValue = row[nextKey]; + bank_details.opening_balance = extractNumber(balanceValue) || 0; + } + } + } + } + if (!bank_details.ifsc) { + for (let i = 0; i < Math.min(15, rawData.length); i++) { + const row = rawData[i]; + for (const key in row) { + if (row.hasOwnProperty(key) && row[key] !== null) { + const value = row[key].toString().trim(); + if (value.toLowerCase().includes("rtgs/neft ifsc")) { + const parts = value.split(':'); + if (parts.length > 1) { + bank_details.ifsc = parts[1].trim().split(' ')[0]; + break; + } + } + } + } + if(bank_details.ifsc) break; + } + } + // 2. Identify Header Row & Create Key Mapping + for (let i = 0; i < rawData.length; i++) { + const row = rawData[i]; + let isHeaderRow = false; + const headerValues = ["date", "narration", "description", + "details", "withdrawal", "debit", + "deposit", "credit", "balance", + "closing balance"]; + + let foundCount = 0; + for (const key in row) { + if (row.hasOwnProperty(key) && row[key] !== null) { + const value = row[key].toString().toLowerCase().trim(); + if (headerValues.some(hv => value.includes(hv))) { + foundCount++; + } + } + } + + if (foundCount >= 5) { //Adjust threshold if needed + isHeaderRow = true; + } + + + if (isHeaderRow) { + headerRowIndex = i; + for (const key in row) { + if (row.hasOwnProperty(key) && row[key] !== null) { + const value = row[key].toString().toLowerCase().trim(); + if (value.includes("date")) { + keyMap.dateKey = key; + } else if (value.includes("narration") || value.includes("description") || value.includes("details")) { + keyMap.narrationKey = key; + } else if (value.includes("debit") || value.includes("withdrawal")) { + keyMap.debitKey = key; + } else if (value.includes("credit") || value.includes("deposit")) { + keyMap.creditKey = key; + } else if (value.includes("balance") || value.includes("closing balance")) { + keyMap.balanceKey = key; + } + } + } + break; + } + } + + // 3. Process Transactions + if (headerRowIndex !== -1) { + for (let i = headerRowIndex + 1; i < rawData.length; i++) { + const row = rawData[i]; + if (!row || Object.keys(row).length === 0) continue; // Skip empty rows + + const dateString = keyMap.dateKey ? row[keyMap.dateKey] : null; + const desc = keyMap.narrationKey ? (row[keyMap.narrationKey] || '').toString().trim() : null; + const debitStr = keyMap.debitKey ? row[keyMap.debitKey] : null; + const creditStr = keyMap.creditKey ? row[keyMap.creditKey] : null; + const balanceStr = keyMap.balanceKey ? row[keyMap.balanceKey] : null; + + const date = parseDate(dateString); + const debit = extractNumber(debitStr); + const credit = extractNumber(creditStr); + const balance = extractNumber(balanceStr); + + if (!date && !desc && debit === null && credit === null && balance === null) continue; // Skip non-transaction rows + + let type = null; + let amount = null; + + if (debit !== null) { + type = "withdrawal"; + amount = Math.abs(debit); + } else if (credit !== null) { + type = "deposit"; + amount = Math.abs(credit); + } + + if (type && amount !== null) { + const transaction = { + date: date, + voucher_number: voucherNumber++, + amount: amount, + desc: desc, + from: null, + to: null, + type: type, + balance: balance !== null ? balance : null + }; + + transactions.push(transaction); + } + } + } + + 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;