aafae2a26f
- Introduced new event types for notifications, including email and push, to support a broader range of delivery methods. - Added a `persistedEventType` function to map delivery channels to their corresponding event types, improving clarity and maintainability. - Updated the `sendToRecipient` method to ensure in-app notifications are always persisted, enhancing the reliability of notification records. - Improved error logging for in-app notification persistence failures, providing better context for debugging. This update enhances the notification system's flexibility and reliability by supporting multiple delivery channels and improving error handling.
516 lines
14 KiB
Go
516 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 ¬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 {
|
|
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) {
|
|
// Always persist an in-app record so every send appears in the notification center.
|
|
if err := s.persistNotification(ctx, recipient, req, link, image, DeliveryChannelInApp); err != nil {
|
|
s.logger.Error("Failed to persist in-app notification", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": recipient.UserID,
|
|
})
|
|
}
|
|
|
|
for _, channel := range req.Channels {
|
|
if channel == DeliveryChannelInApp {
|
|
continue
|
|
}
|
|
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, ¬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: metadata,
|
|
Methods: notification.NotificationMethods{
|
|
Push: v,
|
|
},
|
|
ScheduledAt: req.ScheduleAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
if slices.Contains(req.Channels, DeliveryChannelEmail) && 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: 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 {
|
|
eventType, ok := persistedEventType(channel)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
methods := map[string]string{}
|
|
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:
|
|
methods["in_app"] = "true"
|
|
}
|
|
|
|
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 = ¬ification.Link
|
|
}
|
|
|
|
// Handle Image pointer - return nil if empty
|
|
var imagePtr *string
|
|
if notification.Image != "" {
|
|
imagePtr = ¬ification.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 = ¬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.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,
|
|
})
|
|
}
|
|
}
|