recurly 3.3.1 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.bumpversion.cfg +5 -1
  3. data/.github/workflows/docs.yml +28 -0
  4. data/.travis.yml +1 -0
  5. data/CHANGELOG.md +92 -1
  6. data/GETTING_STARTED.md +61 -1
  7. data/README.md +1 -1
  8. data/lib/data/ca-certificates.crt +3464 -29
  9. data/lib/recurly.rb +1 -0
  10. data/lib/recurly/client.rb +217 -111
  11. data/lib/recurly/client/operations.rb +301 -1
  12. data/lib/recurly/connection_pool.rb +40 -0
  13. data/lib/recurly/errors.rb +30 -0
  14. data/lib/recurly/errors/network_errors.rb +2 -0
  15. data/lib/recurly/http.rb +13 -8
  16. data/lib/recurly/pager.rb +31 -12
  17. data/lib/recurly/requests/add_on_create.rb +15 -3
  18. data/lib/recurly/requests/add_on_update.rb +9 -1
  19. data/lib/recurly/requests/billing_info_create.rb +26 -2
  20. data/lib/recurly/requests/external_transaction.rb +26 -0
  21. data/lib/recurly/requests/plan_create.rb +8 -0
  22. data/lib/recurly/requests/plan_update.rb +8 -0
  23. data/lib/recurly/requests/shipping_method_create.rb +26 -0
  24. data/lib/recurly/requests/shipping_method_update.rb +26 -0
  25. data/lib/recurly/requests/subscription_add_on_create.rb +5 -1
  26. data/lib/recurly/requests/subscription_add_on_tier.rb +18 -0
  27. data/lib/recurly/requests/subscription_add_on_update.rb +6 -2
  28. data/lib/recurly/requests/subscription_change_create.rb +1 -1
  29. data/lib/recurly/requests/tier.rb +18 -0
  30. data/lib/recurly/resources/add_on.rb +8 -0
  31. data/lib/recurly/resources/line_item.rb +1 -1
  32. data/lib/recurly/resources/payment_method.rb +4 -0
  33. data/lib/recurly/resources/plan.rb +8 -0
  34. data/lib/recurly/resources/shipping_method.rb +4 -0
  35. data/lib/recurly/resources/subscription_add_on.rb +12 -0
  36. data/lib/recurly/resources/subscription_add_on_tier.rb +18 -0
  37. data/lib/recurly/resources/tier.rb +18 -0
  38. data/lib/recurly/resources/transaction.rb +4 -0
  39. data/lib/recurly/version.rb +1 -1
  40. data/openapi/api.yaml +5325 -2782
  41. data/recurly.gemspec +8 -3
  42. data/scripts/changelog +2 -0
  43. data/scripts/format +5 -1
  44. data/scripts/release +5 -3
  45. metadata +17 -38
  46. data/lib/recurly/client/adapter.rb +0 -39
@@ -7,6 +7,7 @@ require "recurly/requests"
7
7
  require "recurly/resources"
8
8
  require "recurly/http"
9
9
  require "recurly/errors"
10
+ require "recurly/connection_pool"
10
11
  require "recurly/client"
11
12
 
12
13
  module Recurly
@@ -1,18 +1,26 @@
1
- require "faraday"
2
1
  require "logger"
3
2
  require "erb"
3
+ require "net/https"
4
+ require "base64"
5
+ require "securerandom"
4
6
  require_relative "./schema/json_parser"
5
7
  require_relative "./schema/file_parser"
6
- require_relative "./client/adapter"
7
8
 
8
9
  module Recurly
9
10
  class Client
10
11
  require_relative "./client/operations"
11
12
 
12
- BASE_URL = "https://v3.recurly.com/"
13
+ BASE_HOST = "v3.recurly.com"
14
+ BASE_PORT = 443
15
+ CA_FILE = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt")
13
16
  BINARY_TYPES = [
14
17
  "application/pdf",
15
- ]
18
+ ].freeze
19
+ JSON_CONTENT_TYPE = "application/json"
20
+ MAX_RETRIES = 3
21
+ LOG_LEVELS = %i(debug info warn error fatal).freeze
22
+ BASE36_ALPHABET = (("0".."9").to_a + ("a".."z").to_a).freeze
23
+ REQUEST_OPTIONS = [:headers].freeze
16
24
 
17
25
  # Initialize a client. It requires an API key.
18
26
  #
@@ -38,27 +46,42 @@ module Recurly
38
46
  # sub = client.get_subscription(subscription_id: 'uuid-abcd7890')
39
47
  #
40
48
  # @param api_key [String] The private API key
41
- # @param site_id [String] The site you wish to be scoped to.
42
- # @param subdomain [String] Optional subdomain for the site you wish to be scoped to. Providing this makes all the `site_id` parameters optional.
43
- def initialize(api_key:, site_id: nil, subdomain: nil, **options)
49
+ # @param logger [Logger] A logger to use. Defaults to creating a new STDOUT logger with level WARN.
50
+ def initialize(api_key:, site_id: nil, subdomain: nil, logger: nil)
44
51
  set_site_id(site_id, subdomain)
45
- set_options(options)
46
- set_faraday_connection(api_key)
52
+ set_api_key(api_key)
53
+
54
+ if logger.nil?
55
+ @logger = Logger.new(STDOUT).tap do |l|
56
+ l.level = Logger::WARN
57
+ end
58
+ else
59
+ unless LOG_LEVELS.all? { |lev| logger.respond_to?(lev) }
60
+ raise ArgumentError, "You must pass in a logger implementation that responds to the following messages: #{LOG_LEVELS}"
61
+ end
62
+ @logger = logger
63
+ end
64
+
65
+ if @logger.level < Logger::INFO
66
+ msg = <<-MSG
67
+ The Recurly logger should not be initialized
68
+ beyond the level INFO. It is currently configured to emit
69
+ headers and request / response bodies. This has the potential to leak
70
+ PII and other sensitive information and should never be used in production.
71
+ MSG
72
+ log_warn("SECURITY_WARNING", message: msg)
73
+ end
47
74
 
