saucy 0.8.2 → 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ 0.8.3
2
+
3
+ Added coupon support. In your app, add the following migration:
4
+
5
+ class AddCoupons < ActiveRecord::Migration
6
+ def self.up
7
+ create_table :coupons do |table|
8
+ table.string :name, :default => "", :null => false
9
+ table.integer :free_months, :default => 1, :null => false
10
+ end
11
+ add_column :accounts, :coupon_id, :integer
12
+ end
13
+
14
+ def self.down
15
+ remove_column :accounts, :coupon_id
16
+ drop_table :coupons
17
+ end
18
+ end
19
+
1
20
  0.8.2
2
21
 
3
22
  Added a uniqueness validation and database constraint for member_id/project_id on permissions. This prevents somebody from being added to the same project twice, which would display it multiple times in the project index and dropdown. When upgrading, make sure this index is in place:
@@ -1,4 +1,6 @@
1
1
  class AccountsController < ApplicationController
2
+ include Saucy::Coupons
3
+
2
4
  before_filter :authorize, :only => [:index, :edit, :update]
3
5
  before_filter :authorize_admin, :except => [:new, :create, :index]
4
6
  before_filter :ensure_active_account, :only => [:edit, :update]
@@ -12,10 +14,13 @@ class AccountsController < ApplicationController
12
14
  end
13
15
 
14
16
  def create
15
- @plan = Plan.find(params[:plan_id])
17
+ @plan = Plan.find(params[:plan_id])
16
18
  @signup = Signup.new(params[:signup])
17
- @signup.user = current_user
18
- @signup.plan = @plan
19
+
20
+ @signup.coupon = Coupon.find_by_name(session[:coupon_name])
21
+ @signup.user = current_user
22
+ @signup.plan = @plan
23
+
19
24
  if @signup.save
20
25
  Saucy::Configuration.notify("account_created", :request => request,
21
26
  :account => @signup.account)
@@ -0,0 +1,7 @@
1
+ module CouponsHelper
2
+ def days_until_expiration
3
+ if session[:coupon_name] && @coupon ||= Coupon.find_by_name(session[:coupon_name])
4
+ @coupon.free_months * 30
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ class Coupon < ActiveRecord::Base
2
+ has_many :accounts
3
+ validates_presence_of :name
4
+ end
data/app/models/signup.rb CHANGED
@@ -7,13 +7,13 @@ class Signup
7
7
 
8
8
  FIELDS = {
9
9
  :account => {
10
- :cardholder_name => :cardholder_name,
11
- :billing_email => :billing_email,
12
- :card_number => :card_number,
13
- :expiration_month => :expiration_month,
14
- :expiration_year => :expiration_year,
10
+ :cardholder_name => :cardholder_name,
11
+ :billing_email => :billing_email,
12
+ :card_number => :card_number,
13
+ :expiration_month => :expiration_month,
14
+ :expiration_year => :expiration_year,
15
15
  :verification_code => :verification_code,
16
- :plan => :plan
16
+ :plan => :plan
17
17
  },
18
18
  :user => {
19
19
  :email => :email,
@@ -21,8 +21,8 @@ class Signup
21
21
  }
22
22
  }.freeze
23
23
 
24
- attr_accessor :billing_email, :password, :cardholder_name, :email,
25
- :card_number, :expiration_month, :expiration_year, :plan, :verification_code
24
+ attr_accessor :billing_email, :password, :cardholder_name, :email,
25
+ :card_number, :expiration_month, :expiration_year, :plan, :verification_code, :coupon
26
26
 
27
27
  def initialize(attributes = {})
28
28
  if attributes
@@ -43,7 +43,7 @@ class Signup
43
43
  populate_additional_account_fields
44
44
 
45
45
  if !existing_user
46
- delegate_attributes_for(:user)
46
+ delegate_attributes_for(:user)
47
47
  populate_additional_user_fields
48
48
  end
49
49
 
@@ -154,6 +154,7 @@ class Signup
154
154
  end
155
155
 
156
156
  def save!
157
+ account.coupon = coupon
157
158
  Account.transaction do
158
159
  account.save!
