stripe_saas 0.0.1

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 (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