saucy 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile CHANGED
@@ -13,6 +13,7 @@ gem "factory_girl", :require => false
13
13
  gem "sinatra"
14
14
  gem "sham_rack"
15
15
  gem "braintree"
16
+ gem "timecop"
16
17
 
17
18
  # used by the rails app in cucumber
18
19
  gem "cucumber-rails", :require => false
@@ -155,6 +155,7 @@ GEM
155
155
  rack (>= 1.0.0)
156
156
  thor (0.14.6)
157
157
  tilt (1.2.1)
158
+ timecop (0.3.5)
158
159
  treetop (1.4.9)
159
160
  polyglot (>= 0.3.1)
160
161
  tzinfo (0.3.23)
@@ -186,3 +187,4 @@ DEPENDENCIES
186
187
  sinatra
187
188
  sqlite3-ruby
188
189
  thin
190
+ timecop
data/README.md CHANGED
@@ -26,6 +26,8 @@ After you bundle, run the generators:
26
26
  rails generate saucy:install
27
27
  rails generate saucy:views
28
28
 
29
+ You will want to include the `ensure_active_account` `before_filter` in any controller actions that you want to protect if the user is using an past due paid account.
30
+
29
31
  Development environment
30
32
  -----------------------
31
33
 
@@ -1,6 +1,7 @@
1
1
  class AccountsController < ApplicationController
2
2
  before_filter :authenticate, :only => [:index, :edit, :update]
3
3
  before_filter :authorize_admin, :except => [:new, :create, :index]
4
+ before_filter :ensure_active_account, :only => [:edit, :update]
4
5
  layout Saucy::Layouts.to_proc
5
6
 
6
7
  def new
@@ -50,4 +51,8 @@ class AccountsController < ApplicationController
50
51
  def current_account
51
52
  Account.find_by_keyword!(params[:id])
52
53
  end
54
+
55
+ def current_account?
56
+ params[:id].present?
57
+ end
53
58
  end
@@ -1,4 +1,6 @@
1
1
  class BillingsController < ApplicationController
2
+ before_filter :authorize_admin, :except => [:show]
3
+
2
4
  def show
3
5
  end
4
6
 
@@ -31,6 +31,8 @@
31
31
  <%= render :partial => 'billings/form', :locals => { :form => form } %>
32
32
  <% end -%>
33
33
 
34
+ <p>By clicking <strong>Sign up</strong> you agree to our <%= link_to "Terms of Service", "/pages/terms" %>.</p>
35
+
34
36
  <%= form.buttons do %>
35
37
  <%= form.commit_button "Sign up" %>
36
38
  <% end %>
File without changes
@@ -1,3 +1,5 @@
1
- <div class="current_credit_card">
2
- We're currently charging the credit card ending in <%= current_account.credit_card.last_4 %>. <%= link_to "Change", edit_account_billing_path(current_account) %>
3
- </div>
1
+ <% if current_user.admin_of?(current_account) -%>
2
+ <div class="current_credit_card">
3
+ We're currently charging the credit card ending in <%= current_account.credit_card.last_4 %>. <%= link_to "Change", edit_account_billing_path(current_account) %>
4
+ </div>
5
+ <% end -%>
@@ -17,6 +17,7 @@ Feature: generate a saucy application and run rake
17
17
  gem "bourne"
18
18
  gem "shoulda"
19
19
  gem "launchy"
