Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion internal/app/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery"
"github.com/joshsoftware/code-curiosity-2025/internal/app/contribution"
"github.com/joshsoftware/code-curiosity-2025/internal/app/github"
"github.com/joshsoftware/code-curiosity-2025/internal/app/goal"
repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository"
"github.com/joshsoftware/code-curiosity-2025/internal/app/transaction"
"github.com/joshsoftware/code-curiosity-2025/internal/app/user"
Expand All @@ -23,17 +24,20 @@ type Dependencies struct {
UserHandler user.Handler
ContributionHandler contribution.Handler
RepositoryHandler repoService.Handler
GoalHandler goal.Handler
AppCfg config.AppConfig
Client config.Bigquery
}

func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery, httpClient *http.Client) Dependencies {
goalRepository := repository.NewGoalRepository(db)
userRepository := repository.NewUserRepository(db)
contributionRepository := repository.NewContributionRepository(db)
repositoryRepository := repository.NewRepositoryRepository(db)
transactionRepository := repository.NewTransactionRepository(db)

userService := user.NewService(userRepository)
goalService := goal.NewService(goalRepository, contributionRepository)
userService := user.NewService(userRepository, goalService)
authService := auth.NewService(userService, appCfg)
bigqueryService := bigquery.NewService(client, userRepository)
githubService := github.NewService(appCfg, httpClient)
Expand All @@ -45,6 +49,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque
userHandler := user.NewHandler(userService)
repositoryHandler := repoService.NewHandler(repositoryService, githubService)
contributionHandler := contribution.NewHandler(contributionService)
goalHandler := goal.NewHandler(goalService)

return Dependencies{
ContributionService: contributionService,
Expand All @@ -53,6 +58,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque
UserHandler: userHandler,
RepositoryHandler: repositoryHandler,
ContributionHandler: contributionHandler,
GoalHandler: goalHandler,
AppCfg: appCfg,
Client: client,
}
Expand Down
26 changes: 26 additions & 0 deletions internal/app/goal/domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package goal

import "time"

type Goal struct {
Id int
Level string
CreatedAt time.Time
UpdatedAt time.Time
}

type GoalContribution struct {
Id int
GoalId int
ContributionScoreId int
TargetCount int
IsCustom bool
SetByUserId int
CreatedAt time.Time
UpdatedAt time.Time
}

type CustomGoalLevelTarget struct {
ContributionType string `json:"contribution_type"`
Target int `json:"target"`
}
117 changes: 117 additions & 0 deletions internal/app/goal/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package goal

import (
"encoding/json"
"log/slog"
"net/http"

"github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware"
"github.com/joshsoftware/code-curiosity-2025/internal/pkg/response"
)

type handler struct {
goalService Service
}

type Handler interface {
ListGoalLevels(w http.ResponseWriter, r *http.Request)
ListGoalLevelTargets(w http.ResponseWriter, r *http.Request)
CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request)
ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request)
}

func NewHandler(goalService Service) Handler {
return &handler{
goalService: goalService,
}
}

func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

gaols, err := h.goalService.ListGoalLevels(ctx)
if err != nil {
slog.Error("error fetching users conributed repos", "error", err)
status, errorMessage := apperrors.MapError(err)
response.WriteJson(w, status, errorMessage, nil)
return
}

response.WriteJson(w, http.StatusOK, "goal levels fetched successfully", gaols)
}

func (h *handler) ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

userIdCtxVal := ctx.Value(middleware.UserIdKey)
userId, ok := userIdCtxVal.(int)
if !ok {
slog.Error("error obtaining user id from context")
status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
response.WriteJson(w, status, errorMessage, nil)
return
}

goalLevelTargets, err := h.goalService.ListGoalLevelTargetDetail(ctx, userId)
if err != nil {
slog.Error("error fetching goal level targets", "error", err)
status, errorMessage := apperrors.MapError(err)
response.WriteJson(w, status, errorMessage, nil)
return
}

response.WriteJson(w, http.StatusOK, "goal level targets fetched successfully", goalLevelTargets)
}

func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

