saucy 0.1.18 → 0.2.0

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