485 lines
17 KiB
Ruby
485 lines
17 KiB
Ruby
class JamRuby::AffiliatePartner < ActiveRecord::Base
|
|
self.table_name = 'affiliate_partners'
|
|
|
|
belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner
|
|
has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id
|
|
belongs_to :affiliate_legalese, :class_name => "JamRuby::AffiliateLegalese", :foreign_key => :legalese_id
|
|
has_many :sale_line_items, :class_name => 'JamRuby::SaleLineItem', foreign_key: :affiliate_referral_id
|
|
has_many :quarters, :class_name => 'JamRuby::AffiliateQuarterlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
|
|
has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
|
|
has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
|
|
has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
|
|
attr_accessible :partner_name, :partner_code, :partner_user_id
|
|
|
|
ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other }
|
|
|
|
KEY_ADDR1 = 'address1'
|
|
KEY_ADDR2 = 'address2'
|
|
KEY_CITY = 'city'
|
|
KEY_STATE = 'state'
|
|
KEY_POSTAL = 'postal_code'
|
|
KEY_COUNTRY = 'country'
|
|
|
|
# ten dollars in cents
|
|
PAY_THRESHOLD = 10 * 100
|
|
|
|
AFFILIATE_PARAMS="utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate="
|
|
|
|
ADDRESS_SCHEMA = {
|
|
KEY_ADDR1 => '',
|
|
KEY_ADDR2 => '',
|
|
KEY_CITY => '',
|
|
KEY_STATE => '',
|
|
KEY_POSTAL => '',
|
|
KEY_COUNTRY => '',
|
|
}
|
|
|
|
PARAM_REFERRAL = :ref
|
|
PARAM_COOKIE = :affiliate_ref
|
|
|
|
PARTNER_CODE_REGEX = /^[#{Regexp.escape('abcdefghijklmnopqrstuvwxyz0123456789-._+,')}]+{2,128}$/i
|
|
|
|
#validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email
|
|
#validates :partner_code, format: { with: PARTNER_CODE_REGEX }, :allow_blank => true
|
|
validates :entity_type, inclusion: {in: ENTITY_TYPES, message: "invalid entity type"}
|
|
|
|
serialize :address, JSON
|
|
|
|
before_save do |record|
|
|
record.address ||= ADDRESS_SCHEMA.clone
|
|
record.entity_type ||= ENTITY_TYPES.first
|
|
end
|
|
|
|
def display_name
|
|
partner_name || (partner_user ? partner_user.name : 'abandoned')
|
|
end
|
|
|
|
def admin_url
|
|
APP_CONFIG.admin_root_url + "/admin/affiliates/#{id}"
|
|
end
|
|
|
|
# used by admin
|
|
def self.create_with_params(params={})
|
|
raise 'not supported'
|
|
oo = self.new
|
|
oo.partner_name = params[:partner_name].try(:strip)
|
|
oo.partner_code = params[:partner_code].try(:strip).try(:downcase)
|
|
oo.partner_user = User.where(:email => params[:user_email].try(:strip)).limit(1).first
|
|
oo.partner_user_id = oo.partner_user.try(:id)
|
|
oo.entity_type = params[:entity_type] || ENTITY_TYPES.first
|
|
oo.save
|
|
oo
|
|
end
|
|
|
|
# used by web
|
|
def self.create_with_web_params(user, params={})
|
|
oo = self.new
|
|
oo.partner_name = params[:partner_name].try(:strip)
|
|
oo.partner_user = user if user # user is not required
|
|
oo.entity_type = params[:entity_type] || ENTITY_TYPES.first
|
|
oo.signed_at = Time.now
|
|
oo.save
|
|
oo
|
|
end
|
|
|
|
def self.coded_id(code=nil)
|
|
self.where(:partner_code => code).limit(1).pluck(:id).first if code.present?
|
|
end
|
|
|
|
def self.is_code?(code)
|
|
self.where(:partner_code => code).limit(1).pluck(:id).present?
|
|
end
|
|
|
|
def referrals_by_date
|
|
by_date = User.where(:affiliate_referral_id => self.id)
|
|
.group('DATE(created_at)')
|
|
.having("COUNT(*) > 0")
|
|
.order('date_created_at DESC')
|
|
.count
|
|
block_given? ? yield(by_date) : by_date
|
|
end
|
|
|
|
def signed_legalese(legalese)
|
|
self.affiliate_legalese = legalese
|
|
self.signed_at = Time.now
|
|
save!
|
|
end
|
|
|
|
def update_address_value(key, val)
|
|
self.address[key] = val
|
|
self.update_attribute(:address, self.address)
|
|
end
|
|
|
|
def address_value(key)
|
|
self.address[key]
|
|
end
|
|
|
|
def created_within_affiliate_window(user, sale_time)
|
|
sale_time - user.created_at < 2.years
|
|
end
|
|
|
|
def should_attribute_sale?(shopping_cart)
|
|
if shopping_cart.is_jam_track?
|
|
if created_within_affiliate_window(shopping_cart.user, Time.now)
|
|
product_info = shopping_cart.product_info
|
|
# subtract the total quantity from the freebie quantity, to see how much we should attribute to them
|
|
real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
|
|
{fee_in_cents: real_quantity * 20}
|
|
else
|
|
false
|
|
end
|
|
else
|
|
raise 'shopping cart type not implemented yet'
|
|
end
|
|
end
|
|
|
|
def cumulative_earnings_in_dollars
|
|
cumulative_earnings_in_cents.to_f / 100.to_f
|
|
end
|
|
|
|
def self.quarter_info(date)
|
|
|
|
year = date.year
|
|
|
|
# which quarter?
|
|
quarter = -1
|
|
if date.month >= 1 && date.month <= 3
|
|
quarter = 0
|
|
elsif date.month >= 4 && date.month <= 6
|
|
quarter = 1
|
|
elsif date.month >= 7 && date.month <= 9
|
|
quarter = 2
|
|
elsif date.month >= 10 && date.month <= 12
|
|
quarter = 3
|
|
end
|
|
|
|
raise 'quarter should never be -1' if quarter == -1
|
|
|
|
previous_quarter = quarter - 1
|
|
previous_year = date.year
|
|
if previous_quarter == -1
|
|
previous_quarter = 3
|
|
previous_year = year - 1
|
|
end
|
|
|
|
raise 'previous quarter should never be -1' if previous_quarter == -1
|
|
|
|
{year: year, quarter: quarter, previous_quarter: previous_quarter, previous_year: previous_year}
|
|
end
|
|
|
|
def self.did_quarter_elapse?(quarter_info, last_tallied_info)
|
|
if last_tallied_info.nil?
|
|
true
|
|
else
|
|
quarter_info == last_tallied_info
|
|
end
|
|
end
|
|
|
|
# meant to be run regularly; this routine will make summarized counts in the
|
|
# AffiliateQuarterlyPayment table
|
|
# AffiliatePartner.cumulative_earnings_in_cents, AffiliatePartner.referral_user_count
|
|
def self.tally_up(day)
|
|
|
|
AffiliatePartner.transaction do
|
|
quarter_info = quarter_info(day)
|
|
last_tallied_info = quarter_info(GenericState.affiliate_tallied_at) if GenericState.affiliate_tallied_at
|
|
|
|
quarter_elapsed = did_quarter_elapse?(quarter_info, last_tallied_info)
|
|
|
|
if quarter_elapsed
|
|
tally_monthly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter])
|
|
tally_quarterly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter])
|
|
end
|
|
tally_monthly_payments(quarter_info[:year], quarter_info[:quarter])
|
|
tally_quarterly_payments(quarter_info[:year], quarter_info[:quarter])
|
|
|
|
tally_traffic_totals(GenericState.affiliate_tallied_at, day)
|
|
|
|
tally_partner_totals
|
|
|
|
state = GenericState.singleton
|
|
state.affiliate_tallied_at = day
|
|
state.save!
|
|
end
|
|
|
|
end
|
|
|
|
# this just makes sure that the quarter rows exist before later manipulations with UPDATEs
|
|
def self.ensure_quarters_exist(year, quarter)
|
|
|
|
sql = %{
|
|
INSERT INTO affiliate_quarterly_payments (quarter, year, affiliate_partner_id)
|
|
(SELECT #{quarter}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN
|
|
(SELECT affiliate_partner_id FROM affiliate_quarterly_payments WHERE year = #{year} AND quarter = #{quarter}))
|
|
}
|
|
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
end
|
|
|
|
# this just makes sure that the quarter rows exist before later manipulations with UPDATEs
|
|
def self.ensure_months_exist(year, quarter)
|
|
|
|
months = [1, 2, 3].collect! { |i| quarter * 3 + i }
|
|
|
|
months.each do |month|
|
|
sql = %{
|
|
INSERT INTO affiliate_monthly_payments (month, year, affiliate_partner_id)
|
|
(SELECT #{month}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN
|
|
(SELECT affiliate_partner_id FROM affiliate_monthly_payments WHERE year = #{year} AND month = #{month}))
|
|
}
|
|
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
end
|
|
end
|
|
|
|
|
|
def self.sale_items_subquery(start_date, end_date, table_name)
|
|
%{
|
|
FROM sale_line_items
|
|
WHERE
|
|
(DATE(sale_line_items.created_at) >= DATE('#{start_date}') AND DATE(sale_line_items.created_at) <= DATE('#{end_date}'))
|
|
AND
|
|
sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id
|
|
}
|
|
end
|
|
|
|
def self.sale_items_refunded_subquery(start_date, end_date, table_name)
|
|
%{
|
|
FROM sale_line_items
|
|
WHERE
|
|
(DATE(sale_line_items.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(sale_line_items.affiliate_refunded_at) <= DATE('#{end_date}'))
|
|
AND
|
|
sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id
|
|
AND
|
|
sale_line_items.affiliate_refunded = TRUE
|
|
}
|
|
end
|
|
# total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id
|
|
# don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE
|
|
def self.total_months(year, quarter)
|
|
|
|
months = [1, 2, 3].collect! { |i| quarter * 3 + i }
|
|
|
|
|
|
months.each do |month|
|
|
|
|
start_date, end_date = boundary_dates_for_month(year, month)
|
|
|
|
sql = %{
|
|
UPDATE affiliate_monthly_payments
|
|
SET
|
|
last_updated = NOW(),
|
|
jamtracks_sold =
|
|
COALESCE(
|
|
(SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
|
|
#{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')}
|
|
), 0)
|
|
+
|
|
COALESCE(
|
|
(SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
|
|
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')}
|
|
), 0),
|
|
due_amount_in_cents =
|
|
COALESCE(
|
|
(SELECT SUM(affiliate_referral_fee_in_cents)
|
|
#{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')}
|
|
), 0)
|
|
+
|
|
COALESCE(
|
|
(SELECT -SUM(affiliate_referral_fee_in_cents)
|
|
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')}
|
|
), 0)
|
|
|
|
WHERE closed = FALSE AND year = #{year} AND month = #{month}
|
|
}
|
|
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
end
|
|
end
|
|
|
|
# close any quarters that are done, so we don't manipulate them again
|
|
def self.close_months(year, quarter)
|
|
# close any quarters that occurred before this quarter
|
|
month = quarter * 3 + 1
|
|
|
|
sql = %{
|
|
UPDATE affiliate_monthly_payments
|
|
SET
|
|
closed = TRUE, closed_at = NOW()
|
|
WHERE year < #{year} OR month < #{month}
|
|
}
|
|
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
|
|
end
|
|
|
|
# total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id
|
|
# don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE
|
|
def self.total_quarters(year, quarter)
|
|
start_date, end_date = boundary_dates(year, quarter)
|
|
|
|
sql = %{
|
|
UPDATE affiliate_quarterly_payments
|
|
SET
|
|
last_updated = NOW(),
|
|
jamtracks_sold =
|
|
COALESCE(
|
|
(SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
|
|
#{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
|
|
), 0)
|
|
+
|
|
COALESCE(
|
|
(SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
|
|
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
|
|
), 0),
|
|
due_amount_in_cents =
|
|
COALESCE(
|
|
(SELECT SUM(affiliate_referral_fee_in_cents)
|
|
#{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
|
|
), 0)
|
|
+
|
|
COALESCE(
|
|
(SELECT -SUM(affiliate_referral_fee_in_cents)
|
|
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
|
|
), 0)
|
|
|
|
WHERE closed = FALSE AND paid = FALSE AND year = #{year} AND quarter = #{quarter}
|
|
}
|
|
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
end
|
|
|
|
# close any quarters that are done, so we don't manipulate them again
|
|
def self.close_quarters(year, quarter)
|
|
# close any quarters that occurred before this quarter
|
|
sql = %{
|
|
UPDATE affiliate_quarterly_payments
|
|
SET
|
|
closed = TRUE, closed_at = NOW()
|
|
WHERE year < #{year} OR quarter < #{quarter}
|
|
}
|
|
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
end
|
|
|
|
def self.tally_quarterly_payments(year, quarter)
|
|
ensure_quarters_exist(year, quarter)
|
|
|
|
total_quarters(year, quarter)
|
|
|
|
close_quarters(year, quarter)
|
|
end
|
|
|
|
|
|
def self.tally_monthly_payments(year, quarter)
|
|
ensure_months_exist(year, quarter)
|
|
|
|
total_months(year, quarter)
|
|
|
|
close_months(year, quarter)
|
|
end
|
|
|
|
def self.tally_partner_totals
|
|
sql = %{
|
|
UPDATE affiliate_partners SET
|
|
referral_user_count = (SELECT count(*) FROM users WHERE affiliate_partners.id = users.affiliate_referral_id),
|
|
cumulative_earnings_in_cents = (SELECT COALESCE(SUM(due_amount_in_cents), 0) FROM affiliate_quarterly_payments AS aqp WHERE aqp.affiliate_partner_id = affiliate_partners.id AND closed = TRUE and paid = TRUE)
|
|
}
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
end
|
|
|
|
def self.tally_traffic_totals(last_tallied_at, target_day)
|
|
|
|
if last_tallied_at
|
|
start_date = last_tallied_at.to_date
|
|
end_date = target_day.to_date
|
|
else
|
|
start_date = target_day.to_date - 1
|
|
end_date = target_day.to_date
|
|
end
|
|
|
|
if start_date == end_date
|
|
return
|
|
end
|
|
|
|
sql = %{
|
|
INSERT INTO affiliate_traffic_totals(SELECT day, 0, 0, ap.id FROM affiliate_partners AS ap CROSS JOIN (select (generate_series('#{start_date}', '#{end_date - 1}', '1 day'::interval))::date as day) AS lurp)
|
|
}
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
|
|
sql = %{
|
|
UPDATE affiliate_traffic_totals traffic SET visits = COALESCE((SELECT COALESCE(count(affiliate_partner_id), 0) FROM affiliate_referral_visits v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_partner_id = traffic.affiliate_partner_id GROUP BY affiliate_partner_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}')
|
|
}
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
|
|
sql = %{
|
|
UPDATE affiliate_traffic_totals traffic SET signups = COALESCE((SELECT COALESCE(count(v.id), 0) FROM users v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_referral_id = traffic.affiliate_partner_id GROUP BY affiliate_referral_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}')
|
|
}
|
|
ActiveRecord::Base.connection.execute(sql)
|
|
end
|
|
|
|
def self.boundary_dates(year, quarter)
|
|
if quarter == 0
|
|
[Date.new(year, 1, 1), Date.new(year, 3, 31)]
|
|
elsif quarter == 1
|
|
[Date.new(year, 4, 1), Date.new(year, 6, 30)]
|
|
elsif quarter == 2
|
|
[Date.new(year, 7, 1), Date.new(year, 9, 30)]
|
|
elsif quarter == 3
|
|
[Date.new(year, 10, 1), Date.new(year, 12, 31)]
|
|
else
|
|
raise "invalid quarter #{quarter}"
|
|
end
|
|
end
|
|
|
|
# 1-based month
|
|
def self.boundary_dates_for_month(year, month)
|
|
[Date.new(year, month, 1), Date.civil(year, month, -1)]
|
|
end
|
|
|
|
# Finds all affiliates that need to be paid
|
|
def self.unpaid
|
|
|
|
joins(:quarters)
|
|
.where('affiliate_quarterly_payments.paid = false').where('affiliate_quarterly_payments.closed = true')
|
|
.group('affiliate_partners.id')
|
|
.having('sum(due_amount_in_cents) >= ?', PAY_THRESHOLD)
|
|
.order('sum(due_amount_in_cents) DESC')
|
|
|
|
end
|
|
|
|
# does this one affiliate need to be paid?
|
|
def unpaid
|
|
due_amount_in_cents > PAY_THRESHOLD
|
|
end
|
|
|
|
# admin function: mark the affiliate paid
|
|
def mark_paid
|
|
if unpaid
|
|
transaction do
|
|
now = Time.now
|
|
quarters.where(paid:false, closed:true).update_all(paid:true, paid_at: now)
|
|
self.last_paid_at = now
|
|
self.save!
|
|
end
|
|
end
|
|
end
|
|
|
|
# how much is this affiliate due?
|
|
def due_amount_in_cents
|
|
total_in_cents = 0
|
|
quarters.where(paid:false, closed:true).each do |quarter|
|
|
total_in_cents = total_in_cents + quarter.due_amount_in_cents
|
|
end
|
|
total_in_cents
|
|
end
|
|
|
|
def affiliate_query_params
|
|
AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s
|
|
end
|
|
|
|
def to_s
|
|
display_name
|
|
end
|
|
end
|