Skip to content
This repository was archived by the owner on Aug 13, 2020. It is now read-only.

Pricing Plans #1333

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/models/audience.rb
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ class Audience < ApplicationRecord
# relationships .............................................................
has_many :campaigns
has_many :properties
has_many :prices

# validations ...............................................................
# callbacks .................................................................
12 changes: 10 additions & 2 deletions app/models/campaign.rb
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@
# creative_id :bigint
# legacy_id :uuid
# organization_id :bigint
# pricing_plan_id :bigint
# user_id :bigint
#
# Indexes
@@ -56,6 +57,7 @@
# index_campaigns_on_negative_keywords (negative_keywords) USING gin
# index_campaigns_on_organization_id (organization_id)
# index_campaigns_on_paid_fallback (paid_fallback)
# index_campaigns_on_pricing_plan_id (pricing_plan_id)
# index_campaigns_on_prohibited_property_ids (prohibited_property_ids) USING gin
# index_campaigns_on_province_codes (province_codes) USING gin
# index_campaigns_on_region_ids (region_ids) USING gin
@@ -84,10 +86,11 @@ class Campaign < ApplicationRecord
include Taggable

# relationships .............................................................
belongs_to :campaign_bundle, optional: true
belongs_to :audience, optional: true
belongs_to :region, optional: true
belongs_to :campaign_bundle, optional: true
belongs_to :creative, -> { includes :creative_images }, optional: true
belongs_to :pricing_plan, optional: true
belongs_to :region, optional: true
belongs_to :user
has_many :pixel_conversions

@@ -293,7 +296,12 @@ def campaign_pricing_strategy?
pricing_strategy == ENUMS::CAMPAIGN_PRICING_STRATEGIES::CAMPAIGN
end

def pricing_plan_strategy?
pricing_strategy == ENUMS::CAMPAIGN_PRICING_STRATEGIES::PRICING_PLAN
end

def pricing_strategy
return ENUMS::CAMPAIGN_PRICING_STRATEGIES::PRICING_PLAN if pricing_plan
return ENUMS::CAMPAIGN_PRICING_STRATEGIES::REGION_AND_AUDIENCE if campaign_bundle
return ENUMS::CAMPAIGN_PRICING_STRATEGIES::REGION_AND_AUDIENCE if start_date >= Date.parse("2020-06-01")
ENUMS::CAMPAIGN_PRICING_STRATEGIES::CAMPAIGN
11 changes: 7 additions & 4 deletions app/models/campaign_bundle.rb
Original file line number Diff line number Diff line change
@@ -10,14 +10,16 @@
# created_at :datetime not null
# updated_at :datetime not null
# organization_id :bigint not null
# pricing_plan_id :bigint
# user_id :bigint not null
#
# Indexes
#
# index_campaign_bundles_on_end_date (end_date)
# index_campaign_bundles_on_name (lower((name)::text))
# index_campaign_bundles_on_region_ids (region_ids) USING gin
# index_campaign_bundles_on_start_date (start_date)
# index_campaign_bundles_on_end_date (end_date)
# index_campaign_bundles_on_name (lower((name)::text))
# index_campaign_bundles_on_pricing_plan_id (pricing_plan_id)
# index_campaign_bundles_on_region_ids (region_ids) USING gin
# index_campaign_bundles_on_start_date (start_date)
#

class CampaignBundle < ApplicationRecord
@@ -27,6 +29,7 @@ class CampaignBundle < ApplicationRecord

# relationships .............................................................
belongs_to :organization
belongs_to :pricing_plan, optional: true
belongs_to :user
has_many :campaigns

26 changes: 19 additions & 7 deletions app/models/concerns/properties/reportable.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Properties
module Reportable
extend ActiveSupport::Concern

