-
Notifications
You must be signed in to change notification settings - Fork 0
Implement monthly overview feature #30
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/leaderboard-service
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 |
|---|---|---|
|
|
@@ -61,3 +61,10 @@ type Transaction struct { | |
| CreatedAt time.Time `db:"created_at"` | ||
| UpdatedAt time.Time `db:"updated_at"` | ||
| } | ||
|
|
||
| type ContributionTypeSummary struct { | ||
| ContributionType string `db:"contribution_type"` | ||
| ContributionCount int `db:"contribution_count"` | ||
|
Comment on lines
+66
to
+67
Collaborator
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. rename to
Collaborator
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. Also why this struct as db tags? |
||
| TotalCoins int `db:"total_coins"` | ||
| Month time.Time `db:"month"` | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ type handler struct { | |
|
|
||
| type Handler interface { | ||
| FetchUserContributions(w http.ResponseWriter, r *http.Request) | ||
| GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) | ||
| } | ||
|
|
||
| func NewHandler(contributionService Service) Handler { | ||
|
|
@@ -35,3 +36,19 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) | |
|
|
||
| response.WriteJson(w, http.StatusOK, "user contributions fetched successfully", userContributions) | ||
| } | ||
|
|
||
| func (h *handler) GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) { | ||
|
Collaborator
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. Name like ListMonlyContributionSummary would be more preferable |
||
| ctx := r.Context() | ||
|
|
||
| month := r.URL.Query().Get("month") | ||
|
|
||
|
Comment on lines
+43
to
+44
Collaborator
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. validate month before passing it to service layer |
||
| contributionTypeSummaryForMonth, err := h.contributionService.GetContributionTypeSummaryForMonth(ctx, month) | ||
| if err != nil { | ||
| slog.Error("error fetching contribution type summary for month") | ||
|
Collaborator
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. also print error in the message |
||
| status, errorMessage := apperrors.MapError(err) | ||
| response.WriteJson(w, status, errorMessage, nil) | ||
| return | ||
| } | ||
|
|
||
| response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", contributionTypeSummaryForMonth) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,8 +3,10 @@ package contribution | |
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "log/slog" | ||
| "net/http" | ||
| "time" | ||
|
|
||
| "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" | ||
| repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" | ||
|
|
@@ -66,6 +68,7 @@ type Service interface { | |
| GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) | ||
| FetchUserContributions(ctx context.Context) ([]Contribution, error) | ||
| GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) | ||
| GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) | ||
| } | ||
|
|
||
| func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { | ||
|
|
@@ -287,3 +290,35 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven | |
|
|
||
| return Contribution(contribution), nil | ||
| } | ||
|
|
||
| func (s *service) GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) { | ||
|
Collaborator
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. Instead of taking a string, it would be good to take in month and year as int separatly in the body and pass to service.
Collaborator
Author
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. I can do GET /api/v1/user/overview?year=2025&month=06 this way. But to pass 'int' to service I need to convert string to integer in handler. This conversion is logic so shouldn't that be part of service layer?
Collaborator
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. The service should accept year and month as integers. Ideally, here repo layer should do the conversion to string, as the specific format is only used by the query. It will keep the service and handler layers decoupled from formatting concerns, making the service easier to use, callers won't need to be aware of the exact format required by the database. |
||
| month, err := time.Parse("2006-01", monthParam) | ||
| if err != nil { | ||
| slog.Error("error parsing month query parameter", "error", err) | ||
|
Collaborator
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. In Go programs, printing an error typically indicates that we've handled it. Therefore, it's best practice to either print the error or return it, but not both.
Collaborator
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. to add more detail you can always wrap errors |
||
| return nil, err | ||
| } | ||
|
|
||
| contributionTypes, err := s.contributionRepository.GetAllContributionTypes(ctx, nil) | ||
|
Collaborator
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. As a practice in go, Use List as prefix and not get whenever fetching an array. |
||
| if err != nil { | ||
| slog.Error("error fetching contribution types", "error", err) | ||
| return nil, err | ||
| } | ||
|
|
||
| var contributionTypeSummaryForMonth []ContributionTypeSummary | ||
|
|
||
| for _, contributionType := range contributionTypes { | ||
| contributionTypeSummary, err := s.contributionRepository.GetContributionTypeSummaryForMonth(ctx, nil, contributionType.ContributionType, month) | ||
| if err != nil { | ||
| if errors.Is(err, apperrors.ErrNoContributionForContributionType) { | ||
| contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary{ContributionType: contributionType.ContributionType}) | ||
| continue | ||
| } | ||
| slog.Error("error fetching contribution type summary", "error", err) | ||
| return nil, err | ||
| } | ||
|
|
||
| contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary(contributionTypeSummary)) | ||
| } | ||
|
Comment on lines
+307
to
+321
Collaborator
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. We could fetch all the Contribution summary for this month in a single db call then categorise them. This would save a lot of db calls. |
||
|
|
||
| return contributionTypeSummaryForMonth, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ func NewRouter(deps Dependencies) http.Handler { | |
| router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) | ||
|
|
||
| router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) | ||
| router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.GetContributionTypeSummaryForMonth, deps.AppCfg)) | ||
|
Collaborator
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. With standard naming convention it should be |
||
|
|
||
| router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) | ||
| router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "database/sql" | ||
| "errors" | ||
| "log/slog" | ||
| "time" | ||
|
|
||
| "github.com/jmoiron/sqlx" | ||
| "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" | ||
|
|
@@ -21,6 +22,8 @@ type ContributionRepository interface { | |
| GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) | ||
| FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) | ||
| GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) | ||
| GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) | ||
| GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) | ||
| } | ||
|
|
||
| func NewContributionRepository(db *sqlx.DB) ContributionRepository { | ||
|
|
@@ -47,7 +50,23 @@ const ( | |
|
|
||
| fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` | ||
|
|
||
| GetContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1` | ||
| getContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1` | ||
|
|
||
| getAllContributionTypesQuery = `SELECT * from contribution_score` | ||
|
|
||
| GetContributionTypeSummaryForMonthQuery = ` | ||
| SELECT | ||
| DATE_TRUNC('month', contributed_at) AS month, | ||
| contribution_type, | ||
| COUNT(*) AS contribution_count, | ||
| SUM(balance_change) AS total_coins | ||
| FROM contributions | ||
| WHERE user_id = $1 | ||
| AND contribution_type = $2 | ||
| AND contributed_at >= DATE_TRUNC('month', $3::timestamptz) | ||
| AND contributed_at < DATE_TRUNC('month', $3::timestamptz) + INTERVAL '1 month' | ||
| GROUP BY | ||
| month, contribution_type;` | ||
| ) | ||
|
|
||
| func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { | ||
|
|
@@ -114,7 +133,7 @@ func (cr *contributionRepository) GetContributionByGithubEventId(ctx context.Con | |
| executer := cr.BaseRepository.initiateQueryExecuter(tx) | ||
|
|
||
| var contribution Contribution | ||
| err := executer.GetContext(ctx, &contribution, GetContributionByGithubEventIdQuery, githubEventId) | ||
| err := executer.GetContext(ctx, &contribution, getContributionByGithubEventIdQuery, githubEventId) | ||
| if err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| slog.Error("contribution not found", "error", err) | ||
|
|
@@ -127,3 +146,40 @@ func (cr *contributionRepository) GetContributionByGithubEventId(ctx context.Con | |
| return contribution, nil | ||
|
|
||
| } | ||
|
|
||
| func (cr *contributionRepository) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) { | ||
| executer := cr.BaseRepository.initiateQueryExecuter(tx) | ||
|
|
||
| var contributionTypes []ContributionScore | ||
| err := executer.SelectContext(ctx, &contributionTypes, getAllContributionTypesQuery) | ||
| if err != nil { | ||
| slog.Error("error fetching all contribution types", "error", err) | ||
| return nil, apperrors.ErrFetchingContributionTypes | ||
| } | ||
|
|
||
| return contributionTypes, nil | ||
| } | ||
|
|
||
| func (cr *contributionRepository) GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) { | ||
| userIdValue := ctx.Value(middleware.UserIdKey) | ||
|
Collaborator
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. userId should be passed by the service layer, and ctx values are generally accessed in the handler layer itself. |
||
|
|
||
| userId, ok := userIdValue.(int) | ||
| if !ok { | ||
| slog.Error("error obtaining user id from context") | ||
| return ContributionTypeSummary{}, apperrors.ErrInternalServer | ||
| } | ||
|
|
||
| executer := cr.BaseRepository.initiateQueryExecuter(tx) | ||
|
|
||
| var contributionTypeSummary ContributionTypeSummary | ||
| err := executer.GetContext(ctx, &contributionTypeSummary, GetContributionTypeSummaryForMonthQuery, userId, contributionType, month) | ||
| if err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| return ContributionTypeSummary{}, apperrors.ErrNoContributionForContributionType | ||
| } | ||
| slog.Error("error fetching contribution summary for contribution type", "error", err) | ||
| return ContributionTypeSummary{}, apperrors.ErrInternalServer | ||
| } | ||
|
|
||
| return contributionTypeSummary, nil | ||
| } | ||
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.
update name