koudoku 0.0.8 → 0.0.9
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.
- data/README.md +19 -3
- data/Rakefile +6 -13
- data/app/concerns/koudoku/plan.rb +2 -6
- data/app/concerns/koudoku/subscription.rb +73 -76
- data/app/controllers/koudoku/webhooks_controller.rb +2 -3
- data/app/helpers/koudoku/application_helper.rb +22 -0
- data/app/views/koudoku/subscriptions/_pricing_table.html.erb +1 -1
- data/{test/dummy/public/favicon.ico → config/environment.rb} +0 -0
- data/lib/generators/koudoku/install_generator.rb +1 -1
- data/lib/koudoku.rb +2 -2
- data/lib/koudoku/engine.rb +6 -0
- data/lib/koudoku/version.rb +1 -1
- data/spec/concerns/koudoku/plan_spec.rb +39 -0
- data/spec/concerns/koudoku/subscription_spec.rb +0 -0
- data/spec/controllers/koudoku/subscriptions_controller.rb +21 -0
- data/spec/controllers/koudoku/webhooks_controller_spec.rb +76 -0
- data/{test → spec}/dummy/README.rdoc +0 -0
- data/{test → spec}/dummy/Rakefile +0 -0
- data/{test → spec}/dummy/app/assets/javascripts/application.js +0 -0
- data/{test → spec}/dummy/app/assets/stylesheets/application.css +0 -0
- data/{test → spec}/dummy/app/controllers/application_controller.rb +0 -0
- data/{test → spec}/dummy/app/helpers/application_helper.rb +0 -0
- data/spec/dummy/app/models/coupon.rb +3 -0
- data/spec/dummy/app/models/customer.rb +7 -0
- data/spec/dummy/app/models/plan.rb +10 -0
- data/spec/dummy/app/models/subscription.rb +8 -0
- data/spec/dummy/app/views/koudoku/subscriptions/_social_proof.html.erb +11 -0
- data/{test → spec}/dummy/app/views/layouts/application.html.erb +0 -0
- data/{test → spec}/dummy/config.ru +0 -0
- data/{test → spec}/dummy/config/application.rb +2 -0
- data/{test → spec}/dummy/config/boot.rb +0 -0
- data/{test → spec}/dummy/config/database.yml +0 -0
- data/{test → spec}/dummy/config/environment.rb +0 -0
- data/{test → spec}/dummy/config/environments/development.rb +0 -0
- data/{test → spec}/dummy/config/environments/production.rb +0 -0
- data/{test → spec}/dummy/config/environments/test.rb +0 -0
- data/{test → spec}/dummy/config/initializers/backtrace_silencers.rb +0 -0
- data/spec/dummy/config/initializers/devise.rb +0 -0
- data/{test → spec}/dummy/config/initializers/inflections.rb +0 -0
- data/spec/dummy/config/initializers/koudoku.rb +7 -0
- data/{test → spec}/dummy/config/initializers/mime_types.rb +0 -0
- data/{test → spec}/dummy/config/initializers/secret_token.rb +0 -0
- data/{test → spec}/dummy/config/initializers/session_store.rb +0 -0
- data/{test → spec}/dummy/config/initializers/wrap_parameters.rb +0 -0
- data/spec/dummy/config/locales/devise.en.yml +59 -0
- data/{test → spec}/dummy/config/locales/en.yml +0 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20130318201927_create_customers.rb +8 -0
- data/spec/dummy/db/migrate/20130318204455_create_subscriptions.rb +16 -0
- data/spec/dummy/db/migrate/20130318204458_create_plans.rb +14 -0
- data/spec/dummy/db/migrate/20130318204502_create_coupons.rb +10 -0
- data/spec/dummy/db/migrate/20130520163946_add_interval_to_plan.rb +5 -0
- data/spec/dummy/db/schema.rb +53 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +931 -0
- data/spec/dummy/log/test.log +2728 -0
- data/{test → spec}/dummy/public/404.html +0 -0
- data/{test → spec}/dummy/public/422.html +0 -0
- data/{test → spec}/dummy/public/500.html +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/{test → spec}/dummy/script/rails +0 -0
- data/spec/dummy/spec/factories/coupons.rb +8 -0
- data/spec/dummy/spec/factories/plans.rb +12 -0
- data/spec/dummy/spec/factories/subscriptions.rb +14 -0
- data/spec/dummy/spec/models/coupon_spec.rb +5 -0
- data/spec/dummy/spec/models/plan_spec.rb +5 -0
- data/spec/dummy/spec/models/subscription_spec.rb +5 -0
- data/spec/dummy/test/fixtures/customers.yml +7 -0
- data/{test/integration/navigation_test.rb → spec/dummy/test/unit/customer_test.rb} +1 -4
- data/spec/helpers/koudoku/application_helper_spec.rb +29 -0
- data/spec/spec_helper.rb +31 -0
- metadata +218 -67
- data/test/dummy/config/routes.rb +0 -4
- data/test/koudoku_test.rb +0 -7
- data/test/test_helper.rb +0 -15
data/README.md
CHANGED
@@ -15,13 +15,15 @@ After running `bundle install`, you can run a Rails generator to do the rest. Be
|
|
15
15
|
rails g koudoku:install user
|
16
16
|
rake db:migrate
|
17
17
|
|
18
|
-
After installing, you'll need to add some subscription plans. (
|
18
|
+
After installing, you'll need to add some subscription plans. (You can see an explanation of each of the attributes in the table below.)
|
19
19
|
|
20
20
|
Plan.create({
|
21
21
|
name: 'Personal',
|
22
22
|
price: 10.00,
|
23
|
-
|
24
|
-
|
23
|
+
interval: 'month',
|
24
|
+
stripe_id: '1',
|
25
|
+
features: ['1 Project', '1 Page', '1 User', '1 Organization'].join("\n\n"),
|
26
|
+
interval: 'month',
|
25
27
|
display_order: 1
|
26
28
|
})
|
27
29
|
|
@@ -29,6 +31,7 @@ After installing, you'll need to add some subscription plans. (Note that we high
|
|
29
31
|
name: 'Team',
|
30
32
|
highlight: true, # This highlights the plan on the pricing page.
|
31
33
|
price: 30.00,
|
34
|
+
interval: 'month',
|
32
35
|
stripe_id: '2',
|
33
36
|
features: ['3 Projects', '3 Pages', '3 Users', '3 Organizations'].join("\n\n"),
|
34
37
|
display_order: 2
|
@@ -37,11 +40,24 @@ After installing, you'll need to add some subscription plans. (Note that we high
|
|
37
40
|
Plan.create({
|
38
41
|
name: 'Enterprise',
|
39
42
|
price: 100.00,
|
43
|
+
interval: 'month',
|
40
44
|
stripe_id: '3',
|
41
45
|
features: ['10 Projects', '10 Pages', '10 Users', '10 Organizations'].join("\n\n"),
|
42
46
|
display_order: 3
|
43
47
|
})
|
44
48
|
|
49
|
+
To help you understand the attributes:
|
50
|
+
|
51
|
+
| Attribute | Type | Function |
|
52
|
+
| --------------- | ------- | -------- |
|
53
|
+
| `name` | string | Name for the plan to be presented to customers. |
|
54
|
+
| `price` | float | Price per billing cycle. |
|
55
|
+
| `interval` | string | *Optional.* What is the billing cycle? Valid options are `month`, `year`, `week`, `3-month`, `6-month`. Defaults to `month`. |
|
56
|
+
| `stripe_id` | string | The Plan ID in Stripe. |
|
57
|
+
| `features` | string | A list of features. Supports Markdown syntax. |
|
58
|
+
| `display_order` | integer | Order in which to display plans. |
|
59
|
+
| `highlight` | boolean | *Optional.* Whether to highlight the plan on the pricing page. |
|
60
|
+
|
45
61
|
The only view installed locally into your app by default is the `koudoku/subscriptions/_social_proof.html.erb` partial which is displayed alongside the pricing table. It's designed as a placeholder where you can provide quotes about your product from customers that could positively influence your visitors.
|
46
62
|
|
47
63
|
### Configuring Stripe API Keys
|
data/Rakefile
CHANGED
@@ -20,21 +20,14 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
20
20
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
21
|
end
|
22
22
|
|
23
|
-
APP_RAKEFILE = File.expand_path("../
|
23
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
24
24
|
load 'rails/tasks/engine.rake'
|
25
25
|
|
26
|
-
|
27
|
-
|
28
26
|
Bundler::GemHelper.install_tasks
|
29
27
|
|
30
|
-
require '
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
t.libs << 'test'
|
35
|
-
t.pattern = 'test/**/*_test.rb'
|
36
|
-
t.verbose = false
|
37
|
-
end
|
38
|
-
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
31
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
39
32
|
|
40
|
-
task :default => :
|
33
|
+
task :default => :spec
|
@@ -12,7 +12,8 @@ module Koudoku::Subscription
|
|
12
12
|
belongs_to :plan
|
13
13
|
|
14
14
|
# update details.
|
15
|
-
before_save
|
15
|
+
before_save :processing!
|
16
|
+
def processing!
|
16
17
|
|
17
18
|
# if their package level has changed ..
|
18
19
|
if changing_plans?
|
@@ -137,103 +138,99 @@ module Koudoku::Subscription
|
|
137
138
|
module ClassMethods
|
138
139
|
end
|
139
140
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
if persisted?
|
145
|
-
"Upgrade"
|
146
|
-
else
|
147
|
-
if Koudoku.free_trial?
|
148
|
-
"Start Trial"
|
149
|
-
else
|
150
|
-
"Upgrade"
|
151
|
-
end
|
152
|
-
end
|
141
|
+
def describe_difference(plan_to_describe)
|
142
|
+
if plan.nil?
|
143
|
+
if persisted?
|
144
|
+
"Upgrade"
|
153
145
|
else
|
154
|
-
if
|
146
|
+
if Koudoku.free_trial?
|
147
|
+
"Start Trial"
|
148
|
+
else
|
155
149
|
"Upgrade"
|
156
|
-
else
|
157
|
-
"Downgrade"
|
158
150
|
end
|
159
151
|
end
|
152
|
+
else
|
153
|
+
if plan_to_describe.is_upgrade_from?(plan)
|
154
|
+
"Upgrade"
|
155
|
+
else
|
156
|
+
"Downgrade"
|
157
|
+
end
|
160
158
|
end
|
159
|
+
end
|
161
160
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
161
|
+
# Pretty sure this wouldn't conflict with anything someone would put in their model
|
162
|
+
def subscription_owner
|
163
|
+
# Return whatever we belong to.
|
164
|
+
# If this object doesn't respond to 'name', please update owner_description.
|
165
|
+
send Koudoku.subscriptions_owned_by
|
166
|
+
end
|
168
167
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
168
|
+
def subscription_owner_description
|
169
|
+
# assuming owner responds to name.
|
170
|
+
# we should check for whether it responds to this or not.
|
171
|
+
"#{subscription_owner.id}"
|
172
|
+
end
|
174
173
|
|
175
|
-
|
176
|
-
|
177
|
-
|
174
|
+
def changing_plans?
|
175
|
+
plan_id_changed?
|
176
|
+
end
|
178
177
|
|
179
|
-
|
180
|
-
|
181
|
-
|
178
|
+
def downgrading?
|
179
|
+
plan.present? and plan_id_was.present? and plan_id_was > self.plan_id
|
180
|
+
end
|
182
181
|
|
183
|
-
|
184
|
-
|
185
|
-
|
182
|
+
def upgrading?
|
183
|
+
(plan_id_was.present? and plan_id_was < plan_id) or plan_id_was.nil?
|
184
|
+
end
|
186
185
|
|
187
|
-
|
188
|
-
|
189
|
-
|
186
|
+
# Template methods.
|
187
|
+
def prepare_for_plan_change
|
188
|
+
end
|
190
189
|
|
191
|
-
|
192
|
-
|
190
|
+
def prepare_for_new_subscription
|
191
|
+
end
|
193
192
|
|
194
|
-
|
195
|
-
|
193
|
+
def prepare_for_upgrade
|
194
|
+
end
|
196
195
|
|
197
|
-
|
198
|
-
|
196
|
+
def prepare_for_downgrade
|
197
|
+
end
|
199
198
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
199
|
+
def prepare_for_cancelation
|
200
|
+
end
|
201
|
+
|
202
|
+
def prepare_for_card_update
|
203
|
+
end
|
205
204
|
|
206
|
-
|
207
|
-
|
205
|
+
def finalize_plan_change!
|
206
|
+
end
|
208
207
|
|
209
|
-
|
210
|
-
|
208
|
+
def finalize_new_subscription!
|
209
|
+
end
|
211
210
|
|
212
|
-
|
213
|
-
|
211
|
+
def finalize_upgrade!
|
212
|
+
end
|
214
213
|
|
215
|
-
|
216
|
-
|
214
|
+
def finalize_downgrade!
|
215
|
+
end
|
217
216
|
|
218
|
-
|
219
|
-
|
217
|
+
def finalize_cancelation!
|
218
|
+
end
|
220
219
|
|
221
|
-
|
222
|
-
|
220
|
+
def finalize_card_update!
|
221
|
+
end
|
223
222
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
end
|
236
|
-
|
223
|
+
def card_was_declined
|
224
|
+
end
|
225
|
+
|
226
|
+
# stripe web-hook callbacks.
|
227
|
+
def payment_succeeded(amount)
|
228
|
+
end
|
229
|
+
|
230
|
+
def charge_failed
|
231
|
+
end
|
232
|
+
|
233
|
+
def charge_disputed
|
237
234
|
end
|
238
235
|
|
239
236
|
end
|
@@ -2,14 +2,14 @@ module Koudoku
|
|
2
2
|
class WebhooksController < ApplicationController
|
3
3
|
|
4
4
|
def create
|
5
|
-
|
5
|
+
|
6
6
|
raise "API key not configured. For security reasons you must configure this in 'config/koudoku.rb'." unless Koudoku.webhooks_api_key.present?
|
7
7
|
raise "Invalid API key. Be sure the webhooks URL Stripe is configured with includes ?api_key= and the correct key." unless params[:api_key] == Koudoku.webhooks_api_key
|
8
8
|
|
9
9
|
data_json = JSON.parse request.body.read
|
10
10
|
|
11
11
|
if data_json['type'] == "invoice.payment_succeeded"
|
12
|
-
|
12
|
+
|
13
13
|
stripe_id = data_json['data']['object']['customer']
|
14
14
|
amount = data_json['data']['object']['total'].to_f / 100.0
|
15
15
|
subscription = ::Subscription.find_by_stripe_id(stripe_id)
|
@@ -27,7 +27,6 @@ module Koudoku
|
|
27
27
|
stripe_id = data_json['data']['object']['customer']
|
28
28
|
|
29
29
|
subscription = ::Subscription.find_by_stripe_id(stripe_id)
|
30
|
-
listing = subscription.listing
|
31
30
|
subscription.charge_disputed
|
32
31
|
|
33
32
|
end
|
@@ -1,4 +1,26 @@
|
|
1
1
|
module Koudoku
|
2
2
|
module ApplicationHelper
|
3
|
+
|
4
|
+
def plan_price(plan)
|
5
|
+
"#{number_to_currency(plan.price)}/#{plan_interval(plan)}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def plan_interval(plan)
|
9
|
+
case plan.interval
|
10
|
+
when "month"
|
11
|
+
"month"
|
12
|
+
when "year"
|
13
|
+
"year"
|
14
|
+
when "week"
|
15
|
+
"week"
|
16
|
+
when "6-month"
|
17
|
+
"half-year"
|
18
|
+
when "3-month"
|
19
|
+
"quarter"
|
20
|
+
else
|
21
|
+
"month"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
3
25
|
end
|
4
26
|
end
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<div class="thumbnail">
|
5
5
|
<div class="caption">
|
6
6
|
<h3><%= plan.name %></h3>
|
7
|
-
<h4><%=
|
7
|
+
<h4><%= plan_price(plan) %></h4>
|
8
8
|
<div class="call-to-action">
|
9
9
|
<% if @subscription.nil? %>
|
10
10
|
<%= link_to Koudoku.free_trial? ? 'Start Trial' : 'Sign Up', koudoku.new_subscription_path(plan: plan.id), class: "btn btn-success btn-large" %>
|
File without changes
|
@@ -42,7 +42,7 @@ RUBY
|
|
42
42
|
gsub_file "app/models/subscription.rb", /ActiveRecord::Base/, "ActiveRecord::Base\n include Koudoku::Subscription\n\n belongs_to :#{subscription_owner_model}\n belongs_to :coupon\n"
|
43
43
|
|
44
44
|
# Add the plans.
|
45
|
-
generate("model", "plan name:string stripe_id:string price:float features:text highlight:boolean display_order:integer")
|
45
|
+
generate("model", "plan name:string stripe_id:string price:float interval:string features:text highlight:boolean display_order:integer")
|
46
46
|
gsub_file "app/models/plan.rb", /ActiveRecord::Base/, "ActiveRecord::Base\n include Koudoku::Plan\n belongs_to :#{subscription_owner_model}\n belongs_to :coupon\n has_many :subscriptions\n"
|
47
47
|
|
48
48
|
# Add coupons.
|
data/lib/koudoku.rb
CHANGED
data/lib/koudoku/engine.rb
CHANGED
@@ -3,5 +3,11 @@ require 'bluecloth'
|
|
3
3
|
module Koudoku
|
4
4
|
class Engine < ::Rails::Engine
|
5
5
|
isolate_namespace Koudoku
|
6
|
+
config.generators do |g|
|
7
|
+
g.test_framework :rspec, :fixture => false
|
8
|
+
g.fixture_replacement :factory_girl, :dir => 'spec/factories'
|
9
|
+
g.assets false
|
10
|
+
g.helper false
|
11
|
+
end
|
6
12
|
end
|
7
13
|
end
|
data/lib/koudoku/version.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Koudoku::Plan do
|
4
|
+
describe '#is_upgrade_from?' do
|
5
|
+
before do
|
6
|
+
class Plan
|
7
|
+
attr_accessor :price
|
8
|
+
include Koudoku::Plan
|
9
|
+
end
|
10
|
+
end
|
11
|
+
it 'returns true if the price is higher' do
|
12
|
+
plan = Plan.new
|
13
|
+
plan.price = 123.23
|
14
|
+
cheaper_plan = Plan.new
|
15
|
+
cheaper_plan.price = 61.61
|
16
|
+
plan.is_upgrade_from?(cheaper_plan).should be_true
|
17
|
+
end
|
18
|
+
it 'returns true if the price is the same' do
|
19
|
+
plan = Plan.new
|
20
|
+
plan.price = 123.23
|
21
|
+
plan.is_upgrade_from?(plan).should be_true
|
22
|
+
end
|
23
|
+
it 'returns false if the price is the same or higher' do
|
24
|
+
plan = Plan.new
|
25
|
+
plan.price = 61.61
|
26
|
+
more_expensive_plan = Plan.new
|
27
|
+
more_expensive_plan.price = 123.23
|
28
|
+
plan.is_upgrade_from?(more_expensive_plan).should be_false
|
29
|
+
end
|
30
|
+
it 'handles a nil value gracefully' do
|
31
|
+
plan = Plan.new
|
32
|
+
plan.price = 123.23
|
33
|
+
cheaper_plan = Plan.new
|
34
|
+
lambda {
|
35
|
+
plan.is_upgrade_from?(cheaper_plan).should be_true
|
36
|
+
}.should_not raise_error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Koudoku::SubscriptionsController do
|
4
|
+
describe 'when customer is signed in' do
|
5
|
+
before do
|
6
|
+
@customer = Customer.create(email: 'andrew.culver@gmail.com')
|
7
|
+
ApplicationController.any_instance.stub(:current_customer).and_return(@customer)
|
8
|
+
end
|
9
|
+
it 'works' do
|
10
|
+
get :index, use_route: 'koudoku'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
describe 'when customer is not signed in' do
|
14
|
+
before do
|
15
|
+
ApplicationController.any_instance.stub(:current_customer).and_return(nil)
|
16
|
+
end
|
17
|
+
it 'works' do
|
18
|
+
get :index, use_route: 'koudoku'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|