48
75
  # execute block with this client if given
49
76
  yield(self) if block_given?
50
77
  end
51
78
 
52
- def next_page(pager)
53
- req = HTTP::Request.new(:get, pager.next, nil)
54
- faraday_resp = run_request(req, headers)
55
- handle_response! req, faraday_resp
56
- end
57
-
58
79
  protected
59
80
 
81
+ # Used by the operations.rb file to interpolate paths
82
+ attr_reader :site_id
83
+
60
84
  def pager(path, **options)
61
- path = scope_by_site(path, **options)
62
85
  Pager.new(
63
86
  client: self,
64
87
  path: path,
@@ -66,70 +89,158 @@ module Recurly
66
89
  )
67
90
  end
68
91
 
92
+ def head(path, **options)
93
+ request = Net::HTTP::Head.new build_url(path, options)
94
+ set_headers(request, options[:headers])
95
+ http_response = run_request(request, options)
96
+ handle_response! request, http_response
97
+ end
98
+
69
99
  def get(path, **options)
70
- path = scope_by_site(path, **options)
71
- request = HTTP::Request.new(:get, path, nil)
72
- faraday_resp = run_request(request, headers)
73
- handle_response! request, faraday_resp
74
- rescue Faraday::ClientError => ex
75
- raise_network_error!(ex)
100
+ request = Net::HTTP::Get.new build_url(path, options)
101
+ set_headers(request, options[:headers])
102
+ http_response = run_request(request, options)
103
+ handle_response! request, http_response
76
104
  end
77
105
 
78
106
  def post(path, request_data, request_class, **options)
79
107
  request_class.new(request_data).validate!
80
- path = scope_by_site(path, **options)
81
- request = HTTP::Request.new(:post, path, JSON.dump(request_data))
82
- faraday_resp = run_request(request, headers)
83
- handle_response! request, faraday_resp
84
- rescue Faraday::ClientError => ex
85
- raise_network_error!(ex)
108
+ request = Net::HTTP::Post.new build_url(path, options)
109
+ request.set_content_type(JSON_CONTENT_TYPE)
110
+ set_headers(request, options[:headers])
111
+ request.body = JSON.dump(request_data)
112
+ http_response = run_request(request, options)
113
+ handle_response! request, http_response
86
114
  end
87
115
 
88
116
  def put(path, request_data = nil, request_class = nil, **options)
89
- path = scope_by_site(path, **options)
90
- request = HTTP::Request.new(:put, path)
117
+ request = Net::HTTP::Put.new build_url(path, options)
118
+ request.set_content_type(JSON_CONTENT_TYPE)
119
+ set_headers(request, options[:headers])
91
120
  if request_data
92
121
  request_class.new(request_data).validate!
93
- logger.info("PUT BODY #{JSON.dump(request_data)}")
94
- request.body = JSON.dump(request_data)
122
+ json_body = JSON.dump(request_data)
123
+ request.body = json_body
95
124
  end
96
- faraday_resp = run_request(request, headers)
97
- handle_response! request, faraday_resp
98
- rescue Faraday::ClientError => ex
99
- raise_network_error!(ex)
125
+ http_response = run_request(request, options)
126
+ handle_response! request, http_response
100
127
  end
101
128
 
102
129
  def delete(path, **options)
103
- path = scope_by_site(path, **options)
104
- request = HTTP::Request.new(:delete, path, nil)
105
- faraday_resp = run_request(request, headers)
106
- handle_response! request, faraday_resp
107
- rescue Faraday::ClientError => ex
108
- raise_network_error!(ex)
130
+ request = Net::HTTP::Delete.new build_url(path, options)
131
+ set_headers(request, options[:headers])
132
+ http_response = run_request(request, options)
133
+ handle_response! request, http_response
109
134
  end
110
135
 
111
- protected
136
+ private
112
137
 
113
- # Used by the operations.rb file to interpolate paths
114
- attr_reader :site_id
138
+ @connection_pool = Recurly::ConnectionPool.new
115
139
 
