package notification import ( "context" "errors" "slices" "strings" "tm/internal/customer" "tm/internal/user" "tm/pkg/logger" "tm/pkg/notification" "tm/pkg/response" ) // NotificationService defines the interface for notification business logic type Service interface { Send(ctx context.Context, req *NotificationRequest) error GetNotifications(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationListResponse, error) GetNotification(ctx context.Context, notificationID string) (*NotificationResponse, error) MarkSeen(ctx context.Context, notificationID, userID string) error AllMarkSeen(ctx context.Context, userID string) error getUsers(ctx context.Context, userIDs []string) ([]notificationRecipient, error) getCustomers(ctx context.Context, values []string, target string) ([]notificationRecipient, error) } // notificationService implements NotificationService interface type notificationService struct { sdk notification.SDK repository Repository userService user.Service customerService customer.Service logger logger.Logger } // NewService creates a new notification service func NewService( sdk notification.SDK, repository Repository, userService user.Service, customerService customer.Service, logger logger.Logger, ) Service { return ¬ificationService{ sdk: sdk, repository: repository, userService: userService, customerService: customerService, logger: logger, } } // Send sends a multiple notification func (s *notificationService) Send(ctx context.Context, req *NotificationRequest) error { image := "" if req.Image != nil { image = *req.Image } link := "" if req.Link != nil { link = *req.Link } s.logger.Info("Sending notification", map[string]interface{}{ "recipient": req.Recipient, "type": req.Type, "priority": req.Priority, "link": link, "image": image, }) if req.Target == TargetAudienceAllUsers || req.Target == TargetAudienceSpecificUsers { recipients, err := s.getUsers(ctx, req.Recipient) if err != nil { return err } for _, recipient := range recipients { if slices.Contains(req.Channels, DeliveryChannelPush) { for _, v := range recipient.DeviceTokens { s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ UserID: recipient.UserID, Title: req.Title, Message: req.Description, Type: string(req.Type), Priority: string(req.Priority), EventType: notification.EventTypePush, Link: link, Image: image, Metadata: map[string]any{ "tender": strings.TrimSpace(req.Tender), }, Methods: notification.NotificationMethods{ Push: v, }, ScheduledAt: req.ScheduleAt, }) } } if slices.Contains(req.Channels, DeliveryChannelEmail) { if recipient.Email != "" { s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ UserID: recipient.UserID, Title: req.Title, Message: req.Description, Type: string(req.Type), Priority: string(req.Priority), EventType: notification.EventTypeEmail, Link: link, Image: image, Metadata: map[string]any{ "tender": strings.TrimSpace(req.Tender), }, Methods: notification.NotificationMethods{ Email: recipient.Email, }, ScheduledAt: req.ScheduleAt, }) } } } } else { recipients, err := s.getCustomers(ctx, req.Recipient, string(req.Target)) if err != nil { return err } for _, recipient := range recipients { if slices.Contains(req.Channels, DeliveryChannelPush) { for _, v := range recipient.DeviceTokens { s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ UserID: recipient.UserID, Title: req.Title, Message: req.Description, Type: string(req.Type), Priority: string(req.Priority), EventType: notification.EventTypePush, Link: link, Image: image, Methods: notification.NotificationMethods{ Push: v, }, ScheduledAt: req.ScheduleAt, Metadata: map[string]any{ "tender": strings.TrimSpace(req.Tender), }, }) } } if slices.Contains(req.Channels, DeliveryChannelEmail) { if recipient.Email != "" { s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ UserID: recipient.UserID, Title: req.Title, Message: req.Description, Type: string(req.Type), Priority: string(req.Priority), EventType: notification.EventTypeEmail, Link: link, Image: image, Methods: notification.NotificationMethods{ Email: recipient.Email, }, ScheduledAt: req.ScheduleAt, Metadata: map[string]any{ "tender": strings.TrimSpace(req.Tender), }, }) } } } s.logger.Info("Notification sent", map[string]interface{}{ "recipient": req.Recipient, "channels": req.Channels, }) return nil } return nil } func (s *notificationService) GetNotifications(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationListResponse, error) { s.logger.Info("Getting notifications", map[string]interface{}{ "status": req.Status, "method": req.Method, "event_type": req.EventType, "type": req.Type, "recipient": req.Recipient, "seen": req.Seen, "priority": req.Priority, "limit": pagination.Limit, "offset": pagination.Offset, }) page, err := s.repository.GetByUserID(ctx, req.Recipient, req.Status, req.Seen, req.Priority, req.EventType, req.Type, pagination) if err != nil { s.logger.Error("Failed to get notifications", map[string]interface{}{ "error": err.Error(), }) return nil, err } notificationsResponse := make([]*NotificationResponse, 0) for _, notification := range page.Items { usr, _ := s.userService.GetUserByID(ctx, notification.UserID) cust, _ := s.customerService.GetByID(ctx, notification.UserID) var recipient any if usr != nil { recipient = usr } else if cust != nil { recipient = cust } // Handle Link pointer - return nil if empty var linkPtr *string if notification.Link != "" { linkPtr = ¬ification.Link } // Handle Image pointer - return nil if empty var imagePtr *string if notification.Image != "" { imagePtr = ¬ification.Image } notificationsResponse = append(notificationsResponse, &NotificationResponse{ ID: notification.ID, UserID: notification.UserID, Recipient: recipient, Title: notification.Title, Message: notification.Message, Link: linkPtr, Priority: notification.Priority, Type: notification.Type, EventType: notification.EventType, Status: notification.Status, Methods: notification.Methods, Metadata: notification.Metadata, ScheduleAt: notification.ScheduleAt, IsScheduled: notification.IsScheduled, Image: imagePtr, Seen: notification.Seen, SeenAt: notification.SeenAt, ScheduledAt: notification.ScheduledAt, CreatedAt: notification.CreatedAt, UpdatedAt: notification.UpdatedAt, }) } return &NotificationListResponse{ Notifications: notificationsResponse, Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset), }, nil } func (s *notificationService) GetNotification(ctx context.Context, notificationID string) (*NotificationResponse, error) { s.logger.Info("Getting notification", map[string]interface{}{ "notification_id": notificationID, }) notification, err := s.repository.GetByID(ctx, notificationID) if err != nil { s.logger.Error("Failed to get notification", map[string]interface{}{ "error": err.Error(), "notification_id": notificationID, }) return nil, err } // Convert to detailed response format var linkPtr *string if notification.Link != "" { linkPtr = ¬ification.Link } var imagePtr *string if notification.Image != "" { imagePtr = ¬ification.Image } usr, _ := s.userService.GetUserByID(ctx, notification.UserID) cust, _ := s.customerService.GetByID(ctx, notification.UserID) var recipient any if usr != nil { recipient = usr } else if cust != nil { recipient = cust } detailedResponse := &NotificationResponse{ ID: notification.ID, UserID: notification.UserID, Recipient: recipient, Title: notification.Title, Message: notification.Message, Link: linkPtr, Image: imagePtr, Type: notification.Type, Priority: notification.Priority, EventType: notification.EventType, CreatedAt: notification.CreatedAt, UpdatedAt: notification.UpdatedAt, Methods: notification.Methods, Metadata: notification.Metadata, Status: notification.Status, Seen: notification.Seen, SeenAt: notification.SeenAt, ScheduledAt: notification.ScheduledAt, IsScheduled: notification.IsScheduled, } return detailedResponse, nil } func (s *notificationService) MarkSeen(ctx context.Context, notificationID, userID string) error { s.logger.Info("Marking notification as seen", map[string]interface{}{ "notification_id": notificationID, }) err := s.repository.MarkAsSeen(ctx, notificationID, userID) if err != nil { s.logger.Error("Failed to mark notification as seen", map[string]interface{}{ "error": err.Error(), "notification_id": notificationID, }) return err } return nil } func (s *notificationService) AllMarkSeen(ctx context.Context, userID string) error { s.logger.Info("Marking all notifications as seen", map[string]interface{}{ "user_id": userID, }) err := s.repository.MarkAllAsSeen(ctx, userID) if err != nil { s.logger.Error("Failed to mark all notifications as seen", map[string]interface{}{ "error": err.Error(), "user_id": userID, }) return err } return nil } func (s *notificationService) getUsers(ctx context.Context, userIDs []string) ([]notificationRecipient, error) { recipients := make([]notificationRecipient, 0) offset := 0 for { var users *user.UserListResponse var err error if len(userIDs) > 0 { users, err = s.userService.GetUsersByIDs(ctx, userIDs) if err != nil { return nil, err } } else { users, err = s.userService.Search(ctx, &user.SearchUsersForm{}, &response.Pagination{Limit: 100, Offset: offset}) if err != nil { return nil, err } offset += 100 if len(users.Users) == 0 { break } } for _, user := range users.Users { recipients = append(recipients, notificationRecipient{ UserID: user.ID, Email: user.Email, DeviceTokens: user.DeviceToken, }) } if len(recipients) == 0 { return nil, errors.New("no users found") } } return recipients, nil } func (s *notificationService) getCustomers(ctx context.Context, values []string, target string) ([]notificationRecipient, error) { recipients := make([]notificationRecipient, 0) if target == string(TargetAudienceSpecificRole) { if len(values) == 0 { return nil, errors.New("no role provided") } customers, err := s.customerService.GetCustomersByRole(ctx, values[0]) if err != nil { return nil, err } for _, customer := range customers { recipients = append(recipients, notificationRecipient{ UserID: customer.ID, Email: customer.Email, DeviceTokens: customer.DeviceToken, }) } } else if target == string(TargetAudienceSpecificCompany) { customers, err := s.customerService.GetCustomersByCompanies(ctx, values) if err != nil { return nil, err } for _, customer := range customers { recipients = append(recipients, notificationRecipient{ UserID: customer.ID, Email: customer.Email, DeviceTokens: customer.DeviceToken, }) } } else if target == string(TargetAudienceSpecificCustomer) { customers, err := s.customerService.GetCustomersByIDs(ctx, values) if err != nil { return nil, err } for _, customer := range customers { recipients = append(recipients, notificationRecipient{ UserID: customer.ID, Email: customer.Email, DeviceTokens: customer.DeviceToken, }) } } else { offset := 0 for { customers, err := s.customerService.SearchNotification(ctx, &customer.SearchCustomersForm{}, &response.Pagination{Limit: 100, Offset: offset}) if err != nil { return nil, err } for _, customer := range customers { recipients = append(recipients, notificationRecipient{ UserID: customer.ID, Email: customer.Email, DeviceTokens: customer.DeviceToken, }) } offset += 100 if len(customers) == 0 { break } } } if len(recipients) == 0 { return nil, errors.New("no customers found") } return recipients, nil }