saucy 0.1.18 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/Gemfile +4 -1
  2. data/Gemfile.lock +12 -2
  3. data/README.md +75 -0
  4. data/app/controllers/accounts_controller.rb +2 -0
  5. data/app/controllers/billings_controller.rb +22 -0
  6. data/app/controllers/plans_controller.rb +12 -0
  7. data/app/models/limit.rb +5 -0
  8. data/app/models/signup.rb +20 -6
  9. data/app/views/accounts/_tab_bar.html.erb +3 -1
  10. data/app/views/accounts/edit.html.erb +6 -0
  11. data/app/views/accounts/new.html.erb +14 -4
  12. data/app/views/billings/_form.html.erb +8 -0
  13. data/app/views/billings/edit.html.erb +11 -0
  14. data/app/views/billings/show.html.erb +3 -0
  15. data/app/views/plans/_plan.html.erb +2 -0
  16. data/app/views/plans/edit.html.erb +23 -0
  17. data/config/routes.rb +2 -0
  18. data/features/run_features.feature +4 -0
  19. data/lib/generators/saucy/features/features_generator.rb +4 -0
  20. data/lib/generators/saucy/features/templates/factories.rb +14 -2
  21. data/lib/generators/saucy/features/templates/features/manage_account.feature +0 -2
  22. data/lib/generators/saucy/features/templates/features/manage_billing.feature +119 -0
  23. data/lib/generators/saucy/features/templates/features/sign_up.feature +2 -0
  24. data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +76 -0
  25. data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +7 -0
  26. data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +22 -0
  27. data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +9 -0
  28. data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
  29. data/lib/generators/saucy/install/templates/create_saucy_tables.rb +14 -1
  30. data/lib/generators/saucy/specs/specs_generator.rb +20 -0
  31. data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
  32. data/lib/saucy/account.rb +134 -1
  33. data/lib/saucy/braintree.rb +100 -0
  34. data/lib/saucy/plan.rb +23 -0
  35. data/spec/controllers/accounts_controller_spec.rb +2 -2
  36. data/spec/environment.rb +4 -1
  37. data/spec/models/account_spec.rb +182 -0
  38. data/spec/models/limit_spec.rb +9 -0
  39. data/spec/models/plan_spec.rb +54 -0
  40. data/spec/models/signup_spec.rb +9 -0
  41. data/spec/support/braintree.rb +5 -0
  42. metadata +74 -8
  43. data/README +0 -38
@@ -0,0 +1,100 @@
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
data/lib/saucy/plan.rb CHANGED
@@ -4,8 +4,31 @@ module Saucy
4
4
 
5
5
  included do
6
6
  has_many :accounts
7
+ has_many :limits
7
8
 
8
9
  validates_presence_of :name
9
10
  end
11
+
12
+ module InstanceMethods
13
+ def free?
14
+ price.zero?
15
+ end
16
+
17
+ def billed?
18
+ !free?
19
+ end
20
+
21
+ def can_add_more?(limit, amount)
22
+ limits.where(:name => limit, :value_type => :number).first.value > amount
23
+ end
24
+
25
+ def allows?(limit)
26
+ limits.where(:name => limit, :value_type => :boolean).first.value != 0
27
+ end
28
+
29
+ def limit(limit_name)
30
+ limits.where(:name => limit_name).first
31
+ end
32
+ end
10
33
  end
11
34
  end
@@ -32,7 +32,7 @@ end
32
32
 
33
33
  describe AccountsController, "successful create for a confirmed user" do
34
34
  let(:user) { Factory.stub(:user) }
35
- let(:signup) { stub('signup', :user => user, :user= => nil) }
35
+ let(:signup) { stub('signup', :user => user, :user= => nil, :plan= => plan) }
36
36
  let(:signup_attributes) { "attributes" }
37
37
  let(:plan) { Factory(:plan) }
38
38
 
@@ -62,7 +62,7 @@ describe AccountsController, "successful create for a confirmed user" do
62
62
  end
63
63
 
