317 lines
9.4 KiB
Go
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
|
|
}
|