Files
tm_back/internal/notification/service.go
T
Mazyar 9d8ce12816 Refactor notification service to support multiple delivery channels
- Updated the `sendToRecipient` method to iterate over the channels specified in the notification request, allowing notifications to be persisted for each channel.
- Modified the `persistNotification` method to accept a channel parameter, enhancing the logic for handling different delivery methods (email, push, in-app).
- Improved error logging to include the channel information when persisting notifications fails, providing better context for debugging.

This update enhances the flexibility of the notification service by enabling it to handle multiple delivery channels more effectively, improving overall notification management.
2026-07-11 21:07:37 +03:30

505 lines
14 KiB
Go

package notification
import (
"context"
"errors"
"slices"
"strings"
"time"
"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 &notificationService{
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 {
s.sendToRecipient(ctx, recipient, req, link, image)
}
} else {
recipients, err := s.getCustomers(ctx, req.Recipient, string(req.Target))
if err != nil {
return err
}
for _, recipient := range recipients {
s.sendToRecipient(ctx, recipient, req, link, image)
}
}
s.logger.Info("Notification sent", map[string]interface{}{
"recipient": req.Recipient,
"channels": req.Channels,
"target": req.Target,
})
return nil
}
func (s *notificationService) sendToRecipient(ctx context.Context, recipient notificationRecipient, req *NotificationRequest, link, image string) {
for _, channel := range req.Channels {
if err := s.persistNotification(ctx, recipient, req, link, image, channel); err != nil {
s.logger.Error("Failed to persist notification", map[string]interface{}{
"error": err.Error(),
"user_id": recipient.UserID,
"channel": channel,
})
}
}
metadata := map[string]any{
"tender": strings.TrimSpace(req.Tender),
}
if slices.Contains(req.Channels, DeliveryChannelPush) {
for _, v := range recipient.DeviceTokens {
s.sdk.SendNotification(ctx, &notification.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: metadata,
Methods: notification.NotificationMethods{
Push: v,
},
ScheduledAt: req.ScheduleAt,
})
}
}
if slices.Contains(req.Channels, DeliveryChannelEmail) && recipient.Email != "" {
s.sdk.SendNotification(ctx, &notification.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: metadata,
Methods: notification.NotificationMethods{
Email: recipient.Email,
},
ScheduledAt: req.ScheduleAt,
})
}
}
func (s *notificationService) persistNotification(ctx context.Context, recipient notificationRecipient, req *NotificationRequest, link, image string, channel DeliveryChannel) error {
methods := map[string]string{}
eventType := string(channel)
switch channel {
case DeliveryChannelEmail:
if recipient.Email == "" {
return nil
}
methods["email"] = recipient.Email
case DeliveryChannelPush:
if len(recipient.DeviceTokens) == 0 {
return nil
}
methods["push"] = recipient.DeviceTokens[0]
case DeliveryChannelInApp:
eventType = EventTypeInApp
methods["in_app"] = "true"
default:
return nil
}
status := string(DeliveryStatusSent)
if req.ScheduleAt > 0 {
status = string(DeliveryStatusPending)
}
return s.repository.Create(ctx, &Notification{
UserID: recipient.UserID,
Title: req.Title,
Message: req.Description,
Link: link,
Image: image,
Priority: string(req.Priority),
Type: string(req.Type),
EventType: eventType,
Status: status,
Methods: methods,
Metadata: map[string]any{
"tender": strings.TrimSpace(req.Tender),
},
ScheduleAt: req.ScheduleAt,
ScheduledAt: req.ScheduleAt,
IsScheduled: req.ScheduleAt > 0,
Seen: false,
})
}
func (s *notificationService) GetNotifications(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationListResponse, error) {
s.processDueScheduled(ctx)
search := req.ResolvedSearch()
s.logger.Info("Getting notifications", map[string]interface{}{
"search": search,
"status": req.Status,
"method": req.Method,
"event_type": req.EventType,
"type": req.Type,
"recipient": req.ResolvedRecipients(),
"seen": req.Seen,
"priority": req.Priority,
"limit": pagination.Limit,
"offset": pagination.Offset,
})
page, err := s.repository.GetByUserID(ctx, req.ResolvedRecipients(), search, 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 = &notification.Link
}
// Handle Image pointer - return nil if empty
var imagePtr *string
if notification.Image != "" {
imagePtr = &notification.Image
}
notificationsResponse = append(notificationsResponse, &NotificationResponse{
ID: notification.GetID(),
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.processDueScheduled(ctx)
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 = &notification.Link
}
var imagePtr *string
if notification.Image != "" {
imagePtr = &notification.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.GetID(),
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(userIDs) > 0 || len(users.Users) == 0 {
break
}
}
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
}
func (s *notificationService) processDueScheduled(ctx context.Context) {
count, err := s.repository.MarkDueScheduledAsSent(ctx, time.Now().Unix())
if err != nil {
s.logger.Error("Failed to process due scheduled notifications", map[string]interface{}{
"error": err.Error(),
})
return
}
if count > 0 {
s.logger.Info("Processed due scheduled notifications", map[string]interface{}{
"count": count,
})
}
}