Bunch of buag fixes and updates
This commit is contained in:
@@ -261,12 +261,16 @@ class EmailService {
|
||||
}
|
||||
}
|
||||
|
||||
async sendRepresentativeEmail(recipientEmail, senderName, senderEmail, subject, message, postalCode) {
|
||||
async sendRepresentativeEmail(recipientEmail, senderName, senderEmail, subject, message, postalCode, recipientName = null) {
|
||||
// Generate dynamic subject if not provided
|
||||
const finalSubject = subject || `Message from ${senderName} from ${postalCode}`;
|
||||
|
||||
const templateVariables = {
|
||||
MESSAGE: message,
|
||||
SENDER_NAME: senderName,
|
||||
SENDER_EMAIL: senderEmail,
|
||||
POSTAL_CODE: postalCode
|
||||
POSTAL_CODE: postalCode,
|
||||
RECIPIENT_NAME: recipientName || 'Representative'
|
||||
};
|
||||
|
||||
const emailOptions = {
|
||||
@@ -276,7 +280,7 @@ class EmailService {
|
||||
name: process.env.SMTP_FROM_NAME
|
||||
},
|
||||
replyTo: senderEmail,
|
||||
subject: subject
|
||||
subject: finalSubject
|
||||
};
|
||||
|
||||
return await this.sendTemplatedEmail('representative-contact', templateVariables, emailOptions);
|
||||
|
||||
@@ -70,7 +70,7 @@ class EmailTemplateService {
|
||||
|
||||
// Add default variables
|
||||
const defaultVariables = {
|
||||
APP_NAME: process.env.APP_NAME || 'BNKops Influence Tool',
|
||||
APP_NAME: process.env.APP_NAME || 'BNKops Influence Campaign',
|
||||
TIMESTAMP: new Date().toLocaleString(),
|
||||
...variables
|
||||
};
|
||||
|
||||
@@ -80,19 +80,35 @@ class NocoDBService {
|
||||
// Create record
|
||||
async create(tableId, data) {
|
||||
try {
|
||||
// Clean the data to remove any null values which can cause NocoDB issues
|
||||
// Clean the data to remove any null values and system fields that NocoDB manages
|
||||
const cleanData = Object.keys(data).reduce((clean, key) => {
|
||||
if (data[key] !== null && data[key] !== undefined) {
|
||||
clean[key] = data[key];
|
||||
// Skip null/undefined values
|
||||
if (data[key] === null || data[key] === undefined) {
|
||||
return clean;
|
||||
}
|
||||
|
||||
// Skip any potential ID or system fields that NocoDB manages automatically
|
||||
const systemFields = ['id', 'Id', 'ID', 'CreatedAt', 'UpdatedAt', 'created_at', 'updated_at'];
|
||||
if (systemFields.includes(key)) {
|
||||
console.log(`Skipping system field: ${key}`);
|
||||
return clean;
|
||||
}
|
||||
|
||||
clean[key] = data[key];
|
||||
return clean;
|
||||
}, {});
|
||||
|
||||
console.log(`Creating record in table ${tableId} with data:`, JSON.stringify(cleanData, null, 2));
|
||||
|
||||
const url = this.getTableUrl(tableId);
|
||||
const response = await this.client.post(url, cleanData);
|
||||
console.log(`Record created successfully in table ${tableId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating record:', error);
|
||||
console.error(`Error creating record in table ${tableId}:`, error.message);
|
||||
if (error.response?.data) {
|
||||
console.error('Full error response:', JSON.stringify(error.response.data, null, 2));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +138,24 @@ class NocoDBService {
|
||||
async storeRepresentatives(postalCode, representatives) {
|
||||
try {
|
||||
const stored = [];
|
||||
console.log(`Attempting to store ${representatives.length} representatives for postal code ${postalCode}`);
|
||||
|
||||
// First, clear any existing representatives for this postal code to avoid duplicates
|
||||
try {
|
||||
const existingQuery = await this.getAll(this.tableIds.representatives, {
|
||||
where: `(Postal Code,eq,${postalCode})`
|
||||
});
|
||||
|
||||
if (existingQuery.list && existingQuery.list.length > 0) {
|
||||
console.log(`Found ${existingQuery.list.length} existing representatives for ${postalCode}, using cached data`);
|
||||
return { success: true, count: existingQuery.list.length, cached: true };
|
||||
}
|
||||
} catch (checkError) {
|
||||
console.log('Could not check for existing representatives:', checkError.message);
|
||||
// Continue anyway
|
||||
}
|
||||
|
||||
// Store each representative, handling duplicates gracefully
|
||||
for (const rep of representatives) {
|
||||
const record = {
|
||||
'Postal Code': postalCode,
|
||||
@@ -134,23 +167,33 @@ class NocoDBService {
|
||||
'Representative Set Name': rep.representative_set_name || '',
|
||||
'Profile URL': rep.url || '',
|
||||
'Photo URL': rep.photo_url || '',
|
||||
'Offices': rep.offices ? JSON.stringify(rep.offices) : '[]',
|
||||
'Cached At': new Date().toISOString()
|
||||
};
|
||||
|
||||
const result = await this.create(this.tableIds.representatives, record);
|
||||
stored.push(result);
|
||||
try {
|
||||
const result = await this.create(this.tableIds.representatives, record);
|
||||
stored.push(result);
|
||||
console.log(`Successfully stored representative: ${rep.name}`);
|
||||
} catch (createError) {
|
||||
// Handle any duplicate or constraint errors gracefully
|
||||
if (createError.response?.status === 400) {
|
||||
console.log(`Skipping representative ${rep.name} due to constraint: ${createError.response?.data?.message || createError.message}`);
|
||||
// Continue to next representative without failing
|
||||
} else {
|
||||
console.log(`Error storing representative ${rep.name}:`, createError.message);
|
||||
// For non-400 errors, we might want to continue or fail - let's continue for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Successfully stored ${stored.length} out of ${representatives.length} representatives for ${postalCode}`);
|
||||
return { success: true, count: stored.length };
|
||||
} catch (error) {
|
||||
// If we get a server error, don't throw - just log and return failure
|
||||
if (error.response && error.response.status >= 500) {
|
||||
console.log('NocoDB server unavailable, cannot cache representatives');
|
||||
return { success: false, error: 'Server unavailable' };
|
||||
}
|
||||
|
||||
console.log('Error storing representatives:', error.response?.data?.msg || error.message);
|
||||
return { success: false, error: error.message };
|
||||
} catch (error) {
|
||||
// Catch-all error handler - never let this method throw
|
||||
console.log('Error in storeRepresentatives:', error.response?.data || error.message);
|
||||
return { success: false, error: error.message, count: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +201,25 @@ class NocoDBService {
|
||||
try {
|
||||
// Try to query with the most likely column name
|
||||
const response = await this.getAll(this.tableIds.representatives, {
|
||||
where: `(postal_code,eq,${postalCode})`
|
||||
where: `(Postal Code,eq,${postalCode})`
|
||||
});
|
||||
|
||||
return response.list || [];
|
||||
const cachedRecords = response.list || [];
|
||||
|
||||
// Transform NocoDB format back to API format
|
||||
const transformedRecords = cachedRecords.map(record => ({
|
||||
name: record['Name'],
|
||||
email: record['Email'],
|
||||
district_name: record['District Name'],
|
||||
elected_office: record['Elected Office'],
|
||||
party_name: record['Party Name'],
|
||||
representative_set_name: record['Representative Set Name'],
|
||||
url: record['Profile URL'],
|
||||
photo_url: record['Photo URL'],
|
||||
offices: record['Offices'] ? JSON.parse(record['Offices']) : []
|
||||
}));
|
||||
|
||||
return transformedRecords;
|
||||
} catch (error) {
|
||||
// If we get a 502 or other server error, just return empty array
|
||||
if (error.response && (error.response.status === 502 || error.response.status >= 500)) {
|
||||
@@ -200,6 +258,7 @@ class NocoDBService {
|
||||
'Sender Name': emailData.senderName,
|
||||
'Sender Email': emailData.senderEmail,
|
||||
'Subject': emailData.subject,
|
||||
'Message': emailData.message || '',
|
||||
'Postal Code': emailData.postalCode,
|
||||
'Status': emailData.status,
|
||||
'Sent At': emailData.timestamp,
|
||||
@@ -214,6 +273,41 @@ class NocoDBService {
|
||||
}
|
||||
}
|
||||
|
||||
async logEmailPreview(previewData) {
|
||||
try {
|
||||
// Let NocoDB handle all ID generation - just provide the basic data
|
||||
const record = {
|
||||
'Recipient Email': previewData.recipientEmail,
|
||||
'Sender Name': previewData.senderName,
|
||||
'Sender Email': previewData.senderEmail,
|
||||
'Subject': previewData.subject,
|
||||
'Message': previewData.message || '',
|
||||
'Postal Code': previewData.postalCode,
|
||||
'Status': 'previewed',
|
||||
'Sent At': new Date().toISOString(), // Simple timestamp, let NocoDB handle uniqueness
|
||||
'Sender IP': previewData.senderIP || 'unknown'
|
||||
};
|
||||
|
||||
console.log('Attempting to log email preview...');
|
||||
await this.create(this.tableIds.emails, record);
|
||||
console.log('Email preview logged successfully');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error logging email preview:', error);
|
||||
|
||||
// Check if it's a duplicate record error
|
||||
if (error.response && error.response.data && error.response.data.code === '23505') {
|
||||
console.warn('Duplicate constraint violation - this suggests NocoDB has hidden unique constraints');
|
||||
console.warn('Skipping preview log to avoid breaking the preview functionality');
|
||||
return { success: true, warning: 'Duplicate preview log skipped due to constraint' };
|
||||
}
|
||||
|
||||
// Don't throw error - preview logging is optional and shouldn't break the preview
|
||||
console.warn('Preview logging failed but continuing with preview functionality');
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Check if an email was recently sent to this recipient from this IP
|
||||
async checkRecentEmailSend(senderIP, recipientEmail, windowMinutes = 5) {
|
||||
try {
|
||||
@@ -239,7 +333,7 @@ class NocoDBService {
|
||||
const conditions = [];
|
||||
|
||||
if (filters.postalCode) {
|
||||
conditions.push(`(postal_code,eq,${filters.postalCode})`);
|
||||
conditions.push(`(Postal Code,eq,${filters.postalCode})`);
|
||||
}
|
||||
if (filters.senderEmail) {
|
||||
conditions.push(`(sender_email,eq,${filters.senderEmail})`);
|
||||
@@ -335,6 +429,7 @@ class NocoDBService {
|
||||
'Allow Mailto Link': campaignData.allow_mailto_link,
|
||||
'Collect User Info': campaignData.collect_user_info,
|
||||
'Show Email Count': campaignData.show_email_count,
|
||||
'Allow Email Editing': campaignData.allow_email_editing,
|
||||
'Target Government Levels': campaignData.target_government_levels
|
||||
};
|
||||
|
||||
@@ -361,7 +456,8 @@ class NocoDBService {
|
||||
if (updates.allow_smtp_email !== undefined) mappedUpdates['Allow SMTP Email'] = updates.allow_smtp_email;
|
||||
if (updates.allow_mailto_link !== undefined) mappedUpdates['Allow Mailto Link'] = updates.allow_mailto_link;
|
||||
if (updates.collect_user_info !== undefined) mappedUpdates['Collect User Info'] = updates.collect_user_info;
|
||||
if (updates.show_email_count !== undefined) mappedUpdates['Show Email Count'] = updates.show_email_count;
|
||||
if (updates.show_email_count !== undefined) mappedUpdates['Show Email Count'] = updates.show_email_count;
|
||||
if (updates.allow_email_editing !== undefined) mappedUpdates['Allow Email Editing'] = updates.allow_email_editing;
|
||||
if (updates.target_government_levels !== undefined) mappedUpdates['Target Government Levels'] = updates.target_government_levels;
|
||||
if (updates.updated_at !== undefined) mappedUpdates['UpdatedAt'] = updates.updated_at;
|
||||
|
||||
@@ -406,11 +502,26 @@ class NocoDBService {
|
||||
// Note: 'Sent At' has default value of now() so we don't need to set it
|
||||
};
|
||||
|
||||
const response = await this.create(this.tableIds.campaignEmails, mappedData);
|
||||
return response;
|
||||
try {
|
||||
const response = await this.create(this.tableIds.campaignEmails, mappedData);
|
||||
return response;
|
||||
} catch (createError) {
|
||||
// Handle duplicate record errors gracefully
|
||||
if (createError.response?.status === 400 &&
|
||||
(createError.response?.data?.message?.includes('already exists') ||
|
||||
createError.response?.data?.code === '23505')) {
|
||||
console.log(`Campaign email log already exists for user ${emailData.user_email} and campaign ${emailData.campaign_slug}, skipping...`);
|
||||
// Return a success response to indicate the logging was handled
|
||||
return { success: true, duplicate: true };
|
||||
} else {
|
||||
// Re-throw other errors
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Log campaign email failed:', error);
|
||||
throw error;
|
||||
console.error('Log campaign email failed:', error.response?.data || error.message);
|
||||
// Return a failure response but don't throw - logging should not break the main flow
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user