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)
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)
+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"
"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 &notificationService{
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, &notification.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 = &notification.Data.Link
if notification.Link != "" {
linkPtr = &notification.Link
}
var imagePtr *string
if notification.Data.Image != "" {
imagePtr = &notification.Data.Image
if notification.Image != "" {
imagePtr = &notification.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