freemium-ajb 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.coveralls.yml +1 -0
- data/.gitignore +27 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.md +20 -0
- data/README.md +1 -0
- data/app/mailers/freemium_mailer.rb +36 -0
- data/app/views/subscription_mailer/admin_report.text.erb +4 -0
- data/app/views/subscription_mailer/expiration_notice.text.erb +1 -0
- data/app/views/subscription_mailer/expiration_warning.text.erb +1 -0
- data/app/views/subscription_mailer/invoice.text.erb +5 -0
- data/freemium.gemspec +29 -0
- data/lib/freemium.rb +25 -0
- data/lib/freemium/configuration.rb +27 -0
- data/lib/freemium/coupon.rb +37 -0
- data/lib/freemium/coupon_redemption.rb +59 -0
- data/lib/freemium/credit_card.rb +222 -0
- data/lib/freemium/engine.rb +7 -0
- data/lib/freemium/gateways/base.rb +65 -0
- data/lib/freemium/gateways/brain_tree.rb +175 -0
- data/lib/freemium/gateways/test.rb +36 -0
- data/lib/freemium/rates.rb +33 -0
- data/lib/freemium/response.rb +24 -0
- data/lib/freemium/subscription.rb +384 -0
- data/lib/freemium/subscription_change.rb +20 -0
- data/lib/freemium/subscription_plan.rb +26 -0
- data/lib/freemium/testing/app/controllers/application_controller.rb +7 -0
- data/lib/freemium/testing/application.rb +46 -0
- data/lib/freemium/testing/config/database.yml +11 -0
- data/lib/freemium/testing/config/routes.rb +3 -0
- data/lib/freemium/transaction.rb +15 -0
- data/lib/freemium/version.rb +3 -0
- data/lib/generators/freemium/install/install_generator.rb +58 -0
- data/lib/generators/freemium/install/templates/db/migrate/create_coupon_redemptions.rb +18 -0
- data/lib/generators/freemium/install/templates/db/migrate/create_coupons.rb +28 -0
- data/lib/generators/freemium/install/templates/db/migrate/create_credit_cards.rb +14 -0
- data/lib/generators/freemium/install/templates/db/migrate/create_subscription_changes.rb +21 -0
- data/lib/generators/freemium/install/templates/db/migrate/create_subscription_plans.rb +14 -0
- data/lib/generators/freemium/install/templates/db/migrate/create_subscriptions.rb +31 -0
- data/lib/generators/freemium/install/templates/db/migrate/create_transactions.rb +17 -0
- data/lib/generators/freemium/install/templates/freemium.rb +16 -0
- data/lib/generators/freemium/install/templates/models/coupon.rb +3 -0
- data/lib/generators/freemium/install/templates/models/coupon_redemption.rb +3 -0
- data/lib/generators/freemium/install/templates/models/credit_card.rb +3 -0
- data/lib/generators/freemium/install/templates/models/subscription.rb +3 -0
- data/lib/generators/freemium/install/templates/models/subscription_change.rb +3 -0
- data/lib/generators/freemium/install/templates/models/subscription_plan.rb +3 -0
- data/lib/generators/freemium/install/templates/models/transaction.rb +3 -0
- data/lib/generators/views/USAGE +3 -0
- data/lib/generators/views/views_generator.rb +39 -0
- data/lib/tasks/freemium.rake +17 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/mailers.rb +1 -0
- data/spec/dummy/app/models/models.rb +31 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +45 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +21 -0
- data/spec/dummy/config/environment.rb +6 -0
- data/spec/dummy/config/environments/development.rb +26 -0
- data/spec/dummy/config/environments/production.rb +49 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/freemium.rb +16 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mem_db.rb +12 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/schema.rb +90 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/fixtures/credit_cards.yml +11 -0
- data/spec/fixtures/subscription_plans.yml +15 -0
- data/spec/fixtures/subscriptions.yml +28 -0
- data/spec/fixtures/users.yml +16 -0
- data/spec/lib/tasks/run_billing_rake_spec.rb +14 -0
- data/spec/models/coupon_redemption_spec.rb +287 -0
- data/spec/models/credit_card_spec.rb +124 -0
- data/spec/models/manual_billing_spec.rb +165 -0
- data/spec/models/subscription_plan_spec.rb +46 -0
- data/spec/models/subscription_spec.rb +386 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/helpers.rb +18 -0
- data/spec/support/shared_contexts/rake.rb +19 -0
- metadata +270 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module Freemium
|
2
|
+
module SubscriptionChange
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
belongs_to :subscribable, polymorphic: true
|
7
|
+
|
8
|
+
belongs_to :original_subscription_plan, class_name: "SubscriptionPlan"
|
9
|
+
belongs_to :new_subscription_plan, class_name: "SubscriptionPlan"
|
10
|
+
|
11
|
+
composed_of :new_rate, class_name: 'Money', mapping: [ %w(new_rate_cents cents) ], allow_nil: true
|
12
|
+
composed_of :original_rate, class_name: 'Money', mapping: [ %w(original_rate_cents cents) ], allow_nil: true
|
13
|
+
|
14
|
+
validates_presence_of :reason
|
15
|
+
validates_inclusion_of :reason, :in => %w(new upgrade downgrade expiration cancellation)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# name
|
2
|
+
# key
|
3
|
+
# rate_cents
|
4
|
+
module Freemium
|
5
|
+
module SubscriptionPlan
|
6
|
+
include Rates
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
# yes, subscriptions.subscription_plan_id may not be null, but
|
11
|
+
# this at least makes the delete not happen if there are any active.
|
12
|
+
has_many :subscriptions, dependent: :nullify
|
13
|
+
has_and_belongs_to_many :coupons
|
14
|
+
|
15
|
+
composed_of :rate, class_name: 'Money', mapping: [ %w(rate_cents cents) ], allow_nil: true
|
16
|
+
|
17
|
+
validates :name, presence: true
|
18
|
+
validates :key, presence: true
|
19
|
+
validates :rate_cents, presence: true
|
20
|
+
|
21
|
+
serialize :features, Hash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rails/all'
|
2
|
+
|
3
|
+
module Freemium
|
4
|
+
module Testing
|
5
|
+
APP_ROOT = File.expand_path('..', __FILE__).freeze
|
6
|
+
|
7
|
+
class Application < Rails::Application
|
8
|
+
config.encoding = "utf-8"
|
9
|
+
config.action_mailer.default_url_options = { :host => 'localhost' }
|
10
|
+
|
11
|
+
if Rails::VERSION::MAJOR >= 3 && Rails::VERSION::MINOR >= 1
|
12
|
+
config.paths['config/database'] = "#{APP_ROOT}/config/database.yml"
|
13
|
+
config.paths['config/routes'] << "#{APP_ROOT}/config/routes.rb"
|
14
|
+
config.paths['app/controllers'] << "#{APP_ROOT}/app/controllers"
|
15
|
+
config.paths['app/views'] << "#{APP_ROOT}/app/views"
|
16
|
+
config.paths['log'] = "tmp/log/development.log"
|
17
|
+
config.assets.enabled = true
|
18
|
+
else
|
19
|
+
config.paths.config.database = "#{APP_ROOT}/config/database.yml"
|
20
|
+
config.paths.config.routes << "#{APP_ROOT}/config/routes.rb"
|
21
|
+
config.paths.app.controllers << "#{APP_ROOT}/app/controllers"
|
22
|
+
config.paths.app.views << "#{APP_ROOT}/app/views"
|
23
|
+
config.paths.log = "tmp/log"
|
24
|
+
end
|
25
|
+
|
26
|
+
config.cache_classes = true
|
27
|
+
config.whiny_nils = true
|
28
|
+
config.consider_all_requests_local = true
|
29
|
+
config.action_controller.perform_caching = false
|
30
|
+
config.action_dispatch.show_exceptions = false
|
31
|
+
config.action_controller.allow_forgery_protection = false
|
32
|
+
config.action_mailer.delivery_method = :test
|
33
|
+
config.active_support.deprecation = :stderr
|
34
|
+
config.secret_token = "SECRET_TOKEN_IS_MIN_30_CHARS_LONG"
|
35
|
+
|
36
|
+
def require_environment!
|
37
|
+
initialize!
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize!
|
41
|
+
FileUtils.mkdir_p(Rails.root.join("db").to_s)
|
42
|
+
super unless @initialized
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Freemium
|
2
|
+
module Transaction
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
scope :since, lambda { |time| where(["created_at >= ?", time]) }
|
7
|
+
|
8
|
+
belongs_to :subscription
|
9
|
+
|
10
|
+
composed_of :amount, :class_name => 'Money', :mapping => [ %w(amount_cents cents) ], :allow_nil => true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module Freemium
|
5
|
+
module Generators
|
6
|
+
MODELS = %w(transaction coupon coupon_redemption credit_card
|
7
|
+
subscription subscription_change subscription_plan)
|
8
|
+
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
10
|
+
include Rails::Generators::Migration
|
11
|
+
source_root File.expand_path('../templates', __FILE__)
|
12
|
+
|
13
|
+
def create_freemium_initializer
|
14
|
+
copy_file 'freemium.rb', 'config/initializers/freemium.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_models
|
18
|
+
Freemium::Generators::MODELS.each do |model_name|
|
19
|
+
if !File.exists?("app/models/#{model_name}.rb")
|
20
|
+
copy_file "models/#{model_name}.rb", "app/models/#{model_name}.rb"
|
21
|
+
create_migration "create_#{model_name.pluralize}.rb"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def create_migration(migration_name, config = {})
|
29
|
+
unless migration_exists?(migration_name)
|
30
|
+
migration_template(
|
31
|
+
"db/migrate/#{migration_name}",
|
32
|
+
"db/migrate/#{migration_name}",
|
33
|
+
config
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def migration_exists?(name)
|
39
|
+
existing_migrations.include?(name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def existing_migrations
|
43
|
+
@existing_migrations ||= Dir.glob("db/migrate/*.rb").map do |file|
|
44
|
+
migration_name_without_timestamp(file)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def migration_name_without_timestamp(file)
|
49
|
+
file.sub(%r{^.*(db/migrate/)(?:\d+_)?}, '')
|
50
|
+
end
|
51
|
+
|
52
|
+
# for generating a timestamp when using `create_migration`
|
53
|
+
def self.next_migration_number(dir)
|
54
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateCouponRedemptions < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
|
4
|
+
create_table :coupon_redemptions, :force => true do |t|
|
5
|
+
t.column :subscription_id, :integer, :null => false
|
6
|
+
t.column :coupon_id, :integer, :null => false
|
7
|
+
t.column :redeemed_on, :date, :null => false
|
8
|
+
t.column :expired_on, :date, :null => true
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :coupon_redemptions, :subscription_id
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :coupon_redemptions
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class CreateCoupons < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
|
4
|
+
create_table :coupons, :force => true do |t|
|
5
|
+
t.column :description, :string, :null => false
|
6
|
+
t.column :discount_percentage, :integer, :null => false
|
7
|
+
t.column :redemption_key, :string, :null => true
|
8
|
+
t.column :redemption_limit, :integer, :null => true
|
9
|
+
t.column :redemption_expiration, :date, :null => true
|
10
|
+
t.column :duration_in_months, :integer, :null => true
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :coupons_subscription_plans, :id => false, :force => true do |t|
|
14
|
+
t.column :coupon_id, :integer, :null => false
|
15
|
+
t.column :subscription_plan_id, :integer, :null => false
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
add_index :coupons_subscription_plans, :coupon_id, :name => :on_coupon_id
|
20
|
+
add_index :coupons_subscription_plans, :subscription_plan_id, :name => :on_subscription_plan_id
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.down
|
25
|
+
drop_table :coupons
|
26
|
+
drop_table :coupons_subscription_plans
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateCreditCards < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :credit_cards, :force => true do |t|
|
4
|
+
t.column :display_number, :string, :null => false
|
5
|
+
t.column :card_type, :string, :null => false
|
6
|
+
t.column :expiration_date, :timestamp, :null => false
|
7
|
+
t.column :zip_code, :string, :null => true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
drop_table :credit_cards
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateSubscriptionChanges < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :subscription_changes, :force => true do |t|
|
4
|
+
t.column :subscribable_id, :integer, :null => false
|
5
|
+
t.column :subscribable_type, :string, :null => false
|
6
|
+
t.column :original_subscription_plan_id, :integer, :null => true
|
7
|
+
t.column :new_subscription_plan_id, :integer, :null => true
|
8
|
+
t.column :original_rate_cents, :integer, :null => true
|
9
|
+
t.column :new_rate_cents, :integer, :null => true
|
10
|
+
t.column :reason, :string, :null => false
|
11
|
+
t.column :created_at, :timestamp, :null => false
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :subscription_changes, :reason
|
15
|
+
add_index :subscription_changes, [:subscribable_id, :subscribable_type], name: :subscription_changes_on_subscribable
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :subscription_changes
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateSubscriptionPlans < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :subscription_plans, :force => true do |t|
|
4
|
+
t.column :name, :string, :null => false
|
5
|
+
t.column :key, :string, :null => false
|
6
|
+
t.column :rate_cents, :integer, :null => false
|
7
|
+
t.column :features, :text
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
drop_table :subscription_plans
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class CreateSubscriptions < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :subscriptions, :force => true do |t|
|
4
|
+
t.column :subscribable_id, :integer, :null => false
|
5
|
+
t.column :subscribable_type, :string, :null => false
|
6
|
+
t.column :billing_key, :string, :null => true
|
7
|
+
t.column :credit_card_id, :integer, :null => true
|
8
|
+
t.column :subscription_plan_id, :integer, :null => false
|
9
|
+
t.column :paid_through, :date, :null => true
|
10
|
+
t.column :expire_on, :date, :null => true
|
11
|
+
t.column :billing_key, :string, :null => true
|
12
|
+
t.column :started_on, :date, :null => true
|
13
|
+
t.column :last_transaction_at, :datetime, :null => true
|
14
|
+
t.column :last_transaction_success, :boolean, :null => true
|
15
|
+
t.column :in_trial, :boolean, :null => false, :default => false
|
16
|
+
end
|
17
|
+
# for polymorphic association queries
|
18
|
+
add_index :subscriptions, :subscribable_id
|
19
|
+
add_index :subscriptions, :subscribable_type
|
20
|
+
add_index :subscriptions, [:subscribable_id, :subscribable_type], name: :subscriptions_on_subscribable
|
21
|
+
# for finding due, pastdue, and expiring subscriptions
|
22
|
+
add_index :subscriptions, :paid_through
|
23
|
+
add_index :subscriptions, :expire_on
|
24
|
+
# for applying transactions from automated recurring billing
|
25
|
+
add_index :subscriptions, :billing_key
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.down
|
29
|
+
drop_table :subscriptions
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateTransactions < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :transactions, :force => true do |t|
|
4
|
+
t.column :subscription_id, :integer, :null => false
|
5
|
+
t.column :success, :boolean, :null => false
|
6
|
+
t.column :billing_key, :string, :null => false
|
7
|
+
t.column :amount_cents, :integer, :null => false
|
8
|
+
t.column :message, :string, :null => true
|
9
|
+
t.column :created_at, :timestamp, :null => false
|
10
|
+
end
|
11
|
+
add_index :transactions, :subscription_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :transactions
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Freemium.configure do |c|
|
2
|
+
if Rails.env.test?
|
3
|
+
c.gateway = Freemium::Gateways::Test.new
|
4
|
+
else
|
5
|
+
c.gateway = Freemium::Gateways::BrainTree.new
|
6
|
+
c.gateway.username = "demo"
|
7
|
+
c.gateway.password = "password"
|
8
|
+
end
|
9
|
+
|
10
|
+
c.admin_report_recipients = ["your@email.com"]
|
11
|
+
|
12
|
+
# c.days_grace = 3
|
13
|
+
# c.days_free_trial = 0
|
14
|
+
# c.mailer = FreemiumMailer
|
15
|
+
# c.expired_plan = nil
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module Freemium
|
4
|
+
module Generators
|
5
|
+
class ViewsGenerator < Rails::Generators::Base
|
6
|
+
source_root Freemium.root
|
7
|
+
|
8
|
+
def create_views
|
9
|
+
views.each do |view|
|
10
|
+
copy_file view
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_locales
|
15
|
+
locales.each do |locale|
|
16
|
+
copy_file locale
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def views
|
23
|
+
files_within_root('.', 'app/views/**/*.*')
|
24
|
+
end
|
25
|
+
|
26
|
+
def locales
|
27
|
+
files_within_root('.', 'config/locales/**/*.*')
|
28
|
+
end
|
29
|
+
|
30
|
+
def files_within_root(prefix, glob)
|
31
|
+
root = "#{self.class.source_root}/#{prefix}"
|
32
|
+
|
33
|
+
Dir["#{root}/#{glob}"].sort.map do |full_path|
|
34
|
+
full_path.sub(root, '.').gsub('/./', '/')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|