package service import ( "context" "errors" "gorm.io/gorm" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" ) // DeliveryStatusService implements business logic for message delivery status operations. // Reference: Chatwoot message delivery tracking per recipient (per-contact status tracking) type DeliveryStatusService struct { messageRepo *repository.MessageRepo deliveryStatusRepo *repository.DeliveryStatusRepo } // NewDeliveryStatusService creates a new DeliveryStatus service. func NewDeliveryStatusService(messageRepo *repository.MessageRepo, deliveryStatusRepo *repository.DeliveryStatusRepo) *DeliveryStatusService { return &DeliveryStatusService{messageRepo: messageRepo, deliveryStatusRepo: deliveryStatusRepo} } // ListByMessage retrieves all delivery statuses for a given message. // Validates that the message belongs to the specified account and conversation. func (s *DeliveryStatusService) ListByMessage(ctx context.Context, accountID uint, conversationID uint, messageID uint) ([]model.DeliveryStatus, error) { // Verify message exists and belongs to the account and conversation msg, err := s.messageRepo.FindByAccountAndID(ctx, accountID, messageID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("message not found") } applogger.L().Errorf("DeliveryStatusService.ListByMessage: failed to find message: %v", err) return nil, errors.New("message not found") } if msg.ConversationID != conversationID { return nil, errors.New("message does not belong to the specified conversation") } return s.deliveryStatusRepo.FindByMessage(ctx, messageID) } // CreateDeliveryStatusRequest is the DTO for creating a delivery status record. type CreateDeliveryStatusRequest struct { MessageID uint `json:"message_id" validate:"required"` InboxID uint `json:"inbox_id" validate:"required"` ContactID uint `json:"contact_id" validate:"required"` Status model.MessageStatus `json:"status" validate:"required,oneof=sent delivered read failed"` } // Create inserts a new delivery status record for a message. func (s *DeliveryStatusService) Create(ctx context.Context, accountID uint, req CreateDeliveryStatusRequest) (*model.DeliveryStatus, error) { // Verify message exists and belongs to the account msg, err := s.messageRepo.FindByAccountAndID(ctx, accountID, req.MessageID) if err != nil { return nil, errors.New("message not found") } status := &model.DeliveryStatus{ MessageID: msg.ID, InboxID: req.InboxID, ContactID: req.ContactID, Status: req.Status, } if err := s.deliveryStatusRepo.Create(ctx, status); err != nil { applogger.L().Errorf("DeliveryStatusService.Create: failed to create delivery status: %v", err) return nil, errors.New("failed to create delivery status") } return status, nil } // UpdateDeliveryStatusRequest is the DTO for updating a delivery status. type UpdateDeliveryStatusRequest struct { Status model.MessageStatus `json:"status" validate:"required,oneof=sent delivered read failed"` DeliveredAt *string `json:"delivered_at,omitempty"` ReadAt *string `json:"read_at,omitempty"` } // Update modifies an existing delivery status record. func (s *DeliveryStatusService) Update(ctx context.Context, accountID uint, id uint, req UpdateDeliveryStatusRequest) (*model.DeliveryStatus, error) { status, err := s.deliveryStatusRepo.FindByID(ctx, id) if err != nil { return nil, errors.New("delivery status not found") } // Verify the message belongs to the account _, err = s.messageRepo.FindByAccountAndID(ctx, accountID, status.MessageID) if err != nil { return nil, errors.New("delivery status not found") } status.Status = req.Status if err := s.deliveryStatusRepo.Update(ctx, status); err != nil { applogger.L().Errorf("DeliveryStatusService.Update: failed to update delivery status: %v", err) return nil, errors.New("failed to update delivery status") } return status, nil }