recurly 0.1.4 → 0.2.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.md CHANGED
@@ -16,13 +16,14 @@ Installation
16
16
 
17
17
  This library can be installed as a gem or a plugin. Your choice.
18
18
 
19
- **Gem Installation:**
19
+ **Rails3 Bundle Integration:**
20
20
 
21
- gem install recurly --source=http://gemcutter.org
22
-
23
- **Plugin Installation:**
21
+ gem "recurly", :git => "http://github.com/railsjedi/recurly-client-ruby.git"
24
22
 
25
- script/plugin install git@github.com:recurly/recurly-client-ruby.git
23
+
24
+ **Plugin Installation (not recommended):**
25
+
26
+ script/plugin install http://github.com/railsjedi/recurly-client-ruby.git
26
27
 
27
28
 
28
29
  Authentication
@@ -35,6 +36,7 @@ Create a file in your Rails app at __/config/initializers/recurly_config.rb__ wi
35
36
  Recurly.configure do |c|
36
37
  c.username = 'api@yourcompany.com'
37
38
  c.password = 'super_secret_password'
39
+ c.site = 'https://my-recurly-site.recurly.com'
38
40
  end
39
41
 
40
42
 
@@ -43,13 +45,57 @@ Demo Application
43
45
 
44
46
  [Recurly Ruby Demo App](http://github.com/recurly/recurly-client-ruby-demo)
45
47
 
48
+
46
49
  Examples
47
50
  --------
48
51
 
49
- All the functionality is demonstrated by the unit tests in the __test__ directory.
52
+ All the functionality is demonstrated by the tests in the __spec__ directory.
53
+
54
+
55
+ Running the Specs
56
+ ------------------
57
+
58
+ Recurly gem uses RSpec2 for testing. It also uses VCR / Webmock to handle fast and repeatable full integration tests with the API.
59
+
60
+ The way this works is when each spec is first run, it will save each HTTP request generated within the spec/vcr folder. Subsequent http requests will be mocked using the data contained in these YML files.
61
+
62
+ The first thing to do is install bundler if you don't already have it:
63
+
64
+ gem install bundler
65
+
66
+ The next thing is to setup all the spec dependencies
67
+
68
+ bundle
69
+
70
+ When first running the specs, you'll need to setup a recurly test account. Use the provided rake task to walk you through creating spec/spec_settings.yml with all the authentication info.
71
+
72
+ rake recurly:setup
73
+
74
+ Now when you run `rake` it will hit recurly's api to run all the specs. Subsequent calls will no longer hit the API (and be run locally).
75
+
76
+
77
+ Something go Wrong?
78
+ ------------------
79
+
80
+ You can view the full http interactions with Recurly at spec/vcr. Please attached these to any bug reports so we can replicate.
81
+
82
+
83
+ Clearing Test Data
84
+ ------------------
85
+
86
+ You can delete the spec/vcr folder at any time, and it will regenerate the requests to recurly's apis. However if you do this, you'll also need to clear the test data on your recurly account. Here's how (manually):
87
+
88
+ * Login to Recurly
89
+ * Click "Configuration"" on the top right menu
90
+ * Select "Clear Test Data"
91
+
92
+ This is also automated via a rake task. It will delete the spec/vcr files, and clear the data for you on the server (using your spec_settings.yml authentication info).
93
+
94
+ rake recurly:clear
95
+
50
96
 
51
97
 
52
98
  API Documentation
53
99
  -----------------
54
100
 
55
- Please see the [Recurly API](http://support.recurly.com/faqs/api/) for more information.
101
+ Please see the [Recurly API](http://docs.recurly.com/api/basics) for more information.
@@ -1,17 +1,70 @@
1
1
  module Recurly
2
2
  class Account < RecurlyBase
3
3
  self.element_name = "account"
4
-
4
+ self.primary_key = :account_code
5
+
6
+ # Maps the
7
+ SHOW_PARAMS = {
8
+ :active => "active_subscribers",
9
+ :pastdue => "pastdue_subscribers",
10
+ :free => "non_subscribers"
11
+ }
12
+
13
+ # Lists all accounts (with optional filter)
14
+ #
15
+ # examples:
16
+ # Account.list(:all) #=> returns all accounts
17
+ # Account.list(:active) #=> returns active accounts
18
+ # Account.list(:pastdue) #=> returns pastdue accounts
19
+ # Account.list(:free) #=> returns the free accounts
20
+ #
21
+ def self.list(status = :all)
22
+ opts = {}
23
+ if status && status != :all
24
+ opts[:params] = {:show => SHOW_PARAMS[status] || status}
25
+ end
26
+ find(:all, opts)
27
+ end
28
+
5
29
  def close_account
6
30
  destroy
7
31
  end
8
-
9
- def primary_key
10
- self.account_code
32
+
33
+ def charges
34
+ Charge.list(account_code)
35
+ end
36
+ memoize :charges
37
+
38
+ def lookup_charge(id)
39
+ Charge.lookup(account_code, id)
40
+ end
41
+
42
+ def credits
43
+ Credit.list(account_code)
44
+ end
45
+ memoize :credits
46
+
47
+ def lookup_credit(id)
48
+ Credit.lookup(account_code, id)
49
+ end
50
+
51
+ def transactions(status)
52
+ Transaction.list(account_code, status)
53
+ end
54
+ memoize :transactions
55
+
56
+ def lookup_transaction(id)
57
+ Transaction.lookup(account_code, id)
58
+ end
59
+
60
+ def invoices
61
+ Invoice.list(account_code)
11
62
  end
12
-
13
- def to_param
14
- self.account_code
63
+ memoize :invoices
64
+
65
+ def lookup_invoice(id)
66
+ Invoice.lookup(account_code, id)
15
67
  end
68
+
16
69
  end
17
70
  end
data/lib/recurly/base.rb CHANGED
@@ -1,14 +1,24 @@
1
- require "rubygems"
2
- require "active_resource"
1
+ require 'active_support/memoizable'
2
+
3
+ module Recurly
4
+ class RecurlyBase < ActiveResource::Base
5
+ extend ActiveSupport::Memoizable
6
+
7
+ self.format = Recurly::Formats::XmlWithPaginationFormat.new
8
+
9
+ # Add User-Agent to headers
10
+ def headers
11
+ super
12
+ @headers['User-Agent'] = "Recurly Ruby Client v#{VERSION}"
13
+ @headers
14
+ end
3
15
 
4
- # See http://github.com/rails/rails/commit/1488c6cc9e6237ce794e3c4a6201627b9fd4ca09
5
- # Errors in Rails 2.3.4 are not parsed correctly.
6
- module ActiveResource
7
- class Base
8
16
  def update_only
9
17
  false
10
18
  end
11
-
19
+
20
+ # See http://github.com/rails/rails/commit/1488c6cc9e6237ce794e3c4a6201627b9fd4ca09
21
+ # Errors in Rails 2.3.4 are not parsed correctly.
12
22
  def save
13
23
  if update_only
14
24
  update
@@ -16,7 +26,7 @@ module ActiveResource
16
26
  save_without_validation
17
27
  end
18
28
  true
19
- rescue ResourceInvalid => error
29
+ rescue ActiveResource::ResourceInvalid => error
20
30
  case error.response['Content-Type']
21
31
  when /application\/xml/
22
32
  errors.from_xml(error.response.body)
@@ -25,52 +35,59 @@ module ActiveResource
25
35
  end
26
36
  false
27
37
  end
28
- end
29
- end
30
38
 
31
- module Recurly
32
-
33
- VERSION = '0.1.3'
34
-
35
- class << self
36
- attr_accessor :username, :password, :site
37
-
38
- def configure
39
- yield self
40
-
41
- RecurlyBase.user = username
42
- RecurlyBase.password = password
43
- RecurlyBase.site = site || "https://app.recurly.com"
44
- true
39
+ # patch persisted? so it looks to see if it actually is persisted
40
+ def persisted?
41
+ @persisted ||= false
42
+ @persisted
45
43
  end
46
- end
47
-
48
- class RecurlyBase < ActiveResource::Base
49
-
50
- # Add User-Agent to headers
51
- def headers
44
+
45
+ # patch new? to be the opposite of persisted
46
+ def new?
47
+ !persisted?
48
+ end
49
+
50
+ # patch load_attributes_from_response so it marks result records as persisted
51
+ def load_attributes_from_response(response)
52
52
  super
53
- @headers['User-Agent'] = "Recurly Ruby Client v#{VERSION}"
54
- @headers
53
+ @persisted = true
54
+ end
55
+
56
+ # patch instantiate_record so it marks result records as persisted
57
+ def self.instantiate_record(record, prefix_options)
58
+ result = super
59
+ result.instance_eval{ @persisted = true }
60
+ result
55
61
  end
56
62
  end
57
-
63
+
58
64
  # ActiveRecord treats resources as plural by default. Some resources are singular.
59
- class RecurlySingularResourceBase < RecurlyBase
60
-
65
+ class RecurlyAccountBase < RecurlyBase
66
+ self.prefix = "/accounts/:account_code/"
67
+
61
68
  # Override element_path because this is a singular resource
62
69
  def self.element_path(id, prefix_options = {}, query_options = nil)
63
70
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
64
- # original: "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"
65
- "#{prefix(prefix_options)}#{CGI::escape(id || '')}/#{element_name}.#{format.extension}#{query_string(query_options)}"
71
+ prefix_options.merge!(:account_code => id) if id
72
+ # original: "#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
73
+ "#{prefix(prefix_options)}#{element_name}.#{format.extension}#{query_string(query_options)}"
66
74
  end
67
-
75
+
76
+ # element path
77
+ def element_path(options = nil)
78
+ self.class.element_path(to_param, options || prefix_options)
79
+ end
80
+
81
+ def to_param
82
+ attributes[:account_code]
83
+ end
84
+
68
85
  # Override collection_path because this is a singular resource
69
- def self.collection_path(prefix_options = {}, query_options = nil)
70
- prefix_options, query_options = split_options(prefix_options) if query_options.nil?
71
- # original: "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
72
- "#{prefix(prefix_options)}/#{element_name}.#{format.extension}#{query_string(query_options)}"
86
+ def self.collection_path(prefix_options = {}, query_options = nil)
87
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
88
+ # original: "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
89
+ "#{prefix(prefix_options)}#{element_name}.#{format.extension}#{query_string(query_options)}"
73
90
  end
74
-
91
+
75
92
  end
76
93
  end
@@ -1,8 +1,7 @@
1
1
  module Recurly
2
- class BillingInfo < RecurlySingularResourceBase
2
+ class BillingInfo < RecurlyAccountBase
3
3
  self.element_name = "billing_info"
4
- self.prefix = "/accounts/:account_code"
5
-
4
+
6
5
  def update_only
7
6
  true
8
7
  end
@@ -2,9 +2,14 @@ module Recurly
2
2
  class Charge < RecurlyBase
3
3
  self.element_name = "charge"
4
4
  self.prefix = "/accounts/:account_code/"
5
-
5
+
6
6
  def self.list(account_code)
7
- Charge.find(:all, :params => { :account_code => account_code })
7
+ find(:all, :params => { :account_code => account_code })
8
8
  end
9
+
10
+ def self.lookup(account_code, id)
11
+ find(id, :params => { :account_code => account_code })
12
+ end
13
+
9
14
  end
10
15
  end
@@ -2,9 +2,14 @@ module Recurly
2
2
  class Credit < RecurlyBase
3
3
  self.element_name = "credit"
4
4
  self.prefix = "/accounts/:account_code/"
5
-
5
+
6
6
  def self.list(account_code)
7
- Credit.find(:all, :params => { :account_code => account_code })
7
+ find(:all, :params => { :account_code => account_code })
8
8
  end
9
+
10
+ def self.lookup(account_code, id)
11
+ find(id, :params => { :account_code => account_code })
12
+ end
13
+
9
14
  end
10
15
  end
@@ -0,0 +1,38 @@
1
+ module Recurly
2
+ module Formats
3
+ class XmlWithPaginationFormat
4
+ include ActiveResource::Formats::XmlFormat
5
+
6
+ def decode(xml)
7
+ data = super
8
+
9
+ if data.is_a?(Hash) and data["type"] == "collection" and data["current_page"]
10
+ data = paginate_data(data)
11
+ end
12
+
13
+ data
14
+ end
15
+
16
+ # convert the data into a paginated resultset (array with singleton methods)
17
+ def paginate_data(data)
18
+ # find the first array and use that as the resultset (lame workaround)
19
+ results = data.values.select{|v| v.is_a?(Array)}.first
20
+
21
+ # use a singleton methods for now (maybe wrap in WillPaginate later?)
22
+ total_entries = data["total_entries"] || 0
23
+ def results.total_entries; total_entries; end
24
+
25
+ current_page = data["current_page"] || 1
26
+ def results.current_page; current_page; end
27
+
28
+ per_page = data["per_page"]
29
+ if per_page
30
+ def results.per_page; per_page; end
31
+ end
32
+
33
+ results
34
+ end
35
+ end
36
+ end
37
+ end
38
+
@@ -1,9 +1,24 @@
1
1
  module Recurly
2
2
  class Invoice < RecurlyBase
3
3
  self.element_name = "invoice"
4
-
4
+ self.prefix = "/accounts/:account_code/"
5
+
5
6
  def self.list(account_code)
6
- Invoice.find(:all, :from => "/accounts/#{account_code}/invoices")
7
+ find(:all, :params => { :account_code => account_code })
7
8
  end
9
+
10
+ def self.lookup(account_code, id)
11
+ find(id, :params => { :account_code => account_code })
12
+ end
13
+
14
+ def self.element_path(id, prefix_options = {}, query_options = nil)
15
+ path = super
16
+
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] || '')}/invoices/", "/invoices/")
21
+ end
22
+
8
23
  end
