Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

misc(usage): Scope usage caching to the charge filter level #2678

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
5 changes: 4 additions & 1 deletion app/services/events/post_process_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ def expire_cached_charges(subscriptions)
charges = billable_metric.charges
.joins(:plan)
.where(plans: {id: active_subscription.map(&:plan_id)})
.includes(filters: {values: :billable_metric_filter})

charges.each do |charge|
charge_filter = ChargeFilters::EventMatchingService.call(charge:, event:).charge_filter

active_subscription.each do |subscription|
Subscriptions::ChargeCacheService.new(subscription:, charge:).expire_cache
Subscriptions::ChargeCacheService.new(subscription:, charge:, charge_filter:).expire_cache
end
end
end
Expand Down
48 changes: 27 additions & 21 deletions app/services/fees/charge_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@

module Fees
class ChargeService < BaseService
def initialize(invoice:, charge:, subscription:, boundaries:)
def initialize(invoice:, charge:, subscription:, boundaries:, current_usage: false, cache_middleware: nil)
@invoice = invoice
@charge = charge
@subscription = subscription
@is_current_usage = false
@boundaries = OpenStruct.new(boundaries)
@currency = subscription.plan.amount.currency

@current_usage = current_usage
@cache_middleware = cache_middleware || Subscriptions::ChargeCacheMiddleware.new(
subscription:, charge:, to_datetime: boundaries[:charges_to_datetime], cache: false
)

super(nil)
end

def call
return result if already_billed?
return result if !current_usage && already_billed?

init_fees
return result if current_usage

