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
@@ -24,5 +24,3 @@ Feature: Manage account
24
24
  Then I should see "Projection"
25
25
  When I follow "Users"
26
26
  Then I should see "captain@awesome.com"
27
- When I follow "Billing"
28
- Then I should see "Billing"
@@ -0,0 +1,119 @@
1
+ Feature: Manage Projects
2
+ As a admin user
3
+ I want to be able to manage projects
4
+ In order to have a project for each of my software applications
5
+
6
+ Scenario: Update the billing information on an account with a paid plan
7
+ Given a paid plan exists with a name of "Paid"
8
+ And the following account exists:
9
+ | name | keyword | plan | cardholder_name | billing_email | card_number | verification_code | expiration_month | expiration_year |
10
+ | Test | test | name: Paid | Joe Smith | jsmith@example.com | 4111111111115555 | 122 | 01 | 2015 |
11
+ And I have signed in with "joe@example.com/test"
12
+ And "joe@example.com" is an admin of the "Test" account
13
+ When I go to the billing page for the "Test" account
14
+ Then I should see "card ending in 5555"
15
+ And I follow "Change" within ".current_credit_card"
16
+
17
+ Then the "Cardholder name" field should contain "Joe Smith"
18
+ And the "Billing email" field should contain "jsmith@example.com"
19
+ And the "Card number" field should be empty
20
+ And the "Verification code" field should be empty
21
+ And the "Expiration month" field should contain "01"
22
+ And the "Expiration year" field should contain "2015"
23
+
24
+ And I fill in "Cardholder name" with "Ralph Robot"
25
+ And I fill in "Billing email" with "accounting@example.com"
26
+ And I fill in "Card number" with "4111111111111111"
27
+ And I fill in "Verification code" with "123"
28
+ And I select "March" from "Expiration month"
29
+ And I select "2020" from "Expiration year"
30
+ And I press "Update"
31
+ Then I should see "updated successfully"
32
+ Then I should see "card ending in 1111"
33
+
34
+ Scenario: Change the plan to another paid plan on an account with a paid plan
35
+ Given a paid plan exists with a name of "Basic"
36
+ And a paid plan exists with a name of "Premium"
37
+ And the following account exists:
38
+ | name | keyword | plan | cardholder_name | billing_email | card_number | verification_code | expiration_month | expiration_year |
39
+ | Test | test | name: Basic | Joe Smith | jsmith@example.com | 4111111111115555 | 122 | 01 | 2015 |
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 settings page for the "Test" account
43
+ Then I should see "Your Plan"
44
+ And I should not see "Billing Information"
45
+ And I should see "Basic"
46
+ And I follow "Upgrade"
47
+ Then I should see "Upgrade Your Plan"
48
+ When I choose the "Premium" plan
49
+ And I press "Upgrade"
50
+ Then I should see "Plan changed successfully"
51
+ Then I should see "Premium"
52
+
53
+ Scenario: Change the plan from free to paid
54
+ Given a plan exists with a name of "Free"
55
+ And a paid plan exists with a name of "Basic"
56
+ And the following account exists:
57
+ | name | keyword | plan |
58
+ | Test | test | name: Free |
59
+ And I have signed in with "joe@example.com/test"
60
+ And "joe@example.com" is an admin of the "Test" account
61
+ When I go to the settings page for the "Test" account
62
+ Then I should not see "Billing" within ".tabs"
63
+ Then I should see "Your Plan"
64
+ And I follow "Upgrade"
65
+ Then I should see "Upgrade Your Plan"
66
+ When I choose the "Basic" plan
67
+ Then I should see "Billing Information"
68
+ And I fill in "Cardholder name" with "Ralph Robot"
69
+ And I fill in "Billing email" with "accounting@example.com"
70
+ And I fill in "Card number" with "4111111111111111"
71
+ And I fill in "Verification code" with "123"
72
+ And I select "March" from "Expiration month"
73
+ And I select "2020" from "Expiration year"
74
+ When I press "Upgrade"
75
+ Then I should see "Plan changed successfully"
76
+ And I should see "Basic"
77
+ And I follow "Billing"
78
+ Then I should see "card ending in 1111"
79
+
80
+ Scenario: Change the plan to a free on an account with a paid plan
81
+ Given a paid plan exists with a name of "Basic"
82
+ And a plan exists with a name of "Free"
83
+ And the following account exists:
84
+ | name | keyword | plan | cardholder_name | billing_email | card_number | verification_code | expiration_month | expiration_year |
85
+ | Test | test | name: Basic | Joe Smith | jsmith@example.com | 4111111111115555 | 122 | 01 | 2015 |
86
+ And I have signed in with "joe@example.com/test"
87
+ And "joe@example.com" is an admin of the "Test" account
88
+ When I go to the settings page for the "Test" account
89
+ Then I should see "Your Plan"
90
+ And I should not see "Billing Information"
91
+ And I should see "Basic"
92
+ And I follow "Upgrade"
93
+ Then I should see "Upgrade Your Plan"
94
+ When I choose the "Free" plan
95
+ And I press "Upgrade"
96
+ Then I should see "Plan changed successfully"
97
+ Then I should see "Free"
98
+
99
+ Scenario: Attempting to downgrade when beyond the limits
100
+ Given a paid plan exists with a name of "Basic"
101
+ And a plan exists with a name of "Free"
102
+ And the following account exists:
103
+ | name | keyword | plan | cardholder_name | billing_email | card_number | verification_code | expiration_month | expiration_year |
104
+ | Test | test | name: Basic | Joe Smith | jsmith@example.com | 4111111111115555 | 122 | 01 | 2015 |
105
+ And the following limits exist:
106
+ | plan | name | value |
107
+ | name: Basic | users | 1 |
108
+ | name: Free | users | 0 |
109
+ | name: Basic | projects | 5 |
110
+ | name: Free | projects | 1 |
111
+ And I have signed in with "joe@example.com/test"
112
+ And "joe@example.com" is an admin of the "Test" account
113
+ When I go to the settings page for the "Test" account
114
+ Then I should see "Your Plan"
115
+ And I should not see "Billing Information"
116
+ And I should see "Basic"
117
+ And I follow "Upgrade"
118
+ Then I should see "Upgrade Your Plan"
119
+ And the "Free" plan should be disabled
@@ -8,9 +8,11 @@ Feature: Sign up
8
8
 
