package service import ( "context" "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" ) // InboxMemberService implements business logic for InboxMember operations. // Reference: Chatwoot app/models/inbox_member.rb type InboxMemberService struct { repo *repository.InboxMemberRepo } // NewInboxMemberService creates a new InboxMember service. func NewInboxMemberService(repo *repository.InboxMemberRepo) *InboxMemberService { return &InboxMemberService{repo: repo} } // GetByID retrieves a single inbox_member. func (s *InboxMemberService) GetByID(ctx context.Context, id uint) (*model.InboxMember, error) { return s.repo.FindByID(ctx, id) } // ListByInbox retrieves all members (agents) assigned to an inbox. func (s *InboxMemberService) ListByInbox(ctx context.Context, inboxID uint) ([]model.InboxMember, error) { return s.repo.FindByInbox(ctx, inboxID) } // ListByUser retrieves all inboxes a user is assigned to. func (s *InboxMemberService) ListByUser(ctx context.Context, userID uint) ([]model.InboxMember, error) { return s.repo.FindByUser(ctx, userID) } // AddMemberRequest is the DTO for adding a member to an inbox. type AddMemberRequest struct { InboxID uint `json:"inbox_id" validate:"required"` UserID uint `json:"user_id" validate:"required"` } // AddMember assigns a user (agent) to an inbox. func (s *InboxMemberService) AddMember(ctx context.Context, req AddMemberRequest) (*model.InboxMember, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } existing, err := s.repo.FindByInboxAndUser(ctx, req.InboxID, req.UserID) if err == nil && existing != nil { return existing, nil } im := &model.InboxMember{ InboxID: req.InboxID, UserID: req.UserID, } if err := s.repo.Create(ctx, im); err != nil { applogger.L().Errorf("InboxMemberService.AddMember failed: %v", err) return nil, err } return s.repo.FindByInboxAndUser(ctx, req.InboxID, req.UserID) } // RemoveMember removes a user from an inbox. func (s *InboxMemberService) RemoveMember(ctx context.Context, inboxID, userID uint) error { return s.repo.DeleteByInboxAndUser(ctx, inboxID, userID) } // RemoveAllMembers removes all members from an inbox. func (s *InboxMemberService) RemoveAllMembers(ctx context.Context, inboxID uint) error { return s.repo.DeleteByInbox(ctx, inboxID) } // UpdateMemberRequest is the DTO for updating a member's role/availability. type UpdateMemberRequest struct { Role string `json:"role" validate:"omitempty,oneof=agent supervisor"` AvailabilityStatus string `json:"availability_status" validate:"omitempty,oneof=online offline busy"` } // UpdateMember updates a member's role or availability status. func (s *InboxMemberService) UpdateMember(ctx context.Context, inboxID, userID uint, req UpdateMemberRequest) (*model.InboxMember, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } updates := map[string]interface{}{} if req.Role != "" { updates["role"] = req.Role } if req.AvailabilityStatus != "" { updates["availability_status"] = req.AvailabilityStatus } if len(updates) == 0 { // Nothing to update, just return the existing record return s.repo.FindByInboxAndUser(ctx, inboxID, userID) } if err := s.repo.UpdateByInboxAndUser(ctx, inboxID, userID, updates); err != nil { applogger.L().Errorf("InboxMemberService.UpdateMember failed: %v", err) return nil, err } return s.repo.FindByInboxAndUser(ctx, inboxID, userID) } // UpdateMultipleRequest is the DTO for batch-updating inbox members. // Reference: Chatwoot inbox_members_controller#update (batch replace all members) type UpdateMultipleRequest struct { InboxID uint `json:"inbox_id" validate:"required"` UserIDs []uint `json:"user_ids"` } // AddMembers adds the requested users without removing existing members. // Reference: Chatwoot inbox_members#create - add_members(agents_to_be_added_ids). func (s *InboxMemberService) AddMembers(ctx context.Context, req UpdateMultipleRequest) ([]model.InboxMember, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } current, err := s.repo.FindByInbox(ctx, req.InboxID) if err != nil { return nil, err } currentIDs := map[uint]bool{} for _, member := range current { currentIDs[member.UserID] = true } for _, userID := range uniqueUintIDs(req.UserIDs) { if currentIDs[userID] { continue } im := &model.InboxMember{InboxID: req.InboxID, UserID: userID, Role: "agent", AvailabilityStatus: "offline"} if err := s.repo.Create(ctx, im); err != nil { applogger.L().Errorf("InboxMemberService.AddMembers: failed to add member user_id=%d: %v", userID, err) return nil, err } } return s.repo.FindByInbox(ctx, req.InboxID) } // UpdateMultiple replaces all members of an inbox with the given user list. // Reference: Chatwoot inbox_members#update - replaces entire member list in one call. // This calculates the add/remove diff instead of blindly recreating rows. func (s *InboxMemberService) UpdateMultiple(ctx context.Context, req UpdateMultipleRequest) ([]model.InboxMember, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } current, err := s.repo.FindByInbox(ctx, req.InboxID) if err != nil { return nil, err } desiredIDs := map[uint]bool{} for _, userID := range uniqueUintIDs(req.UserIDs) { desiredIDs[userID] = true } currentIDs := map[uint]bool{} for _, member := range current { currentIDs[member.UserID] = true if !desiredIDs[member.UserID] { if err := s.RemoveMember(ctx, req.InboxID, member.UserID); err != nil { applogger.L().Errorf("InboxMemberService.UpdateMultiple: failed to remove member user_id=%d: %v", member.UserID, err) return nil, err } } } for userID := range desiredIDs { if currentIDs[userID] { continue } im := &model.InboxMember{InboxID: req.InboxID, UserID: userID, Role: "agent", AvailabilityStatus: "offline"} if err := s.repo.Create(ctx, im); err != nil { applogger.L().Errorf("InboxMemberService.UpdateMultiple: failed to add member user_id=%d: %v", userID, err) return nil, err } } return s.repo.FindByInbox(ctx, req.InboxID) } // IsMemberOfInbox checks whether a user is a member (assigned agent) of an inbox. // Used by ConversationService.AssignAgent to validate seat assignment routing. func (s *InboxMemberService) IsMemberOfInbox(ctx context.Context, inboxID, userID uint) bool { member, err := s.repo.FindByInboxAndUser(ctx, inboxID, userID) if err != nil || member == nil { return false } return true } func uniqueUintIDs(ids []uint) []uint { seen := map[uint]bool{} result := make([]uint, 0, len(ids)) for _, id := range ids { if id == 0 || seen[id] { continue } seen[id] = true result = append(result, id) } return result }