From 3b26c4f5e185a953eb2e7aea5a08cae328c1294e Mon Sep 17 00:00:00 2001 From: Mazyar Date: Thu, 11 Jun 2026 01:08:59 +0330 Subject: [PATCH 1/2] Implement AI onboarding and recommendation features in company service - Added new AI onboarding and recommendation endpoints in the company handler for starting onboarding and retrieving ranked tender recommendations. - Introduced `StartAIOnboarding` and `GetAIRecommendations` methods in the company service to handle AI interactions. - Updated the company service constructor to include the AI recommendation client. - Enhanced the AI summarizer client with methods for onboarding and fetching recommendations. - Added response structures for onboarding and recommended tenders in the company form. This update enhances the tender management system by integrating AI capabilities for onboarding and tender recommendations, improving user experience and operational efficiency. --- cmd/web/main.go | 4 +- cmd/web/router/routes.go | 8 ++ internal/company/form.go | 12 ++ internal/company/handler.go | 67 +++++++++++ internal/company/recommendation.go | 186 +++++++++++++++++++++++++++++ internal/company/service.go | 32 +++-- pkg/ai_summarizer/client.go | 66 ++++++++++ pkg/ai_summarizer/entities.go | 24 ++++ 8 files changed, 389 insertions(+), 10 deletions(-) create mode 100644 internal/company/recommendation.go diff --git a/cmd/web/main.go b/cmd/web/main.go index f7ac0ac..8b6d241 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -167,9 +167,11 @@ func main() { // Initialize AI Summarizer service var aiSummarizerClient tender.AISummarizerClient var aiSummarizerStorage tender.AISummarizerStorage + var aiRecommendationClient company.AIRecommendationClient if c := bootstrap.InitAISummarizerClient(conf.AISummarizer, mongoManager, logger); c != nil { aiSummarizerClient = c + aiRecommendationClient = c } if s := bootstrap.InitAISummarizerStorage(conf.AISummarizer, logger); s != nil { aiSummarizerStorage = s @@ -210,7 +212,7 @@ func main() { auditLogger := audit.NewLogger(logger) userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK, redisClient, auditLogger) categoryService := company_category.NewService(categoryRepository, logger) - companyService := company.NewService(companyRepository, categoryService, fileStoreService, logger) + companyService := company.NewService(companyRepository, categoryService, fileStoreService, aiRecommendationClient, logger) customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator, auditLogger) tenderService := tender.NewService(tenderRepository, companyService, customerService, logger, aiSummarizerClient, aiSummarizerStorage, conf.AISummarizer.DefaultLanguage) feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 78625b8..9d4aaf7 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -291,6 +291,14 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende companiesGP.GET("", companyHandler.MyCompany) } + // AI tender recommendation routes + recommendationGP := v1.Group("") + recommendationGP.Use(customerHandler.AuthMiddleware()) + { + recommendationGP.POST("/onboarding", companyHandler.StartOnboarding) + recommendationGP.POST("/recommend", companyHandler.RecommendTenders) + } + // Public Inquiry Routes inquiryGP := v1.Group("/inquiries") { diff --git a/internal/company/form.go b/internal/company/form.go index 80a8942..2cd4c58 100644 --- a/internal/company/form.go +++ b/internal/company/form.go @@ -185,6 +185,18 @@ func (c *CompanyTags) ToResponse(categories []*CategoryResponse) *CompanyTagsRes } } +// OnboardingResponse is returned after starting AI company onboarding. +type OnboardingResponse struct { + Status string `json:"status"` +} + +// RecommendedTenderResponse is one ranked tender recommendation from the AI service. +type RecommendedTenderResponse struct { + Rank string `json:"rank"` + TenderID string `json:"tender_id"` + Analysis string `json:"analysis"` +} + // CompanyProfileResponse represents the company profile data sent in API responses type CompanyProfileResponse struct { ID string `json:"id"` diff --git a/internal/company/handler.go b/internal/company/handler.go index c7ad89e..a16d2c5 100644 --- a/internal/company/handler.go +++ b/internal/company/handler.go @@ -1,6 +1,7 @@ package company import ( + "errors" "mime/multipart" "tm/internal/user" @@ -53,6 +54,72 @@ func (h *Handler) MyCompany(c echo.Context) error { return response.Success(c, company, "Company retrieved successfully") } +// StartOnboarding starts AI onboarding for the authenticated customer's company (Mobile App) +// @Summary Start AI company onboarding +// @Description Send company profile, uploaded documents, and website URL to the AI team to start tender recommendation onboarding +// @Tags Company +// @Accept json +// @Produce json +// @Success 200 {object} response.APIResponse{data=OnboardingResponse} +// @Failure 401 {object} response.APIResponse "Unauthorized - User not authenticated" +// @Failure 404 {object} response.APIResponse "Company not found" +// @Failure 503 {object} response.APIResponse "AI recommendation service unavailable" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /api/v1/onboarding [post] +func (h *Handler) StartOnboarding(c echo.Context) error { + companyID, err := user.GetCompanyIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + result, err := h.service.StartAIOnboarding(c.Request().Context(), companyID) + if err != nil { + if errors.Is(err, ErrAIRecommendationNotConfigured) { + return response.ServiceUnavailable(c, "AI recommendation service is not configured") + } + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to start AI onboarding") + } + + return response.Success(c, result, "AI onboarding started successfully") +} + +// RecommendTenders returns AI-ranked tender recommendations for the authenticated customer's company (Mobile App) +// @Summary Get AI tender recommendations +// @Description Retrieve ranked tender recommendations from the AI team for the authenticated customer's company +// @Tags Company +// @Accept json +// @Produce json +// @Success 200 {object} response.APIResponse{data=[]RecommendedTenderResponse} +// @Failure 401 {object} response.APIResponse "Unauthorized - User not authenticated" +// @Failure 404 {object} response.APIResponse "Company not found" +// @Failure 503 {object} response.APIResponse "AI recommendation service unavailable" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /api/v1/recommend [post] +func (h *Handler) RecommendTenders(c echo.Context) error { + companyID, err := user.GetCompanyIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + result, err := h.service.GetAIRecommendations(c.Request().Context(), companyID) + if err != nil { + if errors.Is(err, ErrAIRecommendationNotConfigured) { + return response.ServiceUnavailable(c, "AI recommendation service is not configured") + } + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to retrieve AI recommendations") + } + + return response.Success(c, result, "AI recommendations retrieved successfully") +} + // **************************** ADMIN ENDPOINTS **************************** // Create creates a new company (Web Panel) diff --git a/internal/company/recommendation.go b/internal/company/recommendation.go new file mode 100644 index 0000000..7d9654b --- /dev/null +++ b/internal/company/recommendation.go @@ -0,0 +1,186 @@ +package company + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + "tm/pkg/ai_summarizer" +) + +var ErrAIRecommendationNotConfigured = errors.New("AI recommendation service is not configured") + +// AIRecommendationClient defines the interface for company tender recommendation AI operations. +type AIRecommendationClient interface { + StartOnboarding(ctx context.Context, reqBody ai_summarizer.OnboardingRequest) (*ai_summarizer.OnboardingResponse, error) + FetchRecommendations(ctx context.Context, reqBody ai_summarizer.RecommendRequest) ([]ai_summarizer.RecommendedTender, error) +} + +type onboardingDocumentPayload struct { + Filename string `json:"filename"` + ContentType string `json:"content_type"` + Content string `json:"content"` +} + +// StartAIOnboarding sends company profile data to the AI team to start recommendation onboarding. +func (s *companyService) StartAIOnboarding(ctx context.Context, companyID string) (*OnboardingResponse, error) { + if s.aiRecommendationClient == nil { + return nil, ErrAIRecommendationNotConfigured + } + if strings.TrimSpace(companyID) == "" { + return nil, errors.New("company ID is required") + } + + company, err := s.repository.GetByID(ctx, companyID) + if err != nil { + s.logger.Error("Failed to load company for AI onboarding", map[string]interface{}{ + "company_id": companyID, + "error": err.Error(), + }) + return nil, errors.New("company not found") + } + + documents, err := s.buildOnboardingDocuments(company.DocumentFileIDs) + if err != nil { + s.logger.Error("Failed to prepare company documents for AI onboarding", map[string]interface{}{ + "company_id": companyID, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to prepare company documents: %w", err) + } + + websiteURL := "" + if company.Website != nil { + websiteURL = strings.TrimSpace(*company.Website) + } + + s.logger.Info("Starting AI company onboarding", map[string]interface{}{ + "company_id": companyID, + "documents_count": len(company.DocumentFileIDs), + "has_website_url": websiteURL != "", + }) + + result, err := s.aiRecommendationClient.StartOnboarding(ctx, ai_summarizer.OnboardingRequest{ + CompanyID: companyID, + Documents: documents, + WebsiteURL: websiteURL, + }) + if err != nil { + s.logger.Error("AI onboarding request failed", map[string]interface{}{ + "company_id": companyID, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to start AI onboarding: %w", err) + } + + return &OnboardingResponse{Status: result.Status}, nil +} + +// GetAIRecommendations retrieves ranked tender recommendations from the AI team. +func (s *companyService) GetAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, error) { + if s.aiRecommendationClient == nil { + return nil, ErrAIRecommendationNotConfigured + } + if strings.TrimSpace(companyID) == "" { + return nil, errors.New("company ID is required") + } + + if _, err := s.repository.GetByID(ctx, companyID); err != nil { + s.logger.Error("Failed to load company for AI recommendations", map[string]interface{}{ + "company_id": companyID, + "error": err.Error(), + }) + return nil, errors.New("company not found") + } + + s.logger.Info("Fetching AI tender recommendations", map[string]interface{}{ + "company_id": companyID, + }) + + items, err := s.aiRecommendationClient.FetchRecommendations(ctx, ai_summarizer.RecommendRequest{ + CompanyID: companyID, + }) + if err != nil { + s.logger.Error("AI recommend request failed", map[string]interface{}{ + "company_id": companyID, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to fetch AI recommendations: %w", err) + } + + responses := make([]RecommendedTenderResponse, 0, len(items)) + for _, item := range items { + responses = append(responses, RecommendedTenderResponse{ + Rank: item.Rank, + TenderID: item.TenderID, + Analysis: item.Analysis, + }) + } + + return responses, nil +} + +func (s *companyService) buildOnboardingDocuments(fileIDs []string) (string, error) { + if len(fileIDs) == 0 { + return "[]", nil + } + if s.fileStore == nil { + return "", errors.New("file storage is not configured") + } + + payloads := make([]onboardingDocumentPayload, 0, len(fileIDs)) + for _, fileID := range fileIDs { + if strings.TrimSpace(fileID) == "" { + continue + } + + reader, info, err := s.fileStore.Download(fileID) + if err != nil { + s.logger.Warn("Skipping company document for AI onboarding", map[string]interface{}{ + "file_id": fileID, + "error": err.Error(), + }) + continue + } + + content, readErr := io.ReadAll(reader) + closeErr := reader.Close() + if readErr != nil { + return "", fmt.Errorf("failed to read document %s: %w", fileID, readErr) + } + if closeErr != nil { + s.logger.Warn("Failed to close company document reader", map[string]interface{}{ + "file_id": fileID, + "error": closeErr.Error(), + }) + } + + filename := fileID + contentType := "application/octet-stream" + if info != nil { + if info.Filename != "" { + filename = info.Filename + } + if info.ContentType != "" { + contentType = info.ContentType + } + } + + payloads = append(payloads, onboardingDocumentPayload{ + Filename: filename, + ContentType: contentType, + Content: base64.StdEncoding.EncodeToString(content), + }) + } + + encoded, err := json.Marshal(payloads) + if err != nil { + return "", fmt.Errorf("failed to encode company documents: %w", err) + } + + return string(encoded), nil +} diff --git a/internal/company/service.go b/internal/company/service.go index 9590b88..e0db91a 100644 --- a/internal/company/service.go +++ b/internal/company/service.go @@ -34,23 +34,37 @@ type Service interface { // UploadDocuments uploads files and attaches them to a company UploadDocuments(ctx context.Context, companyID, uploadedBy string, files []*multipart.FileHeader) (*UploadDocumentsResponse, error) + + // StartAIOnboarding sends company data to the AI team for recommendation onboarding + StartAIOnboarding(ctx context.Context, companyID string) (*OnboardingResponse, error) + + // GetAIRecommendations returns ranked tender recommendations from the AI team + GetAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, error) } // companyService implements the Service interface type companyService struct { - repository Repository - categoryService company_category.Service - fileStore filestore.FileStoreService - logger logger.Logger + repository Repository + categoryService company_category.Service + fileStore filestore.FileStoreService + aiRecommendationClient AIRecommendationClient + logger logger.Logger } // NewService creates a new company service -func NewService(repository Repository, categoryService company_category.Service, fileStore filestore.FileStoreService, logger logger.Logger) Service { +func NewService( + repository Repository, + categoryService company_category.Service, + fileStore filestore.FileStoreService, + aiRecommendationClient AIRecommendationClient, + logger logger.Logger, +) Service { return &companyService{ - repository: repository, - categoryService: categoryService, - fileStore: fileStore, - logger: logger, + repository: repository, + categoryService: categoryService, + fileStore: fileStore, + aiRecommendationClient: aiRecommendationClient, + logger: logger, } } diff --git a/pkg/ai_summarizer/client.go b/pkg/ai_summarizer/client.go index 70c3551..6d4f620 100644 --- a/pkg/ai_summarizer/client.go +++ b/pkg/ai_summarizer/client.go @@ -209,6 +209,72 @@ func (c *Client) TriggerPipelineSummarize(ctx context.Context) (*PipelineActionR return c.triggerPipelineAction(ctx, "/pipeline/summarize", "pipeline summarize") } +// StartOnboarding calls POST /onboarding to start company profile indexing for recommendations. +func (c *Client) StartOnboarding(ctx context.Context, reqBody OnboardingRequest) (*OnboardingResponse, error) { + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("ai_summarizer: failed to marshal onboarding request body: %w", err) + } + + url := c.config.APIBaseURL + "/onboarding" + httpResp, bodyBytes, err := c.doRawPost(ctx, url, jsonBody) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + if httpResp.StatusCode >= 400 { + return nil, fmt.Errorf("%w: status %d, body: %s", ErrAPINonSuccess, httpResp.StatusCode, string(bodyBytes)) + } + + var result OnboardingResponse + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, &result); err != nil { + return nil, fmt.Errorf("ai_summarizer: failed to decode onboarding response: %w", err) + } + } + + c.logger.Info("AI onboarding request succeeded", map[string]interface{}{ + "company_id": reqBody.CompanyID, + "status": result.Status, + }) + + return &result, nil +} + +// FetchRecommendations calls POST /recommend to retrieve ranked tenders for a company. +func (c *Client) FetchRecommendations(ctx context.Context, reqBody RecommendRequest) ([]RecommendedTender, error) { + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("ai_summarizer: failed to marshal recommend request body: %w", err) + } + + url := c.config.APIBaseURL + "/recommend" + httpResp, bodyBytes, err := c.doRawPost(ctx, url, jsonBody) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + if httpResp.StatusCode >= 400 { + return nil, fmt.Errorf("%w: status %d, body: %s", ErrAPINonSuccess, httpResp.StatusCode, string(bodyBytes)) + } + + var result []RecommendedTender + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, &result); err != nil { + return nil, fmt.Errorf("ai_summarizer: failed to decode recommend response: %w", err) + } + } + + c.logger.Info("AI recommend request succeeded", map[string]interface{}{ + "company_id": reqBody.CompanyID, + "count": len(result), + }) + + return result, nil +} + func (c *Client) triggerPipelineAction(ctx context.Context, path, label string) (*PipelineActionResponse, error) { url := c.config.APIBaseURL + path httpResp, bodyBytes, err := c.doRawPost(ctx, url, nil) diff --git a/pkg/ai_summarizer/entities.go b/pkg/ai_summarizer/entities.go index 430d04b..d751f49 100644 --- a/pkg/ai_summarizer/entities.go +++ b/pkg/ai_summarizer/entities.go @@ -138,6 +138,30 @@ type PipelineActionResponse struct { Status string `json:"status"` } +// OnboardingRequest is the payload for POST /onboarding. +type OnboardingRequest struct { + CompanyID string `json:"company_id"` + Documents string `json:"documents"` + WebsiteURL string `json:"website_url"` +} + +// OnboardingResponse is returned by POST /onboarding. +type OnboardingResponse struct { + Status string `json:"status"` +} + +// RecommendRequest is the payload for POST /recommend. +type RecommendRequest struct { + CompanyID string `json:"company_id"` +} + +// RecommendedTender is one ranked tender from POST /recommend. +type RecommendedTender struct { + Rank string `json:"rank"` + TenderID string `json:"tender_id"` + Analysis string `json:"analysis"` +} + // StoredTranslation is one language entry inside tender.json translations map. type StoredTranslation struct { Title string `json:"title"` From f16f9fb5a9cc938ff1d0735e09fcdad4c451a9ef Mon Sep 17 00:00:00 2001 From: Mazyar Date: Thu, 11 Jun 2026 01:33:47 +0330 Subject: [PATCH 2/2] Enhance notification search functionality and update API documentation - Added `search` and `q` query parameters to the `SearchForm` for improved notification searching capabilities. - Implemented `ResolvedSearch` method to prioritize search term resolution from the new parameters. - Updated `GetByUserID` repository method to support searching notifications by title and message using regex. - Enhanced logging in the `GetNotifications` service method to include search parameters. - Updated Swagger documentation to reflect the new search parameters for the notification API. This update improves the user experience by allowing more flexible and efficient searching of notifications. --- internal/notification/form.go | 27 +++++++++++++++++++++------ internal/notification/handler.go | 2 ++ internal/notification/repository.go | 14 ++++++++++++-- internal/notification/service.go | 5 ++++- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/internal/notification/form.go b/internal/notification/form.go index 7878db1..1a15eb5 100644 --- a/internal/notification/form.go +++ b/internal/notification/form.go @@ -1,6 +1,8 @@ package notification import ( + "strings" + "tm/pkg/response" ) @@ -21,13 +23,26 @@ type NotificationRequest struct { // SearchForm represents the form for searching notifications type SearchForm struct { - Status string `query:"status" valid:"optional"` - Method string `query:"method" valid:"optional"` - EventType string `query:"event_type" valid:"optional"` - Type string `query:"type" valid:"optional"` + Q *string `query:"q" valid:"optional"` + Search *string `query:"search" valid:"optional"` + Status string `query:"status" valid:"optional"` + Method string `query:"method" valid:"optional"` + EventType string `query:"event_type" valid:"optional"` + Type string `query:"type" valid:"optional"` Recipient []string `query:"recipient" json:"recipient" valid:"optional"` - Seen *bool `query:"seen" valid:"optional"` - Priority string `query:"priority" valid:"optional"` + Seen *bool `query:"seen" valid:"optional"` + Priority string `query:"priority" valid:"optional"` +} + +// ResolvedSearch returns the search term from either the search or q query parameter. +func (f *SearchForm) ResolvedSearch() string { + if f.Search != nil && strings.TrimSpace(*f.Search) != "" { + return strings.TrimSpace(*f.Search) + } + if f.Q != nil { + return strings.TrimSpace(*f.Q) + } + return "" } type NotificationListResponse struct { diff --git a/internal/notification/handler.go b/internal/notification/handler.go index 74922ae..73236f2 100644 --- a/internal/notification/handler.go +++ b/internal/notification/handler.go @@ -58,6 +58,8 @@ func (h *NotificationHandler) Send(c echo.Context) error { // @Tags Admin-Notification // @Accept json // @Produce json +// @Param search query string false "Search query for title and message" +// @Param q query string false "Search query for title and message (alias of search)" // @Param status query string false "Status filter" // @Param method query string false "Method filter" // @Param event_type query string false "Event type filter" diff --git a/internal/notification/repository.go b/internal/notification/repository.go index 98f9c9f..dcb8ea4 100644 --- a/internal/notification/repository.go +++ b/internal/notification/repository.go @@ -3,6 +3,8 @@ package notification import ( "context" "errors" + "regexp" + "strings" "time" "tm/pkg/logger" @@ -52,7 +54,7 @@ func (n *Notification) GetID() string { type Repository interface { Create(ctx context.Context, notification *Notification) error GetByID(ctx context.Context, id string) (*Notification, error) - GetByUserID(ctx context.Context, userIDs []string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) + GetByUserID(ctx context.Context, userIDs []string, search, 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 @@ -115,9 +117,17 @@ func (r *notificationRepository) GetByID(ctx context.Context, id string) (*Notif } // 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) { +func (r *notificationRepository) GetByUserID(ctx context.Context, userIDs []string, search, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) { filter := bson.M{} + if q := strings.TrimSpace(search); q != "" { + pattern := regexp.QuoteMeta(q) + filter["$or"] = bson.A{ + bson.M{"title": bson.M{"$regex": pattern, "$options": "i"}}, + bson.M{"message": bson.M{"$regex": pattern, "$options": "i"}}, + } + } + if len(userIDs) == 1 { filter["user_id"] = userIDs[0] } else if len(userIDs) > 1 { diff --git a/internal/notification/service.go b/internal/notification/service.go index 208ce81..59eb593 100644 --- a/internal/notification/service.go +++ b/internal/notification/service.go @@ -183,7 +183,10 @@ func (s *notificationService) persistNotification(ctx context.Context, recipient } func (s *notificationService) GetNotifications(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationListResponse, error) { + search := req.ResolvedSearch() + s.logger.Info("Getting notifications", map[string]interface{}{ + "search": search, "status": req.Status, "method": req.Method, "event_type": req.EventType, @@ -195,7 +198,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF "offset": pagination.Offset, }) - page, err := s.repository.GetByUserID(ctx, req.Recipient, req.Status, req.Seen, req.Priority, req.EventType, req.Type, pagination) + page, err := s.repository.GetByUserID(ctx, req.Recipient, search, 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(),