Implement multi-file upload functionality for companies and enhance file upload routes
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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: company.DocumentFileIDs,
|
||||
Uploaded: uploaded,
|
||||
Failed: failed,
|
||||
}, errors.New("all file uploads failed")
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
@@ -22,9 +22,8 @@ type (
|
||||
Address *AddressForm `json:"address,omitempty"`
|
||||
|
||||
// Contact information
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
||||
Email *string `json:"email,omitempty" valid:"optional,email" example:"info@opplens.com"`
|
||||
// GridFS document references (uploaded via /api/v1/files/upload or /admin/v1/files/upload)
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
||||
Email *string `json:"email,omitempty" valid:"optional,email" example:"info@opplens.com"`
|
||||
DocumentFileIDs []string `json:"document_file_ids,omitempty" valid:"optional"`
|
||||
|
||||
// Tags for categorization and search
|
||||
@@ -188,13 +187,13 @@ func (c *CompanyTags) ToResponse(categories []*CategoryResponse) *CompanyTagsRes
|
||||
|
||||
// CompanyProfileResponse represents the company profile data sent in API responses
|
||||
type CompanyProfileResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RegistrationNumber string `json:"registration_number"`
|
||||
Industry string `json:"industry"`
|
||||
FoundedYear *int `json:"founded_year,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RegistrationNumber string `json:"registration_number"`
|
||||
Industry string `json:"industry"`
|
||||
FoundedYear *int `json:"founded_year,omitempty"`
|
||||
DocumentFileIDs []string `json:"document_file_ids,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// ToResponse converts Company to CompanyResponse
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package company
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
"tm/internal/user"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
@@ -307,3 +309,70 @@ func (h *Handler) UpdateVerification(c echo.Context) error {
|
||||
"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"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"mime/multipart"
|
||||
|
||||
"tm/internal/company_category"
|
||||
"tm/pkg/filestore"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
@@ -28,20 +31,25 @@ type Service interface {
|
||||
|
||||
// Get my company
|
||||
GetMyCompany(ctx context.Context, id string) (*CompanyProfileResponse, error)
|
||||
|
||||
// UploadDocuments uploads files and attaches them to a company
|
||||
UploadDocuments(ctx context.Context, companyID, uploadedBy string, files []*multipart.FileHeader) (*UploadDocumentsResponse, error)
|
||||
}
|
||||
|
||||
// companyService implements the Service interface
|
||||
type companyService struct {
|
||||
repository Repository
|
||||
categoryService company_category.Service
|
||||
fileStore filestore.FileStoreService
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new company service
|
||||
func NewService(repository Repository, categoryService company_category.Service, logger logger.Logger) Service {
|
||||
func NewService(repository Repository, categoryService company_category.Service, fileStore filestore.FileStoreService, logger logger.Logger) Service {
|
||||
return &companyService{
|
||||
repository: repository,
|
||||
categoryService: categoryService,
|
||||
fileStore: fileStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user