Integrate Notification Management into Tender Management System

- Added a new notification domain, including entities, services, handlers, and repositories to manage notifications effectively.
- Implemented CRUD operations for notifications, allowing for creation, retrieval, updating, and deletion of notifications via the API.
- Enhanced API documentation with Swagger comments for new notification endpoints, ensuring clarity for API consumers.
- Updated routes to include notification management, improving administrative capabilities within the web panel.
- Introduced validation for notification requests and responses, ensuring data integrity and proper error handling.
- Updated the go.mod file to include the latest version of the go-redis library, ensuring compatibility with the notification service.
This commit is contained in:
n.nakhostin
2025-09-17 16:01:49 +03:30
parent 6906577f6e
commit a06dc5097e
12 changed files with 2244 additions and 16 deletions
+110
View File
@@ -0,0 +1,110 @@
package notification
import (
"tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
)
// NotificationType represents the type of notification
type NotificationType string
const (
NotificationTypeInfo NotificationType = "info"
NotificationTypeWarning NotificationType = "warning"
NotificationTypeAlert NotificationType = "alert"
NotificationTypeReject NotificationType = "reject"
)
// NotificationPriority represents the priority level of notification
type NotificationPriority string
const (
NotificationPriorityLow NotificationPriority = "low"
NotificationPriorityMedium NotificationPriority = "medium"
NotificationPriorityImportant NotificationPriority = "important"
)
// DeliveryChannel represents the delivery channel for notification
type DeliveryChannel string
const (
DeliveryChannelPush DeliveryChannel = "push"
DeliveryChannelEmail DeliveryChannel = "email"
)
// DeliveryStatus represents the delivery status of notification
type NotificationStatus string
const (
DeliveryStatusPending NotificationStatus = "pending"
DeliveryStatusSent NotificationStatus = "sent"
DeliveryStatusFailed NotificationStatus = "failed"
)
// TargetAudienceType represents the type of target audience
// Supported audience types:
// - all_users: All users in the system
// - specific_users: Specific users by their IDs
// - all_role: All users with a specific role (e.g., all admins)
// - specific_role: Users with a specific role, optionally filtered by tender
// - specific_company: All users belonging to a specific company
// - all_companies: All users from all companies
// - specific_customer: All users belonging to a specific customer
// - all_customers: All users from all customers
type TargetAudienceType string
const (
TargetAudienceAllUsers TargetAudienceType = "all_users"
TargetAudienceSpecificUsers TargetAudienceType = "specific_users"
TargetAudienceAllRole TargetAudienceType = "all_role"
TargetAudienceSpecificRole TargetAudienceType = "specific_role"
TargetAudienceAllCompanies TargetAudienceType = "all_companies"
TargetAudienceSpecificCompany TargetAudienceType = "specific_company"
TargetAudienceAllCustomers TargetAudienceType = "all_customers"
TargetAudienceSpecificCustomer TargetAudienceType = "specific_customer"
)
// Notification represents a notification entity
type Notification struct {
mongo.Model `bson:",inline"`
Recipient []string `bson:"recipients"`
Tender bson.ObjectID `bson:"tender"`
Channels []DeliveryChannel `bson:"channels"`
Type NotificationType `bson:"type"`
Priority NotificationPriority `bson:"priority"`
Status NotificationStatus `bson:"status"`
Target TargetAudienceType `bson:"target"`
Title string `bson:"title"`
Description string `bson:"description"`
Link *string `bson:"link"`
Schedule Schedule `bson:"schedule"`
}
type Schedule struct {
Time int64 `bson:"time"`
}
func (n *Notification) SetID(id string) {
n.ID, _ = bson.ObjectIDFromHex(id)
}
func (n *Notification) GetID() string {
return n.ID.Hex()
}
func (n *Notification) SetCreatedAt(timestamp int64) {
n.CreatedAt = timestamp
}
func (n *Notification) SetUpdatedAt(timestamp int64) {
n.UpdatedAt = timestamp
}
func (n *Notification) GetCreatedAt() int64 {
return n.CreatedAt
}
func (n *Notification) GetUpdatedAt() int64 {
return n.UpdatedAt
}
+70
View File
@@ -0,0 +1,70 @@
package notification
import (
"tm/pkg/response"
"go.mongodb.org/mongo-driver/v2/bson"
)
// CreateRequest represents the request to create a notification
type NotificationRequest struct {
Recipient []string `json:"recipient" valid:"required,stringlength(1|100)"`
Tender bson.ObjectID `json:"tender" valid:"optional,stringlength(1|100)"`
Channels []DeliveryChannel `json:"channels" valid:"required,minlength(1)"`
Type NotificationType `json:"type" valid:"required,notification_type"`
Priority NotificationPriority `json:"priority" valid:"required,notification_priority"`
Target TargetAudienceType `json:"target" valid:"required,target_audience"`
Title string `json:"title" valid:"required,stringlength(1|200)"`
Description string `json:"description" valid:"required,stringlength(1|1000)"`
Link *string `json:"link" valid:"optional,url"`
Schedule ScheduleRequest `json:"schedule" valid:"optional"`
}
type ScheduleRequest struct {
Time int64 `json:"time" valid:"required,int64"`
}
// NotificationResponse represents the response after creating a notification
type NotificationResponse struct {
ID string `json:"id"`
Tender string `json:"tender"`
Channels []DeliveryChannel `json:"channels"`
Type NotificationType `json:"type"`
Priority NotificationPriority `json:"priority"`
Target TargetAudienceType `json:"target"`
Title string `json:"title"`
Description string `json:"description"`
Link string `json:"link"`
Schedule ScheduleResponse `json:"schedule"`
}
type ScheduleResponse struct {
Time int64 `json:"time"`
}
type NotificationsResponse struct {
Notifications []*NotificationResponse `json:"notifications"`
Meta *response.Meta `json:"-"`
}
func (n *Notification) ToResponse() *NotificationResponse {
return &NotificationResponse{
ID: n.ID.Hex(),
Tender: n.Tender.Hex(),
Channels: n.Channels,
Type: n.Type,
Priority: n.Priority,
Target: n.Target,
Title: n.Title,
Description: n.Description,
Link: *n.Link,
Schedule: ScheduleResponse{
Time: n.Schedule.Time,
},
}
}
// SearchForm represents the form for searching notifications
type SearchForm struct {
Search *string `query:"q" valid:"optional"`
}
+188
View File
@@ -0,0 +1,188 @@
package notification
import (
"github.com/asaskevich/govalidator"
"github.com/labstack/echo/v4"
"tm/pkg/logger"
"tm/pkg/response"
)
// NotificationHandler handles HTTP requests for notification operations
type NotificationHandler struct {
service NotificationService
logger logger.Logger
}
// NewNotificationHandler creates a new notification handler
func NewHandler(service NotificationService, logger logger.Logger) *NotificationHandler {
return &NotificationHandler{
service: service,
logger: logger,
}
}
// CreateNotification creates a new notification
// @Summary Create a new notification
// @Description Create a new notification with the provided information
// @Tags Admin-Notification
// @Accept json
// @Produce json
// @Param notification body NotificationRequest true "Notification information"
// @Success 201 {object} response.APIResponse "Notification created successfully"
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/notifications [post]
func (h *NotificationHandler) Create(c echo.Context) error {
var req NotificationRequest
if err := c.Bind(&req); err != nil {
return response.BadRequest(c, "Invalid request format", err.Error())
}
// Validate request
if valid, err := govalidator.ValidateStruct(req); !valid {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Create notification
resp, err := h.service.Create(c.Request().Context(), &req)
if err != nil {
return response.InternalServerError(c, "Failed to create notification")
}
return response.Created(c, resp, "Notification created successfully")
}
// UpdateNotification updates a notification
// @Summary Update notification
// @Description Update an existing notification
// @Tags Admin-Notification
// @Accept json
// @Produce json
// @Param id path string true "Notification ID"
// @Param notification body NotificationRequest true "Updated notification information"
// @Success 200 {object} response.APIResponse "Notification updated successfully"
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/notifications/{id} [put]
func (h *NotificationHandler) UpdateNotification(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Notification ID is required", "")
}
var req NotificationRequest
if err := c.Bind(&req); err != nil {
return response.BadRequest(c, "Invalid request format", err.Error())
}
// Update notification
_, err := h.service.Update(c.Request().Context(), id, &req)
if err != nil {
if err.Error() == "notification not found" {
return response.NotFound(c, "Notification not found")
}
return response.BadRequest(c, "Failed to update notification", err.Error())
}
return response.Success(c, nil, "Notification updated successfully")
}
// GetNotifications retrieves notifications for a user
// @Summary Get user notifications
// @Description Retrieve notifications for a specific user with filtering and pagination
// @Tags Admin-Notification
// @Accept json
// @Produce json
// @Param recipient query string true "Recipient"
// @Param type query string false "Notification type (info, warning, alert, reject)"
// @Param priority query string false "Notification priority (low, medium, important)"
// @Param status query string false "Delivery status (pending, sent, failed)"
// @Param is_read query bool false "Read status"
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(20)
// @Param sort_by query string false "Sort field" default(created_at)
// @Param sort_order query string false "Sort order (asc, desc)" default(desc)
// @Success 200 {object} response.APIResponse{data=NotificationsResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/notifications [get]
func (h *NotificationHandler) Search(c echo.Context) error {
req, err := response.Parse[SearchForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination := response.NewPagination(c)
// Validate request
if valid, err := govalidator.ValidateStruct(req); !valid {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Get notifications
resp, err := h.service.Search(c.Request().Context(), req, pagination)
if err != nil {
return response.InternalServerError(c, "Failed to get notifications")
}
return response.Success(c, resp, "Notifications retrieved successfully")
}
// Get retrieves a notification by ID
// @Summary Get notification by ID
// @Description Retrieve a specific notification by its ID
// @Tags Admin-Notification
// @Accept json
// @Produce json
// @Param id path string true "Notification ID"
// @Success 200 {object} response.APIResponse{data=NotificationResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/notifications/{id} [get]
func (h *NotificationHandler) Get(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Notification ID is required", "")
}
// Get notification
resp, err := h.service.Get(c.Request().Context(), id)
if err != nil {
if err.Error() == "notification not found" {
return response.NotFound(c, "Notification not found")
}
return response.InternalServerError(c, "Failed to get notification")
}
return response.Success(c, resp, "Notification retrieved successfully")
}
// DeleteNotification deletes a notification
// @Summary Delete notification
// @Description Delete a specific notification
// @Tags Admin-Notification
// @Accept json
// @Produce json
// @Param id path string true "Notification ID"
// @Success 200 {object} response.APIResponse "Notification deleted successfully"
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/notifications/{id} [delete]
func (h *NotificationHandler) DeleteNotification(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Notification ID is required", "")
}
// Delete notification
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
return response.InternalServerError(c, "Failed to delete notification")
}
return response.Success(c, nil, "Notification deleted successfully")
}
+103
View File
@@ -0,0 +1,103 @@
package notification
import (
"context"
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/v2/bson"
)
// NotificationRepository defines the interface for notification data operations
type NotificationRepository interface {
Create(ctx context.Context, notification *Notification) error
Update(ctx context.Context, notification *Notification) error
Get(ctx context.Context, id string) (*Notification, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Notification, int64, error)
Delete(ctx context.Context, id string) error
}
// notificationRepository implements NotificationRepository interface
type notificationRepository struct {
ormRepo orm.Repository[Notification]
logger logger.Logger
}
func collection() string {
return "notifications"
}
// NewNotificationRepository creates a new notification repository
func NewNotificationRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) NotificationRepository {
// Create indexes using the ORM's index management
indexes := []orm.Index{
*orm.CreateTextIndex("search_idx", "title", "description"),
}
// Create indexes
err := mongoManager.CreateIndexes(collection(), indexes)
if err != nil {
logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
})
}
ormRepo := orm.NewRepository[Notification](mongoManager.GetCollection(collection()), logger)
return &notificationRepository{
ormRepo: ormRepo,
logger: logger,
}
}
// Create creates a new notification
func (r *notificationRepository) Create(ctx context.Context, notification *Notification) error {
notification.SetCreatedAt(time.Now().Unix())
return r.ormRepo.Create(ctx, notification)
}
// Create creates a new notification
func (r *notificationRepository) Update(ctx context.Context, notification *Notification) error {
notification.SetUpdatedAt(time.Now().Unix())
return r.ormRepo.Update(ctx, notification)
}
// Get gets a notification by id
func (r *notificationRepository) Get(ctx context.Context, id string) (*Notification, error) {
return r.ormRepo.FindByID(ctx, id)
}
// Search searches for notifications
func (r *notificationRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Notification, int64, error) {
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(pagination.SortBy, pagination.SortOrder)
filter := bson.M{}
if form.Search != nil {
filter["$text"] = bson.M{"$search": *form.Search}
}
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder.Build())
if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
"search": form.Search,
})
return nil, 0, err
}
return result.Items, result.TotalCount, nil
}
// Delete deletes a notification
func (r *notificationRepository) Delete(ctx context.Context, id string) error {
return r.ormRepo.Delete(ctx, id)
}
+199
View File
@@ -0,0 +1,199 @@
package notification
import (
"context"
"errors"
"fmt"
"tm/pkg/logger"
"tm/pkg/response"
)
// NotificationService defines the interface for notification business logic
type NotificationService interface {
Create(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error)
Update(ctx context.Context, id string, req *NotificationRequest) (*NotificationResponse, error)
Get(ctx context.Context, id string) (*NotificationResponse, error)
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationsResponse, error)
Delete(ctx context.Context, id string) error
}
// notificationService implements NotificationService interface
type notificationService struct {
repository NotificationRepository
logger logger.Logger
}
// NewNotificationService creates a new notification service
func NewNotificationService(
repository NotificationRepository,
logger logger.Logger,
) NotificationService {
return &notificationService{
repository: repository,
logger: logger,
}
}
// Create creates a single notification
func (s *notificationService) Create(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) {
s.logger.Info("Creating notification", map[string]interface{}{
"recipient": req.Recipient,
"type": req.Type,
"priority": req.Priority,
})
// Create notification entity
notification := &Notification{
Recipient: req.Recipient,
Tender: req.Tender,
Channels: req.Channels,
Type: req.Type,
Priority: req.Priority,
Title: req.Title,
Description: req.Description,
Link: req.Link,
Status: DeliveryStatusPending,
Schedule: Schedule{
Time: req.Schedule.Time,
},
}
// Create notification in repository
if err := s.repository.Create(ctx, notification); err != nil {
s.logger.Error("Failed to create notification", map[string]interface{}{
"recipient": req.Recipient,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to create notification")
}
s.logger.Info("Notification created", map[string]interface{}{
"notification_id": notification.ID,
"recipient": req.Recipient,
"channels": req.Channels,
})
return notification.ToResponse(), nil
}
// Update updates a notification
func (s *notificationService) Update(ctx context.Context, id string, req *NotificationRequest) (*NotificationResponse, error) {
s.logger.Info("Updating notification", map[string]interface{}{
"notification_id": id,
})
notification, err := s.repository.Get(ctx, id)
if err != nil {
s.logger.Error("Failed to get notification", map[string]interface{}{
"notification_id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get notification")
}
if notification.Status == DeliveryStatusSent {
return nil, fmt.Errorf("notification already sent")
}
// update notification fields
notification.Recipient = req.Recipient
notification.Tender = req.Tender
notification.Channels = req.Channels
notification.Type = req.Type
notification.Priority = req.Priority
notification.Title = req.Title
notification.Description = req.Description
notification.Link = req.Link
notification.Schedule = Schedule{
Time: req.Schedule.Time,
}
notification.Status = DeliveryStatusPending
// update notification in repository
if err := s.repository.Update(ctx, notification); err != nil {
s.logger.Error("Failed to update notification", map[string]interface{}{
"notification_id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to update notification")
}
s.logger.Info("Notification updated", map[string]interface{}{
"notification_id": id,
"recipient": req.Recipient,
"channels": req.Channels,
})
return notification.ToResponse(), nil
}
// Get gets a notification
func (s *notificationService) Get(ctx context.Context, id string) (*NotificationResponse, error) {
s.logger.Info("Getting notification", map[string]interface{}{
"notification_id": id,
})
notification, err := s.repository.Get(ctx, id)
if err != nil {
s.logger.Error("Failed to get notification", map[string]interface{}{
"notification_id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get notification")
}
return notification.ToResponse(), nil
}
// Search searches for notifications
func (s *notificationService) Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationsResponse, error) {
notifications, total, err := s.repository.Search(ctx, req, pagination)
if err != nil {
s.logger.Error("Failed to search notifications", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search notifications")
}
var notificationResponses []*NotificationResponse
for _, notification := range notifications {
notificationResponses = append(notificationResponses, notification.ToResponse())
}
return &NotificationsResponse{
Notifications: notificationResponses,
Meta: pagination.Response(total),
}, nil
}
// Delete deletes a notification
func (s *notificationService) Delete(ctx context.Context, id string) error {
s.logger.Info("Deleting notification", map[string]interface{}{
"notification_id": id,
})
notification, err := s.repository.Get(ctx, id)
if err != nil {
s.logger.Error("Failed to get notification", map[string]interface{}{
"notification_id": id,
"error": err.Error(),
})
return fmt.Errorf("failed to get notification")
}
if notification.Status == DeliveryStatusSent {
return fmt.Errorf("notification already sent")
}
err = s.repository.Delete(ctx, id)
if err != nil {
s.logger.Error("Failed to delete notification", map[string]interface{}{
"notification_id": id,
"error": err.Error(),
})
return err
}
return nil
}