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_type, :rate, as: :admin 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 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: (product_info[:price] * 100 * real_quantity * rate.to_f).round} else false 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