From a1b4689d250c16b690ffd58b1317598fc8f6143c Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 12:43:29 +0530 Subject: [PATCH 1/4] implement goal service --- internal/app/dependencies.go | 8 ++- internal/app/goal/domain.go | 26 +++++++ internal/app/goal/handler.go | 94 ++++++++++++++++++++++++ internal/app/goal/service.go | 103 ++++++++++++++++++++++++++ internal/app/router.go | 5 ++ internal/app/user/domain.go | 4 ++ internal/app/user/handler.go | 32 +++++++++ internal/app/user/service.go | 24 ++++++- internal/pkg/apperrors/errors.go | 6 +- internal/repository/domain.go | 18 +++++ internal/repository/goal.go | 120 +++++++++++++++++++++++++++++++ internal/repository/user.go | 17 ++++- 12 files changed, 453 insertions(+), 4 deletions(-) create mode 100644 internal/app/goal/domain.go create mode 100644 internal/app/goal/handler.go create mode 100644 internal/app/goal/service.go create mode 100644 internal/repository/goal.go diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index cb95a53..5c3ac80 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -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" @@ -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) @@ -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, @@ -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, } diff --git a/internal/app/goal/domain.go b/internal/app/goal/domain.go new file mode 100644 index 0000000..163d615 --- /dev/null +++ b/internal/app/goal/domain.go @@ -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"` +} diff --git a/internal/app/goal/handler.go b/internal/app/goal/handler.go new file mode 100644 index 0000000..33f21e3 --- /dev/null +++ b/internal/app/goal/handler.go @@ -0,0 +1,94 @@ +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) +} + +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) +} diff --git a/internal/app/goal/service.go b/internal/app/goal/service.go new file mode 100644 index 0000000..da0a917 --- /dev/null +++ b/internal/app/goal/service.go @@ -0,0 +1,103 @@ +package goal + +import ( + "context" + "log/slog" + + "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) +} + +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 +} + +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 +} diff --git a/internal/app/router.go b/internal/app/router.go index 76759a1..5f99a71 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -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)) @@ -32,5 +33,9 @@ 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)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index cf0e527..236102d 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -53,3 +53,7 @@ type LeaderboardUser struct { CurrentBalance int `db:"current_balance"` Rank int `db:"rank"` } + +type GoalLevel struct { + Level string `json:"level"` +} diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 1a0dc22..2d1bfcd 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -19,6 +19,7 @@ type Handler interface { SoftDeleteUser(w http.ResponseWriter, r *http.Request) ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) + UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) } func NewHandler(userService Service) Handler { @@ -110,3 +111,34 @@ func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank) } + +func (h *handler) UpdateCurrentActiveGoalId(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 goal GoalLevel + err := json.NewDecoder(r.Body).Decode(&goal) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + goalId, err := h.userService.UpdateCurrentActiveGoalId(ctx, userId, goal.Level) + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + status, errMsg := apperrors.MapError(err) + response.WriteJson(w, status, errMsg, nil) + return + } + + response.WriteJson(w, http.StatusOK, "Goal updated successfully", goalId) +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index fa37e18..2b01e82 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -5,6 +5,7 @@ import ( "log/slog" "time" + "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" "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/repository" @@ -12,6 +13,7 @@ import ( type service struct { userRepository repository.UserRepository + goalService goal.Service } type Service interface { @@ -25,11 +27,13 @@ type Service interface { UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) + UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) } -func NewService(userRepository repository.UserRepository) Service { +func NewService(userRepository repository.UserRepository, goalService goal.Service) Service { return &service{ userRepository: userRepository, + goalService: goalService, } } @@ -158,3 +162,21 @@ func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (Leaderboa return LeaderboardUser(currentUserRank), nil } + +func (s *service) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) { + + goalId, err := s.goalService.GetGoalIdByGoalLevel(ctx, level) + + if err != nil { + slog.Error("error occured while fetching goal id by goal level") + return 0, err + } + + goalId, err = s.userRepository.UpdateCurrentActiveGoalId(ctx, nil, userId, goalId) + + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + } + + return goalId, err +} diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 4c0430c..2ddfbaa 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -51,6 +51,10 @@ var ( ErrTransactionCreationFailed = errors.New("error failed to create transaction") ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") + + ErrFetchingGoals = errors.New("error fetching goal levels ") + ErrGoalNotFound = errors.New("goal not found") + ErrCustomGoalTargetCreationFailed = errors.New("failed to create targets for custom goal level") ) func MapError(err error) (statusCode int, errMessage string) { @@ -61,7 +65,7 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound: + case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound, ErrGoalNotFound: return http.StatusNotFound, err.Error() case ErrInvalidToken: return http.StatusUnprocessableEntity, err.Error() diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 4ac6312..037ffc3 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -92,3 +92,21 @@ type MonthlyContributionSummary struct { TotalCoins int `db:"total_coins"` Month time.Time `db:"month"` } + +type Goal struct { + Id int `db:"id"` + Level string `db:"level"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type GoalContribution struct { + Id int `db:"id"` + GoalId int `db:"goal_id"` + ContributionScoreId int `db:"contribution_score_id"` + TargetCount int `db:"target_count"` + IsCustom bool `db:"is_custom"` + SetByUserId int `db:"set_by_user_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/repository/goal.go b/internal/repository/goal.go new file mode 100644 index 0000000..1e7c1d3 --- /dev/null +++ b/internal/repository/goal.go @@ -0,0 +1,120 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type goalRepository struct { + BaseRepository +} + +type GoalRepository interface { + RepositoryTransaction + ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) + GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) + ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) + CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) +} + +func NewGoalRepository(db *sqlx.DB) GoalRepository { + return &goalRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getGoalLevelQuery = "SELECT * from goal;" + + fetchGoalIdByGoalNameQuery = "SELECT id from goal where level=$1" + + getGoalContributionDetailQuery = ` + SELECT * from goal_contribution + where goal_id + IN + (SELECT current_active_goal_id from users where id=$1)` + + createCustomGoalLevelTargetQuery = ` + INSERT INTO goal_contribution( + goal_id, + contribution_score_id, + target_count, + is_custom, + set_by_user_id + ) + VALUES + ($1, $2, $3, $4, $5) + RETURNING *` +) + +func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goals []Goal + err := executer.SelectContext(ctx, &goals, getGoalLevelQuery) + if err != nil { + slog.Error("error fetching goal levels", "error", err) + return nil, apperrors.ErrFetchingGoals + } + + return goals, nil +} + +func (gr *goalRepository) GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalId int + err := executer.GetContext(ctx, &goalId, fetchGoalIdByGoalNameQuery, level) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("error goal not found", "error", err) + return 0, apperrors.ErrGoalNotFound + } + + slog.Error("error occured while getting goal id by goal level", "error", err) + return 0, apperrors.ErrInternalServer + } + + return goalId, nil +} + +func (gr *goalRepository) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalLevelTargets []GoalContribution + err := executer.SelectContext(ctx, &goalLevelTargets, getGoalContributionDetailQuery, userId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("error goal not found", "error", err) + return nil, apperrors.ErrInternalServer + } + + slog.Error("error occured while getting goal id by goal level", "error", err) + return nil, apperrors.ErrInternalServer + } + + return goalLevelTargets, nil +} + +func (gr *goalRepository) CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var customGoalContribution GoalContribution + err := executer.GetContext(ctx, &customGoalContribution, createCustomGoalLevelTargetQuery, + customGoalContributionInfo.GoalId, + customGoalContributionInfo.ContributionScoreId, + customGoalContributionInfo.TargetCount, + true, + customGoalContributionInfo.SetByUserId) + if err != nil { + slog.Error("error creating custom goal level targets", "error", err) + return GoalContribution{}, apperrors.ErrCustomGoalTargetCreationFailed + } + + return customGoalContribution, nil +} diff --git a/internal/repository/user.go b/internal/repository/user.go index d9a6cc9..15b1f6a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -28,6 +28,7 @@ type UserRepository interface { UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) + UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -87,6 +88,8 @@ const ( ) ranked_users WHERE id = $1;` + + updateCurrentActiveGoalIdQuery = "UPDATE users SET current_active_goal_id=$1 where id=$2" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -242,4 +245,16 @@ func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, u } return currentUserRank, nil -} \ No newline at end of file +} + +func (ur *userRepository) UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateCurrentActiveGoalIdQuery, goalId, userId) + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + return 0, apperrors.ErrInternalServer + } + + return goalId, nil +} From 7e434ebc68bbdd0b99796a4c7b6515b905fd055e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 12:45:56 +0530 Subject: [PATCH 2/4] remove unwanted db tags --- internal/app/transaction/domain.go | 18 +++++++++--------- internal/app/user/domain.go | 28 ++++++++++++++-------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/internal/app/transaction/domain.go b/internal/app/transaction/domain.go index b8989b6..6a02f13 100644 --- a/internal/app/transaction/domain.go +++ b/internal/app/transaction/domain.go @@ -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 } type Contribution struct { diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index 236102d..087f25f 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -35,23 +35,23 @@ type Email struct { } 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 } type LeaderboardUser struct { - Id int `db:"id"` - GithubUsername string `db:"github_username"` - AvatarUrl string `db:"avatar_url"` - CurrentBalance int `db:"current_balance"` - Rank int `db:"rank"` + Id int + GithubUsername string + AvatarUrl string + CurrentBalance int + Rank int } type GoalLevel struct { From 06a2716e56426ca51f0b07ba711846287f3faa93 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 13:22:48 +0530 Subject: [PATCH 3/4] rename query const according to function names --- internal/repository/goal.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/repository/goal.go b/internal/repository/goal.go index 1e7c1d3..87f84de 100644 --- a/internal/repository/goal.go +++ b/internal/repository/goal.go @@ -29,11 +29,11 @@ func NewGoalRepository(db *sqlx.DB) GoalRepository { } const ( - getGoalLevelQuery = "SELECT * from goal;" + listGoalLevelQuery = "SELECT * from goal;" - fetchGoalIdByGoalNameQuery = "SELECT id from goal where level=$1" + getGoalIdByGoalLevelQuery = "SELECT id from goal where level=$1" - getGoalContributionDetailQuery = ` + listUserGoalLevelTargetsQuery = ` SELECT * from goal_contribution where goal_id IN @@ -56,7 +56,7 @@ func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Go executer := gr.BaseRepository.initiateQueryExecuter(tx) var goals []Goal - err := executer.SelectContext(ctx, &goals, getGoalLevelQuery) + err := executer.SelectContext(ctx, &goals, listGoalLevelQuery) if err != nil { slog.Error("error fetching goal levels", "error", err) return nil, apperrors.ErrFetchingGoals @@ -69,7 +69,7 @@ func (gr *goalRepository) GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, executer := gr.BaseRepository.initiateQueryExecuter(tx) var goalId int - err := executer.GetContext(ctx, &goalId, fetchGoalIdByGoalNameQuery, level) + err := executer.GetContext(ctx, &goalId, getGoalIdByGoalLevelQuery, level) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("error goal not found", "error", err) @@ -87,7 +87,7 @@ func (gr *goalRepository) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx executer := gr.BaseRepository.initiateQueryExecuter(tx) var goalLevelTargets []GoalContribution - err := executer.SelectContext(ctx, &goalLevelTargets, getGoalContributionDetailQuery, userId) + err := executer.SelectContext(ctx, &goalLevelTargets, listUserGoalLevelTargetsQuery, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("error goal not found", "error", err) From 4db322e61c04c24547c0b432131453d39216ad82 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 15:24:11 +0530 Subject: [PATCH 4/4] implement goal level achieved target --- internal/app/goal/handler.go | 23 +++++++++++++ internal/app/goal/service.go | 51 ++++++++++++++++++++++++++++- internal/app/router.go | 1 + internal/repository/contribution.go | 18 +++++++++- 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/internal/app/goal/handler.go b/internal/app/goal/handler.go index 33f21e3..4fb1227 100644 --- a/internal/app/goal/handler.go +++ b/internal/app/goal/handler.go @@ -18,6 +18,7 @@ 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 { @@ -92,3 +93,25 @@ func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Req 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) +} diff --git a/internal/app/goal/service.go b/internal/app/goal/service.go index da0a917..3c50125 100644 --- a/internal/app/goal/service.go +++ b/internal/app/goal/service.go @@ -2,7 +2,9 @@ package goal import ( "context" + "fmt" "log/slog" + "time" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -16,7 +18,8 @@ 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) + 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 { @@ -101,3 +104,49 @@ func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userID int, c 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 +} diff --git a/internal/app/router.go b/internal/app/router.go index 5f99a71..4122522 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -36,6 +36,7 @@ func NewRouter(deps Dependencies) http.Handler { 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) } diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index da5163a..7febf7f 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -23,6 +23,7 @@ type ContributionRepository interface { GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) + GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -64,6 +65,8 @@ const ( AND DATE_TRUNC('month', contributed_at) = MAKE_DATE($2, $3, 1)::timestamptz GROUP BY month, contribution_type;` + + getContributionTypeByContributionScoreIdQuery = `SELECT contribution_type from contribution_score where id=$1` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -161,7 +164,7 @@ func (cr *contributionRepository) ListMonthlyContributionSummary(ctx context.Con executer := cr.BaseRepository.initiateQueryExecuter(tx) var contributionTypeSummary []MonthlyContributionSummary - err := executer.SelectContext(ctx, &contributionTypeSummary, getMonthlyContributionSummaryQuery, userId, month) + err := executer.SelectContext(ctx, &contributionTypeSummary, getMonthlyContributionSummaryQuery, userId, year, month) if err != nil { slog.Error("error fetching monthly contribution summary for user", "error", err) return nil, apperrors.ErrInternalServer @@ -169,3 +172,16 @@ func (cr *contributionRepository) ListMonthlyContributionSummary(ctx context.Con return contributionTypeSummary, nil } + +func (cr *contributionRepository) GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionType string + err := executer.GetContext(ctx, &contributionType, getContributionTypeByContributionScoreIdQuery, contributionScoreId) + if err != nil { + slog.Error("error occured while getting contribution type by contribution score id", "error", err) + return contributionType, err + } + + return contributionType, nil +}