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
@@ -1,13 +1,68 @@
1
1
  module Recurly
2
- class Coupon < AccountBase
3
- self.element_name = "coupon"
4
-
5
- def self.known_attributes
6
- [
7
- "coupon_code",
8
- "redeemed_at"
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
@@ -1,24 +1,46 @@
1
1
  module Recurly
2
- class Invoice < Base
3
- self.element_name = "invoice"
4
- self.prefix = "/accounts/:account_code/"
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
- def self.list(account_code)
7
- find(:all, :params => { :account_code => account_code })
8
- end
16
+ # @return [Account]
17
+ belongs_to :account
9
18
 
10
- def self.lookup(account_code, id)
11
- find(id, :params => { :account_code => account_code })
12
- end
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
- def self.element_path(id, prefix_options = {}, query_options = nil)
15
- path = super
35
+ private
16
36
 
17
- # postprocess generated element url.
18
- # changes /accounts/:account_code/invoices/:id to /invoices/:id
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
@@ -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
@@ -1,18 +1,29 @@
1
1
  module Recurly
2
- class Plan < Base
3
- self.element_name = "plan"
4
- self.primary_key = :plan_code
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