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
data/Gemfile CHANGED
@@ -5,11 +5,14 @@ gem "rake"
5
5
  gem "rspec-rails", :require => false
6
6
  gem "rails", ">= 3.0.3"
7
7
  gem "thin"
8
- gem "clearance", :ref => '71d7c7f7a29ea757cf84fcc497cbc8ecf6cac211', :git => "git://github.com/thoughtbot/clearance.git", :require => false
8
+ gem "clearance", :git => "git://github.com/thoughtbot/clearance.git", :require => false
9
9
  gem "shoulda", :require => false
10
10
  gem "bourne", :require => false
11
11
  gem "sqlite3-ruby", :require => false
12
12
  gem "factory_girl", :require => false
13
+ gem "sinatra"
14
+ gem "sham_rack"
15
+ gem "braintree"
13
16
 
14
17
  # used by the rails app in cucumber
15
18
  gem "cucumber-rails", :require => false
data/Gemfile.lock CHANGED
@@ -1,7 +1,6 @@
1
1
  GIT
2
2
  remote: git://github.com/thoughtbot/clearance.git
3
- revision: 71d7c7f7a29ea757cf84fcc497cbc8ecf6cac211
4
- ref: 71d7c7f7a29ea757cf84fcc497cbc8ecf6cac211
3
+ revision: 7fb410051e0e7cce758883c98ec7b13824265cd0
5
4
  specs:
6
5
  clearance (0.9.1)
7
6
  rails (~> 3.0.0)
@@ -43,6 +42,8 @@ GEM
43
42
  background_process (1.2)
44
43
  bourne (1.0)
45
44
  mocha (= 0.9.8)
45
+ braintree (2.6.2)
46
+ builder
46
47
  builder (2.1.2)
47
48
  capybara (0.4.0)
48
49
  celerity (>= 0.7.9)
@@ -140,7 +141,12 @@ GEM
140
141
  ffi (~> 0.6.3)
141
142
  json_pure
142
143
  rubyzip
144
+ sham_rack (1.3.3)
145
+ rack
143
146
  shoulda (2.11.3)
147
+ sinatra (1.1.2)
148
+ rack (~> 1.1)
149
+ tilt (~> 1.2)
144
150
  sqlite3-ruby (1.3.2)
145
151
  term-ansicolor (1.0.5)
146
152
  thin (1.2.7)
@@ -148,6 +154,7 @@ GEM
148
154
  eventmachine (>= 0.12.6)
149
155
  rack (>= 1.0.0)
150
156
  thor (0.14.6)
157
+ tilt (1.2.1)
151
158
  treetop (1.4.9)
152
159
  polyglot (>= 0.3.1)
153
160
  tzinfo (0.3.23)
@@ -160,6 +167,7 @@ PLATFORMS
160
167
  DEPENDENCIES
161
168
  aruba
162
169
  bourne
170
+ braintree
163
171
  capybara
164
172
  clearance!
165
173
  cucumber
@@ -173,6 +181,8 @@ DEPENDENCIES
173
181
  rails (>= 3.0.3)
174
182
  rake
175
183
  rspec-rails
184
+ sham_rack
176
185
  shoulda
186
+ sinatra
177
187
  sqlite3-ruby
