chargebee 2.55.0 → 2.57.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -0
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +52 -0
  6. data/chargebee.gemspec +4 -2
  7. data/lib/chargebee/environment.rb +14 -8
  8. data/lib/chargebee/models/addon.rb +24 -7
  9. data/lib/chargebee/models/address.rb +6 -2
  10. data/lib/chargebee/models/attached_item.rb +16 -5
  11. data/lib/chargebee/models/business_entity.rb +6 -2
  12. data/lib/chargebee/models/card.rb +18 -5
  13. data/lib/chargebee/models/comment.rb +12 -4
  14. data/lib/chargebee/models/configuration.rb +3 -2
  15. data/lib/chargebee/models/coupon.rb +32 -9
  16. data/lib/chargebee/models/coupon_code.rb +12 -4
  17. data/lib/chargebee/models/coupon_set.rb +24 -7
  18. data/lib/chargebee/models/credit_note.rb +65 -31
  19. data/lib/chargebee/models/credit_note_estimate.rb +1 -1
  20. data/lib/chargebee/models/currency.rb +20 -6
  21. data/lib/chargebee/models/customer.rb +92 -25
  22. data/lib/chargebee/models/customer_entitlement.rb +2 -1
  23. data/lib/chargebee/models/differential_price.rb +16 -5
  24. data/lib/chargebee/models/entitlement.rb +6 -2
  25. data/lib/chargebee/models/entitlement_override.rb +6 -2
  26. data/lib/chargebee/models/estimate.rb +42 -20
  27. data/lib/chargebee/models/event.rb +7 -5
  28. data/lib/chargebee/models/export.rb +70 -18
  29. data/lib/chargebee/models/feature.rb +28 -8
  30. data/lib/chargebee/models/gift.rb +24 -7
  31. data/lib/chargebee/models/hosted_page.rb +107 -45
  32. data/lib/chargebee/models/in_app_subscription.rb +16 -4
  33. data/lib/chargebee/models/invoice.rb +198 -80
  34. data/lib/chargebee/models/invoice_estimate.rb +1 -1
  35. data/lib/chargebee/models/item.rb +16 -5
  36. data/lib/chargebee/models/item_entitlement.rb +12 -4
  37. data/lib/chargebee/models/item_family.rb +16 -5
  38. data/lib/chargebee/models/item_price.rb +20 -7
  39. data/lib/chargebee/models/non_subscription.rb +4 -1
  40. data/lib/chargebee/models/omnichannel_subscription.rb +6 -3
  41. data/lib/chargebee/models/omnichannel_subscription_item.rb +8 -2
  42. data/lib/chargebee/models/order.rb +42 -12
  43. data/lib/chargebee/models/payment_intent.rb +10 -3
  44. data/lib/chargebee/models/payment_schedule_scheme.rb +10 -3
  45. data/lib/chargebee/models/payment_source.rb +60 -16
  46. data/lib/chargebee/models/payment_voucher.rb +10 -4
  47. data/lib/chargebee/models/plan.rb +24 -7
  48. data/lib/chargebee/models/portal_session.rb +14 -4
  49. data/lib/chargebee/models/price_variant.rb +16 -5
  50. data/lib/chargebee/models/pricing_page_session.rb +8 -2
  51. data/lib/chargebee/models/promotional_credit.rb +16 -5
  52. data/lib/chargebee/models/purchase.rb +6 -2
  53. data/lib/chargebee/models/quote.rb +76 -22
  54. data/lib/chargebee/models/quote_line_group.rb +1 -1
  55. data/lib/chargebee/models/quoted_ramp.rb +22 -0
  56. data/lib/chargebee/models/ramp.rb +16 -5
  57. data/lib/chargebee/models/recorded_purchase.rb +6 -2
  58. data/lib/chargebee/models/resource_migration.rb +2 -1
  59. data/lib/chargebee/models/rule.rb +2 -1
  60. data/lib/chargebee/models/site_migration_detail.rb +2 -1
  61. data/lib/chargebee/models/subscription.rb +134 -37
  62. data/lib/chargebee/models/subscription_entitlement.rb +6 -2
  63. data/lib/chargebee/models/time_machine.rb +10 -3
  64. data/lib/chargebee/models/transaction.rb +34 -11
  65. data/lib/chargebee/models/unbilled_charge.rb +20 -6
  66. data/lib/chargebee/models/usage.rb +16 -5
  67. data/lib/chargebee/models/usage_event.rb +4 -2
  68. data/lib/chargebee/models/usage_file.rb +4 -2
  69. data/lib/chargebee/models/virtual_bank_account.rb +20 -6
  70. data/lib/chargebee/nativeRequest.rb +118 -63
  71. data/lib/chargebee/request.rb +4 -4
  72. data/lib/chargebee/result.rb +8 -2
  73. data/lib/chargebee.rb +2 -1
  74. data/spec/chargebee/list_result_spec.rb +1 -1
  75. data/spec/chargebee/native_request_spec.rb +199 -0
  76. data/spec/spec_helper.rb +3 -0
  77. metadata +5 -2
