booker_ruby 1.14.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/booker/client.rb +154 -49
- data/lib/booker/errors.rb +6 -2
- data/lib/booker/model.rb +84 -0
- data/lib/booker/v4.1/availability.rb +21 -0
- data/lib/booker/v4.1/booking.rb +87 -0
- data/lib/booker/v4.1/merchant.rb +55 -0
- data/lib/booker/v4/business_client.rb +9 -0
- data/lib/booker/v4/business_rest.rb +88 -0
- data/lib/booker/v4/common_rest.rb +21 -0
- data/lib/booker/v4/customer_client.rb +15 -0
- data/lib/booker/v4/customer_rest.rb +15 -0
- data/lib/booker/v4/models/address.rb +14 -0
- data/lib/booker/v4/models/appointment.rb +79 -0
- data/lib/booker/v4/models/appointment_treatment.rb +53 -0
- data/lib/booker/v4/models/available_time.rb +15 -0
- data/lib/booker/v4/models/business_type.rb +7 -0
- data/lib/booker/v4/models/category.rb +7 -0
- data/lib/booker/v4/models/class_instance.rb +27 -0
- data/lib/booker/v4/models/country.rb +21 -0
- data/lib/booker/v4/models/current_price.rb +7 -0
- data/lib/booker/v4/models/customer.rb +54 -0
- data/lib/booker/v4/models/customer_2.rb +7 -0
- data/lib/booker/v4/models/customer_record_type.rb +7 -0
- data/lib/booker/v4/models/discount.rb +7 -0
- data/lib/booker/v4/models/dynamic_price.rb +13 -0
- data/lib/booker/v4/models/employee.rb +12 -0
- data/lib/booker/v4/models/feature_settings.rb +9 -0
- data/lib/booker/v4/models/final_total.rb +7 -0
- data/lib/booker/v4/models/gender.rb +7 -0
- data/lib/booker/v4/models/itinerary_time_slot.rb +9 -0
- data/lib/booker/v4/models/itinerary_time_slots_list.rb +9 -0
- data/lib/booker/v4/models/location.rb +40 -0
- data/lib/booker/v4/models/location_day_schedule.rb +20 -0
- data/lib/booker/v4/models/model.rb +78 -0
- data/lib/booker/v4/models/multi_service_availability_result.rb +9 -0
- data/lib/booker/v4/models/notification_settings.rb +14 -0
- data/lib/booker/v4/models/online_booking_settings.rb +25 -0
- data/lib/booker/v4/models/original_price.rb +7 -0
- data/lib/booker/v4/models/payment_method.rb +7 -0
- data/lib/booker/v4/models/preferred_staff_gender.rb +7 -0
- data/lib/booker/v4/models/price.rb +10 -0
- data/lib/booker/v4/models/receipt_display_price.rb +7 -0
- data/lib/booker/v4/models/room.rb +14 -0
- data/lib/booker/v4/models/shipping_address.rb +7 -0
- data/lib/booker/v4/models/source.rb +7 -0
- data/lib/booker/v4/models/spa.rb +7 -0
- data/lib/booker/v4/models/spa_employee_availability_search_item.rb +13 -0
- data/lib/booker/v4/models/status.rb +7 -0
- data/lib/booker/v4/models/sub_category.rb +7 -0
- data/lib/booker/v4/models/tag_price.rb +7 -0
- data/lib/booker/v4/models/teacher.rb +7 -0
- data/lib/booker/v4/models/teacher_2.rb +7 -0
- data/lib/booker/v4/models/time_zone.rb +10 -0
- data/lib/booker/v4/models/treatment.rb +21 -0
- data/lib/booker/v4/models/treatment_time_slot.rb +7 -0
- data/lib/booker/v4/models/type.rb +10 -0
- data/lib/booker/v4/models/user.rb +75 -0
- data/lib/booker/v4/request_helper.rb +33 -0
- data/lib/booker/v5/availability.rb +45 -0
- data/lib/booker/v5/models/availability.rb +18 -0
- data/lib/booker/v5/models/availability_result.rb +12 -0
- data/lib/booker/v5/models/location_hour.rb +16 -0
- data/lib/booker/v5/models/model.rb +9 -0
- data/lib/booker/v5/models/service.rb +13 -0
- data/lib/booker/v5/models/service_category.rb +11 -0
- data/lib/booker/version.rb +1 -1
- data/lib/booker_ruby.rb +70 -52
- metadata +80 -54
- data/lib/booker/business_client.rb +0 -22
- data/lib/booker/business_rest.rb +0 -112
- data/lib/booker/common_rest.rb +0 -43
- data/lib/booker/customer_client.rb +0 -17
- data/lib/booker/customer_rest.rb +0 -53
- data/lib/booker/models/address.rb +0 -12
- data/lib/booker/models/appointment.rb +0 -77
- data/lib/booker/models/appointment_treatment.rb +0 -51
- data/lib/booker/models/available_time.rb +0 -13
- data/lib/booker/models/business_type.rb +0 -5
- data/lib/booker/models/category.rb +0 -5
- data/lib/booker/models/class_instance.rb +0 -25
- data/lib/booker/models/country.rb +0 -19
- data/lib/booker/models/current_price.rb +0 -5
- data/lib/booker/models/customer.rb +0 -52
- data/lib/booker/models/customer_2.rb +0 -5
- data/lib/booker/models/customer_record_type.rb +0 -5
- data/lib/booker/models/discount.rb +0 -5
- data/lib/booker/models/dynamic_price.rb +0 -11
- data/lib/booker/models/employee.rb +0 -10
- data/lib/booker/models/feature_settings.rb +0 -7
- data/lib/booker/models/final_total.rb +0 -5
- data/lib/booker/models/gender.rb +0 -5
- data/lib/booker/models/itinerary_time_slot.rb +0 -7
- data/lib/booker/models/itinerary_time_slots_list.rb +0 -7
- data/lib/booker/models/location.rb +0 -38
- data/lib/booker/models/location_day_schedule.rb +0 -18
- data/lib/booker/models/model.rb +0 -150
- data/lib/booker/models/multi_service_availability_result.rb +0 -7
- data/lib/booker/models/notification_settings.rb +0 -12
- data/lib/booker/models/online_booking_settings.rb +0 -23
- data/lib/booker/models/original_price.rb +0 -5
- data/lib/booker/models/payment_method.rb +0 -5
- data/lib/booker/models/preferred_staff_gender.rb +0 -5
- data/lib/booker/models/price.rb +0 -8
- data/lib/booker/models/receipt_display_price.rb +0 -5
- data/lib/booker/models/room.rb +0 -12
- data/lib/booker/models/shipping_address.rb +0 -5
- data/lib/booker/models/source.rb +0 -5
- data/lib/booker/models/spa.rb +0 -5
- data/lib/booker/models/spa_employee_availability_search_item.rb +0 -11
- data/lib/booker/models/status.rb +0 -5
- data/lib/booker/models/sub_category.rb +0 -5
- data/lib/booker/models/tag_price.rb +0 -5
- data/lib/booker/models/teacher.rb +0 -5
- data/lib/booker/models/teacher_2.rb +0 -5
- data/lib/booker/models/time_zone.rb +0 -8
- data/lib/booker/models/treatment.rb +0 -19
- data/lib/booker/models/treatment_time_slot.rb +0 -5
- data/lib/booker/models/type.rb +0 -8
- data/lib/booker/models/user.rb +0 -73
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 131d08ade59a2472091a0a054a83a44e4ca61b3a
|
4
|
+
data.tar.gz: 1b310a5e471337ef53435cd490fefac9a6d70d12
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb7ca3e5420dfdcf5844bd3eb95da5372322998e000b76c3260d4399360ffde955c51061b9b4ee1bd88b991b1af25b09cb5df83c296afbe8514dc1dab386f2b4
|
7
|
+
data.tar.gz: ea1ea1d93267b933769e09516ffb1f4330819c278fca418443e710443e995ad7da2d65209f55560da699a8b37ca3945a2afc33c758804316ef2ee051d1882826
|
data/lib/booker/client.rb
CHANGED
@@ -1,27 +1,55 @@
|
|
1
1
|
module Booker
|
2
2
|
class Client
|
3
|
-
attr_accessor :base_url, :client_id, :client_secret, :temp_access_token,
|
4
|
-
:token_store, :token_store_callback_method
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
attr_accessor :base_url, :auth_base_url, :client_id, :client_secret, :temp_access_token,
|
4
|
+
:temp_access_token_expires_at, :token_store, :token_store_callback_method, :api_subscription_key,
|
5
|
+
:access_token_scope, :refresh_token, :location_id, :auth_with_client_credentials
|
6
|
+
|
7
|
+
CREATE_TOKEN_CONTENT_TYPE = 'application/x-www-form-urlencoded'.freeze
|
8
|
+
CLIENT_CREDENTIALS_GRANT_TYPE = 'client_credentials'.freeze
|
9
|
+
REFRESH_TOKEN_GRANT_TYPE = 'refresh_token'.freeze
|
10
|
+
CREATE_TOKEN_PATH = '/v5/auth/connect/token'.freeze
|
11
|
+
UPDATE_TOKEN_CONTEXT_PATH = '/v5/auth/context/update'.freeze
|
12
|
+
VALID_ACCESS_TOKEN_SCOPES = %w(public merchant parter-payment internal).map(&:freeze).freeze
|
13
|
+
API_GATEWAY_ERRORS = {
|
14
|
+
503 => Booker::ServiceUnavailable,
|
15
|
+
504 => Booker::ServiceUnavailable,
|
16
|
+
429 => Booker::RateLimitExceeded,
|
17
|
+
401 => Booker::InvalidApiCredentials,
|
18
|
+
403 => Booker::InvalidApiCredentials
|
19
|
+
}.freeze
|
20
|
+
BOOKER_SERVER_TIMEZONE = 'Eastern Time (US & Canada)'.freeze
|
21
|
+
DEFAULT_CONTENT_TYPE = 'application/json'.freeze
|
22
|
+
ENV_BASE_URL_KEY = 'BOOKER_API_BASE_URL'.freeze
|
23
|
+
DEFAULT_BASE_URL = 'https://api-staging.booker.com'.freeze
|
24
|
+
DEFAULT_AUTH_BASE_URL = 'https://api-staging.booker.com'
|
9
25
|
|
10
26
|
def initialize(options = {})
|
11
27
|
options.each { |key, value| send(:"#{key}=", value) }
|
12
28
|
self.base_url ||= get_base_url
|
29
|
+
self.auth_base_url ||= ENV['BOOKER_API_BASE_URL'] || DEFAULT_AUTH_BASE_URL
|
13
30
|
self.client_id ||= ENV['BOOKER_CLIENT_ID']
|
14
31
|
self.client_secret ||= ENV['BOOKER_CLIENT_SECRET']
|
32
|
+
self.api_subscription_key ||= ENV['BOOKER_API_SUBSCRIPTION_KEY']
|
33
|
+
if self.auth_with_client_credentials.nil?
|
34
|
+
self.auth_with_client_credentials = ENV['BOOKER_API_AUTH_WITH_CLIENT_CREDENTIALS'] == 'true'
|
35
|
+
end
|
36
|
+
if self.temp_access_token.present?
|
37
|
+
begin
|
38
|
+
self.temp_access_token_expires_at = token_expires_at(self.temp_access_token)
|
39
|
+
self.access_token_scope = token_scope(self.temp_access_token)
|
40
|
+
rescue JWT::ExpiredSignature => ex
|
41
|
+
raise ex unless self.auth_with_client_credentials || self.refresh_token.present?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
if self.access_token_scope.blank?
|
45
|
+
self.access_token_scope = VALID_ACCESS_TOKEN_SCOPES.first
|
46
|
+
elsif !self.access_token_scope.in?(VALID_ACCESS_TOKEN_SCOPES)
|
47
|
+
raise ArgumentError, "access_token_scope must be one of: #{VALID_ACCESS_TOKEN_SCOPES.join(', ')}"
|
48
|
+
end
|
15
49
|
end
|
16
50
|
|
17
51
|
def get_base_url
|
18
|
-
|
19
|
-
|
20
|
-
if env_key.present?
|
21
|
-
ENV[env_key] || try(:default_base_url)
|
22
|
-
else
|
23
|
-
try(:default_base_url)
|
24
|
-
end
|
52
|
+
ENV[self.class::ENV_BASE_URL_KEY] || self.class::DEFAULT_BASE_URL
|
25
53
|
end
|
26
54
|
|
27
55
|
def get(path, params, booker_model=nil)
|
@@ -42,15 +70,21 @@ module Booker
|
|
42
70
|
build_resources(booker_resources, booker_model)
|
43
71
|
end
|
44
72
|
|
73
|
+
def delete(path, params=nil, body=nil, booker_model=nil)
|
74
|
+
booker_resources = get_booker_resources(:delete, path, params, body.to_json, booker_model)
|
75
|
+
|
76
|
+
build_resources(booker_resources, booker_model)
|
77
|
+
end
|
78
|
+
|
45
79
|
def paginated_request(method:, path:, params:, model: nil, fetched: [], fetch_all: true)
|
46
|
-
page_size = params[
|
47
|
-
page_number = params[
|
80
|
+
page_size = params[:PageSize]
|
81
|
+
page_number = params[:PageNumber]
|
48
82
|
|
49
|
-
if page_size.nil? || page_size < 1 || page_number.nil? || page_number < 1 || !params[
|
83
|
+
if page_size.nil? || page_size < 1 || page_number.nil? || page_number < 1 || !params[:UsePaging]
|
50
84
|
raise ArgumentError, 'params must include valid PageSize, PageNumber and UsePaging'
|
51
85
|
end
|
52
86
|
|
53
|
-
puts "fetching #{path} with #{params.except(
|
87
|
+
puts "fetching #{path} with #{params.except(:access_token)}. #{fetched.length} results so far."
|
54
88
|
|
55
89
|
results = self.send(method, path, params, model)
|
56
90
|
|
@@ -65,7 +99,7 @@ module Booker
|
|
65
99
|
if results_length > 0
|
66
100
|
# TODO (#111186744): Add logging to see if any pages with less than expected data (as seen in the /appointments endpoint)
|
67
101
|
new_params = params.deep_dup
|
68
|
-
new_params[
|
102
|
+
new_params[:PageNumber] = page_number + 1
|
69
103
|
paginated_request(method: method, path: path, params: new_params, model: model, fetched: fetched)
|
70
104
|
else
|
71
105
|
fetched
|
@@ -82,20 +116,28 @@ module Booker
|
|
82
116
|
|
83
117
|
# Allow it to retry the first time unless it is an authorization error
|
84
118
|
begin
|
85
|
-
|
119
|
+
response = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options))
|
86
120
|
rescue Booker::Error, Net::ReadTimeout => ex
|
87
121
|
if ex.is_a? Booker::InvalidApiCredentials
|
88
122
|
raise ex
|
89
123
|
else
|
90
124
|
sleep 1
|
91
|
-
|
125
|
+
response = nil # Force a retry (see logic below)
|
92
126
|
end
|
93
127
|
end
|
94
128
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
129
|
+
unless response.nil? || nil_or_empty_hash?(response.parsed_response)
|
130
|
+
return results_from_response(response, booker_model)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Retry on blank responses (happens in certain v4 API methods in lieu of an actual error)
|
134
|
+
response = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options))
|
135
|
+
unless response.nil? || nil_or_empty_hash?(response.parsed_response)
|
136
|
+
return results_from_response(response, booker_model)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Raise if response is still blank
|
140
|
+
raise Booker::Error.new(url: url, request: http_options, response: response)
|
99
141
|
end
|
100
142
|
|
101
143
|
def full_url(path)
|
@@ -106,6 +148,9 @@ module Booker
|
|
106
148
|
def handle_errors!(url, request, response)
|
107
149
|
puts "BOOKER RESPONSE: #{response}" if ENV['BOOKER_API_DEBUG'] == 'true'
|
108
150
|
|
151
|
+
error_class = API_GATEWAY_ERRORS[response.code]
|
152
|
+
raise error_class.new(url: url, request: request, response: response) if error_class
|
153
|
+
|
109
154
|
ex = Booker::Error.new(url: url, request: request, response: response)
|
110
155
|
if ex.error.present? || !response.success?
|
111
156
|
case ex.error
|
@@ -126,13 +171,6 @@ module Booker
|
|
126
171
|
(self.temp_access_token && !temp_access_token_expired?) ? self.temp_access_token : get_access_token
|
127
172
|
end
|
128
173
|
|
129
|
-
def access_token_options
|
130
|
-
{
|
131
|
-
client_id: self.client_id,
|
132
|
-
client_secret: self.client_secret
|
133
|
-
}
|
134
|
-
end
|
135
|
-
|
136
174
|
def update_token_store
|
137
175
|
if self.token_store.present? && self.token_store_callback_method.present?
|
138
176
|
self.token_store.send(self.token_store_callback_method, self.temp_access_token, self.temp_access_token_expires_at)
|
@@ -140,36 +178,88 @@ module Booker
|
|
140
178
|
end
|
141
179
|
|
142
180
|
def get_access_token
|
143
|
-
|
144
|
-
|
181
|
+
unless self.auth_with_client_credentials || self.refresh_token
|
182
|
+
raise ArgumentError, 'Cannot get new access token without auth_with_client_credentials or a refresh_token'
|
183
|
+
end
|
145
184
|
|
146
|
-
|
147
|
-
|
185
|
+
resp = access_token_response
|
186
|
+
token = resp.parsed_response['access_token']
|
187
|
+
raise Booker::InvalidApiCredentials.new(response: resp) if token.blank?
|
188
|
+
|
189
|
+
if self.auth_with_client_credentials && self.location_id
|
190
|
+
self.temp_access_token = get_location_access_token(token, self.location_id)
|
191
|
+
else
|
192
|
+
self.temp_access_token = token
|
193
|
+
end
|
194
|
+
|
195
|
+
self.temp_access_token_expires_at = token_expires_at(self.temp_access_token)
|
148
196
|
|
149
197
|
update_token_store
|
150
198
|
|
151
199
|
self.temp_access_token
|
152
200
|
end
|
153
201
|
|
154
|
-
def
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
202
|
+
def access_token_response
|
203
|
+
body = {
|
204
|
+
grant_type: self.auth_with_client_credentials ? CLIENT_CREDENTIALS_GRANT_TYPE : REFRESH_TOKEN_GRANT_TYPE,
|
205
|
+
client_id: self.client_id,
|
206
|
+
client_secret: self.client_secret,
|
207
|
+
scope: self.access_token_scope
|
208
|
+
}
|
209
|
+
body[:refresh_token] = self.refresh_token if body[:grant_type] == REFRESH_TOKEN_GRANT_TYPE
|
210
|
+
options = {
|
211
|
+
headers: {
|
212
|
+
'Content-Type' => CREATE_TOKEN_CONTENT_TYPE,
|
213
|
+
'Ocp-Apim-Subscription-Key' => self.api_subscription_key
|
214
|
+
},
|
215
|
+
body: body.to_query
|
216
|
+
}
|
217
|
+
|
218
|
+
url = "#{self.auth_base_url}#{CREATE_TOKEN_PATH}"
|
219
|
+
|
220
|
+
begin
|
221
|
+
handle_errors! url, options, HTTParty.post(url, options)
|
222
|
+
rescue Booker::ServiceUnavailable, Booker::RateLimitExceeded
|
223
|
+
# retry once
|
224
|
+
sleep 1
|
225
|
+
handle_errors! url, options, HTTParty.post(url, options)
|
161
226
|
end
|
162
227
|
end
|
163
228
|
|
164
|
-
def
|
165
|
-
|
229
|
+
def get_location_access_token(existing_token, location_id)
|
230
|
+
options = {
|
231
|
+
headers: {
|
232
|
+
'Content-Type' => DEFAULT_CONTENT_TYPE,
|
233
|
+
'Accept' => 'application/json',
|
234
|
+
'Authorization' => "Bearer #{existing_token}",
|
235
|
+
'Ocp-Apim-Subscription-Key' => self.api_subscription_key
|
236
|
+
},
|
237
|
+
query: {
|
238
|
+
locationId: location_id
|
239
|
+
},
|
240
|
+
open_timeout: 120
|
241
|
+
}
|
242
|
+
url = "#{self.auth_base_url}#{UPDATE_TOKEN_CONTEXT_PATH}"
|
243
|
+
|
244
|
+
begin
|
245
|
+
resp = handle_errors! url, options, HTTParty.post(url, options)
|
246
|
+
rescue Booker::ServiceUnavailable, Booker::RateLimitExceeded
|
247
|
+
# retry once
|
248
|
+
sleep 1
|
249
|
+
resp = handle_errors! url, options, HTTParty.post(url, options)
|
250
|
+
end
|
251
|
+
|
252
|
+
resp.parsed_response
|
166
253
|
end
|
167
254
|
|
168
255
|
private
|
169
256
|
def request_options(query=nil, body=nil)
|
170
257
|
options = {
|
171
258
|
headers: {
|
172
|
-
'Content-Type' =>
|
259
|
+
'Content-Type' => DEFAULT_CONTENT_TYPE,
|
260
|
+
'Accept' => DEFAULT_CONTENT_TYPE,
|
261
|
+
'Authorization' => "Bearer #{access_token}",
|
262
|
+
'Ocp-Apim-Subscription-Key' => self.api_subscription_key
|
173
263
|
},
|
174
264
|
open_timeout: 120
|
175
265
|
}
|
@@ -196,17 +286,32 @@ module Booker
|
|
196
286
|
end
|
197
287
|
|
198
288
|
def results_from_response(response, booker_model=nil)
|
199
|
-
|
289
|
+
parsed_response = response.parsed_response
|
290
|
+
|
291
|
+
return parsed_response unless parsed_response.is_a?(Hash)
|
292
|
+
return parsed_response['Results'] unless parsed_response['Results'].nil?
|
200
293
|
|
201
294
|
if booker_model
|
202
295
|
model_name = booker_model.to_s.demodulize
|
203
|
-
return
|
296
|
+
return parsed_response[model_name] unless parsed_response[model_name].nil?
|
204
297
|
|
205
298
|
pluralized = model_name.pluralize
|
206
|
-
return
|
299
|
+
return parsed_response[pluralized] unless parsed_response[pluralized].nil?
|
207
300
|
end
|
208
301
|
|
209
|
-
|
302
|
+
parsed_response
|
303
|
+
end
|
304
|
+
|
305
|
+
def nil_or_empty_hash?(obj)
|
306
|
+
obj.nil? || (obj.is_a?(Hash) && obj.blank?)
|
307
|
+
end
|
308
|
+
|
309
|
+
def token_expires_at(token)
|
310
|
+
Time.at(JWT.decode(token, nil, false)[0]['exp'])
|
311
|
+
end
|
312
|
+
|
313
|
+
def token_scope(token)
|
314
|
+
JWT.decode(token, nil, false)[0]['scope']
|
210
315
|
end
|
211
316
|
end
|
212
317
|
end
|
data/lib/booker/errors.rb
CHANGED
@@ -9,8 +9,10 @@ module Booker
|
|
9
9
|
|
10
10
|
if response.present?
|
11
11
|
self.response = response
|
12
|
-
|
13
|
-
|
12
|
+
if response.parsed_response.is_a?(Hash)
|
13
|
+
self.error = response.parsed_response['error'] || response.parsed_response['ErrorMessage']
|
14
|
+
self.description = response.parsed_response['error_description']
|
15
|
+
end
|
14
16
|
end
|
15
17
|
|
16
18
|
self.url = url
|
@@ -18,4 +20,6 @@ module Booker
|
|
18
20
|
end
|
19
21
|
|
20
22
|
class InvalidApiCredentials < Error; end
|
23
|
+
class ServiceUnavailable < Error; end
|
24
|
+
class RateLimitExceeded < Error; end
|
21
25
|
end
|
data/lib/booker/model.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Booker
|
2
|
+
class Model
|
3
|
+
CONSTANTIZE_MODULE = Booker
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@attributes = []
|
7
|
+
options.each do |k, v|
|
8
|
+
send(:"#{k}=", v)
|
9
|
+
@attributes << k.to_sym
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_hash
|
14
|
+
hash = {}
|
15
|
+
@attributes.each do |attr|
|
16
|
+
value = self.send(attr)
|
17
|
+
if value.is_a? Array
|
18
|
+
new_value = hash_list(value)
|
19
|
+
elsif value.is_a? Booker::Model
|
20
|
+
new_value = value.to_hash
|
21
|
+
elsif value.is_a? Time
|
22
|
+
new_value = self.class.try(:time_to_booker_datetime, value) || value
|
23
|
+
elsif value.is_a? Date
|
24
|
+
time = value.in_time_zone
|
25
|
+
new_value = self.class.try(:time_to_booker_datetime, time) || value
|
26
|
+
else
|
27
|
+
new_value = value
|
28
|
+
end
|
29
|
+
hash[attr] = new_value
|
30
|
+
end
|
31
|
+
hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_hash(hash)
|
35
|
+
model = self.new
|
36
|
+
hash.each do |k, v|
|
37
|
+
if model.respond_to?(:"#{k}")
|
38
|
+
constantized = self.constantize(k)
|
39
|
+
if constantized
|
40
|
+
if v.is_a?(Array) && v.first.is_a?(Hash)
|
41
|
+
model.send(:"#{k}=", constantized.from_list(v))
|
42
|
+
next
|
43
|
+
elsif v.is_a? Hash
|
44
|
+
model.send(:"#{k}=", constantized.from_hash(v))
|
45
|
+
next
|
46
|
+
end
|
47
|
+
end
|
48
|
+
if v.is_a?(String) && v.start_with?('/Date(')
|
49
|
+
model.send(:"#{k}=", try(:time_from_booker_datetime, v) || v)
|
50
|
+
next
|
51
|
+
end
|
52
|
+
model.send(:"#{k}=", v)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
model
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_json; Oj.dump(to_hash, mode: :compat); end
|
59
|
+
|
60
|
+
def self.from_list(array); array.map { |item| self.from_hash(item) }; end
|
61
|
+
|
62
|
+
def self.constantize(key)
|
63
|
+
begin
|
64
|
+
self::CONSTANTIZE_MODULE.const_get key.to_s.camelize.singularize
|
65
|
+
rescue NameError
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def hash_list(array)
|
73
|
+
array.map do |item|
|
74
|
+
if item.is_a? Array
|
75
|
+
hash_list(item)
|
76
|
+
elsif item.is_a? Booker::Model
|
77
|
+
item.to_hash
|
78
|
+
else
|
79
|
+
item
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Booker
|
2
|
+
module V41
|
3
|
+
class Availability < Client
|
4
|
+
include Booker::V4::RequestHelper
|
5
|
+
|
6
|
+
API_METHODS = {
|
7
|
+
class_availability: '/v4.1/availability/availability/class'.freeze
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
def class_availability(location_id:, from_start_date_time:, to_start_date_time:, options: {})
|
11
|
+
post API_METHODS[:class_availability], build_params({
|
12
|
+
FromStartDateTime: from_start_date_time,
|
13
|
+
LocationID: location_id,
|
14
|
+
OnlyIfAvailable: true,
|
15
|
+
ToStartDateTime: to_start_date_time,
|
16
|
+
ExcludeClosedDates: true
|
17
|
+
}, options), Booker::V4::Models::ClassInstance
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Booker
|
2
|
+
module V41
|
3
|
+
class Booking < Client
|
4
|
+
include Booker::V4::RequestHelper
|
5
|
+
|
6
|
+
API_METHODS = {
|
7
|
+
appointment: '/v4.1/booking/appointment'.freeze,
|
8
|
+
cancel_appointment: '/v4.1/booking/appointment/cancel'.freeze,
|
9
|
+
create_appointment: '/v4.1/booking/appointment/create'.freeze,
|
10
|
+
appointment_hold: '/v4.1/booking/appointment/hold'.freeze,
|
11
|
+
employees: '/v4.1/booking/employees'.freeze,
|
12
|
+
services: '/v4.1/booking/services'.freeze,
|
13
|
+
location: '/v4.1/booking/location'.freeze,
|
14
|
+
locations: '/v4.1/booking/locations'.freeze
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def appointment(id:)
|
18
|
+
get "#{API_METHODS[:appointment]}/#{id}", build_params, Booker::V4::Models::Appointment
|
19
|
+
end
|
20
|
+
|
21
|
+
def cancel_appointment(id:, options:{})
|
22
|
+
put API_METHODS[:cancel_appointment], build_params({ID: id}, options), Booker::V4::Models::Appointment
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_appointment(location_id:, available_time:, customer:, options: {})
|
26
|
+
post API_METHODS[:create_appointment], build_params({
|
27
|
+
LocationID: location_id,
|
28
|
+
ItineraryTimeSlotList: [
|
29
|
+
TreatmentTimeSlots: [available_time]
|
30
|
+
],
|
31
|
+
Customer: customer
|
32
|
+
}, options), Booker::V4::Models::Appointment
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_appointment_hold(location_id:, available_time:, customer:, options: {})
|
36
|
+
post API_METHODS[:appointment_hold], build_params({
|
37
|
+
LocationID: location_id,
|
38
|
+
ItineraryTimeSlot: {
|
39
|
+
TreatmentTimeSlots: [available_time]
|
40
|
+
},
|
41
|
+
Customer: customer
|
42
|
+
}, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete_appointment_hold(location_id:, incomplete_appointment_id:)
|
46
|
+
delete API_METHODS[:appointment_hold], nil, build_params({
|
47
|
+
LocationID: location_id,
|
48
|
+
IncompleteAppointmentID: incomplete_appointment_id
|
49
|
+
})
|
50
|
+
end
|
51
|
+
|
52
|
+
def employees(location_id:, fetch_all: true, options: {})
|
53
|
+
paginated_request(
|
54
|
+
method: :post,
|
55
|
+
path: API_METHODS[:employees],
|
56
|
+
params: build_params({LocationID: location_id}, options, true),
|
57
|
+
model: Booker::V4::Models::Employee,
|
58
|
+
fetch_all: fetch_all
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def location(id:)
|
63
|
+
response = get("#{API_METHODS[:location]}/#{id}", build_params)
|
64
|
+
Booker::V4::Models::Location.from_hash(response)
|
65
|
+
end
|
66
|
+
|
67
|
+
def locations(options: {})
|
68
|
+
paginated_request(
|
69
|
+
method: :post,
|
70
|
+
path: API_METHODS[:locations],
|
71
|
+
params: build_params({}, options, true),
|
72
|
+
model: Booker::V4::Models::Location
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def services(location_id:, fetch_all: true, params: {})
|
77
|
+
paginated_request(
|
78
|
+
method: :post,
|
79
|
+
path: API_METHODS[:services],
|
80
|
+
params: build_params({LocationID: location_id}, params, true),
|
81
|
+
model: Booker::V4::Models::Treatment,
|
82
|
+
fetch_all: fetch_all
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|