178
188
  thin
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ Saucy
2
+ =====
3
+
4
+ Saucy is a Rails engine for monthly subscription-style SaaS apps.
5
+
6
+ Example scenarios covered by Saucy:
7
+
8
+ * I sign up for "Free" plan under new account "thoughtbot"
9
+ * I am an admin and can be reached at "dan@example.com"
10
+ * I create a project "Hoptoad"
11
+ * I upgrade to the "Basic" plan and my credit card is charged
12
+ * I now have permissions to add users and projects to the "thoughtbot" account
13
+ * I invite "joe@example.com" to "Hoptoad"
14
+ * I create a project "Trajectory"
15
+ * I invite "mike@example.com" to "Trajectory"
16
+
17
+ Installation
18
+ ------------
19
+
20
+ In your Gemfile:
21
+
22
+ gem "saucy", :git => 'git@github.com:thoughtbot/saucy.git'
23
+
24
+ After you bundle, run the generators:
25
+
26
+ rails generate saucy:install
27
+ rails generate saucy:views
28
+
29
+ Development environment
30
+ -----------------------
31
+
32
+ Plans need to exist for users to sign up for. In db/seeds.rb:
33
+
34
+ %w(free expensive mega-expensive).each do |plan_name|
35
+ Plan.find_or_create_by_name(plan_name)
36
+ end
37
+
38
+ Then run: rake db:seed
39
+
40
+ Test environment
41
+ ----------------
42
+
43
+ Generate the Braintree Fake for your specs:
44
+
45
+ rails generate saucy:specs
46
+
47
+ Generate feature coverage:
48
+
49
+ rails generate saucy:features
50
+
51
+ To use seed data in your Cucumber, add this to features/support/seed.rb:
52
+
53
+ require Rails.root.join('db','seeds')
54
+
55
+ Customization
56
+ -------------
57
+
58
+ To change the layout for a controller inside of saucy, add a line like this to
59
+ your config/application.rb:
60
+
61
+ config.saucy.layouts.accounts.index = "custom"
62
+
63
+ Your layout should yield(:header) in order to get the headers from saucy views.
64
+
65
+ To extend the ProjectsController:
66
+
67
+ class ProjectsController < ApplicationController
68
+ include Saucy::ProjectsController
69
+
70
+ def edit
71
+ super
72
+ @deleters = @project.deleters
73
+ end
74
+ end
75
+
@@ -9,8 +9,10 @@ class AccountsController < ApplicationController
9
9
  end
10
10
 
11
11
  def create
12
+ @plan = Plan.find(params[:plan_id])
12
13
  @signup = Signup.new(params[:signup])
13
14
  @signup.user = current_user
15
+ @signup.plan = @plan
14
16
  if @signup.save
15
17
  flash[:success] = "Account was created."
16
18
  sign_in @signup.user
@@ -0,0 +1,22 @@
1
+ class BillingsController < ApplicationController
2
+ def show
3
+ end
4
+
5
+ def edit
6
+ @account = current_account
7
+
8
+ @account.cardholder_name = @account.credit_card.cardholder_name
9
+ @account.billing_email = @account.customer.email
10
+ @account.expiration_month = @account.credit_card.expiration_month
11
+ @account.expiration_year = @account.credit_card.expiration_year
12
+ end
13
+
14
+ def update
15
+ @account = current_account
16
+ if @account.save_braintree!(params[:account])
17
+ redirect_to account_billing_path(@account), :notice => t('.update.notice', :default => "Billing information updated successfully")
18
+ else
19
+ render :edit
20
+ end
21
+ end
22
+ end
@@ -4,4 +4,16 @@ class PlansController < ApplicationController
4
4
  def index
5
5
  @plans = Plan.all
6
6
  end
7
+
8
+ def edit
9
+ @plans = Plan.all
10
+ @account = current_account
11
+ end
12
+
13
+ def update
14
+ @plans = Plan.all
15
+ @account = current_account
16
+ @account.save_braintree!(params[:account])
17
+ redirect_to edit_account_path(@account), :notice => t('.update.notice', :default => "Plan changed successfully")
18
+ end
7
19
  end
@@ -0,0 +1,5 @@
1
+ class Limit < ActiveRecord::Base
2
+ belongs_to :plan
3
+
4
+ validates_presence_of :name, :value
5
+ end
data/app/models/signup.rb CHANGED
@@ -9,6 +9,13 @@ class Signup
9
9
  :account => {
10
10
  :name => :account_name,
11
11
  :keyword => :keyword,
12
+ :cardholder_name => :cardholder_name,
13
+ :billing_email => :billing_email,
14
+ :card_number => :card_number,
15
+ :expiration_month => :expiration_month,
16
+ :expiration_year => :expiration_year,
17
+ :verification_code => :verification_code,
18
+ :plan => :plan
12
19
  },
