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