@@ -15,13 +15,15 @@ module ChargeBee
15
15
  def self.upload(params, env=nil, headers={})
16
16
  jsonKeys = {
17
17
  }
18
- Request.send('post', uri_path("usage_files","upload"), params, env, headers, "file-ingest", false, jsonKeys)
18
+ options = {}
19
+ Request.send('post', uri_path("usage_files","upload"), params, env, headers, "file-ingest", false, jsonKeys, options)
19
20
  end
20
21
 
21
22
  def self.status(id, env=nil, headers={})
22
23
  jsonKeys = {
23
24
  }
24
- Request.send('get', uri_path("usage_files",id.to_s,"status"), {}, env, headers, "file-ingest", false, jsonKeys)
25
+ options = {}
26
+ Request.send('get', uri_path("usage_files",id.to_s,"status"), {}, env, headers, "file-ingest", false, jsonKeys, options)
25
27
  end
26
28
 
27
29
  end # ~UsageFile
@@ -11,37 +11,51 @@ module ChargeBee
11
11
  def self.create_using_permanent_token(params, env=nil, headers={})
12
12
  jsonKeys = {
13
13
  }
14
- Request.send('post', uri_path("virtual_bank_accounts","create_using_permanent_token"), params, env, headers,nil, false, jsonKeys)
14
+ options = {
15
+ :isIdempotent => true
16
+ }
17
+ Request.send('post', uri_path("virtual_bank_accounts","create_using_permanent_token"), params, env, headers,nil, false, jsonKeys, options)
15
18
  end
16
19
 
17
20
  def self.create(params, env=nil, headers={})
18
21
  jsonKeys = {
19
22
  }
20
- Request.send('post', uri_path("virtual_bank_accounts"), params, env, headers,nil, false, jsonKeys)
23
+ options = {
24
+ :isIdempotent => true
25
+ }
26
+ Request.send('post', uri_path("virtual_bank_accounts"), params, env, headers,nil, false, jsonKeys, options)
21
27
  end
22
28
 
23
29
  def self.retrieve(id, env=nil, headers={})
24
30
  jsonKeys = {
25
31
  }
26
- Request.send('get', uri_path("virtual_bank_accounts",id.to_s), {}, env, headers,nil, false, jsonKeys)
32
+ options = {}
33
+ Request.send('get', uri_path("virtual_bank_accounts",id.to_s), {}, env, headers,nil, false, jsonKeys, options)
27
34
  end
28
35
 
29
36
  def self.list(params={}, env=nil, headers={})
30
37
  jsonKeys = {
31
38
  }
32
- Request.send_list_request('get', uri_path("virtual_bank_accounts"), params, env, headers,nil, false, jsonKeys)
39
+ options = {}
40
+ Request.send_list_request('get', uri_path("virtual_bank_accounts"), params, env, headers,nil, false, jsonKeys, options)
33
41
  end
34
42
 
35
43
  def self.delete(id, env=nil, headers={})
36
44
  jsonKeys = {
37
45
  }
38
- Request.send('post', uri_path("virtual_bank_accounts",id.to_s,"delete"), {}, env, headers,nil, false, jsonKeys)
46
+ options = {
47
+ :isIdempotent => true
48
+ }
49
+ Request.send('post', uri_path("virtual_bank_accounts",id.to_s,"delete"), {}, env, headers,nil, false, jsonKeys, options)
39
50
  end
40
51
 
41
52
  def self.delete_local(id, env=nil, headers={})
42
53
  jsonKeys = {
43
54
  }
44
- Request.send('post', uri_path("virtual_bank_accounts",id.to_s,"delete_local"), {}, env, headers,nil, false, jsonKeys)
55
+ options = {
56
+ :isIdempotent => true
57
+ }
58
+ Request.send('post', uri_path("virtual_bank_accounts",id.to_s,"delete_local"), {}, env, headers,nil, false, jsonKeys, options)
45
59
  end
46
60
 
47
61
  end # ~VirtualBankAccount
@@ -2,85 +2,139 @@ require 'json'
2
2
  require 'net/http'
