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.
Files changed (58) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +16 -0
  3. data/LICENSE +20 -0
  4. data/README.md +96 -0
  5. data/Rakefile +6 -0
  6. data/app/models/subscription_fu/plan.rb +43 -0
  7. data/app/models/subscription_fu/subscription.rb +117 -0
  8. data/app/models/subscription_fu/transaction.rb +131 -0
  9. data/config/locales/en.yml +7 -0
  10. data/examples/routes.rb +7 -0
  11. data/examples/subscriptions_controller.rb +12 -0
  12. data/examples/transactions_controller.rb +33 -0
  13. data/lib/generators/subscription_fu/install_generator.rb +23 -0
  14. data/lib/generators/subscription_fu/templates/en.yml +17 -0
  15. data/lib/generators/subscription_fu/templates/initializer.rb +24 -0
  16. data/lib/generators/subscription_fu/templates/migration.rb +38 -0
  17. data/lib/subscription_fu/config.rb +36 -0
  18. data/lib/subscription_fu/engine.rb +5 -0
  19. data/lib/subscription_fu/models.rb +54 -0
  20. data/lib/subscription_fu/paypal.rb +101 -0
  21. data/lib/subscription_fu/railtie.rb +12 -0
  22. data/lib/subscription_fu/version.rb +3 -0
  23. data/lib/subscription_fu.rb +15 -0
  24. data/spec/app/.gitignore +1 -0
  25. data/spec/app/Rakefile +8 -0
  26. data/spec/app/app/controllers/application_controller.rb +6 -0
  27. data/spec/app/app/models/initiator.rb +2 -0
  28. data/spec/app/app/models/subject.rb +4 -0
  29. data/spec/app/app/views/layouts/application.html.haml +6 -0
  30. data/spec/app/config/application.rb +11 -0
  31. data/spec/app/config/boot.rb +4 -0
  32. data/spec/app/config/database.yml +18 -0
  33. data/spec/app/config/environment.rb +5 -0
  34. data/spec/app/config/environments/development.rb +18 -0
  35. data/spec/app/config/environments/test.rb +22 -0
  36. data/spec/app/config/initializers/backtrace_silencers.rb +7 -0
  37. data/spec/app/config/initializers/inflections.rb +2 -0
  38. data/spec/app/config/initializers/secret_token.rb +2 -0
  39. data/spec/app/config/initializers/subscription_fu.rb +28 -0
  40. data/spec/app/config/locales/subscription_fu.en.yml +17 -0
  41. data/spec/app/config/routes.rb +3 -0
  42. data/spec/app/config.ru +4 -0
  43. data/spec/app/db/.gitignore +1 -0
  44. data/spec/app/db/migrate/20110516061428_create_subjects.rb +13 -0
  45. data/spec/app/db/migrate/20110516061443_create_initiators.rb +14 -0
  46. data/spec/app/db/migrate/20110516070948_create_subscription_fu_tables.rb +38 -0
  47. data/spec/app/db/schema.rb +62 -0
  48. data/spec/app/script/rails +10 -0
  49. data/spec/factories/initiator.rb +4 -0
  50. data/spec/factories/subject.rb +2 -0
  51. data/spec/factories/subscription.rb +6 -0
  52. data/spec/factories/transaction.rb +7 -0
  53. data/spec/models/subscription_spec.rb +277 -0
  54. data/spec/models/subscription_transaction_spec.rb +135 -0
  55. data/spec/spec_helper.rb +18 -0
  56. data/spec/support/paypal_test_helper.rb +82 -0
  57. data/subscription_fu.gemspec +23 -0
  58. metadata +134 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ pkg/*
4
+ log
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,6 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ task :default => :spec
6
+ RSpec::Core::RakeTask.new
@@ -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
@@ -0,0 +1,7 @@
1
+ en:
2
+ subscription_fu:
3
+ subscription:
4
+ description: "%{plan_name} subscription for %{subject_desc}, %{price} per month"
5
+ cancel_notes:
6
+ update: "Changed subscription plan"
7
+ cancel: "Subscription cancelled"
@@ -0,0 +1,7 @@
1
+ MyApp::Application.routes.draw do
2
+ resource :subscription
3
+ resource :transaction do
4
+ match 'confirm' => 'transactions#confirm'
5
+ match 'abort' => 'transactions#abort'
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ module SubscriptionFu
2
+ class Engine < ::Rails::Engine #:nodoc:
3
+ end
4
+ end
5
+