From 870f6758e0bc07b8c27b01ff29c859cc6c3bda56 Mon Sep 17 00:00:00 2001 From: "m.nazemi" Date: Wed, 22 Apr 2026 18:39:40 +0330 Subject: [PATCH] profile keyword manual update and recommended tenders --- cmd/web/router/routes.go | 1 + internal/customer/form.go | 5 ++ internal/customer/handler.go | 41 ++++++++++ internal/customer/service.go | 154 ++++++++++++++++++++++------------- internal/tender/handler.go | 28 ++++++- 5 files changed, 169 insertions(+), 60 deletions(-) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 86b816d..5cc6479 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -229,6 +229,7 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende profileGP := customerGP.Group("") profileGP.Use(customerHandler.AuthMiddleware()) profileGP.GET("", customerHandler.GetProfile) + profileGP.PUT("/keywords", customerHandler.UpdateProfileKeywords) profileGP.DELETE("/logout", customerHandler.Logout) } diff --git a/internal/customer/form.go b/internal/customer/form.go index 0b95e54..e413b6f 100644 --- a/internal/customer/form.go +++ b/internal/customer/form.go @@ -181,6 +181,11 @@ type ResetPasswordForm struct { NewPassword string `json:"new_password" valid:"required,length(5|128)" example:"NewPass!123"` } +// UpdateProfileKeywordsForm represents the form for updating profile keywords +type UpdateProfileKeywordsForm struct { + Keywords []string `json:"keywords" valid:"required"` +} + // RequestResetPasswordResponse represents the response for password reset request type RequestResetPasswordResponse struct { Message string `json:"message" example:"Password reset code sent to your email"` diff --git a/internal/customer/handler.go b/internal/customer/handler.go index ba15681..7a54374 100644 --- a/internal/customer/handler.go +++ b/internal/customer/handler.go @@ -1,6 +1,7 @@ package customer import ( + "strings" "tm/internal/user" "tm/pkg/authorization" "tm/pkg/logger" @@ -57,6 +58,9 @@ func (h *Handler) CreateCustomer(c echo.Context) error { err.Error() == "company with this tax ID already exists" { return response.Conflict(c, err.Error()) } + if strings.HasPrefix(err.Error(), "invalid company ID") || err.Error() == "invalid username format" { + return response.BadRequest(c, err.Error(), "") + } return response.InternalServerError(c, "Failed to create customer") } @@ -397,6 +401,43 @@ func (h *Handler) GetProfile(c echo.Context) error { return response.Success(c, resp, "Profile retrieved successfully") } +// UpdateProfileKeywords updates customer profile keywords (Mobile) +// @Summary Update customer profile keywords +// @Description Update profile keywords manually and trigger asynchronous GLM enhancement based on customer company information (including country). +// @Tags Authorization +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body UpdateProfileKeywordsForm true "Manual keywords" +// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Profile keywords update started" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token" +// @Failure 404 {object} response.APIResponse "Not found - Customer not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Router /api/v1/profile/keywords [patch] +func (h *Handler) UpdateProfileKeywords(c echo.Context) error { + customerID, err := GetCustomerIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "Customer not authenticated") + } + + form, err := response.Parse[UpdateProfileKeywordsForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + resp, err := h.service.UpdateProfileKeywords(c.Request().Context(), customerID, form.Keywords) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to update profile keywords") + } + + return response.Success(c, resp, "Profile keywords update started") +} + // Logout handles customer logout (Mobile) // @Summary Customer logout // @Description Logout customer and invalidate access token diff --git a/internal/customer/service.go b/internal/customer/service.go index 8fd547a..0e15209 100644 --- a/internal/customer/service.go +++ b/internal/customer/service.go @@ -41,6 +41,7 @@ type Service interface { Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) + UpdateProfileKeywords(ctx context.Context, customerID string, keywords []string) (*CustomerResponse, error) Logout(ctx context.Context, id string, accessToken string) error // Forgot password operations @@ -96,6 +97,9 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm var Companies []string if len(form.Companies) > 0 { for _, companyID := range form.Companies { + if _, err := bson.ObjectIDFromHex(companyID); err != nil { + return nil, errors.New("invalid company ID format: " + companyID) + } _, err := s.companyService.GetByID(ctx, companyID) if err != nil { s.logger.Error("Invalid company ID provided", map[string]interface{}{ @@ -405,62 +409,7 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus customer.KeywordsStatus = KeywordsStatusInProgress // Save customer with in_progress status first, then process asynchronously - customerID := customer.ID.Hex() - go func() { - ctx := context.Background() - // Get fresh customer data for processing - cust, err := s.repository.GetByID(ctx, customerID) - if err != nil { - s.logger.Error("Failed to get customer for keyword processing", map[string]interface{}{ - "error": err.Error(), - "customer_id": customerID, - }) - return - } - - // If GLM SDK is not available, keep manual keywords and set status to ready - if s.glmSDK == nil { - s.logger.Info("GLM SDK not available, keeping manual keywords", map[string]interface{}{ - "customer_id": customerID, - }) - cust.KeywordsStatus = KeywordsStatusReady - } else { - // Regenerate keywords based on updated customer info - keywords, err := s.generateKeywords(ctx, cust) - if err != nil { - s.logger.Warn("Failed to process keywords after manual update", map[string]interface{}{ - "error": err.Error(), - "customer_id": customerID, - }) - // Keep manual keywords but set status to failed - cust.KeywordsStatus = KeywordsStatusFailed - } else if len(keywords) > 0 { - // Update with AI-generated keywords - cust.Keywords = keywords - cust.KeywordsStatus = KeywordsStatusReady - } else { - // Empty keywords from GLM, keep manual keywords and set status to ready - s.logger.Warn("GLM returned empty keywords, keeping manual keywords", map[string]interface{}{ - "customer_id": customerID, - }) - cust.KeywordsStatus = KeywordsStatusReady - } - } - - // Update customer with new status - err = s.repository.Update(ctx, cust) - if err != nil { - s.logger.Error("Failed to update customer keywords status", map[string]interface{}{ - "error": err.Error(), - "customer_id": customerID, - }) - } else { - s.logger.Info("Keywords processed successfully", map[string]interface{}{ - "customer_id": customerID, - "status": cust.KeywordsStatus, - }) - } - }() + s.processKeywordsAsync(customer.ID.Hex()) } else if infoChanged || companiesChanged { // Customer info changed, regenerate keywords customer.KeywordsStatus = KeywordsStatusInProgress @@ -907,6 +856,42 @@ func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerI return customerResponse, nil } +// UpdateProfileKeywords updates customer profile keywords and triggers AI enhancement asynchronously. +func (s *customerService) UpdateProfileKeywords(ctx context.Context, customerID string, keywords []string) (*CustomerResponse, error) { + customer, err := s.repository.GetByID(ctx, customerID) + if err != nil { + s.logger.Error("Failed to get customer for profile keyword update", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return nil, errors.New("customer not found") + } + + customer.Keywords = keywords + customer.KeywordsStatus = KeywordsStatusInProgress + + if err := s.repository.Update(ctx, customer); err != nil { + s.logger.Error("Failed to save profile keywords", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return nil, errors.New("failed to update profile keywords") + } + + s.processKeywordsAsync(customerID) + + companies, err := s.loadCompaniesForCustomer(ctx, customer) + if err != nil { + s.logger.Error("Failed to load companies after profile keyword update", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + companies = []*CompanySummary{} + } + + return customer.ToResponse(companies), nil +} + // Logout handles customer logout func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error { s.logger.Info("Customer logout", map[string]interface{}{ @@ -1398,6 +1383,12 @@ func (s *customerService) generateKeywords(ctx context.Context, customer *Custom if comp.Industry != "" { customerInfo.WriteString(fmt.Sprintf(" (Industry: %s)", comp.Industry)) } + if comp.Address != nil && comp.Address.Country != "" { + customerInfo.WriteString(fmt.Sprintf(" (Country: %s)", comp.Address.Country)) + } + if comp.Tags != nil && len(comp.Tags.Keywords) > 0 { + customerInfo.WriteString(fmt.Sprintf(" (Company Keywords: %s)", strings.Join(comp.Tags.Keywords, ", "))) + } } } customerInfo.WriteString("\n") @@ -1452,3 +1443,54 @@ Return ONLY a comma-separated list of keywords, no explanations or additional te return keywords, nil } + +func (s *customerService) processKeywordsAsync(customerID string) { + go func() { + ctx := context.Background() + cust, err := s.repository.GetByID(ctx, customerID) + if err != nil { + s.logger.Error("Failed to get customer for keyword processing", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return + } + + if s.glmSDK == nil { + s.logger.Info("GLM SDK not available, keeping manual keywords", map[string]interface{}{ + "customer_id": customerID, + }) + cust.KeywordsStatus = KeywordsStatusReady + } else { + keywords, err := s.generateKeywords(ctx, cust) + if err != nil { + s.logger.Warn("Failed to process keywords after manual update", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + cust.KeywordsStatus = KeywordsStatusFailed + } else if len(keywords) > 0 { + cust.Keywords = keywords + cust.KeywordsStatus = KeywordsStatusReady + } else { + s.logger.Warn("GLM returned empty keywords, keeping manual keywords", map[string]interface{}{ + "customer_id": customerID, + }) + cust.KeywordsStatus = KeywordsStatusReady + } + } + + err = s.repository.Update(ctx, cust) + if err != nil { + s.logger.Error("Failed to update customer keywords status", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + } else { + s.logger.Info("Keywords processed successfully", map[string]interface{}{ + "customer_id": customerID, + "status": cust.KeywordsStatus, + }) + } + }() +} diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 712eef9..74a6dde 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -234,6 +234,16 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { return response.InternalServerError(c, "Failed to retrieve tenders") } + if result != nil { + filtered := make([]TenderResponse, 0, len(result.Tenders)) + for _, t := range result.Tenders { + if t.TenderDeadline != 0 { + filtered = append(filtered, t) + } + } + result.Tenders = filtered + result.Metadata.Total = int(len(filtered)) + } return response.SuccessWithMeta(c, result, result.Metadata, "Tenders retrieved successfully") } @@ -277,6 +287,11 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error { return response.ValidationError(c, err.Error(), "") } + result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c)) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve tenders") + } + form.Status = []string{string(TenderStatusActive)} var companyID *string @@ -291,11 +306,16 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error { } form.CustomerID = customerID - result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c)) - if err != nil { - return response.InternalServerError(c, "Failed to retrieve tenders") + if result != nil { + filtered := make([]TenderResponse, 0, len(result.Tenders)) + for _, t := range result.Tenders { + if t.TenderDeadline != 0 { + filtered = append(filtered, t) + } + } + result.Tenders = filtered + result.Metadata.Total = int(len(filtered)) } - return response.SuccessWithMeta(c, result, result.Metadata, "Tenders retrieved successfully") }