feat: Backend - Companies API endpoint with pagination and search (#12840)

## Description

Adds API endpoint to list companies with pagination, search, and
sorting.

Fixes
https://linear.app/chatwoot/issue/CW-5930/add-backend-routes-to-get-companies-result
Parent issue:
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Added comprehensive specs to
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`:
- Pagination (25 per page, multiple pages)
- Search by name and domain (case-insensitive)
- Counter cache for contacts_count
- Account scoping
- Authorization

To reproduce:
```bash
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb
bundle exec rubocop enterprise/app/controllers/api/v1/accounts/companies_controller.rb
```

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] 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
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Vinay Keerthi
2025-11-18 14:28:56 +05:30
committed by GitHub
parent 7acf3c8817
commit 58ca82c720
15 changed files with 282 additions and 16 deletions

View File

@@ -1,9 +1,29 @@
class Api::V1::Accounts::CompaniesController < Api::V1::Accounts::EnterpriseAccountsController
include Sift
sort_on :name, type: :string
sort_on :domain, type: :string
sort_on :created_at, type: :datetime
RESULTS_PER_PAGE = 25
before_action :check_authorization
before_action :set_current_page, only: [:index, :search]
before_action :fetch_company, only: [:show, :update, :destroy]
def index
@companies = Current.account.companies.ordered_by_name
@companies = fetch_companies(resolved_companies)
@companies_count = @companies.total_count
end
def search
if params[:q].blank?
return render json: { error: I18n.t('errors.companies.search.query_missing') },
status: :unprocessable_entity
end
companies = resolved_companies.search_by_name_or_domain(params[:q])
@companies = fetch_companies(companies)
@companies_count = @companies.total_count
end
def show; end
@@ -24,6 +44,20 @@ class Api::V1::Accounts::CompaniesController < Api::V1::Accounts::EnterpriseAcco
private
def resolved_companies
@resolved_companies ||= Current.account.companies
end
def set_current_page
@current_page = params[:page] || 1
end
def fetch_companies(companies)
filtrate(companies)
.page(@current_page)
.per(RESULTS_PER_PAGE)
end
def check_authorization
raise Pundit::NotAuthorizedError unless ChatwootApp.enterprise?

View File

@@ -2,13 +2,14 @@
#
# Table name: companies
#
# id :bigint not null, primary key
# description :text
# domain :string
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# id :bigint not null, primary key
# contacts_count :integer
# description :text
# domain :string
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
@@ -31,4 +32,7 @@ class Company < ApplicationRecord
has_many :contacts, dependent: :nullify
scope :ordered_by_name, -> { order(:name) }
scope :search_by_name_or_domain, lambda { |query|
where('name ILIKE :search OR domain ILIKE :search', search: "%#{query.strip}%")
}
end

View File

@@ -1,7 +1,7 @@
module Enterprise::Concerns::Contact
extend ActiveSupport::Concern
included do
belongs_to :company, optional: true
belongs_to :company, optional: true, counter_cache: true
after_commit :associate_company_from_email,
on: [:create, :update],

View File

@@ -3,6 +3,10 @@ class CompanyPolicy < ApplicationPolicy
true
end
def search?
true
end
def show?
true
end

View File

@@ -3,12 +3,13 @@ class Contacts::CompanyAssociationService
return nil if skip_association?(contact)
company = find_or_create_company(contact)
# rubocop:disable Rails/SkipsModelValidations
# Intentionally using update_column here to:
# 1. Avoid triggering callbacks
# 2. Improve performance (We're only setting company_id, no need for validation)
contact.update_column(:company_id, company.id) if company
# rubocop:enable Rails/SkipsModelValidations
if company
# rubocop:disable Rails/SkipsModelValidations
# Using update_column and increment_counter to avoid triggering callbacks while maintaining counter cache
contact.update_column(:company_id, company.id)
Company.increment_counter(:contacts_count, company.id)
# rubocop:enable Rails/SkipsModelValidations
end
company
end

View File

@@ -1,5 +1,6 @@
json.id company.id
json.name company.name
json.contacts_count company.contacts_count
json.domain company.domain
json.description company.description
json.avatar_url company.avatar_url

View File

@@ -1,3 +1,8 @@
json.meta do
json.total_count @companies_count
json.page @current_page
end
json.payload do
json.array! @companies do |company|
json.partial! 'company', company: company

View File

@@ -0,0 +1,10 @@
json.meta do
json.total_count @companies_count
json.page @current_page
end
json.payload do
json.array! @companies do |company|
json.partial! 'company', company: company
end
end