package service import ( "context" "fmt" "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" ) // ProfileService implements business logic for user profile operations. // Reference: Chatwoot app/controllers/api/v1/profile_controller.rb type ProfileService struct { userRepo *repository.UserRepo accountUserRepo *repository.AccountUserRepo } // NewProfileService creates a new Profile service. func NewProfileService(userRepo *repository.UserRepo, accountUserRepo *repository.AccountUserRepo) *ProfileService { return &ProfileService{userRepo: userRepo, accountUserRepo: accountUserRepo} } // UpdateProfileRequest is the DTO for updating user profile. // Reference: Chatwoot profiles_controller#update — params wrapped in "profile" key type UpdateProfileRequest struct { Name string `json:"name,omitempty" validate:"omitempty,min=2"` Email string `json:"email,omitempty" validate:"omitempty,email"` AvatarURL string `json:"avatar_url,omitempty"` Availability string `json:"availability,omitempty" validate:"omitempty,oneof=online offline busy"` } // ProfileUpdatePayload wraps UpdateProfileRequest under the "profile" key, // matching Chatwoot's params.require(:profile).permit(:name, :email, ...) type ProfileUpdatePayload struct { Profile UpdateProfileRequest `json:"profile" validate:"required"` } // UpdateAvatarRequest is the DTO for updating user avatar. type UpdateAvatarRequest struct { AvatarURL string `json:"avatar_url" validate:"required"` } // AvailabilityRequest is the DTO for updating user availability per account. // Reference: Chatwoot profiles_controller#availability — params wrapped in "profile" key type AvailabilityRequest struct { AccountID uint `json:"account_id" validate:"required"` Availability string `json:"availability" validate:"required,oneof=online offline busy"` } // ProfileAvailabilityPayload wraps AvailabilityRequest under the "profile" key, // matching Chatwoot's params.require(:profile).permit(:account_id, :availability) type ProfileAvailabilityPayload struct { Profile AvailabilityRequest `json:"profile" validate:"required"` } // AutoOfflineRequest is the DTO for updating auto_offline setting per account. // Reference: Chatwoot profiles_controller#auto_offline — params wrapped in "profile" key type AutoOfflineRequest struct { AccountID uint `json:"account_id" validate:"required"` AutoOffline bool `json:"auto_offline"` } // ProfileAutoOfflinePayload wraps AutoOfflineRequest under the "profile" key, // matching Chatwoot's params.require(:profile).permit(:account_id, :auto_offline) type ProfileAutoOfflinePayload struct { Profile AutoOfflineRequest `json:"profile" validate:"required"` } // SetActiveAccountRequest is the DTO for setting the active account. // Reference: Chatwoot profiles_controller#set_active_account — params wrapped in "profile" key type SetActiveAccountRequest struct { AccountID uint `json:"account_id" validate:"required"` } // ProfileSetActiveAccountPayload wraps SetActiveAccountRequest under the "profile" key, // matching Chatwoot's params.require(:profile).permit(:account_id) type ProfileSetActiveAccountPayload struct { Profile SetActiveAccountRequest `json:"profile" validate:"required"` } // Get retrieves the current user's profile. func (s *ProfileService) Get(ctx context.Context, userID uint) (*model.User, error) { user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } return user, nil } // Update updates the current user's profile. func (s *ProfileService) Update(ctx context.Context, userID uint, req UpdateProfileRequest) (*model.User, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } if req.Name != "" { user.Name = req.Name } if req.Email != "" { user.Email = req.Email } if req.AvatarURL != "" { user.AvatarURL = req.AvatarURL } // Map availability to user.Available boolean if req.Availability == "online" { user.Available = true } else if req.Availability == "offline" || req.Availability == "busy" { user.Available = false } if err := s.userRepo.Update(ctx, user); err != nil { applogger.L().Errorf("failed to update profile: %v", err) return nil, fmt.Errorf("failed to update profile: %w", err) } return user, nil } // UpdateAvatar updates the current user's avatar URL. func (s *ProfileService) UpdateAvatar(ctx context.Context, userID uint, req UpdateAvatarRequest) (*model.User, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } user.AvatarURL = req.AvatarURL if err := s.userRepo.Update(ctx, user); err != nil { applogger.L().Errorf("failed to update avatar: %v", err) return nil, fmt.Errorf("failed to update avatar: %w", err) } return user, nil } // SetAvailability updates the user's availability status for a specific account. // Reference: Chatwoot profiles_controller#availability — POST /api/v1/profile/availability func (s *ProfileService) SetAvailability(ctx context.Context, userID uint, req AvailabilityRequest) (*model.AccountUser, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } if err := s.accountUserRepo.UpdateAvailability(ctx, req.AccountID, userID, req.Availability); err != nil { return nil, fmt.Errorf("failed to update availability: %w", err) } return s.accountUserRepo.FindByAccountAndUserOrFail(ctx, req.AccountID, userID) } // SetAutoOffline updates the user's auto_offline setting for a specific account. // Reference: Chatwoot profiles_controller#auto_offline — POST /api/v1/profile/auto_offline func (s *ProfileService) SetAutoOffline(ctx context.Context, userID uint, req AutoOfflineRequest) (*model.AccountUser, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } if err := s.accountUserRepo.UpdateAutoOffline(ctx, req.AccountID, userID, req.AutoOffline); err != nil { return nil, fmt.Errorf("failed to update auto_offline: %w", err) } return s.accountUserRepo.FindByAccountAndUserOrFail(ctx, req.AccountID, userID) } // SetActiveAccount updates the active_at timestamp for the user's account membership. // Reference: Chatwoot profiles_controller#set_active_account — PUT /api/v1/profile/set_active_account func (s *ProfileService) SetActiveAccount(ctx context.Context, userID uint, req SetActiveAccountRequest) error { if err := pkgvalidator.ValidateStruct(req); err != nil { return err } if err := s.accountUserRepo.UpdateActiveAt(ctx, req.AccountID, userID); err != nil { return fmt.Errorf("failed to set active account: %w", err) } return nil } // ResendConfirmation sends a confirmation email to the user if not yet confirmed. // Reference: Chatwoot auth/resend_confirmations_controller#create — POST /api/v1/resend_confirmation func (s *ProfileService) ResendConfirmation(ctx context.Context, userID uint) error { user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return fmt.Errorf("user not found: %w", err) } if user.ConfirmedAt != nil { // Already confirmed, do nothing (per chatwoot: skip if confirmed) return nil } // TODO: integrate email sending service for confirmation emails applogger.L().Infof("ResendConfirmation called for user %d (%s)", userID, user.Email) return nil } // ResetAccessToken regenerates the user's access token. // Reference: Chatwoot profiles_controller#reset_access_token — POST /api/v1/profile/reset_access_token // In chatwoot this regenerates the Doorkeeper OAuth token. In gochat with JWT auth, // the "reset" means the current JWT is invalidated and a new one must be obtained. func (s *ProfileService) ResetAccessToken(ctx context.Context, userID uint) (*model.User, error) { user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } user.SignInCount++ if err := s.userRepo.Update(ctx, user); err != nil { return nil, fmt.Errorf("failed to reset access token: %w", err) } return user, nil } // DeleteAvatar removes the user's avatar image. // Reference: Chatwoot ProfilesController#destroy_avatar (DELETE :avatar on: :collection) func (s *ProfileService) DeleteAvatar(ctx context.Context, userID uint) (*model.User, error) { user, err := s.userRepo.FindByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } user.AvatarURL = "" if err := s.userRepo.Update(ctx, user); err != nil { return nil, fmt.Errorf("failed to remove avatar: %w", err) } return user, nil }