116
- private
140
+ class << self
141
+ # @return [Recurly::ConnectionPool]
142
+ attr_accessor :connection_pool
143
+ end
144
+
145
+ def run_request(request, options = {})
146
+ self.class.connection_pool.with_connection do |http|
147
+ set_http_options(http, options)
148
+
149
+ retries = 0
150
+
151
+ begin
152
+ http.start unless http.started?
153
+ log_attrs = {
154
+ method: request.method,
155
+ path: request.path,
156
+ }
157
+ if @logger.level < Logger::INFO
158
+ log_attrs[:request_body] = request.body
159
+ # No need to log the authorization header
160
+ headers = request.to_hash.reject { |k, _| k&.downcase == "authorization" }
161
+ log_attrs[:request_headers] = headers
162
+ end
163
+
164
+ log_info("Request", **log_attrs)
165
+ start = Time.now
166
+ response = http.request(request)
167
+ elapsed = Time.now - start
168
+
169
+ # GETs are safe to retry after a server error, requests with an Idempotency-Key will return the prior response
170
+ if response.kind_of?(Net::HTTPServerError) && request.is_a?(Net::HTTP::Get)
171
+ retries += 1
172
+ log_info("Retrying", retries: retries, **log_attrs)
173
+ start = Time.now
174
+ response = http.request(request) if retries < MAX_RETRIES
175
+ elapsed = Time.now - start
176
+ end
177
+
178
+ if @logger.level < Logger::INFO
179
+ log_attrs[:response_body] = response.body
180
+ log_attrs[:response_headers] = response.to_hash
181
+ end
182
+ log_info("Response", time_ms: (elapsed * 1_000).floor, status: response.code, **log_attrs)
183
+
184
+ response
185
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ECONNABORTED,
186
+ Errno::EPIPE, Errno::ETIMEDOUT, Net::OpenTimeout, EOFError, SocketError => ex
187
+ retries += 1
188
+ if retries < MAX_RETRIES
189
+ retry
190
+ end
191
+
192
+ if ex.kind_of?(Net::OpenTimeout) || ex.kind_of?(Errno::ETIMEDOUT)
193
+ raise Recurly::Errors::TimeoutError, "Request timed out"
194
+ end
195
+
196
+ raise Recurly::Errors::ConnectionFailedError, "Failed to connect to Recurly: #{ex.message}"
197
+ rescue Net::ReadTimeout, Timeout::Error
198
+ raise Recurly::Errors::TimeoutError, "Request timed out"
199
+ rescue OpenSSL::SSL::SSLError => ex
200
+ raise Recurly::Errors::SSLError, ex.message
201
+ rescue StandardError => ex
202
+ raise Recurly::Errors::NetworkError, ex.message
203
+ end
204
+ end
205
+ end
117
206
 
118
- # @return [Logger]
119
- attr_reader :logger
207
+ def set_headers(request, additional_headers = {})
208
+ # TODO this is undocumented until we finalize it
209
+ additional_headers.each { |header, v| request[header] = v } if additional_headers
210
+
211
+ request["Accept"] = "application/vnd.recurly.#{api_version}".chomp # got this method from operations.rb
212
+ request["Authorization"] = "Basic #{Base64.encode64(@api_key)}".chomp
213
+ request["User-Agent"] = "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}"
214
+
215
+ unless request.is_a?(Net::HTTP::Get) || request.is_a?(Net::HTTP::Head)
216
+ request["Idempotency-Key"] ||= generate_idempotency_key
217
+ end
218
+ end
120
219
 
121
- def run_request(request, headers)
122
- read_headers @conn.run_request(request.method, request.path, request.body, headers)
220
+ # from https://github.com/rails/rails/blob/6-0-stable/activesupport/lib/active_support/core_ext/securerandom.rb
221
+ def generate_idempotency_key(n = 16)
222
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
223
+ idx = byte % 64
224
+ idx = SecureRandom.random_number(36) if idx >= 36
225
+ BASE36_ALPHABET[idx]
226
+ end.join
123
227
  end
124
228
 
125
- def handle_response!(request, faraday_resp)
126
- response = HTTP::Response.new(faraday_resp, request)
127
- raise_api_error!(response) unless (200...300).include?(response.status)
229
+ def set_http_options(http, options)
230
+ http.open_timeout = options[:open_timeout] || 20
231
+ http.read_timeout = options[:read_timeout] || 60
232
+ end
233
+
234
+ def handle_response!(request, http_response)
235
+ response = HTTP::Response.new(http_response, request)
236
+ raise_api_error!(http_response, response) unless http_response.kind_of?(Net::HTTPSuccess)
128
237
  resource = if response.body
129
- if BINARY_TYPES.include?(response.content_type)
238
+ if http_response.content_type&.include?(JSON_CONTENT_TYPE)
239
+ JSONParser.parse(self, response.body)
240
+ elsif BINARY_TYPES.include?(http_response.content_type)
130
241
  FileParser.parse(response.body)
131
242
  else
132
- JSONParser.parse(self, response.body)
243
+ raise Recurly::Errors::InvalidResponseError, "Unexpected content type: #{http_response.content_type}"
133
244
  end
134
245
  else
135
246
  Resources::Empty.new
@@ -139,46 +250,34 @@ module Recurly
139
250
  resource
140
251
  end
141
252
 
142
- def raise_network_error!(ex)
143
- error_class = case ex
144
- when Faraday::TimeoutError
145
- Errors::TimeoutError
146
- when Faraday::ConnectionFailed
147
- Errors::ConnectionFailedError
148
- when Faraday::SSLError
149
- Errors::SSLError
150
- else
151
- Errors::NetworkError
152
- end
253
+ def raise_api_error!(http_response, response)
254
+ if response.content_type.include?(JSON_CONTENT_TYPE)
255
+ error = JSONParser.parse(self, response.body)
256
+ error_class = Errors::APIError.error_class(error.type)
257
+ raise error_class.new(response, error)
258
+ end
153
259
 
154
- raise error_class, ex.message
155
- end
260
+ error_class = Errors::APIError.from_response(http_response)
156
261
 
157
- def raise_api_error!(response)
158
- error = JSONParser.parse(self, response.body)
159
- error_class = Errors::APIError.error_class(error.type)
160
- raise error_class.new(response, error)
262
+ if error_class <= Recurly::Errors::APIError
263
+ error = Recurly::Resources::Error.new(message: "#{http_response.code}: #{http_response.message}")
264
+ raise error_class.new(response, error)
265
+ else
266
+ raise error_class, "#{http_response.code}: #{http_response.message}"
267
+ end
161
268
  end
162
269
 
163
270
  def read_headers(response)
164
271
  if !@_ignore_deprecation_warning && response.headers["Recurly-Deprecated"]&.upcase == "TRUE"