9
24
  end
data/lib/recurly/plan.rb CHANGED
@@ -2,9 +2,6 @@ module Recurly
2
2
  class Plan < RecurlyBase
3
3
  self.element_name = "plan"
4
4
  self.prefix = "/company/"
5
-
6
- def to_param
7
- self.plan_code
8
- end
5
+ self.primary_key = :plan_code
9
6
  end
10
7
  end
@@ -1,36 +1,35 @@
1
1
  module Recurly
2
- class Subscription < RecurlySingularResourceBase
2
+ class Subscription < RecurlyAccountBase
3
3
  self.element_name = "subscription"
4
- self.prefix = "/accounts/:account_code"
5
-
4
+
6
5
  def self.refund(account_code, refund_type = :partial)
7
6
  raise "Refund type must be :full or :partial." unless refund_type == :full or refund_type == :partial
8
7
  Subscription.delete(nil, {:account_code => account_code, :refund => refund_type})
9
8
  end
10
-
9
+
11
10
  # Stops the subscription from renewing. The subscription remains valid until the end of
12
11
  # the current term (current_period_ends_at).
13
- def cancel (account_code)
12
+ def cancel(account_code)
14
13
  Subscription.delete(account_code)
15
14
  end
16
-
15
+
17
16
  # Terminates the subscription immediately and processes a full or partial refund
