package services import ( "errors" "gorm.io/gorm" "github.com/treytartt/mycrib-api/internal/dto/requests" "github.com/treytartt/mycrib-api/internal/dto/responses" "github.com/treytartt/mycrib-api/internal/models" "github.com/treytartt/mycrib-api/internal/repositories" ) // Document-related errors 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, ErrDocumentNotFound } return nil, err } // Check access via residence hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrDocumentAccessDenied } resp := responses.NewDocumentResponse(document) return &resp, nil } // ListDocuments lists all documents accessible to a user func (s *DocumentService) ListDocuments(userID uint) (*responses.DocumentListResponse, error) { residences, err := s.residenceRepo.FindByUser(userID) if err != nil { return nil, err } residenceIDs := make([]uint, len(residences)) for i, r := range residences { residenceIDs[i] = r.ID } if len(residenceIDs) == 0 { return &responses.DocumentListResponse{Count: 0, Results: []responses.DocumentResponse{}}, nil } documents, err := s.documentRepo.FindByUser(residenceIDs) if err != nil { return nil, err } resp := responses.NewDocumentListResponse(documents) return &resp, nil } // ListWarranties lists all warranty documents func (s *DocumentService) ListWarranties(userID uint) (*responses.DocumentListResponse, error) { residences, err := s.residenceRepo.FindByUser(userID) if err != nil { return nil, err } residenceIDs := make([]uint, len(residences)) for i, r := range residences { residenceIDs[i] = r.ID } if len(residenceIDs) == 0 { return &responses.DocumentListResponse{Count: 0, Results: []responses.DocumentResponse{}}, nil } documents, err := s.documentRepo.FindWarranties(residenceIDs) if err != nil { return nil, err } resp := responses.NewDocumentListResponse(documents) return &resp, 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, err } if !hasAccess { return nil, ErrResidenceAccessDenied } 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, err } // Reload with relations document, err = s.documentRepo.FindByID(document.ID) if err != nil { return nil, 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, ErrDocumentNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrDocumentAccessDenied } // 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, err } // Reload document, err = s.documentRepo.FindByID(documentID) if err != nil { return nil, 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 ErrDocumentNotFound } return err } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return err } if !hasAccess { return ErrDocumentAccessDenied } return s.documentRepo.Delete(documentID) } // 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, ErrDocumentNotFound } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrDocumentAccessDenied } if err := s.documentRepo.Activate(documentID); err != nil { return nil, err } // Reload doc, err := s.documentRepo.FindByID(documentID) if err != nil { return nil, 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, ErrDocumentNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrDocumentAccessDenied } if err := s.documentRepo.Deactivate(documentID); err != nil { return nil, err } document.IsActive = false resp := responses.NewDocumentResponse(document) return &resp, nil }