recurly 3.3.0 → 3.6.0

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.
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)