464 lines
13 KiB
Go
464 lines
13 KiB
Go
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 {
|
|
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) {
|
|
if err := s.persistNotification(ctx, recipient, req, link, image); err != nil {
|
|
s.logger.Error("Failed to persist in-app notification", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": recipient.UserID,
|
|
})
|
|
}
|
|
|
|
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) error {
|
|
methods := make(map[string]string)
|
|
if slices.Contains(req.Channels, DeliveryChannelEmail) && recipient.Email != "" {
|
|
methods["email"] = recipient.Email
|
|
}
|
|
if slices.Contains(req.Channels, DeliveryChannelPush) && len(recipient.DeviceTokens) > 0 {
|
|
methods["push"] = recipient.DeviceTokens[0]
|
|
}
|
|
|
|
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),
|
|
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.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.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.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
|
|
}
|