booker_ruby 1.14.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/lib/booker/client.rb +154 -49
  3. data/lib/booker/errors.rb +6 -2
  4. data/lib/booker/model.rb +84 -0
  5. data/lib/booker/v4.1/availability.rb +21 -0
  6. data/lib/booker/v4.1/booking.rb +87 -0
  7. data/lib/booker/v4.1/merchant.rb +55 -0
  8. data/lib/booker/v4/business_client.rb +9 -0
  9. data/lib/booker/v4/business_rest.rb +88 -0
  10. data/lib/booker/v4/common_rest.rb +21 -0
  11. data/lib/booker/v4/customer_client.rb +15 -0
  12. data/lib/booker/v4/customer_rest.rb +15 -0
  13. data/lib/booker/v4/models/address.rb +14 -0
  14. data/lib/booker/v4/models/appointment.rb +79 -0
  15. data/lib/booker/v4/models/appointment_treatment.rb +53 -0
  16. data/lib/booker/v4/models/available_time.rb +15 -0
  17. data/lib/booker/v4/models/business_type.rb +7 -0
  18. data/lib/booker/v4/models/category.rb +7 -0
  19. data/lib/booker/v4/models/class_instance.rb +27 -0
  20. data/lib/booker/v4/models/country.rb +21 -0
  21. data/lib/booker/v4/models/current_price.rb +7 -0
  22. data/lib/booker/v4/models/customer.rb +54 -0
  23. data/lib/booker/v4/models/customer_2.rb +7 -0
  24. data/lib/booker/v4/models/customer_record_type.rb +7 -0
  25. data/lib/booker/v4/models/discount.rb +7 -0
  26. data/lib/booker/v4/models/dynamic_price.rb +13 -0
  27. data/lib/booker/v4/models/employee.rb +12 -0
  28. data/lib/booker/v4/models/feature_settings.rb +9 -0
  29. data/lib/booker/v4/models/final_total.rb +7 -0
  30. data/lib/booker/v4/models/gender.rb +7 -0
  31. data/lib/booker/v4/models/itinerary_time_slot.rb +9 -0
  32. data/lib/booker/v4/models/itinerary_time_slots_list.rb +9 -0
  33. data/lib/booker/v4/models/location.rb +40 -0
  34. data/lib/booker/v4/models/location_day_schedule.rb +20 -0
  35. data/lib/booker/v4/models/model.rb +78 -0
  36. data/lib/booker/v4/models/multi_service_availability_result.rb +9 -0
  37. data/lib/booker/v4/models/notification_settings.rb +14 -0
  38. data/lib/booker/v4/models/online_booking_settings.rb +25 -0
  39. data/lib/booker/v4/models/original_price.rb +7 -0
  40. data/lib/booker/v4/models/payment_method.rb +7 -0
  41. data/lib/booker/v4/models/preferred_staff_gender.rb +7 -0
  42. data/lib/booker/v4/models/price.rb +10 -0
  43. data/lib/booker/v4/models/receipt_display_price.rb +7 -0
  44. data/lib/booker/v4/models/room.rb +14 -0
  45. data/lib/booker/v4/models/shipping_address.rb +7 -0
  46. data/lib/booker/v4/models/source.rb +7 -0
  47. data/lib/booker/v4/models/spa.rb +7 -0
  48. data/lib/booker/v4/models/spa_employee_availability_search_item.rb +13 -0
  49. data/lib/booker/v4/models/status.rb +7 -0
  50. data/lib/booker/v4/models/sub_category.rb +7 -0
  51. data/lib/booker/v4/models/tag_price.rb +7 -0
  52. data/lib/booker/v4/models/teacher.rb +7 -0
  53. data/lib/booker/v4/models/teacher_2.rb +7 -0
  54. data/lib/booker/v4/models/time_zone.rb +10 -0
  55. data/lib/booker/v4/models/treatment.rb +21 -0
  56. data/lib/booker/v4/models/treatment_time_slot.rb +7 -0
  57. data/lib/booker/v4/models/type.rb +10 -0
  58. data/lib/booker/v4/models/user.rb +75 -0
  59. data/lib/booker/v4/request_helper.rb +33 -0
  60. data/lib/booker/v5/availability.rb +45 -0
  61. data/lib/booker/v5/models/availability.rb +18 -0
  62. data/lib/booker/v5/models/availability_result.rb +12 -0
  63. data/lib/booker/v5/models/location_hour.rb +16 -0
  64. data/lib/booker/v5/models/model.rb +9 -0
  65. data/lib/booker/v5/models/service.rb +13 -0
  66. data/lib/booker/v5/models/service_category.rb +11 -0
  67. data/lib/booker/version.rb +1 -1
  68. data/lib/booker_ruby.rb +70 -52
  69. metadata +80 -54
  70. data/lib/booker/business_client.rb +0 -22
  71. data/lib/booker/business_rest.rb +0 -112
  72. data/lib/booker/common_rest.rb +0 -43
  73. data/lib/booker/customer_client.rb +0 -17
  74. data/lib/booker/customer_rest.rb +0 -53
  75. data/lib/booker/models/address.rb +0 -12
  76. data/lib/booker/models/appointment.rb +0 -77
  77. data/lib/booker/models/appointment_treatment.rb +0 -51
  78. data/lib/booker/models/available_time.rb +0 -13
  79. data/lib/booker/models/business_type.rb +0 -5
  80. data/lib/booker/models/category.rb +0 -5
  81. data/lib/booker/models/class_instance.rb +0 -25
  82. data/lib/booker/models/country.rb +0 -19
  83. data/lib/booker/models/current_price.rb +0 -5
  84. data/lib/booker/models/customer.rb +0 -52
  85. data/lib/booker/models/customer_2.rb +0 -5
  86. data/lib/booker/models/customer_record_type.rb +0 -5
  87. data/lib/booker/models/discount.rb +0 -5
  88. data/lib/booker/models/dynamic_price.rb +0 -11
  89. data/lib/booker/models/employee.rb +0 -10
  90. data/lib/booker/models/feature_settings.rb +0 -7
  91. data/lib/booker/models/final_total.rb +0 -5
  92. data/lib/booker/models/gender.rb +0 -5
  93. data/lib/booker/models/itinerary_time_slot.rb +0 -7
  94. data/lib/booker/models/itinerary_time_slots_list.rb +0 -7
  95. data/lib/booker/models/location.rb +0 -38
  96. data/lib/booker/models/location_day_schedule.rb +0 -18
  97. data/lib/booker/models/model.rb +0 -150
  98. data/lib/booker/models/multi_service_availability_result.rb +0 -7
  99. data/lib/booker/models/notification_settings.rb +0 -12
  100. data/lib/booker/models/online_booking_settings.rb +0 -23
  101. data/lib/booker/models/original_price.rb +0 -5
  102. data/lib/booker/models/payment_method.rb +0 -5
  103. data/lib/booker/models/preferred_staff_gender.rb +0 -5
  104. data/lib/booker/models/price.rb +0 -8
  105. data/lib/booker/models/receipt_display_price.rb +0 -5
  106. data/lib/booker/models/room.rb +0 -12
  107. data/lib/booker/models/shipping_address.rb +0 -5
  108. data/lib/booker/models/source.rb +0 -5
  109. data/lib/booker/models/spa.rb +0 -5
  110. data/lib/booker/models/spa_employee_availability_search_item.rb +0 -11
  111. data/lib/booker/models/status.rb +0 -5
  112. data/lib/booker/models/sub_category.rb +0 -5
  113. data/lib/booker/models/tag_price.rb +0 -5
  114. data/lib/booker/models/teacher.rb +0 -5
  115. data/lib/booker/models/teacher_2.rb +0 -5
  116. data/lib/booker/models/time_zone.rb +0 -8
  117. data/lib/booker/models/treatment.rb +0 -19
  118. data/lib/booker/models/treatment_time_slot.rb +0 -5
  119. data/lib/booker/models/type.rb +0 -8
  120. data/lib/booker/models/user.rb +0 -73
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cfd498c0a58816c09c0bf5211fde0a6f4e56a76d
4
- data.tar.gz: 4f7a16d04823d03eff57c794425914e5c6c5876b
3
+ metadata.gz: 131d08ade59a2472091a0a054a83a44e4ca61b3a
4
+ data.tar.gz: 1b310a5e471337ef53435cd490fefac9a6d70d12
5
5
  SHA512:
