Files
tm_back/internal/company/repository.go
T
Mazyar 89faa08b1c
continuous-integration/drone/push Build is passing
Add functionality to manage external links for companies
- Introduced the ability to append external links to company profiles through a new API endpoint.
- Enhanced the `Company` entity to include a `Links` field for storing external resource links.
- Created `AddLinksForm` for validating incoming link data and implemented corresponding logic in the service layer.
- Added error handling for link validation, ensuring only valid URLs are accepted and limiting the number of links to 20.
- Implemented unit tests for link management functions, including sanitization and merging of links.
- Updated relevant API documentation to reflect the new functionality.

This update significantly enhances the company management capabilities by allowing the addition of external links, improving the overall user experience and data richness in company profiles.
2026-07-06 00:05:48 +03:30

480 lines
12 KiB
Go

package company
import (
"context"
"errors"
"regexp"
"strings"
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
// Repository defines the interface for company data operations
type Repository interface {
Create(ctx context.Context, company *Company) error
Update(ctx context.Context, company *Company) error
GetByID(ctx context.Context, id string) (*Company, error)
GetByIDs(ctx context.Context, ids []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)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Company], error)
Delete(ctx context.Context, id string) error
UpdateStatus(ctx context.Context, id string, status CompanyStatus) error
UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error
RemoveDocumentFileID(ctx context.Context, fileID string) error
}
// companyRepository implements the Repository interface using the MongoDB ORM
type companyRepository struct {
ormRepo orm.Repository[Company]
collection *mongo.Collection
logger logger.Logger
}
// NewCompanyRepository creates a new company repository
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management
indexes := []orm.Index{
*orm.NewIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
}
// Create indexes
err := mongoManager.CreateIndexes("companies", indexes)
if err != nil {
logger.Warn("Failed to create company indexes", map[string]interface{}{
"error": err.Error(),
})
}
collection := mongoManager.GetCollection("companies")
// Create ORM repository
ormRepo := orm.NewRepository[Company](collection, logger)
return &companyRepository{
ormRepo: ormRepo,
collection: collection,
logger: logger,
}
}
func mapCompanySortField(sortBy string) string {
switch sortBy {
case "country":
return "address.country"
case "state":
return "address.state"
default:
return sortBy
}
}
// 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)
// 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, orm.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
}
// GetByIDs retrieves companies by IDs
func (r *companyRepository) GetByIDs(ctx context.Context, ids []string) ([]Company, error) {
objectIDs := make([]bson.ObjectID, len(ids))
for i, id := range ids {
objectID, err := bson.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
objectIDs[i] = objectID
}
companies, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": objectIDs}}, orm.NewPaginationBuilder().Build())
if err != nil {
return nil, err
}
return companies.Items, nil
}
// GetByName retrieves a company by name
func (r *companyRepository) GetByName(ctx context.Context, name string) (*Company, error) {
company, err := r.ormRepo.FindOne(ctx, bson.M{"name": name})
if err != nil {
return nil, err
}
return company, nil
}
// GetByRegistrationNumber retrieves a company by registration number
func (r *companyRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error) {
company, err := r.ormRepo.FindOne(ctx, bson.M{"registration_number": registrationNumber})
if err != nil {
return nil, err
}
return company, nil
}
// GetByTaxID retrieves a company by tax ID
func (r *companyRepository) GetByTaxID(ctx context.Context, taxID string) (*Company, error) {
company, err := r.ormRepo.FindOne(ctx, bson.M{"tax_id": taxID})
if err != nil {
return nil, err
}
return company, 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())
clearDocuments := company.DocumentFileIDs != nil && len(company.DocumentFileIDs) == 0
clearLinks := company.Links != nil && len(company.Links) == 0
// 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
}
// BSON omitempty can skip empty slices in $set, so persist explicit empty arrays.
if clearDocuments || clearLinks {
setFields := bson.M{
"updated_at": company.UpdatedAt,
}
if clearDocuments {
setFields["document_file_ids"] = []string{}
}
if clearLinks {
setFields["links"] = []CompanyLink{}
}
_, err = r.collection.UpdateOne(ctx, bson.M{"_id": company.ID}, bson.M{
"$set": setFields,
})
if err != nil {
r.logger.Error("Failed to clear company collection fields", 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
func (r *companyRepository) Delete(ctx context.Context, id string) error {
// Get company first
_, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update in database
err = r.ormRepo.Delete(ctx, id)
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
}
// Search retrieves companies with search and filters
func (r *companyRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Company], error) {
// Build filter
filter := bson.M{}
if form.Search != nil {
filter["$text"] = bson.M{"$search": *form.Search}
}
if form.Name != nil {
name := strings.TrimSpace(*form.Name)
if name != "" {
filter["name"] = bson.M{
"$regex": regexp.QuoteMeta(name),
"$options": "i",
}
}
}
if form.Email != nil {
email := strings.TrimSpace(*form.Email)
if email != "" {
filter["email"] = bson.M{
"$regex": regexp.QuoteMeta(email),
"$options": "i",
}
}
}
if form.Phone != nil {
phone := strings.TrimSpace(*form.Phone)
if phone != "" {
filter["phone"] = bson.M{
"$regex": regexp.QuoteMeta(phone),
"$options": "i",
}
}
}
if form.Type != nil {
filter["type"] = *form.Type
}
if form.Status != nil {
filter["status"] = *form.Status
}
if form.Industry != nil {
filter["industry"] = *form.Industry
}
if form.IsVerified != nil {
filter["is_verified"] = *form.IsVerified
}
if form.IsCompliant != nil {
filter["is_compliant"] = *form.IsCompliant
}
if form.Language != nil {
filter["language"] = *form.Language
}
if form.Currency != nil {
filter["currency"] = *form.Currency
}
// Tag filters
if len(form.CPVCodes) > 0 {
filter["tags.cpv_codes"] = bson.M{"$in": form.CPVCodes}
}
if len(form.Categories) > 0 {
filter["tags.categories"] = bson.M{"$in": form.Categories}
}
if len(form.Keywords) > 0 {
filter["tags.keywords"] = bson.M{"$in": form.Keywords}
}
if len(form.Specializations) > 0 {
filter["tags.specializations"] = bson.M{"$in": form.Specializations}
}
// Range filters
if form.EmployeeCount != nil {
filter["employee_count"] = *form.EmployeeCount
} else if form.EmployeeCountMin != nil || form.EmployeeCountMax != nil {
rangeFilter := bson.M{}
if form.EmployeeCountMin != nil {
rangeFilter["$gte"] = *form.EmployeeCountMin
}
if form.EmployeeCountMax != nil {
rangeFilter["$lte"] = *form.EmployeeCountMax
}
filter["employee_count"] = rangeFilter
}
if form.AnnualRevenueMin != nil || form.AnnualRevenueMax != nil {
rangeFilter := bson.M{}
if form.AnnualRevenueMin != nil {
rangeFilter["$gte"] = *form.AnnualRevenueMin
}
if form.AnnualRevenueMax != nil {
rangeFilter["$lte"] = *form.AnnualRevenueMax
}
filter["annual_revenue"] = rangeFilter
}
if form.FoundedYearMin != nil || form.FoundedYearMax != nil {
rangeFilter := bson.M{}
if form.FoundedYearMin != nil {
rangeFilter["$gte"] = *form.FoundedYearMin
}
if form.FoundedYearMax != nil {
rangeFilter["$lte"] = *form.FoundedYearMax
}
filter["founded_year"] = rangeFilter
}
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
mapCompanySortField(pagination.SortBy),
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
return nil, err
}
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search companies", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return result, 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
}
// RemoveDocumentFileID removes a file ID from all companies that reference it.
func (r *companyRepository) RemoveDocumentFileID(ctx context.Context, fileID string) error {
if fileID == "" {
return nil
}
filter := bson.M{"document_file_ids": fileID}
update := bson.M{
"$pull": bson.M{"document_file_ids": fileID},
"$set": bson.M{"updated_at": time.Now().Unix()},
}
result, err := r.collection.UpdateMany(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to remove document file ID from companies", map[string]interface{}{
"error": err.Error(),
"file_id": fileID,
})
return err
}
if result.ModifiedCount > 0 {
r.logger.Info("Removed document file ID from companies", map[string]interface{}{
"file_id": fileID,
"modified_count": result.ModifiedCount,
})
}
return nil
}