165
- puts "[recurly-client-ruby] WARNING: Your current API version \"#{api_version}\" is deprecated and will be sunset on #{response.headers["Recurly-Sunset-Date"]}"
272
+ log_warn("DEPRECTATION WARNING", message: "Your current API version \"#{api_version}\" is deprecated and will be sunset on #{response.headers["Recurly-Sunset-Date"]}")
166
273
  end
167
274
  response
168
275
  end
169
276
 
170
- def headers
171
- {
172
- "Accept" => "application/vnd.recurly.#{api_version}", # got this method from operations.rb
173
- "Content-Type" => "application/json",
174
- "User-Agent" => "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}",
175
- }.merge(@extra_headers)
176
- end
177
-
178
- def interpolate_path(path, **options)
277
+ def validate_path_parameters!(**options)
278
+ # Check to see that we are passing the correct data types
279
+ # This prevents a confusing error if the user passes in a non-primitive by mistake
179
280
  options.each do |k, v|
180
- # Check to see that we are passing the correct data types
181
- # This prevents a confusing error if the user passes in a non-primitive by mistake
182
281
  unless [String, Symbol, Integer, Float].include?(v.class)
183
282
  message = "We cannot build the url with the given argument #{k}=#{v.inspect}."
184
283
  if k =~ /_id$/
@@ -186,6 +285,17 @@ module Recurly
186
285
  end
187
286
  raise ArgumentError, message
188
287
  end
288
+ end
289
+ # Check to make sure that parameters are not empty string values
290
+ empty_strings = options.select { |_, v| v.is_a?(String) && v.strip.empty? }
291
+ if empty_strings.any?
292
+ raise ArgumentError, "#{empty_strings.keys.join(", ")} cannot be an empty string"
293
+ end
294
+ end
295
+
296
+ def interpolate_path(path, **options)
297
+ validate_path_parameters!(options)
298
+ options.each do |k, v|
189
299
  # We need to encode the values for the url
190
300
  options[k] = ERB::Util.url_encode(v.to_s)
191
301
  end
@@ -201,42 +311,38 @@ module Recurly
201
311
  end
202
312
  end
203
313
 
204
- def scope_by_site(path, **options)
205
- if site = site_id || options[:site_id]
206
- "/sites/#{site}#{path}"
314
+ def set_api_key(api_key)
315
+ @api_key = api_key
316
+ end
317
+
318
+ def build_url(path, options)
319
+ path = scope_by_site(path, options)
320
+ query_params = options.reject { |k, _| REQUEST_OPTIONS.include?(k.to_sym) }
321
+ if query_params.any?
322
+ "#{path}?#{URI.encode_www_form(query_params)}"
207
323
  else
208
324
  path
209
325
  end
210
326
  end
211
327
 
212
- def set_faraday_connection(api_key)
213
- options = {
214
- url: BASE_URL,
215
- request: { timeout: 60, open_timeout: 50 },
216
- ssl: { verify: true },
217
- }
218
- # Let's not use the bundled cert in production yet
219
- # but we will use these certs for any other staging or dev environment
220
- unless BASE_URL.end_with?(".recurly.com")
221
- options[:ssl][:ca_file] = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt")
328
+ def scope_by_site(path, **options)
329
+ if site = site_id || options[:site_id]
330
+ # Ensure that we are only including the site_id once because the Pager operations
331
+ # will use the cursor returned from the API which may already have these components
332
+ path.start_with?("/sites/#{site}") ? path : "/sites/#{site}#{path}"
333
+ else
334
+ path
222
335
  end
336
+ end
223
337
 
224
- @conn = Faraday.new(options) do |faraday|
225
- if [Logger::DEBUG, Logger::INFO].include?(@log_level)
226
- faraday.response :logger
338
+ # Define a private `log_<level>` method for each log level
339
+ LOG_LEVELS.each do |level|
340
+ define_method "log_#{level}" do |tag, **attrs|
341
+ @logger.send(level, "Recurly") do
342
+ msg = attrs.each_pair.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
343
+ "[#{tag}] #{msg}"
227
344
  end
228
- faraday.basic_auth(api_key, "")
229
- configure_net_adapter(faraday)
230
345
  end
231
346
  end
232
-
233
- def set_options(options)
234
- @log_level = options[:log_level] || Logger::WARN
235
- @logger = Logger.new(STDOUT)
236
- @logger.level = @log_level
237
-
238
- # TODO this is undocumented until we finalize it
239
- @extra_headers = options[:headers] || {}
240
- end
241
347
  end
242
348
  end
@@ -30,6 +30,7 @@ module Recurly
30
30
  # order. In descending order updated records will move behind the cursor and could
31
31
  # prevent some records from being returned.
32
32
  #
33
+ # @param state [String] Filter by state.
33
34
  # @return [Pager<Resources::Site>] A list of sites.
34
35
  # @example
35
36
  # sites = @client.list_sites(limit: 200)
@@ -48,6 +49,16 @@ module Recurly
48
49
  #
49
50
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
50
51
  # @return [Resources::Site] A site.
52
+ # @example
53
+ # begin
54
+ # site = @client.get_site(site_id: site_id)
55
+ # puts "Got Site #{site}"
56
+ # rescue Recurly::Errors::NotFoundError
57
+ # # If the resource was not found, you may want to alert the user or
58
+ # # just return nil
59
+ # puts "Resource Not Found"
60
+ # end
61
+ #
51
62
  def get_site(site_id:)
52
63
  path = interpolate_path("/sites/{site_id}", site_id: site_id)
53
64
  get(path)