6
- metadata.gz: 73606447296d323f61f298a52627c06eefe1fdc8c117d4edd69a3cd8171d965ef2931baaccf3e555cb8a5a8c70a23e005c3067b4aa83d34a83f5a5121b212beb
7
- data.tar.gz: b279e278941f1bf91ebf09335d667044273597d741fb501ebc0286d3ac46d5d6ebbe0dd5bdf037549f67057cd6a9e3e5921ccddb2089f95ad4123546b621c303
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, :temp_access_token_expires_at,
4
- :token_store, :token_store_callback_method
5
-
6
- ACCESS_TOKEN_HTTP_METHOD = :get
7
- ACCESS_TOKEN_ENDPOINT = '/access_token'.freeze
8
- TimeZone = 'Eastern Time (US & Canada)'.freeze
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
- env_key = try(:env_base_url_key)
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['PageSize']
47
- page_number = params['PageNumber']
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['UsePaging']
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('access_token')}. #{fetched.length} results so far."
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['PageNumber'] = page_number + 1
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
- booker_resources = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options))
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
- booker_resources = nil
125
+ response = nil # Force a retry (see logic below)
92
126
  end
93
127
  end
94
128
 
95
- return results_from_response(booker_resources, booker_model) if booker_resources.present?
96
- booker_resources = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options))
97
- return results_from_response(booker_resources, booker_model) if booker_resources.present?
98
- raise Booker::Error.new(url: url, request: http_options, response: booker_resources)
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
- http_options = access_token_options
144
- response = raise_invalid_api_credentials_for_empty_resp! { access_token_response(http_options) }
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
- self.temp_access_token_expires_at = Time.now + response['expires_in'].to_i.seconds
147
- self.temp_access_token = response['access_token']
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 raise_invalid_api_credentials_for_empty_resp!
155
- yield
156
- rescue Booker::Error => ex
157
- if (response = ex.response).present?
158
- raise ex
159
- else
160
- raise Booker::InvalidApiCredentials.new(url: ex.url, request: ex.request, response: response)
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 access_token_response(http_options)
165
- send(self.class::ACCESS_TOKEN_HTTP_METHOD, self.class::ACCESS_TOKEN_ENDPOINT, http_options, nil)
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' => 'application/json; charset=utf-8'
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
- return response['Results'] unless response['Results'].nil?
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 response[model_name] unless response[model_name].nil?
296
+ return parsed_response[model_name] unless parsed_response[model_name].nil?
204
297
 
205
298
  pluralized = model_name.pluralize
206
- return response[pluralized] unless response[pluralized].nil?
299
+ return parsed_response[pluralized] unless parsed_response[pluralized].nil?
207
300
  end
208
301
 
209
- response.parsed_response if response
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
- self.error = response['error'] || response['ErrorMessage']
13
- self.description = response['error_description']
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
@@ -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