Item request notifications
This commit is contained in:
@@ -63,6 +63,7 @@ class TemplateManager {
|
||||
"forumAnswerAcceptedToCommentAuthor.html",
|
||||
"forumThreadActivityToParticipant.html",
|
||||
"forumPostClosed.html",
|
||||
"forumItemRequestNotification.html",
|
||||
];
|
||||
|
||||
for (const templateFile of templateFiles) {
|
||||
|
||||
@@ -8,6 +8,7 @@ const TemplateManager = require("../core/TemplateManager");
|
||||
* - Sending reply notifications to comment authors
|
||||
* - Sending answer accepted notifications
|
||||
* - Sending thread activity notifications to participants
|
||||
* - Sending location-based item request notifications to nearby users
|
||||
*/
|
||||
class ForumEmailService {
|
||||
constructor() {
|
||||
@@ -384,6 +385,66 @@ class ForumEmailService {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to nearby users about an item request
|
||||
* @param {Object} recipient - Recipient user object
|
||||
* @param {string} recipient.firstName - Recipient's first name
|
||||
* @param {string} recipient.email - Recipient's email
|
||||
* @param {Object} requester - User who posted the item request
|
||||
* @param {string} requester.firstName - Requester's first name
|
||||
* @param {string} requester.lastName - Requester's last name
|
||||
* @param {Object} post - Forum post object (item request)
|
||||
* @param {number} post.id - Post ID
|
||||
* @param {string} post.title - Item being requested
|
||||
* @param {string} post.content - Request description
|
||||
* @param {string|number} distance - Distance from recipient to request location (in miles)
|
||||
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||
*/
|
||||
async sendItemRequestNotification(recipient, requester, post, distance) {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
|
||||
|
||||
const variables = {
|
||||
recipientName: recipient.firstName || "there",
|
||||
requesterName:
|
||||
`${requester.firstName} ${requester.lastName}`.trim() || "Someone",
|
||||
itemRequested: post.title,
|
||||
requestDescription: post.content,
|
||||
postUrl: postUrl,
|
||||
distance: distance,
|
||||
};
|
||||
|
||||
const htmlContent = await this.templateManager.renderTemplate(
|
||||
"forumItemRequestNotification",
|
||||
variables
|
||||
);
|
||||
|
||||
const subject = `Someone nearby is looking for: ${post.title}`;
|
||||
|
||||
const result = await this.emailClient.sendEmail(
|
||||
recipient.email,
|
||||
subject,
|
||||
htmlContent
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log(
|
||||
`Item request notification email sent to ${recipient.email}`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Failed to send item request notification email:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ForumEmailService;
|
||||
|
||||
112
backend/services/locationService.js
Normal file
112
backend/services/locationService.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const { sequelize } = require('../models');
|
||||
const { QueryTypes } = require('sequelize');
|
||||
|
||||
class LocationService {
|
||||
/**
|
||||
* Find users within a specified radius of coordinates
|
||||
* Uses the Haversine formula to calculate great-circle distance between two points
|
||||
*
|
||||
* @param {number} latitude - Center point latitude
|
||||
* @param {number} longitude - Center point longitude
|
||||
* @param {number} radiusMiles - Search radius in miles (default: 10)
|
||||
* @returns {Promise<Array>} Array of users with their distance from the center point
|
||||
*/
|
||||
async findUsersInRadius(latitude, longitude, radiusMiles = 10) {
|
||||
if (!latitude || !longitude) {
|
||||
throw new Error('Latitude and longitude are required');
|
||||
}
|
||||
|
||||
if (radiusMiles <= 0 || radiusMiles > 100) {
|
||||
throw new Error('Radius must be between 1 and 100 miles');
|
||||
}
|
||||
|
||||
try {
|
||||
// Haversine formula:
|
||||
// distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2))
|
||||
// * cos(radians(lng2) - radians(lng1))
|
||||
// + sin(radians(lat1)) * sin(radians(lat2)))
|
||||
// Note: 3959 is Earth's radius in miles
|
||||
const query = `
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u."firstName",
|
||||
u."lastName",
|
||||
ua.latitude,
|
||||
ua.longitude,
|
||||
(3959 * acos(
|
||||
LEAST(1.0,
|
||||
cos(radians(:lat)) * cos(radians(ua.latitude))
|
||||
* cos(radians(ua.longitude) - radians(:lng))
|
||||
+ sin(radians(:lat)) * sin(radians(ua.latitude))
|
||||
)
|
||||
)) AS distance
|
||||
FROM "Users" u
|
||||
INNER JOIN "UserAddresses" ua ON u.id = ua."userId"
|
||||
WHERE ua."isPrimary" = true
|
||||
AND ua.latitude IS NOT NULL
|
||||
AND ua.longitude IS NOT NULL
|
||||
HAVING distance < :radiusMiles
|
||||
ORDER BY distance ASC
|
||||
`;
|
||||
|
||||
const users = await sequelize.query(query, {
|
||||
replacements: {
|
||||
lat: parseFloat(latitude),
|
||||
lng: parseFloat(longitude),
|
||||
radiusMiles: parseFloat(radiusMiles)
|
||||
},
|
||||
type: QueryTypes.SELECT
|
||||
});
|
||||
|
||||
return users.map(user => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
latitude: parseFloat(user.latitude),
|
||||
longitude: parseFloat(user.longitude),
|
||||
distance: parseFloat(user.distance).toFixed(2) // Round to 2 decimal places
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error finding users in radius:', error);
|
||||
throw new Error(`Failed to find users in radius: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points using Haversine formula
|
||||
*
|
||||
* @param {number} lat1 - First point latitude
|
||||
* @param {number} lon1 - First point longitude
|
||||
* @param {number} lat2 - Second point latitude
|
||||
* @param {number} lon2 - Second point longitude
|
||||
* @returns {number} Distance in miles
|
||||
*/
|
||||
calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 3959; // Earth's radius in miles
|
||||
const dLat = this.toRadians(lat2 - lat1);
|
||||
const dLon = this.toRadians(lon2 - lon1);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const distance = R * c;
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees to radians
|
||||
* @param {number} degrees
|
||||
* @returns {number} Radians
|
||||
*/
|
||||
toRadians(degrees) {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LocationService();
|
||||
Reference in New Issue
Block a user