booker_ruby 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/lib/booker/booker.rb +5 -0
  3. data/lib/booker/business_client.rb +32 -0
  4. data/lib/booker/business_rest.rb +89 -0
  5. data/lib/booker/client.rb +182 -0
  6. data/lib/booker/common_rest.rb +38 -0
  7. data/lib/booker/customer_client.rb +31 -0
  8. data/lib/booker/customer_rest.rb +43 -0
  9. data/lib/booker/errors.rb +19 -0
  10. data/lib/booker/generic_token_store.rb +25 -0
  11. data/lib/booker/models/address.rb +12 -0
  12. data/lib/booker/models/appointment.rb +77 -0
  13. data/lib/booker/models/appointment_treatment.rb +51 -0
  14. data/lib/booker/models/available_time.rb +13 -0
  15. data/lib/booker/models/business_type.rb +5 -0
  16. data/lib/booker/models/category.rb +5 -0
  17. data/lib/booker/models/class_instance.rb +25 -0
  18. data/lib/booker/models/country.rb +5 -0
  19. data/lib/booker/models/current_price.rb +5 -0
  20. data/lib/booker/models/customer.rb +51 -0
  21. data/lib/booker/models/customer_2.rb +5 -0
  22. data/lib/booker/models/customer_record_type.rb +5 -0
  23. data/lib/booker/models/discount.rb +5 -0
  24. data/lib/booker/models/dynamic_price.rb +11 -0
  25. data/lib/booker/models/employee.rb +10 -0
  26. data/lib/booker/models/final_total.rb +5 -0
  27. data/lib/booker/models/gender.rb +5 -0
  28. data/lib/booker/models/location.rb +18 -0
  29. data/lib/booker/models/model.rb +123 -0
  30. data/lib/booker/models/online_booking_settings.rb +23 -0
  31. data/lib/booker/models/original_price.rb +5 -0
  32. data/lib/booker/models/payment_method.rb +5 -0
  33. data/lib/booker/models/preferred_staff_gender.rb +5 -0
  34. data/lib/booker/models/price.rb +8 -0
  35. data/lib/booker/models/receipt_display_price.rb +5 -0
  36. data/lib/booker/models/room.rb +12 -0
  37. data/lib/booker/models/shipping_address.rb +5 -0
  38. data/lib/booker/models/source.rb +5 -0
  39. data/lib/booker/models/spa.rb +5 -0
  40. data/lib/booker/models/spa_employee_availability_search_item.rb +11 -0
  41. data/lib/booker/models/status.rb +5 -0
  42. data/lib/booker/models/sub_category.rb +5 -0
  43. data/lib/booker/models/tag_price.rb +5 -0
  44. data/lib/booker/models/teacher.rb +5 -0
  45. data/lib/booker/models/teacher_2.rb +5 -0
  46. data/lib/booker/models/time_zone.rb +8 -0
  47. data/lib/booker/models/treatment.rb +19 -0
  48. data/lib/booker/models/type.rb +8 -0
  49. data/lib/booker/models/user.rb +73 -0
  50. data/lib/booker/version.rb +3 -0
  51. data/lib/booker_ruby.rb +86 -0
  52. metadata +192 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8b39923daabcfec1bb62dd8c6c0a479249cd6846
