@speakr/speakr-module-services
Version:
SPEAKR Shared Service Module
762 lines (703 loc) • 24 kB
JavaScript
;
const _ = require("underscore");
const moment = require("moment");
module.exports = {
publicData,
sortByCreated,
typeaheadPublicData,
reportData
};
function publicData(options) {
// 1. Get the campaign base object
let campaign = options.campaigns;
let platforms = options.platforms;
let networks = options.networks;
let agencies = options.agencies;
let contacts = options.contacts;
let brands = options.brands;
let initiatives = options.initiatives;
// 2. Delete the options campaign
delete options.campaign;
// 3. Reset the options campaign
options.campaign = {
id: campaign.id,
id_hash: campaign.id_hash,
title: campaign.title,
created_at: campaign.created_at,
start_date: campaign.start_date,
end_date: campaign.end_date,
description: campaign.description,
budget: campaign.budget,
impressions: campaign.impressions || 0,
cpm: campaign.cpm,
added_value: campaign.added_value,
product_price: campaign.product_price,
total_cost: campaign.total_cost,
program_value: campaign.program_value,
estimated_engagements: campaign.estimated_engagements || 0,
inventory_types: campaign.inventory_types,
other_interests: campaign.other_interests,
rating: campaign.rating,
categories: campaign.categories,
tones: campaign.tones,
ethnicities: campaign.ethnicities,
age: campaign.age,
gender: campaign.gender,
countries: campaign.countries,
states: campaign.states,
cities: campaign.cities,
audience_ethnicities: campaign.audience_ethnicities,
audience_age: campaign.audience_age,
audience_gender: campaign.audience_gender,
audience_countries: campaign.audience_countries,
audience_states: campaign.audience_states,
audience_cities: campaign.audience_cities,
audience_interests: campaign.audience_interests,
account_categories: campaign.account_categories,
approval_requirements: campaign.approval_requirements,
hashtags: campaign.hashtags,
keywords_to_avoid: campaign.keywords_to_avoid,
platforms: platforms,
networks: networks,
agencies: agencies,
contacts: contacts,
brands: brands,
initiatives: initiatives
};
}
function sortByCreated(options) {
options.campaigns = _.sortBy(options.campaigns, "created_at");
return Promise.resolve(options);
}
function typeaheadPublicData(options) {
return Promise.resolve(
options.campaigns.map(campaign => {
let brand_name;
let agency_name;
options.brands.forEach(brand => {
if (brand.id === campaign.brandname_id) {
brand_name = brand.name;
}
});
options.agencies.forEach(agency => {
if (agency.id === campaign.agency_id) {
agency_name = agency.name;
}
});
return {
id_hash: campaign.id_hash,
name: campaign.title,
brand: brand_name,
agency: agency_name,
network_id_hash: campaign.network.id_hash
};
})
);
}
function reportData(options) {
// array of columns that need null values replaced with zeros
const engagementsColumns = [
"facebook_shares",
"facebook_comments",
"facebook_likes",
"facebook_video_views",
"instagram_comments",
"instagram_likes",
"instagram_video_views",
"twitter_comment_count",
"twitter_likes_count",
"twitter_retweet_count",
"twitter_video_views",
"youtube_comments",
"youtube_likes",
"youtube_video_views",
"youtube_shares"
];
try {
const campaign = options.campaigns;
const platforms = options.platforms;
const networks = options.networks;
const originalStats = options.tracking_stats ?
options.tracking_stats
.filter(stat => stat)
.map(stat => (stat.dataValues ? stat.dataValues : stat)) :
[];
const creatives = options.creatives ? options.creatives : [];
const initiatives = campaign.initiatives ? campaign.initiatives : [];
const customStats = {
engagements: campaign.campaign_reports_customs ?
campaign.campaign_reports_customs.filter(
customStat => customStat.category === "engagements"
) :
[],
impressions: campaign.campaign_reports_customs ?
campaign.campaign_reports_customs.filter(
customStat => customStat.category === "impressions"
) :
[],
totalEngagements: campaign.campaign_reports_customs ?
campaign.campaign_reports_customs
.filter(customStat => customStat.category === "engagements")
.reduce((sum, next) => {
return Number(sum) + Number(next.value);
}, 0) :
[],
totalImpressions: campaign.campaign_reports_customs ?
campaign.campaign_reports_customs
.filter(customStat => customStat.category === "impressions")
.reduce((sum, next) => {
return Number(sum) + Number(next.value);
}, 0) :
[]
};
// create arrays to push customStats into
customStats.engagements.platform = [];
customStats.engagements.product = [];
customStats.engagements.media_type = [];
customStats.engagements.initiative = [];
customStats.engagements.engagement_type = [];
customStats.impressions.platform = [];
customStats.impressions.product = [];
customStats.impressions.media_type = [];
customStats.impressions.initiative = [];
customStats.engagements.forEach(customStat => {
switch (customStat.type) {
case "platform":
return customStats.engagements.platform.push(customStat);
case "product":
return customStats.engagements.product.push(customStat);
case "media_type":
return customStats.engagements.media_type.push(customStat);
case "initiative":
return customStats.engagements.initiative.push(customStat);
case "engagement_type":
return customStats.engagements.engagement_type.push(customStat);
}
});
customStats.impressions.forEach(customStat => {
switch (customStat.type) {
case "platform":
return customStats.impressions.platform.push(customStat);
case "product":
return customStats.impressions.product.push(customStat);
case "media_type":
return customStats.impressions.media_type.push(customStat);
case "initiative":
return customStats.impressions.initiative.push(customStat);
}
});
const stats = originalStats.map(stat => {
// convert null data to zeros
Object.keys(stat).forEach(key => {
if (engagementsColumns.includes(key)) {
stat[key] = stat[key] !== null ? stat[key] : 0;
}
});
// add content types to stats
creatives.forEach(creative => {
let init = creative.initiative ? creative.initiative : {};
if (creative.id_hash === stat.creative_id_hash) {
stat.content_type = creative.content_type ?
creative.content_type :
null;
stat.product_type = init.product_type ? init.product_type : null;
if (!stat.content_type) {
if (creative.content_type === "image" || creative.image_link)
stat.content_type = "image";
if (creative.content_type === "video" || creative.video_link)
stat.content_type = "video";
if (
creative.content_type === "text only" ||
(!creative.image_link && !creative.video_link)
)
stat.content_type = "text only";
}
return stat;
}
});
return stat;
}); // end stat .map()
const initsByProduct = {
content_dist: initiatives.filter(
init => init.product.toLowerCase() === "content distribution"
),
content_creation: initiatives.filter(
init => init.product.toLowerCase() === "content creation"
),
experiential: initiatives.filter(
init => init.product.toLowerCase() === "experiential"
),
brand_in_hand: initiatives.filter(
init => init.product.toLowerCase() === "brand_in_hand"
)
};
const initIdsByProduct = {
content_dist: initsByProduct.content_dist.map(init => init.id),
content_creation: initsByProduct.content_creation.map(init => init.id),
experiential: initsByProduct.experiential.map(init => init.id),
brand_in_hand: initsByProduct.brand_in_hand.map(init => init.id)
};
const creativeIdHashesByInitProductType = {
content_dist: creatives
.filter(creative =>
initIdsByProduct.content_dist.includes(creative.initiative_id)
)
.map(c => c.id_hash),
content_creation: creatives
.filter(creative =>
initIdsByProduct.content_creation.includes(creative.initiative_id)
)
.map(c => c.id_hash),
experiential: creatives
.filter(creative =>
initIdsByProduct.experiential.includes(creative.initiative_id)
)
.map(c => c.id_hash),
brand_in_hand: creatives
.filter(creative =>
initIdsByProduct.brand_in_hand.includes(creative.initiative_id)
)
.map(c => c.id_hash)
};
const statsByInitProductType = {
content_dist: stats.filter(stat =>
creativeIdHashesByInitProductType.content_dist.includes(
stat.creative_id_hash
)
),
content_creation: stats.filter(stat =>
creativeIdHashesByInitProductType.content_creation.includes(
stat.creative_id_hash
)
),
experiential: stats.filter(stat =>
creativeIdHashesByInitProductType.experiential.includes(
stat.creative_id_hash
)
),
brand_in_hand: stats.filter(stat =>
creativeIdHashesByInitProductType.brand_in_hand.includes(
stat.creative_id_hash
)
)
};
const impressionsByProductType = {
"Content Distribution": sumFollowers(
statsByInitProductType.content_dist,
"sum"
),
"Content Creation": sumFollowers(
statsByInitProductType.content_creation,
"sum"
),
Experiential: sumFollowers(statsByInitProductType.experiential, "sum")
};
const engagementsByProductType = {
"Content Distribution": sumEngagements(
statsByInitProductType.content_dist
),
"Content Creation": sumEngagements(
statsByInitProductType.content_creation
),
Experiential: sumEngagements(statsByInitProductType.experiential)
};
let impressionsByInitiative = {};
let engagementsByInitiative = {};
initiatives.forEach(init => {
impressionsByInitiative[init.name] = {};
engagementsByInitiative[init.name] = {};
});
const statsGroupedByInitId = _.groupBy(stats, stat => stat.initiative_id);
let initIdToNameDictionary = {};
let totalImpressionsByInitId = {};
let totalEngagementssByInitId = {};
Object.keys(statsGroupedByInitId).forEach(key => {
// create dictionary to match init_id to init_nam
initIdToNameDictionary[key] = initiatives
.filter(init => init.id === Number(key))
.map(init => init.name);
// sum the impressions for this init (followers)
totalImpressionsByInitId[key] =
statsGroupedByInitId[key].length > 0 ?
statsGroupedByInitId[key].reduce((sum, next) => {
return Number(sum) + Number(next.followers);
}, 0) :
[];
// sum engagements for this init
totalEngagementssByInitId[key] = sumEngagementsColumns(
statsGroupedByInitId[key]
);
});
Object.keys(totalImpressionsByInitId).forEach(key => {
const initName = initIdToNameDictionary[key];
impressionsByInitiative[initName] = totalImpressionsByInitId[key];
engagementsByInitiative[initName] = totalEngagementssByInitId[key];
});
Object.keys(impressionsByInitiative).forEach(key => {
// replace empty objects with value of zero
impressionsByInitiative[key] =
typeof impressionsByInitiative[key] === "object" ?
0 :
Number(impressionsByInitiative[key]);
});
Object.keys(engagementsByInitiative).forEach(key => {
// replace empty objects with value of zero
engagementsByInitiative[key] =
typeof engagementsByInitiative[key] === "object" ?
0 :
Number(engagementsByInitiative[key]);
});
const cpm =
campaign.budget && campaign.impressions ?
(campaign.budget / campaign.impressions * 1000).toFixed(2) :
0;
const estimated_cpe =
campaign.budget && campaign.estimated_engagements ?
(campaign.budget / campaign.estimated_engagements).toFixed(2) :
0;
const estimated_cpm = campaign.budget / campaign.impressions * 1000;
const added_value =
campaign.program_value && campaign.budget ?
campaign.program_value - campaign.budget :
0;
const impressions_delivered = sumFollowers(stats, "sum");
const engagements_delivered = sumEngagements(stats);
const effective_cpm =
campaign.budget /
(impressions_delivered + customStats.totalImpressions) *
1000; // campaign_cost / impressions_delivered * 100
const effective_cpe =
campaign.budget / (engagements_delivered + customStats.totalEngagements); // campaign_cost / engagements_delivered
const engagement_rate =
(engagements_delivered + customStats.totalEngagements) /
(impressions_delivered + customStats.totalImpressions) *
100;
const impressions_overage =
(impressions_delivered +
customStats.totalImpressions -
campaign.impressions) /
campaign.impressions *
100;
const engagements_overage =
(engagements_delivered +
customStats.totalEngagements -
campaign.estimated_engagements) /
campaign.estimated_engagements *
100;
const impressions_savings =
(estimated_cpm - effective_cpm) / estimated_cpm * 100;
const engagements_savings =
(estimated_cpe - effective_cpe) / estimated_cpe * 100;
const initiative_first_start = moment.min(
initiatives.map(init => moment(init.start_date, "MM/DD/YYYY"))
);
const initiative_last_end = moment.max(
initiatives.map(init => moment(init.end_date, "MM/DD/YYYY"))
);
options.campaigns = {
id: campaign.id,
id_hash: campaign.id_hash,
title: campaign.title,
created_at: campaign.created_at,
status: campaign.status,
start_date: campaign.start_date ? campaign.start_date : new Date(),
end_date: campaign.end_date ? campaign.end_date : new Date(),
description: campaign.description,
budget: campaign.budget,
impressions: campaign.impressions,
product_price: campaign.product_price,
total_cost: campaign.total_cost,
program_value: campaign.program_value,
estimated_engagements: campaign.estimated_engagements,
report_url: campaign.report_url,
platforms: platforms,
networks: networks,
initiatives: initiatives,
cpm: cpm || 0,
estimated_cpe: estimated_cpe || 0,
estimated_cpm: estimated_cpm || 0,
added_value: added_value || 0,
stats: stats,
custom_stats: customStats,
impressions_delivered: impressions_delivered ?
impressions_delivered + customStats.totalImpressions :
customStats.totalImpressions,
engagements_delivered: engagements_delivered ?
engagements_delivered + customStats.totalEngagements :
customStats.totalEngagements,
effective_cpm: effective_cpm || 0,
effective_cpe: effective_cpe || 0,
engagement_rate: engagement_rate || 0,
creatives: creatives,
impressions_platform_data: Object.assign({},
sumFollowers(stats, "platform"), {
customStats: customStats.impressions.platform
}
),
impressions_product_data: Object.assign({}, impressionsByProductType, {
customStats: customStats.impressions.product
}),
impressions_media_type_data: Object.assign({},
sumFollowers(stats, "media"), {
customStats: customStats.impressions.media_type
}
),
impressions_initiative_data: Object.assign({}, impressionsByInitiative, {
customStats: customStats.impressions.initiative
}),
engagements_platform_data: Object.assign({},
sumEngagements(stats, "platform"), {
customStats: customStats.engagements.platform
}
),
engagements_product_data: Object.assign({}, engagementsByProductType, {
customStats: customStats.engagements.product
}),
engagements_media_type_data: Object.assign({},
sumEngagements(stats, "media"), {
customStats: customStats.engagements.media_type
}
),
engagements_initiative_data: Object.assign({}, engagementsByInitiative, {
customStats: customStats.engagements.initiative
}),
engagements_engagement_type_data: Object.assign({},
sumEngagementsByType(stats), {
customStats: customStats.engagements.engagement_type
}
),
impressions_overage: impressions_overage || 0,
engagements_overage: engagements_overage || 0,
impressions_savings: impressions_savings || 0,
engagements_savings: engagements_savings || 0,
initiative_first_start: initiative_first_start,
initiative_last_end: initiative_last_end
};
return Promise.resolve(options);
} catch (error) {
console.log("\n\n\nError formating report data.", error);
return Promise.reject(error);
}
}
function sumFollowers(originalArray = [], type) {
const arr = originalArray.map(item => {
return item.dataValues ? item.dataValues : item;
});
if (type === "sum") {
return arr.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
} else if (type === "platform") {
const facebook = arr.filter(item => {
return item.platform.toLowerCase() === "facebook";
});
const instagram = arr.filter(
item => item.platform.toLowerCase() === "instagram"
);
const twitter = arr.filter(
item => item.platform.toLowerCase() === "twitter"
);
const youtube = arr.filter(
item => item.platform.toLowerCase() === "youtube"
);
const facebookTotal = facebook.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
const instagramTotal = instagram.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
const twitterTotal = twitter.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
const youtubeTotal = youtube.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
const platformTotals = {
facebook: facebookTotal,
instagram: instagramTotal,
twitter: twitterTotal,
youtube: youtubeTotal
};
return platformTotals;
} else if (type === "media") {
let text = [];
let image = [];
let video = [];
arr.forEach(item => {
if (
item.content_type === "image" ||
(!item.video_link && item.image_link)
) {
image.push(item);
} else if (
item.content_type === "video" ||
(item.video_link && !item.image_link)
) {
video.push(item);
} else {
text.push(item);
}
});
const textTotal = text.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
const imageTotal = image.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
const videoTotal = video.reduce((sum, item) => {
return sum + Number(item.followers);
}, 0);
const mediaTypeTotals = {
text: textTotal,
image: imageTotal,
video: videoTotal
};
return mediaTypeTotals;
}
}
function sumEngagementsByType(arr = []) {
let byType = {
"RTs/Shares": 0,
Likes: 0,
Comments: 0,
Views: 0,
Clicks: 0
};
if (arr.length > 0) {
arr.forEach(item => {
byType["RTs/Shares"] +=
item.facebook_shares +
item.twitter_retweet_count +
Number(item.youtube_shares);
byType.Likes +=
item.facebook_likes +
item.instagram_likes +
item.twitter_likes_count +
Number(item.youtube_likes);
byType.Comments +=
item.facebook_comments +
item.instagram_comments +
item.twitter_comment_count +
Number(item.youtube_comments);
(byType.Views +=
item.facebook_video_views +
item.instagram_video_views +
Number(item.twitter_video_views)),
Number(item.youtube_video_views);
byType.Clicks += item.bitlyclicks;
});
return byType;
} else {
return byType;
}
}
function sumEngagementsColumns(arr = []) {
if (arr.length > 0) {
return arr.reduce((sum, item) => {
return (
sum +
item.facebook_shares +
item.facebook_comments +
item.facebook_likes +
item.facebook_video_views +
item.instagram_comments +
item.instagram_likes +
item.instagram_video_views +
item.twitter_comment_count +
item.twitter_likes_count +
item.twitter_retweet_count +
Number(item.twitter_video_views) +
Number(item.youtube_comments) +
Number(item.youtube_likes) +
Number(item.youtube_video_views) +
Number(item.youtube_shares)
);
}, 0);
} else {
return 0;
}
}
function sumEngagements(arr = [], type = "sum") {
const facebook = arr.filter(
item => item.platform.toLowerCase() === "facebook"
);
const instagram = arr.filter(
item => item.platform.toLowerCase() === "instagram"
);
const twitter = arr.filter(item => item.platform.toLowerCase() === "twitter");
const youtube = arr.filter(item => item.platform.toLowerCase() === "youtube");
const facebookTotal = facebook.reduce((sum, item) => {
return (
sum +
item.facebook_shares +
item.facebook_comments +
item.facebook_likes +
item.facebook_video_views
);
}, 0);
const instagramTotal = instagram.reduce((sum, item) => {
return (
sum +
item.instagram_comments +
item.instagram_likes +
item.instagram_video_views
);
}, 0);
const twitterTotal = twitter.reduce((sum, item) => {
return (
sum +
item.twitter_comment_count +
item.twitter_likes_count +
item.twitter_retweet_count +
Number(item.twitter_video_views)
);
}, 0);
const youtubeTotal = youtube.reduce((sum, yt) => {
return (
sum +
Number(yt.youtube_comments) +
Number(yt.youtube_likes) +
Number(yt.youtube_video_views) +
Number(yt.youtube_shares)
);
}, 0);
if (type === "sum") {
return facebookTotal + instagramTotal + twitterTotal + youtubeTotal;
} else if (type === "platform") {
const platformTotals = {
facebook: facebookTotal,
instagram: instagramTotal,
twitter: twitterTotal,
youtube: youtubeTotal
};
return platformTotals;
} else if (type === "media") {
let text = [];
let image = [];
let video = [];
arr.forEach(item => {
if (
item.content_type === "image" ||
(!item.video_link && item.image_link)
) {
image.push(item);
} else if (
item.content_type === "video" ||
(item.video_link && !item.image_link)
) {
video.push(item);
} else {
text.push(item);
}
});
const textTotal = sumEngagementsColumns(text);
const imageTotal = sumEngagementsColumns(image);
const videoTotal = sumEngagementsColumns(video);
const mediaTypeTotals = {
text: textTotal,
image: imageTotal,
video: videoTotal
};
return mediaTypeTotals;
}
}