freemium-ajb 0.0.4
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.
- 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
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
@@ -0,0 +1,27 @@
|
|
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/
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
freemium
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p194
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
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.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
## Freemium [](https://codeclimate.com/github/adamjacobbecker/freemium) [](https://travis-ci.org/adamjacobbecker/freemium) [](https://coveralls.io/r/adamjacobbecker/freemium)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class FreemiumMailer < ActionMailer::Base
|
2
|
+
prepend_view_path(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
default :from => 'billing@example.com',
|
5
|
+
:return_path => 'no-reply@example.com'
|
6
|
+
|
7
|
+
def invoice(transaction)
|
8
|
+
@amount = transaction.amount
|
9
|
+
@subscription = transaction.subscription
|
10
|
+
mail(:to => transaction.subscription.subscribable.email,
|
11
|
+
:bcc => Freemium.configuration.admin_report_recipients,
|
12
|
+
:subject => "Your invoice")
|
13
|
+
end
|
14
|
+
|
15
|
+
def expiration_warning(subscription)
|
16
|
+
@subscription = subscription
|
17
|
+
mail(:to => subscription.subscribable.email,
|
18
|
+
:bcc => Freemium.configuration.admin_report_recipients,
|
19
|
+
:subject => "Your subscription is set to expire")
|
20
|
+
end
|
21
|
+
|
22
|
+
def expiration_notice(subscription)
|
23
|
+
@subscription = subscription
|
24
|
+
mail(:to => subscription.subscribable.email,
|
25
|
+
:bcc => Freemium.configuration.admin_report_recipients,
|
26
|
+
:subject => "Your subscription has expired")
|
27
|
+
end
|
28
|
+
|
29
|
+
def admin_report(transactions)
|
30
|
+
@amount_charged = transactions.select{|t| t && t.success?}.collect{|t| t.amount}.sum
|
31
|
+
@transactions = transactions
|
32
|
+
@amount_charged = @amount_charged
|
33
|
+
mail(:to => Freemium.configuration.admin_report_recipients,
|
34
|
+
:subject => "Billing report (#{@amount_charged} charged)")
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Your subscription has expired.
|
@@ -0,0 +1 @@
|
|
1
|
+
We were unable to process your payment, and your subscription is set to expire in <%= @subscription.remaining_days_of_grace %> days. Please contact us to correct this.
|
data/freemium.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'freemium/version'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'freemium-ajb'
|
7
|
+
s.version = Freemium::VERSION
|
8
|
+
s.authors = ['Lance Ivy', 'Anton Oryol', 'Christian Trosclair', 'Adam Becker']
|
9
|
+
s.email = %q{adam@dobt.co}
|
10
|
+
s.homepage = %q{http://github.com/adamjacobbecker/freemium}
|
11
|
+
s.summary = ""
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
15
|
+
s.extra_rdoc_files = %w(LICENSE.md README.md)
|
16
|
+
s.rdoc_options = ['--charset=UTF-8']
|
17
|
+
s.require_paths = ['lib']
|
18
|
+
|
19
|
+
s.required_ruby_version = Gem::Requirement.new('>= 1.9.2')
|
20
|
+
|
21
|
+
s.add_dependency 'rails', '>= 3.0'
|
22
|
+
s.add_dependency 'money'
|
23
|
+
s.add_development_dependency 'rspec-rails'
|
24
|
+
s.add_development_dependency 'bundler'
|
25
|
+
s.add_development_dependency 'rspec-rails'
|
26
|
+
s.add_development_dependency 'sqlite3'
|
27
|
+
s.add_development_dependency 'bundler'
|
28
|
+
s.add_development_dependency 'coveralls'
|
29
|
+
end
|
data/lib/freemium.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'money'
|
2
|
+
require_relative '../app/mailers/freemium_mailer'
|
3
|
+
require "freemium/version"
|
4
|
+
require 'freemium/configuration'
|
5
|
+
require 'freemium/coupon'
|
6
|
+
require 'freemium/coupon_redemption'
|
7
|
+
require 'freemium/credit_card'
|
8
|
+
require 'freemium/engine'
|
9
|
+
require 'freemium/rates'
|
10
|
+
require 'freemium/response'
|
11
|
+
require 'freemium/subscription'
|
12
|
+
require 'freemium/subscription_change'
|
13
|
+
require 'freemium/subscription_plan'
|
14
|
+
require 'freemium/transaction'
|
15
|
+
require 'freemium/gateways/base'
|
16
|
+
require 'freemium/gateways/brain_tree'
|
17
|
+
require 'freemium/gateways/test'
|
18
|
+
|
19
|
+
module Freemium
|
20
|
+
class CreditCardStorageError < RuntimeError; end
|
21
|
+
|
22
|
+
def self.root
|
23
|
+
File.expand_path('../..', __FILE__)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Freemium
|
2
|
+
class Configuration
|
3
|
+
attr_accessor \
|
4
|
+
:mailer,
|
5
|
+
:gateway,
|
6
|
+
:days_grace,
|
7
|
+
:days_free_trial,
|
8
|
+
:expired_plan,
|
9
|
+
:admin_report_recipients
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@mailer = FreemiumMailer
|
13
|
+
@gateway = Freemium::Gateways::Test.new
|
14
|
+
@days_grace = 3
|
15
|
+
@days_free_trial = 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
attr_accessor :configuration
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.configure
|
24
|
+
self.configuration ||= Configuration.new
|
25
|
+
yield configuration
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Freemium
|
2
|
+
module Coupon
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
has_many :coupon_redemptions, :dependent => :destroy
|
7
|
+
has_many :subscriptions, :through => :coupon_redemptions
|
8
|
+
has_and_belongs_to_many :subscription_plans
|
9
|
+
|
10
|
+
validates :description, presence: true
|
11
|
+
validates :discount_percentage, presence: true, :inclusion => 0..100
|
12
|
+
|
13
|
+
before_save :normalize_redemption_key
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def discount(rate)
|
18
|
+
rate * (1 - self.discount_percentage.to_f / 100)
|
19
|
+
end
|
20
|
+
|
21
|
+
def expired?
|
22
|
+
(self.redemption_expiration && Date.today > self.redemption_expiration) || (self.redemption_limit && self.coupon_redemptions.count >= self.redemption_limit)
|
23
|
+
end
|
24
|
+
|
25
|
+
def applies_to_plan?(subscription_plan)
|
26
|
+
return true if self.subscription_plans.blank? # applies to all plans
|
27
|
+
self.subscription_plans.include?(subscription_plan)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def normalize_redemption_key
|
33
|
+
self.redemption_key.downcase! unless self.redemption_key.blank?
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,59 @@
|
|
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_validation :upgrade_plan_automatically
|
10
|
+
before_create :set_redeemed_on
|
11
|
+
|
12
|
+
validates_presence_of :coupon
|
13
|
+
validates_presence_of :subscription
|
14
|
+
validates_uniqueness_of :coupon_id, :scope => :subscription_id, :message => "has already been applied"
|
15
|
+
validate :custom_validation, :on => :create
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def expire!(date = Date.today)
|
20
|
+
self.update_attribute :expired_on, date
|
21
|
+
end
|
22
|
+
|
23
|
+
def active?(date = Date.today)
|
24
|
+
expires_on ? date <= self.expires_on : true
|
25
|
+
end
|
26
|
+
|
27
|
+
def expires_on
|
28
|
+
return nil unless self.coupon.duration_in_months
|
29
|
+
self.redeemed_on + self.coupon.duration_in_months.months
|
30
|
+
end
|
31
|
+
|
32
|
+
def redeemed_on
|
33
|
+
self['redeemed_on'] || Date.today
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def set_redeemed_on
|
39
|
+
self.redeemed_on = Date.today
|
40
|
+
end
|
41
|
+
|
42
|
+
def upgrade_plan_automatically
|
43
|
+
if self.coupon &&
|
44
|
+
self.coupon.discount_percentage == 100 &&
|
45
|
+
self.coupon.subscription_plans.count == 1 &&
|
46
|
+
self.coupon.subscription_plans.first.rate > self.subscription.subscription_plan.rate
|
47
|
+
|
48
|
+
self.subscription.subscription_plan = self.coupon.subscription_plans.first
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def custom_validation
|
53
|
+
errors.add :subscription, "must be paid" if self.subscription && !self.subscription.subscription_plan.paid?
|
54
|
+
errors.add :coupon, "has expired" if self.coupon && (self.coupon.expired? || self.coupon.expired?)
|
55
|
+
errors.add :coupon, "is not valid for selected plan" if self.coupon && self.subscription && !self.coupon.applies_to_plan?(self.subscription.subscription_plan)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module Freemium
|
2
|
+
module CreditCard
|
3
|
+
|
4
|
+
CARD_COMPANIES = {
|
5
|
+
:visa => /^4[0-9]{12}(?:[0-9]{3})?$/,
|
6
|
+
:master => /^(5[1-5]\d{4}|677189)\d{10}$/,
|
7
|
+
:discover => /^(6011|65\d{2})\d{12}$/,
|
8
|
+
:american_express => /^3[47]\d{13}$/,
|
9
|
+
:diners_club => /^3(0[0-5]|[68]\d)\d{11}$/,
|
10
|
+
:jcb => /^3528\d{12}$/,
|
11
|
+
:switch => /^6759\d{12}(\d{2,3})?$/,
|
12
|
+
:solo => /^6767\d{12}(\d{2,3})?$/,
|
13
|
+
:dankort => /^5019\d{12}$/,
|
14
|
+
:maestro => /(^6759[0-9]{2}([0-9]{10})$)|(^6759[0-9]{2}([0-9]{12})$)|(^6759[0-9]{2}([0-9]{13})$)/,
|
15
|
+
:forbrugsforeningen => /^600722\d{10}$/,
|
16
|
+
:laser => /^(6304[89]\d{11}(\d{2,3})?|670695\d{13})$/
|
17
|
+
}
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
base.class_eval do
|
21
|
+
# Essential attributes for a valid, non-bogus creditcards
|
22
|
+
attr_accessor :number, :month, :year, :name
|
23
|
+
|
24
|
+
# Required for Switch / Solo cards
|
25
|
+
attr_accessor :start_month, :start_year, :issue_number
|
26
|
+
|
27
|
+
# Optional verification_value (CVV, CVV2 etc). Gateways will try their best to
|
28
|
+
# run validation on the passed in value if it is supplied
|
29
|
+
attr_accessor :verification_value
|
30
|
+
|
31
|
+
has_one :subscription
|
32
|
+
|
33
|
+
before_validation :sanitize_data, :if => :new_or_changed?
|
34
|
+
|
35
|
+
validate :validate_card, :if => :new_or_changed?
|
36
|
+
|
37
|
+
validates :name, presence: true, :if => :new_or_changed?
|
38
|
+
validates :month, presence: true, inclusion: 1..12, :if => :new_or_changed?
|
39
|
+
validates :year, presence: true, inclusion: (Time.now.year..Time.now.year + 20), :if => :new_or_changed?
|
40
|
+
validates :number, presence: true, :if => :new_or_changed?
|
41
|
+
validates :verification_value, presence: true, :if => :new_or_changed?
|
42
|
+
end
|
43
|
+
|
44
|
+
base.extend(ClassMethods)
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
## Callbacks
|
49
|
+
##
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def sanitize_data #:nodoc:
|
54
|
+
self.month = month.to_i if month
|
55
|
+
self.year = year.to_i if year
|
56
|
+
self.number = number.to_s.gsub(/[^\d]/, "")
|
57
|
+
self.card_type = self.class.card_type?(number) if card_type.blank?
|
58
|
+
self.display_number = display_number
|
59
|
+
end
|
60
|
+
|
61
|
+
public
|
62
|
+
|
63
|
+
##
|
64
|
+
## Class Methods
|
65
|
+
##
|
66
|
+
|
67
|
+
module ClassMethods
|
68
|
+
# Returns true if it validates. Optionally, you can pass a card type as an argument and
|
69
|
+
# make sure it is of the correct type.
|
70
|
+
#
|
71
|
+
# References:
|
72
|
+
# - http://perl.about.com/compute/perl/library/nosearch/P073000.htm
|
73
|
+
# - http://www.beachnet.com/~hstiles/cardtype.html
|
74
|
+
def valid_number?(number)
|
75
|
+
valid_card_number_length?(number) &&
|
76
|
+
valid_checksum?(number)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Regular expressions for the known card companies.
|
80
|
+
#
|
81
|
+
# References:
|
82
|
+
# - http://en.wikipedia.org/wiki/Credit_card_number
|
83
|
+
# - http://www.barclaycardbusiness.co.uk/information_zone/processing/bin_rules.html
|
84
|
+
def card_companies
|
85
|
+
CARD_COMPANIES
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns a string containing the type of card from the list of known information below.
|
89
|
+
# Need to check the cards in a particular order, as there is some overlap of the allowable ranges
|
90
|
+
#--
|
91
|
+
# TODO Refactor this method. We basically need to tighten up the Maestro Regexp.
|
92
|
+
#
|
93
|
+
# Right now the Maestro regexp overlaps with the MasterCard regexp (IIRC). If we can tighten
|
94
|
+
# things up, we can boil this whole thing down to something like...
|
95
|
+
#
|
96
|
+
# def type?(number)
|
97
|
+
# return 'visa' if valid_test_mode_card_number?(number)
|
98
|
+
# card_companies.find([nil]) { |type, regexp| number =~ regexp }.first.dup
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
def card_type?(number)
|
102
|
+
card_companies.each do |company, pattern|
|
103
|
+
return company if number =~ pattern
|
104
|
+
end
|
105
|
+
|
106
|
+
return nil
|
107
|
+
end
|
108
|
+
|
109
|
+
def last_digits(number)
|
110
|
+
number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
|
111
|
+
end
|
112
|
+
|
113
|
+
def mask(number)
|
114
|
+
"XXXX-XXXX-XXXX-#{last_digits(number)}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# Checks to see if the calculated type matches the specified type
|
118
|
+
def matching_card_type?(number, card_type)
|
119
|
+
card_type?(number) == card_type
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def valid_card_number_length?(number) #:nodoc:
|
125
|
+
number.to_s.length >= 12
|
126
|
+
end
|
127
|
+
|
128
|
+
# Checks the validity of a card number by use of the the Luhn Algorithm.
|
129
|
+
# Please see http://en.wikipedia.org/wiki/Luhn_algorithm for details.
|
130
|
+
def valid_checksum?(number) #:nodoc:
|
131
|
+
sum = 0
|
132
|
+
for i in 0..number.length
|
133
|
+
weight = number[-1 * (i + 2), 1].to_i * (2 - (i % 2))
|
134
|
+
sum += (weight < 10) ? weight : weight - 9
|
135
|
+
end
|
136
|
+
|
137
|
+
(number[-1,1].to_i == (10 - sum % 10) % 10)
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
## From ActiveMerchant::Billing::CreditCard
|
144
|
+
##
|
145
|
+
|
146
|
+
# Provides proxy access to an expiry date object
|
147
|
+
def expiration_date
|
148
|
+
if @month && @year
|
149
|
+
month_days = [nil,31,28,31,30,31,30,31,31,30,31,30,31]
|
150
|
+
begin
|
151
|
+
month_days[2] = 29 if Date.leap?(@year)
|
152
|
+
str = "#{year}/#{@month}/#{month_days[@month]} 23:59:59"
|
153
|
+
self['expiration_date'] = Time.parse(str)
|
154
|
+
end
|
155
|
+
else
|
156
|
+
self['expiration_date']
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def expired?
|
161
|
+
return false unless expiration_date
|
162
|
+
Time.now > expiration_date
|
163
|
+
end
|
164
|
+
|
165
|
+
# Show the card number, with all but last 4 numbers replace with "X". (XXXX-XXXX-XXXX-4338)
|
166
|
+
def display_number
|
167
|
+
self['display_number'] ||= self.class.mask(number)
|
168
|
+
self['display_number']
|
169
|
+
end
|
170
|
+
|
171
|
+
def last_digits
|
172
|
+
self.class.last_digits(number)
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
## Overrides
|
177
|
+
##
|
178
|
+
|
179
|
+
# We're overriding AR#changed? to include instance vars that aren't persisted to see if a new card is being set
|
180
|
+
def changed?
|
181
|
+
card_type_changed? || [:number, :month, :year, :name, :start_month, :start_year, :issue_number, :verification_value].any? {|attr| !self.send(attr).nil?}
|
182
|
+
end
|
183
|
+
|
184
|
+
def new_or_changed?
|
185
|
+
new_record? || changed?
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
## Validation
|
190
|
+
##
|
191
|
+
|
192
|
+
def validate_card
|
193
|
+
errors.add :year, "expired" if expired?
|
194
|
+
validate_card_number
|
195
|
+
validate_switch_or_solo_attributes
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def validate_card_number #:nodoc:
|
201
|
+
errors.add :number, "is not a valid credit card number" unless self.class.valid_number?(number)
|
202
|
+
end
|
203
|
+
|
204
|
+
def validate_switch_or_solo_attributes #:nodoc:
|
205
|
+
if %w[switch solo].include?(card_type)
|
206
|
+
unless valid_month?(@start_month) && valid_start_year?(@start_year) || valid_issue_number?(@issue_number)
|
207
|
+
errors.add :start_month, "is invalid" unless valid_month?(@start_month)
|
208
|
+
errors.add :start_year, "is invalid" unless valid_start_year?(@start_year)
|
209
|
+
errors.add :issue_number, "cannot be empty" unless valid_issue_number?(@issue_number)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def valid_start_year?(year)
|
215
|
+
year.to_s =~ /^\d{4}$/ && year.to_i > 1987
|
216
|
+
end
|
217
|
+
|
218
|
+
def valid_issue_number?(number)
|
219
|
+
number.to_s =~ /^\d{1,2}$/
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|