177 lines
No EOL
5.9 KiB
JavaScript
177 lines
No EOL
5.9 KiB
JavaScript
// Create a new file: src/services/notificationService.js
|
|
|
|
const nodemailer = require('nodemailer');
|
|
const config = require('../config');
|
|
|
|
/**
|
|
* Service for handling notifications including stock alerts
|
|
*/
|
|
const notificationService = {
|
|
/**
|
|
* Create email transporter
|
|
* @returns {Object} Configured nodemailer transporter
|
|
*/
|
|
createTransporter() {
|
|
return nodemailer.createTransport({
|
|
host: config.email.host,
|
|
port: config.email.port,
|
|
auth: {
|
|
user: config.email.user,
|
|
pass: config.email.pass
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Process pending low stock notifications
|
|
* @param {Object} pool - Database connection pool
|
|
* @param {Function} query - Database query function
|
|
* @returns {Promise<number>} Number of notifications processed
|
|
*/
|
|
async processLowStockNotifications(pool, query) {
|
|
const client = await pool.connect();
|
|
let processedCount = 0;
|
|
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
// Get pending low stock notifications
|
|
const pendingNotifications = await client.query(`
|
|
SELECT id
|
|
FROM notification_logs
|
|
WHERE notification_type = 'low_stock_alert'
|
|
AND status = 'pending'
|
|
LIMIT 50
|
|
`);
|
|
|
|
if (pendingNotifications.rows.length === 0) {
|
|
await client.query('COMMIT');
|
|
return 0;
|
|
}
|
|
|
|
// Get products with current stock below threshold
|
|
const lowStockProducts = await client.query(`
|
|
SELECT
|
|
p.id,
|
|
p.name,
|
|
p.stock_quantity,
|
|
p.stock_notification
|
|
FROM products p
|
|
WHERE
|
|
p.stock_notification IS NOT NULL
|
|
AND p.stock_notification->>'enabled' = 'true'
|
|
AND p.stock_notification->>'email' IS NOT NULL
|
|
AND p.stock_quantity <= (p.stock_notification->>'threshold')::int
|
|
`);
|
|
|
|
if (lowStockProducts.rows.length === 0) {
|
|
// Mark notifications as processed with no action
|
|
const notificationIds = pendingNotifications.rows.map(n => n.id);
|
|
await client.query(`
|
|
UPDATE notification_logs
|
|
SET status = 'completed', error_message = 'No eligible products found'
|
|
WHERE id = ANY($1)
|
|
`, [notificationIds]);
|
|
|
|
await client.query('COMMIT');
|
|
return 0;
|
|
}
|
|
|
|
// Initialize email transporter
|
|
const transporter = this.createTransporter();
|
|
|
|
// Send notifications for each low stock product
|
|
console.log(JSON.stringify(lowStockProducts, null, 4))
|
|
for (const product of lowStockProducts.rows) {
|
|
console.log(JSON.stringify(product, null, 4))
|
|
console.log(typeof product.stock_notification)
|
|
const notification = JSON.parse(product.stock_notification);
|
|
|
|
try {
|
|
// Send email notification
|
|
await transporter.sendMail({
|
|
from: config.email.reply,
|
|
to: notification.email,
|
|
subject: `Low Stock Alert: ${product.name}`,
|
|
html: this.generateLowStockEmailTemplate(product)
|
|
});
|
|
|
|
// Mark one notification as processed
|
|
if (pendingNotifications.rows[processedCount]) {
|
|
await client.query(`
|
|
UPDATE notification_logs
|
|
SET status = 'success'
|
|
WHERE id = $1
|
|
`, [pendingNotifications.rows[processedCount].id]);
|
|
|
|
processedCount++;
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error sending low stock notification for product ${product.id}:`, error);
|
|
|
|
// Mark notification as failed
|
|
if (pendingNotifications.rows[processedCount]) {
|
|
await client.query(`
|
|
UPDATE notification_logs
|
|
SET status = 'failed', error_message = $2
|
|
WHERE id = $1
|
|
`, [pendingNotifications.rows[processedCount].id, error.message]);
|
|
|
|
processedCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
return processedCount;
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
console.error('Error processing low stock notifications:', error);
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Generate email template for low stock notification
|
|
* @param {Object} product - Product with low stock
|
|
* @returns {string} HTML email template
|
|
*/
|
|
generateLowStockEmailTemplate(product) {
|
|
const stockNotification = JSON.parse(product.stock_notification);
|
|
const threshold = stockNotification.threshold || 0;
|
|
|
|
return `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<div style="background-color: #f8f8f8; padding: 20px; text-align: center;">
|
|
<h1 style="color: #ff6b6b;">Low Stock Alert</h1>
|
|
</div>
|
|
|
|
<div style="padding: 20px;">
|
|
<p>Hello,</p>
|
|
|
|
<p>This is an automated notification to inform you that the following product is running low on stock:</p>
|
|
|
|
<div style="background-color: #f8f8f8; padding: 15px; margin: 20px 0; border-left: 4px solid #ff6b6b;">
|
|
<h3 style="margin-top: 0;">${product.name}</h3>
|
|
<p><strong>Current Stock:</strong> ${product.stock_quantity}</p>
|
|
<p><strong>Threshold:</strong> ${threshold}</p>
|
|
</div>
|
|
|
|
<p>You might want to restock this item soon to avoid running out of inventory.</p>
|
|
|
|
<div style="margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px;">
|
|
<p>This is an automated notification. You received this because you set up stock notifications for this product.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="background-color: #333; color: white; padding: 15px; text-align: center; font-size: 12px;">
|
|
<p>© ${new Date().getFullYear()} Rocks, Bones & Sticks. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
};
|
|
|
|
module.exports = notificationService; |