@@ -251,6 +262,28 @@ module Recurly
251
262
  # @param body [Requests::AccountAcquisitionUpdatable] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::AccountAcquisitionUpdatable}
252
263
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
253
264
  # @return [Resources::AccountAcquisition] An account's updated acquisition data.
265
+ # @example
266
+ # begin
267
+ # acquisition_update = {
268
+ # campaign: "podcast-marketing",
269
+ # channel: "social_media",
270
+ # subchannel: "twitter",
271
+ # cost: {
272
+ # currency: "USD",
273
+ # amount: 0.50
274
+ # }
275
+ # }
276
+ # acquisition = @client.update_account_acquisition(
277
+ # account_id: account_id,
278
+ # body: acquisition_update
279
+ # )
280
+ # puts "Updated AccountAcqusition #{acquisition}"
281
+ # rescue Recurly::Errors::ValidationError => e
282
+ # # If the request was invalid, you may want to tell your user
283
+ # # why. You can find the invalid params and reasons in e.recurly_error.params
284
+ # puts "ValidationError: #{e.recurly_error.params}"
285
+ # end
286
+ #
254
287
  def update_account_acquisition(account_id:, body:, **options)
255
288
  path = interpolate_path("/accounts/{account_id}/acquisition", account_id: account_id)
256
289
  put(path, body, Requests::AccountAcquisitionUpdatable, **options)
@@ -845,6 +878,26 @@ module Recurly
845
878
  # @param body [Requests::ShippingAddressCreate] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::ShippingAddressCreate}
846
879
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
847
880
  # @return [Resources::ShippingAddress] Returns the new shipping address.
881
+ # @example
882
+ # begin
883
+ # shipping_address_create = {
884
+ # nickname: 'Work',
885
+ # street1: '900 Camp St',
886
+ # city: 'New Orleans',
887
+ # region: 'LA',
888
+ # country: 'US',
889
+ # postal_code: '70115',
890
+ # first_name: 'Joanna',
891
+ # last_name: 'Du Monde'
892
+ # }
893
+ # shipping_address = @client.create_shipping_address(account_id: account_id, body: shipping_address_create)
894
+ # puts "Created Shipping Address #{shipping_address}"
895
+ # rescue Recurly::Errors::NotFoundError
896
+ # # If the resource was not found, you may want to alert the user or
897
+ # # just return nil
898
+ # puts "Resource Not Found"
899
+ # end
900
+ #
848
901
  def create_shipping_address(account_id:, body:, **options)
849
902
  path = interpolate_path("/accounts/{account_id}/shipping_addresses", account_id: account_id)
850
903
  post(path, body, Requests::ShippingAddressCreate, **options)
@@ -1230,11 +1283,46 @@ module Recurly
1230
1283
  # @param body [Requests::CouponUpdate] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::CouponUpdate}
1231
1284
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1232
1285
  # @return [Resources::Coupon] The updated coupon.
1286
+ # @example
1287
+ # begin
1288
+ # coupon_update = {
1289
+ # name: "New Coupon Name"
1290
+ # }
1291
+ # coupon = @client.update_coupon(coupon_id: coupon_id, body: coupon_update)
1292
+ # puts "Updated Coupon #{coupon}"
1293
+ # rescue Recurly::Errors::NotFoundError
1294
+ # # If the resource was not found, you may want to alert the user or
1295
+ # # just return nil
1296
+ # puts "Resource Not Found"
1297
+ # end
1298
+ #
1233
1299
  def update_coupon(coupon_id:, body:, **options)
1234
1300
  path = interpolate_path("/coupons/{coupon_id}", coupon_id: coupon_id)
1235
1301
  put(path, body, Requests::CouponUpdate, **options)
1236
1302
  end
1237
1303
 
1304
+ # Expire a coupon
1305
+ #
1306
+ # {https://developers.recurly.com/api/v2019-10-10#operation/deactivate_coupon deactivate_coupon api documenation}
1307
+ #
1308
+ # @param coupon_id [String] Coupon ID or code. For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-10off+.
1309
+ # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1310
+ # @return [Resources::Coupon] The expired Coupon
1311
+ # @example
1312
+ # begin
1313
+ # coupon = @client.deactivate_coupon(coupon_id: coupon_id)
1314
+ # puts "Deactivated Coupon #{coupon}"
1315
+ # rescue Recurly::Errors::NotFoundError
1316
+ # # If the resource was not found, you may want to alert the user or
1317
+ # # just return nil
1318
+ # puts "Resource Not Found"
1319
+ # end
1320
+ #
1321
+ def deactivate_coupon(coupon_id:, **options)
1322
+ path = interpolate_path("/coupons/{coupon_id}", coupon_id: coupon_id)
1323
+ delete(path, **options)
1324
+ end
1325
+
1238
1326
  # List unique coupon codes associated with a bulk coupon
1239
1327
  #
1240
1328
  # {https://developers.recurly.com/api/v2019-10-10#operation/list_unique_coupon_codes list_unique_coupon_codes api documenation}
@@ -1361,6 +1449,18 @@ module Recurly
1361
1449
  # @param custom_field_definition_id [String] Custom Field Definition ID
1362
1450
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1363
1451
  # @return [Resources::CustomFieldDefinition] An custom field definition.
1452
+ # @example
1453
+ # begin
1454
+ # custom_field_definition = @client.get_custom_field_definition(
1455
+ # custom_field_definition_id: custom_field_definition_id
1456
+ # )
1457
+ # puts "Got Custom Field Definition #{custom_field_definition}"
1458
+ # rescue Recurly::Errors::NotFoundError
1459
+ # # If the resource was not found, you may want to alert the user or
1460
+ # # just return nil
1461
+ # puts "Resource Not Found"
1462
+ # end
1463
+ #
1364
1464
  def get_custom_field_definition(custom_field_definition_id:, **options)
