stripe_saas 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.asc +159 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/javascripts/stripe_saas/application.js +13 -0
  6. data/app/assets/stylesheets/stripe_saas/application.css +15 -0
  7. data/app/concerns/stripe_saas/feature.rb +23 -0
  8. data/app/concerns/stripe_saas/plan.rb +45 -0
  9. data/app/concerns/stripe_saas/plan_feature.rb +25 -0
  10. data/app/concerns/stripe_saas/subscription.rb +182 -0
  11. data/app/controllers/stripe_saas/application_controller.rb +5 -0
  12. data/app/controllers/stripe_saas/subscriptions_controller.rb +200 -0
  13. data/app/helpers/stripe_saas/application_helper.rb +26 -0
  14. data/app/views/stripe_saas/subscriptions/_card.html.erb +51 -0
  15. data/app/views/stripe_saas/subscriptions/_card_form.html.erb +42 -0
  16. data/app/views/stripe_saas/subscriptions/_pricing_table.html.erb +43 -0
  17. data/app/views/stripe_saas/subscriptions/edit.html.erb +7 -0
  18. data/app/views/stripe_saas/subscriptions/index.html.erb +2 -0
  19. data/app/views/stripe_saas/subscriptions/new.html.erb +1 -0
  20. data/app/views/stripe_saas/subscriptions/show.html.erb +15 -0
  21. data/app/views/stripe_saas/subscriptions/unauthorized.html.erb +1 -0
  22. data/config/environment.rb +0 -0
  23. data/config/routes.rb +10 -0
  24. data/lib/generators/stripe_saas/install_generator.rb +60 -0
  25. data/lib/generators/stripe_saas/templates/app/models/feature.rb +8 -0
  26. data/lib/generators/stripe_saas/templates/app/models/plan.rb +9 -0
  27. data/lib/generators/stripe_saas/templates/app/models/plan_feature.rb +6 -0
  28. data/lib/generators/stripe_saas/templates/app/models/subscription.rb +5 -0
  29. data/lib/generators/stripe_saas/templates/config/initializers/stripe_saas.rb +7 -0
  30. data/lib/generators/stripe_saas/views_generator.rb +20 -0
  31. data/lib/stripe_saas.rb +9 -0
  32. data/lib/stripe_saas/configuration.rb +51 -0
  33. data/lib/stripe_saas/engine.rb +41 -0
  34. data/lib/stripe_saas/version.rb +3 -0
  35. data/lib/tasks/stripe_saas_tasks.rake +11 -0
  36. data/spec/concerns/plan_spec.rb +50 -0
  37. data/spec/dummy/README.rdoc +28 -0
  38. data/spec/dummy/Rakefile +6 -0
  39. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  40. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  41. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  42. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  43. data/spec/dummy/app/models/plan.rb +7 -0
  44. data/spec/dummy/app/models/subscription.rb +5 -0
  45. data/spec/dummy/app/models/user.rb +8 -0
  46. data/spec/dummy/app/views/layouts/application.html.erb +16 -0
  47. data/spec/dummy/bin/bundle +3 -0
  48. data/spec/dummy/bin/rails +4 -0
  49. data/spec/dummy/bin/rake +4 -0
  50. data/spec/dummy/bin/setup +29 -0
  51. data/spec/dummy/config.ru +4 -0
  52. data/spec/dummy/config/application.rb +27 -0
  53. data/spec/dummy/config/boot.rb +5 -0
  54. data/spec/dummy/config/database.yml +25 -0
  55. data/spec/dummy/config/environment.rb +5 -0
  56. data/spec/dummy/config/environments/development.rb +41 -0
  57. data/spec/dummy/config/environments/production.rb +79 -0
  58. data/spec/dummy/config/environments/test.rb +42 -0
  59. data/spec/dummy/config/initializers/assets.rb +11 -0
  60. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  61. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  62. data/spec/dummy/config/initializers/devise.rb +259 -0
  63. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  64. data/spec/dummy/config/initializers/inflections.rb +16 -0
  65. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  66. data/spec/dummy/config/initializers/session_store.rb +3 -0
  67. data/spec/dummy/config/initializers/stripe_saas.rb +7 -0
  68. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/spec/dummy/config/locales/devise.en.yml +60 -0
  70. data/spec/dummy/config/locales/en.yml +23 -0
  71. data/spec/dummy/config/routes.rb +10 -0
  72. data/spec/dummy/config/secrets.yml +22 -0
  73. data/spec/dummy/db/development.sqlite3 +0 -0
  74. data/spec/dummy/db/migrate/20150101233243_devise_create_users.rb +42 -0
  75. data/spec/dummy/db/migrate/20150102001921_create_subscriptions.rb +14 -0
  76. data/spec/dummy/db/migrate/20150102001930_create_plans.rb +19 -0
  77. data/spec/dummy/db/schema.rb +61 -0
  78. data/spec/dummy/db/test.sqlite3 +0 -0
  79. data/spec/dummy/log/development.log +170 -0
  80. data/spec/dummy/log/test.log +110 -0
  81. data/spec/dummy/public/404.html +67 -0
  82. data/spec/dummy/public/422.html +67 -0
  83. data/spec/dummy/public/500.html +66 -0
  84. data/spec/dummy/public/favicon.ico +0 -0
  85. data/spec/integration/navigation_test.rb +9 -0
  86. data/spec/rails_helper.rb +23 -0
  87. data/spec/spec_helper.rb +22 -0
  88. metadata +278 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c193fb3ee82f3bcec3747eb5bf791125b143615f
