1
Upload
2
Review
3
Analysis
4
Proposal

Step 1: Upload Your Merchant Statement

Drop your PDF here or click to select

Supports PDF files up to 50MB

Step 2: Review Extracted Data

Upload a PDF first to see extracted data

Step 3: Fee Analysis

Step 4: Savings Proposal

document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.step').forEach(s => { const sNum = parseInt(s.dataset.step); s.classList.remove('active', 'completed'); if (sNum < step) s.classList.add('completed'); if (sNum === step) s.classList.add('active'); }); document.getElementById('panel-' + step).classList.add('active'); currentStep = step; window.scrollTo({ top: 0, behavior: 'smooth' }); // Auto-update rate preview when entering proposal step if (step === 4 && analysisResults.totalFees) { updateRatePreview(); } } // ===== Tabs ===== function switchTab(tab) { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); event.target.classList.add('active'); document.getElementById('tab-' + tab).classList.add('active'); } // ===== Pricing Model Toggle ===== function updatePricingFields() { const model = document.getElementById('pricingModel').value; document.getElementById('tiered-fields').style.display = model === 'tiered' ? 'block' : 'none'; document.getElementById('icp-fields').style.display = model === 'interchange_plus' ? 'block' : 'none'; document.getElementById('flat-fields').style.display = model === 'flat_rate' ? 'block' : 'none'; } function updateProposedFields() { // Simplified — proposal now uses target savings % slider instead of manual pricing } // ===== File Upload ===== const uploadZone = document.getElementById('uploadZone'); const fileInput = document.getElementById('fileInput'); uploadZone.addEventListener('click', () => fileInput.click()); uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover')); uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFile(e.target.files[0]); }); async function handleFile(file) { if (file.type !== 'application/pdf') { alert('Please upload a PDF file.'); return; } uploadZone.style.display = 'none'; const proc = document.getElementById('processing'); proc.classList.add('active'); try { document.getElementById('processingStatus').textContent = 'Loading PDF...'; const arrayBuffer = await file.arrayBuffer(); pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; let fullText = ''; let structuredLines = []; // position-aware line reconstruction for (let i = 1; i <= pdf.numPages; i++) { document.getElementById('processingStatus').textContent = `Extracting page ${i} of ${pdf.numPages}...`; const page = await pdf.getPage(i); const textContent = await page.getTextContent(); const viewport = page.getViewport({ scale: 1.0 }); // ---- Position-aware text reconstruction ---- // Group text items by their Y position (within tolerance) to rebuild lines const items = textContent.items.filter(item => item.str.trim().length > 0); if (items.length === 0) continue; // Build line groups by Y coordinate (tolerance of 3 units) const lineMap = new Map(); items.forEach(item => { const y = Math.round(item.transform[5] / 3) * 3; // snap Y to grid if (!lineMap.has(y)) lineMap.set(y, []); lineMap.get(y).push({ x: item.transform[4], text: item.str, width: item.width || 0 }); }); // Sort lines top-to-bottom (PDF Y is bottom-up, so reverse) const sortedYs = [...lineMap.keys()].sort((a, b) => b - a); let pageLines = []; sortedYs.forEach(y => { const lineItems = lineMap.get(y).sort((a, b) => a.x - b.x); // Join items on the same line, adding spacing based on X gaps let lineText = ''; lineItems.forEach((item, idx) => { if (idx > 0) { const prevEnd = lineItems[idx - 1].x + lineItems[idx - 1].width; const gap = item.x - prevEnd; if (gap > 15) lineText += ' '; // big gap = tab/column separator else if (gap > 3) lineText += ' '; } lineText += item.text; }); pageLines.push(lineText); }); const pageText = pageLines.join('\n'); fullText += `--- Page ${i} ---\n${pageText}\n\n`; structuredLines.push(...pageLines); } extractedText = fullText; document.getElementById('rawText').textContent = fullText; document.getElementById('processingStatus').textContent = 'Parsing merchant data...'; parseStatementData(fullText, structuredLines); // Show confidence summary proc.classList.remove('active'); goToStep(2); showExtractionConfidence(); } catch (err) { console.error(err); alert('Error reading PDF: ' + err.message); proc.classList.remove('active'); uploadZone.style.display = ''; } } // Show extraction confidence badges on the form function showExtractionConfidence() { const fields = ['totalVolume','totalTransactions','totalFees','interchangeFees','assessmentFees','discountFees','transactionFees','monthlyFees']; let filled = 0; let total = fields.length; fields.forEach(id => { const el = document.getElementById(id); if (el && parseFloat(el.value) > 0) { filled++; el.style.borderColor = 'var(--success)'; el.style.background = '#f0fdf4'; } else if (el) { el.style.borderColor = 'var(--warning)'; el.style.background = '#fffbeb'; } }); const pct = Math.round(filled / total * 100); const confDiv = document.getElementById('confidenceBanner'); if (confDiv) { confDiv.style.display = 'block'; confDiv.innerHTML = `
Extraction Confidence: ${pct}% (${filled}/${total} fields)
${pct >= 70 ? 'Good extraction! Review green fields and fill any yellow ones.' : pct >= 40 ? 'Partial extraction. Please verify and fill in the yellow fields.' : 'Low extraction — this statement format may need manual entry for some fields.'}
`; } } // ===== SMART AI Statement Parser v3 ===== // Built from real Card Dynamics / ISO statement formats. // Uses table-structure detection, column-position mapping, and // multi-strategy extraction for real-world merchant statements. function parseStatementData(text, structuredLines) { const lines = structuredLines && structuredLines.length > 0 ? structuredLines : text.split('\n'); const fullTextLower = text.toLowerCase(); console.log('[Parser] Total lines:', lines.length); // ========== UTILITY FUNCTIONS ========== // Find ALL dollar amounts on a line (including negative like $-835.07) function findAllDollars(str) { const results = []; const re = /\$\s*-?\s*([\d,]+\.?\d{0,2})/g; let m; while ((m = re.exec(str)) !== null) { const val = parseFloat(m[1].replace(/,/g, '')); if (!isNaN(val) && val > 0) results.push(val); } // Also try plain numbers with decimals (no $ sign) if (results.length === 0) { const re2 = /(? 0.50) results.push(val); } } return results; } // Search lines for a label, return dollar amount(s) function findLabelValue(keywords, opts = {}) { const { minVal = 0, maxVal = Infinity, preferLargest = false, returnAll = false } = opts; let candidates = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lower = line.toLowerCase(); const matched = keywords.some(kw => typeof kw === 'string' ? lower.includes(kw) : kw.test(lower) ); if (!matched) continue; let amounts = findAllDollars(line); // Also check next line if (amounts.length === 0 && i + 1 < lines.length) { amounts = findAllDollars(lines[i + 1]); } amounts = amounts.filter(v => v > minVal && v < maxVal); if (returnAll) return amounts; candidates.push(...amounts); } if (candidates.length === 0) return 0; if (preferLargest) return Math.max(...candidates); return candidates[0]; } // Search for text value near a label function findLabelText(keywords) { for (let i = 0; i < lines.length; i++) { const lower = lines[i].toLowerCase(); for (const kw of keywords) { const kwStr = typeof kw === 'string' ? kw : null; const isMatch = kwStr ? lower.includes(kwStr) : kw.test(lower); if (!isMatch) continue; let after = ''; if (kwStr) { after = lines[i].substring(lower.indexOf(kwStr) + kwStr.length); } else { const m = lines[i].match(kw); if (m) after = lines[i].substring(m.index + m[0].length); } after = after.replace(/^[\s:\-–]+/, '').trim(); if (after.length > 1 && after.length < 80) return after; // Try next line if (i + 1 < lines.length) { const next = lines[i + 1].trim(); if (next.length > 1 && next.length < 80 && !/^\$/.test(next) && !/^\d+$/.test(next)) return next; } } } return ''; } // ========== BATCH TOTAL TABLE DETECTOR ========== // Many processor statements (Card Dynamics, First Data, etc.) have a // "Batch Totals" or "Settlement Summary" table with a Total row. // Format: Total [count] $[sales] $[returns] $[adjust] $[chbks] $[fees] $[deposits] // The key insight: detect the HEADER row, then find the TOTAL row and map columns. let batchData = null; function detectBatchTotalTable() { for (let i = 0; i < lines.length; i++) { const lower = lines[i].toLowerCase(); // Find header row of batch/settlement table const isHeader = ( (lower.includes('date') && lower.includes('sales') && lower.includes('fees')) || (lower.includes('date') && lower.includes('transaction') && lower.includes('deposits')) || (lower.includes('date') && lower.includes('count') && lower.includes('fees')) || (lower.includes('batch') && lower.includes('sales') && lower.includes('fees')) ); if (!isHeader) continue; console.log('[Parser] Found batch table header at line', i, ':', lines[i].trim()); // Parse column names from header const headerWords = lower.split(/\s{2,}|\t+/); console.log('[Parser] Header columns:', headerWords); // Now scan forward to find the "Total" row for (let j = i + 1; j < Math.min(i + 100, lines.length); j++) { const rowLower = lines[j].toLowerCase().trim(); if (/^total\b/.test(rowLower)) { console.log('[Parser] Found Total row at line', j, ':', lines[j].trim()); // Extract all numbers from the total row const dollarAmounts = findAllDollars(lines[j]); // Also find plain integers (transaction count) const allNums = []; const numRe = /(? 0 && v < 999999) allNums.push(v); } console.log('[Parser] Total row - integers:', allNums, 'dollars:', dollarAmounts); // Map columns based on header keywords // Typical column order: Date | Count | Sales | Returns | Adjust | Chbks | Fees | Deposits if (dollarAmounts.length >= 2) { batchData = { transactions: allNums.length > 0 ? allNums[0] : 0, sales: dollarAmounts[0] || 0, // First dollar = sales fees: 0, deposits: 0, returns: 0, }; // Map remaining dollar amounts based on header const headerLower = lines[i].toLowerCase(); // Figure out which column index each dollar amount corresponds to // by matching header column positions if (dollarAmounts.length >= 5) { // Full table: Sales, Returns, Adjust, Chbks, Fees, Deposits batchData.sales = dollarAmounts[0]; batchData.returns = dollarAmounts[1]; // dollarAmounts[2] = adjust, [3] = chbks (both usually 0) batchData.fees = dollarAmounts.length >= 6 ? dollarAmounts[4] : dollarAmounts[dollarAmounts.length - 2]; batchData.deposits = dollarAmounts[dollarAmounts.length - 1]; } else if (dollarAmounts.length >= 3) { // Shorter: Sales, Fees, Deposits batchData.sales = dollarAmounts[0]; batchData.fees = dollarAmounts[dollarAmounts.length - 2]; batchData.deposits = dollarAmounts[dollarAmounts.length - 1]; } else { // Just Sales and one other batchData.sales = dollarAmounts[0]; batchData.fees = dollarAmounts[1] || 0; } // Sanity: fees should be much smaller than sales if (batchData.fees > batchData.sales * 0.15) { // Fees and deposits might be swapped — fees is the smaller one const smaller = Math.min(batchData.fees, batchData.deposits); const larger = Math.max(batchData.fees, batchData.deposits); batchData.fees = smaller; batchData.deposits = larger; } console.log('[Parser] Batch data extracted:', batchData); return true; } } } } return false; } const hasBatchTable = detectBatchTotalTable(); // ========== DAILY FEE / MONTHLY FEE DETECTOR ========== let dailyFees = 0; let monthlyFees = 0; let totalStatementFees = 0; for (let i = 0; i < lines.length; i++) { const lower = lines[i].toLowerCase(); const amounts = findAllDollars(lines[i]); if (/(?:less\s+)?daily\s*fees/i.test(lower) && amounts.length > 0) { dailyFees = amounts[0]; console.log('[Parser] Daily fees:', dailyFees); } if (/total\s*monthly\s*fees/i.test(lower) && amounts.length > 0) { monthlyFees = amounts[0]; console.log('[Parser] Monthly fees:', monthlyFees); } if (/billed\s*amount/i.test(lower) && amounts.length > 0) { totalStatementFees = amounts[0]; } } // ========== MERCHANT INFO ========== // Merchant name — try label-based, then fall back to looking for the name // right after "Merchant Number" line (common in Card Dynamics format) let merchantName = findLabelText([ 'merchant name', 'merchant dba', 'dba name', 'dba:', 'business name', /merchant\s*(?:name|dba)/i ]); // Card Dynamics format: name is on the line AFTER the Merchant Number line if (!merchantName) { for (let i = 0; i < lines.length; i++) { if (/merchant\s*number/i.test(lines[i].toLowerCase()) && i + 1 < lines.length) { const nextLine = lines[i + 1].trim(); // Make sure it's not a number or address if (nextLine.length > 2 && nextLine.length < 60 && !/^\d/.test(nextLine) && !/^[\$]/.test(nextLine)) { merchantName = nextLine; break; } } } } if (merchantName) document.getElementById('merchantName').value = merchantName; // MID let mid = ''; for (const line of lines) { if (/merchant\s*(?:id|#|number|no)/i.test(line.toLowerCase())) { const digitMatch = line.match(/(\d{6,16})/); if (digitMatch) { mid = digitMatch[1]; break; } } } // Also try "acct" / "account" if (!mid) { for (const line of lines) { if (/\b(?:acct|account)\s*(?:#|number|no)/i.test(line.toLowerCase())) { const digitMatch = line.match(/(\d{6,16})/); if (digitMatch) { mid = digitMatch[1]; break; } } } } if (mid) document.getElementById('merchantId').value = mid; // Statement period let period = ''; for (const line of lines) { // "Statement period 03/01/2026 thru 03/31/2026" let m = line.match(/(?:statement\s*)?period\s*[:\-]?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}\s*(?:thru|to|through|[-–])\s*\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i); if (m) { period = m[1]; } } if (period) document.getElementById('period').value = period; // ========== FEE CATEGORIES ========== let interchangeFees = 0; let assessmentFees = 0; let discountFees = 0; let transactionFees = 0; for (let i = 0; i < lines.length; i++) { const lower = lines[i].toLowerCase(); const amounts = findAllDollars(lines[i]); if ((/interchange\s*(?:fees?|discount)/i.test(lower) || /ic\s*fees?/i.test(lower)) && amounts.length > 0) { interchangeFees = amounts[0]; } if ((/assessment\s*(?:fees?|amt)/i.test(lower) || /visa\s*assessment|mastercard\s*assessment|amex\s*assessment|discover\s*assessment/i.test(lower)) && amounts.length > 0) { assessmentFees = amounts[0]; } if ((/discount\s*(?:fees?|amt)|batch\s*discount/i.test(lower) && /\$(\d+|\d+\.\d+)/i.test(lines[i])) && amounts.length > 0) { discountFees = amounts[0]; } if ((/transaction\s*(?:fees?|amt|charge)|network.*(?:fee|charge)/i.test(lower) && /\$(\d+|\d+\.\d+)/i.test(lines[i])) && amounts.length > 0) { transactionFees = amounts[0]; } } // POPULATE FORM if (batchData) { document.getElementById('totalVolume').value = Math.round(batchData.sales); document.getElementById('totalFees').value = batchData.fees.toFixed(2); document.getElementById('totalTransactions').value = batchData.transactions || 0; } else { // Fallback: use batch fees if available const totalVol = findLabelValue(['total sales', 'total volume', 'total amount', 'gross amount', 'sales'], { preferLargest: true, minVal: 100 }); const totalF = findLabelValue(['total fees', 'total cost of processing', 'fees due', 'monthly charge', 'billed amount'], { preferLargest: true, minVal: 0.01 }); if (totalVol > 0) document.getElementById('totalVolume').value = Math.round(totalVol); if (totalF > 0) document.getElementById('totalFees').value = totalF.toFixed(2); } if (interchangeFees > 0) document.getElementById('interchangeFees').value = interchangeFees.toFixed(2); if (assessmentFees > 0) document.getElementById('assessmentFees').value = assessmentFees.toFixed(2); if (discountFees > 0) document.getElementById('discountFees').value = discountFees.toFixed(2); if (transactionFees > 0) document.getElementById('transactionFees').value = transactionFees.toFixed(2); if (monthlyFees > 0) document.getElementById('monthlyFees').value = monthlyFees.toFixed(2); // ========== ANALYSIS: Calculate Savings Opportunity ========== const vol = parseFloat(document.getElementById('totalVolume').value) || 0; const fees = parseFloat(document.getElementById('totalFees').value) || 0; if (vol > 0 && fees > 0) { analysisResults = { totalVolume: vol, totalFees: fees, currentRate: (fees / vol * 100).toFixed(2), industryRate: 2.19, // Typical Visa/Mastercard rate for mid-size merchants potentialSavings: (vol * 0.0219 - fees), // annual savings at industry rate monthlyPotential: (vol * 0.0219 - fees) / 12 }; console.log('[Parser] Analysis results:', analysisResults); } } // ===== RATE PREVIEW ===== function updateRatePreview() { const targetSavingsPercent = parseFloat(document.getElementById('targetSavings').value) || 0; const currentRate = analysisResults.currentRate || 2.5; const targetRate = currentRate * (1 - targetSavingsPercent / 100); const vol = analysisResults.totalVolume || 0; const newFees = vol * (targetRate / 100); const savingsAmount = (analysisResults.totalFees || 0) - newFees; document.getElementById('targetRate').textContent = targetRate.toFixed(2) + '%'; document.getElementById('estimatedSavings').textContent = '$' + savingsAmount.toFixed(2); document.getElementById('annualSavings').textContent = '$' + (savingsAmount * 12).toFixed(2); } // ===== PDF GENERATION ===== async function generateProposal() { if (!analysisResults.totalVolume || !analysisResults.totalFees) { alert('Please upload and analyze a statement first'); return; } // Gather proposal details from form const proposalData = { timestamp: new Date().toLocaleString(), merchantName: document.getElementById('merchantName').value || 'Merchant', merchantId: document.getElementById('merchantId').value || '', period: document.getElementById('period').value || '', analysis: analysisResults, proposedRates: { creditCard: (parseFloat(document.getElementById('creditCardRate').value) || 1.89).toFixed(2), debit: (parseFloat(document.getElementById('debitRate').value) || 0.79).toFixed(2), amex: (parseFloat(document.getElementById('amexRate').value) || 2.89).toFixed(2), discover: (parseFloat(document.getElementById('discoverRate').value) || 1.99).toFixed(2), }, pricingModel: document.getElementById('pricingModel').value, contactName: document.getElementById('contactName').value || '', contactEmail: document.getElementById('contactEmail').value || '', contactPhone: document.getElementById('contactPhone').value || '', }; try { const { jsPDF } = window.jspdf; const doc = new jsPDF(); const pdf = doc; // Colors const darkBlue = [20, 40, 100]; const lightBlue = [65, 100, 220]; const green = [40, 150, 80]; const darkGray = [60, 60, 60]; const lightGray = [200, 200, 200]; // ===== Header ===== pdf.setFillColor(...darkBlue); pdf.rect(0, 0, 210, 35, 'F'); pdf.setTextColor(255, 255, 255); pdf.setFontSize(22); pdf.setFont('helvetica', 'bold'); pdf.text('MERCHANT FEE AUDIT REPORT', 15, 15); pdf.setFontSize(10); pdf.setFont('helvetica', 'normal'); pdf.text('AI-Powered Analysis & Savings Proposal', 15, 23); pdf.setTextColor(...darkGray); pdf.setFontSize(9); pdf.text(`Report Generated: ${proposalData.timestamp}`, 15, 30); let y = 40; // ===== Merchant Info Section ===== pdf.setTextColor(...darkBlue); pdf.setFontSize(14); pdf.setFont('helvetica', 'bold'); pdf.text('Merchant Information', 15, y); y += 7; pdf.setTextColor(...darkGray); pdf.setFontSize(10); pdf.setFont('helvetica', 'normal'); const infoLines = [ `Merchant Name: ${proposalData.merchantName}`, `Merchant ID: ${proposalData.merchantId || 'N/A'}`, `Statement Period: ${proposalData.period || 'N/A'}`, ]; infoLines.forEach(line => { pdf.text(line, 15, y); y += 5; }); y += 3; // ===== Current Analysis ===== pdf.setTextColor(...darkBlue); pdf.setFontSize(14); pdf.setFont('helvetica', 'bold'); pdf.text('Current Processing Fees Analysis', 15, y); y += 7; // Summary table const tableData = [ ['Metric', 'Amount'], ['Total Monthly Volume', '$' + analysisResults.totalVolume.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })], ['Total Monthly Fees', '$' + analysisResults.totalFees.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })], ['Current Effective Rate', analysisResults.currentRate + '%'], ['Industry Standard Rate', analysisResults.industryRate + '%'], ['Monthly Overage', '$' + (analysisResults.totalFees - analysisResults.totalVolume * analysisResults.industryRate / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })], ]; pdf.autoTable({ startY: y, head: [tableData[0]], body: tableData.slice(1), headStyles: { fillColor: lightBlue, textColor: 255, fontSize: 11, fontStyle: 'bold' }, bodyStyles: { fontSize: 10 }, alternateRowStyles: { fillColor: [240, 245, 255] }, margin: { left: 15, right: 15 }, }); y = pdf.lastAutoTable.finalY + 10; // ===== Proposed Rates Section ===== pdf.setTextColor(...darkBlue); pdf.setFontSize(14); pdf.setFont('helvetica', 'bold'); pdf.text('Proposed Rates & Estimated Savings', 15, y); y += 7; pdf.setTextColor(...darkGray); pdf.setFontSize(10); pdf.setFont('helvetica', 'normal'); pdf.text(`Pricing Model: ${proposalData.pricingModel === 'interchange_plus' ? 'Interchange Plus' : proposalData.pricingModel === 'tiered' ? 'Tiered' : 'Flat Rate'}`, 15, y); y += 5; const rateLines = [ `Credit Card Rate: ${proposalData.proposedRates.creditCard}%`, `Debit Card Rate: ${proposalData.proposedRates.debit}%`, `American Express Rate: ${proposalData.proposedRates.amex}%`, `Discover Rate: ${proposalData.proposedRates.discover}%`, ]; rateLines.forEach(line => { pdf.text(line, 20, y); y += 5; }); y += 3; // Savings highlight const estimatedNewFees = analysisResults.totalVolume * (2.19 / 100); const monthlySavings = analysisResults.totalFees - estimatedNewFees; const annualSavings = monthlySavings * 12; pdf.setFillColor(...green); pdf.rect(15, y, 180, 20, 'F'); pdf.setTextColor(255, 255, 255); pdf.setFontSize(14); pdf.setFont('helvetica', 'bold'); pdf.text('Estimated Annual Savings: $' + annualSavings.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }), 105, y + 13, { align: 'center' }); y += 25; // ===== Footer with Contact Info ===== if (proposalData.contactEmail || proposalData.contactPhone) { pdf.setTextColor(...darkGray); pdf.setFontSize(9); pdf.setFont('helvetica', 'normal'); pdf.text(`Contact: ${proposalData.contactName || 'N/A'} | ${proposalData.contactEmail} | ${proposalData.contactPhone}`, 15, pdf.internal.pageSize.height - 10); } // Save const fileName = `Merchant_Fee_Audit_${proposalData.merchantName.replace(/\s+/g, '_')}_${new Date().getTime()}.pdf`; pdf.save(fileName); alert('Proposal PDF generated and downloaded!'); } catch (err) { console.error('PDF generation error:', err); alert('Error generating PDF: ' + err.message); } } // ===== Event Listeners ===== document.getElementById('pricingModel').addEventListener('change', updatePricingFields); document.getElementById('targetSavings').addEventListener('input', updateRatePreview); common for ISOs } } else { detectedModel = 'interchange_plus'; } } document.getElementById('pricingModel').value = detectedModel; updatePricingFields(); // ========== OUTPUT: Populate Form Fields ========== // Set all the inputs we extracted document.getElementById('totalVolume').value = totalVolume; document.getElementById('totalFees').value = totalFees.toFixed(2); document.getElementById('totalTransactions').value = totalTransactions; document.getElementById('interchangeFees').value = interchangeFees.toFixed(2); document.getElementById('assessmentFees').value = assessmentFees.toFixed(2); document.getElementById('discountFees').value = discountFees.toFixed(2); document.getElementById('transactionFees').value = transactionFees.toFixed(2); document.getElementById('monthlyFees').value = monthlyFees.toFixed(2); document.getElementById('dailyFees').value = dailyFees.toFixed(2); // Set extracted text to debug field (optionally hidden) if (document.getElementById('rawText')) { document.getElementById('rawText').value = text; } // Calculate savings const currentRate = totalVolume > 0 && totalFees > 0 ? (totalFees / totalVolume * 100).toFixed(2) : 'N/A'; const industryRate = 2.19; analysisResults = { totalVolume, totalFees, currentRate, industryRate, potentialSavings: totalVolume * industryRate / 100 - totalFees, monthlyPotential: (totalVolume * industryRate / 100 - totalFees), }; console.log('Parse complete. Analysis results:', analysisResults); } // ===== STEP 1: UPLOAD ===== const uploadZone = document.getElementById('uploadZone'); const fileInput = document.getElementById('fileInput'); if (uploadZone && fileInput) { uploadZone.addEventListener('click', () => fileInput.click()); uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); }); uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) handleFileUpload(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) handleFileUpload(e.target.files[0]); }); } async function handleFileUpload(file) { if (file.type !== 'application/pdf') { alert('Please select a PDF file'); return; } const procDiv = document.getElementById('processing'); if (procDiv) procDiv.style.display = 'block'; try { const arrayBuffer = await file.arrayBuffer(); pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; let fullText = ''; for (let i = 1; i <= Math.min(pdf.numPages, 10); i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); fullText += textContent.items.map(item => item.str).join(' ') + '\n'; } // Parse the extracted text parseStatementData(fullText, fullText.split('\n')); if (procDiv) procDiv.style.display = 'none'; goToStep(2); } catch (err) { alert('Error processing PDF: ' + err.message); console.error(err); if (procDiv) procDiv.style.display = 'none'; } } // ===== STEP 2: REVIEW ===== function reviewData() { goToStep(2); document.getElementById('noReviewContent').style.display = 'none'; document.getElementById('reviewContent').style.display = 'block'; } // ===== STEP 3: ANALYSIS ===== function analyzeData() { if (!analysisResults || analysisResults.totalVolume === 0) { alert('Please upload and review statement data first'); return; } goToStep(3); updateAnalysisDisplay(); } function updateAnalysisDisplay() { const res = analysisResults; if (!res) return; document.getElementById('analysisContent').style.display = 'block'; document.getElementById('noAnalysisContent').style.display = 'none'; document.getElementById('totalVolume').textContent = '$' + (res.totalVolume || 0).toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); document.getElementById('totalFees').textContent = '$' + (res.totalFees || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); document.getElementById('currentRate').textContent = res.currentRate + '%'; document.getElementById('industryRate').textContent = res.industryRate + '%'; document.getElementById('monthlySavings').textContent = '$' + (res.potentialSavings || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // ===== STEP 4: PROPOSAL ===== function generateProposalPDF() { if (!analysisResults) { alert('Please complete analysis first'); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); // Header doc.setFillColor(20, 40, 100); doc.rect(0, 0, 210, 25, 'F'); doc.setTextColor(255, 255, 255); doc.setFontSize(18); doc.setFont('helvetica', 'bold'); doc.text('MERCHANT FEE AUDIT PROPOSAL', 15, 15); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.text('AI-Powered Savings Analysis', 15, 22); let y = 35; // Summary doc.setTextColor(20, 40, 100); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Financial Summary', 15, y); y += 8; doc.setTextColor(60, 60, 60); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const summaryData = [ ['Metric', 'Current', 'Industry Std', 'Difference'], ['Monthly Volume', '$' + analysisResults.totalVolume.toLocaleString(), 'Baseline', ''], ['Monthly Fees', '$' + analysisResults.totalFees.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }), '$' + (analysisResults.totalVolume * analysisResults.industryRate / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }), '$' + analysisResults.potentialSavings.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })], ['Effective Rate', analysisResults.currentRate + '%', analysisResults.industryRate + '%', 'SAVE'], ]; doc.autoTable({ startY: y, head: [summaryData[0]], body: summaryData.slice(1), headStyles: { fillColor: [100, 150, 220], textColor: 255 }, bodyStyles: { fontSize: 9 }, alternateRowStyles: { fillColor: [240, 245, 255] }, }); y = doc.lastAutoTable.finalY + 10; // Highlight savings const annualSavings = analysisResults.potentialSavings * 12; doc.setFillColor(40, 150, 80); doc.rect(15, y, 180, 15, 'F'); doc.setTextColor(255, 255, 255); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Annual Savings Opportunity: $' + annualSavings.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }), 105, y + 10, { align: 'center' }); y += 20; // Next steps doc.setTextColor(20, 40, 100); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Next Steps', 15, y); y += 7; doc.setTextColor(60, 60, 60); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const steps = [ '1. Review this proposal with your team', '2. Contact us to discuss implementation', '3. We handle the transition seamlessly', '4. Start saving immediately' ]; steps.forEach(step => { doc.text(step, 20, y); y += 6; }); doc.save('Merchant_Fee_Audit_Proposal.pdf'); alert('Proposal PDF downloaded successfully!'); } // ===== UI HELPERS ===== function goToStep(step) { document.querySelectorAll('.step-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.step').forEach(el => el.classList.remove('active', 'completed')); for (let i = 1; i < step; i++) { document.querySelector(`.step[data-step="${i}"]`)?.classList.add('completed'); } document.querySelector(`.step[data-step="${step}"]`)?.classList.add('active'); document.getElementById(`step-${step}`)?.classList.add('active'); } function formatMoney(val) { if (typeof val !== 'number' || isNaN(val)) return '$0.00'; return '$' + val.toFixed(2).replace(/\B(?=(\d{3})+(?!d))/g, ','); } // Init updatePricingFields(); updateProposedFields(); /div>
${formatMoney(r.processorMarkup)}/mo
Processing Rate vs. Industry
--