3
3
  require 'uri'
4
4
  require 'stringio'
5
+ require 'zlib'
6
+ require 'securerandom'
5
7
 
6
8
  module ChargeBee
7
9
  module NativeRequest
8
-
9
- def self.request(method, url, env, params = nil, headers = {}, subdomain = nil, isJsonRequest = false)
10
+ def self.request(method, url, env, params = nil, headers = {}, subdomain = nil, isJsonRequest = false, options={})
10
11
  raise Error.new('No environment configured.') unless env
11
12
  api_key = env.api_key
12
13
 
13
- uri = URI(env.api_url(url, subdomain))
14
+ uri = build_uri(method, env.api_url(url, subdomain), params)
15
+
16
+ payload = build_payload(method, params, isJsonRequest)
17
+ request = build_http_request(method, uri, headers, isJsonRequest)
18
+ request.body = payload if payload
19
+ request.basic_auth(api_key, nil)
20
+
21
+ http = configure_http_client(uri, env)
22
+
23
+ retry_config = env.retry_config || {}
24
+ retry_enabled = retry_config[:enabled] == true
25
+ max_retries = retry_enabled ? (retry_config[:max_retries] || 3) : 0
26
+ delay_ms = retry_enabled ? (retry_config[:delay_ms] || 500) : 0
27
+ retry_on = retry_config[:retry_on] || [500, 502, 503, 504]
28
+ enable_debug = env.enable_debug_logs
14
29
 
15
- case method.to_s.downcase.to_sym
16
- when :get, :head, :delete
17
- uri.query = URI.encode_www_form(params) if params
18
- payload = nil
30
+ attempts = 0
31
+ response = nil
32
+
33
+ while attempts <= max_retries
34
+ begin
35
+ attempts += 1
36
+ if attempts > 0
37
+ request["X-CB-Retry-Attempt"] = attempts.to_s
38
+ if options[:isIdempotent] && request["chargebee-idempotency-key"].nil?
39
+ request["chargebee-idempotency-key"] = SecureRandom.uuid
40
+ end
41
+ end
42
+ response = http.request(request)
43
+
44
+ break unless retry_enabled && retry_on.include?(response.code.to_i) && attempts <= max_retries
45
+
46
+ retry_delay = extract_retry_delay(response, delay_ms, attempts)
47
+ puts "[ChargeBee] Retrying request (status #{response.code}) attempt #{attempts} after #{retry_delay.round(2)}s" if enable_debug
48
+ sleep(retry_delay)
49
+ rescue => e
50
+ puts "[ChargeBee] HTTP request failed on attempt #{attempts}: #{e}" if enable_debug
51
+
52
+ if retry_enabled && attempts <= max_retries
53
+ retry_delay = backoff_delay(delay_ms, attempts)
54
+ sleep(retry_delay)
55
+ next
56
+ else
57
+ raise IOError.new("IO Exception when trying to connect to ChargeBee with URL #{uri} . Reason: #{e}", e)
58
+ end
59
+ end
60
+ end
61
+
62
+ handle_response(response, headers)
63
+ end
64
+
65
+ def self.build_uri(method, url, params)
66
+ uri = URI(url)
67
+ if %i[get head delete].include?(method.to_s.downcase.to_sym) && params
68
+ uri.query = URI.encode_www_form(params)
69
+ end
70
+ uri
71
+ end
72
+
73
+ def self.build_payload(method, params, is_json)
74
+ if %i[get head delete].include?(method.to_s.downcase.to_sym)
75
+ nil
19
76
  else
20
- payload = isJsonRequest ? params : URI.encode_www_form(params || {})
77
+ is_json ? params.to_json : URI.encode_www_form(params || {})
21
78
  end
22
- user_agent = ChargeBee.user_agent
23
- content_type_header = isJsonRequest ? "application/json;charset=UTF-8" : "application/x-www-form-urlencoded"
79
+ end
80
+
81
+ def self.build_http_request(method, uri, custom_headers, is_json)
82
+ request_class = {
83
+ get: Net::HTTP::Get,
84
+ post: Net::HTTP::Post,
85
+ put: Net::HTTP::Put,
86
+ delete: Net::HTTP::Delete
87
+ }[method.to_s.downcase.to_sym] || raise(Error.new("Unsupported HTTP method: #{method}"))
88
+
89
+ content_type = is_json ? "application/json;charset=UTF-8" : "application/x-www-form-urlencoded"
90
+
24
91
  headers = {
25
- "User-Agent" => user_agent,
92
+ "User-Agent" => ChargeBee.user_agent,
26
93
  "Accept" => "application/json",
27
94
  "Lang-Version" => RUBY_VERSION,
28
95
  "OS-Version" => RUBY_PLATFORM,
29
- "Content-Type" => content_type_header
30
- }.merge(headers)
96
+ "Content-Type" => content_type
97
+ }.merge(custom_headers)
98
+
99
+ request_class.new(uri, headers)
100
+ end
31
101
 