13
20
  :user => {
14
21
  :name => :user_name,
@@ -18,8 +25,9 @@ class Signup
18
25
  }
19
26
  }.freeze
20
27
 
21
- attr_accessor :account_name, :keyword, :user_name, :email, :password,
22
- :password_confirmation
28
+ attr_accessor :account_name, :keyword, :user_name, :billing_email, :password,
29
+ :password_confirmation, :cardholder_name, :email, :card_number,
30
+ :expiration_month, :expiration_year, :plan, :verification_code
23
31
 
24
32
  def initialize(attributes = {})
25
33
  attributes.each do |attribute, value|
@@ -38,8 +46,14 @@ class Signup
38
46
  delegate_attributes_for(:user) unless existing_user
39
47
 
40
48
  if valid?
41
- save!
42
- true
49
+ begin
50
+ save!
51
+ true
52
+ rescue ActiveRecord::RecordNotSaved
53
+ delegate_errors_for(:account)
54
+ delegate_errors_for(:user)
55
+ false
56
+ end
43
57
  else
44
58
  false
45
59
  end
@@ -68,8 +82,8 @@ class Signup
68
82
 
69
83
  def membership
70
84
  @membership ||= Membership.new(:user => user,
71
- :account => account,
72
- :admin => true)
85
+ :account => account,
86
+ :admin => true)
73
87
  end
74
88
 
75
89
  def valid?
@@ -3,6 +3,8 @@
3
3
  <li class="accounts"><%= link_to "Account Info", edit_account_url(current_account) %></li>
4
4
  <li class="projects"><%= link_to "Projects", account_projects_url(current_account) %></li>
5
5
  <li class="users"><%= link_to "Users", account_memberships_url(current_account) %></li>
6
- <li class="billing"><a href="#">Billing</a></li>
6
+ <% if current_account.plan.billed? %>
7
+ <li class="billing"><%= link_to "Billing", account_billing_path(current_account) %></li>
8
+ <% end -%>
7
9
  </ul>
8
10
  </div>
@@ -4,6 +4,12 @@
4
4
 
5
5
  <%= render :partial => 'tab_bar' %>
6
6
 
7
+ <div class="plan current">
8
+ <h3>Your Plan</h3>
9
+ <%= render @account.plan %>
10
+ <%= link_to "Upgrade", edit_account_plan_path(@account) %>
11
+ </div>
12
+
7
13
  <%= content_tag_for :div, @account do -%>
8
14
  <%= semantic_form_for @account do |form| %>
9
15
  <%= form.inputs do %>
@@ -7,20 +7,30 @@
7
7
 
8
8
  <h5 class="legend">Basic Information</h5>
9
9
  <%= form.inputs do %>
10
- <%= form.input :account_name, :label => 'Company Name', :hint => account_url(Account.new(:keyword => 'keyword')).html_safe %>
10
+ <%= form.input :account_name, :label => 'Company Name', :hint => account_url(Account.new(:keyword => 'keyword')).html_safe, :required => true %>
11
11
  <%= form.input :keyword, :wrapper_html => { :style => 'display: none' } %>
12
12
  <% end %>
13
13
 
14
14
  <% unless signed_in? -%>
15
15
  <h5 class="legend">User Information</h5>
16
16
  <%= form.inputs do %>
17
- <%= form.input :user_name, :label => "Your name" %>
18
- <%= form.input :email %>
19
- <%= form.input :password %>
17
+ <%= form.input :user_name, :label => "Your name", :required => true %>
18
+ <%= form.input :email, :required => true %>
19
+ <%= form.input :password, :required => true %>
20
20
  <%= form.input :password_confirmation, :label => "Confirm password", :required => true %>
