AutomationFlowsWeb Scraping › Generate Customer Statements with Credit Risk Analysis, PDF Export & Gmail…

Generate Customer Statements with Credit Risk Analysis, PDF Export & Gmail…

Original n8n title: Generate Customer Statements with Credit Risk Analysis, PDF Export & Gmail Delivery

ByJitesh Dugar @jiteshdugar on n8n.io

Transform account statement management from hours to minutes - automatically compile transaction histories, calculate aging analysis, monitor credit limits, assess payment risk, and deliver professional PDF statements while syncing with accounting systems and alerting your team…

Webhook trigger★★★★☆ complexity18 nodesN8N Nodes HtmlcsstopdfGoogle DriveGmailHTTP Request
Web Scraping Trigger: Webhook Nodes: 18 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #10593 — we link there as the canonical source.

This workflow follows the Gmail → Google Drive recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "f74ccefe-8b1d-4ace-8a07-74c8a005c412",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1424,
        32
      ],
      "parameters": {
        "path": "statement-generator",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 1
    },
    {
      "id": "a5967318-4841-4893-87b6-3b04332223f9",
      "name": "Validate Statement Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -1200,
        32
      ],
      "parameters": {
        "jsCode": "// Validate incoming statement data\nconst item = $input.first().json;\n\n// Required fields validation\nconst requiredFields = ['customerName', 'customerEmail', 'accountNumber', 'statementPeriod', 'transactions'];\nconst missingFields = requiredFields.filter(field => !item[field] || (typeof item[field] === 'string' && item[field].trim() === ''));\n\n// Validate transactions array\nif (!item.transactions || !Array.isArray(item.transactions) || item.transactions.length === 0) {\n  missingFields.push('transactions (must be array with at least one transaction)');\n}\n\nif (missingFields.length > 0) {\n  throw new Error(`Missing required fields: ${missingFields.join(', ')}`);\n}\n\n// Validate email format\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nif (!emailRegex.test(item.customerEmail)) {\n  throw new Error('Invalid email format for customerEmail');\n}\n\n// Validate statement period\nif (!item.statementPeriod.startDate || !item.statementPeriod.endDate) {\n  throw new Error('Statement period must include startDate and endDate');\n}\n\n// Validate and process transactions\nfor (let i = 0; i < item.transactions.length; i++) {\n  const txn = item.transactions[i];\n  if (!txn.date || !txn.description) {\n    throw new Error(`Transaction ${i + 1} missing required fields (date, description)`);\n  }\n  \n  // Ensure numeric values\n  txn.debitAmount = parseFloat(txn.debitAmount) || 0;\n  txn.creditAmount = parseFloat(txn.creditAmount) || 0;\n  \n  // Parse transaction date\n  txn.dateObj = new Date(txn.date);\n}\n\n// Sort transactions by date (oldest first)\nitem.transactions.sort((a, b) => a.dateObj - b.dateObj);\n\n// Calculate running balance\nlet runningBalance = parseFloat(item.openingBalance) || 0;\nfor (let i = 0; i < item.transactions.length; i++) {\n  const txn = item.transactions[i];\n  runningBalance += txn.debitAmount - txn.creditAmount;\n  item.transactions[i].balance = runningBalance;\n}\n\n// Calculate totals\nitem.totalDebits = item.transactions.reduce((sum, txn) => sum + txn.debitAmount, 0);\nitem.totalCredits = item.transactions.reduce((sum, txn) => sum + txn.creditAmount, 0);\nitem.currentBalance = runningBalance;\nitem.openingBalance = parseFloat(item.openingBalance) || 0;\n\n// Calculate aging analysis\nconst today = new Date();\nconst aging = {\n  current: 0,\n  days30: 0,\n  days60: 0,\n  days90plus: 0\n};\n\n// Group outstanding invoices by age\nconst outstandingInvoices = item.transactions.filter(txn => \n  txn.debitAmount > 0 && txn.type === 'invoice' && !txn.paid\n);\n\noutstandingInvoices.forEach(invoice => {\n  const invoiceDate = new Date(invoice.date);\n  const daysOld = Math.floor((today - invoiceDate) / (1000 * 60 * 60 * 24));\n  \n  if (daysOld <= 30) {\n    aging.current += invoice.debitAmount;\n  } else if (daysOld <= 60) {\n    aging.days30 += invoice.debitAmount;\n  } else if (daysOld <= 90) {\n    aging.days60 += invoice.debitAmount;\n  } else {\n    aging.days90plus += invoice.debitAmount;\n  }\n});\n\nitem.agingAnalysis = aging;\nitem.totalOutstanding = aging.current + aging.days30 + aging.days60 + aging.days90plus;\n\n// Determine if account is overdue\nitem.isOverdue = aging.days30 + aging.days60 + aging.days90plus > 0;\nitem.overdueAmount = aging.days30 + aging.days60 + aging.days90plus;\n\n// Set currency defaults\nitem.currency = item.currency || 'USD';\nitem.currencySymbol = item.currencySymbol || '$';\n\n// Generate statement number\nif (!item.statementNumber) {\n  const date = new Date();\n  const year = date.getFullYear();\n  const month = String(date.getMonth() + 1).padStart(2, '0');\n  item.statementNumber = `STMT-${year}${month}-${item.accountNumber}`;\n}\n\n// Format dates\nconst formatDate = (dateStr) => {\n  return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });\n};\n\nitem.statementPeriod.startDateFormatted = formatDate(item.statementPeriod.startDate);\nitem.statementPeriod.endDateFormatted = formatDate(item.statementPeriod.endDate);\nitem.statementDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });\n\nreturn { json: item };"
      },
      "typeVersion": 2
    },
    {
      "id": "0a42850e-1843-457c-8deb-4cd5c23774d2",
      "name": "Enrich with Company Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -992,
        32
      ],
      "parameters": {
        "jsCode": "// Add company information and branding\nconst item = $input.first().json;\n\n// Company information (customize these)\nconst companyDefaults = {\n  companyName: item.companyName || 'Media Jade',\n  companyAddress: item.companyAddress || '456 Company Street',\n  companyCity: item.companyCity || 'San Francisco, CA 94105',\n  companyEmail: item.companyEmail || 'user@example.com',\n  companyPhone: item.companyPhone || '+1234567890',\n  companyWebsite: item.companyWebsite || 'www.mediajde.com',\n  companyLogo: item.companyLogo || '',\n  \n  // Account management\n  accountManagerName: item.accountManagerName || 'Account Manager',\n  accountManagerEmail: item.accountManagerEmail || 'user@example.com',\n  accountManagerPhone: item.accountManagerPhone || '+1234567890',\n  \n  // Payment details\n  bankName: item.bankName || 'Chase Bank',\n  accountNumber: item.accountNumber || '****1234',\n  routingNumber: item.routingNumber || '****5678',\n  swiftCode: item.swiftCode || 'CHASUS33',\n  \n  // Payment links\n  paymentLink: item.paymentLink || '',\n  \n  // Customer defaults\n  customerCompany: item.customerCompany || '',\n  customerAddress: item.customerAddress || '',\n  customerCity: item.customerCity || '',\n  customerPhone: item.customerPhone || '',\n  creditLimit: item.creditLimit || 0,\n  paymentTerms: item.paymentTerms || 'Net 30 days',\n  \n  // Statement notes\n  notes: item.notes || 'Thank you for your continued business. Please contact us if you have any questions about this statement.',\n  disputeProcess: item.disputeProcess || 'To dispute any transaction, please contact us within 30 days of this statement date with supporting documentation.'\n};\n\nreturn { json: { ...item, ...companyDefaults } };"
      },
      "typeVersion": 2
    },
    {
      "id": "541c5bba-919d-42e3-bdbb-859d3ee79263",
      "name": "Generate Statement HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        -768,
        32
      ],
      "parameters": {
        "jsCode": "// Generate professional statement HTML\nconst item = $input.first().json;\n\n// Format currency\nconst formatCurrency = (amount) => {\n  return `${item.currencySymbol}${Math.abs(amount).toFixed(2)}`;\n};\n\n// Format date for table\nconst formatDate = (dateStr) => {\n  return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });\n};\n\nconst html = `\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Statement - ${item.statementNumber}</title>\n    <style>\n        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');\n        * { margin: 0; padding: 0; box-sizing: border-box; }\n        body { font-family: 'Inter', sans-serif; line-height: 1.6; color: #1f2937; background: #ffffff; }\n        .container { max-width: 900px; margin: 0 auto; padding: 40px; }\n        .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 3px solid #8b5cf6; }\n        .company-info { flex: 1; }\n        .logo { max-width: 160px; height: auto; margin-bottom: 15px; }\n        .company-name { color: #8b5cf6; font-size: 26px; font-weight: 700; margin-bottom: 8px; }\n        .company-details { font-size: 13px; color: #6b7280; line-height: 1.7; }\n        .statement-badge { text-align: right; }\n        .statement-title { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; padding: 12px 25px; border-radius: 8px; font-size: 20px; font-weight: 700; letter-spacing: 1.5px; margin-bottom: 12px; }\n        .statement-number { font-size: 14px; color: #1f2937; font-weight: 600; margin-bottom: 8px; }\n        .overdue-badge { display: inline-block; padding: 6px 16px; border-radius: 20px; font-size: 12px; font-weight: 700; letter-spacing: 1px; color: white; background: #ef4444; margin-top: 8px; }\n        .info-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 35px; }\n        .info-card { background: #faf5ff; padding: 18px; border-radius: 10px; border: 1px solid #e9d5ff; }\n        .info-card h3 { color: #8b5cf6; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 10px; }\n        .info-card p { font-size: 13px; color: #4b5563; margin: 5px 0; }\n        .info-card .value { font-size: 16px; font-weight: 600; color: #1f2937; }\n        .aging-section { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); padding: 25px; border-radius: 10px; margin-bottom: 30px; border-left: 5px solid #f59e0b; }\n        .aging-section h2 { color: #92400e; font-size: 16px; font-weight: 700; margin-bottom: 20px; }\n        .aging-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }\n        .aging-box { background: rgba(255, 255, 255, 0.8); padding: 15px; border-radius: 8px; text-align: center; }\n        .aging-box .label { font-size: 12px; color: #78350f; font-weight: 600; margin-bottom: 8px; text-transform: uppercase; }\n        .aging-box .amount { font-size: 20px; font-weight: 700; color: #92400e; }\n        .aging-box.overdue { border: 2px solid #ef4444; }\n        .balance-summary { background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); padding: 25px; border-radius: 10px; margin-bottom: 30px; border-left: 5px solid #3b82f6; }\n        .balance-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }\n        .balance-item { display: flex; justify-content: space-between; padding: 10px 0; font-size: 15px; }\n        .balance-item.total { border-top: 3px solid #3b82f6; padding-top: 15px; margin-top: 10px; font-weight: 700; font-size: 20px; color: #1e40af; }\n        .transactions-table { width: 100%; border-collapse: separate; border-spacing: 0; margin-bottom: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); border-radius: 10px; overflow: hidden; }\n        .transactions-table thead { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); }\n        .transactions-table th { padding: 12px; text-align: left; font-weight: 600; font-size: 12px; color: white; text-transform: uppercase; }\n        .transactions-table tbody tr { background: white; }\n        .transactions-table tbody tr:nth-child(even) { background: #faf5ff; }\n        .transactions-table td { padding: 12px; border-bottom: 1px solid #e5e7eb; font-size: 13px; color: #374151; }\n        .transactions-table tbody tr:last-child td { border-bottom: none; }\n        .text-right { text-align: right; }\n        .debit { color: #dc2626; font-weight: 600; }\n        .credit { color: #16a34a; font-weight: 600; }\n        .payment-info { margin-top: 35px; padding: 25px; background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); border-left: 5px solid #10b981; border-radius: 8px; }\n        .payment-info h3 { color: #065f46; font-size: 16px; font-weight: 700; margin-bottom: 15px; }\n        .payment-info p { color: #047857; font-size: 13px; margin: 6px 0; }\n        .payment-info strong { color: #064e3b; }\n        .contact-section { margin-top: 30px; padding: 20px; background: #f8fafc; border-radius: 8px; }\n        .contact-section h3 { color: #475569; font-size: 15px; font-weight: 700; margin-bottom: 12px; }\n        .contact-section p { color: #64748b; font-size: 13px; line-height: 1.6; }\n        .footer { margin-top: 35px; padding-top: 20px; border-top: 2px solid #e5e7eb; text-align: center; color: #6b7280; font-size: 12px; }\n        .footer p { margin: 5px 0; }\n        @media print { body { print-color-adjust: exact; -webkit-print-color-adjust: exact; } }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <div class=\"header\">\n            <div class=\"company-info\">\n                ${item.companyLogo ? `<img src=\"${item.companyLogo}\" alt=\"${item.companyName}\" class=\"logo\">` : `<h1 class=\"company-name\">${item.companyName}</h1>`}\n                <div class=\"company-details\">\n                    ${item.companyAddress}<br>\n                    ${item.companyCity}<br>\n                    ${item.companyEmail} | ${item.companyPhone}\n                </div>\n            </div>\n            <div class=\"statement-badge\">\n                <div class=\"statement-title\">ACCOUNT STATEMENT</div>\n                <div class=\"statement-number\">${item.statementNumber}</div>\n                <div class=\"statement-number\">Account: ${item.accountNumber}</div>\n                ${item.isOverdue ? '<div class=\"overdue-badge\">OVERDUE</div>' : ''}\n            </div>\n        </div>\n\n        <div class=\"info-grid\">\n            <div class=\"info-card\">\n                <h3>Account Holder</h3>\n                <p class=\"value\">${item.customerName}</p>\n                ${item.customerCompany ? `<p><strong>${item.customerCompany}</strong></p>` : ''}\n                <p>${item.customerEmail}</p>\n                ${item.customerPhone ? `<p>${item.customerPhone}</p>` : ''}\n            </div>\n            <div class=\"info-card\">\n                <h3>Statement Period</h3>\n                <p><strong>From:</strong> ${item.statementPeriod.startDateFormatted}</p>\n                <p><strong>To:</strong> ${item.statementPeriod.endDateFormatted}</p>\n                <p><strong>Date:</strong> ${item.statementDate}</p>\n            </div>\n            <div class=\"info-card\">\n                <h3>Account Details</h3>\n                <p><strong>Terms:</strong> ${item.paymentTerms}</p>\n                ${item.creditLimit > 0 ? `<p><strong>Credit Limit:</strong> ${formatCurrency(item.creditLimit)}</p>` : ''}\n                <p><strong>Manager:</strong> ${item.accountManagerName}</p>\n            </div>\n        </div>\n\n        <div class=\"aging-section\">\n            <h2>\u23f0 Aging Analysis - Outstanding Balance Breakdown</h2>\n            <div class=\"aging-grid\">\n                <div class=\"aging-box\">\n                    <div class=\"label\">Current</div>\n                    <div class=\"amount\">${formatCurrency(item.agingAnalysis.current)}</div>\n                </div>\n                <div class=\"aging-box ${item.agingAnalysis.days30 > 0 ? 'overdue' : ''}\">\n                    <div class=\"label\">31-60 Days</div>\n                    <div class=\"amount\">${formatCurrency(item.agingAnalysis.days30)}</div>\n                </div>\n                <div class=\"aging-box ${item.agingAnalysis.days60 > 0 ? 'overdue' : ''}\">\n                    <div class=\"label\">61-90 Days</div>\n                    <div class=\"amount\">${formatCurrency(item.agingAnalysis.days60)}</div>\n                </div>\n                <div class=\"aging-box ${item.agingAnalysis.days90plus > 0 ? 'overdue' : ''}\">\n                    <div class=\"label\">90+ Days</div>\n                    <div class=\"amount\">${formatCurrency(item.agingAnalysis.days90plus)}</div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"balance-summary\">\n            <div class=\"balance-grid\">\n                <div>\n                    <div class=\"balance-item\">\n                        <span>Opening Balance:</span>\n                        <span>${formatCurrency(item.openingBalance)}</span>\n                    </div>\n                    <div class=\"balance-item\">\n                        <span>Total Charges:</span>\n                        <span class=\"debit\">+${formatCurrency(item.totalDebits)}</span>\n                    </div>\n                    <div class=\"balance-item\">\n                        <span>Total Payments:</span>\n                        <span class=\"credit\">-${formatCurrency(item.totalCredits)}</span>\n                    </div>\n                    <div class=\"balance-item total\">\n                        <span>Current Balance:</span>\n                        <span>${formatCurrency(item.currentBalance)}</span>\n                    </div>\n                </div>\n                <div>\n                    ${item.isOverdue ? `\n                    <div style=\"background: #fee2e2; padding: 20px; border-radius: 8px; border: 2px solid #ef4444;\">\n                        <div style=\"color: #991b1b; font-size: 14px; font-weight: 700; margin-bottom: 10px;\">\u26a0\ufe0f OVERDUE AMOUNT</div>\n                        <div style=\"color: #dc2626; font-size: 28px; font-weight: 700;\">${formatCurrency(item.overdueAmount)}</div>\n                        <div style=\"color: #991b1b; font-size: 12px; margin-top: 8px;\">Please remit payment immediately</div>\n                    </div>\n                    ` : `\n                    <div style=\"background: #d1fae5; padding: 20px; border-radius: 8px; text-align: center;\">\n                        <div style=\"color: #065f46; font-size: 14px; font-weight: 700;\">\u2713 Account In Good Standing</div>\n                        <div style=\"color: #047857; font-size: 12px; margin-top: 8px;\">No overdue payments</div>\n                    </div>\n                    `}\n                </div>\n            </div>\n        </div>\n\n        <h2 style=\"color: #6b21a8; font-size: 18px; font-weight: 700; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #e9d5ff;\">\ud83d\udccb Transaction History</h2>\n        \n        <table class=\"transactions-table\">\n            <thead>\n                <tr>\n                    <th style=\"width: 12%;\">Date</th>\n                    <th style=\"width: 38%;\">Description</th>\n                    <th style=\"width: 15%;\">Reference</th>\n                    <th class=\"text-right\" style=\"width: 12%;\">Charges</th>\n                    <th class=\"text-right\" style=\"width: 12%;\">Payments</th>\n                    <th class=\"text-right\" style=\"width: 11%;\">Balance</th>\n                </tr>\n            </thead>\n            <tbody>\n                <tr style=\"background: #f3f4f6; font-weight: 600;\">\n                    <td>${item.statementPeriod.startDateFormatted}</td>\n                    <td>Opening Balance</td>\n                    <td>-</td>\n                    <td class=\"text-right\">-</td>\n                    <td class=\"text-right\">-</td>\n                    <td class=\"text-right\">${formatCurrency(item.openingBalance)}</td>\n                </tr>\n                ${item.transactions.map(txn => `\n                <tr>\n                    <td>${formatDate(txn.date)}</td>\n                    <td>${txn.description}</td>\n                    <td>${txn.invoiceNumber || '-'}</td>\n                    <td class=\"text-right\">${txn.debitAmount > 0 ? `<span class=\"debit\">${formatCurrency(txn.debitAmount)}</span>` : '-'}</td>\n                    <td class=\"text-right\">${txn.creditAmount > 0 ? `<span class=\"credit\">${formatCurrency(txn.creditAmount)}</span>` : '-'}</td>\n                    <td class=\"text-right\"><strong>${formatCurrency(txn.balance)}</strong></td>\n                </tr>\n                `).join('')}\n                <tr style=\"background: #eff6ff; font-weight: 700; font-size: 14px;\">\n                    <td colspan=\"3\" class=\"text-right\">Closing Balance:</td>\n                    <td class=\"text-right debit\">${formatCurrency(item.totalDebits)}</td>\n                    <td class=\"text-right credit\">${formatCurrency(item.totalCredits)}</td>\n                    <td class=\"text-right\" style=\"color: #1e40af; font-size: 16px;\">${formatCurrency(item.currentBalance)}</td>\n                </tr>\n            </tbody>\n        </table>\n\n        <div class=\"payment-info\">\n            <h3>\ud83d\udcb3 Payment Information</h3>\n            <p><strong>Amount Due:</strong> ${formatCurrency(item.currentBalance)}</p>\n            ${item.isOverdue ? `<p style=\"color: #dc2626; font-weight: 700;\"><strong>\u26a0\ufe0f Overdue Amount:</strong> ${formatCurrency(item.overdueAmount)}</p>` : ''}\n            <p><strong>Payment Terms:</strong> ${item.paymentTerms}</p>\n            <p style=\"margin-top: 12px;\"><strong>Bank Details:</strong></p>\n            <p><strong>Bank Name:</strong> ${item.bankName}</p>\n            <p><strong>Account Number:</strong> ${item.accountNumber}</p>\n            <p><strong>Routing Number:</strong> ${item.routingNumber}</p>\n            ${item.swiftCode ? `<p><strong>SWIFT Code:</strong> ${item.swiftCode}</p>` : ''}\n            ${item.paymentLink ? `<p style=\"margin-top: 12px;\"><strong>\ud83d\udcb0 Pay Online:</strong> <a href=\"${item.paymentLink}\" style=\"color: #047857; text-decoration: underline;\">${item.paymentLink}</a></p>` : ''}\n        </div>\n\n        <div class=\"contact-section\">\n            <h3>\ud83d\udcde Questions About Your Statement?</h3>\n            <p><strong>Account Manager:</strong> ${item.accountManagerName}</p>\n            <p><strong>Email:</strong> ${item.accountManagerEmail} | <strong>Phone:</strong> ${item.accountManagerPhone}</p>\n            <p style=\"margin-top: 10px;\"><strong>Dispute Process:</strong> ${item.disputeProcess}</p>\n        </div>\n\n        <div class=\"footer\">\n            <p><strong>${item.companyName}</strong></p>\n            <p>${item.notes}</p>\n            <p style=\"margin-top: 10px;\">This statement is generated electronically and does not require a signature.</p>\n        </div>\n    </div>\n</body>\n</html>\n`;\n\nreturn {\n  json: {\n    html: html,\n    statementNumber: item.statementNumber,\n    customerName: item.customerName,\n    customerEmail: item.customerEmail,\n    accountNumber: item.accountNumber,\n    currentBalance: item.currentBalance,\n    isOverdue: item.isOverdue,\n    overdueAmount: item.overdueAmount,\n    currency: item.currency,\n    fileName: `Statement_${item.statementNumber}_${item.customerName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`,\n    ...item\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "3cd046ba-22ab-494d-bc81-ca0da0a7afd1",
      "name": "HTML to PDF",
      "type": "n8n-nodes-htmlcsstopdf.htmlcsstopdf",
      "position": [
        -544,
        32
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "01dae504-3365-4b0c-81d7-c92bb7450c9e",
      "name": "Save to Google Drive",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        -320,
        -64
      ],
      "parameters": {
        "name": "={{ $('Generate Statement HTML').item.json.fileName }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "list",
          "value": "root"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "8fd63f8a-a678-480c-9a1c-597a78c1082c",
      "name": "Send Email to Customer",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -320,
        144
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "ea91a9d6-9beb-468d-8bc8-c007f46c07dc",
      "name": "Credit Limit & Risk Analysis",
      "type": "n8n-nodes-base.code",
      "position": [
        -80,
        32
      ],
      "parameters": {
        "jsCode": "// Check credit limit and generate alerts\nconst item = $input.first().json;\n\nconst creditLimit = parseFloat(item.creditLimit) || 0;\nconst currentBalance = parseFloat(item.currentBalance) || 0;\n\n// Calculate credit utilization\nlet creditUtilization = 0;\nlet creditAvailable = 0;\nlet creditStatus = 'No Credit Limit';\nlet creditAlert = false;\nlet creditAlertLevel = 'none';\n\nif (creditLimit > 0) {\n  creditUtilization = (currentBalance / creditLimit) * 100;\n  creditAvailable = creditLimit - currentBalance;\n  \n  // Determine credit status and alert level\n  if (currentBalance > creditLimit) {\n    creditStatus = 'Over Limit';\n    creditAlert = true;\n    creditAlertLevel = 'critical';\n  } else if (creditUtilization >= 90) {\n    creditStatus = 'Near Limit (90%+)';\n    creditAlert = true;\n    creditAlertLevel = 'high';\n  } else if (creditUtilization >= 75) {\n    creditStatus = 'High Utilization (75%+)';\n    creditAlert = true;\n    creditAlertLevel = 'medium';\n  } else if (creditUtilization >= 50) {\n    creditStatus = 'Moderate Utilization';\n    creditAlert = false;\n    creditAlertLevel = 'low';\n  } else {\n    creditStatus = 'Good Standing';\n    creditAlert = false;\n    creditAlertLevel = 'none';\n  }\n}\n\n// Calculate days since last payment\nconst paymentTransactions = item.transactions.filter(txn => txn.creditAmount > 0);\nlet daysSinceLastPayment = null;\nlet lastPaymentDate = null;\nlet lastPaymentAmount = 0;\n\nif (paymentTransactions.length > 0) {\n  // Get most recent payment\n  const lastPayment = paymentTransactions[paymentTransactions.length - 1];\n  lastPaymentDate = new Date(lastPayment.date);\n  lastPaymentAmount = lastPayment.creditAmount;\n  \n  const today = new Date();\n  daysSinceLastPayment = Math.floor((today - lastPaymentDate) / (1000 * 60 * 60 * 24));\n}\n\n// Calculate average days to pay\nconst invoices = item.transactions.filter(txn => txn.type === 'invoice' && txn.debitAmount > 0);\nlet avgDaysToPay = null;\nlet paymentBehavior = 'No Payment History';\n\nif (paymentTransactions.length > 0 && invoices.length > 0) {\n  // Simplified calculation - in real scenario, would match invoices to payments\n  avgDaysToPay = Math.floor(daysSinceLastPayment / 2); // Simplified\n  \n  if (avgDaysToPay <= 30) {\n    paymentBehavior = 'Excellent - Pays on Time';\n  } else if (avgDaysToPay <= 45) {\n    paymentBehavior = 'Good - Occasional Delays';\n  } else if (avgDaysToPay <= 60) {\n    paymentBehavior = 'Fair - Frequent Delays';\n  } else {\n    paymentBehavior = 'Poor - Chronic Late Payment';\n  }\n}\n\n// Risk score calculation (0-100, higher = more risk)\nlet riskScore = 0;\n\nif (item.isOverdue) {\n  riskScore += 30;\n  if (item.agingAnalysis.days60 > 0) riskScore += 20;\n  if (item.agingAnalysis.days90plus > 0) riskScore += 30;\n}\n\nif (creditLimit > 0 && creditUtilization > 90) {\n  riskScore += 20;\n}\n\nif (daysSinceLastPayment && daysSinceLastPayment > 60) {\n  riskScore += 10;\n}\n\nlet riskLevel = 'Low Risk';\nif (riskScore >= 70) {\n  riskLevel = 'High Risk';\n} else if (riskScore >= 40) {\n  riskLevel = 'Medium Risk';\n} else if (riskScore > 0) {\n  riskLevel = 'Low Risk';\n} else {\n  riskLevel = 'No Risk';\n}\n\n// Generate recommendations\nconst recommendations = [];\n\nif (creditAlert) {\n  if (creditAlertLevel === 'critical') {\n    recommendations.push('URGENT: Account has exceeded credit limit. Suspend new credit immediately.');\n    recommendations.push('Contact customer to arrange payment plan and credit limit discussion.');\n  } else if (creditAlertLevel === 'high') {\n    recommendations.push('Account approaching credit limit (90%+). Review credit extension policy.');\n    recommendations.push('Proactively contact customer about payment schedule.');\n  } else if (creditAlertLevel === 'medium') {\n    recommendations.push('Monitor account closely - high credit utilization detected.');\n  }\n}\n\nif (item.isOverdue) {\n  if (item.agingAnalysis.days90plus > 0) {\n    recommendations.push('CRITICAL: Invoices 90+ days overdue. Escalate to collections.');\n  } else if (item.agingAnalysis.days60 > 0) {\n    recommendations.push('Urgent: Invoices 60+ days overdue. Schedule payment call.');\n  } else if (item.agingAnalysis.days30 > 0) {\n    recommendations.push('Send payment reminder for invoices 30+ days overdue.');\n  }\n}\n\nif (daysSinceLastPayment && daysSinceLastPayment > 45) {\n  recommendations.push(`No payment received in ${daysSinceLastPayment} days. Follow up with customer.`);\n}\n\nif (riskScore >= 70) {\n  recommendations.push('High risk account - consider requiring prepayment for new orders.');\n}\n\nif (recommendations.length === 0) {\n  recommendations.push('Account in good standing. Continue monitoring per standard procedures.');\n}\n\nreturn {\n  json: {\n    ...item,\n    creditLimit: creditLimit,\n    creditUtilization: creditUtilization,\n    creditAvailable: creditAvailable,\n    creditStatus: creditStatus,\n    creditAlert: creditAlert,\n    creditAlertLevel: creditAlertLevel,\n    daysSinceLastPayment: daysSinceLastPayment,\n    lastPaymentDate: lastPaymentDate ? lastPaymentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : 'No payments',\n    lastPaymentAmount: lastPaymentAmount,\n    avgDaysToPay: avgDaysToPay,\n    paymentBehavior: paymentBehavior,\n    riskScore: riskScore,\n    riskLevel: riskLevel,\n    recommendations: recommendations\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "f9b42cb9-2497-4a3b-98c5-bd1b845f3a1f",
      "name": "Update Accounting System",
      "type": "n8n-nodes-base.code",
      "position": [
        176,
        32
      ],
      "parameters": {
        "jsCode": "// Update accounting system with statement delivery\nconst item = $input.first().json;\n\n// Prepare data for accounting system update\n// This simulates an API call to QuickBooks, Xero, FreshBooks, etc.\n\nconst accountingUpdate = {\n  // Statement metadata\n  statementNumber: item.statementNumber,\n  accountNumber: item.accountNumber,\n  statementDate: new Date().toISOString(),\n  statementPeriod: {\n    startDate: item.statementPeriod.startDate,\n    endDate: item.statementPeriod.endDate\n  },\n  \n  // Customer information\n  customerId: item.customerId || item.accountNumber,\n  customerName: item.customerName,\n  customerEmail: item.customerEmail,\n  \n  // Financial data\n  openingBalance: item.openingBalance,\n  closingBalance: item.currentBalance,\n  totalCharges: item.totalDebits,\n  totalPayments: item.totalCredits,\n  \n  // Aging data\n  agingAnalysis: item.agingAnalysis,\n  totalOutstanding: item.totalOutstanding,\n  overdueAmount: item.overdueAmount,\n  \n  // Account status\n  isOverdue: item.isOverdue,\n  creditStatus: item.creditStatus,\n  creditUtilization: item.creditUtilization,\n  riskLevel: item.riskLevel,\n  riskScore: item.riskScore,\n  \n  // Payment tracking\n  daysSinceLastPayment: item.daysSinceLastPayment,\n  lastPaymentDate: item.lastPaymentDate,\n  lastPaymentAmount: item.lastPaymentAmount,\n  paymentBehavior: item.paymentBehavior,\n  \n  // Statement delivery\n  statementDeliveredAt: new Date().toISOString(),\n  deliveryMethod: 'email',\n  recipientEmail: item.customerEmail,\n  pdfStorageUrl: item.pdfStorageUrl || 'pending',\n  \n  // Actions taken\n  recommendedActions: item.recommendations,\n  \n  // For API integration (replace with actual endpoint)\n  apiEndpoint: 'https://api.youraccountingsystem.com/statements',\n  apiMethod: 'POST'\n};\n\n// In production, this would be an HTTP Request node calling your accounting API\n// For now, we're logging the data that would be sent\n\n// Generate activity note for CRM/Accounting system\nconst activityNote = `Account Statement ${item.statementNumber} generated and delivered.\n\nAccount Summary:\n- Current Balance: ${item.currencySymbol}${item.currentBalance.toFixed(2)}\n- Status: ${item.isOverdue ? 'OVERDUE' : 'Current'}\n- Credit Status: ${item.creditStatus}\n- Risk Level: ${item.riskLevel} (Score: ${item.riskScore}/100)\n\nAging Breakdown:\n- Current: ${item.currencySymbol}${item.agingAnalysis.current.toFixed(2)}\n- 31-60 Days: ${item.currencySymbol}${item.agingAnalysis.days30.toFixed(2)}\n- 61-90 Days: ${item.currencySymbol}${item.agingAnalysis.days60.toFixed(2)}\n- 90+ Days: ${item.currencySymbol}${item.agingAnalysis.days90plus.toFixed(2)}\n\nPayment Behavior: ${item.paymentBehavior}\nDays Since Last Payment: ${item.daysSinceLastPayment || 'N/A'}\n\nRecommended Actions:\n${item.recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\\n')}\n\nStatement delivered to ${item.customerEmail} and archived in Google Drive.`;\n\nreturn {\n  json: {\n    ...item,\n    accountingUpdate: accountingUpdate,\n    activityNote: activityNote,\n    syncedToAccounting: true,\n    syncTimestamp: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "8a567cad-14db-49b4-a51a-6b5124efaebf",
      "name": "Has Overdue Balance?",
      "type": "n8n-nodes-base.if",
      "position": [
        416,
        32
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $('Credit Limit & Risk Analysis').item.json.isOverdue }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "e191eeb0-3770-47b1-8131-39464bfefb88",
      "name": "Notify Team - Overdue",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        656,
        -64
      ],
      "parameters": {
        "url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
        "options": {},
        "jsonBody": "={\n  \"text\": \"\u26a0\ufe0f OVERDUE Statement Sent\",\n  \"blocks\": [\n    {\n      \"type\": \"section\",\n      \"text\": {\n        \"type\": \"mrkdwn\",\n        \"text\": \"*\u26a0\ufe0f OVERDUE Account Statement*\\n\\n*Statement:* {{ $('Generate Statement HTML').item.json.statementNumber }}\\n*Customer:* {{ $('Generate Statement HTML').item.json.customerName }}\\n*Account:* {{ $('Generate Statement HTML').item.json.accountNumber }}\\n*Current Balance:* {{ $('Generate Statement HTML').item.json.currency }} {{ $('Generate Statement HTML').item.json.currentBalance }}\\n*Overdue Amount:* {{ $('Generate Statement HTML').item.json.currency }} {{ $('Generate Statement HTML').item.json.overdueAmount }}\\n\\n*Aging Breakdown:*\\n\u2022 31-60 days: {{ $('Generate Statement HTML').item.json.currency }} {{ $('Generate Statement HTML').item.json.agingAnalysis.days30 }}\\n\u2022 61-90 days: {{ $('Generate Statement HTML').item.json.currency }} {{ $('Generate Statement HTML').item.json.agingAnalysis.days60 }}\\n\u2022 90+ days: {{ $('Generate Statement HTML').item.json.currency }} {{ $('Generate Statement HTML').item.json.agingAnalysis.days90plus }}\\n\\n*Risk Assessment:*\\n\u2022 Risk Level: {{ $('Credit Limit & Risk Analysis').item.json.riskLevel }}\\n\u2022 Risk Score: {{ $('Credit Limit & Risk Analysis').item.json.riskScore }}/100\\n\u2022 Credit Status: {{ $('Credit Limit & Risk Analysis').item.json.creditStatus }}\\n\u2022 Payment Behavior: {{ $('Credit Limit & Risk Analysis').item.json.paymentBehavior }}\\n\\n*Recommended Actions:*\\n{{ $('Credit Limit & Risk Analysis').item.json.recommendations[0] }}\\n\\n<{{ $('Save to Google Drive').item.json.webViewLink }}|View Statement in Drive>\\n\\n\ud83d\udea8 *Action Required:* Follow up with customer immediately\"\n      }\n    }\n  ]\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.1
    },
    {
      "id": "286fe777-094d-42e1-b022-6707877d680b",
      "name": "Notify Team - Current",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        656,
        144
      ],
      "parameters": {
        "url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
        "options": {},
        "jsonBody": "={\n  \"text\": \"\u2705 Statement Sent - Account Current\",\n  \"blocks\": [\n    {\n      \"type\": \"section\",\n      \"text\": {\n        \"type\": \"mrkdwn\",\n        \"text\": \"*\ud83d\udcca Account Statement Sent*\\n\\n*Statement:* {{ $('Generate Statement HTML').item.json.statementNumber }}\\n*Customer:* {{ $('Generate Statement HTML').item.json.customerName }}\\n*Account:* {{ $('Generate Statement HTML').item.json.accountNumber }}\\n*Current Balance:* {{ $('Generate Statement HTML').item.json.currency }} {{ $('Generate Statement HTML').item.json.currentBalance }}\\n*Status:* \u2713 Account in good standing\\n\\n*Credit Health:*\\n\u2022 Credit Status: {{ $('Credit Limit & Risk Analysis').item.json.creditStatus }}\\n\u2022 Credit Available: {{ $('Generate Statement HTML').item.json.currency }} {{ $('Credit Limit & Risk Analysis').item.json.creditAvailable }}\\n\u2022 Payment Behavior: {{ $('Credit Limit & Risk Analysis').item.json.paymentBehavior }}\\n\u2022 Risk Level: {{ $('Credit Limit & Risk Analysis').item.json.riskLevel }}\\n\\n<{{ $('Save to Google Drive').item.json.webViewLink }}|View Statement in Drive>\"\n      }\n    }\n  ]\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.1
    },
    {
      "id": "02aae56b-1955-43b0-9bdf-1a16f12b85aa",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2032,
        -720
      ],
      "parameters": {
        "width": 432,
        "height": 640,
        "content": "## How it works\n\nThis workflow automates monthly/quarterly account statement generation with aging analysis. It receives account data from your billing system, calculates running balances and payment aging (30/60/90+ days), generates professional PDF statements, and delivers them to customers while archiving for compliance. The system also performs credit risk assessment and alerts your AR team about overdue accounts with specific collection recommendations.\n\n## Setup steps\n\n1. **Configure webhook URL** in your accounting system (QuickBooks/Xero/etc.)\n2. **Add company branding** in \"Enrich with Company Data\" node\n3. **Connect HTML to PDF API** (get free API key at htmlcsstoimage.com)\n4. **Authenticate Google Drive** for statement archival\n5. **Connect Gmail** for customer delivery\n6. **Add Slack webhook URLs** in both notification nodes\n7. **Test with sample data** before connecting live system\n\n\ud83d\udca1 **Required data:** customerName, customerEmail, accountNumber, statementPeriod (startDate/endDate), transactions array"
      },
      "typeVersion": 1
    },
    {
      "id": "abfa0356-8d1c-499a-af8b-fa42fc7d1c63",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1456,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 464,
        "content": "## Data Input & Processing\n\nReceives account data, validates required fields, calculates running balances and aging analysis, then enriches with your company branding and payment terms."
      },
      "typeVersion": 1
    },
    {
      "id": "070165b2-fd8c-4475-994f-355e7b4fbff6",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 464,
        "content": "### Statement Design\n\nGenerates professional HTML with color-coded aging boxes and converts to print-ready PDF. Requires HTML to PDF API credentials."
      },
      "typeVersion": 1
    },
    {
      "id": "f6f83d5e-673a-4ef6-8700-bd36de925319",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        -240
      ],
      "parameters": {
        "color": 7,
        "height": 528,
        "content": "## Delivery & Archive\n\nArchives PDF to Google Drive for audit trail, then emails statement to customer. Ensures regulatory compliance and easy retrieval."
      },
      "typeVersion": 1
    },
    {
      "id": "f60c101e-06d1-4030-8214-601b4f43575d",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 480,
        "content": "## Risk Assessment\n\nAnalyzes credit utilization, payment behavior, and calculates risk score (0-100). Generates specific collection recommendations for AR team."
      },
      "typeVersion": 1
    },
    {
      "id": "9cdffdd4-d413-4aac-b97e-2486bf52dfa1",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 560,
        "content": "## Team Notifications\n\nRoutes alerts based on payment status. Overdue accounts get urgent notifications with aging breakdown; current accounts get standard confirmation."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "HTML to PDF": {
      "main": [
        [
          {
            "node": "Save to Google Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Validate Statement Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Overdue Balance?": {
      "main": [
        [
          {
            "node": "Notify Team - Overdue",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify Team - Current",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Google Drive": {
      "main": [
        [
          {
            "node": "Send Email to Customer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email to Customer": {
      "main": [
        [
          {
            "node": "Credit Limit & Risk Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Statement HTML": {
      "main": [
        [
          {
            "node": "HTML to PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Statement Data": {
      "main": [
        [
          {
            "node": "Enrich with Company Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enrich with Company Data": {
      "main": [
        [
          {
            "node": "Generate Statement HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Accounting System": {
      "main": [
        [
          {
            "node": "Has Overdue Balance?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Credit Limit & Risk Analysis": {
      "main": [
        [
          {
            "node": "Update Accounting System",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Transform account statement management from hours to minutes - automatically compile transaction histories, calculate aging analysis, monitor credit limits, assess payment risk, and deliver professional PDF statements while syncing with accounting systems and alerting your team…

Source: https://n8n.io/workflows/10593/ — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

A comprehensive n8n workflow template that completely automates the startup pitch deck submission process for accelerators, incubators, VC firms, and startup competitions. This workflow validates foun

Google Drive, Gmail, N8N Nodes Verifiemail +2
Web Scraping

This workflow automates the entire parent consent process for school field trips, replacing manual paper forms with a secure, verified, and legally compliant digital system.

Google Drive, Gmail, N8N Nodes Verifiemail +2
Web Scraping

Transform new hire onboarding from 3-4 hours of manual document compilation to 3 minutes of automated generation - creates personalized, role-specific document packages including welcome letters, bene

N8N Nodes Htmlcsstopdf, Google Drive, Gmail +1
Web Scraping

Automatically generate professional PDF invoices when new orders are placed in Shopify. This template creates beautifully formatted invoices from order data, converts them to PDF, saves to Google Driv

Google Drive, Gmail, N8N Nodes Htmlcsstopdf +1
Web Scraping

Verified Corporate Training Certificate with CEUs – Fully Automated & Verifiable

Google Drive, Slack, Google Sheets +4