profile keyword manual update and recommended tenders
This commit is contained in:
@@ -229,6 +229,7 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
|
|||||||
profileGP := customerGP.Group("")
|
profileGP := customerGP.Group("")
|
||||||
profileGP.Use(customerHandler.AuthMiddleware())
|
profileGP.Use(customerHandler.AuthMiddleware())
|
||||||
profileGP.GET("", customerHandler.GetProfile)
|
profileGP.GET("", customerHandler.GetProfile)
|
||||||
|
profileGP.PUT("/keywords", customerHandler.UpdateProfileKeywords)
|
||||||
profileGP.DELETE("/logout", customerHandler.Logout)
|
profileGP.DELETE("/logout", customerHandler.Logout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,11 @@ type ResetPasswordForm struct {
|
|||||||
NewPassword string `json:"new_password" valid:"required,length(5|128)" example:"NewPass!123"`
|
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
|
// RequestResetPasswordResponse represents the response for password reset request
|
||||||
type RequestResetPasswordResponse struct {
|
type RequestResetPasswordResponse struct {
|
||||||
Message string `json:"message" example:"Password reset code sent to your email"`
|
Message string `json:"message" example:"Password reset code sent to your email"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package customer
|
package customer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"tm/internal/user"
|
"tm/internal/user"
|
||||||
"tm/pkg/authorization"
|
"tm/pkg/authorization"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
@@ -57,6 +58,9 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
|
|||||||
err.Error() == "company with this tax ID already exists" {
|
err.Error() == "company with this tax ID already exists" {
|
||||||
return response.Conflict(c, err.Error())
|
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")
|
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")
|
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)
|
// Logout handles customer logout (Mobile)
|
||||||
// @Summary Customer logout
|
// @Summary Customer logout
|
||||||
// @Description Logout customer and invalidate access token
|
// @Description Logout customer and invalidate access token
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Service interface {
|
|||||||
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
|
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
|
||||||
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
||||||
GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, 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
|
Logout(ctx context.Context, id string, accessToken string) error
|
||||||
|
|
||||||
// Forgot password operations
|
// Forgot password operations
|
||||||
@@ -96,6 +97,9 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
|
|||||||
var Companies []string
|
var Companies []string
|
||||||
if len(form.Companies) > 0 {
|
if len(form.Companies) > 0 {
|
||||||
for _, companyID := range form.Companies {
|
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)
|
_, err := s.companyService.GetByID(ctx, companyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Invalid company ID provided", map[string]interface{}{
|
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
|
customer.KeywordsStatus = KeywordsStatusInProgress
|
||||||
|
|
||||||
// Save customer with in_progress status first, then process asynchronously
|
// Save customer with in_progress status first, then process asynchronously
|
||||||
customerID := customer.ID.Hex()
|
s.processKeywordsAsync(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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else if infoChanged || companiesChanged {
|
} else if infoChanged || companiesChanged {
|
||||||
// Customer info changed, regenerate keywords
|
// Customer info changed, regenerate keywords
|
||||||
customer.KeywordsStatus = KeywordsStatusInProgress
|
customer.KeywordsStatus = KeywordsStatusInProgress
|
||||||
@@ -907,6 +856,42 @@ func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerI
|
|||||||
return customerResponse, nil
|
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
|
// Logout handles customer logout
|
||||||
func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error {
|
func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error {
|
||||||
s.logger.Info("Customer logout", map[string]interface{}{
|
s.logger.Info("Customer logout", map[string]interface{}{
|
||||||
@@ -1398,6 +1383,12 @@ func (s *customerService) generateKeywords(ctx context.Context, customer *Custom
|
|||||||
if comp.Industry != "" {
|
if comp.Industry != "" {
|
||||||
customerInfo.WriteString(fmt.Sprintf(" (Industry: %s)", 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")
|
customerInfo.WriteString("\n")
|
||||||
@@ -1452,3 +1443,54 @@ Return ONLY a comma-separated list of keywords, no explanations or additional te
|
|||||||
|
|
||||||
return keywords, nil
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
@@ -234,6 +234,16 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
|
|||||||
return response.InternalServerError(c, "Failed to retrieve tenders")
|
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")
|
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(), "")
|
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)}
|
form.Status = []string{string(TenderStatusActive)}
|
||||||
|
|
||||||
var companyID *string
|
var companyID *string
|
||||||
@@ -291,11 +306,16 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
form.CustomerID = customerID
|
form.CustomerID = customerID
|
||||||
|
|
||||||
result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c))
|
if result != nil {
|
||||||
if err != nil {
|
filtered := make([]TenderResponse, 0, len(result.Tenders))
|
||||||
return response.InternalServerError(c, "Failed to retrieve 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")
|
return response.SuccessWithMeta(c, result, result.Metadata, "Tenders retrieved successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user