Compare commits

..
Author SHA1 Message Date
Sojan JoseandGitHub 3b1e387509 Merge branch 'develop' into chore/update-rails 2026-02-03 21:14:55 -08:00
Sojan Jose a7c8a7bc26 fix(specs): scope whatsapp cloud contact assertions 2026-02-03 07:47:50 -08:00
Sojan Jose b1e1448b8a fix(specs): avoid raw sql in contact inbox spec 2026-02-02 23:15:49 -08:00
Sojan Jose c837dd5cb7 fix(search): guard message reindex without searchkick 2026-02-02 23:15:37 -08:00
Sojan Jose dd31bfa572 fix(specs): stabilize bulk actions job specs 2026-02-02 23:02:07 -08:00
Sojan JoseandGitHub 97a53f987b Merge branch 'develop' into chore/update-rails 2026-02-02 16:31:59 -08:00
Sojan JoseandGitHub 5e2fc6e339 Merge branch 'develop' into chore/update-rails 2026-01-30 15:50:33 -08:00
Sojan Jose d94392a277 test(telegram): stabilize contact assertions 2026-01-25 23:44:00 -08:00
Sojan Jose f2ff78ee47 test(reports): stabilize builder specs 2026-01-25 16:36:56 -08:00
Sojan Jose a161fdd3e3 test(macros): simplify team setup in macro spec 2026-01-24 02:56:48 -08:00
Sojan Jose a7cd958087 test(macros): assert assignee id in macro execution 2026-01-24 02:51:25 -08:00
Sojan Jose b0784413e9 test(mailers): set smtp env in shared context 2026-01-24 01:52:26 -08:00
Sojan Jose 2e37ecacba fix(integrations): normalize linear issue params 2026-01-24 01:41:02 -08:00
Sojan Jose 195dde9fde docs: note .env handling for specs 2026-01-24 01:23:56 -08:00
Sojan Jose 0a96b31f7f fix(integrations): set hook_type only on create 2026-01-24 01:09:24 -08:00
Sojan Jose eadd831c95 test(reports): stabilize report specs 2026-01-24 00:57:08 -08:00
Sojan Jose 0dadaccb9e Merge remote-tracking branch 'origin/develop' into chore/update-rails 2026-01-23 23:02:16 -08:00
Sojan Jose 7d1f69632e style(integrations): satisfy rubocop hook guard 2026-01-22 22:21:25 -08:00
Sojan Jose 05a5d81ad2 fix(integrations): avoid readonly hook_type updates 2026-01-22 21:56:07 -08:00
Sojan Jose ab010ab71c revert(integrations): keep hook_type assignment behavior 2026-01-22 20:33:14 -08:00
Sojan JoseandGitHub 69fabf0efd Merge branch 'develop' into chore/update-rails 2026-01-22 18:48:54 -08:00
Sojan Jose 9e53743592 test: stabilize mailer and signup validation specs 2026-01-22 18:32:08 -08:00
Sojan Jose 52d403d37c chore(vite): align vite plugin versions 2026-01-22 17:45:02 -08:00
Sojan JoseandGitHub 9ad83d101e Merge branch 'develop' into chore/update-rails 2026-01-22 09:09:25 -08:00
Sojan Jose 6caa572c24 Revert "chore(vite): upgrade vite deps"
This reverts commit 0d0abf6808.
2026-01-22 09:08:00 -08:00
Sojan Jose 0d0abf6808 chore(vite): upgrade vite deps 2026-01-21 17:44:01 -08:00
Sojan Jose b167e24d2b merge: develop 2026-01-21 09:18:06 -08:00
Sojan Jose ffe323261c revert(config): drop config loader guard 2026-01-20 23:51:54 -08:00
Sojan Jose 657afe0ffe test(reports): avoid update_column in specs 2026-01-20 23:38:54 -08:00
Sojan Jose e244d0c56d test(reports): stabilize metrics specs 2026-01-20 23:18:41 -08:00
Sojan Jose 59bf85d993 revert: action cable broadcast guard 2026-01-20 20:47:52 -08:00
Sojan Jose c8bbd6bbd8 test(linear): accept params in spec 2026-01-20 20:46:05 -08:00
Sojan Jose a1a355bc90 chore(linear): revert params to_h 2026-01-20 20:34:11 -08:00
Sojan JoseandGitHub b05311a169 Merge branch 'develop' into chore/update-rails 2026-01-20 16:48:14 -08:00
Sojan Jose 77b286c41f Install libyaml build deps in Docker 2026-01-18 19:23:37 -08:00
Sojan Jose 3cf252a2fa Skip seeds in chatwoot_prepare for test 2026-01-16 15:59:22 -08:00
Sojan Jose 491eaa5bce Run chatwoot_prepare in test env on CircleCI 2026-01-16 14:33:40 -08:00
Sojan Jose 8e4483c722 Revert "Update commonmarker"
This reverts commit dd98bb398e.
2026-01-16 13:31:30 -08:00
Sojan Jose dd98bb398e Update commonmarker 2026-01-16 01:16:46 -08:00
Sojan Jose 7061c3dde0 Reduce rubocop diff 2026-01-16 01:09:07 -08:00
Sojan Jose 9ef476c6d8 Guard config loader during cache clear 2026-01-16 01:01:04 -08:00
Sojan Jose 88fc18617d Fix macro assignment spec 2026-01-16 00:40:14 -08:00
Sojan Jose f5a81964c3 Pass hash params to Linear processor 2026-01-16 00:33:18 -08:00
Sojan Jose 251ef4f8bb Fix RuboCop spec offenses 2026-01-16 00:26:21 -08:00
Sojan Jose 45533f6bf8 Guard ConfigLoader against re-entrance 2026-01-16 00:19:36 -08:00
Sojan Jose 1748c668cf Fix ActionCable broadcast and LeadSquared setup 2026-01-16 00:11:39 -08:00
Sojan Jose be752af4c5 Fix user conversations alias 2026-01-15 23:51:05 -08:00
Sojan Jose 68529278e0 Update vulnerable gems 2026-01-15 23:50:19 -08:00
Sojan Jose d27c78068c Fix serialize coder for otp backup codes 2026-01-15 23:45:16 -08:00
Sojan Jose 3eeab14136 Fix RuboCop config 2026-01-15 23:38:28 -08:00
Sojan Jose 5e06466f18 Merge develop into chore/update-rails 2026-01-15 23:29:19 -08:00
Sojan Jose a0e4ce20f0 chore: remove cosmetic blank line from conversation reply mailer
- Remove extra blank line after include statement
- Another Layout/EmptyLineAfterGuardClause cosmetic change eliminated
- Reduce PR to 13 files by removing non-functional change
2025-08-14 14:30:51 +02:00
Sojan Jose ac1b6d070e chore: remove cosmetic blank line from message.rb
- Remove extra blank line after include statement
- This was a Layout/EmptyLineAfterGuardClause cosmetic change
- Reduce PR to 14 files by eliminating another non-functional change
2025-08-14 14:25:52 +02:00
Sojan Jose 54bd800a82 fix: update enum syntax for Rails 7.2 compatibility
- Change enum availability from keyword args to positional args
- Fix deprecation warning: 'Defining enums with keyword arguments is deprecated'
- Required for Rails 7.2.2.2 enum syntax changes
- Application now starts successfully with overmind
2025-08-14 14:20:21 +02:00
Sojan Jose 3c67d93426 chore: update sidekiq-cron and revert temporary compatibility fix
- Update sidekiq-cron from 1.12.0 to 2.3.1 with Rails 7.2 compatibility
- Update sidekiq from 7.3.1 to 7.3.9
- Restore require 'sidekiq/cron/web' as compatibility is now built-in
- Remove temporary FIXME workaround comments
2025-08-14 14:15:02 +02:00
Sojan Jose 8ed8e809e3 chore: remove remaining blank lines after include statements
- Remove extra blank lines added by Layout/EmptyLineAfterGuardClause rule
- Reduce PR to final 16 files with only functional Rails upgrade changes
2025-08-14 14:11:35 +02:00
Sojan Jose c2e095adba chore: revert memoization patterns and redundant default values
- Disable Style/MemoizeInstanceVariable and Style/RedundantAssignment rules
- Revert all instance_variable_defined? patterns back to ||= memoization
- Restore || 0 and || 1 default values that were removed by RuboCop
- Restore ternary operator formatting
- Reduce PR from 35 to 18 files by removing cosmetic style changes
2025-08-14 14:07:35 +02:00
Sojan Jose d24b513028 chore: revert final cosmetic Performance optimization
- Disable Performance/MapHash rule
- Revert .pluck(:id) back to .map { |tool| tool[:id] }
- Reduce PR to 35 files focused on functional Rails upgrade changes
2025-08-14 14:01:28 +02:00
Sojan Jose 87d703ad3a fix: correct RuboCop rule names in config
- Fix RSpec/RedundantTypeSpec -> RSpec/DescribeClass
- Fix Performance/MapPluck -> Performance/MapCompact
2025-08-14 13:56:58 +02:00
Sojan Jose fa58046c68 chore: revert remaining RuboCop cosmetic changes to minimize PR size
- Disable Performance/Count, RSpec/PredicateMatcher, RSpec/RedundantTypeSpec, Performance/MapPluck rules
- Revert .count.zero?/.none?, .count==1/.one?, .count.positive?/.any? changes
- Revert be_invalid/not_to be_valid predicate matcher changes
- Revert type: :helper/:model removals from RSpec describe blocks
- Revert .pluck()/.map{} performance optimizations
- Revert enum syntax and alias_attribute style changes
- Focus PR on functional Rails upgrade changes only
2025-08-14 13:56:15 +02:00
Sojan Jose 965c316b77 chore: revert more RuboCop cosmetic changes to reduce PR size
Reverted Layout/EmptyLineAfterGuardClause fixes in:
- 29+ controller files
- Multiple service, worker, and mailbox files
- Several spec files
- Enterprise module files

This keeps the PR focused on functional Rails 7.2 upgrade changes only.
2025-08-14 13:33:49 +02:00
Sojan Jose 9d0e2bc258 chore: disable RuboCop rules to keep Rails upgrade PR focused
- Disable Layout/EmptyLineAfterGuardClause
- Disable Performance/Count
- Fix Naming/PredicateMethod -> Naming/PredicateName
- Revert cosmetic RuboCop fixes to reduce PR size
2025-08-14 13:30:39 +02:00
Sojan Jose 00ac2519a2 chore: upgrade Vite compatibility and related dependencies
Fixes vite-plugin-ruby compatibility issue by upgrading:
- vite_rails 3.0.17 -> 3.0.19
- vite_ruby 3.8.0 -> 3.9.2