159
160
  user.save!
@@ -11,8 +11,11 @@
11
11
  </p>
12
12
  <ul class="features">
13
13
  <% if plan.trial? -%>
14
- <li class="trial"><%= t(".trial_will_expire",
15
- :default => "Your trial will expire after 30 days.") %></li>
14
+ <li class="trial">
15
+ <%= t(".trial_will_expire",
16
+ :default => "Your trial will expire after %{days_until_expiration} days.",
17
+ :days_until_expiration => days_until_expiration) %>
18
+ </li>
16
19
  <% else -%>
17
20
  <% plan.limits.numbered.each do |limit| %>
18
21
  <%= content_tag_for :li, limit, :class => limit.name do %>
@@ -69,3 +69,7 @@ Factory.define :limit do |f|
69
69
  f.name { Factory.next(:name) }
70
70
  f.association :plan
71
71
  end
72
+
73
+ Factory.define :coupon do |factory|
74
+ factory.name { "RAMEN" }
75
+ end
@@ -0,0 +1,31 @@
1
+ Feature: Sign up with coupon
2
+
3
+ In order to try the site with extended time
4
+ As a user
5
+ I want to use a coupon provided to me
6
+
7
+ Background:
8
+ Given the following plan exists:
9
+ | name | price | trial |
10
+ | Free | 0 | true |
11
+ And the following coupon exists:
12
+ | name | free months |
13
+ | RAMEN | 3 |
14
+
15
+ Scenario: Sign up for a trial plan with coupon
16
+ When I sign up with the "RAMEN" coupon for the "Free" plan
17
+ Then I should see "Your trial will expire after 90 days"
18
+ When I fill in "Email" with "email@person.com"
19
+ And I fill in "Password" with "password"
20
+ And I press "Sign up"
21
+ Then I should see "created"
22
+
23
+ Scenario: Receive a reminder about an expiring trial plan with coupon
24
+ Given a "Free" account with "RAMEN" coupon exists with a name of "Test" created 83 days ago
25
+ And a user exists with an email of "admin@example.com"
26
+ And "admin@example.com" is an admin of the "Test" account
27
+ When the daily Saucy jobs are processed
28
+ And I sign in as "admin@example.com"
29
+ And I follow the link sent to "admin@example.com" with subject "Your trial is expiring soon"
30
+ Then I should be on the upgrade plan page for the "Test" account
31
+
@@ -3,6 +3,17 @@ Given /^an? "([^"]+)" account exists with a name of "([^"]*)" created (\d+) days
3
3
  Factory(:account, :created_at => days.to_i.days.ago, :plan => plan, :name => account_name)
4
4
  end
5
5
 
6
+ Given /^a "([^"]*)" account with "([^"]*)" coupon exists with a name of "([^"]*)" created (\d+) days ago$/ do |plan_name, coupon_name, account_name, days|
7
+ plan = Plan.find_by_name!(plan_name)
8
+ coupon = Coupon.find_by_name!(coupon_name)
9
+ Factory(:account, :created_at => days.to_i.days.ago, :plan => plan, :coupon => coupon, :name => account_name)
10
+ end
11
+
12
+ Given /^I sign up with the "([^"]*)" coupon for the "([^"]+)" plan$/ do |coupon_name, plan_name|
13
+ plan = Plan.find_by_name!(plan_name)
14
+ visit new_plan_account_path(plan, :coupon => coupon_name)
15
+ end
16
+
6
17
  Given /^the "([^"]*)" account was created (\d+) days ago$/ do |account_name, days|
7
18
  Account.find_by_name!(account_name).tap do |account|
8
19
  account.created_at = days.to_i.days.ago
@@ -12,6 +12,7 @@ class CreateSaucyTables < ActiveRecord::Migration
12
12
 
13
13
  create_table :accounts do |table|
14
14
  table.belongs_to :plan
15
+ table.belongs_to :coupon
15
16
  table.string :name
16
17
  table.string :keyword
17
18
  table.datetime :created_at
@@ -28,11 +29,17 @@ class CreateSaucyTables < ActiveRecord::Migration
28
29
  table.datetime :trial_expires_at