userIdCtxVal := ctx.Value(middleware.UserIdKey)
userId, ok := userIdCtxVal.(int)
if !ok {
slog.Error("error obtaining user id from context")
status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
response.WriteJson(w, status, errorMessage, nil)
return
}

var customGoalLevelTarget []CustomGoalLevelTarget
err := json.NewDecoder(r.Body).Decode(&customGoalLevelTarget)
if err != nil {
slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err)
response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil)
return
}

createdCustomGoalLevelTargets, err := h.goalService.CreateCustomGoalLevelTarget(ctx, userId, customGoalLevelTarget)
if err != nil {
slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err)
response.WriteJson(w, http.StatusBadRequest, err.Error(), nil)
return
}

response.WriteJson(w, http.StatusOK, "custom goal level targets created successfully", createdCustomGoalLevelTargets)
}

func (h *handler) ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

userIdCtxVal := ctx.Value(middleware.UserIdKey)
userId, ok := userIdCtxVal.(int)
if !ok {
slog.Error("error obtaining user id from context")
status, errorMessage := apperrors.MapError(apperrors.ErrContextValue)
response.WriteJson(w, status, errorMessage, nil)
return
}

goalLevelAchievedTarget, err := h.goalService.ListGoalLevelAchievedTarget(ctx, userId)
if err != nil {
slog.Error("error failed to list goal level achieved targets", "error", err)
response.WriteJson(w, http.StatusBadRequest, err.Error(), nil)
return
}

response.WriteJson(w, http.StatusOK, "goal level achieved targets fetched successfully", goalLevelAchievedTarget)
}
152 changes: 152 additions & 0 deletions internal/app/goal/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package goal

import (
"context"
"fmt"
"log/slog"
"time"

"github.com/joshsoftware/code-curiosity-2025/internal/repository"
)

type service struct {
goalRepository repository.GoalRepository
contributionRepository repository.ContributionRepository
}

type Service interface {
ListGoalLevels(ctx context.Context) ([]Goal, error)
GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error)
ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error)
CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error)
ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error)
}

func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository) Service {
return &service{
goalRepository: goalRepository,
contributionRepository: contributionRepository,
}
}

func (s *service) ListGoalLevels(ctx context.Context) ([]Goal, error) {
goals, err := s.goalRepository.ListGoalLevels(ctx, nil)
if err != nil {
slog.Error("error fetching goal levels", "error", err)
return nil, err
}

serviceGoals := make([]Goal, len(goals))

for i, g := range goals {
serviceGoals[i] = Goal(g)
}

return serviceGoals, nil
}

func (s *service) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) {
goalId, err := s.goalRepository.GetGoalIdByGoalLevel(ctx, nil, level)

if err != nil {
slog.Error("failed to get goal id by goal level", "error", err)
return 0, err
}

return goalId, err

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect error return on success

The GetGoalIdByGoalLevel function returns err instead of nil on successful execution. This will cause the function to always return an error, breaking all callers. Change return goalId, err to return goalId, nil.

Code suggestion
Check the AI-generated fix before applying
Suggested change
return goalId, err
return goalId, nil

Code Review Run #463da1


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

}

func (s *service) ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) {
goalLevelTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId)
if err != nil {
slog.Error("error fetching goal level targets", "error", err)
return nil, err
}

serviceGoalLevelTargets := make([]GoalContribution, len(goalLevelTargets))
for i, g := range goalLevelTargets {
serviceGoalLevelTargets[i] = GoalContribution(g)
}

return serviceGoalLevelTargets, nil
}

func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userID int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) {
customGoalLevelId, err := s.GetGoalIdByGoalLevel(ctx, "Custom")
if err != nil {
slog.Error("error fetching custom goal level id", "error", err)
return nil, err
}
var goalContributions []GoalContribution

goalContributionInfo := make([]GoalContribution, len(customGoalLevelTarget))
for i, c := range customGoalLevelTarget {
goalContributionInfo[i].GoalId = customGoalLevelId

contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, c.ContributionType)
if err != nil {
slog.Error("error fetching contribution score details by type", "error", err)
return nil, err
}

goalContributionInfo[i].ContributionScoreId = contributionScoreDetails.Id
goalContributionInfo[i].TargetCount = c.Target
goalContributionInfo[i].SetByUserId = userID

goalContribution, err := s.goalRepository.CreateCustomGoalLevelTarget(ctx, nil, repository.GoalContribution(goalContributionInfo[i]))
if err != nil {
slog.Error("error creating custom goal level target", "error", err)
return nil, err
}

goalContributions = append(goalContributions, GoalContribution(goalContribution))
}

