diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 3c90423..9316048 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -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"` + TotalCoins int `db:"total_coins"` + Month time.Time `db:"month"` +} diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 2c2045d..85c5e7d 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -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) { + ctx := r.Context() + + month := r.URL.Query().Get("month") + + contributionTypeSummaryForMonth, err := h.contributionService.GetContributionTypeSummaryForMonth(ctx, month) + if err != nil { + slog.Error("error fetching contribution type summary for month") + 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) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index f927207..88d4d77 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -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) { + month, err := time.Parse("2006-01", monthParam) + if err != nil { + slog.Error("error parsing month query parameter", "error", err) + return nil, err + } + + contributionTypes, err := s.contributionRepository.GetAllContributionTypes(ctx, nil) + 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)) + } + + return contributionTypeSummaryForMonth, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 02f9c7e..c61e717 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -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)) 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)) diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 31c9e49..930ce96 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -37,14 +37,16 @@ var ( ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") - ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") - ErrNextContribution = errors.New("error while loading next bigquery contribution") - ErrContributionCreationFailed = errors.New("failed to create contrbitution") - ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") - ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") - ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") - ErrFetchingContribution = errors.New("error fetching contribution by github repo id") - ErrContributionNotFound = errors.New("contribution not found") + ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") + ErrNextContribution = errors.New("error while loading next bigquery contribution") + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") + ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") + ErrFetchingContribution = errors.New("error fetching contribution by github repo id") + ErrContributionNotFound = errors.New("contribution not found") + ErrFetchingContributionTypes = errors.New("failed to fetch all contribution types") + ErrNoContributionForContributionType = errors.New("contribution for contribution type does not exist") ErrTransactionCreationFailed = errors.New("error failed to create transaction") ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index eb1d1a5..697028b 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -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) + + 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 +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index d52494b..9d75663 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -85,3 +85,10 @@ type LeaderboardUser struct { CurrentBalance int `db:"current_balance"` Rank int `db:"rank"` } + +type ContributionTypeSummary struct { + ContributionType string `db:"contribution_type"` + ContributionCount int `db:"contribution_count"` + TotalCoins int `db:"total_coins"` + Month time.Time `db:"month"` +}