102
+ def self.configure_http_client(uri, env)
32
103
  http = Net::HTTP.new(uri.host, uri.port)
33
- http.use_ssl = true
34
- http.open_timeout=env.connect_timeout
35
- http.read_timeout=env.read_timeout
104
+ http.use_ssl = uri.scheme == 'https'
105
+ http.open_timeout = env.connect_timeout
106
+ http.read_timeout = env.read_timeout
36
107
  if ChargeBee.verify_ca_certs?
37
108
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
38
109
  http.ca_file = ChargeBee.ca_cert_path
39
110
  else
40
111
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
41
112
  end
113
+ http
114
+ end
42
115
 
43
- request_class = case method.to_s.downcase.to_sym
44
- when :get then Net::HTTP::Get
45
- when :post then Net::HTTP::Post
46
- when :put then Net::HTTP::Put
47
- when :delete then Net::HTTP::Delete
48
- else raise Error.new("Unsupported HTTP method: #{method}")
49
- end
50
-
51
- request = request_class.new(uri, headers)
52
- request.body = payload if payload
53
-
54
- request.basic_auth(api_key, nil)
55
-
56
- begin
57
- response = http.request(request)
58
- rescue => e
59
- raise IOError.new("IO Exception when trying to connect to ChargeBee with URL #{uri} . Reason: #{e}", e)
116
+ def self.extract_retry_delay(response, delay_ms, attempt)
117
+ retry_after = response['Retry-After']
118
+ retry_after_delay = Integer(retry_after) rescue 0
119
+ if retry_after_delay > 0
120
+ retry_after_delay
121
+ else
122
+ backoff_delay(delay_ms, attempt)
60
123
  end
61
- handle_response(response, headers)
124
+ end
125
+
126
+ def self.backoff_delay(delay_ms, attempt)
127
+ jitter = rand(100)
128
+ (delay_ms * (2 ** (attempt - 1)) + jitter) / 1000.0
62
129
  end
63
130
 
64
131
  def self.handle_response(response, headers)
65
132
  rcode = response.code.to_i
66
133
  rbody = response.body
67
-
68
- # converting headers to rest-client format previously we were using rest-client,
69
- # and mapping headers to that format to support backward compatability
70
134
  rheaders = beautify_headers(response.to_hash)
71
135
 
72
- # When a custom 'Accept-Encoding' header is set to gzip, Net::HTTP will not automatically
73
- # decompress the response. Therefore, we need to manually handle decompression
74
- # based on the 'Content-Encoding' header in the response.
75
- # https://github.com/ruby/ruby/blob/19c1f0233eb5202403c52b196f1d573893eacab7/lib/net/http/generic_request.rb#L82
76
136
  if rheaders[:content_encoding] == 'gzip' && rbody && !rbody.empty?
77
- rbody = StringIO.new(rbody)
78
- gz = Zlib::GzipReader.new(rbody)
79
- begin
80
- rbody = gz.read
81
- ensure
82
- gz.close
83
- end
137
+ rbody = decompress_gzip(rbody)
84
138
  end
85
139
 
86
140
  if rcode >= 200 && rcode < 300
@@ -95,13 +149,22 @@ module ChargeBee
95
149
  end
96
150
  end
97
151
 
152
+ def self.decompress_gzip(rbody)
153
+ gz = Zlib::GzipReader.new(StringIO.new(rbody))
154
+ begin
155
+ gz.read
156
+ ensure
157
+ gz.close
158
+ end
159
+ end
160
+
98
161
  def self.handle_json_error(rbody, e)
99
162
  if rbody.include?("503")
100
- raise Error.new("Sorry, the server is currently unable to handle the request due to a temporary overload or scheduled maintenance. Please retry after sometime. \n type: internal_temporary_error, \n http_status_code: 503, \n error_code: internal_temporary_error,\n content: #{rbody.inspect}",e)
163
+ raise Error.new("Sorry, the server is currently unable to handle the request due to a temporary overload or scheduled maintenance. Please retry after sometime. \n type: internal_temporary_error, \n http_status_code: 503, \n error_code: internal_temporary_error,\n content: #{rbody.inspect}", e)
101
164
  elsif rbody.include?("504")
