From 347f974cd94795e7cbb9cca8e57fdb891ffcac28 Mon Sep 17 00:00:00 2001 From: "m.nazemi" Date: Sat, 25 Apr 2026 18:34:39 +0330 Subject: [PATCH] notification bug fix and refactor --- cmd/web/main.go | 5 +- internal/notification/repository.go | 251 ++++++++++++++++++++++++++++ internal/notification/service.go | 123 ++++++-------- 3 files changed, 302 insertions(+), 77 deletions(-) create mode 100644 internal/notification/repository.go diff --git a/cmd/web/main.go b/cmd/web/main.go index 98587fb..d4eaccf 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -232,8 +232,9 @@ func main() { boardRepository := kanban.NewBoardRepository(mongoManager, logger) columnRepository := kanban.NewColumnRepository(mongoManager, logger) cardRepository := kanban.NewCardRepository(mongoManager, logger) + notificationRepository := notification.NewRepository(mongoManager, logger) logger.Info("Repositories initialized successfully", map[string]interface{}{ - "repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "contact", "cms", "notice", "kanban_board", "kanban_column", "kanban_card"}, + "repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "contact", "cms", "notice", "kanban_board", "kanban_column", "kanban_card", "notification"}, }) // Initialize validation services @@ -251,7 +252,7 @@ func main() { tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger) inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy) flagService := assets.NewService(logger, conf.Assets.FlagsPath) - notificationService := notification.NewService(notificationSDK, userService, customerService, logger) + notificationService := notification.NewService(notificationSDK, notificationRepository, userService, customerService, logger) contactService := contact.NewService(contactRepository, logger, notificationSDK) cmsService := cms.NewService(cmsRepository, logger) tedConfig := buildTEDConfig(&conf.TED) diff --git a/internal/notification/repository.go b/internal/notification/repository.go new file mode 100644 index 0000000..79d6c0a --- /dev/null +++ b/internal/notification/repository.go @@ -0,0 +1,251 @@ +package notification + +import ( + "context" + "errors" + "time" + + "tm/pkg/logger" + orm "tm/pkg/mongo" + "tm/pkg/response" + + "go.mongodb.org/mongo-driver/v2/bson" + mongopkg "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +var ( + ErrNotificationNotFound = errors.New("notification not found") +) + +// Notification represents a stored notification in the database +type Notification struct { + ID string `bson:"_id,omitempty" json:"id"` + UserID string `bson:"user_id" json:"user_id"` + Title string `bson:"title" json:"title"` + Message string `bson:"message" json:"message"` + Link string `bson:"link,omitempty" json:"link,omitempty"` + Image string `bson:"image,omitempty" json:"image,omitempty"` + Priority string `bson:"priority" json:"priority"` + Type string `bson:"type" json:"type"` + EventType string `bson:"event_type,omitempty" json:"event_type,omitempty"` + Status string `bson:"status" json:"status"` + 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"` +} + +// 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) ([]Notification, int64, error) + Update(ctx context.Context, notification *Notification) error + MarkAsSeen(ctx context.Context, notificationID, userID string) error + MarkAllAsSeen(ctx context.Context, userID string) error +} + +type notificationRepository struct { + ormRepo orm.Repository[Notification] + collection *mongopkg.Collection + logger logger.Logger +} + +// NewRepository creates a new notification repository +func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { + // Create indexes + indexes := []orm.Index{ + *orm.NewIndex("user_id_idx", bson.D{{Key: "user_id", Value: 1}}), + *orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), + *orm.NewIndex("seen_idx", bson.D{{Key: "seen", Value: 1}}), + *orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), + } + + err := mongoManager.CreateIndexes("notifications", indexes) + if err != nil { + logger.Warn("Failed to create notification indexes", map[string]interface{}{ + "error": err.Error(), + }) + } + + collection := mongoManager.GetCollection("notifications") + ormRepo := orm.NewRepository[Notification](collection, logger) + + return ¬ificationRepository{ + ormRepo: ormRepo, + collection: collection, + logger: logger, + } +} + +// Create creates a new notification +func (r *notificationRepository) Create(ctx context.Context, notification *Notification) error { + now := time.Now().Unix() + notification.CreatedAt = now + notification.UpdatedAt = now + + err := r.ormRepo.Create(ctx, notification) + if err != nil { + r.logger.Error("Failed to create notification", map[string]interface{}{ + "error": err.Error(), + "user_id": notification.UserID, + }) + return err + } + + return nil +} + +// GetByID retrieves a notification by ID +func (r *notificationRepository) GetByID(ctx context.Context, id string) (*Notification, error) { + 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) ([]Notification, int64, error) { + filter := bson.M{"user_id": userID} + + if status != "" { + filter["status"] = status + } + + if seen != nil { + filter["seen"] = *seen + } + + if priority != "" { + filter["priority"] = priority + } + + if eventType != "" { + filter["event_type"] = eventType + } + + if notificationType != "" { + filter["type"] = notificationType + } + + // Get total count + total, err := r.collection.CountDocuments(ctx, filter) + if err != nil { + r.logger.Error("Failed to count notifications", map[string]interface{}{ + "error": err.Error(), + "user_id": userID, + }) + return nil, 0, err + } + + // Build find options + findOptions := options.Find() + if pagination.Limit > 0 { + findOptions.SetLimit(int64(pagination.Limit)) + } + if pagination.Offset > 0 { + findOptions.SetSkip(int64(pagination.Offset)) + } + + // Sort by created_at descending + findOptions.SetSort(bson.D{{Key: "created_at", Value: -1}}) + + cursor, err := r.collection.Find(ctx, filter, findOptions) + if err != nil { + r.logger.Error("Failed to find notifications", map[string]interface{}{ + "error": err.Error(), + "user_id": userID, + }) + return nil, 0, err + } + defer cursor.Close(ctx) + + var notifications []Notification + if err := cursor.All(ctx, ¬ifications); err != nil { + r.logger.Error("Failed to decode notifications", map[string]interface{}{ + "error": err.Error(), + "user_id": userID, + }) + return nil, 0, err + } + + return notifications, total, nil +} + +// Update updates a notification +func (r *notificationRepository) Update(ctx context.Context, notification *Notification) error { + notification.UpdatedAt = time.Now().Unix() + + err := r.ormRepo.Update(ctx, notification) + if err != nil { + r.logger.Error("Failed to update notification", map[string]interface{}{ + "error": err.Error(), + "user_id": notification.UserID, + }) + return err + } + + return nil +} + +// MarkAsSeen marks a notification as seen +func (r *notificationRepository) MarkAsSeen(ctx context.Context, notificationID, userID string) error { + filter := bson.M{ + "_id": notificationID, + "user_id": userID, + } + + update := bson.M{ + "$set": bson.M{ + "seen": true, + "seen_at": time.Now().Unix(), + "updated_at": time.Now().Unix(), + }, + } + + result, err := r.collection.UpdateOne(ctx, filter, update) + if err != nil { + r.logger.Error("Failed to mark notification as seen", map[string]interface{}{ + "error": err.Error(), + "notification_id": notificationID, + "user_id": userID, + }) + return err + } + + if result.MatchedCount == 0 { + return ErrNotificationNotFound + } + + return nil +} + +// MarkAllAsSeen marks all notifications for a user as seen +func (r *notificationRepository) MarkAllAsSeen(ctx context.Context, userID string) error { + filter := bson.M{ + "user_id": userID, + "seen": false, + } + + update := bson.M{ + "$set": bson.M{ + "seen": true, + "seen_at": time.Now().Unix(), + "updated_at": time.Now().Unix(), + }, + } + + _, err := r.collection.UpdateMany(ctx, filter, update) + if err != nil { + r.logger.Error("Failed to mark all notifications as seen", map[string]interface{}{ + "error": err.Error(), + "user_id": userID, + }) + return err + } + + return nil +} diff --git a/internal/notification/service.go b/internal/notification/service.go index 3158b46..8de3dcc 100644 --- a/internal/notification/service.go +++ b/internal/notification/service.go @@ -5,7 +5,6 @@ import ( "errors" "slices" "strings" - "time" "tm/internal/customer" "tm/internal/user" @@ -28,6 +27,7 @@ type Service interface { // notificationService implements NotificationService interface type notificationService struct { sdk notification.SDK + repository Repository userService user.Service customerService customer.Service logger logger.Logger @@ -36,12 +36,14 @@ type notificationService struct { // 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, @@ -192,17 +194,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF "offset": pagination.Offset, }) - notifications, err := s.sdk.GetNotifications(ctx, ¬ification.GetNotificationsRequest{ - Status: req.Status, - Method: req.Method, - EventType: req.EventType, - Type: req.Type, - UserID: req.Recipient, - Seen: req.Seen, - Priority: req.Priority, - PerPage: pagination.Limit, - Page: (pagination.Offset / pagination.Limit) + 1, // Convert 0-based offset to 1-based page number - }) + notifications, total, 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(), @@ -211,18 +203,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF } notificationsResponse := make([]*NotificationResponse, 0) - for _, notification := range notifications.Data { - createdAt, _ := time.Parse(time.RFC3339, notification.CreatedAt) - createdAtUnix := createdAt.Unix() - updatedAt, _ := time.Parse(time.RFC3339, notification.UpdatedAt) - updatedAtUnix := updatedAt.Unix() - if createdAtUnix < 0 { - createdAtUnix = 0 - } - if updatedAtUnix < 0 { - updatedAtUnix = 0 - } - + for _, notification := range notifications { usr, _ := s.userService.GetUserByID(ctx, notification.UserID) cust, _ := s.customerService.GetByID(ctx, notification.UserID) @@ -258,25 +239,31 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF Status: notification.Status, Methods: notification.Methods, Metadata: notification.Metadata, - ScheduleAt: notification.ScheduledAt, + ScheduleAt: notification.ScheduleAt, IsScheduled: notification.IsScheduled, Image: imagePtr, Seen: notification.Seen, SeenAt: notification.SeenAt, ScheduledAt: notification.ScheduledAt, - CreatedAt: createdAtUnix, - UpdatedAt: updatedAtUnix, + CreatedAt: notification.CreatedAt, + UpdatedAt: notification.UpdatedAt, }) } + // Calculate total pages + totalPages := int(total) / pagination.Limit + if int(total)%pagination.Limit > 0 { + totalPages++ + } + return &NotificationListResponse{ Notifications: notificationsResponse, Meta: &response.Meta{ - Total: notifications.Pagination.Total, - Limit: notifications.Pagination.PerPage, + Total: int(total), + Limit: pagination.Limit, Offset: pagination.Offset, - Page: notifications.Pagination.CurrentPage, - Pages: notifications.Pagination.TotalPages, + Page: (pagination.Offset / pagination.Limit) + 1, + Pages: totalPages, }, }, nil } @@ -286,7 +273,7 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI "notification_id": notificationID, }) - notification, err := s.sdk.GetNotification(ctx, notificationID) + notification, err := s.repository.GetByID(ctx, notificationID) if err != nil { s.logger.Error("Failed to get notification", map[string]interface{}{ "error": err.Error(), @@ -297,17 +284,17 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI // Convert to detailed response format var linkPtr *string - if notification.Data.Link != "" { - linkPtr = ¬ification.Data.Link + if notification.Link != "" { + linkPtr = ¬ification.Link } var imagePtr *string - if notification.Data.Image != "" { - imagePtr = ¬ification.Data.Image + if notification.Image != "" { + imagePtr = ¬ification.Image } - usr, _ := s.userService.GetUserByID(ctx, notification.Data.UserID) - cust, _ := s.customerService.GetByID(ctx, notification.Data.UserID) + usr, _ := s.userService.GetUserByID(ctx, notification.UserID) + cust, _ := s.customerService.GetByID(ctx, notification.UserID) var recipient any if usr != nil { @@ -316,35 +303,26 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI recipient = cust } - createdAt := notification.Data.CreatedAt.Unix() - updatedAt := notification.Data.UpdatedAt.Unix() - if createdAt < 0 { - createdAt = 0 - } - if updatedAt < 0 { - updatedAt = 0 - } - detailedResponse := &NotificationResponse{ - ID: notification.Data.ID, - UserID: notification.Data.UserID, + ID: notification.ID, + UserID: notification.UserID, Recipient: recipient, - Title: notification.Data.Title, - Message: notification.Data.Message, + Title: notification.Title, + Message: notification.Message, Link: linkPtr, Image: imagePtr, - Type: notification.Data.Type, - Priority: notification.Data.Priority, - EventType: notification.Data.EventType, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - Methods: notification.Data.Methods, - Metadata: notification.Data.Metadata, - Status: notification.Data.Status, - Seen: notification.Data.Seen, - SeenAt: notification.Data.SeenAt, - ScheduledAt: notification.Data.ScheduledAt, - IsScheduled: notification.Data.IsScheduled, + 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 @@ -355,23 +333,14 @@ func (s *notificationService) MarkSeen(ctx context.Context, notificationID, user "notification_id": notificationID, }) - notif, err := s.sdk.GetNotification(ctx, notificationID) + err := s.repository.MarkAsSeen(ctx, notificationID, userID) if err != nil { - s.logger.Error("Failed to get notification", map[string]interface{}{ + s.logger.Error("Failed to mark notification as seen", map[string]interface{}{ "error": err.Error(), "notification_id": notificationID, }) return err } - - if notif.Data.UserID != userID { - return errors.New("notification not found") - } - - _, err = s.sdk.MarkSeen(ctx, notificationID) - if err != nil { - return err - } return nil } @@ -380,8 +349,12 @@ func (s *notificationService) AllMarkSeen(ctx context.Context, userID string) er "user_id": userID, }) - _, err := s.sdk.AllMarkSeen(ctx, 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