package repositories import ( "crypto/rand" "errors" "math/big" "time" "gorm.io/gorm" "github.com/treytartt/casera-api/internal/models" ) // ResidenceRepository handles database operations for residences type ResidenceRepository struct { db *gorm.DB } // NewResidenceRepository creates a new residence repository func NewResidenceRepository(db *gorm.DB) *ResidenceRepository { return &ResidenceRepository{db: db} } // FindByID finds a residence by ID with preloaded relations func (r *ResidenceRepository) FindByID(id uint) (*models.Residence, error) { var residence models.Residence err := r.db.Preload("Owner"). Preload("Users"). Preload("PropertyType"). Where("id = ? AND is_active = ?", id, true). First(&residence).Error if err != nil { return nil, err } return &residence, nil } // FindByIDSimple finds a residence by ID without preloading (for quick checks) func (r *ResidenceRepository) FindByIDSimple(id uint) (*models.Residence, error) { var residence models.Residence err := r.db.Where("id = ? AND is_active = ?", id, true).First(&residence).Error if err != nil { return nil, err } return &residence, nil } // FindByUser finds all residences accessible to a user (owned or shared) func (r *ResidenceRepository) FindByUser(userID uint) ([]models.Residence, error) { var residences []models.Residence // Find residences where user is owner OR user is in the shared users list err := r.db.Preload("Owner"). Preload("Users"). Preload("PropertyType"). Where("is_active = ?", true). Where("owner_id = ? OR id IN (?)", userID, r.db.Table("residence_residence_users").Select("residence_id").Where("user_id = ?", userID), ). Order("is_primary DESC, created_at DESC"). Find(&residences).Error if err != nil { return nil, err } return residences, nil } // FindOwnedByUser finds all residences owned by a user func (r *ResidenceRepository) FindOwnedByUser(userID uint) ([]models.Residence, error) { var residences []models.Residence err := r.db.Preload("Owner"). Preload("Users"). Preload("PropertyType"). Where("owner_id = ? AND is_active = ?", userID, true). Order("is_primary DESC, created_at DESC"). Find(&residences).Error if err != nil { return nil, err } return residences, nil } // Create creates a new residence func (r *ResidenceRepository) Create(residence *models.Residence) error { return r.db.Create(residence).Error } // Update updates a residence func (r *ResidenceRepository) Update(residence *models.Residence) error { return r.db.Save(residence).Error } // Delete soft-deletes a residence by setting is_active to false func (r *ResidenceRepository) Delete(id uint) error { return r.db.Model(&models.Residence{}). Where("id = ?", id). Update("is_active", false).Error } // AddUser adds a user to a residence's shared users func (r *ResidenceRepository) AddUser(residenceID, userID uint) error { // Using raw SQL for the many-to-many join table return r.db.Exec( "INSERT INTO residence_residence_users (residence_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING", residenceID, userID, ).Error } // RemoveUser removes a user from a residence's shared users func (r *ResidenceRepository) RemoveUser(residenceID, userID uint) error { return r.db.Exec( "DELETE FROM residence_residence_users WHERE residence_id = ? AND user_id = ?", residenceID, userID, ).Error } // GetResidenceUsers returns all users with access to a residence func (r *ResidenceRepository) GetResidenceUsers(residenceID uint) ([]models.User, error) { residence, err := r.FindByID(residenceID) if err != nil { return nil, err } users := make([]models.User, 0, len(residence.Users)+1) users = append(users, residence.Owner) users = append(users, residence.Users...) return users, nil } // HasAccess checks if a user has access to a residence func (r *ResidenceRepository) HasAccess(residenceID, userID uint) (bool, error) { var count int64 // Check if user is owner err := r.db.Model(&models.Residence{}). Where("id = ? AND owner_id = ? AND is_active = ?", residenceID, userID, true). Count(&count).Error if err != nil { return false, err } if count > 0 { return true, nil } // Check if user is in shared users err = r.db.Table("residence_residence_users"). Where("residence_id = ? AND user_id = ?", residenceID, userID). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // IsOwner checks if a user is the owner of a residence func (r *ResidenceRepository) IsOwner(residenceID, userID uint) (bool, error) { var count int64 err := r.db.Model(&models.Residence{}). Where("id = ? AND owner_id = ? AND is_active = ?", residenceID, userID, true). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // CountByOwner counts residences owned by a user func (r *ResidenceRepository) CountByOwner(userID uint) (int64, error) { var count int64 err := r.db.Model(&models.Residence{}). Where("owner_id = ? AND is_active = ?", userID, true). Count(&count).Error return count, err } // === Share Code Operations === // CreateShareCode creates a new share code for a residence func (r *ResidenceRepository) CreateShareCode(residenceID, createdByID uint, expiresIn time.Duration) (*models.ResidenceShareCode, error) { // Deactivate existing codes for this residence err := r.db.Model(&models.ResidenceShareCode{}). Where("residence_id = ? AND is_active = ?", residenceID, true). Update("is_active", false).Error if err != nil { return nil, err } // Generate unique 6-character code code, err := r.generateUniqueCode() if err != nil { return nil, err } expiresAt := time.Now().UTC().Add(expiresIn) shareCode := &models.ResidenceShareCode{ ResidenceID: residenceID, Code: code, CreatedByID: createdByID, IsActive: true, ExpiresAt: &expiresAt, } if err := r.db.Create(shareCode).Error; err != nil { return nil, err } return shareCode, nil } // FindShareCodeByCode finds an active share code by its code string func (r *ResidenceRepository) FindShareCodeByCode(code string) (*models.ResidenceShareCode, error) { var shareCode models.ResidenceShareCode err := r.db.Preload("Residence"). Where("code = ? AND is_active = ?", code, true). First(&shareCode).Error if err != nil { return nil, err } // Check if expired if shareCode.ExpiresAt != nil && time.Now().UTC().After(*shareCode.ExpiresAt) { return nil, errors.New("share code has expired") } return &shareCode, nil } // DeactivateShareCode deactivates a share code func (r *ResidenceRepository) DeactivateShareCode(codeID uint) error { return r.db.Model(&models.ResidenceShareCode{}). Where("id = ?", codeID). Update("is_active", false).Error } // GetActiveShareCode gets the active share code for a residence (if any) func (r *ResidenceRepository) GetActiveShareCode(residenceID uint) (*models.ResidenceShareCode, error) { var shareCode models.ResidenceShareCode err := r.db.Where("residence_id = ? AND is_active = ?", residenceID, true). First(&shareCode).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } // Check if expired if shareCode.ExpiresAt != nil && time.Now().UTC().After(*shareCode.ExpiresAt) { // Auto-deactivate expired code r.DeactivateShareCode(shareCode.ID) return nil, nil } return &shareCode, nil } // generateUniqueCode generates a unique 6-character alphanumeric code func (r *ResidenceRepository) generateUniqueCode() (string, error) { const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Removed ambiguous chars: 0, O, I, 1 const codeLength = 6 maxAttempts := 10 for attempt := 0; attempt < maxAttempts; attempt++ { code := make([]byte, codeLength) for i := range code { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) if err != nil { return "", err } code[i] = charset[num.Int64()] } codeStr := string(code) // Check if code already exists var count int64 r.db.Model(&models.ResidenceShareCode{}). Where("code = ? AND is_active = ?", codeStr, true). Count(&count) if count == 0 { return codeStr, nil } } return "", errors.New("failed to generate unique share code") } // === Residence Type Operations === // GetAllResidenceTypes returns all residence types func (r *ResidenceRepository) GetAllResidenceTypes() ([]models.ResidenceType, error) { var types []models.ResidenceType err := r.db.Order("id").Find(&types).Error return types, err } // FindResidenceTypeByID finds a residence type by ID func (r *ResidenceRepository) FindResidenceTypeByID(id uint) (*models.ResidenceType, error) { var residenceType models.ResidenceType err := r.db.First(&residenceType, id).Error if err != nil { return nil, err } return &residenceType, nil } // GetTasksForReport returns all tasks for a residence with related data for report generation func (r *ResidenceRepository) GetTasksForReport(residenceID uint) ([]models.Task, error) { var tasks []models.Task err := r.db. Preload("Category"). Preload("Priority"). Preload("Status"). Preload("Completions"). Where("residence_id = ?", residenceID). Order("due_date ASC NULLS LAST, created_at DESC"). Find(&tasks).Error return tasks, err }