subscription_fu 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +16 -0
- data/LICENSE +20 -0
- data/README.md +96 -0
- data/Rakefile +6 -0
- data/app/models/subscription_fu/plan.rb +43 -0
- data/app/models/subscription_fu/subscription.rb +117 -0
- data/app/models/subscription_fu/transaction.rb +131 -0
- data/config/locales/en.yml +7 -0
- data/examples/routes.rb +7 -0
- data/examples/subscriptions_controller.rb +12 -0
- data/examples/transactions_controller.rb +33 -0
- data/lib/generators/subscription_fu/install_generator.rb +23 -0
- data/lib/generators/subscription_fu/templates/en.yml +17 -0
- data/lib/generators/subscription_fu/templates/initializer.rb +24 -0
- data/lib/generators/subscription_fu/templates/migration.rb +38 -0
- data/lib/subscription_fu/config.rb +36 -0
- data/lib/subscription_fu/engine.rb +5 -0
- data/lib/subscription_fu/models.rb +54 -0
- data/lib/subscription_fu/paypal.rb +101 -0
- data/lib/subscription_fu/railtie.rb +12 -0
- data/lib/subscription_fu/version.rb +3 -0
- data/lib/subscription_fu.rb +15 -0
- data/spec/app/.gitignore +1 -0
- data/spec/app/Rakefile +8 -0
- data/spec/app/app/controllers/application_controller.rb +6 -0
- data/spec/app/app/models/initiator.rb +2 -0
- data/spec/app/app/models/subject.rb +4 -0
- data/spec/app/app/views/layouts/application.html.haml +6 -0
- data/spec/app/config/application.rb +11 -0
- data/spec/app/config/boot.rb +4 -0
- data/spec/app/config/database.yml +18 -0
- data/spec/app/config/environment.rb +5 -0
- data/spec/app/config/environments/development.rb +18 -0
- data/spec/app/config/environments/test.rb +22 -0
- data/spec/app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/app/config/initializers/inflections.rb +2 -0
- data/spec/app/config/initializers/secret_token.rb +2 -0
- data/spec/app/config/initializers/subscription_fu.rb +28 -0
- data/spec/app/config/locales/subscription_fu.en.yml +17 -0
- data/spec/app/config/routes.rb +3 -0
- data/spec/app/config.ru +4 -0
- data/spec/app/db/.gitignore +1 -0
- data/spec/app/db/migrate/20110516061428_create_subjects.rb +13 -0
- data/spec/app/db/migrate/20110516061443_create_initiators.rb +14 -0
- data/spec/app/db/migrate/20110516070948_create_subscription_fu_tables.rb +38 -0
- data/spec/app/db/schema.rb +62 -0
- data/spec/app/script/rails +10 -0
- data/spec/factories/initiator.rb +4 -0
- data/spec/factories/subject.rb +2 -0
- data/spec/factories/subscription.rb +6 -0
- data/spec/factories/transaction.rb +7 -0
- data/spec/models/subscription_spec.rb +277 -0
- data/spec/models/subscription_transaction_spec.rb +135 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/paypal_test_helper.rb +82 -0
- data/subscription_fu.gemspec +23 -0
- metadata +134 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in galakei.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem 'rails'
|
8
|
+
gem 'sqlite3'
|
9
|
+
gem 'haml'
|
10
|
+
gem 'rspec', '>= 2.5.0'
|
11
|
+
gem 'rspec-rails'
|
12
|
+
gem 'shoulda-matchers'
|
13
|
+
gem 'factory_girl'
|
14
|
+
gem 'time_travel', '0.1.0'
|
15
|
+
gem 'webmock'
|
16
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Mobalean LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Subscriptions for Rails
|
2
|
+
|
3
|
+
This gem helps with building services which have paid subscriptions. It includes the models to store subscription status, and provides integration with PayPal for paid subscriptions.
|
4
|
+
|
5
|
+
## Assumptions
|
6
|
+
|
7
|
+
SubscriptionFu makes the following assumptions on how subscriptions are used:
|
8
|
+
|
9
|
+
* There is a subscription subject, i.e. a user or some other object which needs a subscription in order to access your site. In the examples below we'll use "group".
|
10
|
+
|
11
|
+
* You have a subscription initiator, which is usually the user object. In the examples below we'll use "user"
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add to your Gemfile:
|
16
|
+
|
17
|
+
gem 'subscription_fu', :git => "git://github.com/mobalean/subscription_fu.git"
|
18
|
+
|
19
|
+
Run "bundle install".
|
20
|
+
|
21
|
+
Then install the required files:
|
22
|
+
|
23
|
+
rails g subscription_fu:install
|
24
|
+
|
25
|
+
## Configuration
|
26
|
+
|
27
|
+
1. Edit config/initializers/subscription\_fu.rb (generated by the install generator)
|
28
|
+
|
29
|
+
2. Add "needs\_subscription" to the subscription subject:
|
30
|
+
|
31
|
+
class Group < ActiveRecord::Base
|
32
|
+
needs_subscription
|
33
|
+
...
|
34
|
+
end
|
35
|
+
|
36
|
+
3. Create subscriptions and transactions controllers and views, for example see examples
|
37
|
+
|
38
|
+
## General subscription flow
|
39
|
+
|
40
|
+
1. The user starts the subscription process by selecting a plan (if multiple ones are available, otherwise you can skip this and just have a subscribe button). We assume you'll do that selection in SubscriptionsController#new.
|
41
|
+
|
42
|
+
link_to image_tag("https://www.paypal.com/ja_JP/JP/i/btn/btn_xpressCheckout.gif", :alt => "PayPal で決済を行う"), subscription_path, :method => :post
|
43
|
+
|
44
|
+
2. The subscribe form posts to SubscriptionsController#create, which creates an inactive subscription and associated transaction, and finally redirects the user to a checkout URL:
|
45
|
+
|
46
|
+
@subscription = current_group.build_next_subscription("basic")
|
47
|
+
@subscription.save!
|
48
|
+
@transaction = @subscription.initiate_activation(current_user)
|
49
|
+
redirect_to @transaction.start_checkout(url_for(:action => :confirm, :controller => "transactions"), url_for(:action => :abort, :controller => "transactions"))
|
50
|
+
|
51
|
+
3. If the transaction gets approved, the user will get back to TransactionsController#confirm. Otherwise she might not return or return to TransactionsController#abort.
|
52
|
+
|
53
|
+
4. The TransactionsController#confirm is supposed to show the user a final confirmation screen before executing the transaction. To load a pending transaction, use a before filter like this:
|
54
|
+
|
55
|
+
before_filter :require_valid_transaction
|
56
|
+
def require_valid_transaction
|
57
|
+
@token = params[:token]
|
58
|
+
@transaction = current_group.pending_transaction(@token)
|
59
|
+
unless @transaction
|
60
|
+
logger.info("Invalid transaction for token: #{@token}")
|
61
|
+
flash[:error] = "Invalid transaction, please try again."
|
62
|
+
redirect_to root_path
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
The form would look like this: (HAML with simple_form)
|
67
|
+
|
68
|
+
= simple_form_for @transaction, :url => transaction_path do |f|
|
69
|
+
= hidden_field_tag :token, @token
|
70
|
+
.submit= f.button :submit, '申込む'
|
71
|
+
|
72
|
+
5. The confirmation form posts to TransactionsController#update, which completes the transaction:
|
73
|
+
|
74
|
+
if @transaction.complete
|
75
|
+
flash[:notice] = "Sucessfully updated your subscription."
|
76
|
+
else
|
77
|
+
flash[:error] = "Transaction was not successfull, please try again."
|
78
|
+
end
|
79
|
+
redirect_to root_path
|
80
|
+
|
81
|
+
That's it.
|
82
|
+
|
83
|
+
## Using the subscriptions
|
84
|
+
|
85
|
+
Once setup, your subscription subject (the group in our example) will get a couple new methods. To check whether or not it has a subscription, use:
|
86
|
+
|
87
|
+
group.active_subscription?
|
88
|
+
|
89
|
+
For getting the group's plan, you can simply use:
|
90
|
+
|
91
|
+
group.subscription_plan
|
92
|
+
|
93
|
+
Which will give you an instance of the subscription plan as defined in the initializer.
|
94
|
+
|
95
|
+
For more details, see SubscriptionFu::Models
|
96
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
class SubscriptionFu::Plan
|
2
|
+
include ActionView::Helpers::NumberHelper # for number_to_currency
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
TAX = 0.05
|
6
|
+
|
7
|
+
attr_accessor :key
|
8
|
+
attr_accessor :price
|
9
|
+
|
10
|
+
def initialize(key, price, data)
|
11
|
+
self.key = key
|
12
|
+
self.price = price
|
13
|
+
data.each {|k,v| self.send("#{k}=", v) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def human_name
|
17
|
+
I18n.t(key, :scope => [:subscription_fu, :plan, :options])
|
18
|
+
end
|
19
|
+
|
20
|
+
def human_price
|
21
|
+
number_to_currency(price_with_tax)
|
22
|
+
end
|
23
|
+
|
24
|
+
def free_plan?
|
25
|
+
price == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def price_with_tax
|
29
|
+
(price * (1.0 + TAX)).to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def price_tax
|
33
|
+
(price * TAX).to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def currency
|
37
|
+
"JPY"
|
38
|
+
end
|
39
|
+
|
40
|
+
def <=>(other)
|
41
|
+
price <=> other.price
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
class SubscriptionFu::Subscription < ActiveRecord::Base
|
2
|
+
set_table_name :subscriptions
|
3
|
+
|
4
|
+
AVAILABLE_CANCEL_REASONS = %w( update cancel timeout admin )
|
5
|
+
|
6
|
+
default_scope order("created_at ASC", "id ASC")
|
7
|
+
|
8
|
+
belongs_to :subject, :polymorphic => true
|
9
|
+
belongs_to :prev_subscription, :class_name => "SubscriptionFu::Subscription"
|
10
|
+
has_many :transactions, :class_name => "SubscriptionFu::Transaction"
|
11
|
+
has_many :next_subscriptions, :class_name => "SubscriptionFu::Subscription", :foreign_key => "prev_subscription_id"
|
12
|
+
|
13
|
+
validates :subject, :presence => true
|
14
|
+
validates :plan_key, :presence => true, :inclusion => SubscriptionFu.config.available_plans.keys, :on => :create
|
15
|
+
validates :starts_at, :presence => true
|
16
|
+
validates :billing_starts_at, :presence => true
|
17
|
+
validates :paypal_profile_id, :presence => true, :if => :activated_paid_subscription?
|
18
|
+
validates :cancel_reason, :presence => true, :inclusion => AVAILABLE_CANCEL_REASONS, :if => :canceled?
|
19
|
+
|
20
|
+
scope :activated, where("subscriptions.activated_at IS NOT NULL")
|
21
|
+
scope :current, lambda {|time| activated.where("subscriptions.starts_at <= ? AND (subscriptions.canceled_at IS NULL OR subscriptions.canceled_at > ?)", time, time) }
|
22
|
+
|
23
|
+
# TODO this should probably only take plan?key, prev_sub
|
24
|
+
def self.build_for_initializing(plan_key, start_time = Time.now, billing_start_time = start_time, prev_sub = nil)
|
25
|
+
new(:plan_key => plan_key, :starts_at => start_time, :billing_starts_at => billing_start_time, :prev_subscription => prev_sub)
|
26
|
+
end
|
27
|
+
|
28
|
+
def paid_subscription?
|
29
|
+
! plan.free_plan? && ! sponsored?
|
30
|
+
end
|
31
|
+
|
32
|
+
def activated?
|
33
|
+
! activated_at.blank?
|
34
|
+
end
|
35
|
+
|
36
|
+
def activated_paid_subscription?
|
37
|
+
activated? && paid_subscription?
|
38
|
+
end
|
39
|
+
|
40
|
+
def canceled?
|
41
|
+
! canceled_at.blank?
|
42
|
+
end
|
43
|
+
|
44
|
+
def plan
|
45
|
+
SubscriptionFu.config.available_plans[self.plan_key]
|
46
|
+
end
|
47
|
+
|
48
|
+
def human_description
|
49
|
+
I18n.t(:description, :scope => [:subscription_fu, :subscription]) % {
|
50
|
+
:plan_name => plan.human_name,
|
51
|
+
:subject_desc => subject.human_description_for_subscription,
|
52
|
+
:price => plan.human_price }
|
53
|
+
end
|
54
|
+
|
55
|
+
# billing time data about the subscription
|
56
|
+
|
57
|
+
def next_billing_date
|
58
|
+
paypal_recurring_details[:next_billing_date]
|
59
|
+
end
|
60
|
+
|
61
|
+
def estimated_next_billing_date
|
62
|
+
p = last_billing_date
|
63
|
+
p.next_month unless p.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
def last_billing_date
|
67
|
+
paypal_recurring_details[:last_payment_date]
|
68
|
+
end
|
69
|
+
|
70
|
+
def successor_start_date(new_plan_name)
|
71
|
+
new_plan = SubscriptionFu.config.available_plans[new_plan_name]
|
72
|
+
if new_plan > self.plan
|
73
|
+
# higher plans always start immediately
|
74
|
+
Time.now
|
75
|
+
else
|
76
|
+
# otherwise they start with the next billing cycle
|
77
|
+
successor_billing_start_date
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def successor_billing_start_date
|
82
|
+
# in case this plan was already canceled, this date takes
|
83
|
+
# precedence (there won't be a next billing time anymore).
|
84
|
+
canceled_at || next_billing_date || estimated_next_billing_date || Time.now
|
85
|
+
end
|
86
|
+
|
87
|
+
# billing API
|
88
|
+
|
89
|
+
def initiate_activation(admin)
|
90
|
+
gateway = (plan.free_plan? || sponsored?) ? 'nogw' : 'paypal'
|
91
|
+
transactions.create_activation(gateway, admin).tap do |t|
|
92
|
+
if prev_subscription
|
93
|
+
to_cancel = [prev_subscription]
|
94
|
+
to_cancel.push(*prev_subscription.next_subscriptions.where("subscriptions.id <> ?", self).all)
|
95
|
+
to_cancel.each {|s| s.initiate_cancellation(admin, t) }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def initiate_cancellation(admin, activation_transaction)
|
101
|
+
transactions.create_cancellation(admin, activation_transaction, self)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def paypal_recurring_details
|
107
|
+
@paypal_recurring_details ||= (paypal_profile_id.blank? ? {} : SubscriptionFu::Paypal.paypal.recurring_details(paypal_profile_id))
|
108
|
+
end
|
109
|
+
|
110
|
+
def convert_paypal_status(paypal_status)
|
111
|
+
case paypal_status
|
112
|
+
when "ActiveProfile" then "complete"
|
113
|
+
when "PendingProfile" then "pending"
|
114
|
+
else "invalid"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
class SubscriptionFu::Transaction < ActiveRecord::Base
|
2
|
+
set_table_name :subscription_transactions
|
3
|
+
|
4
|
+
belongs_to :subscription
|
5
|
+
belongs_to :initiator, :polymorphic => true
|
6
|
+
belongs_to :related_transaction, :class_name => "SubscriptionFu::Transaction"
|
7
|
+
has_many :related_transactions, :class_name => "SubscriptionFu::Transaction", :foreign_key => "related_transaction_id"
|
8
|
+
|
9
|
+
delegate :plan, :human_description, :starts_at, :billing_starts_at, :paypal_profile_id, :activated?, :canceled?, :cancel_reason, :to => :subscription, :prefix => :sub
|
10
|
+
|
11
|
+
validates :subscription, :presence => true
|
12
|
+
validates :initiator, :presence => true
|
13
|
+
validates :gateway, :presence => true, :inclusion => %w( paypal nogw )
|
14
|
+
validates :action, :presence => true, :inclusion => %w( activation cancellation )
|
15
|
+
validates :status, :presence => true, :inclusion => %w( initiated complete failed aborted )
|
16
|
+
|
17
|
+
scope :paypal, where(:gateway => "paypal")
|
18
|
+
scope :initiated, where(:status => "initiated")
|
19
|
+
|
20
|
+
def self.create_activation(gateway, initiator)
|
21
|
+
create!(:initiator => initiator, :gateway => gateway, :status => 'initiated', :action => "activation")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.create_cancellation(initiator, related_transaction, subscription)
|
25
|
+
gateway = subscription.paypal_profile_id.blank? ? 'nogw' : 'paypal'
|
26
|
+
create!(:initiator => initiator, :gateway => gateway, :status => 'initiated', :action => "cancellation", :related_transaction => related_transaction)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initiator_email
|
30
|
+
initiator.email if initiator.respond_to?(:email)
|
31
|
+
end
|
32
|
+
|
33
|
+
# billing API
|
34
|
+
|
35
|
+
def needs_authorization?
|
36
|
+
gateway == "paypal" && action == "activation"
|
37
|
+
end
|
38
|
+
|
39
|
+
def start_checkout(return_url, cancel_url)
|
40
|
+
raise "start_checkout is only for activation" unless action == "activation"
|
41
|
+
raise "start_checkout is only for non-activated subscriptions" if sub_activated?
|
42
|
+
raise "start_checkout already called once, have a token" unless identifier.blank?
|
43
|
+
raise "start_checkout only available in initiated state, but: #{status}" unless status == "initiated"
|
44
|
+
|
45
|
+
send("start_checkout_#{gateway}", return_url, cancel_url)
|
46
|
+
end
|
47
|
+
|
48
|
+
def complete(opts = {})
|
49
|
+
raise "complete only available in initiated state, but: #{status}" unless status == "initiated"
|
50
|
+
|
51
|
+
success = true
|
52
|
+
begin
|
53
|
+
send("complete_#{action}_#{gateway}", opts)
|
54
|
+
update_attributes!(:status => "complete")
|
55
|
+
rescue Exception => err
|
56
|
+
if defined? ::ExceptionNotifier
|
57
|
+
data = (err.respond_to?(:data) ? err.data : {}).merge(:subscription => subscription.inspect, :transaction => self.inspect)
|
58
|
+
::ExceptionNotifier::Notifier.background_exception_notification(err, :data => data).deliver
|
59
|
+
else
|
60
|
+
logger.warn(err)
|
61
|
+
logger.debug(err.backtrace.join("\n"))
|
62
|
+
end
|
63
|
+
update_attributes!(:status => "failed")
|
64
|
+
related_transactions.each { |t| t.abort }
|
65
|
+
success = false
|
66
|
+
end
|
67
|
+
success
|
68
|
+
end
|
69
|
+
|
70
|
+
def abort
|
71
|
+
raise "abort only available in initiated state, but: #{status}" unless status == "initiated"
|
72
|
+
update_attributes(:status => "aborted")
|
73
|
+
related_transactions.each { |t| t.abort }
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def start_checkout_paypal(return_url, cancel_url)
|
80
|
+
token = SubscriptionFu::Paypal.paypal.start_checkout(return_url, cancel_url, initiator_email, sub_plan.price_with_tax, sub_plan.currency, sub_human_description)
|
81
|
+
update_attributes!(:identifier => token)
|
82
|
+
"#{SubscriptionFu.config.paypal_landing_url}?cmd=_express-checkout&token=#{CGI.escape(token)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def start_checkout_nogw(return_url, cancel_url)
|
86
|
+
update_attributes!(:identifier => SubscriptionFu.friendly_token)
|
87
|
+
return_url
|
88
|
+
end
|
89
|
+
|
90
|
+
def complete_activation_paypal(opts)
|
91
|
+
raise "did you call start_checkout first?" if identifier.blank?
|
92
|
+
raise "already activated" if sub_activated?
|
93
|
+
|
94
|
+
paypal_profile_id, paypal_status =
|
95
|
+
SubscriptionFu::Paypal.paypal.create_recurring(identifier, sub_billing_starts_at, sub_plan.price, sub_plan.price_tax, sub_plan.currency, sub_human_description)
|
96
|
+
subscription.update_attributes!(:paypal_profile_id => paypal_profile_id, :activated_at => Time.now)
|
97
|
+
complete_activation
|
98
|
+
end
|
99
|
+
|
100
|
+
def complete_activation_nogw(opts)
|
101
|
+
raise "already activated" if sub_activated?
|
102
|
+
|
103
|
+
subscription.update_attributes!(:activated_at => Time.now)
|
104
|
+
complete_activation
|
105
|
+
end
|
106
|
+
|
107
|
+
def complete_activation
|
108
|
+
related_transactions.each do |t|
|
109
|
+
t.complete(:effective => sub_starts_at, :reason => :update)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def complete_cancellation_paypal(opts)
|
114
|
+
# update the record beforehand, because paypal raises an error if
|
115
|
+
# the profile is already cancelled
|
116
|
+
complete_cancellation(opts)
|
117
|
+
SubscriptionFu::Paypal.paypal.cancel_recurring(sub_paypal_profile_id, sub_cancel_reason)
|
118
|
+
end
|
119
|
+
|
120
|
+
def complete_cancellation_nogw(opts)
|
121
|
+
complete_cancellation(opts)
|
122
|
+
end
|
123
|
+
|
124
|
+
def complete_cancellation(opts)
|
125
|
+
unless sub_canceled?
|
126
|
+
cancel_timestamp = opts[:effective] || Time.now
|
127
|
+
cancel_reason = opts[:reason] || :cancel
|
128
|
+
subscription.update_attributes!(:canceled_at => cancel_timestamp, :cancel_reason => cancel_reason.to_s)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
data/examples/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class SubscriptionsController < ApplicationController
|
2
|
+
def new
|
3
|
+
end
|
4
|
+
|
5
|
+
def create
|
6
|
+
@subscription = current_group.build_next_subscription("basic")
|
7
|
+
@subscription.save!
|
8
|
+
@transaction = @subscription.initiate_activation(current_user)
|
9
|
+
redirect_to @transaction.start_checkout(url_for(:action => :confirm, :controller => "transactions"), url_for(:action => :abort, :controller => "transactions"))
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class TransactionsController < ApplicationController
|
2
|
+
before_filter :require_valid_transaction
|
3
|
+
|
4
|
+
def confirm
|
5
|
+
end
|
6
|
+
|
7
|
+
def abort
|
8
|
+
@transaction.abort
|
9
|
+
flash[:notice] = "Transaction aborted."
|
10
|
+
redirect_to root_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def update
|
14
|
+
if @transaction.complete
|
15
|
+
flash[:notice] = "Sucessfully updated your subscription."
|
16
|
+
else
|
17
|
+
flash[:error] = "Transaction was not successfull, please try again."
|
18
|
+
end
|
19
|
+
redirect_to root_path
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def require_valid_transaction
|
25
|
+
@token = params[:token]
|
26
|
+
@transaction = current_group.pending_transaction(@token)
|
27
|
+
unless @transaction
|
28
|
+
logger.info("Invalid transaction for token: #{@token}")
|
29
|
+
flash[:error] = "Invalid transaction, please try again."
|
30
|
+
redirect_to root_path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "rails/generators/active_record/migration"
|
2
|
+
|
3
|
+
module SubscriptionFu
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
extend ActiveRecord::Generators::Migration
|
8
|
+
|
9
|
+
source_root File.expand_path("../templates", __FILE__)
|
10
|
+
desc "Generates the migrations require for subscription_fu"
|
11
|
+
|
12
|
+
def create_migration_file
|
13
|
+
migration_template 'migration.rb', 'db/migrate/create_subscription_fu_tables.rb'
|
14
|
+
end
|
15
|
+
def copy_initializer
|
16
|
+
template 'initializer.rb', 'config/initializers/subscription_fu.rb'
|
17
|
+
end
|
18
|
+
def copy_language_file
|
19
|
+
template 'en.yml', 'config/locales/subscription_fu.en.yml'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
en:
|
2
|
+
subscription_fu:
|
3
|
+
subscription:
|
4
|
+
# description used for Paypal
|
5
|
+
description: "%{plan_name} subscription for %{subject_desc}, %{price} per month"
|
6
|
+
# cancel notes used as description for Paypal
|
7
|
+
cancel_notes:
|
8
|
+
update: "Changed subscription plan"
|
9
|
+
cancel: "Subscription cancelled"
|
10
|
+
plan:
|
11
|
+
options:
|
12
|
+
# match this section with the plan names you are using in
|
13
|
+
# config/initializers/subscription_fu.rb to get human readable names
|
14
|
+
profess: Professional
|
15
|
+
premium: Premium
|
16
|
+
basic: Basic
|
17
|
+
free: Free
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<%= Rails.application.class.name %>.configure do
|
2
|
+
|
3
|
+
# change this to your PayPal API User ID
|
4
|
+
config.subscription_fu.paypal_api_user_id = "michae_1272617165_biz_api1.mobalean.com"
|
5
|
+
# change this to your PayPal API password
|
6
|
+
config.subscription_fu.paypal_api_pwd = "1272617171"
|
7
|
+
# change this to your PayPal API signature
|
8
|
+
config.subscription_fu.paypal_api_sig = "ATpPRKe6SEGaLcgDfFD-kBQgVsGuA9iFQwK6d4x6Qs4iti0XYRkZQl9Q"
|
9
|
+
|
10
|
+
# Your subscription plans. You'll need to add at least one plan.
|
11
|
+
|
12
|
+
# You can use a custom class for billing plans. The default is
|
13
|
+
# SubscriptionFu::Plan, which you can use as the base for custom plans.
|
14
|
+
# Using your custom plan class allows you to further configure system
|
15
|
+
# parameters based on a selected plan.
|
16
|
+
#config.subscription_fu.plan_class_name = "MyPlan"
|
17
|
+
|
18
|
+
# The first parameter is an identifier for this plan. For non-free plans,
|
19
|
+
# the second parameter isthe price. If you would like to add custom plan
|
20
|
+
# parameters, you can change the class used for plans (see above).
|
21
|
+
config.subscription_fu.add_free_plan 'free'
|
22
|
+
config.subscription_fu.add_plan 'basic', 1000
|
23
|
+
config.subscription_fu.add_plan 'premium', 5000
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class CreateSubscriptionFuTables < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table "subscriptions", :force => true do |t|
|
4
|
+
t.references "subject", :polymorphic => true
|
5
|
+
t.references "prev_subscription"
|
6
|
+
t.string "plan_key", :limit => 10, :null => false
|
7
|
+
t.boolean "sponsored", :null => false, :default => false
|
8
|
+
t.string "paypal_profile_id"
|
9
|
+
t.datetime "starts_at", :null => false
|
10
|
+
t.datetime "billing_starts_at", :null => false
|
11
|
+
t.datetime "activated_at"
|
12
|
+
t.datetime "canceled_at"
|
13
|
+
t.string "cancel_reason", :limit => 10
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
add_index "subscriptions", ["subject_id", "subject_type"]
|
18
|
+
|
19
|
+
create_table "subscription_transactions" do |t|
|
20
|
+
t.references "subscription", :null => false
|
21
|
+
t.references "initiator", :null => false, :polymorphic => true
|
22
|
+
t.string "action", :limit => 15, :null => false
|
23
|
+
t.string "status", :limit => 15, :null => false
|
24
|
+
t.string "gateway", :limit => 10, :null => false
|
25
|
+
t.string "identifier"
|
26
|
+
t.references "related_transaction"
|
27
|
+
t.timestamps
|
28
|
+
end
|
29
|
+
|
30
|
+
add_index "subscription_transactions", ["identifier"]
|
31
|
+
add_index "subscription_transactions", ["subscription_id"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.down
|
35
|
+
drop_table "subscriptions"
|
36
|
+
drop_table "subscription_transactions"
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SubscriptionFu
|
2
|
+
class Config
|
3
|
+
attr_accessor :plan_class_name, :paypal_nvp_api_url, :paypal_api_user_id, :paypal_api_pwd, :paypal_api_sig, :paypal_landing_url
|
4
|
+
attr_reader :available_plans
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@available_plans = {}
|
8
|
+
@plan_class_name = "SubscriptionFu::Plan"
|
9
|
+
paypal_use_production!
|
10
|
+
end
|
11
|
+
|
12
|
+
def paypal_use_sandbox!
|
13
|
+
self.paypal_nvp_api_url = "https://api-3t.sandbox.paypal.com/nvp"
|
14
|
+
self.paypal_landing_url = "https://www.sandbox.paypal.com/cgi-bin/webscr"
|
15
|
+
end
|
16
|
+
|
17
|
+
def paypal_use_production!
|
18
|
+
self.paypal_nvp_api_url = "https://api-3t.paypal.com/nvp"
|
19
|
+
self.paypal_landing_url = "https://www.paypal.com/cgi-bin/webscr"
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_plan(key, price, data = {})
|
23
|
+
available_plans[key] = plan_class.new(key, price, data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_free_plan(key, data = {})
|
27
|
+
add_plan(key, 0, data)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def plan_class
|
33
|
+
plan_class_name.constantize
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|