package services import ( "errors" "gorm.io/gorm" "github.com/treytartt/casera-api/internal/apperrors" "github.com/treytartt/casera-api/internal/dto/requests" "github.com/treytartt/casera-api/internal/dto/responses" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/repositories" ) // Document-related errors // DEPRECATED: These constants are deprecated. Use apperrors package instead. // var ( // ErrDocumentNotFound = errors.New("document not found") // ErrDocumentAccessDenied = errors.New("you do not have access to this document") // ) // DocumentService handles document business logic type DocumentService struct { documentRepo *repositories.DocumentRepository residenceRepo *repositories.ResidenceRepository } // NewDocumentService creates a new document service func NewDocumentService(documentRepo *repositories.DocumentRepository, residenceRepo *repositories.ResidenceRepository) *DocumentService { return &DocumentService{ documentRepo: documentRepo, residenceRepo: residenceRepo, } } // GetDocument gets a document by ID with access check func (s *DocumentService) GetDocument(documentID, userID uint) (*responses.DocumentResponse, error) { document, err := s.documentRepo.FindByID(documentID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.document_not_found") } return nil, apperrors.Internal(err) } // Check access via residence hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } if !hasAccess { return nil, apperrors.Forbidden("error.document_access_denied") } resp := responses.NewDocumentResponse(document) return &resp, nil } // ListDocuments lists all documents accessible to a user, with optional filters. func (s *DocumentService) ListDocuments(userID uint, filter *repositories.DocumentFilter) ([]responses.DocumentResponse, error) { // Get residence IDs (lightweight - no preloads) residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) if err != nil { return nil, apperrors.Internal(err) } if len(residenceIDs) == 0 { return []responses.DocumentResponse{}, nil } // If a specific residence filter is set, narrow to that single residence (if user has access) if filter != nil && filter.ResidenceID != nil { found := false for _, rid := range residenceIDs { if rid == *filter.ResidenceID { found = true break } } if !found { return nil, apperrors.Forbidden("error.residence_access_denied") } residenceIDs = []uint{*filter.ResidenceID} } documents, err := s.documentRepo.FindByUserFiltered(residenceIDs, filter) if err != nil { return nil, apperrors.Internal(err) } return responses.NewDocumentListResponse(documents), nil } // ListWarranties lists all warranty documents func (s *DocumentService) ListWarranties(userID uint) ([]responses.DocumentResponse, error) { // Get residence IDs (lightweight - no preloads) residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) if err != nil { return nil, apperrors.Internal(err) } if len(residenceIDs) == 0 { return []responses.DocumentResponse{}, nil } documents, err := s.documentRepo.FindWarranties(residenceIDs) if err != nil { return nil, apperrors.Internal(err) } return responses.NewDocumentListResponse(documents), nil } // CreateDocument creates a new document func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, userID uint) (*responses.DocumentResponse, error) { // Check residence access hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } if !hasAccess { return nil, apperrors.Forbidden("error.residence_access_denied") } documentType := req.DocumentType if documentType == "" { documentType = models.DocumentTypeGeneral } document := &models.Document{ ResidenceID: req.ResidenceID, CreatedByID: userID, Title: req.Title, Description: req.Description, DocumentType: documentType, FileURL: req.FileURL, FileName: req.FileName, FileSize: req.FileSize, MimeType: req.MimeType, PurchaseDate: req.PurchaseDate, ExpiryDate: req.ExpiryDate, PurchasePrice: req.PurchasePrice, Vendor: req.Vendor, SerialNumber: req.SerialNumber, ModelNumber: req.ModelNumber, TaskID: req.TaskID, IsActive: true, } if err := s.documentRepo.Create(document); err != nil { return nil, apperrors.Internal(err) } // Create images if provided for _, imageURL := range req.ImageURLs { if imageURL != "" { img := &models.DocumentImage{ DocumentID: document.ID, ImageURL: imageURL, } if err := s.documentRepo.CreateDocumentImage(img); err != nil { // Log but don't fail the whole operation continue } } } // Reload with relations document, err = s.documentRepo.FindByID(document.ID) if err != nil { return nil, apperrors.Internal(err) } resp := responses.NewDocumentResponse(document) return &resp, nil } // UpdateDocument updates a document func (s *DocumentService) UpdateDocument(documentID, userID uint, req *requests.UpdateDocumentRequest) (*responses.DocumentResponse, error) { document, err := s.documentRepo.FindByID(documentID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.document_not_found") } return nil, apperrors.Internal(err) } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } if !hasAccess { return nil, apperrors.Forbidden("error.document_access_denied") } // Apply updates if req.Title != nil { document.Title = *req.Title } if req.Description != nil { document.Description = *req.Description } if req.DocumentType != nil { document.DocumentType = *req.DocumentType } if req.FileURL != nil { document.FileURL = *req.FileURL } if req.FileName != nil { document.FileName = *req.FileName } if req.FileSize != nil { document.FileSize = req.FileSize } if req.MimeType != nil { document.MimeType = *req.MimeType } if req.PurchaseDate != nil { document.PurchaseDate = req.PurchaseDate } if req.ExpiryDate != nil { document.ExpiryDate = req.ExpiryDate } if req.PurchasePrice != nil { document.PurchasePrice = req.PurchasePrice } if req.Vendor != nil { document.Vendor = *req.Vendor } if req.SerialNumber != nil { document.SerialNumber = *req.SerialNumber } if req.ModelNumber != nil { document.ModelNumber = *req.ModelNumber } if req.TaskID != nil { document.TaskID = req.TaskID } if err := s.documentRepo.Update(document); err != nil { return nil, apperrors.Internal(err) } // Reload document, err = s.documentRepo.FindByID(documentID) if err != nil { return nil, apperrors.Internal(err) } resp := responses.NewDocumentResponse(document) return &resp, nil } // DeleteDocument soft-deletes a document func (s *DocumentService) DeleteDocument(documentID, userID uint) error { document, err := s.documentRepo.FindByID(documentID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return apperrors.NotFound("error.document_not_found") } return apperrors.Internal(err) } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return apperrors.Internal(err) } if !hasAccess { return apperrors.Forbidden("error.document_access_denied") } if err := s.documentRepo.Delete(documentID); err != nil { return apperrors.Internal(err) } return nil } // ActivateDocument activates a document func (s *DocumentService) ActivateDocument(documentID, userID uint) (*responses.DocumentResponse, error) { // First check if document exists (even if inactive) var document models.Document if err := s.documentRepo.FindByIDIncludingInactive(documentID, &document); err != nil { return nil, apperrors.NotFound("error.document_not_found") } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } if !hasAccess { return nil, apperrors.Forbidden("error.document_access_denied") } if err := s.documentRepo.Activate(documentID); err != nil { return nil, apperrors.Internal(err) } // Reload doc, err := s.documentRepo.FindByID(documentID) if err != nil { return nil, apperrors.Internal(err) } resp := responses.NewDocumentResponse(doc) return &resp, nil } // DeactivateDocument deactivates a document func (s *DocumentService) DeactivateDocument(documentID, userID uint) (*responses.DocumentResponse, error) { document, err := s.documentRepo.FindByID(documentID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.document_not_found") } return nil, apperrors.Internal(err) } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } if !hasAccess { return nil, apperrors.Forbidden("error.document_access_denied") } if err := s.documentRepo.Deactivate(documentID); err != nil { return nil, apperrors.Internal(err) } document.IsActive = false resp := responses.NewDocumentResponse(document) return &resp, nil } // UploadDocumentImage adds an image to an existing document func (s *DocumentService) UploadDocumentImage(documentID, userID uint, imageURL, caption string) (*responses.DocumentResponse, error) { document, err := s.documentRepo.FindByID(documentID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.document_not_found") } return nil, apperrors.Internal(err) } // Check access via residence hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } if !hasAccess { return nil, apperrors.Forbidden("error.document_access_denied") } img := &models.DocumentImage{ DocumentID: documentID, ImageURL: imageURL, Caption: caption, } if err := s.documentRepo.CreateDocumentImage(img); err != nil { return nil, apperrors.Internal(err) } // Reload with relations document, err = s.documentRepo.FindByID(documentID) if err != nil { return nil, apperrors.Internal(err) } resp := responses.NewDocumentResponse(document) return &resp, nil } // DeleteDocumentImage removes an image from a document func (s *DocumentService) DeleteDocumentImage(documentID, imageID, userID uint) (*responses.DocumentResponse, error) { // Find the image first image, err := s.documentRepo.FindImageByID(imageID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.document_image_not_found") } return nil, apperrors.Internal(err) } // Verify image belongs to the specified document if image.DocumentID != documentID { return nil, apperrors.NotFound("error.document_image_not_found") } // Find parent document to check access document, err := s.documentRepo.FindByID(documentID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.document_not_found") } return nil, apperrors.Internal(err) } // Check access via residence hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } if !hasAccess { return nil, apperrors.Forbidden("error.document_access_denied") } if err := s.documentRepo.DeleteDocumentImage(imageID); err != nil { return nil, apperrors.Internal(err) } // Reload with relations document, err = s.documentRepo.FindByID(documentID) if err != nil { return nil, apperrors.Internal(err) } resp := responses.NewDocumentResponse(document) return &resp, nil }