18
17
  def refund(refund_type)
19
18
  raise "Refund type must be :full or :partial." unless refund_type == :full or refund_type == :partial
20
19
  Subscription.delete(nil, {:account_code => self.subscription_account_code, :refund => refund_type})
21
20
  end
22
-
21
+
23
22
  # Valid timeframe: :now or :renewal
24
23
  # Valid options: plan_code, quantity, unit_amount
25
24
  def change(timeframe, options = {})
26
- raise "Timeframe must be :full or :partial." unless timeframe == 'now' or timeframe == 'renewal'
25
+ raise "Timeframe must be :full or :renewal." unless timeframe == 'now' or timeframe == 'renewal'
27
26
  options[:timeframe] = timeframe
28
27
  path = "/accounts/#{CGI::escape(self.subscription_account_code || '')}/subscription.xml"
29
- connection.put(path,
30
- self.class.format.encode(options, :root => :subscription),
28
+ connection.put(path,
29
+ self.class.format.encode(options, :root => :subscription),
31
30
  self.class.headers)
32
31
  end
33
-
32
+
34
33
  def subscription_account_code
35
34
  acct_code = self.account_code if defined?(self.account_code) and !self.account_code.nil? and !self.account_code.blank?
36
35
  acct_code ||= account.account_code if defined?(account) and !account.nil?
@@ -1,6 +1,21 @@
1
1
  module Recurly
2
2
  class Transaction < RecurlyBase
3
3
  self.element_name = "transaction"
4
- self.prefix = "/"
4
+
5
+ def self.list(account_code, status = :all)
6
+ results = find(:all, :from => "/accounts/#{CGI::escape(account_code || '')}/transactions")
7
+
8
+ # filter by status
9
+ if status != :all
10
+ results = results.select{|t| t.status == status.to_s }
11
+ end
12
+
13
+ results
14
+ end
15
+
16
+ def self.lookup(account_code, id)
17
+ find(id, :params => { :account_code => account_code })
18
+ end
19
+
5
20
  end
6
21
  end
@@ -0,0 +1,3 @@
1
+ module Recurly #:nodoc
2
+ VERSION = "0.2.0"
3
+ end
data/lib/recurly.rb CHANGED
@@ -1,11 +1,34 @@
1
+ require 'rubygems'
1
2
  require "active_resource"
2
3
 
3
- require File.dirname(__FILE__) + '/recurly/base'
4
- require File.dirname(__FILE__) + '/recurly/account'
5
- require File.dirname(__FILE__) + '/recurly/billing_info'
6
- require File.dirname(__FILE__) + '/recurly/charge'
7
- require File.dirname(__FILE__) + '/recurly/credit'
8
- require File.dirname(__FILE__) + '/recurly/invoice'
9
- require File.dirname(__FILE__) + '/recurly/plan'
10
- require File.dirname(__FILE__) + '/recurly/subscription'
11
- require File.dirname(__FILE__) + '/recurly/transaction'
4
+ require 'cgi'
5
+
6
+ require 'recurly/version'
7
+ require 'recurly/formats/xml_with_pagination'
8
+
9
+ # configuration
10
+ module Recurly
11
+ class << self
12
+ attr_accessor :username, :password, :site
13
+
14
+ def configure
15
+ yield self
16
+
17
+ RecurlyBase.user = username
18
+ RecurlyBase.password = password
19
+ RecurlyBase.site = site || "https://app.recurly.com"
20
+
21
+ true
22
+ end
23
+ end
24
+ end
25
+
26
+ require 'recurly/base'
27
+ require 'recurly/account'
28
+ require 'recurly/billing_info'
29
+ require 'recurly/charge'
30
+ require 'recurly/credit'
31
+ require 'recurly/invoice'
32
+ require 'recurly/plan'
33
+ require 'recurly/subscription'
34
+ require 'recurly/transaction'
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+
3
+ module Recurly
4
+ describe Account do
5
+ timestamp = File.mtime(__FILE__).to_i
6
+
7
+ describe "#create" do
8
+ use_vcr_cassette "account/create/#{timestamp}"
9
+
10
+ before(:each) do
11
+ @account = Factory.create_account("account-create-#{timestamp}")
12
+ end
13
+
14
+ it "should have a created_at date" do
15
+ @account.created_at.should_not be_nil
16
+ end
17
+ end
18
+
19
+ describe "#find" do
20
+ use_vcr_cassette "account/find/#{timestamp}"
21
+ let(:orig){ Factory.create_account("account-get-#{timestamp}") }
22
+
23
+ before(:each) do
24
+ @account = Account.find(orig.account_code)
25
+ end
26
+
27
+ it "should return the account object" do
28
+ @account.should_not be_nil
29
+ end
30
+
31
+ describe "returned account" do
32
+ it "should have a created_at date" do
33
+ @account.created_at.should_not be_nil
34
+ end
35
+
36
+ it "should match the original account code" do
37
+ @account.account_code.should == orig.account_code
38
+ end
39
+
40
+ it "should match the original account email" do
41
+ @account.email.should == orig.email
42
+ end
43
+
44
+ it "should match the original first name" do
45
+ @account.first_name.should == orig.first_name
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "#update" do
51
+ around(:each){ |e| VCR.use_cassette("account/update/#{timestamp}", &e) }
52
+
53
+ let(:orig){ Factory.create_account("account-update-#{timestamp}") }
54
+
55
+ before(:each) do
56
+ # update account data
57
+ @account = Account.find(orig.account_code)
58
+ @account.last_name = "Update Test"
59
+ @account.company_name = "Recurly Ruby Gem -- Update"
60
+ @account.save!
61
+
62
+ # refetch account
63
+ @account = Account.find(orig.account_code)
64
+ end
65
+
66
+ it "should not have updated the email address" do
67
+ @account.email.should == orig.email
68
+ end
69
+
70
+ it "should have updated the last_name" do
71
+ @account.last_name.should == "Update Test"
72
+ end
73
+
74
+ it "should have updated the company_name" do
75
+ @account.company_name.should == "Recurly Ruby Gem -- Update"
76
+ end
77
+ end
78
+
79
+ describe "#close_account" do
80
+ use_vcr_cassette "account/close/#{timestamp}"
81
+ let(:account){ Factory.create_account("account-close-#{timestamp}") }
82
+
83
+ before(:each) do
84
+ account.close_account
85
+ end
86
+
87
+ it "should mark the account as closed" do
88
+ Account.find(account.account_code).state.should == "closed"
89
+ end
90
+ end
91
+ end
92
+ end