64
64
  describe AccountsController, "failed create" do
65
- let(:signup) { stub('signup', :user= => nil) }
65
+ let(:signup) { stub('signup', :user= => nil, :plan= => plan) }
66
66
  let(:signup_attributes) { "attributes" }
67
67
  let(:plan) { Factory(:plan) }
68
68
 
data/spec/environment.rb CHANGED
@@ -23,6 +23,10 @@ class User < ActiveRecord::Base
23
23
  include Saucy::User
24
24
  end
25
25
 
26
+ class Plan < ActiveRecord::Base
27
+ include Saucy::Plan
28
+ end
29
+
26
30
  module Testapp
27
31
  class Application < Rails::Application
28
32
  config.action_mailer.default_url_options = { :host => 'localhost' }
@@ -89,4 +93,3 @@ end
89
93
 
90
94
  ClearanceCreateUsers.suppress_messages { ClearanceCreateUsers.migrate(:up) }
91
95
  CreateSaucyTables.suppress_messages { CreateSaucyTables.migrate(:up) }
92
-
@@ -6,11 +6,13 @@ describe Account do
6
6
  it { should have_many(:memberships) }
7
7
  it { should have_many(:users).through(:memberships) }
8
8
  it { should have_many(:projects) }
9
+ it { should belong_to(:plan) }
9
10
 
10
11
  it { should validate_uniqueness_of(:name) }
11
12
  it { should validate_uniqueness_of(:keyword) }
12
13
  it { should validate_presence_of( :name) }
13
14
  it { should validate_presence_of(:keyword) }
15
+ it { should validate_presence_of(:plan_id) }
14
16
 
15
17
  it { should_not allow_mass_assignment_of(:id) }
16
18
  it { should_not allow_mass_assignment_of(:updated_at) }
@@ -63,5 +65,185 @@ describe Account do
63
65
 
64
66
  result.should == expected
65
67
  end
