diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java index 3e69a2340..7736e4c86 100644 --- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java @@ -194,7 +194,10 @@ public class FacebookErrors { public static final int POKE_OUTSTANDING = 511; public static final int POKE_RATE = 512; public static final int POKE_USER_BLOCKED = 513; - + + // Rate limiting errors + public static final int USER_APP_TOO_MANY_CALLS = 613; + // Ref errors public static final int REF_SET_FAILED = 700; @@ -304,6 +307,9 @@ public class FacebookErrors { public static final int TEST_ACCOUNTS_INVALID_ID = 2901; public static final int TEST_ACCOUNTS_CANT_REMOVE_APP = 2902; public static final int TEST_ACCOUNTS_CANT_DELETE = 2903; + + // Ad Level Rate Limit error + public static final int AD_CREATION_LIMIT_EXCEEDED = 1487225; public static boolean isGeneralError(int code) { return code < 100; diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java index 7f717849a..8a723d443 100644 --- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java @@ -131,7 +131,16 @@ public interface GraphApi { * @param data the data to publish to the connection. */ void post(String objectId, String connectionName, MultiValueMap data); - + + /** + * Updates data of an object. + * Requires appropriate permission to update to the object connection. + * @param objectId the object ID to update. + * @param data the data to update in the object. + * @return true if update was successful + */ + boolean update(String objectId, MultiValueMap data); + /** * Deletes an object. * Requires appropriate permission to delete the object. diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/InvalidParameterException.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/InvalidParameterException.java new file mode 100644 index 000000000..86d3dc3a4 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/InvalidParameterException.java @@ -0,0 +1,15 @@ +package org.springframework.social.facebook.api; + +import org.springframework.social.ApiException; + +/** + * Exception thrown when one of the supplied parameters is invalid. + * + * @author Sebastian Górecki + */ +public class InvalidParameterException extends ApiException { + + public InvalidParameterException(String providerId, String message) { + super(providerId, message); + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AccountOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AccountOperations.java new file mode 100644 index 000000000..fb80e4379 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AccountOperations.java @@ -0,0 +1,123 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.ApiException; +import org.springframework.social.InsufficientPermissionException; +import org.springframework.social.MissingAuthorizationException; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.AdUser.AdUserRole; + +/** + * Defines operations for working with Facebook Marketing API Ad Account object. + * + * @author Sebastian Górecki + */ +public interface AccountOperations { + + static final String[] AD_ACCOUNT_FIELDS = { + "id", "account_id", "account_status", "age", "amount_spent", "balance", "business_city", "business_country_code", + "business_name", "business_state", "business_street", "business_street2", "business_zip", "capabilities", + "created_time", "currency", "daily_spend_limit", "end_advertiser", "funding_source", "funding_source_details", + "is_personal", "media_agency", "name", "offsite_pixels_tos_accepted", "partner", "spend_cap", "timezone_id", + "timezone_name", "timezone_offset_hours_utc", "users", "tax_id_status" + }; + + static final String[] AD_ACCOUNT_INSIGHT_FIELDS = { + "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks", + "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr", + "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas", + "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach", + "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions", + "unique_actions", "cost_per_action_type", "video_start_actions" + }; + + /** + * Get all ad accounts for given user. + * + * @param userId the id of an user + * @return the list of {@link AdAccount} objects + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAdAccounts(String userId); + + /** + * Get the ad account by given id. + * + * @param accountId the ID of the ad account (account_id) + * @return the {@link AdAccount} object + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + AdAccount getAdAccount(String accountId); + + /** + * Get all ad campaigns of an ad account. + * + * @param accountId the ID of the ad account (account_id) + * @return the list of {@link AdCampaign} objects + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAdAccountCampaigns(String accountId); + + /** + * Get all users of the ad account. + * + * @param accountId the ID of the ad account, the string act_{ad_account_id} + * @return the list of {@link AdUser} objects + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAdAccountUsers(String accountId); + + /** + * Add the user to the ad account. + * + * @param accountId the ID of the ad account (account_id) + * @param userId the id of an user (App Scoped User ID) + * @param role the role for the new user in ad account + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + void addUserToAdAccount(String accountId, String userId, AdUserRole role); + + /** + * Remove user's access to an ad account. + * + * @param accountId the ID of the ad account (account_id) + * @param userId the id of an user (App Scoped User ID) + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + void deleteUserFromAdAccount(String accountId, String userId); + + /** + * Get the insight for the ad account in aggregate. + * + * @param accountId the ID of the ad account (account_id) + * @return the {@link AdInsight} object + * @throws ApiException if there is an error while communicating with Facebook. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + */ + AdInsight getAdAccountInsight(String accountId); + + /** + * Updates the ad account with information given in adAccount object. + * Currently only updates on ad account name and spend_cap are supported. + * + * @param accountId the ID of the ad account (account_id) + * @param adAccount the ad account object containing updated account information + * @return true if the update succeeded + * @throws ApiException if there is an error while communicating with Facebook. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + */ + boolean updateAdAccount(String accountId, AdAccount adAccount); +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Ad.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Ad.java new file mode 100644 index 000000000..a171dc089 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Ad.java @@ -0,0 +1,109 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.Date; + +/** + * @author Sebastian Górecki + */ +public class Ad { + private String id; + private AdStatus status; + private String name; + private BidType bidType; + private BidInfo bidInfo; + + private String accountId; + private String adSetId; + private String campaignId; + + private String creativeId; + private Targeting targeting; + + private Date createdTime; + private Date updatedTime; + + public void setStatus(AdStatus status) { + this.status = status; + } + + public void setName(String name) { + this.name = name; + } + + public void setBidInfo(BidInfo bidInfo) { + this.bidInfo = bidInfo; + } + + public void setAdSetId(String adSetId) { + this.adSetId = adSetId; + } + + public void setCreativeId(String creativeId) { + this.creativeId = creativeId; + } + + public String getId() { + return id; + } + + public AdStatus getStatus() { + return status; + } + + public String getName() { + return name; + } + + public BidType getBidType() { + return bidType; + } + + public BidInfo getBidInfo() { + return bidInfo; + } + + public String getAccountId() { + return accountId; + } + + public String getAdSetId() { + return adSetId; + } + + public String getCampaignId() { + return campaignId; + } + + public String getCreativeId() { + return creativeId; + } + + public Targeting getTargeting() { + return targeting; + } + + public Date getCreatedTime() { + return createdTime; + } + + public Date getUpdatedTime() { + return updatedTime; + } + + public enum AdStatus { + ACTIVE, PAUSED, CAMPAIGN_PAUSED, CAMPAIGN_GROUP_PAUSED, CREDIT_CARD_NEEDED, DISABLED, DISAPPROVED, PENDING_REVIEW, + PREAPPROVED, PENDING_BILLING_INFO, ARCHIVED, DELETED, UNKNOWN; + + @JsonCreator + public static AdStatus fromValue(String value) { + for (AdStatus status : AdStatus.values()) { + if (status.name().equals(value)) { + return status; + } + } + return UNKNOWN; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccount.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccount.java new file mode 100644 index 000000000..d67517c50 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccount.java @@ -0,0 +1,330 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import org.springframework.social.facebook.api.FacebookObject; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Model class representing an ad account. + * + * @author Sebastian Górecki + */ +public class AdAccount extends FacebookObject { + private String id; + private List accountGroups; + private long accountId; + private AccountStatus status; + private int age; + private AgencyClientDeclaration agencyClientDeclaration; + private String amountSpent; + private String balance; + private String businessCity; + private String businessCountryCode; + private String businessName; + private String businessState; + private String businessStreet; + private String businessStreet2; + private String businessZip; + private List capabilities; + private Date createdTime; + private String currency; + private String dailySpendLimit; + private long endAdvertiser; + private String fundingSource; + private Map fundingSourceDetails; + private int isPersonal; + private long mediaAgency; + private String name; + private boolean offsitePixelsTOSAccepted; + private long partner; + private String spendCap; + private int timezoneId; + private String timezoneName; + private int timezoneOffsetHoursUTC; + private Map tosAccepted; + private List users; + private TaxStatus taxStatus; + + public String getId() { + return id; + } + + public List getAccountGroups() { + return accountGroups; + } + + public long getAccountId() { + return accountId; + } + + public AccountStatus getStatus() { + return status; + } + + public int getAge() { + return age; + } + + public AgencyClientDeclaration getAgencyClientDeclaration() { + return agencyClientDeclaration; + } + + public String getAmountSpent() { + return amountSpent; + } + + public String getBalance() { + return balance; + } + + public String getBusinessCity() { + return businessCity; + } + + public String getBusinessCountryCode() { + return businessCountryCode; + } + + public String getBusinessName() { + return businessName; + } + + public String getBusinessState() { + return businessState; + } + + public String getBusinessStreet() { + return businessStreet; + } + + public String getBusinessStreet2() { + return businessStreet2; + } + + public String getBusinessZip() { + return businessZip; + } + + public List getCapabilities() { + return capabilities; + } + + public Date getCreatedTime() { + return createdTime; + } + + public String getCurrency() { + return currency; + } + + public String getDailySpendLimit() { + return dailySpendLimit; + } + + public long getEndAdvertiser() { + return endAdvertiser; + } + + public String getFundingSource() { + return fundingSource; + } + + public Map getFundingSourceDetails() { + return fundingSourceDetails; + } + + public int getIsPersonal() { + return isPersonal; + } + + public long getMediaAgency() { + return mediaAgency; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isOffsitePixelsTOSAccepted() { + return offsitePixelsTOSAccepted; + } + + public long getPartner() { + return partner; + } + + public String getSpendCap() { + return spendCap; + } + + public void setSpendCap(String spendCap) { + this.spendCap = spendCap; + } + + public int getTimezoneId() { + return timezoneId; + } + + public String getTimezoneName() { + return timezoneName; + } + + public int getTimezoneOffsetHoursUTC() { + return timezoneOffsetHoursUTC; + } + + public Map getTosAccepted() { + return tosAccepted; + } + + public List getUsers() { + return users; + } + + public TaxStatus getTaxStatus() { + return taxStatus; + } + + public enum AccountStatus { + ACTIVE(1), DISABLED(2), UNSETTLED(3), PENDING_REVIEW(7), IN_GRACE_PERIOD(9), TEMPORARILY_UNAVAILABLE(101), + PENDING_CLOSURE(100), UNKNOWN(0); + + private final int value; + + AccountStatus(int value) { + this.value = value; + } + + @JsonCreator + public static AccountStatus fromValue(int value) { + for (AccountStatus status : AccountStatus.values()) { + if (status.getValue() == value) { + return status; + } + } + return UNKNOWN; + } + + @JsonGetter + public int getValue() { + return value; + } + } + + public enum Capability { + BULK_ACCOUNT, CAN_CREATE_LOOKALIKES_WITH_CUSTOM_RATIO, CAN_USE_CONVERSION_LOOKALIKES, CAN_USE_MOBILE_EXTERNAL_PAGE_TYPE_FOR_LPP, + CAN_USE_REACH_AND_FREQUENCY, CUSTOM_CLUSTER_SHARING, DIRECT_SALES, HAS_AD_SET_TARGETING, HAS_AVAILABLE_PAYMENT_METHODS, + HOLDOUT_VIEW_TAGS, MOBILE_ADVERTISER_ID_UPLOAD, MOBILE_APP_REENGAGEMENT_ADS, MOBILE_APP_VIDEO_ADS, + NEKO_DESKTOP_CANVAS_APP_ADS, NEW_CAMPAIGN_STRUCTURE, PREMIUM, VIEW_TAGS, PRORATED_BUDGET, OFFSITE_CONVERSION_HIGH_BID, + CAN_USE_MOBILE_EXTERNAL_PAGE_TYPE, CAN_USE_OLD_AD_TYPES, CAN_USE_VIDEO_METRICS_BREAKDOWN, ADS_CF_INSTORE_DAILY_BUDGET, + AD_SET_PROMOTED_OBJECT_APP, AD_SET_PROMOTED_OBJECT_OFFER, AD_SET_PROMOTED_OBJECT_PAGE, AD_SET_PROMOTED_OBJECT_PIXEL, + CONNECTIONS_UI_V2, LOOKALIKE_AUDIENCE, CUSTOM_AUDIENCES_OPT_OUT_LINK, CUSTOM_AUDIENCES_FOLDERS, UNKNOWN; + + @JsonCreator + public static Capability fromValue(String value) { + for (Capability capability : Capability.values()) { + if (capability.name().equals(value)) { + return capability; + } + } + return UNKNOWN; + } + } + + public enum TaxStatus { + VAT_NOT_REQUIRED_US_CA(1), VAT_INFORMATION_REQUIRED(2), VAT_INFORMATION_SUBMITTED(3), OFFLINE_VAT_VALIDATION_FAILED(4), + ACCOUNT_IS_PERSONAL_ACCOUNT(5), UNKNOWN(0); + + private final int value; + + TaxStatus(int value) { + this.value = value; + } + + @JsonCreator + public static TaxStatus fromValue(int value) { + for (TaxStatus status : TaxStatus.values()) { + if (status.getValue() == value) { + return status; + } + } + return UNKNOWN; + } + + @JsonGetter + public int getValue() { + return value; + } + } + + public class AgencyClientDeclaration { + private int agencyRepresentingClient; + private int clientBasedInFrance; + private String clientCity; + private String clientCountryCode; + private String clientEmailAddress; + private String clientName; + private String clientPostalCode; + private String clientProvince; + private String clientStreet; + private String clientStreet2; + private int hasWrittenMandateFromAdvertiser; + private int isClientPayingInvoices; + + public int getAgencyRepresentingClient() { + return agencyRepresentingClient; + } + + public int getClientBasedInFrance() { + return clientBasedInFrance; + } + + public String getClientCity() { + return clientCity; + } + + public String getClientCountryCode() { + return clientCountryCode; + } + + public String getClientEmailAddress() { + return clientEmailAddress; + } + + public String getClientName() { + return clientName; + } + + public String getClientPostalCode() { + return clientPostalCode; + } + + public String getClientProvince() { + return clientProvince; + } + + public String getClientStreet() { + return clientStreet; + } + + public String getClientStreet2() { + return clientStreet2; + } + + public int getHasWrittenMandateFromAdvertiser() { + return hasWrittenMandateFromAdvertiser; + } + + public int getIsClientPayingInvoices() { + return isClientPayingInvoices; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccountGroup.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccountGroup.java new file mode 100644 index 000000000..6b958f935 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccountGroup.java @@ -0,0 +1,26 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.facebook.api.FacebookObject; + +/** + * Model class representing an ad account group. + * + * @author Sebastian Górecki + */ +public class AdAccountGroup extends FacebookObject { + private String id; + private String name; + private int status; + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getStatus() { + return status; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCampaign.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCampaign.java new file mode 100644 index 000000000..4f2eef78c --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCampaign.java @@ -0,0 +1,111 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; +import org.springframework.social.facebook.api.FacebookObject; + +/** + * Model class representing an ad campaign. + * + * @author Sebastian Górecki + */ +public class AdCampaign extends FacebookObject { + private String id; + private String accountId; + private BuyingType buyingType; + private CampaignStatus status; + private String name; + private CampaignObjective objective; + private int spendCap; + + public CampaignStatus getStatus() { + return status; + } + + public void setStatus(CampaignStatus status) { + this.status = status; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public String getAccountId() { + return accountId; + } + + public BuyingType getBuyingType() { + return buyingType; + } + + public void setBuyingType(BuyingType buyingType) { + this.buyingType = buyingType; + } + + public CampaignObjective getObjective() { + return objective; + } + + public void setObjective(CampaignObjective objective) { + this.objective = objective; + } + + public int getSpendCap() { + return spendCap; + } + + public void setSpendCap(int spendCap) { + this.spendCap = spendCap; + } + + public enum BuyingType { + AUCTION, FIXED_CPM, RESERVED, MIXED, UNKNOWN; + + @JsonCreator + public static BuyingType fromValue(String value) { + for (BuyingType type : BuyingType.values()) { + if (type.name().equals(value)) { + return type; + } + } + return UNKNOWN; + } + } + + public enum CampaignStatus { + ACTIVE, PAUSED, ARCHIVED, DELETED, UNKNOWN; + + @JsonCreator + public static CampaignStatus fromValue(String value) { + for (CampaignStatus status : CampaignStatus.values()) { + if (status.name().equals(value)) { + return status; + } + } + return UNKNOWN; + } + } + + public enum CampaignObjective { + CANVAS_APP_ENGAGEMENT, CANVAS_APP_INSTALLS, EVENT_RESPONSES, LOCAL_AWARENESS, MOBILE_APP_ENGAGEMENT, + MOBILE_APP_INSTALLS, NONE, OFFER_CLAIMS, PAGE_LIKES, POST_ENGAGEMENT, VIDEO_VIEWS, WEBSITE_CLICKS, + WEBSITE_CONVERSIONS, UNKNOWN; + + @JsonCreator + public static CampaignObjective fromValue(String value) { + for (CampaignObjective objective : CampaignObjective.values()) { + if (objective.name().equals(value)) { + return objective; + } + } + return UNKNOWN; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCreative.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCreative.java new file mode 100644 index 000000000..759691904 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCreative.java @@ -0,0 +1,161 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * @author Sebastian Górecki + */ +public class AdCreative { + private String id; + private AdCreativeType type; + private String name; + private String title; + private AdCreativeStatus status; + + private String body; + private String objectId; + private String imageHash; + private String imageUrl; + private String linkUrl; + private String objectStoryId; + private String objectUrl; + private String urlTags; + private String thumbnailUrl; + + public AdCreativeType getType() { + return type; + } + + public void setType(AdCreativeType type) { + this.type = type; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public void setThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public AdCreativeStatus getStatus() { + return status; + } + + public void setStatus(AdCreativeStatus status) { + this.status = status; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getObjectId() { + return objectId; + } + + public void setObjectId(String objectId) { + this.objectId = objectId; + } + + public String getImageHash() { + return imageHash; + } + + public void setImageHash(String imageHash) { + this.imageHash = imageHash; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getLinkUrl() { + return linkUrl; + } + + public void setLinkUrl(String linkUrl) { + this.linkUrl = linkUrl; + } + + public String getObjectStoryId() { + return objectStoryId; + } + + public void setObjectStoryId(String objectStoryId) { + this.objectStoryId = objectStoryId; + } + + public String getObjectUrl() { + return objectUrl; + } + + public void setObjectUrl(String objectUrl) { + this.objectUrl = objectUrl; + } + + public String getUrlTags() { + return urlTags; + } + + public void setUrlTags(String urlTags) { + this.urlTags = urlTags; + } + + public enum AdCreativeType { + PAGE, DOMAIN, EVENT, STORE_ITEM, OFFER, SHARE, PHOTO, STATUS, VIDEO, APPLICATION, INVALID, UNKNOWN; + + @JsonCreator + public static AdCreativeType fromValue(String value) { + for (AdCreativeType type : AdCreativeType.values()) { + if (type.name().equals(value)) { + return type; + } + } + return UNKNOWN; + } + } + + public enum AdCreativeStatus { + PENDING, ACTIVE, PAUSED, DELETED, PENDING_REVIEW, DISAPPROVED, PREAPPROVED, PENDING_BILLING_INFO, + CAMPAIGN_PAUSED, ADGROUP_PAUSED, CAMPAIGN_GROUP_PAUSED, ARCHIVED, UNKNOWN; + + @JsonCreator + public static AdCreativeStatus fromValue(String value) { + for (AdCreativeStatus status : AdCreativeStatus.values()) { + if (status.name().equals(value)) { + return status; + } + } + return UNKNOWN; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsight.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsight.java new file mode 100644 index 000000000..8828a3f66 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsight.java @@ -0,0 +1,252 @@ +package org.springframework.social.facebook.api.ads; + +import java.util.Date; +import java.util.List; + +/** + * Class representing response object given by Ad Insights API for request + * about AdAccount, AdCampaign, AdSet or Ad. + * + * @author Sebastian Górecki + */ +public class AdInsight { + // id fields + private String accountId; + private String adGroupId; + private String campaignId; + private String campaignGroupId; + + // name fields + private String accountName; + private String adGroupName; + private String campaignGroupName; + private String camapignName; + + // date fields + private Date dateStart; + private Date dateStop; + private Date campaignStart; + private Date campaignEnd; + private Date campaignGroupEnd; + + // general fields + private double actionsPerImpression; + private int clicks; + private int uniqueClicks; + private double costPerResult; + private double costPerTotalAction; + private double costPerClick; + private double costPerUniqueClick; + private double cpm; + private double cpp; + private double ctr; + private double uniqueCtr; + private double frequency; + private int impressions; + private int uniqueImpressions; + private String objective; + private int reach; + private double resultRate; + private int results; + private int roas; + private int socialClicks; + private int uniqueSocialClicks; + private int socialImpressions; + private int uniqueSocialImpressions; + private int socialReach; + private int spend; + private int todaySpend; + private int totalActionValue; + private int totalActions; + private int totalUniqueActions; + + // action and video fields + private List actions; + private List uniqueActions; + private List costPerActionType; + private List videoStartActions; + + public String getAccountId() { + return accountId; + } + + public String getAdGroupId() { + return adGroupId; + } + + public String getCampaignId() { + return campaignId; + } + + public String getCampaignGroupId() { + return campaignGroupId; + } + + public String getAccountName() { + return accountName; + } + + public String getAdGroupName() { + return adGroupName; + } + + public String getCampaignGroupName() { + return campaignGroupName; + } + + public String getCamapignName() { + return camapignName; + } + + public Date getDateStart() { + return dateStart; + } + + public Date getDateStop() { + return dateStop; + } + + public Date getCampaignStart() { + return campaignStart; + } + + public Date getCampaignEnd() { + return campaignEnd; + } + + public Date getCampaignGroupEnd() { + return campaignGroupEnd; + } + + public double getActionsPerImpression() { + return actionsPerImpression; + } + + public int getClicks() { + return clicks; + } + + public int getUniqueClicks() { + return uniqueClicks; + } + + public double getCostPerResult() { + return costPerResult; + } + + public double getCostPerTotalAction() { + return costPerTotalAction; + } + + public double getCostPerClick() { + return costPerClick; + } + + public double getCostPerUniqueClick() { + return costPerUniqueClick; + } + + public double getCpm() { + return cpm; + } + + public double getCpp() { + return cpp; + } + + public double getCtr() { + return ctr; + } + + public double getUniqueCtr() { + return uniqueCtr; + } + + public double getFrequency() { + return frequency; + } + + public int getImpressions() { + return impressions; + } + + public int getUniqueImpressions() { + return uniqueImpressions; + } + + public String getObjective() { + return objective; + } + + public int getReach() { + return reach; + } + + public double getResultRate() { + return resultRate; + } + + public int getResults() { + return results; + } + + public int getRoas() { + return roas; + } + + public int getSocialClicks() { + return socialClicks; + } + + public int getUniqueSocialClicks() { + return uniqueSocialClicks; + } + + public int getSocialImpressions() { + return socialImpressions; + } + + public int getUniqueSocialImpressions() { + return uniqueSocialImpressions; + } + + public int getSocialReach() { + return socialReach; + } + + public int getSpend() { + return spend; + } + + public int getTodaySpend() { + return todaySpend; + } + + public int getTotalActionValue() { + return totalActionValue; + } + + public int getTotalActions() { + return totalActions; + } + + public int getTotalUniqueActions() { + return totalUniqueActions; + } + + public List getActions() { + return actions; + } + + public List getUniqueActions() { + return uniqueActions; + } + + public List getCostPerActionType() { + return costPerActionType; + } + + public List getVideoStartActions() { + return videoStartActions; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsightAction.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsightAction.java new file mode 100644 index 000000000..ce2908799 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsightAction.java @@ -0,0 +1,19 @@ +package org.springframework.social.facebook.api.ads; + +/** + * Model class representing an ad insight action. + * + * @author Sebastian Górecki + */ +public class AdInsightAction { + private String actionType; + private double value; + + public String getActionType() { + return actionType; + } + + public double getValue() { + return value; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdOperations.java new file mode 100644 index 000000000..6b30ef701 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdOperations.java @@ -0,0 +1,111 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.ApiException; +import org.springframework.social.InsufficientPermissionException; +import org.springframework.social.MissingAuthorizationException; +import org.springframework.social.facebook.api.PagedList; + +/** + * @author Sebastian Górecki + */ +public interface AdOperations { + static final String[] AD_FIELDS = { + "id", "account_id", "adgroup_status", "bid_type", "bid_info", "campaign_id", "campaign_group_id", "created_time", + "creative", "name", "targeting", "updated_time" + }; + + static final String[] AD_INSIGHT_FIELDS = { + "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks", + "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr", + "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas", + "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach", + "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions", + "unique_actions", "cost_per_action_type", "video_start_actions" + }; + + /** + * Get all ads from one ad account. + * + * @param accountId the ID of the ad account + * @return the list of the {@link Ad} objects. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAccountAds(String accountId); + + /** + * Get all ads from one ad campaign. + * + * @param campaignId the id of the ad campaign. + * @return the list of the {@link Ad} objects. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getCampaignAds(String campaignId); + + /** + * Get all ads from one ad set. + * + * @param adSetId the of of the ad set. + * @return the list of the {@link Ad} objects. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAdSetAds(String adSetId); + + /** + * Get details about an ad. + * + * @param adId the id of an ad. + * @return the {@link Ad} object. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + Ad getAd(String adId); + + /** + * Get the insights from ad object. + * + * @param adId the id of an ad. + * @return the {@link AdInsight} object. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + AdInsight getAdInsight(String adId); + + /** + * Synchronously creates one ad. + * + * @param accountId the ID of the ad account + * @param ad the {@link Ad} object. + * @return the id of created ad. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + String createAd(String accountId, Ad ad); + + /** + * Updates certain fields in an ad. + * + * @param adId the if of an ad. + * @param ad the {@link Ad} object. + * @return true if successful. + */ + boolean updateAd(String adId, Ad ad); + + /** + * Deletes an ad object. + * + * @param adId the if of an ad. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + void deleteAd(String adId); +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSet.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSet.java new file mode 100644 index 000000000..bb046bb17 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSet.java @@ -0,0 +1,171 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; +import org.springframework.social.facebook.api.FacebookObject; + +import java.util.Date; +import java.util.List; + +/** + * Model class representing an ad set. + * + * @author Sebastian Górecki + */ +public class AdSet extends FacebookObject { + private String id; + private String accountId; + private String campaignId; + private String name; + private AdSetStatus status; + + private boolean autobid; + private BidInfo bidInfo; + private BidType bidType; + + private int budgetRemaining; + private int dailyBudget; + private int lifetimeBudget; + + private List creativeSequence; + private PromotedObject promotedObject; + private Targeting targeting; + + private Date startTime; + private Date endTime; + private Date createdTime; + private Date updatedTime; + + public String getId() { + return id; + } + + public String getAccountId() { + return accountId; + } + + public String getCampaignId() { + return campaignId; + } + + public void setCampaignId(String campaignId) { + this.campaignId = campaignId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public AdSetStatus getStatus() { + return status; + } + + public void setStatus(AdSetStatus status) { + this.status = status; + } + + public boolean isAutobid() { + return autobid; + } + + public void setAutobid(boolean autobid) { + this.autobid = autobid; + } + + public BidInfo getBidInfo() { + return bidInfo; + } + + public void setBidInfo(BidInfo bidInfo) { + this.bidInfo = bidInfo; + } + + public BidType getBidType() { + return bidType; + } + + public void setBidType(BidType bidType) { + this.bidType = bidType; + } + + public int getBudgetRemaining() { + return budgetRemaining; + } + + public int getDailyBudget() { + return dailyBudget; + } + + public void setDailyBudget(int dailyBudget) { + this.dailyBudget = dailyBudget; + } + + public int getLifetimeBudget() { + return lifetimeBudget; + } + + public void setLifetimeBudget(int lifetimeBudget) { + this.lifetimeBudget = lifetimeBudget; + } + + public List getCreativeSequence() { + return creativeSequence; + } + + public PromotedObject getPromotedObject() { + return promotedObject; + } + + public void setPromotedObject(PromotedObject promotedObject) { + this.promotedObject = promotedObject; + } + + public Targeting getTargeting() { + return targeting; + } + + public void setTargeting(Targeting targeting) { + this.targeting = targeting; + } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public Date getEndTime() { + return endTime; + } + + public void setEndTime(Date endTime) { + this.endTime = endTime; + } + + public Date getCreatedTime() { + return createdTime; + } + + public Date getUpdatedTime() { + return updatedTime; + } + + public enum AdSetStatus { + ACTIVE, PAUSED, ARCHIVED, DELETED, CAMPAIGN_GROUP_PAUSED, UNKNOWN; + + @JsonCreator + public static AdSetStatus fromValue(String value) { + for (AdSetStatus status : AdSetStatus.values()) { + if (status.name().equals(value)) { + return status; + } + } + return UNKNOWN; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSetOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSetOperations.java new file mode 100644 index 000000000..c72d00894 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSetOperations.java @@ -0,0 +1,106 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.ApiException; +import org.springframework.social.InsufficientPermissionException; +import org.springframework.social.MissingAuthorizationException; +import org.springframework.social.facebook.api.PagedList; + +/** + * Defines operations for working with Facebook Ad set object. + * + * @author Sebastian Górecki + */ +public interface AdSetOperations { + static final String[] AD_SET_FIELDS = { + "account_id", "bid_info", "bid_type", "budget_remaining", "campaign_group_id", "campaign_status", "created_time", + "creative_sequence", "daily_budget", "end_time", "id", "is_autobid", "lifetime_budget", "name", "promoted_object", + "start_time", "targeting", "updated_time" + }; + + static final String[] AD_SET_INSIGHT_FIELDS = { + "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks", + "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr", + "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas", + "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach", + "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions", + "unique_actions", "cost_per_action_type", "video_start_actions" + }; + + /** + * Gets all ad sets from ad account given by account id. + * + * @param accountId the ID of an ad account + * @return the list of {@link AdSet} objects + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAccountAdSets(String accountId); + + /** + * Get all ad sets for the given campaign + * + * @param campaignId the id of the ad campaing + * @return the list of {@link AdSet} objects. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getCampaignAdSets(String campaignId); + + /** + * Gets ad set by given id. + * + * @param id the id of the ad set + * @return the {@link AdSet} object + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + AdSet getAdSet(String id); + + /** + * Get the insight for the ad set. + * + * @param adSetId the id of the ad set + * @return the {@link AdInsight} object + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + AdInsight getAdSetInsight(String adSetId); + + /** + * Creates an ad set in the given account + * + * @param accountId the account id + * @param adSet the ad set object + * @return the id of the new ad set. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + String createAdSet(String accountId, AdSet adSet); + + /** + * Updates the ad set. + * + * @param adSetId the id of the ad set + * @param adSet the ad set object + * @return true if update was successful + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + boolean updateAdSet(String adSetId, AdSet adSet); + + /** + * Deletes ad set given by id. + * + * @param adSetId the id of the ad set + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + void deleteAdSet(String adSetId); +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdUser.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdUser.java new file mode 100644 index 000000000..9e4da7279 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdUser.java @@ -0,0 +1,85 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import org.springframework.social.facebook.api.FacebookObject; + +import java.util.List; + +/** + * Model class representing an ad user. + * + * @author Sebastian Górecki + */ +public class AdUser extends FacebookObject { + private String id; + private String name; + private List permissions; + private AdUserRole role; + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public List getPermissions() { + return permissions; + } + + public AdUserRole getRole() { + return role; + } + + public enum AdUserPermission { + ACCOUNT_ADMIN(1), ADMANAGER_READ(2), ADMANAGER_WRITE(3), BILLING_READ(4), BILLING_WRITE(5), REPORTS(7), UNKNOWN(0); + + private final int value; + + AdUserPermission(int value) { + this.value = value; + } + + @JsonCreator + public static AdUserPermission forValue(int value) { + for (AdUserPermission permission : AdUserPermission.values()) { + if (permission.getValue() == value) { + return permission; + } + } + return UNKNOWN; + } + + @JsonGetter + public int getValue() { + return value; + } + } + + public enum AdUserRole { + ADMINISTRATOR(1001), ADVERTISER(1002), ANALYST(1003), SALES(1004), UNKNOWN(0); + + private final int value; + + AdUserRole(int value) { + this.value = value; + } + + @JsonCreator + public static AdUserRole forValue(int value) { + for (AdUserRole role : AdUserRole.values()) { + if (role.getValue() == value) { + return role; + } + } + return UNKNOWN; + } + + @JsonGetter + public int getValue() { + return value; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidInfo.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidInfo.java new file mode 100644 index 000000000..d6f1ca73f --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidInfo.java @@ -0,0 +1,9 @@ +package org.springframework.social.facebook.api.ads; + +import java.util.HashMap; + +/** + * @author Sebastian Górecki + */ +public class BidInfo extends HashMap { +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidType.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidType.java new file mode 100644 index 000000000..2f1934b37 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidType.java @@ -0,0 +1,22 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * Enum used in Ad and AdSet objects. + * + * @author Sebastian Górecki + */ +public enum BidType { + CPM, CPC, MULTI_PREMIUM, ABSOLUTE_OCPM, CPA, UNKNOWN; + + @JsonCreator + public static BidType fromValue(String value) { + for (BidType type : BidType.values()) { + if (type.name().equals(value)) { + return type; + } + } + return UNKNOWN; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CampaignOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CampaignOperations.java new file mode 100644 index 000000000..e2501eb32 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CampaignOperations.java @@ -0,0 +1,106 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.ApiException; +import org.springframework.social.InsufficientPermissionException; +import org.springframework.social.MissingAuthorizationException; +import org.springframework.social.facebook.api.PagedList; + +/** + * Defines operations for working with Facebook Ad Campaign object. + * + * @author Sebastian Górecki + */ +public interface CampaignOperations { + + static final String[] AD_CAMPAIGN_FIELDS = { + "id", "account_id", "buying_type", "campaign_group_status", "name", "objective", "spend_cap" + }; + + static final String[] AD_CAMPAIGN_INSIGHT_FIELDS = { + "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks", + "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr", + "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas", + "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach", + "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions", + "unique_actions", "cost_per_action_type", "video_start_actions" + }; + + /** + * Gets all ad campaigns belonging to user. + * + * @param accountId the ID of the ad account (account_id) + * @return the list of {@link AdCampaign} objects + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAdCampaigns(String accountId); + + /** + * Get the campaign by given id. + * + * @param id the id of the campaign + * @return the {@link AdCampaign} object. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + AdCampaign getAdCampaign(String id); + + /** + * Get all ad sets from one ad campaign. + * + * @param campaignId the id of the campaign + * @return the list of {@link AdSet} objects + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAdCampaignSets(String campaignId); + + /** + * Get the insight of the ad campaign. + * + * @param campaignId the id of the campaign + * @return the {@link AdInsight} object + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + AdInsight getAdCampaignInsight(String campaignId); + + /** + * Creates new campaign based on adCampaign object. + * + * @param accountId the ID of the ad account (account_id) + * @param adCampaign the ad campaign object + * @return the id of the created ad campaign + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + * @throws InvalidCampaignStatusException if you provided wrong status for the new campaign + */ + String createAdCampaign(String accountId, AdCampaign adCampaign); + + /** + * Updates the ad campaign with information in adCampaign object. + * + * @param campaignId the ID of the ad campaign to update + * @param adCampaign the ad campaign object + * @return true if update was successful + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + boolean updateAdCampaign(String campaignId, AdCampaign adCampaign); + + /** + * Deletes the ad campaign given by id. + * + * @param campaignId the id of the campaign to delete + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + void deleteAdCampaign(String campaignId); +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CreativeOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CreativeOperations.java new file mode 100644 index 000000000..753045f96 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CreativeOperations.java @@ -0,0 +1,86 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.ApiException; +import org.springframework.social.InsufficientPermissionException; +import org.springframework.social.MissingAuthorizationException; +import org.springframework.social.facebook.api.PagedList; + +/** + * Defines operations for working with Facebook Creative object. + * + * @author Sebastian Górecki + */ +public interface CreativeOperations { + + static final String[] AD_CREATIVE_FIELDS = { + "actor_id", "body", "image_hash", "image_url", "link_url", "name", "object_id", "object_story_id", + "object_url", "run_status", "title", "url_tags", "thumbnail_url", "object_type", "id" + }; + + /** + * Retrieve an account's ad creatives. + * + * @param accountId the ID of the ad account. + * @return the list of {@link AdCreative} objects. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAccountCreatives(String accountId); + + /** + * Retrieve an ad set's ad creatives. + * + * @param adSetId the id of the ad set. + * @return the list of {@link AdCreative} objects. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + PagedList getAdSetCreatives(String adSetId); + + /** + * Get information about an ad creative. + * + * @param creativeId the id of the ad creative + * @return the {@link AdCreative} object. + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + AdCreative getAdCreative(String creativeId); + + /** + * Create an ad creative in the given account. + * + * @param accountId the ID of the ad account. + * @param creative the {@link AdInsight} object. + * @return the id of the ad creative + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + String createAdCreative(String accountId, AdCreative creative); + + /** + * Rename an ad creative in creative library. + * + * @param creativeId the id of the ad creative. + * @param name new name of the ad creative + * @return true if rename was successful + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + boolean renameAdCreative(String creativeId, String name); + + /** + * Delete given ad creative. + * + * @param creativeId the id of the ad creative + * @throws ApiException if there is an error while communicating with Facebook. + * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission. + * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token. + */ + void deleteAdCreative(String creativeId); +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/FacebookAds.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/FacebookAds.java new file mode 100644 index 000000000..ba3ff951f --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/FacebookAds.java @@ -0,0 +1,46 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.facebook.api.ads.impl.FacebookAdsTemplate; + +/** + * Interface specifying a basic set of operations for interacting with Facebook Marketing API. + * Implemented by {@link FacebookAdsTemplate}. + * + * @author Sebastian Górecki + */ +public interface FacebookAds { + /** + * API for working with Facebook Ad account. + * + * @return {@link AccountOperations} + */ + AccountOperations accountOperations(); + + /** + * API for working with Facebook Ad campaign. + * + * @return {@link CampaignOperations} + */ + CampaignOperations campaignOperations(); + + /** + * API for working with Facebook AdSet. + * + * @return {@link AdSetOperations} + */ + AdSetOperations adSetOperations(); + + /** + * API for working with Facebook creative. + * + * @return {@link CreativeOperations} + */ + CreativeOperations creativeOperations(); + + /** + * API for working with Facebook ad. + * + * @return {@link AdOperations} + */ + AdOperations adOperations(); +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/InvalidCampaignStatusException.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/InvalidCampaignStatusException.java new file mode 100644 index 000000000..87548c1e7 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/InvalidCampaignStatusException.java @@ -0,0 +1,14 @@ +package org.springframework.social.facebook.api.ads; + +import org.springframework.social.facebook.api.InvalidParameterException; + +/** + * Exception is thrown when a new ad campaign is created with status other that ACTIVE or PAUSED. + * + * @author Sebastian Górecki + */ +public class InvalidCampaignStatusException extends InvalidParameterException { + public InvalidCampaignStatusException(String providerId, String message) { + super(providerId, message); + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/PromotedObject.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/PromotedObject.java new file mode 100644 index 000000000..f9df1dbd4 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/PromotedObject.java @@ -0,0 +1,9 @@ +package org.springframework.social.facebook.api.ads; + +import java.util.HashMap; + +/** + * @author Sebastian Górecki + */ +public class PromotedObject extends HashMap { +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Targeting.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Targeting.java new file mode 100644 index 000000000..1bf968cb7 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Targeting.java @@ -0,0 +1,319 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.List; + +/** + * @author Sebastian Górecki + */ +public class Targeting { + + // demographics + private List genders; + private Integer ageMin; + private Integer ageMax; + private List relationshipStatuses; + private List interestedIn; + + // location + private TargetingLocation geoLocations; + private TargetingLocation excludedGeoLocations; + + // placement + private List pageTypes; + + // connections + private List connections; + private List excludedConnections; + private List friendsOfConnections; + + // interests + private List interests; + + // behaviors + private List behaviors; + + // education and workplace + private List educationSchools; + private List educationStatuses; + private List collegeYears; + private List educationMajors; + private List workEmployers; + private List workPositions; + + public List getGenders() { + return genders; + } + + public void setGenders(List genders) { + this.genders = genders; + } + + public Integer getAgeMin() { + return ageMin; + } + + public void setAgeMin(Integer ageMin) { + this.ageMin = ageMin; + } + + public Integer getAgeMax() { + return ageMax; + } + + public void setAgeMax(Integer ageMax) { + this.ageMax = ageMax; + } + + public List getRelationshipStatuses() { + return relationshipStatuses; + } + + public void setRelationshipStatuses(List relationshipStatuses) { + this.relationshipStatuses = relationshipStatuses; + } + + public List getInterestedIn() { + return interestedIn; + } + + public void setInterestedIn(List interestedIn) { + this.interestedIn = interestedIn; + } + + public TargetingLocation getGeoLocations() { + return geoLocations; + } + + public void setGeoLocations(TargetingLocation geoLocations) { + this.geoLocations = geoLocations; + } + + public TargetingLocation getExcludedGeoLocations() { + return excludedGeoLocations; + } + + public void setExcludedGeoLocations(TargetingLocation excludedGeoLocations) { + this.excludedGeoLocations = excludedGeoLocations; + } + + public List getPageTypes() { + return pageTypes; + } + + public void setPageTypes(List pageTypes) { + this.pageTypes = pageTypes; + } + + public List getConnections() { + return connections; + } + + public void setConnections(List connections) { + this.connections = connections; + } + + public List getExcludedConnections() { + return excludedConnections; + } + + public void setExcludedConnections(List excludedConnections) { + this.excludedConnections = excludedConnections; + } + + public List getFriendsOfConnections() { + return friendsOfConnections; + } + + public void setFriendsOfConnections(List friendsOfConnections) { + this.friendsOfConnections = friendsOfConnections; + } + + public List getInterests() { + return interests; + } + + public void setInterests(List interests) { + this.interests = interests; + } + + public List getBehaviors() { + return behaviors; + } + + public void setBehaviors(List behaviors) { + this.behaviors = behaviors; + } + + public List getEducationSchools() { + return educationSchools; + } + + public void setEducationSchools(List educationSchools) { + this.educationSchools = educationSchools; + } + + public List getEducationStatuses() { + return educationStatuses; + } + + public void setEducationStatuses(List educationStatuses) { + this.educationStatuses = educationStatuses; + } + + public List getCollegeYears() { + return collegeYears; + } + + public void setCollegeYears(List collegeYears) { + this.collegeYears = collegeYears; + } + + public List getEducationMajors() { + return educationMajors; + } + + public void setEducationMajors(List educationMajors) { + this.educationMajors = educationMajors; + } + + public List getWorkEmployers() { + return workEmployers; + } + + public void setWorkEmployers(List workEmployers) { + this.workEmployers = workEmployers; + } + + public List getWorkPositions() { + return workPositions; + } + + public void setWorkPositions(List workPositions) { + this.workPositions = workPositions; + } + + public enum Gender { + UNKNOWN(0), MALE(1), FEMALE(2); + + private final int value; + + Gender(int value) { + this.value = value; + } + + @JsonCreator + public static Gender fromValue(int value) { + for (Gender gender : Gender.values()) { + if (gender.getValue() == value) return gender; + } + return UNKNOWN; + } + + @JsonValue + public int getValue() { + return value; + } + } + + public enum RelationshipStatus { + UNKNOWN(0), SINGLE(1), IN_RELATIONSHIP(2), MARRIED(3), ENGAGED(4), NOT_SPECIFIED(6), IN_CIVIL_UNION(7), + IN_DOMESTIC_PARTNERSHIP(8), IN_OPEN_RELATIONSHIP(9), ITS_COMPLICATED(10), SEPARATED(11), DIVORCED(12), + WIDOWED(13); + + private final int value; + + RelationshipStatus(int value) { + this.value = value; + } + + @JsonCreator + public static RelationshipStatus fromValue(int value) { + for (RelationshipStatus status : RelationshipStatus.values()) { + if (status.getValue() == value) return status; + } + return UNKNOWN; + } + + @JsonValue + public int getValue() { + return value; + } + } + + public enum InterestedIn { + UNKNOWN(0), MEN(1), WOMEN(2), MAN_AND_WOMAN(3), NOT_SPECIFIED(4); + + private final int value; + + InterestedIn(int value) { + this.value = value; + } + + @JsonCreator + public static InterestedIn fromValue(int value) { + for (InterestedIn interestedIn : InterestedIn.values()) { + if (interestedIn.getValue() == value) return interestedIn; + } + return UNKNOWN; + } + + @JsonValue + public int getValue() { + return value; + } + } + + public enum PageType { + UNKNOWN("unknown"), DESKTOP("desktop"), FEED("feed"), DESKTOP_FEED("desktopfeed"), MOBILE("mobile"), + MOBILE_FEED_AND_EXTERNAL("mobilefeed-and-external"), MOBILE_FEED("mobilefeed"), RIGHTCOLUMN("rightcolumn"), + RIGHTCOLUMN_AND_MOBILE("rightcolumn-and-mobile"), HOME("home"), DESKTOP_AND_MOBILE_AND_EXTERNAL("desktop-and-mobile-and-external"), + FEED_AND_EXTERNAL("feed-and-external"), RIGHTCOLUMN_AND_MOBILE_AND_EXTERNAL("rightcolumn-and-mobile-and-external"); + + private final String value; + + PageType(String value) { + this.value = value; + } + + @JsonCreator + public static PageType fromValue(String value) { + for (PageType type : PageType.values()) { + if (type.getValue().equals(value)) return type; + } + return UNKNOWN; + } + + @JsonValue + public String getValue() { + return value; + } + } + + public enum EducationStatus { + UNKNOWN(0), HIGH_SCHOOL(1), UNDERGRAD(2), ALUM(3), HIGH_SCHOOL_GRAD(4), SOME_COLLEGE(5), ASSOCIATE_DEGREE(6), + IN_GRAD_SCHOOL(7), SOME_GRAD_SCHOOL(8), MASTER_DEGREE(9), PROFESSIONAL_DEGREE(10), DOCTORATE_DEGREE(11), + UNSPECIFIED(12), SOME_HIGH_SCHOOL(13); + + private final int value; + + EducationStatus(int value) { + this.value = value; + } + + @JsonCreator + public static EducationStatus fromValue(int value) { + for (EducationStatus status : EducationStatus.values()) { + if (status.getValue() == value) return status; + } + return UNKNOWN; + } + + @JsonValue + public int getValue() { + return value; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingCityEntry.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingCityEntry.java new file mode 100644 index 000000000..1ab1de3b2 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingCityEntry.java @@ -0,0 +1,31 @@ +package org.springframework.social.facebook.api.ads; + +/** + * @author Sebastian Górecki + */ +public class TargetingCityEntry { + private String key; + private int radius; + private String distanceUnit; + + TargetingCityEntry() { + } + + public TargetingCityEntry(String key, int radius, String distanceUnit) { + this.key = key; + this.radius = radius; + this.distanceUnit = distanceUnit; + } + + public String getKey() { + return key; + } + + public int getRadius() { + return radius; + } + + public String getDistanceUnit() { + return distanceUnit; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingEntry.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingEntry.java new file mode 100644 index 000000000..51314fee7 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingEntry.java @@ -0,0 +1,25 @@ +package org.springframework.social.facebook.api.ads; + +/** + * @author Sebastian Górecki + */ +public class TargetingEntry { + private long id; + private String name; + + public TargetingEntry() { + } + + public TargetingEntry(long id, String name) { + this.id = id; + this.name = name; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingLocation.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingLocation.java new file mode 100644 index 000000000..9ed228033 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingLocation.java @@ -0,0 +1,80 @@ +package org.springframework.social.facebook.api.ads; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.List; + +/** + * @author Sebastian Górecki + */ +public class TargetingLocation { + private List countries; + private List regions; + private List cities; + private List zips; + private List locationTypes; + + public List getLocationTypes() { + return locationTypes; + } + + public void setLocationTypes(List locationTypes) { + this.locationTypes = locationTypes; + } + + public List getCountries() { + return countries; + } + + public void setCountries(List countries) { + this.countries = countries; + } + + public List getRegions() { + return regions; + } + + public void setRegions(List regions) { + this.regions = regions; + } + + public List getCities() { + return cities; + } + + public void setCities(List cities) { + this.cities = cities; + } + + public List getZips() { + return zips; + } + + public void setZips(List zips) { + this.zips = zips; + } + + public enum LocationType { + UNKNOWN("unknown"), RECENT("recent"), HOME("home"), TRAVEL_IN("travel_in"); + + private final String value; + + LocationType(String value) { + this.value = value; + } + + @JsonCreator + public static LocationType fromValue(String value) { + for (LocationType type : LocationType.values()) { + if (type.getValue().equals(value)) return type; + } + return UNKNOWN; + } + + @JsonValue + public String getValue() { + return value; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AccountTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AccountTemplate.java new file mode 100644 index 000000000..31fc40f60 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AccountTemplate.java @@ -0,0 +1,72 @@ +package org.springframework.social.facebook.api.ads.impl; + +import org.springframework.social.facebook.api.GraphApi; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.*; +import org.springframework.social.facebook.api.ads.AdUser.AdUserRole; +import org.springframework.social.facebook.api.impl.AbstractFacebookOperations; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * @author Sebastian Górecki + */ +public class AccountTemplate extends AbstractFacebookOperations implements AccountOperations { + + private final GraphApi graphApi; + + public AccountTemplate(GraphApi graphApi, boolean isAuthorizedForUser) { + super(isAuthorizedForUser); + this.graphApi = graphApi; + } + + public PagedList getAdAccounts(String userId) { + requireAuthorization(); + return graphApi.fetchConnections(userId, "adaccounts", AdAccount.class, AD_ACCOUNT_FIELDS); + } + + public AdAccount getAdAccount(String accountId) { + requireAuthorization(); + return graphApi.fetchObject(getAdAccountId(accountId), AdAccount.class, AD_ACCOUNT_FIELDS); + } + + public PagedList getAdAccountCampaigns(String accountId) { + return graphApi.fetchConnections(getAdAccountId(accountId), "adcampaign_groups", AdCampaign.class, CampaignOperations.AD_CAMPAIGN_FIELDS); + } + + public PagedList getAdAccountUsers(String accountId) { + requireAuthorization(); + return graphApi.fetchConnections(getAdAccountId(accountId), "users", AdUser.class); + } + + public void addUserToAdAccount(String accountId, String userId, AdUserRole role) { + requireAuthorization(); + MultiValueMap map = new LinkedMultiValueMap(); + map.set("uid", userId); + map.set("role", String.valueOf(role.getValue())); + graphApi.post(getAdAccountId(accountId) + "/users", "", map); + } + + public void deleteUserFromAdAccount(String accountId, String userId) { + requireAuthorization(); + graphApi.delete(getAdAccountId(accountId) + "/users/" + userId); + } + + public AdInsight getAdAccountInsight(String accountId) { + requireAuthorization(); + PagedList insights = graphApi.fetchConnections(getAdAccountId(accountId), "insights", AdInsight.class, AD_ACCOUNT_INSIGHT_FIELDS); + return insights.get(0); + } + + public boolean updateAdAccount(String accountId, AdAccount adAccount) { + requireAuthorization(); + MultiValueMap map = new LinkedMultiValueMap(); + if (adAccount.getName() != null) { + map.set("name", adAccount.getName()); + } + if (adAccount.getSpendCap() != null) { + map.set("spend_cap", adAccount.getSpendCap()); + } + return graphApi.update(getAdAccountId(accountId), map); + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdSetTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdSetTemplate.java new file mode 100644 index 000000000..8d46e4846 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdSetTemplate.java @@ -0,0 +1,112 @@ +package org.springframework.social.facebook.api.ads.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.social.facebook.api.GraphApi; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.AdInsight; +import org.springframework.social.facebook.api.ads.AdSet; +import org.springframework.social.facebook.api.ads.AdSetOperations; +import org.springframework.social.facebook.api.impl.AbstractFacebookOperations; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * @author Sebastian Górecki + */ +public class AdSetTemplate extends AbstractFacebookOperations implements AdSetOperations { + private GraphApi graphApi; + private ObjectMapper mapper; + + public AdSetTemplate(GraphApi graphApi, ObjectMapper mapper, boolean authorized) { + super(authorized); + this.graphApi = graphApi; + this.mapper = mapper; + } + + public PagedList getAccountAdSets(String accountId) { + requireAuthorization(); + return graphApi.fetchConnections(getAdAccountId(accountId), "adcampaigns", AdSet.class, AdSetOperations.AD_SET_FIELDS); + } + + public PagedList getCampaignAdSets(String campaignId) { + requireAuthorization(); + return graphApi.fetchConnections(campaignId, "adcampaigns", AdSet.class, AdSetOperations.AD_SET_FIELDS); + } + + public AdSet getAdSet(String id) { + requireAuthorization(); + return graphApi.fetchObject(id, AdSet.class, AdSetOperations.AD_SET_FIELDS); + } + + public AdInsight getAdSetInsight(String adSetId) { + requireAuthorization(); + PagedList insights = graphApi.fetchConnections(adSetId, "insights", AdInsight.class, AdSetOperations.AD_SET_INSIGHT_FIELDS); + return insights.get(0); + } + + public String createAdSet(String accountId, AdSet adSet) { + requireAuthorization(); + MultiValueMap data = mapCommonFields(adSet); + data.set("campaign_group_id", adSet.getCampaignId()); + return graphApi.publish(getAdAccountId(accountId), "adcampaigns", data); + } + + public boolean updateAdSet(String adSetId, AdSet adSet) { + requireAuthorization(); + MultiValueMap data = mapCommonFields(adSet); + return graphApi.update(adSetId, data); + } + + public void deleteAdSet(String adSetId) { + requireAuthorization(); + MultiValueMap data = new LinkedMultiValueMap(); + data.add("campaign_status", "DELETED"); + graphApi.post(adSetId, data); + } + + private MultiValueMap mapCommonFields(AdSet adSet) { + MultiValueMap data = new LinkedMultiValueMap(); + data.set("date_format", "U"); + if (adSet.getName() != null) { + data.set("name", adSet.getName()); + } + if (adSet.getStatus() != null) { + data.set("campaign_status", adSet.getStatus().name()); + } + data.set("is_autobid", String.valueOf(adSet.isAutobid())); + if (adSet.getBidInfo() != null) { + try { + data.set("bid_info", mapper.writeValueAsString(adSet.getBidInfo())); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + if (adSet.getBidType() != null) { + data.set("bid_type", adSet.getBidType().name()); + } + data.set("daily_budget", String.valueOf(adSet.getDailyBudget())); + data.set("lifetime_budget", String.valueOf(adSet.getLifetimeBudget())); + if (adSet.getPromotedObject() != null) { + try { + data.set("promoted_object", mapper.writeValueAsString(adSet.getPromotedObject())); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + if (adSet.getTargeting() != null) { + try { + data.set("targeting", mapper.writeValueAsString(adSet.getTargeting())); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + if (adSet.getStartTime() != null) { + data.set("start_time", getUnixTime(adSet.getStartTime())); + } + if (adSet.getEndTime() != null) { + data.set("end_time", getUnixTime(adSet.getEndTime())); + } + return data; + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdTemplate.java new file mode 100644 index 000000000..e25eaad18 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdTemplate.java @@ -0,0 +1,95 @@ +package org.springframework.social.facebook.api.ads.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.social.facebook.api.GraphApi; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.Ad; +import org.springframework.social.facebook.api.ads.AdInsight; +import org.springframework.social.facebook.api.ads.AdOperations; +import org.springframework.social.facebook.api.impl.AbstractFacebookOperations; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * @author Sebastian Górecki + */ +public class AdTemplate extends AbstractFacebookOperations implements AdOperations { + + private final GraphApi graphApi; + private final RestTemplate restTemplate; + private ObjectMapper mapper; + + public AdTemplate(GraphApi graphApi, RestTemplate restTemplate, ObjectMapper mapper, boolean isAuthorizedForUser) { + super(isAuthorizedForUser); + this.graphApi = graphApi; + this.restTemplate = restTemplate; + this.mapper = mapper; + } + + public PagedList getAccountAds(String accountId) { + requireAuthorization(); + return graphApi.fetchConnections(getAdAccountId(accountId), "adgroups", Ad.class, AdOperations.AD_FIELDS); + } + + public PagedList getCampaignAds(String campaignId) { + requireAuthorization(); + return graphApi.fetchConnections(campaignId, "adgroups", Ad.class, AdOperations.AD_FIELDS); + } + + public PagedList getAdSetAds(String adSetId) { + requireAuthorization(); + return graphApi.fetchConnections(adSetId, "adgroups", Ad.class, AdOperations.AD_FIELDS); + } + + public Ad getAd(String adId) { + requireAuthorization(); + return graphApi.fetchObject(adId, Ad.class, AdOperations.AD_FIELDS); + } + + public AdInsight getAdInsight(String adId) { + requireAuthorization(); + PagedList insights = graphApi.fetchConnections(adId, "insights", AdInsight.class, AdOperations.AD_INSIGHT_FIELDS); + return insights.get(0); + } + + public String createAd(String accountId, Ad ad) { + requireAuthorization(); + MultiValueMap data = mapCommonFields(ad); + data.add("campaign_id", ad.getAdSetId()); + return graphApi.publish(getAdAccountId(accountId), "adgroups", data); + } + + public boolean updateAd(String adId, Ad ad) { + requireAuthorization(); + MultiValueMap data = mapCommonFields(ad); + return graphApi.update(adId, data); + } + + public void deleteAd(String adId) { + requireAuthorization(); + restTemplate.delete(GraphApi.GRAPH_API_URL + adId); + } + + private MultiValueMap mapCommonFields(Ad ad) { + MultiValueMap data = new LinkedMultiValueMap(); + if (ad.getName() != null) { + data.add("name", ad.getName()); + } + if (ad.getStatus() != null) { + data.add("adgroup_status", ad.getStatus().name()); + } + if (ad.getBidInfo() != null) { + try { + data.add("bid_info", mapper.writeValueAsString(ad.getBidInfo())); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + if (ad.getCreativeId() != null) { + data.add("creative", "{\"creative_id\": \"" + ad.getCreativeId() + "\"}"); + } + return data; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CampaignTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CampaignTemplate.java new file mode 100644 index 000000000..6293bff12 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CampaignTemplate.java @@ -0,0 +1,82 @@ +package org.springframework.social.facebook.api.ads.impl; + +import org.springframework.social.facebook.api.GraphApi; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.*; +import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus; +import org.springframework.social.facebook.api.impl.AbstractFacebookOperations; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * @author Sebastian Górecki + */ +public class CampaignTemplate extends AbstractFacebookOperations implements CampaignOperations { + + private final GraphApi graphApi; + + public CampaignTemplate(GraphApi graphApi, boolean isAuthorizedForUser) { + super(isAuthorizedForUser); + this.graphApi = graphApi; + } + + public PagedList getAdCampaigns(String accountId) { + requireAuthorization(); + return graphApi.fetchConnections(getAdAccountId(accountId), "adcampaign_groups", AdCampaign.class, CampaignOperations.AD_CAMPAIGN_FIELDS); + } + + public AdCampaign getAdCampaign(String id) { + requireAuthorization(); + return graphApi.fetchObject(id, AdCampaign.class, CampaignOperations.AD_CAMPAIGN_FIELDS); + } + + public PagedList getAdCampaignSets(String campaignId) { + requireAuthorization(); + return graphApi.fetchConnections(campaignId, "adcampaigns", AdSet.class, AdSetOperations.AD_SET_FIELDS); + } + + public AdInsight getAdCampaignInsight(String campaignId) { + requireAuthorization(); + PagedList insights = graphApi.fetchConnections(campaignId, "insights", AdInsight.class, CampaignOperations.AD_CAMPAIGN_INSIGHT_FIELDS); + return insights.get(0); + } + + public String createAdCampaign(String accountId, AdCampaign adCampaign) { + requireAuthorization(); + MultiValueMap map = mapCommonFields(adCampaign); + if (adCampaign.getBuyingType() != null) { + map.add("buying_type", adCampaign.getBuyingType().name()); + } + return graphApi.publish(getAdAccountId(accountId), "adcampaign_groups", map); + } + + public boolean updateAdCampaign(String campaignId, AdCampaign adCampaign) { + requireAuthorization(); + MultiValueMap map = mapCommonFields(adCampaign); + return graphApi.update(campaignId, map); + } + + public void deleteAdCampaign(String campaignId) { + requireAuthorization(); + MultiValueMap map = new LinkedMultiValueMap(); + map.add("campaign_group_status", CampaignStatus.DELETED.name()); + graphApi.post(campaignId, map); + } + + private MultiValueMap mapCommonFields(AdCampaign adCampaign) { + MultiValueMap map = new LinkedMultiValueMap(); + if (adCampaign.getName() != null) { + map.add("name", adCampaign.getName()); + } + if (adCampaign.getStatus() != null) { + map.add("campaign_group_status", adCampaign.getStatus().name()); + } + if (adCampaign.getObjective() != null) { + map.add("objective", adCampaign.getObjective().name()); + } + if (adCampaign.getSpendCap() != 0) { + map.add("spend_cap", String.valueOf(adCampaign.getSpendCap())); + } + return map; + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CreativeTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CreativeTemplate.java new file mode 100644 index 000000000..51f06a066 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CreativeTemplate.java @@ -0,0 +1,104 @@ +package org.springframework.social.facebook.api.ads.impl; + +import org.springframework.social.facebook.api.GraphApi; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.AdCreative; +import org.springframework.social.facebook.api.ads.AdInsight; +import org.springframework.social.facebook.api.ads.CreativeOperations; +import org.springframework.social.facebook.api.impl.AbstractFacebookOperations; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * @author Sebastian Górecki + */ +public class CreativeTemplate extends AbstractFacebookOperations implements CreativeOperations { + + private final GraphApi graphApi; + private final RestTemplate restTemplate; + + + public CreativeTemplate(GraphApi graphApi, RestTemplate restTemplate, boolean isAuthorizedForUser) { + super(isAuthorizedForUser); + this.graphApi = graphApi; + this.restTemplate = restTemplate; + } + + public PagedList getAccountCreatives(String accountId) { + requireAuthorization(); + return graphApi.fetchConnections(getAdAccountId(accountId), "adcreatives", AdCreative.class, CreativeOperations.AD_CREATIVE_FIELDS); + } + + public PagedList getAdSetCreatives(String adSetId) { + requireAuthorization(); + return graphApi.fetchConnections(adSetId, "adcreatives", AdCreative.class, CreativeOperations.AD_CREATIVE_FIELDS); + } + + public AdCreative getAdCreative(String creativeId) { + requireAuthorization(); + return graphApi.fetchObject(creativeId, AdCreative.class, CreativeOperations.AD_CREATIVE_FIELDS); + } + + public String createAdCreative(String accountId, AdCreative creative) { + requireAuthorization(); + MultiValueMap data = mapCommonFields(creative); + return graphApi.publish(getAdAccountId(accountId), "adcreatives", data); + } + + public boolean renameAdCreative(String creativeId, String name) { + requireAuthorization(); + MultiValueMap data = new LinkedMultiValueMap(); + data.add("name", name); + return graphApi.update(creativeId, data); + } + + public void deleteAdCreative(String creativeId) { + requireAuthorization(); + restTemplate.delete(GraphApi.GRAPH_API_URL + creativeId); + } + + private MultiValueMap mapCommonFields(AdCreative creative) { + MultiValueMap data = new LinkedMultiValueMap(); + if (creative.getType() != null) { + data.add("object_type", creative.getType().name()); + } + if (creative.getName() != null) { + data.add("name", creative.getName()); + } + if (creative.getTitle() != null) { + data.add("title", creative.getTitle()); + } + if (creative.getStatus() != null) { + data.add("run_status", creative.getStatus().name()); + } + if (creative.getBody() != null) { + data.add("body", creative.getBody()); + } + if (creative.getObjectId() != null) { + data.add("object_id", creative.getObjectId()); + } + if (creative.getImageHash() != null) { + data.add("image_hash", creative.getImageHash()); + } + if (creative.getImageUrl() != null) { + data.add("image_url", creative.getImageUrl()); + } + if (creative.getLinkUrl() != null) { + data.add("link_url", creative.getLinkUrl()); + } + if (creative.getObjectStoryId() != null) { + data.add("object_story_id", creative.getObjectStoryId()); + } + if (creative.getObjectUrl() != null) { + data.add("object_url", creative.getObjectUrl()); + } + if (creative.getUrlTags() != null) { + data.add("url_tags", creative.getUrlTags()); + } + if (creative.getThumbnailUrl() != null) { + data.add("thumbnail_url", creative.getThumbnailUrl()); + } + return data; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/FacebookAdsTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/FacebookAdsTemplate.java new file mode 100644 index 000000000..df43cb05b --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/FacebookAdsTemplate.java @@ -0,0 +1,56 @@ +package org.springframework.social.facebook.api.ads.impl; + +import org.springframework.social.facebook.api.ads.*; +import org.springframework.social.facebook.api.impl.FacebookTemplate; + +/** + * This is the central class for interacting with Facebook Marketing API. + * + * @author Sebastian Górecki + */ +public class FacebookAdsTemplate extends FacebookTemplate implements FacebookAds { + + private AccountOperations accountOperations; + private CampaignOperations campaignOperations; + private AdSetOperations adSetOperations; + private CreativeOperations creativeOperations; + private AdOperations adOperations; + + public FacebookAdsTemplate() { + super(null); + initSubApis(); + } + + public FacebookAdsTemplate(String accessToken) { + super(accessToken); + initSubApis(); + } + + public AccountOperations accountOperations() { + return accountOperations; + } + + public CampaignOperations campaignOperations() { + return campaignOperations; + } + + public AdSetOperations adSetOperations() { + return adSetOperations; + } + + public CreativeOperations creativeOperations() { + return creativeOperations; + } + + public AdOperations adOperations() { + return adOperations; + } + + private void initSubApis() { + accountOperations = new AccountTemplate(this, isAuthorized()); + campaignOperations = new CampaignTemplate(this, isAuthorized()); + adSetOperations = new AdSetTemplate(this, getJsonMessageConverter().getObjectMapper(), isAuthorized()); + creativeOperations = new CreativeTemplate(this, getRestTemplate(), isAuthorized()); + adOperations = new AdTemplate(this, getRestTemplate(), getJsonMessageConverter().getObjectMapper(), isAuthorized()); + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountGroupMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountGroupMixin.java new file mode 100644 index 000000000..40e68e7b7 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountGroupMixin.java @@ -0,0 +1,23 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +/** + * Annotated mixin to add Jackson annotations to AdAccountGroup. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AdAccountGroupMixin extends FacebookObjectMixin { + + @JsonProperty("account_group_id") + String id; + + @JsonProperty("name") + String name; + + @JsonProperty("status") + int status; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountMixin.java new file mode 100644 index 000000000..ba5e4925f --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountMixin.java @@ -0,0 +1,199 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.springframework.social.facebook.api.ads.AdAccount.AccountStatus; +import org.springframework.social.facebook.api.ads.AdAccount.AgencyClientDeclaration; +import org.springframework.social.facebook.api.ads.AdAccount.Capability; +import org.springframework.social.facebook.api.ads.AdAccount.TaxStatus; +import org.springframework.social.facebook.api.ads.AdAccountGroup; +import org.springframework.social.facebook.api.ads.AdUser; +import org.springframework.social.facebook.api.impl.json.FacebookModule; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +import java.io.IOException; +import java.util.*; + +/** + * Annotated mixin to add Jackson annotations to AdAccount. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AdAccountMixin extends FacebookObjectMixin { + + @JsonProperty("id") + String id; + + @JsonProperty("account_groups") + List accountGroups; + + @JsonProperty("account_id") + long accountId; + + @JsonProperty("account_status") + AccountStatus status; + + @JsonProperty("age") + int age; + + @JsonProperty("agency_client_declaration") + AgencyClientDeclaration agencyClientDeclaration; + + @JsonProperty("amount_spent") + String amountSpent; + + @JsonProperty("balance") + String balance; + + @JsonProperty("business_city") + String businessCity; + + @JsonProperty("business_country_code") + String businessCountryCode; + + @JsonProperty("business_name") + String businessName; + + @JsonProperty("business_state") + String businessState; + + @JsonProperty("business_street") + String businessStreet; + + @JsonProperty("business_street2") + String businessStreet2; + + @JsonProperty("business_zip") + String businessZip; + + @JsonProperty("capabilities") + List capabilities; + + @JsonProperty("created_time") + Date createdTime; + + @JsonProperty("currency") + String currency; + + @JsonProperty("daily_spend_limit") + String dailySpendLimit; + + @JsonProperty("end_advertiser") + long endAdvertiser; + + @JsonProperty("funding_source") + String fundingSource; + + @JsonProperty("funding_source_details") + Map fundingSourceDetails; + + @JsonProperty("is_personal") + int isPersonal; + + @JsonProperty("media_agency") + long mediaAgency; + + @JsonProperty("name") + String name; + + @JsonProperty("offsite_pixels_tos_accepted") + boolean offsitePixelsTOSAccepted; + + @JsonProperty("partner") + long partner; + + @JsonProperty("spend_cap") + String spendCap; + + @JsonProperty("timezone_id") + int timezoneId; + + @JsonProperty("timezone_name") + String timezoneName; + + @JsonProperty("timezone_offset_hours_utc") + int timezoneOffsetHoursUTC; + + @JsonProperty("tos_accepted") + Map tosAccepted; + + @JsonProperty("users") + @JsonDeserialize(using = AdUserListDeserializer.class) + List users; + + @JsonProperty("tax_id_status") + TaxStatus taxStatus; + + private static class AdUserListDeserializer extends JsonDeserializer> { + @SuppressWarnings("unchecked") + @Override + public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new FacebookModule()); + jp.setCodec(mapper); + if (jp.hasCurrentToken()) { + try { + JsonNode dataNode = jp.readValueAs(JsonNode.class).get("data"); + if (dataNode != null) { + return (List) mapper.reader(new TypeReference>() { + }).readValue(dataNode); + } + } catch (IOException e) { + return Collections.emptyList(); + } + } + + return Collections.emptyList(); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract class AgencyClientDeclarationMixin extends FacebookObjectMixin { + + @JsonProperty("agency_representing_client") + private int agencyRepresentingClient; + + @JsonProperty("client_based_in_france") + private int clientBasedInFrance; + + @JsonProperty("client_city") + private String clientCity; + + @JsonProperty("client_country_code") + private String clientCountryCode; + + @JsonProperty("client_email_address") + private String clientEmailAddress; + + @JsonProperty("client_name") + private String clientName; + + @JsonProperty("client_postal_code") + private String clientPostalCode; + + @JsonProperty("client_province") + private String clientProvince; + + @JsonProperty("client_street") + private String clientStreet; + + @JsonProperty("client_street2") + private String clientStreet2; + + @JsonProperty("has_written_mandate_from_advertiser") + private int hasWrittenMandateFromAdvertiser; + + @JsonProperty("is_client_paying_invoices") + private int isClientPayingInvoices; + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCampaignMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCampaignMixin.java new file mode 100644 index 000000000..bca215d6f --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCampaignMixin.java @@ -0,0 +1,38 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.ads.AdCampaign.BuyingType; +import org.springframework.social.facebook.api.ads.AdCampaign.CampaignObjective; +import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +/** + * Annotated mixin to add Jackson annotations to AdCampaign. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +abstract public class AdCampaignMixin extends FacebookObjectMixin { + + @JsonProperty("id") + String id; + + @JsonProperty("account_id") + String accountId; + + @JsonProperty("buying_type") + BuyingType buyingType; + + @JsonProperty("campaign_group_status") + CampaignStatus status; + + @JsonProperty("name") + String name; + + @JsonProperty("objective") + CampaignObjective objective; + + @JsonProperty("spend_cap") + int spendCap; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCreativeMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCreativeMixin.java new file mode 100644 index 000000000..e6bba4f6e --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCreativeMixin.java @@ -0,0 +1,57 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.ads.AdCreative; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +/** + * Annotated mixin to add Jackson annotations to AdCreative. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +abstract public class AdCreativeMixin extends FacebookObjectMixin { + + @JsonProperty("id") + String id; + + @JsonProperty("object_type") + AdCreative.AdCreativeType type; + + @JsonProperty("name") + String name; + + @JsonProperty("title") + String title; + + @JsonProperty("run_status") + AdCreative.AdCreativeStatus status; + + @JsonProperty("body") + String body; + + @JsonProperty("object_id") + String objectId; + + @JsonProperty("image_hash") + String imageHash; + + @JsonProperty("image_url") + String imageUrl; + + @JsonProperty("link_url") + String linkUrl; + + @JsonProperty("object_story_id") + String objectStoryId; + + @JsonProperty("object_url") + String objectUrl; + + @JsonProperty("url_tags") + String urlTags; + + @JsonProperty("thumbnail_url") + String thumbnailUrl; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightActionMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightActionMixin.java new file mode 100644 index 000000000..f0215ab0d --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightActionMixin.java @@ -0,0 +1,19 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +/** + * Annotated mixin to add Jackson annotations to AdInsightAction. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AdInsightActionMixin extends FacebookObjectMixin { + @JsonProperty("action_type") + String actionType; + + @JsonProperty("value") + double value; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightMixin.java new file mode 100644 index 000000000..ab9c633c9 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightMixin.java @@ -0,0 +1,160 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.ads.AdInsightAction; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +import java.util.Date; +import java.util.List; + +/** + * Annotated mixin to add Jackson annotations to AdInsight. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AdInsightMixin extends FacebookObjectMixin { + // id fields + @JsonProperty("account_id") + String accountId; + + @JsonProperty("adgroup_id") + String adGroupId; + + @JsonProperty("campaign_id") + String campaignId; + + @JsonProperty("campaign_group_id") + String campaignGroupId; + + // name fields + @JsonProperty("account_name") + String accountName; + + @JsonProperty("adgroup_name") + String adGroupName; + + @JsonProperty("campaign_group_name") + String campaignGroupName; + + @JsonProperty("campaign_name") + String camapignName; + + // date fields + @JsonProperty("date_start") + Date dateStart; + + @JsonProperty("date_stop") + Date dateStop; + + @JsonProperty("campaign_start") + Date campaignStart; + + @JsonProperty("campaign_end") + Date campaignEnd; + + @JsonProperty("campaign_group_end") + Date campaignGroupEnd; + + // general fields + @JsonProperty("actions_per_impression") + double actionsPerImpression; + + @JsonProperty("clicks") + int clicks; + + @JsonProperty("unique_clicks") + int uniqueClicks; + + @JsonProperty("cost_per_result") + double costPerResult; + + @JsonProperty("cost_per_total_action") + double costPerTotalAction; + + @JsonProperty("cpc") + double costPerClick; + + @JsonProperty("cost_per_unique_click") + double costPerUniqueClick; + + @JsonProperty("cpm") + double cpm; + + @JsonProperty("cpp") + double cpp; + + @JsonProperty("ctr") + double ctr; + + @JsonProperty("unique_ctr") + double uniqueCtr; + + @JsonProperty("frequency") + double frequency; + + @JsonProperty("impressions") + int impressions; + + @JsonProperty("unique_impressions") + int uniqueImpressions; + + @JsonProperty("objective") + String objective; + + @JsonProperty("reach") + int reach; + + @JsonProperty("result_rate") + double resultRate; + + @JsonProperty("results") + int results; + + @JsonProperty("roas") + int roas; + + @JsonProperty("social_clicks") + int socialClicks; + + @JsonProperty("unique_social_clicks") + int uniqueSocialClicks; + + @JsonProperty("social_impressions") + int socialImpressions; + + @JsonProperty("unique_social_impressions") + int uniqueSocialImpressions; + + @JsonProperty("social_reach") + int socialReach; + + @JsonProperty("spend") + int spend; + + @JsonProperty("today_spend") + int todaySpend; + + @JsonProperty("total_action_value") + int totalActionValue; + + @JsonProperty("total_actions") + int totalActions; + + @JsonProperty("total_unique_actions") + int totalUniqueActions; + + // action and video fields + @JsonProperty("actions") + List actions; + + @JsonProperty("unique_actions") + List uniqueActions; + + @JsonProperty("cost_per_action_type") + List costPerActionType; + + @JsonProperty("video_start_actions") + List videoStartActions; +} \ No newline at end of file diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdMixin.java new file mode 100644 index 000000000..4a0c589a7 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdMixin.java @@ -0,0 +1,81 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.springframework.social.facebook.api.ads.Ad; +import org.springframework.social.facebook.api.ads.BidInfo; +import org.springframework.social.facebook.api.ads.BidType; +import org.springframework.social.facebook.api.ads.Targeting; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Annotated mixin to add Jackson annotations to Ad. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AdMixin { + + @JsonProperty("id") + String id; + + @JsonProperty("adgroup_status") + Ad.AdStatus status; + + @JsonProperty("name") + String name; + + @JsonProperty("bid_type") + BidType bidType; + + @JsonProperty("bid_info") + BidInfo bidInfo; + + @JsonProperty("account_id") + String accountId; + + @JsonProperty("campaign_id") + String adSetId; + + @JsonProperty("campaign_group_id") + String campaignId; + + @JsonProperty("creative") + @JsonDeserialize(using = CreativeIdDeserializer.class) + String creativeId; + + @JsonProperty("targeting") + Targeting targeting; + + @JsonProperty("created_time") + Date createdTime; + + @JsonProperty("updated_time") + Date updatedTime; + + private static class CreativeIdDeserializer extends JsonDeserializer { + @SuppressWarnings("unchecked") + @Override + public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (jp.getCurrentToken() == JsonToken.START_OBJECT) { + try { + Map map = jp.readValueAs(HashMap.class); + return map.get("id"); + } catch (IOException e) { + return null; + } + } + return null; + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdSetMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdSetMixin.java new file mode 100644 index 000000000..55681508c --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdSetMixin.java @@ -0,0 +1,75 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.ads.AdSet.AdSetStatus; +import org.springframework.social.facebook.api.ads.BidInfo; +import org.springframework.social.facebook.api.ads.BidType; +import org.springframework.social.facebook.api.ads.PromotedObject; +import org.springframework.social.facebook.api.ads.Targeting; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +import java.util.Date; +import java.util.List; + +/** + * Annotated mixin to add Jackson annotations to AdSet. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +abstract public class AdSetMixin extends FacebookObjectMixin { + @JsonProperty("id") + String id; + + @JsonProperty("account_id") + String accountId; + + @JsonProperty("campaign_group_id") + String campaignId; + + @JsonProperty("name") + String name; + + @JsonProperty("campaign_status") + AdSetStatus status; + + @JsonProperty("is_autobid") + boolean autobid; + + @JsonProperty("bid_info") + BidInfo bidInfo; + + @JsonProperty("bid_type") + BidType bidType; + + @JsonProperty("budget_remaining") + int budgetRemaining; + + @JsonProperty("daily_budget") + int dailyBudget; + + @JsonProperty("lifetime_budget") + int lifetimeBudget; + + @JsonProperty("creative_sequence") + List creativeSequence; + + @JsonProperty("promoted_object") + PromotedObject promotedObject; + + @JsonProperty("targeting") + Targeting targeting; + + @JsonProperty("start_time") + Date startTime; + + @JsonProperty("end_time") + Date endTime; + + @JsonProperty("created_time") + Date createdTime; + + @JsonProperty("updated_time") + Date updatedTime; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdUserMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdUserMixin.java new file mode 100644 index 000000000..34007dff6 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdUserMixin.java @@ -0,0 +1,29 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.ads.AdUser.AdUserPermission; +import org.springframework.social.facebook.api.ads.AdUser.AdUserRole; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +import java.util.List; + +/** + * Annotated mixin to add Jackson annotations to AdUser. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AdUserMixin extends FacebookObjectMixin { + @JsonProperty("id") + String id; + + @JsonProperty("name") + String name; + + @JsonProperty("permissions") + List permissions; + + @JsonProperty("role") + AdUserRole role; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingCityEntryMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingCityEntryMixin.java new file mode 100644 index 000000000..0587d5230 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingCityEntryMixin.java @@ -0,0 +1,22 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +/** + * Annotated mixin to add Jackson annotations to TargetingCityEntry. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class TargetingCityEntryMixin extends FacebookObjectMixin { + @JsonProperty("key") + String key; + + @JsonProperty("radius") + int radius; + + @JsonProperty("distance_unit") + String distanceUnit; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingEntryMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingEntryMixin.java new file mode 100644 index 000000000..cad5a8bb8 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingEntryMixin.java @@ -0,0 +1,17 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +/** + * Annotated mixin to add Jackson annotations to TargetingEntry. + * + * @author Sebastian Górecki + */ +public abstract class TargetingEntryMixin extends FacebookObjectMixin { + @JsonProperty("id") + long id; + + @JsonProperty("name") + String name; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationMixin.java new file mode 100644 index 000000000..a81dbafe9 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationMixin.java @@ -0,0 +1,66 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.social.facebook.api.ads.TargetingCityEntry; +import org.springframework.social.facebook.api.ads.TargetingLocation.LocationType; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** + * Annotated mixin to add Jackson annotations to TargetingLocation. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSerialize(using = TargetingLocationSerializer.class) +public abstract class TargetingLocationMixin extends FacebookObjectMixin { + @JsonProperty("countries") + List countries; + + @JsonProperty("regions") + @JsonDeserialize(using = ListOfMapsDeserializer.class) + List regions; + + @JsonProperty("cities") + List cities; + + @JsonProperty("zips") + @JsonDeserialize(using = ListOfMapsDeserializer.class) + List zips; + + @JsonProperty("location_types") + List locationTypes; + + private static class ListOfMapsDeserializer extends JsonDeserializer> { + @SuppressWarnings("unchecked") + @Override + public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (jp.getCurrentToken() == JsonToken.START_ARRAY) { + List retList = new ArrayList(); + try { + while (jp.nextToken() != JsonToken.END_ARRAY) { + HashMap regionMap = jp.readValueAs(HashMap.class); + retList.add(regionMap.get("key")); + } + return retList; + } catch (IOException e) { + return Collections.emptyList(); + } + } + return Collections.emptyList(); + } + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationSerializer.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationSerializer.java new file mode 100644 index 000000000..99b748146 --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationSerializer.java @@ -0,0 +1,49 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.springframework.social.facebook.api.ads.TargetingLocation; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Sebastian Górecki + */ +public class TargetingLocationSerializer extends JsonSerializer { + @Override + public void serialize(TargetingLocation location, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeStartObject(); + if (location.getCountries() != null) { + jgen.writeObjectField("countries", location.getCountries()); + } + if (location.getRegions() != null) { + serializeLocationValueEntries("regions", location.getRegions(), jgen); + } + if (location.getCities() != null) { + jgen.writeObjectField("cities", location.getCities()); + } + if (location.getZips() != null) { + serializeLocationValueEntries("zips", location.getZips(), jgen); + } + if (location.getLocationTypes() != null) { + jgen.writeObjectField("location_types", location.getLocationTypes()); + } + jgen.writeEndObject(); + } + + private void serializeLocationValueEntries(String entryName, List entries, JsonGenerator jgen) throws IOException { + jgen.writeFieldName(entryName); + jgen.writeStartArray(); + for (String entry : entries) { + Map map = new HashMap(); + map.put("key", entry); + jgen.writeObject(map); + } + jgen.writeEndArray(); + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingMixin.java new file mode 100644 index 000000000..87cb8d08a --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingMixin.java @@ -0,0 +1,84 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.social.facebook.api.ads.Targeting.*; +import org.springframework.social.facebook.api.ads.TargetingEntry; +import org.springframework.social.facebook.api.ads.TargetingLocation; +import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin; + +import java.util.List; + +/** + * Annotated mixin to add Jackson annotations to Targeting. + * + * @author Sebastian Górecki + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSerialize(using = TargetingSerializer.class) +public abstract class TargetingMixin extends FacebookObjectMixin { + // demographics + @JsonProperty("genders") + List genders; + + @JsonProperty("age_min") + Integer ageMin; + + @JsonProperty("age_max") + Integer ageMax; + + @JsonProperty("relationship_statuses") + List relationshipStatuses; + + @JsonProperty("interested_in") + List interestedIn; + + // location + @JsonProperty("geo_locations") + TargetingLocation geoLocations; + + @JsonProperty("excluded_geo_locations") + TargetingLocation excludedGeoLocations; + + // placement + @JsonProperty("page_types") + List pageTypes; + + // connections + @JsonProperty("connections") + List connections; + + @JsonProperty("excluded_connections") + List excludedConnections; + + @JsonProperty("friends_of_connections") + List friendsOfConnections; + + // interests + @JsonProperty("interests") + List interests; + + // behaviors + @JsonProperty("behaviors") + List behaviors; + + // education and workplace + @JsonProperty("education_schools") + List educationSchools; + + @JsonProperty("education_statuses") + List educationStatuses; + + @JsonProperty("college_years") + List collegeYears; + + @JsonProperty("education_majors") + List educationMajors; + + @JsonProperty("work_employers") + List workEmployers; + + @JsonProperty("work_positions") + List workPositions; +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingSerializer.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingSerializer.java new file mode 100644 index 000000000..88ba845ea --- /dev/null +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingSerializer.java @@ -0,0 +1,79 @@ +package org.springframework.social.facebook.api.ads.impl.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.springframework.social.facebook.api.ads.Targeting; + +import java.io.IOException; + +/** + * @author Sebastian Górecki + */ +public class TargetingSerializer extends JsonSerializer { + @Override + public void serialize(Targeting targeting, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeStartObject(); + + if (targeting.getGenders() != null) { + jgen.writeObjectField("genders", targeting.getGenders()); + } + if (targeting.getAgeMin() != null) { + jgen.writeObjectField("age_min", targeting.getAgeMin()); + } + if (targeting.getAgeMax() != null) { + jgen.writeObjectField("age_max", targeting.getAgeMax()); + } + if (targeting.getRelationshipStatuses() != null) { + jgen.writeObjectField("relationship_statuses", targeting.getRelationshipStatuses()); + } + if (targeting.getInterestedIn() != null) { + jgen.writeObjectField("interested_in", targeting.getInterestedIn()); + } + if (targeting.getGeoLocations() != null) { + jgen.writeObjectField("geo_locations", targeting.getGeoLocations()); + } + if (targeting.getExcludedGeoLocations() != null) { + jgen.writeObjectField("excluded_geo_locations", targeting.getExcludedGeoLocations()); + } + if (targeting.getPageTypes() != null) { + jgen.writeObjectField("page_types", targeting.getPageTypes()); + } + if (targeting.getConnections() != null) { + jgen.writeObjectField("connections", targeting.getConnections()); + } + if (targeting.getExcludedConnections() != null) { + jgen.writeObjectField("excluded_connections", targeting.getExcludedConnections()); + } + if (targeting.getFriendsOfConnections() != null) { + jgen.writeObjectField("friends_of_connections", targeting.getFriendsOfConnections()); + } + if (targeting.getInterests() != null) { + jgen.writeObjectField("interests", targeting.getInterests()); + } + if (targeting.getBehaviors() != null) { + jgen.writeObjectField("behaviors", targeting.getBehaviors()); + } + if (targeting.getEducationSchools() != null) { + jgen.writeObjectField("education_schools", targeting.getEducationSchools()); + } + if (targeting.getEducationStatuses() != null) { + jgen.writeObjectField("education_statuses", targeting.getEducationStatuses()); + } + if (targeting.getCollegeYears() != null) { + jgen.writeObjectField("college_years", targeting.getCollegeYears()); + } + if (targeting.getEducationMajors() != null) { + jgen.writeObjectField("education_majors", targeting.getEducationMajors()); + } + if (targeting.getWorkEmployers() != null) { + jgen.writeObjectField("work_employers", targeting.getWorkEmployers()); + } + if (targeting.getWorkPositions() != null) { + jgen.writeObjectField("work_positions", targeting.getWorkPositions()); + } + + jgen.writeEndObject(); + } +} diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java index e2d7816e6..c28619880 100644 --- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java @@ -17,18 +17,27 @@ import org.springframework.social.MissingAuthorizationException; -class AbstractFacebookOperations { +import java.util.Date; + +public class AbstractFacebookOperations { private final boolean isAuthorized; public AbstractFacebookOperations(boolean isAuthorized) { this.isAuthorized = isAuthorized; } - + + public String getAdAccountId(String id) { + return "act_" + id; + } + protected void requireAuthorization() { if (!isAuthorized) { throw new MissingAuthorizationException("facebook"); } } - + + public String getUnixTime(Date date) { + return date != null ? String.valueOf(date.getTime() / 1000L) : ""; + } } diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java index fc695c500..a6963c056 100644 --- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java @@ -36,6 +36,8 @@ import org.springframework.social.ServerException; import org.springframework.social.UncategorizedApiException; import org.springframework.social.facebook.api.FacebookError; +import org.springframework.social.facebook.api.InvalidParameterException; +import org.springframework.social.facebook.api.ads.InvalidCampaignStatusException; import org.springframework.web.client.DefaultResponseErrorHandler; import com.fasterxml.jackson.core.JsonFactory; @@ -71,7 +73,9 @@ void handleFacebookError(HttpStatus statusCode, FacebookError error) { throw new UncategorizedApiException(FACEBOOK_PROVIDER_ID, error.getMessage(), null); } else if (code == SERVICE) { throw new ServerException(FACEBOOK_PROVIDER_ID, error.getMessage()); - } else if (code == TOO_MANY_CALLS || code == USER_TOO_MANY_CALLS || code == EDIT_FEED_TOO_MANY_USER_CALLS || code == EDIT_FEED_TOO_MANY_USER_ACTION_CALLS) { + } else if (code == TOO_MANY_CALLS || code == USER_TOO_MANY_CALLS || code == EDIT_FEED_TOO_MANY_USER_CALLS || + code == EDIT_FEED_TOO_MANY_USER_ACTION_CALLS || code == USER_APP_TOO_MANY_CALLS || + code == AD_CREATION_LIMIT_EXCEEDED) { throw new RateLimitExceededException(FACEBOOK_PROVIDER_ID); } else if (code == PERMISSION_DENIED || isUserPermissionError(code)) { throw new InsufficientPermissionException(FACEBOOK_PROVIDER_ID); @@ -87,6 +91,10 @@ void handleFacebookError(HttpStatus statusCode, FacebookError error) { throw new DuplicateStatusException(FACEBOOK_PROVIDER_ID, error.getMessage()); } else if (code == DATA_OBJECT_NOT_FOUND || code == PATH_UNKNOWN) { throw new ResourceNotFoundException(FACEBOOK_PROVIDER_ID, error.getMessage()); + } else if (code == PARAM && error.getSubcode() != null && error.getSubcode() == 1487564) { + throw new InvalidCampaignStatusException(FACEBOOK_PROVIDER_ID, error.getUserMessage()); + } else if (code == PARAM && error.getSubcode() != null) { + throw new InvalidParameterException(FACEBOOK_PROVIDER_ID, error.getUserMessage()); } else { throw new UncategorizedApiException(FACEBOOK_PROVIDER_ID, error.getMessage(), null); } diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java index 3cca6c86c..fb44b1d83 100644 --- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java @@ -15,39 +15,16 @@ */ package org.springframework.social.facebook.api.impl; -import static org.springframework.social.facebook.api.impl.PagedListUtils.*; - -import java.io.IOException; -import java.net.URI; -import java.util.List; -import java.util.Map; - -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.springframework.http.*; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.social.NotAuthorizedException; import org.springframework.social.UncategorizedApiException; -import org.springframework.social.facebook.api.AchievementOperations; -import org.springframework.social.facebook.api.CommentOperations; -import org.springframework.social.facebook.api.EventOperations; -import org.springframework.social.facebook.api.Facebook; -import org.springframework.social.facebook.api.FeedOperations; -import org.springframework.social.facebook.api.FriendOperations; -import org.springframework.social.facebook.api.GroupOperations; -import org.springframework.social.facebook.api.ImageType; -import org.springframework.social.facebook.api.LikeOperations; -import org.springframework.social.facebook.api.MediaOperations; -import org.springframework.social.facebook.api.OpenGraphOperations; -import org.springframework.social.facebook.api.PageOperations; -import org.springframework.social.facebook.api.PagedList; -import org.springframework.social.facebook.api.PagingParameters; -import org.springframework.social.facebook.api.SocialContextOperations; -import org.springframework.social.facebook.api.TestUserOperations; -import org.springframework.social.facebook.api.UserOperations; +import org.springframework.social.facebook.api.*; import org.springframework.social.facebook.api.impl.json.FacebookModule; import org.springframework.social.oauth2.AbstractOAuth2ApiBinding; import org.springframework.social.oauth2.OAuth2Version; @@ -58,10 +35,12 @@ import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.type.CollectionType; -import com.fasterxml.jackson.databind.type.TypeFactory; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Map; + +import static org.springframework.social.facebook.api.impl.PagedListUtils.getPagedListParameters; /** *

This is the central class for interacting with Facebook.

@@ -72,38 +51,39 @@ * Attempts to perform secured operations through such an instance, however, * will result in {@link NotAuthorizedException} being thrown. *

+ * * @author Craig Walls */ public class FacebookTemplate extends AbstractOAuth2ApiBinding implements Facebook { private String appId; - + private AchievementOperations achievementOperations; - + private UserOperations userOperations; - + private FriendOperations friendOperations; - + private FeedOperations feedOperations; - + private GroupOperations groupOperations; - + private CommentOperations commentOperations; - + private LikeOperations likeOperations; - + private EventOperations eventOperations; - + private MediaOperations mediaOperations; - + private PageOperations pageOperations; - + private OpenGraphOperations openGraphOperations; - + private SocialContextOperations socialContextOperations; - + private TestUserOperations testUserOperations; - + private ObjectMapper objectMapper; private String applicationNamespace; @@ -111,6 +91,7 @@ public class FacebookTemplate extends AbstractOAuth2ApiBinding implements Facebo /** * Create a new instance of FacebookTemplate. * This constructor creates the FacebookTemplate using a given access token. + * * @param accessToken An access token given by Facebook after a successful OAuth 2 authentication (or through Facebook's JS library). */ public FacebookTemplate(String accessToken) { @@ -120,14 +101,14 @@ public FacebookTemplate(String accessToken) { public FacebookTemplate(String accessToken, String applicationNamespace) { this(accessToken, applicationNamespace, null); } - + public FacebookTemplate(String accessToken, String applicationNamespace, String appId) { super(accessToken); this.applicationNamespace = applicationNamespace; this.appId = appId; initialize(); } - + @Override public void setRequestFactory(ClientHttpRequestFactory requestFactory) { // Wrap the request factory with a BufferingClientHttpRequestFactory so that the error handler can do repeat reads on the response.getBody() @@ -137,11 +118,11 @@ public void setRequestFactory(ClientHttpRequestFactory requestFactory) { public AchievementOperations achievementOperations() { return achievementOperations; } - + public UserOperations userOperations() { return userOperations; } - + public LikeOperations likeOperations() { return likeOperations; } @@ -149,11 +130,11 @@ public LikeOperations likeOperations() { public FriendOperations friendOperations() { return friendOperations; } - + public FeedOperations feedOperations() { return feedOperations; } - + public GroupOperations groupOperations() { return groupOperations; } @@ -161,39 +142,39 @@ public GroupOperations groupOperations() { public CommentOperations commentOperations() { return commentOperations; } - + public EventOperations eventOperations() { return eventOperations; } - + public MediaOperations mediaOperations() { return mediaOperations; } - + public PageOperations pageOperations() { return pageOperations; } - + public RestOperations restOperations() { return getRestTemplate(); } - + public OpenGraphOperations openGraphOperations() { return openGraphOperations; } - + public SocialContextOperations socialContextOperations() { return socialContextOperations; } - + public String getApplicationNamespace() { return applicationNamespace; } - + public TestUserOperations testUserOperations() { return testUserOperations; } - + // low-level Graph API operations public T fetchObject(String objectId, Class type) { URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId).build(); @@ -202,10 +183,10 @@ public T fetchObject(String objectId, Class type) { public T fetchObject(String objectId, Class type, String... fields) { MultiValueMap queryParameters = new LinkedMultiValueMap(); - if(fields.length > 0) { + if (fields.length > 0) { String joinedFields = join(fields); queryParameters.set("fields", joinedFields); - } + } return fetchObject(objectId, type, queryParameters); } @@ -216,10 +197,10 @@ public T fetchObject(String objectId, Class type, MultiValueMap PagedList fetchConnections(String objectId, String connectionType, Class type, String... fields) { MultiValueMap queryParameters = new LinkedMultiValueMap(); - if(fields.length > 0) { + if (fields.length > 0) { String joinedFields = join(fields); queryParameters.set("fields", joinedFields); - } + } return fetchConnections(objectId, connectionType, type, queryParameters); } @@ -238,23 +219,23 @@ public PagedList fetchPagedConnections(String objectId, String connection } public PagedList fetchConnections(String objectId, String connectionType, Class type, MultiValueMap queryParameters, String... fields) { - if(fields.length > 0) { + if (fields.length > 0) { String joinedFields = join(fields); queryParameters.set("fields", joinedFields); } return fetchPagedConnections(objectId, connectionType, type, queryParameters); } - + private PagedList pagify(Class type, JsonNode jsonNode) { List data = deserializeDataList(jsonNode.get("data"), type); if (!jsonNode.has("paging")) { return new PagedList(data, null, null); } - + JsonNode pagingNode = jsonNode.get("paging"); PagingParameters previousPage = getPagedListParameters(pagingNode, "previous"); PagingParameters nextPage = getPagedListParameters(pagingNode, "next"); - + Integer totalCount = null; if (jsonNode.has("summary")) { JsonNode summaryNode = jsonNode.get("summary"); @@ -262,20 +243,20 @@ private PagedList pagify(Class type, JsonNode jsonNode) { totalCount = summaryNode.get("total_count").intValue(); } } - + return new PagedList(data, previousPage, nextPage, totalCount); } public byte[] fetchImage(String objectId, String connectionType, ImageType type) { URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + "/" + connectionType + "?type=" + type.toString().toLowerCase()).build(); ResponseEntity response = getRestTemplate().getForEntity(uri, byte[].class); - if(response.getStatusCode() == HttpStatus.FOUND) { + if (response.getStatusCode() == HttpStatus.FOUND) { throw new UnsupportedOperationException("Attempt to fetch image resulted in a redirect which could not be followed. Add Apache HttpComponents HttpClient to the classpath " + "to be able to follow redirects."); } return response.getBody(); } - + @SuppressWarnings("unchecked") public String publish(String objectId, String connectionType, MultiValueMap data) { MultiValueMap requestData = new LinkedMultiValueMap(data); @@ -283,38 +264,45 @@ public String publish(String objectId, String connectionType, MultiValueMap response = getRestTemplate().postForObject(uri, requestData, Map.class); return (String) response.get("id"); } - + public void post(String objectId, MultiValueMap data) { post(objectId, null, data); } - + public void post(String objectId, String connectionType, MultiValueMap data) { String connectionPath = connectionType != null ? "/" + connectionType : ""; URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + connectionPath).build(); getRestTemplate().postForObject(uri, new LinkedMultiValueMap(data), String.class); } - + + public boolean update(String objectId, MultiValueMap data) { + MultiValueMap requestData = new LinkedMultiValueMap(data); + URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId).build(); + Map response = getRestTemplate().postForObject(uri, requestData, Map.class); + return (Boolean) response.get("success"); + } + public void delete(String objectId) { LinkedMultiValueMap deleteRequest = new LinkedMultiValueMap(); deleteRequest.set("method", "delete"); URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId).build(); getRestTemplate().postForObject(uri, deleteRequest, String.class); } - + public void delete(String objectId, String connectionType) { LinkedMultiValueMap deleteRequest = new LinkedMultiValueMap(); deleteRequest.set("method", "delete"); URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + "/" + connectionType).build(); getRestTemplate().postForObject(uri, deleteRequest, String.class); } - + public void delete(String objectId, String connectionType, MultiValueMap data) { data.set("method", "delete"); URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + "/" + connectionType).build(); HttpEntity> entity = new HttpEntity>(data, new HttpHeaders()); getRestTemplate().exchange(uri, HttpMethod.POST, entity, String.class); } - + // AbstractOAuth2ApiBinding hooks @Override protected OAuth2Version getOAuth2Version() { @@ -329,19 +317,19 @@ protected void configureRestTemplate(RestTemplate restTemplate) { @Override protected MappingJackson2HttpMessageConverter getJsonMessageConverter() { MappingJackson2HttpMessageConverter converter = super.getJsonMessageConverter(); - objectMapper = new ObjectMapper(); + objectMapper = new ObjectMapper(); objectMapper.registerModule(new FacebookModule()); - converter.setObjectMapper(objectMapper); + converter.setObjectMapper(objectMapper); return converter; } - + // private helpers private void initialize() { // Wrap the request factory with a BufferingClientHttpRequestFactory so that the error handler can do repeat reads on the response.getBody() super.setRequestFactory(ClientHttpRequestFactorySelector.bufferRequests(getRestTemplate().getRequestFactory())); initSubApis(); } - + private void initSubApis() { achievementOperations = new AchievementTemplate(this); openGraphOperations = new OpenGraphTemplate(this); @@ -357,7 +345,7 @@ private void initSubApis() { testUserOperations = new TestUserTemplate(getRestTemplate(), appId); socialContextOperations = new SocialContextTemplate(getRestTemplate()); } - + @SuppressWarnings("unchecked") private List deserializeDataList(JsonNode jsonNode, final Class elementType) { try { @@ -367,10 +355,10 @@ private List deserializeDataList(JsonNode jsonNode, final Class elemen throw new UncategorizedApiException("facebook", "Error deserializing data from Facebook: " + e.getMessage(), e); } } - + private String join(String[] strings) { StringBuilder builder = new StringBuilder(); - if(strings.length > 0) { + if (strings.length > 0) { builder.append(strings[0]); for (int i = 1; i < strings.length; i++) { builder.append("," + strings[i]); @@ -378,5 +366,5 @@ private String join(String[] strings) { } return builder.toString(); } - + } diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java index ade1c0078..2907f264c 100644 --- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java @@ -15,61 +15,12 @@ */ package org.springframework.social.facebook.api.impl.json; -import org.springframework.social.facebook.api.Account; -import org.springframework.social.facebook.api.Achievement; -import org.springframework.social.facebook.api.AchievementType; -import org.springframework.social.facebook.api.Action; -import org.springframework.social.facebook.api.Album; -import org.springframework.social.facebook.api.ApplicationReference; -import org.springframework.social.facebook.api.Comment; -import org.springframework.social.facebook.api.CoverPhoto; -import org.springframework.social.facebook.api.Currency; -import org.springframework.social.facebook.api.Device; -import org.springframework.social.facebook.api.EducationExperience; -import org.springframework.social.facebook.api.Engagement; -import org.springframework.social.facebook.api.Event; -import org.springframework.social.facebook.api.EventInvitee; -import org.springframework.social.facebook.api.Experience; -import org.springframework.social.facebook.api.FamilyMember; -import org.springframework.social.facebook.api.FriendList; -import org.springframework.social.facebook.api.Group; -import org.springframework.social.facebook.api.GroupMemberReference; -import org.springframework.social.facebook.api.GroupMembership; -import org.springframework.social.facebook.api.ImageSource; -import org.springframework.social.facebook.api.Invitation; -import org.springframework.social.facebook.api.Location; -import org.springframework.social.facebook.api.MailingAddress; -import org.springframework.social.facebook.api.MessageTag; -import org.springframework.social.facebook.api.Page; -import org.springframework.social.facebook.api.PageParking; -import org.springframework.social.facebook.api.PagePaymentOptions; -import org.springframework.social.facebook.api.PageRestaurantServices; -import org.springframework.social.facebook.api.PageRestaurantSpecialties; -import org.springframework.social.facebook.api.PaymentPricePoint; -import org.springframework.social.facebook.api.PaymentPricePoints; -import org.springframework.social.facebook.api.Photo; +import org.springframework.social.facebook.api.*; import org.springframework.social.facebook.api.Photo.Image; -import org.springframework.social.facebook.api.Place; -import org.springframework.social.facebook.api.PlaceTag; -import org.springframework.social.facebook.api.Post; -import org.springframework.social.facebook.api.PostProperty; -import org.springframework.social.facebook.api.ProfilePictureSource; -import org.springframework.social.facebook.api.Reference; -import org.springframework.social.facebook.api.RestaurantServices; -import org.springframework.social.facebook.api.SecuritySettings; -import org.springframework.social.facebook.api.StoryAttachment; -import org.springframework.social.facebook.api.Tag; -import org.springframework.social.facebook.api.TestUser; -import org.springframework.social.facebook.api.User; -import org.springframework.social.facebook.api.UserIdForApp; -import org.springframework.social.facebook.api.UserInvitableFriend; -import org.springframework.social.facebook.api.UserTaggableFriend; -import org.springframework.social.facebook.api.Video; import org.springframework.social.facebook.api.Video.VideoFormat; -import org.springframework.social.facebook.api.VideoUploadLimits; -import org.springframework.social.facebook.api.VoipInfo; -import org.springframework.social.facebook.api.WorkEntry; import org.springframework.social.facebook.api.WorkEntry.Project; +import org.springframework.social.facebook.api.ads.*; +import org.springframework.social.facebook.api.ads.impl.json.*; import org.springframework.social.facebook.api.impl.json.PhotoMixin.ImageMixin; import org.springframework.social.facebook.api.impl.json.VideoMixin.VideoFormatMixin; import org.springframework.social.facebook.api.impl.json.WorkEntryMixin.ProjectMixin; @@ -170,6 +121,25 @@ public void setupModule(SetupContext context) { context.setMixInAnnotations(VideoUploadLimits.class, VideoUploadLimitsMixin.class); context.setMixInAnnotations(ProfilePictureSource.class, ProfilePictureSourceMixin.class); - + + + context.setMixInAnnotations(AdAccountGroup.class, AdAccountGroupMixin.class); + context.setMixInAnnotations(AdAccount.AgencyClientDeclaration.class, AdAccountMixin.AgencyClientDeclarationMixin.class); + context.setMixInAnnotations(AdUser.class, AdUserMixin.class); + + context.setMixInAnnotations(AdInsightAction.class, AdInsightActionMixin.class); + context.setMixInAnnotations(AdInsight.class, AdInsightMixin.class); + + context.setMixInAnnotations(AdAccount.class, AdAccountMixin.class); + context.setMixInAnnotations(AdCampaign.class, AdCampaignMixin.class); + + context.setMixInAnnotations(Targeting.class, TargetingMixin.class); + context.setMixInAnnotations(TargetingCityEntry.class, TargetingCityEntryMixin.class); + context.setMixInAnnotations(TargetingEntry.class, TargetingEntryMixin.class); + context.setMixInAnnotations(TargetingLocation.class, TargetingLocationMixin.class); + context.setMixInAnnotations(AdSet.class, AdSetMixin.class); + + context.setMixInAnnotations(AdCreative.class, AdCreativeMixin.class); + context.setMixInAnnotations(Ad.class, AdMixin.class); } } diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java index 823687680..a8274c36d 100644 --- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java +++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java @@ -23,9 +23,9 @@ * @author Craig Walls */ @JsonIgnoreProperties(ignoreUnknown = true) -abstract class FacebookObjectMixin { +public abstract class FacebookObjectMixin { @JsonAnySetter - abstract void add(String key, Object value); + protected abstract void add(String key, Object value); } diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java index 2c5444bbc..2933a90b8 100644 --- a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java +++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java @@ -83,7 +83,18 @@ public void code100NonExistingFields() throws Exception { facebook.fetchObject("me", User.class); fail(); } - + + @Test(expected = InvalidParameterException.class) + public void code100InvalidParameter() throws Exception { + mockServer.expect(requestTo(fbUrl("me/feed"))) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withStatus(HttpStatus.BAD_REQUEST).body(jsonResource("error-100-invalidParameter")).contentType(MediaType.APPLICATION_JSON)); + facebook.feedOperations().post("me", ""); + fail(); + } + + @Test(expected=InvalidAuthorizationException.class) public void code104NoAccessToken() throws Exception { mockServer.expect(requestTo(fbUrl("me"))) @@ -182,6 +193,5 @@ public void code803UnknownPath() throws Exception { .andRespond(withStatus(HttpStatus.BAD_REQUEST).body(jsonResource("error-2500-bogusPath")).contentType(MediaType.APPLICATION_JSON)); facebook.fetchObject("me", User.class); fail(); - } - + } } diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AbstractFacebookAdsApiTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AbstractFacebookAdsApiTest.java new file mode 100644 index 000000000..0edd3e18f --- /dev/null +++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AbstractFacebookAdsApiTest.java @@ -0,0 +1,48 @@ +package org.springframework.social.facebook.api.ads; + +import org.junit.Before; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.social.facebook.api.ads.impl.FacebookAdsTemplate; +import org.springframework.test.web.client.MockRestServiceServer; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * @author Sebastian Górecki + */ +public class AbstractFacebookAdsApiTest { + protected static final String ACCESS_TOKEN = "someAccessToken"; + private static final DateFormat FB_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH); + protected static final double EPSILON = 0.000000000001; + protected FacebookAdsTemplate facebookAds; + protected FacebookAdsTemplate unauthorizedFacebookAds; + protected MockRestServiceServer mockServer; + protected MockRestServiceServer unauthorizedMockServer; + + @Before + public void setUp() throws Exception { + facebookAds = new FacebookAdsTemplate(ACCESS_TOKEN); + mockServer = MockRestServiceServer.createServer(facebookAds.getRestTemplate()); + + unauthorizedFacebookAds = new FacebookAdsTemplate(); + unauthorizedMockServer = MockRestServiceServer.createServer(unauthorizedFacebookAds.getRestTemplate()); + } + + protected Resource jsonResource(String filename) { + return new ClassPathResource(filename + ".json", getClass()); + } + + protected Date toDate(String dateString) { + try { + return FB_DATE_FORMAT.parse(dateString); + } catch (ParseException e) { + return null; + } + } + +} diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AccountTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AccountTemplateTest.java new file mode 100644 index 000000000..b86f10409 --- /dev/null +++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AccountTemplateTest.java @@ -0,0 +1,522 @@ +package org.springframework.social.facebook.api.ads; + +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.social.NotAuthorizedException; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.AdAccount.Capability; +import org.springframework.social.facebook.api.ads.AdAccount.TaxStatus; +import org.springframework.social.facebook.api.ads.AdCampaign.BuyingType; +import org.springframework.social.facebook.api.ads.AdCampaign.CampaignObjective; +import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus; +import org.springframework.social.facebook.api.ads.AdUser.AdUserPermission; +import org.springframework.social.facebook.api.ads.AdUser.AdUserRole; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * @author Sebastian Górecki + */ +public class AccountTemplateTest extends AbstractFacebookAdsApiTest { + + private static final String GET_ADACCOUNT_REQUEST_URI = "https://graph.facebook.com/v2.3/act_123456789?fields=id%2Caccount_id%2Caccount_status%2Cage%2Camount_spent%2Cbalance%2Cbusiness_city%2Cbusiness_country_code%2Cbusiness_name%2Cbusiness_state%2Cbusiness_street%2Cbusiness_street2%2Cbusiness_zip%2Ccapabilities%2Ccreated_time%2Ccurrency%2Cdaily_spend_limit%2Cend_advertiser%2Cfunding_source%2Cfunding_source_details%2Cis_personal%2Cmedia_agency%2Cname%2Coffsite_pixels_tos_accepted%2Cpartner%2Cspend_cap%2Ctimezone_id%2Ctimezone_name%2Ctimezone_offset_hours_utc%2Cusers%2Ctax_id_status"; + private static final String GET_ADACCOUNTS_REQUEST_URI = "https://graph.facebook.com/v2.3/1234/adaccounts?fields=id%2Caccount_id%2Caccount_status%2Cage%2Camount_spent%2Cbalance%2Cbusiness_city%2Cbusiness_country_code%2Cbusiness_name%2Cbusiness_state%2Cbusiness_street%2Cbusiness_street2%2Cbusiness_zip%2Ccapabilities%2Ccreated_time%2Ccurrency%2Cdaily_spend_limit%2Cend_advertiser%2Cfunding_source%2Cfunding_source_details%2Cis_personal%2Cmedia_agency%2Cname%2Coffsite_pixels_tos_accepted%2Cpartner%2Cspend_cap%2Ctimezone_id%2Ctimezone_name%2Ctimezone_offset_hours_utc%2Cusers%2Ctax_id_status"; + private static final String GET_ADACCOUNT_USERS_REQUEST_URI = "https://graph.facebook.com/v2.3/act_123456789/users"; + private static final String GET_ADACCOUNT_INSIGHT = "https://graph.facebook.com/v2.3/act_123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions"; + + + @Test + public void getAccounts() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNTS_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-accounts"), MediaType.APPLICATION_JSON)); + + List adAccounts = facebookAds.accountOperations().getAdAccounts("1234"); + assertEquals(2, adAccounts.size()); + assertAdAccountsFields(adAccounts); + } + + @Test(expected = NotAuthorizedException.class) + public void getAccounts_unauthorized() throws Exception { + unauthorizedFacebookAds.accountOperations().getAdAccounts("1234"); + } + + @Test + public void getAdAccount() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertEquals(AdAccount.AccountStatus.ACTIVE, adAccount.getStatus()); + assertEquals(1, adAccount.getUsers().size()); + assertEquals("1234", adAccount.getUsers().get(0).getId()); + assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccount.getUsers().get(0).getPermissions().get(0)); + assertEquals(AdUserPermission.ADMANAGER_READ, adAccount.getUsers().get(0).getPermissions().get(1)); + assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccount.getUsers().get(0).getPermissions().get(2)); + assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole()); + } + + @Test + public void getAdAccount_withStatusTemporarilyUnavailable() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-temporarily-unavailable"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertEquals(AdAccount.AccountStatus.TEMPORARILY_UNAVAILABLE, adAccount.getStatus()); + assertEquals(1, adAccount.getUsers().size()); + assertEquals("1234", adAccount.getUsers().get(0).getId()); + assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccount.getUsers().get(0).getPermissions().get(0)); + assertEquals(AdUserPermission.ADMANAGER_READ, adAccount.getUsers().get(0).getPermissions().get(1)); + assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccount.getUsers().get(0).getPermissions().get(2)); + assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole()); + } + + @Test + public void getAdAccount_withUnknownCapabilities() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-unknown-capabilities"), MediaType.APPLICATION_JSON)); + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertEquals("act_123456789", adAccount.getId()); + assertEquals(123456789, adAccount.getAccountId()); + assertEquals(4, adAccount.getCapabilities().size()); + assertEquals(Capability.UNKNOWN, adAccount.getCapabilities().get(0)); + assertEquals(Capability.UNKNOWN, adAccount.getCapabilities().get(1)); + assertEquals(Capability.UNKNOWN, adAccount.getCapabilities().get(2)); + assertEquals(Capability.PREMIUM, adAccount.getCapabilities().get(3)); + } + + @Test + public void getAdAccount_withEmptyUsers() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-no-users"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertEquals(0, adAccount.getUsers().size()); + } + + @Test + public void getAdAccount_withFewUsers() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-with-few-users"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertEquals(2, adAccount.getUsers().size()); + assertEquals("1234", adAccount.getUsers().get(0).getId()); + assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccount.getUsers().get(0).getPermissions().get(0)); + assertEquals(AdUserPermission.ADMANAGER_READ, adAccount.getUsers().get(0).getPermissions().get(1)); + assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccount.getUsers().get(0).getPermissions().get(2)); + assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole()); + assertEquals("3421", adAccount.getUsers().get(1).getId()); + assertEquals(AdUserPermission.BILLING_WRITE, adAccount.getUsers().get(1).getPermissions().get(0)); + assertEquals(AdUserPermission.REPORTS, adAccount.getUsers().get(1).getPermissions().get(1)); + assertEquals(AdUserRole.ANALYST, adAccount.getUsers().get(1).getRole()); + } + + @Test + public void getAdAccount_userWithNoPermissions() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-without-permission"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertEquals(1, adAccount.getUsers().size()); + assertEquals("1234", adAccount.getUsers().get(0).getId()); + assertEquals(0, adAccount.getUsers().get(0).getPermissions().size()); + assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole()); + } + + @Test + public void getAdAccount_withAgencyClientDeclaration() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-with-agency"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertEquals(1, adAccount.getAgencyClientDeclaration().getAgencyRepresentingClient()); + assertEquals(0, adAccount.getAgencyClientDeclaration().getClientBasedInFrance()); + assertEquals("Warsaw", adAccount.getAgencyClientDeclaration().getClientCity()); + assertEquals("PL", adAccount.getAgencyClientDeclaration().getClientCountryCode()); + assertEquals("example@example.com", adAccount.getAgencyClientDeclaration().getClientEmailAddress()); + assertEquals("Some client", adAccount.getAgencyClientDeclaration().getClientName()); + assertEquals("66-777", adAccount.getAgencyClientDeclaration().getClientPostalCode()); + assertEquals("malopolska", adAccount.getAgencyClientDeclaration().getClientProvince()); + assertEquals("Marszalkowska", adAccount.getAgencyClientDeclaration().getClientStreet()); + assertEquals("1A", adAccount.getAgencyClientDeclaration().getClientStreet2()); + assertEquals(0, adAccount.getAgencyClientDeclaration().getHasWrittenMandateFromAdvertiser()); + assertEquals(1, adAccount.getAgencyClientDeclaration().getIsClientPayingInvoices()); + } + + @Test + public void getAdAccount_withFundingDetails() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-with-funding-details"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertTrue(adAccount.getFundingSourceDetails().containsKey("id")); + assertEquals("12345678987654321", adAccount.getFundingSourceDetails().get("id")); + assertTrue(adAccount.getFundingSourceDetails().containsKey("display_string")); + assertEquals("Visa *0001", adAccount.getFundingSourceDetails().get("display_string")); + assertTrue(adAccount.getFundingSourceDetails().containsKey("type")); + assertEquals(1, adAccount.getFundingSourceDetails().get("type")); + } + + @Test + public void getAdAccount_withTosAccepted() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-with-tos-accepted"), MediaType.APPLICATION_JSON)); + + AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789"); + assertAdAccountFields(adAccount); + assertTrue(adAccount.getTosAccepted().containsKey("206760949512025")); + assertEquals((Integer) 1, adAccount.getTosAccepted().get("206760949512025")); + assertTrue(adAccount.getTosAccepted().containsKey("215449065224656")); + assertEquals((Integer) 1, adAccount.getTosAccepted().get("215449065224656")); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdAccount_unauthorized() throws Exception { + unauthorizedFacebookAds.accountOperations().getAdAccount("123456789"); + } + + @Test + public void getAdAccountCampaigns() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-campaigns"), MediaType.APPLICATION_JSON)); + PagedList campaigns = facebookAds.accountOperations().getAdAccountCampaigns("123456789"); + assertEquals(3, campaigns.size()); + assertEquals("601123456789", campaigns.get(0).getId()); + assertEquals("123456789", campaigns.get(0).getAccountId()); + assertEquals(BuyingType.AUCTION, campaigns.get(0).getBuyingType()); + assertEquals(CampaignStatus.ACTIVE, campaigns.get(0).getStatus()); + assertEquals("Campaign #1", campaigns.get(0).getName()); + assertEquals(CampaignObjective.POST_ENGAGEMENT, campaigns.get(0).getObjective()); + assertEquals(0, campaigns.get(0).getSpendCap()); + assertEquals("602123456789", campaigns.get(1).getId()); + assertEquals("123456789", campaigns.get(1).getAccountId()); + assertEquals(BuyingType.FIXED_CPM, campaigns.get(1).getBuyingType()); + assertEquals(CampaignStatus.PAUSED, campaigns.get(1).getStatus()); + assertEquals("Campaign #2", campaigns.get(1).getName()); + assertEquals(CampaignObjective.NONE, campaigns.get(1).getObjective()); + assertEquals(0, campaigns.get(1).getSpendCap()); + assertEquals("603123456789", campaigns.get(2).getId()); + assertEquals("123456789", campaigns.get(2).getAccountId()); + assertEquals(BuyingType.RESERVED, campaigns.get(2).getBuyingType()); + assertEquals(CampaignStatus.ARCHIVED, campaigns.get(2).getStatus()); + assertEquals("Campaign #3", campaigns.get(2).getName()); + assertEquals(CampaignObjective.WEBSITE_CONVERSIONS, campaigns.get(2).getObjective()); + assertEquals(50000, campaigns.get(2).getSpendCap()); + } + + @Test + public void getAccountUsers() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_USERS_REQUEST_URI)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-users"), MediaType.APPLICATION_JSON)); + + List adAccountUsers = facebookAds.accountOperations().getAdAccountUsers("123456789"); + assertEquals(3, adAccountUsers.size()); + assertEquals("123456789", adAccountUsers.get(0).getId()); + assertEquals("Account #1", adAccountUsers.get(0).getName()); + assertEquals(6, adAccountUsers.get(0).getPermissions().size()); + assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccountUsers.get(0).getPermissions().get(0)); + assertEquals(AdUserPermission.ADMANAGER_READ, adAccountUsers.get(0).getPermissions().get(1)); + assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccountUsers.get(0).getPermissions().get(2)); + assertEquals(AdUserPermission.BILLING_READ, adAccountUsers.get(0).getPermissions().get(3)); + assertEquals(AdUserPermission.BILLING_WRITE, adAccountUsers.get(0).getPermissions().get(4)); + assertEquals(AdUserPermission.REPORTS, adAccountUsers.get(0).getPermissions().get(5)); + assertEquals(AdUserRole.ADMINISTRATOR, adAccountUsers.get(0).getRole()); + assertEquals("987654321", adAccountUsers.get(1).getId()); + assertEquals("Account #2", adAccountUsers.get(1).getName()); + assertEquals(1, adAccountUsers.get(1).getPermissions().size()); + assertEquals(AdUserPermission.REPORTS, adAccountUsers.get(1).getPermissions().get(0)); + assertEquals(AdUserRole.ANALYST, adAccountUsers.get(1).getRole()); + assertEquals("1122334455", adAccountUsers.get(2).getId()); + assertEquals("Account #3", adAccountUsers.get(2).getName()); + assertEquals(2, adAccountUsers.get(2).getPermissions().size()); + assertEquals(AdUserPermission.ADMANAGER_READ, adAccountUsers.get(2).getPermissions().get(0)); + assertEquals(AdUserPermission.BILLING_READ, adAccountUsers.get(2).getPermissions().get(1)); + assertEquals(AdUserRole.SALES, adAccountUsers.get(2).getRole()); + } + + @Test(expected = NotAuthorizedException.class) + public void getAccountUsers_unauthorized() throws Exception { + unauthorizedFacebookAds.accountOperations().getAdAccountUsers("123456789"); + } + + @Test + public void addUserToAccount() throws Exception { + String requestBody = "uid=123456&role=1002"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/users/")) + .andExpect(method(POST)) + .andExpect(content().string(requestBody)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON)); + facebookAds.accountOperations().addUserToAdAccount("123456789", "123456", AdUserRole.ADVERTISER); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void addUserToAccount_unauthorized() throws Exception { + unauthorizedFacebookAds.accountOperations().addUserToAdAccount("123456789", "123456", AdUserRole.ADVERTISER); + } + + @Test + public void deleteUserFromAccount() throws Exception { + String requestBody = "method=delete"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/users/123456")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON)); + facebookAds.accountOperations().deleteUserFromAdAccount("123456789", "123456"); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void deleteUserFromAccount_unauthorized() throws Exception { + unauthorizedFacebookAds.accountOperations().deleteUserFromAdAccount("123456789", "123456"); + } + + @Test + public void getAccountInsight() throws Exception { + mockServer.expect(requestTo(GET_ADACCOUNT_INSIGHT)) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-insights"), MediaType.APPLICATION_JSON)); + + AdInsight insight = facebookAds.accountOperations().getAdAccountInsight("123456789"); + assertEquals("123456789", insight.getAccountId()); + assertEquals("Account Test Name #1", insight.getAccountName()); + assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON); + assertEquals(8, insight.getClicks()); + assertEquals(5, insight.getUniqueClicks()); + assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON); + assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON); + assertEquals(0.25, insight.getCostPerClick(), EPSILON); + assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON); + assertEquals(10.695187165775, insight.getCpm(), EPSILON); + assertEquals(10.869565217391, insight.getCpp(), EPSILON); + assertEquals(4.2780748663102, insight.getCtr(), EPSILON); + assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON); + assertEquals(1.0163043478261, insight.getFrequency(), EPSILON); + assertEquals(187, insight.getImpressions()); + assertEquals(184, insight.getUniqueImpressions()); + assertEquals(184, insight.getReach()); + assertEquals(1.6042780748663, insight.getResultRate(), EPSILON); + assertEquals(3, insight.getResults()); + assertEquals(1, insight.getRoas()); + assertEquals(2, insight.getSocialClicks()); + assertEquals(3, insight.getUniqueSocialClicks()); + assertEquals(4, insight.getSocialImpressions()); + assertEquals(5, insight.getUniqueSocialImpressions()); + assertEquals(6, insight.getSocialReach()); + assertEquals(2, insight.getSpend()); + assertEquals(0, insight.getTodaySpend()); + assertEquals(0, insight.getTotalActionValue()); + assertEquals(3, insight.getTotalActions()); + assertEquals(2, insight.getTotalUniqueActions()); + assertEquals(4, insight.getActions().size()); + assertEquals("comment", insight.getActions().get(0).getActionType()); + assertEquals(2, insight.getActions().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getActions().get(1).getActionType()); + assertEquals(1, insight.getActions().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getActions().get(2).getActionType()); + assertEquals(3, insight.getActions().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getActions().get(3).getActionType()); + assertEquals(3, insight.getActions().get(3).getValue(), EPSILON); + assertEquals(4, insight.getUniqueActions().size()); + assertEquals("comment", insight.getUniqueActions().get(0).getActionType()); + assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getUniqueActions().get(1).getActionType()); + assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType()); + assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType()); + assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON); + assertEquals(4, insight.getCostPerActionType().size()); + assertEquals("comment", insight.getCostPerActionType().get(0).getActionType()); + assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType()); + assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType()); + assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType()); + assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON); + assertEquals(1, insight.getVideoStartActions().size()); + assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType()); + assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON); + } + + @Test(expected = NotAuthorizedException.class) + public void getAccountInsight_unauthorized() throws Exception { + unauthorizedFacebookAds.accountOperations().getAdAccountInsight("123456789"); + } + + @Test + public void updateAdAccount_nameOnly() throws Exception { + String requestBody = "name=New+Test+Name"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON)); + AdAccount adAccount = new AdAccount(); + adAccount.setName("New Test Name"); + boolean updateStatus = facebookAds.accountOperations().updateAdAccount("123456789", adAccount); + assertTrue(updateStatus); + mockServer.verify(); + } + + @Test + public void updateAdAccount_spendCapOnly() throws Exception { + String requestBody = "spend_cap=10000"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON)); + AdAccount adAccount = new AdAccount(); + adAccount.setSpendCap("10000"); + boolean updateStatus = facebookAds.accountOperations().updateAdAccount("123456789", adAccount); + assertTrue(updateStatus); + mockServer.verify(); + } + + @Test + public void updateAdAccount_bothNameAndSpendCap() throws Exception { + String requestBody = "name=Super+cool+name&spend_cap=11111"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON)); + AdAccount adAccount = new AdAccount(); + adAccount.setName("Super cool name"); + adAccount.setSpendCap("11111"); + boolean updateStatus = facebookAds.accountOperations().updateAdAccount("123456789", adAccount); + assertTrue(updateStatus); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void updateAdAccount_unauthorized() throws Exception { + AdAccount adAccount = new AdAccount(); + adAccount.setName("abc"); + unauthorizedFacebookAds.accountOperations().updateAdAccount("123456789", adAccount); + } + + private void assertAdAccountsFields(List adAccounts) { + assertAdAccountFields(adAccounts.get(0)); + assertAdAccountFields(adAccounts.get(0)); + assertEquals("act_77777777", adAccounts.get(1).getId()); + assertEquals(2, adAccounts.get(1).getAccountGroups().size()); + assertEquals("987654321", adAccounts.get(1).getAccountGroups().get(0).getId()); + assertEquals("Test group name", adAccounts.get(1).getAccountGroups().get(0).getName()); + assertEquals(1, adAccounts.get(1).getAccountGroups().get(0).getStatus()); + assertEquals("11223344", adAccounts.get(1).getAccountGroups().get(1).getId()); + assertEquals("Test group name 2", adAccounts.get(1).getAccountGroups().get(1).getName()); + assertEquals(1, adAccounts.get(1).getAccountGroups().get(1).getStatus()); + assertEquals(77777777, adAccounts.get(1).getAccountId()); + assertEquals(AdAccount.AccountStatus.DISABLED, adAccounts.get(1).getStatus()); + assertEquals(777, adAccounts.get(1).getAge()); + assertEquals("7777", adAccounts.get(1).getAmountSpent()); + assertEquals("77777", adAccounts.get(1).getBalance()); + assertEquals("Warsaw", adAccounts.get(1).getBusinessCity()); + assertEquals("PL", adAccounts.get(1).getBusinessCountryCode()); + assertEquals("Some business name for the account 2", adAccounts.get(1).getBusinessName()); + assertEquals("mazowieckie", adAccounts.get(1).getBusinessState()); + assertEquals("Some street 2", adAccounts.get(1).getBusinessStreet()); + assertEquals(null, adAccounts.get(1).getBusinessStreet2()); + assertEquals("77-777", adAccounts.get(1).getBusinessZip()); + assertEquals(2, adAccounts.get(1).getCapabilities().size()); + assertEquals(Capability.DIRECT_SALES, adAccounts.get(1).getCapabilities().get(0)); + assertEquals(Capability.VIEW_TAGS, adAccounts.get(1).getCapabilities().get(1)); + assertEquals(toDate("2015-04-20T00:31:33+0100"), adAccounts.get(1).getCreatedTime()); + assertEquals("PLN", adAccounts.get(1).getCurrency()); + assertEquals("77", adAccounts.get(1).getDailySpendLimit()); + assertEquals(987654321, adAccounts.get(1).getEndAdvertiser()); + assertEquals("77", adAccounts.get(1).getFundingSource()); + assertEquals(1, adAccounts.get(1).getIsPersonal()); + assertEquals(54321, adAccounts.get(1).getMediaAgency()); + assertEquals("This is a test account 2", adAccounts.get(1).getName()); + assertEquals(false, adAccounts.get(1).isOffsitePixelsTOSAccepted()); + assertEquals(111222333, adAccounts.get(1).getPartner()); + assertEquals("0", adAccounts.get(1).getSpendCap()); + assertEquals(106, adAccounts.get(1).getTimezoneId()); + assertEquals("Europe/Warsaw", adAccounts.get(1).getTimezoneName()); + assertEquals(2, adAccounts.get(1).getTimezoneOffsetHoursUTC()); + assertEquals(TaxStatus.ACCOUNT_IS_PERSONAL_ACCOUNT, adAccounts.get(1).getTaxStatus()); + } + + + private void assertAdAccountFields(AdAccount adAccount) { + assertEquals("act_123456789", adAccount.getId()); + assertEquals(1, adAccount.getAccountGroups().size()); + assertEquals("987654321", adAccount.getAccountGroups().get(0).getId()); + assertEquals("Test group name", adAccount.getAccountGroups().get(0).getName()); + assertEquals(1, adAccount.getAccountGroups().get(0).getStatus()); + assertEquals(123456789, adAccount.getAccountId()); + assertEquals(10, adAccount.getAge()); + assertEquals("1234", adAccount.getAmountSpent()); + assertEquals("10000", adAccount.getBalance()); + assertEquals("Poznan", adAccount.getBusinessCity()); + assertEquals("PL", adAccount.getBusinessCountryCode()); + assertEquals("Some business name for the account", adAccount.getBusinessName()); + assertEquals("wielkopolska", adAccount.getBusinessState()); + assertEquals("Some street 79A", adAccount.getBusinessStreet()); + assertEquals(null, adAccount.getBusinessStreet2()); + assertEquals("66-777", adAccount.getBusinessZip()); + assertEquals(2, adAccount.getCapabilities().size()); + assertEquals(Capability.DIRECT_SALES, adAccount.getCapabilities().get(0)); + assertEquals(Capability.VIEW_TAGS, adAccount.getCapabilities().get(1)); + assertEquals(toDate("2015-02-19T00:31:33+0100"), adAccount.getCreatedTime()); + assertEquals("PLN", adAccount.getCurrency()); + assertEquals("93263", adAccount.getDailySpendLimit()); + assertEquals(987654321, adAccount.getEndAdvertiser()); + assertEquals("1122334455", adAccount.getFundingSource()); + assertEquals(1, adAccount.getIsPersonal()); + assertEquals(54321, adAccount.getMediaAgency()); + assertEquals("This is a test account", adAccount.getName()); + assertEquals(false, adAccount.isOffsitePixelsTOSAccepted()); + assertEquals(111222333, adAccount.getPartner()); + assertEquals("0", adAccount.getSpendCap()); + assertEquals(106, adAccount.getTimezoneId()); + assertEquals("Europe/Warsaw", adAccount.getTimezoneName()); + assertEquals(2, adAccount.getTimezoneOffsetHoursUTC()); + assertEquals(TaxStatus.VAT_INFORMATION_SUBMITTED, adAccount.getTaxStatus()); + } +} diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdSetTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdSetTemplateTest.java new file mode 100644 index 000000000..fcfd08c90 --- /dev/null +++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdSetTemplateTest.java @@ -0,0 +1,489 @@ +package org.springframework.social.facebook.api.ads; + +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.social.NotAuthorizedException; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.AdSet.AdSetStatus; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; + +import static org.junit.Assert.*; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * @author Sebastian Górecki + */ +public class AdSetTemplateTest extends AbstractFacebookAdsApiTest { + + @Test + public void getAdSets() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-sets"), MediaType.APPLICATION_JSON)); + + PagedList adSets = facebookAds.adSetOperations().getAccountAdSets("123456789"); + verifyAdSets(adSets); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdSets_unauthorized() throws Exception { + unauthorizedFacebookAds.adSetOperations().getAccountAdSets("123456789"); + } + + @Test + public void getCampaignAdsets() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789/adcampaigns?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-sets"), MediaType.APPLICATION_JSON)); + + PagedList adSets = facebookAds.adSetOperations().getCampaignAdSets("600123456789"); + verifyAdSets(adSets); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getCampaignAdSets_unauthorized() throws Exception { + unauthorizedFacebookAds.adSetOperations().getCampaignAdSets("600123456789"); + } + + @Test + public void getAdSet() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-set"), MediaType.APPLICATION_JSON)); + AdSet adSet = facebookAds.adSetOperations().getAdSet("700123456789"); + assertEquals("700123456789", adSet.getId()); + assertEquals("123456789", adSet.getAccountId()); + assertEquals("600123456789", adSet.getCampaignId()); + assertEquals("Test AdSet", adSet.getName()); + assertEquals(AdSetStatus.ACTIVE, adSet.getStatus()); + assertTrue(adSet.isAutobid()); + assertEquals(BidType.ABSOLUTE_OCPM, adSet.getBidType()); + assertEquals(50, adSet.getBudgetRemaining()); + assertEquals(0, adSet.getDailyBudget()); + assertEquals(200, adSet.getLifetimeBudget()); + // targeting + assertEquals(Integer.valueOf(20), adSet.getTargeting().getAgeMax()); + assertEquals(Integer.valueOf(18), adSet.getTargeting().getAgeMin()); + assertEquals(1, adSet.getTargeting().getBehaviors().size()); + assertEquals(6004854404172L, adSet.getTargeting().getBehaviors().get(0).getId()); + assertEquals("Technology late adopters", adSet.getTargeting().getBehaviors().get(0).getName()); + assertEquals(1, adSet.getTargeting().getGenders().size()); + assertEquals(Targeting.Gender.MALE, adSet.getTargeting().getGenders().get(0)); + assertEquals(1, adSet.getTargeting().getGeoLocations().getCountries().size()); + assertEquals("PL", adSet.getTargeting().getGeoLocations().getCountries().get(0)); + assertEquals(2, adSet.getTargeting().getGeoLocations().getRegions().size()); + assertEquals("3847", adSet.getTargeting().getGeoLocations().getRegions().get(0)); + assertEquals("1122", adSet.getTargeting().getGeoLocations().getRegions().get(1)); + assertEquals(2, adSet.getTargeting().getGeoLocations().getCities().size()); + assertEquals("2430536", adSet.getTargeting().getGeoLocations().getCities().get(0).getKey()); + assertEquals(12, adSet.getTargeting().getGeoLocations().getCities().get(0).getRadius()); + assertEquals("mile", adSet.getTargeting().getGeoLocations().getCities().get(0).getDistanceUnit()); + assertEquals("11223344", adSet.getTargeting().getGeoLocations().getCities().get(1).getKey()); + assertEquals(55, adSet.getTargeting().getGeoLocations().getCities().get(1).getRadius()); + assertEquals("kilometer", adSet.getTargeting().getGeoLocations().getCities().get(1).getDistanceUnit()); + assertEquals(2, adSet.getTargeting().getGeoLocations().getZips().size()); + assertEquals("US:94304", adSet.getTargeting().getGeoLocations().getZips().get(0)); + assertEquals("US:00501", adSet.getTargeting().getGeoLocations().getZips().get(1)); + assertEquals(2, adSet.getTargeting().getGeoLocations().getLocationTypes().size()); + assertEquals(TargetingLocation.LocationType.HOME, adSet.getTargeting().getGeoLocations().getLocationTypes().get(0)); + assertEquals(TargetingLocation.LocationType.RECENT, adSet.getTargeting().getGeoLocations().getLocationTypes().get(1)); + assertEquals(1, adSet.getTargeting().getInterests().size()); + assertEquals(6003629266583L, adSet.getTargeting().getInterests().get(0).getId()); + assertEquals("Hard drives", adSet.getTargeting().getInterests().get(0).getName()); + assertEquals(2, adSet.getTargeting().getPageTypes().size()); + assertEquals(Targeting.PageType.FEED, adSet.getTargeting().getPageTypes().get(0)); + assertEquals(Targeting.PageType.DESKTOP_AND_MOBILE_AND_EXTERNAL, adSet.getTargeting().getPageTypes().get(1)); + assertEquals(2, adSet.getTargeting().getRelationshipStatuses().size()); + assertEquals(Targeting.RelationshipStatus.IN_RELATIONSHIP, adSet.getTargeting().getRelationshipStatuses().get(0)); + assertEquals(Targeting.RelationshipStatus.IN_OPEN_RELATIONSHIP, adSet.getTargeting().getRelationshipStatuses().get(1)); + assertEquals(1, adSet.getTargeting().getInterestedIn().size()); + assertEquals(Targeting.InterestedIn.WOMEN, adSet.getTargeting().getInterestedIn().get(0)); + assertEquals(1, adSet.getTargeting().getEducationSchools().size()); + assertEquals(105930651606L, adSet.getTargeting().getEducationSchools().get(0).getId()); + assertEquals("Harvard University", adSet.getTargeting().getEducationSchools().get(0).getName()); + assertEquals(3, adSet.getTargeting().getEducationStatuses().size()); + assertEquals(Targeting.EducationStatus.HIGH_SCHOOL, adSet.getTargeting().getEducationStatuses().get(0)); + assertEquals(Targeting.EducationStatus.MASTER_DEGREE, adSet.getTargeting().getEducationStatuses().get(1)); + assertEquals(Targeting.EducationStatus.SOME_HIGH_SCHOOL, adSet.getTargeting().getEducationStatuses().get(2)); + assertEquals(1, adSet.getTargeting().getWorkEmployers().size()); + assertEquals(50431654L, adSet.getTargeting().getWorkEmployers().get(0).getId()); + assertEquals("Microsoft", adSet.getTargeting().getWorkEmployers().get(0).getName()); + assertEquals(1, adSet.getTargeting().getWorkPositions().size()); + assertEquals(105763692790962L, adSet.getTargeting().getWorkPositions().get(0).getId()); + assertEquals("Business Analyst", adSet.getTargeting().getWorkPositions().get(0).getName()); + + assertEquals(toDate("2015-04-12T09:19:00+0200"), adSet.getStartTime()); + assertEquals(toDate("2015-04-13T09:19:00+0200"), adSet.getEndTime()); + assertEquals(toDate("2015-04-10T09:28:54+0200"), adSet.getCreatedTime()); + assertEquals(toDate("2015-04-10T13:32:09+0200"), adSet.getUpdatedTime()); + } + + @Test + public void getAdSet_wrongStatus() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/709123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-set-wrong-status"), MediaType.APPLICATION_JSON)); + AdSet adSet = facebookAds.adSetOperations().getAdSet("709123456789"); + assertEquals("709123456789", adSet.getId()); + assertEquals("123456789", adSet.getAccountId()); + assertEquals("600123456789", adSet.getCampaignId()); + assertEquals("Test AdSet", adSet.getName()); + assertEquals(AdSetStatus.UNKNOWN, adSet.getStatus()); + mockServer.verify(); + } + + @Test + public void getAdSet_wrongBidType() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/710123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-set-wrong-bid-type"), MediaType.APPLICATION_JSON)); + AdSet adSet = facebookAds.adSetOperations().getAdSet("710123456789"); + assertEquals("710123456789", adSet.getId()); + assertEquals("123456789", adSet.getAccountId()); + assertEquals("600123456789", adSet.getCampaignId()); + assertEquals("Test AdSet", adSet.getName()); + assertEquals(AdSetStatus.ACTIVE, adSet.getStatus()); + assertEquals(BidType.UNKNOWN, adSet.getBidType()); + mockServer.verify(); + } + + @Test + public void getAdSet_withPromotedObject() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/705123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-set-promoted-object"), MediaType.APPLICATION_JSON)); + AdSet adSet = facebookAds.adSetOperations().getAdSet("705123456789"); + assertEquals("705123456789", adSet.getId()); + assertEquals("123456789", adSet.getAccountId()); + assertEquals(Integer.valueOf(500), adSet.getBidInfo().get("CLICKS")); + assertEquals(BidType.ABSOLUTE_OCPM, adSet.getBidType()); + assertEquals(807, adSet.getBudgetRemaining()); + assertEquals("600123456789", adSet.getCampaignId()); + assertEquals(AdSetStatus.PAUSED, adSet.getStatus()); + assertEquals(2000, adSet.getDailyBudget()); + assertFalse(adSet.isAutobid()); + assertEquals(0, adSet.getLifetimeBudget()); + assertEquals("Test promoted object", adSet.getName()); + assertEquals("999888777666555", adSet.getPromotedObject().get("page_id")); + assertEquals(toDate("2015-07-06T14:18:55+0200"), adSet.getCreatedTime()); + assertEquals(toDate("2015-07-06T14:18:55+0200"), adSet.getStartTime()); + assertEquals(toDate("2015-07-06T14:18:55+0200"), adSet.getUpdatedTime()); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdSet_unauthorized() throws Exception { + unauthorizedFacebookAds.adSetOperations().getAdSet("700123456789"); + } + + @Test + public void getAdSetInsights() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-set-insights"), MediaType.APPLICATION_JSON)); + + AdInsight insight = facebookAds.adSetOperations().getAdSetInsight("700123456789"); + + assertEquals("123456789", insight.getAccountId()); + assertEquals("Test account name", insight.getAccountName()); + assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON); + assertEquals(8, insight.getClicks()); + assertEquals(5, insight.getUniqueClicks()); + assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON); + assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON); + assertEquals(0.25, insight.getCostPerClick(), EPSILON); + assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON); + assertEquals(10.695187165775, insight.getCpm(), EPSILON); + assertEquals(10.869565217391, insight.getCpp(), EPSILON); + assertEquals(4.2780748663102, insight.getCtr(), EPSILON); + assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON); + assertEquals(1.0163043478261, insight.getFrequency(), EPSILON); + assertEquals(187, insight.getImpressions()); + assertEquals(184, insight.getUniqueImpressions()); + assertEquals(184, insight.getReach()); + assertEquals(1.6042780748663, insight.getResultRate(), EPSILON); + assertEquals(3, insight.getResults()); + assertEquals(0, insight.getRoas()); + assertEquals(0, insight.getSocialClicks()); + assertEquals(0, insight.getUniqueSocialClicks()); + assertEquals(0, insight.getSocialImpressions()); + assertEquals(0, insight.getUniqueSocialImpressions()); + assertEquals(0, insight.getSocialReach()); + assertEquals(2, insight.getSpend()); + assertEquals(0, insight.getTodaySpend()); + assertEquals(0, insight.getTotalActionValue()); + assertEquals(3, insight.getTotalActions()); + assertEquals(2, insight.getTotalUniqueActions()); + assertEquals(4, insight.getActions().size()); + assertEquals("comment", insight.getActions().get(0).getActionType()); + assertEquals(2, insight.getActions().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getActions().get(1).getActionType()); + assertEquals(1, insight.getActions().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getActions().get(2).getActionType()); + assertEquals(3, insight.getActions().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getActions().get(3).getActionType()); + assertEquals(3, insight.getActions().get(3).getValue(), EPSILON); + assertEquals(4, insight.getUniqueActions().size()); + assertEquals("comment", insight.getUniqueActions().get(0).getActionType()); + assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getUniqueActions().get(1).getActionType()); + assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType()); + assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType()); + assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON); + assertEquals(4, insight.getCostPerActionType().size()); + assertEquals("comment", insight.getCostPerActionType().get(0).getActionType()); + assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType()); + assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType()); + assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType()); + assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON); + assertEquals(1, insight.getVideoStartActions().size()); + assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType()); + assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON); + + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdSetInsights_unauthorized() throws Exception { + unauthorizedFacebookAds.adSetOperations().getAdSetInsight("700123456789"); + } + + @Test + public void createAdSet() throws Exception { + String requestBody = "date_format=U&name=Test+AdSet&campaign_status=PAUSED&is_autobid=true&bid_type=ABSOLUTE_OCPM&daily_budget=0&lifetime_budget=200&targeting=%7B%22geo_locations%22%3A%7B%22countries%22%3A%5B%22PL%22%5D%7D%7D&end_time=1432231200&campaign_group_id=600123456789"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"701123456789\"}", MediaType.APPLICATION_JSON)); + AdSet adSet = createSampleAdSet(); + + assertEquals("701123456789", facebookAds.adSetOperations().createAdSet("123456789", adSet)); + mockServer.verify(); + } + + @Test + public void createAdSet_withAllFields() throws Exception { + String requestBody = "date_format=U&name=Test+AdSet+2&campaign_status=ACTIVE&is_autobid=false&" + + "bid_info=%7B%22REACH%22%3A1000%2C%22ACTIONS%22%3A200%2C%22SOCIAL%22%3A110%2C%22CLICKS%22%3A500%7D&" + + "bid_type=ABSOLUTE_OCPM&daily_budget=4000&lifetime_budget=0&" + + "targeting=%7B%22genders%22%3A%5B1%2C2%5D%2C%22age_min%22%3A45%2C%22age_max%22%3A55%2C%22relationship_statuses%22%3A%5B10%2C12%5D%2C%22interested_in%22%3A%5B1%2C2%5D%2C%22geo_locations%22%3A%7B%22countries%22%3A%5B%22PL%22%2C%22DE%22%2C%22US%22%2C%22FR%22%5D%2C%22regions%22%3A%5B%7B%22key%22%3A%223847%22%7D%2C%7B%22key%22%3A%221111%22%7D%2C%7B%22key%22%3A%221234%22%7D%2C%7B%22key%22%3A%229888%22%7D%5D%2C%22cities%22%3A%5B%7B%22key%22%3A%222430536%22%2C%22radius%22%3A12%2C%22distance_unit%22%3A%22mile%22%7D%2C%7B%22key%22%3A%22777555%22%2C%22radius%22%3A1024%2C%22distance_unit%22%3A%22kilometer%22%7D%5D%2C%22zips%22%3A%5B%7B%22key%22%3A%22PL%3A62030%22%7D%2C%7B%22key%22%3A%22US%3A88123%22%7D%2C%7B%22key%22%3A%22FR%3A33144%22%7D%5D%2C%22location_types%22%3A%5B%22home%22%2C%22recent%22%5D%7D%2C%22excluded_geo_locations%22%3A%7B%22countries%22%3A%5B%22HU%22%2C%22JP%22%5D%2C%22regions%22%3A%5B%7B%22key%22%3A%221122%22%7D%2C%7B%22key%22%3A%2231415%22%7D%5D%2C%22cities%22%3A%5B%7B%22key%22%3A%2288997766%22%2C%22radius%22%3A12345%2C%22distance_unit%22%3A%22mile%22%7D%5D%2C%22zips%22%3A%5B%7B%22key%22%3A%22JP%3A44552%22%7D%5D%2C%22location_types%22%3A%5B%22home%22%5D%7D%2C%22page_types%22%3A%5B%22desktopfeed%22%2C%22mobilefeed-and-external%22%5D%2C%22connections%22%3A%5B%22123456789%22%2C%2255442211%22%5D%2C%22excluded_connections%22%3A%5B%2233441122%22%5D%2C%22friends_of_connections%22%3A%5B%22987654321%22%5D%2C%22interests%22%3A%5B%7B%22id%22%3A986123123123%2C%22name%22%3A%22Football%22%7D%5D%2C%22behaviors%22%3A%5B%7B%22id%22%3A1%2C%22name%22%3A%22Some+behavior%22%7D%5D%2C%22education_schools%22%3A%5B%7B%22id%22%3A10593123549%2C%22name%22%3A%22Poznan+University+of+Technology%22%7D%5D%2C%22education_statuses%22%3A%5B9%5D%2C%22college_years%22%3A%5B8%5D%2C%22education_majors%22%3A%5B%7B%22id%22%3A12%2C%22name%22%3A%22Some+major%22%7D%5D%2C%22work_employers%22%3A%5B%7B%22id%22%3A43125%2C%22name%22%3A%22Super+company%22%7D%5D%2C%22work_positions%22%3A%5B%7B%22id%22%3A11111%2C%22name%22%3A%22Developer%22%7D%5D%7D&" + + "start_time=1432742400&end_time=1435420799&campaign_group_id=601123456789"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"702123456789\"}", MediaType.APPLICATION_JSON)); + AdSet adSet = new AdSet(); + adSet.setCampaignId("601123456789"); + adSet.setName("Test AdSet 2"); + adSet.setStatus(AdSetStatus.ACTIVE); + adSet.setAutobid(false); + BidInfo bidInfo = new BidInfo(); + bidInfo.put("ACTIONS", 200); + bidInfo.put("REACH", 1000); + bidInfo.put("CLICKS", 500); + bidInfo.put("SOCIAL", 110); + adSet.setBidInfo(bidInfo); + adSet.setBidType(BidType.ABSOLUTE_OCPM); + adSet.setDailyBudget(4000); + adSet.setLifetimeBudget(0); + // targeting + Targeting targeting = new Targeting(); + targeting.setGenders(Arrays.asList(Targeting.Gender.MALE, Targeting.Gender.FEMALE)); + targeting.setAgeMin(45); + targeting.setAgeMax(55); + targeting.setRelationshipStatuses(Arrays.asList(Targeting.RelationshipStatus.ITS_COMPLICATED, Targeting.RelationshipStatus.DIVORCED)); + targeting.setInterestedIn(Arrays.asList(Targeting.InterestedIn.MEN, Targeting.InterestedIn.WOMEN)); + // targeting - geoLocations + TargetingLocation geoLocation = new TargetingLocation(); + geoLocation.setCountries(Arrays.asList("PL", "DE", "US", "FR")); + geoLocation.setRegions(Arrays.asList("3847", "1111", "1234", "9888")); + geoLocation.setCities(Arrays.asList(new TargetingCityEntry("2430536", 12, "mile"), new TargetingCityEntry("777555", 1024, "kilometer"))); + geoLocation.setZips(Arrays.asList("PL:62030", "US:88123", "FR:33144")); + geoLocation.setLocationTypes(Arrays.asList(TargetingLocation.LocationType.HOME, TargetingLocation.LocationType.RECENT)); + targeting.setGeoLocations(geoLocation); + // targeting - excludedGeoLocations + TargetingLocation excludedGeoLocations = new TargetingLocation(); + excludedGeoLocations.setCountries(Arrays.asList("HU", "JP")); + excludedGeoLocations.setRegions(Arrays.asList("1122", "31415")); + excludedGeoLocations.setCities(Arrays.asList(new TargetingCityEntry("88997766", 12345, "mile"))); + excludedGeoLocations.setZips(Arrays.asList("JP:44552")); + excludedGeoLocations.setLocationTypes(Arrays.asList(TargetingLocation.LocationType.HOME)); + targeting.setExcludedGeoLocations(excludedGeoLocations); + // targeting cd. + targeting.setPageTypes(Arrays.asList(Targeting.PageType.DESKTOP_FEED, Targeting.PageType.MOBILE_FEED_AND_EXTERNAL)); + targeting.setConnections(Arrays.asList("123456789", "55442211")); + targeting.setExcludedConnections(Arrays.asList("33441122")); + targeting.setFriendsOfConnections(Arrays.asList("987654321")); + targeting.setInterests(Arrays.asList(new TargetingEntry(986123123123L, "Football"))); + targeting.setBehaviors(Arrays.asList(new TargetingEntry(1L, "Some behavior"))); + targeting.setEducationSchools(Arrays.asList(new TargetingEntry(10593123549L, "Poznan University of Technology"))); + targeting.setEducationStatuses(Arrays.asList(Targeting.EducationStatus.MASTER_DEGREE)); + targeting.setCollegeYears(Arrays.asList(Integer.valueOf(8))); + targeting.setEducationMajors(Arrays.asList(new TargetingEntry(12L, "Some major"))); + targeting.setWorkEmployers(Arrays.asList(new TargetingEntry(43125L, "Super company"))); + targeting.setWorkPositions(Arrays.asList(new TargetingEntry(11111L, "Developer"))); + adSet.setTargeting(targeting); + + DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + adSet.setStartTime(formatter.parse("2015-05-27 18:00:00")); + adSet.setEndTime(formatter.parse("2015-06-27 17:59:59")); + + assertEquals("702123456789", facebookAds.adSetOperations().createAdSet("123456789", adSet)); + mockServer.verify(); + } + + @Test + public void createAdSet_withPromotedObject() throws Exception { + String requestBody = "date_format=U&name=Test+AdSet&campaign_status=PAUSED&is_autobid=true&bid_type=ABSOLUTE_OCPM&daily_budget=0&lifetime_budget=200&promoted_object=%7B%22page_id%22%3A%22111222333444555%22%7D&targeting=%7B%22geo_locations%22%3A%7B%22countries%22%3A%5B%22PL%22%5D%7D%7D&end_time=1432231200&campaign_group_id=600123456789"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"702123456789\"}", MediaType.APPLICATION_JSON)); + + AdSet adSet = createSampleAdSet(); + PromotedObject promotedObject = new PromotedObject(); + promotedObject.put("page_id", "111222333444555"); + adSet.setPromotedObject(promotedObject); + + assertEquals("702123456789", facebookAds.adSetOperations().createAdSet("123456789", adSet)); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void createAdSet_unauthorized() throws Exception { + unauthorizedFacebookAds.adSetOperations().createAdSet("123456789", new AdSet()); + } + + @Test + public void updateAdSet() throws Exception { + String requestBody = "date_format=U&name=New+AdSet+name&campaign_status=ARCHIVED&is_autobid=true&daily_budget=0&lifetime_budget=50000&start_time=1432833720"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + + AdSet adSet = new AdSet(); + adSet.setName("New AdSet name"); + adSet.setStatus(AdSetStatus.ARCHIVED); + adSet.setLifetimeBudget(50000); + adSet.setAutobid(true); + DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + adSet.setStartTime(formatter.parse("2015-05-28 19:22:00")); + + assertTrue(facebookAds.adSetOperations().updateAdSet("700123456789", adSet)); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void updateAdSet_unauthorized() throws Exception { + unauthorizedFacebookAds.adSetOperations().updateAdSet("700123456789", new AdSet()); + } + + @Test + public void deleteAdSet() throws Exception { + String requestBody = "campaign_status=DELETED"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"status\": \"true\"}", MediaType.APPLICATION_JSON)); + facebookAds.adSetOperations().deleteAdSet("700123456789"); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void deleteAdSet_unauthorized() throws Exception { + unauthorizedFacebookAds.adSetOperations().deleteAdSet("700123456789"); + } + + private void verifyAdSets(PagedList adSets) { + assertEquals(2, adSets.size()); + assertEquals("123456789", adSets.get(0).getAccountId()); + assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(0).getBidType()); + assertEquals(37407, adSets.get(0).getBudgetRemaining()); + assertEquals("600123456789", adSets.get(0).getCampaignId()); + assertEquals(AdSetStatus.PAUSED, adSets.get(0).getStatus()); + assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getCreatedTime()); + assertEquals(40000, adSets.get(0).getDailyBudget()); + assertEquals(toDate("2015-05-29T22:26:40+0200"), adSets.get(0).getEndTime()); + assertEquals("700123456789", adSets.get(0).getId()); + assertTrue(adSets.get(0).isAutobid()); + assertEquals(0, adSets.get(0).getLifetimeBudget()); + assertEquals("Test AdSet", adSets.get(0).getName()); + assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getStartTime()); + assertEquals(Integer.valueOf(65), adSets.get(0).getTargeting().getAgeMax()); + assertEquals(Integer.valueOf(18), adSets.get(0).getTargeting().getAgeMin()); + assertEquals("BR", adSets.get(0).getTargeting().getGeoLocations().getCountries().get(0)); + assertEquals(TargetingLocation.LocationType.HOME, adSets.get(0).getTargeting().getGeoLocations().getLocationTypes().get(0)); + assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getUpdatedTime()); + + assertEquals("123456789", adSets.get(1).getAccountId()); + assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(1).getBidType()); + assertEquals(0, adSets.get(1).getBudgetRemaining()); + assertEquals("600123456789", adSets.get(1).getCampaignId()); + assertEquals(AdSetStatus.ACTIVE, adSets.get(1).getStatus()); + assertEquals(toDate("2015-04-10T09:28:54+0200"), adSets.get(1).getCreatedTime()); + assertEquals(0, adSets.get(1).getDailyBudget()); + assertEquals(toDate("2015-04-13T09:19:00+0200"), adSets.get(1).getEndTime()); + assertEquals("701123456789", adSets.get(1).getId()); + assertTrue(adSets.get(1).isAutobid()); + assertEquals(200, adSets.get(1).getLifetimeBudget()); + assertEquals("Real ad set", adSets.get(1).getName()); + assertEquals(toDate("2015-04-12T09:19:00+0200"), adSets.get(1).getStartTime()); + assertEquals(Integer.valueOf(20), adSets.get(1).getTargeting().getAgeMax()); + assertEquals(Integer.valueOf(18), adSets.get(1).getTargeting().getAgeMin()); + assertEquals(6004854404172L, adSets.get(1).getTargeting().getBehaviors().get(0).getId()); + assertEquals("Technology late adopters", adSets.get(1).getTargeting().getBehaviors().get(0).getName()); + assertEquals(Targeting.Gender.MALE, adSets.get(1).getTargeting().getGenders().get(0)); + assertEquals("PL", adSets.get(1).getTargeting().getGeoLocations().getCountries().get(0)); + assertEquals(TargetingLocation.LocationType.HOME, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(0)); + assertEquals(TargetingLocation.LocationType.RECENT, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(1)); + assertEquals(6003629266583L, adSets.get(1).getTargeting().getInterests().get(0).getId()); + assertEquals("Hard drives", adSets.get(1).getTargeting().getInterests().get(0).getName()); + assertEquals(Targeting.PageType.FEED, adSets.get(1).getTargeting().getPageTypes().get(0)); + assertEquals(toDate("2015-04-10T13:32:09+0200"), adSets.get(1).getUpdatedTime()); + } + + private AdSet createSampleAdSet() throws ParseException { + AdSet adSet = new AdSet(); + adSet.setAutobid(true); + adSet.setBidType(BidType.ABSOLUTE_OCPM); + adSet.setCampaignId("600123456789"); + adSet.setStatus(AdSetStatus.PAUSED); + DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + adSet.setEndTime(formatter.parse("2015-05-21 20:00:00")); + adSet.setName("Test AdSet"); + TargetingLocation location = new TargetingLocation(); + location.setCountries(Arrays.asList("PL")); + Targeting targeting = new Targeting(); + targeting.setGeoLocations(location); + adSet.setTargeting(targeting); + adSet.setLifetimeBudget(200); + return adSet; + } +} diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdTemplateTest.java new file mode 100644 index 000000000..e97b62aa2 --- /dev/null +++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdTemplateTest.java @@ -0,0 +1,330 @@ +package org.springframework.social.facebook.api.ads; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.social.NotAuthorizedException; +import org.springframework.social.facebook.api.PagedList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.HttpMethod.*; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * @author Sebastian Górecki + */ +public class AdTemplateTest extends AbstractFacebookAdsApiTest { + + @Test + public void getAccountAds() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adgroups?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-same-account"), MediaType.APPLICATION_JSON)); + + PagedList accountAds = facebookAds.adOperations().getAccountAds("123456789"); + assertEquals(3, accountAds.size()); + assertEquals("101123456789", accountAds.get(0).getId()); + assertEquals("801123456789", accountAds.get(0).getAdSetId()); + assertEquals("701123456789", accountAds.get(0).getCampaignId()); + assertEquals("123456789", accountAds.get(0).getAccountId()); + assertEquals("102123456789", accountAds.get(1).getId()); + assertEquals("802123456789", accountAds.get(1).getAdSetId()); + assertEquals("702123456789", accountAds.get(1).getCampaignId()); + assertEquals("123456789", accountAds.get(1).getAccountId()); + assertEquals("103123456789", accountAds.get(2).getId()); + assertEquals("803123456789", accountAds.get(2).getAdSetId()); + assertEquals("703123456789", accountAds.get(2).getCampaignId()); + assertEquals("123456789", accountAds.get(2).getAccountId()); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAccountAds_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().getAccountAds("123456789"); + } + + @Test + public void getCampaignAds() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/701123456789/adgroups?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-same-campaign"), MediaType.APPLICATION_JSON)); + + PagedList campaignAds = facebookAds.adOperations().getCampaignAds("701123456789"); + assertEquals(3, campaignAds.size()); + assertEquals("101123456789", campaignAds.get(0).getId()); + assertEquals("801123456789", campaignAds.get(0).getAdSetId()); + assertEquals("701123456789", campaignAds.get(0).getCampaignId()); + assertEquals("102123456789", campaignAds.get(1).getId()); + assertEquals("802123456789", campaignAds.get(1).getAdSetId()); + assertEquals("701123456789", campaignAds.get(1).getCampaignId()); + assertEquals("103123456789", campaignAds.get(2).getId()); + assertEquals("803123456789", campaignAds.get(2).getAdSetId()); + assertEquals("701123456789", campaignAds.get(2).getCampaignId()); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getCampaignAds_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().getCampaignAds("701123456789"); + } + + @Test + public void getAdSetAds() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789/adgroups?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-same-ad-set"), MediaType.APPLICATION_JSON)); + + PagedList adSetAds = facebookAds.adOperations().getAdSetAds("800123456789"); + assertEquals(2, adSetAds.size()); + assertEquals("101123456789", adSetAds.get(0).getId()); + assertEquals("800123456789", adSetAds.get(0).getAdSetId()); + assertEquals("700123456789", adSetAds.get(0).getCampaignId()); + + assertEquals("102123456789", adSetAds.get(1).getId()); + assertEquals("800123456789", adSetAds.get(1).getAdSetId()); + assertEquals("701123456789", adSetAds.get(1).getCampaignId()); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdSetAds_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().getAdSetAds("800123456789"); + } + + @Test + public void getAd() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad"), MediaType.APPLICATION_JSON)); + + Ad ad = facebookAds.adOperations().getAd("100123456789"); + verifyAd(ad); + assertEquals(Ad.AdStatus.ACTIVE, ad.getStatus()); + assertEquals(BidType.ABSOLUTE_OCPM, ad.getBidType()); + mockServer.verify(); + } + + @Test + public void getAd_withWrongStatus() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-wrong-status"), MediaType.APPLICATION_JSON)); + + Ad ad = facebookAds.adOperations().getAd("100123456789"); + verifyAd(ad); + assertEquals(Ad.AdStatus.UNKNOWN, ad.getStatus()); + assertEquals(BidType.ABSOLUTE_OCPM, ad.getBidType()); + mockServer.verify(); + } + + @Test + public void getAd_withWrongBidType() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-wrong-bid-type"), MediaType.APPLICATION_JSON)); + + Ad ad = facebookAds.adOperations().getAd("100123456789"); + verifyAd(ad); + assertEquals(Ad.AdStatus.ACTIVE, ad.getStatus()); + assertEquals(BidType.UNKNOWN, ad.getBidType()); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAd_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().getAd("100123456789"); + } + + @Test + public void getAdInsight() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-insights"), MediaType.APPLICATION_JSON)); + + AdInsight insight = facebookAds.adOperations().getAdInsight("100123456789"); + Assert.assertEquals("123456789", insight.getAccountId()); + Assert.assertEquals("Test account name", insight.getAccountName()); + Assert.assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON); + Assert.assertEquals(8, insight.getClicks()); + Assert.assertEquals(5, insight.getUniqueClicks()); + Assert.assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON); + Assert.assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON); + Assert.assertEquals(0.25, insight.getCostPerClick(), EPSILON); + Assert.assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON); + Assert.assertEquals(10.695187165775, insight.getCpm(), EPSILON); + Assert.assertEquals(10.869565217391, insight.getCpp(), EPSILON); + Assert.assertEquals(4.2780748663102, insight.getCtr(), EPSILON); + Assert.assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON); + Assert.assertEquals(1.0163043478261, insight.getFrequency(), EPSILON); + Assert.assertEquals(187, insight.getImpressions()); + Assert.assertEquals(184, insight.getUniqueImpressions()); + Assert.assertEquals(184, insight.getReach()); + Assert.assertEquals(1.6042780748663, insight.getResultRate(), EPSILON); + Assert.assertEquals(3, insight.getResults()); + Assert.assertEquals(0, insight.getRoas()); + Assert.assertEquals(0, insight.getSocialClicks()); + Assert.assertEquals(0, insight.getUniqueSocialClicks()); + Assert.assertEquals(0, insight.getSocialImpressions()); + Assert.assertEquals(0, insight.getUniqueSocialImpressions()); + Assert.assertEquals(0, insight.getSocialReach()); + Assert.assertEquals(2, insight.getSpend()); + Assert.assertEquals(0, insight.getTodaySpend()); + Assert.assertEquals(0, insight.getTotalActionValue()); + Assert.assertEquals(3, insight.getTotalActions()); + Assert.assertEquals(2, insight.getTotalUniqueActions()); + Assert.assertEquals(4, insight.getActions().size()); + Assert.assertEquals("comment", insight.getActions().get(0).getActionType()); + Assert.assertEquals(2, insight.getActions().get(0).getValue(), EPSILON); + Assert.assertEquals("post_like", insight.getActions().get(1).getActionType()); + Assert.assertEquals(1, insight.getActions().get(1).getValue(), EPSILON); + Assert.assertEquals("page_engagement", insight.getActions().get(2).getActionType()); + Assert.assertEquals(3, insight.getActions().get(2).getValue(), EPSILON); + Assert.assertEquals("post_engagement", insight.getActions().get(3).getActionType()); + Assert.assertEquals(3, insight.getActions().get(3).getValue(), EPSILON); + Assert.assertEquals(4, insight.getUniqueActions().size()); + Assert.assertEquals("comment", insight.getUniqueActions().get(0).getActionType()); + Assert.assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON); + Assert.assertEquals("post_like", insight.getUniqueActions().get(1).getActionType()); + Assert.assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON); + Assert.assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType()); + Assert.assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON); + Assert.assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType()); + Assert.assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON); + Assert.assertEquals(4, insight.getCostPerActionType().size()); + Assert.assertEquals("comment", insight.getCostPerActionType().get(0).getActionType()); + Assert.assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON); + Assert.assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType()); + Assert.assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON); + Assert.assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType()); + Assert.assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON); + Assert.assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType()); + Assert.assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON); + Assert.assertEquals(1, insight.getVideoStartActions().size()); + Assert.assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType()); + Assert.assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON); + + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdInsight_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().getAdInsight("100123456789"); + } + + @Test + public void createAd() throws Exception { + String requestBody = "name=Test+ad&adgroup_status=PAUSED&creative=%7B%22creative_id%22%3A+%22900123456789%22%7D&campaign_id=800123456789"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adgroups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"100123456789\"}", MediaType.APPLICATION_JSON)); + + Ad ad = new Ad(); + ad.setName("Test ad"); + ad.setStatus(Ad.AdStatus.PAUSED); + ad.setAdSetId("800123456789"); + ad.setCreativeId("900123456789"); + assertEquals("100123456789", facebookAds.adOperations().createAd("123456789", ad)); + mockServer.verify(); + } + + @Test + public void createAd_withBidInfo() throws Exception { + String requestBody = "name=Test+ad&adgroup_status=PAUSED&bid_info=%7B%22REACH%22%3A11%2C%22ACTIONS%22%3A10%2C%22SOCIAL%22%3A50%2C%22CLICKS%22%3A12%7D&creative=%7B%22creative_id%22%3A+%22900123456789%22%7D&campaign_id=800123456789"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adgroups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"100123456789\"}", MediaType.APPLICATION_JSON)); + + Ad ad = new Ad(); + ad.setName("Test ad"); + ad.setStatus(Ad.AdStatus.PAUSED); + ad.setAdSetId("800123456789"); + ad.setCreativeId("900123456789"); + BidInfo bidInfo = new BidInfo(); + bidInfo.put("ACTIONS", 10); + bidInfo.put("REACH", 11); + bidInfo.put("CLICKS", 12); + bidInfo.put("SOCIAL", 50); + ad.setBidInfo(bidInfo); + assertEquals("100123456789", facebookAds.adOperations().createAd("123456789", ad)); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void createAd_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().createAd("123456789", new Ad()); + } + + @Test + public void updateAd() throws Exception { + String requestBody = "name=Updated+Ad&adgroup_status=ARCHIVED&bid_info=%7B%22CLICKS%22%3A500%7D"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + + Ad ad = new Ad(); + ad.setStatus(Ad.AdStatus.ARCHIVED); + ad.setName("Updated Ad"); + BidInfo bidInfo = new BidInfo(); + bidInfo.put("CLICKS", 500); + ad.setBidInfo(bidInfo); + assertTrue(facebookAds.adOperations().updateAd("100123456789", ad)); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void updateAd_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().updateAd("100123456789", new Ad()); + } + + @Test + public void deleteAd() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789")) + .andExpect(method(DELETE)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + facebookAds.adOperations().deleteAd("100123456789"); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void deleteAd_unauthorized() throws Exception { + unauthorizedFacebookAds.adOperations().deleteAd("100123456789"); + } + + private void verifyAd(Ad ad) { + assertEquals("100123456789", ad.getId()); + assertEquals("123456789", ad.getAccountId()); + assertEquals("800123456789", ad.getAdSetId()); + assertEquals("700123456789", ad.getCampaignId()); + assertEquals(toDate("2015-04-10T09:28:54+0200"), ad.getCreatedTime()); + assertEquals("900123456789", ad.getCreativeId()); + assertEquals("Test ad name", ad.getName()); + assertEquals(Integer.valueOf(18), ad.getTargeting().getAgeMin()); + assertEquals(Integer.valueOf(20), ad.getTargeting().getAgeMax()); + assertEquals(6004854404172L, ad.getTargeting().getBehaviors().get(0).getId()); + assertEquals("Technology late adopters", ad.getTargeting().getBehaviors().get(0).getName()); + assertEquals(Targeting.Gender.MALE, ad.getTargeting().getGenders().get(0)); + assertEquals("PL", ad.getTargeting().getGeoLocations().getCountries().get(0)); + assertEquals(TargetingLocation.LocationType.HOME, ad.getTargeting().getGeoLocations().getLocationTypes().get(0)); + assertEquals(TargetingLocation.LocationType.RECENT, ad.getTargeting().getGeoLocations().getLocationTypes().get(1)); + assertEquals(6003629266583L, ad.getTargeting().getInterests().get(0).getId()); + assertEquals("Hard drives", ad.getTargeting().getInterests().get(0).getName()); + assertEquals(Targeting.PageType.FEED, ad.getTargeting().getPageTypes().get(0)); + assertEquals(toDate("2015-04-10T13:32:09+0200"), ad.getUpdatedTime()); + } +} diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CampaignTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CampaignTemplateTest.java new file mode 100644 index 000000000..1b301c51d --- /dev/null +++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CampaignTemplateTest.java @@ -0,0 +1,466 @@ +package org.springframework.social.facebook.api.ads; + +import org.junit.Test; +import org.springframework.http.MediaType; + +import org.springframework.social.NotAuthorizedException; +import org.springframework.social.facebook.api.PagedList; +import org.springframework.social.facebook.api.ads.AdCampaign.BuyingType; +import org.springframework.social.facebook.api.ads.AdCampaign.CampaignObjective; +import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus; + +import static org.junit.Assert.*; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * @author Sebastian Górecki + */ +public class CampaignTemplateTest extends AbstractFacebookAdsApiTest { + + @Test + public void getAdCampaigns() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-account-campaigns"), MediaType.APPLICATION_JSON)); + PagedList campaigns = facebookAds.campaignOperations().getAdCampaigns("123456789"); + assertEquals(3, campaigns.size()); + assertEquals("601123456789", campaigns.get(0).getId()); + assertEquals("123456789", campaigns.get(0).getAccountId()); + assertEquals(BuyingType.AUCTION, campaigns.get(0).getBuyingType()); + assertEquals(CampaignStatus.ACTIVE, campaigns.get(0).getStatus()); + assertEquals("Campaign #1", campaigns.get(0).getName()); + assertEquals(CampaignObjective.POST_ENGAGEMENT, campaigns.get(0).getObjective()); + assertEquals(0, campaigns.get(0).getSpendCap()); + assertEquals("602123456789", campaigns.get(1).getId()); + assertEquals("123456789", campaigns.get(1).getAccountId()); + assertEquals(BuyingType.FIXED_CPM, campaigns.get(1).getBuyingType()); + assertEquals(CampaignStatus.PAUSED, campaigns.get(1).getStatus()); + assertEquals("Campaign #2", campaigns.get(1).getName()); + assertEquals(CampaignObjective.NONE, campaigns.get(1).getObjective()); + assertEquals(0, campaigns.get(1).getSpendCap()); + assertEquals("603123456789", campaigns.get(2).getId()); + assertEquals("123456789", campaigns.get(2).getAccountId()); + assertEquals(BuyingType.RESERVED, campaigns.get(2).getBuyingType()); + assertEquals(CampaignStatus.ARCHIVED, campaigns.get(2).getStatus()); + assertEquals("Campaign #3", campaigns.get(2).getName()); + assertEquals(CampaignObjective.WEBSITE_CONVERSIONS, campaigns.get(2).getObjective()); + assertEquals(50000, campaigns.get(2).getSpendCap()); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdCampaigns_unauthorized() throws Exception { + unauthorizedFacebookAds.campaignOperations().getAdCampaigns("123456789"); + } + + @Test + public void getCampaign() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-campaign"), MediaType.APPLICATION_JSON)); + + AdCampaign campaign = facebookAds.campaignOperations().getAdCampaign("600123456789"); + assertEquals("600123456789", campaign.getId()); + assertEquals("123456789", campaign.getAccountId()); + assertEquals(BuyingType.AUCTION, campaign.getBuyingType()); + assertEquals(CampaignStatus.ACTIVE, campaign.getStatus()); + assertEquals("The test campaign name", campaign.getName()); + assertEquals(CampaignObjective.POST_ENGAGEMENT, campaign.getObjective()); + assertEquals(1000, campaign.getSpendCap()); + } + + @Test + public void getCampaign_withUnknownEnums() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-campaign-with-unknown-enums"), MediaType.APPLICATION_JSON)); + + AdCampaign campaign = facebookAds.campaignOperations().getAdCampaign("600123456789"); + assertEquals("600123456789", campaign.getId()); + assertEquals("123456789", campaign.getAccountId()); + assertEquals(BuyingType.UNKNOWN, campaign.getBuyingType()); + assertEquals(CampaignStatus.UNKNOWN, campaign.getStatus()); + assertEquals("The test campaign name", campaign.getName()); + assertEquals(CampaignObjective.UNKNOWN, campaign.getObjective()); + mockServer.verify(); + } + + @Test + public void getCampaign_withEmptyBuyingType() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/609123456789?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-campaign-empty-buying-type"), MediaType.APPLICATION_JSON)); + + AdCampaign campaign = facebookAds.campaignOperations().getAdCampaign("609123456789"); + assertEquals("609123456789", campaign.getId()); + assertEquals("123456789", campaign.getAccountId()); + assertNull(campaign.getBuyingType()); + assertEquals(CampaignStatus.ACTIVE, campaign.getStatus()); + assertEquals("The test campaign name", campaign.getName()); + assertEquals(CampaignObjective.POST_ENGAGEMENT, campaign.getObjective()); + assertEquals(1000, campaign.getSpendCap()); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getCampaign_unauthorized() throws Exception { + unauthorizedFacebookAds.campaignOperations().getAdCampaign("600123456789"); + } + + @Test + public void getAdCampaignSets() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789/adcampaigns?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-sets"), MediaType.APPLICATION_JSON)); + + PagedList adSets = facebookAds.campaignOperations().getAdCampaignSets("600123456789"); + assertEquals(2, adSets.size()); + assertEquals("123456789", adSets.get(0).getAccountId()); + assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(0).getBidType()); + assertEquals(37407, adSets.get(0).getBudgetRemaining()); + assertEquals("600123456789", adSets.get(0).getCampaignId()); + assertEquals(AdSet.AdSetStatus.PAUSED, adSets.get(0).getStatus()); + assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getCreatedTime()); + assertEquals(40000, adSets.get(0).getDailyBudget()); + assertEquals(toDate("2015-05-29T22:26:40+0200"), adSets.get(0).getEndTime()); + assertEquals("700123456789", adSets.get(0).getId()); + assertTrue(adSets.get(0).isAutobid()); + assertEquals(0, adSets.get(0).getLifetimeBudget()); + assertEquals("Test AdSet", adSets.get(0).getName()); + assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getStartTime()); + assertEquals(Integer.valueOf(65), adSets.get(0).getTargeting().getAgeMax()); + assertEquals(Integer.valueOf(18), adSets.get(0).getTargeting().getAgeMin()); + assertEquals("BR", adSets.get(0).getTargeting().getGeoLocations().getCountries().get(0)); + assertEquals(TargetingLocation.LocationType.HOME, adSets.get(0).getTargeting().getGeoLocations().getLocationTypes().get(0)); + assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getUpdatedTime()); + + assertEquals("123456789", adSets.get(1).getAccountId()); + assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(1).getBidType()); + assertEquals(0, adSets.get(1).getBudgetRemaining()); + assertEquals("600123456789", adSets.get(1).getCampaignId()); + assertEquals(AdSet.AdSetStatus.ACTIVE, adSets.get(1).getStatus()); + assertEquals(toDate("2015-04-10T09:28:54+0200"), adSets.get(1).getCreatedTime()); + assertEquals(0, adSets.get(1).getDailyBudget()); + assertEquals(toDate("2015-04-13T09:19:00+0200"), adSets.get(1).getEndTime()); + assertEquals("701123456789", adSets.get(1).getId()); + assertTrue(adSets.get(1).isAutobid()); + assertEquals(200, adSets.get(1).getLifetimeBudget()); + assertEquals("Real ad set", adSets.get(1).getName()); + assertEquals(toDate("2015-04-12T09:19:00+0200"), adSets.get(1).getStartTime()); + assertEquals(Integer.valueOf(20), adSets.get(1).getTargeting().getAgeMax()); + assertEquals(Integer.valueOf(18), adSets.get(1).getTargeting().getAgeMin()); + assertEquals(6004854404172L, adSets.get(1).getTargeting().getBehaviors().get(0).getId()); + assertEquals("Technology late adopters", adSets.get(1).getTargeting().getBehaviors().get(0).getName()); + assertEquals(Targeting.Gender.MALE, adSets.get(1).getTargeting().getGenders().get(0)); + assertEquals("PL", adSets.get(1).getTargeting().getGeoLocations().getCountries().get(0)); + assertEquals(TargetingLocation.LocationType.HOME, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(0)); + assertEquals(TargetingLocation.LocationType.RECENT, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(1)); + assertEquals(6003629266583L, adSets.get(1).getTargeting().getInterests().get(0).getId()); + assertEquals("Hard drives", adSets.get(1).getTargeting().getInterests().get(0).getName()); + assertEquals(Targeting.PageType.FEED, adSets.get(1).getTargeting().getPageTypes().get(0)); + assertEquals(toDate("2015-04-10T13:32:09+0200"), adSets.get(1).getUpdatedTime()); + + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdCampaignSets_unauthorized() throws Exception { + unauthorizedFacebookAds.campaignOperations().getAdCampaignSets("600123456789"); + } + + @Test + public void createCampaign_withNameOnly() throws Exception { + String requestBody = "name=Campaign+created+by+SpringSocialFacebook"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setName("Campaign created by SpringSocialFacebook"); + assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign)); + mockServer.verify(); + } + + @Test + public void createCampaign_withStatusOnly() throws Exception { + String requestBody = "campaign_group_status=PAUSED"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setStatus(CampaignStatus.PAUSED); + assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign)); + mockServer.verify(); + } + + @Test + public void createCampaign_withInvalidStatus() throws Exception { + // new campaigns can be created only with statuses ACTIVE or PAUSED + String requestBody = "name=Campaign+with+invalid+status&campaign_group_status=ARCHIVED"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withBadRequest().body(jsonResource("error-invalid-update-campaign-status")).contentType(MediaType.APPLICATION_JSON)); + + AdCampaign campaign = new AdCampaign(); + campaign.setName("Campaign with invalid status"); + campaign.setStatus(CampaignStatus.ARCHIVED); + try { + facebookAds.campaignOperations().createAdCampaign("123456789", campaign); + fail(); + } catch (InvalidCampaignStatusException e) { + assertEquals("New campaigns need to be either active or paused.", e.getMessage()); + assertEquals("facebook", e.getProviderId()); + } + } + + @Test + public void createCampaign_withObjectiveOnly() throws Exception { + String requestBody = "objective=VIDEO_VIEWS"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setObjective(CampaignObjective.VIDEO_VIEWS); + assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign)); + mockServer.verify(); + } + + @Test + public void createCampaign_withSpendCapOnly() throws Exception { + String requestBody = "spend_cap=240000"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setSpendCap(240000); + assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign)); + mockServer.verify(); + } + + @Test + public void createCampaign_withBuyingTypeOnly() throws Exception { + String requestBody = "buying_type=AUCTION"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setBuyingType(BuyingType.AUCTION); + assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign)); + mockServer.verify(); + } + + + @Test + public void createCampaign_withAllFields() throws Exception { + String requestBody = "name=Full+campaign&campaign_group_status=ACTIVE&objective=PAGE_LIKES&spend_cap=60000&buying_type=RESERVED"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setName("Full campaign"); + campaign.setStatus(CampaignStatus.ACTIVE); + campaign.setObjective(CampaignObjective.PAGE_LIKES); + campaign.setSpendCap(60000); + campaign.setBuyingType(BuyingType.RESERVED); + assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign)); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void createCampaign_unauthorized() throws Exception { + unauthorizedFacebookAds.campaignOperations().createAdCampaign("123456789", new AdCampaign()); + } + + @Test + public void updateAdCampaign_withNameOnly() throws Exception { + String requestBody = "name=New+campaign+name"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setName("New campaign name"); + assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign)); + mockServer.verify(); + } + + @Test + public void updateAdCampaign_withStatusOnly() throws Exception { + String requestBody = "campaign_group_status=ACTIVE"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setStatus(CampaignStatus.ACTIVE); + assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign)); + mockServer.verify(); + } + + @Test + public void updateAdCampaign_withObjectiveOnly() throws Exception { + String requestBody = "objective=POST_ENGAGEMENT"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setObjective(CampaignObjective.POST_ENGAGEMENT); + assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign)); + mockServer.verify(); + } + + @Test + public void updateAdCampaign_withSpendCapOnly() throws Exception { + String requestBody = "spend_cap=60000"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setSpendCap(60000); + assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign)); + mockServer.verify(); + } + + @Test + public void updateAdCampaign_withAllFields() throws Exception { + String requestBody = "name=Updated+campaign&campaign_group_status=ARCHIVED&objective=CANVAS_APP_ENGAGEMENT&spend_cap=60000"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + AdCampaign campaign = new AdCampaign(); + campaign.setName("Updated campaign"); + campaign.setStatus(CampaignStatus.ARCHIVED); + campaign.setObjective(CampaignObjective.CANVAS_APP_ENGAGEMENT); + campaign.setSpendCap(60000); + assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign)); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void updateAdCampaign_unauthorized() throws Exception { + unauthorizedFacebookAds.campaignOperations().updateAdCampaign("600123456789", new AdCampaign()); + } + + @Test + public void deleteAdCampaign() throws Exception { + String requestBody = "campaign_group_status=DELETED"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + facebookAds.campaignOperations().deleteAdCampaign("600123456789"); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void deleteAdCampaign_unauthorized() throws Exception { + unauthorizedFacebookAds.campaignOperations().deleteAdCampaign("600123456789"); + } + + @Test + public void getAdCampaignInsights() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-campaign-insights"), MediaType.APPLICATION_JSON)); + + AdInsight insight = facebookAds.campaignOperations().getAdCampaignInsight("600123456789"); + assertEquals("123456789", insight.getAccountId()); + assertEquals("Test account name", insight.getAccountName()); + assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON); + assertEquals(8, insight.getClicks()); + assertEquals(5, insight.getUniqueClicks()); + assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON); + assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON); + assertEquals(0.25, insight.getCostPerClick(), EPSILON); + assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON); + assertEquals(10.695187165775, insight.getCpm(), EPSILON); + assertEquals(10.869565217391, insight.getCpp(), EPSILON); + assertEquals(4.2780748663102, insight.getCtr(), EPSILON); + assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON); + assertEquals(1.0163043478261, insight.getFrequency(), EPSILON); + assertEquals(187, insight.getImpressions()); + assertEquals(184, insight.getUniqueImpressions()); + assertEquals(184, insight.getReach()); + assertEquals(1.6042780748663, insight.getResultRate(), EPSILON); + assertEquals(3, insight.getResults()); + assertEquals(0, insight.getRoas()); + assertEquals(0, insight.getSocialClicks()); + assertEquals(0, insight.getUniqueSocialClicks()); + assertEquals(0, insight.getSocialImpressions()); + assertEquals(0, insight.getUniqueSocialImpressions()); + assertEquals(0, insight.getSocialReach()); + assertEquals(2, insight.getSpend()); + assertEquals(0, insight.getTodaySpend()); + assertEquals(0, insight.getTotalActionValue()); + assertEquals(3, insight.getTotalActions()); + assertEquals(2, insight.getTotalUniqueActions()); + assertEquals(4, insight.getActions().size()); + assertEquals("comment", insight.getActions().get(0).getActionType()); + assertEquals(2, insight.getActions().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getActions().get(1).getActionType()); + assertEquals(1, insight.getActions().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getActions().get(2).getActionType()); + assertEquals(3, insight.getActions().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getActions().get(3).getActionType()); + assertEquals(3, insight.getActions().get(3).getValue(), EPSILON); + assertEquals(4, insight.getUniqueActions().size()); + assertEquals("comment", insight.getUniqueActions().get(0).getActionType()); + assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getUniqueActions().get(1).getActionType()); + assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType()); + assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType()); + assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON); + assertEquals(4, insight.getCostPerActionType().size()); + assertEquals("comment", insight.getCostPerActionType().get(0).getActionType()); + assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON); + assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType()); + assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON); + assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType()); + assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON); + assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType()); + assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON); + assertEquals(1, insight.getVideoStartActions().size()); + assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType()); + assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON); + + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdCampaignInsights_unauthorized() throws Exception { + unauthorizedFacebookAds.campaignOperations().getAdCampaignInsight("600123456789"); + } +} diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CreativeTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CreativeTemplateTest.java new file mode 100644 index 000000000..76743a57c --- /dev/null +++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CreativeTemplateTest.java @@ -0,0 +1,220 @@ +package org.springframework.social.facebook.api.ads; + +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.social.NotAuthorizedException; +import org.springframework.social.facebook.api.PagedList; + +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * @author Sebastian Górecki + */ +public class CreativeTemplateTest extends AbstractFacebookAdsApiTest { + + @Test + public void getAccountCreatives() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcreatives?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-creatives"), MediaType.APPLICATION_JSON)); + + List creatives = facebookAds.creativeOperations().getAccountCreatives("123456789"); + verifyAdCreatives(creatives); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAccountCreatives_unauthorized() throws Exception { + unauthorizedFacebookAds.creativeOperations().getAccountCreatives("123456789"); + } + + @Test + public void getAdSetCreatives() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789/adcreatives?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-creatives"), MediaType.APPLICATION_JSON)); + + List creatives = facebookAds.creativeOperations().getAdSetCreatives("700123456789"); + verifyAdCreatives(creatives); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdSetCreatives_unauthorized() throws Exception { + unauthorizedFacebookAds.creativeOperations().getAdSetCreatives("700123456789"); + } + + @Test + public void getAdCreative() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-creative"), MediaType.APPLICATION_JSON)); + + AdCreative creative = facebookAds.creativeOperations().getAdCreative("800123456789"); + assertEquals("800123456789", creative.getId()); + assertEquals(AdCreative.AdCreativeType.STATUS, creative.getType()); + assertEquals("https://example.net/safe_image.php?d=123456789", creative.getThumbnailUrl()); + assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creative.getStatus()); + assertEquals("123456789_987654321", creative.getObjectStoryId()); + assertEquals("Creative test name", creative.getName()); + mockServer.verify(); + } + + @Test + public void getAdCreative_withWrongType() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/801123456789?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-creative-wrong-type"), MediaType.APPLICATION_JSON)); + AdCreative creative = facebookAds.creativeOperations().getAdCreative("801123456789"); + assertEquals("801123456789", creative.getId()); + assertEquals(AdCreative.AdCreativeType.UNKNOWN, creative.getType()); + verifyAdCreative(creative); + assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creative.getStatus()); + mockServer.verify(); + } + + @Test + public void getAdCreative_withWrongStatus() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/802123456789?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid")) + .andExpect(method(GET)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess(jsonResource("ad-creative-wrong-status"), MediaType.APPLICATION_JSON)); + AdCreative creative = facebookAds.creativeOperations().getAdCreative("802123456789"); + assertEquals("802123456789", creative.getId()); + assertEquals(AdCreative.AdCreativeType.DOMAIN, creative.getType()); + verifyAdCreative(creative); + assertEquals(AdCreative.AdCreativeStatus.UNKNOWN, creative.getStatus()); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void getAdCreative_unauthorized() throws Exception { + unauthorizedFacebookAds.creativeOperations().getAdCreative("800123456789"); + } + + @Test + public void createAdCreative_linkAd() throws Exception { + String requestBody = "title=Ad+creative+title&body=Over+the+past+few+years+REST+has+become+an+important&image_url=http%3A%2F%2Fexample.com%2Fstatics_s2_20150603-0041%2Fstyles%2Fi%2Flogo_bigger.jpg&object_url=http%3A%2F%2Fwww.example.com%2Fsome_object"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcreatives")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"801123456789\"}", MediaType.APPLICATION_JSON)); + + AdCreative creative = new AdCreative(); + creative.setTitle("Ad creative title"); + creative.setObjectUrl("http://www.example.com/some_object"); + creative.setBody("Over the past few years REST has become an important"); + creative.setImageUrl("http://example.com/statics_s2_20150603-0041/styles/i/logo_bigger.jpg"); + assertEquals("801123456789", facebookAds.creativeOperations().createAdCreative("123456789", creative)); + mockServer.verify(); + } + + @Test + public void createAdCreative_postLikeAd() throws Exception { + String requestBody = "name=Post+Like+Ad+Creative&body=Some+body+here&object_id=600123456789"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcreatives")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"id\": \"802123456789\"}", MediaType.APPLICATION_JSON)); + + AdCreative creative = new AdCreative(); + creative.setName("Post Like Ad Creative"); + creative.setObjectId("600123456789"); + creative.setBody("Some body here"); + assertEquals("802123456789", facebookAds.creativeOperations().createAdCreative("123456789", creative)); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void createAdCreative_unauthorized() throws Exception { + unauthorizedFacebookAds.creativeOperations().createAdCreative("123456789", new AdCreative()); + } + + @Test + public void renameAdCreative() throws Exception { + String requestBody = "name=New+creative+name"; + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789")) + .andExpect(method(POST)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andExpect(content().string(requestBody)) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + + facebookAds.creativeOperations().renameAdCreative("800123456789", "New creative name"); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void renameAdCreative_unauthorized() throws Exception { + unauthorizedFacebookAds.creativeOperations().renameAdCreative("800123456789", "New anauthorized name!"); + } + + @Test + public void deleteAdCreative() throws Exception { + mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789")) + .andExpect(method(DELETE)) + .andExpect(header("Authorization", "OAuth someAccessToken")) + .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON)); + + facebookAds.creativeOperations().deleteAdCreative("800123456789"); + mockServer.verify(); + } + + @Test(expected = NotAuthorizedException.class) + public void deleteAdCreative_unauthorized() throws Exception { + unauthorizedFacebookAds.creativeOperations().deleteAdCreative("800123456789"); + } + + private void verifyAdCreatives(List creatives) { + assertEquals(3, creatives.size()); + assertEquals("This is the body of the ad creative1", creatives.get(0).getBody()); + assertEquals("ac67a06abb35a3bef8256c8c9085548e1", creatives.get(0).getImageHash()); + assertEquals("https://example.com/images/image1.png", creatives.get(0).getImageUrl()); + assertEquals("Name of the ad creative1", creatives.get(0).getName()); + assertEquals("900123456789", creatives.get(0).getObjectId()); + assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creatives.get(0).getStatus()); + assertEquals("https://example.com/thumbnails/thuumbnail_url1", creatives.get(0).getThumbnailUrl()); + assertEquals(AdCreative.AdCreativeType.PAGE, creatives.get(0).getType()); + assertEquals("801123456789", creatives.get(0).getId()); + assertEquals("This is the body of the ad creative2", creatives.get(1).getBody()); + assertEquals("ac67a06abb35a3bef8256c8c9085548e2", creatives.get(1).getImageHash()); + assertEquals("https://example.com/images/image2.png", creatives.get(1).getImageUrl()); + assertEquals("Name of the ad creative2", creatives.get(1).getName()); + assertEquals("http://example.com/objects/object", creatives.get(1).getObjectUrl()); + assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creatives.get(1).getStatus()); + assertEquals("Test Ad Creative", creatives.get(1).getTitle()); + assertEquals("https://example.com/thumbnails/thuumbnail_url2", creatives.get(1).getThumbnailUrl()); + assertEquals(AdCreative.AdCreativeType.DOMAIN, creatives.get(1).getType()); + assertEquals("802123456789", creatives.get(1).getId()); + assertEquals("This is the body of the ad creative3", creatives.get(2).getName()); + assertEquals("987654321_123456789", creatives.get(2).getObjectStoryId()); + assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creatives.get(2).getStatus()); + assertEquals("https://example.com/thumbnails/thuumbnail_url3", creatives.get(2).getThumbnailUrl()); + assertEquals(AdCreative.AdCreativeType.STATUS, creatives.get(2).getType()); + assertEquals("803123456789", creatives.get(2).getId()); + } + + private void verifyAdCreative(AdCreative creative) { + assertEquals("This is the body of the ad creative", creative.getBody()); + assertEquals("ac67a06abb35a3bef8256c8c9085548e", creative.getImageHash()); + assertEquals("https://example.com/images/image.png", creative.getImageUrl()); + assertEquals("Name of the ad creative", creative.getName()); + assertEquals("http://example.com/objects/object", creative.getObjectUrl()); + assertEquals("Title of the ad creative", creative.getTitle()); + assertEquals("https://example.com/thumbnails/thuumbnail_url", creative.getThumbnailUrl()); + } +} diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-campaigns.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-campaigns.json new file mode 100644 index 000000000..764a787db --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-campaigns.json @@ -0,0 +1,35 @@ +{ + "data": [ + { + "id": "601123456789", + "account_id": "123456789", + "buying_type": "AUCTION", + "campaign_group_status": "ACTIVE", + "name": "Campaign #1", + "objective": "POST_ENGAGEMENT" + }, + { + "id": "602123456789", + "account_id": "123456789", + "buying_type": "FIXED_CPM", + "campaign_group_status": "PAUSED", + "name": "Campaign #2", + "objective": "NONE" + }, + { + "id": "603123456789", + "account_id": "123456789", + "buying_type": "RESERVED", + "campaign_group_status": "ARCHIVED", + "name": "Campaign #3", + "objective": "WEBSITE_CONVERSIONS", + "spend_cap": 50000 + } + ], + "paging": { + "cursors": { + "before": "NjAyNDE2Nzg5NTY1MA==", + "after": "NjAyNTIwMDQ1MTQ1MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-insights.json new file mode 100644 index 000000000..a1af55448 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-insights.json @@ -0,0 +1,106 @@ +{ + "data": [ + { + "account_id": "123456789", + "account_name": "Account Test Name #1", + "date_start": "2015-02-19", + "date_stop": "2015-04-23", + "actions_per_impression": 0.016042780748663, + "clicks": 8, + "unique_clicks": 5, + "cost_per_result": 0.66666666666667, + "cost_per_total_action": 0.66666666666667, + "cpc": 0.25, + "cost_per_unique_click": 0.4, + "cpm": 10.695187165775, + "cpp": 10.869565217391, + "ctr": 4.2780748663102, + "unique_ctr": 2.7173913043478, + "frequency": 1.0163043478261, + "impressions": "187", + "unique_impressions": 184, + "reach": 184, + "result_rate": 1.6042780748663, + "results": 3, + "roas": 1, + "social_clicks": 2, + "unique_social_clicks": 3, + "social_impressions": 4, + "unique_social_impressions": 5, + "social_reach": 6, + "spend": 2, + "today_spend": 0, + "total_action_value": 0, + "total_actions": 3, + "total_unique_actions": 2, + "actions": [ + { + "action_type": "comment", + "value": 2 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 3 + }, + { + "action_type": "post_engagement", + "value": 3 + } + ], + "unique_actions": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 2 + }, + { + "action_type": "post_engagement", + "value": 2 + } + ], + "cost_per_action_type": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 2 + }, + { + "action_type": "page_engagement", + "value": 0.66666666666667 + }, + { + "action_type": "post_engagement", + "value": 0.66666666666667 + } + ], + "video_start_actions": [ + { + "action_type": "video_view", + "value": 0 + } + ], + "date_start": "2015-02-19", + "date_stop": "2015-04-21" + } + ], + "paging": { + "cursors": { + "before": "MA==", + "after": "MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-no-users.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-no-users.json new file mode 100644 index 000000000..af48c322b --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-no-users.json @@ -0,0 +1,44 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-temporarily-unavailable.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-temporarily-unavailable.json new file mode 100644 index 000000000..9858ecc61 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-temporarily-unavailable.json @@ -0,0 +1,53 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 101, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + } + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-unknown-capabilities.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-unknown-capabilities.json new file mode 100644 index 000000000..72057e89f --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-unknown-capabilities.json @@ -0,0 +1,10 @@ +{ + "capabilities": [ + "UNKNOWN_CAPABILITY1", + "UNKNOWN_CAPABILITY2", + "UNKNOWN_CAPABILITY3", + "PREMIUM" + ], + "account_id": 123456789, + "id": "act_123456789" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-users.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-users.json new file mode 100644 index 000000000..7fe5875f6 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-users.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "id": "123456789", + "name": "Account #1", + "permissions": [ + 1, + 2, + 3, + 4, + 5, + 7 + ], + "role": 1001 + }, + { + "id": "987654321", + "name": "Account #2", + "permissions": [ + 7 + ], + "role": 1003 + }, + { + "id": "1122334455", + "name": "Account #3", + "permissions": [ + 2, + 4 + ], + "role": 1004 + } + ] +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-agency.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-agency.json new file mode 100644 index 000000000..d44c3db26 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-agency.json @@ -0,0 +1,67 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "agency_client_declaration": { + "agency_representing_client": 1, + "client_based_in_france": 0, + "client_city": "Warsaw", + "client_country_code": "PL", + "client_email_address": "example@example.com", + "client_name": "Some client", + "client_postal_code": "66-777", + "client_province": "malopolska", + "client_street": "Marszalkowska", + "client_street2": "1A", + "has_written_mandate_from_advertiser": 0, + "is_client_paying_invoices": 1 + }, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + } + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-few-users.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-few-users.json new file mode 100644 index 000000000..0a4fea6dd --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-few-users.json @@ -0,0 +1,61 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + }, + { + "id": "3421", + "permissions": [ + 5, + 7 + ], + "role": 1003 + } + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-funding-details.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-funding-details.json new file mode 100644 index 000000000..627697608 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-funding-details.json @@ -0,0 +1,58 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "funding_source_details": { + "id": "12345678987654321", + "display_string": "Visa *0001", + "type": 1 + }, + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + } + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-tos-accepted.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-tos-accepted.json new file mode 100644 index 000000000..742a628a9 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-tos-accepted.json @@ -0,0 +1,57 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "tos_accepted": { + "206760949512025": 1, + "215449065224656": 1 + }, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + } + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-without-permission.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-without-permission.json new file mode 100644 index 000000000..dae3477ba --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-without-permission.json @@ -0,0 +1,50 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + ], + "role": 1001 + } + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account.json new file mode 100644 index 000000000..1dfeb8673 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account.json @@ -0,0 +1,53 @@ +{ + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + } + ] + }, + "tax_id_status": 3 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-accounts.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-accounts.json new file mode 100644 index 000000000..d2636341a --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-accounts.json @@ -0,0 +1,122 @@ +{ + "data": [ + { + "id": "act_123456789", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + } + ], + "account_id": 123456789, + "account_status": 1, + "age": 10.0036226851851852, + "amount_spent": "1234", + "balance": "10000", + "business_city": "Poznan", + "business_country_code": "PL", + "business_name": "Some business name for the account", + "business_state": "wielkopolska", + "business_street": "Some street 79A", + "business_zip": "66-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-02-19T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": "93263", + "end_advertiser": 987654321, + "funding_source": "1122334455", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": "0", + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + } + ] + }, + "tax_id_status": 3 + }, + { + "id": "act_77777777", + "account_groups": [ + { + "account_group_id": "987654321", + "name": "Test group name", + "status": 1 + }, + { + "account_group_id": "11223344", + "name": "Test group name 2", + "status": 1 + } + + ], + "account_id": 77777777, + "account_status": 2, + "age": 777.777, + "amount_spent": 7777, + "balance": 77777, + "business_city": "Warsaw", + "business_country_code": "PL", + "business_name": "Some business name for the account 2", + "business_state": "mazowieckie", + "business_street": "Some street 2", + "business_zip": "77-777", + "capabilities": [ + "DIRECT_SALES", + "VIEW_TAGS" + ], + "created_time": "2015-04-20T00:31:33+0100", + "currency": "PLN", + "daily_spend_limit": 77, + "end_advertiser": 987654321, + "funding_source": "77", + "is_personal": 1, + "media_agency": 54321, + "name": "This is a test account 2", + "offsite_pixels_tos_accepted": false, + "partner": 111222333, + "spend_cap": 0, + "timezone_id": 106, + "timezone_name": "Europe/Warsaw", + "timezone_offset_hours_utc": 2, + "users": { + "data": [ + { + "id": "1234", + "permissions": [ + 1, + 2, + 3 + ], + "role": 1001 + } + ] + }, + "tax_id_status": 5 + } + ], + "paging": { + "cursors": { + "before": "NjAyMjEyODA0OTQ1MA==", + "after": "NjAyMjEyODA0OTQ1MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-empty-buying-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-empty-buying-type.json new file mode 100644 index 000000000..221b3d224 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-empty-buying-type.json @@ -0,0 +1,8 @@ +{ + "id": "609123456789", + "account_id": "123456789", + "campaign_group_status": "ACTIVE", + "name": "The test campaign name", + "objective": "POST_ENGAGEMENT", + "spend_cap": 1000 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-insights.json new file mode 100644 index 000000000..2b40792de --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-insights.json @@ -0,0 +1,105 @@ +{ + "data": [ + { + "account_id": "123456789", + "account_name": "Test account name", + "date_start": "2015-04-12", + "date_stop": "2015-04-13", + "actions_per_impression": 0.016042780748663, + "clicks": 8, + "unique_clicks": 5, + "cost_per_result": 0.66666666666667, + "cost_per_total_action": 0.66666666666667, + "cpc": 0.25, + "cost_per_unique_click": 0.4, + "cpm": 10.695187165775, + "cpp": 10.869565217391, + "ctr": 4.2780748663102, + "unique_ctr": 2.7173913043478, + "frequency": 1.0163043478261, + "impressions": "187", + "unique_impressions": 184, + "objective": "POST_ENGAGEMENT", + "reach": 184, + "result_rate": 1.6042780748663, + "results": 3, + "roas": 0, + "social_clicks": 0, + "unique_social_clicks": 0, + "social_impressions": 0, + "unique_social_impressions": 0, + "social_reach": 0, + "spend": 2, + "today_spend": 0, + "total_action_value": 0, + "total_actions": 3, + "total_unique_actions": 2, + "actions": [ + { + "action_type": "comment", + "value": 2 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 3 + }, + { + "action_type": "post_engagement", + "value": 3 + } + ], + "unique_actions": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 2 + }, + { + "action_type": "post_engagement", + "value": 2 + } + ], + "cost_per_action_type": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 2 + }, + { + "action_type": "page_engagement", + "value": 0.66666666666667 + }, + { + "action_type": "post_engagement", + "value": 0.66666666666667 + } + ], + "video_start_actions": [ + { + "action_type": "video_view", + "value": 0 + } + ] + } + ], + "paging": { + "cursors": { + "before": "MA==", + "after": "MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-with-unknown-enums.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-with-unknown-enums.json new file mode 100644 index 000000000..d1489a710 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-with-unknown-enums.json @@ -0,0 +1,8 @@ +{ + "id": "600123456789", + "account_id": "123456789", + "buying_type": "OTHER", + "campaign_group_status": "DEACTIVATED", + "name": "The test campaign name", + "objective": "AD_SELLING" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign.json new file mode 100644 index 000000000..cf9511acc --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign.json @@ -0,0 +1,9 @@ +{ + "id": "600123456789", + "account_id": "123456789", + "buying_type": "AUCTION", + "campaign_group_status": "ACTIVE", + "name": "The test campaign name", + "objective": "POST_ENGAGEMENT", + "spend_cap": 1000 +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-link-ad.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-link-ad.json new file mode 100644 index 000000000..c722b4179 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-link-ad.json @@ -0,0 +1,12 @@ +{ + "body": "This is the body of the ad creative", + "image_hash": "ac67a06abb35a3bef8256c8c9085548e", + "image_url": "https://example.com/images/image.png", + "name": "Name of the ad creative", + "object_url": "http://example.com/objects/object", + "run_status": "ACTIVE", + "title": "Title of the ad creative", + "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url", + "object_type": "DOMAIN", + "id": "803123456789" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-status.json new file mode 100644 index 000000000..2aeec90b0 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-status.json @@ -0,0 +1,12 @@ +{ + "body": "This is the body of the ad creative", + "image_hash": "ac67a06abb35a3bef8256c8c9085548e", + "image_url": "https://example.com/images/image.png", + "name": "Name of the ad creative", + "object_url": "http://example.com/objects/object", + "run_status": "WRONG_STATUS", + "title": "Title of the ad creative", + "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url", + "object_type": "DOMAIN", + "id": "802123456789" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-type.json new file mode 100644 index 000000000..f4051aabb --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-type.json @@ -0,0 +1,12 @@ +{ + "body": "This is the body of the ad creative", + "image_hash": "ac67a06abb35a3bef8256c8c9085548e", + "image_url": "https://example.com/images/image.png", + "name": "Name of the ad creative", + "object_url": "http://example.com/objects/object", + "run_status": "ACTIVE", + "title": "Title of the ad creative", + "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url", + "object_type": "WRONG_TYPE", + "id": "801123456789" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative.json new file mode 100644 index 000000000..bd207d7a0 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative.json @@ -0,0 +1,8 @@ +{ + "name": "Creative test name", + "object_story_id": "123456789_987654321", + "run_status": "ACTIVE", + "thumbnail_url": "https://example.net/safe_image.php?d=123456789", + "object_type": "STATUS", + "id": "800123456789" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creatives.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creatives.json new file mode 100644 index 000000000..1fd77f820 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creatives.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "body": "This is the body of the ad creative1", + "image_hash": "ac67a06abb35a3bef8256c8c9085548e1", + "image_url": "https://example.com/images/image1.png", + "name": "Name of the ad creative1", + "object_id": "900123456789", + "run_status": "ACTIVE", + "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url1", + "object_type": "PAGE", + "id": "801123456789" + }, + { + "body": "This is the body of the ad creative2", + "image_hash": "ac67a06abb35a3bef8256c8c9085548e2", + "image_url": "https://example.com/images/image2.png", + "name": "Name of the ad creative2", + "object_url": "http://example.com/objects/object", + "run_status": "ACTIVE", + "title": "Test Ad Creative", + "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url2", + "object_type": "DOMAIN", + "id": "802123456789" + }, + { + "name": "This is the body of the ad creative3", + "object_story_id": "987654321_123456789", + "run_status": "ACTIVE", + "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url3", + "object_type": "STATUS", + "id": "803123456789" + } + ], + "paging": { + "cursors": { + "before": "NjAyNjU3MzQzNDQ1MA==", + "after": "NjAyNDE2Nzg5NjY1MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-insights.json new file mode 100644 index 000000000..2b40792de --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-insights.json @@ -0,0 +1,105 @@ +{ + "data": [ + { + "account_id": "123456789", + "account_name": "Test account name", + "date_start": "2015-04-12", + "date_stop": "2015-04-13", + "actions_per_impression": 0.016042780748663, + "clicks": 8, + "unique_clicks": 5, + "cost_per_result": 0.66666666666667, + "cost_per_total_action": 0.66666666666667, + "cpc": 0.25, + "cost_per_unique_click": 0.4, + "cpm": 10.695187165775, + "cpp": 10.869565217391, + "ctr": 4.2780748663102, + "unique_ctr": 2.7173913043478, + "frequency": 1.0163043478261, + "impressions": "187", + "unique_impressions": 184, + "objective": "POST_ENGAGEMENT", + "reach": 184, + "result_rate": 1.6042780748663, + "results": 3, + "roas": 0, + "social_clicks": 0, + "unique_social_clicks": 0, + "social_impressions": 0, + "unique_social_impressions": 0, + "social_reach": 0, + "spend": 2, + "today_spend": 0, + "total_action_value": 0, + "total_actions": 3, + "total_unique_actions": 2, + "actions": [ + { + "action_type": "comment", + "value": 2 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 3 + }, + { + "action_type": "post_engagement", + "value": 3 + } + ], + "unique_actions": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 2 + }, + { + "action_type": "post_engagement", + "value": 2 + } + ], + "cost_per_action_type": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 2 + }, + { + "action_type": "page_engagement", + "value": 0.66666666666667 + }, + { + "action_type": "post_engagement", + "value": 0.66666666666667 + } + ], + "video_start_actions": [ + { + "action_type": "video_view", + "value": 0 + } + ] + } + ], + "paging": { + "cursors": { + "before": "MA==", + "after": "MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-account.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-account.json new file mode 100644 index 000000000..6f2fea91c --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-account.json @@ -0,0 +1,73 @@ +{ + "data": [ + { + "id": "101123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "801123456789", + "campaign_group_id": "701123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name1", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + }, + { + "id": "102123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "802123456789", + "campaign_group_id": "702123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name2", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + }, + { + "id": "103123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "803123456789", + "campaign_group_id": "703123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name3", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + } + ], + "paging": { + "cursors": { + "before": "NjAyMjEyODA0OTQ1MA==", + "after": "NjAyMjEyODA0OTQ1MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-ad-set.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-ad-set.json new file mode 100644 index 000000000..b472825e6 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-ad-set.json @@ -0,0 +1,52 @@ +{ + "data": [ + { + "id": "101123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "800123456789", + "campaign_group_id": "700123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name1", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + }, + { + "id": "102123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "800123456789", + "campaign_group_id": "701123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name2", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + } + ], + "paging": { + "cursors": { + "before": "NjAyMjEyODA0OTQ1MA==", + "after": "NjAyMjEyODA0OTQ1MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-campaign.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-campaign.json new file mode 100644 index 000000000..99a36f3ce --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-campaign.json @@ -0,0 +1,73 @@ +{ + "data": [ + { + "id": "101123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "801123456789", + "campaign_group_id": "701123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name1", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + }, + { + "id": "102123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "802123456789", + "campaign_group_id": "701123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name2", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + }, + { + "id": "103123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "803123456789", + "campaign_group_id": "701123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name3", + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "updated_time": "2015-04-10T13:32:09+0200" + } + ], + "paging": { + "cursors": { + "before": "NjAyMjEyODA0OTQ1MA==", + "after": "NjAyMjEyODA0OTQ1MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-insights.json new file mode 100644 index 000000000..2b40792de --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-insights.json @@ -0,0 +1,105 @@ +{ + "data": [ + { + "account_id": "123456789", + "account_name": "Test account name", + "date_start": "2015-04-12", + "date_stop": "2015-04-13", + "actions_per_impression": 0.016042780748663, + "clicks": 8, + "unique_clicks": 5, + "cost_per_result": 0.66666666666667, + "cost_per_total_action": 0.66666666666667, + "cpc": 0.25, + "cost_per_unique_click": 0.4, + "cpm": 10.695187165775, + "cpp": 10.869565217391, + "ctr": 4.2780748663102, + "unique_ctr": 2.7173913043478, + "frequency": 1.0163043478261, + "impressions": "187", + "unique_impressions": 184, + "objective": "POST_ENGAGEMENT", + "reach": 184, + "result_rate": 1.6042780748663, + "results": 3, + "roas": 0, + "social_clicks": 0, + "unique_social_clicks": 0, + "social_impressions": 0, + "unique_social_impressions": 0, + "social_reach": 0, + "spend": 2, + "today_spend": 0, + "total_action_value": 0, + "total_actions": 3, + "total_unique_actions": 2, + "actions": [ + { + "action_type": "comment", + "value": 2 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 3 + }, + { + "action_type": "post_engagement", + "value": 3 + } + ], + "unique_actions": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 1 + }, + { + "action_type": "page_engagement", + "value": 2 + }, + { + "action_type": "post_engagement", + "value": 2 + } + ], + "cost_per_action_type": [ + { + "action_type": "comment", + "value": 1 + }, + { + "action_type": "post_like", + "value": 2 + }, + { + "action_type": "page_engagement", + "value": 0.66666666666667 + }, + { + "action_type": "post_engagement", + "value": 0.66666666666667 + } + ], + "video_start_actions": [ + { + "action_type": "video_view", + "value": 0 + } + ] + } + ], + "paging": { + "cursors": { + "before": "MA==", + "after": "MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-promoted-object.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-promoted-object.json new file mode 100644 index 000000000..41838d2ec --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-promoted-object.json @@ -0,0 +1,33 @@ +{ + "id": "705123456789", + "account_id": "123456789", + "bid_info": { + "CLICKS": 500 + }, + "bid_type": "ABSOLUTE_OCPM", + "budget_remaining": 807, + "campaign_group_id": "600123456789", + "campaign_status": "PAUSED", + "created_time": "2015-07-06T14:18:55+0200", + "daily_budget": 2000, + "is_autobid": false, + "lifetime_budget": 0, + "name": "Test promoted object", + "promoted_object": { + "page_id": "999888777666555" + }, + "start_time": "2015-07-06T14:18:55+0200", + "targeting": { + "age_max": 65, + "age_min": 18, + "geo_locations": { + "countries": [ + "PL" + ], + "location_types": [ + "home" + ] + } + }, + "updated_time": "2015-07-06T14:18:55+0200" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-bid-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-bid-type.json new file mode 100644 index 000000000..372393f63 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-bid-type.json @@ -0,0 +1,23 @@ +{ + "id": "710123456789", + "account_id": "123456789", + "campaign_group_id": "600123456789", + "name": "Test AdSet", + "campaign_status": "ACTIVE", + "is_autobid": true, + "bid_type": "WRONG_BID_TYPE", + "budget_remaining": 50, + "daily_budget": 0, + "lifetime_budget": 200, + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "start_time": "2015-04-12T09:19:00+0200", + "end_time": "2015-04-13T09:19:00+0200", + "created_time": "2015-04-10T09:28:54+0200", + "updated_time": "2015-04-10T13:32:09+0200" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-status.json new file mode 100644 index 000000000..44a73105c --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-status.json @@ -0,0 +1,23 @@ +{ + "id": "709123456789", + "account_id": "123456789", + "campaign_group_id": "600123456789", + "name": "Test AdSet", + "campaign_status": "WRONG_STATUS", + "is_autobid": true, + "bid_type": "ABSOLUTE_OCPM", + "budget_remaining": 50, + "daily_budget": 0, + "lifetime_budget": 200, + "targeting": { + "geo_locations": { + "countries": [ + "PL" + ] + } + }, + "start_time": "2015-04-12T09:19:00+0200", + "end_time": "2015-04-13T09:19:00+0200", + "created_time": "2015-04-10T09:28:54+0200", + "updated_time": "2015-04-10T13:32:09+0200" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set.json new file mode 100644 index 000000000..88619f964 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set.json @@ -0,0 +1,106 @@ +{ + "id": "700123456789", + "account_id": "123456789", + "campaign_group_id": "600123456789", + "name": "Test AdSet", + "campaign_status": "ACTIVE", + "is_autobid": true, + "bid_type": "ABSOLUTE_OCPM", + "budget_remaining": 50, + "daily_budget": 0, + "lifetime_budget": 200, + "targeting": { + "age_max": 20, + "age_min": 18, + "behaviors": [ + { + "id": "6004854404172", + "name": "Technology late adopters" + } + ], + "genders": [ + 1 + ], + "relationship_statuses": [ + 2, + 9 + ], + "interested_in": [ + 2 + ], + "geo_locations": { + "countries": [ + "PL" + ], + "regions": [ + { + "key": "3847" + }, + { + "key": "1122" + } + ], + "cities": [ + { + "key": "2430536", + "radius": 12, + "distance_unit": "mile" + }, + { + "key": "11223344", + "radius": 55, + "distance_unit": "kilometer" + } + ], + "zips": [ + { + "key": "US:94304" + }, + { + "key": "US:00501" + } + ], + "location_types": [ + "home", + "recent" + ] + }, + "interests": [ + { + "id": "6003629266583", + "name": "Hard drives" + } + ], + "page_types": [ + "feed", + "desktop-and-mobile-and-external" + ], + "education_schools": [ + { + "id": 105930651606, + "name": "Harvard University" + } + ], + "education_statuses": [ + 1, + 9, + 13 + ], + "work_employers": [ + { + "id": "50431654", + "name": "Microsoft" + } + ], + "work_positions": [ + { + "id": 105763692790962, + "name": "Business Analyst" + } + ] + }, + "start_time": "2015-04-12T09:19:00+0200", + "end_time": "2015-04-13T09:19:00+0200", + "created_time": "2015-04-10T09:28:54+0200", + "updated_time": "2015-04-10T13:32:09+0200" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-sets.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-sets.json new file mode 100644 index 000000000..306032dcf --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-sets.json @@ -0,0 +1,85 @@ +{ + "data": [ + { + "account_id": "123456789", + "bid_type": "ABSOLUTE_OCPM", + "budget_remaining": 37407, + "campaign_group_id": "600123456789", + "campaign_status": "PAUSED", + "created_time": "2015-05-27T11:58:34+0200", + "daily_budget": 40000, + "end_time": "2015-05-29T22:26:40+0200", + "id": "700123456789", + "is_autobid": true, + "lifetime_budget": 0, + "name": "Test AdSet", + "start_time": "2015-05-27T11:58:34+0200", + "targeting": { + "age_max": 65, + "age_min": 18, + "geo_locations": { + "countries": [ + "BR" + ], + "location_types": [ + "home" + ] + } + }, + "updated_time": "2015-05-27T11:58:34+0200" + }, + { + "account_id": "123456789", + "bid_type": "ABSOLUTE_OCPM", + "budget_remaining": 0, + "campaign_group_id": "600123456789", + "campaign_status": "ACTIVE", + "created_time": "2015-04-10T09:28:54+0200", + "daily_budget": 0, + "end_time": "2015-04-13T09:19:00+0200", + "id": "701123456789", + "is_autobid": true, + "lifetime_budget": 200, + "name": "Real ad set", + "start_time": "2015-04-12T09:19:00+0200", + "targeting": { + "age_max": 20, + "age_min": 18, + "behaviors": [ + { + "id": "6004854404172", + "name": "Technology late adopters" + } + ], + "genders": [ + 1 + ], + "geo_locations": { + "countries": [ + "PL" + ], + "location_types": [ + "home", + "recent" + ] + }, + "interests": [ + { + "id": "6003629266583", + "name": "Hard drives" + } + ], + "page_types": [ + "feed" + ] + }, + "updated_time": "2015-04-10T13:32:09+0200" + } + ], + "paging": { + "cursors": { + "before": "NjAyNDE2Nzg5NTY1MA==", + "after": "NjAyNTIwMDQ1MTQ1MA==" + } + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-bid-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-bid-type.json new file mode 100644 index 000000000..861789a50 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-bid-type.json @@ -0,0 +1,45 @@ +{ + "id": "100123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "WRONG_BID_TYPE", + "campaign_id": "800123456789", + "campaign_group_id": "700123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name", + "targeting": { + "age_max": 20, + "age_min": 18, + "behaviors": [ + { + "id": "6004854404172", + "name": "Technology late adopters" + } + ], + "genders": [ + 1 + ], + "geo_locations": { + "countries": [ + "PL" + ], + "location_types": [ + "home", + "recent" + ] + }, + "interests": [ + { + "id": "6003629266583", + "name": "Hard drives" + } + ], + "page_types": [ + "feed" + ] + }, + "updated_time": "2015-04-10T13:32:09+0200" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-status.json new file mode 100644 index 000000000..e71a4d52b --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-status.json @@ -0,0 +1,45 @@ +{ + "id": "100123456789", + "account_id": "123456789", + "adgroup_status": "WRONG_STATUS", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "800123456789", + "campaign_group_id": "700123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name", + "targeting": { + "age_max": 20, + "age_min": 18, + "behaviors": [ + { + "id": "6004854404172", + "name": "Technology late adopters" + } + ], + "genders": [ + 1 + ], + "geo_locations": { + "countries": [ + "PL" + ], + "location_types": [ + "home", + "recent" + ] + }, + "interests": [ + { + "id": "6003629266583", + "name": "Hard drives" + } + ], + "page_types": [ + "feed" + ] + }, + "updated_time": "2015-04-10T13:32:09+0200" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad.json new file mode 100644 index 000000000..8842790a9 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad.json @@ -0,0 +1,45 @@ +{ + "id": "100123456789", + "account_id": "123456789", + "adgroup_status": "ACTIVE", + "bid_type": "ABSOLUTE_OCPM", + "campaign_id": "800123456789", + "campaign_group_id": "700123456789", + "created_time": "2015-04-10T09:28:54+0200", + "creative": { + "id": "900123456789" + }, + "name": "Test ad name", + "targeting": { + "age_max": 20, + "age_min": 18, + "behaviors": [ + { + "id": "6004854404172", + "name": "Technology late adopters" + } + ], + "genders": [ + 1 + ], + "geo_locations": { + "countries": [ + "PL" + ], + "location_types": [ + "home", + "recent" + ] + }, + "interests": [ + { + "id": "6003629266583", + "name": "Hard drives" + } + ], + "page_types": [ + "feed" + ] + }, + "updated_time": "2015-04-10T13:32:09+0200" +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/error-invalid-update-campaign-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/error-invalid-update-campaign-status.json new file mode 100644 index 000000000..df66933d9 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/error-invalid-update-campaign-status.json @@ -0,0 +1,11 @@ +{ + "error": { + "message": "Invalid parameter", + "type": "FacebookApiException", + "code": 100, + "error_subcode": 1487564, + "is_transient": false, + "error_user_title": "Update Campaign Status", + "error_user_msg": "New campaigns need to be either active or paused." + } +} \ No newline at end of file diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/error-100-invalidParameter.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/error-100-invalidParameter.json new file mode 100644 index 000000000..86480ae33 --- /dev/null +++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/error-100-invalidParameter.json @@ -0,0 +1,11 @@ +{ + "error": { + "message": "Invalid parameter", + "type": "FacebookApiException", + "code": 100, + "error_subcode": 1349125, + "is_transient": false, + "error_user_title": "Missing Message Or Attachment", + "error_user_msg": "Missing message or attachment." + } +} \ No newline at end of file