Merge pull request 'added AI auto-generated keywords to customer profile' (#17) from TM-358 into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_back/pulls/17
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
"tm/pkg/authorization"
|
||||
"tm/pkg/config"
|
||||
"tm/pkg/glm"
|
||||
"tm/pkg/hcaptcha"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/mongo"
|
||||
@@ -268,3 +269,49 @@ func InitHCaptchaService(conf config.HCaptchaConfig, log logger.Logger) hcaptcha
|
||||
|
||||
return hcaptchaVerifier
|
||||
}
|
||||
|
||||
// InitGLMService initializes the GLM service
|
||||
func InitGLMService(conf GLMConfig, log logger.Logger) *glm.SDK {
|
||||
glmSDK, err := glm.New(
|
||||
&glm.Config{
|
||||
BaseURL: conf.BaseURL,
|
||||
APIKey: conf.APIKey,
|
||||
Timeout: conf.Timeout,
|
||||
RetryAttempts: conf.RetryAttempts,
|
||||
RetryDelay: conf.RetryDelay,
|
||||
EnableLogging: conf.EnableLogging,
|
||||
UserAgent: conf.UserAgent,
|
||||
DefaultModel: conf.DefaultModel,
|
||||
DefaultMaxTokens: conf.DefaultMaxTokens,
|
||||
DefaultTopP: conf.DefaultTopP,
|
||||
DefaultTemperature: conf.DefaultTemperature,
|
||||
DefaultFrequencyPenalty: conf.DefaultFrequencyPenalty,
|
||||
DefaultPresencePenalty: conf.DefaultPresencePenalty,
|
||||
EnableStreaming: conf.EnableStreaming,
|
||||
EnableThinking: conf.EnableThinking,
|
||||
TranslationOptions: glm.TranslationOption{
|
||||
Model: conf.TranslateOptions.Model,
|
||||
MaxTokens: conf.TranslateOptions.MaxTokens,
|
||||
Temperature: conf.TranslateOptions.Temperature,
|
||||
FrequencyPenalty: conf.TranslateOptions.FrequencyPenalty,
|
||||
PresencePenalty: conf.TranslateOptions.PresencePenalty,
|
||||
TopP: conf.TranslateOptions.TopP,
|
||||
EnableThinking: conf.TranslateOptions.EnableThinking,
|
||||
},
|
||||
}, log)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Failed to initialize GLM SDK", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
log.Warn("GLM service will be disabled, keyword generation features will not be available", map[string]interface{}{})
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("GLM SDK initialized successfully", map[string]interface{}{
|
||||
"base_url": conf.BaseURL,
|
||||
"timeout": conf.Timeout,
|
||||
})
|
||||
|
||||
return glmSDK
|
||||
}
|
||||
|
||||
@@ -16,11 +16,41 @@ type Config struct {
|
||||
Notification config.NotificationConfig
|
||||
Ollama config.OllamaConfig
|
||||
HCaptcha config.HCaptchaConfig
|
||||
GLM GLMConfig
|
||||
Scraper ScraperConfig
|
||||
UserAuth AuthConfig
|
||||
CustomerAuth AuthConfig
|
||||
}
|
||||
|
||||
type GLMConfig struct {
|
||||
BaseURL string `env:"GLM_BASE_URL" envDefault:"https://api.z.ai"`
|
||||
APIKey string `env:"GLM_API_KEY" envDefault:""`
|
||||
Timeout time.Duration `env:"GLM_TIMEOUT" envDefault:"300s"`
|
||||
RetryAttempts int `env:"GLM_RETRY_ATTEMPTS" envDefault:"3"`
|
||||
RetryDelay time.Duration `env:"GLM_RETRY_DELAY" envDefault:"2s"`
|
||||
EnableLogging bool `env:"GLM_ENABLE_LOGGING" envDefault:"true"`
|
||||
UserAgent string `env:"GLM_USER_AGENT" envDefault:"Opplens-GLMSDK/1.0"`
|
||||
DefaultModel string `env:"GLM_DEFAULT_MODEL" envDefault:"glm-4.5"`
|
||||
DefaultMaxTokens int `env:"GLM_DEFAULT_MAX_TOKENS" envDefault:"4096"`
|
||||
DefaultTopP float64 `env:"GLM_DEFAULT_TOP_P" envDefault:"1.0"`
|
||||
DefaultTemperature float64 `env:"GLM_DEFAULT_TEMPERATURE" envDefault:"0.7"`
|
||||
DefaultFrequencyPenalty float64 `env:"GLM_DEFAULT_FREQUENCY_PENALTY" envDefault:"0"`
|
||||
DefaultPresencePenalty float64 `env:"GLM_DEFAULT_PRESENCE_PENALTY" envDefault:"0"`
|
||||
EnableStreaming bool `env:"GLM_ENABLE_STREAMING" envDefault:"false"`
|
||||
EnableThinking bool `env:"GLM_ENABLE_THINKING" envDefault:"false"`
|
||||
TranslateOptions TranslateOptions
|
||||
}
|
||||
|
||||
type TranslateOptions struct {
|
||||
Model string `env:"GLM_TRANSLATION_MODEL" envDefault:"glm-4.5"`
|
||||
MaxTokens int `env:"GLM_TRANSLATION_MAX_TOKENS" envDefault:"2048"`
|
||||
Temperature float64 `env:"GLM_TRANSLATION_TEMPERATURE" envDefault:"0.3"`
|
||||
FrequencyPenalty float64 `env:"GLM_TRANSLATION_FREQUENCY_PENALTY" envDefault:"0.1"`
|
||||
PresencePenalty float64 `env:"GLM_TRANSLATION_PRESENCE_PENALTY" envDefault:"0.1"`
|
||||
TopP float64 `env:"GLM_TRANSLATION_TOP_P" envDefault:"1.0"`
|
||||
EnableThinking bool `env:"GLM_TRANSLATION_ENABLE_THINKING" envDefault:"false"`
|
||||
}
|
||||
|
||||
type ScraperConfig struct {
|
||||
BaseURL string `env:"SCRAPER_BASE_URL"`
|
||||
Timeout time.Duration `env:"SCRAPER_TIMEOUT"`
|
||||
|
||||
+5
-2
@@ -149,6 +149,9 @@ func main() {
|
||||
// Initialize hCaptcha verifier
|
||||
hcaptchaVerifier := bootstrap.InitHCaptchaService(conf.HCaptcha, logger)
|
||||
|
||||
// Initialize GLM service
|
||||
glmSDK := bootstrap.InitGLMService(conf.GLM, logger)
|
||||
|
||||
// Initialize authorization service
|
||||
userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger)
|
||||
customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger)
|
||||
@@ -176,8 +179,8 @@ func main() {
|
||||
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK)
|
||||
categoryService := company_category.NewService(categoryRepository, logger)
|
||||
companyService := company.NewService(companyRepository, categoryService, logger)
|
||||
customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator)
|
||||
tenderService := tender.NewService(tenderRepository, companyService, logger)
|
||||
customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator, glmSDK)
|
||||
tenderService := tender.NewService(tenderRepository, companyService, customerService, logger)
|
||||
feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger)
|
||||
tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger)
|
||||
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy)
|
||||
|
||||
@@ -32,6 +32,16 @@ const (
|
||||
CustomerRoleAnalyst CustomerRole = "analyst"
|
||||
)
|
||||
|
||||
// KeywordsStatus represents the status of keyword processing
|
||||
type KeywordsStatus string
|
||||
|
||||
const (
|
||||
KeywordsStatusPending KeywordsStatus = "pending"
|
||||
KeywordsStatusInProgress KeywordsStatus = "in_progress"
|
||||
KeywordsStatusReady KeywordsStatus = "ready"
|
||||
KeywordsStatusFailed KeywordsStatus = "failed"
|
||||
)
|
||||
|
||||
// Customer represents a customer in the tender management system
|
||||
// A customer can have an associated company for login purposes
|
||||
type Customer struct {
|
||||
@@ -44,8 +54,10 @@ type Customer struct {
|
||||
Type CustomerType `bson:"type"`
|
||||
Role CustomerRole `bson:"role"`
|
||||
Phone *string `bson:"phone"`
|
||||
Companies []string `bson:"companies"`
|
||||
DeviceToken []string `bson:"device_token"`
|
||||
Companies []string `bson:"companies"`
|
||||
Keywords []string `bson:"keywords,omitempty"`
|
||||
KeywordsStatus KeywordsStatus `bson:"keywords_status,omitempty"`
|
||||
DeviceToken []string `bson:"device_token"`
|
||||
LastLoginAt *int64 `bson:"last_login_at"`
|
||||
UpdatedAt int64 `bson:"updated_at"`
|
||||
CreatedAt int64 `bson:"created_at"`
|
||||
|
||||
+27
-14
@@ -28,6 +28,7 @@ type UpdateCustomerForm struct {
|
||||
Username string `json:"username" valid:"required,username,length(3|30)" example:"user"`
|
||||
Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
||||
Keywords []string `json:"keywords,omitempty" valid:"optional"`
|
||||
}
|
||||
|
||||
// CustomerStatusForm represents the form for suspending a customer
|
||||
@@ -63,25 +64,37 @@ type CustomerResponse struct {
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
LastLoginAt *int64 `json:"last_login_at"`
|
||||
Companies []*CompanySummary `json:"companies"`
|
||||
DeviceToken []string `json:"-"`
|
||||
Companies []*CompanySummary `json:"companies"`
|
||||
Keywords []string `json:"keywords"`
|
||||
KeywordsStatus string `json:"keywords_status"`
|
||||
DeviceToken []string `json:"-"`
|
||||
}
|
||||
|
||||
// ToResponse converts Customer to CustomerResponse
|
||||
func (c *Customer) ToResponse(companies []*CompanySummary) *CustomerResponse {
|
||||
keywords := c.Keywords
|
||||
if keywords == nil {
|
||||
keywords = []string{}
|
||||
}
|
||||
keywordsStatus := string(c.KeywordsStatus)
|
||||
if keywordsStatus == "" {
|
||||
keywordsStatus = string(KeywordsStatusReady)
|
||||
}
|
||||
return &CustomerResponse{
|
||||
ID: c.ID.Hex(),
|
||||
FullName: c.FullName,
|
||||
Username: c.Username,
|
||||
Email: c.Email,
|
||||
Type: string(c.Type),
|
||||
Status: string(c.Status),
|
||||
Role: string(c.Role),
|
||||
Phone: c.Phone,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
CreatedAt: c.CreatedAt,
|
||||
LastLoginAt: c.LastLoginAt,
|
||||
Companies: companies,
|
||||
ID: c.ID.Hex(),
|
||||
FullName: c.FullName,
|
||||
Username: c.Username,
|
||||
Email: c.Email,
|
||||
Type: string(c.Type),
|
||||
Status: string(c.Status),
|
||||
Role: string(c.Role),
|
||||
Phone: c.Phone,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
CreatedAt: c.CreatedAt,
|
||||
LastLoginAt: c.LastLoginAt,
|
||||
Companies: companies,
|
||||
Keywords: keywords,
|
||||
KeywordsStatus: keywordsStatus,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tm/internal/company"
|
||||
"tm/pkg/glm"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/notification"
|
||||
"tm/pkg/redis"
|
||||
@@ -61,10 +62,11 @@ type customerService struct {
|
||||
redisClient redis.Client
|
||||
notification notification.SDK
|
||||
validator ValidationService
|
||||
glmSDK *glm.SDK
|
||||
}
|
||||
|
||||
// New creates a new customer service
|
||||
func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK, validator ValidationService) Service {
|
||||
func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK, validator ValidationService, glmSDK *glm.SDK) Service {
|
||||
return &customerService{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
@@ -73,6 +75,7 @@ func NewService(repository Repository, logger logger.Logger, authService authori
|
||||
redisClient: redisClient,
|
||||
notification: notificationSDK,
|
||||
validator: validator,
|
||||
glmSDK: glmSDK,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +130,21 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
|
||||
Phone: form.Phone,
|
||||
}
|
||||
|
||||
// Generate keywords using GLM AI
|
||||
customer.KeywordsStatus = KeywordsStatusInProgress
|
||||
keywords, err := s.generateKeywords(ctx, customer)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to generate keywords, continuing without keywords", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
keywords = []string{}
|
||||
customer.KeywordsStatus = KeywordsStatusFailed
|
||||
} else {
|
||||
customer.Keywords = keywords
|
||||
customer.KeywordsStatus = KeywordsStatusReady
|
||||
}
|
||||
|
||||
// Save to database
|
||||
err = s.repository.Register(ctx, customer)
|
||||
if err != nil {
|
||||
@@ -335,7 +353,24 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
||||
|
||||
// Handle company assignments
|
||||
companies := make([]*CompanySummary, 0)
|
||||
companiesChanged := false
|
||||
if len(form.Companies) > 0 {
|
||||
// Check if companies changed
|
||||
if len(form.Companies) != len(customer.Companies) {
|
||||
companiesChanged = true
|
||||
} else {
|
||||
companyMap := make(map[string]bool)
|
||||
for _, id := range customer.Companies {
|
||||
companyMap[id] = true
|
||||
}
|
||||
for _, id := range form.Companies {
|
||||
if !companyMap[id] {
|
||||
companiesChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
customer.Companies = form.Companies
|
||||
companies, err = s.loadCompaniesForCustomer(ctx, customer)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
|
||||
@@ -345,17 +380,101 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
||||
}
|
||||
}
|
||||
|
||||
if form.FullName != nil {
|
||||
infoChanged := false
|
||||
if form.FullName != nil && (customer.FullName == nil || *form.FullName != *customer.FullName) {
|
||||
customer.FullName = form.FullName
|
||||
infoChanged = true
|
||||
}
|
||||
|
||||
if form.Phone != nil {
|
||||
if form.Phone != nil && (customer.Phone == nil || *form.Phone != *customer.Phone) {
|
||||
customer.Phone = form.Phone
|
||||
infoChanged = true
|
||||
}
|
||||
|
||||
// Update role if provided
|
||||
if form.Role != "" {
|
||||
if form.Role != "" && customer.Role != CustomerRole(form.Role) {
|
||||
customer.Role = CustomerRole(form.Role)
|
||||
infoChanged = true
|
||||
}
|
||||
|
||||
// Handle keywords
|
||||
if form.Keywords != nil {
|
||||
// Keywords explicitly provided by customer, set status to in_progress
|
||||
// Customer's manual keywords are saved, but we'll enhance them with AI
|
||||
customer.Keywords = form.Keywords
|
||||
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,
|
||||
})
|
||||
}
|
||||
}()
|
||||
} else if infoChanged || companiesChanged {
|
||||
// Customer info changed, regenerate keywords
|
||||
customer.KeywordsStatus = KeywordsStatusInProgress
|
||||
keywords, err := s.generateKeywords(ctx, customer)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to regenerate keywords, keeping existing keywords", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
customer.KeywordsStatus = KeywordsStatusFailed
|
||||
} else {
|
||||
customer.Keywords = keywords
|
||||
customer.KeywordsStatus = KeywordsStatusReady
|
||||
}
|
||||
}
|
||||
|
||||
// Update in database
|
||||
@@ -1244,3 +1363,92 @@ func (s *customerService) AssignRole(ctx context.Context, customerID string, for
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateKeywords generates keywords for a customer using GLM AI based on their information
|
||||
func (s *customerService) generateKeywords(ctx context.Context, customer *Customer) ([]string, error) {
|
||||
// If GLM SDK is not available, return empty keywords
|
||||
if s.glmSDK == nil {
|
||||
s.logger.Warn("GLM SDK not available, skipping keyword generation", map[string]interface{}{
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Build customer information string for AI
|
||||
var customerInfo strings.Builder
|
||||
if customer.FullName != nil {
|
||||
customerInfo.WriteString(fmt.Sprintf("Name: %s\n", *customer.FullName))
|
||||
}
|
||||
customerInfo.WriteString(fmt.Sprintf("Type: %s\n", customer.Type))
|
||||
customerInfo.WriteString(fmt.Sprintf("Role: %s\n", customer.Role))
|
||||
if customer.Phone != nil {
|
||||
customerInfo.WriteString(fmt.Sprintf("Phone: %s\n", *customer.Phone))
|
||||
}
|
||||
|
||||
// Get company information if available
|
||||
if len(customer.Companies) > 0 {
|
||||
customerInfo.WriteString("Companies: ")
|
||||
for i, companyID := range customer.Companies {
|
||||
comp, err := s.companyService.GetByID(ctx, companyID)
|
||||
if err == nil {
|
||||
if i > 0 {
|
||||
customerInfo.WriteString(", ")
|
||||
}
|
||||
customerInfo.WriteString(comp.Name)
|
||||
if comp.Industry != "" {
|
||||
customerInfo.WriteString(fmt.Sprintf(" (Industry: %s)", comp.Industry))
|
||||
}
|
||||
}
|
||||
}
|
||||
customerInfo.WriteString("\n")
|
||||
}
|
||||
|
||||
systemPrompt := `You are a keyword extraction assistant for a tender management system.
|
||||
Analyze the customer information and generate 5-10 relevant keywords that would help match them with appropriate tenders.
|
||||
Keywords should be:
|
||||
- Relevant to tender/procurement categories
|
||||
- Based on customer type, role, and company information
|
||||
- Professional and specific
|
||||
- Suitable for matching with tender descriptions
|
||||
|
||||
Return ONLY a comma-separated list of keywords, no explanations or additional text.`
|
||||
|
||||
userMessage := fmt.Sprintf("Generate keywords for this customer:\n\n%s", customerInfo.String())
|
||||
|
||||
s.logger.Info("Generating keywords using GLM AI", map[string]interface{}{
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
|
||||
response, err := s.glmSDK.QuickChatWithSystem(ctx, systemPrompt, userMessage)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate keywords using GLM AI", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
return []string{}, fmt.Errorf("failed to generate keywords: %w", err)
|
||||
}
|
||||
|
||||
// Parse comma-separated keywords
|
||||
keywordsStr := strings.TrimSpace(response)
|
||||
if keywordsStr == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Split by comma and clean up
|
||||
keywordList := strings.Split(keywordsStr, ",")
|
||||
keywords := make([]string, 0, len(keywordList))
|
||||
for _, kw := range keywordList {
|
||||
kw = strings.TrimSpace(kw)
|
||||
if kw != "" {
|
||||
keywords = append(keywords, kw)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Keywords generated successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID,
|
||||
"keywords": keywords,
|
||||
"count": len(keywords),
|
||||
})
|
||||
|
||||
return keywords, nil
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type SearchForm struct {
|
||||
CountryCodes []string `query:"country_codes" valid:"optional"`
|
||||
RegionCodes []string `query:"region_codes" valid:"optional"`
|
||||
CompanyID *string `query:"company_id" valid:"optional"`
|
||||
CustomerID *string `query:"customer_id" valid:"optional"`
|
||||
Status []string `query:"status" valid:"optional"`
|
||||
Classifications []string `query:"classifications" valid:"optional"`
|
||||
MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"`
|
||||
|
||||
@@ -216,13 +216,19 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
|
||||
|
||||
form.Status = []string{string(TenderStatusActive)}
|
||||
|
||||
// Get company ID from customer context for automatic matching
|
||||
// Get company ID and customer ID from customer context for automatic matching
|
||||
var companyID *string
|
||||
if customerCompanyID, ok := c.Get("company_id").(string); ok {
|
||||
companyID = &customerCompanyID
|
||||
}
|
||||
form.CompanyID = companyID
|
||||
|
||||
var customerID *string
|
||||
if customerIDStr, ok := c.Get("customer_id").(string); ok {
|
||||
customerID = &customerIDStr
|
||||
}
|
||||
form.CustomerID = customerID
|
||||
|
||||
result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve tenders")
|
||||
@@ -277,9 +283,14 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error {
|
||||
if customerCompanyID, ok := c.Get("company_id").(string); ok {
|
||||
companyID = &customerCompanyID
|
||||
}
|
||||
|
||||
form.CompanyID = companyID
|
||||
|
||||
var customerID *string
|
||||
if customerIDStr, ok := c.Get("customer_id").(string); ok {
|
||||
customerID = &customerIDStr
|
||||
}
|
||||
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")
|
||||
|
||||
+86
-24
@@ -3,8 +3,10 @@ package tender
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"tm/internal/company"
|
||||
"tm/internal/customer"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
@@ -26,17 +28,19 @@ type Service interface {
|
||||
|
||||
// tenderService implements TenderService interface
|
||||
type tenderService struct {
|
||||
repository TenderRepository
|
||||
companyService company.Service
|
||||
logger logger.Logger
|
||||
repository TenderRepository
|
||||
companyService company.Service
|
||||
customerService customer.Service
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new tender service
|
||||
func NewService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service {
|
||||
func NewService(repository TenderRepository, companyService company.Service, customerService customer.Service, logger logger.Logger) Service {
|
||||
return &tenderService{
|
||||
repository: repository,
|
||||
companyService: companyService,
|
||||
logger: logger,
|
||||
repository: repository,
|
||||
companyService: companyService,
|
||||
customerService: customerService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +160,8 @@ func (s *tenderService) GetDocumentSummaries(ctx context.Context, id string) ([]
|
||||
}
|
||||
|
||||
s.logger.Info("Retrieved document summaries for tender", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
"summaries_count": len(tender.DocumentSummaries),
|
||||
"tender_id": id,
|
||||
"summaries_count": len(tender.DocumentSummaries),
|
||||
})
|
||||
|
||||
return tender.DocumentSummaries, nil
|
||||
@@ -166,13 +170,41 @@ func (s *tenderService) GetDocumentSummaries(ctx context.Context, id string) ([]
|
||||
// ListTenders retrieves tenders with pagination and filtering
|
||||
func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
|
||||
s.logger.Info("Listing tenders", map[string]interface{}{
|
||||
"limit": pagination.Limit,
|
||||
"offset": pagination.Offset,
|
||||
"company_id": form.CompanyID,
|
||||
"from": form.DeadlineFrom,
|
||||
"to": form.DeadlineTo,
|
||||
"limit": pagination.Limit,
|
||||
"offset": pagination.Offset,
|
||||
"company_id": form.CompanyID,
|
||||
"customer_id": form.CustomerID,
|
||||
"from": form.DeadlineFrom,
|
||||
"to": form.DeadlineTo,
|
||||
})
|
||||
|
||||
// If customer ID is provided, get customer keywords and add to search
|
||||
if form.CustomerID != nil && *form.CustomerID != "" {
|
||||
customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
|
||||
if err == nil && customerResp != nil {
|
||||
// Only use keywords if status is ready
|
||||
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
|
||||
// Combine customer keywords with existing search query
|
||||
keywordsQuery := strings.Join(customerResp.Keywords, " ")
|
||||
if form.Search != nil && *form.Search != "" {
|
||||
combinedSearch := *form.Search + " " + keywordsQuery
|
||||
form.Search = &combinedSearch
|
||||
} else {
|
||||
form.Search = &keywordsQuery
|
||||
}
|
||||
s.logger.Info("Using customer keywords for search", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords": customerResp.Keywords,
|
||||
})
|
||||
} else {
|
||||
s.logger.Info("Customer keywords not ready, skipping keyword-based search", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords_status": customerResp.KeywordsStatus,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tenders, total, err := s.repository.Search(ctx, form, pagination)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list tenders", map[string]interface{}{
|
||||
@@ -240,20 +272,50 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
|
||||
// Recommend retrieves tenders with pagination and filtering
|
||||
func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
|
||||
s.logger.Info("Recommend tenders", map[string]interface{}{
|
||||
"company_id": form.CompanyID,
|
||||
"company_id": form.CompanyID,
|
||||
"customer_id": form.CustomerID,
|
||||
})
|
||||
|
||||
company, err := s.companyService.GetByID(ctx, *form.CompanyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company", map[string]interface{}{
|
||||
"company_id": form.CompanyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to get company: %w", err)
|
||||
form.Status = []string{string(TenderStatusActive)}
|
||||
|
||||
// Priority: Use customer keywords if available, otherwise use company CPV codes
|
||||
if form.CustomerID != nil && *form.CustomerID != "" {
|
||||
customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
|
||||
if err == nil && customerResp != nil {
|
||||
// Only use keywords if status is ready
|
||||
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
|
||||
// Use customer keywords for recommendation
|
||||
keywordsQuery := strings.Join(customerResp.Keywords, " ")
|
||||
form.Search = &keywordsQuery
|
||||
s.logger.Info("Using customer keywords for recommendation", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords": customerResp.Keywords,
|
||||
})
|
||||
} else {
|
||||
s.logger.Info("Customer keywords not ready, falling back to company CPV codes", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords_status": customerResp.KeywordsStatus,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form.Status = []string{string(TenderStatusActive)}
|
||||
form.Classifications = company.Tags.CPVCodes
|
||||
// Fallback to company CPV codes if no customer keywords
|
||||
if form.Search == nil || *form.Search == "" {
|
||||
if form.CompanyID != nil && *form.CompanyID != "" {
|
||||
company, err := s.companyService.GetByID(ctx, *form.CompanyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company", map[string]interface{}{
|
||||
"company_id": form.CompanyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to get company: %w", err)
|
||||
}
|
||||
if company.Tags != nil && len(company.Tags.CPVCodes) > 0 {
|
||||
form.Classifications = company.Tags.CPVCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tenders, total, err := s.repository.Search(ctx, form, pagination)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user