diff --git a/admin/app/admin/ad_campaigns.rb b/admin/app/admin/ad_campaigns.rb
new file mode 100644
index 000000000..c9294e3e5
--- /dev/null
+++ b/admin/app/admin/ad_campaigns.rb
@@ -0,0 +1,154 @@
+module AdCampaignsHelper
+ def self.spacer(val, total)
+ percentage = ((val * 100) / total.to_f).round(1).to_s
+ ('%-5.5s' % percentage).gsub(' ', ' ') + '% - ' + val.to_s
+ end
+
+ def self.cac(campaign)
+ if campaign.subscribed && campaign.subscribed > 0
+ (campaign.spend/campaign.subscribed.to_f).round(2)
+ end
+ end
+
+ def self.cac_divide_by_ltv(campaign)
+ customer_ltv = GenericState.singleton.customer_ltv
+ if cac(campaign) && customer_ltv && customer_ltv > 0
+ return (cac(campaign)/customer_ltv.to_f).round(2)
+ end
+ end
+
+ def self.format_number(num)
+ if num
+ num.to_s.reverse.scan(/\d{3}|.+/).join(",").reverse
+ end
+ end
+
+end
+
+
+ActiveAdmin.register JamRuby::AdCampaign, as: 'AdCampaign' do
+ menu :label => 'Ad Campaigns', :parent => 'Reports'
+ before_filter :skip_sidebar!, :only => :index
+ config.batch_actions = false
+
+ index do
+ div do
+ render 'customer_ltv'
+ end
+
+ column "Campaign" do |campaign|
+ campaign.origin_utm_campaign
+ end
+ column "Medium" do |campaign|
+ campaign.origin_utm_medium
+ end
+ column "End Date" do |campaign|
+ best_in_place campaign, :end_date, as: :date, url: inplace_update_admin_ad_campaigns_path(campaign: campaign.origin_utm_campaign, medium: campaign.origin_utm_medium), param: 'ad_campaign', classes: 'ac_bip'
+ end
+ column "Hard Date" do |campaign|
+ (campaign.end_date + 45.days).strftime('%Y-%m-%d') if campaign.end_date.present?
+ end
+ column "Subscribed" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.subscribed, campaign.joined))
+ end
+ column "Spend" do |campaign|
+ best_in_place campaign, :spend, as: :input, url: inplace_update_admin_ad_campaigns_path(campaign: campaign.origin_utm_campaign, medium: campaign.origin_utm_medium), param: 'ad_campaign', display_with: Proc.new{|spend| number_to_currency(spend) }, classes: 'ac_bip'
+ end
+ column "CAC" do |campaign|
+ number_to_currency(AdCampaignsHelper.cac(campaign)) if AdCampaignsHelper.cac(campaign) && AdCampaignsHelper.cac(campaign) > 0
+ end
+ column "LTV/CAC" do |campaign|
+ AdCampaignsHelper.cac_divide_by_ltv(campaign)
+ end
+ column "Referred" do |campaign|
+ best_in_place campaign, :referred, as: :input, url: inplace_update_admin_ad_campaigns_path(campaign: campaign.origin_utm_campaign, medium: campaign.origin_utm_medium), param: 'ad_campaign', display_with: Proc.new{|referred| AdCampaignsHelper.format_number(referred) }, classes: 'ac_bip'
+ end
+ column "Signed Up" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.joined, campaign.referred)) if campaign.referred && campaign.referred > 0
+ end
+ column "Downloaded" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.downloaded, campaign.joined))
+ end
+ column "Ran Client" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.ran_client, campaign.joined))
+ end
+ column "FTUE" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.ftue, campaign.joined))
+ end
+ column "Any Session" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.any_session, campaign.joined))
+ end
+ column "2+ Session" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.real_session, campaign.joined))
+ end
+ column "Good Session" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.good_session, campaign.joined))
+ end
+ column "Invited" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.invited, campaign.joined))
+ end
+ column "Friended" do |campaign|
+ raw(AdCampaignsHelper.spacer(campaign.friended, campaign.joined))
+ end
+ column "Platinum" do |campaign|
+ campaign.platinum
+ end
+ column "Gold" do |campaign|
+ campaign.gold
+ end
+ column "Silver" do |campaign|
+ campaign.silver
+ end
+
+ end
+
+ controller do
+ def scoped_collection
+ User.select("users.origin_utm_campaign,
+ users.origin_utm_medium, COUNT(users.id) AS joined,
+ COUNT(users.first_downloaded_client_at) AS downloaded,
+ COUNT(users.first_subscribed_at) AS subscribed,
+ COUNT(users.first_ran_client_at) AS ran_client,
+ COUNT(users.first_certified_gear_at) AS ftue,
+ COUNT(users.first_music_session_at) AS any_session,
+ COUNT(users.first_real_music_session_at) AS real_session,
+ COUNT(users.first_good_music_session_at) AS good_session,
+ COUNT(users.first_invited_at) AS invited,
+ COUNT(users.first_friended_at) AS friended,
+ COUNT(CASE WHEN users.first_subscribed_plan_code = 'jamsubplatinum' OR users.first_subscribed_plan_code = 'jamsubplatinumyearly' THEN users.first_subscribed_plan_code END) AS platinum,
+ COUNT(CASE WHEN users.first_subscribed_plan_code = 'jamsubgold' OR users.first_subscribed_plan_code = 'jamsubgoldyearly' THEN users.first_subscribed_plan_code END) AS gold,
+ COUNT(CASE WHEN users.first_subscribed_plan_code = 'jamsubsilver' OR users.first_subscribed_plan_code = 'jamsubsilveryearly' THEN users.first_subscribed_plan_code END) AS silver,
+ COUNT(CASE WHEN users.subscription_plan_code = 'jamsubgold' THEN users.subscription_plan_code END) AS gold,
+ COUNT(CASE WHEN users.subscription_plan_code = 'jamsubsilver' THEN users.subscription_plan_code END) AS silver,
+ ad_campaigns.id,
+ COALESCE(MAX(ad_campaigns.referred), NULL) as referred,
+ COALESCE(MAX(ad_campaigns.end_date), NULL) AS end_date,
+ COALESCE(MAX(ad_campaigns.spend), 0) AS spend").joins("
+ LEFT JOIN ad_campaigns ON users.origin_utm_campaign = ad_campaigns.campaign
+ AND users.origin_utm_medium = ad_campaigns.medium").where("
+ users.origin_utm_campaign IS NOT NULL AND users.origin_utm_medium IS NOT NULL").group("
+ ad_campaigns.id, users.origin_utm_campaign, users.origin_utm_medium").order("
+ users.origin_utm_campaign DESC")
+ end
+
+
+ def permitted_params
+ params.permit :campaign, :medium, :_method, ad_campaign: [:spend, :referred, :end_date]
+ end
+ end
+
+ collection_action :inplace_update, method: :put do
+ campaign = permitted_params[:campaign]
+ medium = permitted_params[:medium]
+ @ad_campaign = JamRuby::AdCampaign.where(campaign: campaign, medium: medium).first_or_create
+ respond_to do |format|
+ if @ad_campaign.update_attributes(permitted_params[:ad_campaign])
+ format.json { head :ok }
+ else
+ format.json{ render :json => @ad_campaign.errors.full_messages, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+
+end
\ No newline at end of file
diff --git a/admin/app/admin/ad_campaigns_page.rb b/admin/app/admin/ad_campaigns_page.rb
new file mode 100644
index 000000000..42cd3e762
--- /dev/null
+++ b/admin/app/admin/ad_campaigns_page.rb
@@ -0,0 +1,43 @@
+# module AdCampaignsHelper
+# def campaign_brought_in_users(campaign, medium)
+# User.where(origin_utm_campaign: campaign, origin_utm_medium: medium)
+# end
+# end
+
+# ActiveAdmin.register JamRuby::AdCampaign do
+# permit_params :campaign, :medium, :spend
+# end
+
+# ActiveAdmin.register_page "Ad campaigns" do
+# menu parent: 'Reports'
+# content :title => "Paid Advertising Report" do
+# table_for User.select("users.origin_utm_campaign, users.origin_utm_medium, COALESCE(MAX(ad_campaigns.end_date), NULL) AS end_date, COALESCE(MAX(ad_campaigns.spend), NULL) AS spend").joins("LEFT JOIN ad_campaigns ON users.origin_utm_campaign = ad_campaigns.campaign AND users.origin_utm_medium = ad_campaigns.medium").group("ad_campaigns.id, users.origin_utm_campaign, users.origin_utm_medium") do
+# column "Campaign" do |campaign|
+# campaign.origin_utm_campaign
+# end
+# column "Medium" do |campaign|
+# campaign.origin_utm_medium
+# end
+# column "End Date" do |campaign|
+# campaign.end_date
+# end
+# column "Hard Date" do |campaign|
+# campaign.end_date + 45.days if campaign.end_date.present?
+# end
+# column "Subscribed" do |campaign|
+# end
+# column "Spend" do |campaign|
+# best_in_place campaign, :spend, as: :input, url: admin_ad_campaigns_update_path(campaign: campaign.origin_utm_campaign, medium: campaign.origin_utm_medium), param: 'ad_campaign'
+# end
+# end
+# end
+
+# page_action :update, method: :put do
+# campaign = params[:campaign]
+# medium = params[:medium]
+# ad_campaign = AdCampaign.where(campaign: campaign, medium: medium).first_or_initialize
+# ad_campaign.attributes = params["ad_campaign"]
+# ad_campaign.save!
+# respond_with_bip(ad_campaign)
+# end
+# end
\ No newline at end of file
diff --git a/admin/app/admin/generic_state.rb b/admin/app/admin/generic_state.rb
index 3190556fe..48df08165 100644
--- a/admin/app/admin/generic_state.rb
+++ b/admin/app/admin/generic_state.rb
@@ -3,7 +3,7 @@ ActiveAdmin.register JamRuby::GenericState, :as => 'GenericState' do
config.clear_action_items!
filter :env
- permit_params :top_message, :event_page_top_logo_url, :connection_policy
+ permit_params :top_message, :event_page_top_logo_url, :customer_ltv, :connection_policy
actions :all, :except => [:destroy]
diff --git a/admin/app/assets/javascripts/active_admin.js b/admin/app/assets/javascripts/active_admin.js
index a79135b71..380447e9a 100644
--- a/admin/app/assets/javascripts/active_admin.js
+++ b/admin/app/assets/javascripts/active_admin.js
@@ -13,8 +13,11 @@
// //= require autocomplete-rails
//= require base
//= require_tree .
-
+//= require best_in_place.jquery-ui
$(document).ready(function() {
- jQuery(".best_in_place").best_in_place()
+ jQuery(".best_in_place").best_in_place();
+ $.datepicker.setDefaults({
+ dateFormat: 'yy-mm-dd',
+ });
})
diff --git a/admin/app/assets/javascripts/ad_campaigns.js b/admin/app/assets/javascripts/ad_campaigns.js
new file mode 100644
index 000000000..2a0b1228a
--- /dev/null
+++ b/admin/app/assets/javascripts/ad_campaigns.js
@@ -0,0 +1,3 @@
+$(document).ready(function() {
+ jQuery(".ac_bip").bind("ajax:success", function(){ window.location.reload(); });
+})
diff --git a/admin/app/views/admin/ad_campaigns/_customer_ltv.html.erb b/admin/app/views/admin/ad_campaigns/_customer_ltv.html.erb
new file mode 100644
index 000000000..9a7dea4be
--- /dev/null
+++ b/admin/app/views/admin/ad_campaigns/_customer_ltv.html.erb
@@ -0,0 +1,4 @@
+<%= label_tag :customer_ltv, 'Customer LTV : $' %>
+<%= best_in_place GenericState.singleton, :customer_ltv, :as => :input, url: "/admin/generic_states/#{GenericState.singleton.id}", place_holder: "---", :ok_button => 'Save', :cancel_button => 'Cancel', classes: 'ac_bip' %>
+
+
\ No newline at end of file
diff --git a/ruby/db/migrate/20210416154316_create_ad_campaigns.rb b/ruby/db/migrate/20210416154316_create_ad_campaigns.rb
new file mode 100644
index 000000000..bb4cf7320
--- /dev/null
+++ b/ruby/db/migrate/20210416154316_create_ad_campaigns.rb
@@ -0,0 +1,24 @@
+ class CreateAdCampaigns < ActiveRecord::Migration
+
+ def self.up
+ execute( <<-SQL
+ CREATE TABLE public.ad_campaigns (
+ id character varying(64) DEFAULT public.uuid_generate_v4() NOT NULL,
+ campaign character varying(256),
+ medium character varying(128),
+ spend integer default 0,
+ end_date date,
+ cac NUMERIC (8,2),
+ referred integer,
+ created_at timestamp without time zone DEFAULT now() NOT NULL,
+ expired_at timestamp without time zone);
+ SQL
+ )
+ execute("CREATE INDEX index_ad_campaigns_campaign_medium ON public.ad_campaigns USING btree (campaign, medium);")
+ end
+
+ def self.down
+ execute("DROP INDEX index_ad_campaigns_campaign_medium")
+ execute("DROP TABLE public.ad_campaigns")
+ end
+ end
diff --git a/ruby/db/migrate/20210419161459_add_ltv_to_generic_state.rb b/ruby/db/migrate/20210419161459_add_ltv_to_generic_state.rb
new file mode 100644
index 000000000..904045ec1
--- /dev/null
+++ b/ruby/db/migrate/20210419161459_add_ltv_to_generic_state.rb
@@ -0,0 +1,8 @@
+ class AddLtvToGenericState < ActiveRecord::Migration
+ def self.up
+ execute "ALTER TABLE generic_state ADD COLUMN customer_ltv INTEGER"
+ end
+ def self.down
+ execute "ALTER TABLE generic_state DROP COLUMN customer_ltv"
+ end
+ end
diff --git a/ruby/db/migrate/20210421215451_add_index_users_campaign_medium.rb b/ruby/db/migrate/20210421215451_add_index_users_campaign_medium.rb
new file mode 100644
index 000000000..79a7e7c78
--- /dev/null
+++ b/ruby/db/migrate/20210421215451_add_index_users_campaign_medium.rb
@@ -0,0 +1,9 @@
+ class AddIndexUsersCampaignMedium < ActiveRecord::Migration
+ def self.up
+ execute("CREATE INDEX index_users_origin_utm_campaign_origin_utm_medium ON public.users USING btree (origin_utm_campaign, origin_utm_medium);")
+ end
+
+ def self.down
+ execute("DROP INDEX index_users_origin_utm_campaign_origin_utm_medium;")
+ end
+ end
diff --git a/ruby/db/migrate/20210421220209_add_first_subscribed_plan_code_to_users.rb b/ruby/db/migrate/20210421220209_add_first_subscribed_plan_code_to_users.rb
new file mode 100644
index 000000000..63177b2a6
--- /dev/null
+++ b/ruby/db/migrate/20210421220209_add_first_subscribed_plan_code_to_users.rb
@@ -0,0 +1,9 @@
+ class AddFirstSubscribedPlanCodeToUsers < ActiveRecord::Migration
+ def self.up
+ execute("ALTER TABLE users ADD COLUMN first_subscribed_plan_code VARCHAR(100);")
+ end
+
+ def self.down
+ execute("ALTER TABLE users DROP COLUMN first_subscribed_plan_code;")
+ end
+ end
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index 8433ab63c..52e9ea9ca 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -337,6 +337,7 @@ require "jam_ruby/models/mobile_recording"
require "jam_ruby/app/uploaders/mobile_recording_uploader"
require "jam_ruby/models/mobile_recording_upload"
require "jam_ruby/models/temp_token"
+require "jam_ruby/models/ad_campaign"
include Jampb
diff --git a/ruby/lib/jam_ruby/models/ad_campaign.rb b/ruby/lib/jam_ruby/models/ad_campaign.rb
new file mode 100644
index 000000000..fb81c1630
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/ad_campaign.rb
@@ -0,0 +1,8 @@
+module JamRuby
+ class AdCampaign < ActiveRecord::Base
+ self.primary_key = 'id'
+ self.table_name = 'ad_campaigns'
+
+ attr_accessible :campaign, :medium, :spend, :end_date, :referred
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/generic_state.rb b/ruby/lib/jam_ruby/models/generic_state.rb
index 7c02f5c4a..85a0a8372 100644
--- a/ruby/lib/jam_ruby/models/generic_state.rb
+++ b/ruby/lib/jam_ruby/models/generic_state.rb
@@ -3,7 +3,7 @@ module JamRuby
class GenericState < ActiveRecord::Base
- attr_accessible :top_message, :event_page_top_logo_url, :connection_policy, as: :admin
+ attr_accessible :top_message, :event_page_top_logo_url, :connection_policy, :customer_ltv, as: :admin
self.table_name = 'generic_state'
@@ -47,6 +47,10 @@ module JamRuby
GenericState.singleton.recurly_transactions_last_sync_at
end
+ def self.customer_ltv
+ GenericState.singleton.customer_ltv
+ end
+
def self.connection_policy
GenericState.connection_policy
end
diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb
index 36ada3676..41b18c465 100644
--- a/ruby/lib/jam_ruby/recurly_client.rb
+++ b/ruby/lib/jam_ruby/recurly_client.rb
@@ -345,6 +345,7 @@ module JamRuby
subscription = create_subscription(current_user, plan_code, account, current_user.subscription_trial_ended? ? nil : current_user.subscription_trial_ends_at)
current_user.recurly_subscription_id = subscription.uuid
current_user.first_subscribed_at = Time.now if current_user.first_subscribed_at.nil?
+ current_user.first_subscribed_plan_code = plan_code if current_user.first_subscribed_plan_code.nil?
if current_user.subscription_trial_ended?
current_user.subscription_plan_code = get_highest_plan(subscription)
current_user.subscription_plan_code_set_at = DateTime.now
@@ -464,6 +465,7 @@ module JamRuby
if subscription && user.recurly_subscription_id.nil?
puts "Repairing subscription ID on account"
user.update_attribute(:recurly_subscription_id, subscription.uuid)
+ user.update_attribute(:first_subscribed_plan_code, subscription.plan.plan_code)
user.recurly_subscription_id = subscription.uuid
user.first_subscribed_at = Time.now if user.first_subscribed_at.nil?
end