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.
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