Files
tm_back/internal/company/repository.go
T
n.nakhostin e3ee3d64ce Remove obsolete customer endpoint and update related documentation
- Deleted the `/admin/v1/customers/{id}/with-companies` endpoint from the API documentation and Swagger files to streamline the API structure.
- Updated the router to remove the corresponding route registration for the deprecated endpoint.
- Enhanced the company repository and service to support batch retrieval of companies by IDs, improving efficiency in data handling.
- Adjusted customer service methods to utilize the new batch retrieval functionality for loading companies associated with customers.
- Improved logging practices to provide better traceability for company retrieval operations.
2025-08-11 19:12:31 +03:30

817 lines
24 KiB
Go

package company
import (
"context"
"errors"
"time"
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Repository defines the interface for company data operations
type Repository interface {
Create(ctx context.Context, company *Company) error
GetByID(ctx context.Context, id string) (*Company, error)
GetByName(ctx context.Context, name string) (*Company, error)
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error)
GetByTaxID(ctx context.Context, taxID string) (*Company, error)
GetByIDs(ctx context.Context, ids []string) ([]*Company, error)
Update(ctx context.Context, company *Company) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, limit, offset int) ([]*Company, error)
Search(ctx context.Context, search string, companyType *string, status *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, cpvCodes []string, categories []string, keywords []string, specializations []string, employeeCountMin *int, employeeCountMax *int, annualRevenueMin *float64, annualRevenueMax *float64, foundedYearMin *int, foundedYearMax *int, limit, offset int, sortBy, sortOrder string) ([]*Company, error)
Count(ctx context.Context) (int64, error)
CountByType(ctx context.Context, companyType CompanyType) (int64, error)
CountByStatus(ctx context.Context, status CompanyStatus) (int64, error)
CountByIndustry(ctx context.Context, industry string) (int64, error)
CountVerified(ctx context.Context) (int64, error)
UpdateStatus(ctx context.Context, id string, status CompanyStatus) error
UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error
UpdateTags(ctx context.Context, id string, tags *CompanyTags) error
AddTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error
RemoveTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error
GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error)
SearchByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error)
}
// companyRepository implements the Repository interface using the MongoDB ORM
type companyRepository struct {
ormRepo mongopkg.Repository[Company]
logger logger.Logger
}
// NewCompanyRepository creates a new company repository
func NewCompanyRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management
indexes := []mongopkg.Index{
*mongopkg.CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}),
*mongopkg.CreateUniqueIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
*mongopkg.CreateUniqueIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
*mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}),
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
*mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}),
*mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}),
*mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}),
*mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}),
*mongopkg.NewIndex("employee_count_idx", bson.D{{Key: "employee_count", Value: 1}}),
*mongopkg.NewIndex("annual_revenue_idx", bson.D{{Key: "annual_revenue", Value: 1}}),
*mongopkg.NewIndex("founded_year_idx", bson.D{{Key: "founded_year", Value: 1}}),
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
// Tag indexes for efficient search
*mongopkg.NewIndex("cpv_codes_idx", bson.D{{Key: "tags.cpv_codes", Value: 1}}),
*mongopkg.NewIndex("categories_idx", bson.D{{Key: "tags.categories", Value: 1}}),
*mongopkg.NewIndex("keywords_idx", bson.D{{Key: "tags.keywords", Value: 1}}),
*mongopkg.NewIndex("specializations_idx", bson.D{{Key: "tags.specializations", Value: 1}}),
*mongopkg.NewIndex("certifications_idx", bson.D{{Key: "tags.certifications", Value: 1}}),
// Text index for search
*mongopkg.CreateTextIndex("search_idx", "name", "description", "industry"),
}
// Create indexes
err := mongoManager.CreateIndexes("companies", indexes)
if err != nil {
logger.Warn("Failed to create company indexes", map[string]interface{}{
"error": err.Error(),
})
}
// Create ORM repository
ormRepo := mongopkg.NewRepository[Company](mongoManager.GetCollection("companies"), logger)
return &companyRepository{
ormRepo: ormRepo,
logger: logger,
}
}
// Create creates a new company
func (r *companyRepository) Create(ctx context.Context, company *Company) error {
// Set created/updated timestamps using Unix timestamps
now := time.Now().Unix()
company.SetCreatedAt(now)
company.SetUpdatedAt(now)
// Use ORM to create company
err := r.ormRepo.Create(ctx, company)
if err != nil {
r.logger.Error("Failed to create company", map[string]interface{}{
"error": err.Error(),
"name": company.Name,
"registration_number": company.RegistrationNumber,
})
return err
}
r.logger.Info("Company created successfully", map[string]interface{}{
"company_id": company.ID,
"name": company.Name,
"type": company.Type,
})
return nil
}
// GetByID retrieves a company by ID
func (r *companyRepository) GetByID(ctx context.Context, id string) (*Company, error) {
company, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("company not found")
}
r.logger.Error("Failed to get company by ID", map[string]interface{}{
"error": err.Error(),
"company_id": id,
})
return nil, err
}
return company, nil
}
// GetByName retrieves a company by name
func (r *companyRepository) GetByName(ctx context.Context, name string) (*Company, error) {
filter := bson.M{"name": name}
company, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("company not found")
}
r.logger.Error("Failed to get company by name", map[string]interface{}{
"error": err.Error(),
"name": name,
})
return nil, err
}
return company, nil
}
// GetByRegistrationNumber retrieves a company by registration number
func (r *companyRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error) {
filter := bson.M{"registration_number": registrationNumber}
company, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("company not found")
}
r.logger.Error("Failed to get company by registration number", map[string]interface{}{
"error": err.Error(),
"registration_number": registrationNumber,
})
return nil, err
}
return company, nil
}
// GetByTaxID retrieves a company by tax ID
func (r *companyRepository) GetByTaxID(ctx context.Context, taxID string) (*Company, error) {
filter := bson.M{"tax_id": taxID}
company, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("company not found")
}
r.logger.Error("Failed to get company by tax ID", map[string]interface{}{
"error": err.Error(),
"tax_id": taxID,
})
return nil, err
}
return company, nil
}
// GetByIDs retrieves multiple companies by their IDs
func (r *companyRepository) GetByIDs(ctx context.Context, ids []string) ([]*Company, error) {
if len(ids) == 0 {
return []*Company{}, nil
}
// Convert string IDs to ObjectIDs for MongoDB query
objectIDs := make([]interface{}, len(ids))
for i, id := range ids {
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
r.logger.Warn("Invalid company ID format", map[string]interface{}{
"id": id,
"error": err.Error(),
})
continue
}
objectIDs[i] = objectID
}
if len(objectIDs) == 0 {
return []*Company{}, nil
}
filter := bson.M{"_id": bson.M{"$in": objectIDs}}
companies, err := r.ormRepo.FindMany(ctx, filter, len(ids))
if err != nil {
r.logger.Error("Failed to get companies by IDs", map[string]interface{}{
"error": err.Error(),
"ids": ids,
})
return nil, err
}
// Convert to pointer slice
result := make([]*Company, len(companies))
for i := range companies {
result[i] = &companies[i]
}
r.logger.Info("Retrieved companies by IDs", map[string]interface{}{
"requested_count": len(ids),
"found_count": len(result),
})
return result, nil
}
// Update updates a company
func (r *companyRepository) Update(ctx context.Context, company *Company) error {
// Set updated timestamp using Unix timestamp
company.SetUpdatedAt(time.Now().Unix())
// Use ORM to update company
err := r.ormRepo.Update(ctx, company)
if err != nil {
r.logger.Error("Failed to update company", map[string]interface{}{
"error": err.Error(),
"company_id": company.ID,
})
return err
}
r.logger.Info("Company updated successfully", map[string]interface{}{
"company_id": company.ID,
"name": company.Name,
})
return nil
}
// Delete deletes a company (soft delete by setting status to inactive)
func (r *companyRepository) Delete(ctx context.Context, id string) error {
// Get company first
company, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update status to inactive
company.Status = CompanyStatusInactive
company.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, company)
if err != nil {
r.logger.Error("Failed to delete company", map[string]interface{}{
"error": err.Error(),
"company_id": id,
})
return err
}
r.logger.Info("Company deleted successfully", map[string]interface{}{
"company_id": id,
})
return nil
}
// List retrieves companies with pagination
func (r *companyRepository) List(ctx context.Context, limit, offset int) ([]*Company, error) {
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
// Only active companies by default
filter := bson.M{"status": bson.M{"$ne": CompanyStatusInactive}}
// Use ORM to find companies
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to list companies", map[string]interface{}{
"error": err.Error(),
"limit": limit,
"offset": offset,
})
return nil, err
}
// Convert []Company to []*Company
companies := make([]*Company, len(result.Items))
for i := range result.Items {
companies[i] = &result.Items[i]
}
return companies, nil
}
// Search retrieves companies with search and filters
func (r *companyRepository) Search(ctx context.Context, search string, companyType *string, status *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, cpvCodes []string, categories []string, keywords []string, specializations []string, employeeCountMin *int, employeeCountMax *int, annualRevenueMin *float64, annualRevenueMax *float64, foundedYearMin *int, foundedYearMax *int, limit, offset int, sortBy, sortOrder string) ([]*Company, error) {
// Build filter
filter := bson.M{}
if search != "" {
filter["$text"] = bson.M{"$search": search}
}
if companyType != nil {
filter["type"] = *companyType
}
if status != nil {
filter["status"] = *status
}
if industry != nil {
filter["industry"] = *industry
}
if isVerified != nil {
filter["is_verified"] = *isVerified
}
if isCompliant != nil {
filter["is_compliant"] = *isCompliant
}
if language != nil {
filter["language"] = *language
}
if currency != nil {
filter["currency"] = *currency
}
// Tag filters
if len(cpvCodes) > 0 {
filter["tags.cpv_codes"] = bson.M{"$in": cpvCodes}
}
if len(categories) > 0 {
filter["tags.categories"] = bson.M{"$in": categories}
}
if len(keywords) > 0 {
filter["tags.keywords"] = bson.M{"$in": keywords}
}
if len(specializations) > 0 {
filter["tags.specializations"] = bson.M{"$in": specializations}
}
// Range filters
if employeeCountMin != nil || employeeCountMax != nil {
rangeFilter := bson.M{}
if employeeCountMin != nil {
rangeFilter["$gte"] = *employeeCountMin
}
if employeeCountMax != nil {
rangeFilter["$lte"] = *employeeCountMax
}
filter["employee_count"] = rangeFilter
}
if annualRevenueMin != nil || annualRevenueMax != nil {
rangeFilter := bson.M{}
if annualRevenueMin != nil {
rangeFilter["$gte"] = *annualRevenueMin
}
if annualRevenueMax != nil {
rangeFilter["$lte"] = *annualRevenueMax
}
filter["annual_revenue"] = rangeFilter
}
if foundedYearMin != nil || foundedYearMax != nil {
rangeFilter := bson.M{}
if foundedYearMin != nil {
rangeFilter["$gte"] = *foundedYearMin
}
if foundedYearMax != nil {
rangeFilter["$lte"] = *foundedYearMax
}
filter["founded_year"] = rangeFilter
}
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset)
// Set sort
if sortBy != "" {
if sortOrder == "desc" {
pagination.SortDesc(sortBy)
} else {
pagination.SortAsc(sortBy)
}
} else {
pagination.SortDesc("created_at") // Default sort
}
// Use ORM to find companies
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
if err != nil {
r.logger.Error("Failed to search companies", map[string]interface{}{
"error": err.Error(),
"search": search,
})
return nil, err
}
// Convert []Company to []*Company
companies := make([]*Company, len(result.Items))
for i := range result.Items {
companies[i] = &result.Items[i]
}
return companies, nil
}
// Count counts all companies
func (r *companyRepository) Count(ctx context.Context) (int64, error) {
filter := bson.M{"status": bson.M{"$ne": CompanyStatusInactive}}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count companies", map[string]interface{}{
"error": err.Error(),
})
return 0, err
}
return count, nil
}
// CountByType counts companies by type
func (r *companyRepository) CountByType(ctx context.Context, companyType CompanyType) (int64, error) {
filter := bson.M{
"type": companyType,
"status": bson.M{"$ne": CompanyStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count companies by type", map[string]interface{}{
"error": err.Error(),
"company_type": companyType,
})
return 0, err
}
return count, nil
}
// CountByStatus counts companies by status
func (r *companyRepository) CountByStatus(ctx context.Context, status CompanyStatus) (int64, error) {
filter := bson.M{"status": status}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count companies by status", map[string]interface{}{
"error": err.Error(),
"status": status,
})
return 0, err
}
return count, nil
}
// CountByIndustry counts companies by industry
func (r *companyRepository) CountByIndustry(ctx context.Context, industry string) (int64, error) {
filter := bson.M{
"industry": industry,
"status": bson.M{"$ne": CompanyStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count companies by industry", map[string]interface{}{
"error": err.Error(),
"industry": industry,
})
return 0, err
}
return count, nil
}
// CountVerified counts verified companies
func (r *companyRepository) CountVerified(ctx context.Context) (int64, error) {
filter := bson.M{
"is_verified": true,
"status": bson.M{"$ne": CompanyStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count verified companies", map[string]interface{}{
"error": err.Error(),
})
return 0, err
}
return count, nil
}
// UpdateStatus updates company status
func (r *companyRepository) UpdateStatus(ctx context.Context, id string, status CompanyStatus) error {
// Get company first
company, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update status
company.Status = status
company.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, company)
if err != nil {
r.logger.Error("Failed to update company status", map[string]interface{}{
"error": err.Error(),
"company_id": id,
"status": status,
})
return err
}
r.logger.Info("Company status updated successfully", map[string]interface{}{
"company_id": id,
"status": status,
})
return nil
}
// UpdateVerification updates company verification status
func (r *companyRepository) UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error {
// Get company first
company, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update verification fields
company.IsVerified = isVerified
company.IsCompliant = isCompliant
company.ComplianceNotes = complianceNotes
company.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, company)
if err != nil {
r.logger.Error("Failed to update company verification", map[string]interface{}{
"error": err.Error(),
"company_id": id,
})
return err
}
r.logger.Info("Company verification updated successfully", map[string]interface{}{
"company_id": id,
"is_verified": isVerified,
"is_compliant": isCompliant,
})
return nil
}
// UpdateTags updates company tags
func (r *companyRepository) UpdateTags(ctx context.Context, id string, tags *CompanyTags) error {
// Get company first
company, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update tags
company.Tags = tags
company.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, company)
if err != nil {
r.logger.Error("Failed to update company tags", map[string]interface{}{
"error": err.Error(),
"company_id": id,
})
return err
}
r.logger.Info("Company tags updated successfully", map[string]interface{}{
"company_id": id,
})
return nil
}
// AddTags adds tags to a company
func (r *companyRepository) AddTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error {
// Get company first
company, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Initialize tags if nil
if company.Tags == nil {
company.Tags = &CompanyTags{}
}
// Add tags (avoiding duplicates)
company.Tags.CPVCodes = r.addUniqueStrings(company.Tags.CPVCodes, cpvCodes)
company.Tags.Categories = r.addUniqueStrings(company.Tags.Categories, categories)
company.Tags.Keywords = r.addUniqueStrings(company.Tags.Keywords, keywords)
company.Tags.Specializations = r.addUniqueStrings(company.Tags.Specializations, specializations)
company.Tags.Certifications = r.addUniqueStrings(company.Tags.Certifications, certifications)
company.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, company)
if err != nil {
r.logger.Error("Failed to add tags to company", map[string]interface{}{
"error": err.Error(),
"company_id": id,
})
return err
}
r.logger.Info("Tags added to company successfully", map[string]interface{}{
"company_id": id,
})
return nil
}
// RemoveTags removes tags from a company
func (r *companyRepository) RemoveTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error {
// Get company first
company, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Initialize tags if nil
if company.Tags == nil {
company.Tags = &CompanyTags{}
}
// Remove tags
company.Tags.CPVCodes = r.removeStrings(company.Tags.CPVCodes, cpvCodes)
company.Tags.Categories = r.removeStrings(company.Tags.Categories, categories)
company.Tags.Keywords = r.removeStrings(company.Tags.Keywords, keywords)
company.Tags.Specializations = r.removeStrings(company.Tags.Specializations, specializations)
company.Tags.Certifications = r.removeStrings(company.Tags.Certifications, certifications)
company.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, company)
if err != nil {
r.logger.Error("Failed to remove tags from company", map[string]interface{}{
"error": err.Error(),
"company_id": id,
})
return err
}
r.logger.Info("Tags removed from company successfully", map[string]interface{}{
"company_id": id,
})
return nil
}
// GetCompanyStats returns company statistics
func (r *companyRepository) GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error) {
// This would typically use aggregation pipelines for better performance
// For now, we'll use basic counts
totalCompanies, _ := r.Count(ctx)
activeCompanies, _ := r.CountByStatus(ctx, CompanyStatusActive)
verifiedCompanies, _ := r.CountVerified(ctx)
// For this implementation, we'll return basic stats
// In a real implementation, you'd use MongoDB aggregation pipelines
stats := &CompanyStatsResponse{
TotalCompanies: totalCompanies,
ActiveCompanies: activeCompanies,
VerifiedCompanies: verifiedCompanies,
CompaniesByType: make(map[string]int64),
CompaniesByIndustry: make(map[string]int64),
CompaniesByStatus: make(map[string]int64),
TopCategories: []CategoryCount{},
TopCPVCodes: []CPVCodeCount{},
}
return stats, nil
}
// SearchByTags searches companies by tags
func (r *companyRepository) SearchByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error) {
filter := bson.M{}
// Build tag filters
var tagFilters []bson.M
if len(cpvCodes) > 0 {
tagFilters = append(tagFilters, bson.M{"tags.cpv_codes": bson.M{"$in": cpvCodes}})
}
if len(categories) > 0 {
tagFilters = append(tagFilters, bson.M{"tags.categories": bson.M{"$in": categories}})
}
if len(keywords) > 0 {
tagFilters = append(tagFilters, bson.M{"tags.keywords": bson.M{"$in": keywords}})
}
if len(specializations) > 0 {
tagFilters = append(tagFilters, bson.M{"tags.specializations": bson.M{"$in": specializations}})
}
if len(tagFilters) > 0 {
filter["$or"] = tagFilters
}
// Only active companies
filter["status"] = bson.M{"$ne": CompanyStatusInactive}
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
// Use ORM to find companies
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to search companies by tags", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
// Convert []Company to []*Company
companies := make([]*Company, len(result.Items))
for i := range result.Items {
companies[i] = &result.Items[i]
}
return companies, nil
}
// Helper functions for tag management
func (r *companyRepository) addUniqueStrings(existing []string, new []string) []string {
existingMap := make(map[string]bool)
for _, s := range existing {
existingMap[s] = true
}
result := make([]string, len(existing))
copy(result, existing)
for _, s := range new {
if !existingMap[s] {
result = append(result, s)
existingMap[s] = true
}
}
return result
}
func (r *companyRepository) removeStrings(existing []string, toRemove []string) []string {
removeMap := make(map[string]bool)
for _, s := range toRemove {
removeMap[s] = true
}
var result []string
for _, s := range existing {
if !removeMap[s] {
result = append(result, s)
}
}
return result
}