2014-04-20 22:54:49 +00:00
class JamRuby :: AffiliatePartner < ActiveRecord :: Base
2015-05-28 13:20:14 +00:00
self . table_name = 'affiliate_partners'
2014-04-20 22:54:49 +00:00
2015-05-28 13:20:14 +00:00
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
2014-04-20 22:54:49 +00:00
attr_accessible :partner_name , :partner_code , :partner_user_id
2015-05-28 13:20:14 +00:00
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 = > '' ,
}
2014-04-22 01:55:40 +00:00
PARAM_REFERRAL = :ref
PARAM_COOKIE = :affiliate_ref
2014-04-20 23:20:27 +00:00
PARTNER_CODE_REGEX = / ^[ #{ Regexp . escape ( 'abcdefghijklmnopqrstuvwxyz0123456789-._+,' ) } ]+{2,128}$ /i
2015-05-28 13:20:14 +00:00
#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
2014-04-20 22:54:49 +00:00
2015-05-28 13:20:14 +00:00
before_save do | record |
record . address || = ADDRESS_SCHEMA . clone
record . entity_type || = ENTITY_TYPES . first
end
2015-10-18 14:05:24 +00:00
def display_name
partner_name || ( partner_user ? partner_user . name : 'abandoned' )
end
2015-10-19 19:34:06 +00:00
def admin_url
2015-10-19 22:53:50 +00:00
APP_CONFIG . admin_root_url + " /admin/affiliates/ #{ id } "
2015-10-19 19:34:06 +00:00
end
2015-05-28 13:20:14 +00:00
# used by admin
2014-04-20 22:54:49 +00:00
def self . create_with_params ( params = { } )
2015-05-28 13:20:14 +00:00
raise 'not supported'
2014-04-20 22:54:49 +00:00
oo = self . new
2014-04-20 23:20:27 +00:00
oo . partner_name = params [ :partner_name ] . try ( :strip )
2014-04-22 02:14:22 +00:00
oo . partner_code = params [ :partner_code ] . try ( :strip ) . try ( :downcase )
2014-04-20 23:20:27 +00:00
oo . partner_user = User . where ( :email = > params [ :user_email ] . try ( :strip ) ) . limit ( 1 ) . first
2014-04-20 22:54:49 +00:00
oo . partner_user_id = oo . partner_user . try ( :id )
2015-05-28 13:20:14 +00:00
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
2014-04-22 01:55:40 +00:00
oo . save
2014-04-20 22:54:49 +00:00
oo
end
def self . coded_id ( code = nil )
self . where ( :partner_code = > code ) . limit ( 1 ) . pluck ( :id ) . first if code . present?
end
2014-04-22 02:14:22 +00:00
def self . is_code? ( code )
2015-05-28 13:20:14 +00:00
self . where ( :partner_code = > code ) . limit ( 1 ) . pluck ( :id ) . present?
2014-04-22 02:14:22 +00:00
end
2014-04-20 22:54:49 +00:00
2014-04-22 07:12:05 +00:00
def referrals_by_date
2014-04-23 06:34:21 +00:00
by_date = User . where ( :affiliate_referral_id = > self . id )
2015-05-28 13:20:14 +00:00
. group ( 'DATE(created_at)' )
. having ( " COUNT(*) > 0 " )
. order ( 'date_created_at DESC' )
. count
2014-04-23 06:34:21 +00:00
block_given? ? yield ( by_date ) : by_date
2014-04-22 07:12:05 +00:00
end
2015-05-28 13:20:14 +00:00
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
2015-05-29 20:30:23 +00:00
[ Date . new ( year , 10 , 1 ) , Date . new ( year , 12 , 31 ) ]
2015-05-28 13:20:14 +00:00
else
raise " invalid quarter #{ quarter } "
end
end
2015-05-29 20:30:23 +00:00
# 1-based month
2015-05-28 13:20:14 +00:00
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
2015-10-19 22:53:50 +00:00
def to_s
display_name
end
2014-04-22 02:14:22 +00:00
end