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 }