Class: Booker::Client

Inherits:
Object
  • Object
show all
Includes:
RequestHelper
Defined in:
lib/booker/client.rb

Constant Summary collapse

CREATE_TOKEN_CONTENT_TYPE =
'application/x-www-form-urlencoded'.freeze
CLIENT_CREDENTIALS_GRANT_TYPE =
'client_credentials'.freeze
REFRESH_TOKEN_GRANT_TYPE =
'refresh_token'.freeze
CREATE_TOKEN_PATH =
'/v5/auth/connect/token'.freeze
UPDATE_TOKEN_CONTEXT_PATH =
'/v5/auth/context/update'.freeze
VALID_ACCESS_TOKEN_SCOPES =
%w(public merchant parter-payment internal).map(&:freeze).freeze
API_GATEWAY_ERRORS =
{
  503 => Booker::ServiceUnavailable,
  504 => Booker::ServiceUnavailable,
  429 => Booker::RateLimitExceeded,
  401 => Booker::InvalidApiCredentials,
  403 => Booker::InvalidApiCredentials
}.freeze
BOOKER_SERVER_TIMEZONE =
'Eastern Time (US & Canada)'.freeze
DEFAULT_CONTENT_TYPE =
'application/json'.freeze
ENV_BASE_URL_KEY =
'BOOKER_API_BASE_URL'.freeze
DEFAULT_BASE_URL =
'https://api-staging.booker.com'.freeze
DEFAULT_AUTH_BASE_URL =
'https://api-staging.booker.com'
DEFAULT_REQUEST_TIMEOUT =
60

Constants included from RequestHelper

RequestHelper::DEFAULT_PAGINATION_PARAMS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from RequestHelper

#build_params

Constructor Details

#initialize(options = {}) ⇒ Client

Returns a new instance of Client.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/booker/client.rb', line 29

def initialize(options = {})
  options.each { |key, value| send(:"#{key}=", value) }
  self.request_timeout ||= DEFAULT_REQUEST_TIMEOUT
  self.base_url ||= get_base_url
  self.auth_base_url ||= ENV['BOOKER_API_BASE_URL'] || DEFAULT_AUTH_BASE_URL
  self.client_id ||= ENV['BOOKER_CLIENT_ID']
  self.client_secret ||= ENV['BOOKER_CLIENT_SECRET']
  self.api_subscription_key ||= ENV['BOOKER_API_SUBSCRIPTION_KEY']
  if self.auth_with_client_credentials.nil?
    self.auth_with_client_credentials = ENV['BOOKER_API_AUTH_WITH_CLIENT_CREDENTIALS'] == 'true'
  end
  if self.temp_access_token.present?
    begin
      self.temp_access_token_expires_at = token_expires_at(self.temp_access_token)
      self.access_token_scope = token_scope(self.temp_access_token)
    rescue JWT::ExpiredSignature => ex
      raise ex unless self.auth_with_client_credentials || self.refresh_token.present?
    end
  end
  if self.access_token_scope.blank?
    self.access_token_scope = VALID_ACCESS_TOKEN_SCOPES.first
  elsif !self.access_token_scope.in?(VALID_ACCESS_TOKEN_SCOPES)
    raise ArgumentError, "access_token_scope must be one of: #{VALID_ACCESS_TOKEN_SCOPES.join(', ')}"
  end
end

Instance Attribute Details

#access_token_scopeObject

Returns the value of attribute access_token_scope.



5
6
7
# File 'lib/booker/client.rb', line 5

def access_token_scope
  @access_token_scope
end

#api_subscription_keyObject

Returns the value of attribute api_subscription_key.



5
6
7
# File 'lib/booker/client.rb', line 5

def api_subscription_key
  @api_subscription_key
end

#auth_base_urlObject

Returns the value of attribute auth_base_url.



5
6
7
# File 'lib/booker/client.rb', line 5

def auth_base_url
  @auth_base_url
end

#auth_with_client_credentialsObject

Returns the value of attribute auth_with_client_credentials.



5
6
7
# File 'lib/booker/client.rb', line 5

def auth_with_client_credentials
  @auth_with_client_credentials
end

#base_urlObject

Returns the value of attribute base_url.



5
6
7
# File 'lib/booker/client.rb', line 5

def base_url
  @base_url
end

#client_idObject

Returns the value of attribute client_id.



5
6
7
# File 'lib/booker/client.rb', line 5

def client_id
  @client_id
end

#client_secretObject

Returns the value of attribute client_secret.



5
6
7
# File 'lib/booker/client.rb', line 5

def client_secret
  @client_secret
end

#location_idObject

Returns the value of attribute location_id.



5
6
7
# File 'lib/booker/client.rb', line 5

def location_id
  @location_id
end

#refresh_tokenObject

Returns the value of attribute refresh_token.



5
6
7
# File 'lib/booker/client.rb', line 5

