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

feat(DynamicPricing) - Add dynamic charge model & validator #2613

Draft
wants to merge 7 commits into
base: main
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
6 changes: 6 additions & 0 deletions app/models/charge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Charge < ApplicationRecord
volume
graduated_percentage
custom
dynamic
].freeze

REGROUPING_PAID_FEES_OPTIONS = %i[invoice].freeze
Expand All @@ -37,6 +38,7 @@ class Charge < ApplicationRecord
validate :validate_percentage, if: -> { percentage? }
validate :validate_volume, if: -> { volume? }
validate :validate_graduated_percentage, if: -> { graduated_percentage? }
validate :validate_dynamic, if: -> { dynamic? }

validates :min_amount_cents, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
validates :charge_model, presence: true
Expand Down Expand Up @@ -79,6 +81,10 @@ def validate_graduated_percentage
validate_charge_model(Charges::Validators::GraduatedPercentageService)
end

def validate_dynamic
validate_charge_model(Charges::Validators::DynamicService)
end

def validate_charge_model(validator)
instance = validator.new(charge: self)
return if instance.valid?
Expand Down
15 changes: 8 additions & 7 deletions app/models/clickhouse/events_raw.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ class EventsRaw < BaseRecord
#
# Table name: events_raw
#
# code :string not null
# properties :string not null
# timestamp :datetime not null
# external_customer_id :string not null
# external_subscription_id :string not null
# organization_id :string not null
# transaction_id :string not null
# code :string not null
# precise_total_amount_cents :decimal(40, 15)
# properties :string not null
# timestamp :datetime not null
# external_customer_id :string not null
# external_subscription_id :string not null
# organization_id :string not null
# transaction_id :string not null
#
4 changes: 4 additions & 0 deletions app/services/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ def call_async(**args, &block)
raise NotImplementedError
end

protected

attr_writer :result

private

attr_reader :result, :source
Expand Down
8 changes: 8 additions & 0 deletions app/services/billable_metrics/aggregations/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def aggregate(options: {})
else
compute_aggregation(options:)
end
if charge.dynamic?
compute_precise_total_amount_cents(options:)
end
result
end

def compute_aggregation(options: {})
Expand All @@ -36,6 +40,10 @@ def compute_grouped_by_aggregation(options: {})
raise NotImplementedError
end

def compute_precise_total_amount_cents(options: {})
raise NotImplementedError
end

def per_event_aggregation(exclude_event: false)
Result.new.tap do |result|
result.event_aggregation = compute_per_event_aggregation(exclude_event:)
Expand Down
6 changes: 5 additions & 1 deletion app/services/billable_metrics/aggregations/sum_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module BillableMetrics
module Aggregations
class SumService < BillableMetrics::Aggregations::BaseService
def initialize(...)
super(...)
super

event_store.numeric_property = true
event_store.aggregation_property = billable_metric.field_name
Expand Down Expand Up @@ -64,6 +64,10 @@ def compute_grouped_by_aggregation(options: {})
result.service_failure!(code: 'aggregation_failure', message: e.message)
end

def compute_precise_total_amount_cents(options: {})
result.precise_total_amount_cents = event_store.sum_precise_total_amount_cents
end

# NOTE: Return cumulative sum of field_name based on the number of free units
# (per_events or per_total_aggregation).
def running_total(options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ module BillableMetrics
module ProratedAggregations
class SumService < BillableMetrics::ProratedAggregations::BaseService
def initialize(**args)
super
@base_aggregator = BillableMetrics::Aggregations::SumService.new(**args)

super(**args)
@base_aggregator.result = result

event_store.numeric_property = true
event_store.aggregation_property = billable_metric.field_name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ module BillableMetrics
module ProratedAggregations
class UniqueCountService < BillableMetrics::ProratedAggregations::BaseService
def initialize(**args)
@base_aggregator = BillableMetrics::Aggregations::UniqueCountService.new(**args)
super

super(**args)
@base_aggregator = BillableMetrics::Aggregations::UniqueCountService.new(**args)
@base_aggregator.result = result

event_store.aggregation_property = billable_metric.field_name
event_store.use_from_boundary = !billable_metric.recurring
Expand Down
5 changes: 5 additions & 0 deletions app/services/charges/build_default_properties_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def call
when :percentage then default_percentage_properties
when :volume then default_volume_properties
when :graduated_percentage then default_graduated_percentage_properties
when :dynamic then default_dynamic_properties
end
end

Expand Down Expand Up @@ -77,5 +78,9 @@ def default_graduated_percentage_properties
]
}
end

