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
data/lib/recurly/coupon.rb
CHANGED
@@ -1,13 +1,68 @@
|
|
1
1
|
module Recurly
|
2
|
-
class Coupon <
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
class Coupon < Resource
|
3
|
+
# @macro [attach] scope
|
4
|
+
# @scope class
|
5
|
+
# @return [Pager<Coupon>] A pager that yields +$1+ coupons.
|
6
|
+
scope :redeemable, :state => :redeemable
|
7
|
+
scope :expired, :state => :expired
|
8
|
+
scope :maxed_out, :state => :maxed_out
|
9
|
+
|
10
|
+
# @return [Pager<Redemption>]
|
11
|
+
has_many :redemptions
|
12
|
+
|
13
|
+
define_attribute_methods %w(
|
14
|
+
coupon_code
|
15
|
+
name
|
16
|
+
state
|
17
|
+
discount_type
|
18
|
+
discount_percent
|
19
|
+
discount_in_cents
|
20
|
+
redeem_by_date
|
21
|
+
single_use
|
22
|
+
applies_for_months
|
23
|
+
max_redemptions
|
24
|
+
applies_to_all_plans
|
25
|
+
created_at
|
26
|
+
currencies
|
27
|
+
plan_codes
|
28
|
+
)
|
29
|
+
alias to_param coupon_code
|
30
|
+
|
31
|
+
# Redeem a coupon with a given account or account code.
|
32
|
+
#
|
33
|
+
# @return [true]
|
34
|
+
# @param account_or_code [Account, String]
|
35
|
+
# @example
|
36
|
+
# coupon = Coupon.find coupon_code
|
37
|
+
# coupon.redeem account_code
|
38
|
+
#
|
39
|
+
# coupon = Coupon.find coupon_code
|
40
|
+
# account = Account.find account_code
|
41
|
+
# coupon.redeem account
|
42
|
+
def redeem account_or_code, currency = nil
|
43
|
+
return false unless self[:redeem]
|
44
|
+
|
45
|
+
account_code = if account_or_code.is_a? Account
|
46
|
+
account_or_code.account_code
|
47
|
+
else
|
48
|
+
account_or_code
|
49
|
+
end
|
50
|
+
|
51
|
+
Redemption.from_response self[:redeem].call(
|
52
|
+
:body => (redemption = redemptions.new(
|
53
|
+
:account_code => account_code,
|
54
|
+
:currency => currency || Recurly.default_currency
|
55
|
+
)).to_xml
|
56
|
+
)
|
57
|
+
rescue API::UnprocessableEntity => e
|
58
|
+
redemption.apply_errors e
|
59
|
+
redemption
|
10
60
|
end
|
11
61
|
|
62
|
+
def redeem! account_code, currency = nil
|
63
|
+
redemption = redeem account_code, currency
|
64
|
+
raise Invalid.new(self) unless redemption.persisted?
|
65
|
+
redemption
|
66
|
+
end
|
12
67
|
end
|
13
68
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Recurly
|
2
|
+
module Helper
|
3
|
+
def camelize underscored_word
|
4
|
+
underscored_word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
5
|
+
end
|
6
|
+
|
7
|
+
def classify table_name
|
8
|
+
camelize singularize(table_name.to_s.sub(/.*\./, ''))
|
9
|
+
end
|
10
|
+
|
11
|
+
def demodulize class_name_in_module
|
12
|
+
class_name_in_module.to_s.sub(/^.*::/, '')
|
13
|
+
end
|
14
|
+
|
15
|
+
def pluralize word
|
16
|
+
word.to_s.sub(/([^s])$/, '\1s')
|
17
|
+
end
|
18
|
+
|
19
|
+
def singularize word
|
20
|
+
word.to_s.sub(/s$/, '').sub(/ie$/, 'y')
|
21
|
+
end
|
22
|
+
|
23
|
+
def underscore camel_cased_word
|
24
|
+
word = camel_cased_word.to_s.dup
|
25
|
+
word.gsub!(/::/, '/')
|
26
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
27
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
28
|
+
word.tr! "-", "_"
|
29
|
+
word.downcase!
|
30
|
+
word
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash_with_indifferent_read_access
|
34
|
+
Hash.new { |hash, key| hash[key.to_s] if key.is_a? Symbol }
|
35
|
+
end
|
36
|
+
|
37
|
+
extend self
|
38
|
+
end
|
39
|
+
end
|
data/lib/recurly/invoice.rb
CHANGED
@@ -1,24 +1,46 @@
|
|
1
1
|
module Recurly
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
# Invoices are created through account objects.
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# account = Account.find account_code
|
6
|
+
# account.invoice!
|
7
|
+
class Invoice < Resource
|
8
|
+
# @macro [attach] scope
|
9
|
+
# @scope class
|
10
|
+
# @return [Pager<Invoice>] A pager that yields +$1+ invoices.
|
11
|
+
scope :open, :state => :open
|
12
|
+
scope :collected, :state => :collected
|
13
|
+
scope :failed, :state => :failed
|
14
|
+
scope :past_due, :state => :past_due
|
5
15
|
|
6
|
-
|
7
|
-
|
8
|
-
end
|
16
|
+
# @return [Account]
|
17
|
+
belongs_to :account
|
9
18
|
|
10
|
-
|
11
|
-
|
12
|
-
|
19
|
+
define_attribute_methods %w(
|
20
|
+
uuid
|
21
|
+
state
|
22
|
+
invoice_number
|
23
|
+
po_number
|
24
|
+
vat_number
|
25
|
+
subtotal_in_cents
|
26
|
+
tax_in_cents
|
27
|
+
total_in_cents
|
28
|
+
currency
|
29
|
+
created_at
|
30
|
+
line_items
|
31
|
+
transactions
|
32
|
+
)
|
33
|
+
alias to_param uuid
|
13
34
|
|
14
|
-
|
15
|
-
path = super
|
35
|
+
private
|
16
36
|
|
17
|
-
|
18
|
-
|
19
|
-
# this breaks update, however I dont believe recurly allows invoice updates anyways
|
20
|
-
path.sub("/accounts/#{CGI::escape(prefix_options[:account_code].to_s)}/invoices/", "/invoices/")
|
37
|
+
def initialize attributes = {}
|
38
|
+
super({ :currency => Recurly.default_currency }.merge attributes)
|
21
39
|
end
|
22
40
|
|
41
|
+
# Invoices are only writeable through {Account} instances.
|
42
|
+
embedded!
|
43
|
+
undef save
|
44
|
+
undef destroy
|
23
45
|
end
|
24
|
-
end
|
46
|
+
end
|
data/lib/recurly/js.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Recurly
|
4
|
+
# A collection of helper methods to use to verify
|
5
|
+
# {Recurly.js}[http://js.recurly.com/] callbacks.
|
6
|
+
module JS
|
7
|
+
# Raised when signature verification fails.
|
8
|
+
class RequestForgery < Error
|
9
|
+
end
|
10
|
+
|
11
|
+
# Used to prevent strings from being escaped during digest.
|
12
|
+
class SafeString < String
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# @return [String] A private key for Recurly.js.
|
17
|
+
# @raise [ConfigurationError] No private key has been set.
|
18
|
+
def private_key
|
19
|
+
defined? @private_key and @private_key or raise(
|
20
|
+
ConfigurationError, "private_key not configured"
|
21
|
+
)
|
22
|
+
end
|
23
|
+
attr_writer :private_key
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
def sign_billing_info account_code
|
27
|
+
sign 'billinginfoupdate', 'account_code' => account_code
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String]
|
31
|
+
def sign_transaction amount_in_cents, currency = nil, account_code = nil
|
32
|
+
sign 'transactioncreate', {
|
33
|
+
'amount_in_cents' => amount_in_cents,
|
34
|
+
'currency' => currency || Recurly.default_currency,
|
35
|
+
'account_code' => account_code
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [true]
|
40
|
+
# @raise [RequestForgery] If verification fails.
|
41
|
+
def verify_billing_info! params
|
42
|
+
verify! 'billinginfoupdated', params
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [true]
|
46
|
+
# @raise [RequestForgery] If verification fails.
|
47
|
+
def verify_transaction! params
|
48
|
+
verify! 'subscriptioncreated', params
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [true]
|
52
|
+
# @raise [RequestForgery] If verification fails.
|
53
|
+
def verify_subscription! params
|
54
|
+
verify! 'subscriptioncreated', params
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [String]
|
58
|
+
def inspect
|
59
|
+
'Recurly.js'
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def sign claim, params, timestamp = Time.now
|
65
|
+
signature = OpenSSL::HMAC.hexdigest(
|
66
|
+
OpenSSL::Digest::Digest.new('SHA1'),
|
67
|
+
Digest::SHA1.digest(private_key),
|
68
|
+
digest([Time.now.to_i, claim, params])
|
69
|
+
)
|
70
|
+
"#{signature}-#{timestamp}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def verify! claim, params
|
74
|
+
params = Hash[params.map { |key, value| [key.to_s, value] }]
|
75
|
+
signature = params.delete('signature') or raise(
|
76
|
+
RequestForgery, 'missing signature'
|
77
|
+
)
|
78
|
+
timestamp = signature.split('-').last
|
79
|
+
age = Time.now.to_i - timestamp.to_i
|
80
|
+
unless (-3600..3600).include? age
|
81
|
+
raise RequestForgery, 'stale timestamp'
|
82
|
+
end
|
83
|
+
|
84
|
+
if signature != sign(claim, params, timestamp)
|
85
|
+
raise RequestForgery,
|
86
|
+
"signature can't be verified (invalid request or private key)"
|
87
|
+
end
|
88
|
+
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def digest data
|
93
|
+
case data
|
94
|
+
when Array
|
95
|
+
return if data.empty?
|
96
|
+
SafeString.new "[#{data.map { |d| digest d }.compact.join ','}]"
|
97
|
+
when Hash
|
98
|
+
data = Hash[data.map { |key, value| [key.to_s, value] }]
|
99
|
+
digest data.keys.sort.map { |key|
|
100
|
+
next unless value = digest(data[key])
|
101
|
+
SafeString.new "#{"#{key}:" unless key =~ /^\d+$/}#{value}"
|
102
|
+
}
|
103
|
+
when SafeString
|
104
|
+
data
|
105
|
+
when String
|
106
|
+
SafeString.new data.gsub(/([\[\]\,\:\\])/, '\\\\\1')
|
107
|
+
else
|
108
|
+
data
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Recurly
|
2
|
+
# Represents a collection of currencies (in cents).
|
3
|
+
class Money
|
4
|
+
# @return A money object representing multiple currencies (in cents).
|
5
|
+
# @param currencies [Hash] A hash of currency codes and amounts.
|
6
|
+
# @example
|
7
|
+
# # 12 United States dollars.
|
8
|
+
# Recurly::Money.new :USD => 12_00
|
9
|
+
#
|
10
|
+
# # $9.99 (or €6.99).
|
11
|
+
# Recurly::Money.new :USD => 9_99, :EUR => 6_99
|
12
|
+
#
|
13
|
+
# # Using a default currency.
|
14
|
+
# Recurly.default_currency = 'USD'
|
15
|
+
# Recurly::Money.new(49_00) # => #<Recurly::Money USD: 49_00>
|
16
|
+
def initialize currencies = {}
|
17
|
+
@currencies = Helper.hash_with_indifferent_read_access
|
18
|
+
|
19
|
+
if currencies.respond_to? :each_pair
|
20
|
+
currencies.each_pair { |key, value| @currencies[key.to_s] = value }
|
21
|
+
else
|
22
|
+
@currencies[Recurly.default_currency] = currencies
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Hash] A hash of currency codes to amounts.
|
27
|
+
def to_hash
|
28
|
+
currencies.dup
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [true, false] Whether or not the currency is equal to another
|
32
|
+
# instance.
|
33
|
+
# @param other [Money]
|
34
|
+
def eql? other
|
35
|
+
other.respond_to?(:currencies) && currencies.eql?(other.currencies)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Integer] Unique identifier.
|
39
|
+
# @see Hash#hash
|
40
|
+
def hash
|
41
|
+
currencies.hash
|
42
|
+
end
|
43
|
+
|
44
|
+
# Implemented so that solitary currencies can be compared and sorted.
|
45
|
+
#
|
46
|
+
# @return [-1, 0, 1]
|
47
|
+
# @param other [Money]
|
48
|
+
# @example
|
49
|
+
# [Recurly::Money.new(2_00), Recurly::Money.new(1_00)].sort
|
50
|
+
# # => [#<Recurly::Money USD: 1_00>, #<Recurly::Money USD: 2_00>]
|
51
|
+
# @see Hash#<=>
|
52
|
+
def <=> other
|
53
|
+
if currencies.keys.length == 1 && other.currencies.length == 1
|
54
|
+
if currencies.keys == other.currencies.keys
|
55
|
+
return currencies.values.first <=> other.currencies.values.first
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
currencies <=> other.currencies
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [true, false]
|
63
|
+
# @see Object#respond_to?
|
64
|
+
def respond_to? method_name, include_private = false
|
65
|
+
super || currencies.respond_to?(method_name, include_private)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [String]
|
69
|
+
def inspect
|
70
|
+
string = "#<#{self.class}"
|
71
|
+
if currencies.any?
|
72
|
+
string << " %s" % currencies.keys.sort.map { |code|
|
73
|
+
value = currencies[code].to_s
|
74
|
+
value.gsub!(/^(\d)$/, '0_0\1')
|
75
|
+
value.gsub!(/^(\d{2})$/, '0_\1')
|
76
|
+
value.gsub!(/(\d)(\d{2})$/, '\1_\2')
|
77
|
+
value.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, '\1_')
|
78
|
+
"#{code}: #{value}"
|
79
|
+
}.join(', ')
|
80
|
+
end
|
81
|
+
string << '>'
|
82
|
+
end
|
83
|
+
alias to_s inspect
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
attr_reader :currencies
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def method_missing name, *args, &block
|
92
|
+
if currencies.respond_to? name
|
93
|
+
return currencies.send name, *args, &block
|
94
|
+
elsif c = currencies[Recurly.default_currency] and c.respond_to? name
|
95
|
+
if currencies.keys.length > 1
|
96
|
+
raise TypeError, "can't convert multicurrency into Integer"
|
97
|
+
else
|
98
|
+
return c.send name, *args, &block
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
super
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/recurly/plan.rb
CHANGED
@@ -1,18 +1,29 @@
|
|
1
1
|
module Recurly
|
2
|
-
class Plan <
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
def self.known_attributes
|
7
|
-
[
|
8
|
-
"plan_code",
|
9
|
-
"name",
|
10
|
-
"description",
|
11
|
-
"success_url",
|
12
|
-
"cancel_url",
|
13
|
-
"created_at"
|
14
|
-
]
|
15
|
-
end
|
2
|
+
class Plan < Resource
|
3
|
+
# @return [Pager<AddOn>, Array]
|
4
|
+
has_many :add_ons
|
16
5
|
|
6
|
+
define_attribute_methods %w(
|
7
|
+
plan_code
|
8
|
+
name
|
9
|
+
description
|
10
|
+
success_url
|
11
|
+
cancel_url
|
12
|
+
display_donation_amounts
|
13
|
+
display_quantity
|
14
|
+
display_phone_number
|
15
|
+
bypass_hosted_confirmation
|
16
|
+
unit_name
|
17
|
+
payment_page_tos_link
|
18
|
+
payment_page_css
|
19
|
+
setup_fee_in_cents
|
20
|
+
unit_amount_in_cents
|
21
|
+
plan_interval_length
|
22
|
+
plan_interval_unit
|
23
|
+
trial_interval_length
|
24
|
+
trial_interval_unit
|
25
|
+
created_at
|
26
|
+
)
|
27
|
+
alias to_param plan_code
|
17
28
|
end
|
18
|
-
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Recurly
|
2
|
+
# Redemptions are not top-level resources, but they can be accessed (and
|
3
|
+
# created) through {Coupon} instances.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# coupon = Coupon.find "summer2011"
|
7
|
+
# coupon.redemptions.each { |r| p r }
|
8
|
+
# coupon.redeem Account.find("groupon_lover")
|
9
|
+
class Redemption < Resource
|
10
|
+
# @return [Coupon]
|
11
|
+
belongs_to :coupon
|
12
|
+
# @return [Account]
|
13
|
+
belongs_to :account, :readonly => false
|
14
|
+
|
15
|
+
define_attribute_methods %w(
|
16
|
+
single_use
|
17
|
+
total_discounted_in_cents
|
18
|
+
currency
|
19
|
+
created_at
|
20
|
+
)
|
21
|
+
|
22
|
+
def save
|
23
|
+
return false if persisted?
|
24
|
+
copy_from coupon.redeem account, currency
|
25
|
+
true
|
26
|
+
rescue Recurly::API::UnprocessableEntity => e
|
27
|
+
apply_errors e
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
# Redemptions are only writeable through {Coupon} instances.
|
32
|
+
embedded!
|
33
|
+
end
|
34
|
+
end
|