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
Show file tree
Hide file tree
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
Expand Up @@ -16,6 +16,7 @@ class Audience < ApplicationRecord
# relationships .............................................................
has_many :campaigns
has_many :properties
has_many :prices

# validations ...............................................................
# callbacks .................................................................
Expand Down
12 changes: 10 additions & 2 deletions app/models/campaign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
# creative_id :bigint
# legacy_id :uuid
# organization_id :bigint
# pricing_plan_id :bigint
# user_id :bigint
#
# Indexes
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions app/models/campaign_bundle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +29,7 @@ class CampaignBundle < ApplicationRecord

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

Expand Down
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))
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions app/models/daily_summary_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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 ...................................................
Expand Down
18 changes: 15 additions & 3 deletions app/models/impression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
Expand Up @@ -31,6 +31,7 @@ class Region < ApplicationRecord
# relationships .............................................................
has_many :campaigns
has_many :campaign_bundles
has_many :prices

# validations ...............................................................
# callbacks .................................................................
Expand Down
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
#
Expand Down Expand Up @@ -85,6 +82,7 @@ STATUS_COLORS:

CAMPAIGN_PRICING_STRATEGIES:
- campaign
- pricing_plan
- region_and_audience

CAMPAIGN_STATUSES:
Expand Down
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
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading