-
Notifications
You must be signed in to change notification settings - Fork 0
Implement goal service #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/Delete
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"` | ||
| } |
| 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) | ||
| } |
| 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 | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing database struct tags break sqlx operations
The Code suggestionCheck the AI-generated fix before applying
Suggested change
Code Review Run #463da1 Should Bito avoid suggestions like this for future reviews? (Manage Rules)
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| type Contribution struct { | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
GetGoalIdByGoalLevelfunction returnserrinstead ofnilon successful execution. This will cause the function to always return an error, breaking all callers. Changereturn goalId, errtoreturn goalId, nil.Code suggestion
Code Review Run #463da1
Should Bito avoid suggestions like this for future reviews? (Manage Rules)