1edf42187d
continuous-integration/drone/push Build is passing
- Added functionality to retrieve merged AI recommendations for multiple companies, improving the relevance of tender suggestions based on company-specific data. - Introduced normalization functions to clean and deduplicate company IDs, ensuring accurate processing of recommendations. - Enhanced the company context resolution in customer middleware to support multiple assigned companies, improving the handling of company-specific requests. - Updated the tender recommendation logic to utilize the new merged recommendations and handle exclusions for rejected tenders accordingly. - Added unit tests to verify the new recommendation merging logic and company ID normalization, ensuring robust functionality. This update significantly enhances the tender recommendation process by allowing for more comprehensive and relevant suggestions based on multiple company contexts, improving user experience and satisfaction.
467 lines
19 KiB
Go
467 lines
19 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")
|
|
}
|
|
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")
|
|
}
|
|
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)
|
|
}
|
|
|
|
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"]
|
|
}
|