diff --git a/lib/mindbody-api.rb b/lib/mindbody-api.rb index ef100cb..7d77635 100644 --- a/lib/mindbody-api.rb +++ b/lib/mindbody-api.rb @@ -18,13 +18,16 @@ def configuration end class Config - attr_accessor :log_level, :open_timeout, :read_timeout, :source_name, :source_key, :site_ids + attr_accessor :log_level, :open_timeout, :read_timeout, :source_name, :source_key, :site_ids, :username, :password, :prod_mode def initialize @log_level = :debug @source_name = ENV['MINDBODY_SOURCE_NAME'] || '' @source_key = ENV['MINDBODY_SOURCE_KEY'] || '' @site_ids = (ENV['MINDBODY_SITE_IDS'] || '').scan(/-?\d+/).map(&:to_i) + @username = ENV['MINDBODY_USERNAME'] || nil + @password = ENV['MINDBODY_PASSWORD'] || nil + @prod_mode = ENV['MINDBODY_PROD_MODE'] || false end # Make sure site_ids is always an Array diff --git a/lib/mindbody-api/client.rb b/lib/mindbody-api/client.rb index 6d17444..9553807 100644 --- a/lib/mindbody-api/client.rb +++ b/lib/mindbody-api/client.rb @@ -10,9 +10,13 @@ def call(operation_name, locals = {}, &block) @globals.open_timeout(MindBody.configuration.open_timeout) @globals.read_timeout(MindBody.configuration.read_timeout) @globals.log_level(MindBody.configuration.log_level) + + auth = locals[:auth] + locals.delete(:auth) + locals = locals.has_key?(:message) ? locals[:message] : locals locals = fixup_locals(locals) - params = {:message => {'Request' => auth_params.merge(locals)}} + params = {:message => {'Request' => auth_params(auth).merge(locals)}} # Run the request response = super(operation_name, params, &block) @@ -20,10 +24,41 @@ def call(operation_name, locals = {}, &block) end private - def auth_params - {'SourceCredentials'=>{'SourceName'=>MindBody.configuration.source_name, - 'Password'=>MindBody.configuration.source_key, - 'SiteIDs'=>{'int'=>MindBody.configuration.site_ids}}} + def auth_params(auth = nil) + auth ||= + { + :source_name => MindBody.configuration.source_name, + :source_key => MindBody.configuration.source_key, + :username => MindBody.configuration.username, + :password => MindBody.configuration.password, + + :site_ids => MindBody.configuration.site_ids + } + + params = + { + 'SourceCredentials' => { + 'SourceName' => auth[:source_name], + 'Password' => auth[:source_key], + 'SiteIDs' => { + 'int' => auth[:site_ids] + } + } + } + + if (auth[:username] && auth[:password]) + params['UserCredentials'] = + { + 'Username' => auth[:username], + 'Password' => auth[:password], + 'SiteIDs' => { + 'int' => auth[:site_ids] + } + } + end + + params.merge!({"Test" => true}) unless MindBody.configuration.prod_mode + params end def fixup_locals(locals) diff --git a/lib/mindbody-api/ext/savon_ext.rb b/lib/mindbody-api/ext/savon_ext.rb index d9ed2f4..41606ca 100644 --- a/lib/mindbody-api/ext/savon_ext.rb +++ b/lib/mindbody-api/ext/savon_ext.rb @@ -15,9 +15,23 @@ def #{operation.to_s.snakecase}(#{params.join(',')}) .join(',')} } locals ||= {} + client.call #{operation.inspect}, :message => locals.merge(req_hash) end RUBY_EVAL + + class_operation_module.module_eval <<-RUBY_EVAL, __FILE__, __LINE__+1 + def #{operation.to_s.snakecase}_with_auth(auth#{!params.empty? ? ',' : ''} #{params.join(',')}) + req_hash = { + #{params.select{|p| p != 'locals = {}'} + .map{|p| "'#{params_key(p)}' => #{p}"} + .join(',')} + } + locals ||= {} + + client.call #{operation.inspect}, :auth => auth, :message => locals.merge(req_hash) + end + RUBY_EVAL end # Defines an instance-level SOAP operation. @@ -31,6 +45,12 @@ def #{operation.to_s.snakecase}(#{params.join(',')}) self.class.#{operation.to_s.snakecase} #{params.join(',').chomp(' = {}')} end RUBY_EVAL + + instance_operation_module.module_eval <<-RUBY_EVAL, __FILE__, __LINE__+1 + def #{operation.to_s.snakecase}_with_auth(auth#{!params.empty? ? ',' : ''} #{params.join(',')}) + self.class.#{operation.to_s.snakecase}_with_auth(auth, #{params.join(',').chomp(' = {}')}) + end + RUBY_EVAL end # Builds the array of params the diff --git a/lib/mindbody-api/models.rb b/lib/mindbody-api/models.rb index e8c4a3e..12685a6 100644 --- a/lib/mindbody-api/models.rb +++ b/lib/mindbody-api/models.rb @@ -8,6 +8,7 @@ class Base end end +require 'mindbody-api/models/resource' require 'mindbody-api/models/location' require 'mindbody-api/models/client' require 'mindbody-api/models/schedule_type' @@ -29,6 +30,6 @@ class Base require 'mindbody-api/models/sale' require 'mindbody-api/models/sale_item' require 'mindbody-api/models/service' -require 'mindbody-api/models/resource' require 'mindbody-api/models/appointment_status' require 'mindbody-api/models/appointment' +require 'mindbody-api/models/waitlist_entry' \ No newline at end of file diff --git a/lib/mindbody-api/models/class.rb b/lib/mindbody-api/models/class.rb index 4c54e87..3258587 100644 --- a/lib/mindbody-api/models/class.rb +++ b/lib/mindbody-api/models/class.rb @@ -22,6 +22,7 @@ class Class < Base attribute :end_date_time, DateTime attribute :class_description, ClassDescription attribute :staff, Staff + attribute :resource, Resource def name class_description.name diff --git a/lib/mindbody-api/models/client.rb b/lib/mindbody-api/models/client.rb index 96f6bf7..302e69b 100644 --- a/lib/mindbody-api/models/client.rb +++ b/lib/mindbody-api/models/client.rb @@ -5,6 +5,7 @@ class Client < Base attribute :first_name, String attribute :last_name, String attribute :gender, String + attribute :account_balance, Float attribute :email, String attribute :email_opt_in, Boolean attribute :address_line1, String @@ -23,6 +24,10 @@ class Client < Base attribute :photo_url, String attribute :username, String attribute :first_appointment_date, DateTime + attribute :emergency_contact_info_name, String + attribute :emergency_contact_info_relationship, String + attribute :emergency_contact_info_phone, String + attribute :emergency_contact_info_email, String def name "#{first_name} #{last_name}" diff --git a/lib/mindbody-api/models/visit.rb b/lib/mindbody-api/models/visit.rb index c8edc91..b7f4827 100644 --- a/lib/mindbody-api/models/visit.rb +++ b/lib/mindbody-api/models/visit.rb @@ -12,6 +12,7 @@ class Visit < Base attribute :web_signup, Boolean attribute :signed_in, Boolean attribute :make_up, Boolean + attribute :late_cancelled, Boolean attribute :service, ClientService end end diff --git a/lib/mindbody-api/models/waitlist_entry.rb b/lib/mindbody-api/models/waitlist_entry.rb new file mode 100644 index 0000000..8a6cb13 --- /dev/null +++ b/lib/mindbody-api/models/waitlist_entry.rb @@ -0,0 +1,11 @@ +module MindBody + module Models + class WaitlistEntry < Base + attribute :id, Integer + attribute :class_id, Integer + attribute :class_date, DateTime + attribute :client, Client + attribute :class_schedule, ClassSchedule + end + end +end \ No newline at end of file diff --git a/lib/mindbody-api/response.rb b/lib/mindbody-api/response.rb index 8157c6c..559141e 100644 --- a/lib/mindbody-api/response.rb +++ b/lib/mindbody-api/response.rb @@ -67,9 +67,11 @@ def normalize_hash(hash) hash[key] = normalize_hash(value) key_singular = map_key(key).singularize.to_sym + key_plural = key.to_s.pluralize.to_sym if value.has_key?(key_singular) - hash[key] = value[key_singular] + val = value[key_singular] + hash[key] = (val.is_a?(Array)) ? val : [val] elsif value.has_key?(:string) hash[key] = value[:string] elsif value.has_key?(:int) diff --git a/lib/mindbody-api/services/class_service.rb b/lib/mindbody-api/services/class_service.rb index 63ffbaf..170a5b9 100644 --- a/lib/mindbody-api/services/class_service.rb +++ b/lib/mindbody-api/services/class_service.rb @@ -4,9 +4,12 @@ class ClassService < Service service "ClassService" operation :get_classes - operation :get_class_visits, required:[:class_id] + operation :get_class_visits, required:[:class_id] operation :get_class_descriptions operation :get_class_schedules + operation :get_waitlist_entries, required: [:class_ids] + operation :add_clients_to_classes, required: [:class_ids, :client_ids] + operation :remove_clients_from_classes, required: [:class_ids, :client_ids] end end end diff --git a/lib/mindbody-api/services/sale_service.rb b/lib/mindbody-api/services/sale_service.rb index 50e7c05..b5beeea 100644 --- a/lib/mindbody-api/services/sale_service.rb +++ b/lib/mindbody-api/services/sale_service.rb @@ -5,6 +5,7 @@ class SaleService < Service operation :get_accepted_card_type, :locals => false operation :get_sales + operation :checkout_shopping_cart, :required => [:client_id, :cart_items, :payments] operation :get_services operation :get_packages operation :get_products diff --git a/mindbody-api.gemspec b/mindbody-api.gemspec index 4d3a621..2cde5e3 100644 --- a/mindbody-api.gemspec +++ b/mindbody-api.gemspec @@ -26,4 +26,5 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'simplecov', '~> 0.8.2' gem.add_development_dependency 'guard-rspec', '~> 4.2.8' gem.add_development_dependency 'rake', '~> 10.0.3' + gem.add_development_dependency 'pry' end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index e6e22de..a0be5ce 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -7,6 +7,11 @@ creds.stub(:source_name).and_return('test') creds.stub(:source_key).and_return('test_key') creds.stub(:site_ids).and_return([-99]) + creds.stub(:open_timeout).and_return(nil) + creds.stub(:read_timeout).and_return(nil) + creds.stub(:username).and_return(nil) + creds.stub(:password).and_return(nil) + creds.stub(:prod_mode).and_return(true) MindBody.stub(:configuration).and_return(creds) @client = MindBody::Services::Client.new(:wsdl => 'spec/fixtures/wsdl/geotrust.wsdl') @@ -30,6 +35,58 @@ } }}} end + + # We use #send here because `auth_params` is a private method and when + # adding this functionality I didn't want to change the public API contract. + # This should be considered an external-facing unit test as calls to + # {#call} now respect `auth:` alongside `:message`. + describe " (when using `auth:`)" do + it 'should inject MindBody.configuration values for the nil case' do + ret = subject.send(:auth_params, nil) + + expect(ret["SourceCredentials"]["SourceName"]).to eq('test') + expect(ret["SourceCredentials"]["Password"]).to eq('test_key') + expect(ret["SourceCredentials"]["SiteIDs"]["int"]).to eq([-99]) + expect(ret["UserCredentials"]).to be_nil + end + + it 'should defer to passed-in values when provided' do + auth = { + :source_name => "override_name", + :source_key => "override_key", + :site_ids => [ -105 ] + } + + ret = subject.send(:auth_params, auth) + + expect(ret["SourceCredentials"]["SourceName"]).to eq('override_name') + expect(ret["SourceCredentials"]["Password"]).to eq('override_key') + expect(ret["SourceCredentials"]["SiteIDs"]["int"]).to eq([-105]) + expect(ret["UserCredentials"]).to be_nil + end + + it 'should inject UserCredentials when provided' do + auth = { + :source_name => "override_name", + :source_key => "override_key", + + :username => "override_user", + :password => "override_pass", + + :site_ids => [ -105 ] + } + + ret = subject.send(:auth_params, auth) + + expect(ret["SourceCredentials"]["SourceName"]).to eq('override_name') + expect(ret["SourceCredentials"]["Password"]).to eq('override_key') + expect(ret["SourceCredentials"]["SiteIDs"]["int"]).to eq([-105]) + expect(ret["UserCredentials"]["Username"]).to eq('override_user') + expect(ret["UserCredentials"]["Password"]).to eq('override_pass') + expect(ret["UserCredentials"]["SiteIDs"]["int"]).to eq([-105]) + end + end + it 'should inject the auth params' do Savon::Operation.any_instance.should_receive(:call).once.with(@locals) subject.call(:hello) diff --git a/spec/service_spec.rb b/spec/service_spec.rb index fe88b38..38ae890 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -30,11 +30,18 @@ MindBody::Services::Service.operation :test end - it { should respond_to(:test) } - its(:new) { should respond_to(:test) } + it do + should respond_to(:test) + should respond_to(:test_with_auth) + end + its(:new) do + should respond_to(:test) + should respond_to(:test_with_auth) + end it 'should have optional locals' do expect(subject.method(:test).arity).to eql(-1) + expect(subject.method(:test_with_auth).arity).to eql(-2) end it 'should delegate to the class method' do @@ -42,8 +49,10 @@ locals = {:foo => :bar} subject.should_receive(:test).with(locals).once - instance.test(locals) + + subject.should_receive(:test_with_auth).with({}, locals).once + instance.test_with_auth({}, locals) end end @@ -52,11 +61,18 @@ MindBody::Services::Service.operation :test_required, :required => [:foo, :bar] end - it { should respond_to(:test_required) } - its(:new) { should respond_to(:test_required) } + it do + should respond_to(:test_required) + should respond_to(:test_required_with_auth) + end + its(:new) do + should respond_to(:test_required) + should respond_to(:test_required_with_auth) + end it 'should have two required params and optional locals' do expect(subject.method(:test_required).arity).to eql(-3) + expect(subject.method(:test_required_with_auth).arity).to eql(-4) end it 'should delegate to the class method' do @@ -64,11 +80,14 @@ locals = {:foo => :bar} subject.should_receive(:test_required).with(:foo, :bar, locals).once - instance.test_required(:foo, :bar, locals) + + subject.should_receive(:test_required_with_auth).with({}, :foo, :bar, locals).once + instance.test_required_with_auth({}, :foo, :bar, locals) end it 'should require two params' do + expect{subject.send(:test_required_with_auth, {})}.to raise_error(ArgumentError) expect{subject.send(:test_required)}.to raise_error(ArgumentError) end end @@ -83,21 +102,26 @@ it 'should have two required params and optional locals' do expect(subject.method(:test_no_locals).arity).to eql(2) + expect(subject.method(:test_no_locals_with_auth).arity).to eql(3) end it 'should delegate to the class method' do instance = subject.new subject.should_receive(:test_no_locals).with(:foo, :bar).once - instance.test_no_locals(:foo, :bar) + + subject.should_receive(:test_no_locals_with_auth).with({}, :foo, :bar).once + instance.test_no_locals_with_auth({}, :foo, :bar) end it 'should require two params' do + expect{subject.send(:test_no_locals_with_auth, {})}.to raise_error(ArgumentError) expect{subject.send(:test_no_locals)}.to raise_error(ArgumentError) end it 'should not allow locals' do + expect{subject.send(:test_no_locals_with_auth, {}, :foo, :bar, :foobar => 'foobar')}.to raise_error(ArgumentError) expect{subject.send(:test_no_locals, :foo, :bar, :foobar => 'foobar')}.to raise_error(ArgumentError) end end