diff --git a/app/services/events/stores/postgres/unique_count_query.rb b/app/services/events/stores/postgres/unique_count_query.rb index db2be5f94d8..4beb8b0514d 100644 --- a/app/services/events/stores/postgres/unique_count_query.rb +++ b/app/services/events/stores/postgres/unique_count_query.rb @@ -34,6 +34,35 @@ def query SQL end + def prorated_query + <<-SQL + #{events_cte_sql}, + event_values AS ( + SELECT + property, + operation_type, + timestamp + FROM ( + SELECT + timestamp, + property, + operation_type, + #{operation_value_sql} AS adjusted_value + FROM events_data + ORDER BY timestamp ASC + ) adjusted_event_values + WHERE adjusted_value != 0 -- adjusted_value = 0 does not impact the total + GROUP BY property, operation_type, timestamp + ) + + SELECT SUM(period_ratio) as aggregation + FROM ( + SELECT (#{period_ratio_sql}) AS period_ratio + FROM event_values + ) cumulated_ratios + SQL + end + # NOTE: Not used in production, only for debug purpose to check the computed values before aggregation # Returns an array of event's timestamp, property, operation type and operation value # Example: @@ -62,7 +91,7 @@ def breakdown_query attr_reader :store - delegate :events, :sanitized_property_name, to: :store + delegate :events, :charges_duration, :sanitized_property_name, to: :store def events_cte_sql # NOTE: Common table expression returning event's timestamp, property name and operation type. @@ -99,6 +128,21 @@ def operation_value_sql END SQL end + + def period_ratio_sql + <<-SQL + CASE WHEN operation_type = 'add' + THEN + -- NOTE: duration in seconds between current event and next one - using end of period as final boundaries + EXTRACT(EPOCH FROM LEAD(timestamp, 1, :to_datetime) OVER (PARTITION BY property ORDER BY timestamp) - timestamp) + / + -- NOTE: full duration of the period + #{charges_duration.days.to_i} + ELSE + 0 -- NOTE: duration was null so usage is null + END + SQL + end end end end diff --git a/app/services/events/stores/postgres_store.rb b/app/services/events/stores/postgres_store.rb index 37e33b16621..2083fc33d00 100644 --- a/app/services/events/stores/postgres_store.rb +++ b/app/services/events/stores/postgres_store.rb @@ -87,6 +87,21 @@ def unique_count_breakdown ).rows end + def prorated_unique_count + query = Events::Stores::Postgres::UniqueCountQuery.new(store: self) + sql = ActiveRecord::Base.sanitize_sql_for_conditions( + [ + query.prorated_query, + { + to_datetime: to_datetime.ceil, + }, + ], + ) + result = ActiveRecord::Base.connection.select_one(sql) + + result['aggregation'] + end + def max events.maximum("(#{sanitized_property_name})::numeric") end diff --git a/spec/services/events/stores/postgres_store_spec.rb b/spec/services/events/stores/postgres_store_spec.rb index 5848d2af257..03a1c519dc1 100644 --- a/spec/services/events/stores/postgres_store_spec.rb +++ b/spec/services/events/stores/postgres_store_spec.rb @@ -160,6 +160,38 @@ end end + describe '#prorated_unique_count' do + it 'returns the number of unique active event properties' do + create( + :event, + organization_id: organization.id, + external_subscription_id: subscription.external_id, + external_customer_id: customer.external_id, + code:, + timestamp: boundaries[:from_datetime] + 1.day, + properties: { billable_metric.field_name => 2 }, + ) + + create( + :event, + organization_id: organization.id, + external_subscription_id: subscription.external_id, + external_customer_id: customer.external_id, + code:, + timestamp: boundaries[:from_datetime] + 2.days, + properties: { + billable_metric.field_name => 2, + operation_type: 'remove', + }, + ) + + event_store.aggregation_property = billable_metric.field_name + + # NOTE: Events calculation: 16/31 + 1/31 + 14/31 + 13/31 + 12/31 + expect(event_store.prorated_unique_count.round(3)).to eq(1.806) + end + end + describe '#events_values' do it 'returns the value attached to each event' do event_store.aggregation_property = billable_metric.field_name