koudoku 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in koudoku.gemspec
4
+ gemspec
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,5 @@
1
+ require "koudoku/version"
2
+ require "koudoku/subscription"
3
+
4
+ module Koudoku
5
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Koudoku
2
+ VERSION = "0.0.2"
3
+ 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: []