package postgres import ( "context" "database/sql" "fmt" "time" "counter/internal/domain/entities" "counter/internal/domain/repositories" ) // CounterRepository implements the CounterRepository interface for PostgreSQL type CounterRepository struct { db *sql.DB } // NewCounterRepository creates a new counter repository func NewCounterRepository(db *sql.DB) repositories.CounterRepository { return &CounterRepository{db: db} } // Create creates a new counter func (r *CounterRepository) Create(ctx context.Context, counter *entities.Counter) error { query := ` INSERT INTO counters (user_id, name, description) VALUES ($1, $2, $3) RETURNING id, created_at, updated_at ` err := r.db.QueryRowContext(ctx, query, counter.UserID, counter.Name, counter.Description). Scan(&counter.ID, &counter.CreatedAt, &counter.UpdatedAt) if err != nil { return err } return nil } // FindByID finds a counter by ID with stats func (r *CounterRepository) FindByID(ctx context.Context, id, userID int) (*entities.CounterWithStats, error) { query := ` SELECT c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at, COALESCE(SUM(ce.value), 0) as total_value, COALESCE(SUM(CASE WHEN ce.date = CURRENT_DATE THEN ce.value ELSE 0 END), 0) as today_value, COALESCE(SUM(CASE WHEN ce.date >= CURRENT_DATE - INTERVAL '7 days' THEN ce.value ELSE 0 END), 0) as week_value, COALESCE(SUM(CASE WHEN ce.date >= DATE_TRUNC('month', CURRENT_DATE) THEN ce.value ELSE 0 END), 0) as month_value, COUNT(ce.id) as entry_count FROM counters c LEFT JOIN counter_entries ce ON c.id = ce.counter_id WHERE c.id = $1 AND c.user_id = $2 GROUP BY c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at ` counter := &entities.CounterWithStats{} err := r.db.QueryRowContext(ctx, query, id, userID). Scan(&counter.ID, &counter.UserID, &counter.Name, &counter.Description, &counter.CreatedAt, &counter.UpdatedAt, &counter.TotalValue, &counter.TodayValue, &counter.WeekValue, &counter.MonthValue, &counter.EntryCount) if err == sql.ErrNoRows { return nil, entities.ErrCounterNotFound } if err != nil { return nil, err } return counter, nil } // FindByUserID finds all counters for a user with stats func (r *CounterRepository) FindByUserID(ctx context.Context, userID int, search string) ([]*entities.CounterWithStats, error) { query := ` SELECT c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at, COALESCE(SUM(ce.value), 0) as total_value, COALESCE(SUM(CASE WHEN ce.date = CURRENT_DATE THEN ce.value ELSE 0 END), 0) as today_value, COALESCE(SUM(CASE WHEN ce.date >= CURRENT_DATE - INTERVAL '7 days' THEN ce.value ELSE 0 END), 0) as week_value, COALESCE(SUM(CASE WHEN ce.date >= DATE_TRUNC('month', CURRENT_DATE) THEN ce.value ELSE 0 END), 0) as month_value, COUNT(ce.id) as entry_count FROM counters c LEFT JOIN counter_entries ce ON c.id = ce.counter_id WHERE c.user_id = $1 ` args := []interface{}{userID} if search != "" { query += " AND (c.name ILIKE $2 OR c.description ILIKE $2)" args = append(args, "%"+search+"%") } query += " GROUP BY c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at ORDER BY c.updated_at DESC" rows, err := r.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var counters []*entities.CounterWithStats for rows.Next() { counter := &entities.CounterWithStats{} err := rows.Scan(&counter.ID, &counter.UserID, &counter.Name, &counter.Description, &counter.CreatedAt, &counter.UpdatedAt, &counter.TotalValue, &counter.TodayValue, &counter.WeekValue, &counter.MonthValue, &counter.EntryCount) if err != nil { return nil, err } counters = append(counters, counter) } return counters, nil } // Update updates a counter func (r *CounterRepository) Update(ctx context.Context, counter *entities.Counter) error { query := ` UPDATE counters SET name = $1, description = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3 AND user_id = $4 RETURNING updated_at ` err := r.db.QueryRowContext(ctx, query, counter.Name, counter.Description, counter.ID, counter.UserID). Scan(&counter.UpdatedAt) if err == sql.ErrNoRows { return entities.ErrCounterNotFound } if err != nil { return err } return nil } // Delete deletes a counter func (r *CounterRepository) Delete(ctx context.Context, id, userID int) error { query := `DELETE FROM counters WHERE id = $1 AND user_id = $2` result, err := r.db.ExecContext(ctx, query, id, userID) if err != nil { return err } rowsAffected, err := result.RowsAffected() if err != nil { return err } if rowsAffected == 0 { return entities.ErrCounterNotFound } return nil } // AddEntry adds a new counter entry func (r *CounterRepository) AddEntry(ctx context.Context, entry *entities.CounterEntry) error { query := ` INSERT INTO counter_entries (counter_id, value, date) VALUES ($1, $2, $3) RETURNING id, created_at ` err := r.db.QueryRowContext(ctx, query, entry.CounterID, entry.Value, entry.Date). Scan(&entry.ID, &entry.CreatedAt) if err != nil { return err } // Update counter's updated_at timestamp _, err = r.db.ExecContext(ctx, "UPDATE counters SET updated_at = CURRENT_TIMESTAMP WHERE id = $1", entry.CounterID) if err != nil { // Log error but don't fail the request // This could be improved with proper logging } return nil } // GetEntries retrieves entries for a specific counter func (r *CounterRepository) GetEntries(ctx context.Context, counterID, userID int, startDate, endDate *time.Time) ([]*entities.CounterEntry, error) { // First verify counter belongs to user exists, err := r.Exists(ctx, counterID, userID) if err != nil { return nil, err } if !exists { return nil, entities.ErrCounterNotFound } query := ` SELECT id, counter_id, value, date, created_at FROM counter_entries WHERE counter_id = $1 ` args := []interface{}{counterID} if startDate != nil { query += fmt.Sprintf(" AND date >= $%d", len(args)+1) args = append(args, *startDate) if endDate != nil { query += fmt.Sprintf(" AND date <= $%d", len(args)+1) args = append(args, *endDate) } } else if endDate != nil { query += fmt.Sprintf(" AND date <= $%d", len(args)+1) args = append(args, *endDate) } query += " ORDER BY date DESC, created_at DESC" rows, err := r.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var entries []*entities.CounterEntry for rows.Next() { entry := &entities.CounterEntry{} err := rows.Scan(&entry.ID, &entry.CounterID, &entry.Value, &entry.Date, &entry.CreatedAt) if err != nil { return nil, err } entries = append(entries, entry) } return entries, nil } // GetDailyStats retrieves daily statistics for a counter func (r *CounterRepository) GetDailyStats(ctx context.Context, counterID, userID int, days int) ([]*entities.DailyStat, error) { // First verify counter belongs to user exists, err := r.Exists(ctx, counterID, userID) if err != nil { return nil, err } if !exists { return nil, entities.ErrCounterNotFound } query := ` SELECT date, SUM(value) as daily_total FROM counter_entries WHERE counter_id = $1 AND date >= CURRENT_DATE - INTERVAL '%d days' GROUP BY date ORDER BY date DESC ` rows, err := r.db.QueryContext(ctx, fmt.Sprintf(query, days), counterID) if err != nil { return nil, err } defer rows.Close() var stats []*entities.DailyStat for rows.Next() { stat := &entities.DailyStat{} err := rows.Scan(&stat.Date, &stat.Total) if err != nil { return nil, err } stats = append(stats, stat) } return stats, nil } // Exists checks if a counter exists and belongs to the user func (r *CounterRepository) Exists(ctx context.Context, id, userID int) (bool, error) { query := `SELECT EXISTS(SELECT 1 FROM counters WHERE id = $1 AND user_id = $2)` var exists bool err := r.db.QueryRowContext(ctx, query, id, userID).Scan(&exists) if err != nil { return false, err } return exists, nil }