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 +1 -0
- data/Gemfile.lock +2 -0
- data/README.md +2 -0
- data/app/controllers/accounts_controller.rb +5 -0
- data/app/controllers/billings_controller.rb +2 -0
- data/app/views/accounts/new.html.erb +2 -0
- data/app/views/billings/past_due.html.erb +0 -0
- data/app/views/billings/show.html.erb +5 -3
- data/features/run_features.feature +1 -0
- data/lib/generators/saucy/features/templates/features/manage_billing.feature +32 -0
- data/lib/generators/saucy/features/templates/features/sign_up.feature +3 -1
- data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +5 -0
- data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +2 -4
- data/lib/generators/saucy/features/templates/support/braintree.rb +1 -1
- data/lib/generators/saucy/install/templates/create_saucy_tables.rb +3 -0
- data/lib/generators/saucy/specs/templates/support/braintree.rb +1 -1
- data/lib/saucy/account.rb +20 -0
- data/lib/saucy/account_authorization.rb +11 -0
- data/lib/saucy/fake_braintree.rb +114 -0
- data/lib/saucy/projects_controller.rb +2 -1
- data/spec/models/account_spec.rb +50 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/braintree.rb +1 -1
- metadata +6 -5
- data/lib/saucy/braintree.rb +0 -100
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
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
|
@@ -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
|
-
|
2
|
-
|
3
|
-
|
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 -%>
|
@@ -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
|
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
|
5
|
-
|
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|
|
@@ -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
|
data/lib/saucy/account.rb
CHANGED
@@ -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
|
data/spec/models/account_spec.rb
CHANGED
@@ -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
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/braintree.rb
CHANGED
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:
|
4
|
+
hash: 17
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.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-
|
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
|
data/lib/saucy/braintree.rb
DELETED
@@ -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
|