This resolves the Line service spec test failures that were caused by the Vite compatibility error.
2025-08-14 12:18:05 +02:00
Sojan Jose e59784dccc chore: update gem dependencies after merging develop
Updates Rails from 7.1.5.2 to 7.2.2.2 and resolve dependency conflicts
including devise-secure_password 2.0.1 to 2.1.0
2025-08-14 12:13:49 +02:00
Sojan Jose 807a21daca Merge branch 'develop' into chore/update-rails 2025-08-14 12:05:42 +02:00
Sojan Jose 4e9bdebe16 chore: rubocop fixes 2025-08-13 21:25:52 +02:00
Sojan JoseandGitHub 3ecd14e3ec Merge branch 'develop' into chore/update-rails 2025-08-13 17:32:10 +02:00
Sojan Jose 9638163b6f fix: resolve RuboCop violations
- Auto-fixed 143 code style violations using rubocop -A
- Fixed RuboCop configuration: moved rubocop-rspec_rails to plugins
- Removed redundant || 0 patterns after .to_i calls
- Fixed RSpec describe class format
- Added remaining naming violations to exclude list for future cleanup
- Reduced violations from 100 to 0 offenses
2025-08-13 17:27:34 +02:00
Sojan Jose 1db6c7add3 Merge develop into chore/update-rails 2025-08-12 16:17:41 +02:00
Sojan JoseandGitHub 35760ba464 Merge branch 'develop' into chore/update-rails 2025-07-18 01:11:01 -07:00
Sojan c2d20abce3 chore: gem updates 2025-06-29 01:30:11 -07:00
Sojan JoseandGitHub 06cfdf80d7 Merge branch 'develop' into chore/update-rails 2025-06-29 01:03:18 -07:00
Sojan JoseandGitHub b9fa8fe038 Merge branch 'develop' into chore/update-rails 2025-06-17 19:08:20 -04:00
Sojan JoseandGitHub 1cf5bd50a6 Merge branch 'develop' into chore/update-rails 2025-06-10 15:55:15 -04:00
Sojan JoseandGitHub e21ba3a0b5 Merge branch 'develop' into chore/update-rails 2025-05-28 14:47:11 -06:00
Shivam MishraandGitHub e4e10caf0d Merge branch 'develop' into chore/update-rails 2025-05-28 08:56:23 +05:30
Sojan JoseandGitHub d4e3320fdf Merge branch 'develop' into chore/update-rails 2025-05-27 16:52:33 -07:00
Sojan 0edcca3435 chore: rubocop fixes 2025-05-21 20:36:09 -07:00
Sojan e833b67caa chore: fixes 2025-05-21 20:24:07 -07:00
Sojan JoseandGitHub 01c7252a99 Merge branch 'develop' into chore/update-rails 2025-05-21 20:21:15 -07:00
Sojan 9efd8b761a chore: fixes 2025-05-21 02:02:05 -07:00
Sojan 8764e463f4 chore: fixes 2025-05-21 01:37:33 -07:00
Sojan 8316c98d12 chore: pnpm lock fix 2025-05-21 01:31:28 -07:00
Sojan eecd0a1fc7 Merge branch 'develop' into chore/update-rails
# Conflicts:
#	Gemfile.lock
#	package.json
#	pnpm-lock.yaml
2025-05-21 01:29:23 -07:00
Sojan 23afd1bc3f chore: fixes 2025-04-18 00:31:15 -07:00
Sojan 38f66afed9 chore: update gems 2025-04-18 00:27:23 -07:00
Sojan 1d4047a2e2 Merge branch 'develop' into chore/update-rails
# Conflicts:
#	Gemfile.lock
#	package.json
#	pnpm-lock.yaml
2025-04-18 00:23:26 -07:00
Sojan JoseandGitHub 8cd45e6fdd Merge branch 'develop' into chore/update-rails 2025-03-18 01:29:19 -07:00
Sojan JoseandGitHub 458a374a8d Merge branch 'develop' into chore/update-rails 2025-03-13 03:01:02 -07:00
Sojan ec614e17a2 chore: another fix 2025-03-12 01:59:05 -07:00
Sojan f1f10cea88 chore: spec fixes 2025-03-12 01:02:13 -07:00
Sojan baeb7826cf chore: fixes 2025-03-12 00:48:44 -07:00
Sojan 533ca3e919 chore: fixes 2025-03-12 00:28:52 -07:00
Sojan e23f8ca0c2 chore: fix spec 2025-03-12 00:25:08 -07:00
Sojan 099caa495a chore: fix security issue 2025-03-12 00:13:00 -07:00
Sojan JoseandGitHub 2043148305 Merge branch 'develop' into chore/update-rails 2025-03-12 00:05:01 -07:00
Sojan JoseandGitHub 35ca2d2acd Merge branch 'develop' into chore/update-rails 2025-03-07 15:11:14 -08:00
Sojan 204f28d301 chore: update 2025-03-07 15:05:12 -08:00
Sojan da995000e1 chore: rails 7.2.2 2025-03-07 01:44:50 -08:00
Sojan 478d84b8a1 chore: fix rubocop 2025-03-07 00:15:26 -08:00
Sojan 19d5786cf4 chore: fixes 2025-03-06 20:35:11 -08:00
Sojan JoseandGitHub 10ed3c374e Merge branch 'develop' into chore/update-rails 2025-03-06 20:02:27 -08:00
Sojan a6325b0ce9 chore: swagger build 2025-03-06 19:09:07 -08:00
Sojan 9e3a79303e chore: fixes 2025-03-06 18:53:34 -08:00
Sojan ba1ef0873f chore: Change version 2025-03-06 18:33:11 -08:00
Sojan JoseandGitHub a5addd7549 Merge branch 'develop' into chore/update-rails 2025-03-06 16:12:07 -08:00
Sojan 9ef87ffd19 chore: Upgrade Rails to 7.1.5.1 and update Gemfile dependencies 2025-03-06 16:10:39 -08:00
51 changed files with 415 additions and 1119 deletions
+3 -3
View File
@@ -279,10 +279,10 @@ jobs:
echo -en "\nINSTALLATION_ENV=circleci" >> ".env"
echo -en "\nOPENSEARCH_URL=http://localhost:9200" >> ".env"
# Database setup
# Database setup (match GitHub Actions flow)
- run:
name: Run DB migrations
command: bundle exec rails db:chatwoot_prepare
name: Create database + load schema
command: bundle exec rake db:create db:schema:load
# Run backend tests (parallelized)
- run:
+7
View File
@@ -2,6 +2,7 @@ plugins:
- rubocop-performance
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
- rubocop-factory_bot
require:
@@ -203,6 +204,12 @@ RSpec/MultipleExpectations:
RSpec/MultipleMemoizedHelpers:
Max: 14
RSpecRails/InferredSpecType:
Enabled: false
RSpecRails/NegationBeValid:
Enabled: false
# custom rules
UseFromEmail:
Enabled: true
+1
View File
@@ -13,6 +13,7 @@
- **Ruby Version**: Manage Ruby via `rbenv` and install the version listed in `.ruby-version` (e.g., `rbenv install $(cat .ruby-version)`)
- **rbenv setup**: Before running any `bundle` or `rspec` commands, init rbenv in your shell (`eval "$(rbenv init -)"`) so the correct Ruby/Bundler versions are used
- Always prefer `bundle exec` for Ruby CLI tasks (rspec, rake, rubocop, etc.)
- **Test env**: Specs should run without `.env`. If present, temporarily rename it (e.g., `.env` -> `.env.bak`) while running specs and restore afterward.
## Code Style
+13 -10
View File
@@ -3,8 +3,10 @@ source 'https://rubygems.org'
ruby '3.4.4'
##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors'
gem 'rails', '~> 7.1'
gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 7.2.0'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -31,7 +33,9 @@ gem 'haikunator'
# Template parsing safely
gem 'liquid'
# Parse Markdown to HTML
gem 'commonmarker'
# ref: https://github.com/gjtorikian/commonmarker/issues/358
# can upgrade one this issue is fixed
gem 'commonmarker', '~> 0.23.11'
# Validate Data against JSON Schema
gem 'json_schemer'
# used in swagger build
@@ -49,9 +53,7 @@ gem 'csv-safe'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
# original gem isn't maintained actively
# we wanted updated version of faraday which is a dependency for slack-ruby-client
gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby', branch: 'chatwoot', require: false
gem 'azure-blob', require: false
gem 'google-cloud-storage', '>= 1.48.0', require: false
gem 'image_processing'
@@ -89,9 +91,9 @@ gem 'jwt'
gem 'pundit'
# super admin
gem 'administrate', '>= 0.20.1'
gem 'administrate-field-active_storage', '>= 1.0.3'
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
gem 'administrate'
gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -131,7 +133,7 @@ gem 'sentry-sidekiq', '>= 5.19.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.3.1'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0'
gem 'sidekiq-cron', '>= 2.3.1'
# for sidekiq healthcheck
gem 'sidekiq_alive'
@@ -262,6 +264,7 @@ group :development, :test do
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'rubocop-rspec_rails', require: false
gem 'rubocop-factory_bot', require: false
gem 'seed_dump'
gem 'shoulda-matchers'
+193 -194
View File
@@ -1,110 +1,89 @@
GIT
remote: https://github.com/chatwoot/azure-storage-ruby
revision: 9957cf899d33a285b5dfe15bdb875292398e392b
branch: chatwoot
specs:
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
nokogiri (~> 1, >= 1.10.8)
azure-storage-common (2.0.4)
faraday (~> 2.0)
faraday-follow_redirects (~> 0.3.0)
faraday-net_http_persistent (~> 2.0)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
GIT
remote: https://github.com/chatwoot/devise-secure_password
revision: adcc85fe1babfe40feae73dbcae64d14fff86e69
revision: 479987594b576dbf23aed8dbd962e78342bc683c
branch: chatwoot
specs:
devise-secure_password (2.0.1)
devise-secure_password (2.1.0)
devise (>= 4.0.0, < 5.0.0)
railties (>= 5.0.0, < 8.0.0)
GEM
remote: https://rubygems.org/
specs:
actioncable (7.1.5.2)
actionpack (= 7.1.5.2)
activesupport (= 7.1.5.2)
actioncable (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.5.2)
actionpack (= 7.1.5.2)
activejob (= 7.1.5.2)
activerecord (= 7.1.5.2)
activestorage (= 7.1.5.2)
activesupport (= 7.1.5.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.5.2)
actionpack (= 7.1.5.2)
actionview (= 7.1.5.2)
activejob (= 7.1.5.2)
activesupport (= 7.1.5.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
actionmailbox (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
mail (>= 2.8.0)
actionmailer (7.2.2.2)
actionpack (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activesupport (= 7.2.2.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.1.5.2)
actionview (= 7.1.5.2)
activesupport (= 7.1.5.2)
actionpack (7.2.2.2)
actionview (= 7.2.2.2)
activesupport (= 7.2.2.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.5.2)
actionpack (= 7.1.5.2)
activerecord (= 7.1.5.2)
activestorage (= 7.1.5.2)
activesupport (= 7.1.5.2)
useragent (~> 0.16)
actiontext (7.2.2.2)
actionpack (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.5.2)
activesupport (= 7.1.5.2)
actionview (7.2.2.2)
activesupport (= 7.2.2.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
active_record_query_trace (1.8)
activejob (7.1.5.2)
activesupport (= 7.1.5.2)
activejob (7.2.2.2)
activesupport (= 7.2.2.2)
globalid (>= 0.3.6)
activemodel (7.1.5.2)
activesupport (= 7.1.5.2)
activerecord (7.1.5.2)
activemodel (= 7.1.5.2)
activesupport (= 7.1.5.2)
activemodel (7.2.2.2)
activesupport (= 7.2.2.2)
activerecord (7.2.2.2)
activemodel (= 7.2.2.2)
activesupport (= 7.2.2.2)
timeout (>= 0.4.0)
activerecord-import (2.1.0)
activerecord (>= 4.2)
activestorage (7.1.5.2)
actionpack (= 7.1.5.2)
activejob (= 7.1.5.2)
activerecord (= 7.1.5.2)
activesupport (= 7.1.5.2)
activestorage (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activesupport (= 7.2.2.2)
marcel (~> 1.0)
activesupport (7.1.5.2)
activesupport (7.2.2.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
mutex_m
securerandom (>= 0.3)
tzinfo (~> 2.0)
tzinfo (~> 2.0, >= 2.0.5)
acts-as-taggable-on (12.0.0)
activerecord (>= 7.1, < 8.1)
zeitwerk (>= 2.4, < 3.0)
@@ -121,10 +100,10 @@ GEM
administrate-field-active_storage (1.0.3)
administrate (>= 0.2.2)
rails (>= 7.0)
administrate-field-belongs_to_search (0.9.0)
administrate-field-belongs_to_search (0.10.0)
administrate (>= 0.3, < 1.0)
jbuilder (~> 2)
rails (>= 4.2, < 7.2)
rails (>= 4.2, < 8.0)
selectize-rails (~> 0.6)
ai-agents (0.7.0)
ruby_llm (~> 1.8.2)
@@ -141,8 +120,8 @@ GEM
aws-sdk-s3 (~> 1, >= 1.123.0)
aws-sdk-sns (~> 1, >= 1.61.0)
aws-eventstream (1.4.0)
aws-partitions (1.1198.0)
aws-sdk-core (3.240.0)
aws-partitions (1.1205.0)
aws-sdk-core (3.241.3)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -150,11 +129,11 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sdk-kms (1.120.0)
aws-sdk-core (~> 3, >= 3.241.3)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.208.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-s3 (1.211.0)
aws-sdk-core (~> 3, >= 3.241.3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-sns (1.70.0)
@@ -162,20 +141,22 @@ GEM
aws-sigv4 (~> 1.1)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
azure-blob (0.5.9.1)
rexml
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.4.1)
bigdecimal (3.2.2)
benchmark (0.5.0)
bigdecimal (4.0.1)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.1)
browser (5.3.1)
builder (3.3.0)
bullet (8.0.7)
bullet (7.2.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundle-audit (0.1.0)
@@ -184,17 +165,21 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
cgi (0.5.1)
childprocess (5.1.0)
logger (~> 1.5)
climate_control (1.2.0)
coderay (1.1.3)
commonmarker (0.23.10)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
commonmarker (0.23.11)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
cronex (0.15.0)
tzinfo
unicode (>= 0.4.4.5)
csv (3.3.0)
csv-safe (3.3.1)
csv (~> 3.0)
@@ -204,14 +189,15 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
datadog (2.19.0)
datadog-ruby_core_source (~> 3.4, >= 3.4.1)
libdatadog (~> 18.1.0.1.0)
libddwaf (~> 1.24.1.0.3)
datadog (2.25.0)
cgi
datadog-ruby_core_source (~> 3.5, >= 3.5.0)
libdatadog (~> 24.0.1.1.0)
libddwaf (~> 1.30.0.0.0)
logger
msgpack
datadog-ruby_core_source (3.4.1)
date (3.4.1)
datadog-ruby_core_source (3.5.1)
date (3.5.1)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
@@ -222,10 +208,10 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (6.1.0)
activesupport (>= 7.0, < 8.1)
devise (~> 4.0)
railties (>= 7.0, < 8.1)
devise-two-factor (6.3.0)
activesupport (>= 7.0, < 8.2)
devise (>= 4.0, < 5.0)
railties (>= 7.0, < 8.2)
rotp (~> 6.0)
devise_token_auth (1.2.5)
bcrypt (~> 3.0)
@@ -244,31 +230,31 @@ GEM
down (5.4.0)
addressable (~> 2.8)
drb (2.2.3)
dry-cli (1.1.0)
dry-cli (1.4.1)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.1.0)
dry-core (1.2.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-inflector (1.3.1)
dry-initializer (3.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-schema (1.14.1)
dry-schema (1.15.0)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.1)
dry-initializer (~> 3.2)
dry-logic (~> 1.5)
dry-logic (~> 1.6)
dry-types (~> 1.8)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
dry-types (1.9.0)
bigdecimal (>= 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
@@ -282,8 +268,9 @@ GEM
ruby2_keywords
email-provider-info (0.0.1)
email_reply_trimmer (0.1.13)
erubi (1.13.0)
et-orbi (1.2.11)
erb (6.0.1)
erubi (1.13.1)
et-orbi (1.3.0)
tzinfo
event_stream_parser (1.0.0)
execjs (2.8.1)
@@ -297,34 +284,27 @@ GEM
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (2.13.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-mashify (1.0.0)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-mashify (1.0.2)
faraday (~> 2.0)
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
faraday-net_http (3.1.0)
net-http
faraday-retry (2.2.1)
faraday (~> 2.0)
faraday_middleware-aws-sigv4 (1.0.1)
aws-sigv4 (~> 1.0)
faraday (>= 2.0, < 3)
fast-mcp (1.5.0)
fast-mcp (1.6.0)
addressable (~> 2.8)
base64
dry-schema (~> 1.14)
json (~> 2.0)
mime-types (~> 3.4)
rack (~> 3.1)
rack (>= 2.0, < 4.0)
fcm (1.0.8)
faraday (>= 1.0.0, < 3.0)
googleauth (~> 1)
@@ -441,19 +421,21 @@ GEM
http-cookie (1.0.5)
domain_name (~> 0.5)
http-form_data (2.3.0)
httparty (0.24.0)
httparty (0.24.2)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.14.7)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.6.0)
irb (1.7.2)
reline (>= 0.3.6)
io-console (0.8.2)
irb (1.16.0)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
iso-639 (0.3.8)
csv
jbuilder (2.11.5)
@@ -464,7 +446,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.13.2)
json (2.12.0)
json_refs (0.1.8)
hana
json_schemer (0.2.24)
@@ -479,7 +461,7 @@ GEM
judoscale-sidekiq (1.8.2)
judoscale-ruby (= 1.8.2)
sidekiq (>= 5.0)
jwt (2.10.1)
jwt (2.8.1)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
@@ -506,15 +488,15 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
libdatadog (18.1.0.1.0)
libdatadog (18.1.0.1.0-x86_64-linux)
libddwaf (1.24.1.0.3)
libdatadog (24.0.1.1.0)
libdatadog (24.0.1.1.0-x86_64-linux)
libddwaf (1.30.0.0.0)
ffi (~> 1.0)
libddwaf (1.24.1.0.3-arm64-darwin)
libddwaf (1.30.0.0.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.24.1.0.3-x86_64-darwin)
libddwaf (1.30.0.0.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.24.1.0.3-x86_64-linux)
libddwaf (1.30.0.0.0-x86_64-linux)
ffi (~> 1.0)
line-bot-api (1.28.0)
lint_roller (1.1.0)
@@ -531,7 +513,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.23.1)
loofah (2.25.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -541,9 +523,9 @@ GEM
net-smtp
marcel (1.0.4)
maxminddb (0.1.22)
meta_request (0.8.5)
meta_request (0.8.3)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 9)
railties (>= 3.0.0, < 8)
method_source (1.1.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
@@ -551,21 +533,19 @@ GEM
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.5)
minitest (5.27.0)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.8.0)
multi_json (1.15.0)
multi_xml (0.8.0)
multi_xml (0.8.1)
bigdecimal (>= 3.1, < 5)
multipart-post (2.3.0)
mutex_m (0.3.0)
neighbor (0.2.3)
activerecord (>= 5.2)
net-http (0.6.0)
net-http (0.4.1)
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.20)
date
net-protocol
@@ -582,14 +562,14 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.18.9)
nokogiri (1.19.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.9-arm64-darwin)
nokogiri (1.19.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-darwin)
nokogiri (1.19.0-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@@ -607,9 +587,8 @@ GEM
oj (3.16.10)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.4)
omniauth (2.1.2)
hashie (>= 3.4.6)
logger
rack (>= 2.2.3)
rack-protection
omniauth-google-oauth2 (1.1.3)
@@ -661,6 +640,9 @@ GEM
activerecord (>= 5.2)
activesupport (>= 5.2)
pgvector (0.1.1)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
procore-sift (1.0.0)
activerecord (>= 6.1)
@@ -669,6 +651,9 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
psych (5.3.1)
date
stringio
public_suffix (6.0.2)
puma (6.4.3)
nio4r (~> 2.0)
@@ -676,7 +661,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.3)
rack (3.1.19)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
@@ -694,53 +679,57 @@ GEM
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack-test (2.2.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rackup (2.2.1)
rackup (2.3.1)
rack (>= 3)
rails (7.1.5.2)
actioncable (= 7.1.5.2)
actionmailbox (= 7.1.5.2)
actionmailer (= 7.1.5.2)
actionpack (= 7.1.5.2)
actiontext (= 7.1.5.2)
actionview (= 7.1.5.2)
activejob (= 7.1.5.2)
activemodel (= 7.1.5.2)
activerecord (= 7.1.5.2)
activestorage (= 7.1.5.2)
activesupport (= 7.1.5.2)
rails (7.2.2.2)
actioncable (= 7.2.2.2)
actionmailbox (= 7.2.2.2)
actionmailer (= 7.2.2.2)
actionpack (= 7.2.2.2)
actiontext (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activemodel (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
bundler (>= 1.15.0)
railties (= 7.1.5.2)
rails-dom-testing (2.2.0)
railties (= 7.2.2.2)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.1)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (7.1.5.2)
actionpack (= 7.1.5.2)
activesupport (= 7.1.5.2)
irb
railties (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rake (13.3.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rdoc (7.1.0)
erb
psych (>= 4.0.0)
tsort
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.22.2)
redis-client (0.25.2)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
regexp_parser (2.10.0)
reline (0.3.6)
reline (0.6.3)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -810,6 +799,10 @@ GEM
rubocop-rspec (3.6.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
ruby-openai (7.3.1)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
@@ -832,9 +825,8 @@ GEM
faraday-net_http (>= 1)
faraday-retry (>= 1)
marcel (~> 1.0)
ruby_llm-schema (~> 0.2.1)
zeitwerk (~> 2)
ruby_llm-schema (0.2.5)
ruby_llm-schema (0.1.0)
ruby_parser (3.20.0)
sexp_processor (~> 4.16)
sass (3.7.4)
@@ -854,9 +846,8 @@ GEM
parser
scss_lint (0.60.0)
sass (~> 3.5, >= 3.5.5)
searchkick (5.5.2)
activemodel (>= 7.1)
hashie
searchkick (6.0.3)
activemodel (>= 7.2)
securerandom (0.4.1)
seed_dump (3.3.1)
activerecord (>= 4)
@@ -885,16 +876,17 @@ GEM
zeitwerk (~> 2.5)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.3.1)
concurrent-ruby (< 2)
sidekiq (7.3.9)
base64
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
sidekiq-cron (2.3.1)
cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1)
sidekiq (>= 6)
sidekiq (>= 6.5.0)
sidekiq_alive (2.5.0)
gserver (~> 0.0.1)
sidekiq (>= 5, < 9)
@@ -934,12 +926,13 @@ GEM
squasher (0.7.2)
stackprof (0.2.25)
statsd-ruby (1.5.0)
stripe (18.0.1)
stringio (3.2.0)
stripe (18.1.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.4.0)
tidewave (0.2.0)
fast-mcp (~> 1.5.0)
thor (1.5.0)
tidewave (0.4.1)
fast-mcp (~> 1.6.0)
rack (>= 2.0)
rails (>= 7.1.0)
tilt (2.3.0)
@@ -948,7 +941,8 @@ GEM
i18n
timeout (0.4.3)
trailblazer-option (0.1.2)
twilio-ruby (7.6.0)
tsort (0.2.0)
twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0)
jwt (>= 1.5, < 3.0)
nokogiri (>= 1.6, < 2.0)
@@ -964,21 +958,25 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode (0.4.4.5)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.17.0)
uri (1.0.4)
uri (1.1.1)
uri_template (0.7.0)
useragent (0.16.11)
valid_email2 (5.2.6)
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.4)
vite_rails (3.0.17)
railties (>= 5.1, < 8)
vite_rails (3.0.20)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.8.0)
vite_ruby (3.9.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
mutex_m
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
warden (1.2.9)
@@ -995,7 +993,7 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@@ -1003,7 +1001,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.17)
zeitwerk (2.7.4)
PLATFORMS
arm64-darwin-20
@@ -1020,16 +1018,16 @@ DEPENDENCIES
active_record_query_trace
activerecord-import
acts-as-taggable-on
administrate (>= 0.20.1)
administrate-field-active_storage (>= 1.0.3)
administrate-field-belongs_to_search (>= 0.9.0)
administrate
administrate-field-active_storage
administrate-field-belongs_to_search
ai-agents (>= 0.7.0)
annotaterb
attr_extras
audited (~> 5.4, >= 5.4.1)
aws-actionmailbox-ses (~> 0)
aws-sdk-s3
azure-storage-blob!
azure-blob
barnes
bootsnap
brakeman
@@ -1038,7 +1036,7 @@ DEPENDENCIES
bundle-audit
byebug
climate_control
commonmarker
commonmarker (~> 0.23.11)
csv-safe
database_cleaner
datadog (~> 2.0)
@@ -1108,10 +1106,10 @@ DEPENDENCIES
puma
pundit
rack-attack (>= 6.7.0)
rack-cors (= 2.0.0)
rack-cors
rack-mini-profiler (>= 3.2.0)
rack-timeout
rails (~> 7.1)
rails (~> 7.2.0)
redis
redis-namespace
responders (>= 3.1.1)
@@ -1124,6 +1122,7 @@ DEPENDENCIES
rubocop-performance
rubocop-rails
rubocop-rspec
rubocop-rspec_rails
ruby-openai
ruby_llm (>= 1.8.2)
ruby_llm-schema
@@ -1137,7 +1136,7 @@ DEPENDENCIES
shopify_api
shoulda-matchers
sidekiq (>= 7.3.1)
sidekiq-cron (>= 1.12.0)
sidekiq-cron (>= 2.3.1)
sidekiq_alive
simplecov (>= 0.21)
simplecov_json_formatter
@@ -1,16 +1,14 @@
class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder
HIDEABLE_METRICS = %w[incoming_messages_count outgoing_messages_count reply_time].freeze
def summary
{
conversations_count: count('conversations_count'),
incoming_messages_count: count_unless_hidden('incoming_messages_count'),
outgoing_messages_count: count_unless_hidden('outgoing_messages_count'),
incoming_messages_count: count('incoming_messages_count'),
outgoing_messages_count: count('outgoing_messages_count'),
avg_first_response_time: count('avg_first_response_time'),
avg_resolution_time: count('avg_resolution_time'),
resolutions_count: count('resolutions_count'),
reply_time: count_unless_hidden('reply_time')
}.compact
reply_time: count('reply_time')
}
end
def bot_summary
@@ -26,18 +24,6 @@ class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::Ba
builder_class(metric).new(account, builder_params(metric)).aggregate_value
end
def count_unless_hidden(metric)
return nil if metric_hidden?(metric)
count(metric)
end
def metric_hidden?(metric)
return false unless HIDEABLE_METRICS.include?(metric)
(params[:hidden_metrics] || []).include?(metric)
end
def builder_params(metric)
params.merge({ metric: metric })
end
@@ -1,79 +0,0 @@
class V2::Reports::OutgoingMessagesCountBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
def build
send("build_by_#{params[:group_by]}")
end
private
def base_messages
account.messages.outgoing.unscope(:order).where(created_at: range)
end
def build_by_agent
counts = base_messages
.where(sender_type: 'User')
.where.not(sender_id: nil)
.group(:sender_id)
.count
user_names = account.users.where(id: counts.keys).index_by(&:id)
counts.map do |user_id, count|
user = user_names[user_id]
{ id: user_id, name: user&.name, outgoing_messages_count: count }
end
end
def build_by_team
counts = base_messages
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
.where.not(conversations: { team_id: nil })
.group('conversations.team_id')
.count
team_names = account.teams.where(id: counts.keys).index_by(&:id)
counts.map do |team_id, count|
team = team_names[team_id]
{ id: team_id, name: team&.name, outgoing_messages_count: count }
end
end
def build_by_inbox
counts = base_messages
.group(:inbox_id)
.count
inbox_names = account.inboxes.where(id: counts.keys).index_by(&:id)
counts.map do |inbox_id, count|
inbox = inbox_names[inbox_id]
{ id: inbox_id, name: inbox&.name, outgoing_messages_count: count }
end
end
def build_by_label
counts = base_messages
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
.joins("INNER JOIN taggings ON taggings.taggable_id = conversations.id
AND taggings.taggable_type = 'Conversation' AND taggings.context = 'labels'")
.joins('INNER JOIN tags ON tags.id = taggings.tag_id')
.group('tags.name')
.count
label_ids = account.labels.where(title: counts.keys).index_by(&:title)
counts.map do |label_name, count|
label = label_ids[label_name]
{ id: label&.id, name: label_name, outgoing_messages_count: count }
end
end
end
@@ -28,7 +28,7 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
end
def create_issue
issue = linear_processor_service.create_issue(permitted_params, Current.user)
issue = linear_processor_service.create_issue(permitted_params.to_h.stringify_keys, Current.user)
if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity
else
@@ -1,14 +1,10 @@
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
include Api::V2::Accounts::ReportsHelper
include Api::V2::Accounts::HeatmapHelper
include Reports::MetricFilter
before_action :check_authorization
def index
# Return empty timeseries array for hidden metrics (preserves expected response shape)
return render json: [] if metric_hidden?(params[:metric])
builder = V2::Reports::Conversations::ReportBuilder.new(Current.account, report_params)
data = builder.timeseries
render json: data
@@ -82,15 +78,6 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render json: builder.build
end
OUTGOING_MESSAGES_ALLOWED_GROUP_BY = %w[agent team inbox label].freeze
def outgoing_messages_count
return head :unprocessable_entity unless OUTGOING_MESSAGES_ALLOWED_GROUP_BY.include?(params[:group_by])
builder = V2::Reports::OutgoingMessagesCountBuilder.new(Current.account, outgoing_messages_count_params)
render json: builder.build
end
private
def generate_csv(filename, template)
@@ -116,8 +103,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
common_params.merge({
since: range[:current][:since],
until: range[:current][:until],
timezone_offset: params[:timezone_offset],
hidden_metrics: hidden_metrics
timezone_offset: params[:timezone_offset]
})
end
@@ -125,8 +111,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
common_params.merge({
since: range[:previous][:since],
until: range[:previous][:until],
timezone_offset: params[:timezone_offset],
hidden_metrics: hidden_metrics
timezone_offset: params[:timezone_offset]
})
end
@@ -186,12 +171,4 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
until: params[:until]
}
end
def outgoing_messages_count_params
{
group_by: params[:group_by],
since: params[:since],
until: params[:until]
}
end
end
@@ -1,6 +1,4 @@
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
include Reports::MetricFilter
before_action :check_authorization
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label, :channel]
@@ -42,7 +40,7 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
def render_report_with(builder_class)
builder = builder_class.new(account: Current.account, params: @builder_params)
render json: filter_hidden_metrics_from_array(builder.build)
render json: builder.build
end
def permitted_params
@@ -1,59 +0,0 @@
module Reports::MetricFilter
extend ActiveSupport::Concern
HIDEABLE_METRICS = %w[outgoing_messages_count incoming_messages_count reply_time].freeze
# Alias mapping (canonical form)
METRIC_ALIASES = {
'avg_reply_time' => 'reply_time'
}.freeze
private
def filter_hidden_metrics_from_hash(data)
return data if hidden_metrics.blank?
data.reject { |key, _| metric_key_hidden?(key) }
end
def filter_hidden_metrics_from_array(data)
return data if hidden_metrics.blank?
data.map { |item| item.reject { |key, _| metric_key_hidden?(key) } }
end
# Filter summary data including nested 'previous' key (handles both symbol and string keys)
def filter_summary_with_previous(data)
return data if hidden_metrics.blank?
filtered = filter_hidden_metrics_from_hash(data)
previous_data = filtered[:previous] || filtered['previous']
if previous_data.present?
filtered_previous = filter_hidden_metrics_from_hash(previous_data)
# Preserve the original key type
if filtered.key?(:previous)
filtered[:previous] = filtered_previous
else
filtered['previous'] = filtered_previous
end
end
filtered
end
def hidden_metrics
@hidden_metrics ||= (Current.account.report_hidden_metrics || []) & HIDEABLE_METRICS
end
def metric_hidden?(metric_name)
# Normalize to canonical form (e.g., avg_reply_time -> reply_time)
normalized = METRIC_ALIASES[metric_name.to_s] || metric_name.to_s
hidden_metrics.include?(normalized)
end
# Check if a hash key should be hidden (handles both symbol and string keys, plus aliases)
def metric_key_hidden?(key)
key_str = key.to_s
normalized = METRIC_ALIASES[key_str] || key_str
hidden_metrics.include?(normalized)
end
end
@@ -20,7 +20,9 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
end
def instance_meta
@metrics['Database Migrations'] = ActiveRecord::Base.connection.migration_context.needs_migration? ? 'pending' : 'completed'
migrations_paths = ActiveRecord::Migrator.migrations_paths
migrations_context = ActiveRecord::MigrationContext.new(migrations_paths)
@metrics['Database Migrations'] = migrations_context.needs_migration? ? 'pending' : 'completed'
end
def chatwoot_version
@@ -18,9 +18,7 @@ const dialogRef = ref(null);
const uiFlags = useMapGetter('captainResponses/getUIFlags');
const responses = useMapGetter('captainResponses/getRecords');
const meta = useMapGetter('captainResponses/getMeta');
const isFetching = computed(() => uiFlags.value.fetchingList);
const totalCount = computed(() => meta.value.totalCount || 0);
const handleClose = () => {
emit('close');
@@ -39,7 +37,7 @@ defineExpose({ dialogRef });
<Dialog
ref="dialogRef"
type="edit"
:title="`${t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.TITLE')} (${totalCount})`"
:title="t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.TITLE')"
:description="t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.DESCRIPTION')"
:show-cancel-button="false"
:show-confirm-button="false"
@@ -1,29 +0,0 @@
import { computed } from 'vue';
import { useAccount } from './useAccount';
// Alias mapping for consistency (canonical form is reply_time)
const METRIC_ALIASES = {
avg_reply_time: 'reply_time',
};
export function useHiddenMetrics() {
const { currentAccount } = useAccount();
const hiddenMetrics = computed(() => {
return currentAccount.value?.settings?.report_hidden_metrics || [];
});
const isMetricHidden = metricKey => {
const normalized = METRIC_ALIASES[metricKey] || metricKey;
return hiddenMetrics.value.includes(normalized);
};
const filterVisibleReportKeys = reportKeys => {
// Filter out hidden metrics from reportKeys object
return Object.fromEntries(
Object.entries(reportKeys).filter(([, value]) => !isMetricHidden(value))
);
};
return { hiddenMetrics, isMetricHidden, filterVisibleReportKeys };
}
@@ -1,7 +1,6 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { useAlert, useTrack } from 'dashboard/composables';
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
import ReportFilterSelector from './components/FilterSelector.vue';
import { GROUP_BY_FILTER } from './constants';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
@@ -9,7 +8,7 @@ import { generateFileName } from 'dashboard/helper/downloadHelper';
import ReportContainer from './ReportContainer.vue';
import ReportHeader from './components/ReportHeader.vue';
const ALL_REPORTS_KEYS = {
const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
INCOMING_MESSAGES: 'incoming_messages_count',
OUTGOING_MESSAGES: 'outgoing_messages_count',
@@ -27,10 +26,6 @@ export default {
ReportContainer,
V4Button,
},
setup() {
const { filterVisibleReportKeys } = useHiddenMetrics();
return { filterVisibleReportKeys };
},
data() {
return {
from: 0,
@@ -39,11 +34,6 @@ export default {
businessHours: false,
};
},
computed: {
reportKeys() {
return this.filterVisibleReportKeys(ALL_REPORTS_KEYS);
},
},
methods: {
fetchAllData() {
this.fetchAccountSummary();
@@ -57,10 +47,18 @@ export default {
}
},
fetchChartData() {
Object.keys(this.reportKeys).forEach(async key => {
[
'CONVERSATIONS',
'INCOMING_MESSAGES',
'OUTGOING_MESSAGES',
'FIRST_RESPONSE_TIME',
'RESOLUTION_TIME',
'RESOLUTION_COUNT',
'REPLY_TIME',
].forEach(async key => {
try {
await this.$store.dispatch('fetchAccountReport', {
metric: this.reportKeys[key],
metric: REPORTS_KEYS[key],
...this.getRequestPayload(),
});
} catch {
@@ -123,6 +121,6 @@ export default {
show-group-by-filter
@filter-change="onFilterChange"
/>
<ReportContainer :group-by="groupBy" :report-keys="reportKeys" />
<ReportContainer :group-by="groupBy" />
</div>
</template>
@@ -2,7 +2,6 @@
import ReportFilterSelector from './FilterSelector.vue';
import { formatTime } from '@chatwoot/utils';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
import Table from 'dashboard/components/table/Table.vue';
import { generateFileName } from 'dashboard/helper/downloadHelper';
import {
@@ -45,7 +44,6 @@ import SummaryReportLink from './SummaryReportLink.vue';
const rowItems = useMapGetter([props.getterKey]) || [];
const reportMetrics = useMapGetter([props.summaryKey]) || [];
const { isMetricHidden } = useHiddenMetrics();
const getMetrics = id =>
reportMetrics.value.find(metrics => metrics.id === Number(id)) || {};
@@ -61,42 +59,38 @@ const defaulSpanRender = cellProps =>
cellProps.getValue()
);
const columns = computed(() => {
const allColumns = [
columnHelper.accessor('name', {
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
width: 300,
cell: cellProps => h(SummaryReportLink, cellProps),
}),
columnHelper.accessor('conversationsCount', {
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgFirstResponseTime', {
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgResolutionTime', {
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
width: 200,
cell: defaulSpanRender,
}),
!isMetricHidden('reply_time') &&
columnHelper.accessor('avgReplyTime', {
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('resolutionsCount', {
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
width: 200,
cell: defaulSpanRender,
}),
];
return allColumns.filter(Boolean);
});
const columns = computed(() => [
columnHelper.accessor('name', {
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
width: 300,
cell: cellProps => h(SummaryReportLink, cellProps),
}),
columnHelper.accessor('conversationsCount', {
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgFirstResponseTime', {
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgResolutionTime', {
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('avgReplyTime', {
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
width: 200,
cell: defaulSpanRender,
}),
columnHelper.accessor('resolutionsCount', {
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
width: 200,
cell: defaulSpanRender,
}),
]);
const renderAvgTime = value => (value ? formatTime(value) : '--');
@@ -1,7 +1,6 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { useAlert, useTrack } from 'dashboard/composables';
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
import ReportFilters from './ReportFilters.vue';
import ReportContainer from '../ReportContainer.vue';
import { GROUP_BY_FILTER } from '../constants';
@@ -64,10 +63,6 @@ export default {
default: null,
},
},
setup() {
const { filterVisibleReportKeys } = useHiddenMetrics();
return { filterVisibleReportKeys };
},
data() {
return {
from: 0,
@@ -87,7 +82,7 @@ export default {
return this.type === 'agent';
},
reportKeys() {
const allKeys = {
return {
CONVERSATIONS: 'conversations_count',
...(!this.isAgentType && {
INCOMING_MESSAGES: 'incoming_messages_count',
@@ -98,8 +93,6 @@ export default {
RESOLUTION_COUNT: 'resolutions_count',
REPLY_TIME: 'reply_time',
};
// Filter out hidden metrics
return this.filterVisibleReportKeys(allKeys);
},
},
mounted() {
-8
View File
@@ -67,13 +67,6 @@ class Account < ApplicationRecord
'help_center_search': { 'type': %w[boolean null] }
},
'additionalProperties': false
},
'report_hidden_metrics': {
'type': %w[array null],
'items': {
'type': 'string',
'enum': %w[outgoing_messages_count incoming_messages_count reply_time]
}
}
},
'required': [],
@@ -95,7 +88,6 @@ class Account < ApplicationRecord
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
store_accessor :settings, :captain_models, :captain_features
store_accessor :settings, :report_hidden_metrics
has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async
+5
View File
@@ -19,6 +19,11 @@ class InstallationConfig < ApplicationRecord
# https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
# FIX ME : fixes breakage of installation config. we need to migrate.
# Fix configuration in application.rb
#
# Note: This whole thing is because we store the installation config serialized in YAML in Database
# This serialized version stores HashWithIndifferentAccess, We could avoid all this complexity if we store the value as JSONB
# We could also avoid this issue if we migrate the installation config to JSONB
# We should do this migration at some point in time.
serialize :serialized_value, coder: YAML, type: ActiveSupport::HashWithIndifferentAccess
before_validation :set_lock
+4 -2
View File
@@ -18,7 +18,7 @@ class Integrations::Hook < ApplicationRecord
include Reauthorizable
attr_readonly :app_id, :account_id, :inbox_id, :hook_type
before_validation :ensure_hook_type
before_validation :ensure_hook_type, on: :create
after_create :trigger_setup_if_crm
# TODO: Remove guard once encryption keys become mandatory (target 3-4 releases out).
@@ -86,7 +86,9 @@ class Integrations::Hook < ApplicationRecord
end
def ensure_hook_type
self.hook_type = app.params[:hook_type] if app.present?
return if app.blank?
self.hook_type = app.params[:hook_type]
end
def validate_settings_json_schema
+2
View File
@@ -421,6 +421,8 @@ class Message < ApplicationRecord
end
def reindex_for_search
return unless respond_to?(:reindex)
reindex(mode: :async)
end
end
+3 -3
View File
@@ -69,7 +69,7 @@ class User < ApplicationRecord
# TODO: remove in a future version once online status is moved to account users
# remove the column availability from users
enum availability: { online: 0, offline: 1, busy: 2 }
enum :availability, { online: 0, offline: 1, busy: 2 }
# The validation below has been commented out as it does not
# work because :validatable in devise overrides this.
@@ -77,7 +77,7 @@ class User < ApplicationRecord
validates :email, presence: true
serialize :otp_backup_codes, type: Array
serialize :otp_backup_codes, coder: JSON, type: Array
# Encrypt sensitive MFA fields
encrypts :otp_secret, deterministic: true
@@ -88,7 +88,7 @@ class User < ApplicationRecord
accepts_nested_attributes_for :account_users
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify, inverse_of: :assignee
alias_attribute :conversations, :assigned_conversations
alias conversations assigned_conversations
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify, inverse_of: :assigned_agent
has_many :reviewed_csat_survey_responses, foreign_key: 'review_notes_updated_by_id', class_name: 'CsatSurveyResponse',
dependent: :nullify, inverse_of: :review_notes_updated_by
@@ -82,7 +82,7 @@ class Crm::Leadsquared::SetupService
end
def update_hook_settings(params)
@hook.settings = @hook.settings.merge(params)
@hook.settings = @hook.settings.merge(params.stringify_keys)
@hook.save!
end
+1 -1
View File
@@ -36,7 +36,7 @@ end
module Chatwoot
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
config.load_defaults 7.2
config.eager_load_paths << Rails.root.join('lib')
config.eager_load_paths << Rails.root.join('enterprise/lib')
-1
View File
@@ -446,7 +446,6 @@ Rails.application.routes.draw do
get :bot_metrics
get :inbox_label_matrix
get :first_response_time_distribution
get :outgoing_messages_count
end
end
resource :year_in_review, only: [:show]
+1 -1
View File
@@ -23,7 +23,7 @@ google:
# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
microsoft:
service: AzureStorage
service: AzureBlob
storage_account_name: <%= ENV.fetch('AZURE_STORAGE_ACCOUNT_NAME', '') %>
storage_access_key: <%= ENV.fetch('AZURE_STORAGE_ACCESS_KEY', '') %>
container: <%= ENV.fetch('AZURE_STORAGE_CONTAINER', '') %>
@@ -12,8 +12,11 @@ class ArticleKeyConverter
def convert_key(id)
verifier_name = 'ActiveStorage'
key_generator = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base, iterations: 1000,
hash_digest_class: OpenSSL::Digest::SHA1)
secret_key_base = Rails.application.credentials.secret_key_base ||
Rails.application.secrets.secret_key_base
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base,
iterations: 1000,
hash_digest_class: OpenSSL::Digest::SHA1)
key_generator = ActiveSupport::CachingKeyGenerator.new(key_generator)
secret = key_generator.generate_key(verifier_name.to_s)
verifier = ActiveSupport::MessageVerifier.new(secret)
+2
View File
@@ -29,6 +29,8 @@ RUN apk update && apk add --no-cache \
tar \
build-base \
tzdata \
yaml-dev \
pkgconf \
postgresql-dev \
postgresql-client \
git \
@@ -2,9 +2,6 @@ require 'administrate/field/base'
class AccountLimitsField < Administrate::Field::Base
def to_s
defaults = { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil, emails: nil }
overrides = (data.presence || {}).to_h.symbolize_keys.compact
defaults.merge(overrides).to_json
data.present? ? data.to_json : { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil, emails: nil }.to_json
end
end
+1 -1
View File
@@ -15,7 +15,7 @@ class Seeders::Reports::MessageCreator
end
def create_messages
message_count = rand(MESSAGES_PER_CONVERSATION..MESSAGES_PER_CONVERSATION + 5)
message_count = rand(MESSAGES_PER_CONVERSATION..(MESSAGES_PER_CONVERSATION + 5))
first_agent_reply = true
message_count.times do |i|
+2 -2
View File
@@ -144,8 +144,8 @@
"prosemirror-model": "^1.22.3",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.13",
"vite": "^5.4.21",
"vite-plugin-ruby": "^5.0.0",
"vite": "5.4.21",
"vite-plugin-ruby": "^5.1.1",
"vitest": "3.0.5"
},
"engines": {
+9 -9
View File
@@ -351,8 +351,8 @@ importers:
specifier: 5.4.21
version: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
vite-plugin-ruby:
specifier: ^5.0.0
version: 5.0.0(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
specifier: ^5.1.1
version: 5.1.1(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
vitest:
specifier: 3.0.5
version: 3.0.5(@types/node@22.7.0)(jsdom@27.2.0)(sass@1.79.3)(terser@5.33.0)
@@ -4519,8 +4519,8 @@ packages:
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
vite-plugin-ruby@5.0.0:
resolution: {integrity: sha512-c8PjTp21Ah/ttgnNUyu0qvCXZI08Jr9I24oUKg3TRIRhF5GcOZ++6wtlTCrNFd9COEQbpXHxlRIXd/MEg0iZJw==}
vite-plugin-ruby@5.1.1:
resolution: {integrity: sha512-I1dXJq2ywdvTD2Cz5LYNcYLujqQ3eUxPoCjruRdfm2QBtHBY15NEeb6x5HuPM3T5S+y8S3p9fwRsieQQCjk0gg==}
peerDependencies:
vite: 5.4.21
@@ -6245,7 +6245,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.4.0
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -7647,7 +7647,7 @@ snapshots:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
debug: 4.4.0
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -7661,7 +7661,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.0
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -9599,9 +9599,9 @@ snapshots:
- supports-color
- terser
vite-plugin-ruby@5.0.0(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
vite-plugin-ruby@5.1.1(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
dependencies:
debug: 4.3.5
debug: 4.4.3
fast-glob: 3.3.2
vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
transitivePeerDependencies:
+16 -9
View File
@@ -1,15 +1,22 @@
require 'rails_helper'
describe V2::ReportBuilder do
RSpec.describe V2::ReportBuilder do
include ActiveJob::TestHelper
let_it_be(:account) { create(:account) }
let_it_be(:label_1) { create(:label, title: 'Label_1', account: account) }
let_it_be(:label_2) { create(:label, title: 'Label_2', account: account) }
self.use_transactional_tests = false
def truncate_test_data
connection = ActiveRecord::Base.connection
connection.truncate_tables(*connection.tables)
end
before { truncate_test_data }
let(:account) { create(:account) }
let!(:label_1) { create(:label, title: 'Label_1', account: account) }
let!(:label_2) { create(:label, title: 'Label_2', account: account) }
describe '#timeseries' do
# Use before_all to share expensive setup across all tests in this describe block
# This runs once instead of 21 times, dramatically speeding up the suite
before_all do
before do
travel_to(Time.zone.today) do
user = create(:user, account: account)
inbox = create(:inbox, account: account)
@@ -21,7 +28,7 @@ describe V2::ReportBuilder do
perform_enqueued_jobs do
10.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: Time.zone.today)
create_list(:message, 5, message_type: 'outgoing',
account: account, inbox: inbox,
@@ -37,7 +44,7 @@ describe V2::ReportBuilder do
5.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: (Time.zone.today - 2.days))
create_list(:message, 3, message_type: 'outgoing',
account: account, inbox: inbox,
@@ -2,11 +2,19 @@ require 'rails_helper'
RSpec.describe V2::Reports::LabelSummaryBuilder do
include ActiveJob::TestHelper
self.use_transactional_tests = false
let_it_be(:account) { create(:account) }
let_it_be(:label_1) { create(:label, title: 'label_1', account: account) }
let_it_be(:label_2) { create(:label, title: 'label_2', account: account) }
let_it_be(:label_3) { create(:label, title: 'label_3', account: account) }
def truncate_test_data
connection = ActiveRecord::Base.connection
connection.truncate_tables(*connection.tables)
end
before { truncate_test_data }
let(:account) { create(:account) }
let!(:label_1) { create(:label, title: 'label_1', account: account) }
let!(:label_2) { create(:label, title: 'label_2', account: account) }
let!(:label_3) { create(:label, title: 'label_3', account: account) }
let(:params) do
{
@@ -92,7 +100,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
# Create conversations with label_1
3.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: Time.zone.today)
create_list(:message, 2, message_type: 'outgoing',
account: account, inbox: inbox,
@@ -110,7 +118,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
# Create conversations with label_2
2.times do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: Time.zone.today)
create_list(:message, 1, message_type: 'outgoing',
account: account, inbox: inbox,
@@ -229,7 +237,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
perform_enqueued_jobs do
# Conversation within range
conversation_in_range = create(:conversation, account: account,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: 2.days.ago)
conversation_in_range.update_labels('label_1')
conversation_in_range.label_list
@@ -244,7 +252,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
# Conversation outside range (too old)
conversation_out_of_range = create(:conversation, account: account,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: 1.week.ago)
conversation_out_of_range.update_labels('label_1')
conversation_out_of_range.label_list
@@ -286,7 +294,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
perform_enqueued_jobs do
conversation = create(:conversation, account: account,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: Time.zone.today)
conversation.update_labels('label_1')
conversation.label_list
@@ -323,8 +331,8 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
let(:account2_builder) do
described_class.new(account: account2, params: {
business_hours: false,
since: test_date.to_time.to_i.to_s,
until: test_date.end_of_day.to_time.to_i.to_s,
since: test_date.in_time_zone.to_i.to_s,
until: test_date.end_of_day.in_time_zone.to_i.to_s,
timezone_offset: 0
})
end
@@ -343,7 +351,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
perform_enqueued_jobs do
conversation = create(:conversation, account: account2,
inbox: inbox, assignee: user,
inbox: inbox,
created_at: test_date)
conversation.update_labels(unique_label_name)
conversation.label_list
@@ -358,6 +366,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
# Second resolution
conversation.resolved!
end
perform_enqueued_jobs
end
end
@@ -388,7 +388,7 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do
headers: administrator.create_new_auth_token
end
expect(conversation.messages.activity.last.content).to eq("Assigned to #{user_1.name} by #{administrator.name}")
expect(conversation.reload.assignee_id).to eq(user_1.id)
end
it 'Assign the agent when he is not inbox member' do
@@ -402,7 +402,7 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do
headers: administrator.create_new_auth_token
end
expect(conversation.messages.activity.last.content).not_to eq("Assigned to #{user_1.name} by #{administrator.name}")
expect(conversation.reload.assignee_id).to be_nil
end
it 'Assign the labels' do
@@ -295,131 +295,4 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do
end
end
end
describe 'GET /api/v2/accounts/{account.id}/reports/outgoing_messages_count' do
let(:since_epoch) { 1.week.ago.to_i.to_s }
let(:until_epoch) { 1.day.from_now.to_i.to_s }
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as admin' do
let(:agent2) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
let(:inbox2) { create(:inbox, account: account) }
# Separate conversations for agent and team grouping because
# model callbacks clear assignee_id when team is set.
before do
conv_agent = create(:conversation, account: account, inbox: inbox, assignee: agent)
conv_agent2 = create(:conversation, account: account, inbox: inbox2, assignee: agent2)
conv_team = create(:conversation, account: account, inbox: inbox, team: team)
create_list(:message, 3, account: account, conversation: conv_agent, inbox: inbox, message_type: :outgoing, sender: agent)
create_list(:message, 2, account: account, conversation: conv_agent2, inbox: inbox2, message_type: :outgoing, sender: agent2)
create_list(:message, 4, account: account, conversation: conv_team, inbox: inbox, message_type: :outgoing)
# incoming message should not be counted
create(:message, account: account, conversation: conv_agent, inbox: inbox, message_type: :incoming)
end
it 'returns unprocessable_entity for invalid group_by' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'invalid', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns outgoing message counts grouped by agent' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data).to be_an(Array)
agent_entry = data.find { |e| e['id'] == agent.id }
agent2_entry = data.find { |e| e['id'] == agent2.id }
expect(agent_entry['outgoing_messages_count']).to eq(3)
expect(agent2_entry['outgoing_messages_count']).to eq(2)
end
it 'returns outgoing message counts grouped by team' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'team', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data).to be_an(Array)
expect(data.length).to eq(1)
expect(data.first['id']).to eq(team.id)
expect(data.first['outgoing_messages_count']).to eq(4)
end
it 'returns outgoing message counts grouped by inbox' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'inbox', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data).to be_an(Array)
inbox_entry = data.find { |e| e['id'] == inbox.id }
inbox2_entry = data.find { |e| e['id'] == inbox2.id }
expect(inbox_entry['outgoing_messages_count']).to eq(7)
expect(inbox2_entry['outgoing_messages_count']).to eq(2)
end
it 'returns outgoing message counts grouped by label' do
label = create(:label, account: account, title: 'support')
conversation = account.conversations.first
conversation.label_list.add('support')
conversation.save!
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'label', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data).to be_an(Array)
expect(data.length).to eq(1)
expect(data.first['id']).to eq(label.id)
expect(data.first['name']).to eq('support')
end
it 'excludes bot messages when grouped by agent' do
bot = create(:agent_bot)
bot_conversation = create(:conversation, account: account, inbox: inbox)
create(:message, account: account, conversation: bot_conversation, inbox: inbox,
message_type: :outgoing, sender: bot)
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
data = response.parsed_body
agent_entry = data.find { |e| e['id'] == agent.id }
# 3 from before block; bot message excluded (sender_type != 'User')
expect(agent_entry['outgoing_messages_count']).to eq(3)
end
end
end
end
+14 -18
View File
@@ -1,12 +1,6 @@
require 'rails_helper'
RSpec.describe BulkActionsJob do
params = {
type: 'Conversation',
fields: { status: 'snoozed' },
ids: Conversation.first(3).pluck(:display_id)
}
subject(:job) { described_class.perform_later(account: account, params: params, user: agent) }
let(:account) { create(:account) }
@@ -14,6 +8,8 @@ RSpec.describe BulkActionsJob do
let!(:conversation_1) { create(:conversation, account_id: account.id, status: :open) }
let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) }
let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) }
let(:conversation_ids) { Conversation.where(id: [conversation_1.id, conversation_2.id, conversation_3.id]).pluck(:display_id) }
let(:params) { { type: 'Conversation', fields: { status: 'snoozed' }, ids: conversation_ids } }
before do
Conversation.all.find_each do |conversation|
@@ -38,10 +34,10 @@ RSpec.describe BulkActionsJob do
params = {
type: 'Conversation',
fields: { status: 'snoozed', assignee_id: agent.id },
ids: Conversation.first(3).pluck(:display_id)
ids: conversation_ids
}
expect(Conversation.first.status).to eq('open')
expect(conversation_1.status).to eq('open')
described_class.perform_now(account: account, params: params, user: agent)
@@ -54,32 +50,32 @@ RSpec.describe BulkActionsJob do
params = {
type: 'Conversation',
fields: { status: 'snoozed', assignee_id: agent.id },
ids: Conversation.first(3).pluck(:display_id)
ids: conversation_ids
}
expect(Conversation.first.assignee_id).to be_nil
expect(conversation_1.assignee_id).to be_nil
described_class.perform_now(account: account, params: params, user: agent)
expect(Conversation.first.assignee_id).to eq(agent.id)
expect(Conversation.second.assignee_id).to eq(agent.id)
expect(Conversation.third.assignee_id).to eq(agent.id)
expect(conversation_1.reload.assignee_id).to eq(agent.id)
expect(conversation_2.reload.assignee_id).to eq(agent.id)
expect(conversation_3.reload.assignee_id).to eq(agent.id)
end
it 'bulk updates the snoozed_until' do
params = {
type: 'Conversation',
fields: { status: 'snoozed', snoozed_until: Time.zone.now },
ids: Conversation.first(3).pluck(:display_id)
ids: conversation_ids
}
expect(Conversation.first.snoozed_until).to be_nil
expect(conversation_1.snoozed_until).to be_nil
described_class.perform_now(account: account, params: params, user: agent)
expect(Conversation.first.snoozed_until).to be_present
expect(Conversation.second.snoozed_until).to be_present
expect(Conversation.third.snoozed_until).to be_present
expect(conversation_1.reload.snoozed_until).to be_present
expect(conversation_2.reload.snoozed_until).to be_present
expect(conversation_3.reload.snoozed_until).to be_present
end
end
end
@@ -1,11 +1,9 @@
# frozen_string_literal: true
RSpec.shared_context 'with smtp config' do
before do
# We need to use allow_any_instance_of here because smtp_config_set_or_development?
# is defined in ApplicationMailer and needs to be stubbed for all mailer instances
# rubocop:disable RSpec/AnyInstance
allow_any_instance_of(ApplicationMailer).to receive(:smtp_config_set_or_development?).and_return(true)
# rubocop:enable RSpec/AnyInstance
around do |example|
# Set SMTP_ADDRESS so mailers build a Mail::Message in test without touching real SMTP.
# Scoped to this shared context to avoid affecting other specs.
with_modified_env('SMTP_ADDRESS' => 'smtp.example.com') { example.run }
end
end
+1 -2
View File
@@ -28,8 +28,7 @@ RSpec.describe ContactInbox do
obj.reload
# ensure the column is nil in database
results = ActiveRecord::Base.connection.execute('Select * from contact_inboxes;')
expect(results.first['pubsub_token']).to be_nil
expect(described_class.where(id: obj.id).pick(:pubsub_token)).to be_nil
new_token = obj.pubsub_token
obj.update(source_id: '234234323')
@@ -20,7 +20,7 @@ RSpec.describe Account::SignUpEmailValidationService, type: :service do
it 'raises InvalidEmail with invalid message' do
allow(ValidEmail2::Address).to receive(:new).with(email).and_return(invalid_email_address)
expect { service.perform }.to raise_error do |error|
expect(error).to be_a(CustomExceptions::Account::InvalidEmail)
expect(error.class.name).to eq('CustomExceptions::Account::InvalidEmail')
expect(error.message).to eq(I18n.t('errors.signup.invalid_email'))
end
end
@@ -32,7 +32,7 @@ RSpec.describe Account::SignUpEmailValidationService, type: :service do
it 'raises InvalidEmail with blocked domain message' do
allow(ValidEmail2::Address).to receive(:new).with(email).and_return(valid_email_address)
expect { service.perform }.to raise_error do |error|
expect(error).to be_a(CustomExceptions::Account::InvalidEmail)
expect(error.class.name).to eq('CustomExceptions::Account::InvalidEmail')
expect(error.message).to eq(I18n.t('errors.signup.blocked_domain'))
end
end
@@ -44,7 +44,7 @@ RSpec.describe Account::SignUpEmailValidationService, type: :service do
it 'raises InvalidEmail with blocked domain message' do
allow(ValidEmail2::Address).to receive(:new).with(email).and_return(valid_email_address)
expect { service.perform }.to raise_error do |error|
expect(error).to be_a(CustomExceptions::Account::InvalidEmail)
expect(error.class.name).to eq('CustomExceptions::Account::InvalidEmail')
expect(error.message).to eq(I18n.t('errors.signup.blocked_domain'))
end
end
@@ -56,7 +56,7 @@ RSpec.describe Account::SignUpEmailValidationService, type: :service do
it 'raises InvalidEmail with disposable message' do
allow(ValidEmail2::Address).to receive(:new).with(email).and_return(disposable_email_address)
expect { service.perform }.to raise_error do |error|
expect(error).to be_a(CustomExceptions::Account::InvalidEmail)
expect(error.class.name).to eq('CustomExceptions::Account::InvalidEmail')
expect(error.message).to eq(I18n.t('errors.signup.disposable_email'))
end
end
@@ -42,6 +42,11 @@ describe Telegram::IncomingMessageService do
}
end
def contact_for(source_id = nil)
source_id ||= message_params.dig('from', 'id')
ContactInbox.find_by!(inbox: telegram_channel.inbox, source_id: source_id).contact
end
describe '#perform' do
context 'when valid text message params' do
it 'creates appropriate conversations, message and contacts' do
@@ -51,7 +56,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.content).to eq('test')
end
end
@@ -64,9 +69,9 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(Contact.all.first.additional_attributes['social_telegram_user_id']).to eq(23)
expect(Contact.all.first.additional_attributes['social_telegram_user_name']).to eq('sojan')
expect(contact_for.name).to eq('Sojan Jose')
expect(contact_for.additional_attributes['social_telegram_user_id']).to eq(23)
expect(contact_for.additional_attributes['social_telegram_user_name']).to eq('sojan')
expect(telegram_channel.inbox.messages.first.content).to eq('test')
end
end
@@ -107,7 +112,7 @@ describe Telegram::IncomingMessageService do
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
contact = Contact.all.first
contact = contact_for
expect(contact.name).to eq('Sojan Jose')
expect(contact.additional_attributes['language_code']).to eq('en')
message = telegram_channel.inbox.messages.first
@@ -131,7 +136,7 @@ describe Telegram::IncomingMessageService do
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
contact = Contact.all.first
contact = contact_for
expect(contact.name).to eq('Sojan Jose')
# TODO: The language code is not present when we send the first message to the client.
# Should we update it when the user replies?
@@ -161,9 +166,9 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(Contact.all.first.additional_attributes['social_telegram_user_id']).to eq(23)
expect(Contact.all.first.additional_attributes['social_telegram_user_name']).to eq('sojan')
expect(contact_for.name).to eq('Sojan Jose')
expect(contact_for.additional_attributes['social_telegram_user_id']).to eq(23)
expect(contact_for.additional_attributes['social_telegram_user_name']).to eq('sojan')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('audio')
end
end
@@ -182,7 +187,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('image')
end
end
@@ -207,7 +212,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('image')
end
end
@@ -229,7 +234,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('video')
end
end
@@ -258,7 +263,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('video')
end
end
@@ -277,7 +282,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('audio')
end
end
@@ -298,7 +303,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('file')
end
end
@@ -336,7 +341,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('location')
end
@@ -355,7 +360,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
attachment = telegram_channel.inbox.messages.first.attachments.first
expect(attachment.file_type).to eq('location')
@@ -388,8 +393,8 @@ describe Telegram::IncomingMessageService do
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(Contact.all.first.additional_attributes['social_telegram_user_id']).to eq(5_171_248)
expect(contact_for(5_171_248).name).to eq('Sojan Jose')
expect(contact_for(5_171_248).additional_attributes['social_telegram_user_id']).to eq(5_171_248)
expect(telegram_channel.inbox.messages.first.content).to eq('Option 1')
end
end
@@ -406,7 +411,7 @@ describe Telegram::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_for.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('contact')
end
end
@@ -3,6 +3,7 @@ require 'rails_helper'
describe Whatsapp::IncomingMessageWhatsappCloudService do
describe '#perform' do
let!(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
let(:sender_number) { '2423423243' }
let(:params) do
{
phone_number: whatsapp_channel.phone_number,
@@ -10,9 +11,9 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: '2423423243' }],
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: sender_number }],
messages: [{
from: '2423423243',
from: sender_number,
image: {
id: 'b1c68f38-8734-4ad3-b4a1-ef0c10d683',
mime_type: 'image/jpeg',
@@ -48,7 +49,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect_contact_name
expect(whatsapp_channel.inbox.messages.first.content).to eq('Check out my product!')
expect(whatsapp_channel.inbox.messages.first.attachments.present?).to be false
expect(whatsapp_channel.authorization_error_count).to eq(1)
@@ -63,9 +64,9 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: '2423423243' }],
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: sender_number }],
messages: [{
from: '2423423243',
from: sender_number,
image: {
id: 'b1c68f38-8734-4ad3-b4a1-ef0c10d683',
mime_type: 'image/jpeg',
@@ -88,7 +89,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
it 'with attachment errors' do
described_class.new(inbox: whatsapp_channel.inbox, params: error_params).perform
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect_contact_name
expect(whatsapp_channel.inbox.messages.count).to eq(0)
end
end
@@ -98,7 +99,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
described_class.new(inbox: whatsapp_channel.inbox, params: { phone_number: whatsapp_channel.phone_number,
object: 'whatsapp_business_account', entry: {} }).perform
expect(whatsapp_channel.inbox.conversations.count).to eq(0)
expect(Contact.all.first).to be_nil
expect(Contact.find_by(phone_number: contact_phone_number)).to be_nil
expect(whatsapp_channel.inbox.messages.count).to eq(0)
end
end
@@ -136,7 +137,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
end
def expect_contact_name
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(contact_from_number&.name).to eq('Sojan Jose')
end
def expect_message_content
@@ -146,4 +147,12 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
def expect_message_has_attachment
expect(whatsapp_channel.inbox.messages.first.attachments.present?).to be true
end
def contact_phone_number
"+#{sender_number}"
end
def contact_from_number
Contact.find_by(phone_number: contact_phone_number)
end
end
-2
View File
@@ -229,8 +229,6 @@ first_response_time_distribution:
$ref: './resource/reports/first_response_time_distribution.yml'
inbox_label_matrix:
$ref: './resource/reports/inbox_label_matrix.yml'
outgoing_messages_count:
$ref: './resource/reports/outgoing_messages_count.yml'
inbox_summary:
$ref: './resource/reports/inbox_summary.yml'
agent_summary:
@@ -1,21 +0,0 @@
type: array
description: Outgoing messages count report grouped by entity (agent, team, inbox, or label).
items:
type: object
properties:
id:
type: number
description: The ID of the grouped entity (agent, team, inbox, or label).
name:
type: string
description: The name of the grouped entity.
outgoing_messages_count:
type: number
description: The total number of outgoing messages for this entity in the given time range.
example:
- id: 1
name: Agent One
outgoing_messages_count: 42
- id: 2
name: Agent Two
outgoing_messages_count: 18
@@ -1,36 +0,0 @@
tags:
- Reports
operationId: get-outgoing-messages-count
summary: Get outgoing messages count grouped by entity
security:
- userApiKey: []
description: |
Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).
When grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.
**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.
parameters:
- in: query
name: group_by
required: true
schema:
type: string
enum:
- agent
- team
- inbox
- label
description: The entity to group outgoing message counts by.
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/outgoing_messages_count'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
-17
View File
@@ -770,23 +770,6 @@
get:
$ref: './application/reports/inbox_label_matrix.yml'
# Outgoing messages count report
/api/v2/accounts/{account_id}/reports/outgoing_messages_count:
parameters:
- $ref: '#/components/parameters/account_id'
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start (Unix timestamp).
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop (Unix timestamp).
get:
$ref: './application/reports/outgoing_messages_count.yml'
# Conversations Messages
/accounts/{account_id}/conversations/{conversation_id}/messages:
parameters:
-108
View File
@@ -8274,81 +8274,6 @@
}
}
},
"/api/v2/accounts/{account_id}/reports/outgoing_messages_count": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-outgoing-messages-count",
"summary": "Get outgoing messages count grouped by entity",
"security": [
{
"userApiKey": []
}
],
"description": "Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).\nWhen grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
"parameters": [
{
"in": "query",
"name": "group_by",
"required": true,
"schema": {
"type": "string",
"enum": [
"agent",
"team",
"inbox",
"label"
]
},
"description": "The entity to group outgoing message counts by."
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/outgoing_messages_count"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/accounts/{account_id}/conversations/{conversation_id}/messages": {
"parameters": [
{
@@ -12318,39 +12243,6 @@
]
}
},
"outgoing_messages_count": {
"type": "array",
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
},
"name": {
"type": "string",
"description": "The name of the grouped entity."
},
"outgoing_messages_count": {
"type": "number",
"description": "The total number of outgoing messages for this entity in the given time range."
}
}
},
"example": [
{
"id": 1,
"name": "Agent One",
"outgoing_messages_count": 42
},
{
"id": 2,
"name": "Agent Two",
"outgoing_messages_count": 18
}
]
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
-108
View File
@@ -6816,81 +6816,6 @@
}
}
}
},
"/api/v2/accounts/{account_id}/reports/outgoing_messages_count": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-outgoing-messages-count",
"summary": "Get outgoing messages count grouped by entity",
"security": [
{
"userApiKey": []
}
],
"description": "Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).\nWhen grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
"parameters": [
{
"in": "query",
"name": "group_by",
"required": true,
"schema": {
"type": "string",
"enum": [
"agent",
"team",
"inbox",
"label"
]
},
"description": "The entity to group outgoing message counts by."
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/outgoing_messages_count"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
}
},
"components": {
@@ -10825,39 +10750,6 @@
]
}
},
"outgoing_messages_count": {
"type": "array",
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
},
"name": {
"type": "string",
"description": "The name of the grouped entity."
},
"outgoing_messages_count": {
"type": "number",
"description": "The total number of outgoing messages for this entity in the given time range."
}
}
},
"example": [
{
"id": 1,
"name": "Agent One",
"outgoing_messages_count": 42
},
{
"id": 2,
"name": "Agent Two",
"outgoing_messages_count": 18
}
]
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
-33
View File
@@ -4558,39 +4558,6 @@
]
}
},
"outgoing_messages_count": {
"type": "array",
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
},
"name": {
"type": "string",
"description": "The name of the grouped entity."
},
"outgoing_messages_count": {
"type": "number",
"description": "The total number of outgoing messages for this entity in the given time range."
}
}
},
"example": [
{
"id": 1,
"name": "Agent One",
"outgoing_messages_count": 42
},
{
"id": 2,
"name": "Agent Two",
"outgoing_messages_count": 18
}
]
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
-33
View File
@@ -3973,39 +3973,6 @@
]
}
},
"outgoing_messages_count": {
"type": "array",
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
},
"name": {
"type": "string",
"description": "The name of the grouped entity."
},
"outgoing_messages_count": {
"type": "number",
"description": "The total number of outgoing messages for this entity in the given time range."
}
}
},
"example": [
{
"id": 1,
"name": "Agent One",
"outgoing_messages_count": 42
},
{
"id": 2,
"name": "Agent Two",
"outgoing_messages_count": 18
}
]
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
-33
View File
@@ -4734,39 +4734,6 @@
]
}
},
"outgoing_messages_count": {
"type": "array",
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
},
"name": {
"type": "string",
"description": "The name of the grouped entity."
},
"outgoing_messages_count": {
"type": "number",
"description": "The total number of outgoing messages for this entity in the given time range."
}
}
},
"example": [
{
"id": 1,
"name": "Agent One",
"outgoing_messages_count": 42
},
{
"id": 2,
"name": "Agent Two",
"outgoing_messages_count": 18
}
]
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",