9
9
  Scenario: User signs up with invalid data
10
10
  When I go to the sign up page for the "Free" plan
11
+ Then I should see "Free"
11
12
  And I fill in "Email" with "invalidemail"
12
13
  And I fill in "Password" with "password"
13
14
  And I fill in "Confirm password" with ""
15
+ And I should not see "Billing Information"
14
16
  And I press "Sign up"
15
17
  Then I should see error messages
16
18
 
@@ -0,0 +1,76 @@
1
+ Feature: Sign up
2
+ In order to get access to protected sections of the site
3
+ A user
4
+ Should be able to sign up
5
+
6
+ Background:
7
+ Given a paid plan exists with a name of "Paid"
8
+
9
+ Scenario: User signs up for a paid plan with invalid data
10
+ When I go to the sign up page for the "Paid" plan
11
+ And I should see "Paid"
12
+ And I should see "$1/month"
13
+ And the "Card number" field should have autocomplete off
14
+ And the "Verification code" field should have autocomplete off
15
+ And I fill in "Email" with "invalidemail"
16
+ And I fill in "Password" with "password"
17
+ And I fill in "Confirm password" with ""
18
+ And I should see "Billing Information"
19
+ And I press "Sign up"
20
+ Then I should see error messages
21
+
22
+ Scenario: User signs up for a paid plan with valid data
23
+ When I go to the list of plans page
24
+ And I follow "Paid"
25
+ And I fill in "Email" with "email@person.com"
26
+ And I fill in "Password" with "password"
27
+ And I fill in "Confirm password" with "password"
28
+ And I fill in "Your name" with "Robot"
29
+ And I fill in "Company Name" with "Robots, Inc"
30
+ And I fill in "Keyword" with "robotsinc"
31
+ And I fill in "Cardholder name" with "Ralph Robot"
32
+ And I fill in "Billing email" with "accounting@example.com"
33
+ And I fill in "Card number" with "4111111111111111"
34
+ And I fill in "Verification code" with "123"
35
+ And I select "March" from "Expiration month"
36
+ And I select "2020" from "Expiration year"
37
+ And I press "Sign up"
38
+ Then I should see "created"
39
+
40
+ Scenario: User signs up for a paid plan with an invalid credit card number
41
+ Given that the credit card "4111112" is invalid
42
+ When I go to the list of plans page
43
+ And I follow "Paid"
44
+ And I fill in "Email" with "email@person.com"
45
+ And I fill in "Password" with "password"
46
+ And I fill in "Confirm password" with "password"
47
+ And I fill in "Your name" with "Robot"
48
+ And I fill in "Company Name" with "Robots, Inc"
49
+ And I fill in "Keyword" with "robotsinc"
50
+ And I fill in "Cardholder name" with "Ralph Robot"
51
+ And I fill in "Billing email" with "accounting@example.com"
52
+ And I fill in "Card number" with "4111112"
53
+ And I fill in "Verification code" with "123"
54
+ And I select "March" from "Expiration month"
55
+ And I select "2020" from "Expiration year"
56
+ And I press "Sign up"
57
+ Then "Card number" should have the error "is invalid"
58
+
59
+ Scenario: User signs up for a paid plan with a credit card that cannot be processed
60
+ Given that the credit card "4111111111111111" should not be honored
61
+ When I go to the list of plans page
62
+ And I follow "Paid"
63
+ And I fill in "Email" with "email@person.com"
64
+ And I fill in "Password" with "password"
65
+ And I fill in "Confirm password" with "password"
66
+ And I fill in "Your name" with "Robot"
67
+ And I fill in "Company Name" with "Robots, Inc"
68
+ And I fill in "Keyword" with "robotsinc"
69
+ And I fill in "Cardholder name" with "Ralph Robot"
70
+ And I fill in "Billing email" with "accounting@example.com"
71
+ And I fill in "Card number" with "4111111111111111"
72
+ And I fill in "Verification code" with "123"
73
+ And I select "March" from "Expiration month"
74
+ And I select "2020" from "Expiration year"
75
+ And I press "Sign up"
76
+ Then "Card number" should have the error "Do Not Honor"
@@ -0,0 +1,7 @@
1
+ Given /^that the credit card "([^"]*)" is invalid$/ do |number|
2
+ FakeBraintree.failures[number] = { "message" => "Credit card number is invalid.", "errors" => { "customer" => { "errors" => [], "credit-card" => { "errors" => [{ "message" => "Credit card number is invalid.", "code" => 81715, "attribute" => :number }] }}}}
3
+ end
4
+
5
+ Given /^that the credit card "([^"]*)" should not be honored$/ do |number|
6
+ FakeBraintree.failures[number] = { "message" => "Do Not Honor", "code" => "2000", "status" => "processor_declined" }
7
+ end
@@ -1,3 +1,25 @@
1
1
  Then /^the form should have inline error messages$/ do
