Bunch of buag fixes and updates

This commit is contained in:
2025-09-24 12:37:26 -06:00
parent feb2e72c9a
commit f35a1f4be5
33 changed files with 3085 additions and 200 deletions

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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 };
}
}