if invoice.nil? || !invoice.progressive_billing?
init_true_up_fee(
Expand Down Expand Up @@ -45,16 +50,9 @@ def call
result.record_validation_failure!(record: e.record)
end

def current_usage
@is_current_usage = true

init_fees
result
end

private

attr_accessor :invoice, :charge, :subscription, :boundaries, :is_current_usage, :currency
attr_accessor :invoice, :charge, :subscription, :boundaries, :current_usage, :currency, :cache_middleware

delegate :billable_metric, to: :charge
delegate :plan, to: :subscription
Expand All @@ -74,18 +72,26 @@ def init_fees
end

def init_charge_fees(properties:, charge_filter: nil)
charge_model_result = apply_aggregation_and_charge_model(properties:, charge_filter:)
return result.fail_with_error!(charge_model_result.error) unless charge_model_result.success?
fees = cache_middleware.call(charge_filter:) do
charge_model_result = apply_aggregation_and_charge_model(properties:, charge_filter:)

unless charge_model_result.success?
result.fail_with_error!(charge_model_result.error)
return []
end

(charge_model_result.grouped_results || [charge_model_result]).each do |amount_result|
init_fee(amount_result, properties:, charge_filter:)
(charge_model_result.grouped_results || [charge_model_result]).map do |amount_result|
init_fee(amount_result, properties:, charge_filter:)
end
end

result.fees.concat(fees.compact)
end

def init_fee(amount_result, properties:, charge_filter:)
# NOTE: Build fee for case when there is adjusted fee and units or amount has been adjusted.
# Base fee creation flow handles case when only name has been adjusted
if invoice&.draft? && (adjusted = adjusted_fee(
if !current_usage && invoice&.draft? && (adjusted = adjusted_fee(
charge_filter:,
grouped_by: amount_result.grouped_by
)) && !adjusted.adjusted_display_name?
Expand All @@ -107,7 +113,7 @@ def init_fee(amount_result, properties:, charge_filter:)
precise_amount_cents = amount_result.amount * currency.subunit_to_unit.to_d
unit_amount_cents = amount_result.unit_amount * currency.subunit_to_unit

units = if is_current_usage && (charge.pay_in_advance? || charge.prorated?)
units = if current_usage && (charge.pay_in_advance? || charge.prorated?)
amount_result.current_usage_units
elsif charge.prorated?
amount_result.full_units_number.nil? ? amount_result.units : amount_result.full_units_number
Expand Down Expand Up @@ -147,7 +153,7 @@ def init_fee(amount_result, properties:, charge_filter:)
new_fee.invoice_display_name = adjusted.invoice_display_name
end

result.fees << new_fee
new_fee
end

def adjusted_fee(charge_filter:, grouped_by:)
Expand Down Expand Up @@ -197,7 +203,7 @@ def options(properties)
{
free_units_per_events: properties['free_units_per_events'].to_i,
free_units_per_total_aggregation: BigDecimal(properties['free_units_per_total_aggregation'] || 0),
is_current_usage:,
is_current_usage: current_usage,
is_pay_in_advance: charge.pay_in_advance?
}
end
Expand Down Expand Up @@ -227,7 +233,7 @@ def already_billed?
def aggregator(charge_filter:)
BillableMetrics::AggregationFactory.new_instance(
charge:,
current_usage: is_current_usage,
current_usage:,
subscription:,
boundaries: {
from_datetime: boundaries.charges_from_datetime,
Expand All @@ -239,7 +245,7 @@ def aggregator(charge_filter:)
end

def persist_recurring_value(aggregation_results, charge_filter)
return if is_current_usage
return if current_usage

# NOTE: Only weighted sum and custom aggregations are setting this value
return unless aggregation_results.first&.recurring_updated_at
Expand Down
40 changes: 10 additions & 30 deletions app/services/invoices/customer_usage_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,17 @@ def add_charge_fees
end

def charge_usage(charge)
return charge_usage_without_cache(charge) if organization.clickhouse_aggregation?

json = Rails.cache.fetch(charge_cache_key(charge), expires_in: charge_cache_expiration) do
fees_result = Fees::ChargeService.new(
invoice:, charge:, subscription:, boundaries:
).current_usage

fees_result.raise_if_error!

fees_result.fees.to_json
end

JSON.parse(json).map { |j| Fee.new(j.slice(*Fee.column_names)) }
end

def charge_usage_without_cache(charge)
fees_result = Fees::ChargeService.new(
invoice:, charge:, subscription:, boundaries:
).current_usage

fees_result.raise_if_error!
cache_middleware = Subscriptions::ChargeCacheMiddleware.new(
subscription:,
charge:,
to_datetime: boundaries[:charges_to_datetime],
cache: !organization.clickhouse_aggregation? # NOTE: Will be turned on in the future
)

fees_result.fees
Fees::ChargeService
.call(invoice:, charge:, subscription:, boundaries:, current_usage: true, cache_middleware:)
.raise_if_error!
.fees
end

def boundaries
Expand Down Expand Up @@ -181,10 +169,6 @@ def compute_amounts_with_provider_taxes
invoice.total_amount_cents = invoice.fees_amount_cents + invoice.taxes_amount_cents
end

def charge_cache_key(charge)
Subscriptions::ChargeCacheService.new(subscription:, charge:).cache_key
end

def provider_taxes_cache_key
[
'provider-taxes',
Expand All @@ -193,10 +177,6 @@ def provider_taxes_cache_key
].join('/')
end

def charge_cache_expiration
(boundaries[:charges_to_datetime] - Time.current).to_i.seconds
end

def format_usage
OpenStruct.new(
from_datetime: boundaries[:charges_from_datetime].iso8601,
Expand Down
34 changes: 34 additions & 0 deletions app/services/subscriptions/charge_cache_middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Subscriptions
class ChargeCacheMiddleware
def initialize(subscription:, charge:, to_datetime:, cache: true)
@subscription = subscription
@charge = charge
@to_datetime = to_datetime
@cache = cache
end

def call(charge_filter:)
return yield unless cache

json = Rails.cache.fetch(cache_key(charge_filter), expires_in: cache_expiration) do
yield.to_json
end

JSON.parse(json).map { |j| Fee.new(j.slice(*Fee.column_names)) }
end

private

attr_reader :subscription, :charge, :to_datetime, :cache

def cache_key(charge_filter)
Subscriptions::ChargeCacheService.new(subscription:, charge:, charge_filter:).cache_key
end

def cache_expiration
(to_datetime - Time.current).to_i.seconds
end
end
end
26 changes: 18 additions & 8 deletions app/services/subscriptions/charge_cache_service.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
# frozen_string_literal: true

module Subscriptions
class ChargeCacheService < BaseService
class ChargeCacheService
def self.expire_for_subscription(subscription)
subscription.plan.charges.each { new(subscription: subscription, charge: _1).expire_cache }
subscription.plan.charges.includes(:filters)
.find_each { expire_for_subscription_charge(subscription:, charge: _1) }
end

def initialize(subscription:, charge:)
def self.expire_for_subscription_charge(subscription:, charge:)
charge.filters.each do |filter|
new(subscription:, charge:, charge_filter: filter).expire_cache
end

new(subscription:, charge:).expire_cache
end

def initialize(subscription:, charge:, charge_filter: nil)
@subscription = subscription
@charge = charge

super
@charge_filter = charge_filter
end

def cache_key
[
'charge-usage',
charge.id,
subscription.id,
charge.updated_at.iso8601
].join('/')
charge.updated_at.iso8601,
charge_filter&.id,
charge_filter&.updated_at&.iso8601
].compact.join('/')
end

def expire_cache
Expand All @@ -28,6 +38,6 @@ def expire_cache

private

attr_reader :subscription, :charge
attr_reader :subscription, :charge, :charge_filter
end
end
2 changes: 1 addition & 1 deletion lib/tasks/cache.rake
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace :cache do

Charge.where(id: charge_id).includes(plan: :subscriptions).find_each do |charge|
charge.plan.subscriptions.find_each do |subscription|
Subscriptions::ChargeCacheService.new(subscription:, charge:).expire_cache
Subscriptions::ChargeCacheService.expire_for_subscription_charge(subscription:, charge:)
end
end
end
Expand Down
Loading