2
2
  page.should have_css(".error")
3
3
  end
4
+
5
+ When /^the "([^"]*)" field should have autocomplete off$/ do |field|
6
+ field = page.find_field(field)
7
+ field["autocomplete"].should == "off"
8
+ end
9
+
10
+ Then /^"([^"]*)" should have the error "([^"]*)"$/ do |field, error|
11
+ field = page.find_field(field)
12
+ field.find(:xpath, "following-sibling::p[@class='inline-errors'][contains(text(), '#{error}')]").should_not be_nil
13
+ end
14
+
15
+ Then /^the "([^"]*)" field(?: within "([^"]*)")? should be empty$/ do |field, selector|
16
+ with_scope(selector) do
17
+ field = find_field(field)
18
+ field_value = (field.tag_name == 'textarea') ? field.text : field.value
19
+ if field_value.respond_to? :should
20
+ field_value.should be_nil
21
+ else
22
+ assert_nil(field_value)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ When /^I choose the "([^"]*)" plan$/ do |plan_name|
2
+ input_id = page.find(:xpath, "//label/p[contains(text(),'#{plan_name}')]/../@for").node.value
3
+ choose(input_id)
4
+ end
5
+
6
+ Then /^the "([^"]*)" plan should be disabled$/ do |plan_name|
7
+ input_id = page.find(:xpath, "//label/p[contains(text(),'#{plan_name}')]/../@for").node.value
8
+ page.should have_css("##{input_id}[disabled='disabled']")
9
+ end
@@ -0,0 +1,5 @@
1
+ require 'saucy/braintree'
2
+
3
+ After do |s|
4
+ FakeBraintree.clear!
5
+ end
@@ -16,6 +16,8 @@ class CreateSaucyTables < ActiveRecord::Migration
16
16
  table.string :keyword
