From ee7aa7f67414d046f7ee886642059ee4fd92df02 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 3 Jul 2025 15:08:01 +0530 Subject: [PATCH 1/5] create a transaction entry for each contribution --- internal/app/contribution/domain.go | 12 ++++++ internal/app/contribution/service.go | 32 +++++++++++++++- internal/app/dependencies.go | 5 ++- internal/app/transaction/domain.go | 15 ++++++++ internal/app/transaction/service.go | 32 ++++++++++++++++ internal/pkg/apperrors/errors.go | 4 +- internal/repository/domain.go | 12 ++++++ internal/repository/transaction.go | 57 ++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 internal/app/transaction/domain.go create mode 100644 internal/app/transaction/service.go create mode 100644 internal/repository/transaction.go diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 295c851..3c90423 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -49,3 +49,15 @@ type ContributionScore struct { CreatedAt time.Time UpdatedAt time.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"` +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index b8e832b..fecba8e 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -8,6 +8,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" 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" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/repository" @@ -52,6 +53,7 @@ type service struct { contributionRepository repository.ContributionRepository repositoryService repoService.Service userService user.Service + transactionService transaction.Service httpClient *http.Client } @@ -62,14 +64,16 @@ 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) + CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) } -func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { return &service{ bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, userService: userService, + transactionService: transactionService, httpClient: httpClient, } } @@ -151,12 +155,18 @@ func (s *service) ProcessEachContribution(ctx context.Context, contribution Cont return err } - _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) + createdContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) if err != nil { slog.Error("error creating contribution", "error", err) return err } + _, err = s.CreateContributionTransaction(ctx, user.Id, createdContribution) + if err != nil { + slog.Error("error creating transaction for current contribution", "error", err) + return err + } + return nil } @@ -280,3 +290,21 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } + +func (s *service) CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) { + transactionInfo := Transaction{ + UserId: userId, + ContributionId: contributionDetails.Id, + IsRedeemed: false, + IsGained: true, + TransactedBalance: contributionDetails.BalanceChange, + TransactedAt: contributionDetails.ContributedAt, + } + transaction, err := s.transactionService.CreateTransaction(ctx, transaction.Transaction(transactionInfo)) + if err != nil { + slog.Error("error creating transaction for current contribution", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index f390342..0316cea 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -9,6 +9,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" "github.com/joshsoftware/code-curiosity-2025/internal/app/github" 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" "github.com/joshsoftware/code-curiosity-2025/internal/config" @@ -29,13 +30,15 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userRepository := repository.NewUserRepository(db) contributionRepository := repository.NewContributionRepository(db) repositoryRepository := repository.NewRepositoryRepository(db) + transactionRepository := repository.NewTransactionRepository(db) userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) githubService := github.NewService(appCfg, httpClient) repositoryService := repoService.NewService(repositoryRepository, githubService) - contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, httpClient) + transactionService := transaction.NewService(transactionRepository) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, httpClient) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) diff --git a/internal/app/transaction/domain.go b/internal/app/transaction/domain.go new file mode 100644 index 0000000..54a8c95 --- /dev/null +++ b/internal/app/transaction/domain.go @@ -0,0 +1,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"` +} diff --git a/internal/app/transaction/service.go b/internal/app/transaction/service.go new file mode 100644 index 0000000..5f61572 --- /dev/null +++ b/internal/app/transaction/service.go @@ -0,0 +1,32 @@ +package transaction + +import ( + "context" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + transactionRepository repository.TransactionRepository +} + +type Service interface { + CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) +} + +func NewService(transactionRepository repository.TransactionRepository) Service { + return &service{ + transactionRepository: transactionRepository, + } +} + +func (s *service) CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) { + transaction, err := s.transactionRepository.CreateTransaction(ctx, nil, repository.Transaction(transactionInfo)) + if err != nil { + slog.Error("error occured while creating transaction", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 4efe8cf..4d7fe27 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -45,6 +45,8 @@ var ( 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") + + ErrTransactionCreationFailed = errors.New("error failed to create transaction") ) func MapError(err error) (statusCode int, errMessage string) { @@ -55,7 +57,7 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound, ErrRepoNotFound: + case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound: 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 eb4ba97..1a94621 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -65,3 +65,15 @@ type ContributionScore struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +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"` +} diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go new file mode 100644 index 0000000..e98a600 --- /dev/null +++ b/internal/repository/transaction.go @@ -0,0 +1,57 @@ +package repository + +import ( + "context" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type transactionRepository struct { + BaseRepository +} + +type TransactionRepository interface { + RepositoryTransaction + CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) +} + +func NewTransactionRepository(db *sqlx.DB) TransactionRepository { + return &transactionRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createTransactionQuery = `INSERT INTO transactions ( + user_id, + contribution_id, + is_redeemed, + is_gained, + transacted_balance, + transacted_at + ) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING *` +) + +func (tr *transactionRepository) CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) { + executer := tr.BaseRepository.initiateQueryExecuter(tx) + + var transaction Transaction + err := executer.GetContext(ctx, &transaction, createTransactionQuery, + transactionInfo.UserId, + transactionInfo.ContributionId, + transactionInfo.IsRedeemed, + transactionInfo.IsGained, + transactionInfo.TransactedBalance, + transactionInfo.TransactedAt, + ) + if err != nil { + slog.Error("error occured while creating transaction", "error", err) + return Transaction{}, apperrors.ErrTransactionCreationFailed + } + + return transaction, nil +} From 244c44d931bf76b4fac24240e1042c1cbdb4dad6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 12:09:14 +0530 Subject: [PATCH 2/5] update user balance with each transaction created --- internal/app/dependencies.go | 2 +- internal/app/transaction/service.go | 29 +++++++++++++++++++++++++-- internal/app/user/domain.go | 16 +++++++++++++-- internal/app/user/service.go | 24 ++++++++++++++++++++++ internal/pkg/middleware/middleware.go | 14 +++++++++++++ internal/repository/user.go | 15 ++++++++++++++ 6 files changed, 95 insertions(+), 5 deletions(-) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 0316cea..9eda099 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -37,7 +37,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque bigqueryService := bigquery.NewService(client, userRepository) githubService := github.NewService(appCfg, httpClient) repositoryService := repoService.NewService(repositoryRepository, githubService) - transactionService := transaction.NewService(transactionRepository) + transactionService := transaction.NewService(transactionRepository, userService) contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, httpClient) authHandler := auth.NewHandler(authService, appCfg) diff --git a/internal/app/transaction/service.go b/internal/app/transaction/service.go index 5f61572..3606cce 100644 --- a/internal/app/transaction/service.go +++ b/internal/app/transaction/service.go @@ -4,29 +4,54 @@ import ( "context" "log/slog" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type service struct { transactionRepository repository.TransactionRepository + userService user.Service } type Service interface { CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) } -func NewService(transactionRepository repository.TransactionRepository) Service { +func NewService(transactionRepository repository.TransactionRepository, userService user.Service) Service { return &service{ transactionRepository: transactionRepository, + userService: userService, } } func (s *service) CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) { - transaction, err := s.transactionRepository.CreateTransaction(ctx, nil, repository.Transaction(transactionInfo)) + tx, err := s.transactionRepository.BeginTx(ctx) + if err != nil { + slog.Error("failed to start transaction creation") + return Transaction{}, err + } + + ctx = middleware.EmbedTxInContext(ctx, tx) + + defer func() { + if txErr := s.transactionRepository.HandleTransaction(ctx, tx, err); txErr != nil { + slog.Error("failed to handle transaction", "error", txErr) + err = txErr + } + }() + + transaction, err := s.transactionRepository.CreateTransaction(ctx, tx, repository.Transaction(transactionInfo)) if err != nil { slog.Error("error occured while creating transaction", "error", err) return Transaction{}, err } + err = s.userService.UpdateUserCurrentBalance(ctx, user.Transaction(transaction)) + if err != nil { + slog.Error("error occured while updating user current balance", "error", err) + return Transaction{}, err + } + return Transaction(transaction), nil } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index e2d9e6c..471f0b1 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -18,8 +18,8 @@ type User struct { Password string `json:"password"` IsDeleted bool `json:"is_deleted"` DeletedAt sql.NullTime `json:"deleted_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateUserRequestBody struct { @@ -33,3 +33,15 @@ type CreateUserRequestBody struct { type Email struct { Email string `json:"email"` } + +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"` +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 93b8572..6dad743 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -18,6 +18,7 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error + UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error } func NewService(userRepository repository.UserRepository) Service { @@ -74,3 +75,26 @@ func (s *service) UpdateUserEmail(ctx context.Context, email string) error { return nil } + +func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error { + user, err := s.GetUserById(ctx, transaction.UserId) + if err != nil { + slog.Error("error obtaining user by id", "error", err) + return err + } + + user.CurrentBalance += transaction.TransactedBalance + + tx, ok := middleware.ExtractTxFromContext(ctx) + if !ok { + slog.Error("error obtaining tx from context") + } + + err = s.userRepository.UpdateUserCurrentBalance(ctx, tx, repository.User(user)) + if err != nil { + slog.Error("error updating user current balance", "error", err) + return err + } + + return nil +} diff --git a/internal/pkg/middleware/middleware.go b/internal/pkg/middleware/middleware.go index 3ece089..4c40789 100644 --- a/internal/pkg/middleware/middleware.go +++ b/internal/pkg/middleware/middleware.go @@ -5,12 +5,17 @@ import ( "net/http" "strings" + "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jwt" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) +type txKeyType struct{} + +var txKey = txKeyType{} + type contextKey string const ( @@ -18,6 +23,15 @@ const ( IsAdminKey contextKey = "isAdmin" ) +func EmbedTxInContext(ctx context.Context, tx *sqlx.Tx) context.Context { + return context.WithValue(ctx, txKey, tx) +} + +func ExtractTxFromContext(ctx context.Context) (*sqlx.Tx, bool) { + tx, ok := ctx.Value(txKey).(*sqlx.Tx) + return tx, ok +} + func CorsMiddleware(next http.Handler, appCfg config.AppConfig) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", appCfg.ClientURL) diff --git a/internal/repository/user.go b/internal/repository/user.go index 254da1b..a3a748a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -22,6 +22,7 @@ type UserRepository interface { CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) + UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -48,6 +49,8 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" getAllUsersGithubIdQuery = "SELECT github_id from users" + + updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -127,3 +130,15 @@ func (ur *userRepository) GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) return githubIds, nil } + +func (ur *userRepository) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateUserCurrentBalanceQuery, user.CurrentBalance, time.Now(), user.Id) + if err != nil { + slog.Error("failed to update user balance change", "error", err) + return apperrors.ErrInternalServer + } + + return nil +} From 3a8296612db6677bdb34b862d628b517a03d0554 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 18:25:35 +0530 Subject: [PATCH 3/5] refactor code to reduce cognitive complexity of ProcessEachContribution --- internal/app/contribution/service.go | 103 +++++++++++---------------- internal/app/repository/domain.go | 12 ++++ internal/app/repository/service.go | 22 +++++- internal/app/transaction/domain.go | 13 ++++ internal/app/transaction/service.go | 52 ++++++++++++++ internal/repository/transaction.go | 22 ++++++ 6 files changed, 161 insertions(+), 63 deletions(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index fecba8e..f927207 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -60,11 +60,12 @@ type service struct { type Service interface { ProcessFetchedContributions(ctx context.Context) error ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error + GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) + HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) - CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -116,54 +117,28 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } func (s *service) ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error { - _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) - if err == nil { - return nil - } - - if err != apperrors.ErrContributionNotFound { - slog.Error("error fetching contribution by github event id", "error", err) - return err - } - - var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) - if err == nil { - repositoryId = repoFetched.Id - } else if err == apperrors.ErrRepoNotFound { - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) - if err != nil { - slog.Error("error creating repository", "error", err) + obtainedContribution, err := s.GetContributionByGithubEventId(ctx, contribution.ID) + if err != nil { + if err == apperrors.ErrContributionNotFound { + obtainedRepository, err := s.repositoryService.HandleRepositoryCreation(ctx, repoService.ContributionResponse(contribution)) + if err != nil { + slog.Error("error handling repository creation", "error", err) + return err + } + obtainedContribution, err = s.HandleContributionCreation(ctx, obtainedRepository.Id, contribution) + if err != nil { + slog.Error("error handling contribution creation", "error", err) + return err + } + } else { + slog.Error("error fetching contribution by github event id", "error", err) return err } - - repositoryId = repositoryCreated.Id - } else { - slog.Error("error fetching repo by repo id", "error", err) - return err - } - - user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) - if err != nil { - slog.Error("error getting user id", "error", err) - return err - } - - contributionType, err := s.GetContributionType(ctx, contribution) - if err != nil { - slog.Error("error getting contribution type", "error", err) - return err - } - - createdContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) - if err != nil { - slog.Error("error creating contribution", "error", err) - return err } - _, err = s.CreateContributionTransaction(ctx, user.Id, createdContribution) + _, err = s.transactionService.HandleTransactionCreation(ctx, transaction.Contribution(obtainedContribution)) if err != nil { - slog.Error("error creating transaction for current contribution", "error", err) + slog.Error("error handling transaction creation", "error", err) return err } @@ -256,6 +231,28 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin return Contribution(contributionResponse), nil } +func (s *service) HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) { + user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) + if err != nil { + slog.Error("error getting user id", "error", err) + return Contribution{}, err + } + + contributionType, err := s.GetContributionType(ctx, contribution) + if err != nil { + slog.Error("error getting contribution type", "error", err) + return Contribution{}, err + } + + obtainedContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryID, user.Id) + if err != nil { + slog.Error("error creating contribution", "error", err) + return Contribution{}, err + } + + return obtainedContribution, nil +} + func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) { contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, contributionType) if err != nil { @@ -290,21 +287,3 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } - -func (s *service) CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) { - transactionInfo := Transaction{ - UserId: userId, - ContributionId: contributionDetails.Id, - IsRedeemed: false, - IsGained: true, - TransactedBalance: contributionDetails.BalanceChange, - TransactedAt: contributionDetails.ContributedAt, - } - transaction, err := s.transactionService.CreateTransaction(ctx, transaction.Transaction(transactionInfo)) - if err != nil { - slog.Error("error creating transaction for current contribution", "error", err) - return Transaction{}, err - } - - return Transaction(transaction), nil -} diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 60b7400..208f297 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -24,6 +24,18 @@ type FetchUsersContributedReposResponse struct { TotalCoinsEarned int } +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} + type Contribution struct { Id int UserId int diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index bbae42d..e73b75f 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/app/github" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -19,6 +20,7 @@ type Service interface { GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) + HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) @@ -77,6 +79,24 @@ func (s *service) CreateRepository(ctx context.Context, repoGithubId int, Contri return Repository(repositoryCreated), nil } +func (s *service) HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) { + obtainedRepository, err := s.GetRepoByGithubId(ctx, contribution.RepoID) + if err != nil { + if err == apperrors.ErrRepoNotFound { + obtainedRepository, err = s.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) + if err != nil { + slog.Error("error creating repository", "error", err) + return Repository{}, err + } + } else { + slog.Error("error fetching repo by repo id", "error", err) + return Repository{}, err + } + } + + return obtainedRepository, nil +} + func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) if err != nil { @@ -89,7 +109,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C for i, usersContributedRepo := range usersContributedRepos { fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) - contributedRepoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) + contributedRepoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, usersContributedRepo.LanguagesUrl) if err != nil { slog.Error("error fetching languages for repository", "error", err) return nil, err diff --git a/internal/app/transaction/domain.go b/internal/app/transaction/domain.go index 54a8c95..b8989b6 100644 --- a/internal/app/transaction/domain.go +++ b/internal/app/transaction/domain.go @@ -13,3 +13,16 @@ type Transaction struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + GithubEventId string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/transaction/service.go b/internal/app/transaction/service.go index 3606cce..59b4f17 100644 --- a/internal/app/transaction/service.go +++ b/internal/app/transaction/service.go @@ -5,6 +5,7 @@ import ( "log/slog" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "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" ) @@ -16,6 +17,9 @@ type service struct { type Service interface { CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) + GetTransactionByContributionId(ctx context.Context, contributionId int) (Transaction, error) + CreateTransactionForContribution(ctx context.Context, contribution Contribution) (Transaction, error) + HandleTransactionCreation(ctx context.Context, contribution Contribution) (Transaction, error) } func NewService(transactionRepository repository.TransactionRepository, userService user.Service) Service { @@ -55,3 +59,51 @@ func (s *service) CreateTransaction(ctx context.Context, transactionInfo Transac return Transaction(transaction), nil } + +func (s *service) GetTransactionByContributionId(ctx context.Context, contributionId int) (Transaction, error) { + transaction, err := s.transactionRepository.GetTransactionByContributionId(ctx, nil, contributionId) + if err != nil { + slog.Error("error fetching transaction using contribution id", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} + +func (s *service) CreateTransactionForContribution(ctx context.Context, contribution Contribution) (Transaction, error) { + transactionInfo := Transaction{ + UserId: contribution.UserId, + ContributionId: contribution.Id, + IsRedeemed: false, + IsGained: true, + TransactedBalance: contribution.BalanceChange, + TransactedAt: contribution.ContributedAt, + } + transaction, err := s.CreateTransaction(ctx, transactionInfo) + if err != nil { + slog.Error("error creating transaction for current contribution", "error", err) + return Transaction{}, err + } + + return transaction, nil +} + +func (s *service) HandleTransactionCreation(ctx context.Context, contribution Contribution) (Transaction, error) { + var transaction Transaction + + transaction, err := s.GetTransactionByContributionId(ctx, contribution.Id) + if err != nil { + if err == apperrors.ErrTransactionNotFound { + transaction, err = s.CreateTransactionForContribution(ctx, contribution) + if err != nil { + slog.Error("error creating transaction for exisiting contribution", "error", err) + return Transaction{}, err + } + } else { + slog.Error("error fetching transaction", "error", err) + return Transaction{}, err + } + } + + return transaction, nil +} diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index e98a600..b154565 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -2,6 +2,8 @@ package repository import ( "context" + "database/sql" + "errors" "log/slog" "github.com/jmoiron/sqlx" @@ -15,6 +17,7 @@ type transactionRepository struct { type TransactionRepository interface { RepositoryTransaction CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) + GetTransactionByContributionId(ctx context.Context, tx *sqlx.Tx, contributionId int) (Transaction, error) } func NewTransactionRepository(db *sqlx.DB) TransactionRepository { @@ -34,6 +37,8 @@ const ( ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *` + + getTransactionByContributionIdQuery = `SELECT * from transactions where contribution_id=$1` ) func (tr *transactionRepository) CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) { @@ -55,3 +60,20 @@ func (tr *transactionRepository) CreateTransaction(ctx context.Context, tx *sqlx return transaction, nil } + +func (tr *transactionRepository) GetTransactionByContributionId(ctx context.Context, tx *sqlx.Tx, contributionId int) (Transaction, error) { + executer := tr.BaseRepository.initiateQueryExecuter(tx) + + var transaction Transaction + err := executer.GetContext(ctx, &transaction, getTransactionByContributionIdQuery, contributionId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("transaction for the contribution id does not exist", "error", err) + return Transaction{}, apperrors.ErrTransactionNotFound + } + slog.Error("error fetching transaction using contributionid", "error", err) + return Transaction{}, err + } + + return transaction, nil +} From 65519857deb16d9a8ef0ddb3fa63a8b21534e1ec Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 18:26:22 +0530 Subject: [PATCH 4/5] fix incorrect merge conflict: missing fetchusecontribution router --- internal/app/router.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/app/router.go b/internal/app/router.go index bb97fd7..612efd1 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("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, 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)) From 6c1f24c32a74ca58b09c349df28764cfdfb4c578 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 18:27:28 +0530 Subject: [PATCH 5/5] use client from dependencies instead of new client in github handler --- internal/app/dependencies.go | 2 +- internal/app/github/service.go | 8 ++++---- internal/app/repository/handler.go | 11 ++++------- internal/pkg/apperrors/errors.go | 1 + 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 9eda099..86d4c3d 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -42,7 +42,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) - repositoryHandler := repoService.NewHandler(repositoryService) + repositoryHandler := repoService.NewHandler(repositoryService, githubService) contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ diff --git a/internal/app/github/service.go b/internal/app/github/service.go index 8b81912..16637c0 100644 --- a/internal/app/github/service.go +++ b/internal/app/github/service.go @@ -18,8 +18,8 @@ type service struct { type Service interface { configureGithubApiHeaders() map[string]string FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) - FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) - FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) + FetchRepositoryLanguages(ctx context.Context, getRepoLanguagesURL string) (RepoLanguages, error) + FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) } func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { @@ -54,7 +54,7 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails return repoDetails, nil } -func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { +func (s *service) FetchRepositoryLanguages(ctx context.Context, getRepoLanguagesURL string) (RepoLanguages, error) { headers := s.configureGithubApiHeaders() body, err := utils.DoGet(s.httpClient, getRepoLanguagesURL, headers) @@ -73,7 +73,7 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Cli return repoLanguages, nil } -func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { +func (s *service) FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { headers := s.configureGithubApiHeaders() body, err := utils.DoGet(s.httpClient, getRepoContributorsURl, headers) diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 5edbb29..f040a31 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -22,9 +22,10 @@ type Handler interface { FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) } -func NewHandler(repositoryService Service) Handler { +func NewHandler(repositoryService Service, githubService github.Service) Handler { return &handler{ repositoryService: repositoryService, + githubService: githubService, } } @@ -69,8 +70,6 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - client := &http.Client{} - repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { @@ -88,7 +87,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http return } - repoContributors, err := h.githubService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) + repoContributors, err := h.githubService.FetchRepositoryContributors(ctx, repoDetails.ContributorsUrl) if err != nil { slog.Error("error fetching repo contributors", "error", err) status, errorMessage := apperrors.MapError(err) @@ -125,8 +124,6 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - client := &http.Client{} - repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { @@ -144,7 +141,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ return } - repoLanguages, err := h.githubService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) + repoLanguages, err := h.githubService.FetchRepositoryLanguages(ctx, repoDetails.LanguagesUrl) if err != nil { slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 4d7fe27..31c9e49 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -47,6 +47,7 @@ var ( ErrContributionNotFound = errors.New("contribution not found") ErrTransactionCreationFailed = errors.New("error failed to create transaction") + ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") ) func MapError(err error) (statusCode int, errMessage string) {