102
- raise Error.new("The server did not receive a timely response from an upstream server, request aborted. If this problem persists, contact us at support@chargebee.com. \n type: gateway_timeout, \n http_status_code: 504, \n error_code: gateway_timeout,\n content: #{rbody.inspect}",e)
165
+ raise Error.new("The server did not receive a timely response from an upstream server, request aborted. If this problem persists, contact us at support@chargebee.com. \n type: gateway_timeout, \n http_status_code: 504, \n error_code: gateway_timeout,\n content: #{rbody.inspect}", e)
103
166
  else
104
- raise Error.new("Sorry, something went wrong when trying to process the request. If this problem persists, contact us at support@chargebee.com. \n type: internal_error, \n http_status_code: 500, \n error_code: internal_error,\n content: #{rbody.inspect}",e)
167
+ raise Error.new("Sorry, something went wrong when trying to process the request. If this problem persists, contact us at support@chargebee.com. \n type: internal_error, \n http_status_code: 500, \n error_code: internal_error,\n content: #{rbody.inspect}", e)
105
168
  end
106
169
  end
107
170
 
@@ -111,7 +174,7 @@ module ChargeBee
111
174
  error_obj = JSON.parse(rbody)
112
175
  error_obj = Util.symbolize_keys(error_obj)
113
176
  rescue Exception => e
114
- raise Error.new("Error response not in JSON format. The http status code is #{rcode} \n #{rbody.inspect}",e)
177
+ raise Error.new("Error response not in JSON format. The http status code is #{rcode} \n #{rbody.inspect}", e)
115
178
  end
116
179
  type = error_obj[:type]
117
180
  case type
@@ -125,19 +188,11 @@ module ChargeBee
125
188
  raise APIError.new(rcode, error_obj)
126
189
  end
127
190
  end
128
- # directly copying headers formatting from rest-client to support backward compatability for rest-client
191
+
129
192
  def self.beautify_headers(headers)
130
- headers.inject({}) do |out, (key, value)|
193
+ headers.each_with_object({}) do |(key, value), out|
131
194
  key_sym = key.tr('-', '_').downcase.to_sym
132
-
133
- # Handle Set-Cookie specially since it cannot be joined by comma.
134
- if key.downcase == 'set-cookie'
135
- out[key_sym] = value
136
- else
137
- out[key_sym] = value.join(', ')
138
- end
139
-
140
- out
195
+ out[key_sym] = key.downcase == 'set-cookie' ? value : value.join(', ')
141
196
  end
142
197
  end
143
198
  end
@@ -1,7 +1,7 @@
1
1
  module ChargeBee
2
2
  class Request
3
3
 
4
- def self.send_list_request(method, url, params={}, env=nil, headers={}, sub_domain=nil, isJsonRequest=nil, jsonKeys={})
4
+ def self.send_list_request(method, url, params={}, env=nil, headers={}, sub_domain=nil, isJsonRequest=nil, jsonKeys={}, options={})
5
5
  serialized = {}
6
6
  params.each do |k, v|
7
7
  if(v.kind_of? Array)
@@ -9,13 +9,13 @@ module ChargeBee
9
9
  end
10
10
  serialized["#{k}"] = v
11
11
  end
12
- self.send(method, url, serialized, env, headers, sub_domain, isJsonRequest=nil, jsonKeys={})
12
+ self.send(method, url, serialized, env, headers, sub_domain, isJsonRequest=nil, jsonKeys={}, options)
13
13
  end
14
14
 
15
- def self.send(method, url, params={}, env=nil, headers={}, sub_domain=nil, isJsonRequest=nil, jsonKeys={})
15
+ def self.send(method, url, params={}, env=nil, headers={}, sub_domain=nil, isJsonRequest=nil, jsonKeys={}, options={})
16
16
  env ||= ChargeBee.default_env
17
17
  ser_params = isJsonRequest ? params.to_json : Util.serialize(params, nil, nil, jsonKeys)
18
- resp, rheaders, rcode = NativeRequest.request(method, url, env, ser_params||={}, headers, sub_domain, isJsonRequest)
18
+ resp, rheaders, rcode = NativeRequest.request(method, url, env, ser_params||={}, headers, sub_domain, isJsonRequest, options)
19
19
  if resp.has_key?(:list)
20
20
  ListResult.new(resp[:list], resp[:next_offset], rheaders, rcode)
21
21
  else
@@ -201,6 +201,12 @@ module ChargeBee
201
201
  return quoted_charge;
