booker_ruby 1.0.1

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