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 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: []