From 0f084498e47f2ef7bed5605d3b189a06275b8655 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 19 Jun 2020 08:00:37 -0600 Subject: [PATCH 1/7] Add smarts to calculate average RPM --- app/models/concerns/properties/reportable.rb | 46 +++++++++++++++++++- app/models/daily_summary_report.rb | 6 +++ app/models/impression.rb | 4 ++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/properties/reportable.rb b/app/models/concerns/properties/reportable.rb index 82766bc0a..d5d28f29a 100644 --- a/app/models/concerns/properties/reportable.rb +++ b/app/models/concerns/properties/reportable.rb @@ -1,5 +1,37 @@ module Properties module Reportable + extend ActiveSupport::Concern + + module ClassMethods + def average_rpm_by_audience_and_region(start = 91.days.ago, stop = 1.day.ago) + data = Property.includes(:audience).active.each_with_object({}) { |property, memo| + property.average_rpm_by_region(start, stop).each do |region, rpm| + key = "#{property.audience.name} - #{region&.name || "Unknown"}" + memo[key] ||= [] + memo[key] << rpm + end + } + data.each do |key, rpms| + list = rpms.select { |rpm| rpm > 0 } + data[key] = { + min: list.min, + max: list.max, + avg: list.size > 0 ? (list.sum / list.size.to_f) : nil + } + end + + # generate a csv report with this data + # CSV.open Rails.root.join("tmp/rpms.csv"), "wb" do |csv| + # csv << %w[category min max avg] + # data.each do |key, entry| + # csv << [key, entry[:min]&.format, entry[:max]&.format, entry[:avg]&.format] + # end + # end + + data + end + end + def summary(start = nil, stop = nil, paid: true) report = DailySummaryReport.scoped_by(self) .where(impressionable_type: "Campaign", impressionable_id: campaign_ids_relation(paid)) @@ -41,12 +73,24 @@ 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 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 + # Daily report ------------------------------------------------------------------------------------------- def daily_summaries_by_day(start = nil, stop = nil, paid: true) diff --git a/app/models/daily_summary_report.rb b/app/models/daily_summary_report.rb index d5daee88d..6938bef05 100644 --- a/app/models/daily_summary_report.rb +++ b/app/models/daily_summary_report.rb @@ -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 ................................................... diff --git a/app/models/impression.rb b/app/models/impression.rb index de51fc03d..8cfa84514 100644 --- a/app/models/impression.rb +++ b/app/models/impression.rb @@ -224,6 +224,10 @@ def calculate_estimated_revenue_and_save!(recalculate = false) save! if changed? end + def rpm + Money.new calculate_estimated_property_revenue_fractional_cents * 1000, "USD" + end + def obfuscate_ip_address self.ip_address = self.class.obfuscate_ip_address(ip_address) end From ed1afddcb0c9ccf28b827adb9b7c8223291ea1f3 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 19 Jun 2020 12:47:05 -0600 Subject: [PATCH 2/7] Introduce pricing plans to help manage quarterly based pricing --- app/models/audience.rb | 1 + app/models/campaign.rb | 12 +- app/models/campaign_bundle.rb | 11 +- app/models/impression.rb | 5 +- app/models/price.rb | 51 ++++++ app/models/pricing_plan.rb | 39 +++++ app/models/region.rb | 1 + config/enums.yml | 6 +- db/migrate/20200619161949_create_prices.rb | 22 +++ ...0619172708_campaign_add_pricing_plan_id.rb | 9 ++ db/schema.rb | 28 +++- db/seeds/prices.rb | 7 + db/structure.sql | 150 +++++++++++++++++- lib/tasks/custom_seed.rake | 11 ++ test/fixtures/campaign_bundles.yml | 10 +- test/fixtures/campaigns.yml | 2 + test/models/campaign_bundle_test.rb | 12 +- test/models/campaign_test.rb | 2 + test/models/price_test.rb | 28 ++++ test/models/pricing_plan_test.rb | 20 +++ 20 files changed, 403 insertions(+), 24 deletions(-) create mode 100644 app/models/price.rb create mode 100644 app/models/pricing_plan.rb create mode 100644 db/migrate/20200619161949_create_prices.rb create mode 100644 db/migrate/20200619172708_campaign_add_pricing_plan_id.rb create mode 100644 db/seeds/prices.rb create mode 100644 lib/tasks/custom_seed.rake create mode 100644 test/models/price_test.rb create mode 100644 test/models/pricing_plan_test.rb diff --git a/app/models/audience.rb b/app/models/audience.rb index 033b2a673..2e7babff1 100644 --- a/app/models/audience.rb +++ b/app/models/audience.rb @@ -16,6 +16,7 @@ class Audience < ApplicationRecord # relationships ............................................................. has_many :campaigns has_many :properties + has_many :prices # validations ............................................................... # callbacks ................................................................. diff --git a/app/models/campaign.rb b/app/models/campaign.rb index 3bee9e095..5d8f0d3dc 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -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 diff --git a/app/models/campaign_bundle.rb b/app/models/campaign_bundle.rb index b08b84ec4..5fc8ff59a 100644 --- a/app/models/campaign_bundle.rb +++ b/app/models/campaign_bundle.rb @@ -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 diff --git a/app/models/impression.rb b/app/models/impression.rb index 8cfa84514..1b2936670 100644 --- a/app/models/impression.rb +++ b/app/models/impression.rb @@ -195,9 +195,10 @@ 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 + price = campaign.pricing_plan.prices.call(audience: audience, region: region) + price.cpm * campaign.ecpm_multiplier end def calculate_estimated_gross_revenue_fractional_cents diff --git a/app/models/price.rb b/app/models/price.rb new file mode 100644 index 000000000..1f707e2b5 --- /dev/null +++ b/app/models/price.rb @@ -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 diff --git a/app/models/pricing_plan.rb b/app/models/pricing_plan.rb new file mode 100644 index 000000000..349283d69 --- /dev/null +++ b/app/models/pricing_plan.rb @@ -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 diff --git a/app/models/region.rb b/app/models/region.rb index a185ae45c..80aa469d0 100644 --- a/app/models/region.rb +++ b/app/models/region.rb @@ -31,6 +31,7 @@ class Region < ApplicationRecord # relationships ............................................................. has_many :campaigns has_many :campaign_bundles + has_many :prices # validations ............................................................... # callbacks ................................................................. diff --git a/config/enums.yml b/config/enums.yml index 0427005d0..552615c9a 100644 --- a/config/enums.yml +++ b/config/enums.yml @@ -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: diff --git a/db/migrate/20200619161949_create_prices.rb b/db/migrate/20200619161949_create_prices.rb new file mode 100644 index 000000000..781568512 --- /dev/null +++ b/db/migrate/20200619161949_create_prices.rb @@ -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 diff --git a/db/migrate/20200619172708_campaign_add_pricing_plan_id.rb b/db/migrate/20200619172708_campaign_add_pricing_plan_id.rb new file mode 100644 index 000000000..06c177877 --- /dev/null +++ b/db/migrate/20200619172708_campaign_add_pricing_plan_id.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 8dae9bf0a..fb542367f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/db/seeds/prices.rb b/db/seeds/prices.rb new file mode 100644 index 000000000..c420f36ce --- /dev/null +++ b/db/seeds/prices.rb @@ -0,0 +1,7 @@ +pricing_plan = PricingPlan.first_or_create!(name: "2020, 2nd Quarter") + +Audience.all.each do |audience| + Region.all.each do |region| + Price.create! pricing_plan: pricing_plan, audience: audience, region: region, cpm: audience.ecpm_for_region(region) + end +end diff --git a/db/structure.sql b/db/structure.sql index f691efea4..2739927a9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -274,7 +274,8 @@ CREATE TABLE public.campaign_bundles ( end_date date NOT NULL, region_ids bigint[] DEFAULT '{}'::bigint[], created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL + updated_at timestamp(6) without time zone NOT NULL, + pricing_plan_id bigint ); @@ -338,7 +339,8 @@ CREATE TABLE public.campaigns ( campaign_bundle_id bigint, audience_ids bigint[] DEFAULT '{}'::bigint[] NOT NULL, region_ids bigint[] DEFAULT '{}'::bigint[] NOT NULL, - ecpm_multiplier numeric DEFAULT 1.0 NOT NULL + ecpm_multiplier numeric DEFAULT 1.0 NOT NULL, + pricing_plan_id bigint ); @@ -1032,6 +1034,74 @@ CREATE TABLE public.pixels ( ); +-- +-- Name: prices; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.prices ( + id bigint NOT NULL, + pricing_plan_id bigint NOT NULL, + audience_id bigint NOT NULL, + region_id bigint NOT NULL, + cpm_cents integer DEFAULT 0 NOT NULL, + cpm_currency character varying DEFAULT 'USD'::character varying NOT NULL, + rpm_cents integer DEFAULT 0 NOT NULL, + rpm_currency character varying DEFAULT 'USD'::character varying NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: prices_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.prices_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: prices_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.prices_id_seq OWNED BY public.prices.id; + + +-- +-- Name: pricing_plans; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.pricing_plans ( + id bigint NOT NULL, + name character varying NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: pricing_plans_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.pricing_plans_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: pricing_plans_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.pricing_plans_id_seq OWNED BY public.pricing_plans.id; + + -- -- Name: properties; Type: TABLE; Schema: public; Owner: - -- @@ -1738,6 +1808,20 @@ ALTER TABLE ONLY public.organizations ALTER COLUMN id SET DEFAULT nextval('publi ALTER TABLE ONLY public.pixel_conversions ALTER COLUMN id SET DEFAULT nextval('public.pixel_conversions_id_seq'::regclass); +-- +-- Name: prices id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.prices ALTER COLUMN id SET DEFAULT nextval('public.prices_id_seq'::regclass); + + +-- +-- Name: pricing_plans id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pricing_plans ALTER COLUMN id SET DEFAULT nextval('public.pricing_plans_id_seq'::regclass); + + -- -- Name: properties id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1963,6 +2047,22 @@ ALTER TABLE ONLY public.pixels ADD CONSTRAINT pixels_pkey PRIMARY KEY (id); +-- +-- Name: prices prices_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.prices + ADD CONSTRAINT prices_pkey PRIMARY KEY (id); + + +-- +-- Name: pricing_plans pricing_plans_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pricing_plans + ADD CONSTRAINT pricing_plans_pkey PRIMARY KEY (id); + + -- -- Name: properties properties_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2321,6 +2421,13 @@ CREATE INDEX index_campaign_bundles_on_end_date ON public.campaign_bundles USING CREATE INDEX index_campaign_bundles_on_name ON public.campaign_bundles USING btree (lower((name)::text)); +-- +-- Name: index_campaign_bundles_on_pricing_plan_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_campaign_bundles_on_pricing_plan_id ON public.campaign_bundles USING btree (pricing_plan_id); + + -- -- Name: index_campaign_bundles_on_region_ids; Type: INDEX; Schema: public; Owner: - -- @@ -2433,6 +2540,13 @@ CREATE INDEX index_campaigns_on_organization_id ON public.campaigns USING btree CREATE INDEX index_campaigns_on_paid_fallback ON public.campaigns USING btree (paid_fallback); +-- +-- Name: index_campaigns_on_pricing_plan_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_campaigns_on_pricing_plan_id ON public.campaigns USING btree (pricing_plan_id); + + -- -- Name: index_campaigns_on_prohibited_property_ids; Type: INDEX; Schema: public; Owner: - -- @@ -2972,6 +3086,34 @@ CREATE INDEX index_pixels_on_organization_id ON public.pixels USING btree (organ CREATE INDEX index_pixels_on_user_id ON public.pixels USING btree (user_id); +-- +-- Name: index_prices_on_audience_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_prices_on_audience_id ON public.prices USING btree (audience_id); + + +-- +-- Name: index_prices_on_pricing_plan_id_and_audience_id_and_region_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_prices_on_pricing_plan_id_and_audience_id_and_region_id ON public.prices USING btree (pricing_plan_id, audience_id, region_id); + + +-- +-- Name: index_prices_on_region_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_prices_on_region_id ON public.prices USING btree (region_id); + + +-- +-- Name: index_pricing_plans_on_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_pricing_plans_on_name ON public.pricing_plans USING btree (name); + + -- -- Name: index_properties_on_assigned_fallback_campaign_ids; Type: INDEX; Schema: public; Owner: - -- @@ -3424,6 +3566,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200521230331'), ('20200527164824'), ('20200527175633'), -('20200528141603'); +('20200528141603'), +('20200619161949'), +('20200619172708'); diff --git a/lib/tasks/custom_seed.rake b/lib/tasks/custom_seed.rake new file mode 100644 index 000000000..86d1cb52b --- /dev/null +++ b/lib/tasks/custom_seed.rake @@ -0,0 +1,11 @@ +namespace :db do + namespace :seed do + Dir[Rails.root.join("db/seeds/*.rb")].each do |filename| + task_name = File.basename(filename, ".rb") + desc "Seed " + task_name + ", based on the file with the same name in `db/seeds/*.rb`" + task task_name.to_sym => :environment do + load(filename) if File.exist?(filename) + end + end + end +end diff --git a/test/fixtures/campaign_bundles.yml b/test/fixtures/campaign_bundles.yml index f9f373599..562d91772 100644 --- a/test/fixtures/campaign_bundles.yml +++ b/test/fixtures/campaign_bundles.yml @@ -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) # default: diff --git a/test/fixtures/campaigns.yml b/test/fixtures/campaigns.yml index 5f7ba6137..1501608a8 100644 --- a/test/fixtures/campaigns.yml +++ b/test/fixtures/campaigns.yml @@ -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 diff --git a/test/models/campaign_bundle_test.rb b/test/models/campaign_bundle_test.rb index 472e78657..06db7cdbf 100644 --- a/test/models/campaign_bundle_test.rb +++ b/test/models/campaign_bundle_test.rb @@ -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) # require "test_helper" @@ -29,6 +31,7 @@ class CampaignBundleTest < ActiveSupport::TestCase actual = bundle.to_stashable_attributes expected = { "id" => bundle.id, + "pricing_plan_id" => nil, "organization_id" => bundle.organization_id, "user_id" => bundle.user_id, "name" => bundle.name, @@ -40,6 +43,7 @@ class CampaignBundleTest < ActiveSupport::TestCase :campaigns_attributes => [{"campaign_bundle_id" => bundle.id, "id" => campaign.id, + "pricing_plan_id" => nil, "keywords" => campaign.keywords, "negative_keywords" => campaign.negative_keywords, "organization_id" => bundle.organization_id, diff --git a/test/models/campaign_test.rb b/test/models/campaign_test.rb index 2eb37e2d9..7c6c9943c 100644 --- a/test/models/campaign_test.rb +++ b/test/models/campaign_test.rb @@ -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 diff --git a/test/models/price_test.rb b/test/models/price_test.rb new file mode 100644 index 000000000..49b5580e2 --- /dev/null +++ b/test/models/price_test.rb @@ -0,0 +1,28 @@ +# == 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) +# +require "test_helper" + +class PriceTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/pricing_plan_test.rb b/test/models/pricing_plan_test.rb new file mode 100644 index 000000000..a8d49b809 --- /dev/null +++ b/test/models/pricing_plan_test.rb @@ -0,0 +1,20 @@ +# == 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 +# +require "test_helper" + +class PricingPlanTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From ae8fadfac7c5b17cab65fb87c38af794b3ca7168 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 19 Jun 2020 13:45:35 -0600 Subject: [PATCH 3/7] Ensure target rpm --- app/models/impression.rb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/models/impression.rb b/app/models/impression.rb index 1b2936670..083d21097 100644 --- a/app/models/impression.rb +++ b/app/models/impression.rb @@ -197,6 +197,7 @@ 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? + # pricing plan based pricing price = campaign.pricing_plan.prices.call(audience: audience, region: region) price.cpm * campaign.ecpm_multiplier end @@ -206,7 +207,17 @@ def calculate_estimated_gross_revenue_fractional_cents end def calculate_estimated_property_revenue_fractional_cents - calculate_estimated_gross_revenue_fractional_cents * property.revenue_percentage + revenue_percentage = property.revenue_percentage + value = calculate_estimated_gross_revenue_fractional_cents * revenue_percentage + if campaign.pricing_plan_strategy? && campaign.pricing_plan.rpm > 0 + rpm = Money.new(value * 1000, "USD") + while rpm > campaign.pricing_plan.rpm + revenue_percentage -= 0.1 + value = calculate_estimated_gross_revenue_fractional_cents * revenue_percentage + rpm = Money.new(value * 1000, "USD") + end + end + value end def calculate_estimated_house_revenue_fractional_cents @@ -225,10 +236,6 @@ def calculate_estimated_revenue_and_save!(recalculate = false) save! if changed? end - def rpm - Money.new calculate_estimated_property_revenue_fractional_cents * 1000, "USD" - end - def obfuscate_ip_address self.ip_address = self.class.obfuscate_ip_address(ip_address) end From c637d345cbdcf3e7bb08e7fc619a011b871fc556 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 19 Jun 2020 13:50:38 -0600 Subject: [PATCH 4/7] Fix typeo --- app/models/impression.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/impression.rb b/app/models/impression.rb index 083d21097..60ece9341 100644 --- a/app/models/impression.rb +++ b/app/models/impression.rb @@ -198,7 +198,7 @@ def applicable_ecpm return region.ecpm(audience) * campaign.ecpm_multiplier if campaign.region_and_audience_pricing_strategy? # pricing plan based pricing - price = campaign.pricing_plan.prices.call(audience: audience, region: region) + price = campaign.pricing_plan.prices.find_by(audience: audience, region: region) price.cpm * campaign.ecpm_multiplier end From 0abb581bf08c4a7117c6e242f4bf6446e1816e88 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 19 Jun 2020 15:58:46 -0600 Subject: [PATCH 5/7] Set default rpm to 70% of cpm --- app/models/concerns/properties/reportable.rb | 44 +++++++++++--------- app/models/impression.rb | 8 ++-- bin/copy_production_db_to_local | 2 +- db/seeds/prices.rb | 17 ++++++-- 4 files changed, 43 insertions(+), 28 deletions(-) diff --git a/app/models/concerns/properties/reportable.rb b/app/models/concerns/properties/reportable.rb index d5d28f29a..4b91631d7 100644 --- a/app/models/concerns/properties/reportable.rb +++ b/app/models/concerns/properties/reportable.rb @@ -3,32 +3,38 @@ module Reportable extend ActiveSupport::Concern module ClassMethods - def average_rpm_by_audience_and_region(start = 91.days.ago, stop = 1.day.ago) + def rpm_by_audience_and_region(start = 91.days.ago, stop = 1.day.ago) data = Property.includes(:audience).active.each_with_object({}) { |property, memo| property.average_rpm_by_region(start, stop).each do |region, rpm| - key = "#{property.audience.name} - #{region&.name || "Unknown"}" - memo[key] ||= [] - memo[key] << rpm + memo[[property.audience, region]] ||= [] + memo[[property.audience, region]] << rpm end } - data.each do |key, rpms| - list = rpms.select { |rpm| rpm > 0 } - data[key] = { - min: list.min, - max: list.max, - avg: list.size > 0 ? (list.sum / list.size.to_f) : nil + list = data.each_with_object([]) { |((audience, region), rpms), memo| + rpms = rpms.select { |rpm| rpm > 0 } + memo << { + audience: audience, + region: region, + min: rpms.min, + max: rpms.max, + avg: rpms.size > 0 ? (rpms.sum / rpms.size.to_f) : nil } - end + } # generate a csv report with this data - # CSV.open Rails.root.join("tmp/rpms.csv"), "wb" do |csv| - # csv << %w[category min max avg] - # data.each do |key, entry| - # csv << [key, entry[:min]&.format, entry[:max]&.format, entry[:avg]&.format] - # end - # end - - data + CSV.open Rails.root.join("tmp/rpms.csv"), "wb" do |csv| + csv << %w[category min max avg] + list.each do |entry| + csv << [ + "#{entry[:audience].name} - #{entry[:region]&.name}", + entry[:min]&.format, + entry[:max]&.format, + entry[:avg]&.format + ] + end + end + + list end end diff --git a/app/models/impression.rb b/app/models/impression.rb index 60ece9341..ff82c0f13 100644 --- a/app/models/impression.rb +++ b/app/models/impression.rb @@ -207,13 +207,13 @@ def calculate_estimated_gross_revenue_fractional_cents end def calculate_estimated_property_revenue_fractional_cents - revenue_percentage = property.revenue_percentage - value = calculate_estimated_gross_revenue_fractional_cents * 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 - revenue_percentage -= 0.1 - value = calculate_estimated_gross_revenue_fractional_cents * revenue_percentage + percentage -= 0.1 + value = calculate_estimated_gross_revenue_fractional_cents * percentage rpm = Money.new(value * 1000, "USD") end end diff --git a/bin/copy_production_db_to_local b/bin/copy_production_db_to_local index a50eb9819..078b9f18c 100755 --- a/bin/copy_production_db_to_local +++ b/bin/copy_production_db_to_local @@ -4,7 +4,7 @@ source .env export RAILS_ENV=development bundle exec rails db:drop db:create -#./bin/heroku_pg_dump_production_replica +./bin/heroku_pg_dump_production_replica pg_restore -U ${PG_USERNAME:-postgres} --verbose --clean --no-acl --no-owner -h ${PG_HOST:-localhost} -p ${PG_PORT:-5432} -d code_fund_ads_development tmp/production-shallow.dump bundle exec rails db:migrate ./bin/generate_db_artifacts diff --git a/db/seeds/prices.rb b/db/seeds/prices.rb index c420f36ce..f8b0516ec 100644 --- a/db/seeds/prices.rb +++ b/db/seeds/prices.rb @@ -1,7 +1,16 @@ -pricing_plan = PricingPlan.first_or_create!(name: "2020, 2nd Quarter") +ActiveRecord::Base.transaction do + pricing_plan = PricingPlan.first_or_create!(name: "2020, 2nd Quarter") + Price.where(pricing_plan: pricing_plan).delete_all -Audience.all.each do |audience| - Region.all.each do |region| - Price.create! pricing_plan: pricing_plan, audience: audience, region: region, cpm: audience.ecpm_for_region(region) + Audience.all.each do |audience| + Region.all.each do |region| + Price.create!( + pricing_plan: pricing_plan, + audience: audience, + region: region, + cpm: audience.ecpm_for_region(region), + rpm: audience.ecpm_for_region(region) * 0.7 + ) + end end end From b3867e39ddad3b44f9a9e7a34c66a58b4d7a294a Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Thu, 25 Jun 2020 12:44:42 -0600 Subject: [PATCH 6/7] Fix logic error with RPM calc --- app/models/concerns/properties/reportable.rb | 56 ++------------------ 1 file changed, 4 insertions(+), 52 deletions(-) diff --git a/app/models/concerns/properties/reportable.rb b/app/models/concerns/properties/reportable.rb index a696e6244..8cc7d2388 100644 --- a/app/models/concerns/properties/reportable.rb +++ b/app/models/concerns/properties/reportable.rb @@ -2,42 +2,6 @@ module Properties module Reportable extend ActiveSupport::Concern - module ClassMethods - def rpm_by_audience_and_region(start = 91.days.ago, stop = 1.day.ago) - data = Property.includes(:audience).active.each_with_object({}) { |property, memo| - property.average_rpm_by_region(start, stop).each do |region, rpm| - memo[[property.audience, region]] ||= [] - memo[[property.audience, region]] << rpm - end - } - list = data.each_with_object([]) { |((audience, region), rpms), memo| - rpms = rpms.select { |rpm| rpm > 0 } - memo << { - audience: audience, - region: region, - min: rpms.min, - max: rpms.max, - avg: rpms.size > 0 ? (rpms.sum / rpms.size.to_f) : nil - } - } - - # generate a csv report with this data - CSV.open Rails.root.join("tmp/rpms.csv"), "wb" do |csv| - csv << %w[category min max avg] - list.each do |entry| - csv << [ - "#{entry[:audience].name} - #{entry[:region]&.name}", - entry[:min]&.format, - entry[:max]&.format, - entry[:avg]&.format - ] - end - end - - list - end - end - def summary(start = nil, stop = nil, paid: true) report = DailySummaryReport.scoped_by(self) .where(impressionable_type: "Campaign", impressionable_id: campaign_ids_relation(paid)) @@ -61,14 +25,12 @@ 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 an ActiveRecord relation for DailySummaryReports scoped to country @@ -91,16 +53,6 @@ def region_summaries(start = nil, stop = nil) end 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 - # Daily report ------------------------------------------------------------------------------------------- def daily_summaries_by_day(start = nil, stop = nil, paid: true) From da383eea222dcb5f9fbff7e6af41bfbd647b57ec Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Mon, 29 Jun 2020 11:42:04 -0600 Subject: [PATCH 7/7] More work around new pricing --- app/models/concerns/properties/reportable.rb | 10 ++++++ db/structure.sql | 33 ++++++++++---------- lib/tasks/prices.rake | 28 +++++++++++++++++ lib/tasks/pricing_plans.rake | 7 +++++ 4 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 lib/tasks/prices.rake create mode 100644 lib/tasks/pricing_plans.rake diff --git a/app/models/concerns/properties/reportable.rb b/app/models/concerns/properties/reportable.rb index 8cc7d2388..3080e8a3a 100644 --- a/app/models/concerns/properties/reportable.rb +++ b/app/models/concerns/properties/reportable.rb @@ -33,6 +33,16 @@ def average_rpm(start = nil, stop = nil) 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 def country_summaries(start = nil, stop = nil) DailySummaryReport diff --git a/db/structure.sql b/db/structure.sql index 2739927a9..8bdcc7052 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9,6 +9,13 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; +-- +-- Name: hdb_views; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA hdb_views; + + -- -- Name: pg_stat_statements; Type: EXTENSION; Schema: -; Owner: - -- @@ -210,8 +217,8 @@ ALTER SEQUENCE public.active_storage_blobs_id_seq OWNED BY public.active_storage CREATE TABLE public.ar_internal_metadata ( key character varying NOT NULL, value character varying, - created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL ); @@ -522,9 +529,9 @@ CREATE TABLE public.daily_summaries ( scoped_by_id character varying, impressions_count integer DEFAULT 0 NOT NULL, fallbacks_count integer DEFAULT 0 NOT NULL, - fallback_percentage numeric DEFAULT 0.0 NOT NULL, + fallback_percentage numeric DEFAULT 0 NOT NULL, clicks_count integer DEFAULT 0 NOT NULL, - click_rate numeric DEFAULT 0.0 NOT NULL, + click_rate numeric DEFAULT 0 NOT NULL, ecpm_cents integer DEFAULT 0 NOT NULL, ecpm_currency character varying DEFAULT 'USD'::character varying NOT NULL, cost_per_click_cents integer DEFAULT 0 NOT NULL, @@ -709,8 +716,8 @@ CREATE TABLE public.impressions ( ad_template character varying, ad_theme character varying, organization_id bigint, - province_code character varying, - uplift boolean DEFAULT false + uplift boolean DEFAULT false, + province_code character varying ) PARTITION BY RANGE (advertiser_id, displayed_at_date); @@ -743,8 +750,8 @@ CREATE TABLE public.impressions_default ( ad_template character varying, ad_theme character varying, organization_id bigint, - province_code character varying, - uplift boolean DEFAULT false + uplift boolean DEFAULT false, + province_code character varying ); ALTER TABLE ONLY public.impressions ATTACH PARTITION public.impressions_default DEFAULT; @@ -1261,8 +1268,7 @@ ALTER SEQUENCE public.property_traffic_estimates_id_seq OWNED BY public.property CREATE TABLE public.publisher_invoices ( id bigint NOT NULL, user_id bigint NOT NULL, - amount_cents integer DEFAULT 0 NOT NULL, - amount_currency character varying DEFAULT 'USD'::character varying NOT NULL, + amount money NOT NULL, currency character varying NOT NULL, start_date date NOT NULL, end_date date NOT NULL, @@ -3205,13 +3211,6 @@ CREATE INDEX index_property_traffic_estimates_on_property_id ON public.property_ CREATE INDEX index_publisher_invoices_on_end_date ON public.publisher_invoices USING btree (end_date); --- --- Name: index_publisher_invoices_on_paid_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_publisher_invoices_on_paid_at ON public.publisher_invoices USING btree (paid_at); - - -- -- Name: index_publisher_invoices_on_start_date; Type: INDEX; Schema: public; Owner: - -- diff --git a/lib/tasks/prices.rake b/lib/tasks/prices.rake new file mode 100644 index 000000000..3690c2aea --- /dev/null +++ b/lib/tasks/prices.rake @@ -0,0 +1,28 @@ +namespace :prices do + task :create_csv, [:pricing_plan_id] => :environment do |t, args| + plan = PricingPlan.find(args[:pricing_plan_id]) + + CSV.open Rails.root.join("tmp/pricing_plan_#{plan.id}.csv"), "wb" do |csv| + csv << %w[audience region cpm rpm avg_rpm_last_30] + + Audience.all.each do |audience| + Region.all.each do |region| + price = plan.prices.find_by(audience: audience, region: region) + rpms = Property.active.where(audience: audience).map { |property| + property.average_rpm_by_region(30.days.ago, 1.day.ago)[region] + } + rpms = rpms.compact.select { |rpm| rpm > 0 } + avg_rpm_last_30 = rpms.size > 0 ? (rpms.sum / rpms.size) : Money.new(0, "USD") + + csv << [ + audience.name, + region.name, + price.cpm.format, + price.rpm.format, + avg_rpm_last_30.format + ] + end + end + end + end +end diff --git a/lib/tasks/pricing_plans.rake b/lib/tasks/pricing_plans.rake new file mode 100644 index 000000000..d875c82f7 --- /dev/null +++ b/lib/tasks/pricing_plans.rake @@ -0,0 +1,7 @@ +namespace :pricing_plans do + task list: :environment do + PricingPlan.order(created_at: :desc).each do |plan| + puts "#{plan.id.to_s.rjust 4}: #{plan.name}" + end + end +end