17
17
  table.datetime :created_at
18
18
  table.datetime :updated_at
19
+ table.string :customer_token
20
+ table.string :subscription_token
19
21
  end
20
22
  add_index :accounts, :plan_id
21
23
  add_index :accounts, :keyword
@@ -56,13 +58,24 @@ class CreateSaucyTables < ActiveRecord::Migration
56
58
 
57
59
  create_table :plans do |t|
58
60
  t.string :name
59
- t.integer :price
61
+ t.integer :price, :null => false, :default => 0
60
62
 
61
63
  t.timestamps
62
64
  end
63
65
 
64
66
  add_index :plans, :name
65
67
 
68
+ create_table :limits do |t|
69
+ t.belongs_to :plan
70
+ t.string :name
71
+ t.integer :value, :null => false, :default => 0
72
+ t.string :value_type, :null => false, :default => "number"
73
+
74
+ t.timestamps
75
+ end
76
+
77
+ add_index :limits, :plan_id
78
+
66
79
  create_table :invitations_projects, :id => false do |table|
67
80
  table.integer :invitation_id, :null => false
68
81
  table.integer :project_id, :null => false
@@ -0,0 +1,20 @@
1
+ require 'generators/saucy/base'
2
+
3
+ module Saucy
4
+ module Generators
5
+ class SpecsGenerator < Base
6
+
7
+ desc <<DESC
8
+ Description:
9
+ Copy saucy cucumber spec support files to your application.
10
+ DESC
11
+
12
+ def copy_spec_files
13
+ directory "support", "spec/support/saucy"
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+
20
+
@@ -0,0 +1,5 @@
1
+ require 'saucy/braintree'
2
+
3
+ RSpec.configure do |config|
4
+ config.after(:each) { FakeBraintree.clear! }
5
+ end
data/lib/saucy/account.rb CHANGED
@@ -3,6 +3,14 @@ module Saucy
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ CUSTOMER_ATTRIBUTES = { :cardholder_name => :cardholder_name,
7
+ :billing_email => :email,
8
+ :card_number => :number,
9
+ :expiration_month => :expiration_month,
10
+ :expiration_year => :expiration_year,
11
+ :verification_code => :cvv }
12
+
13
+ extend ActiveSupport::Memoizable
6
14
  has_many :memberships, :dependent => :destroy
7
15
  has_many :users, :through => :memberships
8
16
  has_many :projects, :dependent => :destroy
@@ -12,14 +20,23 @@ module Saucy
12
20
 
13
21
  belongs_to :plan
14
22
 
23
+ delegate :free?, :billed?, :to => :plan
24
+
15
25
  validates_uniqueness_of :name, :keyword
16
- validates_presence_of :name, :keyword
26
+ validates_presence_of :name, :keyword, :plan_id
27
+
28
+ attr_accessor *CUSTOMER_ATTRIBUTES.keys
17
29
 
18
30
  attr_accessible :name, :keyword
19
31
 
20
32
  validates_format_of :keyword,
21
33
  :with => %r{^[a-z0-9]+$},
22
34
  :message => "must be only lower case letters."
35
+
36
+ before_create :create_customer
37
+ before_create :create_subscription, :if => :billed?
38
+
39
+ memoize :customer
23
40
  end
24
41
 
25
42
  module InstanceMethods
@@ -46,6 +63,122 @@ module Saucy
46
63
  def memberships_by_name
47
64
  memberships.by_name
48
65
  end
66
+
67
+ def customer
68
+ Braintree::Customer.find(customer_token) if customer_token
69
+ end
70
+
71
+ def credit_card
72
+ customer.credit_cards[0] if customer && customer.credit_cards.any?
73
+ end
74
+
75
+ def subscription
76
+ Braintree::Subscription.find(subscription_token) if subscription_token
77
+ end
78
+
79
+ def save_braintree!(attributes)
80
+ successful = true
81
+ self.plan = ::Plan.find(attributes[:plan_id]) if attributes[:plan_id].present?
82
+ if CUSTOMER_ATTRIBUTES.keys.any? { |attribute| attributes[attribute].present? }
83
+ CUSTOMER_ATTRIBUTES.keys.each do |attribute|
84
+ send("#{attribute}=", attributes[attribute]) if attributes[attribute].present?
85
+ end
86
+ result = Braintree::Customer.update(customer_token, customer_attributes)
87
+ successful = result.success?
88
+ handle_customer_result(result)
89
+ end
90
+ if successful && attributes[:plan_id].present?
91
+ if subscription
92
+ Braintree::Subscription.update(subscription_token, :plan_id => attributes[:plan_id])
93
+ else
94
+ create_subscription
95
+ end
96
+ end
97
+ successful && save
98
+ end
99
+
100
+ def can_change_plan_to?(new_plan)
101
+ plan.limits.where(:value_type => :number).all? do |limit|
102
+ new_plan.limit(limit.name).value >= send(limit.name).count
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def customer_attributes
109
+ {
110
+ :email => billing_email,
111
+ :credit_card => credit_card_attributes
112
+ }
113
+ end
114
+
115
+ def credit_card_attributes
116
+ if plan.billed?
117
+ card_attributes = { :cardholder_name => cardholder_name,
118
+ :number => card_number,
119
+ :expiration_month => expiration_month,
120
+ :expiration_year => expiration_year,
121
+ :cvv => verification_code }
122
+ if credit_card
123
+ card_attributes.merge!(:options => credit_card_options)
124
+ end
125
+ card_attributes
126
+ else
127
+ {}
128
+ end
129
+ end
130
+
131
+ def credit_card_options
132
+ if customer && customer.credit_cards.any?
133
+ { :update_existing_token => customer.credit_cards[0].token }
134
+ else
135
+ {}
136
+ end
137
+ end
138
+
139
+ def create_customer
140
+ result = Braintree::Customer.create(customer_attributes)
141
+ handle_customer_result(result)
142
+ end
143
+
144
+ def handle_customer_result(result)
145
+ if result.success?
146
+ self.customer_token = result.customer.id
147
+ flush_cache :customer
148
+ else
149
+ verification = result.credit_card_verification
150
+ if verification && verification.status == "processor_declined"
151
+ errors[:card_number] << "was denied by the payment processor with the message: #{verification.processor_response_text}"
152
+ elsif verification && verification.status == "gateway_rejected"
153
+ errors[:verification_code] << "did not match"
154
+ elsif result.errors.any?
155
+ result.errors.each do |error|
156
+ if error.attribute == "number"
157
+ errors[:card_number] << error.message.gsub("Credit card number ", "")
158
+ elsif error.attribute == "CVV"
159
+ errors[:verification_code] << error.message.gsub("CVV ", "")
160
+ elsif error.attribute == "expiration_month"
161
+ errors[:expiration_month] << error.message.gsub("Expiration month ", "")
162
+ elsif error.attribute == "expiration_year"
163
+ errors[:expiration_year] << error.message.gsub("Expiration year ", "")
164
+ end
165
+ end
166
+ end
167
+ end
168
+ result.success?
169
+ end
170
+
171
+ def create_subscription
172
+ result = Braintree::Subscription.create(
173
+ :payment_method_token => credit_card.token,
174
+ :plan_id => plan_id
175
+ )
176
+ if result.success?
177
+ self.subscription_token = result.subscription.id
178
+ else
179
+ false
180
+ end
181
+ end
49
182
  end
50
183
  end
51
184
  end