subscription_fu 0.1.0
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 +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
|