def summary(start = nil, stop = nil, paid: true)
report = DailySummaryReport.scoped_by(self)
.where(impressionable_type: "Campaign", impressionable_id: campaign_ids_relation(paid))
@@ -23,14 +25,22 @@ def earnings(start = nil, stop = nil)
# Returns the average RPM (revenue per mille)
def average_rpm(start = nil, stop = nil)
s = summary(start, stop)
if s.impressions_count.to_i > 0
Money.new s.property_revenue.to_i / (s.impressions_count.to_i / 1000.to_f)
else
Money.new 0
end
return Money.new(0, "USD") unless s.impressions_count > 0
return Money.new(0, "USD") unless s.property_revenue.is_a?(Money)
s.property_revenue / (s.impressions_count / 1000.to_f)
rescue => e
Rollbar.error e
Money.new 0
Money.new 0, "USD"
end

# Returns a Hash keyed as: Region => Money
# where the value is the average RPM for the region
def average_rpm_by_region(start = nil, stop = nil)
region_summaries(start, stop).each_with_object({}) do |(region, summaries), memo|
mille = summaries.sum(&:paid_impressions_count) / 1000.to_f
property_revenue = summaries.sum(&:property_revenue)
memo[region] = mille > 0 ? property_revenue / mille : Money.new(0)
end
end

# Returns an ActiveRecord relation for DailySummaryReports scoped to country
@@ -45,7 +55,9 @@ def country_summaries(start = nil, stop = nil)
# where the list is comprised of DailySummaryReports scoped to country
def region_summaries(start = nil, stop = nil)
country_summaries(start, stop).each_with_object({}) do |summary, memo|
region = Region.with_all_country_codes(summary.scoped_by_id).first
region = Rails.local_ephemeral_cache.fetch("region_for_country/#{summary.scoped_by_id}") {
Region.with_all_country_codes(summary.scoped_by_id).first
}
memo[region] ||= []
memo[region] << summary
end
6 changes: 6 additions & 0 deletions app/models/daily_summary_report.rb
Original file line number Diff line number Diff line change
@@ -58,6 +58,8 @@ class DailySummaryReport < ApplicationRecord
.select(arel_table[:unique_ip_addresses_count].sum.as("unique_ip_addresses_count"))
.select(arel_table[:impressions_count].sum.as("impressions_count"))
.select(arel_table[:clicks_count].sum.as("clicks_count"))
.select(arel_table[:fallbacks_count].sum.as("fallbacks_count"))
.select(arel_table[:fallback_clicks_count].sum.as("fallback_clicks_count"))
.select(arel_table[:gross_revenue_cents].sum.as("gross_revenue_cents"))
.select(arel_table[:property_revenue_cents].sum.as("property_revenue_cents"))
.select(arel_table[:house_revenue_cents].sum.as("house_revenue_cents"))
@@ -100,6 +102,10 @@ class DailySummaryReport < ApplicationRecord
monetize :property_revenue_cents, numericality: {greater_than_or_equal_to: 0}
monetize :house_revenue_cents, numericality: {greater_than_or_equal_to: 0}

def paid_impressions_count
impressions_count - fallbacks_count
end

# class methods .............................................................

# public instance methods ...................................................
18 changes: 15 additions & 3 deletions app/models/impression.rb
Original file line number Diff line number Diff line change
@@ -195,17 +195,29 @@ def audience

def applicable_ecpm
return campaign.adjusted_ecpm(country_code) if campaign.campaign_pricing_strategy?
return region.ecpm(audience) * campaign.ecpm_multiplier if campaign.region_and_audience_pricing_strategy?

# region/audience based ecpm i.e. our new sales strategy
region.ecpm(audience) * campaign.ecpm_multiplier
# pricing plan based pricing
price = campaign.pricing_plan.prices.find_by(audience: audience, region: region)
price.cpm * campaign.ecpm_multiplier
end

def calculate_estimated_gross_revenue_fractional_cents
applicable_ecpm.cents / 1_000.to_f
end