4
+ data.tar.gz: cd5967c5745415270ea1d9d16f53b58b2a5d11f0
5
+ SHA512:
6
+ metadata.gz: bc9b5ff26181dcb216b03755ac25467e87ee928a9156a6f726692bf741bbfd5ccb1bfc321076aa8f1cd68283d53e9fe6e3d9f0f4aa0fa38f1ac634717f2f1867
7
+ data.tar.gz: 3013213b0f4c4d360ddabbf8877b1d68a079bbc24a0872c9a3c763e20410293c8cfff9ea82607784086f27637ab8d82c0bdcf5d6273984df874d2f17ded0b6e9
@@ -0,0 +1,5 @@
1
+ module Booker
2
+ def self.config
3
+ @config ||= {}
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ module Booker
2
+ class BusinessClient < Client
3
+ include Booker::BusinessREST
4
+
5
+ attr_accessor :booker_account_name, :booker_username, :booker_password
6
+
7
+ def initialize(options={})
8
+ self.base_url = ENV['BOOKER_BUSINESS_SERVICE_URL'] || 'https://apicurrent-app.booker.ninja/webservice4/json/BusinessService.svc'
9
+ super
10
+ end
11
+
12
+ def get_access_token
13
+ http_options = {
14
+ client_id: self.client_id,
15
+ client_secret: self.client_secret,
16
+ 'AccountName' => self.booker_account_name,
17
+ 'UserName' => self.booker_username,
18
+ 'Password' => self.booker_password
19
+ }
20
+ response = post('/accountlogin', http_options, nil).parsed_response
21
+
22
+ raise Booker::InvalidApiCredentials.new(http_options, response) unless response.present?
23
+
24
+ self.temp_access_token_expires_at = Time.now + response['expires_in'].to_i.seconds
25
+ self.temp_access_token = response['access_token']
26
+
27
+ update_token_store
28
+
29
+ self.temp_access_token
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,89 @@
1
+ module Booker
2
+ module BusinessREST
3
+ include CommonREST
4
+
5
+ def get_logged_in_user
6
+ response = get('/user', build_params)
7
+ result = Booker::Models::User.from_hash(response['User'])
8
+ result.LocationID = response['LocationID']
9
+ result.BrandID = response['BrandID']
10
+ result
11
+ end
12
+
13
+ def get_location(booker_location_id:)
14
+ response = get("/location/#{booker_location_id}", build_params)
15
+ Booker::Models::Location.from_hash(response)
16
+ end
17
+
18
+ def find_locations(params: {})
19
+ paginated_request(
20
+ method: :post,
21
+ path: '/locations',
22
+ params: build_params({}, params, true),
23
+ model: Booker::Models::Location
24
+ )
25
+ end
26
+
27
+ def find_employees(booker_location_id:, fetch_all: true, params: {})
28
+ paginated_request(
29
+ method: :post,
30
+ path: '/employees',
31
+ params: build_params({'LocationID' => booker_location_id}, params, true),
32
+ model: Booker::Models::Employee,
33
+ fetch_all: fetch_all
34
+ )
35
+ end
36
+
37
+ def find_treatments(booker_location_id:, fetch_all: true, params: {})
38
+ paginated_request(
39
+ method: :post,
40
+ path: '/treatments',
41
+ params: build_params({'LocationID' => booker_location_id}, params, true),
42
+ model: Booker::Models::Treatment,
43
+ fetch_all: fetch_all
44
+ )
45
+ end
46
+
47
+ def find_customers(booker_location_id:, fetch_all: true, params: {})
48
+ additional_params = {
49
+ 'FilterByExactLocationID' => true,
50
+ 'LocationID' => booker_location_id,
51
+ 'CustomerRecordType' => 1,
52
+ }
53
+
54
+ paginated_request(
55
+ method: :post,
56
+ path: '/customers',
57
+ params: build_params(additional_params, params, true),
58
+ model: Booker::Models::Customer,
59
+ fetch_all: fetch_all
60
+ )
61
+ end
62
+
63
+ def find_appointments(booker_location_id:, start_at:, end_at:, fetch_all: true, params: {})
64
+ additional_params = {
65
+ 'LocationID' => booker_location_id,
66
+ 'FromStartDate' => start_at,
67
+ 'ToStartDate' => end_at
68
+ }
69
+
70
+ paginated_request(
71
+ method: :post,
72
+ path: '/appointments',
73
+ params: build_params(additional_params, params, true),
74
+ model: Booker::Models::Appointment,
75
+ fetch_all: fetch_all
76
+ )
77
+ end
78
+
79
+ def create_special(booker_location_id:, start_date:, end_date:, coupon_code:, name:, params: {})
80
+ post('/special', build_params({
81
+ 'LocationID' => booker_location_id,
82
+ 'ApplicableStartDate' => start_date.in_time_zone,
83
+ 'ApplicableEndDate' => end_date.in_time_zone,
84
+ 'CouponCode' => coupon_code,
85
+ 'Name' => name
86
+ }, params))
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,182 @@
1
+ module Booker
2
+ class Client
3
+ attr_accessor :base_url, :client_id, :client_secret, :temp_access_token, :temp_access_token_expires_at, :token_store, :token_store_callback_method
4
+
5
+ TimeZone = 'Eastern Time (US & Canada)'.freeze
6
+
7
+ def initialize(options = {})
8
+ options.each do |key, value|
9
+ send(:"#{key}=", value)
10
+ end
11
+ end
12
+
13
+ def get(path, params, booker_model=nil)
14
+ booker_resources = get_booker_resources(:get, path, params, nil, booker_model)
15
+
16
+ build_resources(booker_resources, booker_model)
17
+ end
18
+
19
+ def post(path, data, booker_model=nil)
20
+ booker_resources = get_booker_resources(:post, path, nil, data.to_json, booker_model)
21
+
22
+ build_resources(booker_resources, booker_model)
23
+ end
24
+
25
+ def put(path, data, booker_model=nil)
26
+ booker_resources = get_booker_resources(:put, path, nil, data.to_json, booker_model)
27
+
28
+ build_resources(booker_resources, booker_model)
29
+ end
30
+
31
+ def paginated_request(method:, path:, params:, model: nil, fetched: [], fetch_all: true)
32
+ page_size = params['PageSize']
33
+ page_number = params['PageNumber']
34
+
35
+ if page_size.nil? || page_size < 1 || page_number.nil? || page_number < 1 || !params['UsePaging']
36
+ raise ArgumentError, 'params must include valid PageSize, PageNumber and UsePaging'
37
+ end
38
+
39
+ puts "fetching #{path} with #{params.except('access_token')}. #{fetched.length} results so far."
40
+
41
+ results = self.send(method, path, params, model)
42
+
43
+ unless results.is_a?(Array)
44
+ raise StandardError, "Result from paginated request to #{path} with params: #{params} is not a collection"
45
+ end
46
+
47
+ fetched.concat(results)
48
+ results_length = results.length
49
+
50
+ if fetch_all
51
+ if results_length > 0
52
+ # TODO (#111186744): Add logging to see if any pages with less than expected data (as seen in the /appointments endpoint)
53
+ new_params = params.deep_dup
54
+ new_params['PageNumber'] = page_number + 1
55
+ paginated_request(method: method, path: path, params: new_params, model: model, fetched: fetched)
56
+ else
57
+ fetched
58
+ end
59
+ else
60
+ results
61
+ end
62
+ end
63
+
64
+ def log_issue(message, extra_info = {})
65
+ if (log_message_block = Booker.config[:log_message])
66
+ log_message_block.call(message, extra_info)
67
+ end
68
+ end
69
+
70
+ def get_booker_resources(http_method, path, params=nil, body=nil, booker_model=nil)
71
+ http_options = request_options(params, body)
72
+ puts "BOOKER REQUEST: #{http_method} #{path} #{http_options}" if ENV['BOOKER_API_DEBUG'] == 'true'
73
+
74
+ # Allow it to retry the first time unless it is an authorization error
75
+ begin
76
+ booker_resources = handle_errors!(http_options, HTTParty.send(http_method, "#{self.base_url}#{path}", http_options))
77
+ rescue Booker::Error, Net::ReadTimeout => ex
78
+ if ex.is_a? Booker::InvalidApiCredentials
79
+ raise ex
80
+ else
81
+ sleep 1
82
+ booker_resources = nil
83
+ end
84
+ end
85
+
86
+ if booker_resources
87
+ results_from_response(booker_resources, booker_model)
88
+ else
89
+ booker_resources = handle_errors!(http_options, HTTParty.send(http_method, "#{self.base_url}#{path}", http_options))
90
+
91
+ if booker_resources
92
+ results_from_response(booker_resources, booker_model)
93
+ else
94
+ raise Booker::Error.new(http_options, booker_resources)
95
+ end
96
+ end
97
+ end
98
+
99
+ def handle_errors!(request, response)
100
+ puts "BOOKER RESPONSE: #{response}" if ENV['BOOKER_API_DEBUG'] == 'true'
101
+
102
+ ex = Booker::Error.new(request, response)
103
+ if ex.error.present? || !response.success?
104
+ case ex.error
105
+ when 'invalid_client'
106
+ raise Booker::InvalidApiCredentials.new(request, response)
107
+ when 'invalid access token'
108
+ get_access_token
109
+ return nil
110
+ else
111
+ raise ex
112
+ end
113
+ end
114
+
115
+ response
116
+ end
117
+
118
+ def access_token
119
+ if self.temp_access_token && !temp_access_token_expired?
120
+ self.temp_access_token
121
+ else
122
+ get_access_token
123
+ end
124
+ end
125
+
126
+ private
127
+ def request_options(query=nil, body=nil)
128
+ options = {
129
+ headers: {
130
+ 'Content-Type' => 'application/json; charset=utf-8'
131
+ },
132
+ timeout: 120
133
+ }
134
+
135
+ if body.present?
136
+ options[:body] = body
137
+ end
138
+
139
+ if query.present?
140
+ options[:query] = query
141
+ end
142
+
143
+ options
144
+ end
145
+
146
+ def build_resources(resources, booker_model)
147
+ return resources if booker_model.nil?
148
+
149
+ if resources.is_a? Hash
150
+ booker_model.from_hash(resources)
151
+ elsif resources.is_a? Array
152
+ booker_model.from_list(resources)
153
+ else
154
+ resources
155
+ end
156
+ end
157
+
158
+ def temp_access_token_expired?
159
+ self.temp_access_token_expires_at.nil? || self.temp_access_token_expires_at <= Time.now
160
+ end
161
+
162
+ def update_token_store
163
+ if self.token_store && self.token_store_callback_method
164
+ token_store.send(token_store_callback_method, self.temp_access_token, self.temp_access_token_expires_at)
165
+ end
166
+ end
167
+
168
+ def results_from_response(response, booker_model=nil)
169
+ return response['Results'] unless response['Results'].nil?
170
+
171
+ if booker_model
172
+ model_name = booker_model.to_s.demodulize
173
+ return response[model_name] unless response[model_name].nil?
174
+
175
+ pluralized = model_name.pluralize
176
+ return response[pluralized] unless response[pluralized].nil?
177
+ end
178
+
179
+ response
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,38 @@
1
+ module Booker
2
+ module CommonREST
3
+ DEFAULT_PAGINATION_PARAMS = {
4
+ 'UsePaging' => true,
5
+ 'PageSize' => Integer(ENV['BOOKER_DEFAULT_PAGE_SIZE'] || 10),
6
+ 'PageNumber' => 1
7
+ }
8
+
9
+ def get_online_booking_settings(booker_location_id:)
10
+ response = get("/location/#{booker_location_id}/online_booking_settings", build_params)
11
+ Booker::Models::OnlineBookingSettings.from_hash(response['OnlineBookingSettings'])
12
+ end
13
+
14
+ def confirm_appointment(appointment_id:)
15
+ put '/appointment/confirm', build_params('ID' => appointment_id), Booker::Models::Appointment
16
+ end
17
+
18
+ private
19
+
20
+ def build_params(default_params={}, overrides={}, paginated=false)
21
+ merged = {"access_token" => access_token}.merge(default_params.merge(overrides))
22
+
23
+ merged.each do |k, v|
24
+ if v.is_a?(Time) || v.is_a?(DateTime)
25
+ merged[k] = Booker::Models::Model.time_to_booker_datetime(v)
26
+ elsif v.is_a?(Date)
27
+ merged[k] = Booker::Models::Model.time_to_booker_datetime(v.in_time_zone)
28
+ end
29
+ end
30
+
31
+ if paginated
32
+ DEFAULT_PAGINATION_PARAMS.merge(merged)
33
+ else
34
+ merged
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ module Booker
2
+ class CustomerClient < Client
3
+ include Booker::CustomerREST
4
+
5
+ def initialize(options={})
6
+ self.base_url = ENV['BOOKER_CUSTOMER_SERVICE_URL'] || 'https://apicurrent-app.booker.ninja/webservice4/json/CustomerService.svc'
7
+ self.token_store = GenericTokenStore
8
+ self.token_store_callback_method = :update_booker_access_token!
9
+ super
10
+ end
11
+
12
+ def get_access_token
13
+ http_options = {
14
+ client_id: self.client_id,
15
+ client_secret: self.client_secret,
16
+ grant_type: 'client_credentials'
17
+ }
18
+
19
+ response = get("/access_token", http_options, nil).parsed_response
20
+
21
+ raise Booker::InvalidApiCredentials.new(http_options, response) unless response.present?
22
+
23
+ self.temp_access_token_expires_at = Time.now + response['expires_in'].to_i.seconds
24
+ self.temp_access_token = response['access_token']
25
+
26
+ update_token_store
27
+
28
+ self.temp_access_token
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ module Booker
2
+ module CustomerREST
3
+ include CommonREST
4
+
5
+ def create_appointment(booker_location_id:, available_time:, customer:, options: {})
6
+ post '/appointment/create', build_params({
7
+ 'LocationID' => booker_location_id,
8
+ 'ItineraryTimeSlotList' => [
9
+ 'TreatmentTimeSlots' => [available_time]
10
+ ],
11
+ 'Customer' => customer
12
+ }, options), Booker::Models::Appointment
13
+ end
14
+
15
+ def create_class_appointment(booker_location_id:, class_instance_id:, customer:, options: {})
16
+ post '/class_appointment/create', build_params({
17
+ 'LocationID' => booker_location_id,
18
+ 'ClassInstanceID' => class_instance_id,
19
+ 'Customer' => customer
20
+ }, options), Booker::Models::Appointment
21
+ end
22
+
23
+ def run_multi_spa_multi_sub_category_availability(booker_location_ids:, treatment_sub_category_ids:, start_date_time:, end_date_time:, options: {})
24
+ post '/availability/multispamultisubcategory', build_params({
25
+ 'LocationIDs' => booker_location_ids,
26
+ 'TreatmentSubCategoryIDs' => treatment_sub_category_ids,
27
+ 'StartDateTime' => start_date_time,
28
+ 'EndDateTime' => end_date_time,
29
+ 'MaxTimesPerTreatment' => 1000
30
+ }, options), Booker::Models::SpaEmployeeAvailabilitySearchItem
31
+ end
32
+
33
+ def run_class_availability(booker_location_id:, from_start_date_time:, to_start_date_time:, options: {})
34
+ post '/availability/class', build_params({
35
+ 'FromStartDateTime' => from_start_date_time,
36
+ 'LocationID' => booker_location_id,
37
+ 'OnlyIfAvailable' => true,
38
+ 'ToStartDateTime' => to_start_date_time,
39
+ 'ExcludeClosedDates' => true
40
+ }, options), Booker::Models::ClassInstance
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module Booker
2
+ class Error < StandardError
3
+ attr_accessor :error, :description, :request, :response
4
+
5
+ def initialize(request = nil, response = nil)
6
+ if request.present?
7
+ self.request = request
8
+ end
9
+
10
+ if response.present?
11
+ self.response = response
12
+ self.error = response['error'] || response['ErrorMessage']
13
+ self.description = response['error_description']
14
+ end
15
+ end
16
+ end
17
+
18
+ class InvalidApiCredentials < Error; end
19
+ end