/** * Newsletter Subscription Model * Manages email subscriptions for blog updates and framework news */ const { ObjectId } = require('mongodb'); const { getCollection } = require('../utils/db.util'); class NewsletterSubscription { /** * Subscribe a new email address */ static async subscribe(data) { const collection = await getCollection('newsletter_subscriptions'); // Check if already subscribed const existing = await collection.findOne({ email: data.email.toLowerCase() }); if (existing) { // If previously unsubscribed, reactivate if (!existing.active) { await collection.updateOne( { _id: existing._id }, { $set: { active: true, resubscribed_at: new Date(), updated_at: new Date() } } ); return { ...existing, active: true }; } // Already subscribed and active return existing; } // Create new subscription const subscription = { email: data.email.toLowerCase(), name: data.name || null, source: data.source || 'blog', // blog/homepage/docs/etc interests: data.interests || [], // e.g., ['framework-updates', 'case-studies', 'research'] verification_token: this._generateToken(), verified: false, // Will be true after email verification active: true, subscribed_at: new Date(), created_at: new Date(), updated_at: new Date(), metadata: { ip_address: data.ip_address || null, user_agent: data.user_agent || null, referrer: data.referrer || null } }; const result = await collection.insertOne(subscription); return { ...subscription, _id: result.insertedId }; } /** * Verify email subscription */ static async verify(token) { const collection = await getCollection('newsletter_subscriptions'); const result = await collection.updateOne( { verification_token: token, active: true }, { $set: { verified: true, verified_at: new Date(), updated_at: new Date() } } ); return result.modifiedCount > 0; } /** * Unsubscribe an email address */ static async unsubscribe(email, token = null) { const collection = await getCollection('newsletter_subscriptions'); const filter = token ? { verification_token: token } : { email: email.toLowerCase() }; const result = await collection.updateOne( filter, { $set: { active: false, unsubscribed_at: new Date(), updated_at: new Date() } } ); return result.modifiedCount > 0; } /** * Find subscription by email */ static async findByEmail(email) { const collection = await getCollection('newsletter_subscriptions'); return await collection.findOne({ email: email.toLowerCase() }); } /** * Find subscription by ID */ static async findById(id) { const collection = await getCollection('newsletter_subscriptions'); return await collection.findOne({ _id: new ObjectId(id) }); } /** * List all subscriptions */ static async list(options = {}) { const collection = await getCollection('newsletter_subscriptions'); const { limit = 100, skip = 0, active = true, verified = null, source = null } = options; const filter = {}; if (active !== null) filter.active = active; if (verified !== null) filter.verified = verified; if (source) filter.source = source; const subscriptions = await collection .find(filter) .sort({ created_at: -1 }) .skip(skip) .limit(limit) .toArray(); return subscriptions; } /** * Count subscriptions */ static async count(filter = {}) { const collection = await getCollection('newsletter_subscriptions'); return await collection.countDocuments(filter); } /** * Update subscription preferences */ static async updatePreferences(email, preferences) { const collection = await getCollection('newsletter_subscriptions'); const result = await collection.updateOne( { email: email.toLowerCase(), active: true }, { $set: { interests: preferences.interests || [], name: preferences.name || null, updated_at: new Date() } } ); return result.modifiedCount > 0; } /** * Get subscription statistics */ static async getStats() { const collection = await getCollection('newsletter_subscriptions'); const [ totalSubscribers, activeSubscribers, verifiedSubscribers, recentSubscribers ] = await Promise.all([ collection.countDocuments({}), collection.countDocuments({ active: true }), collection.countDocuments({ active: true, verified: true }), collection.countDocuments({ active: true, created_at: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } }) ]); // Get source breakdown const sourceBreakdown = await collection.aggregate([ { $match: { active: true } }, { $group: { _id: '$source', count: { $sum: 1 } } } ]).toArray(); return { total: totalSubscribers, active: activeSubscribers, verified: verifiedSubscribers, recent_30_days: recentSubscribers, by_source: sourceBreakdown.reduce((acc, item) => { acc[item._id || 'unknown'] = item.count; return acc; }, {}) }; } /** * Delete subscription (admin only) */ static async delete(id) { const collection = await getCollection('newsletter_subscriptions'); const result = await collection.deleteOne({ _id: new ObjectId(id) }); return result.deletedCount > 0; } /** * Generate verification token */ static _generateToken() { return require('crypto').randomBytes(32).toString('hex'); } } module.exports = NewsletterSubscription;