koudoku 0.0.2
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/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +111 -0
- data/Rakefile +1 -0
- data/koudoku.gemspec +24 -0
- data/lib/koudoku.rb +5 -0
- data/lib/koudoku/subscription.rb +188 -0
- data/lib/koudoku/version.rb +3 -0
- metadata +76 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# Koudoku
|
2
|
+
|
3
|
+
Robust subscription support for Ruby on Rails apps using [Stripe](https://stripe.com). Makes it easy to manage actions related to new subscriptions, upgrades, downgrades, cancelations, as well as hooking up notifications, metrics logging, coupons, etc.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Include the following in your `Gemfile`:
|
8
|
+
|
9
|
+
gem 'koudoku'
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
There are no generators at the moment, so you'll need to generate plans, subscriptions, and (optionally) coupons on your own:
|
14
|
+
|
15
|
+
### Subscriptions
|
16
|
+
|
17
|
+
rails g model subscription stripe_id:string plan_id:integer last_four:string coupon_id:integer current_price:float user_id:integer
|
18
|
+
|
19
|
+
Only include `coupon_id` if you want to support coupons. The `user_id` property should actually be the foreign key for whatever model you want your subscriptions to relate to. (User is just the default.)
|
20
|
+
|
21
|
+
Then, dress up your subscription model by including the `Koudoku::Subscription` module and defining some essential relationships:
|
22
|
+
|
23
|
+
class Subscription < ActiveRecord::Base
|
24
|
+
include Koudoku::Subscription
|
25
|
+
|
26
|
+
# Belongs to user. (This is the default.)
|
27
|
+
attr_accessible :user_id
|
28
|
+
belongs_to :user
|
29
|
+
|
30
|
+
# Supports coupons.
|
31
|
+
attr_accessible :coupon_id
|
32
|
+
belongs_to :coupon
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
### Plans
|
37
|
+
|
38
|
+
rails g model plan name:string stripe_id:string price:float
|
39
|
+
|
40
|
+
The `stripe_id` for each plan must match the ID from Stripe. The price here isn't affected by (nor does it affect) the price in Stripe.
|
41
|
+
|
42
|
+
You'll need to create a few plans to start. (You don't need to create a plan to represent "free" accounts.)
|
43
|
+
|
44
|
+
Plan.create(name: 'Personal', price: '10.00')
|
45
|
+
Plan.create(name: 'Team', price: '30.00')
|
46
|
+
Plan.create(name: 'Enterprise', price: '100.00')
|
47
|
+
|
48
|
+
### Coupons
|
49
|
+
|
50
|
+
Again, this is only required if you want to support coupons:
|
51
|
+
|
52
|
+
rails g model coupon code:string free_trial_length:string
|
53
|
+
|
54
|
+
|
55
|
+
## Subscriptions That Belong to Models Other than User
|
56
|
+
|
57
|
+
Here's an example of a subscription that belongs to a company rather than a user:
|
58
|
+
|
59
|
+
class Subscription < ActiveRecord::Base
|
60
|
+
include Koudoku::Subscription
|
61
|
+
|
62
|
+
# Ownership.
|
63
|
+
attr_accessible :company_id
|
64
|
+
belongs_to :company
|
65
|
+
|
66
|
+
# Inform Koudoku::Subscription how to identify the owner of the subscription.
|
67
|
+
def subscription_owner
|
68
|
+
company
|
69
|
+
end
|
70
|
+
|
71
|
+
# Inform Koudoku::Subscription how to represent the owner in emails, etc.
|
72
|
+
def subscription_owner_description
|
73
|
+
"#{company.name} (#{company.primary_contact_name})"
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
## Using Coupons
|
80
|
+
|
81
|
+
While more robust coupon support is expected in the future, the simple way to use a coupon is to first create it:
|
82
|
+
|
83
|
+
coupon = Coupon.create(code: '30-days-free', free_trial_length: 30)
|
84
|
+
|
85
|
+
Then assign it to a _new_ subscription before saving:
|
86
|
+
|
87
|
+
subscription = Subscription.new(...)
|
88
|
+
subscription.coupon = coupon
|
89
|
+
subscription.save
|
90
|
+
|
91
|
+
|
92
|
+
## Implementing Logging, Notifications, etc.
|
93
|
+
|
94
|
+
The included module defined the following empty "template methods" which you're able to provide an implementation for:
|
95
|
+
|
96
|
+
- `prepare_for_plan_change`
|
97
|
+
- `prepare_for_new_subscription`
|
98
|
+
- `prepare_for_upgrade`
|
99
|
+
- `prepare_for_downgrade`
|
100
|
+
- `prepare_for_cancelation`
|
101
|
+
- `finalize_plan_change!`
|
102
|
+
- `finalize_new_subscription!`
|
103
|
+
- `finalize_upgrade!`
|
104
|
+
- `finalize_downgrade!`
|
105
|
+
- `finalize_cancelation!`
|
106
|
+
- `card_was_declined`
|
107
|
+
|
108
|
+
Be sure to include a call to `super` in each of your implementations, especially if you're using multiple concerns to break all this logic into smaller pieces.
|
109
|
+
|
110
|
+
Between `prepare_for_*` and `finalize_*`, so far I've used `finalize_*` almost exclusively. The difference is that `prepare_for_*` runs before we settle things with Stripe, and `finalize_*` runs after everything is settled in Stripe. For that reason, please be sure not to implement anything in `finalize_*` implementations that might cause issues with ActiveRecord saving the updated state of the subscription.
|
111
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/koudoku.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "koudoku/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "koudoku"
|
7
|
+
s.version = Koudoku::VERSION
|
8
|
+
s.authors = ["Andrew Culver"]
|
9
|
+
s.email = ["andrew.culver@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/andrewculver/koudoku"
|
11
|
+
s.summary = %q{Robust subscription support for Rails with Stripe.}
|
12
|
+
s.description = %q{Robust subscription support for Rails with Stripe. Provides package levels, coupons, logging, notifications, etc.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "koudoku"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency "rails"
|
22
|
+
s.add_dependency "stripe"
|
23
|
+
|
24
|
+
end
|
data/lib/koudoku.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
module Koudoku::Subscription
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
|
6
|
+
attr_accessible :plan_id, :stripe_id, :current_price
|
7
|
+
|
8
|
+
# We don't store these one-time use tokens, but this is what Strie provides
|
9
|
+
# client-side after storing the credit card information.
|
10
|
+
attr_accessor :credit_card_token
|
11
|
+
|
12
|
+
belongs_to :plan
|
13
|
+
|
14
|
+
# update details.
|
15
|
+
before_save do
|
16
|
+
|
17
|
+
# if their package level has changed ..
|
18
|
+
if changing_plans?
|
19
|
+
|
20
|
+
prepare_for_plan_change
|
21
|
+
|
22
|
+
# and a customer exists in stripe ..
|
23
|
+
if stripe_id.present?
|
24
|
+
|
25
|
+
# fetch the customer.
|
26
|
+
customer = Stripe::Customer.retrieve(self.stripe_id)
|
27
|
+
|
28
|
+
# if a new plan has been selected
|
29
|
+
if self.plan.present?
|
30
|
+
|
31
|
+
# Record the new plan pricing.
|
32
|
+
self.current_price = self.plan.price
|
33
|
+
|
34
|
+
prepare_for_downgrade if downgrading?
|
35
|
+
prepare_for_upgrade if upgrading?
|
36
|
+
|
37
|
+
# update the package level with stripe.
|
38
|
+
customer.update_subscription(:plan => self.plan.stripe_id)
|
39
|
+
|
40
|
+
finalize_downgrade! if downgrading?
|
41
|
+
finalize_upgrade! if upgrading?
|
42
|
+
|
43
|
+
# if no plan has been selected.
|
44
|
+
else
|
45
|
+
|
46
|
+
prepare_for_cancelation
|
47
|
+
|
48
|
+
# Remove the current pricing.
|
49
|
+
self.current_price = nil
|
50
|
+
|
51
|
+
# delete the subscription.
|
52
|
+
customer.cancel_subscription
|
53
|
+
|
54
|
+
finalize_cancelation!
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
# otherwise
|
59
|
+
else
|
60
|
+
|
61
|
+
# if a new plan has been selected
|
62
|
+
if self.plan.present?
|
63
|
+
|
64
|
+
# Record the new plan pricing.
|
65
|
+
self.current_price = self.plan.price
|
66
|
+
|
67
|
+
prepare_for_new_subscription
|
68
|
+
prepare_for_upgrade
|
69
|
+
|
70
|
+
begin
|
71
|
+
|
72
|
+
customer_attributes = {
|
73
|
+
description: subscription_owner_description,
|
74
|
+
card: credit_card_token, # obtained with Stripe.js
|
75
|
+
plan: plan.stripe_id
|
76
|
+
}
|
77
|
+
|
78
|
+
# If the class we're being included in supports coupons ..
|
79
|
+
if respond_to? :coupon
|
80
|
+
if coupon.present? and coupon.free_trial?
|
81
|
+
customer_attributes[:trial_end] = coupon.free_trial_ends.to_i
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# create a customer at that package level.
|
86
|
+
customer = Stripe::Customer.create(customer_attributes)
|
87
|
+
|
88
|
+
rescue Stripe::CardError => card_error
|
89
|
+
errors[:base] << card_error.message
|
90
|
+
card_was_declined
|
91
|
+
return false
|
92
|
+
end
|
93
|
+
|
94
|
+
# store the customer id.
|
95
|
+
self.stripe_id = customer.id
|
96
|
+
self.last_four = customer.active_card.last4
|
97
|
+
|
98
|
+
finalize_new_subscription!
|
99
|
+
finalize_upgrade!
|
100
|
+
|
101
|
+
else
|
102
|
+
|
103
|
+
# This should never happen.
|
104
|
+
|
105
|
+
self.plan_id = nil
|
106
|
+
|
107
|
+
# Remove any plan pricing.
|
108
|
+
self.current_price = nil
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
finalize_plan_change!
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
module ClassMethods
|
123
|
+
end
|
124
|
+
|
125
|
+
module InstanceMethods
|
126
|
+
|
127
|
+
# Pretty sure this wouldn't conflict with anything someone would put in their model
|
128
|
+
def subscription_owner
|
129
|
+
# Return whatever we belong to.
|
130
|
+
# If this object doesn't respond to 'name', please update owner_description.
|
131
|
+
user
|
132
|
+
end
|
133
|
+
|
134
|
+
def subscription_owner_description
|
135
|
+
# assuming owner responds to name.
|
136
|
+
# we should check for whether it responds to this or not.
|
137
|
+
"#{subscription_owner.name} (#{subscription_owner.id})"
|
138
|
+
end
|
139
|
+
|
140
|
+
def changing_plans?
|
141
|
+
plan_id_changed?
|
142
|
+
end
|
143
|
+
|
144
|
+
def downgrading?
|
145
|
+
plan.present? and plan_id_was.present? and plan_id_was > self.plan_id
|
146
|
+
end
|
147
|
+
|
148
|
+
def upgrading?
|
149
|
+
(plan_id_was.present? and plan_id_was < plan_id) or plan_id_was.nil?
|
150
|
+
end
|
151
|
+
|
152
|
+
# Template methods.
|
153
|
+
def prepare_for_plan_change
|
154
|
+
end
|
155
|
+
|
156
|
+
def prepare_for_new_subscription
|
157
|
+
end
|
158
|
+
|
159
|
+
def prepare_for_upgrade
|
160
|
+
end
|
161
|
+
|
162
|
+
def prepare_for_downgrade
|
163
|
+
end
|
164
|
+
|
165
|
+
def prepare_for_cancelation
|
166
|
+
end
|
167
|
+
|
168
|
+
def finalize_plan_change!
|
169
|
+
end
|
170
|
+
|
171
|
+
def finalize_new_subscription!
|
172
|
+
end
|
173
|
+
|
174
|
+
def finalize_upgrade!
|
175
|
+
end
|
176
|
+
|
177
|
+
def finalize_downgrade!
|
178
|
+
end
|
179
|
+
|
180
|
+
def finalize_cancelation!
|
181
|
+
end
|
182
|
+
|
183
|
+
def card_was_declined
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: koudoku
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Culver
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: &70303128505700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70303128505700
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: stripe
|
27
|
+
requirement: &70303128502880 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70303128502880
|
36
|
+
description: Robust subscription support for Rails with Stripe. Provides package levels,
|
37
|
+
coupons, logging, notifications, etc.
|
38
|
+
email:
|
39
|
+
- andrew.culver@gmail.com
|
40
|
+
executables: []
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- Gemfile
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- koudoku.gemspec
|
49
|
+
- lib/koudoku.rb
|
50
|
+
- lib/koudoku/subscription.rb
|
51
|
+
- lib/koudoku/version.rb
|
52
|
+
homepage: http://github.com/andrewculver/koudoku
|
53
|
+
licenses: []
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project: koudoku
|
72
|
+
rubygems_version: 1.8.15
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Robust subscription support for Rails with Stripe.
|
76
|
+
test_files: []
|