29
30
  end
30
31
  add_index :accounts, :plan_id
32
+ add_index :accounts, :coupon_id
31
33
  add_index :accounts, :keyword, :unique => true
32
34
  add_index :accounts, :next_billing_date
33
35
  add_index :accounts, :created_at
34
36
  add_index :accounts, :trial_expires_at
35
37
 
38
+ create_table :coupons, :force => true do |t|
39
+ t.string :name, :default => "", :null => false
40
+ t.integer :free_months, :default => 1, :null => false
41
+ end
42
+
36
43
  create_table :invitations do |table|
37
44
  table.string :email
38
45
  table.integer :account_id
data/lib/saucy.rb CHANGED
@@ -7,4 +7,4 @@ require 'saucy/account_authorization'
7
7
  require 'saucy/configuration'
8
8
  require 'saucy/engine'
9
9
  require 'saucy/projects_controller'
10
-
10
+ require 'saucy/coupons'
data/lib/saucy/account.rb CHANGED
@@ -16,6 +16,7 @@ module Saucy
16
16
  :conditions => { 'memberships.admin' => false }
17
17
 
18
18
  belongs_to :plan
19
+ belongs_to :coupon
19
20
 
20
21
  delegate :free?, :billed?, :trial?, :to => :plan
21
22
 
@@ -69,7 +70,12 @@ module Saucy
69
70
  end
70
71
 
71
72
  def set_trial_expiration
72
- self.trial_expires_at = 30.days.from_now(created_at || Time.now)
73
+ number_free_days = if coupon
74
+ 30 * coupon.free_months
75
+ else
76
+ 30
77
+ end
78
+ self.trial_expires_at = number_free_days.days.from_now(created_at || Time.now)
73
79
  end
74
80
 
75
81
  def users_count
@@ -0,0 +1,15 @@
1
+ module Saucy
2
+ module Coupons
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_filter :set_coupon_cookie
7
+ end
8
+
9
+ def set_coupon_cookie
10
+ if params[:coupon] && coupon = Coupon.find_by_name(params[:coupon])
11
+ session[:coupon_name] ||= params[:coupon]
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/saucy/engine.rb CHANGED
@@ -23,6 +23,10 @@ module Saucy
23
23
  ActionView::Base.send :include, LimitsHelper
24
24
  end
25
25
 
26
+ initializer 'coupons.helper' do |app|
27
+ ActionView::Base.send :include, CouponsHelper
28
+ end
29
+
26
30
  {:short_date => "%x"}.each do |k, v|
27
31
  Time::DATE_FORMATS[k] = v
28
32
  end
@@ -12,7 +12,7 @@ describe AccountsController, "routes" do
12
12
  end
13
13
 
14
14
  describe AccountsController, "new" do
15
- let(:signup) { stub('signup') }
15
+ let(:signup) { stub('signup', :coupon= => nil) }
16
16
  let(:plan) { Factory(:plan) }
17
17
 
18
18
  before do
@@ -40,6 +40,7 @@ describe AccountsController, "successful create for a confirmed user" do
40
40
  let(:account) { Factory.stub(:account) }
41
41
  let(:signup) { stub('signup', :user => user,
42
42
  :user= => nil,
43
+ :coupon= => nil,
43
44
  :plan= => plan,
44
45
  :account => account) }
45
46
  let(:signup_attributes) { "attributes" }
@@ -76,7 +77,7 @@ describe AccountsController, "successful create for a confirmed user" do
76
77
  end
77
78
 
78
79
  describe AccountsController, "failed create" do
79
- let(:signup) { stub('signup', :user= => nil, :plan= => plan) }
80
+ let(:signup) { stub('signup', :user= => nil, :coupon= => nil, :plan= => plan) }
80
81
  let(:signup_attributes) { "attributes" }
81
82
  let(:plan) { Factory(:plan) }
82
83
 
@@ -7,9 +7,10 @@ describe Account do
7
7
  it { should have_many(:users).through(:memberships) }
8
8
  it { should have_many(:projects) }
9
9
  it { should belong_to(:plan) }
