diff --git a/internal/notification/form.go b/internal/notification/form.go index e78dfb9..7878db1 100644 --- a/internal/notification/form.go +++ b/internal/notification/form.go @@ -25,7 +25,7 @@ type SearchForm struct { Method string `query:"method" valid:"optional"` EventType string `query:"event_type" valid:"optional"` Type string `query:"type" valid:"optional"` - Recipient string `query:"recipient" valid:"optional"` + Recipient []string `query:"recipient" json:"recipient" valid:"optional"` Seen *bool `query:"seen" valid:"optional"` Priority string `query:"priority" valid:"optional"` } diff --git a/internal/notification/handler.go b/internal/notification/handler.go index 7983d24..74922ae 100644 --- a/internal/notification/handler.go +++ b/internal/notification/handler.go @@ -143,7 +143,7 @@ func (h *NotificationHandler) AdminNotifications(c echo.Context) error { return response.ValidationError(c, err.Error(), "") } - form.Recipient = userID + form.Recipient = []string{userID} form.Status = "sent" pagination, err := response.NewPagination(c) @@ -261,7 +261,7 @@ func (h *NotificationHandler) CustomerNotifications(c echo.Context) error { return response.ValidationError(c, err.Error(), "") } - form.Recipient = userID + form.Recipient = []string{userID} form.Status = "sent" pagination, err := response.NewPagination(c) diff --git a/internal/notification/repository.go b/internal/notification/repository.go index 2f6179c..98f9c9f 100644 --- a/internal/notification/repository.go +++ b/internal/notification/repository.go @@ -19,7 +19,7 @@ var ( // Notification represents a stored notification in the database type Notification struct { - ID string `bson:"_id,omitempty" json:"id"` + orm.Model `bson:",inline"` UserID string `bson:"user_id" json:"user_id"` Title string `bson:"title" json:"title"` Message string `bson:"message" json:"message"` @@ -32,19 +32,27 @@ type Notification struct { Methods map[string]string `bson:"methods,omitempty" json:"methods,omitempty"` Metadata map[string]any `bson:"metadata,omitempty" json:"metadata,omitempty"` ScheduleAt int64 `bson:"schedule_at,omitempty" json:"schedule_at,omitempty"` - CreatedAt int64 `bson:"created_at" json:"created_at"` - UpdatedAt int64 `bson:"updated_at" json:"updated_at"` Seen bool `bson:"seen" json:"seen"` SeenAt int64 `bson:"seen_at,omitempty" json:"seen_at,omitempty"` IsScheduled bool `bson:"is_scheduled" json:"is_scheduled"` ScheduledAt int64 `bson:"scheduled_at,omitempty" json:"scheduled_at,omitempty"` } +// SetID sets the notification ID (implements IDSetter interface) +func (n *Notification) SetID(id string) { + n.Model.SetID(id) +} + +// GetID returns the notification ID (implements IDGetter interface) +func (n *Notification) GetID() string { + return n.Model.GetID() +} + // Repository defines methods for notification data access type Repository interface { Create(ctx context.Context, notification *Notification) error GetByID(ctx context.Context, id string) (*Notification, error) - GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) + GetByUserID(ctx context.Context, userIDs []string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) Update(ctx context.Context, notification *Notification) error MarkAsSeen(ctx context.Context, notificationID, userID string) error MarkAllAsSeen(ctx context.Context, userID string) error @@ -106,9 +114,15 @@ func (r *notificationRepository) GetByID(ctx context.Context, id string) (*Notif return r.ormRepo.FindByID(ctx, id) } -// GetByUserID retrieves notifications for a specific user with filters -func (r *notificationRepository) GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) { - filter := bson.M{"user_id": userID} +// GetByUserID retrieves notifications with optional user filters +func (r *notificationRepository) GetByUserID(ctx context.Context, userIDs []string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) { + filter := bson.M{} + + if len(userIDs) == 1 { + filter["user_id"] = userIDs[0] + } else if len(userIDs) > 1 { + filter["user_id"] = bson.M{"$in": userIDs} + } if status != "" { filter["status"] = status @@ -146,8 +160,8 @@ func (r *notificationRepository) GetByUserID(ctx context.Context, userID string, result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination) if err != nil { r.logger.Error("Failed to find notifications", map[string]interface{}{ - "error": err.Error(), - "user_id": userID, + "error": err.Error(), + "user_ids": userIDs, }) return nil, err } @@ -173,8 +187,13 @@ func (r *notificationRepository) Update(ctx context.Context, notification *Notif // MarkAsSeen marks a notification as seen func (r *notificationRepository) MarkAsSeen(ctx context.Context, notificationID, userID string) error { + objectID, err := bson.ObjectIDFromHex(notificationID) + if err != nil { + return ErrNotificationNotFound + } + filter := bson.M{ - "_id": notificationID, + "_id": objectID, "user_id": userID, } diff --git a/internal/notification/service.go b/internal/notification/service.go index 335be65..208ce81 100644 --- a/internal/notification/service.go +++ b/internal/notification/service.go @@ -76,50 +76,7 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest 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, - }) - } - } + s.sendToRecipient(ctx, recipient, req, link, image) } } else { recipients, err := s.getCustomers(ctx, req.Recipient, string(req.Target)) @@ -127,60 +84,104 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest 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.sendToRecipient(ctx, recipient, req, link, image) } - s.logger.Info("Notification sent", map[string]interface{}{ - "recipient": req.Recipient, - "channels": req.Channels, - }) - - return nil } + + 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, @@ -227,7 +228,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF } notificationsResponse = append(notificationsResponse, &NotificationResponse{ - ID: notification.ID, + ID: notification.GetID(), UserID: notification.UserID, Recipient: recipient, Title: notification.Title, @@ -292,7 +293,7 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI } detailedResponse := &NotificationResponse{ - ID: notification.ID, + ID: notification.GetID(), UserID: notification.UserID, Recipient: recipient, Title: notification.Title, @@ -378,11 +379,15 @@ func (s *notificationService) getUsers(ctx context.Context, userIDs []string) ([ }) } - if len(recipients) == 0 { - return nil, errors.New("no users found") + if len(userIDs) > 0 || len(users.Users) == 0 { + break } } + if len(recipients) == 0 { + return nil, errors.New("no users found") + } + return recipients, nil }