def default_dynamic_properties
{}
end
end
end
2 changes: 2 additions & 0 deletions app/services/charges/charge_model_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def self.charge_model_class(charge:, aggregation_result:, properties:)
Charges::ChargeModels::VolumeService
when :custom
Charges::ChargeModels::CustomService
when :dynamic
Charges::ChargeModels::DynamicService
else
raise(NotImplementedError)
end
Expand Down
20 changes: 20 additions & 0 deletions app/services/charges/charge_models/dynamic_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Charges
module ChargeModels
class DynamicService < Charges::ChargeModels::BaseService
protected

def compute_amount
aggregation_result.precise_total_amount_cents
end

def unit_amount
total_units = aggregation_result.full_units_number || units
return 0 if total_units.zero?

compute_amount / total_units
end
end
end
end
22 changes: 22 additions & 0 deletions app/services/charges/validators/dynamic_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Charges
module Validators
class DynamicService < Charges::Validators::BaseService
def valid?
validate_billable_metric

super
end

private

def validate_billable_metric
# Only sum aggregation is compatible with Dynamic Pricing for now
return if charge.billable_metric.sum_agg?

add_error(field: :billable_metric, error_code: 'invalid_value')
end
end
end
end
15 changes: 15 additions & 0 deletions app/services/events/stores/clickhouse_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,21 @@ def grouped_last
prepare_grouped_result(::Clickhouse::EventsRaw.connection.select_all(sql).rows)
end

def sum_precise_total_amount_cents
cte_sql = events.group(DEDUPLICATION_GROUP)
.select(Arel.sql("precise_total_amount_cents as property"))
.to_sql

sql = <<-SQL
with events as (#{cte_sql})

select sum(events.property)
from events
SQL

::Clickhouse::EventsRaw.connection.select_value(sql)
end

def sum
cte_sql = events.group(DEDUPLICATION_GROUP)
.select(Arel.sql("#{sanitized_numeric_property} AS property"))
Expand Down
4 changes: 4 additions & 0 deletions app/services/events/stores/postgres_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ def grouped_last
prepare_grouped_result(Event.connection.select_all(sql).rows)
end

def sum_precise_total_amount_cents
events.sum(:precise_total_amount_cents)
end

def sum
events.sum("(#{sanitized_property_name})::numeric")
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddPreciseTotalAmountCentsToEvents < ActiveRecord::Migration[7.1]
def change
add_column :events_raw, :precise_total_amount_cents, :decimal, precision: 40, scale: 15
end
end
1 change: 1 addition & 0 deletions graphql_schemas/api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ input ChargeInput {

enum ChargeModelEnum {
custom
dynamic
graduated
graduated_percentage
package
Expand Down
6 changes: 6 additions & 0 deletions graphql_schemas/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -3268,6 +3268,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dynamic",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
]
},
Expand Down
1 change: 1 addition & 0 deletions graphql_schemas/customer_portal.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ scalar ChargeFilterValues

enum ChargeModelEnum {
custom
dynamic
graduated
graduated_percentage
package
Expand Down
6 changes: 6 additions & 0 deletions graphql_schemas/customer_portal.json
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dynamic",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
]
},
Expand Down
1 change: 1 addition & 0 deletions schema.graphql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.