def calculate_estimated_property_revenue_fractional_cents
calculate_estimated_gross_revenue_fractional_cents * property.revenue_percentage
percentage = property.revenue_percentage
value = calculate_estimated_gross_revenue_fractional_cents * percentage
if campaign.pricing_plan_strategy? && campaign.pricing_plan.rpm > 0
rpm = Money.new(value * 1000, "USD")
while rpm > campaign.pricing_plan.rpm
percentage -= 0.1
value = calculate_estimated_gross_revenue_fractional_cents * percentage
rpm = Money.new(value * 1000, "USD")
end
end
value
end

def calculate_estimated_house_revenue_fractional_cents
51 changes: 51 additions & 0 deletions app/models/price.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# == Schema Information
#
# Table name: prices
#
# id :bigint not null, primary key
# cpm_cents :integer default(0), not null
# cpm_currency :string default("USD"), not null
# rpm_cents :integer default(0), not null
# rpm_currency :string default("USD"), not null
# created_at :datetime not null
# updated_at :datetime not null
# audience_id :bigint not null
# pricing_plan_id :bigint not null
# region_id :bigint not null
#
# Indexes
#
# index_prices_on_audience_id (audience_id)
# index_prices_on_pricing_plan_id_and_audience_id_and_region_id (pricing_plan_id,audience_id,region_id) UNIQUE
# index_prices_on_region_id (region_id)
#
class Price < ApplicationRecord
# extends ...................................................................
# includes ..................................................................

# relationships .............................................................
belongs_to :pricing_plan
belongs_to :audience
belongs_to :region
has_many :campaign_bundles
has_many :campaigns

# validations ...............................................................
validates :pricing_plan_id, uniqueness: {scope: [:audience_id, :region_id]}
monetize :cpm_cents, numericality: {greater_than_or_equal_to: 0}
monetize :rpm_cents, numericality: {greater_than_or_equal_to: 0}

# callbacks .................................................................
# scopes ....................................................................
# additional config (i.e. accepts_nested_attribute_for etc...) ..............

# class methods .............................................................
class << self
end

# public instance methods ...................................................

# protected instance methods ................................................

# private instance methods ..................................................
end
39 changes: 39 additions & 0 deletions app/models/pricing_plan.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# == Schema Information
#
# Table name: pricing_plans
#
# id :bigint not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_pricing_plans_on_name (name) UNIQUE
#
class PricingPlan < ApplicationRecord
# extends ...................................................................
# includes ..................................................................

# relationships .............................................................
has_many :campaign_bundles
has_many :campaigns
has_many :prices

# validations ...............................................................
validates :name, uniqueness: true

# callbacks .................................................................
# scopes ....................................................................
# additional config (i.e. accepts_nested_attribute_for etc...) ..............

# class methods .............................................................
class << self
end

# public instance methods ...................................................

# protected instance methods ................................................

# private instance methods ..................................................
end
1 change: 1 addition & 0 deletions app/models/region.rb
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ class Region < ApplicationRecord
# relationships .............................................................
has_many :campaigns
has_many :campaign_bundles
has_many :prices

# validations ...............................................................
# callbacks .................................................................
6 changes: 2 additions & 4 deletions config/enums.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# IMPORTANT: All application enums should be defined in this file
#
# They will be made available to the application via: config/initializers/enums.rb
# ...and will be exposed uner the ENUMS module
# IMPORTANT: All application enums should be defined in this file They will be made available to the application via: config/initializers/enums.rb ...and will be exposed uner the ENUMS module
#
# NOTE: Entries in this file can be either key/value or a simple list
#
@@ -85,6 +82,7 @@ STATUS_COLORS:

CAMPAIGN_PRICING_STRATEGIES:
- campaign
- pricing_plan
- region_and_audience

