ads.js•15.7 kB
/**
* Ads routes
* Handles operations related to Facebook ads
*/
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 Ad = require('../models/ad');
const AdSet = require('../models/adSet');
const Campaign = require('../models/campaign');
const facebookApiService = require('../services/facebookApiService');
const analyticsService = require('../services/analyticsService');
const logger = require('../utils/logger');
/**
* @route GET /api/ads
* @desc Get all ads 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 set if provided
if (req.query.adSetId) {
query.adSetId = req.query.adSetId;
}
// Filter by status if provided
if (req.query.status) {
query.status = req.query.status;
}
// Filter by creative type if provided
if (req.query.creativeType) {
query['creative.type'] = req.query.creativeType;
}
// Get ads from database
const ads = await Ad.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
// Get total count
const total = await Ad.countDocuments(query);
return sendPaginated(res, ads, page, limit, total, 'Ads retrieved successfully');
} catch (error) {
logger.error(`Error retrieving ads: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/ads/sync
* @desc Sync ads from Facebook
* @access Private
*/
router.get('/sync', protect, checkFacebookToken, async (req, res, next) => {
try {
// Get ad set ID from query params
const { adSetId } = req.query;
if (!adSetId) {
return next(new ValidationError('Ad set ID is required'));
}
// Check if ad set exists
const adSet = await AdSet.findOne({
adSetId,
userId: req.user._id
});
if (!adSet) {
return next(new NotFoundError('Ad set not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Get ads from Facebook
const fbAds = await fbApi.getAds(adSetId);
// Process and save ads
const savedAds = [];
for (const fbAd of fbAds) {
// Find existing ad or create new one
let ad = await Ad.findOne({
adId: fbAd.id,
userId: req.user._id
});
if (!ad) {
ad = new Ad({
adId: fbAd.id,
adSetId,
userId: req.user._id
});
}
// Update ad details
ad.name = fbAd.name;
ad.status = fbAd.status;
ad.creativeId = fbAd.creative?.id;
// Process creative details
if (fbAd.creative) {
// Determine creative type
let creativeType = 'IMAGE';
if (fbAd.creative.video_id) {
creativeType = 'VIDEO';
} else if (fbAd.creative.product_set_id) {
creativeType = 'CAROUSEL';
}
ad.creative = {
type: creativeType,
title: fbAd.creative.title,
body: fbAd.creative.body,
callToAction: fbAd.creative.call_to_action_type,
url: fbAd.creative.url_tags,
imageUrl: fbAd.creative.image_url,
videoUrl: fbAd.creative.video_id ? `https://www.facebook.com/ads/archive/render_ad/?id=${fbAd.creative.video_id}` : null
};
// Process carousel items if available
if (fbAd.creative.product_set_id) {
ad.creative.carouselItems = fbAd.creative.child_attachments || [];
}
}
ad.previewUrl = fbAd.preview_url;
ad.lastSyncedAt = new Date();
// Save ad
await ad.save();
savedAds.push(ad);
}
return sendSuccess(res, savedAds, 'Ads synced successfully');
} catch (error) {
logger.error(`Error syncing ads: ${error.message}`);
return next(error);
}
});
/**
* @route POST /api/ads
* @desc Create a new ad
* @access Private
*/
router.post('/', protect, checkFacebookToken, validate(schemas.ad.create), async (req, res, next) => {
try {
// Check if ad set exists
const adSet = await AdSet.findOne({
adSetId: req.body.adSetId,
userId: req.user._id
});
if (!adSet) {
return next(new NotFoundError('Ad set not found'));
}
// Get campaign for ad account ID
const campaign = await Campaign.findOne({
campaignId: adSet.campaignId,
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 ad data for Facebook
const adData = {
name: req.body.name,
adset_id: req.body.adSetId,
status: req.body.status || 'PAUSED'
};
// Handle creative
if (req.body.creativeId) {
adData.creative_id = req.body.creativeId;
} else if (req.body.creative) {
// Create creative based on type
const creative = req.body.creative;
// Prepare creative data
const creativeData = {
name: `Creative for ${req.body.name}`,
title: creative.title,
body: creative.body,
call_to_action_type: creative.callToAction,
url_tags: creative.url,
object_story_spec: {}
};
// Add type-specific data
switch (creative.type) {
case 'IMAGE':
creativeData.image_url = creative.imageUrl;
break;
case 'VIDEO':
creativeData.video_id = creative.videoUrl;
break;
case 'CAROUSEL':
creativeData.product_set_id = creative.productSetId;
creativeData.child_attachments = creative.carouselItems;
break;
}
adData.creative = creativeData;
}
// Create ad on Facebook
const fbResponse = await fbApi.createAd(campaign.adAccountId, adData);
// Create ad in database
const ad = new Ad({
adId: fbResponse.id,
name: req.body.name,
adSetId: req.body.adSetId,
userId: req.user._id,
status: req.body.status || 'PAUSED',
creativeId: req.body.creativeId || fbResponse.creative_id,
creative: req.body.creative,
lastSyncedAt: new Date()
});
// Save ad
await ad.save();
return sendSuccess(res, ad, 'Ad created successfully', 201);
} catch (error) {
logger.error(`Error creating ad: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/ads/:id
* @desc Get ad by ID
* @access Private
*/
router.get('/:id', protect, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad not found'));
}
return sendSuccess(res, ad, 'Ad retrieved successfully');
} catch (error) {
logger.error(`Error retrieving ad: ${error.message}`);
return next(error);
}
});
/**
* @route PUT /api/ads/:id
* @desc Update ad
* @access Private
*/
router.put('/:id', protect, checkFacebookToken, validate(schemas.idParam, 'params'), validate(schemas.ad.update), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Prepare ad data for Facebook
const adData = {};
if (req.body.name) adData.name = req.body.name;
if (req.body.status) adData.status = req.body.status;
if (req.body.creativeId) adData.creative_id = req.body.creativeId;
// Update ad on Facebook
await fbApi.updateAd(req.params.id, adData);
// Update ad in database
if (req.body.name) ad.name = req.body.name;
if (req.body.status) ad.status = req.body.status;
if (req.body.creativeId) ad.creativeId = req.body.creativeId;
if (req.body.creative) ad.creative = req.body.creative;
ad.lastSyncedAt = new Date();
// Save ad
await ad.save();
return sendSuccess(res, ad, 'Ad updated successfully');
} catch (error) {
logger.error(`Error updating ad: ${error.message}`);
return next(error);
}
});
/**
* @route DELETE /api/ads/:id
* @desc Delete ad
* @access Private
*/
router.delete('/:id', protect, checkFacebookToken, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad not found'));
}
// Create Facebook API service
const fbApi = await facebookApiService.createForUser(req.user);
// Delete ad on Facebook (actually sets status to DELETED)
await fbApi.updateAd(req.params.id, { status: 'DELETED' });
// Update ad status in database
ad.status = 'DELETED';
await ad.save();
return sendSuccess(res, { id: req.params.id }, 'Ad deleted successfully');
} catch (error) {
logger.error(`Error deleting ad: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/ads/:id/insights
* @desc Get insights for an ad
* @access Private
*/
router.get('/:id/insights', protect, checkFacebookToken, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad 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: 'ad'
});
return sendSuccess(res, insights, 'Ad insights retrieved successfully');
} catch (error) {
logger.error(`Error retrieving ad insights: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/ads/:id/analytics
* @desc Get analytics for an ad
* @access Private
*/
router.get('/:id/analytics', protect, validate(schemas.idParam, 'params'), validate(schemas.analytics.query, 'query'), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad 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.getAdAnalytics(req.params.id, startDate, endDate);
return sendSuccess(res, analyticsData, 'Ad analytics retrieved successfully');
} catch (error) {
logger.error(`Error retrieving ad analytics: ${error.message}`);
return next(error);
}
});
/**
* @route POST /api/ads/:id/fetch-analytics
* @desc Fetch and store analytics for an ad
* @access Private
*/
router.post('/:id/fetch-analytics', protect, checkFacebookToken, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad 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.fetchAdAnalytics(req.params.id, timeRange);
return sendSuccess(res, analyticsData, 'Ad analytics fetched successfully');
} catch (error) {
logger.error(`Error fetching ad analytics: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/ads/:id/creative-recommendations
* @desc Get creative recommendations for an ad
* @access Private
*/
router.get('/:id/creative-recommendations', protect, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad not found'));
}
// Get ad set to get ad account ID
const adSet = await AdSet.findOne({
adSetId: ad.adSetId,
userId: req.user._id
});
if (!adSet) {
return next(new NotFoundError('Ad set not found'));
}
// Get campaign to get ad account ID
const campaign = await Campaign.findOne({
campaignId: adSet.campaignId,
userId: req.user._id
});
if (!campaign) {
return next(new NotFoundError('Campaign not found'));
}
// Create recommendation service
const recommendationService = require('../services/recommendationService').createForUser(req.user);
// Get creative recommendations
const recommendations = await recommendationService.getCreativeRecommendations(campaign.adAccountId);
// Filter recommendations for this ad
const adRecommendations = recommendations.find(rec => rec.adId === ad.adId);
if (!adRecommendations) {
return sendSuccess(res, {
adId: ad.adId,
adName: ad.name,
recommendations: []
}, 'No creative recommendations available for this ad');
}
return sendSuccess(res, adRecommendations, 'Ad creative recommendations retrieved successfully');
} catch (error) {
logger.error(`Error retrieving ad creative recommendations: ${error.message}`);
return next(error);
}
});
/**
* @route GET /api/ads/:id/preview
* @desc Get preview URL for an ad
* @access Private
*/
router.get('/:id/preview', protect, validate(schemas.idParam, 'params'), async (req, res, next) => {
try {
// Get ad from database
const ad = await Ad.findOne({
adId: req.params.id,
userId: req.user._id
});
if (!ad) {
return next(new NotFoundError('Ad not found'));
}
// Check if preview URL exists
if (!ad.previewUrl) {
return next(new NotFoundError('Preview URL not available for this ad'));
}
return sendSuccess(res, { previewUrl: ad.previewUrl }, 'Ad preview URL retrieved successfully');
} catch (error) {
logger.error(`Error retrieving ad preview URL: ${error.message}`);
return next(error);
}
});
module.exports = router;