bing-ads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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