freemium 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. data/.gitignore +53 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +121 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +67 -0
  6. data/Rakefile +23 -0
  7. data/autotest/discover.rb +2 -0
  8. data/config/locales/en.yml +2 -0
  9. data/freemium.gemspec +28 -0
  10. data/lib/freemium/address.rb +18 -0
  11. data/lib/freemium/coupon.rb +38 -0
  12. data/lib/freemium/coupon_redemption.rb +48 -0
  13. data/lib/freemium/credit_card.rb +273 -0
  14. data/lib/freemium/feature_set.rb +45 -0
  15. data/lib/freemium/gateways/base.rb +65 -0
  16. data/lib/freemium/gateways/brain_tree.rb +175 -0
  17. data/lib/freemium/gateways/test.rb +34 -0
  18. data/lib/freemium/manual_billing.rb +73 -0
  19. data/lib/freemium/railtie.tb +7 -0
  20. data/lib/freemium/rates.rb +33 -0
  21. data/lib/freemium/recurring_billing.rb +59 -0
  22. data/lib/freemium/response.rb +24 -0
  23. data/lib/freemium/subscription.rb +350 -0
  24. data/lib/freemium/subscription_change.rb +20 -0
  25. data/lib/freemium/subscription_mailer/admin_report.rhtml +4 -0
  26. data/lib/freemium/subscription_mailer/expiration_notice.rhtml +1 -0
  27. data/lib/freemium/subscription_mailer/expiration_warning.rhtml +1 -0
  28. data/lib/freemium/subscription_mailer/invoice.text.plain.erb +5 -0
  29. data/lib/freemium/subscription_mailer.rb +36 -0
  30. data/lib/freemium/subscription_plan.rb +32 -0
  31. data/lib/freemium/transaction.rb +15 -0
  32. data/lib/freemium/version.rb +3 -0
  33. data/lib/freemium.rb +75 -0
  34. data/lib/generators/active_record/freemium_generator.rb +28 -0
  35. data/lib/generators/active_record/templates/migrations/account_transactions.rb +17 -0
  36. data/lib/generators/active_record/templates/migrations/coupon_redemptions.rb +18 -0
  37. data/lib/generators/active_record/templates/migrations/coupons.rb +28 -0
  38. data/lib/generators/active_record/templates/migrations/credit_cards.rb +14 -0
  39. data/lib/generators/active_record/templates/migrations/subscription_changes.rb +18 -0
  40. data/lib/generators/active_record/templates/migrations/subscription_plans.rb +14 -0
  41. data/lib/generators/active_record/templates/migrations/subscriptions.rb +30 -0
  42. data/lib/generators/active_record/templates/models/account_transaction.rb +3 -0
  43. data/lib/generators/active_record/templates/models/coupon.rb +3 -0
  44. data/lib/generators/active_record/templates/models/coupon_redemption.rb +3 -0
  45. data/lib/generators/active_record/templates/models/credit_card.rb +3 -0
  46. data/lib/generators/active_record/templates/models/subscription.rb +3 -0
  47. data/lib/generators/active_record/templates/models/subscription_change.rb +3 -0
  48. data/lib/generators/active_record/templates/models/subscription_plan.rb +3 -0
  49. data/lib/generators/freemium/freemium_generator.rb +15 -0
  50. data/lib/generators/freemium/install_generator.rb +28 -0
  51. data/lib/generators/freemium/orm_helpers.rb +27 -0
  52. data/lib/generators/templates/freemium.rb +43 -0
  53. data/lib/generators/templates/freemium_feature_sets.yml +5 -0
  54. data/spec/dummy/Rakefile +7 -0
  55. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  56. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  57. data/spec/dummy/app/models/models.rb +32 -0
  58. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  59. data/spec/dummy/config/application.rb +45 -0
  60. data/spec/dummy/config/boot.rb +10 -0
  61. data/spec/dummy/config/database.yml +22 -0
  62. data/spec/dummy/config/environment.rb +5 -0
  63. data/spec/dummy/config/environments/development.rb +26 -0
  64. data/spec/dummy/config/environments/production.rb +49 -0
  65. data/spec/dummy/config/environments/test.rb +35 -0
  66. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  67. data/spec/dummy/config/initializers/inflections.rb +10 -0
  68. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  69. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  70. data/spec/dummy/config/initializers/session_store.rb +8 -0
  71. data/spec/dummy/config/locales/en.yml +5 -0
  72. data/spec/dummy/config/routes.rb +58 -0
  73. data/spec/dummy/config.ru +4 -0
  74. data/spec/dummy/db/schema.rb +92 -0
  75. data/spec/dummy/script/rails +6 -0
  76. data/spec/fixtures/credit_cards.yml +11 -0
  77. data/spec/fixtures/subscription_plans.yml +18 -0
  78. data/spec/fixtures/subscriptions.yml +29 -0
  79. data/spec/fixtures/users.yml +16 -0
  80. data/spec/freemium_feature_sets.yml +9 -0
  81. data/spec/freemium_spec.rb +4 -0
  82. data/spec/models/coupon_redemption_spec.rb +235 -0
  83. data/spec/models/credit_card_spec.rb +114 -0
  84. data/spec/models/manual_billing_spec.rb +174 -0
  85. data/spec/models/recurring_billing_spec.rb +92 -0
  86. data/spec/models/subscription_plan_spec.rb +44 -0
  87. data/spec/models/subscription_spec.rb +386 -0
  88. data/spec/spec_helper.rb +38 -0
  89. data/spec/support/helpers.rb +21 -0
  90. metadata +298 -0
