const NewsletterSubscription = require("../models/newsletterSubscription"); const emailPattern = /^\S+@\S+\.\S+$/; function cleanString(value) { return String(value || "").trim(); } function normalizeEmail(value) { return cleanString(value).toLowerCase(); } function toSubmissionShape(item) { return { _id: item._id, source: "newsletter", name: item.email, email: item.email, phone: "", pageUrl: item.pageUrl || "", status: item.status, internalNote: item.internalNote || "", ipAddress: item.ipAddress || "", userAgent: item.userAgent || "", payload: { email: item.email, }, createdAt: item.createdAt, updatedAt: item.updatedAt, }; } function buildListQuery(query) { const mongoQuery = {}; const search = cleanString(query.search); const status = cleanString(query.status); if (status && NewsletterSubscription.STATUSES.includes(status)) { mongoQuery.status = status; } if (query.startDate || query.endDate) { mongoQuery.createdAt = {}; if (query.startDate) { mongoQuery.createdAt.$gte = new Date(query.startDate); } if (query.endDate) { const end = new Date(query.endDate); end.setHours(23, 59, 59, 999); mongoQuery.createdAt.$lte = end; } } if (search) { mongoQuery.email = new RegExp( search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i", ); } return mongoQuery; } exports.subscribe = async (req, res) => { try { const email = normalizeEmail(req.body.email); if (!email || !emailPattern.test(email)) { return res.status(400).json({ success: false, error: "Please enter a valid email" }); } const subscription = await NewsletterSubscription.findOneAndUpdate( { email }, { $set: { email, status: "subscribed", pageUrl: cleanString(req.body.pageUrl) || "/", ipAddress: req.ip || req.connection?.remoteAddress || "", userAgent: req.get("User-Agent") || "", }, $setOnInsert: { internalNote: "", }, }, { new: true, upsert: true, runValidators: true, setDefaultsOnInsert: true }, ); return res.status(201).json({ success: true, message: "Subscription received", data: { id: subscription._id, email: subscription.email, status: subscription.status, }, }); } catch (error) { if (error.name === "ValidationError") { const errors = Object.values(error.errors).map((item) => item.message); return res.status(400).json({ success: false, error: errors.join(", ") }); } if (error.code === 11000) { return res.status(409).json({ success: false, error: "Email is already subscribed" }); } console.error("newsletter.subscribe error:", error); return res.status(500).json({ success: false, error: "Error subscribing email" }); } }; exports.list = async (req, res) => { try { const page = Math.max(parseInt(req.query.page, 10) || 1, 1); const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 20, 1), 100); const skip = (page - 1) * limit; const query = buildListQuery(req.query); const [items, total] = await Promise.all([ NewsletterSubscription.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(), NewsletterSubscription.countDocuments(query), ]); return res.json({ success: true, data: items.map(toSubmissionShape), pagination: { page, limit, total, totalPages: Math.ceil(total / limit) || 1, }, }); } catch (error) { console.error("newsletter.list error:", error); return res.status(500).json({ success: false, error: "Error loading subscriptions" }); } }; exports.detail = async (req, res) => { try { const subscription = await NewsletterSubscription.findById(req.params.id).lean(); if (!subscription) { return res.status(404).json({ success: false, error: "Subscription not found" }); } return res.json({ success: true, data: toSubmissionShape(subscription) }); } catch (error) { console.error("newsletter.detail error:", error); return res.status(500).json({ success: false, error: "Error loading subscription" }); } }; exports.update = async (req, res) => { try { const update = {}; const status = cleanString(req.body.status); if (status) { if (!NewsletterSubscription.STATUSES.includes(status)) { return res.status(400).json({ success: false, error: "Invalid status" }); } update.status = status; } if (req.body.internalNote !== undefined) { update.internalNote = cleanString(req.body.internalNote); } const subscription = await NewsletterSubscription.findByIdAndUpdate( req.params.id, update, { new: true, runValidators: true }, ); if (!subscription) { return res.status(404).json({ success: false, error: "Subscription not found" }); } return res.json({ success: true, data: toSubmissionShape(subscription) }); } catch (error) { if (error.name === "ValidationError") { const errors = Object.values(error.errors).map((item) => item.message); return res.status(400).json({ success: false, error: errors.join(", ") }); } console.error("newsletter.update error:", error); return res.status(500).json({ success: false, error: "Error updating subscription" }); } };