├── .rspec ├── lib ├── bing-ads-api │ ├── version.rb │ ├── data │ │ ├── bid.rb │ │ ├── predicate.rb │ │ ├── city_target_bid.rb │ │ ├── postal_code_target_bid.rb │ │ ├── radius_target_bid.rb │ │ ├── city_target.rb │ │ ├── radius_target.rb │ │ ├── postal_code_target.rb │ │ ├── location_target.rb │ │ ├── target.rb │ │ ├── report_request_status.rb │ │ ├── budget.rb │ │ ├── keyword.rb │ │ ├── campaign.rb │ │ ├── reporting │ │ │ ├── helpers │ │ │ │ ├── scope_helper.rb │ │ │ │ ├── column_helper.rb │ │ │ │ ├── time_helper.rb │ │ │ │ └── filter_helper.rb │ │ │ ├── performance_report_request.rb │ │ │ ├── share_of_voice_report_request.rb │ │ │ ├── keyword_performance_report_request.rb │ │ │ ├── account_performance_report_request.rb │ │ │ ├── budget_summary_report_request.rb │ │ │ └── campaign_performance_report_request.rb │ │ ├── bulk │ │ │ ├── bulk_download_status.rb │ │ │ └── bulk_upload_status.rb │ │ ├── accounts_info.rb │ │ ├── report_request.rb │ │ ├── ad_group.rb │ │ └── ad.rb │ ├── fault │ │ ├── ad_api_error.rb │ │ ├── application_fault.rb │ │ ├── operation_error.rb │ │ ├── batch_error.rb │ │ ├── ad_api_fault_detail.rb │ │ ├── partial_errors.rb │ │ └── api_fault_detail.rb │ ├── api_exception.rb │ ├── data_object.rb │ ├── config.rb │ ├── service │ │ ├── customer_management.rb │ │ ├── customer_billing.rb │ │ ├── reporting.rb │ │ └── bulk.rb │ ├── constants.rb │ ├── client_proxy.rb │ ├── soap_hasheable.rb │ └── service.rb ├── tasks │ └── bing-ads-api_tasks.rake ├── bing-ads-api.rb └── locales │ └── es.yml ├── .gitignore ├── .project ├── Gemfile ├── Rakefile ├── spec ├── customer_management_spec.rb ├── column_helper_spec.rb ├── report_request_spec.rb ├── performance_report_request_spec.rb ├── config_spec.rb ├── keyword_performance_report_request_spec.rb ├── spec_helper.rb ├── data_object_spec.rb ├── account_performance_report_request_spec.rb ├── campaign_performance_report_request_spec.rb ├── filter_helper_spec.rb ├── client_proxy_spec.rb ├── helpers │ └── bing_ads_factory.rb ├── bulk_spec.rb ├── reporting_spec.rb └── campaign_management_spec.rb ├── bing-ads-api.gemspec ├── MIT-LICENSE └── README.rdoc /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /lib/bing-ads-api/version.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module BingAdsApi 3 | 4 | # Gem Version 5 | VERSION = "0.7.1" 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/bing-ads-api_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :bing-ads-api do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/log/*.log 6 | test/dummy/tmp/ 7 | test/dummy/.sass-cache 8 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | bing-ads-api 4 | 5 | 6 | 7 | 8 | 9 | 10 | com.aptana.projects.webnature 11 | org.radrails.rails.core.railsnature 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/bid.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public: Define a bid 6 | # 7 | # Author:: alex.cavalli@offers.com 8 | # 9 | # Examples 10 | # bid = BingAdsApi::Bid.new(:amount => 1.23) 11 | # # => 12 | class Bid < BingAdsApi::DataObject 13 | 14 | attr_accessor :amount 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/predicate.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | class Predicate < BingAdsApi::DataObject 6 | 7 | attr_accessor :field, :operator, :value 8 | 9 | def to_hash(keys = :underscore) 10 | hash = super(keys) 11 | return hash 12 | end 13 | 14 | private 15 | 16 | def get_key_order 17 | super.concat(BingAdsApi::Config.instance. 18 | customer_billing_orders['predicate']) 19 | end 20 | 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/bing-ads-api/fault/ad_api_error.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines an error object that contains the details that explain why the service operation failed. 6 | # 7 | # Reference : http://msdn.microsoft.com/en-US/library/bing-ads-overview-adapierror.aspx 8 | # 9 | # Author:: jlopezn@neonline.cl 10 | class AdApiError < BingAdsApi::DataObject 11 | 12 | attr_accessor :code, :detail, :error_code, :message 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/bing-ads-api/fault/application_fault.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines the base object from which all fault detail objects derive. 6 | # 7 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-overview-applicationfault.aspx 8 | # 9 | # Author:: jlopezn@neonline.cl 10 | class ApplicationFault < BingAdsApi::DataObject 11 | 12 | attr_accessor :tracking_id 13 | 14 | def to_s 15 | return tracking_id.to_s 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/bing-ads-api/data/city_target_bid.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public: Define a bid 6 | # 7 | # Author:: alex.cavalli@offers.com 8 | # 9 | # Examples 10 | # bid = BingAdsApi::Bid.new(:amount => 1.23) 11 | # # => 12 | class CityTargetBid < BingAdsApi::DataObject 13 | 14 | attr_accessor :bid_adjustment, 15 | :city, 16 | :is_excluded 17 | end 18 | 19 | def to_hash(keys = :camelcase) 20 | hash = super(keys) 21 | return hash 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/postal_code_target_bid.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public: Define a bid 6 | # 7 | # Author:: alex.cavalli@offers.com 8 | # 9 | # Examples 10 | # bid = BingAdsApi::Bid.new(:amount => 1.23) 11 | # # => 12 | class PostalCodeTargetBid < BingAdsApi::DataObject 13 | 14 | attr_accessor :bid_adjustment, 15 | :postal_code, 16 | :is_excluded 17 | end 18 | 19 | def to_hash(keys = :camelcase) 20 | hash = super(keys) 21 | return hash 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Declare your gem's dependencies in bing-ads-api.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | # gem 'ruby-debug19', :require => 'ruby-debug' 15 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/radius_target_bid.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public: Define a bid 6 | # 7 | # Author:: alex.cavalli@offers.com 8 | # 9 | # Examples 10 | # bid = BingAdsApi::Bid.new(:amount => 1.23) 11 | # # => 12 | class RadiusTargetBid < BingAdsApi::DataObject 13 | 14 | attr_accessor :bid_adjustment, 15 | :latitude_degrees, 16 | :longitude_degrees, 17 | :radius, 18 | :radius_unit, 19 | :is_excluded 20 | end 21 | 22 | def to_hash(keys = :camelcase) 23 | hash = super(keys) 24 | return hash 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/bing-ads-api/fault/operation_error.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines an error object that contains the details that explain why the service operation failed. 6 | # 7 | # Author:: jlopezn@neonline.cl 8 | # 9 | # Reference : http://msdn.microsoft.com/en-US/library/bing-ads-overview-operationerror.aspx 10 | class OperationError < BingAdsApi::DataObject 11 | 12 | attr_accessor :code, :details, :error_code, :message 13 | 14 | 15 | # Public : Specified to string method 16 | # 17 | # Author:: jlopezn@neonline.cl 18 | def to_s 19 | "#{code}:#{error_code} - #{message}. #{details}" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/city_target.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object of an ad. 7 | # Do not instantiate this object. Instead you can instantiate the 8 | # BingAdsApi::TextAd, BingAdsApi::MobileAd, or BingAdsApi::ProductAd 9 | # object that derives from this object. 10 | # 11 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-ad.aspx 12 | # 13 | # Author:: jlopezn@neonline.cl 14 | # 15 | class CityTarget < BingAdsApi::DataObject 16 | 17 | attr_accessor :bids 18 | 19 | def to_hash(keys = :camelcase) 20 | hash = super(keys) 21 | return hash 22 | end 23 | 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/radius_target.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object of an ad. 7 | # Do not instantiate this object. Instead you can instantiate the 8 | # BingAdsApi::TextAd, BingAdsApi::MobileAd, or BingAdsApi::ProductAd 9 | # object that derives from this object. 10 | # 11 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-ad.aspx 12 | # 13 | # Author:: jlopezn@neonline.cl 14 | # 15 | class RadiusTarget < BingAdsApi::DataObject 16 | 17 | attr_accessor :bids 18 | 19 | def to_hash(keys = :camelcase) 20 | hash = super(keys) 21 | return hash 22 | end 23 | 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/postal_code_target.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object of an ad. 7 | # Do not instantiate this object. Instead you can instantiate the 8 | # BingAdsApi::TextAd, BingAdsApi::MobileAd, or BingAdsApi::ProductAd 9 | # object that derives from this object. 10 | # 11 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-ad.aspx 12 | # 13 | # Author:: jlopezn@neonline.cl 14 | # 15 | class PostalCodeTarget < BingAdsApi::DataObject 16 | 17 | attr_accessor :bids 18 | 19 | def to_hash(keys = :camelcase) 20 | hash = super(keys) 21 | return hash 22 | end 23 | 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/location_target.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object of an location target. 7 | # 8 | # Reference: https://msdn.microsoft.com/library/4349d964-0553-4d68-a53e-5011ff51a8f9 9 | # 10 | # Author:: seodma@gmailc.com 11 | # 12 | class LocationTarget < BingAdsApi::DataObject 13 | 14 | attr_accessor :id, 15 | :city_target, 16 | :radius_target, 17 | :postal_code_target 18 | 19 | def to_hash(keys = :camelcase) 20 | hash = super(keys) 21 | return hash 22 | end 23 | 24 | private 25 | 26 | # Internal: Retrieve the ordered array of keys corresponding to this data 27 | # object. 28 | # 29 | # Author: alex.cavalli@offers.com 30 | 31 | end 32 | 33 | 34 | end 35 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | begin 8 | require 'rdoc/task' 9 | rescue LoadError 10 | require 'rdoc/rdoc' 11 | require 'rake/rdoctask' 12 | RDoc::Task = Rake::RDocTask 13 | end 14 | 15 | RDoc::Task.new(:rdoc) do |rdoc| 16 | rdoc.rdoc_dir = 'rdoc' 17 | rdoc.title = 'BingAdsApi' 18 | rdoc.options << '--line-numbers' 19 | rdoc.rdoc_files.include('README.rdoc') 20 | rdoc.rdoc_files.include('lib/**/*.rb') 21 | end 22 | 23 | Bundler::GemHelper.install_tasks 24 | 25 | require 'rake/testtask' 26 | 27 | Rake::TestTask.new(:test) do |t| 28 | t.libs << 'lib' 29 | t.libs << 'test' 30 | t.pattern = 'test/**/*_test.rb' 31 | t.verbose = false 32 | end 33 | 34 | 35 | task :default => :test 36 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/target.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object of an location target. 7 | # 8 | # Reference: https://msdn.microsoft.com/library/4349d964-0553-4d68-a53e-5011ff51a8f9 9 | # 10 | # Author:: seodma@gmailc.com 11 | # 12 | class Target < BingAdsApi::DataObject 13 | 14 | attr_accessor :id, 15 | :location 16 | 17 | 18 | # Internal: Retrieve the ordered array of keys corresponding to this data 19 | # object. 20 | # 21 | # Author: alex.cavalli@offers.com 22 | def to_hash(keys = :camelcase) 23 | hash = super(keys) 24 | return hash 25 | end 26 | 27 | private 28 | 29 | # def get_key_order 30 | # super.concat(BingAdsApi::Config.instance. 31 | # campaign_management_orders['target']) 32 | # end 33 | 34 | 35 | end 36 | 37 | 38 | end 39 | -------------------------------------------------------------------------------- /spec/customer_management_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Author:: jlopezn@neonline.cl 5 | describe BingAdsApi::CustomerManagement do 6 | 7 | let(:default_options) do 8 | { 9 | environment: :sandbox, 10 | username: "ruby_bing_ads_sbx", 11 | password: "sandbox123", 12 | developer_token: "BBD37VB98", 13 | customer_id: "21025739", 14 | account_id: "8506945" 15 | } 16 | end 17 | let(:service) { BingAdsApi::CustomerManagement.new(default_options) } 18 | 19 | it "should initialize with options" do 20 | new_service = BingAdsApi::CustomerManagement.new(default_options) 21 | expect(new_service).not_to be_nil 22 | end 23 | 24 | it "should get accounts info" do 25 | response = service.get_accounts_info 26 | expect(response).not_to be_nil 27 | expect(response).to be_kind_of(Array) 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /bing-ads-api.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "bing-ads-api/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "bing-ads-api" 9 | s.version = BingAdsApi::VERSION 10 | s.authors = ["Juan Pablo Lopez N", "Alex Cavalli", "Colin Knox"] 11 | s.email = ["alex.cavalli@offers.com"] 12 | s.homepage = "https://github.com/alexcavalli/bing-ads-api" 13 | s.summary = "Bing Ads API for Ruby" 14 | s.description = "A set of pure Ruby classes for the Bing Ads API" 15 | 16 | s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"] 17 | s.test_files = Dir["spec/**/*"] 18 | 19 | s.add_dependency "savon", "~> 2.5" 20 | s.add_dependency "rest-client" 21 | s.add_dependency "activesupport" 22 | 23 | s.add_development_dependency "rspec" 24 | end 25 | -------------------------------------------------------------------------------- /spec/column_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BingAdsApi::Helpers::ColumnHelper do 4 | include BingAdsApi::Helpers::ColumnHelper 5 | 6 | it "should return true if all the columns are found in the valid column list" do 7 | expect{ 8 | valid_columns( 9 | {account_name: "AccountName"}, 10 | [:account_name, "AccountName"] 11 | ) 12 | }.to raise_error 13 | end 14 | 15 | it "should raise column exception when symbol is not in valid column keys" do 16 | expect{ 17 | valid_columns( 18 | {account_name: "UnknownColumn"}, 19 | [:account_name, :unknown_column] 20 | ) 21 | }.to raise_error 22 | end 23 | 24 | it "should raise column exception when string is not in valid column values" do 25 | expect{ 26 | valid_columns( 27 | {unknown_column: "Acct"}, 28 | [:account_name, "UnknownColumn" ], 29 | ) 30 | }.to raise_error 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/report_request_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Author:: jlopezn@neonline.cl 5 | describe BingAdsApi::ReportRequest do 6 | it "should initialize when valid parameters are provided" do 7 | expect{ 8 | report_request = BingAdsApi::ReportRequest.new(:format => :xml, 9 | :language => :english, :report_name => "My Report", 10 | :return_only_complete_data => true) 11 | }.not_to raise_error 12 | end 13 | 14 | it "should raise an exception when an invalid format is provided" do 15 | expect{ 16 | report_request = BingAdsApi::ReportRequest.new(:format => :invalid, 17 | :language => :english, :report_name => "My Report", 18 | :return_only_complete_data => true) 19 | }.to raise_error 20 | end 21 | 22 | it "should raise an exception when an invalid language is provided" do 23 | expect{ 24 | report_request = BingAdsApi::ReportRequest.new(:format => :xml, 25 | :language => :swahili, :report_name => "My Report", 26 | :return_only_complete_data => true) 27 | }.to raise_error 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 YOURNAME 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/performance_report_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BingAdsApi::PerformanceReportRequest do 4 | it "should initialize when valid parameters are provided" do 5 | expect{ 6 | performance_report_request = BingAdsApi::PerformanceReportRequest.new(:format => :xml, 7 | :language => :english, :report_name => "My Report", 8 | :aggregation => :hourly, :time => :today) 9 | }.not_to raise_error 10 | end 11 | 12 | it "should raise an exception when an invalid aggregation is provided" do 13 | expect{ 14 | performance_report_request = BingAdsApi::PerformanceReportRequest.new(:format => :xml, 15 | :language => :english, :report_name => "My Report", 16 | :aggregation => :invalid_aggregation) 17 | }.to raise_error 18 | end 19 | 20 | it "should raise an exception when an invalid time is provided" do 21 | expect{ 22 | performance_report_request = BingAdsApi::PerformanceReportRequest.new(:format => :xml, 23 | :language => :english, :report_name => "My Report", 24 | :aggregation => :hourly, :time => :invalid_time) 25 | }.to raise_error 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/bing-ads-api/api_exception.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Generic exception thrown by service classes in BingAdsApi. 6 | # Exception of this kind wrap an AdApiFaultDetail or ApiFaultDetail instance 7 | # to look over the specific details of the SOAP request fault. 8 | # 9 | # Author:: jlopezn@neonline.cl 10 | # 11 | # Example 12 | # 13 | # 14 | # 15 | # 16 | class ApiException < Exception 17 | 18 | attr_accessor :fault_object 19 | 20 | # Public : Constructor. Based on the default Exception constructor, 21 | # adds the fault_object instance, that can be an ApiFaultDetail or 22 | # AdApiFaultDetail instance 23 | # 24 | # === Parameters 25 | # * fault_object - AdApiFaultDetail or ApiFaultDetail instance 26 | # * msg - optional message 27 | # 28 | # Author:: jlopezn@neonline.cl 29 | def initialize(fault_object, msg=nil) 30 | super(msg) 31 | self.fault_object = fault_object 32 | end 33 | 34 | 35 | # Public : Specified to string method 36 | # 37 | # Author:: jlopezn@neonline.cl 38 | def to_s 39 | super.to_s + " - " + fault_object.to_s 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BingAdsApi::Config do 4 | 5 | let(:config) { BingAdsApi::Config.instance } 6 | 7 | it "should load" do 8 | expect(config).not_to be_nil 9 | expect(config.config).not_to be_nil 10 | end 11 | 12 | it "should load config constants" do 13 | expect(config.common_constants).not_to be_nil 14 | expect(config.campaign_management_constants).not_to be_nil 15 | expect(config.reporting_constants).not_to be_nil 16 | end 17 | 18 | it "should load config common constants" do 19 | expect(config.common_constants['time_zones']).not_to be_nil 20 | expect(config.common_constants['time_zones']['santiago']).not_to be_nil 21 | end 22 | 23 | it "should load config campaign management orders" do 24 | expect(config.campaign_management_orders['ad']).not_to be_nil 25 | end 26 | 27 | it "should get sandbox wsdl" do 28 | expect(config.service_wsdl(:sandbox, :campaign_management)).not_to be_nil 29 | expect(config.service_wsdl(:sandbox, :reporting)).not_to be_nil 30 | end 31 | 32 | it "should get production wsdl" do 33 | expect(config.service_wsdl(:production, :campaign_management)).not_to be_nil 34 | expect(config.service_wsdl(:production, :reporting)).not_to be_nil 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/report_request_status.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the status of a report request. 7 | # 8 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-reporting-bing-ads-reportrequeststatus.aspx 9 | # 10 | # Author:: jlopezn@neonline.cl 11 | class ReportRequestStatus < BingAdsApi::DataObject 12 | 13 | # Valid report request status for reports 14 | REQUEST_STATUS = BingAdsApi::Config.instance. 15 | reporting_constants['request_status_type'] 16 | 17 | attr_accessor :report_download_url, :status 18 | 19 | # Public:: Returns true if the status is success 20 | # 21 | # Author:: jlopezn@neonline.cl 22 | # 23 | # Returns:: boolean 24 | def success? 25 | return status == REQUEST_STATUS['success'] 26 | end 27 | 28 | # Public:: Returns true if the status is pending 29 | # 30 | # Author:: jlopezn@neonline.cl 31 | # 32 | # Returns:: boolean 33 | def pending? 34 | return status == REQUEST_STATUS['pending'] 35 | end 36 | 37 | # Public:: Returns true if the status is error 38 | # 39 | # Author:: jlopezn@neonline.cl 40 | # 41 | # Returns:: boolean 42 | def error? 43 | return status == REQUEST_STATUS['error'] 44 | end 45 | 46 | end 47 | 48 | end -------------------------------------------------------------------------------- /lib/bing-ads-api/data/budget.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public: Define a keyword 6 | # 7 | # Author:: alex.cavalli@offers.com 8 | # 9 | # Examples 10 | # keyword = BingAdsApi::Keyword.new( 11 | # :bid => BingAdsApi::Bid.new(:amount => 1.23), 12 | # :destination_url => "http://www.example.com/", 13 | # :id => 123456789, 14 | # :match_type => BingAdsApi::Keyword::EXACT, 15 | # :param1 => "param1", 16 | # :param2 => "param2", 17 | # :param3 => "param3", 18 | # :status => BingAdsApi::Keyword::ACTIVE, 19 | # :text => "keyword text") 20 | # # => 21 | class Budget < BingAdsApi::DataObject 22 | 23 | attr_accessor :amount, 24 | :association_count, 25 | :budget_type, 26 | :id, 27 | :name 28 | 29 | def to_hash(keys = :underscore) 30 | hash = super(keys) 31 | hash[:'@xsi:type'] = "#{ClientProxy::NAMESPACE}:Budget" 32 | return hash 33 | end 34 | 35 | private 36 | 37 | # Internal: Retrieve the ordered array of keys corresponding to this data 38 | # object. 39 | # 40 | # Author: alex.cavalli@offers.com 41 | def get_key_order 42 | super.concat(BingAdsApi::Config.instance. 43 | campaign_management_orders['budget']) 44 | end 45 | 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/keyword_performance_report_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BingAdsApi::KeywordPerformanceReportRequest do 4 | it "should initialize campaign performance report request" do 5 | expect{ 6 | request = BingAdsApi::KeywordPerformanceReportRequest.new( 7 | :format => :xml, 8 | :language => :english, 9 | :report_name => "Me report", 10 | :aggregation => :hourly, 11 | :columns => [:account_name, :account_number, :time_period], 12 | # The filter is specified as a hash 13 | :filter => { 14 | # specifies the Bing expected String value 15 | :ad_distribution => "Search", 16 | :ad_type => "Text", 17 | :bid_match_type => "Exact", 18 | :delivered_match_type => "Exact", 19 | # specifies criteria as a snake case symbol 20 | :device_type => :tablet, 21 | :keyword_relevance => [3], 22 | :keywords => ["bing ads"], 23 | :landing_page_relevance => [2], 24 | :landing_page_user_experience => [2], 25 | :language_code => ["EN"], 26 | :quality_score => [7,8,9,10] }, 27 | :max_rows => 10, 28 | :scope => { 29 | :account_ids => [123456, 234567] }, 30 | # predefined date 31 | :time => :this_week) 32 | }.not_to raise_error 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/keyword.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public: Define a keyword 6 | # 7 | # Author:: alex.cavalli@offers.com 8 | # 9 | # Examples 10 | # keyword = BingAdsApi::Keyword.new( 11 | # :bid => BingAdsApi::Bid.new(:amount => 1.23), 12 | # :destination_url => "http://www.example.com/", 13 | # :id => 123456789, 14 | # :match_type => BingAdsApi::Keyword::EXACT, 15 | # :param1 => "param1", 16 | # :param2 => "param2", 17 | # :param3 => "param3", 18 | # :status => BingAdsApi::Keyword::ACTIVE, 19 | # :text => "keyword text") 20 | # # => 21 | class Keyword < BingAdsApi::DataObject 22 | include BingAdsApi::KeywordEditorialStatuses 23 | include BingAdsApi::KeywordStatuses 24 | include BingAdsApi::MatchTypes 25 | 26 | 27 | attr_accessor :bid, 28 | :destination_url, 29 | :editorial_status, 30 | :forward_compatibility_map, 31 | :id, 32 | :match_type, 33 | :param1, 34 | :param2, 35 | :param3, 36 | :status, 37 | :text 38 | 39 | private 40 | 41 | # Internal: Retrieve the ordered array of keys corresponding to this data 42 | # object. 43 | # 44 | # Author: alex.cavalli@offers.com 45 | def get_key_order 46 | super.concat(BingAdsApi::Config.instance. 47 | campaign_management_orders['keyword']) 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | 8 | require 'bing-ads-api' 9 | require 'helpers/bing_ads_factory' 10 | 11 | RSpec.configure do |config| 12 | config.run_all_when_everything_filtered = true 13 | config.filter_run :focus 14 | 15 | # Run specs in random order to surface order dependencies. If you find an 16 | # order dependency and want to debug it, you can fix the order by providing 17 | # the seed, which is printed after each run. 18 | # --seed 1234 19 | config.order = 'random' 20 | 21 | config.after(:all) do 22 | # Clean up everything 23 | account_id = "242492622" 24 | service = BingAdsApi::CampaignManagement.new( 25 | environment: :sandbox, 26 | username: "dmitriiwebstreak1", 27 | password: "webstreak123", 28 | developer_token: "BBD37VB98", 29 | customer_id: "254553215", 30 | account_id: account_id 31 | ) 32 | campaign_ids = service.get_campaigns_by_account_id(account_id).map(&:id) 33 | 34 | campaign_ids.each_slice(100) do |ids| 35 | service.delete_campaigns(account_id, ids) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/data_object_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe BingAdsApi::DataObject do 5 | describe "#to_hash" do 6 | 7 | # TODO: Fix this test to test a "TestDataObject" (the interface) instead of 8 | # the Campaign object implementation 9 | it "should return a hash of the object attributes" do 10 | camp = BingAdsApi::Campaign.new( 11 | :budget_type => "DailyBudgetStandard", 12 | :conversion_tracking_enabled => "false", 13 | :daily_budget => 2000, 14 | :daylight_saving => "false", 15 | :description => "Some Campaign", 16 | :monthly_budget => 5400, 17 | :name => "Some campaign", 18 | :status => "Paused", 19 | :time_zone => "Santiago", 20 | :campaign_type => "DynamicSearchAds") 21 | 22 | h = camp.to_hash 23 | 24 | expect(h).to be_kind_of(Hash) 25 | expect(h).not_to be_nil 26 | 27 | expect(h["budget_type"]).to eq(camp.budget_type) 28 | expect(h["conversion_tracking_enabled"]).to eq(camp.conversion_tracking_enabled) 29 | expect(h["daily_budget"]).to eq(camp.daily_budget) 30 | expect(h["daylight_saving"]).to eq(camp.daylight_saving) 31 | expect(h["description"]).to eq(camp.description) 32 | expect(h["monthly_budget"]).to eq(camp.monthly_budget) 33 | expect(h["name"]).to eq(camp.name) 34 | expect(h["status"]).to eq(camp.status) 35 | expect(h["time_zone"]).to eq(camp.time_zone) 36 | expect(h["campaign_type"]).to eq(camp.campaign_type) 37 | end 38 | 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/campaign.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Define a campaign 6 | # 7 | # Author:: jlopezn@neonline.cl 8 | # 9 | # Examples 10 | # campaign = BingAdsApi::Campaign.new( 11 | # :budget_type => BingAdsApi::Campaign::DAILY_BUDGET_STANDARD, 12 | # :conversion_tracking_enabled => "false", 13 | # :daily_budget => 2000, 14 | # :daylight_saving => "false", 15 | # :description => name + " first description", 16 | # :monthly_budget => 5400, 17 | # :name => name + " first name", 18 | # :status => BingAdsApi::Campaign::PAUSED, 19 | # :time_zone => BingAdsApi::Campaign::SANTIAGO) 20 | # # => 21 | class Campaign < BingAdsApi::DataObject 22 | include BingAdsApi::TimeZone 23 | include BingAdsApi::BudgetLimitType 24 | include BingAdsApi::CampaignStatus 25 | include BingAdsApi::PricingModel 26 | 27 | 28 | attr_accessor :id, 29 | :budget_type, 30 | :conversion_tracking_enabled, 31 | :daily_budget, 32 | :daylight_saving, 33 | :description, 34 | :monthly_budget, 35 | :name, 36 | :status, 37 | :time_zone, 38 | :campaign_type 39 | 40 | private 41 | 42 | # Internal: Retrieve the ordered array of keys corresponding to this data 43 | # object. 44 | # 45 | # Author: alex.cavalli@offers.com 46 | def get_key_order 47 | super.concat(BingAdsApi::Config.instance. 48 | campaign_management_orders['campaign']) 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/account_performance_report_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BingAdsApi::AccountPerformanceReportRequest do 4 | it "initialize account performance report request" do 5 | expect{ 6 | BingAdsApi::AccountPerformanceReportRequest.new( 7 | :format => :xml, :language => :english, :report_name => "My Report", 8 | :aggregation => :hourly, 9 | :columns => [:account_name, :account_number, :time_period], 10 | :filter => { 11 | # String as bing expected 12 | :ad_distribution => "Search", 13 | # snake case symbol 14 | :device_os => :windows, 15 | # no specified value 16 | :device_type => nil 17 | }, 18 | :scope => {:account_ids => 12345}, 19 | :time => :today 20 | ) 21 | }.not_to raise_error 22 | end 23 | 24 | it "should raise an exception if the scope is invalid" do 25 | expect{ 26 | BingAdsApi::AccountPerformanceReportRequest.new( 27 | :format => :xml, :language => :english, :report_name => "My Report", 28 | :aggregation => :hourly, 29 | :columns => [:account_name, :account_number, :time_period], 30 | :filter => { 31 | # String as bing expected 32 | :ad_distribution => "Search", 33 | # snake case symbol 34 | :device_os => :windows, 35 | # no specified value 36 | :device_type => nil 37 | }, 38 | :scope => {}, 39 | :time => :today 40 | ) 41 | }.to raise_error 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/helpers/scope_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi::Helpers 4 | 5 | ## 6 | # Public : Utility module for scope attribute in +ReportRequest+ derived classes 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | # 10 | module ScopeHelper 11 | 12 | include BingAdsApi::SOAPHasheable 13 | 14 | 15 | # Valid filters values for each known criteria 16 | FILTERS_CRITERIA = BingAdsApi::Config.instance. 17 | reporting_constants['filters'] 18 | 19 | 20 | # Internal : Validates the filter attribute at the ReporRequest initialization 21 | # At the first invalid filter criteria or value detected this method raises Exception. 22 | # If all filter criteria and values are ok, this method returns true 23 | # Valid filter criteria are validated against FILTERS_CRITERIA constant 24 | # Valid filter values are validated against FILTERS constant 25 | # 26 | # Author:: jlopezn@neonline.cl 27 | # 28 | # === Parameters 29 | # valid_filters - Hash with the set of valid filter values 30 | # filter - Hash with the filter criteria and values 31 | # 32 | # Returns:: true if filter is valid 33 | # 34 | # Raises:: Exception at the first invalid filter criteria o value 35 | def valid_scope(valid_scopes, scope) 36 | end 37 | 38 | 39 | # Public : Returns the filter attribute as a Hash to SOAP requests 40 | # 41 | # Author:: jlopezn@neonline.cl 42 | # 43 | # filter - Hash with the filter values 44 | # 45 | # Returns:: Hash 46 | def scope_to_hash(keys_case=:undescore) 47 | end 48 | 49 | 50 | end 51 | end -------------------------------------------------------------------------------- /lib/bing-ads-api/data/bulk/bulk_download_status.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public: Wrapper class for GetBulkDownloadStatusResponse. Not an 7 | # actual data object in the Bing API. 8 | # 9 | # http://msdn.microsoft.com/en-US/library/dn600289.aspx 10 | # 11 | # Author:: alex.cavalli@offers.com 12 | class BulkDownloadStatus < BingAdsApi::DataObject 13 | 14 | # Valid report request status for reports 15 | REQUEST_STATUS = BingAdsApi::Config.instance. 16 | bulk_constants['request_status_type'] 17 | 18 | attr_accessor :errors, :forward_compatibility_map, :percent_complete, 19 | :request_status, :result_file_url 20 | 21 | # Public:: Returns true if the status is completed 22 | # 23 | # Author:: alex.cavalli@offers.com 24 | # 25 | # Returns:: boolean 26 | def completed? 27 | return request_status == REQUEST_STATUS['completed'] 28 | end 29 | 30 | # Public:: Returns true if the request_status is in progress 31 | # 32 | # Author:: alex.cavalli@offers.com 33 | # 34 | # Returns:: boolean 35 | def in_progress? 36 | return request_status == REQUEST_STATUS['in_progress'] 37 | end 38 | 39 | # Public:: Returns true if the status is failed 40 | # 41 | # Author:: alex.cavalli@offers.com 42 | # 43 | # Returns:: boolean 44 | def failed? 45 | return request_status == REQUEST_STATUS['failed'] 46 | end 47 | 48 | # Public:: Returns true if the status is failed full sync required 49 | # 50 | # Author:: alex.cavalli@offers.com 51 | # 52 | # Returns:: boolean 53 | def failed_full_sync_required? 54 | return request_status == REQUEST_STATUS['failed_full_sync_required'] 55 | end 56 | 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /lib/bing-ads-api/fault/batch_error.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines an error object that identifies the item within the batch of items 6 | # in the request message that caused the operation to fail, and describes the reason for the failure. 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | # 10 | # Reference : http://msdn.microsoft.com/en-US/library/bing-ads-overview-batcherror.aspx 11 | class BatchError < BingAdsApi::DataObject 12 | 13 | attr_accessor :code, :details, :error_code, :index, :message, :type 14 | 15 | # Public : Specified to string method 16 | # 17 | # Author:: jlopezn@neonline.cl 18 | def to_s 19 | "#{index}:#{code}:#{error_code} - #{message} (#{type})" 20 | end 21 | end 22 | 23 | # Public : Subclass of BatchError present in AdAdds operations 24 | # Defines an error object that identifies the entity with the 25 | # batch of entities that failed editorial review. 26 | # 27 | # Reference : http://msdn.microsoft.com/en-US/library/bing-ads-overview-editorialerror.aspx 28 | # 29 | # Author:: jlopezn@neonline.cl 30 | class EditorialError < BingAdsApi::BatchError 31 | 32 | attr_accessor :appaleable, :disapproved_text, :location, :publisher_country, :reason_code 33 | 34 | # Public : Specified to string method 35 | # 36 | # Author:: jlopezn@neonline.cl 37 | def to_s 38 | str = super() + "\n" 39 | str += "\tAppaleable? #{appaleable}\n" if appaleable 40 | str += "\tDisapproved text: #{disapproved_text}\n" if disapproved_text 41 | str += "\tLocation: #{location}\n" if location 42 | str += "\tDisapproved text: #{disapproved_text}\n" if disapproved_text 43 | str += "\tReason code: #{reason_code} (see: http://msdn.microsoft.com/en-US/library/bing-ads-editorialfailurereasoncodes.aspx)\n" if reason_code 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/accounts_info.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Define an account info 6 | # 7 | # Author:: jlopezn@neonline.cl 8 | # 9 | # Examples 10 | # campaign = BingAdsApi::AccountInfo.new( 11 | # :account_life_cycle_status => BingAdsApi::AccountsInfo::DRAFT 12 | # :name => "Account Name", 13 | # :number => 1234567, 14 | # :pause_reason => "1") 15 | # # => 16 | class AccountInfo < BingAdsApi::DataObject 17 | include BingAdsApi::AccountLifeCycleStatuses 18 | 19 | attr_accessor :id, :account_life_cycle_status, :name, :number, :pause_reason 20 | 21 | # Public:: Returns true if the account is in status active 22 | # 23 | # Author:: jlopezn@neonline.cl 24 | # 25 | # Returns:: boolean 26 | def active? 27 | return account_life_cycle_status == ACTIVE 28 | end 29 | 30 | 31 | # Public:: Returns true if the account is in status draft 32 | # 33 | # Author:: jlopezn@neonline.cl 34 | # 35 | # Returns:: boolean 36 | def draft? 37 | return account_life_cycle_status == DRAFT 38 | end 39 | 40 | 41 | # Public:: Returns true if the account is in status inactive 42 | # 43 | # Author:: jlopezn@neonline.cl 44 | # 45 | # Returns:: boolean 46 | def inactive? 47 | return account_life_cycle_status == INACTIVE 48 | end 49 | 50 | 51 | # Public:: Returns true if the account is in status pause 52 | # 53 | # Author:: jlopezn@neonline.cl 54 | # 55 | # Returns:: boolean 56 | def pause? 57 | return account_life_cycle_status == PAUSE 58 | end 59 | 60 | 61 | # Public:: Returns true if the account is in status pending 62 | # 63 | # Author:: jlopezn@neonline.cl 64 | # 65 | # Returns:: boolean 66 | def pending? 67 | return account_life_cycle_status == PENDING 68 | end 69 | 70 | end 71 | 72 | end -------------------------------------------------------------------------------- /lib/bing-ads-api/fault/ad_api_fault_detail.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines a fault object that operations return when generic errors occur, 6 | # such as an authentication error. 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | class AdApiFaultDetail < BingAdsApi::ApplicationFault 10 | 11 | attr_accessor :errors 12 | 13 | 14 | # Public : Constructor 15 | # 16 | # Author:: jlopezn@neonline.cl 17 | # 18 | # attributes - Hash with the initial attributes 19 | # === Attributes 20 | # * tracking_id : the operation tracking id value 21 | # * errors : array of hashes with the Ad api errors 22 | def initialize(attributes={}) 23 | super(attributes) 24 | if attributes 25 | initialize_errors(attributes[:errors]) 26 | end 27 | end 28 | 29 | # Public : Specified to string method 30 | # 31 | # Author:: jlopezn@neonline.cl 32 | def to_s 33 | str = super.to_s + ":\n" 34 | if errors 35 | str += "\tErrors:\n" + errors.map{ |e| "\t" + e.to_s }.join("\n") 36 | end 37 | 38 | return str 39 | end 40 | 41 | 42 | private 43 | 44 | # Public : Helper method for the AdApiFaultDetail constructor 45 | # to initialize the errors array 46 | # 47 | # Author:: jlopezn@neonline.cl 48 | # 49 | # errors_hash - Hash with the :errors key received from the SOAP request 50 | # 51 | # Returns:: none 52 | def initialize_errors(errors_hash) 53 | return if errors_hash.nil? 54 | 55 | if errors_hash[:ad_api_error].is_a?(Array) 56 | self.errors = [] 57 | errors_hash[:ad_api_error].each do |aae| 58 | self.errors << BingAdsApi::AdApiError.new(aae) 59 | end 60 | elsif errors_hash[:ad_api_error].is_a?(Hash) 61 | self.errors = [BingAdsApi::AdApiError.new(errors_hash[:ad_api_error])] 62 | end 63 | end 64 | 65 | end 66 | 67 | end -------------------------------------------------------------------------------- /spec/campaign_performance_report_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BingAdsApi::CampaignPerformanceReportRequest do 4 | it "should initialize campaign performance report request" do 5 | expect{ 6 | performance_report_request = BingAdsApi::CampaignPerformanceReportRequest.new(:format => :xml, 7 | :language => :english, :report_name => "My Report", 8 | :aggregation => :hourly, :time => :today, 9 | :columns => [:account_name, :account_number, :time_period], 10 | :filter => { 11 | # String as bing expected 12 | :ad_distribution => "Search", 13 | :device_os => "Windows", 14 | # snake case symbol 15 | :device_type => :computer, 16 | # nil criteria 17 | :status => nil 18 | }, 19 | :scope => { 20 | :account_ids => 12345, 21 | :campaigns => [ 22 | {:account_id => 1234, :campaign_id => 1234}, 23 | {:account_id => 1234, :campaign_id => 1234}, 24 | {:account_id => 1234, :campaign_id => 1234} 25 | ] 26 | } 27 | ) 28 | }.not_to raise_error 29 | end 30 | 31 | it "should raise an exception if the scope is invalid" do 32 | expect{ 33 | performance_report_request = BingAdsApi::CampaignPerformanceReportRequest.new(:format => :xml, 34 | :language => :english, :report_name => "My Report", 35 | :aggregation => :hourly, :time => :today, 36 | :columns => [:account_name, :account_number, :time_period ], 37 | :scope => { :campaigns => []}) 38 | }.to raise_error 39 | 40 | expect{ 41 | performance_report_request = BingAdsApi::CampaignPerformanceReportRequest.new(:format => :xml, 42 | :language => :english, :report_name => "My Report", 43 | :aggregation => :hourly, :time => :today, 44 | :columns => [:account_name, :account_number, :time_period ], 45 | :scope => {:account_ids => 12345 }) 46 | }.to raise_error 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/filter_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe BingAdsApi::Helpers::FilterHelper do 4 | include BingAdsApi::Helpers::FilterHelper 5 | 6 | let(:permitted_filters) { 7 | { 8 | "ad_distribution" => "AdDistribution", 9 | "device_os" => "DeviceOS", 10 | "device_type" => "DeviceType", 11 | "status" => "Status" 12 | } 13 | } 14 | 15 | it "should return true for valid filters" do 16 | expect( 17 | valid_filter(permitted_filters, 18 | { 19 | # String as bing expected 20 | :ad_distribution => "Search", 21 | :device_os => "Windows", 22 | # snake case symbol 23 | :device_type => :computer, 24 | # nil criteria 25 | :status => nil 26 | } 27 | ) 28 | ).to eq(true) 29 | end 30 | 31 | it "should raise exception when invalid filter is provided" do 32 | expect{ 33 | valid_filter(permitted_filters, 34 | { 35 | # Wrong String as bing expected 36 | :ad_distribution => "Searched", 37 | :device_os => "Windows", 38 | # snake case symbol 39 | :device_type => :computer, 40 | # nil criteria 41 | :status => nil 42 | } 43 | ) 44 | }.to raise_error 45 | 46 | expect{ 47 | valid_filter(permitted_filters, 48 | { 49 | # Wrong String as bing expected 50 | :ad_distribution => "Search", 51 | :device_os => "Windows", 52 | # Wrong snake case symbol 53 | :device_type => :notebook, 54 | # nil criteria 55 | :status => nil 56 | } 57 | ) 58 | }.to raise_error 59 | end 60 | 61 | it "should raise exception when a bad key is provided" do 62 | expect{ 63 | valid_filter(permitted_filters, 64 | { 65 | # Wrong filter criteria. ie: invalid key 66 | :not_a_valid_criteria => "Bleh", 67 | } 68 | ) 69 | }.to raise_error 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/helpers/column_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi::Helpers 4 | 5 | ## 6 | # Public : Utility module 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | # 10 | module ColumnHelper 11 | 12 | # Internal : Validates the specified columns at the ReporRequest initialization 13 | # At the first invalid column detected this method raises Exception. 14 | # If all column values are ok, this method returns true 15 | # Valid columns are validated against COLUMNS constant 16 | # 17 | # Author:: jlopezn@neonline.cl 18 | # 19 | # valid_columns - Hash with the valid names and values for columns 20 | # columns - Hash with the columns specified in the initialization 21 | # 22 | # Returns:: true if all columns are ok 23 | # Raises:: Exception at the first invalid column detected 24 | def valid_columns(valid_columns, columns) 25 | columns.each do |col| 26 | 27 | if col.is_a?(String) 28 | if !valid_columns.value?(col) 29 | raise Exception.new("Invalid column value '#{col}'") 30 | end 31 | elsif col.is_a?(Symbol) 32 | if !valid_columns.key?(col.to_s) 33 | raise Exception.new("Invalid column name '#{col}'") 34 | end 35 | end 36 | end 37 | return true 38 | end 39 | 40 | 41 | # Public : Return the columns attribute of the ReportRequest as a valid Hash for SOAP requests 42 | # 43 | # Author:: jlopezn@neonline.cl 44 | # 45 | # keys_case - specifies the keys_case for the hash: :underscore or :camelcase 46 | # 47 | # Returns:: Hash 48 | def columns_to_hash(valid_columns, columns, keys_case=:underscore) 49 | raise Exception.new("Invalid time value: nil") if columns.nil? 50 | 51 | key = self.class.to_s.demodulize.gsub(/ReportRequest/, 'ReportColumn') 52 | return { key => 53 | columns.map do |col| 54 | if col.is_a?(String) 55 | col 56 | elsif col.is_a?(Symbol) 57 | valid_columns[col.to_s] 58 | end 59 | end 60 | } 61 | end 62 | 63 | 64 | end 65 | end -------------------------------------------------------------------------------- /lib/bing-ads-api.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | require 'savon' 4 | require 'bing-ads-api/api_exception' 5 | require 'bing-ads-api/config' 6 | require 'bing-ads-api/constants' 7 | require 'bing-ads-api/service' 8 | require 'bing-ads-api/client_proxy' 9 | require 'bing-ads-api/soap_hasheable' 10 | require 'bing-ads-api/data_object' 11 | 12 | 13 | # Require services 14 | Dir[File.join(File.dirname(__FILE__), 'bing-ads-api', 'service', '*.rb')].each { |file| require file } 15 | 16 | # Require data objects 17 | Dir[File.join(File.dirname(__FILE__), 'bing-ads-api', 'data', '*.rb')].each { |file| require file } 18 | 19 | # Require bulk data objects 20 | Dir[File.join(File.dirname(__FILE__), 'bing-ads-api', 'data', 'bulk', '*.rb')].each { |file| require file } 21 | 22 | 23 | # Require Fault objects 24 | require 'bing-ads-api/fault/application_fault' 25 | require 'bing-ads-api/fault/ad_api_error' 26 | require 'bing-ads-api/fault/ad_api_fault_detail' 27 | require 'bing-ads-api/fault/api_fault_detail' 28 | require 'bing-ads-api/fault/batch_error' 29 | require 'bing-ads-api/fault/operation_error' 30 | require 'bing-ads-api/fault/partial_errors' 31 | 32 | # Require Reporting helper objects 33 | Dir[File.join(File.dirname(__FILE__), 'bing-ads-api', 'data', 'reporting', 'helpers', '*.rb')].each { |file| require file } 34 | 35 | # Require report request data objects 36 | require 'bing-ads-api/data/reporting/performance_report_request' 37 | require 'bing-ads-api/data/reporting/account_performance_report_request' 38 | require 'bing-ads-api/data/reporting/campaign_performance_report_request' 39 | require 'bing-ads-api/data/reporting/keyword_performance_report_request' 40 | require 'bing-ads-api/data/reporting/share_of_voice_report_request' 41 | require 'bing-ads-api/data/reporting/budget_summary_report_request' 42 | # require 'bing-ads-api/data/reporting/ad_group_performance_report_request' 43 | # require 'bing-ads-api/data/reporting/ad_performance_report_request' 44 | 45 | # Public : This is the main namespace for all classes and submodules in this BingAdsApi Gem 46 | # 47 | # Author:: jlopezn@neonline.cl 48 | module BingAdsApi 49 | 50 | 51 | end 52 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data_object.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Base Class to define Bing API data objects 7 | # Do not use this class directly, use any of the derived classes 8 | # 9 | # Author:: jlopezn@neonline.cl 10 | # 11 | class DataObject 12 | 13 | 14 | include BingAdsApi::SOAPHasheable 15 | 16 | # Public : Constructor in a ActiveRecord style, with a hash of attributes as input 17 | # 18 | # Author:: jlopezn@neonline.cl 19 | # 20 | # attributes - Hash with the objects attributes 21 | def initialize(attributes={}) 22 | attributes.each { |key, val| send("#{key}=", val) if respond_to?("#{key}=") } 23 | end 24 | 25 | 26 | # Public : Specified to string method 27 | # 28 | # Author:: jlopezn@neonline.cl 29 | def to_s 30 | to_hash.to_s 31 | end 32 | 33 | 34 | # Public: Convert this object's attributes into hash format. It will 35 | # automatically apply key ordering. 36 | # 37 | # Author:: alex.cavalli@offers.com 38 | # 39 | # === Parameters 40 | # * +keys_case+ - specifies the hash keys case, default 'underscore' 41 | # ==== keys_case 42 | # * :camelcase - CamelCase 43 | # * :underscore - underscore_case 44 | def to_hash(keys_case=:underscore) 45 | hash = super 46 | apply_order(hash) 47 | hash 48 | end 49 | 50 | private 51 | 52 | 53 | # Internal: Add an :order! key to the hash pointing to an array of all the 54 | # keys in the hash according to the order from the config file. 55 | # 56 | # Author: alex.cavalli@offers.com 57 | # 58 | # === Parameters 59 | # * +hash+ - the hash to apply an id-elevated order to 60 | def apply_order(hash) 61 | key_order = get_key_order 62 | return if key_order.empty? 63 | key_order = key_order.map { |key| key.to_s.camelcase } 64 | key_order = key_order & hash.keys # remove keys from order not present in hash 65 | hash[:order!] = key_order 66 | end 67 | 68 | 69 | # Internal: Retrieve the ordered array of keys corresponding to this data 70 | # object. Should be overriden by children. 71 | # 72 | # Author: alex.cavalli@offers.com 73 | def get_key_order 74 | [] 75 | end 76 | 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/bing-ads-api/fault/partial_errors.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines an array of error that occur when adding ads 6 | # 7 | # Author:: jlopezn@neonline.cl 8 | class PartialErrors < BingAdsApi::DataObject 9 | 10 | attr_accessor :batch_errors 11 | 12 | 13 | # Public : Constructor 14 | # 15 | # Author:: jlopezn@neonline.cl 16 | # 17 | # attributes - Hash with the initial attributes 18 | # === Attributes 19 | # * tracking_id : the operation tracking id value 20 | # * batch_error : array of hashes with the batch errors 21 | def initialize(attributes={}) 22 | super(attributes) 23 | if attributes 24 | initialize_batch_errors(attributes) 25 | end 26 | end 27 | 28 | 29 | # Public : Specified to string method 30 | # 31 | # Author:: jlopezn@neonline.cl 32 | def to_s 33 | str = " Batch Errors:\n" 34 | if batch_errors 35 | str += batch_errors.map{ |be| "\t" + be.to_s }.join("\n") 36 | end 37 | 38 | return str 39 | end 40 | 41 | 42 | private 43 | 44 | # Public : Helper method for the ApiFaultDetail constructor 45 | # to initialize the batch errors array 46 | # 47 | # Author:: jlopezn@neonline.cl 48 | # 49 | # batch_errors_hash - Hash with the :batch_errors key received from the SOAP request 50 | # 51 | # Returns:: none 52 | def initialize_batch_errors(batch_errors_hash) 53 | return if batch_errors_hash.nil? 54 | 55 | if batch_errors_hash[:batch_error].is_a?(Array) 56 | self.batch_errors = [] 57 | batch_errors_hash[:batch_error].each do |be| 58 | self.batch_errors << init_batch_error(be) 59 | end 60 | elsif batch_errors_hash[:batch_error].is_a?(Hash) 61 | self.batch_errors = [ init_batch_error(batch_errors_hash[:batch_error]) ] 62 | end 63 | end 64 | 65 | 66 | def init_batch_error(batch_error_hash) 67 | if batch_error_hash.key?("@i:type".to_sym) && batch_error_hash["@i:type".to_sym] == "EditorialError" 68 | return BingAdsApi::EditorialError.new(batch_error_hash) 69 | else 70 | return BingAdsApi::BatchError.new(batch_error_hash) 71 | end 72 | end 73 | end 74 | 75 | end -------------------------------------------------------------------------------- /lib/bing-ads-api/config.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'singleton' 3 | 4 | module BingAdsApi 5 | 6 | # Public : Helper class for configuration issues like WSDL URLs and constants 7 | # 8 | # Author:: author@neonline.cl 9 | # 10 | # Examples 11 | # class_usage 12 | # # => class_usage_return 13 | class Config 14 | include Singleton 15 | 16 | # Array with Bing Ads API environments: +sandbox+ and +production+ 17 | ENVIRONMENTS = ['sandbox', 'production'] 18 | 19 | attr_accessor :config 20 | @config = YAML.load_file(File.join(File.dirname(__FILE__),"../bing-ads-api.yml")) 21 | 22 | # Public : Constructor 23 | # 24 | # Author:: jlopezn@neonline.cl 25 | def initialize 26 | @config = YAML.load_file(File.join(File.dirname(__FILE__),"../bing-ads-api.yml")) 27 | end 28 | 29 | # Public : Returns the config file as an Hash instance 30 | # 31 | # Author:: jlopezn@neonline.cl 32 | # 33 | # Returns:: Hash 34 | def self.hash_instance 35 | instance.config 36 | end 37 | 38 | ## Constants 39 | @config['constants'].each do |key, value| 40 | 41 | define_method("#{key.to_s}_constants") do |constant=nil| 42 | value[constant.to_s] if constant 43 | value 44 | end 45 | 46 | end 47 | 48 | ## Orders 49 | @config['orders'].each do |key, value| 50 | 51 | define_method("#{key.to_s}_orders") do |order=nil| 52 | value[order.to_s] if order 53 | value 54 | end 55 | 56 | end 57 | 58 | # Public : Returns a String with WSDL url for the service indicated 59 | # 60 | # Author:: jlopezn@neonline.cl 61 | # 62 | # === Parameters 63 | # environment - Bing Environment: 'sandbox' or 'production' 64 | # service - service name 65 | # 66 | # === Examples 67 | # config.service_wsdl(:sandbox, :campaign_management) 68 | # # => "https://api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v9/CampaignManagementService.svc?singleWsdl" 69 | # 70 | # Returns:: returns 71 | # Raises:: exception 72 | def service_wsdl(environment, service) 73 | if (ENVIRONMENTS.include?(environment.to_s)) 74 | if @config['wsdl'][environment.to_s].include?(service.to_s) 75 | return @config['wsdl'][environment.to_s][service.to_s] 76 | end 77 | raise "Unknown service '#{service.to_s}'. Available services: #{@config['wsdl'][environment.to_s].keys.join(", ")}" 78 | end 79 | raise "Invalid environment: #{environment}. Value should be 'sandbox' or 'production'" 80 | end 81 | 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/helpers/time_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi::Helpers 4 | 5 | ## 6 | # Public : Utility module 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | # 10 | module TimeHelper 11 | 12 | # Valid languages for reports 13 | TIME_PERIODS = BingAdsApi::Config.instance. 14 | reporting_constants['time_periods'] 15 | 16 | # Public : Validates the time attribute present in some report request 17 | # 18 | # Author:: jlopezn@neonline.cl 19 | # 20 | # time - Hash with the time attribute for the report request 21 | # 22 | # Returns:: true if validation is ok, raises Exception otherwise 23 | # Raises:: Exception if custom date range is bad informed, or if time periods specified is unknown 24 | def valid_time(time) 25 | # Custom date range 26 | if time.is_a?(Hash) 27 | raise Exception.new("Invalid time: missing :custom_date_range_start key") if !time.key?(:custom_date_range_start) 28 | raise Exception.new("Invalid time: missing :custom_date_range_end key") if !time.key?(:custom_date_range_end) 29 | # Time periods 30 | else 31 | return TIME_PERIODS.key?(time.to_s) 32 | end 33 | return true 34 | end 35 | 36 | 37 | # Public : Return the time attribute of the ReportRequest as a valid Hash for SOAP requests 38 | # 39 | # Author:: jlopezn@neonline.cl 40 | # 41 | # keys_case - specifies the keys_case for the hash: :underscore or :camelcase 42 | # 43 | # Returns:: Hash 44 | def time_to_hash(keys_case) 45 | raise Exception.new("Invalid time value: nil") if self.time.nil? 46 | 47 | # Custom date range 48 | if time.is_a?(Hash) 49 | return { 50 | get_attribute_key("custom_date_range_end", keys_case) => { 51 | get_attribute_key("day", keys_case) => self.time[:custom_date_range_end][:day], 52 | get_attribute_key("month", keys_case) => self.time[:custom_date_range_end][:month], 53 | get_attribute_key("year", keys_case) => self.time[:custom_date_range_end][:year], 54 | 55 | }, 56 | get_attribute_key("custom_date_range_start", keys_case) => { 57 | get_attribute_key("day", keys_case) => self.time[:custom_date_range_start][:day], 58 | get_attribute_key("month", keys_case) => self.time[:custom_date_range_start][:month], 59 | get_attribute_key("year", keys_case) => self.time[:custom_date_range_start][:year], 60 | } 61 | } 62 | # Time periods 63 | else 64 | return {"PredefinedTime" => TIME_PERIODS[time.to_s]} 65 | end 66 | end 67 | 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/client_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Author:: jlopezn@neonline.cl 5 | describe BingAdsApi::ClientProxy do 6 | 7 | let(:default_options) do 8 | { 9 | username: "ruby_bing_ads_sbx", 10 | password: "sandbox123", 11 | developer_token: "BBD37VB98", 12 | customer_id: "21025739", 13 | account_id: "8506945" 14 | } 15 | end 16 | 17 | it "should create client proxy" do 18 | options = { 19 | username: @username, 20 | password: @password, 21 | developer_token: @developer_token, 22 | customer_id: @customer_id, 23 | account_id: @account_id, 24 | wsdl_url: "https://campaign.api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/V10/CampaignManagementService.svc?singleWsdl" 25 | } 26 | client = BingAdsApi::ClientProxy.new(options) 27 | 28 | expect(client).not_to be_nil 29 | expect(client.service).not_to be_nil 30 | end 31 | 32 | it "should create client proxy with additional settings" do 33 | options = { 34 | username: @username, 35 | password: @password, 36 | developer_token: @developer_token, 37 | customer_id: @customer_id, 38 | account_id: @account_id, 39 | wsdl_url: "https://campaign.api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/V10/CampaignManagementService.svc?singleWsdl", 40 | proxy: { 41 | logger: Logger.new(STDOUT), 42 | encoding: "UTF-8" 43 | } 44 | } 45 | client = BingAdsApi::ClientProxy.new(options) 46 | 47 | expect(client).not_to be_nil 48 | expect(client.service).not_to be_nil 49 | end 50 | 51 | it "should call service" do 52 | options = default_options.merge( 53 | wsdl_url: "https://campaign.api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/V10/CampaignManagementService.svc?singleWsdl" 54 | ) 55 | 56 | client = BingAdsApi::ClientProxy.new(options) 57 | expect(client).not_to be_nil 58 | 59 | response = client.service.call(:get_campaigns_by_account_id, 60 | message: { account_id: client.account_id}) 61 | expect(response).not_to be_nil 62 | end 63 | 64 | it "should create and call from config" do 65 | config = BingAdsApi::Config.instance 66 | options = default_options.merge( 67 | wsdl_url: config.service_wsdl(:sandbox, :campaign_management) 68 | ) 69 | 70 | client = BingAdsApi::ClientProxy.new(options) 71 | expect(client).not_to be_nil 72 | 73 | response = client.service.call(:get_campaigns_by_account_id, 74 | message: { account_id: client.account_id}) 75 | expect(response).not_to be_nil 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/performance_report_request.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base class for 'performance report requests'. 7 | # Do not instantiate this object. Instead, use BingAdsApi::AccountPerformanceReportRequest, 8 | # BingAdsApi::CampaignPerformanceReportRequest, BingAdsApi::AdGroupPerformanceReportRequest or 9 | # BingAdsApi::AdPerformanceReportRequest 10 | # 11 | # Reference: http://msdn.microsoft.com/en-us/library/bing-ads-reporting-bing-ads-reportrequest.aspx 12 | # 13 | # Author:: jlopezn@neonline.cl 14 | class PerformanceReportRequest < BingAdsApi::ReportRequest 15 | 16 | # Adds helper methods to time attribute 17 | include BingAdsApi::Helpers::TimeHelper 18 | 19 | # Adds helper methods to column attribute 20 | include BingAdsApi::Helpers::ColumnHelper 21 | 22 | # Adds helper methods to filter attribute 23 | include BingAdsApi::Helpers::FilterHelper 24 | 25 | 26 | # Valid aggregations for reports 27 | AGGREGATIONS = BingAdsApi::Config.instance. 28 | reporting_constants['aggregation'] 29 | 30 | 31 | attr_accessor :aggregation, :columns, :filter, :scope, :time 32 | 33 | 34 | # Public : Constructor. Adds validations to aggregations and time 35 | # 36 | # Author:: jlopezn@neonline.cl 37 | # 38 | # === Parameters 39 | # attributes - Hash with Performance report request 40 | # 41 | def initialize(attributes={}) 42 | raise Exception.new("Invalid aggregation '#{attributes[:aggregation]}'") if !valid_aggregation(attributes[:aggregation]) 43 | raise Exception.new("Invalid time") if !valid_time(attributes[:time]) 44 | super(attributes) 45 | end 46 | 47 | 48 | # Public:: Returns this object as a Hash for SOAP Requests 49 | # 50 | # Author:: jlopezn@neonline.cl 51 | # 52 | # === Parameters 53 | # * +keys_case+ - specifies the case for the hash keys 54 | # ==== keys_case 55 | # * :camelcase - CamelCase 56 | # * :underscore - underscore_case 57 | # 58 | # === Examples 59 | # performance_report_request.to_hash(:camelcase) 60 | # # => {"Format"=>"Xml", "Language"=>"English", "ReportName"=>"My Report", "Aggregation"=>"Hourly", "Time"=>"Today", "ReturnOnlyCompleteData"=>false} 61 | # 62 | # Returns:: Hash 63 | def to_hash(keys_case = :underscore) 64 | hash = super(keys_case) 65 | hash[get_attribute_key('aggregation', keys_case)] = AGGREGATIONS[self.aggregation.to_s] 66 | hash[get_attribute_key('time', keys_case)] = time_to_hash(keys_case) 67 | return hash 68 | end 69 | 70 | private 71 | 72 | def valid_aggregation(aggregation) 73 | return AGGREGATIONS.key?(aggregation.to_s) 74 | end 75 | 76 | end 77 | 78 | end -------------------------------------------------------------------------------- /spec/helpers/bing_ads_factory.rb: -------------------------------------------------------------------------------- 1 | class BingAdsFactory 2 | 3 | # Helper method to create a campaign on the remote API. Returns the created 4 | # campaign id. 5 | def self.create_campaign 6 | name = "Test Campaign #{SecureRandom.uuid}" 7 | campaigns = [ 8 | BingAdsApi::Campaign.new( 9 | budget_type: BingAdsApi::Campaign::DAILY_BUDGET_STANDARD, 10 | daily_budget: 2000, 11 | daylight_saving: "false", 12 | description: name + " description", 13 | name: name + " name", 14 | time_zone: BingAdsApi::Campaign::SANTIAGO 15 | ) 16 | ] 17 | response = service.add_campaigns(account_id, campaigns) 18 | response[:campaign_ids][:long] 19 | end 20 | 21 | # Helper method to create an ad group on the remote API. Returns the created 22 | # ad group id. 23 | def self.create_ad_group(campaign_id) 24 | name = "Ad Group #{SecureRandom.uuid}" 25 | ad_groups = [ 26 | BingAdsApi::AdGroup.new( 27 | ad_distribution: BingAdsApi::AdGroup::SEARCH, 28 | language: BingAdsApi::AdGroup::SPANISH, 29 | name: name + " name", 30 | pricing_model: BingAdsApi::AdGroup::CPC, 31 | bidding_model: BingAdsApi::AdGroup::KEYWORD 32 | ) 33 | ] 34 | response = service.add_ad_groups(campaign_id, ad_groups) 35 | response[:ad_group_ids][:long] 36 | end 37 | 38 | # Helper method to create an ad on the remote API. Returns the created ad id. 39 | def self.create_text_ad(ad_group_id) 40 | text_ad = BingAdsApi::TextAd.new( 41 | status: BingAdsApi::Ad::ACTIVE, 42 | destination_url: "http://www.adxion.com", 43 | display_url: "AdXion.com", 44 | text: "Text Ad #{SecureRandom.uuid}", 45 | title: "Text Ad" 46 | ) 47 | response = service.add_ads(ad_group_id, text_ad) 48 | response[:ad_ids][:long] 49 | end 50 | 51 | # Helper method to create a keyword on the remote API. Returns the created 52 | # keyword id. 53 | def self.create_keyword(ad_group_id) 54 | keyword = BingAdsApi::Keyword.new( 55 | bid: BingAdsApi::Bid.new(amount: 1.23), 56 | destination_url: "http://www.adxion.com", 57 | match_type: BingAdsApi::Keyword::EXACT, 58 | status: BingAdsApi::Keyword::ACTIVE, 59 | text: "Keyword #{SecureRandom.uuid}" 60 | ) 61 | response = service.add_keywords(ad_group_id, keyword) 62 | response[:keyword_ids][:long] 63 | end 64 | 65 | def self.service 66 | @service ||= BingAdsApi::CampaignManagement.new( 67 | environment: :sandbox, 68 | username: "ruby_bing_ads_sbx", 69 | password: "sandbox123", 70 | developer_token: "BBD37VB98", 71 | customer_id: "21025739", 72 | account_id: account_id 73 | ) 74 | end 75 | 76 | def self.account_id 77 | "8506945" 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/report_request.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object for all report requests. 7 | # Do not instantiate this object. Instead, you may instantiate one 8 | # of the following report request objects which derives from this object to request a report. 9 | # 10 | # Reference: http://msdn.microsoft.com/en-us/library/bing-ads-reporting-bing-ads-reportrequest.aspx 11 | # 12 | # Author:: jlopezn@neonline.cl 13 | class ReportRequest < BingAdsApi::DataObject 14 | 15 | # Valid Formats for reports 16 | FORMATS = BingAdsApi::Config.instance. 17 | reporting_constants['format'] 18 | 19 | # Valid languages for reports 20 | LANGUAGES = BingAdsApi::Config.instance. 21 | reporting_constants['language'] 22 | 23 | # Valid report request status for reports 24 | REQUEST_STATUS = BingAdsApi::Config.instance. 25 | reporting_constants['request_status_type'] 26 | 27 | attr_accessor :format, :language, :report_name, :return_only_complete_data 28 | 29 | # Public : Constructor. Adds validations to format and language attributes 30 | # 31 | # Author:: jlopezn@neonline.cl 32 | # 33 | # === Parameters 34 | # attributes - Hash with Report request parameters 35 | def initialize(attributes={}) 36 | raise Exception.new("Invalid format '#{attributes[:format]}'") if !valid_format(attributes[:format]) 37 | raise Exception.new("Invalid language '#{attributes[:language]}'") if !valid_language(attributes[:language]) 38 | super(attributes) 39 | self.return_only_complete_data = attributes[:return_only_complete_data] || false 40 | end 41 | 42 | 43 | # Public:: Returns this object as a Hash for SOAP Requests 44 | # 45 | # Author:: jlopezn@neonline.cl 46 | # 47 | # === Parameters 48 | # * +keys_case+ - specifies the case for the hash keys 49 | # ==== keys_case 50 | # * :camelcase - CamelCase 51 | # * :underscore - underscore_case 52 | # 53 | # === Examples 54 | # report_request.to_hash(:camelcase) 55 | # # => {"Format"=>"Xml", "Language"=>"English", "ReportName"=>"My Report", "ReturnOnlyCompleteData"=>true} 56 | # 57 | # Returns:: Hash 58 | def to_hash(keys_case = :underscore) 59 | hash = super(keys_case) 60 | hash[get_attribute_key('format', keys_case)] = FORMATS[self.format.to_s] 61 | hash[get_attribute_key('language', keys_case)] = LANGUAGES[self.language.to_s] 62 | return hash 63 | end 64 | 65 | private 66 | 67 | def valid_format(format) 68 | return FORMATS.key?(format.to_s) 69 | end 70 | 71 | 72 | def valid_language(language) 73 | return LANGUAGES.key?(language.to_s) 74 | end 75 | 76 | # Internal: Retrieve the ordered array of keys corresponding to this data 77 | # object. 78 | # 79 | # Author: alex.cavalli@offers.com 80 | def get_key_order 81 | super.concat(BingAdsApi::Config.instance. 82 | reporting_orders['report_request']) 83 | end 84 | 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/bing-ads-api/service/customer_management.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module BingAdsApi 3 | 4 | 5 | # Public : This class represents the Customer Management Services 6 | # defined in the Bing Ads API, to manage customer accounts 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | # 10 | # Examples 11 | # options = { 12 | # :environment => :sandbox, 13 | # :username => "username", 14 | # :password => "pass", 15 | # :developer_token => "SOME_TOKEN", 16 | # :customer_id => "1234567", 17 | # :account_id => "9876543" } 18 | # service = BingAdsApi::CustomerManagement.new(options) 19 | class CustomerManagement < BingAdsApi::Service 20 | 21 | 22 | # Public : Constructor 23 | # 24 | # Author:: jlopezn@neonline.cl 25 | # 26 | # options - Hash with the parameters for the client proxy and the environment 27 | # 28 | # Examples 29 | # options = { 30 | # :environment => :sandbox, 31 | # :username => "username", 32 | # :password => "password", 33 | # :developer_token => "DEV_TOKEN", 34 | # :customer_id => "123456", 35 | # :account_id => "654321" 36 | # } 37 | # service = BingAdsApi::CustomerManagement.new(options) 38 | def initialize(options={}) 39 | super(options) 40 | end 41 | 42 | 43 | ######################### 44 | ## Operations Wrappers ## 45 | ######################### 46 | 47 | # Public : Gets a list of objects that contains account identification information, 48 | # for example the name and identifier of the account, for the specified customer. 49 | # 50 | # Author:: jlopezn@neonline.cl 51 | # 52 | # === Parameters 53 | # +customer_id+ - identifier for the customer who owns the accounts. If nil, then the authentication customer id is used 54 | # +only_parent_accounts+ - boolean to determine whether to return only the accounts that belong to the customer or to also 55 | # return the accounts that the customer manages for other customers. Default false 56 | # 57 | # === Examples 58 | # customer_management_service.get_accounts_info 59 | # # => Array[BingAdsApi::AccountsInfo] 60 | # 61 | # Returns:: Array of BingAdsApi::AccountsInfo 62 | # 63 | # Raises:: exception 64 | def get_accounts_info(customer_id=nil, only_parent_accounts=false) 65 | response = call(:get_accounts_info, 66 | {customer_id: customer_id || self.client_proxy.customer_id, 67 | only_parent_accounts: only_parent_accounts.to_s}) 68 | response_hash = get_response_hash(response, __method__) 69 | accounts = response_hash[:accounts_info][:account_info] 70 | if accounts.is_a?(Array) 71 | accounts.map! do |account_hash| 72 | BingAdsApi::AccountInfo.new(account_hash) 73 | end 74 | else 75 | # when there is only one account api does not return an array but a single object 76 | accounts = [BingAdsApi::AccountInfo.new(accounts)] 77 | end 78 | return accounts 79 | end 80 | 81 | 82 | private 83 | def get_service_name 84 | "customer_management" 85 | end 86 | 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /lib/bing-ads-api/fault/api_fault_detail.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines a fault object that operations return when web service-specific errors occur, 6 | # such as when the request message contains incomplete or invalid data. 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | class ApiFaultDetail < BingAdsApi::ApplicationFault 10 | 11 | attr_accessor :batch_errors, :operation_errors 12 | 13 | 14 | # Public : Constructor 15 | # 16 | # Author:: jlopezn@neonline.cl 17 | # 18 | # attributes - Hash with the initial attributes 19 | # === Attributes 20 | # * tracking_id : the operation tracking id value 21 | # * batch_errors : array of hashes with the batch errors 22 | # * operation_errors : array of hashes with the operation errors 23 | def initialize(attributes={}) 24 | super(attributes) 25 | if attributes 26 | initialize_batch_errors(attributes[:batch_errors]) 27 | initialize_operations_errors(attributes[:operation_errors]) 28 | end 29 | end 30 | 31 | 32 | # Public : Specific to string 33 | # 34 | # Author:: jlopezn@neonline.cl 35 | # 36 | # Returns:: the object 'stringified' 37 | def to_s 38 | str = super.to_s + ":\n" 39 | if batch_errors 40 | str += "\tBatch Errors:\n" + batch_errors.map{ |be| "\t" + be.to_s }.join("\n") 41 | end 42 | 43 | if operation_errors 44 | str += "\tOperation Errors:\n" + operation_errors.map{ |oe| "\t" + oe.to_s }.join("\n") 45 | end 46 | return str 47 | end 48 | 49 | 50 | private 51 | 52 | # Public : Helper method for the ApiFaultDetail constructor 53 | # to initialize the batch errors array 54 | # 55 | # Author:: jlopezn@neonline.cl 56 | # 57 | # batch_errors_hash - Hash with the :batch_errors key received from the SOAP request 58 | # 59 | # Returns:: none 60 | def initialize_batch_errors(batch_errors_hash) 61 | return if batch_errors_hash.nil? 62 | 63 | if batch_errors_hash[:batch_error].is_a?(Array) 64 | self.batch_errors = [] 65 | batch_errors_hash[:batch_error].each do |be| 66 | self.batch_errors << BingAdsApi::BatchError.new(be) 67 | end 68 | elsif batch_errors_hash[:batch_error].is_a?(Hash) 69 | self.batch_errors = [BingAdsApi::BatchError.new(batch_errors_hash[:batch_error])] 70 | end 71 | end 72 | 73 | 74 | # Public : Helper method for the ApiFaultDetail constructor 75 | # to initialize the operation errors array 76 | # 77 | # Author:: jlopezn@neonline.cl 78 | # 79 | # operation_errors_hash - Hash with the :operation_errors key received from the SOAP request 80 | # 81 | # Returns:: none 82 | def initialize_operations_errors(operation_errors_hash) 83 | return if operation_errors_hash.nil? 84 | 85 | if operation_errors_hash[:operations_error].is_a?(Array) 86 | self.operation_errors = [] 87 | operation_errors_hash[:operation_error].each do |oe| 88 | self.operation_errors << BingAdsApi::OperationError.new(oe) 89 | end 90 | elsif operation_errors_hash[:operation_error].is_a?(Hash) 91 | self.operation_errors = [BingAdsApi::OperationError.new(operation_errors_hash[:operation_error])] 92 | end 93 | 94 | end 95 | end 96 | 97 | end -------------------------------------------------------------------------------- /lib/bing-ads-api/service/customer_billing.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module BingAdsApi 3 | 4 | 5 | # Public : This class represents the Campaign Management Services 6 | # defined in the Bing Ads API, to manage advertising campaigns 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | # 10 | # Examples 11 | # options = { 12 | # :environment => :sandbox, 13 | # :username => "username", 14 | # :password => "pass", 15 | # :developer_token => "SOME_TOKEN", 16 | # :customer_id => "1234567", 17 | # :account_id => "9876543" } 18 | # service = BingAdsApi::CampaignManagement.new(options) 19 | class CustomerBilling < BingAdsApi::Service 20 | 21 | 22 | # Public : Constructor 23 | # 24 | # Author:: jlopezn@neonline.cl 25 | # 26 | # options - Hash with the parameters for the client proxy and the environment 27 | # 28 | # Examples 29 | # options = { 30 | # :environment => :sandbox, 31 | # :username => "username", 32 | # :password => "password", 33 | # :developer_token => "DEV_TOKEN", 34 | # :customer_id => "123456", 35 | # :account_id => "654321" 36 | # } 37 | # service = BingAdsApi::CampaignManagement.new(options) 38 | def initialize(options={}) 39 | super(options) 40 | end 41 | 42 | 43 | ######################### 44 | ## Operations Wrappers ## 45 | ######################### 46 | 47 | 48 | # Public : Returns all the insertion orders found in the specified account 49 | # 50 | # Author:: dmitrii@webstreak.com 51 | # 52 | # === Parameters 53 | # account_id 54 | # 55 | # === Examples 56 | # customer_billing_service.get_insertion_orders_by_account_id(1) 57 | # # => Array[1,2,3] 58 | # 59 | # Returns:: Array of Insertion Orders 60 | # 61 | # Raises:: exception 62 | def get_insertion_orders_by_account_id(account_id) 63 | response = call(:get_insertion_orders_by_account, 64 | { account_id: account_id } ) 65 | response_hash = get_response_hash(response, __method__) 66 | #campaign_ids = response_hash[:campaign_id_collection][:id_collection][:ids][:long] 67 | #return campaign_ids 68 | return response_hash 69 | end 70 | 71 | # Public : Returns insertion orders based on specific conditions 72 | # 73 | # Author:: dmitrii@webstreak.com 74 | # 75 | # === Parameters 76 | # ordering 77 | # page_info 78 | # predicates 79 | # 80 | # Returns:: Array of Insertion Orders 81 | # 82 | # Raises:: exception 83 | def search_insertion_orders(account_id) 84 | #predicate = BingAdsApi::Predicate.new(field: 'AccountId', operator: 'Equals', value: account_id) 85 | response = call(:search_insertion_orders, 86 | { predicates: { 87 | predicate: { field: 'AccountId', operator: 'Equals', value: account_id } 88 | } 89 | } 90 | ) 91 | response_hash = get_response_hash(response, __method__) 92 | #campaign_ids = response_hash[:campaign_id_collection][:id_collection][:ids][:long] 93 | #return campaign_ids 94 | return response_hash 95 | end 96 | 97 | 98 | private 99 | 100 | def get_service_name 101 | "customer_billing" 102 | end 103 | 104 | 105 | 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/bulk/bulk_upload_status.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | require 'open-uri' 4 | 5 | module BingAdsApi 6 | 7 | ## 8 | # Public: Wrapper class for GetBulkUploadStatusResponse. Not an 9 | # actual data object in the Bing API. 10 | # 11 | # http://msdn.microsoft.com/en-US/library/dn600289.aspx 12 | # 13 | # Author:: alex.cavalli@offers.com 14 | class BulkUploadStatus < BingAdsApi::DataObject 15 | 16 | # Valid request status 17 | REQUEST_STATUS = BingAdsApi::Config.instance. 18 | bulk_constants['request_status_type'] 19 | 20 | attr_accessor :errors, :forward_compatibility_map, :percent_complete, 21 | :request_id, :request_status, :result_file_url 22 | 23 | # Public:: Returns true if the status is completed 24 | # 25 | # Author:: alex.cavalli@offers.com 26 | # 27 | # Returns:: boolean 28 | def completed? 29 | return request_status == REQUEST_STATUS['completed'] 30 | end 31 | 32 | # Public:: Returns true if the status is completed with errors 33 | # 34 | # Author:: alex.cavalli@offers.com 35 | # 36 | # Returns:: boolean 37 | def completed_with_errors? 38 | return request_status == REQUEST_STATUS['completed_with_errors'] 39 | end 40 | 41 | # Public:: Returns true if the status is failed 42 | # 43 | # Author:: alex.cavalli@offers.com 44 | # 45 | # Returns:: boolean 46 | def failed? 47 | return request_status == REQUEST_STATUS['failed'] 48 | end 49 | 50 | # Public:: Returns true if the status is file uploaded 51 | # 52 | # Author:: alex.cavalli@offers.com 53 | # 54 | # Returns:: boolean 55 | def file_uploaded? 56 | return request_status == REQUEST_STATUS['file_uploaded'] 57 | end 58 | 59 | # Public:: Returns true if the request_status is in progress 60 | # 61 | # Author:: alex.cavalli@offers.com 62 | # 63 | # Returns:: boolean 64 | def in_progress? 65 | return request_status == REQUEST_STATUS['in_progress'] 66 | end 67 | 68 | # Public:: Returns true if the status is pending file upload 69 | # 70 | # Author:: alex.cavalli@offers.com 71 | # 72 | # Returns:: boolean 73 | def pending_file_upload? 74 | return request_status == REQUEST_STATUS['pending_file_upload'] 75 | end 76 | 77 | # Public:: Attempts to download the result file to the specified directory. 78 | # The file name will be the request id appended with ".zip". 79 | # 80 | # Author:: alex.cavalli@offers.com 81 | # 82 | # === Parameters 83 | # +target_directory+ - Directory or file in directory to download the upload 84 | # results file to. 85 | # 86 | # Returns:: The downloaded file path if success, or nil if failure. 87 | def download_result_file(target_directory) 88 | return nil unless completed? || completed_with_errors? 89 | 90 | dir = Dir.exists?(target_directory) ? target_directory : File.dirname(target_directory) 91 | raise ArgumentError, "Could not determine target directory from argument: #{target_directory}" unless Dir.exists?(dir) 92 | 93 | result_file_target = File.join(dir, "#{request_id}.zip") 94 | 95 | File.open(result_file_target, "wb") do |result_file| 96 | result_file << open(result_file_url).read 97 | end 98 | end 99 | 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /lib/bing-ads-api/service/reporting.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module BingAdsApi 3 | 4 | # Public : This class represents the Reporting Services 5 | # defined in the Bing Ads API, to request and download reports 6 | # 7 | # Author:: jlopezn@neonline.cl 8 | # 9 | # Examples 10 | # options = { 11 | # :environment => :sandbox, 12 | # :username => "username", 13 | # :password => "pass", 14 | # :developer_token => "SOME_TOKEN", 15 | # :customer_id => "1234567", 16 | # :account_id => "9876543" } 17 | # service = BingAdsApi::Reporting.new(options) 18 | class Reporting < BingAdsApi::Service 19 | 20 | 21 | # Public : Get the status of a report request 22 | # 23 | # Author:: jlopezn@neonline.cl 24 | # 25 | # === Parameters 26 | # +report_request_id+ - Identifier of the report request 27 | # 28 | # === Examples 29 | # service.poll_generate_report("12345") 30 | # # => Hash 31 | # 32 | # Returns:: Hash with the PollGenerateReportResponse structure 33 | # 34 | # Raises:: exception 35 | def poll_generate_report(report_request_id) 36 | response = call(:poll_generate_report, 37 | {report_request_id: report_request_id} ) 38 | response_hash = get_response_hash(response, __method__) 39 | report_request_status = BingAdsApi::ReportRequestStatus.new( 40 | response_hash[:report_request_status]) 41 | return report_request_status 42 | end 43 | 44 | # Public : Submits a report request 45 | # 46 | # Author:: jlopezn@neonline.cl 47 | # 48 | # === Parameters 49 | # +report_request+ - a BingAdsApi::ReportRequest subclass instance 50 | # 51 | # === Examples 52 | # ==== CampaignPerformanceReportRequest 53 | # report_request = BingAdsApi::CampaignPerformanceReportRequest.new(:format => :xml, 54 | # :language => :english, 55 | # :report_name => "My Report", 56 | # :aggregation => :hourly, 57 | # :columns => [:account_name, :account_number, :time_period, 58 | # :campaign_name, :campaign_id, :status, :currency_code, 59 | # :impressions, :clicks, :ctr, :average_cpc, :spend, 60 | # :conversions, :conversion_rate, :cost_per_conversion], 61 | # :filter => { 62 | # # String as bing expected 63 | # :ad_distribution => "Search", 64 | # :device_os => "Windows", 65 | # # snake case symbol 66 | # :device_type => :computer, 67 | # # nil criteria 68 | # :status => nil 69 | # }, 70 | # :scope => {:account_ids => 5978083, 71 | # :campaigns => [ 72 | # {:account_id => 5978083, :campaign_id => 1951230156}, 73 | # {:account_id => 5978083, :campaign_id => 1951245412}, 74 | # {:account_id => 5978083, :campaign_id => 1951245474}] 75 | # }, 76 | # :time => { 77 | # :custom_date_range_end => {:day => 31, :month => 12, :year => 2013}, 78 | # :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 79 | # }) 80 | # report_request_id = reporting_service.submit_generate_report(report_request) 81 | # # => "1234567890" 82 | # 83 | # Returns:: String with the requested report id 84 | # 85 | # Raises:: Exception if report_request is invalid or SOAP request failed 86 | def submit_generate_report(report_request) 87 | response = call(:submit_generate_report, 88 | {report_request: report_request.to_hash(:camelcase)}) 89 | response_hash = get_response_hash(response, __method__) 90 | report_request_id = response_hash[:report_request_id] 91 | return report_request_id 92 | end 93 | 94 | 95 | private 96 | def get_service_name 97 | "reporting" 98 | end 99 | 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = BingAdsApi 2 | 3 | This project rocks and uses MIT-LICENSE. 4 | 5 | == Presentation 6 | 7 | This is a very simple, straight use gem for integrating any Rails app with the 8 | Bing Ads SOAP Webservices. 9 | 10 | == Installation 11 | 12 | Add to your Gemfile 13 | gem 'bing-ads-api' 14 | 15 | == Examples 16 | So far, I've just integrated some methods from +CampaignManagement+, +CustomerManagement+ and +Reporting+ services 17 | 18 | 19 | === Authentication 20 | 21 | In all cases, you must initialize some service object with the authentication options 22 | Here's an example with the CampaignMangement service object 23 | 24 | options = { 25 | :environment => :sandbox, 26 | :username => "desarrollo_neonline", 27 | :password => "neonline2013", 28 | :developer_token => "BBD37VB98", 29 | :customer_id => "21021746", 30 | :account_id => "5978083" 31 | } 32 | service = BingAdsApi::CampaignManagement.new(options) 33 | 34 | * +environment+ can be one of two values: +:sandbox+ or +:production+ 35 | 36 | 37 | === Campaign Management 38 | 39 | Example of service object initialization: 40 | options = { 41 | :environment => :sandbox, 42 | :username => "desarrollo_neonline", 43 | :password => "neonline2013", 44 | :developer_token => "BBD37VB98", 45 | :customer_id => "21021746", 46 | :account_id => "5978083" 47 | } 48 | service = BingAdsApi::CampaignManagement.new(options) 49 | 50 | 51 | ==== Adding Campaigns 52 | 53 | account_id = 123456789 54 | campaigns = [ 55 | BingAdsApi::Campaign.new( 56 | :budget_type => BingAdsApi::Campaign::DAILY_BUDGET_STANDARD, 57 | :conversion_tracking_enabled => "false", 58 | :daily_budget => 2000, 59 | :daylight_saving => "false", 60 | :description => name + " first description", 61 | :monthly_budget => 5400, 62 | :name => name + " first name", 63 | :status => BingAdsApi::Campaign::PAUSED, 64 | :time_zone => BingAdsApi::Campaign::SANTIAGO), 65 | 66 | BingAdsApi::Campaign.new( 67 | :budget_type => BingAdsApi::Campaign::DAILY_BUDGET_STANDARD, 68 | :conversion_tracking_enabled => "false", 69 | :daily_budget => 2500, 70 | :daylight_saving => "false", 71 | :description => name + " second description", 72 | :monthly_budget => 7800, 73 | :name => name + " second name", 74 | :status => BingAdsApi::Campaign::PAUSED, 75 | :time_zone => BingAdsApi::Campaign::SANTIAGO) ] 76 | response = service.add_campaigns(account_id, campaigns) 77 | 78 | In some cases the methods will return a Hash with the 'response' tag according 79 | to the method invoked. Like the example above. 80 | 81 | 82 | ==== Find Campaigns 83 | 84 | In other cases, like this next example, the method will return an object, like an array or 85 | a object representation of the hash 86 | campaigns = service.get_campaigns_by_account_id(123456789) 87 | # => campaigns is an array of BingAdsApi::Campaign 88 | 89 | 90 | === Customer Management 91 | 92 | Example of service object initialization: 93 | options = { 94 | :environment => :sandbox, 95 | :username => "desarrollo_neonline", 96 | :password => "neonline2013", 97 | :developer_token => "BBD37VB98", 98 | :customer_id => "21021746", 99 | :account_id => "5978083" 100 | } 101 | service = BingAdsApi::CustomerManagement.new(options) 102 | 103 | ==== Get accounts info 104 | accounts = service.get_accounts_info 105 | # => accounts is an array of BingAdsApi::AccountsInfo 106 | 107 | For this method you can also specify a diferent customer id 108 | accounts = service.get_accounts_info(12345) 109 | # => accounts is an array of BingAdsApi::AccountsInfo 110 | That would give you the customer's 12345 accounts 111 | 112 | 113 | 114 | === Reporting 115 | 116 | ==== Submit generate report 117 | 118 | 119 | ==== Poll generate report 120 | 121 | 122 | -------------------------------------------------------------------------------- /lib/bing-ads-api/constants.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | require 'active_support/core_ext/string/inflections' 4 | 5 | module BingAdsApi 6 | 7 | # Public : Utility class for TimeZones values 8 | # 9 | # Example 10 | # BingAdsApi::TimeZone.SANTIAGO 11 | # # => 'Santiago' 12 | # 13 | # Author:: jlopezn@neonline.cl 14 | module TimeZone 15 | 16 | 17 | BingAdsApi::Config.instance.common_constants['time_zones'].each do |key, value| 18 | TimeZone.const_set(key.upcase, value) 19 | end 20 | 21 | end 22 | 23 | # Public : Utility class for AdLanguages values 24 | # 25 | # Example 26 | # BingAdsApi::AdLanguages.SPANISH 27 | # # => 'Spanish' 28 | # BingAdsApi::AdLanguages.SPANISH_CODE 29 | # # => 'ES' 30 | # 31 | # Author:: jlopezn@neonline.cl 32 | module AdLanguage 33 | 34 | BingAdsApi::Config.instance.common_constants['ad_languages'].each do |key, value| 35 | if key == 'codes' 36 | value.each do |code_key, code_value| 37 | AdLanguage.const_set("#{code_key.upcase}_CODE", code_value) 38 | end 39 | else 40 | AdLanguage.const_set(key.upcase, value) 41 | end 42 | end 43 | 44 | end 45 | 46 | 47 | ## Dynamic classes for campaign management constants 48 | BingAdsApi::Config.instance.campaign_management_constants.each do |const_key, const_value| 49 | 50 | const_module = Module.new do 51 | # Dynamically create Constants classes for each value found 52 | const_value.each do |key, value| 53 | self.const_set(key.upcase, value) 54 | end 55 | 56 | end 57 | BingAdsApi.const_set(const_key.camelize, const_module) 58 | 59 | end 60 | 61 | 62 | ## Dynamic classes for customer management constants 63 | BingAdsApi::Config.instance.customer_management_constants.each do |const_key, const_value| 64 | 65 | const_module = Module.new do 66 | # Dynamically create Constants classes for each value found 67 | const_value.each do |key, value| 68 | self.const_set(key.upcase, value) 69 | end 70 | 71 | end 72 | BingAdsApi.const_set(const_key.camelize, const_module) 73 | 74 | end 75 | 76 | 77 | 78 | # Public : Module for Reporting formats 79 | # 80 | # Example 81 | # BingAdsApi::ReportFormat.CSV 82 | # # => 'Csv' 83 | # 84 | # Author:: jlopezn@neonline.cl 85 | module ReportFormat 86 | BingAdsApi::Config.instance.reporting_constants['format'].each do |key, value| 87 | ReportFormat.const_set(key.upcase, value) 88 | end 89 | end 90 | 91 | 92 | # Public : Module for Reporting languages 93 | # 94 | # Example 95 | # BingAdsApi::ReportLanguage.ENGLISH 96 | # # => 'English' 97 | # 98 | # Author:: jlopezn@neonline.cl 99 | module ReportLanguage 100 | BingAdsApi::Config.instance.reporting_constants['language'].each do |key, value| 101 | ReportLanguage.const_set(key.upcase, value) 102 | end 103 | end 104 | 105 | 106 | # Public : Module for Reporting languages 107 | # 108 | # Example 109 | # BingAdsApi::ReportAggregation.SUMMARY 110 | # # => 'Summary' 111 | # BingAdsApi::ReportAggregation.HOURLY 112 | # # => 'Hourly' 113 | # 114 | # Author:: jlopezn@neonline.cl 115 | module ReportAggregation 116 | BingAdsApi::Config.instance.reporting_constants['aggregation'].each do |key, value| 117 | ReportAggregation.const_set(key.upcase, value) 118 | end 119 | end 120 | 121 | 122 | # Public : Module for Reporting languages 123 | # 124 | # Example 125 | # BingAdsApi::ReportTimePeriods.TODAY 126 | # # => 'Today' 127 | # BingAdsApi::ReportTimePeriods.LAST_WEEK 128 | # # => 'LastWeek' 129 | # 130 | # Author:: jlopezn@neonline.cl 131 | module ReportTimePeriods 132 | BingAdsApi::Config.instance.reporting_constants['time_periods'].each do |key, value| 133 | ReportTimePeriods.const_set(key.upcase, value) 134 | end 135 | end 136 | 137 | 138 | ## Dynamic classes for reporting constants 139 | # BingAdsApi::Config.instance.reporting_constants.each do |const_key, const_value| 140 | # 141 | # const_module = Module.new do 142 | # # Dynamically create Constants classes for each value found 143 | # const_value.each do |key, value| 144 | # self.const_set(key.upcase, value) 145 | # end 146 | # 147 | # end 148 | # BingAdsApi.const_set(const_key.camelize, const_module) 149 | # 150 | # end 151 | 152 | end 153 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/ad_group.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Defines an ad group. 6 | # 7 | # Author:: jlopezn@neonline.cl 8 | # 9 | # Examples 10 | # ad_group = BingAdsApi::AdGroup.new( 11 | # :ad_distribution => BingAdsApi::AdGroup::SEARCH, 12 | # :language => BingAdsApi::AdGroup::SPANISH, 13 | # :name => "Ad Group name", 14 | # :pricing_model => BingAdsApi::AdGroup::CPC, 15 | # :bidding_model => BingAdsApi::AdGroup::KEYWORD) 16 | # # => 17 | class AdGroup < BingAdsApi::DataObject 18 | include BingAdsApi::AdDistribution 19 | include BingAdsApi::AdRotationType 20 | include BingAdsApi::PricingModel 21 | include BingAdsApi::BiddingModel 22 | include BingAdsApi::AdLanguage 23 | include BingAdsApi::AdGroupStatus 24 | 25 | 26 | attr_accessor :id, 27 | :ad_distribution, 28 | :ad_rotation, 29 | 30 | :broad_match_bid, 31 | :content_match_bid, 32 | :exact_match_bid, 33 | :phrase_match_bid, 34 | :search_bid, 35 | 36 | :bidding_model, 37 | :pricing_model, 38 | 39 | :language, 40 | :name, 41 | :status, 42 | 43 | :start_date, 44 | :end_date 45 | 46 | 47 | 48 | # Public : Constructor in a ActiveRecord style, with a hash of attributes as input 49 | # 50 | # Author:: jlopezn@neonline.cl 51 | # 52 | # attributes - Hash with the objects attributes 53 | def initialize(attributes={}) 54 | super(attributes) 55 | set_custom_attributes(attributes) 56 | end 57 | 58 | 59 | # Public : Returns this object as a hash to SOAP requests 60 | # This methods is a specialization for the +DataObject#to_hash+ method 61 | # that ads specific hash keys for the AdGroup object 62 | # 63 | # Author:: jlopezn@neonline.cl 64 | # 65 | # keys - specifies the keys case: CamelCase or underscore_case 66 | # 67 | # Returns:: Hash 68 | def to_hash(keys = :underscore) 69 | hash = super(keys) 70 | 71 | amount_key = get_attribute_key("amount", keys) 72 | if self.content_match_bid 73 | #hash.delete(:content_match_bid) 74 | hash[get_attribute_key("content_match_bid", keys)] = {amount_key => self.content_match_bid} 75 | end 76 | 77 | if self.exact_match_bid 78 | hash[get_attribute_key("exact_match_bid", keys)] = {amount_key => self.exact_match_bid} 79 | end 80 | 81 | if self.phrase_match_bid 82 | hash[get_attribute_key("phrase_match_bid", keys)] = {amount_key => self.phrase_match_bid} 83 | end 84 | 85 | if self.broad_match_bid 86 | hash[get_attribute_key("broad_match_bid", keys)] = {amount_key => self.broad_match_bid} 87 | end 88 | 89 | if self.start_date 90 | hash[get_attribute_key("start_date", keys)] = date_to_hash(self.start_date, keys) 91 | end 92 | 93 | if self.end_date 94 | hash[get_attribute_key("end_date", keys)] = date_to_hash(self.end_date, keys) 95 | end 96 | 97 | return hash 98 | end 99 | 100 | 101 | private 102 | def set_custom_attributes(attributes) 103 | self.content_match_bid = attributes[:content_match_bid][:amount] if attributes.key?(:content_match_bid) 104 | self.exact_match_bid = attributes[:exact_match_bid][:amount] if attributes.key?(:exact_match_bid) 105 | self.phrase_match_bid = attributes[:phrase_match_bid][:amount] if attributes.key?(:phrase_match_bid) 106 | self.broad_match_bid = attributes[:broad_match_bid][:amount] if attributes.key?(:broad_match_bid) 107 | 108 | if attributes.key?(:start_date) && !attributes[:start_date].nil? 109 | self.start_date = DateTime.strptime( 110 | "#{attributes[:start_date][:year]}-#{attributes[:start_date][:month]}-#{attributes[:start_date][:day]}", 111 | "%Y-%m-%d") 112 | end 113 | 114 | if attributes.key?(:end_date) && !attributes[:end_date].nil? 115 | self.end_date = DateTime.strptime( 116 | "#{attributes[:end_date][:year]}-#{attributes[:end_date][:month]}-#{attributes[:end_date][:day]}", 117 | "%Y-%m-%d") 118 | end 119 | 120 | end 121 | 122 | 123 | # Internal: Retrieve the ordered array of keys corresponding to this data 124 | # object. 125 | # 126 | # Author: alex.cavalli@offers.com 127 | def get_key_order 128 | super.concat(BingAdsApi::Config.instance. 129 | campaign_management_orders['ad_group']) 130 | end 131 | 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/helpers/filter_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi::Helpers 4 | 5 | ## 6 | # Public : Utility module for filter attribute in +ReportRequest+ derived classes 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | # 10 | module FilterHelper 11 | 12 | include BingAdsApi::SOAPHasheable 13 | 14 | 15 | # Valid filters values for each known criteria 16 | FILTERS_CRITERIA = BingAdsApi::Config.instance. 17 | reporting_constants['filters'] 18 | 19 | 20 | # Internal : Validates the filter attribute at the ReporRequest initialization 21 | # At the first invalid filter criteria or value detected this method raises Exception. 22 | # If all filter criteria and values are ok, this method returns true 23 | # Valid filter criteria are validated against FILTERS_CRITERIA constant 24 | # Valid filter values are validated against FILTERS constant 25 | # 26 | # Author:: jlopezn@neonline.cl 27 | # 28 | # === Parameters 29 | # valid_filters - Hash with the set of valid filter values 30 | # filter - Hash with the filter criteria and values 31 | # 32 | # Returns:: true if filter is valid 33 | # 34 | # Raises:: Exception at the first invalid filter criteria o value 35 | def valid_filter(valid_filters, filter) 36 | if filter && filter.is_a?(Hash) 37 | filter.keys.each do |filter_key| 38 | # validates if filter criteria is recognized 39 | raise Exception.new("Invalid filter criteria '#{filter_key.to_s}'") if !valid_filters.key?(filter_key.to_s) 40 | # validates the filter criteria value 41 | valid_filter_value(filter_key, filter[filter_key]) 42 | end 43 | end 44 | return true 45 | end 46 | 47 | 48 | # Internal : Validates a specific filter criteria and his value 49 | # 50 | # Author:: jlopezn@neonline.cl 51 | # 52 | # key - filter criteria to evaluate 53 | # value - filter criteria to be evaluate 54 | # 55 | # Returns:: true if validation runs ok. Raise exception otherwise 56 | # 57 | # Raises:: Exception if filter value provided is not valid 58 | def valid_filter_value(key, value) 59 | return true if value.nil? 60 | return true if solve_filter_value(key, value) 61 | end 62 | 63 | 64 | # Public : Returns the filter attribute as a Hash to SOAP requests 65 | # 66 | # Author:: jlopezn@neonline.cl 67 | # 68 | # filter - Hash with the filter values 69 | # 70 | # Returns:: Hash 71 | def filter_to_hash(valid_filters, keys_case=:undescore) 72 | hashed_filter = {} 73 | filter.each do |key, value| 74 | hashed_filter[get_attribute_key(key, keys_case)] = solve_filter_value(key, value) 75 | end 76 | return hashed_filter 77 | end 78 | 79 | 80 | # Internal:: Solves the Bing value for the given filter attribute 81 | # 82 | # Author:: jlopezn@neonline.cl 83 | # 84 | # === Parameters 85 | # * +filter_criteria+ - String or symbol with the filter attribute to be solved 86 | # 87 | # === Examples 88 | # solve_filter_value(:ad_distribution, :search) 89 | # # => "Search" 90 | # 91 | # solve_filter_value(:ad_distribution, :other) 92 | # # => Exception "Invalid filter name 'other' for 'ad_distribution' criteria" 93 | # 94 | # solve_filter_value(:ad_distribution, "Search") 95 | # # => "Search" 96 | # 97 | # solve_filter_value(:ad_distribution, "Other") 98 | # # => Exception "Invalid filter value 'Other' for 'ad_distribution' criteria" 99 | # 100 | # Returns:: String with the Bing value for the filter criteria. 101 | # 102 | # Raises:: Exception if the filter's criteria or value are unknown 103 | def solve_filter_value(filter_criteria, filter_value) 104 | filter_criteria_values = FILTERS_CRITERIA[filter_criteria.to_s] 105 | if filter_value.is_a?(String) 106 | if filter_criteria_values.value?(filter_value) 107 | return filter_value 108 | else 109 | raise Exception.new("Invalid filter value '#{filter_value}' for '#{filter_criteria}' criteria") 110 | end 111 | elsif filter_value.is_a?(Symbol) 112 | if filter_criteria_values.key?(filter_value.to_s) 113 | return filter_criteria_values[filter_value.to_s] 114 | else 115 | raise Exception.new("Invalid filter name '#{filter_value}' for '#{filter_criteria}' criteria") 116 | end 117 | elsif filter_value.is_a?(Array) 118 | if filter_value.first.is_a?(Numeric) 119 | filter_value = filter_value.map { |f| {"ins0:int" => f } } 120 | elsif filter_value.first.is_a?(String) 121 | filter_value = filter_value.map { |f| {"ins0:string" => f } } 122 | end 123 | return filter_value 124 | end 125 | return nil 126 | end 127 | 128 | 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/ad.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object of an ad. 7 | # Do not instantiate this object. Instead you can instantiate the 8 | # BingAdsApi::TextAd, BingAdsApi::MobileAd, or BingAdsApi::ProductAd 9 | # object that derives from this object. 10 | # 11 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-ad.aspx 12 | # 13 | # Author:: jlopezn@neonline.cl 14 | # 15 | class Ad < BingAdsApi::DataObject 16 | include BingAdsApi::AdEditorialStatus 17 | include BingAdsApi::AdStatus 18 | include BingAdsApi::AdType 19 | 20 | 21 | attr_accessor :id, 22 | :device_preference, 23 | :editorial_status, 24 | :status, 25 | :type, 26 | :final_urls, 27 | :text, 28 | :title_part_1, 29 | :title_part_2 30 | 31 | private 32 | 33 | # Internal: Retrieve the ordered array of keys corresponding to this data 34 | # object. 35 | # 36 | # Author: alex.cavalli@offers.com 37 | def get_key_order 38 | super.concat(BingAdsApi::Config.instance. 39 | campaign_management_orders['ad']) 40 | end 41 | 42 | end 43 | 44 | ## 45 | # Public : Defines a text ad. 46 | # 47 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-textad.aspx 48 | # 49 | # Author:: jlopezn@neonline.cl 50 | # 51 | class TextAd < BingAdsApi::Ad 52 | 53 | attr_accessor :destination_url, 54 | :display_url, 55 | :text, 56 | :title 57 | 58 | # Public : Specification of DataObject#to_hash method that ads the type attribute based on this specific class 59 | # 60 | # Author:: jlopezn@neonline.cl 61 | # 62 | # keys - specifies the keys case 63 | # 64 | # Returns:: Hash 65 | def to_hash(keys = :underscore) 66 | hash = super(keys) 67 | hash[:'@xsi:type'] = "#{ClientProxy::NAMESPACE}:TextAd" 68 | return hash 69 | end 70 | 71 | private 72 | 73 | # Internal: Retrieve the ordered array of keys corresponding to this data 74 | # object. 75 | # 76 | # Author: alex.cavalli@offers.com 77 | def get_key_order 78 | super.concat(BingAdsApi::Config.instance. 79 | campaign_management_orders['text_ad']) 80 | end 81 | 82 | end 83 | 84 | ## 85 | # Public : Defines expanded text ad. 86 | # 87 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-textad.aspx 88 | # 89 | # Author:: dmitrii@webstreak.com 90 | # 91 | class ExpandedTextAd < BingAdsApi::Ad 92 | 93 | attr_accessor :final_urls, 94 | :text, 95 | :title_part_1, 96 | :title_part_2, 97 | :path_1, 98 | :path_2 99 | 100 | # Public : Specification of DataObject#to_hash method that ads the type attribute based on this specific class 101 | # 102 | # Author:: jlopezn@neonline.cl 103 | # 104 | # keys - specifies the keys case 105 | # 106 | # Returns:: Hash 107 | def to_hash(keys = :underscore) 108 | hash = super(keys) 109 | hash[:'@xsi:type'] = "#{ClientProxy::NAMESPACE}:ExpandedTextAd" 110 | new_hash = {} 111 | hash['FinalUrls'].each_pair do |k,v| 112 | new_hash.merge!({k.downcase => v}) 113 | end 114 | hash['FinalUrls'] = new_hash 115 | return hash 116 | end 117 | 118 | private 119 | 120 | # Internal: Retrieve the ordered array of keys corresponding to this data 121 | # object. 122 | # 123 | # Author: alex.cavalli@offers.com 124 | def get_key_order 125 | super.concat(BingAdsApi::Config.instance. 126 | campaign_management_orders['expanded_text_ad']) 127 | end 128 | 129 | end 130 | 131 | 132 | 133 | 134 | ## 135 | # Public : Defines a product ad. 136 | # 137 | # Reference: http://msdn.microsoft.com/en-US/library/bing-ads-productad-campaign-management.aspx 138 | # 139 | # Author:: jlopezn@neonline.cl 140 | # 141 | class ProductAd < BingAdsApi::Ad 142 | 143 | attr_accessor :promotional_text 144 | 145 | # Public : Specification of DataObject#to_hash method that ads the type attribute based on this specific class 146 | # 147 | # Author:: jlopezn@neonline.cl 148 | # 149 | # keys - specifies the keys case 150 | # 151 | # Returns:: Hash 152 | def to_hash(keys = :underscore) 153 | hash = super(keys) 154 | hash[:'@xsi:type'] = "#{ClientProxy::NAMESPACE}:ProductAd" 155 | return hash 156 | end 157 | 158 | private 159 | 160 | # Internal: Retrieve the ordered array of keys corresponding to this data 161 | # object. 162 | # 163 | # Author: alex.cavalli@offers.com 164 | def get_key_order 165 | super.concat(BingAdsApi::Config.instance. 166 | campaign_management_orders['product_ad']) 167 | end 168 | 169 | end 170 | 171 | end 172 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/share_of_voice_report_request.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | ## 5 | # Public: Reporting service request object for a share of voice report. 6 | # 7 | # Author:: colin.knox@offers.com 8 | # 9 | class ShareOfVoiceReportRequest < BingAdsApi::PerformanceReportRequest 10 | # Valid Columns for this report request 11 | COLUMNS = BingAdsApi::Config.instance. 12 | reporting_constants['share_of_voice_report']['columns'] 13 | 14 | # Valid Filters for this report request 15 | FILTERS = BingAdsApi::Config.instance. 16 | reporting_constants['share_of_voice_report']['filter'] 17 | 18 | attr_accessor :max_rows, :sort 19 | 20 | # Public : Constructor. Adds a validations for the columns and filter 21 | # attributes 22 | # 23 | # Author:: colin.knox@offers.com 24 | # 25 | # === Parameters 26 | # * +attributes+ - Hash with the report request attributes 27 | # 28 | # === Example 29 | # 30 | # request = BingAdsApi::ShareOfVoiceReportRequest.new( 31 | # :format => :xml, 32 | # :language => :english, 33 | # :report_name => "Me report", 34 | # :aggregation => :hourly, 35 | # :columns => [:account_name, :account_number, :time_period], 36 | # # The filter is specified as a hash 37 | # :filter => { 38 | # # specifies the Bing expected String value 39 | # :ad_distribution => "Search", 40 | # :ad_type => "Text", 41 | # :bid_match_type => "Exact", 42 | # :delivered_match_type => "Exact", 43 | # # specifies criteria as a snake case symbol 44 | # :device_type => :tablet, 45 | # :keyword_relevance => [3], 46 | # :keywords => ["bing ads"], 47 | # :language_code => ["EN"]}, 48 | # :max_rows => 10, 49 | # :scope => { 50 | # :account_ids => [123456, 234567], 51 | # :campaigns => [], 52 | # :ad_groups => [] }, 53 | # :sort => [] 54 | # # predefined date 55 | # :time => :this_week) 56 | def initialize(attributes={}) 57 | raise Exception.new("Invalid columns") if !valid_columns(COLUMNS, attributes[:columns]) 58 | raise Exception.new("Invalid filters") if !valid_filter(FILTERS, attributes[:filter]) 59 | super(attributes) 60 | end 61 | 62 | 63 | # Public:: Returns the object as a Hash valid for SOAP requests 64 | # 65 | # Author:: jlopezn@neonline.cl 66 | # 67 | # === Parameters 68 | # * +keys_case+ - case for the hashes keys: underscore or camelcase 69 | # 70 | # Returns:: Hash 71 | def to_hash(keys = :underscore) 72 | hash = super(keys) 73 | hash[get_attribute_key('columns', keys)] = 74 | columns_to_hash(COLUMNS, columns, keys) 75 | hash[get_attribute_key('filter', keys)] = 76 | filter_to_hash(FILTERS, keys) 77 | hash[get_attribute_key('scope', keys)] = scope_to_hash(keys) 78 | hash[get_attribute_key('max_rows', keys)] = max_rows if max_rows 79 | hash[get_attribute_key('sort', keys)] = sort_to_hash(keys) if sort 80 | hash["@xsi:type"] = type_attribute_for_soap 81 | return hash 82 | end 83 | 84 | 85 | private 86 | 87 | # Internal:: Returns the scope attribute as a hash for the SOAP request 88 | # 89 | # Author:: alex.cavalli@offers.com 90 | # 91 | # === Parameters 92 | # * +keys_case+ - case for the hash: underscore or camelcase 93 | # 94 | # Returns:: Hash 95 | def scope_to_hash(keys_case=:underscore) 96 | return { 97 | get_attribute_key('account_ids', keys_case) => 98 | {"ins0:long" => object_to_hash(scope[:account_ids], keys_case) }, 99 | get_attribute_key('ad_group', keys_case) => 100 | { "AdGroupReportScope" => object_to_hash(scope[:ad_groups], keys_case) }, 101 | get_attribute_key('campaigns', keys_case) => 102 | { "CampaignReportScope" => object_to_hash(scope[:campaigns], keys_case) } 103 | } 104 | end 105 | 106 | 107 | # Internal:: Returns the sort attribute as an array for the SOAP request 108 | # 109 | # Author:: alex.cavalli@offers.com 110 | # 111 | # === Parameters 112 | # * +keys_case+ - case for the hash: underscore or camelcase 113 | # 114 | # Returns:: Array 115 | def sort_to_hash(keys_case=:underscore) 116 | sort.map { |sort_object| object_to_hash(sort_object, keys_case) } 117 | end 118 | 119 | 120 | # Internal:: Returns a string with type attribute for the ReportRequest SOAP tag 121 | # 122 | # Author:: jlopezn@neonline.cl 123 | # 124 | # Returns:: "v9:ShareOfVoiceReportRequest" 125 | def type_attribute_for_soap 126 | return BingAdsApi::ClientProxy::NAMESPACE.to_s + ":" + 127 | BingAdsApi::Config.instance. 128 | reporting_constants['share_of_voice_report']['type'] 129 | end 130 | 131 | end 132 | 133 | end 134 | -------------------------------------------------------------------------------- /spec/bulk_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | require 'tempfile' 4 | 5 | # Author:: alex.cavalli@offers.com 6 | describe BingAdsApi::Bulk do 7 | 8 | def generate_bulk_upload_file 9 | raise ArgumentError, "Block is required" unless block_given? 10 | Tempfile.open(["bulk_upload", ".csv"]) do |tempfile| 11 | tempfile.binmode 12 | tempfile << "Type,Status,Id,Parent Id,Campaign,Sync Time,Time Zone,Budget,Budget Type,Name,Campaign Type\n" 13 | tempfile << "Format Version,,,,,,,,,4.0,\n" 14 | tempfile << "Account,,#{default_options[:account_id]},21025739,,,,,,,\n" 15 | tempfile << "Campaign,Active,,#{default_options[:account_id]},Test Campaign #{SecureRandom.uuid},,Santiago,2000,DailyBudgetStandard,,SearchAndContent\n" 16 | tempfile << "Campaign,Active,,#{default_options[:account_id]},Test Campaign #{SecureRandom.uuid},,Santiago,2000,DailyBudgetStandard,,SearchAndContent\n" 17 | tempfile.rewind 18 | yield tempfile.path 19 | end 20 | end 21 | 22 | let(:default_options) do 23 | { 24 | environment: :sandbox, 25 | username: "ruby_bing_ads_sbx", 26 | password: "sandbox123", 27 | developer_token: "BBD37VB98", 28 | customer_id: "21025739", 29 | account_id: "8506945" 30 | } 31 | end 32 | let(:service) { BingAdsApi::Bulk.new(default_options) } 33 | 34 | it "should initialize with options" do 35 | new_service = BingAdsApi::Bulk.new(default_options) 36 | expect(new_service).not_to be_nil 37 | end 38 | 39 | it "should download campaigns by account id" do 40 | campaign_id = BingAdsFactory.create_campaign 41 | account_ids = [default_options[:account_id]] 42 | entities = [:campaigns, :ad_groups, :keywords, :ads] 43 | options = { 44 | data_scope: :entity_performance_data, 45 | download_file_type: :csv, 46 | format_version: 4.0, 47 | last_sync_time_in_utc: "2001-10-26T21:32:52", 48 | location_target_version: "Latest", 49 | performance_stats_date_range: { 50 | custom_date_range_end: {day: 31, month: 12, year: 2013}, 51 | custom_date_range_start: {day: 1, month: 12, year: 2013} 52 | } 53 | } 54 | 55 | download_request_id = nil 56 | expect{ 57 | download_request_id = service.download_campaigns_by_account_ids( 58 | account_ids, 59 | entities, 60 | options 61 | ) 62 | }.not_to raise_error 63 | 64 | expect(download_request_id).not_to be_nil 65 | end 66 | 67 | context "when a bulk request has been requested" do 68 | 69 | before :each do 70 | BingAdsFactory.create_campaign 71 | @download_request_id = service.download_campaigns_by_account_ids( 72 | [default_options[:account_id]], 73 | [:campaigns, :ad_groups, :keywords, :ads] 74 | ) 75 | end 76 | 77 | it "should successfully get detailed response status" do 78 | bulk_download_status = nil 79 | expect{ 80 | bulk_download_status = service.get_bulk_download_status(@download_request_id) 81 | }.not_to raise_error 82 | 83 | expect(bulk_download_status).not_to be_nil 84 | 85 | expect(bulk_download_status.failed?).to eq(false) 86 | end 87 | 88 | end 89 | 90 | it "should retrieve a bulk upload URL and request ID" do 91 | account_id = default_options[:account_id] 92 | options = {response_mode: :errors_and_results} 93 | 94 | upload_request = nil 95 | expect{ 96 | upload_request = service.get_bulk_upload_url( 97 | account_id, 98 | options 99 | ) 100 | }.not_to raise_error 101 | 102 | expect(upload_request[:request_id]).not_to be_nil 103 | expect(upload_request[:upload_url]).not_to be_nil 104 | end 105 | 106 | it "should submit a bulk upload file and return the request ID" do 107 | account_id = default_options[:account_id] 108 | options = {response_mode: :errors_and_results} 109 | 110 | upload_request_id = nil 111 | generate_bulk_upload_file do |upload_file| 112 | expect{ 113 | upload_request_id = service.submit_bulk_upload_file( 114 | upload_file, 115 | account_id, 116 | options 117 | ) 118 | }.not_to raise_error 119 | end 120 | 121 | expect(upload_request_id).not_to be_nil 122 | end 123 | 124 | context "when a bulk request has been requested" do 125 | it "should download result file" do 126 | account_id = default_options[:account_id] 127 | options = {response_mode: :errors_and_results} 128 | 129 | upload_request_id = nil 130 | generate_bulk_upload_file do |upload_file| 131 | upload_request_id = service.submit_bulk_upload_file( 132 | upload_file, 133 | account_id, 134 | options 135 | ) 136 | end 137 | 138 | tries = 0 139 | loop do 140 | upload_status = service.get_bulk_upload_status(upload_request_id) 141 | if upload_status.failed? || upload_status.pending_file_upload? || tries == 5 142 | raise "Upload failed with status: #{upload_status.request_status}. Tried #{tries} times." 143 | end 144 | 145 | downloaded = upload_status.download_result_file(__FILE__) 146 | if downloaded 147 | # clean up downloaded file 148 | FileUtils.rm(downloaded) 149 | break # success 150 | end 151 | tries += 1 152 | sleep(1) 153 | end 154 | end 155 | end 156 | 157 | end 158 | -------------------------------------------------------------------------------- /lib/bing-ads-api/client_proxy.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : ClientProxy es un objeto para encapsular la conexión y request 6 | # de servicios a la API de Bing. En su inicialización requiere los datos 7 | # de autenticación y el WSDL al servicio que se requiere. 8 | # 9 | # Author:: jlopezn@neonline.cl 10 | # 11 | # Examples 12 | # # => hash con datos autenticación y WSDL 13 | # options = { 14 | # :username => "username", 15 | # :password => "password", 16 | # :developer_token => "THE_TOKEN", 17 | # :customer_id => "123456", 18 | # :account_id => "123456", 19 | # :wsdl_url => "https://api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v9/CampaignManagementService.svc?singleWsdl" 20 | # } 21 | # # => Instancia de ClientProxy 22 | # client = BingAdsApi::ClientProxy.new(options) 23 | # # => Llamada a servicio 'GetCampaignsByAccountId' 24 | # response = client.service.call(:get_campaigns_by_account_id, 25 | # message: { Account_id: client.account_id}) 26 | class ClientProxy 27 | 28 | # Public : Namespace para atributos bing. Hace referencia a la versión de API usada 29 | NAMESPACE = :v11 30 | 31 | # Public : Case empleado los nombres de atributos en los XML 32 | KEYS_CASE = :camelcase 33 | 34 | 35 | # Atributos del client proxy 36 | attr_accessor :username, :password, :developer_token, :authentication_token, :wsdl_url, :account_id, :customer_id, :service, :namespace 37 | 38 | # Public : Constructor 39 | # 40 | # Author:: jlopezn@neonline.cl 41 | # 42 | # === Parameters 43 | # options - Hash con valores de autenticación y WSDL 44 | # 45 | # 46 | # === Options 47 | # * username - Bing Ads username 48 | # * passwrod - Bing Ads user's sign-in password 49 | # * developer_token - client application's developer access token 50 | # * customer_id - identifier for the customer that owns the account 51 | # * account_id - identifier of the account that own the entities in the request 52 | # * wsdl_url - URL for the WSDL to be called 53 | # * proxy - Hash with any Savon Client additional options (such as header, logger or enconding) 54 | # 55 | # === Examples 56 | # options = { 57 | # :username => "username", 58 | # :password => "password", 59 | # :developer_token => "THE_TOKEN", 60 | # :customer_id => "123456", 61 | # :account_id => "123456", 62 | # :wsdl_url => "https://api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v9/CampaignManagementService.svc?singleWsdl" 63 | # } 64 | # # => Instancia de ClientProxy 65 | # client = BingAdsApi::ClientProxy.new(options) 66 | # 67 | # Returns:: ClientProxy instance 68 | def initialize(options=nil) 69 | if options 70 | if options[:authentication_token] 71 | @authentication_token ||= options[:authentication_token] 72 | else 73 | @username ||= options[:username] 74 | @password ||= options[:password] 75 | end 76 | @developer_token ||= options[:developer_token] 77 | @wsdl_url ||= options[:wsdl_url] 78 | @account_id ||= options[:account_id] 79 | @customer_id ||= options[:customer_id] 80 | @namespace ||= options[:namespace] 81 | end 82 | self.service = get_proxy(options[:proxy]) 83 | end 84 | 85 | 86 | # Public : Delegate for Savon::Client.call method 87 | # 88 | # Author:: jlopezn@neonline.cl 89 | # 90 | # === Parameters 91 | # service_name - Service to be called 92 | # message - Message for the service 93 | # options - Additional options for the service 94 | # 95 | # === Examples 96 | # client.call_service(:some_service_name, {key: value}) 97 | # # => 98 | # 99 | # Returns:: Response from the Savon::Client 100 | # Raises:: Savon::SOAPFault Savon::HTTPError Savon::InvalidResponseError 101 | def call(service_name, message, options={}) 102 | self.service.call(service_name, message) 103 | end 104 | 105 | 106 | private 107 | # Internal : Wrapper for Savon client instances 108 | # 109 | # Author:: jlopezn@neonline.cl 110 | # 111 | # Examples 112 | # get_proxy 113 | # # => 114 | # 115 | # Returns:: Savon client instance 116 | def get_proxy(client_settings) 117 | settings = { 118 | convert_request_keys_to: KEYS_CASE, 119 | wsdl: self.wsdl_url, 120 | namespace_identifier: NAMESPACE, 121 | soap_header: build_headers 122 | } 123 | settings.merge!(client_settings) if client_settings 124 | 125 | return Savon.client(settings) 126 | end 127 | 128 | 129 | def build_headers 130 | headers = { 131 | "#{NAMESPACE.to_s}:CustomerAccountId" => self.account_id, 132 | "#{NAMESPACE.to_s}:CustomerId" => self.customer_id, 133 | "#{NAMESPACE.to_s}:DeveloperToken" => self.developer_token, 134 | } 135 | if self.authentication_token 136 | headers["#{NAMESPACE.to_s}:AuthenticationToken"] = self.authentication_token 137 | else 138 | headers["#{NAMESPACE.to_s}:UserName"] = self.username 139 | headers["#{NAMESPACE.to_s}:Password"] = self.password 140 | end 141 | return headers 142 | end 143 | end 144 | 145 | end 146 | -------------------------------------------------------------------------------- /lib/bing-ads-api/soap_hasheable.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public:: Module to define an object as Hasheable for the SOAP requests 7 | # 8 | # Author:: jlopezn@neonline.cl 9 | module SOAPHasheable 10 | 11 | 12 | # Internal: Metodo custom para transformar a hash un objeto. 13 | # Se puede indicar si se desean las keys en formato CamelCase o underscore_case 14 | # 15 | # Author:: asaavedrab@neonline.cl 16 | # 17 | # === Parameters 18 | # * keys_case - indica si las keys del hash deben estar en formato 19 | # ** :camelcase - CamelCase 20 | # ** :underscore - underscore_case 21 | # 22 | # === Example 23 | # a=BusinessPartner.new 24 | # a.to_hash 25 | # # => {id => 1, name => "emol"} 26 | # 27 | # Returns:: Hash 28 | def to_hash(keys_case=:underscore) 29 | object_to_hash(self, keys_case) 30 | end 31 | 32 | 33 | # Internal: Internal method to return an object as a SOAP Request alike hash 34 | # If the object is an array, this methods executes object_to_hash for each item. 35 | # If the object is a Hash, this methods normalize the keys according to +keys_case+ and executes object_to_hash for each value 36 | # If the object is a String or Numeric, returns the object 37 | # 38 | # Author:: asaavedrab@neonline.cl, jlopezn@neonline.cl 39 | # 40 | # === Parameters 41 | # * +object+ - object instance to be hashed 42 | # * +keys_case+ - specifies the hash keys case, default 'underscore' 43 | # ==== keys_case 44 | # * :camelcase - CamelCase 45 | # * :underscore - underscore_case 46 | # 47 | # === Example: 48 | # a=Person.new 49 | # a.to_hash(:underscore) 50 | # # => {'id' => 1, 'full_name' => "John Doe"} 51 | # 52 | # a=Person.new 53 | # a.to_hash(:camelcase) 54 | # # => {'Id' => 1, 'FullName' => "John Doe"} 55 | # 56 | # a=[, , ] 57 | # a.to_hash(:underscore) 58 | # # => [{'id' => 1, 'full_name' => "John Doe"}, {'id' => 2, 'full_name' => "Ms Mary"}, {'id' => 3, 'name' => "Mr Whiskers", 'color' => "Gray"}] 59 | # 60 | # Returns:: Hash 61 | def object_to_hash(object, keys_case=:underscore) 62 | 63 | # Nil safe 64 | return nil if object.nil? 65 | 66 | # In case of hash, we only normalize the keys values 67 | return normalize_hash_keys(object, keys_case) if object.is_a?(Hash) 68 | # In case of array, we make hasheable each element 69 | return object.collect{ |item| object_to_hash(item, keys_case) } if object.is_a?(Array) 70 | # In case of number or string, this methods return the object 71 | return object if object.is_a?(String) || object.is_a?(Numeric) 72 | 73 | hash={} 74 | object.instance_variables.each do |var| 75 | if !object.instance_variable_get(var).nil? 76 | value = object.instance_variable_get(var) 77 | hashed_value = case value 78 | when Hash then normalize_hash_keys(value, keys_case) 79 | when Array then value.collect{ |item| object_to_hash(item, keys_case) } 80 | when BingAdsApi::DataObject then value.to_hash 81 | else value 82 | end 83 | hash[get_attribute_key(var, keys_case)] = hashed_value 84 | end 85 | end 86 | return hash 87 | end 88 | 89 | 90 | # Public:: Normalize the keys of a hash with the case specified 91 | # 92 | # Author:: jlopexn@neonline.cl 93 | # 94 | # === Parameters 95 | # * +hash+ - Hash to be normalized 96 | # * +keys_case+ - :underscore or :camelcase 97 | # 98 | # === Examples 99 | # normalize_hash_keys({:some_key => value1}, :camelcase) 100 | # # => {"SomeKey" => value1} 101 | # 102 | # normalize_hash_keys({:some_key => value1}, :underscore) 103 | # # => {"some_key" => value1} 104 | # 105 | # Returns:: Hash 106 | def normalize_hash_keys(hash, keys_case) 107 | return hash.inject({}) { |h, (k, v)| h[get_attribute_key(k, keys_case)] = object_to_hash(v, keys_case); h } 108 | end 109 | 110 | 111 | # Internal : Helper method to determinate the key name in the hash for the SOAP request 112 | # 113 | # Author:: jlopezn@neonline.cl 114 | # 115 | # === Parameters 116 | # * attribute - the attribute name 117 | # * keys_case - defines the case for the attribute name. 118 | # ==== keys_case 119 | # * :camelcase - CamelCase 120 | # * :underscore - underscore_case 121 | # 122 | # === Examples 123 | # get_attribute_key("attribute_name", :underscore) 124 | # # => "attribute_name" 125 | # 126 | # get_attribute_key("name", :camelcase) 127 | # # => "AttributeName" 128 | # 129 | # Returns:: String with the attribute name for the key in the hash 130 | def get_attribute_key(attribute, keys_case = :underscore) 131 | if keys_case == :underscore 132 | return attribute.to_s.delete("@").underscore 133 | elsif keys_case == :camelcase 134 | return attribute.to_s.delete("@").camelcase 135 | end 136 | end 137 | 138 | 139 | # Internal : Returns a DateTime as a hash for SOAP requests 140 | # 141 | # Author:: jlopezn@neonline.cl 142 | # 143 | # === Parameters 144 | # * date - DateTime to be hashed 145 | # * keys_case - defines the case for keys in the hash 146 | # ==== keys_case 147 | # * :camelcase - CamelCase 148 | # * :underscore - underscore_case 149 | # 150 | # Returns:: Hash with the :year, :month, :day keys 151 | def date_to_hash(date, keys_case) 152 | { 153 | get_attribute_key("day", keys_case) => date.day, 154 | get_attribute_key("month", keys_case) => date.month, 155 | get_attribute_key("year", keys_case) => date.year 156 | } 157 | end 158 | 159 | end 160 | 161 | end 162 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/keyword_performance_report_request.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public: Reporting service request object for a keyword performance report. 7 | # 8 | # Author:: alex.cavalli@offers.com 9 | # 10 | # === Usage 11 | # 12 | # request = BingAdsApi::KeywordPerformanceReportRequest.new( 13 | # :format => :xml, 14 | # :language => :english, 15 | # :report_name => "Me report", 16 | # :aggregation => :hourly, 17 | # :columns => [:account_name, :account_number, :time_period], 18 | # # The filter is specified as a hash 19 | # :filter => { 20 | # # specifies the Bing expected String value 21 | # :ad_distribution => "Search", 22 | # :ad_type => "Text", 23 | # :bid_match_type => "Exact", 24 | # :delivered_match_type => "Exact", 25 | # # specifies criteria as a snake case symbol 26 | # :device_type => :tablet, 27 | # :keyword_relevance => [3], 28 | # :keywords => ["bing ads"], 29 | # :landing_page_relevance => [2], 30 | # :landing_page_user_experience => [2], 31 | # :language_code => ["EN"], 32 | # :quality_score => [7,8,9,10] }, 33 | # :max_rows => 10, 34 | # :scope => { 35 | # :account_ids => [123456, 234567], 36 | # :campaigns => [], 37 | # :ad_groups => [] }, 38 | # :sort => [] 39 | # # predefined date 40 | # :time => :this_week) 41 | class KeywordPerformanceReportRequest < BingAdsApi::PerformanceReportRequest 42 | 43 | # Valid Columns for this report request 44 | COLUMNS = BingAdsApi::Config.instance. 45 | reporting_constants['keyword_performance_report']['columns'] 46 | 47 | # Valid Filters for this report request 48 | FILTERS = BingAdsApi::Config.instance. 49 | reporting_constants['keyword_performance_report']['filter'] 50 | 51 | attr_accessor :max_rows, :sort 52 | 53 | # Public : Constructor. Adds a validations for the columns and filter 54 | # attributes 55 | # 56 | # Author:: alex.cavalli@offers.com 57 | # 58 | # === Parameters 59 | # * +attributes+ - Hash with the report request attributes 60 | # 61 | # === Example 62 | # 63 | # request = BingAdsApi::KeywordPerformanceReportRequest.new( 64 | # :format => :xml, 65 | # :language => :english, 66 | # :report_name => "Me report", 67 | # :aggregation => :hourly, 68 | # :columns => [:account_name, :account_number, :time_period], 69 | # # The filter is specified as a hash 70 | # :filter => { 71 | # # specifies the Bing expected String value 72 | # :ad_distribution => "Search", 73 | # :ad_type => "Text", 74 | # :bid_match_type => "Exact", 75 | # :delivered_match_type => "Exact", 76 | # # specifies criteria as a snake case symbol 77 | # :device_type => :tablet, 78 | # :keyword_relevance => [3], 79 | # :landing_page_relevance => [2], 80 | # :landing_page_user_experience => [2], 81 | # :language_code => ["EN"], 82 | # :quality_score => [7,8,9,10] }, 83 | # :max_rows => 10, 84 | # :scope => { 85 | # :account_ids => [123456, 234567], 86 | # :campaigns => [], 87 | # :ad_groups => [] }, 88 | # :sort => [] 89 | # # predefined date 90 | # :time => :this_week) 91 | def initialize(attributes={}) 92 | raise Exception.new("Invalid columns") if !valid_columns(COLUMNS, attributes[:columns]) 93 | raise Exception.new("Invalid filters") if !valid_filter(FILTERS, attributes[:filter]) 94 | super(attributes) 95 | end 96 | 97 | 98 | # Public:: Returns the object as a Hash valid for SOAP requests 99 | # 100 | # Author:: jlopezn@neonline.cl 101 | # 102 | # === Parameters 103 | # * +keys_case+ - case for the hashes keys: underscore or camelcase 104 | # 105 | # Returns:: Hash 106 | def to_hash(keys = :underscore) 107 | hash = super(keys) 108 | hash[get_attribute_key('columns', keys)] = 109 | columns_to_hash(COLUMNS, columns, keys) 110 | hash[get_attribute_key('filter', keys)] = 111 | filter_to_hash(FILTERS, keys) 112 | hash[get_attribute_key('scope', keys)] = scope_to_hash(keys) 113 | hash[get_attribute_key('max_rows', keys)] = max_rows if max_rows 114 | hash[get_attribute_key('sort', keys)] = sort_to_hash(keys) if sort 115 | hash["@xsi:type"] = type_attribute_for_soap 116 | return hash 117 | end 118 | 119 | 120 | private 121 | 122 | # Internal:: Returns the scope attribute as a hash for the SOAP request 123 | # 124 | # Author:: alex.cavalli@offers.com 125 | # 126 | # === Parameters 127 | # * +keys_case+ - case for the hash: underscore or camelcase 128 | # 129 | # Returns:: Hash 130 | def scope_to_hash(keys_case=:underscore) 131 | return { 132 | get_attribute_key('account_ids', keys_case) => 133 | {"ins0:long" => object_to_hash(scope[:account_ids], keys_case) }, 134 | get_attribute_key('ad_group', keys_case) => 135 | { "AdGroupReportScope" => object_to_hash(scope[:ad_groups], keys_case) }, 136 | get_attribute_key('campaigns', keys_case) => 137 | { "CampaignReportScope" => object_to_hash(scope[:campaigns], keys_case) } 138 | } 139 | end 140 | 141 | 142 | # Internal:: Returns the sort attribute as an array for the SOAP request 143 | # 144 | # Author:: alex.cavalli@offers.com 145 | # 146 | # === Parameters 147 | # * +keys_case+ - case for the hash: underscore or camelcase 148 | # 149 | # Returns:: Array 150 | def sort_to_hash(keys_case=:underscore) 151 | sort.map { |sort_object| object_to_hash(sort_object, keys_case) } 152 | end 153 | 154 | 155 | # Internal:: Returns a string with type attribute for the ReportRequest SOAP tag 156 | # 157 | # Author:: jlopezn@neonline.cl 158 | # 159 | # Returns:: "v9:KeywordPerformanceReportRequest" 160 | def type_attribute_for_soap 161 | return BingAdsApi::ClientProxy::NAMESPACE.to_s + ":" + 162 | BingAdsApi::Config.instance. 163 | reporting_constants['keyword_performance_report']['type'] 164 | end 165 | 166 | end 167 | 168 | end 169 | -------------------------------------------------------------------------------- /spec/reporting_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Author:: jlopezn@neonline.cl 5 | describe BingAdsApi::Reporting do 6 | 7 | let(:default_options) do 8 | { 9 | environment: :sandbox, 10 | username: "ruby_bing_ads_sbx", 11 | password: "sandbox123", 12 | developer_token: "BBD37VB98", 13 | customer_id: "21025739", 14 | account_id: "8506945" 15 | } 16 | end 17 | let(:service) { BingAdsApi::Reporting.new(default_options) } 18 | 19 | it "should initialize with options" do 20 | new_service = BingAdsApi::Reporting.new(default_options) 21 | expect(new_service).not_to be_nil 22 | end 23 | 24 | it "should submit campaign performance report" do 25 | campaign_id = BingAdsFactory.create_campaign 26 | report_request = BingAdsApi::CampaignPerformanceReportRequest.new( 27 | :format => :xml, 28 | :language => :english, 29 | :report_name => "My Report", 30 | :aggregation => :hourly, 31 | :columns => [ 32 | :account_name, :account_number, :time_period, 33 | :campaign_name, :campaign_id, :status, :currency_code, 34 | :impressions, :clicks, :ctr, :average_cpc, :spend, 35 | :conversions, :conversion_rate, :cost_per_conversion 36 | ], 37 | :filter => { 38 | # String as bing expected 39 | :ad_distribution => "Search", 40 | :device_os => "Windows", 41 | # snake case symbol 42 | :device_type => :computer, 43 | # nil criteria 44 | :status => nil 45 | }, 46 | :scope => { 47 | :account_ids => default_options[:account_id], 48 | :campaigns => [ 49 | {:account_id => default_options[:account_id], :campaign_id => campaign_id} 50 | ] 51 | }, 52 | :time => { 53 | :custom_date_range_end => {:day => 31, :month => 12, :year => 2013}, 54 | :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 55 | } 56 | ) 57 | 58 | report_request_id = nil 59 | expect{ 60 | report_request_id = service.submit_generate_report(report_request) 61 | }.not_to raise_error 62 | 63 | expect(report_request_id).not_to be_nil 64 | end 65 | 66 | it "should submit keyword performance report" do 67 | campaign_id = BingAdsFactory.create_campaign 68 | report_request = BingAdsApi::KeywordPerformanceReportRequest.new( 69 | :format => :xml, 70 | :language => :english, 71 | :report_name => "Me report", 72 | :aggregation => :hourly, 73 | :columns => [:account_name, :account_number, :time_period, :keyword, :spend], 74 | # The filter is specified as a hash 75 | :filter => { 76 | # specifies the Bing expected String value 77 | :ad_distribution => "Search", 78 | :ad_type => "Text", 79 | :bid_match_type => "Exact", 80 | :delivered_match_type => "Exact", 81 | # specifies criteria as a snake case symbol 82 | :device_type => :tablet, 83 | :keyword_relevance => [3], 84 | :landing_page_relevance => [2], 85 | :landing_page_user_experience => [2], 86 | :language_code => ["EN"], 87 | :quality_score => [7,8,9,10] }, 88 | :max_rows => 10, 89 | :scope => { 90 | :account_ids => default_options[:account_id], 91 | :campaigns => [] 92 | }, 93 | # predefined date 94 | :time => :this_week 95 | ) 96 | 97 | report_request_id = nil 98 | expect{ 99 | report_request_id = service.submit_generate_report(report_request) 100 | }.not_to raise_error 101 | 102 | expect(report_request_id).not_to be_nil 103 | end 104 | 105 | it "should submit account performance report" do 106 | report_request = BingAdsApi::AccountPerformanceReportRequest.new( 107 | :format => :xml, 108 | :language => :english, 109 | :report_name => "My Report", 110 | :aggregation => :hourly, 111 | :columns => [ 112 | :account_name, :account_number, :time_period, 113 | :currency_code, :impressions, :clicks, :ctr, :average_cpc, 114 | :spend, :conversions, :cost_per_conversion 115 | ], 116 | :filter => { 117 | # String as bing expected 118 | :ad_distribution => "Search", 119 | # snake case symbol 120 | :device_os => :windows, 121 | # nil criteria 122 | :device_type => nil 123 | }, 124 | :scope => {:account_ids => default_options[:account_id] }, 125 | :time => { 126 | :custom_date_range_end => {:day => 31, :month => 12, :year => 2013}, 127 | :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 128 | } 129 | ) 130 | 131 | report_request_id = nil 132 | expect{ 133 | report_request_id = service.submit_generate_report(report_request) 134 | }.not_to raise_error 135 | 136 | expect(report_request_id).not_to be_nil 137 | end 138 | 139 | it "should initialize report request status" do 140 | report_request_status = BingAdsApi::ReportRequestStatus.new( 141 | :report_download_url => "http://some.url.com", 142 | :status => "Success" 143 | ) 144 | expect(report_request_status).not_to be_nil 145 | end 146 | 147 | context "when a report has been requested" do 148 | 149 | before :each do 150 | report_request = BingAdsApi::AccountPerformanceReportRequest.new( 151 | :format => :xml, 152 | :language => :english, 153 | :report_name => "My Report", 154 | :aggregation => :hourly, 155 | :columns => [ 156 | :account_name, :account_number, :time_period, 157 | :currency_code, :impressions, :clicks, :ctr, :average_cpc, 158 | :spend, :conversions, :cost_per_conversion 159 | ], 160 | :filter => { 161 | # String as bing expected 162 | :ad_distribution => "Search", 163 | # snake case symbol 164 | :device_os => :windows, 165 | # nil criteria 166 | :device_type => nil 167 | }, 168 | :scope => {:account_ids => default_options[:account_id] }, 169 | :time => { 170 | :custom_date_range_end => {:day => 31, :month => 12, :year => 2013}, 171 | :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 172 | } 173 | ) 174 | 175 | @report_request_id = service.submit_generate_report(report_request) 176 | end 177 | 178 | it "should successfully poll generate report" do 179 | report_request_status = nil 180 | expect{ 181 | report_request_status = service.poll_generate_report(@report_request_id) 182 | }.not_to raise_error 183 | 184 | expect(report_request_status).not_to be_nil 185 | 186 | expect(report_request_status.error?).to eq(false) 187 | end 188 | 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/account_performance_report_request.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object for all report requests. 7 | # Do not instantiate this object. Instead, you may instantiate one 8 | # of the following report request objects which derives from this object to request a report. 9 | # 10 | # Reference: http://msdn.microsoft.com/en-us/library/bing-ads-reporting-bing-ads-reportrequest.aspx 11 | # 12 | # Author:: jlopezn@neonline.cl 13 | # 14 | # === Usage 15 | # 16 | # request = BingAdsApi::AccountPerformanceReportRequest.new( 17 | # :format => :xml, 18 | # :language => :english, 19 | # :report_name => "Me report", 20 | # :aggregation => :hourly, 21 | # :columns => [:account_name, :account_number, :time_period], 22 | # # The filter is specified as a hash 23 | # :filter => { 24 | # :ad_distribution => :search, 25 | # :device_os => :android, 26 | # :device_type => :tablet, 27 | # :status => :submited }, 28 | # :scope => { 29 | # :account_ids => [123456, 234567], 30 | # :campaigns => [] }, 31 | # # predefined date 32 | # :time => :this_week) 33 | # 34 | # request2 = BingAdsApi::AccountPerformanceReportRequest.new( 35 | # :format => :csv, 36 | # :language => :french, 37 | # :report_name => "Me report", 38 | # :aggregation => :daily, 39 | # :columns => [:account_name, :account_number, :time_period], 40 | # # no filter is specified 41 | # :scope => { 42 | # :account_ids => [123456, 234567], 43 | # :campaigns => [] }, 44 | # # Custom date range 45 | # :time => { 46 | # :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 47 | # :custom_date_range_end => {:day => 12, :month => 12, :year => 2013} } 48 | # ) 49 | class AccountPerformanceReportRequest < BingAdsApi::PerformanceReportRequest 50 | 51 | # Valid Columns for this report request 52 | COLUMNS = BingAdsApi::Config.instance. 53 | reporting_constants['account_performance_report']['columns'] 54 | 55 | # Valid Filters for this report request 56 | FILTERS = BingAdsApi::Config.instance. 57 | reporting_constants['account_performance_report']['filter'] 58 | 59 | 60 | # Public : Constructor. Adds a validations for the columns, filter 61 | # and scope attributes 62 | # 63 | # Author:: jlopezn@neonline.cl 64 | # 65 | # === Parameters 66 | # attributes - Hash with the report request attributes 67 | # 68 | # === Example 69 | # 70 | # request = BingAdsApi::AccountPerformanceReportRequest.new( 71 | # :format => :xml, 72 | # :language => :english, 73 | # :report_name => "Me report", 74 | # :aggregation => :hourly, 75 | # :columns => [:account_name, :account_number, :time_period], 76 | # # The filter is specified as a hash 77 | # :filter => { 78 | # :ad_distribution => :search, 79 | # :device_os => :android, 80 | # :device_type => :tablet, 81 | # :status => :submited }, 82 | # :scope => { 83 | # :account_ids => [123456, 234567], 84 | # :campaigns => [] }, 85 | # # predefined date 86 | # :time => :this_week) 87 | # 88 | # request2 = BingAdsApi::AccountPerformanceReportRequest.new( 89 | # :format => :csv, 90 | # :language => :french, 91 | # :report_name => "Me report", 92 | # :aggregation => :daily, 93 | # :columns => [:account_name, :account_number, :time_period], 94 | # # no filter is specified 95 | # :scope => { 96 | # :account_ids => [123456, 234567], 97 | # :campaigns => [] }, 98 | # # Custom date range 99 | # :time => { 100 | # :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 101 | # :custom_date_range_end => {:day => 12, :month => 12, :year => 2013} } 102 | # ) 103 | def initialize(attributes={}) 104 | raise "Invalid columns" if !valid_columns(COLUMNS, attributes[:columns]) 105 | raise "Invalid filters" if !valid_filter(FILTERS, attributes[:filter]) 106 | raise "Invalid scope" if !valid_scope(attributes[:scope]) 107 | super(attributes) 108 | end 109 | 110 | # Public:: Returns the object as a Hash valid for SOAP requests 111 | # 112 | # Author:: jlopezn@neonline.cl 113 | # 114 | # === Parameters 115 | # * +keys_case+ - case for the hashes keys: underscore or camelcase 116 | # 117 | # Returns:: Hash 118 | def to_hash(keys = :underscore) 119 | hash = super(keys) 120 | hash[get_attribute_key('columns', keys)] = 121 | columns_to_hash(COLUMNS, columns, keys) 122 | hash[get_attribute_key('filter', keys)] = 123 | filter_to_hash(FILTERS, keys) 124 | hash[get_attribute_key('scope', keys)] = scope_to_hash(keys) 125 | hash["@xsi:type"] = type_attribute_for_soap 126 | return hash 127 | end 128 | 129 | private 130 | 131 | 132 | 133 | # Internal:: Validates the scope attribute given in the constructor 134 | # 135 | # Author:: jlopezn@neonline.cl 136 | # 137 | # === Parameters 138 | # * +scope+ - value for the 'scope' key in the has initializer 139 | # 140 | # Returns:: true if the scope specification is valid. Raises Exception otherwise 141 | # 142 | # Raises:: Exception if the scope is not valid 143 | def valid_scope(scope) 144 | raise Exception.new("Invalid scope: no account_ids key") if !scope.key?(:account_ids) 145 | return true 146 | end 147 | 148 | 149 | # Internal:: Returns the scope attribute as a hash for the SOAP request 150 | # 151 | # Author:: jlopezn@neonline.cl 152 | # 153 | # === Parameters 154 | # * +keys_case+ - case for the hash: underscore or camelcase 155 | # 156 | # Returns:: Hash 157 | def scope_to_hash(keys_case=:underscore) 158 | return { get_attribute_key('account_ids', keys_case) => 159 | {"ins0:long" => object_to_hash(scope[:account_ids], keys_case)} } 160 | end 161 | 162 | 163 | # Internal:: Returns a string with type attribute for the ReportRequest SOAP tag 164 | # 165 | # Author:: jlopezn@neonline.cl 166 | # 167 | # Returns:: "v9:AccountPerformanceReportRequest" 168 | def type_attribute_for_soap 169 | return BingAdsApi::ClientProxy::NAMESPACE.to_s + ":" + 170 | BingAdsApi::Config.instance. 171 | reporting_constants['account_performance_report']['type'] 172 | end 173 | 174 | end 175 | 176 | end 177 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/budget_summary_report_request.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public: Reporting service request object for a keyword performance report. 7 | # 8 | # Author:: alex.cavalli@offers.com 9 | # 10 | # === Usage 11 | # 12 | # request = BingAdsApi::KeywordPerformanceReportRequest.new( 13 | # :format => :xml, 14 | # :language => :english, 15 | # :report_name => "Me report", 16 | # :aggregation => :hourly, 17 | # :columns => [:account_name, :account_number, :time_period], 18 | # # The filter is specified as a hash 19 | # :filter => { 20 | # # specifies the Bing expected String value 21 | # :ad_distribution => "Search", 22 | # :ad_type => "Text", 23 | # :bid_match_type => "Exact", 24 | # :delivered_match_type => "Exact", 25 | # # specifies criteria as a snake case symbol 26 | # :device_type => :tablet, 27 | # :keyword_relevance => [3], 28 | # :keywords => ["bing ads"], 29 | # :landing_page_relevance => [2], 30 | # :landing_page_user_experience => [2], 31 | # :language_code => ["EN"], 32 | # :quality_score => [7,8,9,10] }, 33 | # :max_rows => 10, 34 | # :scope => { 35 | # :account_ids => [123456, 234567], 36 | # :campaigns => [], 37 | # :ad_groups => [] }, 38 | # :sort => [] 39 | # # predefined date 40 | # :time => :this_week) 41 | class BudgetSummaryReportRequest < BingAdsApi::ReportRequest 42 | 43 | # Adds helper methods to time attribute 44 | include BingAdsApi::Helpers::TimeHelper 45 | 46 | # Adds helper methods to column attribute 47 | include BingAdsApi::Helpers::ColumnHelper 48 | 49 | # Adds helper methods to filter attribute 50 | include BingAdsApi::Helpers::FilterHelper 51 | 52 | 53 | # Valid Columns for this report request 54 | COLUMNS = BingAdsApi::Config.instance. 55 | reporting_constants['budget_summary_report']['columns'] 56 | 57 | # Valid Filters for this report request 58 | FILTERS = BingAdsApi::Config.instance. 59 | reporting_constants['budget_summary_report']['filter'] 60 | 61 | attr_accessor :max_rows, :sort, :columns, :filter, :scope, :time 62 | 63 | 64 | # Public : Constructor. Adds a validations for the columns and filter 65 | # attributes 66 | # 67 | # Author:: alex.cavalli@offers.com 68 | # 69 | # === Parameters 70 | # * +attributes+ - Hash with the report request attributes 71 | # 72 | # === Example 73 | # 74 | # request = BingAdsApi::KeywordPerformanceReportRequest.new( 75 | # :format => :xml, 76 | # :language => :english, 77 | # :report_name => "Me report", 78 | # :aggregation => :hourly, 79 | # :columns => [:account_name, :account_number, :time_period], 80 | # # The filter is specified as a hash 81 | # :filter => { 82 | # # specifies the Bing expected String value 83 | # :ad_distribution => "Search", 84 | # :ad_type => "Text", 85 | # :bid_match_type => "Exact", 86 | # :delivered_match_type => "Exact", 87 | # # specifies criteria as a snake case symbol 88 | # :device_type => :tablet, 89 | # :keyword_relevance => [3], 90 | # :landing_page_relevance => [2], 91 | # :landing_page_user_experience => [2], 92 | # :language_code => ["EN"], 93 | # :quality_score => [7,8,9,10] }, 94 | # :max_rows => 10, 95 | # :scope => { 96 | # :account_ids => [123456, 234567], 97 | # :campaigns => [], 98 | # :ad_groups => [] }, 99 | # :sort => [] 100 | # # predefined date 101 | # :time => :this_week) 102 | def initialize(attributes={}) 103 | raise Exception.new("Invalid columns") if !valid_columns(COLUMNS, attributes[:columns]) 104 | raise Exception.new("Invalid time") if !valid_time(attributes[:time]) 105 | super(attributes) 106 | end 107 | 108 | 109 | # Public:: Returns the object as a Hash valid for SOAP requests 110 | # 111 | # Author:: jlopezn@neonline.cl 112 | # 113 | # === Parameters 114 | # * +keys_case+ - case for the hashes keys: underscore or camelcase 115 | # 116 | # Returns:: Hash 117 | def to_hash(keys = :underscore) 118 | hash = super(keys) 119 | hash[get_attribute_key('columns', keys)] = 120 | columns_to_hash(COLUMNS, columns, keys) 121 | hash[get_attribute_key('filter', keys)] = 122 | filter_to_hash(FILTERS, keys) 123 | hash[get_attribute_key('scope', keys)] = scope_to_hash(keys) 124 | hash[get_attribute_key('max_rows', keys)] = max_rows if max_rows 125 | hash[get_attribute_key('sort', keys)] = sort_to_hash(keys) if sort 126 | hash["@xsi:type"] = type_attribute_for_soap 127 | hash[get_attribute_key('time', keys)] = time_to_hash(keys) 128 | return hash 129 | end 130 | 131 | 132 | private 133 | 134 | # Internal:: Returns the scope attribute as a hash for the SOAP request 135 | # 136 | # Author:: alex.cavalli@offers.com 137 | # 138 | # === Parameters 139 | # * +keys_case+ - case for the hash: underscore or camelcase 140 | # 141 | # Returns:: Hash 142 | def scope_to_hash(keys_case=:underscore) 143 | return { 144 | get_attribute_key('account_ids', keys_case) => 145 | {"ins0:long" => object_to_hash(scope[:account_ids], keys_case) }, 146 | get_attribute_key('campaigns', keys_case) => 147 | { "CampaignReportScope" => object_to_hash(scope[:campaigns], keys_case) } 148 | } 149 | end 150 | 151 | 152 | # Internal:: Returns the sort attribute as an array for the SOAP request 153 | # 154 | # Author:: alex.cavalli@offers.com 155 | # 156 | # === Parameters 157 | # * +keys_case+ - case for the hash: underscore or camelcase 158 | # 159 | # Returns:: Array 160 | def sort_to_hash(keys_case=:underscore) 161 | sort.map { |sort_object| object_to_hash(sort_object, keys_case) } 162 | end 163 | 164 | 165 | # Internal:: Returns a string with type attribute for the ReportRequest SOAP tag 166 | # 167 | # Author:: jlopezn@neonline.cl 168 | # 169 | # Returns:: "v9:KeywordPerformanceReportRequest" 170 | def type_attribute_for_soap 171 | return BingAdsApi::ClientProxy::NAMESPACE.to_s + ":" + 172 | BingAdsApi::Config.instance. 173 | reporting_constants['budget_summary_report']['type'] 174 | end 175 | 176 | end 177 | 178 | end 179 | -------------------------------------------------------------------------------- /lib/bing-ads-api/service.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | # Public : Base class for service object 6 | # 7 | # Author:: jlopezn@neonline.cl 8 | # 9 | class Service 10 | 11 | attr_accessor :client_proxy, :environment, :max_retry_attempts 12 | 13 | # Default logger for services 14 | LOGGER = Logger.new(STDOUT) 15 | 16 | 17 | # Public : Constructor 18 | # 19 | # Author:: jlopezn@neonline.cl 20 | # 21 | # === Parameters 22 | # * +options+ - Hash with autentication and environment settings 23 | # 24 | # === Options 25 | # * environment - +:production+ or +:sandbox+ 26 | # * username - Bing Ads username 27 | # * passwrod - Bing Ads user's sign-in password 28 | # * developer_token - client application's developer access token 29 | # * customer_id - identifier for the customer that owns the account 30 | # * account_id - identifier of the account that own the entities in the request 31 | # * proxy - Hash with any Client Proxy additional options (such as header, logger or enconding) 32 | # 33 | # === Examples 34 | # service = BingAdsApi::Service.new( 35 | # :environment => :sandbox, 36 | # :username => 'username', 37 | # :password => 'pass', 38 | # :developer_token => 'SOME_TOKEN', 39 | # :account_id => 123456, 40 | # :customer_id => 654321, 41 | # :proxy => {:logger => Rails.logger} 42 | # ) 43 | # # => 44 | def initialize(options={}) 45 | 46 | # Service Environment 47 | self.environment = options[:environment] 48 | 49 | # ClientProxy settings 50 | clientProxySettings = { 51 | :authentication_token => options[:authentication_token], 52 | :username => options[:username], 53 | :password => options[:password], 54 | :developer_token => options[:developer_token], 55 | :account_id => options[:account_id], 56 | :customer_id => options[:customer_id], 57 | :wsdl_url => options[:wsdl] || solve_wsdl_url 58 | } 59 | 60 | # Additionsl ClientProxy settings 61 | clientProxySettings[:proxy] = options[:proxy] if options[:proxy] 62 | 63 | # ClientProxy creation 64 | self.client_proxy = BingAdsApi::ClientProxy.new(clientProxySettings) 65 | 66 | self.max_retry_attempts = options[:max_retry_attempts] || 0 67 | end 68 | 69 | 70 | # Public : This is a utility wrapper for calling services into the 71 | # +ClientProxy+. This methods handle all the +Savon::Client+ Exceptions 72 | # and returns a Hash with the call response 73 | # 74 | # Author:: jlopezn@neonline.cl 75 | # 76 | # === Parameters 77 | # +operation+ - name of the operation to be called 78 | # +message+ - hash with the parameters to the operation 79 | # 80 | # === Examples 81 | # service.call(:some_operation, {key: value}) 82 | # # => 83 | # 84 | # Returns:: Hash with the result of the service call 85 | # Raises:: ServiceError if the SOAP call, the ClientProxy fails or the response is invalid 86 | def call(operation, message, &block) 87 | retries_made = 0 88 | raise "You must provide an operation" if operation.nil? 89 | begin 90 | LOGGER.debug "BingAdsApi Service" 91 | LOGGER.debug " Calling #{operation.to_s}" 92 | LOGGER.debug " Message: #{message}" 93 | response = self.client_proxy.call(operation.to_sym, 94 | message: message) 95 | LOGGER.debug "response header:" 96 | LOGGER.debug "\t#{response.header}" 97 | 98 | LOGGER.info "Operation #{operation.to_s} call success" 99 | return response.hash 100 | rescue Savon::SOAPFault => error 101 | LOGGER.error "SOAP Error calling #{operation.to_s}: #{error.http.code}" 102 | fault_detail = error.to_hash[:fault][:detail] 103 | if fault_detail.key?(:api_fault_detail) 104 | api_fault_detail = BingAdsApi::ApiFaultDetail.new(fault_detail[:api_fault_detail]) 105 | raise BingAdsApi::ApiException.new( 106 | api_fault_detail, "SOAP Error calling #{operation.to_s}") 107 | elsif fault_detail.key?(:ad_api_fault_detail) 108 | ad_api_fault_detail = BingAdsApi::AdApiFaultDetail.new(fault_detail[:ad_api_fault_detail]) 109 | raise BingAdsApi::ApiException.new( 110 | ad_api_fault_detail, "SOAP Error calling #{operation.to_s}") 111 | else 112 | raise 113 | end 114 | rescue Savon::HTTPError => error 115 | LOGGER.error "Http Error calling #{operation.to_s}: #{error.http.code}" 116 | raise 117 | rescue Savon::InvalidResponseError => error 118 | LOGGER.error "Invalid server reponse calling #{operation.to_s}" 119 | raise 120 | rescue 121 | # for any other exceptions, retry with an exponential backoff 122 | if retries_made < max_retry_attempts 123 | sleep(2**(retries_made)) 124 | retries_made += 1 125 | retry 126 | else 127 | raise 128 | end 129 | end 130 | end 131 | 132 | 133 | # Public : Extracts the actual response from the entire response hash. 134 | # For example, if you specify 'AddCampaigns', this method will return 135 | # the content of 'AddCampaignsResponse' tag as a Hash 136 | # 137 | # Author:: jlopezn@neonline.cl 138 | # 139 | # === Parameters 140 | # response - The complete response hash received from a Operation call 141 | # method - Name of the method of with the 'reponse' tag is require 142 | # 143 | # === Examples 144 | # service.get_response_hash(Hash, 'add_campaigns') 145 | # # => Hash 146 | # 147 | # Returns:: Hash with the inner structure of the method response hash 148 | # Raises:: exception 149 | def get_response_hash(response, method) 150 | return response[:envelope][:body]["#{method}_response".to_sym] 151 | end 152 | 153 | private 154 | 155 | # Private : This method must be overriden by specific services. 156 | # Returns:: the service name 157 | # 158 | # Author:: jlopezn@neonline.cl 159 | # 160 | # Examples 161 | # get_service_name 162 | # # => "service_name" 163 | # 164 | # Returns:: String with the service name 165 | # Raises:: exception if the specific Service class hasn't overriden this method 166 | def get_service_name 167 | raise "Should return the a service name from config.wsdl keys" 168 | end 169 | 170 | 171 | # Private : Solves the service WSDL URL based on his service name 172 | # and environment values 173 | # 174 | # Author:: jlopezn@neonline.cl 175 | # 176 | # Examples 177 | # solve_wsdl_url 178 | # # => "https://bing.wsdl.url.com" 179 | # 180 | # Returns:: String with the Service url 181 | def solve_wsdl_url 182 | config = BingAdsApi::Config.instance 183 | return config.service_wsdl(environment, get_service_name) 184 | end 185 | 186 | end 187 | 188 | end 189 | -------------------------------------------------------------------------------- /lib/bing-ads-api/data/reporting/campaign_performance_report_request.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | 3 | module BingAdsApi 4 | 5 | ## 6 | # Public : Defines the base object for all report requests. 7 | # Do not instantiate this object. Instead, you may instantiate one 8 | # of the following report request objects which derives from this object to request a report. 9 | # 10 | # Reference: http://msdn.microsoft.com/en-us/library/bing-ads-reporting-bing-ads-reportrequest.aspx 11 | # 12 | # Author:: jlopezn@neonline.cl 13 | # 14 | # === Usage 15 | # 16 | # request = BingAdsApi::CampaignPerformanceReportRequest.new( 17 | # :format => :xml, 18 | # :language => :english, 19 | # :report_name => "Me report", 20 | # :aggregation => :hourly, 21 | # :columns => [:account_name, :account_number, :time_period], 22 | # # The filter is specified as a hash 23 | # :filter => { 24 | # # specifies the Bing expected String value 25 | # :ad_distribution => "Search", 26 | # # specifies criteria as a snake case symbol 27 | # :device_os => :android, 28 | # :device_type => :tablet, 29 | # # criteria nil is similar to not specify it at all 30 | # :status => nil }, 31 | # :scope => { 32 | # :account_ids => [123456, 234567], 33 | # :campaigns => [] }, 34 | # # predefined date 35 | # :time => :this_week) 36 | # 37 | # request2 = BingAdsApi::CampaignPerformanceReportRequest.new( 38 | # :format => :csv, 39 | # :language => :french, 40 | # :report_name => "Me report", 41 | # :aggregation => :daily, 42 | # :columns => [:account_name, :account_number, :time_period], 43 | # # no filter is specified 44 | # :scope => { 45 | # :account_ids => [123456, 234567], 46 | # :campaigns => [] }, 47 | # # Custom date range 48 | # :time => { 49 | # :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 50 | # :custom_date_range_end => {:day => 12, :month => 12, :year => 2013} } 51 | # ) 52 | class CampaignPerformanceReportRequest < BingAdsApi::PerformanceReportRequest 53 | 54 | # Valid Columns for this report request 55 | COLUMNS = BingAdsApi::Config.instance. 56 | reporting_constants['campaign_performance_report']['columns'] 57 | 58 | # Valid Filters for this report request 59 | FILTERS = BingAdsApi::Config.instance. 60 | reporting_constants['campaign_performance_report']['filter'] 61 | 62 | # Public : Constructor. Adds a validations for the columns, filter 63 | # and scope attributes 64 | # 65 | # Author:: jlopezn@neonline.cl 66 | # 67 | # === Parameters 68 | # * +attributes+ - Hash with the report request attributes 69 | # 70 | # === Example 71 | # 72 | # request = BingAdsApi::CampaignPerformanceReportRequest.new( 73 | # :format => :xml, 74 | # :language => :english, 75 | # :report_name => "Me report", 76 | # :aggregation => :hourly, 77 | # :columns => [:account_name, :account_number, :time_period], 78 | # # The filter is specified as a hash 79 | # :filter => { 80 | # # specifies the Bing expected String value 81 | # :ad_distribution => "Search", 82 | # # specifies criteria as a snake case symbol 83 | # :device_os => :android, 84 | # :device_type => :tablet, 85 | # # criteria nil is similar to not specify it at all 86 | # :status => nil }, 87 | # :scope => { 88 | # :account_ids => [123456, 234567], 89 | # :campaigns => [] }, 90 | # # predefined date 91 | # :time => :this_week) 92 | # 93 | # request2 = BingAdsApi::CampaignPerformanceReportRequest.new( 94 | # :format => :csv, 95 | # :language => :french, 96 | # :report_name => "Me report", 97 | # :aggregation => :daily, 98 | # :columns => [:account_name, :account_number, :time_period], 99 | # # no filter is specified 100 | # :scope => { 101 | # :account_ids => [123456, 234567], 102 | # :campaigns => [] }, 103 | # # Custom date range 104 | # :time => { 105 | # :custom_date_range_start => {:day => 1, :month => 12, :year => 2013}, 106 | # :custom_date_range_end => {:day => 12, :month => 12, :year => 2013} } 107 | # ) 108 | def initialize(attributes={}) 109 | raise Exception.new("Invalid columns") if !valid_columns(COLUMNS, attributes[:columns]) 110 | raise Exception.new("Invalid filters") if !valid_filter(FILTERS, attributes[:filter]) 111 | raise Exception.new("Invalid scope") if !valid_scope(attributes[:scope]) 112 | super(attributes) 113 | end 114 | 115 | 116 | # Public:: Returns the object as a Hash valid for SOAP requests 117 | # 118 | # Author:: jlopezn@neonline.cl 119 | # 120 | # === Parameters 121 | # * +keys_case+ - case for the hashes keys: underscore or camelcase 122 | # 123 | # Returns:: Hash 124 | def to_hash(keys = :underscore) 125 | hash = super(keys) 126 | hash[get_attribute_key('columns', keys)] = 127 | columns_to_hash(COLUMNS, columns, keys) 128 | hash[get_attribute_key('filter', keys)] = 129 | filter_to_hash(FILTERS, keys) 130 | hash[get_attribute_key('scope', keys)] = scope_to_hash(keys) 131 | hash["@xsi:type"] = type_attribute_for_soap 132 | return hash 133 | end 134 | 135 | 136 | private 137 | 138 | # Internal:: Validates the scope attribute given in the constructor 139 | # 140 | # Author:: jlopezn@neonline.cl 141 | # 142 | # === Parameters 143 | # * +scope+ - value for the 'scope' key in the has initializer 144 | # 145 | # Returns:: true if the scope specification is valid. Raises Exception otherwise 146 | # 147 | # Raises:: Exception if the scope is not valid 148 | def valid_scope(scope) 149 | raise Exception.new("Invalid scope: no account_ids key") if !scope.key?(:account_ids) 150 | raise Exception.new("Invalid scope: no campaigns key") if !scope.key?(:campaigns) 151 | return true 152 | end 153 | 154 | 155 | # Internal:: Returns the scope attribute as a hash for the SOAP request 156 | # 157 | # Author:: jlopezn@neonline.cl 158 | # 159 | # === Parameters 160 | # * +keys_case+ - case for the hash: underscore or camelcase 161 | # 162 | # Returns:: Hash 163 | def scope_to_hash(keys_case=:underscore) 164 | return { 165 | get_attribute_key('account_ids', keys_case) => {"ins0:long" => object_to_hash(scope[:account_ids], keys_case)}, 166 | get_attribute_key('campaigns', keys_case) => 167 | { "CampaignReportScope" => object_to_hash(scope[:campaigns], keys_case) } 168 | } 169 | end 170 | 171 | 172 | # Internal:: Returns a string with type attribute for the ReportRequest SOAP tag 173 | # 174 | # Author:: jlopezn@neonline.cl 175 | # 176 | # Returns:: "v9:CampaignPerformanceReportRequest" 177 | def type_attribute_for_soap 178 | return BingAdsApi::ClientProxy::NAMESPACE.to_s + ":" + 179 | BingAdsApi::Config.instance. 180 | reporting_constants['campaign_performance_report']['type'] 181 | end 182 | 183 | end 184 | 185 | end 186 | -------------------------------------------------------------------------------- /lib/locales/es.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | es-CL: 5 | 6 | bing: 7 | 8 | constants: 9 | 10 | common: 11 | time_zones: 12 | abu_dhabi_muscat: 'Abu Dhabi y Muscat' 13 | adelaide: 'Adelaide, Australia' 14 | alaska: 'Alaska' 15 | almaty_novosibirsk: 'Almaty y Novosibirsk' 16 | amsterdam_berlin_bern_rome_stockholm_vienna: 'Amsterdam, Berlin, Bern, Rome, Stockholm, y Vienna' 17 | arizona: 'Arizona' 18 | astana_dhaka: 'Astana y Dhaka' 19 | athens_buckarest_istanbul: 'Athens, Istanbul, Beirut, y Minsk' 20 | atlantic_time_canada: 'Atlantic Time y Canada' 21 | auckland_wellington: 'Auckland y Wellington, New Zealand' 22 | azores: 'Azores' 23 | baghdad: 'Baghdad' 24 | baku_tbilisi_yerevan: 'Baku, Tbilisi, y Yerevan' 25 | bangkok_hanoi_jakarta: 'Bangkok, Hanoi, y Jakarta' 26 | beijing_chongqing_hong_kong_urumqi: 'Beijing, Chongqing, Hong Kong, y Urumqi' 27 | belgrade_bratislava_budapest_ljubljana_prague: 'Belgrade, Bratislava, Budapest, Ljubljana, y Prague' 28 | bogota_lima_quito: 'Bogota, Lima, y Quito' 29 | brasilia: 'Brasilia' 30 | brisbane: 'Brisbane' 31 | brussels_copenhagen_madrid_paris: 'Brussels, Copenhagen, Madrid, y Paris' 32 | bucharest: 'Bucharest' 33 | buenos_aires_georgetown: 'Buenos Aires y Georgetown' 34 | cairo: 'Cairo' 35 | canberra_melbourne_sydney: 'Canberra, Melbourne, y Sydney' 36 | cape_verde_island: 'Cape Verde Island' 37 | caracas_la_paz: 'Caracas y La Paz (Bolivia)' 38 | casablanca_monrovia: 'Casablanca y Monrovia' 39 | central_america: 'Central America' 40 | central_time_u_s_canada: 'Central Time U.S. y Canada' 41 | chennai_kolkata_mumbai_new_delhi: 'Chennai, Kolkata, Mumbai, y New Delhi' 42 | chihuahua_la_paz_mazatlan: 'Chihuahua, La Paz (Mexico), y Mazatlan' 43 | darwin: 'Darwin' 44 | eastern_time_u_s_canada: 'Eastern Time U.S. y Canada' 45 | ekaterinburg: 'Ekaterinburg' 46 | fiji_kamchatka_marshall_island: 'Fiji, Kamchatka, y Marshall Island' 47 | greenland: 'Greenland' 48 | greenwich_mean_time_dublin_edinburgh_lisbon_london: 'Greenwich Mean Time, Dublin, Edinburgh, Lisbon, y London' 49 | guadalajara_mexico_city_monterrey: 'Guadalajara, Mexico City, y Monterrey' 50 | guam_port_moresby: 'Guam y Port Moresby' 51 | harare_pretoria: 'Harare y Pretoria' 52 | hawaii: 'Hawaii' 53 | helsinki_kyiv_riga_sofia_tallinn_vilnius: 'Helsinki, Kyiv, Riga, Sofia, Tallinn, y Vilnius' 54 | hobart: 'Hobart' 55 | indiana_east: 'Indiana (East)' 56 | international_date_line_west: 'International Date Line (West)' 57 | irkutsk_ulaan_bataar: 'Irkutsk y Ulaanbaatar' 58 | islandamabad_karachi_tashkent: 'Islamabad, Karachi, y Tashkent' 59 | jerusalem: 'Jerusalem' 60 | kabul: 'Kabul' 61 | kathmandu: 'Kathmandu' 62 | krasnoyarsk: 'Krasnoyarsk' 63 | kuala_lumpur_singapore: 'Kuala Lumpur y Singapore' 64 | kuwait_riyadh: 'Kuwait y Riyadh' 65 | magadan_solomon_island_new_caledonia: 'Magadan, Solomon Islands, y New Caledonia' 66 | mid_atlantic: 'Mid-Atlantic' 67 | midway_islandand_samoa: 'Midway Island y Samoa' 68 | moscow_st_petersburg_volgograd: 'Moscow, St Petersburg, y Volgograd' 69 | mountain_time_u_s_canada: 'Mountain Time U.S. y Canada' 70 | nairobi: 'Nairobi' 71 | newfoundland: 'Newfoundland' 72 | nukualofa: 'Nuku'alofa' 73 | osaka_sapporo_tokyo: 'Osaka, Sapporo, y Tokyo' 74 | pacific_time_u_s_canada_tijuana: 'Pacific Time, U.S. y Canada, y Tijuana' 75 | perth: 'Perth' 76 | rangoon: 'Rangoon' 77 | santiago: 'Santiago' 78 | sarajevo_skopje_warsaw_zagreb: 'Sarajevo, Skopie, Warsaw, y Zagreb' 79 | saskatchewan: 'Saskatchewan' 80 | seoul: 'Seoul' 81 | sri_jayawardenepura: 'Sri Jayawardenepura' 82 | taipei: 'Taipei' 83 | tehran: 'Tehran' 84 | vladivostok: 'Vladivostok' 85 | west_central_africa: 'West Central Africa' 86 | yakutsk: 'Yakutsk' 87 | ad_languages: 88 | danish: 'Danish' 89 | dutch: 'Dutch' 90 | english: 'English' 91 | finnish: 'Finnish' 92 | french: 'French' 93 | german: 'German' 94 | italian: 'Italian' 95 | norwegian: 'Norwegian' 96 | portuguese: 'Portuguese' 97 | spanish: 'Spanish' 98 | swedish: 'Swedish' 99 | traditional_chinese: 'Traditional Chinese' 100 | codes: 101 | danish: 'DA' 102 | dutch: 'NL' 103 | english: 'EN' 104 | finnish: 'FI' 105 | french: 'FR' 106 | german: 'DE' 107 | italian: 'IT' 108 | norwegian: 'NO' 109 | portuguese: 'PT' 110 | spanish: 'ES' 111 | swedish: 'SV' 112 | traditional_chinese: 'ZH' 113 | market_country: 114 | danish: 'Denmark' 115 | dutch: 'Netherlands' 116 | english: 'Australia Canada India Indonesia Ireland Malaysia New Zealand Philippines Singapore Thailand United Kingdom United States Vietnam' 117 | finnish: 'Finland ' 118 | french: 'Belgium Canada France' 119 | german: 'Austria Germany Switzerland' 120 | italian: 'Italy' 121 | norwegian: 'Norway' 122 | portuguese: 'Brazil' 123 | spanish: 'Argentina Chile Colombia Mexico Peru Spain Venezuela' 124 | swedish: 'Sweden' 125 | traditional_chinese: 'Hong Kong Taiwan' 126 | 127 | campaign_management: 128 | budget_limit_type: 129 | monthly_budget_spend_until_depleted: 'MonthlyBudgetSpendUntilDepleted' 130 | daily_budget_accelerated: 'DailyBudgetAccelerated' 131 | daily_budget_standard: 'DailyBudgetStandard' 132 | campaign_status: 133 | active: 'Activa' 134 | paused: 'Pausada' 135 | budget_paused: 'Pausada por presupuesto' 136 | budget_and_manual_paused: 'Pausada por presupuesto y manual' 137 | deleted: 'Deleted' 138 | ad_distribution: 139 | search: 'Búsqueda' 140 | content: 'Contenido' 141 | ad_rotation_type: 142 | optimize_for_clicks: 'Optimizado por clicks' 143 | rotate_ads_evenly: 'Rotar anuncios parejo' 144 | bidding_model: 145 | keyword: 'Keywords' 146 | site_placement: 'Posición en sitio' 147 | pricing_model: 148 | cpc: 'Cpc' 149 | cpm: 'Cpm' 150 | ad_group_status: 151 | draft: 'Borrador' 152 | active: 'Activo' 153 | paused: 'Pausado' 154 | deleted: 'Eliminado' 155 | ad_status: 156 | inactive: 'Inactivo' 157 | Active: 'Activo' 158 | paused: 'Pausado' 159 | deleted: 'Eliminado' 160 | ad_type: 161 | text: 'Texto' 162 | image: 'Imágen' 163 | mobile: 'Mobil' 164 | rich_search: 'Búsqueda enriquecida' 165 | product: 'Producto' 166 | keyword_editorial_status: 167 | active: 'Activo' 168 | disapproved: 'Desaprovado' 169 | inactive: 'Inactivo' 170 | keyword_status: 171 | active: 'Activo' 172 | paused: 'Pausado' 173 | deleted: 'Borrado' 174 | inactive: 'Inactivo' 175 | -------------------------------------------------------------------------------- /lib/bing-ads-api/service/bulk.rb: -------------------------------------------------------------------------------- 1 | require 'rest_client' 2 | 3 | # -*- encoding : utf-8 -*- 4 | module BingAdsApi 5 | 6 | # Public: This class represents the Bulk Services defined in the Bing Ads API 7 | # to request and download and upload bulk account and campaign data 8 | # 9 | # Author:: alex.cavalli@offers.com 10 | # 11 | # Examples 12 | # options = { 13 | # :environment => :sandbox, 14 | # :username => "username", 15 | # :password => "pass", 16 | # :developer_token => "SOME_TOKEN", 17 | # :customer_id => "1234567", 18 | # :account_id => "9876543" } 19 | # service = BingAdsApi::Bulk.new(options) 20 | class Bulk < BingAdsApi::Service 21 | 22 | # Valid download file types for bulk API 23 | DOWNLOAD_FILE_TYPES = BingAdsApi::Config.instance. 24 | bulk_constants['download_file_type'] 25 | 26 | RESPONSE_MODES = BingAdsApi::Config.instance. 27 | bulk_constants['response_mode'] 28 | 29 | # Public: Issue a download request for all entities in an account. 30 | # 31 | # http://msdn.microsoft.com/en-us/library/jj885755.aspx 32 | # 33 | # Author:: alex.cavalli@offers.com 34 | # 35 | # === Parameters 36 | # +account_ids+ - Array of account ids 37 | # +entities+ - Array of entities 38 | # +options+ - Hash of additional options 39 | # 40 | # === Examples 41 | # service.download_campaigns_by_account_ids( 42 | # [1,2,3], 43 | # [:campaigns, :ad_groups, :keywords, :ads], 44 | # data_scope: :entity_data, 45 | # download_file_type: :csv, 46 | # format_version: 2.0, 47 | # last_sync_time_in_utc: "2001-10-26T21:32:52", 48 | # location_target_version: "Latest", 49 | # performance_stats_date_range: { 50 | # custom_date_range_end: {day: 31, month: 12, year: 2013}, 51 | # custom_date_range_start: {day: 1, month: 12, year: 2013} 52 | # } 53 | # ) 54 | # # => "1234567890" 55 | # 56 | # Returns:: String with the download request id 57 | # 58 | # Raises:: exception 59 | def download_campaigns_by_account_ids(account_ids, entities, options={}) 60 | download_request = { 61 | account_ids: {"ins0:long" => account_ids}, 62 | data_scope: data_scope_to_array(options[:data_scope]), 63 | download_file_type: DOWNLOAD_FILE_TYPES[options[:download_file_type].to_s], 64 | entities: entities_for_soap(entities), 65 | format_version: options[:format_version] || 4.0, 66 | last_sync_time_in_utc: options[:last_sync_time_in_utc], 67 | location_target_version: options[:location_target_version], 68 | performance_stats_date_range: options[:performance_stats_date_range] 69 | } 70 | 71 | response = call(:download_campaigns_by_account_ids, 72 | download_request) 73 | response_hash = get_response_hash(response, __method__) 74 | download_request_id = response_hash[:download_request_id] 75 | return download_request_id 76 | end 77 | 78 | 79 | # Public: Get an upload URL and corresponding request ID. The URL can be 80 | # used to post a bulk upload file to, and the request ID can be used to 81 | # track the status of the request once the file has been posted. See 82 | # http://msdn.microsoft.com/en-US/library/bing-ads-bulk-download-and-upload-guide.aspx#bulkupload 83 | # 84 | # Author:: alex.cavalli@offers.com 85 | # 86 | # === Parameters 87 | # +account_id+ - An account id to apply the bulk upload to 88 | # +options+ - Hash of additional options 89 | # 90 | # === Examples 91 | # service.get_bulk_upload_url(111) 92 | # # => {request_id: 222, upload_url: "http://www.example.com/"} 93 | # 94 | # Returns:: Hash with the upload URL and upload request ID 95 | # 96 | # Raises:: exception 97 | def get_bulk_upload_url(account_id, options={}) 98 | response = call(:get_bulk_upload_url, 99 | response_mode: RESPONSE_MODES[options[:response_mode].to_s], 100 | account_id: account_id, 101 | ) 102 | response_hash = get_response_hash(response, __method__) 103 | return response_hash 104 | end 105 | 106 | 107 | # Public: Post a bulk upload file to the Bing Ads API and return the bulk 108 | # request ID. This method does not map to a specific service call in the 109 | # Bing Ads API, but wraps the GetBulkUploadUrl call and POSTs the file 110 | # upload to the provided upload URL. 111 | # http://msdn.microsoft.com/en-US/library/bing-ads-bulk-download-and-upload-guide.aspx#bulkupload 112 | # 113 | # Author:: alex.cavalli@offers.com 114 | # 115 | # === Parameters 116 | # +file+ - A path to the CSV/TSV bulk file to upload 117 | # +account_id+ - An account id to apply the bulk upload to 118 | # +options+ - Hash of additional options 119 | # 120 | # === Examples 121 | # service.submit_bulk_upload_file("bulk_upload.csv", 111) 122 | # # => "1234567890" 123 | # 124 | # Returns:: String with the upload request id 125 | # 126 | # Raises:: exception 127 | def submit_bulk_upload_file(file, account_id, options={}) 128 | upload_request = get_bulk_upload_url(account_id, options) 129 | 130 | # TODO: Extract this HTTP interface to separate object 131 | response = RestClient.post( 132 | upload_request[:upload_url], 133 | {file: File.new(file)}, 134 | "UserName" => client_proxy.username, 135 | "Password" => client_proxy.password, 136 | "DeveloperToken" => client_proxy.developer_token, 137 | "CustomerId" => client_proxy.customer_id, 138 | "AccountId" => account_id, 139 | ) 140 | 141 | raise "File upload failed. HTTP response:\n#{response.to_str}" unless response.code == 200 142 | 143 | return upload_request[:request_id] 144 | end 145 | 146 | 147 | # Public: Get the status of a report request 148 | # 149 | # Author:: alex.cavalli@offers.com 150 | # 151 | # === Parameters 152 | # +download_request_id+ - Identifier of the report request 153 | # 154 | # === Examples 155 | # service.get_bulk_download_status("12345") 156 | # # => BulkDownloadStatus 157 | # 158 | # Returns:: BulkDownloadStatus 159 | # 160 | # Raises:: exception 161 | def get_bulk_download_status(download_request_id) 162 | response = call(:get_bulk_download_status, 163 | {request_id: download_request_id} ) 164 | response_hash = get_response_hash(response, __method__) 165 | return BingAdsApi::BulkDownloadStatus.new(response_hash) 166 | end 167 | 168 | 169 | # Public: Get the status of a upload request 170 | # 171 | # Author:: alex.cavalli@offers.com 172 | # 173 | # === Parameters 174 | # +upload_request_id+ - Identifier of the upload request 175 | # 176 | # === Examples 177 | # service.get_bulk_upload_status("12345") 178 | # # => BulkUploadStatus 179 | # 180 | # Returns:: BulkUploadStatus 181 | # 182 | # Raises:: exception 183 | def get_bulk_upload_status(upload_request_id) 184 | response = call(:get_bulk_upload_status, 185 | {request_id: upload_request_id} ) 186 | response_hash = get_response_hash(response, __method__) 187 | return BingAdsApi::BulkUploadStatus.new(response_hash.merge('request_id' => upload_request_id)) 188 | end 189 | 190 | 191 | private 192 | def get_service_name 193 | "bulk" 194 | end 195 | 196 | 197 | # Internal: Converts data scope symbols (in snake case) to camelcased 198 | # variants for SOAP. 199 | # 200 | # Author:: alex.cavalli@offers.com 201 | # 202 | # Returns:: Array 203 | def data_scope_to_array(data_scope) 204 | return nil unless data_scope 205 | 206 | valid_entities = BingAdsApi::Config.instance.bulk_constants['data_scope'] 207 | 208 | Array(data_scope).map do |data_scope| 209 | if data_scope.is_a?(String) 210 | data_scope 211 | elsif data_scope.is_a?(Symbol) 212 | valid_entities[data_scope.to_s] 213 | end 214 | end 215 | end 216 | 217 | 218 | # Internal: Converts entity symbols (in snake case) to camelcased variants 219 | # for SOAP and joins them by whitespace characters. 220 | # 221 | # Author:: alex.cavalli@offers.com 222 | # 223 | # Returns:: String 224 | def entities_for_soap(entities) 225 | raise Exception.new("Invalid entities value: nil") unless entities 226 | 227 | valid_entities = BingAdsApi::Config.instance.bulk_constants['entities'] 228 | 229 | entities.map do |entity| 230 | if entity.is_a?(String) 231 | entity 232 | elsif entity.is_a?(Symbol) 233 | valid_entities[entity.to_s] 234 | end 235 | end.join(" ") 236 | end 237 | 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /spec/campaign_management_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Author:: jlopezn@neonline.cl 5 | describe BingAdsApi::CampaignManagement do 6 | 7 | let(:default_options) do 8 | { 9 | environment: :sandbox, 10 | username: "ruby_bing_ads_sbx", 11 | password: "sandbox123", 12 | developer_token: "BBD37VB98", 13 | customer_id: "21025739", 14 | account_id: "8506945" 15 | } 16 | end 17 | let(:service) { BingAdsApi::CampaignManagement.new(default_options) } 18 | 19 | it "should initialize with options" do 20 | new_service = BingAdsApi::CampaignManagement.new(default_options) 21 | expect(new_service).not_to be_nil 22 | end 23 | 24 | describe "targets operations" do 25 | 26 | it "should add city target" do 27 | target = BingAdsApi::Target.new( 28 | location: BingAdsApi::LocationTarget.new( 29 | city_target: BingAdsApi::CityTarget.new( 30 | bids: [ 31 | city_target_bid: BingAdsApi::CityTargetBid.new( 32 | bid_adjustment: 0, 33 | city: 'Sydney, NS AU', 34 | is_excluded: false 35 | )] 36 | ) 37 | ) 38 | ) 39 | response = service.add_targets_to_library(default_options[:account_id], target) 40 | expect(response[:target_ids][:long]).not_to be_nil 41 | end 42 | 43 | it "should add radius target" do 44 | target = BingAdsApi::Target.new( 45 | location: BingAdsApi::LocationTarget.new( 46 | radius_target: BingAdsApi::RadiusTarget.new( 47 | bids: [ 48 | radius_target_bid: BingAdsApi::RadiusTargetBid.new( 49 | bid_adjustment: 0, 50 | id: nil, 51 | is_excluded: false, 52 | latitude_degrees: 35.575156, 53 | longitude_degrees: -77.398931, 54 | radius: 20, 55 | radius_unit: BingAdsApi::DistanceUnits::MILES 56 | )] 57 | ) 58 | ) 59 | ) 60 | response = service.add_targets_to_library(default_options[:account_id], target) 61 | expect(response[:target_ids][:long]).not_to be_nil 62 | end 63 | 64 | end 65 | 66 | describe "campaign operations" do 67 | 68 | it "should add a campaign" do 69 | name = "Test Campaign #{SecureRandom.uuid}" 70 | campaigns = [ 71 | BingAdsApi::Campaign.new( 72 | budget_type: BingAdsApi::Campaign::DAILY_BUDGET_STANDARD, 73 | daily_budget: 2000, 74 | daylight_saving: "false", 75 | description: name + " description", 76 | name: name + " name", 77 | time_zone: BingAdsApi::Campaign::SANTIAGO 78 | ) 79 | ] 80 | response = service.add_campaigns(default_options[:account_id], campaigns) 81 | 82 | expect(response[:campaign_ids][:long]).not_to be_nil 83 | end 84 | 85 | it "should get campaigns by account when there are no campaigns" do 86 | # clean all campaigns first 87 | campaign_ids = service.get_campaigns_by_account_id(default_options[:account_id]).map(&:id) 88 | campaign_ids.each_slice(100) do |ids| 89 | service.delete_campaigns(default_options[:account_id], ids) 90 | end 91 | 92 | response = service.get_campaigns_by_account_id(default_options[:account_id]) 93 | expect(response).to be_kind_of(Array) 94 | expect(response).to be_empty 95 | end 96 | 97 | context "when a campaign has already been created" do 98 | 99 | before :each do 100 | @campaign_id = BingAdsFactory.create_campaign 101 | end 102 | 103 | it "should get campaigns by account when there's only one campaign" do 104 | response = service.get_campaigns_by_account_id(default_options[:account_id]) 105 | expect(response).not_to be_nil 106 | expect(response).to be_kind_of(Array) 107 | end 108 | 109 | it "should get campaigns by account when there are multiple campaigns" do 110 | BingAdsFactory.create_campaign 111 | response = service.get_campaigns_by_account_id(default_options[:account_id]) 112 | expect(response).not_to be_nil 113 | expect(response).to be_kind_of(Array) 114 | end 115 | 116 | it "should update campaigns" do 117 | name = "Test Campaign Update #{SecureRandom.uuid}" 118 | campaigns = [ 119 | BingAdsApi::Campaign.new( 120 | id: @campaign_id, 121 | budget_type: BingAdsApi::Campaign::DAILY_BUDGET_STANDARD, 122 | daily_budget: 2000, 123 | daylight_saving: "false", 124 | description: name + " description", 125 | name: name + " name", 126 | time_zone: BingAdsApi::Campaign::SANTIAGO 127 | ) 128 | ] 129 | 130 | response = service.update_campaigns(default_options[:account_id], campaigns) 131 | 132 | expect(response).not_to be_nil 133 | end 134 | 135 | it "should delete campaigns" do 136 | campaign_ids = [@campaign_id] 137 | 138 | response = service.delete_campaigns(default_options[:account_id], campaign_ids) 139 | 140 | expect(response).not_to be_nil 141 | end 142 | 143 | end 144 | end 145 | 146 | describe "ad group operations" do 147 | 148 | before :each do 149 | @campaign_id = BingAdsFactory.create_campaign 150 | end 151 | 152 | it "should add an ad group" do 153 | name = "Ad Group #{SecureRandom.uuid}" 154 | ad_groups = [ 155 | BingAdsApi::AdGroup.new( 156 | ad_distribution: BingAdsApi::AdGroup::SEARCH, 157 | language: BingAdsApi::AdGroup::SPANISH, 158 | name: name + " name", 159 | pricing_model: BingAdsApi::AdGroup::CPC, 160 | bidding_model: BingAdsApi::AdGroup::KEYWORD 161 | ) 162 | ] 163 | response = service.add_ad_groups(@campaign_id, ad_groups) 164 | 165 | expect(response[:ad_group_ids][:long]).not_to be_nil 166 | end 167 | 168 | it "should get ad groups by campaign when there are no ad groups" do 169 | response = service.get_ad_groups_by_campaign_id(@campaign_id) 170 | expect(response).to be_kind_of(Array) 171 | expect(response).to be_empty 172 | end 173 | 174 | context "when an ad group has already been created" do 175 | 176 | before :each do 177 | @ad_group_id = BingAdsFactory.create_ad_group(@campaign_id) 178 | end 179 | 180 | it "should get ad groups by campaign when there's only one ad group" do 181 | response = service.get_ad_groups_by_campaign_id(@campaign_id) 182 | 183 | expect(response).not_to be_empty 184 | expect(response).to be_kind_of(Array) 185 | end 186 | 187 | it "should get ad groups by campaign when there are multiple ad groups" do 188 | BingAdsFactory.create_ad_group(@campaign_id) 189 | response = service.get_ad_groups_by_campaign_id(@campaign_id) 190 | 191 | expect(response).not_to be_empty 192 | expect(response).to be_kind_of(Array) 193 | end 194 | 195 | it "should get ad groups by ids when there's only one ad group" do 196 | response = service.get_ad_groups_by_ids(@campaign_id, [@ad_group_id]) 197 | 198 | expect(response).not_to be_nil 199 | expect(response.size).to eq(1) 200 | end 201 | 202 | it "should get ad groups by ids when there are multiple ad groups" do 203 | ad_group_id_2 = BingAdsFactory.create_ad_group(@campaign_id) 204 | response = service.get_ad_groups_by_ids(@campaign_id, [@ad_group_id, ad_group_id_2]) 205 | 206 | expect(response).not_to be_nil 207 | expect(response.size).to eq(2) 208 | end 209 | 210 | it "should update ad groups" do 211 | name = "Test Ad Group Update #{SecureRandom.uuid}" 212 | ad_groups = [ 213 | BingAdsApi::AdGroup.new( 214 | id: @ad_group_id, 215 | name: name 216 | ) 217 | ] 218 | 219 | response = service.update_ad_groups(@campaign_id, ad_groups) 220 | end 221 | 222 | end 223 | end 224 | 225 | describe "ad operations" do 226 | 227 | before :each do 228 | campaign_id = BingAdsFactory.create_campaign 229 | @ad_group_id = BingAdsFactory.create_ad_group(campaign_id) 230 | end 231 | 232 | describe "expanded text ads" do 233 | 234 | it "should add a single ad" do 235 | 236 | text_ad = BingAdsApi::ExpandedTextAd.new( 237 | final_urls: {'ins1:string'=> 'http://test.com'}, 238 | status: BingAdsApi::Ad::ACTIVE, 239 | text: 'Expanded Text Ad', 240 | title_part_1: 'Expanded Title', 241 | title_part_2: 'Expanded Title 2', 242 | path_1: 'test', 243 | path_2: 'test' 244 | ) 245 | 246 | response = service.add_ads(@ad_group_id, text_ad) 247 | expect(response).not_to be_nil 248 | expect(response[:partial_errors]).to be_nil 249 | expect(response[:ad_ids][:long]).not_to be_nil 250 | end 251 | end 252 | 253 | 254 | describe "text ads" do 255 | 256 | it "should add a single ad" do 257 | text_ad = BingAdsApi::TextAd.new( 258 | status: BingAdsApi::Ad::ACTIVE, 259 | destination_url: "http://www.adxion.com", 260 | display_url: "AdXion.com", 261 | text: "Text Ad #{SecureRandom.uuid}", 262 | title: "Text Ad" 263 | ) 264 | 265 | response = service.add_ads(@ad_group_id, text_ad) 266 | expect(response).not_to be_nil 267 | expect(response[:partial_errors]).to be_nil 268 | expect(response[:ad_ids][:long]).not_to be_nil 269 | end 270 | 271 | it "should add multiple ads" do 272 | text_ads = [ 273 | BingAdsApi::TextAd.new( 274 | status: BingAdsApi::Ad::ACTIVE, 275 | destination_url: "http://www.adxion.com", 276 | display_url: "AdXion.com", 277 | text: "TextAd #{SecureRandom.uuid}", 278 | title: "TextAd" 279 | ), 280 | BingAdsApi::TextAd.new( 281 | status: BingAdsApi::Ad::ACTIVE, 282 | destination_url: "http://www.adxion.com", 283 | display_url: "AdXion.com", 284 | text: "TextAd 2 #{SecureRandom.uuid}", 285 | title: "TextAd 2" 286 | ) 287 | ] 288 | 289 | response = service.add_ads(@ad_group_id, text_ads) 290 | expect(response).not_to be_nil 291 | expect(response[:partial_errors]).to be_nil 292 | expect(response[:ad_ids][:long]).not_to be_nil 293 | end 294 | 295 | end 296 | 297 | it "should add an ad with partial errors" do 298 | text_ad = BingAdsApi::TextAd.new( 299 | status: BingAdsApi::Ad::INACTIVE, 300 | destination_url: "http://www.adxion.com", 301 | display_url: "http://www.adxion.com", 302 | text: "Text Ad #{SecureRandom.uuid}", 303 | title: "Text Ad #{SecureRandom.uuid}" 304 | ) 305 | 306 | response = service.add_ads(@ad_group_id, text_ad) 307 | expect(response).not_to be_nil 308 | expect(response[:partial_errors]).not_to be_nil 309 | end 310 | 311 | it "should get ads by ad group id when there are no ads" do 312 | ads = service.get_ads_by_ad_group_id(@ad_group_id) 313 | expect(ads).to be_empty 314 | end 315 | 316 | context "when an ad has already been created" do 317 | 318 | before :each do 319 | @ad_id = BingAdsFactory.create_text_ad(@ad_group_id) 320 | end 321 | 322 | it "should get ads by ad group id when there's only one ad" do 323 | ads = service.get_ads_by_ad_group_id(@ad_group_id) 324 | expect(ads).not_to be_empty 325 | ad = ads.first 326 | expect(ad).to be_kind_of(BingAdsApi::Ad) 327 | end 328 | 329 | it "should get ads by ad group id when there are multiple ads" do 330 | BingAdsFactory.create_text_ad(@ad_group_id) 331 | ads = service.get_ads_by_ad_group_id(@ad_group_id) 332 | expect(ads).not_to be_empty 333 | ads.each do |ad| 334 | expect(ad).to be_kind_of(BingAdsApi::Ad) 335 | end 336 | end 337 | 338 | it "should get ads by ids when there's only one ad" do 339 | ads_by_ids = service.get_ads_by_ids(@ad_group_id, [@ad_id]) 340 | expect(ads_by_ids).not_to be_nil 341 | expect(ads_by_ids.size).to eq(1) 342 | end 343 | 344 | it "should get ads by ids when there are multiple ads" do 345 | ad_id_2 = BingAdsFactory.create_text_ad(@ad_group_id) 346 | ads_by_ids = service.get_ads_by_ids(@ad_group_id, [@ad_id, ad_id_2]) 347 | expect(ads_by_ids).not_to be_nil 348 | expect(ads_by_ids.size).to eq(2) 349 | ads_by_ids.each do |ad| 350 | expect(ad).to be_kind_of(BingAdsApi::Ad) 351 | end 352 | end 353 | 354 | it "should update ads" do 355 | text_ad = BingAdsApi::TextAd.new( 356 | id: @ad_id, 357 | status: BingAdsApi::Ad::ACTIVE, 358 | destination_url: "http://www.adxion.com", 359 | display_url: "http://www.adxion.com", 360 | text: "Ad #{SecureRandom.uuid}", 361 | title: "Title" 362 | ) 363 | 364 | response = service.update_ads(@ad_group_id, [text_ad]) 365 | expect(response).not_to be_nil 366 | expect(response[:partial_errors]).to be_nil 367 | end 368 | 369 | end 370 | end 371 | 372 | describe "keyword operations" do 373 | 374 | before :each do 375 | campaign_id = BingAdsFactory.create_campaign 376 | @ad_group_id = BingAdsFactory.create_ad_group(campaign_id) 377 | end 378 | 379 | it "should add a single keyword" do 380 | keyword = BingAdsApi::Keyword.new( 381 | bid: BingAdsApi::Bid.new(amount: 1.23), 382 | destination_url: "http://www.adxion.com", 383 | match_type: BingAdsApi::Keyword::EXACT, 384 | status: BingAdsApi::Keyword::ACTIVE, 385 | text: "Keyword #{SecureRandom.uuid}" 386 | ) 387 | 388 | response = service.add_keywords(@ad_group_id, keyword) 389 | expect(response).not_to be_nil 390 | expect(response[:partial_errors]).to be_nil 391 | expect(response[:keyword_ids][:long]).not_to be_nil 392 | end 393 | 394 | it "should add multiple keywords" do 395 | keywords = [ 396 | BingAdsApi::Keyword.new( 397 | bid: BingAdsApi::Bid.new(amount: 1.23), 398 | destination_url: "http://www.adxion.com", 399 | match_type: BingAdsApi::Keyword::EXACT, 400 | status: BingAdsApi::Keyword::ACTIVE, 401 | text: "Keyword #{SecureRandom.uuid}" 402 | ), 403 | BingAdsApi::Keyword.new( 404 | bid: BingAdsApi::Bid.new(amount: 1.23), 405 | destination_url: "http://www.adxion.com", 406 | match_type: BingAdsApi::Keyword::EXACT, 407 | status: BingAdsApi::Keyword::ACTIVE, 408 | text: "Keyword #{SecureRandom.uuid}" 409 | ) 410 | ] 411 | 412 | response = service.add_keywords(@ad_group_id, keywords) 413 | expect(response).not_to be_nil 414 | expect(response[:partial_errors]).to be_nil 415 | expect(response[:keyword_ids][:long]).not_to be_nil 416 | end 417 | 418 | it "should add a keyword with partial errors" do 419 | keyword = BingAdsApi::Keyword.new( 420 | # missing bid 421 | destination_url: "http://www.bing.com/", 422 | match_type: BingAdsApi::Keyword::EXACT, 423 | status: BingAdsApi::Keyword::ACTIVE, 424 | text: "Keyword #{SecureRandom.uuid}" 425 | ) 426 | 427 | response = service.add_keywords(@ad_group_id, keyword) 428 | expect(response).not_to be_nil 429 | expect(response[:partial_errors]).not_to be_nil 430 | end 431 | 432 | it "should get keywords by ad group id when there are no keywords" do 433 | keywords = service.get_keywords_by_ad_group_id(@ad_group_id) 434 | expect(keywords).to be_empty 435 | end 436 | 437 | context "when a keyword has already been created" do 438 | 439 | before :each do 440 | @keyword_id = BingAdsFactory.create_keyword(@ad_group_id) 441 | end 442 | 443 | it "should get keywords by ad group id when there's only one keyword" do 444 | keywords = service.get_keywords_by_ad_group_id(@ad_group_id) 445 | expect(keywords).not_to be_empty 446 | keyword = keywords.first 447 | expect(keyword).to be_kind_of(BingAdsApi::Keyword) 448 | end 449 | 450 | it "should get keywords by ad group id when there are multiple keywords" do 451 | BingAdsFactory.create_keyword(@ad_group_id) 452 | keywords = service.get_keywords_by_ad_group_id(@ad_group_id) 453 | expect(keywords).not_to be_empty 454 | keywords.each do |keyword| 455 | expect(keyword).to be_kind_of(BingAdsApi::Keyword) 456 | end 457 | end 458 | 459 | it "should get keywords by ids when there's only one keyword" do 460 | keywords_by_ids = service.get_keywords_by_ids(@ad_group_id, [@keyword_id]) 461 | expect(keywords_by_ids).not_to be_nil 462 | expect(keywords_by_ids.size).to eq(1) 463 | end 464 | 465 | it "should get keywords by ids when there are multiple keywords" do 466 | keyword_id_2 = BingAdsFactory.create_keyword(@ad_group_id) 467 | keywords_by_ids = service.get_keywords_by_ids(@ad_group_id, [@keyword_id, keyword_id_2]) 468 | expect(keywords_by_ids).not_to be_nil 469 | expect(keywords_by_ids.size).to eq(2) 470 | keywords_by_ids.each do |keyword| 471 | expect(keyword).to be_kind_of(BingAdsApi::Keyword) 472 | end 473 | end 474 | 475 | it "should update keywords" do 476 | keyword = BingAdsApi::Keyword.new( 477 | id: @keyword_id, 478 | bid: BingAdsApi::Bid.new(amount: 99.99) 479 | ) 480 | 481 | response = service.update_keywords(@ad_group_id, [keyword]) 482 | expect(response).not_to be_nil 483 | expect(response[:partial_errors]).to be_nil 484 | end 485 | 486 | end 487 | end 488 | end 489 | --------------------------------------------------------------------------------