1365
1465
  path = interpolate_path("/custom_field_definitions/{custom_field_definition_id}", custom_field_definition_id: custom_field_definition_id)
1366
1466
  get(path, **options)
@@ -1615,6 +1715,20 @@ module Recurly
1615
1715
  # @param body [Requests::InvoiceUpdatable] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::InvoiceUpdatable}
1616
1716
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1617
1717
  # @return [Resources::Invoice] An invoice.
1718
+ # @example
1719
+ # begin
1720
+ # invoice_update = {
1721
+ # customer_notes: "New Notes",
1722
+ # terms_and_conditions: "New Terms and Conditions"
1723
+ # }
1724
+ # invoice = @client.put_invoice(invoice_id: invoice_id, body: invoice_update)
1725
+ # puts "Updated invoice #{invoice}"
1726
+ # rescue Recurly::Errors::NotFoundError
1727
+ # # If the resource was not found, you may want to alert the user or
1728
+ # # just return nil
1729
+ # puts "Resource Not Found"
1730
+ # end
1731
+ #
1618
1732
  def put_invoice(invoice_id:, body:, **options)
1619
1733
  path = interpolate_path("/invoices/{invoice_id}", invoice_id: invoice_id)
1620
1734
  put(path, body, Requests::InvoiceUpdatable, **options)
@@ -1741,11 +1855,34 @@ module Recurly
1741
1855
  # @param invoice_id [String] Invoice ID or number. For ID no prefix is used e.g. +e28zov4fw0v2+. For number use prefix +number-+, e.g. +number-1000+.
1742
1856
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1743
1857
  # @return [Resources::Invoice] The updated invoice.
1858
+ # @example
1859
+ # begin
1860
+ # invoice = @client.void_invoice(invoice_id: invoice_id)
1861
+ # puts "Voided invoice #{invoice}"
1862
+ # rescue Recurly::Errors::NotFoundError
1863
+ # # If the resource was not found, you may want to alert the user or
1864
+ # # just return nil
1865
+ # puts "Resource Not Found"
1866
+ # end
1867
+ #
1744
1868
  def void_invoice(invoice_id:, **options)
1745
1869
  path = interpolate_path("/invoices/{invoice_id}/void", invoice_id: invoice_id)
1746
1870
  put(path, **options)
1747
1871
  end
1748
1872
 
1873
+ # Record an external payment for a manual invoices.
1874
+ #
1875
+ # {https://developers.recurly.com/api/v2019-10-10#operation/record_external_transaction record_external_transaction api documenation}
1876
+ #
1877
+ # @param invoice_id [String] Invoice ID or number. For ID no prefix is used e.g. +e28zov4fw0v2+. For number use prefix +number-+, e.g. +number-1000+.
1878
+ # @param body [Requests::ExternalTransaction] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::ExternalTransaction}
1879
+ # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1880
+ # @return [Resources::Transaction] The recorded transaction.
1881
+ def record_external_transaction(invoice_id:, body:, **options)
1882
+ path = interpolate_path("/invoices/{invoice_id}/transactions", invoice_id: invoice_id)
1883
+ post(path, body, Requests::ExternalTransaction, **options)
1884
+ end
1885
+
1749
1886
  # List an invoice's line items
1750
1887
  #
1751
1888
  # {https://developers.recurly.com/api/v2019-10-10#operation/list_invoice_line_items list_invoice_line_items api documenation}
@@ -1780,6 +1917,15 @@ module Recurly
1780
1917
  # @param type [String] Filter by type field.
1781
1918
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1782
1919
  # @return [Pager<Resources::LineItem>] A list of the invoice's line items.
1920
+ # @example
1921
+ # line_items = @client.list_invoice_line_items(
1922
+ # invoice_id: invoice_id,
1923
+ # limit: 200
1924
+ # )
1925
+ # line_items.each do |line_item|
1926
+ # puts "Line Item: #{line_item.id}"
1927
+ # end
1928
+ #
1783
1929
  def list_invoice_line_items(invoice_id:, **options)
1784
1930
  path = interpolate_path("/invoices/{invoice_id}/line_items", invoice_id: invoice_id)
1785
1931
  pager(path, **options)
@@ -1835,6 +1981,15 @@ module Recurly
1835
1981
  # @param invoice_id [String] Invoice ID or number. For ID no prefix is used e.g. +e28zov4fw0v2+. For number use prefix +number-+, e.g. +number-1000+.
1836
1982
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1837
1983
  # @return [Pager<Resources::Invoice>] A list of the credit or charge invoices associated with the invoice.
1984
+ # @example
1985
+ # invoices = @client.list_related_invoices(
1986
+ # invoice_id: invoice_id,
1987
+ # limit: 200
1988
+ # )
1989
+ # invoices.each do |invoice|
1990
+ # puts "Invoice: #{invoice.number}"
1991
+ # end
1992
+ #
1838
1993
  def list_related_invoices(invoice_id:, **options)
1839
1994
  path = interpolate_path("/invoices/{invoice_id}/related_invoices", invoice_id: invoice_id)
1840
1995
  pager(path, **options)
@@ -1903,6 +2058,14 @@ module Recurly
1903
2058
  # @param type [String] Filter by type field.
1904
2059
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
1905
2060
  # @return [Pager<Resources::LineItem>] A list of the site's line items.
