package company import ( "context" "errors" "mime/multipart" "tm/internal/company_category" "tm/pkg/filestore" "tm/pkg/logger" "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) // 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 aiRecommendationClient AIRecommendationClient logger logger.Logger } // NewService creates a new company 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, aiRecommendationClient: aiRecommendationClient, 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") } // Check if registration number already exists existingCompany, _ = s.repository.GetByRegistrationNumber(ctx, form.RegistrationNumber) if existingCompany != nil { return nil, errors.New("company with this registration number already exists") } // Check if tax ID already exists existingCompany, _ = s.repository.GetByTaxID(ctx, form.TaxID) if existingCompany != nil { return nil, errors.New("company with this tax ID already exists") } // 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: form.DocumentFileIDs, 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, }) 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, }) } 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 = form.DocumentFileIDs } 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, }) 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") } 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" }