Add functionality to manage external links for companies
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- 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.
This commit is contained in:
@@ -88,6 +88,7 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
|
|||||||
companiesGP.PATCH("/:id/status", companyHandler.UpdateStatus)
|
companiesGP.PATCH("/:id/status", companyHandler.UpdateStatus)
|
||||||
companiesGP.PATCH("/:id/verification", companyHandler.UpdateVerification)
|
companiesGP.PATCH("/:id/verification", companyHandler.UpdateVerification)
|
||||||
companiesGP.POST("/:id/documents", companyHandler.UploadDocuments)
|
companiesGP.POST("/:id/documents", companyHandler.UploadDocuments)
|
||||||
|
companiesGP.POST("/:id/links", companyHandler.AddLinks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin Categories Routes
|
// Admin Categories Routes
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ type Company struct {
|
|||||||
// File references stored in GridFS
|
// File references stored in GridFS
|
||||||
DocumentFileIDs []string `bson:"document_file_ids" json:"document_file_ids,omitempty"`
|
DocumentFileIDs []string `bson:"document_file_ids" json:"document_file_ids,omitempty"`
|
||||||
|
|
||||||
|
// External resource links (e.g. company profile pages, portfolios)
|
||||||
|
Links []CompanyLink `bson:"links,omitempty" json:"links,omitempty"`
|
||||||
|
|
||||||
// Tags for categorization and search
|
// Tags for categorization and search
|
||||||
Tags *CompanyTags `bson:"tags,omitempty" json:"tags,omitempty"`
|
Tags *CompanyTags `bson:"tags,omitempty" json:"tags,omitempty"`
|
||||||
|
|
||||||
@@ -97,6 +100,12 @@ func (c *Company) GetUpdatedAt() int64 {
|
|||||||
return c.UpdatedAt
|
return c.UpdatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompanyLink represents an external URL associated with a company.
|
||||||
|
type CompanyLink struct {
|
||||||
|
URL string `bson:"url" json:"url"`
|
||||||
|
Title *string `bson:"title,omitempty" json:"title,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Address represents a company's address
|
// Address represents a company's address
|
||||||
type Address struct {
|
type Address struct {
|
||||||
Street string `bson:"street" json:"street"`
|
Street string `bson:"street" json:"street"`
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ type (
|
|||||||
// Contact information
|
// Contact information
|
||||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
||||||
Email *string `json:"email,omitempty" valid:"optional,email" example:"info@opplens.com"`
|
Email *string `json:"email,omitempty" valid:"optional,email" example:"info@opplens.com"`
|
||||||
DocumentFileIDs []string `json:"document_file_ids,omitempty" valid:"optional"`
|
DocumentFileIDs []string `json:"document_file_ids,omitempty" valid:"optional"`
|
||||||
|
Links []CompanyLinkForm `json:"links,omitempty" valid:"optional"`
|
||||||
|
|
||||||
// Tags for categorization and search
|
// Tags for categorization and search
|
||||||
Tags *CompanyTagsForm `json:"tags,omitempty"`
|
Tags *CompanyTagsForm `json:"tags,omitempty"`
|
||||||
@@ -35,6 +36,12 @@ type (
|
|||||||
Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)" example:"UTC"`
|
Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)" example:"UTC"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompanyLinkForm represents the form for a company link.
|
||||||
|
CompanyLinkForm struct {
|
||||||
|
URL string `json:"url" valid:"required,url" example:"https://example.com/profile"`
|
||||||
|
Title *string `json:"title,omitempty" valid:"optional,length(1|200)" example:"Company profile"`
|
||||||
|
}
|
||||||
|
|
||||||
// AddressForm represents the form for company address
|
// AddressForm represents the form for company address
|
||||||
AddressForm struct {
|
AddressForm struct {
|
||||||
Street string `json:"street" valid:"required,length(5|200)" example:"123 Main St"`
|
Street string `json:"street" valid:"required,length(5|200)" example:"123 Main St"`
|
||||||
@@ -90,6 +97,11 @@ type StatusForm struct {
|
|||||||
Reason *string `json:"reason" valid:"optional,length(10|500)"`
|
Reason *string `json:"reason" valid:"optional,length(10|500)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddLinksForm is the request body for appending links to a company.
|
||||||
|
type AddLinksForm struct {
|
||||||
|
Links []CompanyLinkForm `json:"links" valid:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
// VerificationForm represents the form for updating company verification status
|
// VerificationForm represents the form for updating company verification status
|
||||||
type VerificationForm struct {
|
type VerificationForm struct {
|
||||||
IsVerified bool `json:"is_verified"`
|
IsVerified bool `json:"is_verified"`
|
||||||
@@ -128,6 +140,7 @@ type (
|
|||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
Email *string `json:"email,omitempty"`
|
Email *string `json:"email,omitempty"`
|
||||||
DocumentFileIDs []string `json:"document_file_ids,omitempty"`
|
DocumentFileIDs []string `json:"document_file_ids,omitempty"`
|
||||||
|
Links []CompanyLink `json:"links,omitempty"`
|
||||||
Tags *CompanyTagsResponse `json:"tags,omitempty"`
|
Tags *CompanyTagsResponse `json:"tags,omitempty"`
|
||||||
IsVerified bool `json:"is_verified"`
|
IsVerified bool `json:"is_verified"`
|
||||||
IsCompliant bool `json:"is_compliant"`
|
IsCompliant bool `json:"is_compliant"`
|
||||||
@@ -167,6 +180,7 @@ func (c *Company) ToResponse(categories []*CategoryResponse) *CompanyResponse {
|
|||||||
Phone: c.Phone,
|
Phone: c.Phone,
|
||||||
Email: c.Email,
|
Email: c.Email,
|
||||||
DocumentFileIDs: c.DocumentFileIDs,
|
DocumentFileIDs: c.DocumentFileIDs,
|
||||||
|
Links: c.Links,
|
||||||
Tags: c.Tags.ToResponse(categories),
|
Tags: c.Tags.ToResponse(categories),
|
||||||
IsVerified: c.IsVerified,
|
IsVerified: c.IsVerified,
|
||||||
IsCompliant: c.IsCompliant,
|
IsCompliant: c.IsCompliant,
|
||||||
@@ -208,8 +222,9 @@ type CompanyProfileResponse struct {
|
|||||||
RegistrationNumber string `json:"registration_number"`
|
RegistrationNumber string `json:"registration_number"`
|
||||||
Industry string `json:"industry"`
|
Industry string `json:"industry"`
|
||||||
FoundedYear *int `json:"founded_year,omitempty"`
|
FoundedYear *int `json:"founded_year,omitempty"`
|
||||||
DocumentFileIDs []string `json:"document_file_ids,omitempty"`
|
DocumentFileIDs []string `json:"document_file_ids,omitempty"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Links []CompanyLink `json:"links,omitempty"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToResponse converts Company to CompanyResponse
|
// ToResponse converts Company to CompanyResponse
|
||||||
@@ -221,6 +236,7 @@ func (c *Company) ToProfileResponse() *CompanyProfileResponse {
|
|||||||
Industry: c.Industry,
|
Industry: c.Industry,
|
||||||
FoundedYear: c.FoundedYear,
|
FoundedYear: c.FoundedYear,
|
||||||
DocumentFileIDs: c.DocumentFileIDs,
|
DocumentFileIDs: c.DocumentFileIDs,
|
||||||
|
Links: c.Links,
|
||||||
CreatedAt: c.CreatedAt,
|
CreatedAt: c.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,9 @@ func (h *Handler) Create(c echo.Context) error {
|
|||||||
err.Error() == "failed to validate categories" {
|
err.Error() == "failed to validate categories" {
|
||||||
return response.BadRequest(c, err.Error(), "Invalid category data")
|
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.InternalServerError(c, "Failed to create company")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +241,9 @@ func (h *Handler) Update(c echo.Context) error {
|
|||||||
err.Error() == "failed to validate categories" {
|
err.Error() == "failed to validate categories" {
|
||||||
return response.BadRequest(c, err.Error(), "Invalid category data")
|
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.InternalServerError(c, "Failed to update company")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +461,54 @@ func (h *Handler) UploadDocuments(c echo.Context) error {
|
|||||||
return response.Success(c, result, message)
|
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 {
|
func collectCompanyUploadFiles(form *multipart.Form) []*multipart.FileHeader {
|
||||||
if form == nil {
|
if form == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package company
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxCompanyLinks = 20
|
||||||
|
|
||||||
|
// AddLinksResponse is returned after appending links to a company.
|
||||||
|
type AddLinksResponse struct {
|
||||||
|
Links []CompanyLink `json:"links"`
|
||||||
|
Added []CompanyLink `json:"added"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLinks appends validated links to a company.
|
||||||
|
func (s *companyService) AddLinks(ctx context.Context, companyID string, form *AddLinksForm) (*AddLinksResponse, error) {
|
||||||
|
if form == nil || len(form.Links) == 0 {
|
||||||
|
return nil, errors.New("no links provided")
|
||||||
|
}
|
||||||
|
if len(form.Links) > maxCompanyLinks {
|
||||||
|
return nil, errors.New("too many links in request")
|
||||||
|
}
|
||||||
|
|
||||||
|
company, err := s.repository.GetByID(ctx, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("company not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
newLinks, err := sanitizeCompanyLinks(convertCompanyLinkForms(form.Links))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(newLinks) == 0 {
|
||||||
|
return nil, errors.New("no valid links provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
existing := sanitizeStoredCompanyLinks(company.Links)
|
||||||
|
merged, added := mergeCompanyLinks(existing, newLinks)
|
||||||
|
if len(merged) > maxCompanyLinks {
|
||||||
|
return nil, errors.New("company link limit exceeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
company.Links = merged
|
||||||
|
if err := s.repository.Update(ctx, company); err != nil {
|
||||||
|
s.logger.Error("Failed to save company links", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"company_id": companyID,
|
||||||
|
})
|
||||||
|
return nil, errors.New("failed to update company links")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Company links added", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"added_count": len(added),
|
||||||
|
"links_total": len(company.Links),
|
||||||
|
})
|
||||||
|
|
||||||
|
s.triggerAIOnboardingAsync(companyID)
|
||||||
|
|
||||||
|
return &AddLinksResponse{
|
||||||
|
Links: company.Links,
|
||||||
|
Added: added,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertCompanyLinkForms(forms []CompanyLinkForm) []CompanyLink {
|
||||||
|
if len(forms) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
links := make([]CompanyLink, 0, len(forms))
|
||||||
|
for _, form := range forms {
|
||||||
|
links = append(links, CompanyLink{
|
||||||
|
URL: form.URL,
|
||||||
|
Title: form.Title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeCompanyLinks(links []CompanyLink) ([]CompanyLink, error) {
|
||||||
|
if len(links) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized := make([]CompanyLink, 0, len(links))
|
||||||
|
for _, link := range links {
|
||||||
|
url := strings.TrimSpace(link.URL)
|
||||||
|
if url == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !govalidator.IsURL(url) {
|
||||||
|
return nil, errors.New("invalid link URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
var title *string
|
||||||
|
if link.Title != nil {
|
||||||
|
trimmed := strings.TrimSpace(*link.Title)
|
||||||
|
if trimmed != "" {
|
||||||
|
title = &trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized = append(sanitized, CompanyLink{
|
||||||
|
URL: url,
|
||||||
|
Title: title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return sanitized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeStoredCompanyLinks(links []CompanyLink) []CompanyLink {
|
||||||
|
sanitized, err := sanitizeCompanyLinks(links)
|
||||||
|
if err != nil || len(sanitized) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeCompanyLinks(existing, additional []CompanyLink) ([]CompanyLink, []CompanyLink) {
|
||||||
|
seen := make(map[string]struct{}, len(existing)+len(additional))
|
||||||
|
merged := make([]CompanyLink, 0, len(existing)+len(additional))
|
||||||
|
added := make([]CompanyLink, 0, len(additional))
|
||||||
|
|
||||||
|
appendUnique := func(items []CompanyLink, trackAdded bool) {
|
||||||
|
for _, item := range items {
|
||||||
|
key := strings.ToLower(strings.TrimSpace(item.URL))
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
merged = append(merged, item)
|
||||||
|
if trackAdded {
|
||||||
|
added = append(added, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendUnique(existing, false)
|
||||||
|
appendUnique(additional, true)
|
||||||
|
return merged, added
|
||||||
|
}
|
||||||
|
|
||||||
|
func companyLinkURLs(links []CompanyLink) []string {
|
||||||
|
if len(links) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
urls := make([]string, 0, len(links))
|
||||||
|
for _, link := range links {
|
||||||
|
url := strings.TrimSpace(link.URL)
|
||||||
|
if url != "" {
|
||||||
|
urls = append(urls, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package company
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestMergeCompanyLinks(t *testing.T) {
|
||||||
|
existing := []CompanyLink{
|
||||||
|
{URL: "https://example.com/a"},
|
||||||
|
{URL: "https://example.com/b"},
|
||||||
|
}
|
||||||
|
additional := []CompanyLink{
|
||||||
|
{URL: " https://example.com/b "},
|
||||||
|
{URL: "https://Example.com/C"},
|
||||||
|
{URL: " "},
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, added := mergeCompanyLinks(existing, additional)
|
||||||
|
if len(merged) != 3 {
|
||||||
|
t.Fatalf("mergeCompanyLinks() merged len = %d, want 3", len(merged))
|
||||||
|
}
|
||||||
|
if len(added) != 1 {
|
||||||
|
t.Fatalf("mergeCompanyLinks() added len = %d, want 1", len(added))
|
||||||
|
}
|
||||||
|
if added[0].URL != "https://Example.com/C" {
|
||||||
|
t.Fatalf("added link = %+v, want https://Example.com/C", added[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeCompanyLinks(t *testing.T) {
|
||||||
|
title := " Profile "
|
||||||
|
got, err := sanitizeCompanyLinks([]CompanyLink{
|
||||||
|
{URL: " https://example.com ", Title: &title},
|
||||||
|
{URL: "not-a-url"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("sanitizeCompanyLinks() expected invalid URL error")
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err = sanitizeCompanyLinks([]CompanyLink{
|
||||||
|
{URL: "https://example.com", Title: &title},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sanitizeCompanyLinks() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("sanitizeCompanyLinks() len = %d, want 1", len(got))
|
||||||
|
}
|
||||||
|
if got[0].URL != "https://example.com" {
|
||||||
|
t.Fatalf("sanitized URL = %q, want https://example.com", got[0].URL)
|
||||||
|
}
|
||||||
|
if got[0].Title == nil || *got[0].Title != "Profile" {
|
||||||
|
t.Fatalf("sanitized title = %+v, want Profile", got[0].Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompanyLinkURLs(t *testing.T) {
|
||||||
|
got := companyLinkURLs([]CompanyLink{
|
||||||
|
{URL: " https://a.test "},
|
||||||
|
{URL: " "},
|
||||||
|
{URL: "https://b.test"},
|
||||||
|
})
|
||||||
|
want := []string{"https://a.test", "https://b.test"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("companyLinkURLs() len = %d, want %d", len(got), len(want))
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Fatalf("companyLinkURLs()[%d] = %q, want %q", i, got[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -171,6 +171,7 @@ func (s *companyService) startAIOnboardingInternal(ctx context.Context, companyI
|
|||||||
s.logger.Info("Starting AI company onboarding", map[string]interface{}{
|
s.logger.Info("Starting AI company onboarding", map[string]interface{}{
|
||||||
"company_id": companyID,
|
"company_id": companyID,
|
||||||
"documents_count": len(company.DocumentFileIDs),
|
"documents_count": len(company.DocumentFileIDs),
|
||||||
|
"links_count": len(company.Links),
|
||||||
"has_website_url": websiteURL != "",
|
"has_website_url": websiteURL != "",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -178,6 +179,7 @@ func (s *companyService) startAIOnboardingInternal(ctx context.Context, companyI
|
|||||||
CompanyID: companyID,
|
CompanyID: companyID,
|
||||||
Documents: documents,
|
Documents: documents,
|
||||||
WebsiteURL: websiteURL,
|
WebsiteURL: websiteURL,
|
||||||
|
Links: companyLinkURLs(company.Links),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("AI onboarding request failed", map[string]interface{}{
|
s.logger.Error("AI onboarding request failed", map[string]interface{}{
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ func (r *companyRepository) Update(ctx context.Context, company *Company) error
|
|||||||
company.SetUpdatedAt(time.Now().Unix())
|
company.SetUpdatedAt(time.Now().Unix())
|
||||||
|
|
||||||
clearDocuments := company.DocumentFileIDs != nil && len(company.DocumentFileIDs) == 0
|
clearDocuments := company.DocumentFileIDs != nil && len(company.DocumentFileIDs) == 0
|
||||||
|
clearLinks := company.Links != nil && len(company.Links) == 0
|
||||||
|
|
||||||
// Use ORM to update company
|
// Use ORM to update company
|
||||||
err := r.ormRepo.Update(ctx, company)
|
err := r.ormRepo.Update(ctx, company)
|
||||||
@@ -181,16 +182,23 @@ func (r *companyRepository) Update(ctx context.Context, company *Company) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// BSON omitempty can skip empty slices in $set, so persist an explicit empty array.
|
// BSON omitempty can skip empty slices in $set, so persist explicit empty arrays.
|
||||||
if clearDocuments {
|
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{
|
_, err = r.collection.UpdateOne(ctx, bson.M{"_id": company.ID}, bson.M{
|
||||||
"$set": bson.M{
|
"$set": setFields,
|
||||||
"document_file_ids": []string{},
|
|
||||||
"updated_at": company.UpdatedAt,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to clear company document file IDs", map[string]interface{}{
|
r.logger.Error("Failed to clear company collection fields", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"company_id": company.ID,
|
"company_id": company.ID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ type Service interface {
|
|||||||
// UploadDocuments uploads files and attaches them to a company
|
// UploadDocuments uploads files and attaches them to a company
|
||||||
UploadDocuments(ctx context.Context, companyID, uploadedBy string, files []*multipart.FileHeader) (*UploadDocumentsResponse, error)
|
UploadDocuments(ctx context.Context, companyID, uploadedBy string, files []*multipart.FileHeader) (*UploadDocumentsResponse, error)
|
||||||
|
|
||||||
|
// AddLinks appends external URLs to a company
|
||||||
|
AddLinks(ctx context.Context, companyID string, form *AddLinksForm) (*AddLinksResponse, error)
|
||||||
|
|
||||||
// DetachDocumentFileID removes a file ID from companies when the file is deleted
|
// DetachDocumentFileID removes a file ID from companies when the file is deleted
|
||||||
DetachDocumentFileID(ctx context.Context, fileID string) error
|
DetachDocumentFileID(ctx context.Context, fileID string) error
|
||||||
|
|
||||||
@@ -118,6 +121,14 @@ func (s *companyService) Create(ctx context.Context, form *CompanyForm) (*Compan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
links, err := sanitizeCompanyLinks(convertCompanyLinkForms(form.Links))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(links) > maxCompanyLinks {
|
||||||
|
return nil, errors.New("too many links provided")
|
||||||
|
}
|
||||||
|
|
||||||
// Create company
|
// Create company
|
||||||
company := &Company{
|
company := &Company{
|
||||||
Name: form.Name,
|
Name: form.Name,
|
||||||
@@ -135,6 +146,7 @@ func (s *companyService) Create(ctx context.Context, form *CompanyForm) (*Compan
|
|||||||
Phone: form.Phone,
|
Phone: form.Phone,
|
||||||
Email: form.Email,
|
Email: form.Email,
|
||||||
DocumentFileIDs: s.sanitizeDocumentFileIDs(form.DocumentFileIDs),
|
DocumentFileIDs: s.sanitizeDocumentFileIDs(form.DocumentFileIDs),
|
||||||
|
Links: links,
|
||||||
Tags: s.convertCompanyTagsForm(form.Tags),
|
Tags: s.convertCompanyTagsForm(form.Tags),
|
||||||
IsVerified: false,
|
IsVerified: false,
|
||||||
IsCompliant: false,
|
IsCompliant: false,
|
||||||
@@ -144,7 +156,7 @@ func (s *companyService) Create(ctx context.Context, form *CompanyForm) (*Compan
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
err := s.repository.Create(ctx, company)
|
err = s.repository.Create(ctx, company)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to create company", map[string]interface{}{
|
s.logger.Error("Failed to create company", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -304,6 +316,17 @@ func (s *companyService) Update(ctx context.Context, id string, form *CompanyFor
|
|||||||
company.DocumentFileIDs = s.sanitizeDocumentFileIDs(form.DocumentFileIDs)
|
company.DocumentFileIDs = s.sanitizeDocumentFileIDs(form.DocumentFileIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if form.Links != nil {
|
||||||
|
links, linkErr := sanitizeCompanyLinks(convertCompanyLinkForms(form.Links))
|
||||||
|
if linkErr != nil {
|
||||||
|
return nil, linkErr
|
||||||
|
}
|
||||||
|
if len(links) > maxCompanyLinks {
|
||||||
|
return nil, errors.New("too many links provided")
|
||||||
|
}
|
||||||
|
company.Links = links
|
||||||
|
}
|
||||||
|
|
||||||
if form.Tags != nil {
|
if form.Tags != nil {
|
||||||
company.Tags = s.convertCompanyTagsForm(form.Tags)
|
company.Tags = s.convertCompanyTagsForm(form.Tags)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,9 +249,10 @@ type PipelineMinioStatsResponse map[string]interface{}
|
|||||||
|
|
||||||
// OnboardingRequest is the payload for POST /onboarding.
|
// OnboardingRequest is the payload for POST /onboarding.
|
||||||
type OnboardingRequest struct {
|
type OnboardingRequest struct {
|
||||||
CompanyID string `json:"company_id"`
|
CompanyID string `json:"company_id"`
|
||||||
Documents string `json:"documents"`
|
Documents string `json:"documents"`
|
||||||
WebsiteURL string `json:"website_url"`
|
WebsiteURL string `json:"website_url"`
|
||||||
|
Links []string `json:"links,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnboardingResponse is returned by POST /onboarding.
|
// OnboardingResponse is returned by POST /onboarding.
|
||||||
|
|||||||
Reference in New Issue
Block a user