package repositories import ( "time" "gorm.io/gorm" "github.com/treytartt/honeydue-api/internal/models" ) // DocumentFilter contains optional filter parameters for listing documents. type DocumentFilter struct { ResidenceID *uint DocumentType string IsActive *bool ExpiringSoon *int Search string } // DocumentRepository handles database operations for documents type DocumentRepository struct { db *gorm.DB } // NewDocumentRepository creates a new document repository func NewDocumentRepository(db *gorm.DB) *DocumentRepository { return &DocumentRepository{db: db} } // FindByID finds a document by ID with preloaded relations func (r *DocumentRepository) FindByID(id uint) (*models.Document, error) { var document models.Document err := r.db.Preload("CreatedBy"). Preload("Task"). Preload("Images"). Where("id = ? AND is_active = ?", id, true). First(&document).Error if err != nil { return nil, err } return &document, nil } // FindByResidence finds all documents for a residence func (r *DocumentRepository) FindByResidence(residenceID uint) ([]models.Document, error) { var documents []models.Document err := r.db.Preload("CreatedBy"). Preload("Images"). Where("residence_id = ? AND is_active = ?", residenceID, true). Order("created_at DESC"). Find(&documents).Error return documents, err } // FindByUser finds all documents accessible to a user. // A default limit of 500 is applied to prevent unbounded result sets. func (r *DocumentRepository) FindByUser(residenceIDs []uint) ([]models.Document, error) { var documents []models.Document err := r.db.Preload("CreatedBy"). Preload("Residence"). Preload("Images"). Where("residence_id IN ? AND is_active = ?", residenceIDs, true). Order("created_at DESC"). Limit(500). Find(&documents).Error return documents, err } // FindByUserFiltered finds documents accessible to a user with optional filters. func (r *DocumentRepository) FindByUserFiltered(residenceIDs []uint, filter *DocumentFilter) ([]models.Document, error) { query := r.db.Preload("CreatedBy"). Preload("Residence"). Preload("Images"). Where("residence_id IN ?", residenceIDs) // Default behavior is active-only unless explicitly overridden. if filter == nil || filter.IsActive == nil { query = query.Where("is_active = ?", true) } if filter != nil { if filter.DocumentType != "" { query = query.Where("document_type = ?", filter.DocumentType) } if filter.IsActive != nil { query = query.Where("is_active = ?", *filter.IsActive) } if filter.ExpiringSoon != nil { now := time.Now().UTC() threshold := now.AddDate(0, 0, *filter.ExpiringSoon) query = query.Where("expiry_date IS NOT NULL AND expiry_date > ? AND expiry_date <= ?", now, threshold) } if filter.Search != "" { escaped := escapeLikeWildcards(filter.Search) searchPattern := "%" + escaped + "%" query = query.Where("(title ILIKE ? OR description ILIKE ?)", searchPattern, searchPattern) } } var documents []models.Document err := query.Order("created_at DESC").Limit(500).Find(&documents).Error return documents, err } // FindWarranties finds all warranty documents func (r *DocumentRepository) FindWarranties(residenceIDs []uint) ([]models.Document, error) { var documents []models.Document err := r.db.Preload("CreatedBy"). Preload("Residence"). Preload("Images"). Where("residence_id IN ? AND is_active = ? AND document_type = ?", residenceIDs, true, models.DocumentTypeWarranty). Order("expiry_date ASC NULLS LAST"). Find(&documents).Error return documents, err } // FindExpiringWarranties finds warranties expiring within the specified days func (r *DocumentRepository) FindExpiringWarranties(residenceIDs []uint, days int) ([]models.Document, error) { threshold := time.Now().UTC().AddDate(0, 0, days) now := time.Now().UTC() var documents []models.Document err := r.db.Preload("CreatedBy"). Preload("Residence"). Preload("Images"). Where("residence_id IN ? AND is_active = ? AND document_type = ? AND expiry_date > ? AND expiry_date <= ?", residenceIDs, true, models.DocumentTypeWarranty, now, threshold). Order("expiry_date ASC"). Find(&documents).Error return documents, err } // Create creates a new document func (r *DocumentRepository) Create(document *models.Document) error { return r.db.Create(document).Error } // Update updates a document // Uses Omit to exclude associations that could interfere with Save func (r *DocumentRepository) Update(document *models.Document) error { return r.db.Omit("CreatedBy", "Task", "Images", "Residence").Save(document).Error } // Delete soft-deletes a document func (r *DocumentRepository) Delete(id uint) error { return r.db.Model(&models.Document{}). Where("id = ?", id). Update("is_active", false).Error } // Activate activates a document func (r *DocumentRepository) Activate(id uint) error { return r.db.Model(&models.Document{}). Where("id = ?", id). Update("is_active", true).Error } // Deactivate deactivates a document func (r *DocumentRepository) Deactivate(id uint) error { return r.db.Model(&models.Document{}). Where("id = ?", id). Update("is_active", false).Error } // CountByResidence counts documents in a residence func (r *DocumentRepository) CountByResidence(residenceID uint) (int64, error) { var count int64 err := r.db.Model(&models.Document{}). Where("residence_id = ? AND is_active = ?", residenceID, true). Count(&count).Error return count, err } // CountByResidenceIDs counts all active documents across multiple residences in a single query. // Returns the total count of active documents for the given residence IDs. func (r *DocumentRepository) CountByResidenceIDs(residenceIDs []uint) (int64, error) { if len(residenceIDs) == 0 { return 0, nil } var count int64 err := r.db.Model(&models.Document{}). Where("residence_id IN ? AND is_active = ?", residenceIDs, true). Count(&count).Error return count, err } // FindByIDIncludingInactive finds a document by ID including inactive ones func (r *DocumentRepository) FindByIDIncludingInactive(id uint, document *models.Document) error { return r.db.Preload("CreatedBy").Preload("Images").First(document, id).Error } // CreateDocumentImage creates a new document image func (r *DocumentRepository) CreateDocumentImage(image *models.DocumentImage) error { return r.db.Create(image).Error } // DeleteDocumentImage deletes a document image func (r *DocumentRepository) DeleteDocumentImage(id uint) error { return r.db.Delete(&models.DocumentImage{}, id).Error } // DeleteDocumentImages deletes all images for a document func (r *DocumentRepository) DeleteDocumentImages(documentID uint) error { return r.db.Where("document_id = ?", documentID).Delete(&models.DocumentImage{}).Error } // FindImageByID finds a document image by ID func (r *DocumentRepository) FindImageByID(id uint) (*models.DocumentImage, error) { var image models.DocumentImage err := r.db.First(&image, id).Error if err != nil { return nil, err } return &image, nil }