diff --git a/db/interface.go b/db/interface.go index 1ffd6fa12..8ae99fd09 100644 --- a/db/interface.go +++ b/db/interface.go @@ -213,4 +213,6 @@ type Database interface { GetTicketsWithoutGroup() ([]Tickets, error) UpdateTicketsWithoutGroup(ticket Tickets) error ProcessUpdateTicketsWithoutGroup() + GetNewHunters(r PaymentDateRange) int64 + TotalPeopleByPeriod(r PaymentDateRange) int64 } diff --git a/db/metrics.go b/db/metrics.go index 4db855860..997de887a 100644 --- a/db/metrics.go +++ b/db/metrics.go @@ -5,6 +5,7 @@ import ( "math" "net/http" "strings" + "time" "github.com/stakwork/sphinx-tribes/utils" ) @@ -17,6 +18,38 @@ func (db database) TotalPeopleByDateRange(r PaymentDateRange) int64 { return count } +func (db database) TotalPeopleByPeriod(r PaymentDateRange) int64 { + var count int64 + + // convert timestamp string to int64 + timestamp, err := utils.ConvertStringToInt(r.StartDate) + if err != nil { + fmt.Println("Error parsing date:", err) + } + + // Convert the timestamp to a time.Time object + t := time.Unix(int64(timestamp), 0) + + // Format the time as a human-readable string + formattedTime := t.Format("2006-01-02 15:04:05") + + db.db.Model(&Person{}).Where("created < ?", formattedTime).Count(&count) + return count +} + +func (db database) GetNewHunters(r PaymentDateRange) int64 { + var totalCount int64 + var newHunters int64 = 0 + var huntersByPeriod int64 = db.TotalPeopleByPeriod(r) + + db.db.Model(&Person{}).Count(&totalCount) + + if huntersByPeriod > 0 && totalCount > huntersByPeriod { + newHunters = totalCount - huntersByPeriod + } + return newHunters +} + func (db database) TotalWorkspacesByDateRange(r PaymentDateRange) int64 { var count int64 db.db.Model(&Organization{}).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Count(&count) diff --git a/db/structs.go b/db/structs.go index bad3aa492..d0d7a9aec 100644 --- a/db/structs.go +++ b/db/structs.go @@ -899,6 +899,8 @@ type BountyMetrics struct { AverageCompleted uint `json:"average_completed"` UniqueHuntersPaid int64 `json:"unique_hunters_paid"` NewHuntersPaid int64 `json:"new_hunters_paid"` + NewHunters int64 `json:"new_hunters"` + NewHuntersByPeriod int64 `json:"new_hunters_by_period"` } type MetricsBountyCsv struct { diff --git a/handlers/metrics.go b/handlers/metrics.go index 9c85fcf98..296fc1131 100644 --- a/handlers/metrics.go +++ b/handlers/metrics.go @@ -98,6 +98,13 @@ func PeopleMetrics(w http.ResponseWriter, r *http.Request) { request := db.PaymentDateRange{} body, err := io.ReadAll(r.Body) + + if err != nil { + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode("Request body not accepted") + return + } + r.Body.Close() err = json.Unmarshal(body, &request) @@ -127,6 +134,13 @@ func (mh *metricHandler) BountyMetrics(w http.ResponseWriter, r *http.Request) { request := db.PaymentDateRange{} body, err := io.ReadAll(r.Body) + + if err != nil { + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode("Request body not accepted") + return + } + r.Body.Close() err = json.Unmarshal(body, &request) @@ -163,6 +177,8 @@ func (mh *metricHandler) BountyMetrics(w http.ResponseWriter, r *http.Request) { avgCompletedDays := mh.db.AverageCompletedTime(request, workspace) uniqueHuntersPaid := mh.db.TotalHuntersPaid(request, workspace) newHuntersPaid := mh.db.NewHuntersPaid(request, workspace) + newHunters := mh.db.GetNewHunters(request) + peopleByPeriod := mh.db.TotalPeopleByPeriod(request) bountyMetrics := db.BountyMetrics{ BountiesPosted: totalBountiesPosted, @@ -176,6 +192,8 @@ func (mh *metricHandler) BountyMetrics(w http.ResponseWriter, r *http.Request) { AverageCompleted: avgCompletedDays, UniqueHuntersPaid: uniqueHuntersPaid, NewHuntersPaid: newHuntersPaid, + NewHunters: newHunters, + NewHuntersByPeriod: peopleByPeriod, } if db.RedisError == nil && db.RedisClient != nil { diff --git a/handlers/metrics_test.go b/handlers/metrics_test.go index f8cdd7b37..e02b341c7 100644 --- a/handlers/metrics_test.go +++ b/handlers/metrics_test.go @@ -4,13 +4,14 @@ import ( "bytes" "context" "encoding/json" - "github.com/google/uuid" - "github.com/lib/pq" "net/http" "net/http/httptest" "strconv" "testing" + "github.com/google/uuid" + "github.com/lib/pq" + "fmt" "time" @@ -154,6 +155,8 @@ func TestBountyMetrics(t *testing.T) { avgCompletedDays := uint(0) uniqueHuntersPaid := int64(2) newHuntersPaid := int64(2) + newHunters := db.TestDB.GetNewHunters(dateRange) + peopleByPeriod := db.TestDB.TotalPeopleByPeriod(dateRange) expectedMetricRes := db.BountyMetrics{ BountiesPosted: totalBountiesPosted, @@ -167,6 +170,8 @@ func TestBountyMetrics(t *testing.T) { AverageCompleted: avgCompletedDays, UniqueHuntersPaid: uniqueHuntersPaid, NewHuntersPaid: newHuntersPaid, + NewHunters: newHunters, + NewHuntersByPeriod: peopleByPeriod, } var res db.BountyMetrics diff --git a/mocks/Database.go b/mocks/Database.go index e55417a2e..d8d2a4b4b 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -4866,6 +4866,52 @@ func (_c *Database_GetLnUser_Call) RunAndReturn(run func(string) int64) *Databas return _c } +// GetNewHunters provides a mock function with given fields: r +func (_m *Database) GetNewHunters(r db.PaymentDateRange) int64 { + ret := _m.Called(r) + + if len(ret) == 0 { + panic("no return value specified for GetNewHunters") + } + + var r0 int64 + if rf, ok := ret.Get(0).(func(db.PaymentDateRange) int64); ok { + r0 = rf(r) + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// Database_GetNewHunters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNewHunters' +type Database_GetNewHunters_Call struct { + *mock.Call +} + +// GetNewHunters is a helper method to define mock.On call +// - r db.PaymentDateRange +func (_e *Database_Expecter) GetNewHunters(r interface{}) *Database_GetNewHunters_Call { + return &Database_GetNewHunters_Call{Call: _e.mock.On("GetNewHunters", r)} +} + +func (_c *Database_GetNewHunters_Call) Run(run func(r db.PaymentDateRange)) *Database_GetNewHunters_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(db.PaymentDateRange)) + }) + return _c +} + +func (_c *Database_GetNewHunters_Call) Return(_a0 int64) *Database_GetNewHunters_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_GetNewHunters_Call) RunAndReturn(run func(db.PaymentDateRange) int64) *Database_GetNewHunters_Call { + _c.Call.Return(run) + return _c +} + // GetNextBountyByCreated provides a mock function with given fields: r func (_m *Database) GetNextBountyByCreated(r *http.Request) (uint, error) { ret := _m.Called(r) @@ -8938,6 +8984,52 @@ func (_c *Database_TotalPaidBounties_Call) RunAndReturn(run func(db.PaymentDateR return _c } +// TotalPeopleByPeriod provides a mock function with given fields: r +func (_m *Database) TotalPeopleByPeriod(r db.PaymentDateRange) int64 { + ret := _m.Called(r) + + if len(ret) == 0 { + panic("no return value specified for TotalPeopleByPeriod") + } + + var r0 int64 + if rf, ok := ret.Get(0).(func(db.PaymentDateRange) int64); ok { + r0 = rf(r) + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// Database_TotalPeopleByPeriod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TotalPeopleByPeriod' +type Database_TotalPeopleByPeriod_Call struct { + *mock.Call +} + +// TotalPeopleByPeriod is a helper method to define mock.On call +// - r db.PaymentDateRange +func (_e *Database_Expecter) TotalPeopleByPeriod(r interface{}) *Database_TotalPeopleByPeriod_Call { + return &Database_TotalPeopleByPeriod_Call{Call: _e.mock.On("TotalPeopleByPeriod", r)} +} + +func (_c *Database_TotalPeopleByPeriod_Call) Run(run func(r db.PaymentDateRange)) *Database_TotalPeopleByPeriod_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(db.PaymentDateRange)) + }) + return _c +} + +func (_c *Database_TotalPeopleByPeriod_Call) Return(_a0 int64) *Database_TotalPeopleByPeriod_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_TotalPeopleByPeriod_Call) RunAndReturn(run func(db.PaymentDateRange) int64) *Database_TotalPeopleByPeriod_Call { + _c.Call.Return(run) + return _c +} + // TotalSatsPaid provides a mock function with given fields: r, workspace func (_m *Database) TotalSatsPaid(r db.PaymentDateRange, workspace string) uint { ret := _m.Called(r, workspace) diff --git a/routes/index.go b/routes/index.go index 27d4931c8..4b727df43 100644 --- a/routes/index.go +++ b/routes/index.go @@ -142,35 +142,35 @@ func getFromAuth(path string) (*extractResponse, error) { } // Middleware to handle InternalServerError -func internalServerErrorHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rr := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK} - next.ServeHTTP(rr, r) - - if rr.statusCode == http.StatusInternalServerError { - fmt.Printf("Internal Server Error: %s %s\n", r.Method, r.URL.Path) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} +// func internalServerErrorHandler(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// rr := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK} +// next.ServeHTTP(rr, r) + +// if rr.statusCode == http.StatusInternalServerError { +// fmt.Printf("Internal Server Error: %s %s\n", r.Method, r.URL.Path) +// http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +// } +// }) +// } // Custom ResponseWriter to capture status codes -type responseRecorder struct { - http.ResponseWriter - statusCode int -} +// type responseRecorder struct { +// http.ResponseWriter +// statusCode int +// } -func (rr *responseRecorder) WriteHeader(code int) { - rr.statusCode = code - rr.ResponseWriter.WriteHeader(code) -} +// func (rr *responseRecorder) WriteHeader(code int) { +// rr.statusCode = code +// rr.ResponseWriter.WriteHeader(code) +// } func initChi() *chi.Mux { r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.Logger) r.Use(middleware.Recoverer) - r.Use(internalServerErrorHandler) + // r.Use(internalServerErrorHandler) cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},