saucy 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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