Class: RecurlyApi::Client

Inherits:
Object
  • Object
show all
Includes:
Caching, ExceptionHandler, Logging, RateLimiting
Defined in:
lib/recurly_api/client.rb,
lib/recurly_api/client/plans.rb,
lib/recurly_api/client/accounts.rb,
lib/recurly_api/client/subscriptions.rb,
lib/recurly_api/client/other_requests.rb

Overview

This class fetches the data of Recurly API end-points

Constant Summary collapse

API_DEFAULTS =
{ base_host: 'v3.recurly.com',
api_version: 'v2021-02-25' }.freeze
RECORDS_LIMIT =

request for The number of records to return per page

20

Constants included from Caching

RecurlyApi::Caching::CACHE_SETTINGS

Constants included from RateLimiting

RateLimiting::RATE_LIMIT_MAX_RETRIES

Instance Attribute Summary collapse

Attributes included from Caching

#cache_expires_in, #cache_key, #ignore_caching

Attributes included from RateLimiting

#ratelimit_retries

Attributes included from Logging

#logger

Instance Method Summary collapse

Methods included from Caching

#cache, #cache_key_with_prefix, #cache_prefix_name, #caching_enabled?, #check_logs_for_caching_info, #fetch_response_from_cache, #ignore_caching?, #write_final_resp_to_cache

Methods included from RateLimiting

#rate_limit_exceed_response, #rate_limit_exceeded?, #retry_on_rate_limit_exceed

Methods included from ExceptionHandler

#recurly_api_exception_handler

Methods included from Logging

#logger_cache_heading, #logger_heading

Constructor Details

#initialize(authorization_key:, site_id:, base_host: nil, api_version: nil, ratelimit_retries: RATE_LIMIT_MAX_RETRIES, cache_settings: CACHE_SETTINGS) ⇒ Client

Initialize a client. KeyWord args are optional while initializing and defaults to the values present in recurly_config.yml(Rails only), Ex: If you want to use custom ‘api_version’ instead predefined, initialize client as below,

rc = RecurlyApi::Client.new(api_version: 'v2019-10-10')

rubocop:disable Metrics/ParameterLists

Parameters:

  • authorization_key (String)

    Required, Recurly API auth key.

  • site_id (String)

    Required, ex: ‘subdomain-tribune’ for tribune.recurly.com

  • base_host (String) (defaults to: nil)

    Optional, default: v3.recurly.com

  • api_version (String) (defaults to: nil)

    Optional, Recurly api_version ex: ‘v2021-02-25’ | ‘v2019-10-10’

  • ratelimit_retries (Integer) (defaults to: RATE_LIMIT_MAX_RETRIES)

    Optional, retry limit for rate limit exceeds, default: 3

  • cache_settings (Hash) (defaults to: CACHE_SETTINGS)

    Optional, default settings for caching

Raises:

  • (ArgumentError)


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/recurly_api/client.rb', line 50

def initialize(authorization_key:,
               site_id:,
               base_host: nil,
               api_version: nil,
               ratelimit_retries: RATE_LIMIT_MAX_RETRIES,
               cache_settings: CACHE_SETTINGS)
  @authorization_key = authorization_key
  @site_id = site_id
  raise ArgumentError, "'authorization_key' must be set to a non-nil value" if @authorization_key.nil?
  raise ArgumentError, "'site_id' must be set to a non-nil value" if @site_id.nil?

  @base_host = base_host || API_DEFAULTS[:base_host]
  @api_version = api_version || API_DEFAULTS[:api_version]
  @api_endpoint = "https://#{@base_host}/sites/#{@site_id}"
  @ratelimit_retries = ratelimit_retries
  @cache_settings = cache_settings
  @cache_expires_in = cache_settings[:default_expiry]
  @ignore_caching = cache_settings[:bypass]
end

Instance Attribute Details

#additional_headersObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def additional_headers
  @additional_headers
end

#api_endpointObject

Initialize method attributes



29
30
31
# File 'lib/recurly_api/client.rb', line 29

def api_endpoint
  @api_endpoint
end

#api_versionObject

Initialize method attributes



29
30
31
# File 'lib/recurly_api/client.rb', line 29

def api_version
  @api_version
end

#authorization_keyObject

Initialize method attributes



29
30
31
# File 'lib/recurly_api/client.rb', line 29

def authorization_key
  @authorization_key
