recurly 3.3.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.bumpversion.cfg +5 -1
  3. data/.github/ISSUE_TEMPLATE/bug-report.md +1 -1
  4. data/.github/workflows/docs.yml +28 -0
  5. data/.github_changelog_generator +8 -0
  6. data/.travis.yml +1 -0
  7. data/CHANGELOG.md +209 -0
  8. data/GETTING_STARTED.md +61 -1
  9. data/README.md +1 -1
  10. data/lib/data/ca-certificates.crt +3464 -29
  11. data/lib/recurly.rb +1 -0
  12. data/lib/recurly/client.rb +215 -111
  13. data/lib/recurly/client/operations.rb +263 -0
  14. data/lib/recurly/connection_pool.rb +40 -0
  15. data/lib/recurly/errors.rb +30 -0
  16. data/lib/recurly/errors/network_errors.rb +2 -0
  17. data/lib/recurly/http.rb +13 -8
  18. data/lib/recurly/pager.rb +31 -12
  19. data/lib/recurly/requests/add_on_create.rb +22 -6
  20. data/lib/recurly/requests/add_on_update.rb +13 -1
  21. data/lib/recurly/requests/billing_info_create.rb +21 -1
  22. data/lib/recurly/requests/external_transaction.rb +26 -0
  23. data/lib/recurly/requests/invoice_refund.rb +2 -2
  24. data/lib/recurly/requests/line_item_create.rb +2 -2
  25. data/lib/recurly/requests/plan_create.rb +8 -0
  26. data/lib/recurly/requests/plan_update.rb +8 -0
  27. data/lib/recurly/requests/subscription_add_on_create.rb +9 -1
  28. data/lib/recurly/requests/subscription_add_on_tier.rb +18 -0
  29. data/lib/recurly/requests/subscription_add_on_update.rb +10 -2
  30. data/lib/recurly/requests/subscription_change_create.rb +5 -1
  31. data/lib/recurly/requests/subscription_create.rb +4 -0
  32. data/lib/recurly/requests/subscription_purchase.rb +4 -0
  33. data/lib/recurly/requests/subscription_update.rb +4 -0
  34. data/lib/recurly/requests/tier.rb +18 -0
  35. data/lib/recurly/resources/add_on.rb +12 -0
  36. data/lib/recurly/resources/coupon.rb +4 -0
  37. data/lib/recurly/resources/line_item.rb +4 -4
  38. data/lib/recurly/resources/payment_method.rb +4 -0
  39. data/lib/recurly/resources/plan.rb +8 -0
  40. data/lib/recurly/resources/shipping_method.rb +4 -0
  41. data/lib/recurly/resources/subscription.rb +4 -0
  42. data/lib/recurly/resources/subscription_add_on.rb +12 -0
  43. data/lib/recurly/resources/subscription_add_on_tier.rb +18 -0
  44. data/lib/recurly/resources/subscription_change.rb +8 -0
  45. data/lib/recurly/resources/tier.rb +18 -0
  46. data/lib/recurly/version.rb +1 -1
  47. data/openapi/api.yaml +5973 -3092
  48. data/recurly.gemspec +9 -4
  49. data/scripts/bump +8 -1
  50. data/scripts/changelog +14 -0
  51. data/scripts/format +5 -1
  52. data/scripts/prepare-release +36 -0
  53. data/scripts/release +25 -4
  54. metadata +22 -41
  55. 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,25 @@
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
16
23
 
17
24
  # Initialize a client. It requires an API key.
18
25
  #
@@ -38,27 +45,42 @@ module Recurly
38
45
  # sub = client.get_subscription(subscription_id: 'uuid-abcd7890')
39
46
  #
40
47
  # @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)
48
+ # @param logger [Logger] A logger to use. Defaults to creating a new STDOUT logger with level WARN.
49
+ def initialize(api_key:, site_id: nil, subdomain: nil, logger: nil)
44
50
  set_site_id(site_id, subdomain)
