Files
tm_back/internal/company/documents.go
T
Mazyar a2661651c9
continuous-integration/drone/push Build is passing
Enhance company service with AI recommendation caching and onboarding improvements
- Updated the `companyService` to include Redis caching for AI recommendations, improving performance and reducing redundant AI calls.
- Introduced asynchronous AI onboarding triggered after company profile updates, enhancing user experience by offloading processing.
- Added configuration for recommendation cache TTL in the `AISummarizerConfig`, allowing for flexible cache management.
- Implemented methods for caching, retrieving, and invalidating AI recommendations in the `companyService`, ensuring efficient data handling.

This update enhances the company's AI recommendation capabilities, providing faster responses and a more efficient onboarding process.
2026-06-23 13:24:45 +03:30

168 lines
4.6 KiB
Go

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
}