From 596f95aa15610da6d580d715334fe96216910124 Mon Sep 17 00:00:00 2001 From: Smokashi23 Date: Mon, 7 Jul 2025 14:53:33 +0530 Subject: [PATCH 1/2] - Added quarter wise logic for dyanamic engagers --- internal/api/user.go | 19 +++- internal/app/users/service.go | 35 +++++- internal/repository/postgresdb/user.go | 149 ++++++++++++++----------- internal/repository/user.go | 2 +- 4 files changed, 135 insertions(+), 70 deletions(-) diff --git a/internal/api/user.go b/internal/api/user.go index ea52f25..ba5d5b1 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -228,10 +228,24 @@ func listUsersHandler(userSvc user.Service) http.HandlerFunc { func getActiveUserListHandler(userSvc user.Service) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { - ctx := req.Context() + + quarterStr := req.URL.Query().Get("quarter") + yearStr := req.URL.Query().Get("year") + + quarter, err := strconv.Atoi(quarterStr) + if err != nil || quarter < 1 || quarter > 4 { + http.Error(rw, "Invalid quarter", http.StatusBadRequest) + return + } + year, err := strconv.Atoi(yearStr) + if err != nil || year < 2024 { + http.Error(rw, "Invalid year", http.StatusBadRequest) + return + } + log.Info(ctx, "getActiveUserListHandler: req: ", req) - resp, err := userSvc.GetActiveUserList(req.Context()) + resp, err := userSvc.GetActiveUserList(ctx, quarter, year) if err != nil { dto.ErrorRepsonse(rw, err) return @@ -241,6 +255,7 @@ func getActiveUserListHandler(userSvc user.Service) http.HandlerFunc { dto.SuccessRepsonse(rw, http.StatusOK, "Active Users list", resp) } } + func getUserByIdHandler(userSvc user.Service) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { diff --git a/internal/app/users/service.go b/internal/app/users/service.go index 2342685..62fe1a3 100644 --- a/internal/app/users/service.go +++ b/internal/app/users/service.go @@ -37,7 +37,7 @@ type Service interface { ListUsers(ctx context.Context, reqData dto.ListUsersReq) (resp dto.ListUsersResp, err error) GetUserById(ctx context.Context) (user dto.GetUserByIdResp, err error) UpdateRewardQuota(ctx context.Context) (err error) - GetActiveUserList(ctx context.Context) ([]dto.ActiveUser, error) + GetActiveUserList(ctx context.Context, quarter int, year int) ([]dto.ActiveUser, error) GetTop10Users(ctx context.Context) (users []dto.Top10User, err error) AdminLogin(ctx context.Context, loginReq dto.AdminLoginReq) (resp dto.LoginUserResp, err error) NotificationByAdmin(ctx context.Context, notificationReq dto.AdminNotificationReq) (err error) @@ -483,8 +483,9 @@ func (us *service) GetUserById(ctx context.Context) (user dto.GetUserByIdResp, e return } -func (us *service) GetActiveUserList(ctx context.Context) ([]dto.ActiveUser, error) { - activeUserDb, err := us.userRepo.GetActiveUserList(ctx, nil) +func (us *service) GetActiveUserList(ctx context.Context, quarter int, year int) ([]dto.ActiveUser, error) { + quarterStart, quarterEnd := getQuarterRangeUnixTime(quarter, year) + activeUserDb, err := us.userRepo.GetActiveUserList(ctx, nil, quarterStart, quarterEnd) if err != nil { logger.Errorf(ctx, "userService: GetActiveUserList: err: %v", err) return []dto.ActiveUser{}, err @@ -496,6 +497,34 @@ func (us *service) GetActiveUserList(ctx context.Context) ([]dto.ActiveUser, err } return res, nil } + +func getQuarterRangeUnixTime(quarter int, year int) (start int64, end int64) { + var startMonth, endMonth time.Month + startYear := year + endYear := year + switch quarter { + case 1: + startMonth = time.March + endMonth = time.June + case 2: + startMonth = time.June + endMonth = time.September + case 3: + startMonth = time.September + endMonth = time.December + case 4: + startMonth = time.December + endMonth = time.March + endYear = year + 1 // Q4 ends next year's March + default: + startMonth = time.January + endMonth = time.January + } + startTime := time.Date(startYear, startMonth, 1, 0, 0, 0, 0, time.UTC) + endTime := time.Date(endYear, endMonth, 1, 0, 0, 0, 0, time.UTC) + return startTime.UnixMilli(), endTime.UnixMilli() +} + func (us *service) UpdateRewardQuota(ctx context.Context) error { err := us.userRepo.UpdateRewardQuota(ctx, nil) return err diff --git a/internal/repository/postgresdb/user.go b/internal/repository/postgresdb/user.go index 241c7e0..982ebbf 100644 --- a/internal/repository/postgresdb/user.go +++ b/internal/repository/postgresdb/user.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "time" "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" @@ -262,72 +263,92 @@ func (us *userStore) ListUsers(ctx context.Context, reqData dto.ListUsersReq) (r return } -func (us *userStore) GetActiveUserList(ctx context.Context, tx repository.Transaction) (activeUsers []repository.ActiveUser, err error) { +func getQuarterRangeUnixTime(quarter int, year int) (start int64, end int64) { + var startMonth, endMonth time.Month + switch quarter { + case 1: + startMonth = time.March + endMonth = time.June + case 2: + startMonth = time.June + endMonth = time.September + case 3: + startMonth = time.September + endMonth = time.December + case 4: + startMonth = time.December + endMonth = time.March + year += 1 // Q4 ends next year's March + default: + startMonth = time.January + endMonth = time.January + } + startTime := time.Date(year, startMonth, 1, 0, 0, 0, 0, time.UTC) + endTime := time.Date(year, endMonth, 1, 0, 0, 0, 0, time.UTC) + return startTime.Unix(), endTime.Unix() +} + +func (us *userStore) GetActiveUserList(ctx context.Context, tx repository.Transaction, quarterStart int64, quarterEnd int64) (activeUsers []repository.ActiveUser, err error) { queryExecutor := us.InitiateQueryExecutor(tx) - afterTime := GetQuarterStartUnixTime() query := `WITH user_points AS ( - SELECT - u.id AS user_id, - COALESCE(received.total_received_appreciations, 0) AS total_received_appreciations, - COALESCE(sent.total_sent_appreciations, 0) AS total_sent_appreciations, - COALESCE(given.total_given_rewards, 0) AS total_given_rewards, - (3 * COALESCE(sent.total_sent_appreciations, 0) + 2 * COALESCE(received.total_received_appreciations, 0) + COALESCE(given.total_given_rewards, 0)) AS active_user_points - FROM - users u - LEFT JOIN - (SELECT receiver AS user_id, COUNT(*) AS total_received_appreciations - FROM appreciations - WHERE - Appreciations.is_valid = true AND appreciations.created_at >=$1 - GROUP BY receiver - ) AS received ON u.id = received.user_id - LEFT JOIN - (SELECT sender AS user_id, COUNT(*) AS total_sent_appreciations - FROM appreciations - WHERE - Appreciations.is_valid = true AND appreciations.created_at >=$2 - GROUP BY sender) AS sent ON u.id = sent.user_id - LEFT JOIN - (SELECT sender AS user_id, COUNT(*) AS total_given_rewards - FROM rewards - WHERE - rewards.created_at >=$3 - GROUP BY sender) AS given ON u.id = given.user_id - WHERE - COALESCE(received.total_received_appreciations, 0) > 0 OR - COALESCE(sent.total_sent_appreciations, 0) > 0 OR - COALESCE(given.total_given_rewards, 0) > 0 - ORDER BY - active_user_points DESC, - total_sent_appreciations DESC, - total_given_rewards DESC, - total_received_appreciations DESC -) -SELECT - up.user_id, - u.first_name , - u.last_name , - u.profile_image_url, - b.name AS badge, - COALESCE(ap.appreciation_points, 0) AS appreciation_points -FROM - user_points up -JOIN - users u ON up.user_id = u.id -LEFT JOIN - (SELECT receiver, SUM(total_reward_points) AS appreciation_points - FROM appreciations - GROUP BY receiver) AS ap ON u.id = ap.receiver -LEFT JOIN - (SELECT ub.user_id, b.name - FROM user_badges ub - JOIN badges b ON ub.badge_id = b.id - WHERE ub.id = (SELECT MAX(id) FROM user_badges WHERE user_id = ub.user_id)) AS b ON u.id = b.user_id -LIMIT 10; -` - logger.Info(ctx, "afterTime: ", afterTime) - - rows, err := queryExecutor.Query(query, afterTime, afterTime, afterTime) + SELECT + u.id AS user_id, + COALESCE(received.total_received_appreciations, 0) AS total_received_appreciations, + COALESCE(sent.total_sent_appreciations, 0) AS total_sent_appreciations, + COALESCE(given.total_given_rewards, 0) AS total_given_rewards, + (3 * COALESCE(sent.total_sent_appreciations, 0) + 2 * COALESCE(received.total_received_appreciations, 0) + COALESCE(given.total_given_rewards, 0)) AS active_user_points + FROM + users u + LEFT JOIN + (SELECT receiver AS user_id, COUNT(*) AS total_received_appreciations + FROM appreciations + WHERE appreciations.is_valid = true AND appreciations.created_at >= $1 AND appreciations.created_at < $2 + GROUP BY receiver + ) AS received ON u.id = received.user_id + LEFT JOIN + (SELECT sender AS user_id, COUNT(*) AS total_sent_appreciations + FROM appreciations + WHERE appreciations.is_valid = true AND appreciations.created_at >= $1 AND appreciations.created_at < $2 + GROUP BY sender) AS sent ON u.id = sent.user_id + LEFT JOIN + (SELECT sender AS user_id, COUNT(*) AS total_given_rewards + FROM rewards + WHERE rewards.created_at >= $1 AND rewards.created_at < $2 + GROUP BY sender) AS given ON u.id = given.user_id + WHERE + COALESCE(received.total_received_appreciations, 0) > 0 OR + COALESCE(sent.total_sent_appreciations, 0) > 0 OR + COALESCE(given.total_given_rewards, 0) > 0 + ORDER BY + active_user_points DESC, + total_sent_appreciations DESC, + total_given_rewards DESC, + total_received_appreciations DESC + ) + SELECT + up.user_id, + u.first_name , + u.last_name , + u.profile_image_url, + b.name AS badge, + COALESCE(ap.appreciation_points, 0) AS appreciation_points + FROM + user_points up + JOIN + users u ON up.user_id = u.id + LEFT JOIN + (SELECT receiver, SUM(total_reward_points) AS appreciation_points + FROM appreciations + GROUP BY receiver) AS ap ON u.id = ap.receiver + LEFT JOIN + (SELECT ub.user_id, b.name + FROM user_badges ub + JOIN badges b ON ub.badge_id = b.id + WHERE ub.id = (SELECT MAX(id) FROM user_badges WHERE user_id = ub.user_id)) AS b ON u.id = b.user_id + LIMIT 10;` + logger.Info(ctx, "quarterStart: ", quarterStart, ", quarterEnd: ", quarterEnd) + + rows, err := queryExecutor.Query(query, quarterStart, quarterEnd) if err != nil { logger.Error(ctx, "err: userStore ", err.Error()) return []repository.ActiveUser{}, err diff --git a/internal/repository/user.go b/internal/repository/user.go index c1054ff..66e82b4 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -19,7 +19,7 @@ type UserStorer interface { ListUsers(ctx context.Context, reqData dto.ListUsersReq) (resp []User, count int64, err error) UpdateRewardQuota(ctx context.Context, tx Transaction) (err error) - GetActiveUserList(ctx context.Context, tx Transaction) (activeUsers []ActiveUser, err error) + GetActiveUserList(ctx context.Context, tx Transaction, quarterStart int64, quarterEnd int64) (activeUsers []ActiveUser, err error) GetUserById(ctx context.Context, reqData dto.GetUserByIdReq) (user dto.GetUserByIdResp, err error) GetTop10Users(ctx context.Context, quarterTimestamp int64) (users []Top10Users, err error) GetGradeById(ctx context.Context, id int64) (grade Grade, err error) From c9e88794323565a2cc65dbfd05b25c6428d071cc Mon Sep 17 00:00:00 2001 From: cheemx5395 Date: Wed, 17 Jun 2026 18:02:08 +0530 Subject: [PATCH 2/2] Added dynamic engagers report download option and service --- internal/api/router.go | 3 + internal/api/user.go | 30 +++++++ internal/app/users/mocks/Service.go | 22 +++++ internal/app/users/service.go | 109 +++++++++++++++++++++--- internal/app/users/service_test.go | 13 ++- internal/repository/mocks/UserStorer.go | 38 +++++++-- internal/repository/postgresdb/user.go | 78 +++++++++++++++++ internal/repository/user.go | 12 +++ 8 files changed, 284 insertions(+), 21 deletions(-) diff --git a/internal/api/router.go b/internal/api/router.go index 125710a..b751d84 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -65,6 +65,9 @@ func NewRouter(deps app.Dependencies) *mux.Router { peerlySubrouter.Handle("/admin/reported_appreciation_report", middleware.JwtAuthMiddleware(reportedAppreciationReportHandler(deps.UserService, deps.ReportAppreciationService), constants.Admin)).Methods(http.MethodGet) + peerlySubrouter.Handle("/admin/dynamic_engagers_report", middleware.JwtAuthMiddleware(dynamicEngagersReportHandler(deps.UserService), constants.Admin)).Methods(http.MethodGet) + + //appreciations peerlySubrouter.Handle("/appreciations/{id:[0-9]+}", middleware.JwtAuthMiddleware(getAppreciationByIDHandler(deps.AppreciationService), constants.User)).Methods(http.MethodGet).Headers(versionHeader, v1) diff --git a/internal/api/user.go b/internal/api/user.go index ba5d5b1..2c7ab61 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -356,3 +356,33 @@ func reportedAppreciationReportHandler(userSvc user.Service, reportAppreciationS // dto.SuccessRepsonse(rw, 200, "Excel downloaded successfully", nil) } } + +func dynamicEngagersReportHandler(userSvc user.Service) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + quarterStr := req.URL.Query().Get("quarter") + yearStr := req.URL.Query().Get("year") + + quarter, err := strconv.Atoi(quarterStr) + if err != nil || quarter < 1 || quarter > 4 { + http.Error(rw, "Invalid quarter", http.StatusBadRequest) + return + } + year, err := strconv.Atoi(yearStr) + if err != nil || year < 2024 { + http.Error(rw, "Invalid year", http.StatusBadRequest) + return + } + + tempFileName, err := userSvc.DynamicEngagersReport(ctx, quarter, year) + if err != nil { + logger.Errorf(ctx, "dynamicEngagersReportHandler: err: %v", err) + dto.ErrorRepsonse(rw, err) + return + } + + http.ServeFile(rw, req, tempFileName) + } +} + diff --git a/internal/app/users/mocks/Service.go b/internal/app/users/mocks/Service.go index 801452c..bb75a8b 100644 --- a/internal/app/users/mocks/Service.go +++ b/internal/app/users/mocks/Service.go @@ -53,6 +53,28 @@ func (_m *Service) AllAppreciationReport(ctx context.Context, appreciations []dt return r0, r1 } +// DynamicEngagersReport provides a mock function with given fields: ctx, quarter, year +func (_m *Service) DynamicEngagersReport(ctx context.Context, quarter int, year int) (string, error) { + ret := _m.Called(ctx, quarter, year) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, int, int) string); ok { + r0 = rf(ctx, quarter, year) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int, int) error); ok { + r1 = rf(ctx, quarter, year) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + + // GetActiveUserList provides a mock function with given fields: ctx func (_m *Service) GetActiveUserList(ctx context.Context) ([]dto.ActiveUser, error) { ret := _m.Called(ctx) diff --git a/internal/app/users/service.go b/internal/app/users/service.go index 62fe1a3..f3fd458 100644 --- a/internal/app/users/service.go +++ b/internal/app/users/service.go @@ -43,6 +43,7 @@ type Service interface { NotificationByAdmin(ctx context.Context, notificationReq dto.AdminNotificationReq) (err error) AllAppreciationReport(ctx context.Context, appreciations []dto.AppreciationResponse) (tempFileName string, err error) ReportedAppreciationReport(ctx context.Context, appreciations []dto.ReportedAppreciation) (tempFileName string, err error) + DynamicEngagersReport(ctx context.Context, quarter int, year int) (tempFileName string, err error) } func NewService(userRepo repository.UserStorer) Service { @@ -533,22 +534,34 @@ func GetQuarterStartUnixTime() int64 { now := time.Now() year := now.Year() var startMonth time.Month + var startYear int switch now.Month() { case time.March, time.April, time.May: + // Q1 (Mar-May) startMonth = time.March + startYear = year case time.June, time.July, time.August: + // Q2 (Jun-Aug) startMonth = time.June + startYear = year case time.September, time.October, time.November: + // Q3 (Sep-Nov) startMonth = time.September + startYear = year case time.December: + // Q4 starts Dec of current year startMonth = time.December + startYear = year case time.January, time.February: + // Q4 continues into next year (Dec of previous year) startMonth = time.December - year = year - 1 + startYear = year - 1 + default: + startMonth = time.January + startYear = year } - - quarterStart := time.Date(year, startMonth, 1, 0, 0, 0, 0, time.UTC) - return quarterStart.Unix() * 1000 + quarterStart := time.Date(startYear, startMonth, 1, 0, 0, 0, 0, time.UTC) + return quarterStart.Unix() * 1000 } func (us *service) GetTop10Users(ctx context.Context) (users []dto.Top10User, err error) { @@ -644,7 +657,6 @@ func GetQuarterName(t time.Time) string { } } - func (us *service) AllAppreciationReport(ctx context.Context, appreciations []dto.AppreciationResponse) (tempFileName string, err error) { // Create a new Excel file @@ -659,7 +671,7 @@ func (us *service) AllAppreciationReport(ctx context.Context, appreciations []dt } // Set header - headers := []string{"Core value", "Core value description", "Appreciation description", "Sender Employee ID" ,"Sender first name", "Sender last name", "Sender designation", "Receiver Employee ID", "Receiver first name", "Receiver last name", "Receiver designation", "Total rewards", "Total reward points", "Appreciated Date", "Quarter"} + headers := []string{"Core value", "Core value description", "Appreciation description", "Sender Employee ID", "Sender first name", "Sender last name", "Sender designation", "Receiver Employee ID", "Receiver first name", "Receiver last name", "Receiver designation", "Total rewards", "Total reward points", "Appreciated Date", "Quarter"} for colIndex, header := range headers { cell := fmt.Sprintf("%c1", 'A'+colIndex) @@ -670,10 +682,10 @@ func (us *service) AllAppreciationReport(ctx context.Context, appreciations []dt for rowIndex, app := range appreciations { row := rowIndex + 2 // Starting from row 2 - createdTime := time.UnixMilli(app.CreatedAt) + createdTime := time.UnixMilli(app.CreatedAt) appreciatedAt := time.UnixMilli(app.CreatedAt).Format("02/01/2006") quarter := GetQuarterName(createdTime) - + f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), app.CoreValueName) f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), app.CoreValueDesc) f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), app.Description) @@ -719,7 +731,7 @@ func (us *service) ReportedAppreciationReport(ctx context.Context, appreciations } // Set header - headers := []string{"Core value", "Core value description", "Appreciation description", "Sender Employee ID", "Sender first name", "Sender last name", "Sender designation", "Receiver Employee ID" ,"Receiver first name", "Receiver last name", "Receiver designation", "Appreciated Date", "Reporter Emp ID","Reporting Comment", "Reported by first name", "Reported by last name", "Reported Date", "Moderator comment", "Moderator first name", "Moderator last name", "Status","Quarter"} + headers := []string{"Core value", "Core value description", "Appreciation description", "Sender Employee ID", "Sender first name", "Sender last name", "Sender designation", "Receiver Employee ID", "Receiver first name", "Receiver last name", "Receiver designation", "Appreciated Date", "Reporter Emp ID", "Reporting Comment", "Reported by first name", "Reported by last name", "Reported Date", "Moderator comment", "Moderator first name", "Moderator last name", "Status", "Quarter"} for colIndex, header := range headers { cell := fmt.Sprintf("%c1", 'A'+colIndex) f.SetCellValue(sheetName, cell, header) @@ -732,7 +744,7 @@ func (us *service) ReportedAppreciationReport(ctx context.Context, appreciations reportedAt := time.UnixMilli(app.ReportedAt).Format("02/01/2006") createdTime := time.UnixMilli(app.CreatedAt) - quarter := GetQuarterName(createdTime) + quarter := GetQuarterName(createdTime) row := rowIndex + 2 // Starting from row 2 f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), app.CoreValueName) @@ -746,7 +758,7 @@ func (us *service) ReportedAppreciationReport(ctx context.Context, appreciations f.SetCellValue(sheetName, fmt.Sprintf("I%d", row), app.ReceiverFirstName) f.SetCellValue(sheetName, fmt.Sprintf("J%d", row), app.ReceiverLastName) f.SetCellValue(sheetName, fmt.Sprintf("K%d", row), app.ReceiverDesignation) - f.SetCellValue(sheetName, fmt.Sprintf("L%d", row), appreciatedAt) + f.SetCellValue(sheetName, fmt.Sprintf("L%d", row), appreciatedAt) f.SetCellValue(sheetName, fmt.Sprintf("M%d", row), app.ReporterEmployeeID) f.SetCellValue(sheetName, fmt.Sprintf("N%d", row), app.ReportingComment) f.SetCellValue(sheetName, fmt.Sprintf("O%d", row), app.ReportedByFirstName) @@ -771,3 +783,78 @@ func (us *service) ReportedAppreciationReport(ctx context.Context, appreciations return } + +func getStandardQuarterRange(quarter int, year int) (start int64, end int64) { + var startMonth, endMonth time.Month + startYear := year + endYear := year + switch quarter { + case 1: + // Q1: March 01 - May 31 + startMonth = time.March + endMonth = time.June + case 2: + // Q2: June 01 - August 31 + startMonth = time.June + endMonth = time.September + case 3: + // Q3: September 01 - November 30 + startMonth = time.September + endMonth = time.December + case 4: + // Q4: December 01 - February 28/29 (next year) + startMonth = time.December + endMonth = time.March + endYear = year + 1 + default: + startMonth = time.January + endMonth = time.January + } + startTime := time.Date(startYear, startMonth, 1, 0, 0, 0, 0, time.UTC) + endTime := time.Date(endYear, endMonth, 1, 0, 0, 0, 0, time.UTC) + return startTime.UnixMilli(), endTime.UnixMilli() +} + +func (us *service) DynamicEngagersReport(ctx context.Context, quarter int, year int) (tempFileName string, err error) { + start, end := getStandardQuarterRange(quarter, year) + engagers, err := us.userRepo.GetDynamicEngagersReport(ctx, nil, start, end) + if err != nil { + logger.Errorf(ctx, "userService: DynamicEngagersReport: GetDynamicEngagersReport err: %v", err) + return "", err + } + + f := excelize.NewFile() + sheetName := "DynamicEngagers" + index, err := f.NewSheet(sheetName) + if err != nil { + logger.Errorf(ctx, "userService: DynamicEngagersReport: err in generating newsheet, err: %v", err) + return "", err + } + + headers := []string{"User ID", "First Name", "Last Name", "Sent", "Received", "Reward Points", "Total Points"} + for colIndex, header := range headers { + cell := fmt.Sprintf("%c1", 'A'+colIndex) + f.SetCellValue(sheetName, cell, header) + } + + for rowIndex, eng := range engagers { + row := rowIndex + 2 // Starting from row 2 + f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), eng.UserID) + f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), eng.FirstName) + f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), eng.LastName) + f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), eng.Sent) + f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), eng.Received) + f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), eng.RewardPoints) + f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), eng.TotalPoints) + } + + f.SetActiveSheet(index) + + tempFileName = fmt.Sprintf("dynamic_engagers_report_Q%d_%d-%d.xlsx", quarter, year, year+1) + if err = f.SaveAs(tempFileName); err != nil { + logger.Errorf(ctx, "userService: DynamicEngagersReport: Failed to save file: %v", err) + return "", err + } + + return tempFileName, nil +} diff --git a/internal/app/users/service_test.go b/internal/app/users/service_test.go index 007ded4..b8a9d05 100644 --- a/internal/app/users/service_test.go +++ b/internal/app/users/service_test.go @@ -9,13 +9,20 @@ import ( "github.com/joshsoftware/peerly-backend/internal/pkg/apperrors" "github.com/joshsoftware/peerly-backend/internal/pkg/constants" "github.com/joshsoftware/peerly-backend/internal/pkg/dto" + log "github.com/joshsoftware/peerly-backend/internal/pkg/logger" "github.com/joshsoftware/peerly-backend/internal/pkg/testConfig" "github.com/joshsoftware/peerly-backend/internal/repository" "github.com/joshsoftware/peerly-backend/internal/repository/mocks" + l "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +func init() { + log.Logger = l.New() +} + + func TestLoginUser(t *testing.T) { testConfig.Load() userRepo := mocks.NewUserStorer(t) @@ -482,7 +489,7 @@ func TestGetActiveUserList(t *testing.T) { name: "success", context: context.Background(), setup: func(userMock *mocks.UserStorer) { - userMock.On("GetActiveUserList", mock.Anything, mock.Anything).Return([]repository.ActiveUser{ + userMock.On("GetActiveUserList", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]repository.ActiveUser{ { ID: 55, FirstName: "Deepak", @@ -521,7 +528,7 @@ func TestGetActiveUserList(t *testing.T) { name: "failure", context: context.Background(), setup: func(userMock *mocks.UserStorer) { - userMock.On("GetActiveUserList", mock.Anything, mock.Anything).Return([]repository.ActiveUser{}, apperrors.InternalServer).Once() + userMock.On("GetActiveUserList", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]repository.ActiveUser{}, apperrors.InternalServer).Once() }, expectedResp: []dto.ActiveUser{}, expectedError: apperrors.InternalServer, @@ -533,7 +540,7 @@ func TestGetActiveUserList(t *testing.T) { test.setup(userRepo) // test service - resp, err := service.GetActiveUserList(test.context) + resp, err := service.GetActiveUserList(test.context, 1, 2026) if err != nil { assert.Equal(t, test.expectedError, err) diff --git a/internal/repository/mocks/UserStorer.go b/internal/repository/mocks/UserStorer.go index 29df5de..e95e0b7 100644 --- a/internal/repository/mocks/UserStorer.go +++ b/internal/repository/mocks/UserStorer.go @@ -71,13 +71,13 @@ func (_m *UserStorer) CreateNewUser(ctx context.Context, user dto.User) (reposit return r0, r1 } -// GetActiveUserList provides a mock function with given fields: ctx, tx -func (_m *UserStorer) GetActiveUserList(ctx context.Context, tx repository.Transaction) ([]repository.ActiveUser, error) { - ret := _m.Called(ctx, tx) +// GetActiveUserList provides a mock function with given fields: ctx, tx, quarterStart, quarterEnd +func (_m *UserStorer) GetActiveUserList(ctx context.Context, tx repository.Transaction, quarterStart int64, quarterEnd int64) ([]repository.ActiveUser, error) { + ret := _m.Called(ctx, tx, quarterStart, quarterEnd) var r0 []repository.ActiveUser - if rf, ok := ret.Get(0).(func(context.Context, repository.Transaction) []repository.ActiveUser); ok { - r0 = rf(ctx, tx) + if rf, ok := ret.Get(0).(func(context.Context, repository.Transaction, int64, int64) []repository.ActiveUser); ok { + r0 = rf(ctx, tx, quarterStart, quarterEnd) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]repository.ActiveUser) @@ -85,8 +85,8 @@ func (_m *UserStorer) GetActiveUserList(ctx context.Context, tx repository.Trans } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, repository.Transaction) error); ok { - r1 = rf(ctx, tx) + if rf, ok := ret.Get(1).(func(context.Context, repository.Transaction, int64, int64) error); ok { + r1 = rf(ctx, tx, quarterStart, quarterEnd) } else { r1 = ret.Error(1) } @@ -94,6 +94,30 @@ func (_m *UserStorer) GetActiveUserList(ctx context.Context, tx repository.Trans return r0, r1 } +// GetDynamicEngagersReport provides a mock function with given fields: ctx, tx, quarterStart, quarterEnd +func (_m *UserStorer) GetDynamicEngagersReport(ctx context.Context, tx repository.Transaction, quarterStart int64, quarterEnd int64) ([]repository.DynamicEngager, error) { + ret := _m.Called(ctx, tx, quarterStart, quarterEnd) + + var r0 []repository.DynamicEngager + if rf, ok := ret.Get(0).(func(context.Context, repository.Transaction, int64, int64) []repository.DynamicEngager); ok { + r0 = rf(ctx, tx, quarterStart, quarterEnd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.DynamicEngager) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, repository.Transaction, int64, int64) error); ok { + r1 = rf(ctx, tx, quarterStart, quarterEnd) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + + // GetAdmin provides a mock function with given fields: ctx, email func (_m *UserStorer) GetAdmin(ctx context.Context, email string) (repository.User, error) { ret := _m.Called(ctx, email) diff --git a/internal/repository/postgresdb/user.go b/internal/repository/postgresdb/user.go index 982ebbf..e65dab6 100644 --- a/internal/repository/postgresdb/user.go +++ b/internal/repository/postgresdb/user.go @@ -379,6 +379,84 @@ func (us *userStore) GetActiveUserList(ctx context.Context, tx repository.Transa return activeUsers, nil } +func (us *userStore) GetDynamicEngagersReport(ctx context.Context, tx repository.Transaction, quarterStart int64, quarterEnd int64) (engagers []repository.DynamicEngager, err error) { + queryExecutor := us.InitiateQueryExecutor(tx) + query := `WITH sent_appreciations AS ( + SELECT sender AS user_id, COUNT(*) AS sent + FROM appreciations + WHERE is_valid = true AND created_at >= $1 AND created_at < $2 + GROUP BY sender + ), + received_appreciations AS ( + SELECT receiver AS user_id, COUNT(*) AS received + FROM appreciations + WHERE is_valid = true AND created_at >= $1 AND created_at < $2 + GROUP BY receiver + ), + given_rewards AS ( + SELECT sender AS user_id, SUM(point) AS reward_points + FROM rewards + WHERE created_at >= $1 AND created_at < $2 + GROUP BY sender + ) + SELECT + u.id AS user_id, + u.first_name, + u.last_name, + COALESCE(s.sent, 0) AS sent, + COALESCE(r.received, 0) AS received, + COALESCE(g.reward_points, 0) AS reward_points, + (3 * COALESCE(s.sent, 0) + 2 * COALESCE(r.received, 0) + COALESCE(g.reward_points, 0)) AS total_points + FROM + users u + LEFT JOIN + sent_appreciations s ON u.id = s.user_id + LEFT JOIN + received_appreciations r ON u.id = r.user_id + LEFT JOIN + given_rewards g ON u.id = g.user_id + WHERE + COALESCE(s.sent, 0) > 0 OR + COALESCE(r.received, 0) > 0 OR + COALESCE(g.reward_points, 0) > 0 + ORDER BY + total_points DESC, + first_name ASC, + last_name ASC;` + + rows, err := queryExecutor.Query(query, quarterStart, quarterEnd) + if err != nil { + logger.Error(ctx, "err: userStore GetDynamicEngagersReport query: ", err.Error()) + return nil, err + } + defer rows.Close() + + for rows.Next() { + var user repository.DynamicEngager + if err := rows.Scan( + &user.UserID, + &user.FirstName, + &user.LastName, + &user.Sent, + &user.Received, + &user.RewardPoints, + &user.TotalPoints, + ); err != nil { + logger.Error(ctx, "err: userStore GetDynamicEngagersReport scan: ", err.Error()) + return nil, err + } + engagers = append(engagers, user) + } + + if err = rows.Err(); err != nil { + logger.Error(ctx, "err: userStore GetDynamicEngagersReport rows: ", err.Error()) + return nil, err + } + + return engagers, nil +} + + func (us *userStore) UpdateRewardQuota(ctx context.Context, tx repository.Transaction) (err error) { queryExecutor := us.InitiateQueryExecutor(tx) diff --git a/internal/repository/user.go b/internal/repository/user.go index 66e82b4..09f2ce5 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -20,6 +20,7 @@ type UserStorer interface { UpdateRewardQuota(ctx context.Context, tx Transaction) (err error) GetActiveUserList(ctx context.Context, tx Transaction, quarterStart int64, quarterEnd int64) (activeUsers []ActiveUser, err error) + GetDynamicEngagersReport(ctx context.Context, tx Transaction, quarterStart int64, quarterEnd int64) (engagers []DynamicEngager, err error) GetUserById(ctx context.Context, reqData dto.GetUserByIdReq) (user dto.GetUserByIdResp, err error) GetTop10Users(ctx context.Context, quarterTimestamp int64) (users []Top10Users, err error) GetGradeById(ctx context.Context, id int64) (grade Grade, err error) @@ -80,3 +81,14 @@ type UserBadgeDetails struct { BadgeName sql.NullString `db:"badge_name"` BadgePoints int32 `db:"badge_points"` } + +type DynamicEngager struct { + UserID int64 `db:"user_id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + Sent int64 `db:"sent"` + Received int64 `db:"received"` + RewardPoints int64 `db:"reward_points"` + TotalPoints int64 `db:"total_points"` +} +