diff --git a/app/models/invoice.rb b/app/models/invoice.rb index c161e2e2ace..4627333ed5c 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -159,11 +159,11 @@ def recurring_breakdown(fee) event_store_class: Events::Stores::PostgresStore, charge: fee.charge, subscription: fee.subscription, - group: fee.group, boundaries: { from_datetime: DateTime.parse(fee.properties['charges_from_datetime']), to_datetime: DateTime.parse(fee.properties['charges_to_datetime']), }, + filters: { group: fee.group }, ).breakdown.breakdown end diff --git a/app/services/billable_metrics/aggregations/base_service.rb b/app/services/billable_metrics/aggregations/base_service.rb index 4a0f3587a24..842dbac15ed 100644 --- a/app/services/billable_metrics/aggregations/base_service.rb +++ b/app/services/billable_metrics/aggregations/base_service.rb @@ -3,13 +3,16 @@ module BillableMetrics module Aggregations class BaseService < ::BaseService - def initialize(event_store_class:, charge:, subscription:, boundaries:, group: nil, event: nil) # rubocop:disable Metrics/ParameterLists + def initialize(event_store_class:, charge:, subscription:, boundaries:, filters: {}) super(nil) @event_store_class = event_store_class @charge = charge @subscription = subscription - @group = group - @event = event + + @filters = filters + @group = filters[:group] + @event = filters[:event] + @boundaries = boundaries result.aggregator = self @@ -27,7 +30,7 @@ def per_event_aggregation protected - attr_accessor :event_store_class, :charge, :subscription, :group, :event, :boundaries + attr_accessor :event_store_class, :charge, :subscription, :filters, :group, :event, :boundaries delegate :billable_metric, to: :charge diff --git a/app/services/billable_metrics/aggregations/unique_count_service.rb b/app/services/billable_metrics/aggregations/unique_count_service.rb index 27f3a57f8b4..6315d200e91 100644 --- a/app/services/billable_metrics/aggregations/unique_count_service.rb +++ b/app/services/billable_metrics/aggregations/unique_count_service.rb @@ -172,7 +172,7 @@ def base_scope from_datetime: subscription.started_at, to_datetime: subscription.terminated_at, }, - filters: { group: }, + filters:, ) store.aggregation_property = billable_metric.field_name diff --git a/app/services/billable_metrics/aggregations/weighted_sum_service.rb b/app/services/billable_metrics/aggregations/weighted_sum_service.rb index 6f3bd4dc779..eea45537e02 100644 --- a/app/services/billable_metrics/aggregations/weighted_sum_service.rb +++ b/app/services/billable_metrics/aggregations/weighted_sum_service.rb @@ -60,7 +60,7 @@ def latest_value_from_events code: billable_metric.code, subscription:, boundaries: { to_datetime: from_datetime }, - filters: { group: }, + filters:, ) event_store.use_from_boundary = false diff --git a/app/services/billable_metrics/breakdown/sum_service.rb b/app/services/billable_metrics/breakdown/sum_service.rb index 7a21c51d4a5..12ffce035e6 100644 --- a/app/services/billable_metrics/breakdown/sum_service.rb +++ b/app/services/billable_metrics/breakdown/sum_service.rb @@ -27,7 +27,7 @@ def persisted_breakdown code: billable_metric.code, subscription:, boundaries: { to_datetime: from_datetime }, - filters: { group: }, + filters:, ) event_store.use_from_boundary = false diff --git a/app/services/billable_metrics/prorated_aggregations/sum_service.rb b/app/services/billable_metrics/prorated_aggregations/sum_service.rb index 5d83865b109..44427952122 100644 --- a/app/services/billable_metrics/prorated_aggregations/sum_service.rb +++ b/app/services/billable_metrics/prorated_aggregations/sum_service.rb @@ -67,7 +67,7 @@ def persisted_sum code: billable_metric.code, subscription:, boundaries: { to_datetime: from_datetime }, - filters: { group: }, + filters:, ) event_store.use_from_boundary = false @@ -92,7 +92,7 @@ def recurring_value code: billable_metric.code, subscription:, boundaries: { to_datetime: from_datetime }, - filters: { group: }, + filters:, ) event_store.use_from_boundary = false diff --git a/app/services/charges/pay_in_advance_aggregation_service.rb b/app/services/charges/pay_in_advance_aggregation_service.rb index 5f623b7498a..4cbf196446e 100644 --- a/app/services/charges/pay_in_advance_aggregation_service.rb +++ b/app/services/charges/pay_in_advance_aggregation_service.rb @@ -16,12 +16,11 @@ def call aggregator = BillableMetrics::AggregationFactory.new_instance( charge:, subscription:, - group:, - event:, boundaries: { from_datetime: boundaries[:charges_from_datetime], to_datetime: boundaries[:charges_to_datetime], }, + filters: aggregation_filters, ) aggregator.aggregate(options: aggregation_options) @@ -40,5 +39,20 @@ def aggregation_options free_units_per_total_aggregation: BigDecimal(properties['free_units_per_total_aggregation'] || 0), } end + + def aggregation_filters + filters = { + group:, + event:, + } + + if charge.standard? && charge.properties['grouped_by'].present? + filters[:grouped_by_values] = charge.properties['grouped_by'].index_with do |grouped_by| + event.properties[grouped_by] + end + end + + filters + end end end diff --git a/app/services/events/stores/base_store.rb b/app/services/events/stores/base_store.rb index 63c406e2261..7ac16746ef5 100644 --- a/app/services/events/stores/base_store.rb +++ b/app/services/events/stores/base_store.rb @@ -10,15 +10,15 @@ def initialize(code:, subscription:, boundaries:, filters: {}) @group = filters[:group] @grouped_by = filters[:grouped_by] - @grouped_by_value = filters[:grouped_by_value] + @grouped_by_values = filters[:grouped_by_values] @aggregation_property = nil @numeric_property = false @use_from_boundary = true end - def grouped_by_value? - grouped_by.present? && grouped_by_value.present? + def grouped_by_values? + grouped_by.present? && grouped_by_values.present? end def events(force_from: false) @@ -78,7 +78,7 @@ def charges_duration protected - attr_accessor :code, :subscription, :group, :boundaries, :grouped_by, :grouped_by_value + attr_accessor :code, :subscription, :group, :boundaries, :grouped_by, :grouped_by_values delegate :customer, to: :subscription diff --git a/app/services/events/stores/clickhouse_store.rb b/app/services/events/stores/clickhouse_store.rb index 3651f193459..ac5de482719 100644 --- a/app/services/events/stores/clickhouse_store.rb +++ b/app/services/events/stores/clickhouse_store.rb @@ -18,7 +18,7 @@ def events(force_from: false) scope = scope.where('events_raw.timestamp <= ?', to_datetime) if to_datetime scope = scope.where(numeric_condition) if numeric_property - scope = with_grouped_by_value(scope) if grouped_by_value? + scope = with_grouped_by_values(scope) if grouped_by_values? return scope unless group @@ -178,8 +178,12 @@ def group_scope(scope) scope.where('events_raw.properties[?] = ?', group.parent.key.to_s => group.parent.value.to_s) end - def with_grouped_by_value(scope) - scope.where('events_raw.properties[?] = ?', grouped_by.to_s, grouped_by_value.to_s) + def with_grouped_by_values(scope) + grouped_by_values.each do |grouped_by, grouped_by_value| + scope = scope.where('events_raw.properties[?] = ?', grouped_by, grouped_by_value) + end + + scope end def numeric_condition diff --git a/app/services/events/stores/postgres_store.rb b/app/services/events/stores/postgres_store.rb index 7b5a9b45a70..e0f56ad1797 100644 --- a/app/services/events/stores/postgres_store.rb +++ b/app/services/events/stores/postgres_store.rb @@ -16,7 +16,7 @@ def events(force_from: false) .where(numeric_condition) end - scope = with_grouped_by_value(scope) if grouped_by_value? + scope = with_grouped_by_values(scope) if grouped_by_values? return scope unless group @@ -128,8 +128,12 @@ def group_scope(scope) scope.where('events.properties @> ?', { group.parent.key.to_s => group.parent.value }.to_json) end - def with_grouped_by_value(scope) - scope.where('events.properties @> ?', { grouped_by.to_s => grouped_by_value.to_s }.to_json) + def with_grouped_by_values(scope) + grouped_by_values.each do |grouped_by, grouped_by_value| + scope = scope.where('events.properties @> ?', { grouped_by.to_s => grouped_by_value.to_s }.to_json) + end + + scope end def sanitized_propery_name diff --git a/app/services/fees/charge_service.rb b/app/services/fees/charge_service.rb index e92554387ee..745d0c6038e 100644 --- a/app/services/fees/charge_service.rb +++ b/app/services/fees/charge_service.rb @@ -231,12 +231,12 @@ def aggregator(group:) charge:, current_usage: is_current_usage, subscription:, - group:, boundaries: { from_datetime: boundaries.charges_from_datetime, to_datetime: boundaries.charges_to_datetime, charges_duration: boundaries.charges_duration, }, + filters: { group: }, ) end diff --git a/app/services/fees/create_pay_in_advance_service.rb b/app/services/fees/create_pay_in_advance_service.rb index 69692ba6ea3..046a7a3fd7c 100644 --- a/app/services/fees/create_pay_in_advance_service.rb +++ b/app/services/fees/create_pay_in_advance_service.rb @@ -65,6 +65,7 @@ def create_fee(properties:, group: nil) taxes_amount_cents: 0, unit_amount_cents:, precise_unit_amount: result.unit_amount, + grouped_by: (charge.properties['grouped_by'] || []).map { event.properties[_1] }, ) taxes_result = Fees::ApplyTaxesService.call(fee:) @@ -168,6 +169,7 @@ def cache_aggregation_result(aggregation_result:, group:) current_aggregation: aggregation_result.current_aggregation, max_aggregation: aggregation_result.max_aggregation, max_aggregation_with_proration: aggregation_result.max_aggregation_with_proration, + grouped_by: (charge.properties['grouped_by'] || []).map { event.properties[_1] }, ) end end diff --git a/spec/services/billable_metrics/aggregations/count_service_spec.rb b/spec/services/billable_metrics/aggregations/count_service_spec.rb index f2bafedbd02..56d13595d3f 100644 --- a/spec/services/billable_metrics/aggregations/count_service_spec.rb +++ b/spec/services/billable_metrics/aggregations/count_service_spec.rb @@ -8,12 +8,14 @@ event_store_class:, charge:, subscription:, - group:, - event: pay_in_advance_event, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + event: pay_in_advance_event, + }, ) end diff --git a/spec/services/billable_metrics/aggregations/latest_service_spec.rb b/spec/services/billable_metrics/aggregations/latest_service_spec.rb index bdb104a16a0..f1a2bfaf997 100644 --- a/spec/services/billable_metrics/aggregations/latest_service_spec.rb +++ b/spec/services/billable_metrics/aggregations/latest_service_spec.rb @@ -8,11 +8,11 @@ event_store_class:, charge:, subscription:, - group:, boundaries: { from_datetime:, to_datetime:, }, + filters: { group: }, ) end diff --git a/spec/services/billable_metrics/aggregations/max_service_spec.rb b/spec/services/billable_metrics/aggregations/max_service_spec.rb index 7389c057387..5f1922f92d3 100644 --- a/spec/services/billable_metrics/aggregations/max_service_spec.rb +++ b/spec/services/billable_metrics/aggregations/max_service_spec.rb @@ -8,11 +8,13 @@ event_store_class:, charge:, subscription:, - group:, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + }, ) end diff --git a/spec/services/billable_metrics/aggregations/sum_service_spec.rb b/spec/services/billable_metrics/aggregations/sum_service_spec.rb index e5a7b6cf4ad..9f13de5690f 100644 --- a/spec/services/billable_metrics/aggregations/sum_service_spec.rb +++ b/spec/services/billable_metrics/aggregations/sum_service_spec.rb @@ -8,12 +8,14 @@ event_store_class:, charge:, subscription:, - group:, - event: pay_in_advance_event, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + event: pay_in_advance_event, + }, ) end diff --git a/spec/services/billable_metrics/aggregations/unique_count_service_spec.rb b/spec/services/billable_metrics/aggregations/unique_count_service_spec.rb index 13e4a74e89c..7916099bbf9 100644 --- a/spec/services/billable_metrics/aggregations/unique_count_service_spec.rb +++ b/spec/services/billable_metrics/aggregations/unique_count_service_spec.rb @@ -8,12 +8,14 @@ event_store_class:, charge:, subscription:, - group:, - event: pay_in_advance_event, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + event: pay_in_advance_event, + }, ) end diff --git a/spec/services/billable_metrics/aggregations/weighted_sum_service_spec.rb b/spec/services/billable_metrics/aggregations/weighted_sum_service_spec.rb index 0b62076521e..43dc2ecf360 100644 --- a/spec/services/billable_metrics/aggregations/weighted_sum_service_spec.rb +++ b/spec/services/billable_metrics/aggregations/weighted_sum_service_spec.rb @@ -8,12 +8,12 @@ event_store_class:, charge:, subscription:, - group:, boundaries: { from_datetime:, to_datetime:, charges_duration:, }, + filters: { group: }, ) end diff --git a/spec/services/billable_metrics/breakdown/sum_service_spec.rb b/spec/services/billable_metrics/breakdown/sum_service_spec.rb index 870baf282e8..a632b2f0af9 100644 --- a/spec/services/billable_metrics/breakdown/sum_service_spec.rb +++ b/spec/services/billable_metrics/breakdown/sum_service_spec.rb @@ -8,11 +8,13 @@ event_store_class:, charge:, subscription:, - group:, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + }, ) end diff --git a/spec/services/billable_metrics/breakdown/unique_count_service_spec.rb b/spec/services/billable_metrics/breakdown/unique_count_service_spec.rb index e7475ca0a56..ea486827cee 100644 --- a/spec/services/billable_metrics/breakdown/unique_count_service_spec.rb +++ b/spec/services/billable_metrics/breakdown/unique_count_service_spec.rb @@ -8,11 +8,13 @@ event_store_class:, charge:, subscription:, - group:, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + }, ) end diff --git a/spec/services/billable_metrics/prorated_aggregations/sum_service_spec.rb b/spec/services/billable_metrics/prorated_aggregations/sum_service_spec.rb index 1f98ff717a0..c2f3cce925c 100644 --- a/spec/services/billable_metrics/prorated_aggregations/sum_service_spec.rb +++ b/spec/services/billable_metrics/prorated_aggregations/sum_service_spec.rb @@ -8,12 +8,14 @@ event_store_class:, charge:, subscription:, - group:, - event: pay_in_advance_event, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + event: pay_in_advance_event, + }, ) end diff --git a/spec/services/billable_metrics/prorated_aggregations/unique_count_service_spec.rb b/spec/services/billable_metrics/prorated_aggregations/unique_count_service_spec.rb index 80d975f5a66..7cc387e6d2e 100644 --- a/spec/services/billable_metrics/prorated_aggregations/unique_count_service_spec.rb +++ b/spec/services/billable_metrics/prorated_aggregations/unique_count_service_spec.rb @@ -8,12 +8,14 @@ event_store_class:, charge:, subscription:, - group:, - event: pay_in_advance_event, boundaries: { from_datetime:, to_datetime:, }, + filters: { + group:, + event: pay_in_advance_event, + }, ) end diff --git a/spec/services/charges/pay_in_advance_aggregation_service_spec.rb b/spec/services/charges/pay_in_advance_aggregation_service_spec.rb index 3180813fc18..0ead64218ef 100644 --- a/spec/services/charges/pay_in_advance_aggregation_service_spec.rb +++ b/spec/services/charges/pay_in_advance_aggregation_service_spec.rb @@ -44,18 +44,66 @@ event_store_class: Events::Stores::PostgresStore, charge:, subscription:, - group:, - event:, boundaries: { from_datetime: boundaries[:charges_from_datetime], to_datetime: boundaries[:charges_to_datetime], }, + filters: { + group:, + event:, + }, ) expect(count_service).to have_received(:aggregate).with( options: { free_units_per_events: 0, free_units_per_total_aggregation: 0 }, ) end + + describe 'when charge model has grouped_by property' do + let(:charge) do + create( + :standard_charge, + billable_metric:, + pay_in_advance: true, + properties: { 'grouped_by' => ['operator'], 'amount' => '100' }, + ) + end + + let(:event) do + create( + :event, + organization:, + external_subscription_id: subscription.external_id, + properties: { 'operator' => 'foo' }, + ) + end + + it 'delegates to the count aggregation service' do + allow(BillableMetrics::Aggregations::CountService).to receive(:new).and_return(count_service) + + agg_service.call + + expect(BillableMetrics::Aggregations::CountService).to have_received(:new) + .with( + event_store_class: Events::Stores::PostgresStore, + charge:, + subscription:, + boundaries: { + from_datetime: boundaries[:charges_from_datetime], + to_datetime: boundaries[:charges_to_datetime], + }, + filters: { + group:, + event:, + grouped_by_values: { 'operator' => 'foo' }, + }, + ) + + expect(count_service).to have_received(:aggregate).with( + options: { free_units_per_events: 0, free_units_per_total_aggregation: 0 }, + ) + end + end end describe 'when sum aggregation' do @@ -75,12 +123,14 @@ event_store_class: Events::Stores::PostgresStore, charge:, subscription:, - group:, - event:, boundaries: { from_datetime: boundaries[:charges_from_datetime], to_datetime: boundaries[:charges_to_datetime], }, + filters: { + group:, + event:, + }, ) expect(sum_service).to have_received(:aggregate).with( @@ -105,12 +155,14 @@ event_store_class: Events::Stores::PostgresStore, charge:, subscription:, - group:, - event:, boundaries: { from_datetime: boundaries[:charges_from_datetime], to_datetime: boundaries[:charges_to_datetime], }, + filters: { + group:, + event:, + }, ) expect(unique_count_service).to have_received(:aggregate).with( diff --git a/spec/services/events/stores/clickhouse_store_spec.rb b/spec/services/events/stores/clickhouse_store_spec.rb index 3e4cb90df1b..376f84e8e95 100644 --- a/spec/services/events/stores/clickhouse_store_spec.rb +++ b/spec/services/events/stores/clickhouse_store_spec.rb @@ -8,7 +8,7 @@ code:, subscription:, boundaries:, - filters: { group:, grouped_by:, grouped_by_value: }, + filters: { group:, grouped_by:, grouped_by_values: }, ) end @@ -31,7 +31,7 @@ let(:group) { nil } let(:grouped_by) { nil } - let(:grouped_by_value) { nil } + let(:grouped_by_values) { nil } let(:events) do events = [] @@ -41,7 +41,7 @@ if i.even? properties[group.key.to_s] = group.value.to_s if group - properties[grouped_by] = grouped_by_value if grouped_by_value + properties[grouped_by] = grouped_by_values[grouped_by] if grouped_by_values end events << Clickhouse::EventsRaw.create!( @@ -87,9 +87,9 @@ end end - context 'with grouped_by_value' do + context 'with grouped_by_values' do let(:grouped_by) { 'region' } - let(:grouped_by_value) { 'europe' } + let(:grouped_by_values) { { 'region' => 'europe' } } it 'returns a list of events' do expect(event_store.events.count).to eq(3) diff --git a/spec/services/events/stores/postgres_store_spec.rb b/spec/services/events/stores/postgres_store_spec.rb index 81994fae9f2..0b3440a0fed 100644 --- a/spec/services/events/stores/postgres_store_spec.rb +++ b/spec/services/events/stores/postgres_store_spec.rb @@ -8,7 +8,7 @@ code:, subscription:, boundaries:, - filters: { group:, grouped_by:, grouped_by_value: }, + filters: { group:, grouped_by:, grouped_by_values: }, ) end @@ -31,7 +31,7 @@ let(:group) { nil } let(:grouped_by) { nil } - let(:grouped_by_value) { nil } + let(:grouped_by_values) { nil } let(:events) do events = [] @@ -51,7 +51,7 @@ if i.even? event.properties[group.key] = group.value if group - event.properties[grouped_by] = grouped_by_value if grouped_by_value + event.properties[grouped_by] = grouped_by_values[grouped_by] if grouped_by_values end event.save! @@ -77,9 +77,9 @@ end end - context 'with grouped_by_value' do + context 'with grouped_by_values' do let(:grouped_by) { 'region' } - let(:grouped_by_value) { 'europe' } + let(:grouped_by_values) { { 'region' => 'europe' } } it 'returns a list of events' do expect(event_store.events.count).to eq(3) diff --git a/spec/services/fees/create_pay_in_advance_service_spec.rb b/spec/services/fees/create_pay_in_advance_service_spec.rb index 6035f2b3ef0..91f951e29b5 100644 --- a/spec/services/fees/create_pay_in_advance_service_spec.rb +++ b/spec/services/fees/create_pay_in_advance_service_spec.rb @@ -268,6 +268,57 @@ end end + context 'when charge has a grouped_by property' do + let(:charge) do + create( + :standard_charge, + billable_metric:, + pay_in_advance: true, + properties: { 'grouped_by' => ['operator'], 'amount' => '100' }, + ) + end + + let(:event) do + create( + :event, + organization:, + external_subscription_id: subscription.external_id, + properties: { 'operator' => 'foo' }, + ) + end + + it 'creates a fee' do + result = fee_service.call + + aggregate_failures do + expect(result).to be_success + + expect(result.fees.count).to eq(1) + expect(result.fees.first).to have_attributes( + subscription:, + charge:, + amount_cents: 10, + amount_currency: 'EUR', + fee_type: 'charge', + pay_in_advance: true, + invoiceable: charge, + units: 9, + properties: Hash, + events_count: 1, + group: nil, + pay_in_advance_event_id: event.id, + unit_amount_cents: 1, + precise_unit_amount: 0.01111111111, + grouped_by: ['foo'], + + taxes_rate: 20.0, + taxes_amount_cents: 2, + ) + expect(result.fees.first.applied_taxes.count).to eq(1) + end + end + end + context 'when in estimate mode' do let(:estimate) { true } @@ -336,6 +387,7 @@ expect(cached_aggregation.current_aggregation).to eq(9) expect(cached_aggregation.max_aggregation).to eq(9) expect(cached_aggregation.max_aggregation_with_proration).to be_nil + expect(cached_aggregation.grouped_by).to eq([]) end end end