2061
+ # @example
2062
+ # line_items = @client.list_line_items(
2063
+ # limit: 200
2064
+ # )
2065
+ # line_items.each do |line_item|
2066
+ # puts "LineItem: #{line_item.id}"
2067
+ # end
2068
+ #
1906
2069
  def list_line_items(**options)
1907
2070
  path = interpolate_path("/line_items")
1908
2071
  pager(path, **options)
@@ -2065,6 +2228,19 @@ module Recurly
2065
2228
  # @param body [Requests::PlanUpdate] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::PlanUpdate}
2066
2229
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2067
2230
  # @return [Resources::Plan] A plan.
2231
+ # @example
2232
+ # begin
2233
+ # plan_update = {
2234
+ # name: "Monthly Kombucha Subscription"
2235
+ # }
2236
+ # plan = @client.update_plan(plan_id: plan_id, body: plan_update)
2237
+ # puts "Updated plan #{plan}"
2238
+ # rescue Recurly::Errors::NotFoundError
2239
+ # # If the resource was not found, you may want to alert the user or
2240
+ # # just return nil
2241
+ # puts "Resource Not Found"
2242
+ # end
2243
+ #
2068
2244
  def update_plan(plan_id:, body:, **options)
2069
2245
  path = interpolate_path("/plans/{plan_id}", plan_id: plan_id)
2070
2246
  put(path, body, Requests::PlanUpdate, **options)
@@ -2077,6 +2253,16 @@ module Recurly
2077
2253
  # @param plan_id [String] Plan ID or code. For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-gold+.
2078
2254
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2079
2255
  # @return [Resources::Plan] Plan deleted
2256
+ # @example
2257
+ # begin
2258
+ # plan = @client.remove_plan(plan_id: plan_id)
2259
+ # puts "Removed plan #{plan}"
2260
+ # rescue Recurly::Errors::NotFoundError
2261
+ # # If the resource was not found, you may want to alert the user or
2262
+ # # just return nil
2263
+ # puts "Resource Not Found"
2264
+ # end
2265
+ #
2080
2266
  def remove_plan(plan_id:, **options)
2081
2267
  path = interpolate_path("/plans/{plan_id}", plan_id: plan_id)
2082
2268
  delete(path, **options)
@@ -2136,6 +2322,27 @@ module Recurly
2136
2322
  # @param body [Requests::AddOnCreate] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::AddOnCreate}
2137
2323
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2138
2324
  # @return [Resources::AddOn] An add-on.
2325
+ # @example
2326
+ # begin
2327
+ # new_add_on = {
2328
+ # code: 'coffee_grinder',
2329
+ # name: 'A quality grinder for your beans',
2330
+ # default_quantity: 1,
2331
+ # currencies: [
2332
+ # {
2333
+ # currency: 'USD',
2334
+ # unit_amount: 10_000
2335
+ # }
2336
+ # ]
2337
+ # }
2338
+ # add_on = @client.create_plan_add_on(plan_id: plan_id, body: new_add_on)
2339
+ # puts "Created plan add-on #{add_on}"
2340
+ # rescue Recurly::Errors::NotFoundError
2341
+ # # If the resource was not found, you may want to alert the user or
2342
+ # # just return nil
2343
+ # puts "Resource Not Found"
2344
+ # end
2345
+ #
2139
2346
  def create_plan_add_on(plan_id:, body:, **options)
2140
2347
  path = interpolate_path("/plans/{plan_id}/add_ons", plan_id: plan_id)
2141
2348
  post(path, body, Requests::AddOnCreate, **options)
@@ -2175,6 +2382,23 @@ module Recurly
2175
2382
  # @param body [Requests::AddOnUpdate] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::AddOnUpdate}
2176
2383
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2177
2384
  # @return [Resources::AddOn] An add-on.
2385
+ # @example
2386
+ # begin
2387
+ # add_on_update = {
2388
+ # name: "A quality grinder for your finest beans"
2389
+ # }
2390
+ # add_on = @client.update_plan_add_on(
2391
+ # plan_id: plan_id,
2392
+ # add_on_id: add_on_id,
2393
+ # body: add_on_update
2394
+ # )
2395
+ # puts "Updated add-on #{add_on}"
2396
+ # rescue Recurly::Errors::NotFoundError
2397
+ # # If the resource was not found, you may want to alert the user or
2398
+ # # just return nil
2399
+ # puts "Resource Not Found"
2400
+ # end
2401
+ #
2178
2402
  def update_plan_add_on(plan_id:, add_on_id:, body:, **options)
2179
2403
  path = interpolate_path("/plans/{plan_id}/add_ons/{add_on_id}", plan_id: plan_id, add_on_id: add_on_id)
2180
2404
  put(path, body, Requests::AddOnUpdate, **options)
@@ -2188,6 +2412,19 @@ module Recurly
2188
2412
  # @param add_on_id [String] Add-on ID or code. For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-gold+.
2189
2413
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2190
2414
  # @return [Resources::AddOn] Add-on deleted
2415
+ # @example
2416
+ # begin
2417
+ # add_on = @client.remove_plan_add_on(
2418
+ # plan_id: plan_id,
2419
+ # add_on_id: add_on_id
2420
+ # )
2421
+ # puts "Removed add-on #{add_on}"
2422
+ # rescue Recurly::Errors::NotFoundError
2423
+ # # If the resource was not found, you may want to alert the user or
2424
+ # # just return nil
2425
+ # puts "Resource Not Found"
2426
+ # end
2427
+ #
2191
2428
  def remove_plan_add_on(plan_id:, add_on_id:, **options)
2192
2429
  path = interpolate_path("/plans/{plan_id}/add_ons/{add_on_id}", plan_id: plan_id, add_on_id: add_on_id)
