445 lines
14 KiB
Go
445 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
|
|
"gorm.io/datatypes"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/search"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
|
)
|
|
|
|
// CompanyService implements business logic for Company operations.
|
|
// Reference: Chatwoot app/controllers/api/v1/companies_controller.rb
|
|
type CompanyService struct {
|
|
companyRepo *repository.CompanyRepo
|
|
contactRepo *repository.ContactRepo
|
|
conversationRepo *repository.ConversationRepo
|
|
searchIndexer SearchIndexer
|
|
searchReader CompanySearchReader
|
|
}
|
|
|
|
const CompanyResultsPerPage = 25
|
|
|
|
// NewCompanyService creates a new Company service.
|
|
func NewCompanyService(companyRepo *repository.CompanyRepo, contactRepo *repository.ContactRepo, conversationRepo *repository.ConversationRepo) *CompanyService {
|
|
return &CompanyService{
|
|
companyRepo: companyRepo,
|
|
contactRepo: contactRepo,
|
|
conversationRepo: conversationRepo,
|
|
}
|
|
}
|
|
|
|
func (s *CompanyService) SetSearchIndexer(indexer SearchIndexer) {
|
|
s.searchIndexer = indexer
|
|
}
|
|
|
|
func (s *CompanyService) SetSearchReader(reader CompanySearchReader) {
|
|
s.searchReader = reader
|
|
}
|
|
|
|
func (s *CompanyService) DB() *gorm.DB {
|
|
if s == nil || s.companyRepo == nil {
|
|
return nil
|
|
}
|
|
return s.companyRepo.DB()
|
|
}
|
|
|
|
func (s *CompanyService) indexCompany(ctx context.Context, company *model.Company) {
|
|
if s.searchIndexer != nil {
|
|
logSearchIndexError("company", company.ID, s.searchIndexer.IndexCompany(ctx, company))
|
|
}
|
|
}
|
|
|
|
func (s *CompanyService) deleteCompanyIndex(ctx context.Context, accountID uint, id uint) {
|
|
if s.searchIndexer != nil {
|
|
logSearchIndexError("company", id, s.searchIndexer.DeleteCompany(ctx, accountID, id))
|
|
}
|
|
}
|
|
|
|
// CreateCompanyRequest is the DTO for creating a company.
|
|
type CreateCompanyRequest struct {
|
|
Name string `json:"name" validate:"required,min=1"`
|
|
Description string `json:"description,omitempty"`
|
|
WebsiteURL string `json:"website_url,omitempty"`
|
|
FaviconURL string `json:"favicon_url,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
CustomAttributes datatypes.JSON `json:"custom_attributes,omitempty"`
|
|
Company *CompanyParams `json:"company,omitempty"`
|
|
}
|
|
|
|
type CompanyParams struct {
|
|
Name string `json:"name,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
WebsiteURL string `json:"website_url,omitempty"`
|
|
FaviconURL string `json:"favicon_url,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
CustomAttributes datatypes.JSON `json:"custom_attributes,omitempty"`
|
|
}
|
|
|
|
// UpdateCompanyRequest is the DTO for updating a company.
|
|
type UpdateCompanyRequest struct {
|
|
Name string `json:"name,omitempty" validate:"omitempty,min=1"`
|
|
Description string `json:"description,omitempty"`
|
|
WebsiteURL string `json:"website_url,omitempty"`
|
|
FaviconURL string `json:"favicon_url,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
CustomAttributes datatypes.JSON `json:"custom_attributes,omitempty"`
|
|
Company *CompanyParams `json:"company,omitempty"`
|
|
}
|
|
|
|
// CreateCompanyNoteRequest is the DTO for creating a company note.
|
|
type CreateCompanyNoteRequest struct {
|
|
Content string `json:"content" validate:"required,min=1"`
|
|
}
|
|
|
|
// List retrieves all companies for an account.
|
|
func (s *CompanyService) List(ctx context.Context, accountID uint, offset, limit int, sort string) ([]model.Company, int64, error) {
|
|
return s.companyRepo.ListByAccount(ctx, accountID, offset, limit, sort)
|
|
}
|
|
|
|
// Search searches companies by query with optional sort.
|
|
func (s *CompanyService) Search(ctx context.Context, accountID uint, query string, offset, limit int, sort string, searchMode search.SearchMode) ([]model.Company, int64, error) {
|
|
if query == "" {
|
|
return s.companyRepo.ListByAccount(ctx, accountID, offset, limit, sort)
|
|
}
|
|
if s.searchReader != nil {
|
|
filter := serviceSearchFilter(offset, limit, sort, searchMode, search.ResultTypeCompany)
|
|
results, total, err := s.searchReader.SearchCompanies(ctx, accountID, query, filter)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
companies, err := s.companiesFromSearchResults(ctx, accountID, results)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return companies, total, nil
|
|
}
|
|
return s.companyRepo.Search(ctx, accountID, query, offset, limit, sort, searchMode)
|
|
}
|
|
|
|
func (s *CompanyService) companiesFromSearchResults(ctx context.Context, accountID uint, results []search.SearchResult) ([]model.Company, error) {
|
|
companies := make([]model.Company, 0, len(results))
|
|
for _, result := range results {
|
|
if result.ID == 0 || result.AccountID != accountID {
|
|
continue
|
|
}
|
|
company, err := s.companyRepo.FindByIDAndAccount(ctx, result.ID, accountID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
companies = append(companies, *company)
|
|
}
|
|
return companies, nil
|
|
}
|
|
|
|
// Get retrieves a single company by ID scoped to an account.
|
|
func (s *CompanyService) Get(ctx context.Context, id, accountID uint) (*model.Company, error) {
|
|
company, err := s.companyRepo.FindByIDAndAccount(ctx, id, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return company, nil
|
|
}
|
|
|
|
// Create creates a new company.
|
|
func (s *CompanyService) Create(ctx context.Context, accountID uint, req *CreateCompanyRequest) (*model.Company, error) {
|
|
if req.Company != nil {
|
|
req.Name = req.Company.Name
|
|
req.Description = req.Company.Description
|
|
req.WebsiteURL = req.Company.WebsiteURL
|
|
req.FaviconURL = req.Company.FaviconURL
|
|
req.Domain = req.Company.Domain
|
|
req.CustomAttributes = req.Company.CustomAttributes
|
|
}
|
|
if req.Name == "" {
|
|
return nil, errors.New("name is required")
|
|
}
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
company := &model.Company{
|
|
AccountID: accountID,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
WebsiteURL: req.WebsiteURL,
|
|
FaviconURL: req.FaviconURL,
|
|
Domain: req.Domain,
|
|
CustomAttributes: req.CustomAttributes,
|
|
}
|
|
|
|
if err := s.companyRepo.Create(ctx, company); err != nil {
|
|
applogger.L().Errorf("Create company for account %d: %v", accountID, err)
|
|
return nil, err
|
|
}
|
|
s.indexCompany(ctx, company)
|
|
return company, nil
|
|
}
|
|
|
|
// Update updates a company scoped to an account.
|
|
func (s *CompanyService) Update(ctx context.Context, id, accountID uint, req *UpdateCompanyRequest) (*model.Company, error) {
|
|
if req.Company != nil {
|
|
req.Name = req.Company.Name
|
|
req.Description = req.Company.Description
|
|
req.WebsiteURL = req.Company.WebsiteURL
|
|
req.FaviconURL = req.Company.FaviconURL
|
|
req.Domain = req.Company.Domain
|
|
req.CustomAttributes = req.Company.CustomAttributes
|
|
}
|
|
company, err := s.companyRepo.FindByIDAndAccount(ctx, id, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.Name != "" {
|
|
company.Name = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
company.Description = req.Description
|
|
}
|
|
if req.WebsiteURL != "" {
|
|
company.WebsiteURL = req.WebsiteURL
|
|
}
|
|
if req.FaviconURL != "" {
|
|
company.FaviconURL = req.FaviconURL
|
|
}
|
|
if req.Domain != "" {
|
|
company.Domain = req.Domain
|
|
}
|
|
if req.CustomAttributes != nil {
|
|
company.CustomAttributes = mergeJSON(company.CustomAttributes, req.CustomAttributes)
|
|
}
|
|
|
|
if err := pkgvalidator.ValidateStruct(company); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.companyRepo.Update(ctx, company); err != nil {
|
|
applogger.L().Errorf("Update company %d for account %d: %v", id, accountID, err)
|
|
return nil, err
|
|
}
|
|
s.indexCompany(ctx, company)
|
|
return company, nil
|
|
}
|
|
|
|
// Delete deletes a company scoped to an account.
|
|
func (s *CompanyService) Delete(ctx context.Context, id, accountID uint) error {
|
|
company, err := s.companyRepo.FindByIDAndAccount(ctx, id, accountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if company == nil {
|
|
return errors.New("company not found")
|
|
}
|
|
if err := s.companyRepo.Delete(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
s.deleteCompanyIndex(ctx, accountID, id)
|
|
return nil
|
|
}
|
|
|
|
// ListContacts retrieves contacts associated with a company.
|
|
func (s *CompanyService) ListContacts(ctx context.Context, companyID, accountID uint, offset, limit int) ([]model.Contact, int64, error) {
|
|
// Verify company exists and belongs to the account
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return s.companyRepo.ListContacts(ctx, companyID, accountID, offset, limit)
|
|
}
|
|
|
|
func (s *CompanyService) SearchContacts(ctx context.Context, companyID, accountID uint, query string, offset, limit int) ([]model.Contact, int64, error) {
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if query == "" {
|
|
return nil, 0, errors.New("query is required")
|
|
}
|
|
return s.companyRepo.SearchAssignableContacts(ctx, companyID, accountID, query, offset, limit)
|
|
}
|
|
|
|
func (s *CompanyService) GetContact(ctx context.Context, companyID, accountID, contactID uint) (*model.Contact, error) {
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.contactRepo.FindByAccountAndID(ctx, accountID, contactID)
|
|
}
|
|
|
|
// ListConversations retrieves conversations for all contacts of a company.
|
|
func (s *CompanyService) ListConversations(ctx context.Context, companyID, accountID uint, offset, limit int) ([]model.Conversation, int64, error) {
|
|
// Verify company exists and belongs to the account
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Get contact IDs for this company via the join table
|
|
contacts, _, err := s.companyRepo.ListContacts(ctx, companyID, accountID, 0, 1000)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if len(contacts) == 0 {
|
|
return []model.Conversation{}, 0, nil
|
|
}
|
|
|
|
contactIDs := make([]uint, len(contacts))
|
|
for i, c := range contacts {
|
|
contactIDs[i] = c.ID
|
|
}
|
|
|
|
return s.conversationRepo.FindByContactIDs(ctx, accountID, contactIDs, offset, limit)
|
|
}
|
|
|
|
// ListNotes retrieves notes for a company.
|
|
func (s *CompanyService) ListNotes(ctx context.Context, companyID, accountID uint, offset, limit int) ([]model.CompanyNote, int64, error) {
|
|
// Verify company exists and belongs to the account
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return s.companyRepo.ListNotes(ctx, companyID, offset, limit)
|
|
}
|
|
|
|
// CreateNote creates a note for a company.
|
|
func (s *CompanyService) CreateNote(ctx context.Context, companyID, accountID, userID uint, req *CreateCompanyNoteRequest) (*model.CompanyNote, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify company exists and belongs to the account
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
note := &model.CompanyNote{
|
|
CompanyID: companyID,
|
|
UserID: userID,
|
|
Content: req.Content,
|
|
}
|
|
|
|
if err := s.companyRepo.CreateNote(ctx, note); err != nil {
|
|
applogger.L().Errorf("Create note for company %d: %v", companyID, err)
|
|
return nil, err
|
|
}
|
|
return note, nil
|
|
}
|
|
|
|
// DeleteNote deletes a note from a company.
|
|
func (s *CompanyService) DeleteNote(ctx context.Context, noteID, companyID, accountID uint) error {
|
|
// Verify company exists and belongs to the account
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.companyRepo.DeleteNote(ctx, companyID, noteID); err != nil {
|
|
applogger.L().Errorf("Delete note %d for company %d: %v", noteID, companyID, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddContact adds a contact to a company.
|
|
func (s *CompanyService) AddContact(ctx context.Context, companyID, accountID, contactID uint) error {
|
|
// Verify company exists and belongs to the account
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify contact exists and belongs to the account
|
|
_, err = s.contactRepo.FindByAccountAndID(ctx, accountID, contactID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.companyRepo.AddContact(ctx, companyID, contactID); err != nil {
|
|
applogger.L().Errorf("Add contact %d to company %d: %v", contactID, companyID, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *CompanyService) DestroyCustomAttributes(ctx context.Context, companyID, accountID uint, keys []string) (*model.Company, error) {
|
|
company, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
attrs := map[string]any{}
|
|
if len(company.CustomAttributes) > 0 {
|
|
_ = json.Unmarshal(company.CustomAttributes, &attrs)
|
|
}
|
|
for _, key := range keys {
|
|
delete(attrs, key)
|
|
}
|
|
bytes, _ := json.Marshal(attrs)
|
|
company.CustomAttributes = datatypes.JSON(bytes)
|
|
if err := s.companyRepo.Update(ctx, company); err != nil {
|
|
return nil, err
|
|
}
|
|
s.indexCompany(ctx, company)
|
|
return company, nil
|
|
}
|
|
|
|
func (s *CompanyService) DeleteAvatar(ctx context.Context, companyID, accountID uint) (*model.Company, error) {
|
|
company, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
company.FaviconURL = ""
|
|
if err := s.companyRepo.Update(ctx, company); err != nil {
|
|
return nil, err
|
|
}
|
|
s.indexCompany(ctx, company)
|
|
return company, nil
|
|
}
|
|
|
|
func mergeJSON(current datatypes.JSON, incoming datatypes.JSON) datatypes.JSON {
|
|
if len(incoming) == 0 {
|
|
return current
|
|
}
|
|
merged := map[string]any{}
|
|
if len(current) > 0 {
|
|
_ = json.Unmarshal(current, &merged)
|
|
}
|
|
incomingMap := map[string]any{}
|
|
_ = json.Unmarshal(incoming, &incomingMap)
|
|
for key, value := range incomingMap {
|
|
merged[key] = value
|
|
}
|
|
bytes, _ := json.Marshal(merged)
|
|
return datatypes.JSON(bytes)
|
|
}
|
|
|
|
// RemoveContact removes a contact from a company.
|
|
func (s *CompanyService) RemoveContact(ctx context.Context, companyID, accountID, contactID uint) error {
|
|
// Verify company exists and belongs to the account
|
|
_, err := s.companyRepo.FindByIDAndAccount(ctx, companyID, accountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify contact exists and belongs to the account
|
|
_, err = s.contactRepo.FindByAccountAndID(ctx, accountID, contactID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.companyRepo.RemoveContact(ctx, companyID, contactID); err != nil {
|
|
applogger.L().Errorf("Remove contact %d from company %d: %v", contactID, companyID, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|