recurly 0.4.16 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of recurly might be problematic. Click here for more details.

Files changed (78) hide show
  1. data/README.markdown +118 -0
  2. data/bin/recurly +78 -0
  3. data/lib/rails/generators/recurly/config_generator.rb +16 -0
  4. data/lib/rails/recurly.rb +13 -0
  5. data/lib/recurly.rb +64 -139
  6. data/lib/recurly/account.rb +52 -111
  7. data/lib/recurly/add_on.rb +20 -0
  8. data/lib/recurly/adjustment.rb +51 -0
  9. data/lib/recurly/api.rb +73 -0
  10. data/lib/recurly/api/errors.rb +205 -0
  11. data/lib/recurly/api/net_http.rb +77 -0
  12. data/lib/recurly/billing_info.rb +45 -42
  13. data/lib/recurly/coupon.rb +63 -8
  14. data/lib/recurly/helper.rb +39 -0
  15. data/lib/recurly/invoice.rb +38 -16
  16. data/lib/recurly/js.rb +113 -0
  17. data/lib/recurly/money.rb +105 -0
  18. data/lib/recurly/plan.rb +26 -15
  19. data/lib/recurly/redemption.rb +34 -0
  20. data/lib/recurly/resource.rb +925 -0
  21. data/lib/recurly/resource/pager.rb +210 -0
  22. data/lib/recurly/subscription.rb +90 -67
  23. data/lib/recurly/subscription/add_ons.rb +73 -0
  24. data/lib/recurly/transaction.rb +65 -53
  25. data/lib/recurly/transaction/errors.rb +98 -0
  26. data/lib/recurly/version.rb +16 -2
  27. data/lib/recurly/xml.rb +85 -0
  28. data/lib/recurly/xml/nokogiri.rb +49 -0
  29. data/lib/recurly/xml/rexml.rb +50 -0
  30. metadata +76 -165
  31. data/LICENSE +0 -21
  32. data/README.md +0 -104
  33. data/init.rb +0 -1
  34. data/lib/patches/rails2/active_resource/base.rb +0 -35
  35. data/lib/patches/rails2/active_resource/connection.rb +0 -10
  36. data/lib/patches/rails3/active_model/serializers/xml.rb +0 -28
  37. data/lib/patches/rails3/active_resource/connection.rb +0 -10
  38. data/lib/recurly/account_base.rb +0 -35
  39. data/lib/recurly/base.rb +0 -195
  40. data/lib/recurly/charge.rb +0 -39
  41. data/lib/recurly/config_parser.rb +0 -31
  42. data/lib/recurly/credit.rb +0 -28
  43. data/lib/recurly/exceptions.rb +0 -32
  44. data/lib/recurly/formats/xml_with_errors.rb +0 -132
  45. data/lib/recurly/formats/xml_with_pagination.rb +0 -47
  46. data/lib/recurly/rails2/compatibility.rb +0 -8
  47. data/lib/recurly/rails3/railtie.rb +0 -21
  48. data/lib/recurly/rails3/recurly.rake +0 -28
  49. data/lib/recurly/transparent.rb +0 -148
  50. data/lib/recurly/verification.rb +0 -83
  51. data/spec/config/recurly.yml +0 -6
  52. data/spec/config/test1.yml +0 -4
  53. data/spec/config/test2.yml +0 -7
  54. data/spec/integration/account_spec.rb +0 -286
  55. data/spec/integration/add_on_spec.rb +0 -84
  56. data/spec/integration/billing_info_spec.rb +0 -148
  57. data/spec/integration/charge_spec.rb +0 -176
  58. data/spec/integration/coupon_spec.rb +0 -49
  59. data/spec/integration/credit_spec.rb +0 -106
  60. data/spec/integration/invoice_spec.rb +0 -86
  61. data/spec/integration/plan_spec.rb +0 -87
  62. data/spec/integration/subscription_spec.rb +0 -221
  63. data/spec/integration/transaction_spec.rb +0 -154
  64. data/spec/integration/transparent_spec.rb +0 -99
  65. data/spec/spec_helper.rb +0 -34
  66. data/spec/support/factory.rb +0 -211
  67. data/spec/support/vcr.rb +0 -11
  68. data/spec/unit/account_spec.rb +0 -19
  69. data/spec/unit/billing_info_spec.rb +0 -39
  70. data/spec/unit/charge_spec.rb +0 -20
  71. data/spec/unit/config_spec.rb +0 -42
  72. data/spec/unit/coupon_spec.rb +0 -13
  73. data/spec/unit/credit_spec.rb +0 -20
  74. data/spec/unit/plan_spec.rb +0 -18
  75. data/spec/unit/subscription_spec.rb +0 -25
  76. data/spec/unit/transaction_spec.rb +0 -32
  77. data/spec/unit/transparent_spec.rb +0 -152
  78. data/spec/unit/verification_spec.rb +0 -82
