notification bug fix and refactor

This commit is contained in:
m.nazemi
2026-04-25 18:34:39 +03:30
parent 870f6758e0
commit 347f974cd9
3 changed files with 302 additions and 77 deletions
+3 -2
View File
@@ -232,8 +232,9 @@ func main() {
boardRepository := kanban.NewBoardRepository(mongoManager, logger) boardRepository := kanban.NewBoardRepository(mongoManager, logger)
columnRepository := kanban.NewColumnRepository(mongoManager, logger) columnRepository := kanban.NewColumnRepository(mongoManager, logger)
cardRepository := kanban.NewCardRepository(mongoManager, logger) cardRepository := kanban.NewCardRepository(mongoManager, logger)
notificationRepository := notification.NewRepository(mongoManager, logger)
logger.Info("Repositories initialized successfully", map[string]interface{}{ 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 // Initialize validation services
@@ -251,7 +252,7 @@ func main() {
tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger) tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger)
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy) inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy)
flagService := assets.NewService(logger, conf.Assets.FlagsPath) 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) contactService := contact.NewService(contactRepository, logger, notificationSDK)
cmsService := cms.NewService(cmsRepository, logger) cmsService := cms.NewService(cmsRepository, logger)
tedConfig := buildTEDConfig(&conf.TED) tedConfig := buildTEDConfig(&conf.TED)
+251
View File
@@ -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 &notificationRepository{
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, &notifications); 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
}
+48 -75
View File
@@ -5,7 +5,6 @@ import (
"errors" "errors"
"slices" "slices"
"strings" "strings"
"time"
"tm/internal/customer" "tm/internal/customer"
"tm/internal/user" "tm/internal/user"
@@ -28,6 +27,7 @@ type Service interface {
// notificationService implements NotificationService interface // notificationService implements NotificationService interface
type notificationService struct { type notificationService struct {
sdk notification.SDK sdk notification.SDK
repository Repository
userService user.Service userService user.Service
customerService customer.Service customerService customer.Service
logger logger.Logger logger logger.Logger
@@ -36,12 +36,14 @@ type notificationService struct {
// NewService creates a new notification service // NewService creates a new notification service
func NewService( func NewService(
sdk notification.SDK, sdk notification.SDK,
repository Repository,
userService user.Service, userService user.Service,
customerService customer.Service, customerService customer.Service,
logger logger.Logger, logger logger.Logger,
) Service { ) Service {
return &notificationService{ return &notificationService{
sdk: sdk, sdk: sdk,
repository: repository,
userService: userService, userService: userService,
customerService: customerService, customerService: customerService,
logger: logger, logger: logger,
@@ -192,17 +194,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF
"offset": pagination.Offset, "offset": pagination.Offset,
}) })
notifications, err := s.sdk.GetNotifications(ctx, &notification.GetNotificationsRequest{ notifications, total, err := s.repository.GetByUserID(ctx, req.Recipient, req.Status, req.Seen, req.Priority, req.EventType, req.Type, pagination)
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
})
if err != nil { if err != nil {
s.logger.Error("Failed to get notifications", map[string]interface{}{ s.logger.Error("Failed to get notifications", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -211,18 +203,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF
} }
notificationsResponse := make([]*NotificationResponse, 0) notificationsResponse := make([]*NotificationResponse, 0)
for _, notification := range notifications.Data { for _, notification := range notifications {
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
}
usr, _ := s.userService.GetUserByID(ctx, notification.UserID) usr, _ := s.userService.GetUserByID(ctx, notification.UserID)
cust, _ := s.customerService.GetByID(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, Status: notification.Status,
Methods: notification.Methods, Methods: notification.Methods,
Metadata: notification.Metadata, Metadata: notification.Metadata,
ScheduleAt: notification.ScheduledAt, ScheduleAt: notification.ScheduleAt,
IsScheduled: notification.IsScheduled, IsScheduled: notification.IsScheduled,
Image: imagePtr, Image: imagePtr,
Seen: notification.Seen, Seen: notification.Seen,
SeenAt: notification.SeenAt, SeenAt: notification.SeenAt,
ScheduledAt: notification.ScheduledAt, ScheduledAt: notification.ScheduledAt,
CreatedAt: createdAtUnix, CreatedAt: notification.CreatedAt,
UpdatedAt: updatedAtUnix, UpdatedAt: notification.UpdatedAt,
}) })
} }
// Calculate total pages
totalPages := int(total) / pagination.Limit
if int(total)%pagination.Limit > 0 {
totalPages++
}
return &NotificationListResponse{ return &NotificationListResponse{
Notifications: notificationsResponse, Notifications: notificationsResponse,
Meta: &response.Meta{ Meta: &response.Meta{
Total: notifications.Pagination.Total, Total: int(total),
Limit: notifications.Pagination.PerPage, Limit: pagination.Limit,
Offset: pagination.Offset, Offset: pagination.Offset,
Page: notifications.Pagination.CurrentPage, Page: (pagination.Offset / pagination.Limit) + 1,
Pages: notifications.Pagination.TotalPages, Pages: totalPages,
}, },
}, nil }, nil
} }
@@ -286,7 +273,7 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI
"notification_id": notificationID, "notification_id": notificationID,
}) })
notification, err := s.sdk.GetNotification(ctx, notificationID) notification, err := s.repository.GetByID(ctx, notificationID)
if err != nil { if err != nil {
s.logger.Error("Failed to get notification", map[string]interface{}{ s.logger.Error("Failed to get notification", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -297,17 +284,17 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI
// Convert to detailed response format // Convert to detailed response format
var linkPtr *string var linkPtr *string
if notification.Data.Link != "" { if notification.Link != "" {
linkPtr = &notification.Data.Link linkPtr = &notification.Link
} }
var imagePtr *string var imagePtr *string
if notification.Data.Image != "" { if notification.Image != "" {
imagePtr = &notification.Data.Image imagePtr = &notification.Image
} }
usr, _ := s.userService.GetUserByID(ctx, notification.Data.UserID) usr, _ := s.userService.GetUserByID(ctx, notification.UserID)
cust, _ := s.customerService.GetByID(ctx, notification.Data.UserID) cust, _ := s.customerService.GetByID(ctx, notification.UserID)
var recipient any var recipient any
if usr != nil { if usr != nil {
@@ -316,35 +303,26 @@ func (s *notificationService) GetNotification(ctx context.Context, notificationI
recipient = cust recipient = cust
} }
createdAt := notification.Data.CreatedAt.Unix()
updatedAt := notification.Data.UpdatedAt.Unix()
if createdAt < 0 {
createdAt = 0
}
if updatedAt < 0 {
updatedAt = 0
}
detailedResponse := &NotificationResponse{ detailedResponse := &NotificationResponse{
ID: notification.Data.ID, ID: notification.ID,
UserID: notification.Data.UserID, UserID: notification.UserID,
Recipient: recipient, Recipient: recipient,
Title: notification.Data.Title, Title: notification.Title,
Message: notification.Data.Message, Message: notification.Message,
Link: linkPtr, Link: linkPtr,
Image: imagePtr, Image: imagePtr,
Type: notification.Data.Type, Type: notification.Type,
Priority: notification.Data.Priority, Priority: notification.Priority,
EventType: notification.Data.EventType, EventType: notification.EventType,
CreatedAt: createdAt, CreatedAt: notification.CreatedAt,
UpdatedAt: updatedAt, UpdatedAt: notification.UpdatedAt,
Methods: notification.Data.Methods, Methods: notification.Methods,
Metadata: notification.Data.Metadata, Metadata: notification.Metadata,
Status: notification.Data.Status, Status: notification.Status,
Seen: notification.Data.Seen, Seen: notification.Seen,
SeenAt: notification.Data.SeenAt, SeenAt: notification.SeenAt,
ScheduledAt: notification.Data.ScheduledAt, ScheduledAt: notification.ScheduledAt,
IsScheduled: notification.Data.IsScheduled, IsScheduled: notification.IsScheduled,
} }
return detailedResponse, nil return detailedResponse, nil
@@ -355,23 +333,14 @@ func (s *notificationService) MarkSeen(ctx context.Context, notificationID, user
"notification_id": notificationID, "notification_id": notificationID,
}) })
notif, err := s.sdk.GetNotification(ctx, notificationID) err := s.repository.MarkAsSeen(ctx, notificationID, userID)
if err != nil { 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(), "error": err.Error(),
"notification_id": notificationID, "notification_id": notificationID,
}) })
return err 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 return nil
} }
@@ -380,8 +349,12 @@ func (s *notificationService) AllMarkSeen(ctx context.Context, userID string) er
"user_id": userID, "user_id": userID,
}) })
_, err := s.sdk.AllMarkSeen(ctx, userID) err := s.repository.MarkAllAsSeen(ctx, userID)
if err != nil { 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 err
} }
return nil return nil