10
+ it { should belong_to(:coupon) }
10
11
 
11
12
  it { should validate_uniqueness_of(:keyword) }
12
- it { should validate_presence_of( :name) }
13
+ it { should validate_presence_of(:name) }
13
14
  it { should validate_presence_of(:keyword) }
14
15
  it { should validate_presence_of(:plan_id) }
15
16
 
@@ -216,3 +217,13 @@ describe Account do
216
217
  end
217
218
  end
218
219
 
220
+ describe Account, "with coupon" do
221
+ let(:free_months) { 3 }
222
+ let(:coupon) { Factory(:coupon, :free_months => free_months) }
223
+
224
+ subject { Factory(:account, :coupon => coupon) }
225
+
226
+ it "sets the trial expires at based on coupon free months" do
227
+ subject.reload.trial_expires_at.to_s.should == (free_months * 30).days.from_now.to_s
228
+ end
229
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coupon do
4
+ it { should have_many(:accounts) }
5
+ it { should validate_presence_of(:name) }
6
+ end
@@ -175,3 +175,18 @@ describe Signup, "with an account that doesn't save" do
175
175
  subject.save.should_not be
176
176
  end
177
177
  end
178
+
179
+ describe Signup, "with coupon" do
180
+ let(:coupon) { Factory(:coupon) }
181
+
182
+ subject { Factory.build(:signup) }
183
+
184
+ before do
185
+ subject.coupon = coupon
186
+ subject.save
187
+ end
188
+
189
+ it "saves coupon to account" do
190
+ subject.account.reload.coupon.should == coupon
191
+ end
192
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saucy
3
3
  version: !ruby/object:Gem::Version
4
- hash: 59
4
+ hash: 57
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 8
9
- - 2
10
- version: 0.8.2
9
+ - 3
10
+ version: 0.8.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - thoughtbot, inc.
@@ -18,11 +18,12 @@ autorequire:
18
18
  bindir: bin
19
19
  cert_chain: []
20
20
 
21
- date: 2011-06-07 00:00:00 -04:00
21
+ date: 2011-06-27 00:00:00 -04:00
22
22
  default_executable:
23
23
  dependencies:
24
24
  - !ruby/object:Gem::Dependency
25
- type: :runtime
25
+ name: clearance
26
+ prerelease: false
26
27
  requirement: &id001 !ruby/object:Gem::Requirement
27
28
  none: false
28
29
  requirements:
@@ -34,11 +35,11 @@ dependencies:
34
35
  - 11
35
36
  - 0
36
37
  version: 0.11.0
38
+ type: :runtime
37
39
  version_requirements: *id001
38
- name: clearance
39
- prerelease: false
40
40
  - !ruby/object:Gem::Dependency
41
- type: :runtime
41
+ name: formtastic
42
+ prerelease: false
42
43
  requirement: &id002 !ruby/object:Gem::Requirement
43
44
  none: false
44
45
  requirements:
@@ -49,11 +50,11 @@ dependencies:
49
50
  - 1
50
51
  - 2
51
52
  version: "1.2"
53
+ type: :runtime
52
54
  version_requirements: *id002
53
- name: formtastic
54
- prerelease: false
55
55
  - !ruby/object:Gem::Dependency
56
- type: :runtime
56
+ name: railties
57
+ prerelease: false
57
58
  requirement: &id003 !ruby/object:Gem::Requirement
58
59
  none: false
59
60
  requirements:
@@ -65,11 +66,11 @@ dependencies:
65
66
  - 0
66
67
  - 3
67
68
  version: 3.0.3
69
+ type: :runtime
68
70
  version_requirements: *id003
69
- name: railties
70
- prerelease: false
71
71
  - !ruby/object:Gem::Dependency
72
- type: :runtime
72
+ name: braintree
73
+ prerelease: false
73
74
  requirement: &id004 !ruby/object:Gem::Requirement
74
75
  none: false
75
76
  requirements:
@@ -81,11 +82,11 @@ dependencies:
81
82
  - 6
82
83
  - 2
83
84
  version: 2.6.2
85
+ type: :runtime
84
86
  version_requirements: *id004