@@ -0,0 +1,20 @@
1
+ module Recurly
2
+ class AddOn < Resource
3
+ # @return [Plan]
4
+ belongs_to :plan
5
+
6
+ define_attribute_methods %w(
7
+ add_on_code
8
+ name
9
+ default_quantity
10
+ unit_amount_in_cents
11
+ display_quantity_on_hosted_page
12
+ created_at
13
+ )
14
+ alias to_param add_on_code
15
+
16
+ # Add-ons are only writeable and readable through {Plan} instances.
17
+ embedded!
18
+ private_class_method :find
19
+ end
20
+ end
@@ -0,0 +1,51 @@
1
+ module Recurly
2
+ class Adjustment < Resource
3
+ # @macro [attach] scope
4
+ # @scope class
5
+ # @return [Pager<Adjustment>] a pager that yields +$1+.
6
+ scope :charges, :type => 'charge'
7
+ scope :credits, :type => 'credit'
8
+
9
+ scope :pending, :state => 'pending'
10
+ scope :invoiced, :state => 'invoiced'
11
+
12
+ # @return [Account, nil]
13
+ belongs_to :account
14
+ # @return [Invoice, nil]
15
+ belongs_to :invoice
16
+
17
+ define_attribute_methods %w(
18
+ uuid
19
+ description
20
+ accounting_code
21
+ origin
22
+ unit_amount_in_cents
23
+ quantity
24
+ discount_in_cents
25
+ tax_in_cents
26
+ total_in_cents
27
+ currency
28
+ taxable
29
+ start_date
30
+ end_date
31
+ created_at
32
+ )
33
+ alias to_param uuid
34
+
35
+ # @return ["charge", "credit", nil] The type of adjustment.
36
+ attr_reader :type
37
+
38
+ # Adjustments should be built through {Account} instances.
39
+ #
40
+ # @return [Adjustment] A new adjustment.
41
+ # @example
42
+ # account.adjustments.new attributes
43
+ # @see Resource#initialize
44
+ def initialize attributes = {}
45
+ super({ :currency => Recurly.default_currency }.merge attributes)
46
+ end
47
+
48
+ # Adjustments are only writeable through an {Account} instance.
49
+ embedded!
50
+ end
51
+ end
@@ -0,0 +1,73 @@
1
+ module Recurly
2
+ # The API class handles all requests to the Recurly API. While most of its
3
+ # functionality is leveraged by the Resource class, it can be used directly,
4
+ # as well.
5
+ #
6
+ # Requests are made with methods named after the four main HTTP verbs
7
+ # recognized by the Recurly API.
8
+ #
9
+ # @example
10
+ # Recurly::API.get 'accounts' # => #<Net::HTTPOK ...>
11
+ # Recurly::API.post 'accounts', xml_body # => #<Net::HTTPCreated ...>
12
+ # Recurly::API.put 'accounts/1', xml_body # => #<Net::HTTPOK ...>
13
+ # Recurly::API.delete 'accounts/1' # => #<Net::HTTPNoContent ...>
14
+ class API
15
+ require 'recurly/api/errors'
16
+
17
+ @@base_uri = "https://api.recurly.com/v2/"
18
+
19
+ class << self
20
+ # @return [String]
21
+ attr_accessor :accept_language
22
+
23
+ # @return [Net::HTTPOK, Net::HTTPResponse]
24
+ # @raise [ResponseError] With a non-2xx status code.
25
+ def head uri, params = {}, options = {}
26
+ request :head, uri, { :params => params }.merge(options)
27
+ end
28
+
29
+ # @return [Net::HTTPOK, Net::HTTPResponse]
30
+ # @raise [ResponseError] With a non-2xx status code.
31
+ def get uri, params = {}, options = {}
32
+ request :get, uri, { :params => params }.merge(options)
33
+ end
34
+
35
+ # @return [Net::HTTPCreated, Net::HTTPResponse]
36
+ # @raise [ResponseError] With a non-2xx status code.
37
+ def post uri, body = nil, options = {}
38
+ request :post, uri, { :body => body.to_s }.merge(options)
39
+ end
40
+
41
+ # @return [Net::HTTPOK, Net::HTTPResponse]
42
+ # @raise [ResponseError] With a non-2xx status code.
43
+ def put uri, body = nil, options = {}
44
+ request :put, uri, { :body => body.to_s }.merge(options)
45
+ end
46
+
47
+ # @return [Net::HTTPNoContent, Net::HTTPResponse]
48
+ # @raise [ResponseError] With a non-2xx status code.
49
+ def delete uri, options = {}
50
+ request :delete, uri, options
51
+ end
52
+
53
+ # @return [URI::Generic]
54
+ def base_uri
55
+ URI.parse @@base_uri
56
+ end
57
+
58
+ # @return [String]
59
+ def user_agent
60
+ "Recurly/#{Version::VERSION}; #{RUBY_DESCRIPTION}"
61
+ end
62
+
63
+ private
64
+
65
+ def accept
66
+ 'application/xml'
67
+ end
68
+ alias content_type accept
69
+ end
70
+ end
71
+ end
72
+
73
+ require 'recurly/api/net_http'
@@ -0,0 +1,205 @@
1
+ require 'cgi'
2
+
3
+ module Recurly
4
+ class API
5
+ # The superclass to all errors that occur when making an API request.
6
+ class ResponseError < Error
7
+ attr_reader :request
8
+ attr_reader :response
9
+
10
+ def initialize request, response
11
+ @request, @response = request, response
12
+ end
13
+
14
+ def code
15
+ response.code.to_i if response
16
+ end
17
+
18
+ def to_s
19
+ if description
20
+ return CGI.unescapeHTML [description, details].compact.join(' ')
21
+ end
22
+
23
+ return super unless code
24
+ "%d %s (%s %s)" % [
25
+ code, http_error, request.method, API.base_uri + request.path
26
+ ]
27
+ end
28
+
29
+ def symbol
30
+ xml and xml.root and xml.text '/error/symbol'
31
+ end
32
+
33
+ def description
34
+ xml and xml.root and xml.text '/error/description'
35
+ end
36
+
37
+ def details
38
+ xml and xml.root and xml.text '/error/details'
39
+ end
40
+
41
+ private
42
+
43
+ def http_error
44
+ Helper.demodulize self.class.name.gsub(/([a-z])([A-Z])/, '\1 \2')
45
+ end
46
+
47
+ def xml
48
+ return @xml if defined? @xml
49
+ @xml = (XML.new(response.body) if response && !response.body.empty?)
50
+ end
51
+ end
52
+
53
+ # === 3xx Redirection
54
+ #
55
+ # Not an error, per se, but should result in one in the normal course of
56
+ # API interaction.
57
+ class Redirection < ResponseError
58
+ end
59
+
60
+ # === 304 Not Modified
61
+ #
62
+ # Catchably raised when a request is made with an ETag.
63
+ class NotModified < ResponseError
64
+ end
65
+
66
+ # === 4xx Client Error
67
+ #
68
+ # The superclass to all client errors (responses with status code 4xx).
69
+ class ClientError < ResponseError
70
+ end
71
+
72
+ # === 400 Bad Request
73
+ #
74
+ # The request was invalid or could not be understood by the server.
75
+ # Resubmitting the request will likely result in the same error.
76
+ class BadRequest < ClientError
77
+ end
78
+
79
+ # === 401 Unauthorized
80
+ #
81
+ # The API key is missing or invalid for the given request.
82
+ class Unauthorized < ClientError
83
+ def description
84
+ response.body.strip
85
+ end
86
+ end
87
+
88
+ # === 402 Payment Required
89
+ #
90
+ # Your Recurly account is in production mode but is not in good standing.
91
+ # Please pay any outstanding invoices.
92
+ class PaymentRequired < ClientError
93
+ end
94
+
95
+ # === 403 Forbidden
96
+ #
97
+ # The login is attempting to perform an action it does not have privileges
98
+ # to access. The login credentials are correct.
99
+ class Forbidden < ClientError
100
+ end
101
+
102
+ # === 404 Not Found
103
+ #
104
+ # The resource was not found. This may be returned if the given account
105
+ # code or subscription plan does not exist. The response body will explain
106
+ # which resource was not found.
107
+ class NotFound < ClientError
108
+ end
109
+
110
+ # === 405 Method Not Allowed
111
+ #
112
+ # A method was attempted where it is not allowed.
113
+ #
114
+ # If this is raised, there may be a bug with the client library or with
115
+ # the server. Please contact support@recurly.com or
116
+ # {file a bug}[https://github.com/recurly/recurly-client-ruby/issues].
117
+ class MethodNotAllowed < ClientError
118
+ end
119
+
120
+ # === 406 Not Acceptable
121
+ #
122
+ # The request content type was not acceptable.
123
+ #
124
+ # If this is raised, there may be a bug with the client library or with
125
+ # the server. Please contact support@recurly.com or
126
+ # {file a bug}[https://github.com/recurly/recurly-client-ruby/issues].
127
+ class NotAcceptable < ClientError
128
+ end
129
+
130
+ # === 412 Precondition Failed
131
+ #
132
+ # The request was unsuccessful because a condition was not met. For
133
+ # example, this message may be returned if you attempt to cancel a
134
+ # subscription for an account that has no subscription.
135
+ class PreconditionFailed < ClientError
136
+ end
137
+
138
+ # === 415 Unsupported Media Type
139
+ #
140
+ # The request body was not recognized as XML.
141
+ #
142
+ # If this is raised, there may be a bug with the client library or with
143
+ # the server. Please contact support@recurly.com or
144
+ # {file a bug}[https://github.com/recurly/recurly-client-ruby/issues].
145
+ class UnsupportedMediaType < ClientError
146
+ end
147
+
148
+ # === 422 Unprocessable Entity
149
+ #
150
+ # Could not process a POST or PUT request because the request is invalid.
151
+ # See the response body for more details.
152
+ class UnprocessableEntity < ClientError
153
+ end
154
+
155
+ # === 5xx Server Error
156
+ #
157
+ # The superclass to all server errors (responses with status code 5xx).
158
+ class ServerError < ResponseError
159
+ end
160
+
161
+ # === 500 Internal Server Error
162
+ #
163
+ # The server encountered an error while processing your request and failed.
164
+ class InternalServerError < ServerError
165
+ end
166
+
167
+ # === 502 Gateway Error
168
+ #
169
+ # The load balancer or web server had trouble connecting to the Recurly.
170
+ # Please try the request again.
171
+ class GatewayError < ServerError
172
+ end
173
+
174
+ # === 503 Service Unavailable
175
+ #
176
+ # The service is temporarily unavailable. Please try the request again.
177
+ class ServiceUnavailable < ServerError
178
+ end
179
+
180
+ # Error mapping by status code.
181
+ ERRORS = Hash.new { |hash, code|
182
+ unless hash.key? code
183
+ case code
184
+ when 400...500 then ClientError
185
+ when 500...600 then ServerError
186
+ else ResponseError
187
+ end
188
+ end
189
+ }.update(
190
+ 304 => NotModified,
191
+ 400 => BadRequest,
192
+ 401 => Unauthorized,
193
+ 402 => PaymentRequired,
194
+ 403 => Forbidden,
195
+ 404 => NotFound,
196
+ 406 => NotAcceptable,
197
+ 412 => PreconditionFailed,
198
+ 415 => UnsupportedMediaType,
199
+ 422 => UnprocessableEntity,
200
+ 500 => InternalServerError,
201
+ 502 => GatewayError,
202
+ 503 => ServiceUnavailable
203
+ ).freeze
204
+ end
205
+ end
@@ -0,0 +1,77 @@
1
+ require 'net/https'
2
+
3
+ module Recurly
4
+ class API
5
+ module Net
6
+ module HTTP
7
+ private
8
+
9
+ METHODS = {
10
+ :head => ::Net::HTTP::Head,
11
+ :get => ::Net::HTTP::Get,
12
+ :post => ::Net::HTTP::Post,
13
+ :put => ::Net::HTTP::Put,
14
+ :delete => ::Net::HTTP::Delete
15
+ }
16
+
17
+ def request method, uri, options = {}
18
+ head = { 'Accept' => accept, 'User-Agent' => user_agent }
19
+ accept_language and head['Accept-Language'] ||= accept_language
20
+ head.update options[:head] if options[:head]
21
+ uri = base_uri + uri
22
+ if options[:params] && !options[:params].empty?
23
+ uri += "?#{options[:params].map { |k, v| "#{k}=#{v}" }.join '&' }"
24
+ end
25
+ request = METHODS[method].new uri.request_uri, head
26
+ request.basic_auth Recurly.api_key, nil
27
+ if options[:body]
28
+ head['Content-Type'] = content_type
29
+ request.body = options[:body]
30
+ end
31
+ http = ::Net::HTTP.new uri.host, uri.port
32
+ http.use_ssl = uri.scheme == 'https'
33
+
34
+ if Recurly.logger
35
+ Recurly.log :info, "===> %s %s" % [request.method, uri]
36
+ headers = request.to_hash
37
+ headers['authorization'] &&= ['Basic [FILTERED]']
38
+ Recurly.log :debug, headers.inspect
39
+ if request.body && !request.body.empty?
40
+ Recurly.log :debug, XML.filter(request.body)
41
+ end
42
+ start_time = Time.now
43
+ end
44
+
45
+ response = http.start { http.request request }
46
+ code = response.code.to_i
47
+
48
+ if Recurly.logger
49
+ latency = (Time.now - start_time) * 1_000
50
+ level = case code
51
+ when 200...300 then :info
52
+ when 300...400 then :warn
53
+ when 400...500 then :error
54
+ else :fatal
55
+ end
56
+ Recurly.log level, "<=== %d %s (%.1fms)" % [
57
+ code,
58
+ response.class.name[9, response.class.name.length].gsub(
59
+ /([a-z])([A-Z])/, '\1 \2'
60
+ ),
61
+ latency
62
+ ]
63
+ Recurly.log :debug, response.to_hash.inspect
64
+ Recurly.log :debug, response.body if response.body
65
+ end
66
+
67
+ case code
68
+ when 200...300 then response
69
+ else raise ERRORS[code].new request, response
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ extend Net::HTTP
76
+ end
77
+ end
@@ -1,49 +1,52 @@
1
1
  module Recurly
