Files
gochat/internal/repository/agent_repo.go
T

317 lines
9.4 KiB
Go

package repository
import (
"context"
"errors"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
)
// ErrAlreadyMember indicates the user is already a member of the account.
var ErrAlreadyMember = errors.New("user is already a member of this account")
// AgentRepo implements GORM repository for Agent (AccountUser + User) operations.
// Reference: Chatwoot app/controllers/api/v1/accounts/agents_controller.rb
// In Chatwoot, an "agent" is a User who belongs to an Account via AccountUser.
// The agents controller returns User objects with their AccountUser association.
type AgentRepo struct {
db *gorm.DB
}
// NewAgentRepo creates a new Agent repository.
func NewAgentRepo(db *gorm.DB) *AgentRepo {
return &AgentRepo{db: db}
}
// AgentDetail represents an agent as returned by the API — User with their
// AccountUser role/availability for the specific account.
// Reference: Chatwoot renders User objects with included account_users.
type AgentDetail struct {
model.User
Role string `json:"role"`
Availability string `json:"availability"`
AutoOffline bool `json:"auto_offline"`
InvitedBy uint `json:"invited_by"`
AccountUserID uint `json:"account_user_id"`
CustomRoleID uint `json:"custom_role_id,omitempty"`
}
// ListByAccount retrieves all agents (users) for an account with pagination.
// Reference: Chatwoot agents_controller.rb#index → Current.account.users
// Returns User objects joined with their AccountUser association for the account.
func (r *AgentRepo) ListByAccount(ctx context.Context, accountID uint, offset, limit int) ([]AgentDetail, int64, error) {
var count int64
if err := r.db.WithContext(ctx).
Model(&model.AccountUser{}).
Where("account_users.account_id = ?", accountID).
Count(&count).Error; err != nil {
return nil, 0, err
}
var accountUsers []model.AccountUser
if err := r.db.WithContext(ctx).
Model(&model.AccountUser{}).
Joins("JOIN users ON users.id = account_users.user_id").
Where("account_users.account_id = ?", accountID).
Order("lower(users.name) ASC").
Order("users.id ASC").
Offset(offset).Limit(limit).
Find(&accountUsers).Error; err != nil {
return nil, 0, err
}
if len(accountUsers) == 0 {
return []AgentDetail{}, count, nil
}
// Collect user IDs
userIDs := make([]uint, len(accountUsers))
for i, au := range accountUsers {
userIDs[i] = au.UserID
}
// Fetch users
var users []model.User
if err := r.db.WithContext(ctx).
Where("id IN ?", userIDs).
Find(&users).Error; err != nil {
return nil, 0, err
}
userMap := make(map[uint]model.User, len(users))
for _, u := range users {
userMap[u.ID] = u
}
// Build AgentDetail list in Chatwoot's order_by_full_name order.
details := make([]AgentDetail, 0, len(accountUsers))
for _, au := range accountUsers {
u, ok := userMap[au.UserID]
if !ok {
continue
}
d := AgentDetail{
User: u,
Role: au.Role,
Availability: au.Availability,
AutoOffline: au.AutoOffline,
InvitedBy: au.InvitedBy,
AccountUserID: au.ID,
CustomRoleID: au.CustomRoleID,
}
details = append(details, d)
}
return details, count, nil
}
// FindAgentByID retrieves a single agent (user) by ID scoped to an account.
// Reference: Chatwoot agents_controller.rb#fetch_agent → agents.find(params[:id])
func (r *AgentRepo) FindAgentByID(ctx context.Context, userID, accountID uint) (*AgentDetail, error) {
var au model.AccountUser
if err := r.db.WithContext(ctx).
Where("account_id = ? AND user_id = ?", accountID, userID).
First(&au).Error; err != nil {
return nil, err
}
var user model.User
if err := r.db.WithContext(ctx).First(&user, userID).Error; err != nil {
return nil, err
}
return &AgentDetail{
User: user,
Role: au.Role,
Availability: au.Availability,
AutoOffline: au.AutoOffline,
InvitedBy: au.InvitedBy,
AccountUserID: au.ID,
CustomRoleID: au.CustomRoleID,
}, nil
}
// CreateAgent adds a user to an account (creates AccountUser).
// Reference: Chatwoot agents_controller.rb#create → AgentBuilder.new.perform
// If the user does not exist, creates the user first, then creates the AccountUser.
func (r *AgentRepo) CreateAgent(ctx context.Context, accountID uint, inviterID uint, name, email, role, availability string, autoOffline bool, customRoleID uint) (*AgentDetail, error) {
// Find or create the user
var user model.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err == gorm.ErrRecordNotFound {
// Create new user
if name == "" {
name = email
if atIdx := indexOfAt(email); atIdx > 0 {
name = email[:atIdx]
}
}
user = model.User{
Name: name,
Email: email,
Provider: "email",
Active: true,
}
if err := r.db.WithContext(ctx).Create(&user).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
// Check if AccountUser already exists
var existingAU model.AccountUser
err = r.db.WithContext(ctx).
Where("account_id = ? AND user_id = ?", accountID, user.ID).
First(&existingAU).Error
if err == nil {
// Already a member — return conflict error
return nil, ErrAlreadyMember
} else if err != gorm.ErrRecordNotFound {
return nil, err
}
// Create AccountUser
au := model.AccountUser{
UserID: user.ID,
AccountID: accountID,
Role: role,
CustomRoleID: customRoleID,
Availability: availability,
AutoOffline: autoOffline,
InvitedBy: inviterID,
}
if err := r.db.WithContext(ctx).Select("UserID", "AccountID", "Role", "CustomRoleID", "Availability", "AutoOffline", "InvitedBy").Create(&au).Error; err != nil {
return nil, err
}
return &AgentDetail{
User: user,
Role: au.Role,
Availability: au.Availability,
AutoOffline: au.AutoOffline,
InvitedBy: au.InvitedBy,
AccountUserID: au.ID,
CustomRoleID: au.CustomRoleID,
}, nil
}
// UpdateAgent updates both the User (name) and AccountUser (role, availability, auto_offline).
// Reference: Chatwoot agents_controller.rb#update → agent.update!(name) + current_account_user.update!(role, availability, auto_offline)
func (r *AgentRepo) UpdateAgent(ctx context.Context, userID, accountID uint, name, role, availability string, autoOffline bool, autoOfflineSet bool, customRoleID *uint, customRoleIDSet bool) (*AgentDetail, error) {
// Update user name if provided
if name != "" {
if err := r.db.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", userID).
Update("name", name).Error; err != nil {
return nil, err
}
}
// Update AccountUser attributes
updates := map[string]interface{}{}
if role != "" {
updates["role"] = role
}
if availability != "" {
updates["availability"] = availability
}
if customRoleIDSet {
if customRoleID == nil {
updates["custom_role_id"] = 0
} else {
updates["custom_role_id"] = *customRoleID
}
}
if autoOfflineSet {
updates["auto_offline"] = autoOffline
}
if len(updates) > 0 {
if err := r.db.WithContext(ctx).
Model(&model.AccountUser{}).
Where("account_id = ? AND user_id = ?", accountID, userID).
Updates(updates).Error; err != nil {
return nil, err
}
}
return r.FindAgentByID(ctx, userID, accountID)
}
// DeleteAgent removes the AccountUser association (removes agent from account).
// Reference: Chatwoot agents_controller.rb#destroy → current_account_user.destroy!
// If the user has no other account memberships, deletes the user record too.
func (r *AgentRepo) DeleteAgent(ctx context.Context, userID, accountID uint) error {
// Delete the AccountUser
if err := r.db.WithContext(ctx).
Where("account_id = ? AND user_id = ?", accountID, userID).
Delete(&model.AccountUser{}).Error; err != nil {
return err
}
// Check if user still has other account memberships
var remainingCount int64
r.db.WithContext(ctx).
Model(&model.AccountUser{}).
Where("user_id = ?", userID).
Count(&remainingCount)
// If no remaining memberships, delete user record
// Reference: Chatwoot agents_controller.rb#delete_user_record
if remainingCount == 0 {
if err := r.db.WithContext(ctx).Delete(&model.User{}, userID).Error; err != nil {
return err
}
}
return nil
}
// BulkCreateAgents adds multiple agents to an account by email.
// Reference: Chatwoot agents_controller.rb#bulk_create
// For each email: find or create user, then create AccountUser.
// Silently skips emails that fail (duplicate, invalid, etc.) — matches Chatwoot behavior.
func (r *AgentRepo) BulkCreateAgents(ctx context.Context, accountID uint, inviterID uint, emails []string) ([]AgentDetail, error) {
results := make([]AgentDetail, 0, len(emails))
for _, email := range emails {
name := email // Default name to email; Chatwoot uses email.split('@').first
atIdx := indexOfAt(email)
if atIdx > 0 {
name = email[:atIdx]
}
detail, err := r.CreateAgent(ctx, accountID, inviterID, name, email, "agent", "offline", false, 0)
if err != nil {
// Silently skip — Chatwoot rescues ActiveRecord::RecordInvalid and logs
continue
}
results = append(results, *detail)
}
return results, nil
}
// indexOfAt returns the index of '@' in the email string.
func indexOfAt(email string) int {
for i, c := range email {
if c == '@' {
return i
}
}
return -1
}
// CountByAccount returns the number of agents in an account.
func (r *AgentRepo) CountByAccount(ctx context.Context, accountID uint) (int64, error) {
var count int64
err := r.db.WithContext(ctx).
Model(&model.AccountUser{}).
Where("account_id = ?", accountID).
Count(&count).Error
return count, err
}