subscription_fu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+