45
- set_options(options)
46
- set_faraday_connection(api_key)
51
+ set_api_key(api_key)
52
+
53
+ if logger.nil?
54
+ @logger = Logger.new(STDOUT).tap do |l|
55
+ l.level = Logger::WARN
56
+ end
57
+ else
58
+ unless LOG_LEVELS.all? { |lev| logger.respond_to?(lev) }
59
+ raise ArgumentError, "You must pass in a logger implementation that responds to the following messages: #{LOG_LEVELS}"
60
+ end
61
+ @logger = logger
62
+ end
63
+
64
+ if @logger.level < Logger::INFO
65
+ msg = <<-MSG
66
+ The Recurly logger should not be initialized
67
+ beyond the level INFO. It is currently configured to emit
68
+ headers and request / response bodies. This has the potential to leak
69
+ PII and other sensitive information and should never be used in production.
70
+ MSG
71
+ log_warn("SECURITY_WARNING", message: msg)
72
+ end
47
73
 
48
74
  # execute block with this client if given
49
75
  yield(self) if block_given?
50
76
  end
51
77
 
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
78
  protected
59
79
 
80
+ # Used by the operations.rb file to interpolate paths
81
+ attr_reader :site_id
82
+
60
83
  def pager(path, **options)
61
- path = scope_by_site(path, **options)
62
84
  Pager.new(
63
85
  client: self,
64
86
  path: path,
@@ -66,70 +88,158 @@ module Recurly
66
88
  )
67
89
  end
68
90
 
91
+ def head(path, **options)
92
+ request = Net::HTTP::Head.new build_url(path, options)
93
+ set_headers(request, options[:headers])
94
+ http_response = run_request(request, options)
95
+ handle_response! request, http_response
96
+ end
97
+
69
98
  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)
99
+ request = Net::HTTP::Get.new build_url(path, options)
100
+ set_headers(request, options[:headers])
101
+ http_response = run_request(request, options)
102
+ handle_response! request, http_response
76
103
  end
77
104
 
78
105
  def post(path, request_data, request_class, **options)
79
106
  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)
107
+ request = Net::HTTP::Post.new build_url(path, options)
108
+ request.set_content_type(JSON_CONTENT_TYPE)
109
+ set_headers(request, options[:headers])
110
+ request.body = JSON.dump(request_data)
111
+ http_response = run_request(request, options)
112
+ handle_response! request, http_response
86
113
  end
87
114
 
88
115
  def put(path, request_data = nil, request_class = nil, **options)
89
- path = scope_by_site(path, **options)
90
- request = HTTP::Request.new(:put, path)
116
+ request = Net::HTTP::Put.new build_url(path, options)
117
+ request.set_content_type(JSON_CONTENT_TYPE)
118
+ set_headers(request, options[:headers])
91
119
  if request_data
92
120
  request_class.new(request_data).validate!
93
- logger.info("PUT BODY #{JSON.dump(request_data)}")
94
- request.body = JSON.dump(request_data)
121
+ json_body = JSON.dump(request_data)
122
+ request.body = json_body
95
123
  end
96
- faraday_resp = run_request(request, headers)
97
- handle_response! request, faraday_resp
98
- rescue Faraday::ClientError => ex
99
- raise_network_error!(ex)
124
+ http_response = run_request(request, options)
125
+ handle_response! request, http_response
100
126
  end
101
127
 
102
128
  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)
129
+ request = Net::HTTP::Delete.new build_url(path, options)
130
+ set_headers(request, options[:headers])
131
+ http_response = run_request(request, options)
132
+ handle_response! request, http_response
109
133
  end
110
134
 
111
- protected
135
+ private
112
136
 
113
- # Used by the operations.rb file to interpolate paths
114
- attr_reader :site_id
137
+ @connection_pool = Recurly::ConnectionPool.new
115
138
 
