bing-ads 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ module Bing
2
+ module Ads
3
+ module API
4
+ # Bing::Ads::API::SOAPClient
5
+ class SOAPClient
6
+ attr_accessor :customer_id, :account_id, :developer_token, :wsdl_url, :client_settings
7
+ attr_accessor :authentication_token, :username, :password, :namespace_identifier
8
+ attr_accessor :savon_client
9
+
10
+ def initialize(options)
11
+ @customer_id = options[:customer_id]
12
+ @account_id = options[:account_id]
13
+ @developer_token = options[:developer_token]
14
+ @wsdl_url = options[:wsdl_url]
15
+ @namespace_identifier = options[:namespace_identifier]
16
+ @authentication_token = options[:authentication_token]
17
+ @username = options[:username]
18
+ @password = options[:password]
19
+ @client_settings = options[:client_settings]
20
+ end
21
+
22
+ def call(operation:, payload: {})
23
+ client(client_settings).call(operation, message: payload)
24
+ end
25
+
26
+ def client(settings)
27
+ return savon_client if savon_client
28
+ settings = {
29
+ convert_request_keys_to: :camelcase,
30
+ wsdl: wsdl_url,
31
+ namespace_identifier: namespace_identifier,
32
+ soap_header: soap_header,
33
+ log: true,
34
+ log_level: :debug,
35
+ pretty_print_xml: true
36
+ }
37
+ settings.merge!(client_settings) if client_settings
38
+ @savon_client = Savon.client(settings)
39
+ end
40
+
41
+ private
42
+ def soap_header
43
+ headers = {}
44
+ if authentication_token
45
+ headers[ns('AuthenticationToken')] = authentication_token
46
+ headers[ns('CustomerAccountId')] = account_id
47
+ headers[ns('CustomerId')] = customer_id
48
+ headers[ns('DeveloperToken')] = developer_token
49
+ elsif username && password
50
+ headers[ns('CustomerAccountId')] = account_id
51
+ headers[ns('CustomerId')] = customer_id
52
+ headers[ns('DeveloperToken')] = developer_token
53
+ headers[ns('UserName')] = username
54
+ headers[ns('Password')] = password
55
+ else
56
+ raise Errors::AuthenticationParamsMissing, 'no authentication params provided'
57
+ end
58
+ headers
59
+ end
60
+
61
+ def ns(string)
62
+ "#{namespace_identifier}:#{string}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ module Bing
2
+ module Ads
3
+ module API
4
+ # Bing::Ads::API::V11
5
+ module V11
6
+ NAMESPACE_IDENTIFIER = :v11
7
+
8
+ def self.constants
9
+ Persey.config
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bing
4
+ module Ads
5
+ module API
6
+ # Bing::Ads::API::V11::Constants
7
+ module Constants
8
+ root_v11_path = File.expand_path('../', __FILE__)
9
+
10
+ campaign_management_path = File.join(root_v11_path, 'constants', 'campaign_management.yml')
11
+ languages_path = File.join(root_v11_path, 'constants', 'languages.yml')
12
+ limits_path = File.join(root_v11_path, 'constants', 'limits.yml')
13
+ time_zones_path = File.join(root_v11_path, 'constants', 'time_zones.yml')
14
+ wsdl_path = File.join(root_v11_path, 'constants', 'wsdl.yml')
15
+
16
+ Persey.init(:default) do
17
+ source :yaml, campaign_management_path, :campaign_management
18
+ source :yaml, languages_path, :languages
19
+ source :yaml, limits_path, :limits
20
+ source :yaml, time_zones_path, :time_zones
21
+ source :yaml, wsdl_path, :wsdl
22
+ env :default
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,78 @@
1
+ ad_distribution:
2
+ search: Search
3
+ content: Content
4
+ ad_rotation:
5
+ optimize_for_clicks: OptimizeForClicks
6
+ rotate_ads_evenly: RotateAdsEvenly
7
+ ad_group_status:
8
+ draft: Draft
9
+ active: Active
10
+ paused: Paused
11
+ deleted: Deleted
12
+ ad_editorial_status:
13
+ active: Active
14
+ disapproved: Disapproved
15
+ inactive: Inactive
16
+ active_limited: ActiveLimited
17
+ ad_status:
18
+ inactive: Inactive
19
+ active: Active
20
+ paused: Paused
21
+ deleted: Deleted
22
+ ad_types:
23
+ text_ad: TextAd
24
+ image_ad: ImageAd
25
+ product_ad: ProductAd
26
+ app_install_ad: AppInstallAd
27
+ expanded_text_ad: ExpandedTextAd
28
+ dynamic_search_ad: DynamicSearchAd
29
+ ad_types_for_get:
30
+ text: Text
31
+ image: Image
32
+ product: Product
33
+ app_install: AppInstall
34
+ expanded_text: ExpandedText
35
+ dynamic_search: DynamicSearch
36
+ bidding_model:
37
+ keyword: Keyword
38
+ site_placement: SitePlacement
39
+ bidding_scheme:
40
+ manual_cpc: ManualCpcBiddingScheme
41
+ enhanced_cpc: EnhancedCpcBiddingScheme
42
+ inherit_from_parent: InheritFromParentBiddingScheme
43
+ max_clicks: MaxClicksBiddingScheme
44
+ max_conversions: MaxConversionsBiddingScheme
45
+ target_cpa: TargetCpaBiddingScheme
46
+ budget_limit_type:
47
+ daily_budget_accelerated: DailyBudgetAccelerated
48
+ daily_budget_standard: DailyBudgetStandard
49
+ campaign_status:
50
+ active: Active
51
+ paused: Paused
52
+ budget_paused: BudgetPaused
53
+ budget_and_manual_paused: BudgetAndManualPaused
54
+ deleted: Deleted
55
+ pricing_model:
56
+ cpc: Cpc
57
+ cpm: Cpm
58
+ keyword_editorial_statuses:
59
+ active: Active
60
+ disapproved: Disapproved
61
+ inactive: Inactive
62
+ keyword_statuses:
63
+ active: Active
64
+ paused: Paused
65
+ deleted: Deleted
66
+ inactive: Inactive
67
+ match_types:
68
+ broad: Broad
69
+ content: Content
70
+ exact: Exact
71
+ phrase: Phrase
72
+ network:
73
+ owned_and_operated_and_syndicated_search: OwnedAndOperatedAndSyndicatedSearch
74
+ owned_and_operated_only: OwnedAndOperatedOnly
75
+ syndicated_search_only: SyndicatedSearchOnly
76
+ remarketing_target_setting:
77
+ bid_only: BidOnly
78
+ target_and_bid: TargetAndBid
@@ -0,0 +1,12 @@
1
+ danish: 'Danish'
2
+ dutch: 'Dutch'
3
+ english: 'English'
4
+ finnish: 'Finnish'
5
+ french: 'French'
6
+ german: 'German'
7
+ italian: 'Italian'
8
+ norwegian: 'Norwegian'
9
+ portuguese: 'Portuguese'
10
+ spanish: 'Spanish'
11
+ swedish: 'Swedish'
12
+ traditional_chinese: 'TraditionalChinese'
@@ -0,0 +1,5 @@
1
+ per_call:
2
+ campaign: 100
3
+ ad_group: 1000
4
+ ad: 50
5
+ keyword: 1000
@@ -0,0 +1,75 @@
1
+ abu_dhabi_muscat: 'AbuDhabiMuscat'
2
+ adelaide: 'Adelaide'
3
+ alaska: 'Alaska'
4
+ almaty_novosibirsk: 'Almaty_Novosibirsk'
5
+ amsterdam_berlin_bern_rome_stockholm_vienna: 'AmsterdamBerlinBernRomeStockholmVienna'
6
+ arizona: 'Arizona'
7
+ astana_dhaka: 'AstanaDhaka'
8
+ athens_buckarest_istanbul: 'AthensBuckarestIstanbul'
9
+ atlantic_time_canada: 'AtlanticTimeCanada'
10
+ auckland_wellington: 'AucklandWellington'
11
+ azores: 'Azores'
12
+ baghdad: 'Baghdad'
13
+ baku_tbilisi_yerevan: 'BakuTbilisiYerevan'
14
+ bangkok_hanoi_jakarta: 'BangkokHanoiJakarta'
15
+ beijing_chongqing_hong_kong_urumqi: 'BeijingChongqingHongKongUrumqi'
16
+ belgrade_bratislava_budapest_ljubljana_prague: 'BelgradeBratislavaBudapestLjubljanaPrague'
17
+ bogota_lima_quito: 'BogotaLimaQuito'
18
+ brasilia: 'Brasilia'
19
+ brisbane: 'Brisbane'
20
+ brussels_copenhagen_madrid_paris: 'BrusselsCopenhagenMadridParis'
21
+ bucharest: 'Bucharest'
22
+ buenos_aires_georgetown: 'BuenosAiresGeorgetown'
23
+ cairo: 'Cairo'
24
+ canberra_melbourne_sydney: 'CanberraMelbourneSydney'
25
+ cape_verde_island: 'CapeVerdeIsland'
26
+ caracas_la_paz: 'CaracasLaPaz'
27
+ casablanca_monrovia: 'CasablancaMonrovia'
28
+ central_america: 'CentralAmerica'
29
+ central_time_u_s_canada: 'CentralTimeUSCanada'
30
+ chennai_kolkata_mumbai_new_delhi: 'ChennaiKolkataMumbaiNewDelhi'
31
+ chihuahua_la_paz_mazatlan: 'ChihuahuaLaPazMazatlan'
32
+ darwin: 'Darwin'
33
+ eastern_time_u_s_canada: 'EasternTimeUSCanada'
34
+ ekaterinburg: 'Ekaterinburg'
35
+ fiji_kamchatka_marshall_island: 'FijiKamchatkaMarshallIsland'
36
+ greenland: 'Greenland'
37
+ greenwich_mean_time_dublin_edinburgh_lisbon_london: 'GreenwichMeanTimeDublinEdinburghLisbonLondon'
38
+ guadalajara_mexico_city_monterrey: 'GuadalajaraMexicoCityMonterrey'
39
+ guam_port_moresby: 'GuamPortMoresby'
40
+ harare_pretoria: 'HararePretoria'
41
+ hawaii: 'Hawaii'
42
+ helsinki_kyiv_riga_sofia_tallinn_vilnius: 'HelsinkiKyivRigaSofiaTallinnVilnius'
43
+ hobart: 'Hobart'
44
+ indiana_east: 'IndianaEast'
45
+ international_date_line_west: 'InternationalDateLineWest'
46
+ irkutsk_ulaan_bataar: 'IrkutskUlaanBataar'
47
+ islandamabad_karachi_tashkent: 'IslandamabadKarachiTashkent'
48
+ jerusalem: 'Jerusalem'
49
+ kabul: 'Kabul'
50
+ kathmandu: 'Kathmandu'
51
+ krasnoyarsk: 'Krasnoyarsk'
52
+ kuala_lumpur_singapore: 'KualaLumpurSingapore'
53
+ kuwait_riyadh: 'KuwaitRiyadh'
54
+ magadan_solomon_island_new_caledonia: 'MagadanSolomonIslandNewCaledonia'
55
+ mid_atlantic: 'MidAtlantic'
56
+ midway_islandand_samoa: 'MidwayIslandand_Samoa'
57
+ moscow_st_petersburg_volgograd: 'MoscowStPetersburgVolgograd'
58
+ mountain_time_u_s_canada: 'MountainTime_US_Canada'
59
+ nairobi: 'Nairobi'
60
+ newfoundland: 'Newfoundland'
61
+ nukualofa: 'Nukualofa'
62
+ osaka_sapporo_tokyo: 'OsakaSapporoTokyo'
63
+ pacific_time_u_s_canada_tijuana: 'PacificTimeUSCanadaTijuana'
64
+ perth: 'Perth'
65
+ rangoon: 'Rangoon'
66
+ santiago: 'Santiago'
67
+ sarajevo_skopje_warsaw_zagreb: 'SarajevoSkopjeWarsawZagreb'
68
+ saskatchewan: 'Saskatchewan'
69
+ seoul: 'Seoul'
70
+ sri_jayawardenepura: 'SriJayawardenepura'
71
+ taipei: 'Taipei'
72
+ tehran: 'Tehran'
73
+ vladivostok: 'Vladivostok'
74
+ west_central_africa: 'WestCentralAfrica'
75
+ yakutsk: 'Yakutsk'
@@ -0,0 +1,6 @@
1
+ sandbox:
2
+ campaign_management: "https://campaign.api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/V11/CampaignManagementService.svc?singleWsdl"
3
+ reporting: "https://api.sandbox.bingads.microsoft.com/Api/Advertiser/Reporting/V11/ReportingService.svc?singleWsdl"
4
+ production:
5
+ campaign_management: "https://campaign.api.bingads.microsoft.com/Api/Advertiser/CampaignManagement/V11/CampaignManagementService.svc?singleWsdl"
6
+ reporting: "https://api.bingads.microsoft.com/Api/Advertiser/Reporting/V11/ReportingService.svc?singleWsdl"
@@ -0,0 +1,2 @@
1
+ require_relative './services/base'
2
+ require_relative './services/campaign_management'
@@ -0,0 +1,121 @@
1
+ module Bing
2
+ module Ads
3
+ module API
4
+ module V11
5
+ module Services
6
+ # Bing::Ads::API::V11::Base
7
+ class Base
8
+ attr_accessor :soap_client, :environment, :retry_attempts
9
+
10
+ # @param options - Hash with autentication and environment settings
11
+ # * environment - +:production+ or +:sandbox+
12
+ # * developer_token - client application's developer access token
13
+ # * customer_id - identifier for the customer that owns the account
14
+ # * account_id - identifier of the account that own the entities in the request
15
+ # * client_settings - Hash with any Client additional options (such as header, logger or enconding)
16
+ # * retry_attempts - Number of times the service must retry on failure
17
+ # (EITHER)
18
+ # * authentication_token - OAuth2 token
19
+ # (OR)
20
+ # * username - Bing Ads username
21
+ # * password - Bing Ads password
22
+ def initialize(options = {})
23
+ @environment = options.delete(:environment)
24
+ @retry_attempts = options.delete(:retry_attempts) || 0
25
+ @account_id = options[:account_id]
26
+ raise 'You must set the service environment' unless @environment
27
+ options[:wsdl_url] = service_wsdl_url
28
+ options[:namespace_identifier] = Bing::Ads::API::V11::NAMESPACE_IDENTIFIER
29
+ @soap_client = Bing::Ads::API::SOAPClient.new(options)
30
+ end
31
+
32
+ # This is a utility wrapper for calling services into the
33
+ # SOAPClient. This methods handle the Savon::Client Exceptions
34
+ # and returns a Hash with the call response
35
+ #
36
+ # @param operation - name of the operation to be called
37
+ # @param payload - hash with the parameters to the operation
38
+ #
39
+ # @example
40
+ # service.call(:some_operation, { key: value })
41
+ # # => <Hash>
42
+ #
43
+ # @return Hash with the result of the service call
44
+ # @raise ServiceError if the SOAP call fails or the response is invalid
45
+ def call(operation, payload)
46
+ retries_made = 0
47
+ raise 'You must provide an operation' if operation.nil?
48
+ begin
49
+ response = soap_client.call(operation: operation.to_sym, payload: payload)
50
+ return response.hash
51
+ rescue Savon::SOAPFault => error
52
+ fault_detail = error.to_hash[:fault][:detail]
53
+ if fault_detail.key?(:api_fault_detail)
54
+ handle_soap_fault(operation, fault_detail, :api_fault_detail)
55
+ elsif fault_detail.key?(:ad_api_fault_detail)
56
+ handle_soap_fault(operation, fault_detail, :ad_api_fault_detail)
57
+ else
58
+ raise
59
+ end
60
+ rescue Savon::HTTPError => error
61
+ raise
62
+ rescue Savon::InvalidResponseError => error
63
+ raise
64
+ rescue
65
+ if retries_made < retry_attempts
66
+ sleep(2**retries_made)
67
+ retries_made += 1
68
+ retry
69
+ else
70
+ raise
71
+ end
72
+ end
73
+ end
74
+
75
+ # Extracts the actual response from the entire response hash.
76
+ #
77
+ # @param response - The complete response hash received from a Operation call
78
+ # @param method - Name of the method of with the 'reponse' tag is require
79
+ #
80
+ # @example
81
+ # service.response_body(Hash, 'add_campaigns')
82
+ # # => Hash
83
+ #
84
+ # @return Hash with the content of the called method response hash
85
+ def response_body(response, method)
86
+ response[:envelope][:body]["#{method}_response".to_sym]
87
+ end
88
+
89
+ private
90
+
91
+ # Returns service name. This method must be overriden by specific services.
92
+ #
93
+ # @return String with the service name
94
+ # @raise exception if the specific Service class hasn't overriden this method
95
+ def service_name
96
+ raise 'Should return the a service name from config.wsdl keys'
97
+ end
98
+
99
+ # Gets the service WSDL URL based on the service name and environment
100
+ #
101
+ # @return String with the Service url
102
+ def service_wsdl_url
103
+ Bing::Ads::API::V11.constants.wsdl.send(environment).send(service_name)
104
+ end
105
+
106
+ def handle_soap_fault(operation, fault_detail, key)
107
+ if fault_detail[key][:errors] &&
108
+ fault_detail[key][:errors][:ad_api_error] &&
109
+ fault_detail[key][:errors][:ad_api_error][:error_code] == 'AuthenticationTokenExpired'
110
+ raise Bing::Ads::API::Errors::AuthenticationTokenExpired,
111
+ 'renew authentication token or obtain a new one.'
112
+ else
113
+ raise "SOAP error while calling #{operation}, #{fault_detail[key]}"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,360 @@
1
+ module Bing
2
+ module Ads
3
+ module API
4
+ module V11
5
+ module Services
6
+ # Bing::Ads::API::V11::Services::CampaignManagement
7
+ class CampaignManagement < Base
8
+ def initialize(options = {})
9
+ super(options)
10
+ end
11
+
12
+ def get_campaigns_by_account_id(account_id=nil)
13
+ account_id ||= @account_id
14
+ response = call(:get_campaigns_by_account_id, account_id: account_id)
15
+ response_body = response_body(response, __method__)
16
+ [response_body[:campaigns][:campaign]].flatten.compact
17
+ end
18
+
19
+ def add_campaigns(account_id, campaigns)
20
+ validate_limits!(:campaign, :add, campaigns)
21
+ campaigns = campaigns.map { |campaign| prepare_campaign(campaign) }
22
+ payload = {
23
+ account_id: account_id,
24
+ campaigns: { campaign: campaigns }
25
+ }
26
+ response = call(:add_campaigns, payload)
27
+ response_body(response, __method__)
28
+ end
29
+
30
+ def update_campaigns(account_id, campaigns)
31
+ validate_limits!(:campaign, :update, campaigns)
32
+ campaigns = campaigns.map { |campaign| prepare_campaign(campaign) }
33
+ payload = {
34
+ account_id: account_id,
35
+ campaigns: { campaign: campaigns }
36
+ }
37
+ response = call(:update_campaigns, payload)
38
+ response_body(response, __method__)
39
+ end
40
+
41
+ def delete_campaigns(account_id, campaign_ids)
42
+ payload = {
43
+ account_id: account_id,
44
+ campaign_ids: { 'ins1:long' => campaign_ids }
45
+ }
46
+ response = call(:delete_campaigns, payload)
47
+ response_body(response, __method__)
48
+ end
49
+
50
+ def get_ad_groups_by_campaign_id(campaign_id)
51
+ response = call(:get_ad_groups_by_campaign_id,
52
+ campaign_id: campaign_id)
53
+ response_body = response_body(response, __method__)
54
+ [response_body[:ad_groups][:ad_group]].flatten.compact
55
+ end
56
+
57
+ def get_ad_groups_by_ids(campaign_id, ad_groups_ids)
58
+ payload = {
59
+ campaign_id: campaign_id,
60
+ ad_group_ids: { 'ins1:long' => ad_groups_ids }
61
+ }
62
+ response = call(:get_ad_groups_by_ids, payload)
63
+ response_body = response_body(response, __method__)
64
+ [response_body[:ad_groups][:ad_group]].flatten
65
+ end
66
+
67
+ def add_ad_groups(campaign_id, ad_groups)
68
+ validate_limits!(:ad_group, :add, ad_groups)
69
+ ad_groups = ad_groups.map { |ad_group| prepare_ad_group(ad_group) }
70
+ payload = {
71
+ campaign_id: campaign_id,
72
+ ad_groups: { ad_group: ad_groups }
73
+ }
74
+ response = call(:add_ad_groups, payload)
75
+ response_body(response, __method__)
76
+ end
77
+
78
+ def update_ad_groups(campaign_id, ad_groups)
79
+ validate_limits!(:ad_group, :update, ad_groups)
80
+ ad_groups = ad_groups.map { |ad_group| prepare_ad_group(ad_group) }
81
+ payload = {
82
+ campaign_id: campaign_id,
83
+ ad_groups: { ad_group: ad_groups }
84
+ }
85
+ response = call(:update_ad_groups, payload)
86
+ response_body(response, __method__)
87
+ end
88
+
89
+ def get_ads_by_ad_group_id(ad_group_id)
90
+ payload = {
91
+ ad_group_id: ad_group_id,
92
+ ad_types: all_ad_types
93
+ }
94
+ response = call(:get_ads_by_ad_group_id, payload)
95
+ response_body = response_body(response, __method__)
96
+ response_ads = [response_body[:ads][:ad]].flatten.compact
97
+ response_ads.each_with_object({}) do |ad, obj|
98
+ type = ad['@i:type'.to_sym]
99
+ obj[type] ||= []
100
+ obj[type] << ad
101
+ end
102
+ end
103
+
104
+ def get_ads_by_ids(ad_group_id, ad_ids)
105
+ # Order matters
106
+ payload = {
107
+ ad_group_id: ad_group_id,
108
+ ad_ids: { 'ins1:long' => ad_ids },
109
+ ad_types: all_ad_types
110
+ }
111
+ response = call(:get_ads_by_ids, payload)
112
+ response_body = response_body(response, __method__)
113
+ response_ads = [response_body[:ads][:ad]].flatten
114
+ response_ads.each_with_object({}) do |ad, obj|
115
+ type = ad['@i:type'.to_sym]
116
+ obj[type] ||= []
117
+ obj[type] << ad
118
+ end
119
+ end
120
+
121
+ def add_ads(ad_group_id, ads)
122
+ validate_limits!(:ad, :add, ads)
123
+ ads = ads.map { |ad| prepare_ad(ad) }
124
+ payload = {
125
+ ad_group_id: ad_group_id,
126
+ ads: { ad: ads }
127
+ }
128
+ response = call(:add_ads, payload)
129
+ response_body(response, __method__)
130
+ end
131
+
132
+ def update_ads(ad_group_id, ads)
133
+ validate_limits!(:ad, :update, ads)
134
+ ads = ads.map { |ad| prepare_ad(ad) }
135
+ payload = {
136
+ ad_group_id: ad_group_id,
137
+ ads: { ad: ads }
138
+ }
139
+ response = call(:update_ads, payload)
140
+ response_body(response, __method__)
141
+ end
142
+
143
+ def get_keywords_by_ad_group_id(ad_group_id)
144
+ response = call(:get_keywords_by_ad_group_id, ad_group_id: ad_group_id)
145
+ response_body = response_body(response, __method__)
146
+ [response_body[:keywords][:keyword]].flatten.compact
147
+ end
148
+
149
+ def get_keywords_by_ids(ad_group_id, keyword_ids)
150
+ payload = {
151
+ ad_group_id: ad_group_id,
152
+ keyword_ids: { 'ins1:long' => keyword_ids }
153
+ }
154
+ response = call(:get_keywords_by_ids, payload)
155
+ response_body = response_body(response, __method__)
156
+ [response_body[:keywords][:keyword]].flatten
157
+ end
158
+
159
+ def add_keywords(ad_group_id, keywords)
160
+ validate_limits!(:keyword, :add, keywords)
161
+ keywords = keywords.map { |keyword| prepare_keyword(keyword) }
162
+ payload = {
163
+ ad_group_id: ad_group_id,
164
+ keywords: { keyword: keywords }
165
+ }
166
+ response = call(:add_keywords, payload)
167
+ response_body(response, __method__)
168
+ end
169
+
170
+ def update_keywords(ad_group_id, keywords)
171
+ validate_limits!(:keyword, :update, keywords)
172
+ keywords = keywords.map { |keyword| prepare_keyword(keyword) }
173
+ payload = {
174
+ ad_group_id: ad_group_id,
175
+ keywords: { keyword: keywords }
176
+ }
177
+ response = call(:update_keywords, payload)
178
+ response_body(response, __method__)
179
+ end
180
+
181
+ # TODO add_ad_extensions
182
+ # TODO add_ad_group_criterions
183
+ # TODO add_audiences
184
+ # TODO add_budgets
185
+ # TODO add_campaign_criterions
186
+ # TODO add_conversion_goals
187
+ # TODO add_labels
188
+ # TODO add_list_items_to_shared_list
189
+ # TODO add_media
190
+ # TODO add_negative_keywords_to_entities
191
+ # TODO add_shared_entity
192
+ # TODO add_uet_tags
193
+ # TODO appeal_editorial_rejections
194
+ # TODO apply_offline_conversions
195
+ # TODO apply_product_partition_actions
196
+ # TODO delete_ad_extensions
197
+ # TODO delete_ad_group_criterions
198
+ # TODO delete_ad_groups
199
+ # TODO delete_ads
200
+ # TODO delete_audiences
201
+ # TODO delete_budgets
202
+ # TODO delete_campaign_criterions
203
+ # TODO delete_conversion_goals
204
+ # TODO delete_keywords
205
+ # TODO delete_labels
206
+ # TODO delete_list_items_to_shared_list
207
+ # TODO delete_media
208
+ # TODO delete_negative_keywords_to_entities
209
+ # TODO delete_shared_entity
210
+ # TODO delete_shared_entity_associations
211
+ # TODO get_account_migration_statuses
212
+ # TODO get_account_properties
213
+ # TODO get_ad_extension_ids_by_account_id
214
+ # TODO get_ad_extensions_associations
215
+ # TODO get_ad_extensions_by_ids
216
+ # TODO get_ad_extensions_editorial_reasons
217
+ # TODO get_ad_group_criterions_by_ids
218
+ # TODO get_ads_by_editorial_status
219
+ # TODO get_audiences_by_ids
220
+ # TODO get_bmc_stores_by_customer_id
221
+ # TODO get_bsc_countries
222
+ # TODO get_budgets_by_ids
223
+ # TODO get_campaign_criterions_by_ids
224
+ # TODO get_campaign_ids_by_budget_ids
225
+ # TODO get_campaigns_by_ids
226
+ # TODO get_campaign_sizes_by_account_id
227
+ # TODO get_config_value
228
+ # TODO get_conversion_goals_by_ids
229
+ # TODO get_conversion_goals_by_tag_ids
230
+ # TODO get_editorial_reasons_by_ids
231
+ # TODO get_geo_locations_file_url
232
+ # TODO get_keywords_by_editorial_status
233
+ # TODO get_label_associations_by_entity_ids
234
+ # TODO get_label_associations_by_label_ids
235
+ # TODO get_labels_by_ids
236
+ # TODO get_list_items_by_shared_list
237
+ # TODO get_media_associations
238
+ # TODO get_media_by_ids
239
+ # TODO get_media_meta_data_by_account_id
240
+ # TODO get_media_meta_data_by_ids
241
+ # TODO get_negative_keywords_by_entity_ids
242
+ # TODO get_negative_sites_by_ad_group_ids
243
+ # TODO get_negative_sites_by_campaign_ids
244
+ # TODO get_shared_entities_by_account_id
245
+ # TODO get_shared_entity_associations_by_entity_ids
246
+ # TODO get_shared_entity_associations_by_shared_entity_ids
247
+ # TODO get_uet_tags_by_ids
248
+ # TODO set_account_properties
249
+ # TODO set_ad_extensions_associations
250
+ # TODO set_label_associations
251
+ # TODO set_negative_sites_to_ad_groups
252
+ # TODO set_negative_sites_to_campaigns
253
+ # TODO set_shared_entity_associations
254
+ # TODO update_ad_extensions
255
+ # TODO update_ad_group_criterions
256
+ # TODO update_audiences
257
+ # TODO update_budgets
258
+ # TODO update_campaign_criterions
259
+ # TODO update_conversion_goals
260
+ # TODO update_labels
261
+ # TODO update_list_items_to_shared_list
262
+ # TODO update_media
263
+ # TODO update_negative_keywords_to_entities
264
+ # TODO update_shared_entity
265
+ # TODO update_uet_tags
266
+
267
+ private
268
+
269
+ def service_name
270
+ 'campaign_management'
271
+ end
272
+
273
+ def prepare_campaign(campaign)
274
+ campaign = Bing::Ads::Utils.sort_keys(campaign)
275
+ if campaign[:bidding_scheme]
276
+ campaign[:bidding_scheme] = {
277
+ # TODO support MaxClicksBiddingScheme, MaxConversionsBiddingScheme and TargetCpaBiddingScheme
278
+ type: campaign[:bidding_scheme],
279
+ '@xsi:type' => "#{Bing::Ads::API::V11::NAMESPACE_IDENTIFIER}:#{campaign[:bidding_scheme]}"
280
+ }
281
+ end
282
+ campaign[:languages] = { 'ins1:string' => campaign[:languages] } if campaign[:languages]
283
+ # TODO UrlCustomParameters
284
+ # TODO Settings
285
+ Bing::Ads::Utils.camelcase_keys(campaign)
286
+ end
287
+
288
+ def prepare_ad_group(ad_group)
289
+ ad_group = Bing::Ads::Utils.sort_keys(ad_group)
290
+ ad_group[:ad_rotation] = { type: ad_group[:ad_rotation] } if ad_group[:ad_rotation]
291
+ if ad_group[:bidding_scheme]
292
+ # TODO support MaxClicksBiddingScheme, MaxConversionsBiddingScheme and TargetCpaBiddingScheme
293
+ ad_group[:bidding_scheme] = {
294
+ type: ad_group[:bidding_scheme],
295
+ '@xsi:type' => "#{Bing::Ads::API::V11::NAMESPACE_IDENTIFIER}:#{ad_group[:bidding_scheme]}"
296
+ }
297
+ end
298
+ ad_group[:content_match_bid] = { amount: ad_group[:content_match_bid] } if ad_group[:content_match_bid]
299
+ ad_group[:end_date] = date_hash(ad_group[:end_date]) if ad_group[:end_date]
300
+ ad_group[:search_bid] = { amount: ad_group[:search_bid] } if ad_group[:search_bid]
301
+ ad_group[:start_date] = date_hash(ad_group[:start_date]) if ad_group[:start_date]
302
+ # TODO UrlCustomParameters
303
+ Bing::Ads::Utils.camelcase_keys(ad_group)
304
+ end
305
+
306
+ def prepare_ad(ad)
307
+ ad = Bing::Ads::Utils.sort_keys(ad)
308
+ ad['@xsi:type'] = "#{Bing::Ads::API::V11::NAMESPACE_IDENTIFIER}:#{ad[:type]}"
309
+ ad[:final_mobile_urls] = { 'ins1:string' => ad[:final_mobile_urls] } if ad[:final_mobile_urls]
310
+ ad[:final_urls] = { 'ins1:string' => ad[:final_urls] } if ad[:final_urls]
311
+ # TODO FinalAppUrls
312
+ Bing::Ads::Utils.camelcase_keys(ad)
313
+ end
314
+
315
+ def prepare_keyword(keyword)
316
+ keyword = Bing::Ads::Utils.sort_keys(keyword)
317
+ keyword[:bid] = { amount: keyword[:bid] } if keyword[:bid]
318
+ if keyword[:bidding_scheme]
319
+ # TODO support MaxClicksBiddingScheme, MaxConversionsBiddingScheme and TargetCpaBiddingScheme
320
+ keyword[:bidding_scheme] = {
321
+ type: keyword[:bidding_scheme],
322
+ '@xsi:type' => "#{Bing::Ads::API::V11::NAMESPACE_IDENTIFIER}:#{keyword[:bidding_scheme]}"
323
+ }
324
+ end
325
+ keyword[:final_mobile_urls] = { 'ins1:string' => keyword[:final_mobile_urls] } if keyword[:final_mobile_urls]
326
+ keyword[:final_urls] = { 'ins1:string' => keyword[:final_urls] } if keyword[:final_urls]
327
+ # TODO FinalAppUrls
328
+ # TODO UrlCustomParameters
329
+ Bing::Ads::Utils.camelcase_keys(keyword)
330
+ end
331
+
332
+ def date_hash(date)
333
+ date = Date.parse(date) if date.is_a?(String)
334
+ { day: date.day, month: date.month, year: date.year }
335
+ end
336
+
337
+ def validate_limits!(type, operation, array)
338
+ limit = Bing::Ads::API::V11.constants.limits.per_call.send(type)
339
+ if array.size > limit
340
+ raise Bing::Ads::API::Errors::LimitError.new(operation, limit, type)
341
+ end
342
+ end
343
+
344
+ def all_ad_types
345
+ {
346
+ ad_type: [
347
+ Bing::Ads::API::V11.constants.campaign_management.ad_types_for_get.text,
348
+ Bing::Ads::API::V11.constants.campaign_management.ad_types_for_get.expanded_text,
349
+ Bing::Ads::API::V11.constants.campaign_management.ad_types_for_get.image,
350
+ Bing::Ads::API::V11.constants.campaign_management.ad_types_for_get.product,
351
+ Bing::Ads::API::V11.constants.campaign_management.ad_types_for_get.app_install
352
+ ]
353
+ }
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end