2
- class BillingInfo < AccountBase
3
- self.element_name = "billing_info"
2
+ class BillingInfo < Resource
3
+ # @return [Account]
4
+ belongs_to :account
4
5
 
5
- def self.known_attributes
6
- [
7
- "first_name",
8
- "last_name",
9
- "address1",
10
- "address2",
11
- "city",
12
- "state",
13
- "zip",
14
- "country",
15
- "phone",
16
- "ip_address",
17
- "vat_number"
18
- ]
19
- end
6
+ define_attribute_methods %w(
7
+ first_name
8
+ last_name
9
+ company
10
+ address1
11
+ address2
12
+ city
13
+ state
14
+ zip
15
+ country
16
+ phone
17
+ vat_number
18
+ ip_address
19
+ ip_address_country
20
+ card_type
21
+ year
22
+ month
23
+ start_year
24
+ start_month
25
+ issue_number
26
+ first_six
27
+ last_four
28
+ paypal_billing_agreement_id
29
+ )
20
30
 
21
- # define attributes for inner CreditCard type
22
- class CreditCard < Base
23
- def self.known_attributes
24
- [
25
- "number",
26
- "last_four",
27
- "type",
28
- "verification_value",
29
- "month",
30
- "year",
31
- "start_month",
32
- "start_year",
33
- "issue_number"
34
- ]
35
- end
36
- end
31
+ # @return ["credit_card", "paypal", nil] The type of billing info.
32
+ attr_reader :type
37
33
 
38
- # initialize associations
39
- def initialize(attributes = {}, persisted = false)
40
- attributes = attributes.with_indifferent_access
41
- attributes[:credit_card] ||= {}
42
- super
34
+ # @return [String]
35
+ def inspect
36
+ attributes = self.class.attribute_names
37
+ case type
38
+ when 'credit_card'
39
+ attributes -= %w(paypal_billing_agreement_id)
40
+ when 'paypal'
41
+ attributes -= %w(
42
+ card_type year month start_year start_month issue_number
43
+ first_six last_four
44
+ )
45
+ end
46
+ super attributes
43
47
  end
44
48
 
45
- def update_only
46
- true
47
- end
49
+ # Billing info is only writeable through an {Account} instance.
50
+ embedded!
48
51
  end
49
- end
52
+ end