package company import ( "context" "errors" "mime/multipart" "tm/pkg/filestore" ) const ( companyDocumentCategory = "company_document" maxCompanyDocumentsUpload = 20 ) // UploadDocumentsResponse is returned after uploading company documents. type UploadDocumentsResponse struct { DocumentFileIDs []string `json:"document_file_ids"` Uploaded []filestore.UploadedFileResult `json:"uploaded"` Failed []filestore.FailedUploadResult `json:"failed,omitempty"` } // UploadDocuments uploads one or more files and appends their IDs to the company. func (s *companyService) UploadDocuments(ctx context.Context, companyID, uploadedBy string, files []*multipart.FileHeader) (*UploadDocumentsResponse, error) { if s.fileStore == nil { return nil, errors.New("file storage is not configured") } if len(files) == 0 { return nil, errors.New("no files provided") } if len(files) > maxCompanyDocumentsUpload { return nil, errors.New("too many files in request") } company, err := s.repository.GetByID(ctx, companyID) if err != nil { return nil, errors.New("company not found") } opts := filestore.UploadOptions{ UploadedBy: uploadedBy, Category: companyDocumentCategory, MaxSize: filestore.MaxUploadSize, Extra: map[string]string{ "company_id": companyID, }, } uploaded := make([]filestore.UploadedFileResult, 0, len(files)) failed := make([]filestore.FailedUploadResult, 0) newIDs := make([]string, 0, len(files)) for _, file := range files { result, uploadErr := filestore.UploadFromMultipartFile(s.fileStore, file, opts) if uploadErr != nil { code := filestore.ErrCodeUploadFailed var fsErr *filestore.FileStoreError if errors.As(uploadErr, &fsErr) { code = fsErr.Code } failed = append(failed, filestore.FailedUploadResult{ Filename: file.Filename, Error: uploadErr.Error(), Code: code, }) continue } uploaded = append(uploaded, result) newIDs = append(newIDs, result.FileID) } if len(uploaded) == 0 { s.logger.Warn("Company document upload failed for all files", map[string]interface{}{ "company_id": companyID, "file_count": len(files), }) return &UploadDocumentsResponse{ DocumentFileIDs: s.sanitizeDocumentFileIDs(company.DocumentFileIDs), Uploaded: uploaded, Failed: failed, }, errors.New("all file uploads failed") } company.DocumentFileIDs = s.sanitizeDocumentFileIDs(company.DocumentFileIDs) company.DocumentFileIDs = mergeDocumentFileIDs(company.DocumentFileIDs, newIDs) if err := s.repository.Update(ctx, company); err != nil { s.logger.Error("Failed to save company documents", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return nil, errors.New("failed to update company documents") } s.logger.Info("Company documents uploaded", map[string]interface{}{ "company_id": companyID, "uploaded_count": len(uploaded), "failed_count": len(failed), "documents_total": len(company.DocumentFileIDs), }) s.triggerAIOnboardingAsync(companyID) return &UploadDocumentsResponse{ DocumentFileIDs: company.DocumentFileIDs, Uploaded: uploaded, Failed: failed, }, nil } func mergeDocumentFileIDs(existing, additional []string) []string { seen := make(map[string]struct{}, len(existing)+len(additional)) merged := make([]string, 0, len(existing)+len(additional)) appendUnique := func(ids []string) { for _, id := range ids { if id == "" { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} merged = append(merged, id) } } appendUnique(existing) appendUnique(additional) return merged } // DetachDocumentFileID removes a file ID from every company that still references it. func (s *companyService) DetachDocumentFileID(ctx context.Context, fileID string) error { if err := s.repository.RemoveDocumentFileID(ctx, fileID); err != nil { s.logger.Error("Failed to detach document file ID from companies", map[string]interface{}{ "error": err.Error(), "file_id": fileID, }) return err } return nil } func (s *companyService) sanitizeDocumentFileIDs(ids []string) []string { if len(ids) == 0 || s.fileStore == nil { return ids } sanitized := make([]string, 0, len(ids)) for _, id := range ids { if id == "" { continue } exists, err := s.fileStore.Exists(id) if err != nil { s.logger.Warn("Failed to verify company document file", map[string]interface{}{ "error": err.Error(), "file_id": id, }) continue } if exists { sanitized = append(sanitized, id) } } return sanitized }