From 7e8fe78ecda15869ff9cbbf2901a7d79429a95a5 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 28 Oct 2025 15:12:46 +0530 Subject: [PATCH 1/3] perf: Add database index on conversations identifier (#12715) **Problem** Slack webhook processing was failing with 500 errors due to database timeouts. The query `Conversation.where(identifier: params[:event][:thread_ts]).first` was performing full table scans and hitting PostgreSQL statement timeout. **Solution** Added database index on conversations.identifier and account_id. --- .../20251022152158_add_index_to_conversations_identifier.rb | 6 ++++++ db/schema.rb | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20251022152158_add_index_to_conversations_identifier.rb diff --git a/db/migrate/20251022152158_add_index_to_conversations_identifier.rb b/db/migrate/20251022152158_add_index_to_conversations_identifier.rb new file mode 100644 index 000000000..e7d02b53d --- /dev/null +++ b/db/migrate/20251022152158_add_index_to_conversations_identifier.rb @@ -0,0 +1,6 @@ +class AddIndexToConversationsIdentifier < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + def change + add_index :conversations, [:identifier, :account_id], name: 'index_conversations_on_identifier_and_account_id', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index c0d539f6a..022a0101e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_10_03_091242) do +ActiveRecord::Schema[7.1].define(version: 2025_10_22_152158) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -676,6 +676,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_10_03_091242) do t.index ["contact_id"], name: "index_conversations_on_contact_id" t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id" t.index ["first_reply_created_at"], name: "index_conversations_on_first_reply_created_at" + t.index ["identifier", "account_id"], name: "index_conversations_on_identifier_and_account_id" t.index ["inbox_id"], name: "index_conversations_on_inbox_id" t.index ["priority"], name: "index_conversations_on_priority" t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id" From 26ea87a6cb755caa2dfe1de359aded504aeff99a Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 28 Oct 2025 18:16:29 +0530 Subject: [PATCH 2/3] fix: Extend phone number normalization to Twilio WhatsApp (#12655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem WhatsApp Cloud channels already handle Brazil/Argentina phone number format mismatches (PRs #12492, #11173), but Twilio WhatsApp channels were creating duplicate contacts when: - Template sent to new format: `whatsapp:+5541988887777` (13 digits) - User responds from old format: `whatsapp:+554188887777` (12 digits) ### Solution The solution extends the existing phone number normalization infrastructure to support both WhatsApp providers while handling their different payload formats: ### Provider Format Differences - **WhatsApp Cloud**: `wa_id: "919745786257"` (clean number) - **Twilio WhatsApp**: `From: "whatsapp:+919745786257"` (prefixed format) ### Test Coverage #### Brazil Phone Number Tests **Case 1: New Format (13 digits with "9")** - **Test 1**: No existing contact → Creates new contact with original format - **Test 2**: Contact exists in same format → Appends to existing conversation **Case 2: Old Format (12 digits without "9")** - **Test 3**: Contact exists in old format → Appends to existing conversation - **Test 4** *(Critical)*: Contact exists in new format, message in old format → Finds existing contact, prevents duplicate - **Test 5**: No contact exists → Creates new contact with incoming format #### Argentina Phone Number Tests **Case 3: With "9" after country code** - **Test 6**: No existing contact → Creates new contact - **Test 7**: Contact exists in normalized format → Uses existing contact **Case 4: Without "9" after country code** - **Test 8**: Contact exists in same format → Appends to existing - **Test 9**: No contact exists → Creates new contact Fixes https://linear.app/chatwoot/issue/CW-5565/inconsistencies-for-mobile-numbersargentina-brazil-and-mexico-numbers --- .../twilio/incoming_message_service.rb | 10 +- .../incoming_message_service_helpers.rb | 11 +- .../phone_number_normalization_service.rb | 51 +++- spec/factories/channel/twilio_sms.rb | 4 + .../twilio/incoming_message_service_spec.rb | 224 ++++++++++++++++++ 5 files changed, 278 insertions(+), 22 deletions(-) diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index 7a74488e2..5d695ebb2 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -44,6 +44,12 @@ class Twilio::IncomingMessageService twilio_channel.sms? ? params[:From] : params[:From].gsub('whatsapp:', '') end + def normalized_phone_number + return phone_number unless twilio_channel.whatsapp? + + Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider("whatsapp:#{phone_number}", :twilio) + end + def formatted_phone_number TelephoneNumber.parse(phone_number).international_number end @@ -53,8 +59,10 @@ class Twilio::IncomingMessageService end def set_contact + source_id = twilio_channel.whatsapp? ? normalized_phone_number : params[:From] + contact_inbox = ::ContactInboxWithContactBuilder.new( - source_id: params[:From], + source_id: source_id, inbox: inbox, contact_attributes: contact_attributes ).perform diff --git a/app/services/whatsapp/incoming_message_service_helpers.rb b/app/services/whatsapp/incoming_message_service_helpers.rb index 705babbba..46ad255aa 100644 --- a/app/services/whatsapp/incoming_message_service_helpers.rb +++ b/app/services/whatsapp/incoming_message_service_helpers.rb @@ -47,17 +47,8 @@ module Whatsapp::IncomingMessageServiceHelpers %w[reaction ephemeral unsupported request_welcome].include?(message_type) end - def argentina_phone_number?(phone_number) - phone_number.match(/^54/) - end - - def normalised_argentina_mobil_number(phone_number) - # Remove 9 before country code - phone_number.sub(/^549/, '54') - end - def processed_waid(waid) - Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid) + Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(waid, :cloud) end def error_webhook_event?(message) diff --git a/app/services/whatsapp/phone_number_normalization_service.rb b/app/services/whatsapp/phone_number_normalization_service.rb index cd10db0d0..1e52d9b02 100644 --- a/app/services/whatsapp/phone_number_normalization_service.rb +++ b/app/services/whatsapp/phone_number_normalization_service.rb @@ -1,23 +1,32 @@ # Service to handle phone number normalization for WhatsApp messages # Currently supports Brazil and Argentina phone number format variations -# Designed to be extensible for additional countries in future PRs -# -# Usage: Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact(waid) +# Supports both WhatsApp Cloud API and Twilio WhatsApp providers class Whatsapp::PhoneNumberNormalizationService def initialize(inbox) @inbox = inbox end - # Main entry point for phone number normalization - # Returns the source_id of an existing contact if found, otherwise returns original waid - def normalize_and_find_contact(waid) - normalizer = find_normalizer_for_country(waid) - return waid unless normalizer + # @param raw_number [String] The phone number in provider-specific format + # - Cloud: "5541988887777" (clean number) + # - Twilio: "whatsapp:+5541988887777" (prefixed format) + # @param provider [Symbol] :cloud or :twilio + # @return [String] Normalized source_id in provider format or original if not found + def normalize_and_find_contact_by_provider(raw_number, provider) + # Extract clean number based on provider format + clean_number = extract_clean_number(raw_number, provider) - normalized_waid = normalizer.normalize(waid) - existing_contact_inbox = find_existing_contact_inbox(normalized_waid) + # Find appropriate normalizer for the country + normalizer = find_normalizer_for_country(clean_number) + return raw_number unless normalizer - existing_contact_inbox&.source_id || waid + # Normalize the clean number + normalized_clean_number = normalizer.normalize(clean_number) + + # Format for provider and check for existing contact + provider_format = format_for_provider(normalized_clean_number, provider) + existing_contact_inbox = find_existing_contact_inbox(provider_format) + + existing_contact_inbox&.source_id || raw_number end private @@ -33,6 +42,26 @@ class Whatsapp::PhoneNumberNormalizationService inbox.contact_inboxes.find_by(source_id: normalized_waid) end + # Extract clean number from provider-specific format + def extract_clean_number(raw_number, provider) + case provider + when :twilio + raw_number.gsub(/^whatsapp:\+/, '') # Remove prefix: "whatsapp:+5541988887777" → "5541988887777" + else + raw_number # Default fallback for unknown providers + end + end + + # Format normalized number for provider-specific storage + def format_for_provider(clean_number, provider) + case provider + when :twilio + "whatsapp:+#{clean_number}" # Add prefix: "5541988887777" → "whatsapp:+5541988887777" + else + clean_number # Default for :cloud and unknown providers: "5541988887777" + end + end + NORMALIZERS = [ Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer, Whatsapp::PhoneNormalizers::ArgentinaPhoneNormalizer diff --git a/spec/factories/channel/twilio_sms.rb b/spec/factories/channel/twilio_sms.rb index 94f632efd..1963a4f28 100644 --- a/spec/factories/channel/twilio_sms.rb +++ b/spec/factories/channel/twilio_sms.rb @@ -13,5 +13,9 @@ FactoryBot.define do sequence(:phone_number) { |n| "+123456789#{n}1" } messaging_service_sid { nil } end + + trait :whatsapp do + medium { :whatsapp } + end end end diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb index d32ef59bb..190a6c45a 100644 --- a/spec/services/twilio/incoming_message_service_spec.rb +++ b/spec/services/twilio/incoming_message_service_spec.rb @@ -402,6 +402,230 @@ describe Twilio::IncomingMessageService do existing_contact.reload expect(existing_contact.name).to eq('Alice Johnson') end + + describe 'When the incoming number is a Brazilian number in new format with 9 included' do + let!(:whatsapp_twilio_channel) do + create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx', + inbox: create(:inbox, account: account, greeting_enabled: false)) + end + + it 'creates appropriate conversations, message and contacts if contact does not exist' do + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+5541988887777', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Brazil', + ProfileName: 'João Silva' + } + + described_class.new(params: params).perform + + expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0) + expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('João Silva') + expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Brazil') + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+5541988887777') + end + + it 'appends to existing contact if contact inbox exists' do + # Create existing contact with same format + normalized_contact = create(:contact, account: account, phone_number: '+5541988887777') + contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+5541988887777', contact: normalized_contact, + inbox: whatsapp_twilio_channel.inbox) + last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox) + + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+5541988887777', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Another message from Brazil', + ProfileName: 'João Silva' + } + + described_class.new(params: params).perform + + # No new conversation should be created + expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1) + # Message appended to the last conversation + expect(last_conversation.messages.last.content).to eq('Another message from Brazil') + end + end + + describe 'When incoming number is a Brazilian number in old format without the 9 included' do + let!(:whatsapp_twilio_channel) do + create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx', + inbox: create(:inbox, account: account, greeting_enabled: false)) + end + + it 'appends to existing contact when contact inbox exists in old format' do + # Create existing contact with old format (12 digits) + old_contact = create(:contact, account: account, phone_number: '+554188887777') + contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+554188887777', contact: old_contact, inbox: whatsapp_twilio_channel.inbox) + last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox) + + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+554188887777', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Brazil old format', + ProfileName: 'Maria Silva' + } + + described_class.new(params: params).perform + + # No new conversation should be created + expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1) + # Message appended to the last conversation + expect(last_conversation.messages.last.content).to eq('Test message from Brazil old format') + end + + it 'appends to existing contact when contact inbox exists in new format' do + # Create existing contact with new format (13 digits) + normalized_contact = create(:contact, account: account, phone_number: '+5541988887777') + contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+5541988887777', contact: normalized_contact, + inbox: whatsapp_twilio_channel.inbox) + last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox) + + # Incoming message with old format (12 digits) + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+554188887777', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Brazil', + ProfileName: 'João Silva' + } + + described_class.new(params: params).perform + + # Should find and use existing contact, not create duplicate + expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1) + # Message appended to the existing conversation + expect(last_conversation.messages.last.content).to eq('Test message from Brazil') + # Should use the existing contact's source_id (normalized format) + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+5541988887777') + end + + it 'creates contact inbox with incoming number when no existing contact' do + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+554188887777', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Brazil', + ProfileName: 'Carlos Silva' + } + + described_class.new(params: params).perform + + expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0) + expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('Carlos Silva') + expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Brazil') + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+554188887777') + end + end + + describe 'When the incoming number is an Argentine number with 9 after country code' do + let!(:whatsapp_twilio_channel) do + create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx', + inbox: create(:inbox, account: account, greeting_enabled: false)) + end + + it 'creates appropriate conversations, message and contacts if contact does not exist' do + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+5491123456789', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Argentina', + ProfileName: 'Carlos Mendoza' + } + + described_class.new(params: params).perform + + expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0) + expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('Carlos Mendoza') + expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Argentina') + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+5491123456789') + end + + it 'appends to existing contact if contact inbox exists with normalized format' do + # Create existing contact with normalized format (without 9 after country code) + normalized_contact = create(:contact, account: account, phone_number: '+541123456789') + contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+541123456789', contact: normalized_contact, + inbox: whatsapp_twilio_channel.inbox) + last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox) + + # Incoming message with 9 after country code + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+5491123456789', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Argentina', + ProfileName: 'Carlos Mendoza' + } + + described_class.new(params: params).perform + + # Should find and use existing contact, not create duplicate + expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1) + # Message appended to the existing conversation + expect(last_conversation.messages.last.content).to eq('Test message from Argentina') + # Should use the normalized source_id from existing contact + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+541123456789') + end + end + + describe 'When incoming number is an Argentine number without 9 after country code' do + let!(:whatsapp_twilio_channel) do + create(:channel_twilio_sms, :whatsapp, account: account, account_sid: 'ACxxx', + inbox: create(:inbox, account: account, greeting_enabled: false)) + end + + it 'appends to existing contact when contact inbox exists with same format' do + # Create existing contact with same format (without 9) + contact = create(:contact, account: account, phone_number: '+541123456789') + contact_inbox = create(:contact_inbox, source_id: 'whatsapp:+541123456789', contact: contact, inbox: whatsapp_twilio_channel.inbox) + last_conversation = create(:conversation, inbox: whatsapp_twilio_channel.inbox, contact_inbox: contact_inbox) + + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+541123456789', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Argentina', + ProfileName: 'Ana García' + } + + described_class.new(params: params).perform + + # No new conversation should be created + expect(whatsapp_twilio_channel.inbox.conversations.count).to eq(1) + # Message appended to the last conversation + expect(last_conversation.messages.last.content).to eq('Test message from Argentina') + end + + it 'creates contact inbox with incoming number when no existing contact' do + params = { + SmsSid: 'SMxx', + From: 'whatsapp:+541123456789', + AccountSid: 'ACxxx', + MessagingServiceSid: whatsapp_twilio_channel.messaging_service_sid, + Body: 'Test message from Argentina', + ProfileName: 'Diego López' + } + + described_class.new(params: params).perform + + expect(whatsapp_twilio_channel.inbox.conversations.count).not_to eq(0) + expect(whatsapp_twilio_channel.inbox.contacts.first.name).to eq('Diego López') + expect(whatsapp_twilio_channel.inbox.messages.first.content).to eq('Test message from Argentina') + expect(whatsapp_twilio_channel.inbox.contact_inboxes.first.source_id).to eq('whatsapp:+541123456789') + end + end end end end From ee1ea9576b37a28bab8fd4a25ae3092745c83a68 Mon Sep 17 00:00:00 2001 From: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:26:04 +0530 Subject: [PATCH 3/3] fix: Timezone offset reports broken by DST transition (#12747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fixes timezone offset parameter in V2 reports API that was broken by DST transitions. The issue occurred when UK DST ended on October 26, 2025, causing the test to fail starting October 27th. ~~**Initial diagnosis:** The root cause was that `timezone_name_from_offset` used `zone.now.utc_offset` to match timezones, which changes based on the current date's DST status rather than the data being queried.~~ **Actual root cause:** The test was accidentally passing before DST transition. During BST, `timezone_name_from_offset(0)` matched "Azores" (UTC-1) instead of "Edinburgh" (UTC+0), and the -1 hour offset coincidentally split midnight data into [1,5]. After DST ended, it correctly matched "Edinburgh" (UTC+0), but this grouped all conversations into one day [6], exposing that the test data was flawed. The real issue: Test data created all 6 conversations starting at midnight on a single day, which cannot produce a [1,5] split in true UTC. Fixes CW-5846 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? **Test that was failing:** ```bash bundle exec rspec spec/controllers/api/v2/accounts/reports_controller_spec.rb:25 ``` **Changes:** ~~1. Fixed `timezone_name_from_offset` to use January 1st as reference date instead of current date~~ ~~2. Converted timezone string to `ActiveSupport::TimeZone` object for `group_by_period` compatibility~~ **Revised approach:** 1. Freeze test time to January 2024 using `travel_to`, making timezone matching deterministic and aligned with test data period 2. Start test conversations at 23:00 instead of midnight to properly span two days and test timezone boundary grouping 3. Keep `zone.now.utc_offset` (correct behavior for real users during DST) **Why this works:** - Test runs "in January 2024" → `zone.now.utc_offset` returns January offsets consistently - Offset `-8` correctly matches Pacific Standard Time (UTC-8 in January) - Real users in PDT (summer) with offset `-7` → correctly match Pacific Daylight Time - No production impact, test is deterministic year-round **Verification:** - Test now passes consistently regardless of current DST status - Timezone matching works correctly for real users during DST periods - Reports correctly group data by timezone offset across all seasons ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Shivam Mishra Co-authored-by: Muhsin Keloth --- .rubocop.yml | 4 +- .../v2/accounts/reports_controller_spec.rb | 60 ++++++++++--------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index e30a71ee9..ea688792b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,7 +23,7 @@ Metrics/MethodLength: - 'enterprise/lib/captain/agent.rb' RSpec/ExampleLength: - Max: 25 + Max: 50 Style/Documentation: Enabled: false @@ -336,4 +336,4 @@ FactoryBot/RedundantFactoryOption: Enabled: false FactoryBot/FactoryAssociationWithStrategy: - Enabled: false \ No newline at end of file + Enabled: false diff --git a/spec/controllers/api/v2/accounts/reports_controller_spec.rb b/spec/controllers/api/v2/accounts/reports_controller_spec.rb index 4dbbcf406..2d505822f 100644 --- a/spec/controllers/api/v2/accounts/reports_controller_spec.rb +++ b/spec/controllers/api/v2/accounts/reports_controller_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do context 'when authenticated and authorized' do before do # Create conversations across 24 hours at different times - base_time = Time.utc(2024, 1, 15, 0, 0) # Start at midnight UTC + base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days # Create conversations every 4 hours across 24 hours 6.times do |i| @@ -23,36 +23,38 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do end it 'timezone_offset affects data grouping and timestamps correctly' do - Time.use_zone('UTC') do - base_time = Time.utc(2024, 1, 15, 0, 0) - base_params = { - metric: 'conversations_count', - type: 'account', - since: (base_time - 1.day).to_i.to_s, - until: (base_time + 2.days).to_i.to_s, - group_by: 'day' - } + travel_to Time.utc(2024, 1, 15, 12, 0) do + Time.use_zone('UTC') do + base_time = Time.utc(2024, 1, 14, 23, 0) # Start at 23:00 to span 2 days + base_params = { + metric: 'conversations_count', + type: 'account', + since: (base_time - 1.day).to_i.to_s, + until: (base_time + 2.days).to_i.to_s, + group_by: 'day' + } - responses = [0, -8, 9].map do |offset| - get "/api/v2/accounts/#{account.id}/reports", - params: base_params.merge(timezone_offset: offset), - headers: admin.create_new_auth_token, as: :json - response.parsed_body + responses = [0, -8, 9].map do |offset| + get "/api/v2/accounts/#{account.id}/reports", + params: base_params.merge(timezone_offset: offset), + headers: admin.create_new_auth_token, as: :json + response.parsed_body + end + + data_entries = responses.map { |r| r.select { |e| e['value'] > 0 } } + totals = responses.map { |r| r.sum { |e| e['value'] } } + timestamps = responses.map { |r| r.map { |e| e['timestamp'] } } + + # Data conservation and redistribution + expect(totals.uniq).to eq([6]) + expect(data_entries[0].map { |e| e['value'] }).to eq([1, 5]) + expect(data_entries[1].map { |e| e['value'] }).to eq([3, 3]) + expect(data_entries[2].map { |e| e['value'] }).to eq([4, 2]) + + # Timestamp differences + expect(timestamps.uniq.size).to eq(3) + timestamps[0].zip(timestamps[1]).each { |utc, pst| expect(utc - pst).to eq(-28_800) } end - - data_entries = responses.map { |r| r.select { |e| e['value'] > 0 } } - totals = responses.map { |r| r.sum { |e| e['value'] } } - timestamps = responses.map { |r| r.map { |e| e['timestamp'] } } - - # Data conservation and redistribution - expect(totals.uniq).to eq([6]) - expect(data_entries[0].map { |e| e['value'] }).to eq([1, 5]) - expect(data_entries[1].map { |e| e['value'] }).to eq([3, 3]) - expect(data_entries[2].map { |e| e['value'] }).to eq([4, 2]) - - # Timestamp differences - expect(timestamps.uniq.size).to eq(3) - timestamps[0].zip(timestamps[1]).each { |utc, pst| expect(utc - pst).to eq(-28_800) } end end