data/.gitignore ADDED
@@ -0,0 +1,53 @@
1
+ # yard generated
2
+ doc
3
+ .yardoc
4
+
5
+
6
+ *.gem
7
+ *.rbc
8
+ .bundle
9
+ .config
10
+ .yardoc
11
+ Gemfile.lock
12
+ InstalledFiles
13
+ _yardoc
14
+ coverage
15
+ doc/
16
+ lib/bundler/man
17
+ pkg
18
+ rdoc
19
+ spec/reports
20
+ test/tmp
21
+ test/version_tmp
22
+ tmp
23
+
24
+ spec/dummy/db/*.sqlite3
25
+ spec/dummy/log/*.log
26
+ spec/dummy/tmp/
27
+ spec/dummy/public/
28
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
29
+ #
30
+ # * Create a file at ~/.gitignore
31
+ # * Include files you want ignored
32
+ # * Run: git config --global core.excludesfile ~/.gitignore
33
+ #
34
+ # After doing this, these files will be ignored in all your git projects,
35
+ # saving you from having to 'pollute' every project you touch with them
36
+ #
37
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
38
+ #
39
+ # For MacOS:
40
+ #
41
+ #.DS_Store
42
+ #
43
+ # For TextMate
44
+ #*.tmproj
45
+ #tmtags
46
+ #
47
+ # For emacs:
48
+ #*~
49
+ #\#*
50
+ #.\#*
51
+ #
52
+ # For vim:
53
+ #*.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in freemium.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,121 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ freemium (0.0.1)
5
+ money
6
+ rails (~> 3.1.3)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ZenTest (4.6.2)
12
+ actionmailer (3.1.4)
13
+ actionpack (= 3.1.4)
14
+ mail (~> 2.3.0)
15
+ actionpack (3.1.4)
16
+ activemodel (= 3.1.4)
17
+ activesupport (= 3.1.4)
18
+ builder (~> 3.0.0)
19
+ erubis (~> 2.7.0)
20
+ i18n (~> 0.6)
21
+ rack (~> 1.3.6)
22
+ rack-cache (~> 1.1)
23
+ rack-mount (~> 0.8.2)
24
+ rack-test (~> 0.6.1)
25
+ sprockets (~> 2.0.3)
26
+ activemodel (3.1.4)
27
+ activesupport (= 3.1.4)
28
+ builder (~> 3.0.0)
29
+ i18n (~> 0.6)
30
+ activerecord (3.1.4)
31
+ activemodel (= 3.1.4)
32
+ activesupport (= 3.1.4)
33
+ arel (~> 2.2.3)
34
+ tzinfo (~> 0.3.29)
35
+ activeresource (3.1.4)
36
+ activemodel (= 3.1.4)
37
+ activesupport (= 3.1.4)
38
+ activesupport (3.1.4)
39
+ multi_json (~> 1.0)
40
+ arel (2.2.3)
41
+ autotest (4.4.6)
42
+ ZenTest (>= 4.4.1)
43
+ builder (3.0.0)
44
+ diff-lcs (1.1.3)
45
+ erubis (2.7.0)
46
+ hike (1.2.1)
47
+ i18n (0.6.0)
48
+ json (1.6.5)
49
+ mail (2.3.0)
50
+ i18n (>= 0.4.0)
51
+ mime-types (~> 1.16)
52
+ treetop (~> 1.4.8)
53
+ mime-types (1.17.2)
54
+ money (4.0.2)
55
+ i18n (~> 0.4)
56
+ json
57
+ multi_json (1.1.0)
58
+ polyglot (0.3.3)
59
+ rack (1.3.6)
60
+ rack-cache (1.1)
61
+ rack (>= 0.4)
62
+ rack-mount (0.8.3)
63
+ rack (>= 1.0.0)
64
+ rack-ssl (1.3.2)
65
+ rack
66
+ rack-test (0.6.1)
67
+ rack (>= 1.0)
68
+ rails (3.1.4)
69
+ actionmailer (= 3.1.4)
70
+ actionpack (= 3.1.4)
71
+ activerecord (= 3.1.4)
72
+ activeresource (= 3.1.4)
73
+ activesupport (= 3.1.4)
74
+ bundler (~> 1.0)
75
+ railties (= 3.1.4)
76
+ railties (3.1.4)
77
+ actionpack (= 3.1.4)
78
+ activesupport (= 3.1.4)
79
+ rack-ssl (~> 1.3.2)
80
+ rake (>= 0.8.7)
81
+ rdoc (~> 3.4)
82
+ thor (~> 0.14.6)
83
+ rake (0.9.2.2)
84
+ rdoc (3.12)
85
+ json (~> 1.4)
86
+ rspec (2.8.0)
87
+ rspec-core (~> 2.8.0)
88
+ rspec-expectations (~> 2.8.0)
89
+ rspec-mocks (~> 2.8.0)
90
+ rspec-core (2.8.0)
91
+ rspec-expectations (2.8.0)
92
+ diff-lcs (~> 1.1.2)
93
+ rspec-mocks (2.8.0)
94
+ rspec-rails (2.8.1)
95
+ actionpack (>= 3.0)
96
+ activesupport (>= 3.0)
97
+ railties (>= 3.0)
98
+ rspec (~> 2.8.0)
99
+ sprockets (2.0.3)
100
+ hike (~> 1.2)
101
+ rack (~> 1.0)
102
+ tilt (~> 1.1, != 1.3.0)
103
+ sqlite3 (1.3.5)
104
+ sqlite3-ruby (1.3.3)
105
+ sqlite3 (>= 1.3.3)
106
+ thor (0.14.6)
107
+ tilt (1.3.3)
108
+ treetop (1.4.10)
109
+ polyglot
110
+ polyglot (>= 0.3.1)
111
+ tzinfo (0.3.32)
112
+
113
+ PLATFORMS
114
+ ruby
115
+
116
+ DEPENDENCIES
117
+ autotest
118
+ bundler (~> 1.1.rc.8)
119
+ freemium!
120
+ rspec-rails
121
+ sqlite3-ruby
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Lance Ivy
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.rdoc ADDED
@@ -0,0 +1,67 @@
1
+ = Freemium
2
+
3
+ The Freemium plugin attempts to encapsulate the Right Way to offer service subscriptions. It is built to handle multiple subscription plans (free, premium, premium plus, etc.), let you control your own invoices, and to interact with any merchant gateway that supports either automated recurring billing or credit card storage.
4
+
5
+ Freemium is a different beast from ActiveMerchant. Where the latter is optimized for one-off billing of credit cards (as in a retail environment), Freemium is optimized for storage and recurring billing of credit cards. This makes it a much cleaner and more complete solution for handling subscriptions. At the time of this writing, however, Freemium actually depends on ActiveMerchant because of its excellent CreditCard model, which I plan to steal.
6
+
7
+ This plugin was born out of my attempts to figure out the correct way to handle subscriptions. I decided that the safest/cleanest way was to simply keep track of how far out the subscription had been paid, and when that date came near, bill the subscription again to extend the paid_through date. The approach has turned out well, I think, though I strongly encourage anyone and everyone to review the processes in this plugin to make sure the assumptions it makes are appropriate.
8
+
9
+ = Gateway Requirements
10
+
11
+ Rule #1: You never want to store credit card numbers yourself. This means that you need a gateway that either provides Automated Recurring Billing (ARB - a common offering) or credit card storage (e.g. TrustCommerce's Citadel).
12
+
13
+ Freemium will work with any gateway that provides credit card storage (the preferred method!), but it will not work with every gateway that provides ARB. Just because your gateway provides ARB doesn't mean that your application can fire-and-forget; you still needs to know about successful transactions (to send invoices) and failed transactions (to send warnings and/or expire a subscription). In order for your _application_ to know about these events without your intervention, the ARB module either needs to send event notifications or it needs to provide an API to retrieve and review recent transactions.
14
+
15
+ Freemium will only work with ARB modules that provide an API to retrieve and review recent transactions. This is by far the safest route, since most gateways only send email notifications that must be manually processed by a human (ugh!) and the others can have unreliable event notification systems (e.g. PayPal, see http://talklikeaduck.denhaven2.com/articles/2007/09/02/how-to-cure-the-paypal-subscription-blues). And in any case, ARB modules that send event notifications hardly ever tell you about successful transactions, so you still have to keep track of the periodic cycles so you can send invoices, which makes the whole ARB thing barely useful.
16
+
17
+ So what we really need is a list of known good and known bad gateways. The list below is just the beginning, off the top of my head.
18
+
19
+ === Good Gateways:
20
+ * TrustCommerce with Citadel (can use Citadel and/or ARB)
21
+ * Braintree Payment Solutions (SecureVault, or ARB)
22
+
23
+ === Probably Good Gateways:
24
+ * Authorize.net (CIM, or ARB if they also offer transaction review API)
25
+
26
+ === Bad Gateways:
27
+ * LoudCommerce's LinkPoint (no storage, and no transaction review)
28
+
29
+ = Expiration
30
+
31
+ I've tried to build Freemium with the understanding that sometimes a cron task might not run, and if that happens the customers should not get screwed. That means, for example, not expiring a customer account just because a billing process didn't run. So the process for expiring a subscription is as follows: the first nightly billing process that runs _after_ a subscription's last paid day will set the final expiration date of that subscription. The final expiration date will be calculated as a certain number of days (the grace period) after the date of the billing process (grace begins when the program _knows_ the account is pastdue). The first billing process that runs on or after the expiration date will then actually expire the subscription.
32
+
33
+ So there's some possible slack in the timeline. Suppose a subscription is paid through the 14th and there's a 2 day grace period. That means if a billing process runs on the 13th, then not until the 15th, the subscription will be set to expire on the 17th - the subscriber gets an extra day of grace because your billing process didn't run.
34
+
35
+ = Misc
36
+ * If there's no grace period then the same billing process will both set the expiration date and then actually expire the subscription, thanks to the order of events.
37
+ * Expiring a subscription means downgrading it to a free plan (if any) or removing the plan altogether.
38
+
39
+ = Install
40
+
41
+ 1) Generate and run the migration:
42
+
43
+ > ./script/generate freemium_migration
44
+ > rake db:migrate
45
+
46
+ 2) Populate the database with your subscription plan (create a migration to create SubscriptionPlan records)
47
+
48
+ > ./script/generate migration populate_subscription_plans
49
+
50
+ 3) Create config/initializers/freemium.rb and configure at least the following:
51
+
52
+ gateway pick one, then see rdoc for your gateway's options to see what needs to be configured (api key, etc.)
53
+ billing_control set to :full or :arb, depending on whether you're using your gateway's ARB module
54
+ grace period in days, zero days grace is ok
55
+ mailer for customized invoices, etc.
56
+
57
+ 4) Create a SubscriptionsController (or similar) that does whatever it takes to get a unique billing key. This might mean storing the credit card (e.g. TrustCommerce Citadel) and/or setting up automated recurring billing, or getting the three keys from Amazon FPS. Most of these gateways don't have concrete API classes in Freemium yet. If you write a gateway, let me know and I'll include it.
58
+
59
+ 5) Create association from your User model (or whatever) to the Subscription model.
60
+
61
+ 6) Add a before_filter (or other logic) to properly enforce your premium plan. The filter should check that the User has an active Subscription to a SubscriptionPlan of the appropriate type.
62
+
63
+ 7) Add `/PATH/TO/DEPLOYED/APP/script/runner -e production Subscription.run_billing' to a daily cron task.
64
+
65
+ 8) Tell me how any of this could be improved. I want this plugin to make freemium billing dead-simple.
66
+
67
+ Copyright (c) 2007 Lance Ivy, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
11
+ spec.pattern = 'spec/**/*_spec.rb'
12
+ spec.rcov = true
13
+ end
14
+
15
+ task :default => :spec
16
+
17
+ require 'rdoc/task'
18
+ Rake::RDocTask.new do |rdoc|
19
+ rdoc.rdoc_dir = 'rdoc'
20
+ rdoc.title = Freemium::VERSION
21
+ rdoc.rdoc_files.include('README*')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -0,0 +1,2 @@
1
+ Autotest.add_discovery { "rails" }
2
+ Autotest.add_discovery { "rspec2" }
@@ -0,0 +1,2 @@
1
+ en:
2
+ hello: "Hello world"
data/freemium.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/freemium/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "freemium"
6
+ gem.summary = %Q{Subscription Saas}
7
+ gem.description = %Q{Subscription Saas, tests are green. Needs some refactoring.}
8
+ gem.email = "eagle.anton@gmail.com"
9
+ gem.homepage = "http://github.com/xn/freemium"
10
+ gem.authors = ["Anton Oryol","xn"]
11
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
12
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
13
+ # spec.add_runtime_dependency 'jabber4r', '> 0.1'
14
+ # spec.add_development_dependency 'rspec', '> 1.2.3'
15
+ gem.add_dependency "rails", "~> 3.1.3"
16
+ gem.add_dependency "money"
17
+ gem.add_development_dependency "rspec-rails"
18
+ gem.add_development_dependency "bundler"
19
+ gem.add_development_dependency 'rspec-rails'
20
+ gem.add_development_dependency "autotest"
21
+ gem.add_development_dependency "sqlite3-ruby"
22
+ gem.add_development_dependency "bundler", "~> 1.1.rc.8"
23
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ gem.files = `git ls-files`.split("\n")
25
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+ gem.require_paths = ["lib"]
27
+ gem.version = Freemium::VERSION
28
+ end
@@ -0,0 +1,18 @@
1
+ module Freemium
2
+ # eventually, this should mimic ActiveMerchant's credit card object, with validation and errors, etc.
3
+ # for now it's just a dumb (and therefore untested) data structure.
4
+ class Address
5
+ attr_accessor :address1, :address2, :city, :state, :zip, :country, :email, :phone_number, :ip_address
6
+
7
+ # Allow :street to be used instead of :address1
8
+ alias_method :street, :address1
9
+ alias_method :street=, :address1=
10
+
11
+ def initialize(options = {})
12
+ options.each do |key, value|
13
+ setter = "#{key}="
14
+ self.send(setter, value) if self.respond_to? setter
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ module Freemium
2
+ module Coupon
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ has_many :coupon_redemptions, :dependent => :destroy, :class_name => "CouponRedemption", :foreign_key => :coupon_id
7
+ has_many :subscriptions, :through => :coupon_redemptions
8
+ has_and_belongs_to_many :subscription_plans, :class_name => "SubscriptionPlan",
9
+ :join_table => :coupons_subscription_plans, :foreign_key => :coupon_id, :association_foreign_key => :subscription_plan_id
10
+
11
+ validates_presence_of :description, :discount_percentage
12
+ validates_inclusion_of :discount_percentage, :in => 1..100
13
+
14
+ before_save :normalize_redemption_key
15
+ end
16
+ end
17
+
18
+ def discount(rate)
19
+ rate * (1 - self.discount_percentage.to_f / 100)
20
+ end
21
+
22
+ def expired?
23
+ (self.redemption_expiration && Date.today > self.redemption_expiration) || (self.redemption_limit && self.coupon_redemptions.count >= self.redemption_limit)
24
+ end
25
+
26
+ def applies_to_plan?(subscription_plan)
27
+ return true if self.subscription_plans.blank? # applies to all plans
28
+ self.subscription_plans.include?(subscription_plan)
29
+ end
30
+
31
+ protected
32
+
33
+ def normalize_redemption_key
34
+ self.redemption_key.downcase! unless self.redemption_key.blank?
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ module Freemium
2
+ module CouponRedemption
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ belongs_to :subscription, :class_name => "Subscription"
7
+ belongs_to :coupon, :class_name => "Coupon"
8
+
9
+ before_create :set_redeemed_on
10
+
11
+ validates_presence_of :coupon
12
+ validates_presence_of :subscription
13
+ validates_uniqueness_of :coupon_id, :scope => :subscription_id, :message => "has already been applied"
14
+ validate :custom_validation, :on => :create
15
+ end
16
+ end
17
+
18
+ def expire!
19
+ self.update_attribute :expired_on, Date.today
20
+ end
21
+
22
+ def active?(date = Date.today)
23
+ expires_on ? date <= self.expires_on : true
24
+ end
25
+
26
+ def expires_on
27
+ return nil unless self.coupon.duration_in_months
28
+ self.redeemed_on + self.coupon.duration_in_months.months
29
+ end
30
+
31
+ def redeemed_on
32
+ self['redeemed_on'] || Date.today
33
+ end
34
+
35
+ protected
36
+
37
+ def set_redeemed_on
38
+ self.redeemed_on = Date.today
39
+ end
40
+
41
+ def custom_validation
42
+ errors.add :subscription, "must be paid" if self.subscription && !self.subscription.subscription_plan.paid?
43
+ errors.add :coupon, "has expired" if self.coupon && (self.coupon.expired? || self.coupon.expired?)
44
+ errors.add :coupon, "is not valid for selected plan" if self.coupon && self.subscription && !self.coupon.applies_to_plan?(self.subscription.subscription_plan)
45
+ end
46
+
47
+ end
48
+ end