68
+
69
+ it "manifests braintree processor_declined errors as errors on number and doesn't save" do
70
+ FakeBraintree.failures["4111111111111112"] = { "message" => "Do Not Honor", "code" => "2000", "status" => "processor_declined" }
71
+ account = Factory.build(:account,
72
+ :cardholder_name => "Ralph Robot",
73
+ :billing_email => "ralph@example.com",
74
+ :card_number => "4111111111111112",
75
+ :expiration_month => 5,
76
+ :expiration_year => 2012,
77
+ :plan => Factory(:paid_plan))
78
+ account.save.should_not be
79
+ FakeBraintree.customers.should be_empty
80
+ account.persisted?.should_not be
81
+ account.errors[:card_number].any? { |e| e =~ /denied/ }.should be
82
+ end
83
+
84
+ it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
85
+ FakeBraintree.failures["4111111111111112"] = { "message" => "Gateway Rejected: cvv", "code" => "N", "status" => "gateway_rejected" }
86
+ account = Factory.build(:account,
87
+ :cardholder_name => "Ralph Robot",
88
+ :billing_email => "ralph@example.com",
89
+ :card_number => "4111111111111112",
90
+ :expiration_month => 5,
91
+ :expiration_year => 2012,
92
+ :verification_code => 200,
93
+ :plan => Factory(:paid_plan))
94
+ account.save.should_not be
95
+ FakeBraintree.customers.should be_empty
96
+ account.persisted?.should_not be
97
+ account.errors[:verification_code].any? { |e| e =~ /did not match/ }.should be
98
+ end
99
+
100
+ it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
101
+ FakeBraintree.failures["4111111111111111"] = { "message" => "Credit card number is invalid.", "errors" => { "customer" => { "errors" => [], "credit-card" => { "errors" => [{ "message" => "Credit card number is invalid.", "code" => 81715, "attribute" => :number }] }}}}
102
+ account = Factory.build(:account,
103
+ :cardholder_name => "Ralph Robot",
104
+ :billing_email => "ralph@example.com",
105
+ :card_number => "4111111111111111",
106
+ :expiration_month => 5,
107
+ :expiration_year => 2012,
108
+ :verification_code => 123,
109
+ :plan => Factory(:paid_plan))
110
+ account.save.should_not be
111
+ FakeBraintree.customers.should be_empty
112
+ account.persisted?.should_not be
113
+ account.errors[:card_number].any? { |e| e =~ /is invalid/ }.should be
114
+ end
115
+ end
116
+
117
+ describe Account, "with a paid plan" do
118
+ subject do
119
+ Factory(:account,
120
+ :cardholder_name => "Ralph Robot",
121
+ :billing_email => "ralph@example.com",
122
+ :card_number => "4111111111111111",
123
+ :verification_code => "123",
124
+ :expiration_month => 5,
125
+ :expiration_year => 2012,
126
+ :plan => Factory(:paid_plan))
127
+ end
128
+
129
+ it "has a customer_token" do
130
+ subject.customer_token.should_not be_nil
131
+ end
132
+
133
+ it "has a subscription_token" do
134
+ subject.subscription_token.should_not be_nil
135
+ end
136
+
137
+ it "has a customer" do
138
+ subject.customer.should_not be_nil
139
+ end
140
+
141
+ it "has a credit card" do
142
+ subject.credit_card.should_not be_nil
143
+ end
144
+
145
+ it "has a subscription" do
146
+ subject.subscription.should_not be_nil
147
+ end
148
+
149
+ it "creates a braintree customer, credit card, and subscription" do
150
+ FakeBraintree.customers[subject.customer_token].should_not be_nil
151
+ FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
152
+ FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
153
+ end
154
+
155
+ it "changes the subscription when the plan is changed" do
156
+ new_plan = Factory(:paid_plan, :name => "New Plan")
157
+ subject.save_braintree!(:plan_id => new_plan.id)
158
+ FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
159
+ end
160
+
161
+ it "updates the customer and credit card information when changed" do
162
+ subject.save_braintree!(:billing_email => "jrobot@example.com",
163
+ :cardholder_name => "Jim Robot",
164
+ :card_number => "4111111111111115",
165
+ :verification_code => "123",
166
+ :expiration_month => 5,
167
+ :expiration_year => 2013)
168
+ subject.customer.email.should == "jrobot@example.com"
169
+ subject.credit_card.cardholder_name.should == "Jim Robot"
170
+ end
66
171
  end
67
172
 
