UNPKG

@profullstack/lead-generator

Version:

A CLI tool for sending mass lead emails to prospective clients from CSV files with AI-powered personalization

474 lines (410 loc) 12.3 kB
/** * CSV Status Tracker Module * Handles status and notes tracking for email and voice campaigns */ import fs from 'fs'; import path from 'path'; import csv from 'csv-parser'; import { createObjectCsvWriter } from 'csv-writer'; /** * Email status constants */ export const EMAIL_STATUSES = { SENT: 'sent', FAILED: 'failed', BOUNCED: 'bounced', OPENED: 'opened', CLICKED: 'clicked', REPLIED: 'replied' }; /** * Voice call status constants */ export const VOICE_STATUSES = { ANSWERED: 'answered', VOICEMAIL: 'voicemail', NO_ANSWER: 'no-answer', OPTED_OUT: 'opted-out', SCHEDULED: 'scheduled', FAILED: 'failed' }; /** * SMS status constants */ export const SMS_STATUS = { SENT: 'sent', FAILED: 'failed', DELIVERED: 'delivered', UNDELIVERED: 'undelivered', REPLIED: 'replied', OPTED_OUT: 'opted-out' }; /** * Status column names */ const STATUS_COLUMNS = { EMAIL_STATUS: 'EmailStatus', EMAIL_NOTES: 'EmailNotes', VOICE_STATUS: 'VoiceStatus', VOICE_NOTES: 'VoiceNotes', SMS_STATUS: 'SmsStatus', SMS_NOTES: 'SmsNotes', LAST_UPDATED: 'LastUpdated' }; /** * Check if CSV file has status columns * @param {string} csvPath - Path to CSV file * @returns {Promise<boolean>} True if status columns exist */ async function hasStatusColumns(csvPath) { return new Promise((resolve, reject) => { if (!fs.existsSync(csvPath)) { reject(new Error('File not found')); return; } const stream = fs.createReadStream(csvPath); let headerChecked = false; stream .pipe(csv()) .on('headers', (headers) => { headerChecked = true; const hasEmailStatus = headers.includes(STATUS_COLUMNS.EMAIL_STATUS); const hasVoiceStatus = headers.includes(STATUS_COLUMNS.VOICE_STATUS); const hasSmsStatus = headers.includes(STATUS_COLUMNS.SMS_STATUS); const hasLastUpdated = headers.includes(STATUS_COLUMNS.LAST_UPDATED); resolve(hasEmailStatus && hasVoiceStatus && hasSmsStatus && hasLastUpdated); stream.destroy(); }) .on('error', (error) => { reject(error); }) .on('end', () => { if (!headerChecked) { resolve(false); } }); }); } /** * Read CSV file and return data as array of objects * @param {string} csvPath - Path to CSV file * @returns {Promise<Array>} Array of CSV row objects */ async function readCSVData(csvPath) { return new Promise((resolve, reject) => { const results = []; if (!fs.existsSync(csvPath)) { reject(new Error('File not found')); return; } fs.createReadStream(csvPath) .pipe(csv()) .on('data', (data) => results.push(data)) .on('end', () => resolve(results)) .on('error', (error) => reject(error)); }); } /** * Write CSV data to file * @param {string} csvPath - Path to CSV file * @param {Array} data - Array of objects to write * @param {Array} headers - Array of header names * @returns {Promise<void>} */ async function writeCSVData(csvPath, data, headers) { return new Promise((resolve, reject) => { if (data.length === 0) { reject(new Error('No data to write')); return; } const csvWriter = createObjectCsvWriter({ path: csvPath, header: headers.map(header => ({ id: header, title: header })) }); csvWriter.writeRecords(data) .then(() => resolve()) .catch((error) => reject(error)); }); } /** * Add status columns to CSV file * @param {string} csvPath - Path to CSV file * @returns {Object} Result object with success status */ export function addStatusColumns(csvPath) { try { if (!fs.existsSync(csvPath)) { return { success: false, error: 'File not found' }; } // Read the file synchronously to check headers const content = fs.readFileSync(csvPath, 'utf8'); const lines = content.trim().split('\n'); if (lines.length === 0) { return { success: false, error: 'Empty CSV file' }; } const headers = lines[0].split(',').map(h => h.trim()); // Check if status columns already exist const hasEmailStatus = headers.includes(STATUS_COLUMNS.EMAIL_STATUS); const hasVoiceStatus = headers.includes(STATUS_COLUMNS.VOICE_STATUS); const hasSmsStatus = headers.includes(STATUS_COLUMNS.SMS_STATUS); const hasLastUpdated = headers.includes(STATUS_COLUMNS.LAST_UPDATED); if (hasEmailStatus && hasVoiceStatus && hasSmsStatus && hasLastUpdated) { return { success: true, columnsAdded: false, message: 'Status columns already exist' }; } // Add missing status columns const newHeaders = [...headers]; if (!hasEmailStatus) { newHeaders.push(STATUS_COLUMNS.EMAIL_STATUS); newHeaders.push(STATUS_COLUMNS.EMAIL_NOTES); } if (!hasVoiceStatus) { newHeaders.push(STATUS_COLUMNS.VOICE_STATUS); newHeaders.push(STATUS_COLUMNS.VOICE_NOTES); } if (!hasSmsStatus) { newHeaders.push(STATUS_COLUMNS.SMS_STATUS); newHeaders.push(STATUS_COLUMNS.SMS_NOTES); } if (!hasLastUpdated) { newHeaders.push(STATUS_COLUMNS.LAST_UPDATED); } // Update all lines with empty values for new columns const updatedLines = [newHeaders.join(',')]; for (let i = 1; i < lines.length; i++) { const row = lines[i].split(','); const newRow = [...row]; // Add empty values for new columns const columnsToAdd = newHeaders.length - headers.length; for (let j = 0; j < columnsToAdd; j++) { newRow.push(''); } updatedLines.push(newRow.join(',')); } // Write updated content back to file fs.writeFileSync(csvPath, updatedLines.join('\n')); return { success: true, columnsAdded: true, message: 'Status columns added successfully' }; } catch (error) { return { success: false, error: `Failed to add status columns: ${error.message}` }; } } /** * Update CSV status for a contact * @param {string} csvPath - Path to CSV file * @param {Object} statusUpdate - Status update object * @returns {Object} Result object with success status */ export async function updateCSVStatus(csvPath, statusUpdate) { try { // Check if status columns exist const hasColumns = await hasStatusColumns(csvPath); if (!hasColumns) { return { success: false, error: 'CSV file does not have required status columns. Run addStatusColumns first.' }; } // Read CSV data const data = await readCSVData(csvPath); if (data.length === 0) { return { success: false, error: 'No data found in CSV file' }; } // Find the contact to update let contactFound = false; let updatedData = data.map(row => { let shouldUpdate = false; // Check if this row matches the contact if (statusUpdate.email && row.Email === statusUpdate.email) { shouldUpdate = true; } else if (statusUpdate.phoneNumber && row.PhoneNumber === statusUpdate.phoneNumber) { shouldUpdate = true; } if (shouldUpdate) { contactFound = true; const updatedRow = { ...row }; // Update email status and notes if (statusUpdate.emailStatus) { updatedRow[STATUS_COLUMNS.EMAIL_STATUS] = statusUpdate.emailStatus; } if (statusUpdate.emailNotes) { updatedRow[STATUS_COLUMNS.EMAIL_NOTES] = statusUpdate.emailNotes; } // Update voice status and notes if (statusUpdate.voiceStatus) { updatedRow[STATUS_COLUMNS.VOICE_STATUS] = statusUpdate.voiceStatus; } if (statusUpdate.voiceNotes) { updatedRow[STATUS_COLUMNS.VOICE_NOTES] = statusUpdate.voiceNotes; } // Update SMS status and notes if (statusUpdate.smsStatus) { updatedRow[STATUS_COLUMNS.SMS_STATUS] = statusUpdate.smsStatus; } if (statusUpdate.smsNotes) { updatedRow[STATUS_COLUMNS.SMS_NOTES] = statusUpdate.smsNotes; } // Update timestamp updatedRow[STATUS_COLUMNS.LAST_UPDATED] = new Date().toISOString(); return updatedRow; } return row; }); if (!contactFound) { return { success: true, updated: false, message: 'Contact not found in CSV file' }; } // Get headers from the first row const headers = Object.keys(updatedData[0]); // Write updated data back to CSV await writeCSVData(csvPath, updatedData, headers); return { success: true, updated: true, message: 'Status updated successfully' }; } catch (error) { return { success: false, error: `Failed to update CSV status: ${error.message}` }; } } /** * Get status from CSV for a contact * @param {string} csvPath - Path to CSV file * @param {Object} contact - Contact identifier (email or phoneNumber) * @returns {Object} Result object with status data */ export async function getStatusFromCSV(csvPath, contact) { try { const data = await readCSVData(csvPath); // Find the contact const foundContact = data.find(row => { if (contact.email && row.Email === contact.email) { return true; } if (contact.phoneNumber && row.PhoneNumber === contact.phoneNumber) { return true; } return false; }); if (!foundContact) { return { success: true, found: false, message: 'Contact not found' }; } return { success: true, found: true, status: { emailStatus: foundContact[STATUS_COLUMNS.EMAIL_STATUS] || '', emailNotes: foundContact[STATUS_COLUMNS.EMAIL_NOTES] || '', voiceStatus: foundContact[STATUS_COLUMNS.VOICE_STATUS] || '', voiceNotes: foundContact[STATUS_COLUMNS.VOICE_NOTES] || '', smsStatus: foundContact[STATUS_COLUMNS.SMS_STATUS] || '', smsNotes: foundContact[STATUS_COLUMNS.SMS_NOTES] || '', lastUpdated: foundContact[STATUS_COLUMNS.LAST_UPDATED] || '' } }; } catch (error) { return { success: false, error: `Failed to get status from CSV: ${error.message}` }; } } /** * Create backup of CSV file * @param {string} csvPath - Path to CSV file * @returns {Object} Result object with backup path */ export function createStatusBackup(csvPath) { try { if (!fs.existsSync(csvPath)) { return { success: false, error: 'CSV file not found' }; } const backupPath = csvPath.replace('.csv', '.backup.csv'); fs.copyFileSync(csvPath, backupPath); return { success: true, backupPath, message: 'Backup created successfully' }; } catch (error) { return { success: false, error: `Failed to create backup: ${error.message}` }; } } /** * Get all contacts with specific status * @param {string} csvPath - Path to CSV file * @param {Object} filters - Status filters * @returns {Promise<Object>} Result with filtered contacts */ export async function getContactsByStatus(csvPath, filters = {}) { try { const data = await readCSVData(csvPath); const filteredContacts = data.filter(row => { let matches = true; if (filters.emailStatus && row[STATUS_COLUMNS.EMAIL_STATUS] !== filters.emailStatus) { matches = false; } if (filters.voiceStatus && row[STATUS_COLUMNS.VOICE_STATUS] !== filters.voiceStatus) { matches = false; } if (filters.smsStatus && row[STATUS_COLUMNS.SMS_STATUS] !== filters.smsStatus) { matches = false; } return matches; }); return { success: true, contacts: filteredContacts, count: filteredContacts.length }; } catch (error) { return { success: false, error: `Failed to filter contacts: ${error.message}` }; } } export default { EMAIL_STATUSES, VOICE_STATUSES, SMS_STATUS, addStatusColumns, updateCSVStatus, getStatusFromCSV, createStatusBackup, getContactsByStatus };