784c3d6563
continuous-integration/drone/push Build is passing
- Removed the `RecommendationPageCacheLanguages` configuration from `AISummarizerConfig` to streamline cache management. - Updated the `companyService` and `tenderService` to utilize the new `InvalidateRecommendedTendersPageCache` method for cache invalidation, enhancing clarity and efficiency. - Refactored the `invalidateRecommendedTendersPageCache` method to eliminate unnecessary context parameters, simplifying the function signature. - Improved the handling of page cache refresh logic by consolidating language handling within the `tenderService`, ensuring consistent behavior across services. - Cleaned up related tests and removed deprecated functions to maintain code quality and readability. This update enhances the maintainability of the recommendation caching system by simplifying configuration and improving cache invalidation logic.
603 lines
18 KiB
Go
603 lines
18 KiB
Go
package company
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"mime/multipart"
|
|
"time"
|
|
|
|
"tm/internal/company_category"
|
|
"tm/pkg/filestore"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/redis"
|
|
"tm/pkg/response"
|
|
)
|
|
|
|
// Service defines business logic for company operations
|
|
type Service interface {
|
|
// Core company operations
|
|
Create(ctx context.Context, form *CompanyForm) (*CompanyResponse, error)
|
|
Update(ctx context.Context, id string, form *CompanyForm) (*CompanyResponse, error)
|
|
GetByID(ctx context.Context, id string) (*CompanyResponse, error)
|
|
GetByIDs(ctx context.Context, ids []string) ([]*CompanyResponse, error)
|
|
GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error)
|
|
Delete(ctx context.Context, id string) error
|
|
|
|
// Company listing and search
|
|
SearchCompanies(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CompanyListResponse, error)
|
|
|
|
// Company management
|
|
UpdateStatus(ctx context.Context, id string, form *StatusForm) error
|
|
UpdateVerification(ctx context.Context, id string, form *VerificationForm) error
|
|
|
|
// Get my company
|
|
GetMyCompany(ctx context.Context, id string) (*CompanyProfileResponse, error)
|
|
|
|
// UploadDocuments uploads files and attaches them to a company
|
|
UploadDocuments(ctx context.Context, companyID, uploadedBy string, files []*multipart.FileHeader) (*UploadDocumentsResponse, error)
|
|
|
|
// AddLinks appends external URLs to a company
|
|
AddLinks(ctx context.Context, companyID string, form *AddLinksForm) (*AddLinksResponse, error)
|
|
|
|
// DetachDocumentFileID removes a file ID from companies when the file is deleted
|
|
DetachDocumentFileID(ctx context.Context, fileID string) 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)
|
|
GetMergedAIRecommendations(ctx context.Context, companyIDs []string) ([]RecommendedTenderResponse, error)
|
|
|
|
// ScheduleRefreshCachedAIRecommendationsAfterPipeline re-fetches cached company recommendations after tender sync.
|
|
ScheduleRefreshCachedAIRecommendationsAfterPipeline()
|
|
}
|
|
|
|
// companyService implements the Service interface
|
|
type companyService struct {
|
|
repository Repository
|
|
categoryService company_category.Service
|
|
fileStore filestore.FileStoreService
|
|
aiRecommendationClient AIRecommendationClient
|
|
aiPipelineStatusClient AIPipelineStatusClient
|
|
pageCacheRefresher RecommendedTendersPageCacheRefresher
|
|
redisClient redis.Client
|
|
recommendationCacheTTL time.Duration
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewService creates a new company service
|
|
func NewService(
|
|
repository Repository,
|
|
categoryService company_category.Service,
|
|
fileStore filestore.FileStoreService,
|
|
aiRecommendationClient AIRecommendationClient,
|
|
aiPipelineStatusClient AIPipelineStatusClient,
|
|
redisClient redis.Client,
|
|
recommendationCacheTTL time.Duration,
|
|
logger logger.Logger,
|
|
) Service {
|
|
return &companyService{
|
|
repository: repository,
|
|
categoryService: categoryService,
|
|
fileStore: fileStore,
|
|
aiRecommendationClient: aiRecommendationClient,
|
|
aiPipelineStatusClient: aiPipelineStatusClient,
|
|
redisClient: redisClient,
|
|
recommendationCacheTTL: recommendationCacheTTL,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Create creates a new company
|
|
func (s *companyService) Create(ctx context.Context, form *CompanyForm) (*CompanyResponse, error) {
|
|
// Validate categories if provided
|
|
if form.Tags != nil && len(form.Tags.Categories) > 0 {
|
|
validCategories, err := s.categoryService.GetByIDs(ctx, form.Tags.Categories)
|
|
if err != nil {
|
|
s.logger.Error("Failed to validate categories", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"categories": form.Tags.Categories,
|
|
})
|
|
return nil, errors.New("failed to validate categories")
|
|
}
|
|
|
|
// Check if all provided categories exist
|
|
if len(validCategories) != len(form.Tags.Categories) {
|
|
return nil, errors.New("one or more categories are invalid")
|
|
}
|
|
}
|
|
|
|
// Check if company name already exists
|
|
existingCompany, _ := s.repository.GetByName(ctx, form.Name)
|
|
if existingCompany != nil {
|
|
return nil, errors.New("company with this name already exists")
|
|
}
|
|
|
|
if form.RegistrationNumber != "" {
|
|
existingCompany, _ = s.repository.GetByRegistrationNumber(ctx, form.RegistrationNumber)
|
|
if existingCompany != nil {
|
|
return nil, errors.New("company with this registration number already exists")
|
|
}
|
|
}
|
|
|
|
if form.TaxID != "" {
|
|
existingCompany, _ = s.repository.GetByTaxID(ctx, form.TaxID)
|
|
if existingCompany != nil {
|
|
return nil, errors.New("company with this tax ID already exists")
|
|
}
|
|
}
|
|
|
|
links, err := sanitizeCompanyLinks(convertCompanyLinkForms(form.Links))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(links) > maxCompanyLinks {
|
|
return nil, errors.New("too many links provided")
|
|
}
|
|
|
|
// Create company
|
|
company := &Company{
|
|
Name: form.Name,
|
|
Type: CompanyType(form.Type),
|
|
Status: CompanyStatusActive,
|
|
RegistrationNumber: form.RegistrationNumber,
|
|
TaxID: form.TaxID,
|
|
Industry: form.Industry,
|
|
Description: form.Description,
|
|
Website: form.Website,
|
|
EmployeeCount: form.EmployeeCount,
|
|
AnnualRevenue: form.AnnualRevenue,
|
|
FoundedYear: form.FoundedYear,
|
|
Address: s.convertAddressForm(form.Address),
|
|
Phone: form.Phone,
|
|
Email: form.Email,
|
|
DocumentFileIDs: s.sanitizeDocumentFileIDs(form.DocumentFileIDs),
|
|
Links: links,
|
|
Tags: s.convertCompanyTagsForm(form.Tags),
|
|
IsVerified: false,
|
|
IsCompliant: false,
|
|
Language: s.getDefaultLanguage(form.Language),
|
|
Currency: s.getDefaultCurrency(form.Currency),
|
|
Timezone: s.getDefaultTimezone(form.Timezone),
|
|
}
|
|
|
|
// Save to database
|
|
err = s.repository.Create(ctx, company)
|
|
if err != nil {
|
|
s.logger.Error("Failed to create company", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"name": form.Name,
|
|
"registration_number": form.RegistrationNumber,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("Company created successfully", map[string]interface{}{
|
|
"company_id": company.ID,
|
|
"name": company.Name,
|
|
"type": company.Type,
|
|
})
|
|
|
|
s.triggerAIOnboardingAsync(company.GetID())
|
|
|
|
return company.ToResponse(nil), nil
|
|
}
|
|
|
|
// GetCompanyByID retrieves a company by ID
|
|
func (s *companyService) GetByID(ctx context.Context, id string) (*CompanyResponse, error) {
|
|
company, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get company by ID", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("Company retrieved successfully", map[string]interface{}{
|
|
"company_id": company.ID,
|
|
"name": company.Name,
|
|
})
|
|
|
|
// Get category details for response
|
|
categoryDetails, err := s.categoryService.GetByIDs(ctx, company.Tags.Categories)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to fetch category details for response", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": company.ID,
|
|
})
|
|
}
|
|
|
|
var convertedCategories []*CategoryResponse
|
|
for _, cat := range categoryDetails {
|
|
convertedCategories = append(convertedCategories, &CategoryResponse{
|
|
ID: cat.ID,
|
|
Name: cat.Name,
|
|
})
|
|
}
|
|
|
|
company.DocumentFileIDs = s.sanitizeDocumentFileIDs(company.DocumentFileIDs)
|
|
|
|
return company.ToResponse(convertedCategories), nil
|
|
}
|
|
|
|
// UpdateCompany updates a company
|
|
func (s *companyService) Update(ctx context.Context, id string, form *CompanyForm) (*CompanyResponse, error) {
|
|
// Validate categories if provided
|
|
if form.Tags != nil && len(form.Tags.Categories) > 0 {
|
|
validCategories, err := s.categoryService.GetByIDs(ctx, form.Tags.Categories)
|
|
if err != nil {
|
|
s.logger.Error("Failed to validate categories", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"categories": form.Tags.Categories,
|
|
})
|
|
return nil, errors.New("failed to validate categories")
|
|
}
|
|
|
|
// Check if all provided categories exist
|
|
if len(validCategories) != len(form.Tags.Categories) {
|
|
return nil, errors.New("one or more categories are invalid")
|
|
}
|
|
}
|
|
|
|
// Get existing company
|
|
company, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get company for update", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": id,
|
|
})
|
|
return nil, errors.New("company not found")
|
|
}
|
|
|
|
// Check if name already exists (if changing name)
|
|
if form.Name != "" && form.Name != company.Name {
|
|
existingCompany, _ := s.repository.GetByName(ctx, form.Name)
|
|
if existingCompany != nil {
|
|
return nil, errors.New("company with this name already exists")
|
|
}
|
|
company.Name = form.Name
|
|
}
|
|
|
|
// Check if registration number already exists (if changing)
|
|
if form.RegistrationNumber != "" && form.RegistrationNumber != company.RegistrationNumber {
|
|
existingCompany, _ := s.repository.GetByRegistrationNumber(ctx, form.RegistrationNumber)
|
|
if existingCompany != nil {
|
|
return nil, errors.New("company with this registration number already exists")
|
|
}
|
|
company.RegistrationNumber = form.RegistrationNumber
|
|
}
|
|
|
|
// Check if tax ID already exists (if changing)
|
|
if form.TaxID != "" && form.TaxID != company.TaxID {
|
|
existingCompany, _ := s.repository.GetByTaxID(ctx, form.TaxID)
|
|
if existingCompany != nil {
|
|
return nil, errors.New("company with this tax ID already exists")
|
|
}
|
|
company.TaxID = form.TaxID
|
|
}
|
|
|
|
// Update fields if provided
|
|
if form.Type != "" {
|
|
company.Type = CompanyType(form.Type)
|
|
}
|
|
|
|
if form.Industry != "" {
|
|
company.Industry = form.Industry
|
|
}
|
|
|
|
if form.Description != nil {
|
|
company.Description = form.Description
|
|
}
|
|
|
|
if form.Website != nil {
|
|
company.Website = form.Website
|
|
}
|
|
|
|
if form.EmployeeCount != nil {
|
|
company.EmployeeCount = form.EmployeeCount
|
|
}
|
|
|
|
if form.AnnualRevenue != nil {
|
|
company.AnnualRevenue = form.AnnualRevenue
|
|
}
|
|
|
|
if form.FoundedYear != nil {
|
|
company.FoundedYear = form.FoundedYear
|
|
}
|
|
|
|
if form.Address != nil {
|
|
company.Address = s.convertAddressForm(form.Address)
|
|
}
|
|
|
|
if form.Phone != nil {
|
|
company.Phone = form.Phone
|
|
}
|
|
|
|
if form.Email != nil {
|
|
company.Email = form.Email
|
|
}
|
|
|
|
if form.DocumentFileIDs != nil {
|
|
company.DocumentFileIDs = s.sanitizeDocumentFileIDs(form.DocumentFileIDs)
|
|
}
|
|
|
|
if form.Links != nil {
|
|
links, linkErr := sanitizeCompanyLinks(convertCompanyLinkForms(form.Links))
|
|
if linkErr != nil {
|
|
return nil, linkErr
|
|
}
|
|
if len(links) > maxCompanyLinks {
|
|
return nil, errors.New("too many links provided")
|
|
}
|
|
company.Links = links
|
|
}
|
|
|
|
if form.Tags != nil {
|
|
company.Tags = s.convertCompanyTagsForm(form.Tags)
|
|
}
|
|
|
|
if form.Language != nil {
|
|
company.Language = *form.Language
|
|
}
|
|
|
|
if form.Currency != nil {
|
|
company.Currency = *form.Currency
|
|
}
|
|
|
|
if form.Timezone != nil {
|
|
company.Timezone = *form.Timezone
|
|
}
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, company)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update company", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": id,
|
|
})
|
|
return nil, errors.New("failed to update company")
|
|
}
|
|
|
|
s.logger.Info("Company updated successfully", map[string]interface{}{
|
|
"company_id": company.ID,
|
|
"name": company.Name,
|
|
})
|
|
|
|
s.triggerAIOnboardingAsync(company.GetID())
|
|
|
|
return company.ToResponse(nil), nil
|
|
}
|
|
|
|
// DeleteCompany deletes a company (soft delete)
|
|
func (s *companyService) Delete(ctx context.Context, id string) error {
|
|
// Delete company (soft delete)
|
|
err := s.repository.Delete(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to delete company", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": id,
|
|
})
|
|
return errors.New("failed to delete company")
|
|
}
|
|
|
|
s.logger.Info("Company deleted successfully", map[string]interface{}{
|
|
"company_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// SearchCompanies searches companies with advanced filters
|
|
func (s *companyService) SearchCompanies(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CompanyListResponse, error) {
|
|
|
|
// Get companies using search
|
|
page, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to search companies", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
var companyResponses []*CompanyResponse
|
|
for _, company := range page.Items {
|
|
// Get category details for response
|
|
categoryDetails, err := s.categoryService.GetByIDs(ctx, company.Tags.Categories)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to fetch category details for response", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": company.ID,
|
|
})
|
|
}
|
|
|
|
var convertedCategories []*CategoryResponse
|
|
for _, cat := range categoryDetails {
|
|
convertedCategories = append(convertedCategories, &CategoryResponse{
|
|
ID: cat.ID,
|
|
Name: cat.Name,
|
|
})
|
|
}
|
|
|
|
companyResponses = append(companyResponses, company.ToResponse(convertedCategories))
|
|
}
|
|
|
|
return &CompanyListResponse{
|
|
Companies: companyResponses,
|
|
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
|
|
}, nil
|
|
}
|
|
|
|
// GetByIDs retrieves companies by IDs
|
|
func (s *companyService) GetByIDs(ctx context.Context, ids []string) ([]*CompanyResponse, error) {
|
|
companies, err := s.repository.GetByIDs(ctx, ids)
|
|
if err != nil {
|
|
return nil, errors.New("failed to get companies by IDs")
|
|
}
|
|
|
|
var companyResponses []*CompanyResponse
|
|
for _, company := range companies {
|
|
// Get category details for response
|
|
categoryDetails, err := s.categoryService.GetByIDs(ctx, company.Tags.Categories)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to fetch category details for response", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": company.ID,
|
|
})
|
|
}
|
|
|
|
var convertedCategories []*CategoryResponse
|
|
for _, cat := range categoryDetails {
|
|
convertedCategories = append(convertedCategories, &CategoryResponse{
|
|
ID: cat.ID,
|
|
Name: cat.Name,
|
|
})
|
|
}
|
|
|
|
companyResponses = append(companyResponses, company.ToResponse(convertedCategories))
|
|
}
|
|
|
|
return companyResponses, nil
|
|
}
|
|
|
|
// GetNamesByIDs returns company id and name keyed by company ID without loading categories.
|
|
func (s *companyService) GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error) {
|
|
if len(ids) == 0 {
|
|
return map[string]*CompanyResponse{}, nil
|
|
}
|
|
|
|
companies, err := s.repository.GetByIDs(ctx, ids)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get companies by IDs for names", map[string]interface{}{
|
|
"count": len(ids),
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to get companies by IDs")
|
|
}
|
|
|
|
out := make(map[string]*CompanyResponse, len(companies))
|
|
for _, c := range companies {
|
|
id := c.ID.Hex()
|
|
out[id] = &CompanyResponse{ID: id, Name: c.Name}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// UpdateCompanyStatus updates company status
|
|
func (s *companyService) UpdateStatus(ctx context.Context, id string, form *StatusForm) error {
|
|
// Get company to check if exists
|
|
_, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("company not found")
|
|
}
|
|
|
|
// Update status
|
|
err = s.repository.UpdateStatus(ctx, id, CompanyStatus(form.Status))
|
|
if err != nil {
|
|
s.logger.Error("Failed to update company status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": id,
|
|
"status": CompanyStatus(form.Status),
|
|
})
|
|
return errors.New("failed to update company status")
|
|
}
|
|
|
|
s.logger.Info("Company status updated successfully", map[string]interface{}{
|
|
"company_id": id,
|
|
"status": form.Status,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateCompanyVerification updates company verification status
|
|
func (s *companyService) UpdateVerification(ctx context.Context, id string, form *VerificationForm) error {
|
|
// Get company to check if exists
|
|
_, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("company not found")
|
|
}
|
|
|
|
// Update verification
|
|
err = s.repository.UpdateVerification(ctx, id, form.IsVerified, form.IsCompliant, form.ComplianceNotes)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update company verification", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"company_id": id,
|
|
})
|
|
return errors.New("failed to update company verification")
|
|
}
|
|
|
|
s.logger.Info("Company verification updated successfully", map[string]interface{}{
|
|
"company_id": id,
|
|
"is_verified": form.IsVerified,
|
|
"is_compliant": form.IsCompliant,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMyCompany retrieves the company for the authenticated user
|
|
func (s *companyService) GetMyCompany(ctx context.Context, id string) (*CompanyProfileResponse, error) {
|
|
company, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, errors.New("company not found")
|
|
}
|
|
|
|
company.DocumentFileIDs = s.sanitizeDocumentFileIDs(company.DocumentFileIDs)
|
|
|
|
return company.ToProfileResponse(), nil
|
|
}
|
|
|
|
// Helper methods for converting forms to domain objects
|
|
func (s *companyService) convertAddressForm(form *AddressForm) *Address {
|
|
if form == nil {
|
|
return nil
|
|
}
|
|
|
|
return &Address{
|
|
Street: form.Street,
|
|
City: form.City,
|
|
State: form.State,
|
|
PostalCode: form.PostalCode,
|
|
Country: form.Country,
|
|
}
|
|
}
|
|
|
|
func (s *companyService) convertCompanyTagsForm(form *CompanyTagsForm) *CompanyTags {
|
|
if form == nil {
|
|
return nil
|
|
}
|
|
|
|
return &CompanyTags{
|
|
CPVCodes: form.CPVCodes,
|
|
Categories: form.Categories,
|
|
Keywords: form.Keywords,
|
|
Specializations: form.Specializations,
|
|
Certifications: form.Certifications,
|
|
}
|
|
}
|
|
|
|
func (s *companyService) getDefaultLanguage(language *string) string {
|
|
if language != nil {
|
|
return *language
|
|
}
|
|
return "en"
|
|
}
|
|
|
|
func (s *companyService) getDefaultCurrency(currency *string) string {
|
|
if currency != nil {
|
|
return *currency
|
|
}
|
|
return "USD"
|
|
}
|
|
|
|
func (s *companyService) getDefaultTimezone(timezone *string) string {
|
|
if timezone != nil {
|
|
return *timezone
|
|
}
|
|
return "UTC"
|
|
}
|