116
- private
139
+ class << self
140
+ # @return [Recurly::ConnectionPool]
141
+ attr_accessor :connection_pool
142
+ end
143
+
144
+ def run_request(request, options = {})
145
+ self.class.connection_pool.with_connection do |http|
146
+ set_http_options(http, options)
147
+
148
+ retries = 0
149
+
150
+ begin
151
+ http.start unless http.started?
152
+ log_attrs = {
153
+ method: request.method,
154
+ path: request.path,
155
+ }
156
+ if @logger.level < Logger::INFO
157
+ log_attrs[:request_body] = request.body
158
+ # No need to log the authorization header
159
+ headers = request.to_hash.reject { |k, _| k&.downcase == "authorization" }
160
+ log_attrs[:request_headers] = headers
161
+ end
162
+
163
+ log_info("Request", **log_attrs)
164
+ start = Time.now
165
+ response = http.request(request)
166
+ elapsed = Time.now - start
167
+
168
+ # GETs are safe to retry after a server error, requests with an Idempotency-Key will return the prior response
169
+ if response.kind_of?(Net::HTTPServerError) && request.is_a?(Net::HTTP::Get)
170
+ retries += 1
171
+ log_info("Retrying", retries: retries, **log_attrs)
172
+ start = Time.now
173
+ response = http.request(request) if retries < MAX_RETRIES
174
+ elapsed = Time.now - start
175
+ end
176
+
177
+ if @logger.level < Logger::INFO
178
+ log_attrs[:response_body] = response.body
179
+ log_attrs[:response_headers] = response.to_hash
180
+ end
181
+ log_info("Response", time_ms: (elapsed * 1_000).floor, status: response.code, **log_attrs)
182
+
183
+ response
184
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ECONNABORTED,
185
+ Errno::EPIPE, Errno::ETIMEDOUT, Net::OpenTimeout, EOFError, SocketError => ex
186
+ retries += 1
187
+ if retries < MAX_RETRIES
188
+ retry
189
+ end
190
+
191
+ if ex.kind_of?(Net::OpenTimeout) || ex.kind_of?(Errno::ETIMEDOUT)
192
+ raise Recurly::Errors::TimeoutError, "Request timed out"
193
+ end
194
+
195
+ raise Recurly::Errors::ConnectionFailedError, "Failed to connect to Recurly: #{ex.message}"
196
+ rescue Net::ReadTimeout, Timeout::Error
197
+ raise Recurly::Errors::TimeoutError, "Request timed out"
198
+ rescue OpenSSL::SSL::SSLError => ex
199
+ raise Recurly::Errors::SSLError, ex.message
200
+ rescue StandardError => ex
201
+ raise Recurly::Errors::NetworkError, ex.message
202
+ end
203
+ end
204
+ end
117
205
 
118
- # @return [Logger]
119
- attr_reader :logger
206
+ def set_headers(request, additional_headers = {})
207
+ request["Accept"] = "application/vnd.recurly.#{api_version}".chomp # got this method from operations.rb
208
+ request["Authorization"] = "Basic #{Base64.encode64(@api_key)}".chomp
209
+ request["User-Agent"] = "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}"
120
210
 
121
- def run_request(request, headers)
122
- read_headers @conn.run_request(request.method, request.path, request.body, headers)
211
+ unless request.is_a?(Net::HTTP::Get) || request.is_a?(Net::HTTP::Head)
212
+ request["Idempotency-Key"] ||= generate_idempotency_key
213
+ end
214
+
215
+ # TODO this is undocumented until we finalize it
216
+ additional_headers.each { |header, v| request[header] = v } if additional_headers
217
+ end
218
+
219
+ # from https://github.com/rails/rails/blob/6-0-stable/activesupport/lib/active_support/core_ext/securerandom.rb
220
+ def generate_idempotency_key(n = 16)
221
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
222
+ idx = byte % 64
223
+ idx = SecureRandom.random_number(36) if idx >= 36
224
+ BASE36_ALPHABET[idx]
225
+ end.join
123
226
  end
124
227
 
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)
228
+ def set_http_options(http, options)
229
+ http.open_timeout = options[:open_timeout] || 20
230
+ http.read_timeout = options[:read_timeout] || 60
231
+ end
232
+
233
+ def handle_response!(request, http_response)
234
+ response = HTTP::Response.new(http_response, request)
235
+ raise_api_error!(http_response, response) unless http_response.kind_of?(Net::HTTPSuccess)
128
236
  resource = if response.body
129
- if BINARY_TYPES.include?(response.content_type)
237
+ if http_response.content_type&.include?(JSON_CONTENT_TYPE)
238
+ JSONParser.parse(self, response.body)
239
+ elsif BINARY_TYPES.include?(http_response.content_type)
130
240
  FileParser.parse(response.body)
131
241
  else