def refresh_token
  @refresh_token
end

#request_timeoutObject

Returns the value of attribute request_timeout.



5
6
7
# File 'lib/booker/client.rb', line 5

def request_timeout
  @request_timeout
end

#temp_access_tokenObject

Returns the value of attribute temp_access_token.



5
6
7
# File 'lib/booker/client.rb', line 5

def temp_access_token
  @temp_access_token
end

#temp_access_token_expires_atObject

Returns the value of attribute temp_access_token_expires_at.



5
6
7
# File 'lib/booker/client.rb', line 5

def temp_access_token_expires_at
  @temp_access_token_expires_at
end

#token_storeObject

Returns the value of attribute token_store.



5
6
7
# File 'lib/booker/client.rb', line 5

def token_store
  @token_store
end

#token_store_callback_methodObject

Returns the value of attribute token_store_callback_method.



5
6
7
# File 'lib/booker/client.rb', line 5

def token_store_callback_method
  @token_store_callback_method
end

Instance Method Details

#access_tokenObject



194
195
196
# File 'lib/booker/client.rb', line 194

def access_token
  (self.temp_access_token && !temp_access_token_expired?) ? self.temp_access_token : get_access_token
end

#access_token_responseObject



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/booker/client.rb', line 226

def access_token_response
  body = {
    grant_type: self.auth_with_client_credentials ? CLIENT_CREDENTIALS_GRANT_TYPE : REFRESH_TOKEN_GRANT_TYPE,
    client_id: self.client_id,
    client_secret: self.client_secret,
    scope: self.access_token_scope
  }
  body[:refresh_token] = self.refresh_token if body[:grant_type] == REFRESH_TOKEN_GRANT_TYPE
  options = {
    headers: {
      'Content-Type' => CREATE_TOKEN_CONTENT_TYPE,
      'Ocp-Apim-Subscription-Key' => self.api_subscription_key
    },
    body: body.to_query,
    timeout: 30
  }

  url = "#{self.auth_base_url}#{CREATE_TOKEN_PATH}"

  begin
    handle_errors! url, options, HTTParty.post(url, options), false
  rescue Booker::ServiceUnavailable, Booker::RateLimitExceeded
    # retry once
    sleep 1
    handle_errors! url, options, HTTParty.post(url, options), false
  end
end

#delete(path, params = nil, body = nil, booker_model = nil) ⇒ Object



83
84
85
86
87
# File 'lib/booker/client.rb', line 83

def delete(path, params=nil, body=nil, booker_model=nil)
  booker_resources = get_booker_resources(:delete, path, params, body.to_json, booker_model)

  build_resources(booker_resources, booker_model)
end

#full_url(path) ⇒ Object



159
160
161
162
# File 'lib/booker/client.rb', line 159

def full_url(path)
  uri = URI(path)
  uri.scheme ? path : "#{self.base_url}#{path}"
end

#get(path, params, booker_model = nil) ⇒ Object



59
60
61
62
63
# File 'lib/booker/client.rb', line 59

def get(path, params, booker_model=nil)
  booker_resources = get_booker_resources(:get, path, params, nil, booker_model)

  build_resources(booker_resources, booker_model)
end

#get_access_tokenObject



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/booker/client.rb', line 204

def get_access_token
  unless self.auth_with_client_credentials || self.refresh_token
    raise ArgumentError, 'Cannot get new access token without auth_with_client_credentials or a refresh_token'
  end

  resp = access_token_response
  token = resp.parsed_response['access_token']
  raise Booker::InvalidApiCredentials.new(response: resp) if token.blank?

  if self.auth_with_client_credentials && self.location_id
    self.temp_access_token = get_location_access_token(token, self.location_id)
  else
    self.temp_access_token = token
  end

  self.temp_access_token_expires_at = token_expires_at(self.temp_access_token)

  update_token_store

  self.temp_access_token
end

#get_base_urlObject



55
56
57
# File 'lib/booker/client.rb', line 55

def get_base_url
  ENV[self.class::ENV_BASE_URL_KEY] || self.class::DEFAULT_BASE_URL
end

#get_booker_resources(http_method, path, params = nil, body = nil, booker_model = nil) ⇒ Object

Raises:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/booker/client.rb', line 128

def get_booker_resources(http_method, path, params=nil, body=nil, booker_model=nil)
  http_options = request_options(params, body)
  url = full_url(path)
  puts "BOOKER REQUEST: #{http_method} #{url} #{http_options}" if ENV['BOOKER_API_DEBUG'] == 'true'

  # Allow it to retry the first time unless it is an authorization error
  begin
    response = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options))
  rescue Booker::Error, Net::ReadTimeout => ex
    if ex.is_a? Booker::InvalidApiCredentials
      raise ex
    else
      sleep 1
      response = nil # Force a retry (see logic below)
    end
  end

  unless response_is_error?(response, http_method)
    return results_from_response(response, booker_model)
  end

  # Retry on blank responses (happens in certain v4 API methods in lieu of an actual error)
  response = handle_errors!(url, http_options, HTTParty.send(http_method, url, http_options))
  unless response_is_error?(response, http_method)
    return results_from_response(response, booker_model)
  end

  # Raise if response is still blank
  raise Booker::Error.new(url: url, request: http_options, response: response)
