238 lines
8.3 KiB
Go
238 lines
8.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/pubsub"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// AccountUserService implements business logic for AccountUser operations.
|
|
// 1:1 Chatwoot: app/models/account_user.rb
|
|
// Key behaviors: after_create (notify + create_notification_setting),
|
|
// after_destroy (notify + remove_user_from_account),
|
|
// after_save (update_presence_in_redis on availability change),
|
|
// validates user_id uniqueness scoped to account_id
|
|
type AccountUserService struct {
|
|
repo *repository.AccountUserRepo
|
|
notificationRepo *repository.NotificationSettingRepo
|
|
accountRepo *repository.AccountRepo
|
|
userRepo *repository.UserRepo
|
|
eventBus *pubsub.EventBus
|
|
}
|
|
|
|
// NewAccountUserService creates a new AccountUser service.
|
|
func NewAccountUserService(
|
|
repo *repository.AccountUserRepo,
|
|
notificationRepo *repository.NotificationSettingRepo,
|
|
accountRepo *repository.AccountRepo,
|
|
userRepo *repository.UserRepo,
|
|
eventBus *pubsub.EventBus,
|
|
) *AccountUserService {
|
|
return &AccountUserService{
|
|
repo: repo,
|
|
notificationRepo: notificationRepo,
|
|
accountRepo: accountRepo,
|
|
userRepo: userRepo,
|
|
eventBus: eventBus,
|
|
}
|
|
}
|
|
|
|
// CreateAccountUserRequest is the DTO for adding a user to an account.
|
|
// 1:1 Chatwoot: AccountUsersController#create
|
|
type CreateAccountUserRequest struct {
|
|
UserID uint `json:"user_id" validate:"required"`
|
|
Role string `json:"role" validate:"required,oneof=agent administrator"`
|
|
InviterID uint `json:"inviter_id,omitempty"` // user who invited
|
|
}
|
|
|
|
// AddUserToAccount adds a user to an account with a specified role.
|
|
// 1:1 Chatwoot: after_create_commit — notify_creation + create_notification_setting
|
|
func (s *AccountUserService) AddUserToAccount(ctx context.Context, accountID uint, req *CreateAccountUserRequest) (*model.AccountUser, error) {
|
|
// 1:1 Chatwoot: validates user_id uniqueness scoped to account_id
|
|
existing, err := s.repo.FindByAccountAndUser(ctx, accountID, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existing != nil {
|
|
return nil, fmt.Errorf("user %d already belongs to account %d", req.UserID, accountID)
|
|
}
|
|
|
|
// Verify account exists
|
|
_, err = s.accountRepo.FindByID(ctx, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account not found: %v", err)
|
|
}
|
|
|
|
// Verify user exists
|
|
_, err = s.userRepo.FindByID(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %v", err)
|
|
}
|
|
|
|
au := &model.AccountUser{
|
|
AccountID: accountID,
|
|
UserID: req.UserID,
|
|
Role: req.Role,
|
|
InvitedBy: req.InviterID,
|
|
Availability: "offline", // 1:1 Chatwoot: default availability
|
|
AutoOffline: true, // 1:1 Chatwoot: default auto_offline
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, au); err != nil {
|
|
applogger.L().Errorf("Failed to add user %d to account %d: %v", req.UserID, accountID, err)
|
|
return nil, err
|
|
}
|
|
|
|
// 1:1 Chatwoot: after_create_commit — create_notification_setting
|
|
// Default notification settings: email_conversation_assignment + push_conversation_assignment
|
|
if err := s.createDefaultNotificationSetting(ctx, accountID, req.UserID); err != nil {
|
|
applogger.L().Errorf("Failed to create notification setting for user %d in account %d: %v", req.UserID, accountID, err)
|
|
// Non-blocking: don't fail the whole operation
|
|
}
|
|
|
|
// 1:1 Chatwoot: after_create_commit — publish event
|
|
if s.eventBus != nil {
|
|
payload, _ := json.Marshal(map[string]interface{}{
|
|
"account_id": accountID,
|
|
"user_id": req.UserID,
|
|
"role": req.Role,
|
|
"inviter_id": req.InviterID,
|
|
})
|
|
if err := s.eventBus.Publish(pubsub.TopicAccountUserCreated, payload); err != nil {
|
|
applogger.L().Errorf("Failed to publish AccountUserCreated event: %v", err)
|
|
}
|
|
}
|
|
|
|
return au, nil
|
|
}
|
|
|
|
// RemoveUserFromAccount removes a user from an account.
|
|
// 1:1 Chatwoot: after_destroy — notify_deletion + remove_user_from_account
|
|
func (s *AccountUserService) RemoveUserFromAccount(ctx context.Context, accountID, userID uint) error {
|
|
au, err := s.repo.FindByAccountAndUserOrFail(ctx, accountID, userID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fmt.Errorf("user %d not found in account %d", userID, accountID)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err := s.repo.Delete(ctx, au.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 1:1 Chatwoot: after_destroy — publish event
|
|
if s.eventBus != nil {
|
|
payload, _ := json.Marshal(map[string]interface{}{
|
|
"account_id": accountID,
|
|
"user_id": userID,
|
|
})
|
|
if err := s.eventBus.Publish(pubsub.TopicAccountUserDestroyed, payload); err != nil {
|
|
applogger.L().Errorf("Failed to publish AccountUserDestroyed event: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateAvailability updates a user's availability status in an account.
|
|
// 1:1 Chatwoot: after_save — update_presence_in_redis on availability change
|
|
func (s *AccountUserService) UpdateAvailability(ctx context.Context, accountID, userID uint, availability string) error {
|
|
if availability != "online" && availability != "offline" && availability != "busy" {
|
|
return fmt.Errorf("invalid availability: %s (must be online/offline/busy)", availability)
|
|
}
|
|
|
|
au, err := s.repo.FindByAccountAndUserOrFail(ctx, accountID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldAvailability := au.Availability
|
|
if err := s.repo.UpdateAvailability(ctx, accountID, userID, availability); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 1:1 Chatwoot: after_save — update_presence_in_redis if saved_change_to_availability
|
|
if oldAvailability != availability {
|
|
applogger.L().Infof("AccountUser availability changed: user %d in account %d from %s to %s", userID, accountID, oldAvailability, availability)
|
|
// Redis presence update will be handled by event bus in S10
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateRole changes a user's role in an account.
|
|
func (s *AccountUserService) UpdateRole(ctx context.Context, accountID, userID uint, role string) error {
|
|
if role != "agent" && role != "administrator" {
|
|
return fmt.Errorf("invalid role: %s (must be agent/administrator)", role)
|
|
}
|
|
|
|
au, err := s.repo.FindByAccountAndUserOrFail(ctx, accountID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.repo.UpdateRole(ctx, au.ID, role)
|
|
}
|
|
|
|
// ListByAccount retrieves all account users with pagination.
|
|
func (s *AccountUserService) ListByAccount(ctx context.Context, accountID uint, offset, limit int) ([]model.AccountUser, int64, error) {
|
|
return s.repo.FindByAccount(ctx, accountID, offset, limit)
|
|
}
|
|
|
|
// GetByAccountAndUser retrieves a specific account user membership.
|
|
func (s *AccountUserService) GetByAccountAndUser(ctx context.Context, accountID, userID uint) (*model.AccountUser, error) {
|
|
au, err := s.repo.FindByAccountAndUser(ctx, accountID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if au == nil {
|
|
return nil, fmt.Errorf("user %d not found in account %d", userID, accountID)
|
|
}
|
|
return au, nil
|
|
}
|
|
|
|
// createDefaultNotificationSetting creates the default notification setting
|
|
// for a newly added account user.
|
|
// 1:1 Chatwoot: AccountUser#create_notification_setting
|
|
func (s *AccountUserService) createDefaultNotificationSetting(ctx context.Context, accountID, userID uint) error {
|
|
if s.notificationRepo == nil {
|
|
return nil // repo not available, skip
|
|
}
|
|
|
|
// 1:1 Chatwoot: email_conversation_assignment = bit 1 → 2
|
|
// push_conversation_assignment = bit 1 → 2
|
|
setting := &model.NotificationSetting{
|
|
UserID: userID,
|
|
AccountID: accountID,
|
|
EmailFlags: 2, // bit 1 = email_conversation_assignment
|
|
PushFlags: 2, // bit 1 = push_conversation_assignment
|
|
}
|
|
|
|
_, err := s.notificationRepo.Create(setting)
|
|
return err
|
|
}
|
|
|
|
// MarkActive updates active_at timestamp for a user in an account.
|
|
func (s *AccountUserService) MarkActive(ctx context.Context, accountID, userID uint) error {
|
|
return s.repo.UpdateActiveAt(ctx, accountID, userID)
|
|
}
|
|
|
|
// SetAutoOffline updates the auto_offline setting.
|
|
func (s *AccountUserService) SetAutoOffline(ctx context.Context, accountID, userID uint, autoOffline bool) error {
|
|
return s.repo.UpdateAutoOffline(ctx, accountID, userID, autoOffline)
|
|
}
|
|
|
|
// FindOnlineAgents returns all online agents for an account.
|
|
func (s *AccountUserService) FindOnlineAgents(ctx context.Context, accountID uint) ([]model.AccountUser, error) {
|
|
return s.repo.FindOnlineAgentsByAccount(ctx, accountID)
|
|
}
|