132
- JSONParser.parse(self, response.body)
242
+ raise Recurly::Errors::InvalidResponseError, "Unexpected content type: #{http_response.content_type}"
133
243
  end
134
244
  else
135
245
  Resources::Empty.new
@@ -139,46 +249,34 @@ module Recurly
139
249
  resource
140
250
  end
141
251
 
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
252
+ def raise_api_error!(http_response, response)
253
+ if response.content_type.include?(JSON_CONTENT_TYPE)
254
+ error = JSONParser.parse(self, response.body)
255
+ error_class = Errors::APIError.error_class(error.type)
256
+ raise error_class.new(response, error)
257
+ end
153
258
 
154
- raise error_class, ex.message
155
- end
259
+ error_class = Errors::APIError.from_response(http_response)
156
260
 
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)
261
+ if error_class <= Recurly::Errors::APIError
262
+ error = Recurly::Resources::Error.new(message: "#{http_response.code}: #{http_response.message}")
263
+ raise error_class.new(response, error)
264
+ else
265
+ raise error_class, "#{http_response.code}: #{http_response.message}"
266
+ end
161
267
  end
162
268
 
163
269
  def read_headers(response)
164
270
  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"]}"
271
+ log_warn("DEPRECTATION WARNING", message: "Your current API version \"#{api_version}\" is deprecated and will be sunset on #{response.headers["Recurly-Sunset-Date"]}")
166
272
  end
167
273
  response
168
274
  end
169
275
 
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)
276
+ def validate_path_parameters!(**options)
277
+ # Check to see that we are passing the correct data types
278
+ # This prevents a confusing error if the user passes in a non-primitive by mistake
179
279
  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
280
  unless [String, Symbol, Integer, Float].include?(v.class)
183
281
  message = "We cannot build the url with the given argument #{k}=#{v.inspect}."
184
282
  if k =~ /_id$/
@@ -186,6 +284,17 @@ module Recurly
186
284
  end
187
285
  raise ArgumentError, message
188
286
  end
287
+ end
288
+ # Check to make sure that parameters are not empty string values
289
+ empty_strings = options.select { |_, v| v.is_a?(String) && v.strip.empty? }
290
+ if empty_strings.any?
291
+ raise ArgumentError, "#{empty_strings.keys.join(", ")} cannot be an empty string"
292
+ end
293
+ end
294
+
295
+ def interpolate_path(path, **options)
296
+ validate_path_parameters!(options)
297
+ options.each do |k, v|
189
298
  # We need to encode the values for the url
190
299
  options[k] = ERB::Util.url_encode(v.to_s)
191
300
  end
@@ -201,42 +310,37 @@ module Recurly
201
310
  end
202
311
  end
203
312
 
204
- def scope_by_site(path, **options)
205
- if site = site_id || options[:site_id]
206
- "/sites/#{site}#{path}"
313
+ def set_api_key(api_key)
314
+ @api_key = api_key
315
+ end
316
+
317
+ def build_url(path, options)
318
+ path = scope_by_site(path, options)
319
+ if options.any?
320
+ "#{path}?#{URI.encode_www_form(options)}"
207
321
  else
208
322
  path
209
323
  end
210
324
  end
211
325
 
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")
326
+ def scope_by_site(path, **options)
327
+ if site = site_id || options[:site_id]
328
+ # Ensure that we are only including the site_id once because the Pager operations
329
+ # will use the cursor returned from the API which may already have these components
330
+ path.start_with?("/sites/#{site}") ? path : "/sites/#{site}#{path}"
331
+ else
332
+ path
222
333
  end
334
+ end
223
335
 
224
- @conn = Faraday.new(options) do |faraday|
225
- if [Logger::DEBUG, Logger::INFO].include?(@log_level)
226
- faraday.response :logger
336
+ # Define a private `log_<level>` method for each log level
337
+ LOG_LEVELS.each do |level|
338
+ define_method "log_#{level}" do |tag, **attrs|
339
+ @logger.send(level, "Recurly") do
340
+ msg = attrs.each_pair.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
341
+ "[#{tag}] #{msg}"
227
342
  end
228
- faraday.basic_auth(api_key, "")
229
- configure_net_adapter(faraday)
230
343
  end
231
344
  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
345
  end
242
346
  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,6 +2526,14 @@ 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)