return goalContributions, nil
}

func (s *service) ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) {
goalLevelSetTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId)
if err != nil {
slog.Error("error fetching goal level targets", "error", err)
return nil, err
}

contributionTypes := make([]CustomGoalLevelTarget, len(goalLevelSetTargets))
for i, g := range goalLevelSetTargets {
contributionTypes[i].ContributionType, err = s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId)
if err != nil {
slog.Error("error fetching contribution type by contribution score id", "error", err)
return nil, err
}

contributionTypes[i].Target = g.TargetCount
}

year := int(time.Now().Year())
month := int(time.Now().Month())
monthlyContributionCount, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId)
if err != nil {
slog.Error("error fetching monthly contribution count", "error", err)
return nil, err
}

contributionsAchievedTarget := make(map[string]int, len(monthlyContributionCount))

for _, m := range monthlyContributionCount {
contributionsAchievedTarget[m.Type] = m.Count
}

var completedTarget int
for _, c := range contributionTypes {
if c.Target == contributionsAchievedTarget[c.ContributionType] {
completedTarget += 1
}
}

if completedTarget == len(goalLevelSetTargets) {
fmt.Println("assign badge")
}

return contributionsAchievedTarget, nil
}
6 changes: 6 additions & 0 deletions internal/app/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func NewRouter(deps Dependencies) http.Handler {

router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg))
router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg))
router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg))

router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg))
router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg))
Expand All @@ -32,5 +33,10 @@ func NewRouter(deps Dependencies) http.Handler {
router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg))
router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg))

router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg))
router.HandleFunc("GET /api/v1/user/goal/level/targets", middleware.Authentication(deps.GoalHandler.ListGoalLevelTargets, deps.AppCfg))
router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg))
router.HandleFunc("GET /api/v1/user/goal/level/targets/achieved", middleware.Authentication(deps.GoalHandler.ListGoalLevelAchievedTarget, deps.AppCfg))

return middleware.CorsMiddleware(router, deps.AppCfg)
}
18 changes: 9 additions & 9 deletions internal/app/transaction/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package transaction
import "time"

type Transaction struct {
Id int `db:"id"`
UserId int `db:"user_id"`
ContributionId int `db:"contribution_id"`
IsRedeemed bool `db:"is_redeemed"`
IsGained bool `db:"is_gained"`
TransactedBalance int `db:"transacted_balance"`
TransactedAt time.Time `db:"transacted_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Id int
UserId int
ContributionId int
IsRedeemed bool
IsGained bool
TransactedBalance int
TransactedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
Comment on lines +6 to +14

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing database struct tags break sqlx operations

The db struct tags have been removed from all Transaction struct fields. This will break database operations since the repository layer uses sqlx.GetContext() which relies on these tags to map database columns to struct fields. Please restore all db tags: db:"id", db:"user_id", db:"contribution_id", db:"is_redeemed", db:"is_gained", db:"transacted_balance", db:"transacted_at", db:"created_at", db:"updated_at".

Code suggestion
Check the AI-generated fix before applying
Suggested change
Id int
UserId int
ContributionId int
IsRedeemed bool
IsGained bool
TransactedBalance int
TransactedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
Id int `db:"id"`
UserId int `db:"user_id"`
ContributionId int `db:"contribution_id"`
IsRedeemed bool `db:"is_redeemed"`
IsGained bool `db:"is_gained"`
TransactedBalance int `db:"transacted_balance"`
TransactedAt time.Time `db:"transacted_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`

Code Review Run #463da1


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

}

type Contribution struct {
Expand Down
Loading