21
21
  <% end -%>
22
22
  <% end -%>
23
23
 
24
+ <div class="plan chosen">
25
+ <h3>You're signing up for:</h3>
26
+ <%= render @plan %>
27
+ </div>
28
+
29
+ <% if @plan.billed? -%>
30
+ <h5 class="legend">Billing Information</h5>
31
+ <%= render :partial => 'billings/form', :locals => { :form => form } %>
32
+ <% end -%>
33
+
24
34
  <%= form.buttons do %>
25
35
  <%= form.commit_button "Sign up" %>
26
36
  <% end %>
@@ -0,0 +1,8 @@
1
+ <%= form.inputs do %>
2
+ <%= form.input :cardholder_name, :required => true %>
3
+ <%= form.input :billing_email, :required => true, :hint => "We'll send receipts and billing issues to this address." %>
4
+ <%= form.input :card_number, :required => true, :input_html => { :autocomplete => "off" } %>
5
+ <%= form.input :verification_code, :required => true, :input_html => { :autocomplete => "off" } %>
6
+ <%= form.input :expiration_month, :collection => [['January', '01'], ['February', '02'], ['March', '03'], ['April', '04'], ['May', '05'], ['June', '06'], ['July', '07'], ['August', '08'], ['September', '09'], ['October', '10'], ['November', '11'], ['December', '12']], :required => true %>
7
+ <%= form.input :expiration_year, :collection => (Date.today.year..(Date.today.year+10)).to_a, :required => true %>
8
+ <% end -%>
@@ -0,0 +1,11 @@
1
+ <%= content_for :header do -%>
2
+ <h2>Billing Information</h2>
3
+ <% end -%>
4
+
5
+ <%= semantic_form_for @account, :url => account_billing_path(@account) do |form| %>
6
+ <%= render :partial => 'billings/form', :locals => { :form => form } %>
7
+
8
+ <%= form.buttons do %>
9
+ <%= form.commit_button "Update" %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <div class="current_credit_card">
2
+ We're currently charging the credit card ending in <%= current_account.credit_card.last_4 %>. <%= link_to "Change", edit_account_billing_path(current_account) %>
3
+ </div>
@@ -0,0 +1,2 @@
1
+ <p class="name"><%= plan.name %></p>
2
+ <p class="price"><% if plan.billed? %>$<%= plan.price %>/month<% else %>Free<% end %></p>
@@ -0,0 +1,23 @@
1
+ <%= content_for :header do -%>
2
+ <h2>Upgrade Your Plan</h2>
3
+ <% end -%>
4
+
5
+ <%= semantic_form_for @account, :url => account_plan_path(@account) do |form| %>
6
+ <%= form.inputs :class => "radio optional" do %>
7
+ <% @plans.each do |plan| -%>
8
+ <%= content_tag :li, :class => "#{'disabled' if !@account.can_change_plan_to?(plan)}" do %>
9
+ <%= form.label :plan_id, render(plan), :value => plan.id %>
10
+ <%= form.radio_button :plan_id, plan.id, :disabled => !@account.can_change_plan_to?(plan) %>
11
+ <% end %>
12
+ <% end -%>
13
+ <% end %>
14
+
15
+ <% if !@account.credit_card -%>
16
+ <h5 class="legend">Billing Information</h5>
17
+ <%= render :partial => 'billings/form', :locals => { :form => form } %>
18
+ <% end -%>
19
+
20
+ <%= form.buttons do %>
21
+ <%= form.commit_button "Upgrade" %>
22
+ <% end %>
23
+ <% end %>
data/config/routes.rb CHANGED
@@ -4,6 +4,8 @@ Rails.application.routes.draw do
4
4
  resources :accounts, :only => [:index, :edit, :update]
5
5
 
6
6
  through :accounts do
7
+ resource :billing
8
+ resource :plan
7
9
  resources :projects