202
202
  end
203
203
 
204
+ def quoted_ramp()
205
+ quoted_ramp = get(:quoted_ramp, QuotedRamp,
206
+ {:line_items => QuotedRamp::LineItem, :discounts => QuotedRamp::Discount, :item_tiers => QuotedRamp::ItemTier});
207
+ return quoted_ramp;
208
+ end
209
+
204
210
  def quote_line_group()
205
211
  quote_line_group = get(:quote_line_group, QuoteLineGroup,
206
212
  {:line_items => QuoteLineGroup::LineItem, :discounts => QuoteLineGroup::Discount, :line_item_discounts => QuoteLineGroup::LineItemDiscount, :taxes => QuoteLineGroup::Tax, :line_item_taxes => QuoteLineGroup::LineItemTax});
@@ -450,7 +456,7 @@ module ChargeBee
450
456
  omnichannel_subscription = get(:omnichannel_subscription, OmnichannelSubscription, {},
451
457
  {:omnichannel_subscription_items => OmnichannelSubscriptionItem});
452
458
  omnichannel_subscription.init_dependant_list(@response[:omnichannel_subscription], :omnichannel_subscription_items,
453
- {:upcoming_renewal => OmnichannelSubscriptionItem::UpcomingRenewal});
459
+ {:upcoming_renewal => OmnichannelSubscriptionItem::UpcomingRenewal, :linked_item => OmnichannelSubscriptionItem::LinkedItem});
454
460
  return omnichannel_subscription;
455
461
  end
456
462
 
@@ -461,7 +467,7 @@ module ChargeBee
461
467
 
462
468
  def omnichannel_subscription_item()
463
469
  omnichannel_subscription_item = get(:omnichannel_subscription_item, OmnichannelSubscriptionItem,
464
- {:upcoming_renewal => OmnichannelSubscriptionItem::UpcomingRenewal});
470
+ {:upcoming_renewal => OmnichannelSubscriptionItem::UpcomingRenewal, :linked_item => OmnichannelSubscriptionItem::LinkedItem});
465
471
  return omnichannel_subscription_item;
466
472
  end
467
473
 
data/lib/chargebee.rb CHANGED
@@ -87,11 +87,12 @@ require File.dirname(__FILE__) + '/chargebee/models/usage_event'
87
87
  require File.dirname(__FILE__) + '/chargebee/models/rule'
88
88
  require File.dirname(__FILE__) + '/chargebee/models/omnichannel_subscription_item_scheduled_change'
89
89
  require File.dirname(__FILE__) + '/chargebee/models/usage_file'
90
+ require File.dirname(__FILE__) + '/chargebee/models/quoted_ramp'
90
91
 
91
92
 
92
93
  module ChargeBee
93
94
 
94
- VERSION = '2.55.0'
95
+ VERSION = '2.57.0'
95
96
 
96
97
  @@default_env = nil
97
98
  @@verify_ca_certs = true
@@ -48,6 +48,6 @@ describe ChargeBee::ListResult do
48
48
 
49
49
  it "returns list object, with next offset attribute" do
50
50
  list = ChargeBee::Request.send(:customer, "http://url.com", {:limit => 2})
51
- expect(list.next_offset) =~ ["1345724673000", "1510"]
51
+ expect(list.next_offset).to eq("[\"1345724673000\", \"1510\"]")
52
52
  end
53
53
  end