CAMPAIGN_STATUSES:
22 changes: 22 additions & 0 deletions db/migrate/20200619161949_create_prices.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class CreatePrices < ActiveRecord::Migration[6.0]
def change
create_table :pricing_plans do |t|
t.string :name, null: false
t.timestamps
t.index :name, unique: true
end

create_table :prices do |t|
t.bigint :pricing_plan_id, null: false
t.bigint :audience_id, null: false
t.bigint :region_id, null: false
t.monetize :cpm, null: false, default: Money.new(0, "USD")
t.monetize :rpm, null: false, default: Money.new(0, "USD")
t.timestamps

t.index :audience_id
t.index :region_id
t.index [:pricing_plan_id, :audience_id, :region_id], unique: true
end
end
end
9 changes: 9 additions & 0 deletions db/migrate/20200619172708_campaign_add_pricing_plan_id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class CampaignAddPricingPlanId < ActiveRecord::Migration[6.0]
def change
add_column :campaign_bundles, :pricing_plan_id, :bigint
add_column :campaigns, :pricing_plan_id, :bigint

add_index :campaign_bundles, :pricing_plan_id
add_index :campaigns, :pricing_plan_id
end
end
28 changes: 27 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2020_05_28_141603) do
ActiveRecord::Schema.define(version: 2020_06_19_172708) do

# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -72,8 +72,10 @@
t.bigint "region_ids", default: [], array: true
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "pricing_plan_id"
t.index "lower((name)::text)", name: "index_campaign_bundles_on_name"
t.index ["end_date"], name: "index_campaign_bundles_on_end_date"
t.index ["pricing_plan_id"], name: "index_campaign_bundles_on_pricing_plan_id"
t.index ["region_ids"], name: "index_campaign_bundles_on_region_ids", using: :gin
t.index ["start_date"], name: "index_campaign_bundles_on_start_date"
end
@@ -115,6 +117,7 @@
t.bigint "audience_ids", default: [], null: false, array: true
t.bigint "region_ids", default: [], null: false, array: true
t.decimal "ecpm_multiplier", default: "1.0", null: false
t.bigint "pricing_plan_id"
t.index "lower((name)::text)", name: "index_campaigns_on_name"
t.index ["assigned_property_ids"], name: "index_campaigns_on_assigned_property_ids", using: :gin
t.index ["audience_ids"], name: "index_campaigns_on_audience_ids", using: :gin
@@ -129,6 +132,7 @@
t.index ["negative_keywords"], name: "index_campaigns_on_negative_keywords", using: :gin
t.index ["organization_id"], name: "index_campaigns_on_organization_id"
t.index ["paid_fallback"], name: "index_campaigns_on_paid_fallback"
t.index ["pricing_plan_id"], name: "index_campaigns_on_pricing_plan_id"
t.index ["prohibited_property_ids"], name: "index_campaigns_on_prohibited_property_ids", using: :gin
t.index ["province_codes"], name: "index_campaigns_on_province_codes", using: :gin
t.index ["region_ids"], name: "index_campaigns_on_region_ids", using: :gin
@@ -538,6 +542,28 @@
t.index ["user_id"], name: "index_pixels_on_user_id"
end

create_table "prices", force: :cascade do |t|
t.bigint "pricing_plan_id", null: false
t.bigint "audience_id", null: false
t.bigint "region_id", null: false
t.integer "cpm_cents", default: 0, null: false
t.string "cpm_currency", default: "USD", null: false
t.integer "rpm_cents", default: 0, null: false
t.string "rpm_currency", default: "USD", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["audience_id"], name: "index_prices_on_audience_id"
t.index ["pricing_plan_id", "audience_id", "region_id"], name: "index_prices_on_pricing_plan_id_and_audience_id_and_region_id", unique: true
t.index ["region_id"], name: "index_prices_on_region_id"
end

create_table "pricing_plans", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["name"], name: "index_pricing_plans_on_name", unique: true
end

create_table "properties", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "property_type", default: "website", null: false
Loading
Oops, something went wrong.