app/models/customer.rb
class Customer < ActiveRecord::Base
require_dependency 'customer/roles'
require_dependency 'customer/search'
require_dependency 'customer/special_customers'
require_dependency 'customer/secret_question'
require_dependency 'customer/scopes'
require_dependency 'customer/birthday'
require_dependency 'customer/merge'
include Authentication
include Authentication::ByPassword
include Authentication::ByCookieToken
require 'csv'
has_and_belongs_to_many :labels
has_many :vouchers, -> {
where(:items => {:finalized => true}).
includes(:showdate => :show).
includes(:vouchertype => :valid_vouchers).
order(:updated_at) }
def active_vouchers
vouchers.select { |v| Time.current <= Time.at_end_of_season(v.season) }
end
has_many :vouchertypes, :through => :vouchers
has_many :showdates, :through => :vouchers
has_many :orders, -> { where( 'orders.sold_on IS NOT NULL').order(:sold_on => :desc) }
# nested has_many :through doesn't work in Rails 2, so we define a method instead
# has_many :shows, :through => :showdates
def shows ; self.showdates.map(&:show).uniq ; end
has_many :txns
has_one :most_recent_txn, -> { order('txn_date DESC') }, :class_name=>'Txn'
has_many :donations
has_many :retail_items
has_many :items # the superclass of vouchers,donations,retail_items
belongs_to :ticket_sales_import
# There are multiple 'flavors' of customers with different validation requirements.
# These should be factored out into subclasses.
# | Type | When used | Validations |
# | Customer (base) | | |
# | SelfCreated | signup; edit info; change pw | All |
# | GuestCheckout | guest checkout | nonblank email,first,last,address |
# | GiftRecipient | giftee of someone else | nonblank first,last; nonblank email OR phone |
# | Imported | import from 3rd party | none, but all fields FORCED valid on create |
VALID_EMAIL_REGEXP = /\A\S+@\S+\z/
validates_format_of :email, :if => :self_created?, :with => VALID_EMAIL_REGEXP
def restricted_email
return if (domain = Option.restrict_customer_email_to_domain.to_s).blank?
errors.add(:email, "must end in '#{domain}'") if
!email.blank? && !email.match( /#{domain}\z/i )
end
validate :restricted_email, :if => :self_created?, :on => :create
EMAIL_UNIQUENESS_ERROR_MESSAGE = 'has already been registered.'
validates_uniqueness_of :email,
:allow_blank => true,
:case_sensitive => false,
:message => EMAIL_UNIQUENESS_ERROR_MESSAGE
def unique_email_error
self.errors[:email].include? EMAIL_UNIQUENESS_ERROR_MESSAGE
end
validates_format_of :zip, :if => :self_created?, :with => /\A^[0-9]{5}-?([0-9]{4})?\z/, :allow_blank => true
validate :valid_or_blank_address?, :if => :self_created?
validate :valid_as_gift_recipient?, :if => :gift_recipient_only
NAME_REGEX = /\A[-A-Za-z0-9_\/#\@'":;,.%\ ()&]*\z/
NAME_FORBIDDEN_CHARS = /[^-A-Za-z0-9_\/#\@'":;,.%\ ()&]/
BAD_NAME_MSG = "must not include special characters like <, >, !, etc."
validates_length_of :first_name, :within => 1..50
validates_format_of :first_name, :with => NAME_REGEX, :message => BAD_NAME_MSG
validates_length_of :last_name, :within => 1..50
validates_format_of :last_name, :with => NAME_REGEX, :message => BAD_NAME_MSG
attr_accessor :must_revalidate_password
validates :password, :length => {:in => 3..20}, :on => :create, :if => :self_created?
validates :password, :length => {:in => 3..20}, :on => :update, :if => :must_revalidate_password
validates_confirmation_of :password, :on => :create, :unless => :created_by_admin
validates_confirmation_of :password, :on => :update, :if => :must_revalidate_password
attr_accessor :force_valid
attr_accessor :gift_recipient_only
attr_accessor :password
cattr_reader :replaceable_attributes, :extra_attributes
@@replaceable_attributes =
%w(first_name last_name email street city state zip day_phone eve_phone
company title company_address_line_1 company_address_line_2 company_url
company_city company_state company_zip work_phone cell_phone work_fax
best_way_to_contact
)
@@extra_attributes =
[:company, :title, :company_address_line_1, :company_address_line_2,
:company_city, :company_state, :company_zip, :work_phone, :cell_phone,
:work_fax, :company_url]
def self.user_modifiable_attributes
[:first_name, :last_name, :email, :password, :password_confirmation, :blacklist, :e_blacklist,
:day_phone, :eve_phone, :street, :city, :state, :zip, :birthday]
end
before_validation :force_valid_fields, :on => :create
before_save :trim_whitespace_from_user_entered_strings
after_save :update_email_subscription
before_destroy :cannot_destroy_special_customers
#----------------------------------------------------------------------
# private variables
#----------------------------------------------------------------------
private
def self_created? ; !created_by_admin && !gift_recipient_only ; end
# for things like daemon-created customers, the force_valid flag will cause a customer
# to be created with minimal valid fields so that saving cannot possibly fail validations.
def force_valid_fields
if self.force_valid
self.created_by_admin = true # will skip most validations
self.first_name = '_' if first_name.blank?
self.first_name.gsub!(NAME_FORBIDDEN_CHARS, '_')
self.last_name = '_' if last_name.blank?
self.last_name.gsub!(NAME_FORBIDDEN_CHARS, '_')
self.email = nil unless valid_and_nonexistent(email)
end
true
end
def valid_and_nonexistent(email)
email.blank? ||
(email =~ VALID_EMAIL_REGEXP && Customer.find_by_email(email).nil?)
end
# match up donation customer with an existing one, or create it
def self.for_donation(params)
customer_info = Customer.new params
found_customer = Customer.find_unique(customer_info)
if found_customer
# unique match; but if our stored customer doesn't have a valid name/billing address,
# update it from this info.
found_customer.update_for_purchase_from(customer_info)
# if at this point we have a valid customer, done
return found_customer if found_customer.valid_as_purchaser?
end
# no unique match: find or create from the info we do have
if customer_info.valid_as_purchaser?
# create this customer
Customer.find_or_create!(customer_info)
else
# invalid info given
customer_info # has failed validation as purchaser
end
end
# address is allowed to be blank, but if nonblank, it must be valid
def valid_or_blank_address?
blank_mailing_address? || valid_mailing_address?
end
# when customer is saved, possibly update their email opt-in status
# with external mailing list.
# NOTE: This is an after-save hook, so customer is guaranteed to exist in database.
def update_email_subscription
return unless (e_blacklist_changed? || email_changed? || first_name_changed? || last_name_changed?)
email_list = EmailList.new or return
if e_blacklist # opting out of email
# do nothing, EXCEPT in the case where customer is transitioning from opt-in to opt-out,
# AND they have a in which case unsubscribe them
if !e_blacklist_was and !email_was.blank?
email_list.unsubscribe(self, email_was)
end
else # opt in
if (email_changed? || first_name_changed? || last_name_changed?)
if email_was.blank?
email_list.subscribe(self)
elsif email.blank?
email_list.unsubscribe(self, email_was)
else
email_list.update(self, email_was)
end
else # with same email
email_list.subscribe(self)
end
end
end
def encourage_opt_in_message
if !(m = Option.encourage_email_opt_in).blank?
m << '.' unless m =~ /[.!?:;,]$/
m << ' Click the Billing/Contact tab (above) to update your preferences.'
m
else ''
end
end
def welcome_message
subscriber? ? Option.welcome_page_subscriber_message.to_s :
Option.welcome_page_nonsubscriber_message.to_s
end
#----------------------------------------------------------------------
# public methods
#----------------------------------------------------------------------
public
def self.id_from_route(route)
(Rails.application.routes.recognize_path(route))[:id]
end
# message that will appear in flash[:notice] once only, at login
def login_message
msg = ["Welcome, #{full_name}"]
msg << encourage_opt_in_message if has_opted_out_of_email?
msg << I18n.t('login.setup_secret_question_message') unless has_secret_question?
msg << welcome_message
msg
end
def valid_reset_token?
token_created_at &&
token_created_at >= 10.minutes.ago
end
def record_login!
self.update_attributes!(:last_login => Time.current)
end
def has_ever_logged_in?
last_login > Time.zone.parse('2007-04-07') # sentinel date should match what's in schema.rb
end
def set_labels(labels_list)
self.labels = (
labels_list.respond_to?(:each) ?
Label.all_labels.select { |l| labels_list.include?(l.id) } :
[])
end
def update_labels!(hash)
self.set_labels(hash)
self.save!
end
def valid_as_guest_checkout?
if (first_name.blank? || last_name.blank? || email.blank? || street.blank? || city.blank? || state.blank? || zip.blank?)
errors.add :base, "Please provide your email address for order confirmation, and your credit card billing name and address."
false
else
# this is a HACK: set created_by_admin to bypass most other validations.
# This will be fixed when Customer class is refactored into subclasses with their own validations
self.created_by_admin = true
true
end
end
def valid_as_gift_recipient?
# must have first and last name, mailing addr, and at least one
# phone or email
valid = true
if (first_name.blank? || last_name.blank?)
errors.add :base,"First and last name must be provided"
valid = false
end
if invalid_mailing_address?
errors.add :base,"Valid mailing address must be provided for #{self.full_name}"
valid = false
end
if day_phone.blank? && eve_phone.blank? && !valid_email_address?
errors.add :base,"At least one phone number or email address must be provided for #{self.full_name}"
valid = false
end
valid
end
def valid_as_purchaser?
# must have full address and full name
valid_mailing_address? && !first_name.blank? && !last_name.blank?
end
def update_for_purchase_from(other_customer)
%w(first_name last_name street city state zip).each do |field|
other = other_customer.send(field)
self.send("#{field}=", other) unless other.blank?
end
end
@@user_entered_strings =
%w[first_name last_name street city state zip day_phone eve_phone email]
# strip whitespace before saving
def trim_whitespace_from_user_entered_strings
@@user_entered_strings.each do |col|
c = self.send(col)
c.send(:strip!) if c.kind_of?(String)
end
end
# a convenient wrapper class for the ActiveRecord::sanitize_sql protected method
def self.render_sql(sql)
ActiveRecord::Base.sanitize_sql(sql)
end
# convenience accessors
def to_s
"[#{id}] #{full_name} " << (email.blank? ? '' : "<#{email}> ")
end
def inspect
self.to_s <<
(street.blank? ? '' : " #{street}, #{city} #{state} #{zip} #{day_phone}")
end
def full_name
"#{first_name.to_s.name_capitalize} #{last_name.to_s.name_capitalize}"
end
def full_name_with_email
valid_email_address? ? "#{full_name} (#{email})" : full_name
end
def full_name_with_email_and_address
street.blank? ? full_name_with_email : "#{full_name_with_email} (#{street})"
end
def sortable_name
"#{self.last_name.downcase},#{self.first_name.downcase}"
end
def valid_email_address?
email.to_s.match(/^\S+@\S+/)
end
def has_opted_out_of_email? ; e_blacklist? && valid_email_address? end
def valid_mailing_address?
%w(street city state zip).each do |field|
errors.add(field, "can't be blank") if self.send(field).blank?
end
errors.add(:zip, 'must be between 5 and 10 characters') if !zip.blank? && !zip.to_s.length.between?(5,10)
errors.empty?
end
def invalid_mailing_address? ; !valid_mailing_address? ; end
def blank_mailing_address?
street.blank? && city.blank? && zip.blank?
end
def subscriber?
self.role >= 0 &&
self.vouchers.includes(:vouchertype).detect do |f|
f.vouchertype.subscription? && f.vouchertype.valid_now?
end
end
def next_season_subscriber?
self.role >= 0 &&
self.vouchers.includes(:vouchertype).detect do |f|
f.vouchertype.subscription? &&
f.vouchertype.expiration_date.within_season?(Time.current.at_end_of_season + 1.year)
end
end
# add items to a customer's account - could be vouchers, record of a
# donation, or purchased goods
def add_items(new_items)
self.items << new_items
# new_items.each { |i| i.customer_id = self.id }
# self.items += new_items # <= doesn't work because cardinality of self.items is huge
end
def self.authenticate(email, password)
if (email.blank? || password.blank?)
u = Customer.new
u.errors.add(:login_failed, I18n.t('login.email_or_password_blank'))
return u
end
unless (u = Customer.find_by_email(email)) # need to get the salt
u = Customer.new
u.errors.add(:login_failed, I18n.t('login.no_such_email'))
return u
end
unless u.authenticated?(password)
u.errors.add(:login_failed, I18n.t('login.bad_password'))
end
return u
end
def self.can_ignore_cutoff?(id)
Customer.find(id).is_walkup
end
public
# Override content_columns method to omit password hash and salt
def self.content_columns
super.delete_if { |x| x.name.match(%w[role crypted_password salt _at$ _on$].join('|')) }
end
# Convert list of customers to CSV. If with_errors is true, last column is
# ActiveRecord error messages for the customer (joined with ';').
def self.csv_header
['First name', 'Last name', 'ID', 'Email', 'Street', 'City', 'State', 'Zip',
'Day/main phone', 'Eve/alt phone', "Don't mail", "Don't email"].freeze
end
def self.to_csv(custs,opts={})
CSV::Writer.generate(output='') do |csv|
unless opts[:suppress_header]
header = self.csv_header
header += opts[:extra].map(&:humanize) if opts[:extra]
csv << header
end
custs.each do |c|
row = c.to_csv
opts[:extra].each { |attrib| row << c.send(attrib) }
row << c.errors.full_messages.join('; ') if opts[:include_errors]
csv << row
end
return output
end
end
def name_and_address_to_csv
[
(first_name.name_capitalize unless first_name.blank?),
(last_name.name_capitalize unless last_name.blank?),
email,
street,city,state,zip
]
end
def to_csv
[
(first_name.name_capitalize unless first_name.blank?),
(last_name.name_capitalize unless last_name.blank?),
id,
email,
street,city,state,zip,
day_phone, eve_phone,
(blacklist ? "true" : ""),
(e_blacklist ? "true" : "")
]
end
end