@@ -0,0 +1,199 @@
1
+ require 'spec_helper'
2
+ require 'zlib'
3
+ require 'stringio'
4
+
5
+ module ChargeBee
6
+ describe NativeRequest do
7
+ let(:env) do
8
+ stub(
9
+ api_key: "test_api_key",
10
+ connect_timeout: 2,
11
+ read_timeout: 5,
12
+ retry_config: {
13
+ enabled: true,
14
+ max_retries: 2,
15
+ delay_ms: 0.1,
16
+ retry_on: [503, 429,504]
17
+ },
18
+ enable_debug_logs: false
19
+ ).tap do |env_stub|
20
+ env_stub.define_singleton_method(:api_url) do |url, subdomain|
21
+ URI("https://#{subdomain || 'dummy'}.chargebee.com#{url}")
22
+ end
23
+ end
24
+ end
25
+
26
+
27
+ before do
28
+ ChargeBee.stubs(:user_agent).returns("ChargeBee-TestAgent")
29
+ ChargeBee.stubs(:verify_ca_certs?).returns(false)
30
+ end
31
+
32
+ it "sends a GET request with query params" do
33
+ stub_request(:get, "https://dummy.chargebee.com/test").
34
+ with(query: { "key" => "value" }).
35
+ to_return(body: '{"result": "ok"}', status: 200, headers: { "Content-Type" => "application/json" })
36
+
37
+ resp, headers, code = NativeRequest.request(:get, "/test", env, { "key" => "value" })
38
+
39
+ expect(resp[:result]).to eq("ok")
40
+ expect(code).to eq(200)
41
+ end
42
+
43
+ it "sends a POST request with URL-encoded payload" do
44
+ stub_request(:post, "https://dummy.chargebee.com/test").
45
+ with(body: "foo=bar").
46
+ to_return(body: '{"success":true}', status: 200)
47
+
48
+ resp, _, _ = NativeRequest.request(:post, "/test", env, { foo: "bar" })
49
+ expect(resp[:success]).to eq(true)
50
+ end
51
+
52
+ it "sends a POST request with JSON payload when isJsonRequest=true" do
53
+ stub_request(:post, "https://dummy.chargebee.com/test")
54
+ .with(
55
+ body: { foo: "bar" }.to_json, # match raw JSON string
56
+ headers: {
57
+ 'Accept' => 'application/json',
58
+ 'Content-Type' => 'application/json;charset=UTF-8',
59
+ 'User-Agent' => 'ChargeBee-TestAgent',
60
+ 'Lang-Version' => RUBY_VERSION,
61
+ 'OS-Version' => RUBY_PLATFORM,
62
+ 'Authorization' => "Basic #{Base64.strict_encode64('test_api_key:')}"
63
+ }
64
+ )
65
+ .to_return(status: 200, body: '{"success":true}', headers: { 'Content-Type' => 'application/json' })
66
+
67
+ resp, _, _ = NativeRequest.request(:post, "/test", env, { foo: "bar" }, {}, nil, true)
68
+ expect(resp[:success]).to eq(true)
69
+ end
70
+
71
+ it "retries on 503 with exponential backoff" do
72
+ stub_request(:get, "https://dummy.chargebee.com/test")
73
+ .to_return(
74
+ {
75
+ status: 503,
76
+ body: {
77
+ message: "Sorry, something went wrong when trying to process the request.",
78
+ type: "operation_failed",
79
+ api_error_code: "internal_error",
80
+ error_code: "internal_error",
81
+ error_msg: "Sorry, something went wrong when trying to process the request.",
82
+ http_status_code: 500
83
+ }.to_json,
84
+ headers: { 'Content-Type' => 'application/json' }
85
+ },
86
+ {
87
+ status: 200,
88
+ body: { ok: true }.to_json,
89
+ headers: { 'Content-Type' => 'application/json' }
90
+ }
91
+ )
92
+
93
+ resp, _, code = NativeRequest.request(:get, "/test", env, {})
94
+ expect(resp[:ok]).to eq(true)
95
+ expect(code).to eq(200)
96
+ end
97
+
98
+ it "handles gzip compressed response body" do
99
+ raw_json = '{"gzipped":true}'
100
+ compressed = StringIO.new
101
+ gz = Zlib::GzipWriter.new(compressed)
102
+ gz.write(raw_json)
103
+ gz.close
104
+
105
+ stub_request(:get, "https://dummy.chargebee.com/test").
106
+ to_return(body: compressed.string, headers: { "Content-Encoding" => "gzip" }, status: 200)
107
+
108
+ resp, _, _ = NativeRequest.request(:get, "/test", env)
109
+ expect(resp[:gzipped]).to eq(true)
110
+ end
111
+
112
+ it "raises error for 503 with JSON-like content" do
113
+ stub_request(:get, "https://dummy.chargebee.com/test").
114
+ to_return(body: "503 Service Unavailable", status: 503)
115
+
116
+ expect {
117
+ NativeRequest.request(:get, "/test", env)
118
+ }.to raise_error(Error, /503/)
119
+ end
120
+
121
+ it "raises specific error types based on response type field" do
122
+ ["payment", "operation_failed", "invalid_request", "other"].each do |type|
123
+ error_json = { type: type, message: "error", api_error_code: "code", http_status_code: 400 }.to_json
124
+ stub_request(:get, "https://dummy.chargebee.com/test").
125
+ to_return(body: error_json, status: 400)
126
+
127
+ expect {
128
+ NativeRequest.request(:get, "/test", env)
129
+ }.to raise_error do |err|
130
+ case type
131
+ when "payment"
132
+ expect(err).to be_a(PaymentError)
133
+ when "operation_failed"
134
+ expect(err).to be_a(OperationFailedError)
135
+ when "invalid_request"
136
+ expect(err).to be_a(InvalidRequestError)
137
+ else
138
+ expect(err).to be_a(APIError)
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ it "retries once on HTTP 503 and succeeds on second attempt" do
145
+ stub_request(:get, "https://dummy.chargebee.com/test")
146
+ .to_return({ status: 503, body: "temporary error" },
147
+ { status: 200, body: '{"ok": true}', headers: { 'Content-Type' => 'application/json' } })
148
+
149
+ resp, _, code = NativeRequest.request(:get, "/test", env)
150
+ expect(resp[:ok]).to eq(true)
151
+ expect(code).to eq(200)
152
+ end
153
+
154
+ it "uses Retry-After header if provided" do
155
+ stub = stub_request(:get, "https://dummy.chargebee.com/test")
156
+ .to_return(
157
+ { status: 429, headers: { "Retry-After" => "0" }, body: "{}" },
158
+ { status: 200, body: '{"done": true}', headers: { 'Content-Type' => 'application/json' } }
159
+ )
160
+
161
+ resp, _, code = NativeRequest.request(:get, "/test", env)
162
+
163
+ expect(resp[:done]).to eq(true)
164
+ expect(code).to eq(200)
165
+ expect(stub).to have_been_requested.times(2)
166
+ end
167
+
168
+ it "raises after exhausting max_retries for retryable status codes" do
169
+ stub_request(:get, "https://dummy.chargebee.com/test")
170
+ .to_return(status: 503, body: "fail again")
171
+
172
+ expect {
173
+ NativeRequest.request(:get, "/test", env)
174
+ }.to raise_error(Error, /503/)
175
+ end
176
+
177
+ it "retries on network errors like Timeout" do
178
+ stub_request(:get, "https://dummy.chargebee.com/test")
179
+ .to_timeout.then
180
+ .to_return(status: 200, body: '{"hello":true}', headers: { 'Content-Type' => 'application/json' })
181
+
182
+ resp, _, _ = NativeRequest.request(:get, "/test", env)
183
+ expect(resp[:hello]).to eq(true)
184
+ end
185
+
186
+ it "retries on 504 and then succeeds" do
187
+ stub_request(:get, "https://dummy.chargebee.com/test")
188
+ .to_return(
189
+ { status: 504, body: "gateway timeout" },
190
+ { status: 200, body: '{"recovered":true}', headers: { 'Content-Type' => 'application/json' } }
191
+ )
192
+
193
+ resp, _, code = NativeRequest.request(:get, "/test", env)
194
+ expect(resp[:recovered]).to eq(true)
195
+ expect(code).to eq(200)
196
+ end
197
+
198
+ end
199
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,7 +4,10 @@ require 'rspec'
4
4
  require 'pp'