2193
2430
  delete(path, **options)
@@ -2224,6 +2461,14 @@ module Recurly
2224
2461
  # @param state [String] Filter by state.
2225
2462
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2226
2463
  # @return [Pager<Resources::AddOn>] A list of add-ons.
2464
+ # @example
2465
+ # add_ons = @client.list_add_ons(
2466
+ # limit: 200
2467
+ # )
2468
+ # add_ons.each do |add_on|
2469
+ # puts "AddOn: #{add_on.code}"
2470
+ # end
2471
+ #
2227
2472
  def list_add_ons(**options)
2228
2473
  path = interpolate_path("/add_ons")
2229
2474
  pager(path, **options)
@@ -2236,6 +2481,16 @@ module Recurly
2236
2481
  # @param add_on_id [String] Add-on ID or code. For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-gold+.
2237
2482
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2238
2483
  # @return [Resources::AddOn] An add-on.
2484
+ # @example
2485
+ # begin
2486
+ # add_on = @client.get_add_on(add_on_id: add_on_id)
2487
+ # puts "Got add-on #{add_on}"
2488
+ # rescue Recurly::Errors::NotFoundError
2489
+ # # If the resource was not found, you may want to alert the user or
2490
+ # # just return nil
2491
+ # puts "Resource Not Found"
2492
+ # end
2493
+ #
2239
2494
  def get_add_on(add_on_id:, **options)
2240
2495
  path = interpolate_path("/add_ons/{add_on_id}", add_on_id: add_on_id)
2241
2496
  get(path, **options)
@@ -2271,23 +2526,68 @@ module Recurly
2271
2526
  #
2272
2527
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2273
2528
  # @return [Pager<Resources::ShippingMethod>] A list of the site's shipping methods.
2529
+ # @example
2530
+ # shipping_methods = @client.list_shipping_methods(
2531
+ # limit: 200
2532
+ # )
2533
+ # shipping_methods.each do |shipping_method|
2534
+ # puts "Shipping Method: #{shipping_method.code}"
2535
+ # end
2536
+ #
2274
2537
  def list_shipping_methods(**options)
2275
2538
  path = interpolate_path("/shipping_methods")
2276
2539
  pager(path, **options)
2277
2540
  end
2278
2541
 
2542
+ # Create a new shipping method
2543
+ #
2544
+ # {https://developers.recurly.com/api/v2019-10-10#operation/create_shipping_method create_shipping_method api documenation}
2545
+ #
2546
+ # @param body [Requests::ShippingMethodCreate] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::ShippingMethodCreate}
2547
+ # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2548
+ # @return [Resources::ShippingMethod] A new shipping method.
2549
+ def create_shipping_method(body:, **options)
2550
+ path = interpolate_path("/shipping_methods")
2551
+ post(path, body, Requests::ShippingMethodCreate, **options)
2552
+ end
2553
+
2279
2554
  # Fetch a shipping method
2280
2555
  #
2281
2556
  # {https://developers.recurly.com/api/v2019-10-10#operation/get_shipping_method get_shipping_method api documenation}
2282
2557
  #
2283
2558
  # @param id [String] Shipping Method ID or code. For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-usps_2-day+.
2284
2559
  # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2285
- # @return [Resources::ShippingMethod] A shipping_method.
2560
+ # @return [Resources::ShippingMethod] A shipping method.
2286
2561
  def get_shipping_method(id:, **options)
2287
2562
  path = interpolate_path("/shipping_methods/{id}", id: id)
2288
2563
  get(path, **options)
2289
2564
  end
2290
2565
 
2566
+ # Update an active Shipping Method
2567
+ #
2568
+ # {https://developers.recurly.com/api/v2019-10-10#operation/update_shipping_method update_shipping_method api documenation}
2569
+ #
2570
+ # @param shipping_method_id [String] Shipping Method ID or code. For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-usps_2-day+.
2571
+ # @param body [Requests::ShippingMethodUpdate] The Hash representing the JSON request to send to the server. It should conform to the schema of {Requests::ShippingMethodUpdate}
2572
+ # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2573
+ # @return [Resources::ShippingMethod] The updated shipping method.
2574
+ def update_shipping_method(shipping_method_id:, body:, **options)
2575
+ path = interpolate_path("/shipping_methods/{shipping_method_id}", shipping_method_id: shipping_method_id)
2576
+ put(path, body, Requests::ShippingMethodUpdate, **options)
2577
+ end
2578
+
2579
+ # Deactivate a shipping method
2580
+ #
2581
+ # {https://developers.recurly.com/api/v2019-10-10#operation/deactivate_shipping_method deactivate_shipping_method api documenation}
2582
+ #
2583
+ # @param shipping_method_id [String] Shipping Method ID or code. For ID no prefix is used e.g. +e28zov4fw0v2+. For code use prefix +code-+, e.g. +code-usps_2-day+.
2584
+ # @param site_id [String] Site ID or subdomain. For ID no prefix is used e.g. +e28zov4fw0v2+. For subdomain use prefix +subdomain-+, e.g. +subdomain-recurly+.
2585
+ # @return [Resources::ShippingMethod] A shipping method.
2586
+ def deactivate_shipping_method(shipping_method_id:, **options)
2587
+ path = interpolate_path("/shipping_methods/{shipping_method_id}", shipping_method_id: shipping_method_id)
2588
+ delete(path, **options)
2589
+ end
2590
+
2291
2591
  # List a site's subscriptions
2292
2592
  #
2293
2593
  # {https://developers.recurly.com/api/v2019-10-10#operation/list_subscriptions list_subscriptions api documenation}