end

#base_hostObject

Initialize method attributes



29
30
31
# File 'lib/recurly_api/client.rb', line 29

def base_host
  @base_host
end

#cache_key_nameObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def cache_key_name
  @cache_key_name
end

#cache_settingsObject

Initialize method attributes



29
30
31
# File 'lib/recurly_api/client.rb', line 29

def cache_settings
  @cache_settings
end

#final_responseObject

response attributes



36
37
38
# File 'lib/recurly_api/client.rb', line 36

def final_response
  @final_response
end

#http_methodObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def http_method
  @http_method
end

#optional_paramsObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def optional_params
  @optional_params
end

#path_nameObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def path_name
  @path_name
end

#payloadObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def payload
  @payload
end

#payload_nameObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def payload_name
  @payload_name
end

#query_paramsObject

request attributes for request_api method



32
33
34
# File 'lib/recurly_api/client.rb', line 32

def query_params
  @query_params
end

#recurly_responseObject

response attributes



36
37
38
# File 'lib/recurly_api/client.rb', line 36

def recurly_response
  @recurly_response
end

#site_idObject

Initialize method attributes



29
30
31
# File 'lib/recurly_api/client.rb', line 29

def site_id
  @site_id
end

Instance Method Details

#account_info(ssor_id:, **optional_params) ⇒ Object

Parameters:

  • ssor_id (String)

    , Recurly Account Code(i.e SsorID)

    • The unique identifier of the Recurly account



10
11
12
13
14
15
16
# File 'lib/recurly_api/client/accounts.rb', line 10

def (ssor_id:, **optional_params)
  # cache_key should be uniq for each account(here ssor_id is uniq)
  # final cache key with prefix: tribune_recurly_api.account_info.<abcdef>
  cache_key_name = "account_info.#{ssor_id}"
  request_api(path_name: "accounts/code-#{ssor_id}",
              cache_key_name: cache_key_name, **optional_params)
end

#account_subscriptions(ssor_id:, recurly_query_params: {}, **optional_params) ⇒ Object

