Skip to content

Commit

Permalink
misc(usage): Scope usage caching to the charge filter level
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-pochet committed Oct 11, 2024
1 parent c4fed78 commit 9f957be
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 155 deletions.
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

0 comments on commit 9f957be

Please sign in to comment.