campaigns.js•14.1 kB
/**
* Campaigns routes
* Handles operations related to Facebook ad campaigns
*/
const express = require('express');
const router = express.Router();
const { protect, checkFacebookToken } = require('../middleware/auth');
const { validate, schemas } = require('../middleware/validator');
const { sendSuccess, sendError, sendPaginated } = require('../utils/responseFormatter');
const { NotFoundError, ValidationError } = require('../utils/errorTypes');
const Campaign = require('../models/campaign');
const AdAccount = require('../models/adAccount');
const facebookApiService = require('../services/facebookApiService');
const analyticsService = require('../services/analyticsService');
const logger = require('../utils/logger');
/**
* @route GET /api/campaigns
* @desc Get all campaigns for the current user
* @access Private
*/
router.get('/', protect, async (req, res, next) => {
try {
// Get pagination parameters
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
// Build query
const query = { userId: req.user._id };
// Filter by ad account if provided
if (req.query.adAccountId) {
query.adAccountId = req.query.adAccountId;
}
// Filter by status if provided
if (req.query.status) {
query.status = req.query.status;
}
// Get campaigns from database
const campaigns = await Campaign.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
// Get total count
const total = await Campaign.countDocuments(query);
return sendPaginated(res, campaigns, page, limit, total, 'Campaigns retrieved successfully');
} catch (error) {
logger.error(`Error retrieving campaigns: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/campaigns/sync
* @desc Sync campaigns from Facebook
* @access Private
*/
router.get('/sync', protect, checkFacebookToken, async (req, res, next) => {
try {
// Get ad account ID from query params
const { adAccountId } = req.query;
if (!adAccountId) {
return next(new ValidationError('Ad account ID is required'));
}
// Check if ad account exists
const adAccount = await AdAccount.findOne({
accountId: adAccountId,
userId: req.user._id
});
if (!adAccount) {
return next(new NotFoundError('Ad account not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Get campaigns from Facebook
const fbCampaigns = await fbApi.getCampaigns(adAccountId);
// Process and save campaigns
const savedCampaigns = [];
for (const fbCampaign of fbCampaigns) {
// Find existing campaign or create new one
let campaign = await Campaign.findOne({
campaignId: fbCampaign.id,
userId: req.user._id
});
if (!campaign) {
campaign = new Campaign({
campaignId: fbCampaign.id,
adAccountId: adAccountId,
userId: req.user._id
});
}
// Update campaign details
campaign.name = fbCampaign.name;
campaign.objective = fbCampaign.objective;
campaign.status = fbCampaign.status;
campaign.specialAdCategories = fbCampaign.special_ad_categories || [];
campaign.spendCap = fbCampaign.spend_cap;
campaign.dailyBudget = fbCampaign.daily_budget;
campaign.lifetimeBudget = fbCampaign.lifetime_budget;
campaign.startTime = fbCampaign.start_time ? new Date(fbCampaign.start_time) : null;
campaign.endTime = fbCampaign.stop_time ? new Date(fbCampaign.stop_time) : null;
campaign.lastSyncedAt = new Date();
// Save campaign
await campaign.save();
savedCampaigns.push(campaign);
}
return sendSuccess(res, savedCampaigns, 'Campaigns synced successfully');
} catch (error) {
logger.error(`Error syncing campaigns: ${error.message}`);
return next(error);
}
});
/**
* @route POST /api/campaigns
* @desc Create a new campaign
* @access Private
*/
router.post('/', protect, checkFacebookToken, validate(schemas.campaign.create), async (req, res, next) => {
try {
// Check if ad account exists
const adAccount = await AdAccount.findOne({
accountId: req.body.adAccountId,
userId: req.user._id
});
if (!adAccount) {
return next(new NotFoundError('Ad account not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Prepare campaign data for Facebook
const campaignData = {
name: req.body.name,
objective: req.body.objective,
status: req.body.status || 'PAUSED',
special_ad_categories: req.body.specialAdCategories || [],
spend_cap: req.body.spendCap,
daily_budget: req.body.dailyBudget,
lifetime_budget: req.body.lifetimeBudget,
start_time: req.body.startTime,
end_time: req.body.endTime
};
// Create campaign on Facebook
const fbResponse = await fbApi.createCampaign(req.body.adAccountId, campaignData);
// Create campaign in database
const campaign = new Campaign({
campaignId: fbResponse.id,
name: req.body.name,
adAccountId: req.body.adAccountId,
userId: req.user._id,
objective: req.body.objective,
status: req.body.status || 'PAUSED',
specialAdCategories: req.body.specialAdCategories || [],
spendCap: req.body.spendCap,
dailyBudget: req.body.dailyBudget,
lifetimeBudget: req.body.lifetimeBudget,
startTime: req.body.startTime,
endTime: req.body.endTime,
lastSyncedAt: new Date()
});
// Save campaign
await campaign.save();
return sendSuccess(res, campaign, 'Campaign created successfully', 201);
} catch (error) {
logger.error(`Error creating campaign: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/campaigns/:id
* @desc Get campaign by ID
* @access Private
*/
router.get('/:id', protect, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get campaign from database
const campaign = await Campaign.findOne({
campaignId: req.params.id,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
return sendSuccess(res, campaign, 'Campaign retrieved successfully');
} catch (error) {
logger.error(`Error retrieving campaign: ${error.message}`);
return next(error);
}
});
/**
* @route PUT /api/campaigns/:id
* @desc Update campaign
* @access Private
*/
router.put('/:id', protect, checkFacebookToken, validate(schemas.idParam, 'params'), validate(schemas.campaign.update), async (req, res, next) => {
try {
// Get campaign from database
const campaign = await Campaign.findOne({
campaignId: req.params.id,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Prepare campaign data for Facebook
const campaignData = {};
if (req.body.name) campaignData.name = req.body.name;
if (req.body.status) campaignData.status = req.body.status;
if (req.body.spendCap !== undefined) campaignData.spend_cap = req.body.spendCap;
if (req.body.dailyBudget !== undefined) campaignData.daily_budget = req.body.dailyBudget;
if (req.body.lifetimeBudget !== undefined) campaignData.lifetime_budget = req.body.lifetimeBudget;
if (req.body.startTime) campaignData.start_time = req.body.startTime;
if (req.body.endTime) campaignData.end_time = req.body.endTime;
// Update campaign on Facebook
await fbApi.updateCampaign(req.params.id, campaignData);
// Update campaign in database
if (req.body.name) campaign.name = req.body.name;
if (req.body.status) campaign.status = req.body.status;
if (req.body.spendCap !== undefined) campaign.spendCap = req.body.spendCap;
if (req.body.dailyBudget !== undefined) campaign.dailyBudget = req.body.dailyBudget;
if (req.body.lifetimeBudget !== undefined) campaign.lifetimeBudget = req.body.lifetimeBudget;
if (req.body.startTime) campaign.startTime = req.body.startTime;
if (req.body.endTime) campaign.endTime = req.body.endTime;
campaign.lastSyncedAt = new Date();
// Save campaign
await campaign.save();
return sendSuccess(res, campaign, 'Campaign updated successfully');
} catch (error) {
logger.error(`Error updating campaign: ${error.message}`);
return next(error);
}
});
/**
* @route DELETE /api/campaigns/:id
* @desc Delete campaign
* @access Private
*/
router.delete('/:id', protect, checkFacebookToken, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get campaign from database
const campaign = await Campaign.findOne({
campaignId: req.params.id,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Delete campaign on Facebook (actually sets status to DELETED)
await fbApi.updateCampaign(req.params.id, { status: 'DELETED' });
// Update campaign status in database
campaign.status = 'DELETED';
await campaign.save();
return sendSuccess(res, { id: req.params.id }, 'Campaign deleted successfully');
} catch (error) {
logger.error(`Error deleting campaign: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/campaigns/:id/insights
* @desc Get insights for a campaign
* @access Private
*/
router.get('/:id/insights', protect, checkFacebookToken, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get campaign from database
const campaign = await Campaign.findOne({
campaignId: req.params.id,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Get time range from query params
const timeRange = req.query.timeRange || 'last_30_days';
// Get insights from Facebook
const insights = await fbApi.getInsights(req.params.id, timeRange, [], {
level: 'campaign'
});
return sendSuccess(res, insights, 'Campaign insights retrieved successfully');
} catch (error) {
logger.error(`Error retrieving campaign insights: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/campaigns/:id/adsets
* @desc Get ad sets for a campaign
* @access Private
*/
router.get('/:id/adsets', protect, checkFacebookToken, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get campaign from database
const campaign = await Campaign.findOne({
campaignId: req.params.id,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Get ad sets from Facebook
const adSets = await fbApi.getAdSets(req.params.id);
return sendSuccess(res, adSets, 'Campaign ad sets retrieved successfully');
} catch (error) {
logger.error(`Error retrieving campaign ad sets: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/campaigns/:id/analytics
* @desc Get analytics for a campaign
* @access Private
*/
router.get('/:id/analytics', protect, validate(schemas.idParam, 'params'), validate(schemas.analytics.query, 'query'), async (req, res, next) => {
try {
// Get campaign from database
const campaign = await Campaign.findOne({
campaignId: req.params.id,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
// Create analytics service
const analytics = analyticsService.createForUser(req.user);
// Get start and end dates from query params
const startDate = new Date(req.query.startDate);
const endDate = new Date(req.query.endDate);
// Get analytics data
const analyticsData = await analytics.getCampaignAnalytics(req.params.id, startDate, endDate);
return sendSuccess(res, analyticsData, 'Campaign analytics retrieved successfully');
} catch (error) {
logger.error(`Error retrieving campaign analytics: ${error.message}`);
return next(error);
}
});
/**
* @route POST /api/campaigns/:id/fetch-analytics
* @desc Fetch and store analytics for a campaign
* @access Private
*/
router.post('/:id/fetch-analytics', protect, checkFacebookToken, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get campaign from database
const campaign = await Campaign.findOne({
campaignId: req.params.id,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
// Create analytics service
const analytics = analyticsService.createForUser(req.user);
// Get time range from query params
const timeRange = req.query.timeRange || 'last_30_days';
// Fetch and store analytics data
const analyticsData = await analytics.fetchCampaignAnalytics(req.params.id, timeRange);
return sendSuccess(res, analyticsData, 'Campaign analytics fetched successfully');
} catch (error) {
logger.error(`Error fetching campaign analytics: ${error.message}`);
return next(error);
}
});
module.exports = router;