4
+ data.tar.gz: a1dd02c5730217bbfa261ee773b3213e537e1f9d
5
+ SHA512:
6
+ metadata.gz: 196b684766f5a7f1e2e9ca9ae43a374bf923b64557b58e7ce1ec9d7aaba373f3d6ff3827f4374cacb3c55e70e038b6c3c25fff8d95cfd66f53f196f965fde747
7
+ data.tar.gz: 112b6bbb7df2e6548e6669d5b009befe9272d58d0e27e9576faf784a5f3213d08f979f574e3de59ee6636643679fded38663d529a63665fc381ff4debe38b71f
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Brian Sam-Bodden
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.asc ADDED
@@ -0,0 +1,159 @@
1
+ == StripeSaas
2
+
3
+ A Rails 4 Engine providing Stripe subscription management for SaaS applications.
4
+ Based on the work of Andrew Culver in Koudoku (https://github.com/andrewculver/koudoku).
5
+
6
+ == Installation
7
+
8
+ === Add gem dependency
9
+
10
+ Include the stripe_saas gem in your Gemfile and bundle (install):
11
+
12
+ [source,ruby]
13
+ -------------------------------------------
14
+ gem 'stripe_saas'
15
+ -------------------------------------------
16
+
17
+ === Install subscriptions management on a model.
18
+
19
+ A rails generator is provided to install the StripeSaas models:
20
+
21
+ [source,ruby]
22
+ -------------------------------------------
23
+ rails g stripe_saas:install user
24
+ -------------------------------------------
25
+
26
+ ==== Stripe Subscriptions
27
+
28
+ A model that mirrors a Stripe subscription (https://stripe.com/docs/api/ruby#subscriptions)
29
+ is generated and a one-to-one relationship between it and one of your
30
+ application's models as the owner of the subscription.
31
+
32
+ In the example above the generated StripeSaas::Subscription 'belongs to' your
33
+ application's User:
34
+
35
+ [source,ruby]
36
+ -------------------------------------------
37
+ class Subscription < ActiveRecord::Base
38
+ include StripeSaas::Subscription
39
+
40
+ belongs_to :user
41
+ end
42
+ -------------------------------------------
43
+
44
+ and the User class will 'has one' subscription:
45
+
46
+ [source,ruby]
47
+ -------------------------------------------
48
+ has_one :subscription
49
+ -------------------------------------------
50
+
51
+ ==== Stripe Plans
52
+
53
+ A model that mirrors a Stripe plan (https://stripe.com/docs/api/ruby#plans) is
54
+ generated.
55
+
56
+ [source,ruby]
57
+ -------------------------------------------
58
+ class Plan < ActiveRecord::Base
59
+ has_many :subscriptions
60
+ has_many :plan_features
61
+ has_many :features, through: :plan_features
62
+
63
+ default_scope { order(:display_order) }
64
+
65
+ include StripeSaas::Plan
66
+ end
67
+ -------------------------------------------
68
+
69
+ Plans have PlanFeatures which in turn are join table/model between Plan and Feature:
70
+
71
+ [source,ruby]
72
+ -------------------------------------------
73
+ class Feature < ActiveRecord::Base
74
+ has_many :plan_features
75
+ has_many :plans, through: :plan_features
76
+
77
+ default_scope { order(:display_order) }
78
+
79
+ include StripeSaas::Feature
80
+ end
81
+ -------------------------------------------
82
+
83
+ To create a Feature for example, you could use:
84
+
85
+ [source,ruby]
86
+ -------------------------------------------
87
+ Feature.find_or_create_by(name: 'signals').update({
88
+ description: "Inbound Signals",
89
+ feature_type: :number,
90
+ unit: "signals",
91
+ display_order: 1
92
+ })
93
+ -------------------------------------------
94
+
95
+ Where the feature type can be one of:
96
+
97
+ [source,ruby]
98
+ -------------------------------------------
99
+ FEATURE_TYPES = {
100
+ boolean: 'Boolean',
101
+ interval: 'Interval (in seconds)',
102
+ filesize: 'Filesize (in bytes)',
103
+ number: 'Number',
104
+ percentage: 'Percentage (%)'
105
+ }
106
+ -------------------------------------------
107
+
108
+ To create a plan (in your seeds for example) with a set of features you could use
109
+ something like:
110
+
111
+ [source,ruby]
112
+ -------------------------------------------
113
+ developer_plan = Plan.find_or_create_by(stripe_id: 'developer')
114
+ developer_plan.update({
115
+ name: 'Developer',
116
+ price: 0.0,
117
+ interval: 'month',
118
+ interval_count: 1,
119
+ statement_descriptor: 'Binnacle Developer Plan',
120
+ trial_period_days: 30,
121
+ display_order: 1
122
+ })
123
+
124
+ developer_plan.add_feature(:signals, 50000)
125
+ -------------------------------------------
126
+
127
+ Any plan with a price of 0.0 is considered a free plan in StripeSaas which will
128
+ not require the user to enter credit card information.
129
+
130
+ After running the installer you will have to migrate your database:
131
+
132
+ [source,ruby]
133
+ -------------------------------------------
134
+ rake db:migrate
135
+ -------------------------------------------
136
+
137
+ == Configuration
138
+
139
+ As part of the installation procedure an initializer is generated under config/initializers/stripe_saas.rb:
140
+
141
+ [source,ruby]
142
+ -------------------------------------------
143
+ StripeSaas.setup do |config|
144
+ config.subscriptions_owned_by = :user
145
+ # config.devise_scope = :user
146
+ config.stripe_publishable_key = ENV['STRIPE_PUBLISHABLE_KEY']
147
+ config.stripe_secret_key = ENV['STRIPE_SECRET_KEY']
148
+ config.create_plans_in_stripe = false
149
+ end
150
+ -------------------------------------------
151
+
152
+ * _subscriptions_owned_by_: The symbol of the class that owns the subscription
153
+ * _devise_scope_: If using Devise and the subscription is not owned by the devise
154
+ class (user/customer). For example, if users have accounts, and accounts have
155
+ subscriptions. Then config.subscriptions_owned_by = :account and config.devise_scope = :user
156
+ * _stripe_publishable_key_: Your Stripe Publishable Key https://stripe.com/docs/tutorials/dashboard#api-keys
157
+ * _stripe_secret_key_: Your Stripe Secret Key https://stripe.com/docs/tutorials/dashboard#api-keys
158
+ * _create_plans_in_stripe_: Whether to autogenerate the local Plans in Stripe and
159
+ keep then in synch
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'StripeSaas'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+
37
+ task default: :test
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,23 @@
1
+ module StripeSaas::Feature
2
+ extend ActiveSupport::Concern
3
+
4
+ FEATURE_TYPES = {
5
+ boolean: 'Boolean',
6
+ interval: 'Interval (in seconds)',
7
+ filesize: 'Filesize (in bytes)',
8
+ number: 'Number',
9
+ percentage: 'Percentage (%)'
10
+ }
11
+
12
+ def feature_type=(val)
13
+ val = val.to_sym
14
+ raise(ArgumentError, "#{val} is not a valid feature type") unless FEATURE_TYPES.keys.include?(val)
15
+ self[:feature_type] = val
16
+ self[:unit] = FEATURE_TYPES[val]
17
+ end
18
+
19
+ def to_s
20
+ name
21
+ end
22
+
23
+ end
@@ -0,0 +1,45 @@
1
+ module StripeSaas::Plan
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ monetize :price_cents
6
+ end
7
+
8
+ def is_upgrade_from?(plan)
9
+ (price || 0) >= (plan.price || 0)
10
+ end
11
+
12
+ def is_downgrade_from?(plan)
13
+ !is_upgrade_from?(plan)
14
+ end
15
+
16
+ def free?
17
+ price == 0.0
18
+ end
19
+
20
+ def add_feature(feature, value, display_value=nil)
21
+ feature = Feature.find_by(name: feature.to_s) if feature.is_a?(String) || feature.is_a?(Symbol)
22
+
23
+ plan_features.find_or_create_by(feature: feature).update({
24
+ value: value,
25
+ display_value: display_value
26
+ })
27
+ end
28
+
29
+ def has_feature?(feature)
30
+ if feature.is_a?(String)
31
+ !features.find_by(name: feature).first.nil?
32
+ else
33
+ features.any? { |f| f.id == feature.id }
34
+ end
35
+ end
36
+
37
+ def metadata
38
+ metadata_as_json.present? ? JSON::parse(metadata_as_json) : {}
39
+ end
40
+
41
+ def metadata=(metadata_hash)
42
+ metadata_as_json = metadata_hash.to_json
43
+ end
44
+
45
+ end
@@ -0,0 +1,25 @@
1
+ module StripeSaas::PlanFeature
2
+ extend ActiveSupport::Concern
3
+
4
+ def value=(val)
5
+ super(val.to_s)
6
+ end
7
+
8
+ def value
9
+ case feature.feature_type.to_sym
10
+ when :boolean
11
+ self[:value] == 'true'
12
+ when :number, :percentage, :filesize, :interval
13
+ self[:value].to_i
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ case feature.feature_type
19
+ when :boolean
20
+ feature.name
21
+ when :number, :percentage, :filesize, :interval
22
+ "#{self[:value]} #{feature.description}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,182 @@
1
+ module StripeSaas::Subscription
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ monetize :current_price_cents, allow_nil: true
6
+
7
+ # We don't store these one-time use tokens, but this is what Stripe provides
8
+ # client-side after storing the credit card information.
9
+ attr_accessor :credit_card_token
10
+
11
+ belongs_to :plan
12
+
13
+ # update details.
14
+ before_save :processing!
15
+
16
+ def processing!
17
+ # if their package level has changed ..
18
+ if changing_plans?
19
+ # and a customer exists in stripe ..
20
+ if stripe_id.present?
21
+ # fetch the customer.
22
+ customer = Stripe::Customer.retrieve(stripe_id)
23
+
24
+ if customer.default_card.nil? && !credit_card_token.nil?
25
+ customer.card = credit_card_token
26
+ customer.save
27
+ end
28
+
29
+ # if a new plan has been selected
30
+ if self.plan.present?
31
+
32
+ # Record the new plan pricing.
33
+ self.current_price = self.plan.price
34
+
35
+ # update the package level with stripe.
36
+ customer.update_subscription(:plan => self.plan.stripe_id)
37
+
38
+ # if no plan has been selected.
39
+ else
40
+
41
+ # Remove the current pricing.
42
+ self.current_price = nil
43
+
44
+ # delete the subscription.
45
+ customer.cancel_subscription
46
+
47
+ end
48
+
49
+ # otherwise
50
+ else
51
+ # if a new plan has been selected
52
+ if self.plan.present?
53
+ # Record the new plan pricing.
54
+ self.current_price = self.plan.price
55
+
56
+ begin
57
+ customer_attributes = if self.plan.free?
58
+ {
59
+ description: subscription_owner_description,
60
+ email: subscription_owner_email,
61
+ }
62
+ else
63
+ {
64
+ description: subscription_owner_description,
65
+ email: subscription_owner_email,
66
+ card: credit_card_token
67
+ }
68
+ end
69
+
70
+ # create a customer at that package level.
71
+ customer = Stripe::Customer.create(customer_attributes)
72
+ customer.update_subscription(:plan => plan.stripe_id)
73
+ rescue Stripe::CardError => card_error
74
+ errors[:base] << card_error.message
75
+ card_was_declined
76
+ return false
77
+ end
78
+
79
+ # store the customer id.
80
+ self.stripe_id = customer.id
81
+ unless self.plan.free?
82
+ self.last_four = customer.cards.retrieve(customer.default_card).last4
83
+ end
84
+ else
85
+
86
+ # This should never happen.
87
+
88
+ self.plan_id = nil
89
+
90
+ # Remove any plan pricing.
91
+ self.current_price = nil
92
+
93
+ end
94
+
95
+ end
96
+
97
+ # if they're updating their credit card details.
98
+ elsif self.credit_card_token.present?
99
+ # fetch the customer.
100
+ customer = Stripe::Customer.retrieve(self.stripe_id)
101
+ customer.card = self.credit_card_token
102
+ customer.save
103
+
104
+ # update the last four based on this new card.
105
+ self.last_four = customer.cards.retrieve(customer.default_card).last4
106
+ end
107
+ end
108
+ end
109
+
110
+ # Set a Stripe coupon code that will be used when a new Stripe customer (a.k.a. StripeSaas subscription)
111
+ # is created
112
+ def coupon_code=(new_code)
113
+ @coupon_code = new_code
114
+ end
115
+
116
+ # Pretty sure this wouldn't conflict with anything someone would put in their model
117
+ def subscription_owner
118
+ StripeSaas::Subscription.find_customer(self)
119
+ end
120
+
121
+ def self.find_customer(subscription_or_owner)
122
+ if subscription_or_owner.class.to_s.downcase.to_sym == StripeSaas.subscriptions_owned_by
123
+ owner = subscription_or_owner
124
+ else
125
+ owner = subscription_or_owner.send StripeSaas.subscriptions_owned_by
126
+ end
127
+ # Return whatever we belong to.
128
+ # If this object doesn't respond to 'name', please update owner_description.
129
+ if StripeSaas.customer_accessor
130
+ if StripeSaas.customer_accessor.kind_of?(Array)
131
+ StripeSaas.customer_accessor.inject(subscription_or_owner) {|o, a| o.send(a); o }
132
+ else
133
+ owner.send StripeSaas.customer_accessor
134
+ end
135
+ else
136
+ owner
137
+ end
138
+ end
139
+
140
+ def subscription_owner=(owner)
141
+ # e.g. @subscription.user = @owner
142
+ send StripeSaas.owner_assignment_sym, owner
143
+ end
144
+
145
+ def subscription_owner_description
146
+ # assuming owner responds to name.
147
+ # we should check for whether it responds to this or not.
148
+ "#{subscription_owner.try(:name) || subscription_owner.try(:id)}"
149
+ end
150
+
151
+ def subscription_owner_email
152
+ "#{subscription_owner.try(:email)}"
153
+ end
154
+
155
+ def changing_plans?
156
+ plan_id_changed?
157
+ end
158
+
159
+ def downgrading?
160
+ plan.present? and plan_id_was.present? and plan_id_was > self.plan_id
161
+ end
162
+
163
+ def upgrading?
164
+ (plan_id_was.present? and plan_id_was < plan_id) or plan_id_was.nil?
165
+ end
166
+
167
+ # TODO: this does not belong in here - need a presenter
168
+ def describe_difference(plan_to_describe)
169
+ if plan.nil?
170
+ if persisted?
171
+ "Upgrade"
172
+ end
173
+ else
174
+ if plan_to_describe.is_upgrade_from?(plan)
175
+ "Upgrade"
176
+ else
177
+ "Downgrade"
178
+ end
179
+ end
180
+ end
181
+
182
+ end