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