8
10
  resources :memberships, :only => [:index, :edit, :update, :destroy]
9
11
  resources :invitations, :only => [:show, :update, :new, :create]
@@ -25,6 +25,7 @@ Feature: generate a saucy application and run rake
25
25
 
26
26
  Scenario: generate a saucy application and run rake
27
27
  When I successfully run "rails generate saucy:install"
28
+ And I successfully run "rails generate saucy:specs"
28
29
  And I successfully run "rails generate saucy:features"
29
30
  And I successfully run "rake db:migrate"
30
31
  And I run "rake"
@@ -37,6 +38,7 @@ Feature: generate a saucy application and run rake
37
38
 
38
39
  Scenario: A new saucy app with custom views
39
40
  When I successfully run "rails generate saucy:install"
41
+ And I successfully run "rails generate saucy:specs"
40
42
  And I successfully run "rails generate saucy:features"
41
43
  And I successfully run "rails generate saucy:views"
42
44
  And I successfully run "rake db:migrate"
@@ -51,6 +53,7 @@ Feature: generate a saucy application and run rake
51
53
 
52
54
  Scenario: A new saucy app with custom layouts
53
55
  When I successfully run "rails generate saucy:install"
56
+ And I successfully run "rails generate saucy:specs"
54
57
  And I successfully run "rails generate saucy:features"
55
58
  And I successfully run "rake db:migrate"
56
59
  And I add a custom layout to the accounts index
@@ -64,6 +67,7 @@ Feature: generate a saucy application and run rake
64
67
 
65
68
  Scenario: run specs
66
69
  When I successfully run "rails generate saucy:install"
70
+ And I successfully run "rails generate saucy:specs"
67
71
  And I successfully run "rails generate saucy:features"
68
72
  And I successfully run "rake db:migrate"
69
73
  And I copy the specs for this project
@@ -12,6 +12,7 @@ DESC
12
12
  def copy_feature_files
13
13
  directory "features", "features/saucy"
14
14
  directory "step_definitions", "features/step_definitions/saucy"
15
+ directory "support", "features/support/saucy"
15
16
  template "README", "features/saucy/README"
16
17
  template "README", "features/step_definitions/saucy/README"
17
18
  empty_directory "spec"
@@ -47,6 +48,9 @@ DESC
47
48
  when /sign up page for the "([^"]+)" plan$/i
48
49
  plan = Plan.find_by_name!($1)
49
50
  new_plan_account_path(plan)
51
+ when /^the billing page for the "([^"]+)" account$/
52
+ account = Account.find_by_name!($1)
53
+ account_billing_path(account)
50
54
  PATHS
51
55
 
52
56
  replace_in_file "features/support/paths.rb",
@@ -14,8 +14,9 @@ Factory.define :user do |user|
14
14
  end
15
15
 
16
16
  Factory.define :account do |f|
17
- f.name { Factory.next(:name) }
18
- f.keyword { Factory.next(:name) }
17
+ f.name { Factory.next(:name) }
18
+ f.keyword { Factory.next(:name) }
19
+ f.association :plan
19
20
  end
20
21
 
21
22
  Factory.define :membership do |f|
@@ -30,6 +31,7 @@ Factory.define :signup do |f|
30
31
  f.email { Factory.next :email }
31
32
  f.password { "password" }
32
33
  f.password_confirmation { "password" }
34
+ f.association :plan
33
35
  end
34
36
 
35
37
  Factory.define :project do |f|
@@ -51,3 +53,13 @@ end
51
53
  Factory.define :plan do |f|
52
54
  f.name 'Free'
53
55
  end
56
+
57
+ Factory.define :paid_plan, :parent => :plan do |f|
58
+ f.name 'Paid'
59
+ f.price 1
60
+ end
61
+
62
+ Factory.define :limit do |f|
63
+ f.name { Factory.next(:name) }
64
+ f.association :plan
65
+ end