tribune_recurly_api 0.4.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b9eafd5553b067ea352e854a202c773f0565c728b0fd89c1b8775702f36ee29
4
+ data.tar.gz: 73269fbd07f54d384a52a04bdc3602c1162386cf942763e2becd16b336a728a9
5
+ SHA512:
6
+ metadata.gz: 0765af740d352b35433b5fe4b0ab2b4b0c19428b776c4f69c42dac223f7290d839dc1c4a0345f4cbb53d057d306250ed94708f6743df027d6f16fc227b95015f
7
+ data.tar.gz: cb56612cea6cd6584ecbaae4c31d31d3935167dc7b31c71abd1be887b6fde12b2cbc26b664ad4ecb7ba923b575c2a09bf00915ef1ba28eaf012b270e7bb0baf2
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'recurly_api/version'
4
+ require 'recurly_api/client'
5
+ # some info goes here..
6
+ module RecurlyApi
7
+ class Error < StandardError; end
8
+
9
+ # RecurlyApi.logger.debug("I'm a debug log")
10
+ # logger.info("I'm an info log")
11
+ # logger.warn("I'm a warn log")
12
+ # logger.error("I'm an error log: error message")
13
+ # logger.fatal("I'm a fatal log")
14
+ def self.logger
15
+ # @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
16
+ @@logger ||= Logger.new(STDOUT)
17
+ end
18
+
19
+ def self.logger=(logger)
20
+ @@logger = logger
21
+ end
22
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ require 'rest-client'
4
+ require 'json'
5
+ module RecurlyApi
6
+ # Wrapper class to connect Recurly API end-points
7
+ class Client
8
+ # Requesting API end-points
9
+ require_relative './client/accounts'
10
+ require_relative './client/plans'
11
+ require_relative './client/subscriptions'
12
+ require_relative './client/other_requests.rb'
13
+
14
+ API_DEFAULTS = { base_host: 'v3.recurly.com',
15
+ api_version: 'v2021-02-25' }.freeze
16
+ # request for The number of records to return per page
17
+ RECORDS_LIMIT = 20
18
+ RATE_LIMIT_MAX_RETRIES = 3
19
+
20
+ attr_accessor :site_id, :authorization_key, :base_host,
21
+ :api_version, :api_endpoint, :ratelimit_retries
22
+
23
+ # Initialize a client.
24
+ # KeyWord args are optional while initializing and defaults to the values present in recurly_config.yml,
25
+ # Ex: If you want to use custom 'api_version' instead predefined, initialize client as below,
26
+ # rc = RecurlyApi::Client.new(api_version: 'v2019-10-10')
27
+ # @param authorization_key [String] Required, Recurly API auth key.
28
+ # @param site_id [String] Required, ex: 'subdomain-tribune' for https://tribune.recurly.com
29
+ # @param base_host [String] Optional, default: v3.recurly.com
30
+ # @param api_version [String] Optional, Recurly api_version ex: 'v2021-02-25' | 'v2019-10-10'
31
+ # @param ratelimit_retries [Integer] Optional, retry limit for rate limit exceeds, default: 3
32
+ def initialize(authorization_key: nil,
33
+ site_id: nil,
34
+ base_host: nil,
35
+ api_version: nil,
36
+ ratelimit_retries: RATE_LIMIT_MAX_RETRIES)
37
+ @authorization_key = authorization_key
38
+ @site_id = site_id
39
+ raise ArgumentError, "'authorization_key' must be set to a non-nil value" if @authorization_key.nil?
40
+ raise ArgumentError, "'site_id' must be set to a non-nil value" if @site_id.nil?
41
+
42
+ @base_host = base_host || API_DEFAULTS[:base_host]
43
+ @api_version = api_version || API_DEFAULTS[:api_version]
44
+ @api_endpoint = "https://#{@base_host}/sites/#{@site_id}"
45
+ @ratelimit_retries = ratelimit_retries
46
+ end
47
+
48
+ # request Recurly V3 API (v2021-02-25)(https://developers.recurly.com/api/v2021-02-25/)
49
+ # @param path [String] path for API call -> ex: 'plans/{plan_id}', 'accounts/{account_id}'.
50
+ # @param http_method [Symbol] request method -> ex: get | post | put | delete.
51
+ # @param params [Hash] query parameters -> ex: { limit: 20, order: :asc etc }
52
+ # @param payload [Hash] request body parameters( for post, put etc..)
53
+ # @param add_headers [Hash] additional headers if any ex: User-Agent etc..
54
+ # RestClient retry { https://blog.appsignal.com/2018/05/16/ensure-retry-and-reraise-exceptions-in-ruby.html }
55
+ def request_api(path:, http_method: :get, params: {}, payload: {}, add_headers: {})
56
+ raise ArgumentError, "'request path' must be set to a non-nil value" if path.nil?
57
+
58
+ max_retries = ratelimit_retries # max count to retry if rate limit requests are exceeded.
59
+ retry_count = 0
60
+ delay = 1 # in seconds
61
+ end_point = "#{api_endpoint}/#{path}"
62
+ headers = ensure_request_headers(add_headers).merge!(params: params)
63
+ resp = RestClient::Request.execute(method: http_method,
64
+ url: end_point,
65
+ payload: payload.empty? ? payload : payload.to_json,
66
+ headers: headers)
67
+ rescue RestClient::ExceptionWithResponse => e
68
+ e.response
69
+ rescue RestClient::Exception => e
70
+ # Timed out reading data from server
71
+ e.response
72
+ rescue StandardError => e
73
+ raise e.message
74
+ else
75
+ begin
76
+ raise 'API Rate limit exceeded' if rate_limit_exceeded?(resp)
77
+ rescue StandardError => _e
78
+ RecurlyApi.logger.error "API Rate limit exceeded retrying again. Retries left: #{max_retries - retry_count}"
79
+ sleep delay += retry_count
80
+ retry_count += 1
81
+ retry if retry_count < max_retries
82
+ end
83
+ resp
84
+ end
85
+
86
+ def ensure_request_headers(add_headers)
87
+ accept_header = "application/vnd.recurly.#{api_version}+json"
88
+ { "Authorization": authorization_key,
89
+ "Content-Type": 'application/json',
90
+ accept: accept_header }.merge!(add_headers)
91
+ end
92
+
93
+ def handle_response!(resp, payload_name: :data, single_node: false)
94
+ if rate_limit_exceeded?(resp)
95
+ handle_rate_limit_exceed(resp)
96
+ else
97
+ json_body = JSON.parse(resp.body)
98
+ if json_body['error']
99
+ handle_error_response(resp.code, json_body)
100
+ else
101
+ { success: true, status: resp.code,
102
+ "#{payload_name}": single_node ? json_body : json_body['data'] }
103
+ end
104
+ end
105
+ end
106
+
107
+ def handle_error_response(code, json_body)
108
+ { success: false, status: code,
109
+ error: json_body['error']['type'],
110
+ message: json_body['error']['message'] }
111
+ end
112
+
113
+ # Recurly rate limiting
114
+ # { https://developers.recurly.com/api/v2019-10-10/#section/Getting-Started/Limits }
115
+ # The rate limit is calculated over a sliding 5 minute window.
116
+ # This means a production site could make 4,000 requests within one minute and not hit the rate limit
117
+ # so long as the site made less than 1,000 requests during the prior 4 minute
118
+ # --------------
119
+ # Response Headers that returned related to rate limiting by Recurly.
120
+ # x_ratelimit_limit: The maximum number of requests available in the current time frame(5-minite window)
121
+ # x_ratelimit_remaining: the number of requests left for the 5-minute window
122
+ # x_ratelimit_reset: the remaining window before the rate limit resets, in UNIX Epoch seconds
123
+
124
+ # Check if rate limit exceeded or not using resp status code OR x_ratelimit_remaining header
125
+ def rate_limit_exceeded?(resp)
126
+ ratelimit_remaining = resp.headers[:x_ratelimit_remaining]
127
+ resp.code.eql?(429) || (ratelimit_remaining && ratelimit_remaining.to_i < 1)
128
+ end
129
+
130
+ def handle_rate_limit_exceed(resp)
131
+ resp_headers = resp.headers
132
+ rate_limit_info = { limit: resp_headers[:x_ratelimit_limit],
133
+ remaining: resp_headers[:x_ratelimit_remaining],
134
+ reset_time_seconds: resp_headers[:x_ratelimit_reset],
135
+ reset_time: DateTime.strptime(resp_headers[:x_ratelimit_reset], '%s') }
136
+ { success: false, status: 429,
137
+ error: 'Recurly API Rate limit exceeded',
138
+ message: 'Too Many Requests, See rate-limiting for more info',
139
+ rate_limit_info: rate_limit_info }
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecurlyApi
4
+ # API requests to fetch Accounts related data
5
+ class Client
6
+ # Fetch Account Info--
7
+ # { https://developers.recurly.com/api/v2021-02-25/index.html#operation/get_account }
8
+ # @param ssor_id [String] Required, Account ID(SsorID) or code.
9
+ # For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-bob+.
10
+ def account_info(ssor_id:)
11
+ resp = request_api(path: "accounts/#{ssor_id}")
12
+ handle_response!(resp, payload_name: :account_info, single_node: true)
13
+ end
14
+
15
+ # Deactivate account ---
16
+ # { https://developers.recurly.com/api/v2021-02-25/index.html#operation/deactivate_account }
17
+ # @param ssor_id [String] Required, Account ID(SsorID) or code.
18
+ # For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-bob+.
19
+ def deactivate_account(ssor_id:)
20
+ path = "accounts/#{ssor_id}"
21
+ resp = request_api(path: path, http_method: :delete)
22
+ handle_response!(resp, payload_name: :account_info, single_node: true)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecurlyApi
4
+ # This class fetches the data of Recurly API end-points
5
+ class Client
6
+ # Show the coupon redemptions for an account --
7
+ # { https://developers.recurly.com/api/v2021-02-25/index.html#tag/coupon_redemption }
8
+ # @param ssor_id [String] Recurly Account ID(SsorID) or code.
9
+ # For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-bob+.
10
+ def coupon_redemptions(ssor_id:)
11
+ resp = request_api(path: "accounts/#{ssor_id}/coupon_redemptions")
12
+ handle_response!(resp, payload_name: :coupon_redemptions, single_node: true)
13
+ end
14
+
15
+ # Other request any.....
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecurlyApi
4
+ # API requests to fetch Plans related data
5
+ class Client
6
+ # List a site's plans
7
+ # { https://developers.recurly.com/api/v2021-02-25/index.html#operation/list_plans }
8
+ # @param params [Hash] Optional query string parameters:
9
+ # :ids [String] Filter results by their IDs. Up to 200 IDs can be passed at once using
10
+ # commas as separators, e.g. +ids=h1at4d57xlmy,gyqgg0d3v9n1,jrsm5b4yefg6+.
11
+ # :limit [Integer] Limit number of records 1-200.
12
+ # :order [String] Sort order.
13
+ # :sort [String] Sort field. You *really* only want to sort by +updated_at+ in ascending
14
+ # ex:
15
+ # params = { limit: 2, order: :asc, .....}
16
+ # ex: client.list_plans(params)
17
+ def list_plans(**params)
18
+ resp = request_api(path: 'plans', params: params)
19
+ # handle_response(resp, payload_name: :plans)
20
+ handle_response!(resp)
21
+ end
22
+
23
+ # Fetch Plan Info--
24
+ # { https://developers.recurly.com/api/v2021-02-25/index.html#operation/get_plan}
25
+ # @param plan_id [String] Plan ID or code.
26
+ # For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-gold+.
27
+ def plan_info(plan_id:)
28
+ resp = request_api(path: "plans/#{plan_id}")
29
+ handle_response!(resp, payload_name: :plan_info, single_node: true)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RecurlyApi
4
+ # API requests to fetch Subscriptions related data
5
+ class Client
6
+ # Fetch Account Subscriptions ---
7
+ # { https://developers.recurly.com/api/v2019-10-10/index.html#operation/list_account_subscriptions }
8
+ # @param ssor_id [String] Account ID(SsorID) or code.
9
+ # For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-gold+.
10
+ # For SsorID no prefix is required
11
+ # @param params [Hash] Optional query string parameters
12
+ def account_subscriptions(ssor_id:, **params)
13
+ path = "accounts/#{ssor_id}/subscriptions"
14
+ resp = request_api(path: path, params: params)
15
+ handle_response!(resp)
16
+ end
17
+
18
+ # Checking if user already has subscription OR
19
+ # Checking if user has a subscription for particular plan by its code
20
+ # ---------------
21
+ # @param ssor_id [String] required, Recurly Account Code(i.e SsorID)
22
+ # @prams plan_code [String] optional,
23
+ # - if present then checks if user has subscription for that particular plan by its code
24
+ # Usage ex:
25
+ # client.check_user_subscription(ssor_id: '4900-0272-6875', plan_code: '000150d03d')
26
+ # response => { :success=>true, :status=>200, :has_subscription=>true }
27
+ def check_user_subscription(ssor_id:, plan_code: nil)
28
+ resp = account_subscriptions(ssor_id: "code-#{ssor_id}")
29
+ if resp[:success]
30
+ has_subscription = false
31
+ subs = resp[:data]
32
+ has_subscription = true if subs.any?
33
+ if plan_code
34
+ sub = subs.detect { |s| s['plan']['code'].eql?(plan_code) }
35
+ has_subscription = sub ? true : false
36
+ end
37
+ { success: true, status: 200, has_subscription: has_subscription }
38
+ else
39
+ resp
40
+ end
41
+ end
42
+
43
+ # List a site's subscriptions
44
+ # { https://developers.recurly.com/api/v2021-02-25/#operation/list_account_subscriptions }
45
+ # @param params [Hash] Optional query string parameters:
46
+ # :ids [String] Filter results by their IDs. Up to 200 IDs can be passed at once using
47
+ # commas as separators, e.g. +ids=h1at4d57xlmy,gyqgg0d3v9n1,jrsm5b4yefg6+.
48
+ # :limit [Integer] Limit number of records 1-200.
49
+ # :order [String] Sort order.
50
+ # :sort [String] Sort field. You *really* only want to sort by +updated_at+ in ascending
51
+ # ex:
52
+ # params = { limit: RECORDS_LIMIT, order: :asc, .....}
53
+ # ex: client.site_subscriptions(params)
54
+ def site_subscriptions(**params)
55
+ resp = request_api(path: 'subscriptions', params: params)
56
+ handle_response!(resp)
57
+ end
58
+
59
+ # Create subscription(new purchase) by ssor_id(Recurly Account code)
60
+ # { https://developers.recurly.com/api/v2021-02-25/index.html#operation/create_purchase }
61
+ # @param ssor_id [String] Required, user SSOR_ID (i.e. Reculry Account Code)
62
+ # @param plan_code [String] Required, plan_code or ID
63
+ # @param billing_token [String] Required, token genereted by recurlyjs while checkout
64
+ # @param gateway_code [String] Optional,
65
+ # Payment gateway identifier to be used for the purchase transaction
66
+ # ( required only if handling Braintree Multiple Merchant accounts )
67
+ # @param account_info [Hash] Optional query parameters(user provided info while checkout)
68
+ # :first_name [String], user First Name
69
+ # :last_name [String], user Last Name
70
+ # :email [String] user Email
71
+ # TODO: fetch user email, first_name, last_name by ssor_id connecting to SsorClient
72
+ # Better to pass first_name, last_name, email fetched from Checkout Page,
73
+ # instead of extra DB call to SSOR to get the user details
74
+ def create_subscription(ssor_id:, billing_token:, plan_code:,
75
+ gateway_code: nil, account_info: {})
76
+ account = { code: ssor_id,
77
+ billing_info: { token_id: billing_token } }
78
+ purchase_payload = { currency: 'USD',
79
+ account: account.merge!(account_info),
80
+ subscriptions: [{ plan_code: plan_code }] }
81
+ purchase_payload[:gateway_code] = gateway_code if gateway_code
82
+ path = 'purchases'
83
+ resp = request_api(path: path, http_method: :post,
84
+ payload: purchase_payload)
85
+ handle_response!(resp, payload_name: :purchase_info, single_node: true)
86
+ end
87
+
88
+ # Cancel a subscription by its id or UUID
89
+ # @param sub_id_or_uuid [String] Required
90
+ # Subscription ID or UUID. For ID no prefix is used e.g. e28zov4fw0v2.
91
+ # For UUID use prefix uuid-, e.g. uuid-123457890
92
+ def cancel_subscription(sub_id_or_uuid)
93
+ path = "subscriptions/#{sub_id_or_uuid}/cancel"
94
+ resp = request_api(path: path, http_method: :put)
95
+ handle_response!(resp, payload_name: :subscription_info, single_node: true)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Gem visioning
4
+ module RecurlyApi
5
+ VERSION = '0.4.3'
6
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'recurly_api'
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tribune_recurly_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.3
5
+ platform: ruby
6
+ authors:
7
+ - udevulapally
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-06-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '10.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '10.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rest-client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.8.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.8.0
55
+ description: Wrapper for connecting to Recurly V3 API.
56
+ email:
57
+ - udevulapally@tribpub.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - lib/recurly_api.rb
63
+ - lib/recurly_api/client.rb
64
+ - lib/recurly_api/client/accounts.rb
65
+ - lib/recurly_api/client/other_requests.rb
66
+ - lib/recurly_api/client/plans.rb
67
+ - lib/recurly_api/client/subscriptions.rb
68
+ - lib/recurly_api/version.rb
69
+ - lib/tribune_recurly_api.rb
70
+ homepage:
71
+ licenses: []
72
+ metadata:
73
+ allowed_push_host: https://rubygems.org/
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.1.4
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Tribune's Ruby Client for Recurly's API integration..
93
+ test_files: []