20
+ gem "timecop"
20
21
  """
21
22
  When I add the "saucy" gem from this project as a dependency
22
23
  And I successfully run "bundle install --local"
@@ -30,3 +30,35 @@ Feature: Manage Billing
30
30
  And I press "Update"
31
31
  Then I should see "updated successfully"
32
32
  Then I should see "card ending in 1111"
33
+
34
+ Scenario: Be forced to update the billing information on an account with a paid plan that is past due
35
+ Given a paid plan exists with a name of "Paid"
36
+ And the following account exists:
37
+ | name | keyword | plan | cardholder_name | billing_email | card_number | verification_code | expiration_month | expiration_year |
38
+ | Test | test | name: Paid | Joe Smith | jsmith@example.com | 4111111111115555 | 122 | 01 | 2015 |
39
+ And the "Test" account is past due
40
+ And I have signed in with "joe@example.com/test"
41
+ And "joe@example.com" is an admin of the "Test" account
42
+ When I go to the accounts page
43
+ And I follow "Settings" within ".account"
44
+ Then I should be on the billing page for the "Test" account
45
+ And I should see "There was an issue processing the credit card you have on file. Please update your credit card information."
46
+
47
+ Scenario: Be told to have an admin update the billing information on an account with a paid plan that is past due
48
+ Given a paid plan exists with a name of "Paid"
49
+ And the following account exists:
50
+ | name | keyword | plan | cardholder_name | billing_email | card_number | verification_code | expiration_month | expiration_year |
51
+ | Test | test | name: Paid | Joe Smith | jsmith@example.com | 4111111111115555 | 122 | 01 | 2015 |
52
+ And the "Test" account is past due
53
+ And the following projects exist:
54
+ | name | account |
55
+ | Project | name: Test |
56
+ | Project2 | name: Test |
57
+ And the following user exists:
58
+ | email |
59
+ | jsmith@example.com |
60
+ And "jsmith@example.com" is a member of the "Project" project
61
+ And I sign in as "jsmith@example.com"
62
+ When I go to the accounts page
63
+ Then I should see "There was an issue processing the credit card on file for this account. Please have an administrator on the account update the credit card information."
64
+ Then I should be on the billing page for the "Test" account
@@ -7,10 +7,12 @@ Feature: Sign up
7
7
  Given a plan exists with a name of "Free"
8
8
 
9
9
  Scenario: User sees signup terms
10
- When I go to the plans index
10
+ When I go to the plans page
11
11
  Then I should see "Free"
12
12
  And I should see "Upgrade or Downgrade Anytime"
13
13
  And I there should be a link to the help site
14
+ When I follow "Free"
15
+ Then I should see "By clicking Sign up you agree to our Terms of Service"
14
16
 
15
17
  Scenario: User signs up with invalid data
16
18
  When I go to the sign up page for the "Free" plan
@@ -5,3 +5,8 @@ end
5
5
  Given /^that the credit card "([^"]*)" should not be honored$/ do |number|
6
6
  FakeBraintree.failures[number] = { "message" => "Do Not Honor", "code" => "2000", "status" => "processor_declined" }
7
7
  end
8
+
9
+ Given /^the "([^"]*)" account is past due$/ do |account_name|
10
+ account = Account.find_by_name!(account_name)
11
+ account.update_attribute(:subscription_status, Braintree::Subscription::Status::PastDue)
12
+ end
@@ -1,10 +1,8 @@
1
1
  Given /^"([^"]*)" is a member of the "([^"]*)" project$/ do |email, project_name|
2
2
  user = User.find_by_email!(email)
3
3
  project = Project.find_by_name!(project_name)
4
- membership = Factory(:membership, :user => user,
5
- :account => project.account)
6
- Factory(:permission, :membership => membership,
7
- :project => project)
4
+ membership = Factory(:membership, :user => user, :account => project.account)
5
+ Factory(:permission, :membership => membership, :project => project)
8
6
  end
9
7
 
10
8
  Given /^"([^"]*)" is a member of the "([^"]*)" account/ do |email, account_name|
@@ -1,4 +1,4 @@
1
- require 'saucy/braintree'
1
+ require 'saucy/fake_braintree'
2
2
 
3
3
  After do |s|
4
4
  FakeBraintree.clear!
@@ -18,9 +18,12 @@ class CreateSaucyTables < ActiveRecord::Migration
18
18
  table.datetime :updated_at
19
19
  table.string :customer_token
20
20
  table.string :subscription_token
21
+ table.string :subscription_status
22
+ table.datetime :next_billing_date
21
23
  end
22
24
  add_index :accounts, :plan_id
23
25
  add_index :accounts, :keyword
26
+ add_index :accounts, :next_billing_date
24
27
 
25
28
  create_table :invitations do |table|
26
29
  table.string :email
@@ -1,4 +1,4 @@
1
- require 'saucy/braintree'
1
+ require 'saucy/fake_braintree'
2
2
 
3
3
  RSpec.configure do |config|
4
4
  config.after(:each) { FakeBraintree.clear! }
@@ -3,6 +3,9 @@ module Saucy
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ require "rubygems"
7
+ require "braintree"
8
+
6
9
  CUSTOMER_ATTRIBUTES = { :cardholder_name => :cardholder_name,
7
10
  :billing_email => :email,
8
11
  :card_number => :number,
@@ -103,6 +106,10 @@ module Saucy
103
106
  end
104
107
  end
105
108
 
109
+ def past_due?
110
+ subscription_status == Braintree::Subscription::Status::PastDue
111
+ end
112
+
106
113
  private
107
114
 
108
115
  def customer_attributes
@@ -175,10 +182,23 @@ module Saucy
175
182
  )
176
183
  if result.success?
177
184
  self.subscription_token = result.subscription.id
185
+ self.next_billing_date = result.subscription.next_billing_date
186
+ self.subscription_status = result.subscription.status
178
187
  else
179
188
  false
180
189
  end
181
190
  end
182
191
  end
192
+
193
+ module ClassMethods
194
+ def update_subscriptions!
195
+ recently_billed = where("next_billing_date <= '#{Time.now}'")
196
+ recently_billed.each do |account|
197
+ account.subscription_status = account.subscription.status
198
+ account.next_billing_date = account.subscription.next_billing_date
199
+ account.save!
200
+ end
201
+ end
202
+ end
183
203
  end
184
204
  end
@@ -37,6 +37,17 @@ module Saucy
37
37
  deny_access("You do not have permission for this project.")
38
38
  end
39
39
  end
40
+
41
+ def ensure_active_account
42
+ if current_account? && current_account.past_due?
43
+ if current_user.admin_of?(current_account)
44
+ flash[:alert] = t('.account_past_due.admin', :default => "There was an issue processing the credit card you have on file. Please update your credit card information.")
45
+ else
46
+ flash[:alert] = t('.account_past_due.user', :default => "There was an issue processing the credit card on file for this account. Please have an administrator on the account update the credit card information.")
47
+ end
48
+ redirect_to account_billing_path(current_account)
49
+ end
50
+ end
40
51
  end
41
52
  end
42
53
  end
@@ -0,0 +1,114 @@
1
+ require 'braintree'
2
+
3
+ Braintree::Configuration.environment = :production
4
+ Braintree::Configuration.merchant_id = "xxx"
5
+ Braintree::Configuration.public_key = "xxx"
6
+ Braintree::Configuration.private_key = "xxx"
7
+
8
+ require 'digest/md5'
9
+ require 'sham_rack'
10
+
11
+ class FakeBraintree
12
+ cattr_accessor :customers, :subscriptions, :failures, :transaction
13
+ @@customers = {}
14
+ @@subscriptions = {}
15
+ @@failures = {}
16
+ @@transaction = {}
17
+
18
+ def self.clear!
19
+ @@customers = {}
20
+ @@subscriptions = {}
21
+ @@failures = {}
22
+ @@transaction = {}
23
+ end
24
+
25
+ def self.failure?(card_number)
26
+ self.failures.include?(card_number)
27
+ end
28
+
29
+ def self.failure_response(card_number)
30
+ failure = self.failures[card_number]
31
+ failure["errors"] ||= { "errors" => [] }
32
+ { "message" => failure["message"], "verification" => { "status" => failure["status"], "processor_response_text" => failure["message"], "processor-response-code" => failure["code"], "gateway_rejection_reason" => "cvv", "cvv_response_code" => failure["code"] }, "errors" => failure["errors"], "params" => {}}
33
+ end
34
+ end
35
+
36
+ ShamRack.at("www.braintreegateway.com", 443).sinatra do
37
+ set :show_exceptions, false
38
+ set :dump_errors, true
39
+ set :raise_errors, true
40
+
41
+ post "/merchants/:merchant_id/customers" do
42
+ customer = Hash.from_xml(request.body).delete("customer")
43
+ if !FakeBraintree.failure?(customer["credit_card"]["number"])
44
+ customer["id"] ||= Digest::MD5.hexdigest("#{params[:merchant_id]}#{Time.now.to_f}")
45
+ customer["merchant-id"] = params[:merchant_id]
46
+ if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
47
+ customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
48
+ customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
49
+ credit_card = customer.delete("credit_card")
50
+ customer["credit_cards"] = [credit_card]
51
+ end
52
+ FakeBraintree.customers[customer["id"]] = customer
53
+ [201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
54
+ else
55
+ [422, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.failure_response(customer["credit_card"]["number"]).to_xml(:root => 'api_error_response'))]
56
+ end
57
+ end
58
+
59
+ get "/merchants/:merchant_id/customers/:id" do
60
+ customer = FakeBraintree.customers[params[:id]]
61
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
62
+ end
63
+
64
+ put "/merchants/:merchant_id/customers/:id" do
65
+ customer = Hash.from_xml(request.body).delete("customer")
66
+ customer["id"] = params[:id]
67
+ customer["merchant-id"] = params[:merchant_id]
68
+ if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
69
+ customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
70
+ customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
71
+ credit_card = customer.delete("credit_card")
72
+ customer["credit_cards"] = [credit_card]
73
+ end
74
+ FakeBraintree.customers[params["id"]] = customer
75
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
76
+ end
77
+
78
+ post "/merchants/:merchant_id/subscriptions" do
79
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<subscription>\n <plan-id type=\"integer\">2</plan-id>\n <payment-method-token>b22x</payment-method-token>\n</subscription>\n"
80
+ subscription = Hash.from_xml(request.body).delete("subscription")
81
+ subscription["id"] ||= Digest::MD5.hexdigest("#{subscription["payment_method_token"]}#{Time.now.to_f}")
82
+ subscription["transactions"] = []
83
+ subscription["add_ons"] = []
84
+ subscription["discounts"] = []
85
+ subscription["next_billing_date"] = 1.month.from_now
86
+ subscription["status"] = Braintree::Subscription::Status::Active
87
+ FakeBraintree.subscriptions[subscription["id"]] = subscription
88
+ [201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
89
+ end
90
+
91
+ get "/merchants/:merchant_id/subscriptions/:id" do
92
+ subscription = FakeBraintree.subscriptions[params[:id]]
93
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
94
+ end
95
+
96
+ put "/merchants/:merchant_id/subscriptions/:id" do
97
+ subscription = Hash.from_xml(request.body).delete("subscription")
98
+ subscription["transactions"] = []
99
+ subscription["add_ons"] = []
100
+ subscription["discounts"] = []
101
+ FakeBraintree.subscriptions[params["id"]] = subscription
102
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
103
+ end
104
+
105
+ post "/merchants/:merchant_id/transactions/advanced_search_ids" do
106
+ # "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<search>\n <created-at>\n <min type=\"datetime\">2011-01-10T14:14:26Z</min>\n </created-at>\n</search>\n"
107
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress("<search-results>\n <page-size type=\"integer\">50</page-size>\n <ids type=\"array\">\n <item>49sbx6</item>\n </ids>\n</search-results>\n")]
108
+ end
109
+
110
+ post "/merchants/:merchant_id/transactions/advanced_search" do
111
+ # "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<search>\n <ids type=\"array\">\n <item>49sbx6</item>\n </ids>\n <created-at>\n <min type=\"datetime\">2011-01-10T14:14:26Z</min>\n </created-at>\n</search>\n"
112
+ [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<credit-card-transactions type=\"collection\">\n <current-page-number type=\"integer\">1</current-page-number>\n <page-size type=\"integer\">50</page-size>\n <total-items type=\"integer\">1</total-items>\n <transaction>\n <id>49sbx6</id>\n <status>#{FakeBraintree.transaction[:status]}</status>\n <type>sale</type>\n <currency-iso-code>USD</currency-iso-code>\n <amount>50.00</amount>\n <merchant-account-id>Thoughtbot</merchant-account-id>\n <order-id></order-id>\n <created-at type=\"datetime\">2011-01-11T14:08:12Z</created-at>\n <updated-at type=\"datetime\">2011-01-11T14:08:12Z</updated-at>\n <customer>\n <id>108427</id>\n <first-name nil=\"true\"></first-name>\n <last-name nil=\"true\"></last-name>\n <company nil=\"true\"></company>\n <email>cpytel@thoughtbot.com</email>\n <website nil=\"true\"></website>\n <phone nil=\"true\"></phone>\n <fax nil=\"true\"></fax>\n </customer>\n <billing>\n <id nil=\"true\"></id>\n <first-name nil=\"true\"></first-name>\n <last-name nil=\"true\"></last-name>\n <company nil=\"true\"></company>\n <street-address nil=\"true\"></street-address>\n <extended-address nil=\"true\"></extended-address>\n <locality nil=\"true\"></locality>\n <region nil=\"true\"></region>\n <postal-code nil=\"true\"></postal-code>\n <country-name nil=\"true\"></country-name>\n <country-code-alpha2 nil=\"true\"></country-code-alpha2>\n <country-code-alpha3 nil=\"true\"></country-code-alpha3>\n <country-code-numeric nil=\"true\"></country-code-numeric>\n </billing>\n <refund-id nil=\"true\"></refund-id>\n <refund-ids type=\"array\"/>\n <refunded-transaction-id nil=\"true\"></refunded-transaction-id>\n <settlement-batch-id nil=\"true\"></settlement-batch-id>\n <shipping>\n <id nil=\"true\"></id>\n <first-name nil=\"true\"></first-name>\n <last-name nil=\"true\"></last-name>\n <company nil=\"true\"></company>\n <street-address nil=\"true\"></street-address>\n <extended-address nil=\"true\"></extended-address>\n <locality nil=\"true\"></locality>\n <region nil=\"true\"></region>\n <postal-code nil=\"true\"></postal-code>\n <country-name nil=\"true\"></country-name>\n <country-code-alpha2 nil=\"true\"></country-code-alpha2>\n <country-code-alpha3 nil=\"true\"></country-code-alpha3>\n <country-code-numeric nil=\"true\"></country-code-numeric>\n </shipping>\n <custom-fields>\n </custom-fields>\n <avs-error-response-code nil=\"true\"></avs-error-response-code>\n <avs-postal-code-response-code>I</avs-postal-code-response-code>\n <avs-street-address-response-code>I</avs-street-address-response-code>\n <cvv-response-code>I</cvv-response-code>\n <gateway-rejection-reason nil=\"true\"></gateway-rejection-reason>\n <processor-authorization-code>ZKB4VJ</processor-authorization-code>\n <processor-response-code>1000</processor-response-code>\n <processor-response-text>Approved</processor-response-text>\n <credit-card>\n <token>8yq7</token>\n <bin>411111</bin>\n <last-4>1111</last-4>\n <card-type>Visa</card-type>\n <expiration-month>02</expiration-month>\n <expiration-year>2013</expiration-year>\n <customer-location>US</customer-location>\n <cardholder-name>Chad Lee Pytel</cardholder-name>\n </credit-card>\n <status-history type=\"array\">\n <status-event>\n <timestamp type=\"datetime\">2011-01-11T14:08:12Z</timestamp>\n <status>authorized</status>\n <amount>50.00</amount>\n <user>copycopter</user>\n <transaction-source>CP</transaction-source>\n </status-event>\n <status-event>\n <timestamp type=\"datetime\">2011-01-11T14:08:12Z</timestamp>\n <status>#{FakeBraintree.transaction[:status]}</status>\n <amount>50.00</amount>\n <user>copycopter</user>\n <transaction-source>CP</transaction-source>\n </status-event>\n </status-history>\n <subscription-id>#{FakeBraintree.transaction[:subscription_id]}</subscription-id>\n <add-ons type=\"array\"/>\n <discounts type=\"array\"/>\n </transaction>\n</credit-card-transactions>\n")]
113
+ end
114
+ end
@@ -3,7 +3,8 @@ module Saucy
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- before_filter :authorize_admin
6
+ before_filter :authorize_admin, :except => [:show]
7
+ before_filter :ensure_active_account, :only => [:show, :destroy, :index]
7
8
  layout Saucy::Layouts.to_proc
8
9
  end
9
10
 
@@ -146,6 +146,18 @@ describe Account, "with a paid plan" do
146
146
  subject.subscription.should_not be_nil
147
147
  end
148
148
 
149
+ it "has a next_billing_date" do
150
+ subject.next_billing_date.should_not be_nil
151
+ end
152
+
153
+ it "has an active subscription status" do
154
+ subject.subscription_status.should == Braintree::Subscription::Status::Active
155
+ end
156
+
157
+ it "is not past due" do
158
+ subject.past_due?.should_not be
159
+ end
160
+
149
161
  it "creates a braintree customer, credit card, and subscription" do
150
162
  FakeBraintree.customers[subject.customer_token].should_not be_nil
151
163
  FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
@@ -247,3 +259,41 @@ describe Account, "with a plan and limits, and other plans" do
247
259
  subject.can_change_plan_to?(@cannot_switch).should_not be
248
260
  end
249
261
  end
262
+
263
+ describe Account, "with a paid subscription" do
264
+ subject do
265
+ Factory(:account,
266
+ :cardholder_name => "Ralph Robot",
267
+ :billing_email => "ralph@example.com",
268
+ :card_number => "4111111111111111",
269
+ :verification_code => "123",
270
+ :expiration_month => 5,
271
+ :expiration_year => 2012,
272
+ :plan => Factory(:paid_plan))
273
+ end
274
+
275
+ it "gets marked as past due and updates its next_billing_date when subscriptions are updated and it has been rejected by the gateway" do
276
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
277
+ subscription["status"] = Braintree::Subscription::Status::PastDue
278
+ subscription["next_billing_date"] = 2.months.from_now
279
+
280
+ Timecop.travel(subject.next_billing_date + 1.day) do
281
+ Account.update_subscriptions!
282
+ subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue
283
+ subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
284
+ subject.past_due?.should be
285
+ end
286
+ end
287
+
288
+ it "gets marked as not past due and updates its next_billing_date when the subscription is active after its billing date" do
289
+ subscription = FakeBraintree.subscriptions[subject.subscription_token]
290
+ subscription["status"] = Braintree::Subscription::Status::Active
291
+ subscription["next_billing_date"] = 2.months.from_now
292
+
293
+ Timecop.travel(subject.next_billing_date + 1.day) do
294
+ Account.update_subscriptions!
295
+ subject.reload.subscription_status.should == Braintree::Subscription::Status::Active
296
+ subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
297
+ end
298
+ end
299
+ end
@@ -9,6 +9,7 @@ end
9
9
 
10
10
  require 'rspec/rails'
11
11
  require 'shoulda'
12
+ require 'timecop'
12
13
 
13
14
  # Requires supporting ruby files with custom matchers and macros, etc,
14
15
  # in spec/support/ and its subdirectories.
@@ -1,4 +1,4 @@
1
- require 'saucy/braintree'
1
+ require 'saucy/fake_braintree'
2
2
 
3
3
  RSpec.configure do |config|
4
4
  config.after(:each) { FakeBraintree.clear! }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saucy
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 2
10
- version: 0.2.2
9
+ - 3
10
+ version: 0.2.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - thoughtbot, inc.
@@ -17,7 +17,7 @@ autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
19
 
20
- date: 2011-01-10 00:00:00 -05:00
20
+ date: 2011-01-12 00:00:00 -05:00
21
21
  default_executable:
22
22
  dependencies:
23
23
  - !ruby/object:Gem::Dependency
@@ -149,6 +149,7 @@ files:
149
149
  - app/views/accounts/new.html.erb
150
150
  - app/views/billings/_form.html.erb
151
151
  - app/views/billings/edit.html.erb
152
+ - app/views/billings/past_due.html.erb
152
153
  - app/views/billings/show.html.erb
153
154
  - app/views/invitation_mailer/invitation.text.erb
154
155
  - app/views/invitations/new.html.erb
@@ -190,9 +191,9 @@ files:
190
191
  - lib/generators/saucy/views/views_generator.rb
191
192
  - lib/saucy/account.rb
192
193
  - lib/saucy/account_authorization.rb
193
- - lib/saucy/braintree.rb
194
194
  - lib/saucy/configuration.rb
195
195
  - lib/saucy/engine.rb
196
+ - lib/saucy/fake_braintree.rb
196
197
  - lib/saucy/layouts.rb
197
198
  - lib/saucy/plan.rb
198
199
  - lib/saucy/project.rb
@@ -1,100 +0,0 @@
1
- require 'braintree'
2
-
3
- Braintree::Configuration.environment = :production
4
- Braintree::Configuration.merchant_id = "xxx"
5
- Braintree::Configuration.public_key = "xxx"
6
- Braintree::Configuration.private_key = "xxx"
7
-
8
- require 'digest/md5'
9
- require 'sham_rack'
10
-
11
- class FakeBraintree
12
- cattr_accessor :customers, :subscriptions, :failures
13
- @@customers = {}
14
- @@subscriptions = {}
15
- @@failures = {}
16
-
17
- def self.clear!
18
- @@customers = {}
19
- @@subscriptions = {}
20
- @@failures = {}
21
- end
22
-
23
- def self.failure?(card_number)
24
- self.failures.include?(card_number)
25
- end
26
-
27
- def self.failure_response(card_number)
28
- failure = self.failures[card_number]
29
- failure["errors"] ||= { "errors" => [] }
30
- { "message" => failure["message"], "verification" => { "status" => failure["status"], "processor_response_text" => failure["message"], "processor-response-code" => failure["code"], "gateway_rejection_reason" => "cvv", "cvv_response_code" => failure["code"] }, "errors" => failure["errors"], "params" => {}}
31
- end
32
- end
33
-
34
- ShamRack.at("www.braintreegateway.com", 443).sinatra do
35
- set :show_exceptions, false
36
- set :dump_errors, true
37
- set :raise_errors, true
38
-
39
- post "/merchants/:merchant_id/customers" do
40
- customer = Hash.from_xml(request.body).delete("customer")
41
- if !FakeBraintree.failure?(customer["credit_card"]["number"])
42
- customer["id"] ||= Digest::MD5.hexdigest("#{params[:merchant_id]}#{Time.now.to_f}")
43
- customer["merchant-id"] = params[:merchant_id]
44
- if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
45
- customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
46
- customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
47
- credit_card = customer.delete("credit_card")
48
- customer["credit_cards"] = [credit_card]
49
- end
50
- FakeBraintree.customers[customer["id"]] = customer
51
- [201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
52
- else
53
- [422, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.failure_response(customer["credit_card"]["number"]).to_xml(:root => 'api_error_response'))]
54
- end
55
- end
56
-
57
- get "/merchants/:merchant_id/customers/:id" do
58
- customer = FakeBraintree.customers[params[:id]]
59
- [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
60
- end
61
-
62
- put "/merchants/:merchant_id/customers/:id" do
63
- customer = Hash.from_xml(request.body).delete("customer")
64
- customer["id"] = params[:id]
65
- customer["merchant-id"] = params[:merchant_id]
66
- if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
67
- customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
68
- customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
69
- credit_card = customer.delete("credit_card")
70
- customer["credit_cards"] = [credit_card]
71
- end
72
- FakeBraintree.customers[params["id"]] = customer
73
- [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
74
- end
75
-
76
- post "/merchants/:merchant_id/subscriptions" do
77
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<subscription>\n <plan-id type=\"integer\">2</plan-id>\n <payment-method-token>b22x</payment-method-token>\n</subscription>\n"
78
- subscription = Hash.from_xml(request.body).delete("subscription")
79
- subscription["id"] ||= Digest::MD5.hexdigest("#{subscription["payment_method_token"]}#{Time.now.to_f}")
80
- subscription["transactions"] = []
81
- subscription["add_ons"] = []
82
- subscription["discounts"] = []
83
- FakeBraintree.subscriptions[subscription["id"]] = subscription
84
- [201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
85
- end
86
-
87
- get "/merchants/:merchant_id/subscriptions/:id" do
88
- subscription = FakeBraintree.subscriptions[params[:id]]
89
- [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
90
- end
91
-
92
- put "/merchants/:merchant_id/subscriptions/:id" do
93
- subscription = Hash.from_xml(request.body).delete("subscription")
94
- subscription["transactions"] = []
95
- subscription["add_ons"] = []
96
- subscription["discounts"] = []
97
- FakeBraintree.subscriptions[params["id"]] = subscription
98
- [200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
99
- end
100
- end