173
+ describe Account, "with a free plan" do
174
+ subject do
175
+ Factory(:account, :plan => Factory(:plan))
176
+ end
177
+
178
+ it "has a customer_token" do
179
+ subject.customer_token.should_not be_nil
180
+ end
181
+
182
+ it "has a customer" do
183
+ subject.customer.should_not be_nil
184
+ end
185
+
186
+ it "doesn't have a credit_card" do
187
+ subject.credit_card.should be_nil
188
+ end
189
+
190
+ it "doesn't have a subscription_token" do
191
+ subject.subscription_token.should be_nil
192
+ end
193
+
194
+ it "doesn't have a subscription" do
195
+ subject.subscription.should be_nil
196
+ end
197
+
198
+ it "creates a braintree customer" do
199
+ FakeBraintree.customers[subject.customer_token].should_not be_nil
200
+ end
201
+
202
+ it "doesn't create a credit card, and subscription" do
203
+ FakeBraintree.customers[subject.customer_token]["credit_cards"].should be_nil
204
+ FakeBraintree.subscriptions[subject.subscription_token].should be_nil
205
+ end
206
+
207
+ it "creates a credit card, and subscription when the plan is changed and billing info is supplied" do
208
+ new_plan = Factory(:paid_plan, :name => "New Plan")
209
+ subject.save_braintree!(:plan_id => new_plan.id,
210
+ :cardholder_name => "Ralph Robot",
211
+ :billing_email => "ralph@example.com",
212
+ :card_number => "4111111111111111",
213
+ :verification_code => "123",
214
+ :expiration_month => 5,
215
+ :expiration_year => 2012)
216
+
217
+ FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil
218
+ FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil
219
+ FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id
220
+ subject.credit_card.should_not be_nil
221
+ subject.subscription.should_not be_nil
222
+ end
223
+ end
224
+
225
+ describe Account, "with a plan and limits, and other plans" do
226
+ subject { Factory(:account) }
227
+
228
+ before do
229
+ Factory(:limit, :name => "users", :value => 1, :plan => subject.plan)
230
+ Factory(:limit, :name => "projects", :value => 1, :plan => subject.plan)
231
+ Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => subject.plan)
232
+ @can_switch = Factory(:plan)
233
+ Factory(:limit, :name => "users", :value => 1, :plan => @can_switch)
234
+ Factory(:limit, :name => "projects", :value => 1, :plan => @can_switch)
235
+ Factory(:limit, :name => "ssl", :value => 0, :value_type => :boolean, :plan => @can_switch)
236
+ @cannot_switch = Factory(:plan)
237
+ Factory(:limit, :name => "users", :value => 0, :plan => @cannot_switch)
238
+ Factory(:limit, :name => "projects", :value => 0, :plan => @cannot_switch)
239
+ Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => @cannot_switch)
240
+
241
+ Factory(:membership, :account => subject)
242
+ Factory(:project, :account => subject)
243
+ end
244
+
245
+ it "indicates whether the account can switch to another plan" do
246
+ subject.can_change_plan_to?(@can_switch).should be
247
+ subject.can_change_plan_to?(@cannot_switch).should_not be
248
+ end
249
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Limit do
4
+ subject { Factory(:limit) }
5
+
6
+ it { should belong_to(:plan) }
7
+ it { should validate_presence_of(:name) }
8
+ it { should validate_presence_of(:value) }
9
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Plan do
4
+ subject { Factory(:plan) }
5
+
6
+ it { should have_many(:limits) }
7
+ it { should have_many(:accounts) }
8
+ it { should validate_presence_of(:name) }
9
+ end
10
+
11
+ describe Plan, "free" do
12
+ subject { Factory(:plan) }
13
+
14
+ it "is free" do
15
+ subject.free?.should be
16
+ end
17
+
18
+ it "is not billed" do
19
+ subject.billed?.should_not be
20
+ end
21
+ end
22
+
23
+ describe Plan, "paid" do
24
+ subject { Factory(:paid_plan) }
25
+
26
+ it "is not free" do
27
+ subject.free?.should_not be
28
+ end
29
+
30
+ it "is billed" do
31
+ subject.billed?.should be
32
+ end
33
+ end
34
+
35
+ describe Plan, "with limits" do
36
+ subject { Factory(:plan) }
37
+
38
+ before do
39
+ Factory(:limit, :name => "users", :value => 1, :plan => subject)
40
+ Factory(:limit, :name => "ssl", :value => 0, :value_type => :boolean, :plan => subject)
41
+ Factory(:limit, :name => "lighthouse", :value => 1, :value_type => :boolean, :plan => subject)
42
+ end
43
+
44
+ it "indicates whether or not more users can be created" do
45
+ subject.can_add_more?(:users, 0).should be
46
+ subject.can_add_more?(:users, 1).should_not be
47
+ subject.can_add_more?(:users, 2).should_not be
48
+ end
49
+
50
+ it "indicates whether a plan can do something or not" do
51
+ subject.allows?(:ssl).should_not be
52
+ subject.allows?(:lighthouse).should be
53
+ end
54
+ end
@@ -173,3 +173,12 @@ describe Signup, "invalid with an existing user and correct password" do
173
173
  user.reload.accounts.should_not include(subject.account)
174
174
  end
175
175
  end
176
+
177
+ describe Signup, "with an account that doesn't save" do
178
+ subject { Factory.build(:signup) }
179
+
180
+ it "doesn't raise the transaction and returns false" do
181
+ Account.any_instance.stubs(:save!).raises(ActiveRecord::RecordNotSaved)
182
+ subject.save.should_not be
183
+ end
184
+ end