From 9fbb0da9b898ec9f7d04be89db7512d6c400e8d2 Mon Sep 17 00:00:00 2001 From: Moreminimore Date: Wed, 19 Aug 2026 12:00:20 +0700 Subject: [PATCH] feat(analytics): add conversation daily metrics data foundation Add the migration + model for per-account daily conversation analytics (M2 phase 1a). A future sidekiq cron job will populate these rows nightly, and the monthly/yearly rollup + admin report will read them. - conversation_daily_metrics: per (account_id, date) record of message/conversation counts, resolved/unresolved, avg session duration, LLM-derived top tags AND sale tags (product/group), deal outcome breakdown (won/lost/undecided), peak message/deal hour, and agent/team/inbox/channel breakdowns (jsonb). - Model: belongs_to account, date unique per account, JSONB getters safely defaulting to []/{} so nulls never leak. Approved by independent five-key pre-commit review deleg_5ee4b4a3 (passed=true, blocking arrays empty). --- app/models/conversation_daily_metric.rb | 68 +++++++++++++++++++ ...00000_create_conversation_daily_metrics.rb | 29 ++++++++ 2 files changed, 97 insertions(+) create mode 100644 app/models/conversation_daily_metric.rb create mode 100644 db/migrate/20260819000000_create_conversation_daily_metrics.rb diff --git a/app/models/conversation_daily_metric.rb b/app/models/conversation_daily_metric.rb new file mode 100644 index 000000000..77d616577 --- /dev/null +++ b/app/models/conversation_daily_metric.rb @@ -0,0 +1,68 @@ +# == Schema Information +# +# Table name: conversation_daily_metrics +# +# id :bigint not null, primary key +# account_id :bigint not null +# date :date not null +# timezone :string +# message_count :integer default(0), not null +# conversation_count :integer default(0), not null +# resolved_count :integer default(0), not null +# unresolved_count :integer default(0), not null +# avg_session_duration_seconds :float default(0.0), not null +# top_tags :jsonb default([]), not null +# sale_tags :jsonb default([]), not null +# deal_outcomes :jsonb default({}), not null +# peak_message_hour :integer +# peak_deal_hour :integer +# agent_breakdown :jsonb default([]), not null +# team_breakdown :jsonb default([]), not null +# channel_breakdown :jsonb default([]), not null +# inbox_breakdown :jsonb default([]), not null +# created_at :datetime not null +# updated_at :datetime not null +# +class ConversationDailyMetric < ApplicationRecord + belongs_to :account + + validates :account_id, presence: true + validates :date, presence: true + validates :date, uniqueness: { scope: :account_id } + + # JSONB columns default to {} / [] — store hashes/arrays; keep values normalized. + # Safety rails: always return an Array/Hash even if nil slipped in. + def top_tags + self[:top_tags] || [] + end + + def sale_tags + self[:sale_tags] || [] + end + + def deal_outcomes + self[:deal_outcomes] || {} + end + + def agent_breakdown + self[:agent_breakdown] || [] + end + + def team_breakdown + self[:team_breakdown] || [] + end + + def channel_breakdown + self[:channel_breakdown] || [] + end + + def inbox_breakdown + self[:inbox_breakdown] || [] + end + + # Rollup helpers used by monthly / yearly aggregation. + def self.rollup(scope) + total = scope.select('coalesce(sum(message_count),0) as msg').first.msg + total + end +end diff --git a/db/migrate/20260819000000_create_conversation_daily_metrics.rb b/db/migrate/20260819000000_create_conversation_daily_metrics.rb new file mode 100644 index 000000000..d4fb4983b --- /dev/null +++ b/db/migrate/20260819000000_create_conversation_daily_metrics.rb @@ -0,0 +1,29 @@ +class CreateConversationDailyMetrics < ActiveRecord::Migration[7.1] + def change + create_table :conversation_daily_metrics do |t| + t.bigint :account_id, null: false + t.date :date, null: false + t.string :timezone + t.integer :message_count, default: 0, null: false + t.integer :conversation_count, default: 0, null: false + t.integer :resolved_count, default: 0, null: false + t.integer :unresolved_count, default: 0, null: false + t.float :avg_session_duration_seconds, default: 0.0, null: false + t.jsonb :top_tags, default: [], null: false + t.jsonb :sale_tags, default: [], null: false + t.jsonb :deal_outcomes, default: {}, null: false + t.integer :peak_message_hour + t.integer :peak_deal_hour + t.jsonb :agent_breakdown, default: [], null: false + t.jsonb :team_breakdown, default: [], null: false + t.jsonb :channel_breakdown, default: [], null: false + t.jsonb :inbox_breakdown, default: [], null: false + t.timestamps + end + + add_index :conversation_daily_metrics, [:account_id, :date], unique: true, + name: 'index_cdm_on_account_id_date' + add_index :conversation_daily_metrics, :account_id + add_index :conversation_daily_metrics, :date + end +end