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.
- data/README.markdown +118 -0
- data/bin/recurly +78 -0
- data/lib/rails/generators/recurly/config_generator.rb +16 -0
- data/lib/rails/recurly.rb +13 -0
- data/lib/recurly.rb +64 -139
- data/lib/recurly/account.rb +52 -111
- data/lib/recurly/add_on.rb +20 -0
- data/lib/recurly/adjustment.rb +51 -0
- data/lib/recurly/api.rb +73 -0
- data/lib/recurly/api/errors.rb +205 -0
- data/lib/recurly/api/net_http.rb +77 -0
- data/lib/recurly/billing_info.rb +45 -42
- data/lib/recurly/coupon.rb +63 -8
- data/lib/recurly/helper.rb +39 -0
- data/lib/recurly/invoice.rb +38 -16
- data/lib/recurly/js.rb +113 -0
- data/lib/recurly/money.rb +105 -0
- data/lib/recurly/plan.rb +26 -15
- data/lib/recurly/redemption.rb +34 -0
- data/lib/recurly/resource.rb +925 -0
- data/lib/recurly/resource/pager.rb +210 -0
- data/lib/recurly/subscription.rb +90 -67
- data/lib/recurly/subscription/add_ons.rb +73 -0
- data/lib/recurly/transaction.rb +65 -53
- data/lib/recurly/transaction/errors.rb +98 -0
- data/lib/recurly/version.rb +16 -2
- data/lib/recurly/xml.rb +85 -0
- data/lib/recurly/xml/nokogiri.rb +49 -0
- data/lib/recurly/xml/rexml.rb +50 -0
- metadata +76 -165
- data/LICENSE +0 -21
- data/README.md +0 -104
- data/init.rb +0 -1
- data/lib/patches/rails2/active_resource/base.rb +0 -35
- data/lib/patches/rails2/active_resource/connection.rb +0 -10
- data/lib/patches/rails3/active_model/serializers/xml.rb +0 -28
- data/lib/patches/rails3/active_resource/connection.rb +0 -10
- data/lib/recurly/account_base.rb +0 -35
- data/lib/recurly/base.rb +0 -195
- data/lib/recurly/charge.rb +0 -39
- data/lib/recurly/config_parser.rb +0 -31
- data/lib/recurly/credit.rb +0 -28
- data/lib/recurly/exceptions.rb +0 -32
- data/lib/recurly/formats/xml_with_errors.rb +0 -132
- data/lib/recurly/formats/xml_with_pagination.rb +0 -47
- data/lib/recurly/rails2/compatibility.rb +0 -8
- data/lib/recurly/rails3/railtie.rb +0 -21
- data/lib/recurly/rails3/recurly.rake +0 -28
- data/lib/recurly/transparent.rb +0 -148
- data/lib/recurly/verification.rb +0 -83
- data/spec/config/recurly.yml +0 -6
- data/spec/config/test1.yml +0 -4
- data/spec/config/test2.yml +0 -7
- data/spec/integration/account_spec.rb +0 -286
- data/spec/integration/add_on_spec.rb +0 -84
- data/spec/integration/billing_info_spec.rb +0 -148
- data/spec/integration/charge_spec.rb +0 -176
- data/spec/integration/coupon_spec.rb +0 -49
- data/spec/integration/credit_spec.rb +0 -106
- data/spec/integration/invoice_spec.rb +0 -86
- data/spec/integration/plan_spec.rb +0 -87
- data/spec/integration/subscription_spec.rb +0 -221
- data/spec/integration/transaction_spec.rb +0 -154
- data/spec/integration/transparent_spec.rb +0 -99
- data/spec/spec_helper.rb +0 -34
- data/spec/support/factory.rb +0 -211
- data/spec/support/vcr.rb +0 -11
- data/spec/unit/account_spec.rb +0 -19
- data/spec/unit/billing_info_spec.rb +0 -39
- data/spec/unit/charge_spec.rb +0 -20
- data/spec/unit/config_spec.rb +0 -42
- data/spec/unit/coupon_spec.rb +0 -13
- data/spec/unit/credit_spec.rb +0 -20
- data/spec/unit/plan_spec.rb +0 -18
- data/spec/unit/subscription_spec.rb +0 -25
- data/spec/unit/transaction_spec.rb +0 -32
- data/spec/unit/transparent_spec.rb +0 -152
- data/spec/unit/verification_spec.rb +0 -82
@@ -0,0 +1,210 @@
|
|
1
|
+
module Recurly
|
2
|
+
class Resource
|
3
|
+
# Pages through an index resource, yielding records as it goes. It's rare
|
4
|
+
# to instantiate one on its own: use {Resource.paginate},
|
5
|
+
# {Resource.find_each}, and <tt>Resource#{has_many_association}</tt>
|
6
|
+
# instead.
|
7
|
+
#
|
8
|
+
# Because pagers handle +has_many+ associations, pagers can also build and
|
9
|
+
# create child records.
|
10
|
+
#
|
11
|
+
# @example Through a resource class:
|
12
|
+
# Recurly::Account.paginate # => #<Recurly::Resource::Pager...>
|
13
|
+
#
|
14
|
+
# Recurly::Account.find_each { |a| p a }
|
15
|
+
# @example Through an resource instance:
|
16
|
+
# account.transactions
|
17
|
+
# # => #<Recurly::Resource::Pager...>
|
18
|
+
#
|
19
|
+
# account.transactions.new(attributes) # or #create, or #create!
|
20
|
+
# # => #<Recurly::Transaction ...>
|
21
|
+
class Pager
|
22
|
+
include Enumerable
|
23
|
+
|
24
|
+
# @return [Resource] The resource class of the pager.
|
25
|
+
attr_reader :resource_class
|
26
|
+
|
27
|
+
# @return [Hash, nil] A hash of links to which the pager can page.
|
28
|
+
attr_reader :links
|
29
|
+
|
30
|
+
# @return [String, nil] An ETag for the current page.
|
31
|
+
attr_reader :etag
|
32
|
+
|
33
|
+
# A pager for paginating through resource records.
|
34
|
+
#
|
35
|
+
# @param resource_class [Resource] The resource to be paginated.
|
36
|
+
# @param options [Hash] A hash of pagination options.
|
37
|
+
# @option options [Integer] :per_page The number of records returned per
|
38
|
+
# page.
|
39
|
+
# @option options [DateTime, Time, Integer] :cursor A timestamp that the
|
40
|
+
# pager will skim back to and return records created before it.
|
41
|
+
# @option options [String] :etag When set, will raise {API::NotModified}
|
42
|
+
# if the loaded page content has not changed.
|
43
|
+
# @option options [String] :uri The default location the pager will
|
44
|
+
# request.
|
45
|
+
# @raise [API::NotModified] If the <tt>:etag</tt> option is set and
|
46
|
+
# matches the server's.
|
47
|
+
def initialize resource_class, options = {}
|
48
|
+
options[:cursor] &&= options[:cursor].to_i
|
49
|
+
@parent = options.delete :parent
|
50
|
+
@uri = options.delete :uri
|
51
|
+
@etag = options.delete :etag
|
52
|
+
@resource_class, @options = resource_class, options
|
53
|
+
@collection = @count = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String] The URI of the paginated resource.
|
57
|
+
def uri
|
58
|
+
@uri ||= resource_class.collection_path
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Integer] The total record count of the resource in question.
|
62
|
+
# @see Resource.count
|
63
|
+
def count
|
64
|
+
@count ||= API.head(uri, @options)['X-Records'].to_i
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Array] Iterates through the current page of records.
|
68
|
+
# @yield [record]
|
69
|
+
def each
|
70
|
+
return enum_for :each unless block_given?
|
71
|
+
load! unless @collection
|
72
|
+
@collection.each { |record| yield record }
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [nil]
|
76
|
+
# @see Resource.find_each
|
77
|
+
# @yield [record]
|
78
|
+
def find_each
|
79
|
+
return enum_for :find_each unless block_given?
|
80
|
+
begin
|
81
|
+
each { |record| yield record }
|
82
|
+
end while self.next
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [Array, nil] Refreshes the pager's collection of records with
|
86
|
+
# the next page.
|
87
|
+
def next
|
88
|
+
load_from links['next'], nil if links.key? 'next'
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Array, nil] Refreshes the pager's collection of records with
|
92
|
+
# the first page.
|
93
|
+
def start
|
94
|
+
load_from links['start'], nil if links.key? 'start'
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Array, nil] Load (or reload) the pager's collection from the
|
98
|
+
# original, supplied options.
|
99
|
+
def load!
|
100
|
+
load_from uri, @options
|
101
|
+
end
|
102
|
+
alias reload load!
|
103
|
+
|
104
|
+
# @return [Pager] Duplicates the pager, updating it with the options
|
105
|
+
# supplied. Useful for resource scopes.
|
106
|
+
# @see #initialize
|
107
|
+
# @example
|
108
|
+
# Recurly::Account.active.paginate :per_page => 20
|
109
|
+
def paginate options = {}
|
110
|
+
dup.instance_eval {
|
111
|
+
@collection = @count = @etag = nil
|
112
|
+
@options.update options and self
|
113
|
+
}
|
114
|
+
end
|
115
|
+
alias scoped paginate
|
116
|
+
alias where paginate
|
117
|
+
|
118
|
+
def all options = {}
|
119
|
+
paginate(options).to_a
|
120
|
+
end
|
121
|
+
|
122
|
+
# Instantiates a new record in the scope of the pager.
|
123
|
+
#
|
124
|
+
# @return [Resource] A new record.
|
125
|
+
# @example
|
126
|
+
# account = Recurly::Account.find 'schrader'
|
127
|
+
# subscription = account.subscriptions.new attributes
|
128
|
+
# @see Resource.new
|
129
|
+
def new attributes = {}
|
130
|
+
record = resource_class.send(:new, attributes) { |r|
|
131
|
+
r.attributes[@parent.class.member_name] ||= @parent if @parent
|
132
|
+
r.uri = uri
|
133
|
+
}
|
134
|
+
yield record if block_given?
|
135
|
+
record
|
136
|
+
end
|
137
|
+
|
138
|
+
# Instantiates and saves a record in the scope of the pager.
|
139
|
+
#
|
140
|
+
# @return [Resource] The record.
|
141
|
+
# @raise [Transaction::Error] A monetary transaction failed.
|
142
|
+
# @example
|
143
|
+
# account = Recurly::Account.find 'schrader'
|
144
|
+
# subscription = account.subscriptions.create attributes
|
145
|
+
# @see Resource.create
|
146
|
+
def create attributes = {}
|
147
|
+
new(attributes) { |record| record.save }
|
148
|
+
end
|
149
|
+
|
150
|
+
# Instantiates and saves a record in the scope of the pager.
|
151
|
+
#
|
152
|
+
# @return [Resource] The saved record.
|
153
|
+
# @raise [Invalid] The record is invalid.
|
154
|
+
# @raise [Transaction::Error] A monetary transaction failed.
|
155
|
+
# @example
|
156
|
+
# account = Recurly::Account.find 'schrader'
|
157
|
+
# subscription = account.subscriptions.create! attributes
|
158
|
+
# @see Resource.create!
|
159
|
+
def create! attributes = {}
|
160
|
+
new(attributes) { |record| record.save! }
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [true, false]
|
164
|
+
# @see Object#respond_to?
|
165
|
+
def respond_to? method_name, include_private = false
|
166
|
+
super || [].respond_to?(method_name, include_private)
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def load_from uri, params
|
172
|
+
options = {}
|
173
|
+
options[:head] = { 'If-None-Match' => etag } if etag
|
174
|
+
response = API.get uri, params, options
|
175
|
+
|
176
|
+
@etag = response['ETag']
|
177
|
+
@count = response['X-Records'].to_i
|
178
|
+
@links = {}
|
179
|
+
if links = response['Link']
|
180
|
+
links.scan(/<([^>]+)>; rel="([^"]+)"/).each do |link|
|
181
|
+
@links[link.last] = link.first.freeze
|
182
|
+
end
|
183
|
+
end
|
184
|
+
@links.freeze
|
185
|
+
|
186
|
+
@collection = []
|
187
|
+
document = XML.new response.body
|
188
|
+
document.each_element(resource_class.member_name) do |el|
|
189
|
+
record = resource_class.from_xml el
|
190
|
+
record.attributes[@parent.class.member_name] = @parent if @parent
|
191
|
+
@collection << record
|
192
|
+
end
|
193
|
+
@collection.freeze
|
194
|
+
rescue API::NotModified
|
195
|
+
defined? @collection and @collection or raise
|
196
|
+
end
|
197
|
+
|
198
|
+
def method_missing name, *args, &block
|
199
|
+
scope = resource_class.scopes[name] and return paginate scope
|
200
|
+
|
201
|
+
if [].respond_to? name
|
202
|
+
load! unless defined? @collection
|
203
|
+
return @collection.send name, *args, &block
|
204
|
+
end
|
205
|
+
|
206
|
+
super
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
data/lib/recurly/subscription.rb
CHANGED
@@ -1,87 +1,110 @@
|
|
1
1
|
module Recurly
|
2
|
-
class Subscription <
|
3
|
-
|
2
|
+
class Subscription < Resource
|
3
|
+
autoload :AddOns, 'recurly/subscription/add_ons'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
"trial_ends_at"
|
12
|
-
]
|
13
|
-
end
|
5
|
+
# @macro [attach] scope
|
6
|
+
# @scope class
|
7
|
+
# @return [Pager<Subscription>] A pager that yields +$1+ subscriptions.
|
8
|
+
scope :active, :state => :active
|
9
|
+
scope :in_trial, :state => :in_trial
|
10
|
+
scope :non_renewing, :state => :non_renewing
|
14
11
|
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
attributes[:addons] ||= []
|
20
|
-
super
|
21
|
-
end
|
12
|
+
# @return [Account]
|
13
|
+
belongs_to :account
|
14
|
+
# @return [Plan]
|
15
|
+
belongs_to :plan
|
22
16
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
17
|
+
define_attribute_methods %w(
|
18
|
+
uuid
|
19
|
+
state
|
20
|
+
unit_amount_in_cents
|
21
|
+
currency
|
22
|
+
quantity
|
23
|
+
activated_at
|
24
|
+
canceled_at
|
25
|
+
expires_at
|
26
|
+
current_period_started_at
|
27
|
+
current_period_ends_at
|
28
|
+
trial_started_at
|
29
|
+
trial_ends_at
|
30
|
+
pending_subscription
|
31
|
+
subscription_add_ons
|
32
|
+
)
|
33
|
+
alias to_param uuid
|
34
34
|
|
35
|
-
#
|
36
|
-
def
|
37
|
-
|
35
|
+
# @return [Subscription] A new subscription.
|
36
|
+
def initialize attributes = {}
|
37
|
+
super({ :currency => Recurly.default_currency }.merge attributes)
|
38
38
|
end
|
39
39
|
|
40
|
-
|
41
|
-
|
40
|
+
# Assign a Plan resource (rather than a plan code).
|
41
|
+
#
|
42
|
+
# @param plan [Plan]
|
43
|
+
def plan= plan
|
44
|
+
self[:plan_code] = plan.plan_code if plan.respond_to? :plan_code
|
45
|
+
attributes[:plan] = plan
|
42
46
|
end
|
43
47
|
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
unless account_code.nil?
|
48
|
-
ActiveSupport::Deprecation.warn('Calling Recurly::Subscription#cancel with an account_code has been deprecated. Use the static method Recurly::Subscription.cancel(account_code) instead', caller)
|
49
|
-
end
|
50
|
-
self.class.cancel(account_code || self.subscription_account_code)
|
48
|
+
# @return [AddOns]
|
49
|
+
def subscription_add_ons
|
50
|
+
AddOns.new self, super
|
51
51
|
end
|
52
|
+
alias add_ons subscription_add_ons
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
rescue ActiveResource::Redirection => e
|
57
|
-
return true
|
54
|
+
# Assign an array of subscription add-ons.
|
55
|
+
def subscription_add_ons= subscription_add_ons
|
56
|
+
super AddOns.new self, subscription_add_ons
|
58
57
|
end
|
58
|
+
alias add_ons= subscription_add_ons=
|
59
59
|
|
60
|
-
|
61
|
-
|
60
|
+
# Cancel a subscription so that it will not renew.
|
61
|
+
#
|
62
|
+
# @return [true, false] +true+ when successful, +false+ when unable to
|
63
|
+
# (e.g., the subscription is not active).
|
64
|
+
# @example
|
65
|
+
# account = Account.find account_code
|
66
|
+
# subscription = account.subscriptions.first
|
67
|
+
# subscription.cancel # => true
|
68
|
+
def cancel
|
69
|
+
return false unless self[:cancel]
|
70
|
+
reload self[:cancel].call
|
71
|
+
true
|
62
72
|
end
|
63
73
|
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
74
|
+
# An array of acceptable refund types.
|
75
|
+
REFUND_TYPES = ['none', 'full', 'partial'].freeze
|
76
|
+
|
77
|
+
# Immediately terminate a subscription (with optional refund).
|
78
|
+
#
|
79
|
+
# @return [true, false] +true+ when successful, +false+ when unable to
|
80
|
+
# (e.g., the subscription is not active).
|
81
|
+
# @param refund_type [:none, :full, :partial] <tt>:none</tt> terminates the
|
82
|
+
# subscription with no refund (the default), <tt>:full</tt> refunds the
|
83
|
+
# subscription in full, and <tt>:partial</tt> refunds the subscription in
|
84
|
+
# part.
|
85
|
+
# @raise [ArgumentError] Invalid +refund_type+.
|
86
|
+
# @example
|
87
|
+
# account = Account.find account_code
|
88
|
+
# subscription = account.subscriptions.first
|
89
|
+
# subscription.terminate(:partial) # => true
|
90
|
+
def terminate refund_type = :none
|
91
|
+
return false unless self[:terminate]
|
92
|
+
unless REFUND_TYPES.include? refund_type.to_s
|
93
|
+
raise ArgumentError, "refund must be one of: #{REFUND_TYPES.join ', '}"
|
94
|
+
end
|
95
|
+
reload self[:terminate].call(:params => { :refund => refund_type })
|
96
|
+
true
|
75
97
|
end
|
76
98
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
99
|
+
# Reactivate a subscription.
|
100
|
+
#
|
101
|
+
# @return [true, false] +true+ when successful, +false+ when unable to
|
102
|
+
# (e.g., the subscription is already active), and may raise an exception
|
103
|
+
# if the reactivation fails.
|
104
|
+
def reactivate
|
105
|
+
return false unless self[:reactivate]
|
106
|
+
reload self[:reactivate].call
|
107
|
+
true
|
84
108
|
end
|
85
109
|
end
|
86
110
|
end
|
87
|
-
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Recurly
|
2
|
+
class Subscription < Resource
|
3
|
+
class AddOns
|
4
|
+
instance_methods.each do |method|
|
5
|
+
undef_method method if method !~ /^__|^(object_id|respond_to\?|send)$/
|
6
|
+
end
|
7
|
+
|
8
|
+
# @param subscription [Subscription]
|
9
|
+
# @param add_ons [Array, nil]
|
10
|
+
def initialize subscription, add_ons = []
|
11
|
+
@subscription, @add_ons = subscription, []
|
12
|
+
add_ons and add_ons.each { |a| self << a }
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [self]
|
16
|
+
# @param add_on [AddOn, String, Symbol, Hash] A {Plan} add-on,
|
17
|
+
# +add_on_code+, or hash with optional <tt>:quantity</tt> and
|
18
|
+
# <tt>:unit_amount_in_cents</tt> keys.
|
19
|
+
# @example
|
20
|
+
# pp subscription.add_ons << '1YEARWAR' << '1YEARWAR' << :BONUS
|
21
|
+
# [
|
22
|
+
# {:add_on_code => "1YEARWAR", :quantity => 2},
|
23
|
+
# {:add_on_code => "BONUS"}
|
24
|
+
# ]
|
25
|
+
def << add_on
|
26
|
+
case add_on
|
27
|
+
when AddOn then add_on = { :add_on_code => add_on.add_on_code }
|
28
|
+
when String, Symbol then add_on = { :add_on_code => add_on.to_s }
|
29
|
+
end
|
30
|
+
|
31
|
+
exist = @add_ons.find { |a| a[:add_on_code] == add_on[:add_on_code] }
|
32
|
+
if exist
|
33
|
+
exist[:quantity] ||= 1 and exist[:quantity] += 1
|
34
|
+
|
35
|
+
if add_on[:unit_amount_in_cents]
|
36
|
+
exist[:unit_amount_in_cents] = add_on[:unit_amount_in_cents]
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@add_ons << add_on
|
40
|
+
end
|
41
|
+
|
42
|
+
@subscription[:subscription_add_ons] = to_a and self
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_a
|
46
|
+
@add_ons.dup
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_xml options = {}
|
50
|
+
builder = options[:builder] || XML.new('<subscription_add_ons/>')
|
51
|
+
@add_ons.each do |add_on|
|
52
|
+
node = builder.add_element 'subscription_add_on'
|
53
|
+
add_on.each_pair { |k, v| node.add_element k.to_s, v }
|
54
|
+
end
|
55
|
+
builder.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond_to? method_name, include_private = false
|
59
|
+
super || @add_ons.respond_to?(method_name, include_private)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def method_missing name, *args, &block
|
65
|
+
if @add_ons.respond_to? name
|
66
|
+
return @add_ons.send name, *args, &block
|
67
|
+
end
|
68
|
+
|
69
|
+
super
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|