diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 780f29c..e1ea1fd 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -5625,6 +5625,17 @@ const docTemplate = `{ } } }, + "company.CategoryResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "company.CompanyForm": { "type": "object", "properties": { @@ -5801,7 +5812,7 @@ const docTemplate = `{ "type": "string" }, "tags": { - "$ref": "#/definitions/company.CompanyTags" + "$ref": "#/definitions/company.CompanyTagsResponse" }, "tax_id": { "type": "string" @@ -5820,46 +5831,6 @@ const docTemplate = `{ } } }, - "company.CompanyTags": { - "type": "object", - "properties": { - "categories": { - "description": "Categories", - "type": "array", - "items": { - "type": "string" - } - }, - "certifications": { - "description": "Certifications", - "type": "array", - "items": { - "type": "string" - } - }, - "cpv_codes": { - "description": "CPV codes (Common Procurement Vocabulary)", - "type": "array", - "items": { - "type": "string" - } - }, - "keywords": { - "description": "Keywords for search and matching", - "type": "array", - "items": { - "type": "string" - } - }, - "specializations": { - "description": "Specializations", - "type": "array", - "items": { - "type": "string" - } - } - } - }, "company.CompanyTagsForm": { "type": "object", "properties": { @@ -5910,6 +5881,41 @@ const docTemplate = `{ } } }, + "company.CompanyTagsResponse": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CategoryResponse" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "company.StatusForm": { "type": "object", "properties": { diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 7dadc1d..707c5ec 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -5619,6 +5619,17 @@ } } }, + "company.CategoryResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "company.CompanyForm": { "type": "object", "properties": { @@ -5795,7 +5806,7 @@ "type": "string" }, "tags": { - "$ref": "#/definitions/company.CompanyTags" + "$ref": "#/definitions/company.CompanyTagsResponse" }, "tax_id": { "type": "string" @@ -5814,46 +5825,6 @@ } } }, - "company.CompanyTags": { - "type": "object", - "properties": { - "categories": { - "description": "Categories", - "type": "array", - "items": { - "type": "string" - } - }, - "certifications": { - "description": "Certifications", - "type": "array", - "items": { - "type": "string" - } - }, - "cpv_codes": { - "description": "CPV codes (Common Procurement Vocabulary)", - "type": "array", - "items": { - "type": "string" - } - }, - "keywords": { - "description": "Keywords for search and matching", - "type": "array", - "items": { - "type": "string" - } - }, - "specializations": { - "description": "Specializations", - "type": "array", - "items": { - "type": "string" - } - } - } - }, "company.CompanyTagsForm": { "type": "object", "properties": { @@ -5904,6 +5875,41 @@ } } }, + "company.CompanyTagsResponse": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CategoryResponse" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "company.StatusForm": { "type": "object", "properties": { diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index fc3a65f..8e83d80 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -54,6 +54,13 @@ definitions: example: 123 Main St type: string type: object + company.CategoryResponse: + properties: + id: + type: string + name: + type: string + type: object company.CompanyForm: properties: address: @@ -174,7 +181,7 @@ definitions: status: type: string tags: - $ref: '#/definitions/company.CompanyTags' + $ref: '#/definitions/company.CompanyTagsResponse' tax_id: type: string timezone: @@ -186,34 +193,6 @@ definitions: website: type: string type: object - company.CompanyTags: - properties: - categories: - description: Categories - items: - type: string - type: array - certifications: - description: Certifications - items: - type: string - type: array - cpv_codes: - description: CPV codes (Common Procurement Vocabulary) - items: - type: string - type: array - keywords: - description: Keywords for search and matching - items: - type: string - type: array - specializations: - description: Specializations - items: - type: string - type: array - type: object company.CompanyTagsForm: properties: categories: @@ -247,6 +226,29 @@ definitions: type: string type: array type: object + company.CompanyTagsResponse: + properties: + categories: + items: + $ref: '#/definitions/company.CategoryResponse' + type: array + certifications: + items: + type: string + type: array + cpv_codes: + items: + type: string + type: array + keywords: + items: + type: string + type: array + specializations: + items: + type: string + type: array + type: object company.StatusForm: properties: reason: diff --git a/cmd/web/main.go b/cmd/web/main.go index aadf427..82dac19 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -137,8 +137,8 @@ func main() { // Initialize services with repositories userService := user.NewUserService(userRepository, logger, userAuthService, userValidator) - companyService := company.NewCompanyService(companyRepository, logger) categoryService := company_category.NewCategoryService(categoryRepository, logger) + companyService := company.NewCompanyService(companyRepository, categoryService, logger) customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService) tenderService := tender.NewTenderService(tenderRepository, companyService, logger) feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger) diff --git a/internal/company/form.go b/internal/company/form.go index 1565cf5..999a0e8 100644 --- a/internal/company/form.go +++ b/internal/company/form.go @@ -98,36 +98,52 @@ type CompanyListResponse struct { Meta *response.Meta `json:"-"` } -// CompanyResponse represents the company data sent in API responses -type CompanyResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Status string `json:"status"` - RegistrationNumber string `json:"registration_number"` - TaxID string `json:"tax_id"` - Industry string `json:"industry"` - Description *string `json:"description,omitempty"` - Website *string `json:"website,omitempty"` - EmployeeCount *int `json:"employee_count,omitempty"` - AnnualRevenue *float64 `json:"annual_revenue,omitempty"` - FoundedYear *int `json:"founded_year,omitempty"` - Address *Address `json:"address,omitempty"` - Phone *string `json:"phone,omitempty"` - Email *string `json:"email,omitempty"` - Tags *CompanyTags `json:"tags,omitempty"` - IsVerified bool `json:"is_verified"` - IsCompliant bool `json:"is_compliant"` - ComplianceNotes *string `json:"compliance_notes,omitempty"` - Language string `json:"language"` - Currency string `json:"currency"` - Timezone string `json:"timezone"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` +// CategoryResponse represents category data for company responses +type CategoryResponse struct { + ID string `json:"id"` + Name string `json:"name"` } +// CompanyResponse represents the company data sent in API responses +type ( + CompanyResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` + RegistrationNumber string `json:"registration_number"` + TaxID string `json:"tax_id"` + Industry string `json:"industry"` + Description *string `json:"description,omitempty"` + Website *string `json:"website,omitempty"` + EmployeeCount *int `json:"employee_count,omitempty"` + AnnualRevenue *float64 `json:"annual_revenue,omitempty"` + FoundedYear *int `json:"founded_year,omitempty"` + Address *Address `json:"address,omitempty"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` + Tags *CompanyTagsResponse `json:"tags,omitempty"` + IsVerified bool `json:"is_verified"` + IsCompliant bool `json:"is_compliant"` + ComplianceNotes *string `json:"compliance_notes,omitempty"` + Language string `json:"language"` + Currency string `json:"currency"` + Timezone string `json:"timezone"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + + CompanyTagsResponse struct { + CPVCodes []string `json:"cpv_codes"` + Categories []*CategoryResponse `json:"categories"` + Keywords []string `json:"keywords"` + Specializations []string `json:"specializations"` + Certifications []string `json:"certifications"` + } +) + // ToResponse converts Company to CompanyResponse -func (c *Company) ToResponse() *CompanyResponse { +func (c *Company) ToResponse(categories []*CategoryResponse) *CompanyResponse { return &CompanyResponse{ ID: c.ID.Hex(), Name: c.Name, @@ -144,7 +160,7 @@ func (c *Company) ToResponse() *CompanyResponse { Address: c.Address, Phone: c.Phone, Email: c.Email, - Tags: c.Tags, + Tags: c.Tags.ToResponse(categories), IsVerified: c.IsVerified, IsCompliant: c.IsCompliant, ComplianceNotes: c.ComplianceNotes, @@ -156,6 +172,16 @@ func (c *Company) ToResponse() *CompanyResponse { } } +func (c *CompanyTags) ToResponse(categories []*CategoryResponse) *CompanyTagsResponse { + return &CompanyTagsResponse{ + CPVCodes: c.CPVCodes, + Categories: categories, + Keywords: c.Keywords, + Specializations: c.Specializations, + Certifications: c.Certifications, + } +} + // CompanyProfileResponse represents the company profile data sent in API responses type CompanyProfileResponse struct { ID string `json:"id"` diff --git a/internal/company/handler.go b/internal/company/handler.go index 61ec4b0..e594b36 100644 --- a/internal/company/handler.go +++ b/internal/company/handler.go @@ -80,6 +80,10 @@ func (h *Handler) Create(c echo.Context) error { err.Error() == "company with this tax ID already exists" { return response.Conflict(c, err.Error()) } + if err.Error() == "one or more categories are invalid" || + err.Error() == "failed to validate categories" { + return response.BadRequest(c, err.Error(), "Invalid category data") + } return response.InternalServerError(c, "Failed to create company") } @@ -146,6 +150,10 @@ func (h *Handler) Update(c echo.Context) error { err.Error() == "company with this tax ID already exists" { return response.Conflict(c, err.Error()) } + if err.Error() == "one or more categories are invalid" || + err.Error() == "failed to validate categories" { + return response.BadRequest(c, err.Error(), "Invalid category data") + } return response.InternalServerError(c, "Failed to update company") } diff --git a/internal/company/service.go b/internal/company/service.go index 0b2bca2..4c6f717 100644 --- a/internal/company/service.go +++ b/internal/company/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" + "tm/internal/company_category" "tm/pkg/logger" "tm/pkg/response" ) @@ -30,20 +31,39 @@ type Service interface { // companyService implements the Service interface type companyService struct { - repository Repository - logger logger.Logger + repository Repository + categoryService company_category.Service + logger logger.Logger } // NewCompanyService creates a new company service -func NewCompanyService(repository Repository, logger logger.Logger) Service { +func NewCompanyService(repository Repository, categoryService company_category.Service, logger logger.Logger) Service { return &companyService{ - repository: repository, - logger: logger, + repository: repository, + categoryService: categoryService, + 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 { @@ -103,7 +123,7 @@ func (s *companyService) Create(ctx context.Context, form *CompanyForm) (*Compan "type": company.Type, }) - return company.ToResponse(), nil + return company.ToResponse(nil), nil } // GetCompanyByID retrieves a company by ID @@ -122,11 +142,45 @@ func (s *companyService) GetByID(ctx context.Context, id string) (*CompanyRespon "name": company.Name, }) - return company.ToResponse(), nil + // 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 { @@ -236,7 +290,7 @@ func (s *companyService) Update(ctx context.Context, id string, form *CompanyFor "name": company.Name, }) - return company.ToResponse(), nil + return company.ToResponse(nil), nil } // DeleteCompany deletes a company (soft delete) @@ -273,7 +327,24 @@ func (s *companyService) SearchCompanies(ctx context.Context, form *SearchForm, // Convert to responses var companyResponses []*CompanyResponse for _, company := range companies { - companyResponses = append(companyResponses, company.ToResponse()) + // 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{ @@ -291,7 +362,24 @@ func (s *companyService) GetByIDs(ctx context.Context, ids []string) ([]*Company var companyResponses []*CompanyResponse for _, company := range companies { - companyResponses = append(companyResponses, company.ToResponse()) + // 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 diff --git a/internal/company_category/repository.go b/internal/company_category/repository.go index e0b395a..b773292 100644 --- a/internal/company_category/repository.go +++ b/internal/company_category/repository.go @@ -9,6 +9,7 @@ import ( "tm/pkg/response" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" ) // Repository defines the interface for category data operations @@ -98,7 +99,16 @@ func (r *categoryRepository) GetByID(ctx context.Context, id string) (*Category, // GetByIDs retrieves categories by IDs func (r *categoryRepository) GetByIDs(ctx context.Context, ids []string) ([]Category, error) { - filter := bson.M{"_id": bson.M{"$in": ids}} + objectIDs := make([]primitive.ObjectID, len(ids)) + for i, id := range ids { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + objectIDs[i] = objectID + } + + filter := bson.M{"_id": bson.M{"$in": objectIDs}} paginationBuilder := orm.NewPaginationBuilder(). Build()