Initial commit: MyCrib API in Go
Complete rewrite of Django REST API to Go with: - Gin web framework for HTTP routing - GORM for database operations - GoAdmin for admin panel - Gorush integration for push notifications - Redis for caching and job queues Features implemented: - User authentication (login, register, logout, password reset) - Residence management (CRUD, sharing, share codes) - Task management (CRUD, kanban board, completions) - Contractor management (CRUD, specialties) - Document management (CRUD, warranties) - Notifications (preferences, push notifications) - Subscription management (tiers, limits) Infrastructure: - Docker Compose for local development - Database migrations and seed data - Admin panel for data management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
151
internal/repositories/contractor_repo.go
Normal file
151
internal/repositories/contractor_repo.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// ContractorRepository handles database operations for contractors
|
||||
type ContractorRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewContractorRepository creates a new contractor repository
|
||||
func NewContractorRepository(db *gorm.DB) *ContractorRepository {
|
||||
return &ContractorRepository{db: db}
|
||||
}
|
||||
|
||||
// FindByID finds a contractor by ID with preloaded relations
|
||||
func (r *ContractorRepository) FindByID(id uint) (*models.Contractor, error) {
|
||||
var contractor models.Contractor
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("Specialties").
|
||||
Preload("Tasks").
|
||||
Where("id = ? AND is_active = ?", id, true).
|
||||
First(&contractor).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &contractor, nil
|
||||
}
|
||||
|
||||
// FindByResidence finds all contractors for a residence
|
||||
func (r *ContractorRepository) FindByResidence(residenceID uint) ([]models.Contractor, error) {
|
||||
var contractors []models.Contractor
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("Specialties").
|
||||
Where("residence_id = ? AND is_active = ?", residenceID, true).
|
||||
Order("is_favorite DESC, name ASC").
|
||||
Find(&contractors).Error
|
||||
return contractors, err
|
||||
}
|
||||
|
||||
// FindByUser finds all contractors accessible to a user
|
||||
func (r *ContractorRepository) FindByUser(residenceIDs []uint) ([]models.Contractor, error) {
|
||||
var contractors []models.Contractor
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("Specialties").
|
||||
Preload("Residence").
|
||||
Where("residence_id IN ? AND is_active = ?", residenceIDs, true).
|
||||
Order("is_favorite DESC, name ASC").
|
||||
Find(&contractors).Error
|
||||
return contractors, err
|
||||
}
|
||||
|
||||
// Create creates a new contractor
|
||||
func (r *ContractorRepository) Create(contractor *models.Contractor) error {
|
||||
return r.db.Create(contractor).Error
|
||||
}
|
||||
|
||||
// Update updates a contractor
|
||||
func (r *ContractorRepository) Update(contractor *models.Contractor) error {
|
||||
return r.db.Save(contractor).Error
|
||||
}
|
||||
|
||||
// Delete soft-deletes a contractor
|
||||
func (r *ContractorRepository) Delete(id uint) error {
|
||||
return r.db.Model(&models.Contractor{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_active", false).Error
|
||||
}
|
||||
|
||||
// ToggleFavorite toggles the favorite status of a contractor
|
||||
func (r *ContractorRepository) ToggleFavorite(id uint) (bool, error) {
|
||||
var contractor models.Contractor
|
||||
if err := r.db.First(&contractor, id).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newStatus := !contractor.IsFavorite
|
||||
err := r.db.Model(&models.Contractor{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_favorite", newStatus).Error
|
||||
|
||||
return newStatus, err
|
||||
}
|
||||
|
||||
// GetTasksForContractor gets all tasks associated with a contractor
|
||||
func (r *ContractorRepository) GetTasksForContractor(contractorID uint) ([]models.Task, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status").
|
||||
Where("contractor_id = ?", contractorID).
|
||||
Order("due_date ASC NULLS LAST").
|
||||
Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// SetSpecialties sets the specialties for a contractor
|
||||
func (r *ContractorRepository) SetSpecialties(contractorID uint, specialtyIDs []uint) error {
|
||||
var contractor models.Contractor
|
||||
if err := r.db.First(&contractor, contractorID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear existing specialties
|
||||
if err := r.db.Model(&contractor).Association("Specialties").Clear(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(specialtyIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add new specialties
|
||||
var specialties []models.ContractorSpecialty
|
||||
if err := r.db.Where("id IN ?", specialtyIDs).Find(&specialties).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.db.Model(&contractor).Association("Specialties").Append(specialties)
|
||||
}
|
||||
|
||||
// CountByResidence counts contractors in a residence
|
||||
func (r *ContractorRepository) CountByResidence(residenceID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.Contractor{}).
|
||||
Where("residence_id = ? AND is_active = ?", residenceID, true).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// === Specialty Operations ===
|
||||
|
||||
// GetAllSpecialties returns all contractor specialties
|
||||
func (r *ContractorRepository) GetAllSpecialties() ([]models.ContractorSpecialty, error) {
|
||||
var specialties []models.ContractorSpecialty
|
||||
err := r.db.Order("display_order, name").Find(&specialties).Error
|
||||
return specialties, err
|
||||
}
|
||||
|
||||
// FindSpecialtyByID finds a specialty by ID
|
||||
func (r *ContractorRepository) FindSpecialtyByID(id uint) (*models.ContractorSpecialty, error) {
|
||||
var specialty models.ContractorSpecialty
|
||||
err := r.db.First(&specialty, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &specialty, nil
|
||||
}
|
||||
125
internal/repositories/document_repo.go
Normal file
125
internal/repositories/document_repo.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// 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").
|
||||
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").
|
||||
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
|
||||
func (r *DocumentRepository) FindByUser(residenceIDs []uint) ([]models.Document, error) {
|
||||
var documents []models.Document
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("Residence").
|
||||
Where("residence_id IN ? AND is_active = ?", residenceIDs, true).
|
||||
Order("created_at DESC").
|
||||
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").
|
||||
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").
|
||||
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
|
||||
func (r *DocumentRepository) Update(document *models.Document) error {
|
||||
return r.db.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
|
||||
}
|
||||
|
||||
// FindByIDIncludingInactive finds a document by ID including inactive ones
|
||||
func (r *DocumentRepository) FindByIDIncludingInactive(id uint, document *models.Document) error {
|
||||
return r.db.Preload("CreatedBy").First(document, id).Error
|
||||
}
|
||||
265
internal/repositories/notification_repo.go
Normal file
265
internal/repositories/notification_repo.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// NotificationRepository handles database operations for notifications
|
||||
type NotificationRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewNotificationRepository creates a new notification repository
|
||||
func NewNotificationRepository(db *gorm.DB) *NotificationRepository {
|
||||
return &NotificationRepository{db: db}
|
||||
}
|
||||
|
||||
// === Notifications ===
|
||||
|
||||
// FindByID finds a notification by ID
|
||||
func (r *NotificationRepository) FindByID(id uint) (*models.Notification, error) {
|
||||
var notification models.Notification
|
||||
err := r.db.First(¬ification, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬ification, nil
|
||||
}
|
||||
|
||||
// FindByUser finds all notifications for a user
|
||||
func (r *NotificationRepository) FindByUser(userID uint, limit, offset int) ([]models.Notification, error) {
|
||||
var notifications []models.Notification
|
||||
query := r.db.Where("user_id = ?", userID).
|
||||
Order("created_at DESC")
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
|
||||
err := query.Find(¬ifications).Error
|
||||
return notifications, err
|
||||
}
|
||||
|
||||
// Create creates a new notification
|
||||
func (r *NotificationRepository) Create(notification *models.Notification) error {
|
||||
return r.db.Create(notification).Error
|
||||
}
|
||||
|
||||
// MarkAsRead marks a notification as read
|
||||
func (r *NotificationRepository) MarkAsRead(id uint) error {
|
||||
now := time.Now().UTC()
|
||||
return r.db.Model(&models.Notification{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"read": true,
|
||||
"read_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// MarkAllAsRead marks all notifications for a user as read
|
||||
func (r *NotificationRepository) MarkAllAsRead(userID uint) error {
|
||||
now := time.Now().UTC()
|
||||
return r.db.Model(&models.Notification{}).
|
||||
Where("user_id = ? AND read = ?", userID, false).
|
||||
Updates(map[string]interface{}{
|
||||
"read": true,
|
||||
"read_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// MarkAsSent marks a notification as sent
|
||||
func (r *NotificationRepository) MarkAsSent(id uint) error {
|
||||
now := time.Now().UTC()
|
||||
return r.db.Model(&models.Notification{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"sent": true,
|
||||
"sent_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// SetError sets an error message on a notification
|
||||
func (r *NotificationRepository) SetError(id uint, errorMsg string) error {
|
||||
return r.db.Model(&models.Notification{}).
|
||||
Where("id = ?", id).
|
||||
Update("error_message", errorMsg).Error
|
||||
}
|
||||
|
||||
// CountUnread counts unread notifications for a user
|
||||
func (r *NotificationRepository) CountUnread(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.Notification{}).
|
||||
Where("user_id = ? AND read = ?", userID, false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetPendingNotifications gets notifications that need to be sent
|
||||
func (r *NotificationRepository) GetPendingNotifications(limit int) ([]models.Notification, error) {
|
||||
var notifications []models.Notification
|
||||
err := r.db.Where("sent = ?", false).
|
||||
Order("created_at ASC").
|
||||
Limit(limit).
|
||||
Find(¬ifications).Error
|
||||
return notifications, err
|
||||
}
|
||||
|
||||
// === Notification Preferences ===
|
||||
|
||||
// FindPreferencesByUser finds notification preferences for a user
|
||||
func (r *NotificationRepository) FindPreferencesByUser(userID uint) (*models.NotificationPreference, error) {
|
||||
var prefs models.NotificationPreference
|
||||
err := r.db.Where("user_id = ?", userID).First(&prefs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &prefs, nil
|
||||
}
|
||||
|
||||
// CreatePreferences creates notification preferences for a user
|
||||
func (r *NotificationRepository) CreatePreferences(prefs *models.NotificationPreference) error {
|
||||
return r.db.Create(prefs).Error
|
||||
}
|
||||
|
||||
// UpdatePreferences updates notification preferences
|
||||
func (r *NotificationRepository) UpdatePreferences(prefs *models.NotificationPreference) error {
|
||||
return r.db.Save(prefs).Error
|
||||
}
|
||||
|
||||
// GetOrCreatePreferences gets or creates notification preferences for a user
|
||||
func (r *NotificationRepository) GetOrCreatePreferences(userID uint) (*models.NotificationPreference, error) {
|
||||
prefs, err := r.FindPreferencesByUser(userID)
|
||||
if err == nil {
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
prefs = &models.NotificationPreference{
|
||||
UserID: userID,
|
||||
TaskDueSoon: true,
|
||||
TaskOverdue: true,
|
||||
TaskCompleted: true,
|
||||
TaskAssigned: true,
|
||||
ResidenceShared: true,
|
||||
WarrantyExpiring: true,
|
||||
}
|
||||
if err := r.CreatePreferences(prefs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// === Device Registration ===
|
||||
|
||||
// FindAPNSDeviceByToken finds an APNS device by registration token
|
||||
func (r *NotificationRepository) FindAPNSDeviceByToken(token string) (*models.APNSDevice, error) {
|
||||
var device models.APNSDevice
|
||||
err := r.db.Where("registration_id = ?", token).First(&device).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &device, nil
|
||||
}
|
||||
|
||||
// FindAPNSDevicesByUser finds all APNS devices for a user
|
||||
func (r *NotificationRepository) FindAPNSDevicesByUser(userID uint) ([]models.APNSDevice, error) {
|
||||
var devices []models.APNSDevice
|
||||
err := r.db.Where("user_id = ? AND active = ?", userID, true).Find(&devices).Error
|
||||
return devices, err
|
||||
}
|
||||
|
||||
// CreateAPNSDevice creates a new APNS device
|
||||
func (r *NotificationRepository) CreateAPNSDevice(device *models.APNSDevice) error {
|
||||
return r.db.Create(device).Error
|
||||
}
|
||||
|
||||
// UpdateAPNSDevice updates an APNS device
|
||||
func (r *NotificationRepository) UpdateAPNSDevice(device *models.APNSDevice) error {
|
||||
return r.db.Save(device).Error
|
||||
}
|
||||
|
||||
// DeleteAPNSDevice deletes an APNS device
|
||||
func (r *NotificationRepository) DeleteAPNSDevice(id uint) error {
|
||||
return r.db.Delete(&models.APNSDevice{}, id).Error
|
||||
}
|
||||
|
||||
// DeactivateAPNSDevice deactivates an APNS device
|
||||
func (r *NotificationRepository) DeactivateAPNSDevice(id uint) error {
|
||||
return r.db.Model(&models.APNSDevice{}).
|
||||
Where("id = ?", id).
|
||||
Update("active", false).Error
|
||||
}
|
||||
|
||||
// FindGCMDeviceByToken finds a GCM device by registration token
|
||||
func (r *NotificationRepository) FindGCMDeviceByToken(token string) (*models.GCMDevice, error) {
|
||||
var device models.GCMDevice
|
||||
err := r.db.Where("registration_id = ?", token).First(&device).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &device, nil
|
||||
}
|
||||
|
||||
// FindGCMDevicesByUser finds all GCM devices for a user
|
||||
func (r *NotificationRepository) FindGCMDevicesByUser(userID uint) ([]models.GCMDevice, error) {
|
||||
var devices []models.GCMDevice
|
||||
err := r.db.Where("user_id = ? AND active = ?", userID, true).Find(&devices).Error
|
||||
return devices, err
|
||||
}
|
||||
|
||||
// CreateGCMDevice creates a new GCM device
|
||||
func (r *NotificationRepository) CreateGCMDevice(device *models.GCMDevice) error {
|
||||
return r.db.Create(device).Error
|
||||
}
|
||||
|
||||
// UpdateGCMDevice updates a GCM device
|
||||
func (r *NotificationRepository) UpdateGCMDevice(device *models.GCMDevice) error {
|
||||
return r.db.Save(device).Error
|
||||
}
|
||||
|
||||
// DeleteGCMDevice deletes a GCM device
|
||||
func (r *NotificationRepository) DeleteGCMDevice(id uint) error {
|
||||
return r.db.Delete(&models.GCMDevice{}, id).Error
|
||||
}
|
||||
|
||||
// DeactivateGCMDevice deactivates a GCM device
|
||||
func (r *NotificationRepository) DeactivateGCMDevice(id uint) error {
|
||||
return r.db.Model(&models.GCMDevice{}).
|
||||
Where("id = ?", id).
|
||||
Update("active", false).Error
|
||||
}
|
||||
|
||||
// GetActiveTokensForUser gets all active push tokens for a user
|
||||
func (r *NotificationRepository) GetActiveTokensForUser(userID uint) (iosTokens []string, androidTokens []string, err error) {
|
||||
apnsDevices, err := r.FindAPNSDevicesByUser(userID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
gcmDevices, err := r.FindGCMDevicesByUser(userID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
iosTokens = make([]string, 0, len(apnsDevices))
|
||||
for _, d := range apnsDevices {
|
||||
iosTokens = append(iosTokens, d.RegistrationID)
|
||||
}
|
||||
|
||||
androidTokens = make([]string, 0, len(gcmDevices))
|
||||
for _, d := range gcmDevices {
|
||||
androidTokens = append(androidTokens, d.RegistrationID)
|
||||
}
|
||||
|
||||
return iosTokens, androidTokens, nil
|
||||
}
|
||||
310
internal/repositories/residence_repo.go
Normal file
310
internal/repositories/residence_repo.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-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
|
||||
}
|
||||
203
internal/repositories/subscription_repo.go
Normal file
203
internal/repositories/subscription_repo.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// SubscriptionRepository handles database operations for subscriptions
|
||||
type SubscriptionRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSubscriptionRepository creates a new subscription repository
|
||||
func NewSubscriptionRepository(db *gorm.DB) *SubscriptionRepository {
|
||||
return &SubscriptionRepository{db: db}
|
||||
}
|
||||
|
||||
// === User Subscription ===
|
||||
|
||||
// FindByUserID finds a subscription by user ID
|
||||
func (r *SubscriptionRepository) FindByUserID(userID uint) (*models.UserSubscription, error) {
|
||||
var sub models.UserSubscription
|
||||
err := r.db.Where("user_id = ?", userID).First(&sub).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
// GetOrCreate gets or creates a subscription for a user (defaults to free tier)
|
||||
func (r *SubscriptionRepository) GetOrCreate(userID uint) (*models.UserSubscription, error) {
|
||||
sub, err := r.FindByUserID(userID)
|
||||
if err == nil {
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
sub = &models.UserSubscription{
|
||||
UserID: userID,
|
||||
Tier: models.TierFree,
|
||||
AutoRenew: true,
|
||||
}
|
||||
if err := r.db.Create(sub).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update updates a subscription
|
||||
func (r *SubscriptionRepository) Update(sub *models.UserSubscription) error {
|
||||
return r.db.Save(sub).Error
|
||||
}
|
||||
|
||||
// UpgradeToPro upgrades a user to Pro tier
|
||||
func (r *SubscriptionRepository) UpgradeToPro(userID uint, expiresAt time.Time, platform string) error {
|
||||
now := time.Now().UTC()
|
||||
return r.db.Model(&models.UserSubscription{}).
|
||||
Where("user_id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"tier": models.TierPro,
|
||||
"subscribed_at": now,
|
||||
"expires_at": expiresAt,
|
||||
"cancelled_at": nil,
|
||||
"platform": platform,
|
||||
"auto_renew": true,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// DowngradeToFree downgrades a user to Free tier
|
||||
func (r *SubscriptionRepository) DowngradeToFree(userID uint) error {
|
||||
now := time.Now().UTC()
|
||||
return r.db.Model(&models.UserSubscription{}).
|
||||
Where("user_id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"tier": models.TierFree,
|
||||
"cancelled_at": now,
|
||||
"auto_renew": false,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// SetAutoRenew sets the auto-renew flag
|
||||
func (r *SubscriptionRepository) SetAutoRenew(userID uint, autoRenew bool) error {
|
||||
return r.db.Model(&models.UserSubscription{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("auto_renew", autoRenew).Error
|
||||
}
|
||||
|
||||
// UpdateReceiptData updates the Apple receipt data
|
||||
func (r *SubscriptionRepository) UpdateReceiptData(userID uint, receiptData string) error {
|
||||
return r.db.Model(&models.UserSubscription{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("apple_receipt_data", receiptData).Error
|
||||
}
|
||||
|
||||
// UpdatePurchaseToken updates the Google purchase token
|
||||
func (r *SubscriptionRepository) UpdatePurchaseToken(userID uint, token string) error {
|
||||
return r.db.Model(&models.UserSubscription{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("google_purchase_token", token).Error
|
||||
}
|
||||
|
||||
// === Tier Limits ===
|
||||
|
||||
// GetTierLimits gets the limits for a subscription tier
|
||||
func (r *SubscriptionRepository) GetTierLimits(tier models.SubscriptionTier) (*models.TierLimits, error) {
|
||||
var limits models.TierLimits
|
||||
err := r.db.Where("tier = ?", tier).First(&limits).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Return defaults
|
||||
if tier == models.TierFree {
|
||||
defaults := models.GetDefaultFreeLimits()
|
||||
return &defaults, nil
|
||||
}
|
||||
defaults := models.GetDefaultProLimits()
|
||||
return &defaults, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &limits, nil
|
||||
}
|
||||
|
||||
// GetAllTierLimits gets all tier limits
|
||||
func (r *SubscriptionRepository) GetAllTierLimits() ([]models.TierLimits, error) {
|
||||
var limits []models.TierLimits
|
||||
err := r.db.Find(&limits).Error
|
||||
return limits, err
|
||||
}
|
||||
|
||||
// === Subscription Settings (Singleton) ===
|
||||
|
||||
// GetSettings gets the subscription settings
|
||||
func (r *SubscriptionRepository) GetSettings() (*models.SubscriptionSettings, error) {
|
||||
var settings models.SubscriptionSettings
|
||||
err := r.db.First(&settings).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Return default settings (limitations disabled)
|
||||
return &models.SubscriptionSettings{
|
||||
EnableLimitations: false,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// === Upgrade Triggers ===
|
||||
|
||||
// GetUpgradeTrigger gets an upgrade trigger by key
|
||||
func (r *SubscriptionRepository) GetUpgradeTrigger(key string) (*models.UpgradeTrigger, error) {
|
||||
var trigger models.UpgradeTrigger
|
||||
err := r.db.Where("trigger_key = ? AND is_active = ?", key, true).First(&trigger).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &trigger, nil
|
||||
}
|
||||
|
||||
// GetAllUpgradeTriggers gets all active upgrade triggers
|
||||
func (r *SubscriptionRepository) GetAllUpgradeTriggers() ([]models.UpgradeTrigger, error) {
|
||||
var triggers []models.UpgradeTrigger
|
||||
err := r.db.Where("is_active = ?", true).Find(&triggers).Error
|
||||
return triggers, err
|
||||
}
|
||||
|
||||
// === Feature Benefits ===
|
||||
|
||||
// GetFeatureBenefits gets all active feature benefits
|
||||
func (r *SubscriptionRepository) GetFeatureBenefits() ([]models.FeatureBenefit, error) {
|
||||
var benefits []models.FeatureBenefit
|
||||
err := r.db.Where("is_active = ?", true).Order("display_order").Find(&benefits).Error
|
||||
return benefits, err
|
||||
}
|
||||
|
||||
// === Promotions ===
|
||||
|
||||
// GetActivePromotions gets all currently active promotions for a tier
|
||||
func (r *SubscriptionRepository) GetActivePromotions(tier models.SubscriptionTier) ([]models.Promotion, error) {
|
||||
now := time.Now().UTC()
|
||||
var promotions []models.Promotion
|
||||
err := r.db.Where("is_active = ? AND target_tier = ? AND start_date <= ? AND end_date >= ?",
|
||||
true, tier, now, now).
|
||||
Order("start_date DESC").
|
||||
Find(&promotions).Error
|
||||
return promotions, err
|
||||
}
|
||||
|
||||
// GetPromotionByID gets a promotion by ID
|
||||
func (r *SubscriptionRepository) GetPromotionByID(promotionID string) (*models.Promotion, error) {
|
||||
var promotion models.Promotion
|
||||
err := r.db.Where("promotion_id = ? AND is_active = ?", promotionID, true).First(&promotion).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &promotion, nil
|
||||
}
|
||||
347
internal/repositories/task_repo.go
Normal file
347
internal/repositories/task_repo.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// TaskRepository handles database operations for tasks
|
||||
type TaskRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTaskRepository creates a new task repository
|
||||
func NewTaskRepository(db *gorm.DB) *TaskRepository {
|
||||
return &TaskRepository{db: db}
|
||||
}
|
||||
|
||||
// === Task CRUD ===
|
||||
|
||||
// FindByID finds a task by ID with preloaded relations
|
||||
func (r *TaskRepository) FindByID(id uint) (*models.Task, error) {
|
||||
var task models.Task
|
||||
err := r.db.Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.CompletedBy").
|
||||
First(&task, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// FindByResidence finds all tasks for a residence
|
||||
func (r *TaskRepository) FindByResidence(residenceID uint) ([]models.Task, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Where("residence_id = ?", residenceID).
|
||||
Order("due_date ASC NULLS LAST, created_at DESC").
|
||||
Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// FindByUser finds all tasks accessible to a user (across all their residences)
|
||||
func (r *TaskRepository) FindByUser(userID uint, residenceIDs []uint) ([]models.Task, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Where("residence_id IN ?", residenceIDs).
|
||||
Order("due_date ASC NULLS LAST, created_at DESC").
|
||||
Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// Create creates a new task
|
||||
func (r *TaskRepository) Create(task *models.Task) error {
|
||||
return r.db.Create(task).Error
|
||||
}
|
||||
|
||||
// Update updates a task
|
||||
func (r *TaskRepository) Update(task *models.Task) error {
|
||||
return r.db.Save(task).Error
|
||||
}
|
||||
|
||||
// Delete hard-deletes a task
|
||||
func (r *TaskRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Task{}, id).Error
|
||||
}
|
||||
|
||||
// === Task State Operations ===
|
||||
|
||||
// MarkInProgress marks a task as in progress
|
||||
func (r *TaskRepository) MarkInProgress(id uint, statusID uint) error {
|
||||
return r.db.Model(&models.Task{}).
|
||||
Where("id = ?", id).
|
||||
Update("status_id", statusID).Error
|
||||
}
|
||||
|
||||
// Cancel cancels a task
|
||||
func (r *TaskRepository) Cancel(id uint) error {
|
||||
return r.db.Model(&models.Task{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_cancelled", true).Error
|
||||
}
|
||||
|
||||
// Uncancel uncancels a task
|
||||
func (r *TaskRepository) Uncancel(id uint) error {
|
||||
return r.db.Model(&models.Task{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_cancelled", false).Error
|
||||
}
|
||||
|
||||
// Archive archives a task
|
||||
func (r *TaskRepository) Archive(id uint) error {
|
||||
return r.db.Model(&models.Task{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_archived", true).Error
|
||||
}
|
||||
|
||||
// Unarchive unarchives a task
|
||||
func (r *TaskRepository) Unarchive(id uint) error {
|
||||
return r.db.Model(&models.Task{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_archived", false).Error
|
||||
}
|
||||
|
||||
// === Kanban Board ===
|
||||
|
||||
// GetKanbanData retrieves tasks organized for kanban display
|
||||
func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*models.KanbanBoard, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.CompletedBy").
|
||||
Where("residence_id = ? AND is_archived = ?", residenceID, false).
|
||||
Order("due_date ASC NULLS LAST, priority_id DESC, created_at DESC").
|
||||
Find(&tasks).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Organize into columns
|
||||
now := time.Now().UTC()
|
||||
threshold := now.AddDate(0, 0, daysThreshold)
|
||||
|
||||
overdue := make([]models.Task, 0)
|
||||
dueSoon := make([]models.Task, 0)
|
||||
upcoming := make([]models.Task, 0)
|
||||
inProgress := make([]models.Task, 0)
|
||||
completed := make([]models.Task, 0)
|
||||
cancelled := make([]models.Task, 0)
|
||||
|
||||
for _, task := range tasks {
|
||||
if task.IsCancelled {
|
||||
cancelled = append(cancelled, task)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if completed (has completions)
|
||||
if len(task.Completions) > 0 {
|
||||
completed = append(completed, task)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check status for in-progress (status_id = 2 typically)
|
||||
if task.Status != nil && task.Status.Name == "In Progress" {
|
||||
inProgress = append(inProgress, task)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check due date
|
||||
if task.DueDate != nil {
|
||||
if task.DueDate.Before(now) {
|
||||
overdue = append(overdue, task)
|
||||
} else if task.DueDate.Before(threshold) {
|
||||
dueSoon = append(dueSoon, task)
|
||||
} else {
|
||||
upcoming = append(upcoming, task)
|
||||
}
|
||||
} else {
|
||||
upcoming = append(upcoming, task)
|
||||
}
|
||||
}
|
||||
|
||||
columns := []models.KanbanColumn{
|
||||
{
|
||||
Name: "overdue_tasks",
|
||||
DisplayName: "Overdue",
|
||||
ButtonTypes: []string{"edit", "cancel", "mark_in_progress"},
|
||||
Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"},
|
||||
Color: "#FF3B30",
|
||||
Tasks: overdue,
|
||||
Count: len(overdue),
|
||||
},
|
||||
{
|
||||
Name: "due_soon_tasks",
|
||||
DisplayName: "Due Soon",
|
||||
ButtonTypes: []string{"edit", "complete", "mark_in_progress"},
|
||||
Icons: map[string]string{"ios": "clock", "android": "Schedule"},
|
||||
Color: "#FF9500",
|
||||
Tasks: dueSoon,
|
||||
Count: len(dueSoon),
|
||||
},
|
||||
{
|
||||
Name: "upcoming_tasks",
|
||||
DisplayName: "Upcoming",
|
||||
ButtonTypes: []string{"edit", "cancel"},
|
||||
Icons: map[string]string{"ios": "calendar", "android": "Event"},
|
||||
Color: "#007AFF",
|
||||
Tasks: upcoming,
|
||||
Count: len(upcoming),
|
||||
},
|
||||
{
|
||||
Name: "in_progress_tasks",
|
||||
DisplayName: "In Progress",
|
||||
ButtonTypes: []string{"edit", "complete"},
|
||||
Icons: map[string]string{"ios": "hammer", "android": "Build"},
|
||||
Color: "#5856D6",
|
||||
Tasks: inProgress,
|
||||
Count: len(inProgress),
|
||||
},
|
||||
{
|
||||
Name: "completed_tasks",
|
||||
DisplayName: "Completed",
|
||||
ButtonTypes: []string{"view"},
|
||||
Icons: map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"},
|
||||
Color: "#34C759",
|
||||
Tasks: completed,
|
||||
Count: len(completed),
|
||||
},
|
||||
{
|
||||
Name: "cancelled_tasks",
|
||||
DisplayName: "Cancelled",
|
||||
ButtonTypes: []string{"uncancel", "delete"},
|
||||
Icons: map[string]string{"ios": "xmark.circle", "android": "Cancel"},
|
||||
Color: "#8E8E93",
|
||||
Tasks: cancelled,
|
||||
Count: len(cancelled),
|
||||
},
|
||||
}
|
||||
|
||||
return &models.KanbanBoard{
|
||||
Columns: columns,
|
||||
DaysThreshold: daysThreshold,
|
||||
ResidenceID: string(rune(residenceID)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// === Lookup Operations ===
|
||||
|
||||
// GetAllCategories returns all task categories
|
||||
func (r *TaskRepository) GetAllCategories() ([]models.TaskCategory, error) {
|
||||
var categories []models.TaskCategory
|
||||
err := r.db.Order("display_order, name").Find(&categories).Error
|
||||
return categories, err
|
||||
}
|
||||
|
||||
// GetAllPriorities returns all task priorities
|
||||
func (r *TaskRepository) GetAllPriorities() ([]models.TaskPriority, error) {
|
||||
var priorities []models.TaskPriority
|
||||
err := r.db.Order("level").Find(&priorities).Error
|
||||
return priorities, err
|
||||
}
|
||||
|
||||
// GetAllStatuses returns all task statuses
|
||||
func (r *TaskRepository) GetAllStatuses() ([]models.TaskStatus, error) {
|
||||
var statuses []models.TaskStatus
|
||||
err := r.db.Order("display_order").Find(&statuses).Error
|
||||
return statuses, err
|
||||
}
|
||||
|
||||
// GetAllFrequencies returns all task frequencies
|
||||
func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) {
|
||||
var frequencies []models.TaskFrequency
|
||||
err := r.db.Order("display_order").Find(&frequencies).Error
|
||||
return frequencies, err
|
||||
}
|
||||
|
||||
// FindStatusByName finds a status by name
|
||||
func (r *TaskRepository) FindStatusByName(name string) (*models.TaskStatus, error) {
|
||||
var status models.TaskStatus
|
||||
err := r.db.Where("name = ?", name).First(&status).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// CountByResidence counts tasks in a residence
|
||||
func (r *TaskRepository) CountByResidence(residenceID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.Task{}).
|
||||
Where("residence_id = ? AND is_cancelled = ? AND is_archived = ?", residenceID, false, false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// === Task Completion Operations ===
|
||||
|
||||
// CreateCompletion creates a new task completion
|
||||
func (r *TaskRepository) CreateCompletion(completion *models.TaskCompletion) error {
|
||||
return r.db.Create(completion).Error
|
||||
}
|
||||
|
||||
// FindCompletionByID finds a completion by ID
|
||||
func (r *TaskRepository) FindCompletionByID(id uint) (*models.TaskCompletion, error) {
|
||||
var completion models.TaskCompletion
|
||||
err := r.db.Preload("Task").
|
||||
Preload("CompletedBy").
|
||||
First(&completion, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &completion, nil
|
||||
}
|
||||
|
||||
// FindCompletionsByTask finds all completions for a task
|
||||
func (r *TaskRepository) FindCompletionsByTask(taskID uint) ([]models.TaskCompletion, error) {
|
||||
var completions []models.TaskCompletion
|
||||
err := r.db.Preload("CompletedBy").
|
||||
Where("task_id = ?", taskID).
|
||||
Order("completed_at DESC").
|
||||
Find(&completions).Error
|
||||
return completions, err
|
||||
}
|
||||
|
||||
// FindCompletionsByUser finds all completions by a user
|
||||
func (r *TaskRepository) FindCompletionsByUser(userID uint, residenceIDs []uint) ([]models.TaskCompletion, error) {
|
||||
var completions []models.TaskCompletion
|
||||
err := r.db.Preload("Task").
|
||||
Preload("CompletedBy").
|
||||
Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
|
||||
Where("task_task.residence_id IN ?", residenceIDs).
|
||||
Order("completed_at DESC").
|
||||
Find(&completions).Error
|
||||
return completions, err
|
||||
}
|
||||
|
||||
// DeleteCompletion deletes a task completion
|
||||
func (r *TaskRepository) DeleteCompletion(id uint) error {
|
||||
return r.db.Delete(&models.TaskCompletion{}, id).Error
|
||||
}
|
||||
373
internal/repositories/user_repo.go
Normal file
373
internal/repositories/user_repo.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserExists = errors.New("user already exists")
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrTokenNotFound = errors.New("token not found")
|
||||
ErrCodeNotFound = errors.New("code not found")
|
||||
ErrCodeExpired = errors.New("code expired")
|
||||
ErrCodeUsed = errors.New("code already used")
|
||||
ErrTooManyAttempts = errors.New("too many attempts")
|
||||
ErrRateLimitExceeded = errors.New("rate limit exceeded")
|
||||
)
|
||||
|
||||
// UserRepository handles user-related database operations
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new user repository
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
// FindByID finds a user by ID
|
||||
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByIDWithProfile finds a user by ID with profile preloaded
|
||||
func (r *UserRepository) FindByIDWithProfile(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Preload("Profile").First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByUsername finds a user by username (case-insensitive)
|
||||
func (r *UserRepository) FindByUsername(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Where("LOWER(username) = LOWER(?)", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByEmail finds a user by email (case-insensitive)
|
||||
func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Where("LOWER(email) = LOWER(?)", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByUsernameOrEmail finds a user by username or email
|
||||
func (r *UserRepository) FindByUsernameOrEmail(identifier string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Where("LOWER(username) = LOWER(?) OR LOWER(email) = LOWER(?)", identifier, identifier).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Create creates a new user
|
||||
func (r *UserRepository) Create(user *models.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
// Update updates a user
|
||||
func (r *UserRepository) Update(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
// UpdateLastLogin updates the user's last login timestamp
|
||||
func (r *UserRepository) UpdateLastLogin(userID uint) error {
|
||||
now := time.Now().UTC()
|
||||
return r.db.Model(&models.User{}).Where("id = ?", userID).Update("last_login", now).Error
|
||||
}
|
||||
|
||||
// ExistsByUsername checks if a username exists
|
||||
func (r *UserRepository) ExistsByUsername(username string) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&models.User{}).Where("LOWER(username) = LOWER(?)", username).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// ExistsByEmail checks if an email exists
|
||||
func (r *UserRepository) ExistsByEmail(email string) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&models.User{}).Where("LOWER(email) = LOWER(?)", email).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// --- Auth Token Methods ---
|
||||
|
||||
// GetOrCreateToken gets or creates an auth token for a user
|
||||
func (r *UserRepository) GetOrCreateToken(userID uint) (*models.AuthToken, error) {
|
||||
var token models.AuthToken
|
||||
result := r.db.Where("user_id = ?", userID).First(&token)
|
||||
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
token = models.AuthToken{UserID: userID}
|
||||
if err := r.db.Create(&token).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// DeleteToken deletes an auth token
|
||||
func (r *UserRepository) DeleteToken(token string) error {
|
||||
result := r.db.Where("key = ?", token).Delete(&models.AuthToken{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrTokenNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTokenByUserID deletes an auth token by user ID
|
||||
func (r *UserRepository) DeleteTokenByUserID(userID uint) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&models.AuthToken{}).Error
|
||||
}
|
||||
|
||||
// --- User Profile Methods ---
|
||||
|
||||
// GetOrCreateProfile gets or creates a user profile
|
||||
func (r *UserRepository) GetOrCreateProfile(userID uint) (*models.UserProfile, error) {
|
||||
var profile models.UserProfile
|
||||
result := r.db.Where("user_id = ?", userID).First(&profile)
|
||||
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
profile = models.UserProfile{UserID: userID}
|
||||
if err := r.db.Create(&profile).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
// UpdateProfile updates a user profile
|
||||
func (r *UserRepository) UpdateProfile(profile *models.UserProfile) error {
|
||||
return r.db.Save(profile).Error
|
||||
}
|
||||
|
||||
// SetProfileVerified sets the profile verified status
|
||||
func (r *UserRepository) SetProfileVerified(userID uint, verified bool) error {
|
||||
return r.db.Model(&models.UserProfile{}).Where("user_id = ?", userID).Update("verified", verified).Error
|
||||
}
|
||||
|
||||
// --- Confirmation Code Methods ---
|
||||
|
||||
// CreateConfirmationCode creates a new confirmation code
|
||||
func (r *UserRepository) CreateConfirmationCode(userID uint, code string, expiresAt time.Time) (*models.ConfirmationCode, error) {
|
||||
// Invalidate any existing unused codes for this user
|
||||
r.db.Model(&models.ConfirmationCode{}).
|
||||
Where("user_id = ? AND is_used = ?", userID, false).
|
||||
Update("is_used", true)
|
||||
|
||||
confirmCode := &models.ConfirmationCode{
|
||||
UserID: userID,
|
||||
Code: code,
|
||||
ExpiresAt: expiresAt,
|
||||
IsUsed: false,
|
||||
}
|
||||
|
||||
if err := r.db.Create(confirmCode).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return confirmCode, nil
|
||||
}
|
||||
|
||||
// FindConfirmationCode finds a valid confirmation code for a user
|
||||
func (r *UserRepository) FindConfirmationCode(userID uint, code string) (*models.ConfirmationCode, error) {
|
||||
var confirmCode models.ConfirmationCode
|
||||
if err := r.db.Where("user_id = ? AND code = ? AND is_used = ?", userID, code, false).
|
||||
First(&confirmCode).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrCodeNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !confirmCode.IsValid() {
|
||||
if confirmCode.IsUsed {
|
||||
return nil, ErrCodeUsed
|
||||
}
|
||||
return nil, ErrCodeExpired
|
||||
}
|
||||
|
||||
return &confirmCode, nil
|
||||
}
|
||||
|
||||
// MarkConfirmationCodeUsed marks a confirmation code as used
|
||||
func (r *UserRepository) MarkConfirmationCodeUsed(codeID uint) error {
|
||||
return r.db.Model(&models.ConfirmationCode{}).Where("id = ?", codeID).Update("is_used", true).Error
|
||||
}
|
||||
|
||||
// --- Password Reset Code Methods ---
|
||||
|
||||
// CreatePasswordResetCode creates a new password reset code
|
||||
func (r *UserRepository) CreatePasswordResetCode(userID uint, codeHash string, resetToken string, expiresAt time.Time) (*models.PasswordResetCode, error) {
|
||||
// Invalidate any existing unused codes for this user
|
||||
r.db.Model(&models.PasswordResetCode{}).
|
||||
Where("user_id = ? AND used = ?", userID, false).
|
||||
Update("used", true)
|
||||
|
||||
resetCode := &models.PasswordResetCode{
|
||||
UserID: userID,
|
||||
CodeHash: codeHash,
|
||||
ResetToken: resetToken,
|
||||
ExpiresAt: expiresAt,
|
||||
Used: false,
|
||||
Attempts: 0,
|
||||
MaxAttempts: 5,
|
||||
}
|
||||
|
||||
if err := r.db.Create(resetCode).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resetCode, nil
|
||||
}
|
||||
|
||||
// FindPasswordResetCode finds a password reset code by email and checks validity
|
||||
func (r *UserRepository) FindPasswordResetCodeByEmail(email string) (*models.PasswordResetCode, *models.User, error) {
|
||||
user, err := r.FindByEmail(email)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var resetCode models.PasswordResetCode
|
||||
if err := r.db.Where("user_id = ? AND used = ?", user.ID, false).
|
||||
Order("created_at DESC").
|
||||
First(&resetCode).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, ErrCodeNotFound
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &resetCode, user, nil
|
||||
}
|
||||
|
||||
// FindPasswordResetCodeByToken finds a password reset code by reset token
|
||||
func (r *UserRepository) FindPasswordResetCodeByToken(resetToken string) (*models.PasswordResetCode, error) {
|
||||
var resetCode models.PasswordResetCode
|
||||
if err := r.db.Where("reset_token = ?", resetToken).First(&resetCode).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrCodeNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resetCode.IsValid() {
|
||||
if resetCode.Used {
|
||||
return nil, ErrCodeUsed
|
||||
}
|
||||
if resetCode.Attempts >= resetCode.MaxAttempts {
|
||||
return nil, ErrTooManyAttempts
|
||||
}
|
||||
return nil, ErrCodeExpired
|
||||
}
|
||||
|
||||
return &resetCode, nil
|
||||
}
|
||||
|
||||
// IncrementResetCodeAttempts increments the attempt counter
|
||||
func (r *UserRepository) IncrementResetCodeAttempts(codeID uint) error {
|
||||
return r.db.Model(&models.PasswordResetCode{}).Where("id = ?", codeID).
|
||||
Update("attempts", gorm.Expr("attempts + 1")).Error
|
||||
}
|
||||
|
||||
// MarkPasswordResetCodeUsed marks a password reset code as used
|
||||
func (r *UserRepository) MarkPasswordResetCodeUsed(codeID uint) error {
|
||||
return r.db.Model(&models.PasswordResetCode{}).Where("id = ?", codeID).Update("used", true).Error
|
||||
}
|
||||
|
||||
// CountRecentPasswordResetRequests counts reset requests in the last hour
|
||||
func (r *UserRepository) CountRecentPasswordResetRequests(userID uint) (int64, error) {
|
||||
var count int64
|
||||
oneHourAgo := time.Now().UTC().Add(-1 * time.Hour)
|
||||
if err := r.db.Model(&models.PasswordResetCode{}).
|
||||
Where("user_id = ? AND created_at > ?", userID, oneHourAgo).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// --- Search Methods ---
|
||||
|
||||
// SearchUsers searches users by username, email, first name, or last name
|
||||
func (r *UserRepository) SearchUsers(query string, limit, offset int) ([]models.User, int64, error) {
|
||||
var users []models.User
|
||||
var total int64
|
||||
|
||||
searchQuery := "%" + strings.ToLower(query) + "%"
|
||||
|
||||
baseQuery := r.db.Model(&models.User{}).
|
||||
Where("LOWER(username) LIKE ? OR LOWER(email) LIKE ? OR LOWER(first_name) LIKE ? OR LOWER(last_name) LIKE ?",
|
||||
searchQuery, searchQuery, searchQuery, searchQuery)
|
||||
|
||||
if err := baseQuery.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := baseQuery.Offset(offset).Limit(limit).Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// ListUsers lists all users with pagination
|
||||
func (r *UserRepository) ListUsers(limit, offset int) ([]models.User, int64, error) {
|
||||
var users []models.User
|
||||
var total int64
|
||||
|
||||
if err := r.db.Model(&models.User{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := r.db.Offset(offset).Limit(limit).Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
Reference in New Issue
Block a user