Fetch Account Subscriptions — { developers.recurly.com/api/v2021-02-25/index.html#operation/list_account_subscriptions }

for other params refer documentation

Parameters:

  • ssor_id (String)

    , Recurly Account Code(i.e SsorID)

    • The unique identifier of the Recurly account



11
12
13
14
15
16
17
18
19
# File 'lib/recurly_api/client/subscriptions.rb', line 11

def (ssor_id:, recurly_query_params: {}, **optional_params)
  path = "accounts/code-#{ssor_id}/subscriptions"
  self.cache_expires_in = optional_params[:cache_expires_in] || cache_settings[:subscription_expiry] unless bypass_caching?
  # cache_key should be uniq for each account(here ssor_id is uniq)
  # final cache key with prefix: tribune_recurly_api.account_subscriptions.<abcdef>
  cache_key_name = "account_subscriptions.#{ssor_id}"
  request_api(path_name: path, query_params: recurly_query_params,
              cache_key_name: cache_key_name, **optional_params)
end

#cancel_subscription(sub_id_or_uuid) ⇒ Object

Cancel a subscription by its id or UUID

Parameters:

  • sub_id_or_uuid (String)

    Required Subscription ID or UUID. For ID no prefix is used e.g. e28zov4fw0v2. For UUID use prefix uuid-, e.g. uuid-123457890



96
97
98
99
# File 'lib/recurly_api/client/subscriptions.rb', line 96

def cancel_subscription(sub_id_or_uuid)
  path = "subscriptions/#{sub_id_or_uuid}/cancel"
  request_api(path_name: path, http_method: :put)
end

#check_user_subscription(ssor_id:, plan_code: nil) ⇒ Object

Checking if user already has subscription OR Checking if user has a subscription for particular plan by its code


Usage ex: client.check_user_subscription(ssor_id: ‘4900-0272-6875’, plan_code: ‘000150d03d’) response => { :success=>true, :status_code=>200, :has_subscription=>true }

Parameters:

  • ssor_id (String)

    required, Recurly Account Code(i.e SsorID)

    • The unique identifier of the Recurly account

  • plan_code (String) (defaults to: nil)

    optional,

    • if present then checks if user has subscription for that particular plan by its code



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/recurly_api/client/subscriptions.rb', line 31

def check_user_subscription(ssor_id:, plan_code: nil)
  query_params = { limit: 2 }
  resp = (ssor_id: ssor_id, recurly_query_params: query_params)
  if resp[:success]
    has_subscription = false
    subs = resp[:payload]['data']
    has_subscription = true if subs&.any?
    if plan_code
      sub =  subs.detect { |s| s['plan']['code'].eql?(plan_code) }
      has_subscription = sub ? true : false
    end
    { success: true, status: 200, has_subscription: has_subscription }
  else
    resp
  end
end

#coupon_redemptions(ssor_id:, **optional_params) ⇒ Object

Show the coupon redemptions for an account – { developers.recurly.com/api/v2021-02-25/index.html#tag/coupon_redemption } For ID no prefix is used e.g. e28zov4fw0v2. For code use prefix code-, e.g. code-bob.

Parameters:

  • ssor_id (String)

    Recurly Account ID(SsorID) or code.



10
11
12
13
14
15
16
# File 'lib/recurly_api/client/other_requests.rb', line 10

def coupon_redemptions(ssor_id:, **optional_params)
  # cache_key should be uniq for each account(here ssor_id is uniq)
  # final cache key with prefix: tribune_recurly_api.coupon_redemptions.<abcdef>
  cache_key_name = "coupon_redemptions.#{ssor_id}"
  request_api(path_name: "accounts/code-#{ssor_id}/coupon_redemptions",
              cache_key_name: cache_key_name, **optional_params)
end

#create_subscription(ssor_id:, billing_token:, plan_code:, gateway_code: nil, account_info: {}) ⇒ Object

Create subscription(new purchase) by ssor_id(Recurly Account code) { developers.recurly.com/api/v2021-02-25/index.html#operation/create_purchase } TODO: fetch user email, first_name, last_name by ssor_id connecting to SsorClient Better to pass first_name, last_name, email fetched from Checkout Page,

instead of extra DB call to SSOR to get the user details

Parameters:

  • ssor_id (String)

    Required, user SSOR_ID (i.e. Reculry Account Code)

  • plan_code (String)

    Required, plan_code or ID

  • billing_token (String)

    Required, token genereted by recurlyjs while checkout

  • gateway_code (String) (defaults to: nil)

    Optional, Payment gateway identifier to be used for the purchase transaction ( required only if handling Braintree Multiple Merchant accounts )

  • account_info (Hash) (defaults to: {})

    Optional query parameters(user provided info while checkout) :first_name [String], user First Name :last_name [String], user Last Name :email [String] user Email



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/recurly_api/client/subscriptions.rb', line 79

def create_subscription(ssor_id:, billing_token:, plan_code:,
                        gateway_code: nil, account_info: {})
   = { code: ssor_id,
              billing_info: { token_id: billing_token } }
  purchase_payload = { currency: 'USD',
                       account: .merge!(),
                       subscriptions: [{ plan_code: plan_code }] }
  purchase_payload[:gateway_code] = gateway_code if gateway_code
  path = 'purchases'
  request_api(path_name: path, http_method: :post,
              payload: purchase_payload)
end

#deactivate_account(ssor_id:) ⇒ Object

Parameters:

  • ssor_id (String)

    , Recurly Account Code(i.e SsorID)

    • The unique identifier of the Recurly account



22
23
24
25
# File 'lib/recurly_api/client/accounts.rb', line 22

def (ssor_id:)
  path = "accounts/code-#{ssor_id}"
  request_api(path_name: path, http_method: :delete)
end

#ensure_request_headersObject



161
162
163
164
165
166
# File 'lib/recurly_api/client.rb', line 161

def ensure_request_headers
  accept_header = "application/vnd.recurly.#{api_version}+json"
  { "Authorization": authorization_key,
    "Content-Type": 'application/json',
    accept: accept_header }.merge!(additional_headers)
end

#fetch_response_from_recurly(cache_recurly_resp: false) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/recurly_api/client.rb', line 141

def fetch_response_from_recurly(cache_recurly_resp: false)
  # rescue/raise all your all exceptions in ExceptionHandler's method
  recurly_api_exception_handler do
    end_point = "#{api_endpoint}/#{path_name}"
    headers = ensure_request_headers.merge!(params: query_params)
    self.recurly_response = RestClient::Request.execute(method: http_method,
                                                        url: end_point,
                                                        payload: payload.empty? ? nil : payload.to_json,
                                                        headers: headers)
    self.final_response = handle_recurly_response!
    retry_on_rate_limit_exceed
    # don't cache rate_limit_exceed_response
    if cache_recurly_resp && !rate_limit_exceeded?
      write_final_resp_to_cache
      logger.info("#{logger_cache_heading}: Recurly Response saved to cache_store with Key: #{cache_key_with_prefix}")
    end
    final_response
  end
end

#handle_error_response(code, json_body) ⇒ Object



182
183
184
185
186
# File 'lib/recurly_api/client.rb', line 182

def handle_error_response(code, json_body)
  { success: false, status_code: code,
    error: json_body['error']['type'],
    message: json_body['error']['message'] }
end

#handle_recurly_error_response(resp) ⇒ Object



188
189
190
191
192
193
# File 'lib/recurly_api/client.rb', line 188

def handle_recurly_error_response(resp)
  json_body = JSON.parse(resp.body)
  return unless json_body['error']

  handle_error_response(resp.code, json_body)
end

#handle_recurly_response!Object



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/recurly_api/client.rb', line 168

def handle_recurly_response!
  if rate_limit_exceeded?
    rate_limit_exceed_response
  else
    json_body = JSON.parse(recurly_response.body)
    if json_body['error']
      handle_error_response(recurly_response.code, json_body)
    else
      { success: true, status_code: recurly_response.code,
        "#{payload_name}": json_body }
    end
  end
end

#list_plans(recurly_query_params: {}, **optional_params) ⇒ Object

Examples:

query_params = { limit: 2, order: :asc, ids: ['abc', 'def'..etc], :sort: <value>}
client.list_plans(recurly_query_params: query_params, bypass_cache: true)
client.list_plans(recurly_query_params: query_params, cache_expires_in: 120, payload_name: :fetch_plans)

client.list_plans(recurly_query_params: query_params)



27
28
29
30
31
32
33
# File 'lib/recurly_api/client/plans.rb', line 27

def list_plans(recurly_query_params: {}, **optional_params)
  # payload_name = optional_params[:payload_name] # if not present default value is :payload
  # cache_key_name = 'list_all_plans' # optional by default cache_key is caller method_name in receiver i.e. list_plans here
  self.cache_expires_in = optional_params[:cache_expires_in] || cache_settings[:plan_expiry] unless bypass_caching?
  # cache_key_name = 'all_plans' # for custom cache key pass as extra argument
  request_api(path_name: 'plans', query_params: recurly_query_params, **optional_params)
end

#plan_add_ons(plan_id:, **optional_params) ⇒ Object

Parameters:

  • plan_id (String)

    Required, Plan ID or code.

    • For ID no prefix is used e.g. <e28zov4fw0v2>. For code use prefix <code->, ex. <code-gold>.

  • optional_params (Hash)

    Optional, same as above



54
55
56
57
58
59
60
61
# File 'lib/recurly_api/client/plans.rb', line 54

def plan_add_ons(plan_id:, **optional_params)
  self.cache_expires_in = optional_params[:cache_expires_in] || cache_settings[:plan_expiry] unless bypass_caching?
  # cache_key should be uniq for each plan(here plan_id is uniq)
  # final cache key with prefix: tribune_recurly_api.plan_info.<abcdef>
  cache_key_name = "plan_info.#{plan_id}.add_ons"
  request_api(path_name: "plans/#{plan_id}/add_ons",
              cache_key_name: cache_key_name, **optional_params)
end

#plan_info(plan_id:, **optional_params) ⇒ Object

Parameters:

  • plan_id (String)

    Required, Plan ID or code.

    • For ID no prefix is used e.g. <e28zov4fw0v2>. For code use prefix <code->, ex. <code-gold>.

  • optional_params (Hash)

    Optional, same as above



40
41
42
43
44
45
46
47
# File 'lib/recurly_api/client/plans.rb', line 40

def plan_info(plan_id:, **optional_params)
  self.cache_expires_in = optional_params[:cache_expires_in] || cache_settings[:plan_expiry] unless bypass_caching?
  # cache_key should be uniq for each plan(here plan_id is uniq)
  # final cache key with prefix: tribune_recurly_api.plan_info.<abcdef>
  cache_key_name = "plan_info.#{plan_id}"
  request_api(path_name: "plans/#{plan_id}",
              cache_key_name: cache_key_name, **optional_params)
end

#request_api(path_name:, http_method: :get, query_params: {}, payload: {}, additional_headers: {}, cache_key_name: nil, **optional_params) ⇒ Object

Ful Usage Example: Below is the example for ‘request_api’ call that includes all options

res = request_api(path_name: 'plans', # required
                  http_method: :get,  # optional, :post|put|patch|delete
                  query_params: recurly_query_params, # optional
                  payload: {}, # optional, request body params
                  additional_headers: { 'User-Agent': 'abc', ..}, # optional
                  cache_key_name: 'key123', # optional, defaults to method_name
                  bypass_caching: true, # optionals, default false
                  payload_name: 'list_all_plans', # optional defaults to caller method_name
                  cache_expiry_in: 300 # optional default 300
                  )

RestClient retry { blog.appsignal.com/2018/05/16/ensure-retry-and-reraise-exceptions-in-ruby.html } rubocop:disable Metrics/AbcSize rubocop:disable Metrics/PerceivedComplexity rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/MethodLength

Raises:

  • (ArgumentError)


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/recurly_api/client.rb', line 103

def request_api(path_name:, http_method: :get, query_params: {},
                payload: {}, additional_headers: {},
                cache_key_name: nil, **optional_params)
  raise ArgumentError, "'request path' must be set to a non-nil value" if path_name.nil?

  self.path_name = path_name # set the HTTP Verb (caching is only done for HTTP GET requests)
  self.http_method = http_method # set the HTTP Verb (caching is only done for HTTP GET requests)
  self.query_params = query_params
  self.payload = payload
  self.additional_headers = additional_headers
  self.cache_key_name = cache_key_name
  self.optional_params = optional_params
  # optional params ===>
  self.payload_name = optional_params[:payload_name] || 'payload'
  self.ignore_caching = optional_params[:bypass_caching] || ignore_caching
  self.cache_expires_in = optional_params[:cache_expires_in] || cache_expires_in
  # caller_method_name = caller_locations.first.label # Ruby 2.0 +
  caller_method_name = caller(1..1).first[/`(.*)'/, 1] # Prior Ruby 2.0
  self.cache_key = cache_key_name || caller_method_name # fallbacks to caller method if cache_key_name not present
  # TODO: remove below method once integration testing is completed, also remove unnecessary logs
  check_logs_for_caching_info
  # Cashing related stuff... (caching is performed on response only on :get requests when it is enabled)
  # by default Cache  bypassed for non rails applications
  if caching_enabled? && !bypass_caching? && http_method.eql?(:get)
    cached_response = fetch_response_from_cache
    # Do NOT use a falsy check because we need to distinguish 'false' from 'nil'
    if cached_response.nil?
      fetch_response_from_recurly(cache_recurly_resp: true)
    else
      logger.info("#{logger_cache_heading}: response returned from cache store and key is: #{cache_key_with_prefix}")
      cached_response
    end
  else
    logger.warn("#{logger_cache_heading}: caching of Recurly Response completely bypassed")
    fetch_response_from_recurly
  end
end

#site_subscriptions(recurly_query_params: {}, **optional_params) ⇒ Object

List a site’s subscriptions { developers.recurly.com/api/v2021-02-25/#operation/list_account_subscriptions }

 ii. :limit [Integer] Limit number of records 1-200.
iii. :order [String] Sort order.
 iv: :sort [String] Sort field. You *really* only want to sort by <updated_at> in ascending

ex: recurly_query_params = { limit: RECORDS_LIMIT, order: :asc, .….} ex: client.site_subscriptions(params)

Parameters:

  • recurly_query_params (Hash) (defaults to: {})

    Optional, Recurly query string parameters:

    1. :ids [String] Filter results by their IDs. Up to 200 IDs can be passed at once using

      commas as separators, e.g. <ids=h1at4d57xlmy,gyqgg0d3v9n1,jrsm5b4yefg6>.
      


59
60
61
62
# File 'lib/recurly_api/client/subscriptions.rb', line 59

def site_subscriptions(recurly_query_params: {}, **optional_params)
  self.cache_expires_in = optional_params[:cache_expires_in] || cache_settings[:subscription_expiry] unless bypass_caching?
  request_api(path_name: 'subscriptions', query_params: recurly_query_params, **optional_params)
end