package service import ( "context" "errors" "fmt" "time" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" pkgvalidator "github.com/gochat/gochat/pkg/validator" ) // AccountService implements business logic for Account operations. // Reference: Chatwoot app/controllers/api/v1/accounts_controller.rb type AccountService struct { repo *repository.AccountRepo } // NewAccountService creates a new Account service. func NewAccountService(repo *repository.AccountRepo) *AccountService { return &AccountService{repo: repo} } // ListByUser retrieves all accounts accessible by a user. func (s *AccountService) ListByUser(ctx context.Context, userID uint, offset, limit int) ([]model.Account, int64, error) { return s.repo.FindByUser(ctx, userID, offset, limit) } // GetByID retrieves a single account. func (s *AccountService) GetByID(ctx context.Context, id uint) (*model.Account, error) { return s.repo.FindByID(ctx, id) } // CreateAccountRequest is the DTO for creating an account. type CreateAccountRequest struct { Name string `json:"name" validate:"required,min=2"` Locale string `json:"locale,omitempty" validate:"omitempty,len=2"` Domain string `json:"domain,omitempty" validate:"omitempty,min=3"` } // Create creates a new account and assigns the creator as administrator. func (s *AccountService) Create(ctx context.Context, userID uint, req CreateAccountRequest) (*model.Account, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } account := &model.Account{ Name: req.Name, Locale: req.Locale, Domain: req.Domain, Status: "active", } if err := s.repo.Create(ctx, account); err != nil { applogger.L().Errorf("Failed to create account: %v", err) return nil, err } // Assign creator as administrator if err := s.repo.AddUserToAccount(ctx, account.ID, userID, "administrator"); err != nil { applogger.L().Errorf("Failed to assign creator to account: %v", err) return nil, err } return account, nil } // UpdateAccountRequest is the DTO for updating an account. type UpdateAccountRequest struct { Name string `json:"name,omitempty" validate:"omitempty,min=2"` Locale string `json:"locale,omitempty" validate:"omitempty,len=2"` Domain string `json:"domain,omitempty" validate:"omitempty,min=3"` FeatureFlags string `json:"feature_flags,omitempty"` Status string `json:"status,omitempty" validate:"omitempty,oneof=active inactive"` AutoResolveDuration int `json:"auto_resolve_duration,omitempty" validate:"omitempty,gte=0"` } // Update modifies an existing account. func (s *AccountService) Update(ctx context.Context, id uint, req UpdateAccountRequest) (*model.Account, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } account, err := s.repo.FindByID(ctx, id) if err != nil { return nil, err } if req.Name != "" { account.Name = req.Name } if req.Locale != "" { account.Locale = req.Locale } if req.Domain != "" { account.Domain = req.Domain } if req.FeatureFlags != "" { account.FeatureFlags = req.FeatureFlags } if req.Status != "" { account.Status = req.Status } if req.AutoResolveDuration > 0 { account.AutoResolveDuration = req.AutoResolveDuration } if err := s.repo.Update(ctx, account); err != nil { return nil, err } return account, nil } // Delete soft-deletes an account. func (s *AccountService) Delete(ctx context.Context, id uint) error { if s.repo == nil { return errors.New("account repository is not initialized") } return s.repo.Delete(ctx, id) } // ListUsers retrieves all users belonging to an account. func (s *AccountService) ListUsers(ctx context.Context, accountID uint, offset, limit int) ([]model.User, int64, error) { return s.repo.FindUsersByAccount(ctx, accountID, offset, limit) } // AddUserRequest is the DTO for adding a user to an account. type AddUserRequest struct { UserID uint `json:"user_id" validate:"required"` Role string `json:"role" validate:"required,oneof=agent administrator"` } // AddUser adds a user to an account with a specified role. func (s *AccountService) AddUser(ctx context.Context, accountID uint, req AddUserRequest) error { if err := pkgvalidator.ValidateStruct(req); err != nil { return err } return s.repo.AddUserToAccount(ctx, accountID, req.UserID, req.Role) } // RemoveUser removes a user from an account. func (s *AccountService) RemoveUser(ctx context.Context, accountID, userID uint) error { if accountID == 0 || userID == 0 { return errors.New("account_id and user_id are required") } return s.repo.RemoveUserFromAccount(ctx, accountID, userID) } // UpdateAccountSettingsRequest is the DTO for updating account settings. type UpdateAccountSettingsRequest struct { AutoResolveDuration int `json:"auto_resolve_duration" validate:"gte=0"` Locale string `json:"locale" validate:"omitempty,len=2"` } // UpdateSettings updates account-level settings. func (s *AccountService) UpdateSettings(ctx context.Context, id uint, req UpdateAccountSettingsRequest) (*model.Account, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } account, err := s.repo.FindByID(ctx, id) if err != nil { return nil, err } account.AutoResolveDuration = req.AutoResolveDuration if req.Locale != "" { account.Locale = req.Locale } if err := s.repo.Update(ctx, account); err != nil { return nil, err } return account, nil } // GetAll retrieves all accounts with pagination. // Reference: Chatwoot platform admin listing all accounts. func (s *AccountService) GetAll(ctx context.Context, offset, limit int) ([]model.Account, int64, error) { return s.repo.FindAll(ctx, offset, limit) } // GetAgents retrieves all agents (AccountUser records) for an account with pagination. // Reference: Chatwoot app/controllers/api/v1/accounts/agents_controller.rb#index func (s *AccountService) GetAgents(ctx context.Context, accountID uint, offset, limit int) ([]model.AccountUser, int64, error) { return s.repo.FindAgentsByAccount(ctx, accountID, offset, limit) } // --- Account extension operations (G8) --- // Reference: Chatwoot accounts_controller.rb#update_active_at, #cache_keys // UpdateActiveAt updates the active_at timestamp for a user in an account. func (s *AccountService) UpdateActiveAt(ctx context.Context, accountID, userID uint) error { return s.repo.UpdateActiveAt(ctx, accountID, userID, time.Now()) } // CacheKeys returns cache key identifiers for frontend cache invalidation. // The keys are derived from the account's updatedAt timestamp and user membership. // Reference: Chatwoot accounts_controller.rb#cache_keys func (s *AccountService) CacheKeys(ctx context.Context, accountID, userID uint) (map[string]string, error) { account, err := s.repo.FindByID(ctx, accountID) if err != nil { return nil, err } au, err := s.repo.FindAccountUserByUserAndAccount(ctx, accountID, userID) if err != nil { return nil, err } keys := map[string]string{ "account": fmt.Sprintf("account_%d_%d", accountID, account.UpdatedAt.Unix()), "account_user": fmt.Sprintf("account_user_%d_%d_%d", accountID, userID, au.UpdatedAt.Unix()), } return keys, nil }