Skip to content

Commit

Permalink
Add support for YEARLY subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
forsbergplustwo committed Sep 20, 2023
1 parent 9738221 commit 2f3ac7e
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 60 deletions.
1 change: 1 addition & 0 deletions app/models/graphql/transactions_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
createdAt,
# Apps
... on AppSubscriptionSale {
billingInterval,
netAmount {
amount
},
Expand Down
2 changes: 1 addition & 1 deletion app/models/import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def source_adaptor

def import_payments_after_date
max_allowed_ago = source_adaptor.const_get(:MAX_HISTORY).ago
if user.newest_metric_date.to_i < max_allowed_ago.to_i
if (user.newest_metric_date&.to_time).to_i < max_allowed_ago.to_i
max_allowed_ago
else
user.newest_metric_date
Expand Down
7 changes: 7 additions & 0 deletions app/models/import/adaptor/csv_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class Import::Adaptor::CsvFile
encoding: "UTF-8"
}.freeze

CSV_YEARLY_IDENTIFIER = "yearly".freeze

CSV_REVENUE_TYPES = {
"recurring_revenue" => [
"RecurringApplicationFee",
Expand Down Expand Up @@ -91,6 +93,7 @@ def parse(csv_row)
charge_type: charge_type(csv_row),
payment_date: payment_date(csv_row),
revenue: revenue(csv_row),
is_yearly_revenue: is_yearly_revenue(csv_row),
app_title: app_title(csv_row),
shop: shop(csv_row),
shop_country: shop_country(csv_row)
Expand All @@ -109,6 +112,10 @@ def revenue(csv_row)
csv_row[:partner_share]&.to_f || 0.0
end

def is_yearly_revenue(csv_row)
csv_row[:charge_type].to_s.include?(CSV_YEARLY_IDENTIFIER)
end

def app_title(csv_row)
csv_row[:app_title].presence || Payment::UNKNOWN_APP_TITLE
end
Expand Down
10 changes: 10 additions & 0 deletions app/models/import/adaptor/shopify_payments_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def parse(node)
charge_type: charge_type(node),
payment_date: payment_date(node),
revenue: revenue(node),
is_yearly_revenue: is_yearly_revenue(node),
app_title: app_title(node),
shop: shop(node),
# ShopifyPartnerApi does not return shop country
Expand All @@ -114,6 +115,15 @@ def revenue(node)
end || 0.0
end

def is_yearly_revenue(node)
case node.__typename
when "AppSubscriptionSale"
node.billing_interval&.to_s == "ANNUAL"
else
false
end
end

def app_title(node)
case node.__typename
when "ReferralAdjustment", "ReferralTransaction", "ServiceSale", "ServiceSaleAdjustment"
Expand Down
40 changes: 28 additions & 12 deletions app/models/import/metrics_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,41 @@ def calculate_new_metrics
@import_from.upto(@import_to) do |date|
metrics = []
Metric::CHARGE_TYPES.each do |charge_type|
app_titles = app_titles_for(date: date, charge_type: charge_type)
next if app_titles.empty?
is_yearly_revenue_intervals_for(charge_type).each do |is_yearly_revenue|
app_titles = app_titles_for(date: date, charge_type: charge_type, is_yearly_revenue: is_yearly_revenue)
next if app_titles.empty?

app_titles.each do |app_title|
calculator = Metric::Calculator.new(
user: @user,
date: date,
charge_type: charge_type,
app_title: app_title
)
metrics << new_metric_from(calculator: calculator) if calculator.has_metrics?
app_titles.each do |app_title|
calculator = Metric::Calculator.new(
user: @user,
date: date,
charge_type: charge_type,
app_title: app_title,
is_yearly_revenue: is_yearly_revenue
)
metrics << new_metric_from(calculator: calculator) if calculator.has_metrics?
end
end
end
Metric.import!(metrics, validate: false, no_returning: true) if metrics.present?
@import.touch
end
end

def app_titles_for(date:, charge_type:)
@user.payments.where(payment_date: date, charge_type: charge_type).pluck(:app_title).uniq
def is_yearly_revenue_intervals_for(charge_type)
if Metric::CHARGE_TYPE_CAN_HAVE_YEARLY_INTERVAL[charge_type]
[true, false]
else
[false]
end
end

def app_titles_for(date:, charge_type:, is_yearly_revenue:)
@user.payments.where(
payment_date: date,
charge_type: charge_type,
is_yearly_revenue: is_yearly_revenue
).pluck(:app_title).uniq
end

def new_metric_from(calculator:)
Expand All @@ -50,6 +65,7 @@ def new_metric_from(calculator:)
charge_type: calculator.charge_type,
app_title: calculator.app_title,
revenue: calculator.revenue,
is_yearly_revenue: calculator.is_yearly_revenue,
number_of_charges: calculator.number_of_charges,
number_of_shops: calculator.number_of_shops,
average_revenue_per_shop: calculator.average_revenue_per_shop,
Expand Down
1 change: 1 addition & 0 deletions app/models/import/payments_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def new_payment(payment)
payment_date: payment[:payment_date],
charge_type: payment[:charge_type],
revenue: payment[:revenue],
is_yearly_revenue: payment[:is_yearly_revenue],
app_title: payment[:app_title],
shop: payment[:shop],
shop_country: payment[:shop_country]
Expand Down
13 changes: 13 additions & 0 deletions app/models/metric.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ class Metric < ApplicationRecord

CHARGE_TYPES = ["recurring_revenue", "onetime_revenue", "affiliate_revenue", "refund"].freeze

# Used to calculate metrics in groups of not applicable, yearly & monthly
# nil = not applicable, true = yearly, false = monthly
CHARGE_TYPE_CAN_HAVE_YEARLY_INTERVAL = {
"recurring_revenue" => true,
"onetime_revenue" => false,
"affiliate_revenue" => false,
"refund" => false
}.freeze

DISPLAYABLE_TYPES = [
:recurring_revenue,
:onetime_revenue,
Expand All @@ -22,6 +31,10 @@ def by_optional_charge_type(charge_type)
charge_type.blank? ? all : where(charge_type: charge_type)
end

def by_optional_is_yearly_revenue(is_yearly_revenue)
is_yearly_revenue.nil? ? all : where(is_yearly_revenue: is_yearly_revenue)
end

def by_date_and_period(date:, period:)
previous_period = date - period.days + 1
where(metric_date: previous_period.beginning_of_day..date.end_of_day)
Expand Down
42 changes: 33 additions & 9 deletions app/models/metric/calculator.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
class Metric::Calculator
MONTHLY_RECURRING_BILLING_FREQUENCY = 30.days
MONTHLY_RECURRING_BILLING_CHURN_WINDOW = 15.days
MONTHLY_BILLING_FREQUENCY = 30.days
MONTHLY_BILLING_CHURN_WINDOW = 15.days

def initialize(user:, date:, charge_type:, app_title:)
YEARLY_BILLING_FREQUENCY = 1.year
YEARLY_BILLING_CHURN_WINDOW = 30.days

def initialize(user:, date:, charge_type:, app_title:, is_yearly_revenue:)
@user = user
@date = date
@charge_type = charge_type
@app_title = app_title
@payments = user.payments.where(payment_date: date, charge_type: charge_type, app_title: app_title)
@is_yearly_revenue = is_yearly_revenue
@payments = payments_by_options_and_date(date)
end

attr_reader :user, :date, :charge_type, :app_title, :payments
attr_reader :user, :date, :charge_type, :app_title, :is_yearly_revenue, :payments

def has_metrics?
payments.any?
Expand Down Expand Up @@ -82,18 +86,29 @@ def unique_shops
end

def current_shops
churn_calulation_date_lower_bound = date - (MONTHLY_RECURRING_BILLING_CHURN_WINDOW * 2)
@current_shops ||= user.payments.where(payment_date: churn_calulation_date_lower_bound..date, charge_type: charge_type, app_title: app_title).group_by(&:shop)
@current_shops ||= payments_by_options_and_date(churn_calculations_date_lower_bound..date).group_by(&:shop)
end

def previous_shops
@previous_shops ||= user.payments.where(payment_date: churn_calculation_date, charge_type: charge_type, app_title: app_title).group_by(&:shop)
@previous_shops ||= payments_by_options_and_date(churn_calculation_date).group_by(&:shop)
end

def churn_calculation_date
# To calculate churn, we need to look at the previous set of payments but also
# allow for a lookahead window (due to shifted payment dates).
date - MONTHLY_RECURRING_BILLING_FREQUENCY - MONTHLY_RECURRING_BILLING_CHURN_WINDOW
if is_yearly_revenue == true
date - YEARLY_BILLING_FREQUENCY - YEARLY_BILLING_CHURN_WINDOW
else
date - MONTHLY_BILLING_FREQUENCY - MONTHLY_BILLING_CHURN_WINDOW
end
end

def churn_calculations_date_lower_bound
if is_yearly_revenue == true
date - (YEARLY_BILLING_CHURN_WINDOW * 2)
else
date - (MONTHLY_BILLING_CHURN_WINDOW * 2)
end
end

def churned_shops
Expand All @@ -115,4 +130,13 @@ def not_repeatable_charge_type?
def not_churnable_charge_type?
charge_type != "recurring_revenue" && charge_type != "affiliate_revenue"
end

def payments_by_options_and_date(date)
user.payments.where(
payment_date: date,
charge_type: charge_type,
app_title: app_title,
is_yearly_revenue: is_yearly_revenue
)
end
end
32 changes: 23 additions & 9 deletions app/models/metric/tile_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@ def initialize(filter:, tile_config:)
@column = tile_config[:column]
@display = tile_config[:display]
@positive_change_is_good = tile_config[:positive_change_is_good]
@is_yearly_revenue = tile_config[:is_yearly_revenue]
end
attr_reader :handle, :display, :calculation, :positive_change_is_good
attr_reader :handle, :display, :calculation, :positive_change_is_good, :is_yearly_revenue

def current_value
metrics = @filter.current_period_metrics.by_optional_charge_type(@charge_type)
metrics.calculate_value(@calculation, @column)
metrics = @filter.current_period_metrics
.by_optional_charge_type(@charge_type)
.by_optional_is_yearly_revenue(@is_yearly_revenue)
.calculate_value(@calculation, @column)
metrics.blank? ? 0 : metrics
end

def previous_value
metrics = @filter.previous_period_metrics.by_optional_charge_type(@charge_type)
metrics.calculate_value(@calculation, @column)
metrics = @filter.previous_period_metrics
.by_optional_charge_type(@charge_type)
.by_optional_is_yearly_revenue(@is_yearly_revenue)
.calculate_value(@calculation, @column)
metrics.blank? ? 0 : metrics
end

def change
Expand All @@ -27,13 +34,17 @@ def change
end

def average_value
return 0 if current_value.blank?
current_value / @filter.period
end

def period_ago_value(period_ago)
period_ago_date = @filter.date - (period_ago * @filter.period).days
metrics = @filter.user_metrics_by_app.by_optional_charge_type(@charge_type)
metrics.by_date_and_period(date: period_ago_date, period: @filter.period).calculate_value(@calculation, @column)
@filter.user_metrics_by_app
.by_date_and_period(date: period_ago_date, period: @filter.period)
.by_optional_charge_type(@charge_type)
.by_optional_is_yearly_revenue(@is_yearly_revenue)
.calculate_value(@calculation, @column)
end

def period_ago_change(period_ago)
Expand Down Expand Up @@ -66,8 +77,11 @@ def forecast_chart_data(chart_data)

def metrics_chart_data
@metrics_chart_data ||= begin
metrics = @filter.user_metrics_by_app.by_optional_charge_type(@charge_type)
metrics.chart_data(@filter.date, @filter.period, @calculation, @column).to_h
@filter.user_metrics_by_app
.by_optional_charge_type(@charge_type)
.by_optional_is_yearly_revenue(@is_yearly_revenue)
.chart_data(@filter.date, @filter.period, @calculation, @column)
.to_h
end
end
end
Loading

0 comments on commit 2f3ac7e

Please sign in to comment.