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 +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
|