Files
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

521 lines
21 KiB
Go

package company
import (
"errors"
"mime/multipart"
"tm/internal/user"
"tm/pkg/logger"
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for company operations
type Handler struct {
service Service
userHandler *user.Handler
logger logger.Logger
}
// NewHandler creates a new company handler
func NewHandler(service Service, userHandler *user.Handler, logger logger.Logger) *Handler {
return &Handler{
service: service,
userHandler: userHandler,
logger: logger,
}
}
// **************************** PUBLIC ENDPOINTS ****************************
// MyCompany returns the authenticated user's company profile (Mobile App)
// @Summary Get my company profile
// @Description Retrieve the profile of the company associated with the authenticated user
// @Tags Company
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=CompanyProfileResponse} "Company retrieved successfully"
// @Failure 401 {object} response.APIResponse "Unauthorized - User not authenticated"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/companies [get]
func (h *Handler) MyCompany(c echo.Context) error {
companyID, err := user.GetCompanyIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
company, err := h.service.GetMyCompany(c.Request().Context(), companyID)
if err != nil {
return response.InternalServerError(c, "Failed to get company")
}
return response.Success(c, company, "Company retrieved successfully")
}
// StartOnboarding starts AI onboarding for the authenticated customer's company (Mobile App)
// @Summary Start AI company onboarding
// @Description Send company profile, uploaded documents, and website URL to the AI team to start tender recommendation onboarding
// @Tags Company
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=OnboardingResponse}
// @Failure 401 {object} response.APIResponse "Unauthorized - User not authenticated"
// @Failure 404 {object} response.APIResponse "Company not found"
// @Failure 503 {object} response.APIResponse "AI recommendation service unavailable"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/onboarding [post]
func (h *Handler) StartOnboarding(c echo.Context) error {
companyID, err := user.GetCompanyIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
result, err := h.service.StartAIOnboarding(c.Request().Context(), companyID)
if err != nil {
if errors.Is(err, ErrAIRecommendationNotConfigured) {
return response.ServiceUnavailable(c, "AI recommendation service is not configured")
}
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
return response.InternalServerError(c, "Failed to start AI onboarding")
}
return response.Success(c, result, "AI onboarding started successfully")
}
// RecommendTenders returns AI-ranked tender recommendations for the authenticated customer's company (Mobile App)
// @Summary Get AI tender recommendations
// @Description Retrieve ranked tender recommendations from the AI team for the authenticated customer's company
// @Tags Company
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=[]RecommendedTenderResponse}
// @Failure 401 {object} response.APIResponse "Unauthorized - User not authenticated"
// @Failure 404 {object} response.APIResponse "Company not found"
// @Failure 503 {object} response.APIResponse "AI recommendation service unavailable"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /api/v1/recommend [post]
func (h *Handler) RecommendTenders(c echo.Context) error {
if companyIDs, ok := c.Get("company_ids").([]string); ok && len(companyIDs) > 0 {
result, err := h.service.GetMergedAIRecommendations(c.Request().Context(), companyIDs)
if err != nil {
if errors.Is(err, ErrAIRecommendationNotConfigured) {
return response.ServiceUnavailable(c, "AI recommendation service is not configured")
}
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
return response.InternalServerError(c, "Failed to retrieve AI recommendations")
}
return response.Success(c, result, "AI recommendations retrieved successfully")
}
companyID, err := user.GetCompanyIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
result, err := h.service.GetAIRecommendations(c.Request().Context(), companyID)
if err != nil {
if errors.Is(err, ErrAIRecommendationNotConfigured) {
return response.ServiceUnavailable(c, "AI recommendation service is not configured")
}
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
return response.InternalServerError(c, "Failed to retrieve AI recommendations")
}
return response.Success(c, result, "AI recommendations retrieved successfully")
}
// **************************** ADMIN ENDPOINTS ****************************
// Create creates a new company (Web Panel)
// @Summary Create a new company
// @Description Create a new company with comprehensive information including business details, address, tags, and customer assignment. This endpoint is used by the web panel for full company registration.
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param company body CompanyForm true "Company information including type, business details, address, tags, and optional customer assignment"
// @Success 201 {object} response.APIResponse{data=CompanyResponse} "Company created successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 409 {object} response.APIResponse "Conflict - Company with this name/registration/tax ID already exists"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies [post]
func (h *Handler) Create(c echo.Context) error {
form, err := response.Parse[CompanyForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
company, err := h.service.Create(c.Request().Context(), form)
if err != nil {
if err.Error() == "company with this name already exists" ||
err.Error() == "company with this registration number already exists" ||
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")
}
if err.Error() == "invalid link URL" || err.Error() == "too many links provided" {
return response.BadRequest(c, err.Error(), "")
}
return response.InternalServerError(c, "Failed to create company")
}
return response.Created(c, company, "Company created successfully")
}
// GetCompany retrieves a company by ID (Web Panel)
// @Summary Get company by ID
// @Description Retrieve detailed company information by company ID
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param id path string true "Company ID"
// @Success 200 {object} response.APIResponse{data=CompanyResponse} "Company retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies/{id} [get]
func (h *Handler) Get(c echo.Context) error {
id := c.Param("id")
company, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
return response.InternalServerError(c, "Failed to get company")
}
return response.Success(c, company, "Company retrieved successfully")
}
// Update updates a company (Web Panel)
// @Summary Update company information
// @Description Update company information including business details, address, tags, and customer assignment
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param id path string true "Company ID"
// @Param company body CompanyForm true "Company update information"
// @Success 200 {object} response.APIResponse{data=CompanyResponse} "Company updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
// @Failure 409 {object} response.APIResponse "Conflict - Company with this name/registration/tax ID already exists"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies/{id} [put]
func (h *Handler) Update(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[CompanyForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
company, err := h.service.Update(c.Request().Context(), id, form)
if err != nil {
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
if err.Error() == "company with this name already exists" ||
err.Error() == "company with this registration number already exists" ||
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")
}
if err.Error() == "invalid link URL" || err.Error() == "too many links provided" {
return response.BadRequest(c, err.Error(), "")
}
return response.InternalServerError(c, "Failed to update company")
}
return response.Success(c, company, "Company updated successfully")
}
// Delete deletes a company (Web Panel)
// @Summary Delete company
// @Description Soft delete a company by setting status to inactive
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param id path string true "Company ID"
// @Success 200 {object} response.APIResponse "Company deleted successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies/{id} [delete]
func (h *Handler) Delete(c echo.Context) error {
id := c.Param("id")
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
return response.InternalServerError(c, "Failed to delete company")
}
return response.Success(c, map[string]interface{}{
"message": "Company deleted successfully",
}, "Company deleted successfully")
}
// Search searches companies with advanced filters (Web Panel)
// @Summary Advanced search for companies
// @Description Search companies with advanced filtering capabilities including tags, business criteria, and location
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param q query string false "Search query"
// @Param name query string false "Filter by company name"
// @Param email query string false "Filter by email"
// @Param phone query string false "Filter by phone"
// @Param employee_count query integer false "Filter by exact employee count"
// @Param cpv_codes query array false "CPV codes filter"
// @Param categories query array false "Categories filter"
// @Param keywords query array false "Keywords filter"
// @Param specializations query array false "Specializations filter"
// @Param industry query string false "Industry filter"
// @Param location query string false "Location filter"
// @Param min_employees query integer false "Minimum employees"
// @Param max_employees query integer false "Maximum employees"
// @Param min_revenue query number false "Minimum revenue"
// @Param max_revenue query number false "Maximum revenue"
// @Param is_verified query boolean false "Verification status"
// @Param is_compliant query boolean false "Compliance status"
// @Param limit query integer false "Limit" default(20)
// @Param offset query integer false "Offset" default(0)
// @Param sort_by query string false "Field to sort by" Enums(name, type, industry, created_at, updated_at, status, employee_count, annual_revenue, email, phone, country, state, language, currency) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=CompanyListResponse} "Companies search completed"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies [get]
func (h *Handler) Search(c echo.Context) error {
form, err := response.Parse[SearchForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.SearchCompanies(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to list companies")
}
return response.SuccessWithMeta(c, result, result.Meta, "Companies retrieved successfully")
}
// UpdateStatus updates company status (Web Panel)
// @Summary Update company status
// @Description Update company account status (active, inactive, suspended, pending)
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param id path string true "Company ID"
// @Param status body StatusForm true "New company status"
// @Success 200 {object} response.APIResponse "Company status updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies/{id}/status [patch]
func (h *Handler) UpdateStatus(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[StatusForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
err = h.service.UpdateStatus(c.Request().Context(), id, form)
if err != nil {
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
return response.InternalServerError(c, "Failed to update company status")
}
return response.Success(c, map[string]interface{}{
"message": "Company status updated successfully",
}, "Company status updated successfully")
}
// UpdateVerification updates company verification status (Web Panel)
// @Summary Update company verification status
// @Description Update company verification and compliance status
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param id path string true "Company ID"
// @Param verification body VerificationForm true "Verification status update"
// @Success 200 {object} response.APIResponse "Company verification updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies/{id}/verification [patch]
func (h *Handler) UpdateVerification(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[VerificationForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
err = h.service.UpdateVerification(c.Request().Context(), id, form)
if err != nil {
if err.Error() == "company not found" {
return response.NotFound(c, "Company not found")
}
return response.InternalServerError(c, "Failed to update company verification")
}
return response.Success(c, map[string]interface{}{
"message": "Company verification updated successfully",
}, "Company verification updated successfully")
}
// UploadDocuments uploads multiple documents for a company (Web Panel)
// @Summary Upload company documents
// @Description Upload one or more files and attach them to the company (max 20 files per request). Use form field "files" for multiple files.
// @Tags Admin-Companies
// @Accept multipart/form-data
// @Produce json
// @Param id path string true "Company ID"
// @Param files formData file true "Files to upload (repeat field for multiple files)"
// @Success 200 {object} response.APIResponse{data=UploadDocumentsResponse} "Documents uploaded successfully"
// @Failure 400 {object} response.APIResponse "Bad request - No files or all uploads failed"
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
// @Failure 422 {object} response.APIResponse "Validation error - Too many files"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies/{id}/documents [post]
func (h *Handler) UploadDocuments(c echo.Context) error {
id := c.Param("id")
userID, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
form, err := c.MultipartForm()
if err != nil {
return response.ValidationError(c, "Invalid multipart form", err.Error())
}
files := collectCompanyUploadFiles(form)
if len(files) == 0 {
return response.BadRequest(c, "No files provided", "Use form field \"files\" with one or more files")
}
if len(files) > maxCompanyDocumentsUpload {
return response.ValidationError(c, "Too many files", "Maximum 20 files per request")
}
result, err := h.service.UploadDocuments(c.Request().Context(), id, userID, files)
if err != nil {
switch err.Error() {
case "company not found":
return response.NotFound(c, "Company not found")
case "no files provided", "too many files in request":
return response.BadRequest(c, err.Error(), "")
case "all file uploads failed":
return response.BadRequest(c, "All file uploads failed", "")
default:
return response.InternalServerError(c, "Failed to upload company documents")
}
}
message := "Company documents uploaded successfully"
if len(result.Failed) > 0 {
message = "Some company documents uploaded successfully"
}
return response.Success(c, result, message)
}
// AddLinks appends external links to a company (Web Panel)
// @Summary Add company links
// @Description Append one or more external URLs to the company (max 20 links per request)
// @Tags Admin-Companies
// @Accept json
// @Produce json
// @Param id path string true "Company ID"
// @Param links body AddLinksForm true "Links to append"
// @Success 200 {object} response.APIResponse{data=AddLinksResponse} "Links added successfully"
// @Failure 400 {object} response.APIResponse "Bad request - No links or invalid URLs"
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/companies/{id}/links [post]
func (h *Handler) AddLinks(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[AddLinksForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
if len(form.Links) > maxCompanyLinks {
return response.ValidationError(c, "Too many links", "Maximum 20 links per request")
}
result, err := h.service.AddLinks(c.Request().Context(), id, form)
if err != nil {
switch err.Error() {
case "company not found":
return response.NotFound(c, "Company not found")
case "no links provided", "no valid links provided", "invalid link URL":
return response.BadRequest(c, err.Error(), "")
case "too many links in request", "company link limit exceeded":
return response.ValidationError(c, err.Error(), "")
default:
return response.InternalServerError(c, "Failed to add company links")
}
}
message := "Company links added successfully"
if len(result.Added) == 0 {
message = "No new company links were added"
}
return response.Success(c, result, message)
}
func collectCompanyUploadFiles(form *multipart.Form) []*multipart.FileHeader {
if form == nil {
return nil
}
if files := form.File["files"]; len(files) > 0 {
return files
}
return form.File["file"]
}