end

#get_location_access_token(existing_token, location_id) ⇒ Object



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/booker/client.rb', line 254

def get_location_access_token(existing_token, location_id)
  options = {
    headers: {
      'Accept' => 'application/json',
      'Authorization' => "Bearer #{existing_token}",
      'Ocp-Apim-Subscription-Key' => self.api_subscription_key
    },
    query: {
      locationId: location_id
    },
    timeout: 30
  }
  url = "#{self.auth_base_url}#{UPDATE_TOKEN_CONTEXT_PATH}"

  begin
    resp = handle_errors! url, options, HTTParty.post(url, options), false
  rescue Booker::ServiceUnavailable, Booker::RateLimitExceeded
    # retry once
    sleep 1
    resp = handle_errors! url, options, HTTParty.post(url, options), false
  end

  resp.parsed_response
end

#handle_errors!(url, request, response, retry_unauthorized = true) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/booker/client.rb', line 164

def handle_errors!(url, request, response, retry_unauthorized=true)
  puts "BOOKER RESPONSE: #{response}" if ENV['BOOKER_API_DEBUG'] == 'true'

  error_class = API_GATEWAY_ERRORS[response.code]

  begin
    raise error_class.new(url: url, request: request, response: response) if error_class
  rescue Booker::InvalidApiCredentials => ex
    raise ex unless response.code == 401 && retry_unauthorized
    get_access_token
    return nil
  end

  ex = Booker::Error.new(url: url, request: request, response: response)

  if ex.error.present? || !response.success?
    case ex.error
      when 'invalid_client'
        raise Booker::InvalidApiCredentials.new(url: url, request: request, response: response)
      when 'invalid access token'
        get_access_token
        return nil
      else
        raise ex
    end
  end

  response
end

#paginated_request(method:, path:, params:, model: nil, fetched: [], fetch_all: true) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/booker/client.rb', line 89

def paginated_request(method:, path:, params:, model: nil, fetched: [], fetch_all: true)
  page_size = params[:PageSize]
  page_number = params[:PageNumber]

  if page_size.nil? || page_size < 1 || page_number.nil? || page_number < 1 || !params[:UsePaging]
    raise ArgumentError, 'params must include valid PageSize, PageNumber and UsePaging'
  end

  puts "fetching #{path} with #{params.except(:access_token)}. #{fetched.length} results so far."

  begin
    results = self.send(method, path, params, model)
  rescue Net::ReadTimeout
    results = nil
  end

  unless results.is_a?(Array)
    error_msg = "Result from paginated request to #{path} with params: #{params} is not a collection"
    raise Booker::MidPaginationError.new(message: error_msg, error_occurred_during_params: params,
                                         results_fetched_prior_to_error: fetched)
  end

  fetched.concat(results)
  results_length = results.length

  if fetch_all
    if results_length > 0
      # TODO (#111186744): Add logging to see if any pages with less than expected data (as seen in the /appointments endpoint)
      new_params = params.deep_dup
      new_params[:PageNumber] = page_number + 1
      paginated_request(method: method, path: path, params: new_params, model: model, fetched: fetched)
    else
      fetched
    end
  else
    results
  end
end

#patch(path, data, booker_model = nil) ⇒ Object



77
78
79
80
81
# File 'lib/booker/client.rb', line 77

def patch(path, data, booker_model=nil)
  booker_resources = get_booker_resources(:patch, path, nil, data.to_json, booker_model)

  build_resources(booker_resources, booker_model)
end

#post(path, data, booker_model = nil) ⇒ Object



65
66
67
68
69
# File 'lib/booker/client.rb', line 65

def post(path, data, booker_model=nil)
  booker_resources = get_booker_resources(:post, path, nil, data.to_json, booker_model)

  build_resources(booker_resources, booker_model)
end

#put(path, data, booker_model = nil) ⇒ Object



71
72
73
74
75
# File 'lib/booker/client.rb', line 71

def put(path, data, booker_model=nil)
  booker_resources = get_booker_resources(:put, path, nil, data.to_json, booker_model)

  build_resources(booker_resources, booker_model)
end

#update_token_storeObject



198
199
200
201
202
# File 'lib/booker/client.rb', line 198

def update_token_store
  if self.token_store.present? && self.token_store_callback_method.present?
    self.token_store.send(self.token_store_callback_method, self.temp_access_token, self.temp_access_token_expires_at)
  end
end