Files
gochat/internal/service/inbox_member_service.go
T

209 lines
6.8 KiB
Go

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
}