From 9ecf5f250416793cca3e7e6f113ee052367f055f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 13 Mar 2026 10:52:57 +0530 Subject: [PATCH] feat: add reporting_events_rollup table and model --- app/models/reporting_events_rollup.rb | 48 +++++ ...11145813_create_reporting_events_rollup.rb | 39 ++++ db/schema.rb | 16 ++ spec/factories/reporting_event_rollups.rb | 12 ++ spec/models/reporting_events_rollup_spec.rb | 182 ++++++++++++++++++ 5 files changed, 297 insertions(+) create mode 100644 app/models/reporting_events_rollup.rb create mode 100644 db/migrate/20260211145813_create_reporting_events_rollup.rb create mode 100644 spec/factories/reporting_event_rollups.rb create mode 100644 spec/models/reporting_events_rollup_spec.rb diff --git a/app/models/reporting_events_rollup.rb b/app/models/reporting_events_rollup.rb new file mode 100644 index 000000000..a9e345cba --- /dev/null +++ b/app/models/reporting_events_rollup.rb @@ -0,0 +1,48 @@ +# == Schema Information +# +# Table name: reporting_events_rollups +# +# id :bigint not null, primary key +# count :bigint default(0), not null +# date :date not null +# dimension_id :bigint not null +# dimension_type :string not null +# metric :string not null +# sum_value :float default(0.0), not null +# sum_value_business_hours :float default(0.0), not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# +# Indexes +# +# index_rollup_summary (account_id,dimension_type,date) +# index_rollup_timeseries (account_id,metric,date) +# index_rollup_unique_key (account_id,date,dimension_type,dimension_id,metric) UNIQUE +# + +class ReportingEventsRollup < ApplicationRecord + belongs_to :account + + # Store string values directly in the database for better readability and debugging + enum :dimension_type, %w[account agent inbox team].index_by(&:itself) + enum :metric, %w[ + resolutions_count + first_response + resolution_time + reply_time + bot_resolutions_count + bot_handoffs_count + ].index_by(&:itself) + + validates :account_id, presence: true + validates :date, presence: true + validates :dimension_type, presence: true + validates :dimension_id, presence: true + validates :metric, presence: true + validates :count, numericality: { greater_than_or_equal_to: 0 } + + scope :for_date_range, ->(start_date, end_date) { where(date: start_date..end_date) } + scope :for_dimension, ->(type, id) { where(dimension_type: type, dimension_id: id) } + scope :for_metric, ->(metric) { where(metric: metric) } +end diff --git a/db/migrate/20260211145813_create_reporting_events_rollup.rb b/db/migrate/20260211145813_create_reporting_events_rollup.rb new file mode 100644 index 000000000..3f1680cd0 --- /dev/null +++ b/db/migrate/20260211145813_create_reporting_events_rollup.rb @@ -0,0 +1,39 @@ +class CreateReportingEventsRollup < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + create_rollups_table + add_rollups_indexes + end + + private + + def create_rollups_table + create_table :reporting_events_rollups do |t| + t.integer :account_id, null: false + t.date :date, null: false + t.string :dimension_type, null: false + t.bigint :dimension_id, null: false + t.string :metric, null: false + t.bigint :count, default: 0, null: false + t.float :sum_value, default: 0.0, null: false + t.float :sum_value_business_hours, default: 0.0, null: false + + t.timestamps + end + end + + def add_rollups_indexes + add_index :reporting_events_rollups, + [:account_id, :date, :dimension_type, :dimension_id, :metric], + unique: true, name: 'index_rollup_unique_key', algorithm: :concurrently + + add_index :reporting_events_rollups, + [:account_id, :metric, :date], + name: 'index_rollup_timeseries', algorithm: :concurrently + + add_index :reporting_events_rollups, + [:account_id, :dimension_type, :date], + name: 'index_rollup_summary', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 4bb0ca3af..a0b03d475 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1124,6 +1124,22 @@ ActiveRecord::Schema[7.1].define(version: 2026_02_26_153427) do t.index ["user_id"], name: "index_reporting_events_on_user_id" end + create_table "reporting_events_rollups", force: :cascade do |t| + t.integer "account_id", null: false + t.date "date", null: false + t.string "dimension_type", null: false + t.bigint "dimension_id", null: false + t.string "metric", null: false + t.bigint "count", default: 0, null: false + t.float "sum_value", default: 0.0, null: false + t.float "sum_value_business_hours", default: 0.0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "date", "dimension_type", "dimension_id", "metric"], name: "index_rollup_unique_key", unique: true + t.index ["account_id", "dimension_type", "date"], name: "index_rollup_summary" + t.index ["account_id", "metric", "date"], name: "index_rollup_timeseries" + end + create_table "sla_events", force: :cascade do |t| t.bigint "applied_sla_id", null: false t.bigint "conversation_id", null: false diff --git a/spec/factories/reporting_event_rollups.rb b/spec/factories/reporting_event_rollups.rb new file mode 100644 index 000000000..5cd18a0e8 --- /dev/null +++ b/spec/factories/reporting_event_rollups.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :reporting_events_rollup do + account + date { Date.current } + dimension_type { 'account' } + dimension_id { 1 } + metric { 'first_response' } + count { 1 } + sum_value { 100.0 } + sum_value_business_hours { 50.0 } + end +end diff --git a/spec/models/reporting_events_rollup_spec.rb b/spec/models/reporting_events_rollup_spec.rb new file mode 100644 index 000000000..06d36c517 --- /dev/null +++ b/spec/models/reporting_events_rollup_spec.rb @@ -0,0 +1,182 @@ +require 'rails_helper' + +RSpec.describe ReportingEventsRollup do + describe 'associations' do + it { is_expected.to belong_to(:account) } + end + + describe 'enums' do + describe 'dimension_type enum' do + it 'stores dimension_type as string values' do + rollup = build(:reporting_events_rollup, dimension_type: 'account') + expect(rollup.dimension_type).to eq('account') + end + + it 'has account dimension type' do + rollup = build(:reporting_events_rollup, dimension_type: 'account') + expect(rollup.account?).to be true + end + + it 'has agent dimension type' do + rollup = build(:reporting_events_rollup, dimension_type: 'agent') + expect(rollup.agent?).to be true + end + + it 'has inbox dimension type' do + rollup = build(:reporting_events_rollup, dimension_type: 'inbox') + expect(rollup.inbox?).to be true + end + + it 'has team dimension type' do + rollup = build(:reporting_events_rollup, dimension_type: 'team') + expect(rollup.team?).to be true + end + end + + describe 'metric enum' do + it 'stores metric as string values' do + rollup = build(:reporting_events_rollup, metric: 'first_response') + expect(rollup.metric).to eq('first_response') + end + + it 'has resolutions_count metric' do + rollup = build(:reporting_events_rollup, metric: 'resolutions_count') + expect(rollup.resolutions_count?).to be true + end + + it 'has first_response metric' do + rollup = build(:reporting_events_rollup, metric: 'first_response') + expect(rollup.first_response?).to be true + end + + it 'has resolution_time metric' do + rollup = build(:reporting_events_rollup, metric: 'resolution_time') + expect(rollup.resolution_time?).to be true + end + + it 'has reply_time metric' do + rollup = build(:reporting_events_rollup, metric: 'reply_time') + expect(rollup.reply_time?).to be true + end + + it 'has bot_resolutions_count metric' do + rollup = build(:reporting_events_rollup, metric: 'bot_resolutions_count') + expect(rollup.bot_resolutions_count?).to be true + end + + it 'has bot_handoffs_count metric' do + rollup = build(:reporting_events_rollup, metric: 'bot_handoffs_count') + expect(rollup.bot_handoffs_count?).to be true + end + end + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:account_id) } + it { is_expected.to validate_presence_of(:date) } + it { is_expected.to validate_presence_of(:dimension_type) } + it { is_expected.to validate_presence_of(:dimension_id) } + it { is_expected.to validate_presence_of(:metric) } + it { is_expected.to validate_numericality_of(:count).is_greater_than_or_equal_to(0) } + end + + describe 'scopes' do + let(:account) { create(:account) } + let(:other_account) { create(:account) } + + describe '.for_date_range' do + it 'filters by date range' do + create(:reporting_events_rollup, account: account, date: '2026-02-08'.to_date) + create(:reporting_events_rollup, account: account, date: '2026-02-10'.to_date) + create(:reporting_events_rollup, account: account, date: '2026-02-12'.to_date) + + results = described_class.for_date_range('2026-02-10'.to_date, '2026-02-11'.to_date) + + expect(results.count).to eq(1) + expect(results.first.date).to eq('2026-02-10'.to_date) + end + + it 'returns empty array when no records match' do + create(:reporting_events_rollup, account: account, date: '2026-02-08'.to_date) + + results = described_class.for_date_range('2026-02-20'.to_date, '2026-02-25'.to_date) + + expect(results).to be_empty + end + end + + describe '.for_dimension' do + it 'filters by dimension type and id' do + create(:reporting_events_rollup, account: account, dimension_type: 'account', dimension_id: 1) + create(:reporting_events_rollup, account: account, dimension_type: 'agent', dimension_id: 1) + create(:reporting_events_rollup, account: account, dimension_type: 'agent', dimension_id: 2) + + results = described_class.for_dimension('agent', 1) + + expect(results.count).to eq(1) + expect(results.first.dimension_type).to eq('agent') + expect(results.first.dimension_id).to eq(1) + end + + it 'returns empty array when no records match' do + create(:reporting_events_rollup, account: account, dimension_type: 'account', dimension_id: 1) + + results = described_class.for_dimension('agent', 1) + + expect(results).to be_empty + end + end + + describe '.for_metric' do + it 'filters by metric' do + create(:reporting_events_rollup, account: account, metric: 'first_response', dimension_id: 1) + create(:reporting_events_rollup, account: account, metric: 'resolution_time', dimension_id: 2) + create(:reporting_events_rollup, account: account, metric: 'first_response', dimension_id: 3) + + results = described_class.for_metric('first_response') + + expect(results.count).to eq(2) + expect(results.all? { |r| r.metric == 'first_response' }).to be true + end + + it 'returns empty array when no records match' do + create(:reporting_events_rollup, account: account, metric: 'first_response') + + results = described_class.for_metric('resolution_time') + + expect(results).to be_empty + end + end + end + + describe 'database schema' do + let(:account) { create(:account) } + let(:rollup) do + create(:reporting_events_rollup, + account: account, + date: '2026-02-10'.to_date, + dimension_type: 'account', + metric: 'first_response') + end + + it 'has all required columns' do + expect(rollup).to have_attributes( + account_id: account.id, + date: '2026-02-10'.to_date, + dimension_type: 'account', + dimension_id: 1, + metric: 'first_response', + count: 1, + sum_value: 100.0, + sum_value_business_hours: 50.0 + ) + end + + it 'stores enum values as strings in database' do + rollup + db_record = described_class.find(rollup.id) + expect(db_record.dimension_type).to eq('account') + expect(db_record.metric).to eq('first_response') + end + end +end