288 lines
9.8 KiB
Go
288 lines
9.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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,omitempty" validate:"omitempty,min=2"`
|
|
AccountName string `json:"account_name,omitempty" validate:"omitempty,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 req.Name == "" {
|
|
req.Name = req.AccountName
|
|
}
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Name == "" {
|
|
return nil, errors.New("account_name is required")
|
|
}
|
|
|
|
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"`
|
|
SupportEmail string `json:"support_email,omitempty"`
|
|
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"`
|
|
AutoResolveAfter int `json:"auto_resolve_after,omitempty" validate:"omitempty,gte=0"`
|
|
AutoResolveMessage string `json:"auto_resolve_message,omitempty"`
|
|
AutoResolveIgnoreWaiting *bool `json:"auto_resolve_ignore_waiting,omitempty"`
|
|
AudioTranscriptions *bool `json:"audio_transcriptions,omitempty"`
|
|
AutoResolveLabel string `json:"auto_resolve_label,omitempty"`
|
|
}
|
|
|
|
// UpdateAccountOnboardingRequest matches Chatwoot's account onboarding payload.
|
|
// Reference: Api::V1::Accounts::OnboardingsController#update.
|
|
type UpdateAccountOnboardingRequest struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Locale *string `json:"locale,omitempty"`
|
|
Industry *string `json:"industry,omitempty"`
|
|
CompanySize *string `json:"company_size,omitempty"`
|
|
Timezone *string `json:"timezone,omitempty"`
|
|
ReferralSource *string `json:"referral_source,omitempty"`
|
|
UserRole *string `json:"user_role,omitempty"`
|
|
Website *string `json:"website,omitempty"`
|
|
}
|
|
|
|
// 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.AutoResolveAfter > 0 {
|
|
account.AutoResolveDuration = req.AutoResolveAfter
|
|
} else if req.AutoResolveDuration > 0 {
|
|
account.AutoResolveDuration = req.AutoResolveDuration
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, account); err != nil {
|
|
return nil, err
|
|
}
|
|
return account, nil
|
|
}
|
|
|
|
// UpdateOnboarding updates account details and onboarding custom attributes.
|
|
func (s *AccountService) UpdateOnboarding(ctx context.Context, id uint, req UpdateAccountOnboardingRequest) (*model.Account, error) {
|
|
account, err := s.repo.FindByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
attrs := account.CustomAttributesMap()
|
|
finalize := attrs["onboarding_step"] == "account_details" || account.OnboardingStep == "account_details"
|
|
|
|
if req.Name != nil {
|
|
account.Name = *req.Name
|
|
}
|
|
if req.Locale != nil {
|
|
account.Locale = *req.Locale
|
|
}
|
|
mergeStringAttribute(attrs, "industry", req.Industry)
|
|
mergeStringAttribute(attrs, "company_size", req.CompanySize)
|
|
mergeStringAttribute(attrs, "timezone", req.Timezone)
|
|
mergeStringAttribute(attrs, "referral_source", req.ReferralSource)
|
|
mergeStringAttribute(attrs, "user_role", req.UserRole)
|
|
mergeStringAttribute(attrs, "website", req.Website)
|
|
if req.Timezone != nil {
|
|
account.Timezone = *req.Timezone
|
|
}
|
|
if finalize {
|
|
delete(attrs, "onboarding_step")
|
|
account.OnboardingStep = ""
|
|
}
|
|
if err := account.SetCustomAttributesMap(attrs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, account); err != nil {
|
|
return nil, err
|
|
}
|
|
return account, nil
|
|
}
|
|
|
|
func mergeStringAttribute(attrs map[string]any, key string, value *string) {
|
|
if value != nil {
|
|
attrs[key] = *value
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
if _, err := s.repo.FindByID(ctx, accountID); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := s.repo.FindAccountUserByUserAndAccount(ctx, accountID, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
keys := map[string]string{
|
|
"label": "0000000000",
|
|
"inbox": "0000000000",
|
|
"team": "0000000000",
|
|
}
|
|
return keys, nil
|
|
}
|