const axios = require('axios'); const logger = require('../utils/logger'); class ListmonkService { constructor() { this.baseURL = process.env.LISTMONK_API_URL || 'http://listmonk:9000/api'; this.username = process.env.LISTMONK_USERNAME; this.password = process.env.LISTMONK_PASSWORD; this.lists = { locations: null, users: null, supportLevel1: null, supportLevel2: null, supportLevel3: null, supportLevel4: null, hasSign: null, wantsSign: null, signRegular: null, signLarge: null, signUnsure: null }; // Debug logging for environment variables console.log('🔍 Listmonk Environment Variables:'); console.log(` LISTMONK_SYNC_ENABLED: ${process.env.LISTMONK_SYNC_ENABLED}`); console.log(` LISTMONK_API_URL: ${process.env.LISTMONK_API_URL}`); console.log(` LISTMONK_USERNAME: ${this.username ? 'SET' : 'NOT SET'}`); console.log(` LISTMONK_PASSWORD: ${this.password ? 'SET' : 'NOT SET'}`); this.syncEnabled = process.env.LISTMONK_SYNC_ENABLED === 'true'; // Additional validation - disable if credentials are missing if (this.syncEnabled && (!this.username || !this.password)) { logger.warn('Listmonk credentials missing - disabling sync'); this.syncEnabled = false; } console.log(` Final syncEnabled: ${this.syncEnabled}`); this.lastError = null; this.lastErrorTime = null; } // Create axios instance with auth getClient() { return axios.create({ baseURL: this.baseURL, auth: { username: this.username, password: this.password }, headers: { 'Content-Type': 'application/json' }, timeout: 10000 // 10 second timeout }); } // Test connection to Listmonk async checkConnection() { if (!this.syncEnabled) { return false; } try { console.log(`🔍 Testing connection to: ${this.baseURL}`); console.log(`🔍 Using credentials: ${this.username}:${this.password ? 'SET' : 'NOT SET'}`); const client = this.getClient(); console.log('🔍 Making request to /health endpoint...'); const { data } = await client.get('/health'); console.log('🔍 Response received:', JSON.stringify(data, null, 2)); if (data.data === true) { logger.info('Listmonk connection successful'); this.lastError = null; this.lastErrorTime = null; return true; } console.log('🔍 Health check failed - data.data is not true'); return false; } catch (error) { console.log('🔍 Connection error details:', error.message); if (error.response) { console.log('🔍 Response status:', error.response.status); console.log('🔍 Response data:', error.response.data); } this.lastError = `Listmonk connection failed: ${error.message}`; this.lastErrorTime = new Date(); logger.error(this.lastError); return false; } } // Initialize all lists on startup async initializeLists() { if (!this.syncEnabled) { logger.info('Listmonk sync is disabled'); return false; } try { // Check connection first const connected = await this.checkConnection(); if (!connected) { // Use the actual error message from checkConnection throw new Error(`Cannot connect to Listmonk: ${this.lastError || 'Unknown connection error'}`); } // Create or get main lists this.lists.locations = await this.ensureList({ name: 'Map Locations - All', type: 'private', optin: 'single', tags: ['map', 'locations', 'automated'], description: 'All locations from the map application' }); this.lists.users = await this.ensureList({ name: 'Map Users - All', type: 'private', optin: 'single', tags: ['map', 'users', 'automated'], description: 'All registered users from the map application' }); // Create support level lists this.lists.supportLevel1 = await this.ensureList({ name: 'Support Level 1', type: 'private', optin: 'single', tags: ['map', 'support-level-1', 'automated'], description: 'Strong supporters (Level 1 - Green)' }); this.lists.supportLevel2 = await this.ensureList({ name: 'Support Level 2', type: 'private', optin: 'single', tags: ['map', 'support-level-2', 'automated'], description: 'Moderate supporters (Level 2 - Yellow)' }); this.lists.supportLevel3 = await this.ensureList({ name: 'Support Level 3', type: 'private', optin: 'single', tags: ['map', 'support-level-3', 'automated'], description: 'Weak supporters (Level 3 - Orange)' }); this.lists.supportLevel4 = await this.ensureList({ name: 'Support Level 4', type: 'private', optin: 'single', tags: ['map', 'support-level-4', 'automated'], description: 'Opponents (Level 4 - Red)' }); // Create sign status lists this.lists.hasSign = await this.ensureList({ name: 'Has Campaign Sign', type: 'private', optin: 'single', tags: ['map', 'has-sign', 'automated'], description: 'Locations with campaign signs' }); this.lists.wantsSign = await this.ensureList({ name: 'Sign Requests', type: 'private', optin: 'single', tags: ['map', 'sign-requests', 'automated'], description: 'Supporters without signs (potential sign placements)' }); // Create sign size lists this.lists.signRegular = await this.ensureList({ name: 'Regular Signs', type: 'private', optin: 'single', tags: ['map', 'sign-regular', 'automated'], description: 'Locations with regular-sized campaign signs' }); this.lists.signLarge = await this.ensureList({ name: 'Large Signs', type: 'private', optin: 'single', tags: ['map', 'sign-large', 'automated'], description: 'Locations with large-sized campaign signs' }); this.lists.signUnsure = await this.ensureList({ name: 'Sign Size Unsure', type: 'private', optin: 'single', tags: ['map', 'sign-unsure', 'automated'], description: 'Locations with unsure sign size preferences' }); logger.info('✅ Listmonk lists initialized successfully'); return true; } catch (error) { this.lastError = `Failed to initialize Listmonk lists: ${error.message}`; this.lastErrorTime = new Date(); logger.error(this.lastError); return false; } } // Ensure a list exists, create if not async ensureList(listConfig) { try { const client = this.getClient(); // First, try to find existing list by name const { data: listsResponse } = await client.get('/lists'); const existingList = listsResponse.data.results.find(list => list.name === listConfig.name); if (existingList) { logger.info(`📋 Found existing list: ${listConfig.name}`); return existingList; } // Create new list const { data: createResponse } = await client.post('/lists', listConfig); logger.info(`📋 Created new list: ${listConfig.name}`); return createResponse.data; } catch (error) { logger.error(`Failed to ensure list ${listConfig.name}:`, error.message); throw error; } } // Sync a location to Listmonk async syncLocation(locationData) { console.log('🔍 syncLocation called with:', { hasEmail: !!locationData.Email, email: locationData.Email, syncEnabled: this.syncEnabled, firstName: locationData['First Name'], lastName: locationData['Last Name'], supportLevel: locationData['Support Level'], sign: locationData.Sign, signType: typeof locationData.Sign, signValue: String(locationData.Sign), signRequested: locationData['Sign Requested'], wantsSign: locationData['Wants Sign'] }); // Let's also see all keys in the locationData to see if the field name is different console.log('🔍 All locationData keys:', Object.keys(locationData)); console.log('🔍 Sign-related fields check:', { 'Sign': locationData.Sign, 'Sign Size': locationData['Sign Size'], 'sign': locationData.sign, 'Campaign Sign': locationData['Campaign Sign'], 'Sign Type': locationData['Sign Type'], 'Has Sign': locationData['Has Sign'], 'Yard Sign': locationData['Yard Sign'] }); if (!this.syncEnabled || !locationData.Email) { console.log('🔍 Sync disabled or no email - returning failure'); return { success: false, error: 'Sync disabled or no email provided' }; } try { // Check if they have a sign - based on Sign Size field having a value const signSize = locationData['Sign Size']; const hasSignValue = signSize && signSize !== '' && signSize !== null && signSize !== 'null'; console.log('🔍 Sign detection:', { 'Sign Size': signSize, 'Sign Size Type': typeof signSize, 'Has Sign Value': hasSignValue, 'Sign Boolean': locationData.Sign }); const subscriberData = { email: locationData.Email, name: `${locationData['First Name'] || ''} ${locationData['Last Name'] || ''}`.trim(), status: 'enabled', lists: [this.lists.locations.id], attribs: { address: locationData.Address, support_level: locationData['Support Level'], has_sign: locationData.Sign === true, sign_type: signSize || null, sign_boolean: locationData.Sign, unit_number: locationData['Unit Number'], phone: locationData.Phone, notes: locationData.Notes } }; // Add to support level lists const supportLevel = locationData['Support Level']; if (supportLevel && this.lists[`supportLevel${supportLevel}`]) { subscriberData.lists.push(this.lists[`supportLevel${supportLevel}`].id); } // Add to sign status lists // 1. Has Campaign Sign = only if Sign boolean is true (they physically have a sign) if (locationData.Sign === true) { subscriberData.lists.push(this.lists.hasSign.id); console.log('🔍 Added to hasSign list because Sign boolean is true'); } // 2. Sign Requests = if they selected a sign size but Sign boolean is false (want sign, don't have it yet) if (hasSignValue && locationData.Sign !== true) { subscriberData.lists.push(this.lists.wantsSign.id); console.log('🔍 Added to wantsSign list because they selected sign size but Sign boolean is false'); } // 3. Add to specific sign size lists based on Sign Size selection if (hasSignValue) { const signSizeLower = signSize.toLowerCase(); if (signSizeLower === 'regular' && this.lists.signRegular) { subscriberData.lists.push(this.lists.signRegular.id); console.log('🔍 Added to signRegular list'); } else if (signSizeLower === 'large' && this.lists.signLarge) { subscriberData.lists.push(this.lists.signLarge.id); console.log('🔍 Added to signLarge list'); } else if (signSizeLower === 'unsure' && this.lists.signUnsure) { subscriberData.lists.push(this.lists.signUnsure.id); console.log('🔍 Added to signUnsure list'); } } const client = this.getClient(); // Check if subscriber already exists try { const { data: existingResponse } = await client.get(`/subscribers?query=${encodeURIComponent(`email = '${locationData.Email}'`)}`); if (existingResponse.data.results && existingResponse.data.results.length > 0) { // Update existing subscriber const subscriberId = existingResponse.data.results[0].id; await client.put(`/subscribers/${subscriberId}`, subscriberData); logger.debug(`📍 Updated location subscriber: ${locationData.Email}`); } else { // Create new subscriber await client.post('/subscribers', subscriberData); logger.debug(`📍 Created location subscriber: ${locationData.Email}`); } console.log('🔍 Location sync successful - returning success'); return { success: true }; } catch (subscriptionError) { console.log('🔍 Subscriber search failed, trying to create new:', subscriptionError.message); // If subscriber doesn't exist, create new one await client.post('/subscribers', subscriberData); logger.debug(`📍 Created location subscriber: ${locationData.Email}`); console.log('🔍 Location sync successful (fallback) - returning success'); return { success: true }; } } catch (error) { console.log('🔍 Location sync failed with error:', error.message); logger.error(`Failed to sync location ${locationData.Email}:`, error.message); return { success: false, error: error.message }; } } // Sync a user to Listmonk async syncUser(userData) { if (!this.syncEnabled || !userData.Email) { return { success: false, error: 'Sync disabled or no email provided' }; } try { const subscriberData = { email: userData.Email, name: userData.Name || userData.Email, status: 'enabled', lists: [this.lists.users.id], attribs: { user_type: userData.UserType || 'user', admin: userData.Admin || false, created_at: userData['Created At'] || userData.created_at, last_login: userData['Last Login'] || userData.last_login } }; const client = this.getClient(); // Check if subscriber already exists try { const { data: existingResponse } = await client.get(`/subscribers?query=${encodeURIComponent(`email = '${userData.Email}'`)}`); if (existingResponse.data.results && existingResponse.data.results.length > 0) { // Update existing subscriber const subscriberId = existingResponse.data.results[0].id; await client.put(`/subscribers/${subscriberId}`, subscriberData); logger.debug(`👤 Updated user subscriber: ${userData.Email}`); } else { // Create new subscriber await client.post('/subscribers', subscriberData); logger.debug(`👤 Created user subscriber: ${userData.Email}`); } return { success: true }; } catch (subscriptionError) { // If subscriber doesn't exist, create new one await client.post('/subscribers', subscriberData); logger.debug(`👤 Created user subscriber: ${userData.Email}`); return { success: true }; } } catch (error) { logger.error(`Failed to sync user ${userData.Email}:`, error.message); return { success: false, error: error.message }; } } // Remove subscriber from Listmonk async removeSubscriber(email) { if (!this.syncEnabled || !email) { return { success: false, error: 'Sync disabled or no email provided' }; } try { const client = this.getClient(); const { data: response } = await client.get(`/subscribers?query=${encodeURIComponent(`email = '${email}'`)}`); if (response.data.results && response.data.results.length > 0) { const subscriberId = response.data.results[0].id; await client.delete(`/subscribers/${subscriberId}`); logger.debug(`🗑️ Removed subscriber: ${email}`); return { success: true }; } return { success: false, error: 'Subscriber not found' }; } catch (error) { logger.error(`Failed to remove subscriber ${email}:`, error.message); return { success: false, error: error.message }; } } // Bulk sync multiple records async bulkSync(records, type) { if (!this.syncEnabled || !records || records.length === 0) { return { success: 0, failed: 0 }; } let successCount = 0; let failedCount = 0; for (const record of records) { try { let result; if (type === 'location') { result = await this.syncLocation(record); } else if (type === 'user') { result = await this.syncUser(record); } if (result && result.success) { successCount++; } else { failedCount++; } } catch (error) { failedCount++; logger.error(`Bulk sync failed for record:`, error.message); } } logger.info(`📊 Bulk sync completed: ${successCount} success, ${failedCount} failed`); return { success: successCount, failed: failedCount }; } // Get sync status getSyncStatus() { return { enabled: this.syncEnabled, connected: this.lastError === null, lastError: this.lastError, lastErrorTime: this.lastErrorTime, listsInitialized: Object.values(this.lists).some(list => list !== null) }; } // Get list statistics async getListStats() { if (!this.syncEnabled) return null; try { const client = this.getClient(); const { data } = await client.get('/lists'); const stats = {}; for (const [key, list] of Object.entries(this.lists)) { if (list) { const listData = data.data.results.find(l => l.id === list.id); stats[key] = { name: list.name, subscriber_count: listData ? listData.subscriber_count : 0 }; } } return stats; } catch (error) { logger.error('Failed to get list stats:', error.message); return null; } } } // Export singleton instance module.exports = new ListmonkService();