package sqlite import ( "context" "database/sql" "encoding/json" "fmt" "apocapoc-api/internal/domain/entities" "apocapoc-api/internal/domain/repositories" "apocapoc-api/internal/shared/errors" "apocapoc-api/internal/shared/pagination" "github.com/google/uuid" ) type HabitRepository struct { db *sql.DB } func NewHabitRepository(db *sql.DB) *HabitRepository { return &HabitRepository{db: db} } func (r *HabitRepository) Create(ctx context.Context, habit *entities.Habit) error { habit.ID = uuid.New().String() specificDays, _ := json.Marshal(habit.SpecificDays) specificDates, _ := json.Marshal(habit.SpecificDates) query := ` INSERT INTO habits ( id, user_id, name, description, type, frequency, specific_days, specific_dates, carry_over, is_negative, target_value, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` _, err := r.db.ExecContext(ctx, query, habit.ID, habit.UserID, habit.Name, habit.Description, habit.Type, habit.Frequency, specificDays, specificDates, habit.CarryOver, habit.IsNegative, habit.TargetValue, habit.CreatedAt, ) if err != nil { return fmt.Errorf("failed to create habit: %w", err) } return nil } func (r *HabitRepository) FindByID(ctx context.Context, id string) (*entities.Habit, error) { query := ` SELECT id, user_id, name, description, type, frequency, specific_days, specific_dates, carry_over, is_negative, target_value, created_at, archived_at FROM habits WHERE id = ? ` var ( habit entities.Habit specificDays sql.NullString specificDates sql.NullString archivedAt sql.NullTime ) err := r.db.QueryRowContext(ctx, query, id).Scan( &habit.ID, &habit.UserID, &habit.Name, &habit.Description, &habit.Type, &habit.Frequency, &specificDays, &specificDates, &habit.CarryOver, &habit.IsNegative, &habit.TargetValue, &habit.CreatedAt, &archivedAt, ) if err == sql.ErrNoRows { return nil, errors.ErrNotFound } if err != nil { return nil, fmt.Errorf("failed to find habit: %w", err) } if specificDays.Valid { json.Unmarshal([]byte(specificDays.String), &habit.SpecificDays) } if specificDates.Valid { json.Unmarshal([]byte(specificDates.String), &habit.SpecificDates) } if archivedAt.Valid { habit.ArchivedAt = &archivedAt.Time } return &habit, nil } func (r *HabitRepository) FindActiveByUserID(ctx context.Context, userID string) ([]*entities.Habit, error) { query := ` SELECT id, user_id, name, description, type, frequency, specific_days, specific_dates, carry_over, is_negative, target_value, created_at, archived_at FROM habits WHERE user_id = ? AND archived_at IS NULL ORDER BY created_at DESC ` rows, err := r.db.QueryContext(ctx, query, userID) if err != nil { return nil, fmt.Errorf("failed to find habits: %w", err) } defer rows.Close() return r.scanHabits(rows) } func (r *HabitRepository) Update(ctx context.Context, habit *entities.Habit) error { specificDays, _ := json.Marshal(habit.SpecificDays) specificDates, _ := json.Marshal(habit.SpecificDates) query := ` UPDATE habits SET name = ?, description = ?, type = ?, frequency = ?, specific_days = ?, specific_dates = ?, carry_over = ?, is_negative = ?, target_value = ?, archived_at = ? WHERE id = ? ` result, err := r.db.ExecContext(ctx, query, habit.Name, habit.Description, habit.Type, habit.Frequency, specificDays, specificDates, habit.CarryOver, habit.IsNegative, habit.TargetValue, habit.ArchivedAt, habit.ID, ) if err != nil { return fmt.Errorf("failed to update habit: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return errors.ErrNotFound } return nil } func (r *HabitRepository) scanHabits(rows *sql.Rows) ([]*entities.Habit, error) { var habits []*entities.Habit for rows.Next() { var ( habit entities.Habit specificDays sql.NullString specificDates sql.NullString archivedAt sql.NullTime ) err := rows.Scan( &habit.ID, &habit.UserID, &habit.Name, &habit.Description, &habit.Type, &habit.Frequency, &specificDays, &specificDates, &habit.CarryOver, &habit.IsNegative, &habit.TargetValue, &habit.CreatedAt, &archivedAt, ) if err != nil { return nil, err } if specificDays.Valid { json.Unmarshal([]byte(specificDays.String), &habit.SpecificDays) } if specificDates.Valid { json.Unmarshal([]byte(specificDates.String), &habit.SpecificDates) } if archivedAt.Valid { habit.ArchivedAt = &archivedAt.Time } habits = append(habits, &habit) } return habits, nil } func (r *HabitRepository) FindByUserID(ctx context.Context, userID string) ([]*entities.Habit, error) { query := ` SELECT id, user_id, name, description, type, frequency, specific_days, specific_dates, carry_over, is_negative, target_value, created_at, archived_at FROM habits WHERE user_id = ? ORDER BY created_at DESC ` rows, err := r.db.QueryContext(ctx, query, userID) if err != nil { return nil, fmt.Errorf("failed to find habits: %w", err) } defer rows.Close() return r.scanHabits(rows) } func (r *HabitRepository) Delete(ctx context.Context, id string) error { query := `DELETE FROM habits WHERE id = ?` result, err := r.db.ExecContext(ctx, query, id) if err != nil { return fmt.Errorf("failed to delete habit: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return errors.ErrNotFound } return nil } func (r *HabitRepository) FindActiveByUserIDWithPagination(ctx context.Context, userID string, params pagination.Params) ([]*entities.Habit, error) { query := ` SELECT id, user_id, name, description, type, frequency, specific_days, specific_dates, carry_over, is_negative, target_value, created_at, archived_at FROM habits WHERE user_id = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ? ` rows, err := r.db.QueryContext(ctx, query, userID, params.Limit(), params.Offset()) if err != nil { return nil, fmt.Errorf("failed to find habits: %w", err) } defer rows.Close() return r.scanHabits(rows) } func (r *HabitRepository) CountActiveByUserID(ctx context.Context, userID string) (int, error) { query := ` SELECT COUNT(*) FROM habits WHERE user_id = ? AND archived_at IS NULL ` var count int err := r.db.QueryRowContext(ctx, query, userID).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to count habits: %w", err) } return count, nil } func (r *HabitRepository) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) { baseQuery := ` SELECT id, user_id, name, description, type, frequency, specific_days, specific_dates, carry_over, is_negative, target_value, created_at, archived_at FROM habits WHERE user_id = ?` args := []interface{}{userID} conditions := []string{} if !filter.IncludeArchived { conditions = append(conditions, "archived_at IS NULL") } if filter.Type != nil { conditions = append(conditions, "type = ?") args = append(args, string(*filter.Type)) } if filter.Frequency != nil { conditions = append(conditions, "frequency = ?") args = append(args, string(*filter.Frequency)) } if filter.Search != "" { conditions = append(conditions, "(name LIKE ? OR description LIKE ?)") searchPattern := "%" + filter.Search + "%" args = append(args, searchPattern, searchPattern) } for _, condition := range conditions { baseQuery += " AND " + condition } baseQuery += " ORDER BY created_at DESC" if paginationParams != nil { baseQuery += " LIMIT ? OFFSET ?" args = append(args, paginationParams.Limit(), paginationParams.Offset()) } rows, err := r.db.QueryContext(ctx, baseQuery, args...) if err != nil { return nil, fmt.Errorf("failed to find habits: %w", err) } defer rows.Close() return r.scanHabits(rows) } func (r *HabitRepository) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) { baseQuery := `SELECT COUNT(*) FROM habits WHERE user_id = ?` args := []interface{}{userID} conditions := []string{} if !filter.IncludeArchived { conditions = append(conditions, "archived_at IS NULL") } if filter.Type != nil { conditions = append(conditions, "type = ?") args = append(args, string(*filter.Type)) } if filter.Frequency != nil { conditions = append(conditions, "frequency = ?") args = append(args, string(*filter.Frequency)) } if filter.Search != "" { conditions = append(conditions, "(name LIKE ? OR description LIKE ?)") searchPattern := "%" + filter.Search + "%" args = append(args, searchPattern, searchPattern) } for _, condition := range conditions { baseQuery += " AND " + condition } var count int err := r.db.QueryRowContext(ctx, baseQuery, args...).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to count habits: %w", err) } return count, nil }