5
5
  require 'mocha'
6
6
  require 'json'
7
+ require 'webmock/rspec'
8
+ require 'base64'
7
9
 
10
+ WebMock.disable_net_connect!(allow_localhost: true)
8
11
  RSpec.configure do |config|
9
12
  config.mock_with :mocha
10
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chargebee
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.55.0
4
+ version: 2.57.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rajaraman S
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-06-03 00:00:00.000000000 Z
12
+ date: 2025-06-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cgi
@@ -149,6 +149,7 @@ files:
149
149
  - lib/chargebee/models/quote.rb
150
150
  - lib/chargebee/models/quote_line_group.rb
151
151
  - lib/chargebee/models/quoted_charge.rb
152
+ - lib/chargebee/models/quoted_ramp.rb
152
153
  - lib/chargebee/models/quoted_subscription.rb
153
154
  - lib/chargebee/models/ramp.rb
154
155
  - lib/chargebee/models/recorded_purchase.rb
@@ -175,6 +176,7 @@ files:
175
176
  - lib/chargebee/util.rb
176
177
  - lib/ssl/ca-certs.crt
177
178
  - spec/chargebee/list_result_spec.rb
179
+ - spec/chargebee/native_request_spec.rb
178
180
  - spec/chargebee_spec.rb
179
181
  - spec/errors_spec.rb
180
182
  - spec/sample_response.rb
@@ -207,6 +209,7 @@ specification_version: 2
207
209
  summary: Ruby client for Chargebee API.
208
210
  test_files:
209
211
  - spec/chargebee/list_result_spec.rb
212
+ - spec/chargebee/native_request_spec.rb
210
213
  - spec/chargebee_spec.rb
211
214
  - spec/errors_spec.rb
212
215
  - spec/sample_response.rb