85
- name: braintree
86
- prerelease: false
87
87
  - !ruby/object:Gem::Dependency
88
- type: :runtime
88
+ name: sham_rack
89
+ prerelease: false
89
90
  requirement: &id005 !ruby/object:Gem::Requirement
90
91
  none: false
91
92
  requirements:
@@ -97,11 +98,11 @@ dependencies:
97
98
  - 3
98
99
  - 3
99
100
  version: 1.3.3
101
+ type: :runtime
100
102
  version_requirements: *id005
101
- name: sham_rack
102
- prerelease: false
103
103
  - !ruby/object:Gem::Dependency
104
- type: :runtime
104
+ name: sinatra
105
+ prerelease: false
105
106
  requirement: &id006 !ruby/object:Gem::Requirement
106
107
  none: false
107
108
  requirements:
@@ -113,11 +114,11 @@ dependencies:
113
114
  - 1
114
115
  - 2
115
116
  version: 1.1.2
117
+ type: :runtime
116
118
  version_requirements: *id006
117
- name: sinatra
118
- prerelease: false
119
119
  - !ruby/object:Gem::Dependency
120
- type: :development
120
+ name: aruba
121
+ prerelease: false
121
122
  requirement: &id007 !ruby/object:Gem::Requirement
122
123
  none: false
123
124
  requirements:
@@ -129,9 +130,8 @@ dependencies:
129
130
  - 2
130
131
  - 6
131
132
  version: 0.2.6
133
+ type: :development
132
134
  version_requirements: *id007
133
- name: aruba
134
- prerelease: false
135
135
  description: Clearance-based Rails engine for Software as a Service (Saas) that provides account and project management
136
136
  email: support@thoughtbot.com
137
137
  executables: []
@@ -154,9 +154,11 @@ files:
154
154
  - app/controllers/memberships_controller.rb
155
155
  - app/controllers/plans_controller.rb
156
156
  - app/controllers/profiles_controller.rb
157
+ - app/helpers/coupons_helper.rb
157
158
  - app/helpers/limits_helper.rb
158
159
  - app/mailers/billing_mailer.rb
159
160
  - app/mailers/invitation_mailer.rb
161
+ - app/models/coupon.rb
160
162
  - app/models/invitation.rb
161
163
  - app/models/limit.rb
162
164
  - app/models/membership.rb
@@ -225,6 +227,7 @@ files:
225
227
  - lib/saucy/account.rb
226
228
  - lib/saucy/account_authorization.rb
227
229
  - lib/saucy/configuration.rb
230
+ - lib/saucy/coupons.rb
228
231
  - lib/saucy/engine.rb
229
232
  - lib/saucy/fake_braintree.rb
230
233
  - lib/saucy/layouts.rb
@@ -253,6 +256,7 @@ files:
253
256
  - lib/generators/saucy/features/templates/features/new_account.feature
254
257
  - lib/generators/saucy/features/templates/features/project_dropdown.feature
255
258
  - lib/generators/saucy/features/templates/features/sign_up.feature
259
+ - lib/generators/saucy/features/templates/features/sign_up_coupon.feature
256
260
  - lib/generators/saucy/features/templates/features/sign_up_paid.feature
257
261
  - lib/generators/saucy/features/templates/features/trial_plans.feature
258
262
  - lib/generators/saucy/features/templates/README
@@ -268,6 +272,7 @@ files:
268
272
  - spec/mailers/billing_mailer_spec.rb
269
273
  - spec/mailers/invitiation_mailer_spec.rb
270
274
  - spec/models/account_spec.rb
275
+ - spec/models/coupon_spec.rb
271
276
  - spec/models/invitation_spec.rb
272
277
  - spec/models/limit_spec.rb
273
278
  - spec/models/membership_spec.rb
@@ -317,7 +322,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
317
322
  requirements: []
318
323
 
319
324
  rubyforge_project:
320
- rubygems_version: 1.6.1
325
+ rubygems_version: 1.6.2
321
326
  signing_key:
322
327
  specification_version: 3
323
328
  summary: Clearance-based Rails engine for SaaS