diff --git a/LICENSE b/LICENSE index a86baf9..59ad580 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/Makefile b/Makefile index 0863305..a414e30 100644 --- a/Makefile +++ b/Makefile @@ -81,4 +81,5 @@ help: @echo " dev - Run development server with hot reload" @echo " bench - Run benchmarks" @echo " security - Check for vulnerabilities" - @echo " help - Show this help message" \ No newline at end of file + @echo " help - Show this help message" + \ No newline at end of file diff --git a/generate-changelog.rb b/generate-changelog.rb index c649852..85bd29b 100755 --- a/generate-changelog.rb +++ b/generate-changelog.rb @@ -752,4 +752,4 @@ def self.run(args = ARGV) # Run the CLI if this file is executed directly if __FILE__ == $0 CLI.run -end \ No newline at end of file +end diff --git a/internal/handler/covid_handler.go b/internal/handler/covid_handler.go index 458707a..f1de751 100644 --- a/internal/handler/covid_handler.go +++ b/internal/handler/covid_handler.go @@ -26,14 +26,19 @@ func NewCovidHandler(covidService service.CovidService, db *database.DB) *CovidH // GetNationalCases godoc // // @Summary Get national COVID-19 cases -// @Description Retrieve national COVID-19 cases data with optional date range filtering and sorting +// @Description Retrieve national COVID-19 cases data with optional date range filtering, sorting, and pagination // @Tags national // @Accept json // @Produce json +// @Param limit query integer false "Records per page (default: 50, max: 1000)" +// @Param offset query integer false "Records to skip (default: 0)" +// @Param page query integer false "Page number (1-based, alternative to offset)" +// @Param all query boolean false "Return all data without pagination" // @Param start_date query string false "Start date (YYYY-MM-DD)" // @Param end_date query string false "End date (YYYY-MM-DD)" // @Param sort query string false "Sort by field:order (e.g., date:desc, positive:asc). Default: date:asc" -// @Success 200 {object} Response{data=[]models.NationalCaseResponse} +// @Success 200 {object} Response{data=models.PaginatedResponse{data=[]models.NationalCaseResponse}} "Paginated response" +// @Success 200 {object} Response{data=[]models.NationalCaseResponse} "All data response when all=true" // @Failure 400 {object} Response // @Failure 429 {object} Response "Rate limit exceeded" // @Failure 500 {object} Response @@ -45,31 +50,66 @@ func NewCovidHandler(covidService service.CovidService, db *database.DB) *CovidH func (h *CovidHandler) GetNationalCases(w http.ResponseWriter, r *http.Request) { startDate := r.URL.Query().Get("start_date") endDate := r.URL.Query().Get("end_date") + all := utils.ParseBoolQueryParam(r, "all") // Parse sort parameters (default: date ascending) sortParams := utils.ParseSortParam(r, "date") - if startDate != "" && endDate != "" { - cases, err := h.covidService.GetNationalCasesByDateRangeSorted(startDate, endDate, sortParams) + // Handle pagination parameters + limit, offset := utils.ParsePaginationParams(r) + + // Return all data without pagination if "all" is true + if all { + if startDate != "" && endDate != "" { + cases, err := h.covidService.GetNationalCasesByDateRangeSorted(startDate, endDate, sortParams) + if err != nil { + writeErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + responseData := models.TransformSliceToResponse(cases) + writeSuccessResponse(w, responseData) + return + } + + cases, err := h.covidService.GetNationalCasesSorted(sortParams) if err != nil { writeErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - // Transform to new response structure responseData := models.TransformSliceToResponse(cases) writeSuccessResponse(w, responseData) return } - cases, err := h.covidService.GetNationalCasesSorted(sortParams) + // Return paginated data + if startDate != "" && endDate != "" { + cases, total, err := h.covidService.GetNationalCasesByDateRangePaginatedSorted(startDate, endDate, limit, offset, sortParams) + if err != nil { + writeErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + responseData := models.TransformSliceToResponse(cases) + pagination := models.CalculatePaginationMeta(limit, offset, total) + paginatedResponse := models.PaginatedResponse{ + Data: responseData, + Pagination: pagination, + } + writeSuccessResponse(w, paginatedResponse) + return + } + + cases, total, err := h.covidService.GetNationalCasesPaginatedSorted(limit, offset, sortParams) if err != nil { writeErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - - // Transform to new response structure responseData := models.TransformSliceToResponse(cases) - writeSuccessResponse(w, responseData) + pagination := models.CalculatePaginationMeta(limit, offset, total) + paginatedResponse := models.PaginatedResponse{ + Data: responseData, + Pagination: pagination, + } + writeSuccessResponse(w, paginatedResponse) } // GetLatestNationalCase godoc @@ -145,6 +185,7 @@ func (h *CovidHandler) GetProvinces(w http.ResponseWriter, r *http.Request) { // @Param provinceId path string false "Province ID (e.g., '31' for Jakarta)" // @Param limit query integer false "Records per page (default: 50, max: 1000)" // @Param offset query integer false "Records to skip (default: 0)" +// @Param page query integer false "Page number (1-based, alternative to offset)" // @Param all query boolean false "Return all data without pagination" // @Param start_date query string false "Start date (YYYY-MM-DD)" // @Param end_date query string false "End date (YYYY-MM-DD)" @@ -160,8 +201,7 @@ func (h *CovidHandler) GetProvinceCases(w http.ResponseWriter, r *http.Request) provinceID := vars["provinceId"] // Parse query parameters - limit := utils.ParseIntQueryParam(r, "limit", 50) - offset := utils.ParseIntQueryParam(r, "offset", 0) + limit, offset := utils.ParsePaginationParams(r) all := utils.ParseBoolQueryParam(r, "all") startDate := r.URL.Query().Get("start_date") endDate := r.URL.Query().Get("end_date") @@ -169,9 +209,6 @@ func (h *CovidHandler) GetProvinceCases(w http.ResponseWriter, r *http.Request) // Parse sort parameters (default: date ascending) sortParams := utils.ParseSortParam(r, "date") - // Validate pagination params - limit, offset = utils.ValidatePaginationParams(limit, offset) - if provinceID == "" { // Handle all provinces cases if all { diff --git a/internal/handler/covid_handler_test.go b/internal/handler/covid_handler_test.go index 4dc6c40..f2161a6 100644 --- a/internal/handler/covid_handler_test.go +++ b/internal/handler/covid_handler_test.go @@ -140,6 +140,16 @@ func (m *MockCovidService) GetAllProvinceCasesByDateRangePaginatedSorted(startDa return args.Get(0).([]models.ProvinceCaseWithDate), args.Int(1), args.Error(2) } +func (m *MockCovidService) GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + args := m.Called(limit, offset, sortParams) + return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) +} + +func (m *MockCovidService) GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + args := m.Called(startDate, endDate, limit, offset, sortParams) + return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) +} + func TestCovidHandler_GetNationalCases(t *testing.T) { mockService := new(MockCovidService) handler := NewCovidHandler(mockService, nil) @@ -150,7 +160,7 @@ func TestCovidHandler_GetNationalCases(t *testing.T) { mockService.On("GetNationalCasesSorted", utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - req, err := http.NewRequest("GET", "/api/v1/national", nil) + req, err := http.NewRequest("GET", "/api/v1/national?all=true", nil) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -177,7 +187,7 @@ func TestCovidHandler_GetNationalCases_WithDateRange(t *testing.T) { mockService.On("GetNationalCasesByDateRangeSorted", "2020-03-01", "2020-03-31", utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - req, err := http.NewRequest("GET", "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31", nil) + req, err := http.NewRequest("GET", "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31&all=true", nil) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -199,7 +209,7 @@ func TestCovidHandler_GetNationalCases_ServiceError(t *testing.T) { mockService.On("GetNationalCasesSorted", utils.SortParams{Field: "date", Order: "asc"}).Return([]models.NationalCase{}, errors.New("database error")) - req, err := http.NewRequest("GET", "/api/v1/national", nil) + req, err := http.NewRequest("GET", "/api/v1/national?all=true", nil) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -216,6 +226,73 @@ func TestCovidHandler_GetNationalCases_ServiceError(t *testing.T) { mockService.AssertExpectations(t) } +func TestCovidHandler_GetNationalCases_Paginated(t *testing.T) { + mockService := new(MockCovidService) + handler := NewCovidHandler(mockService, nil) + + expectedCases := []models.NationalCase{ + {ID: 1, Positive: 100, Recovered: 80, Deceased: 5}, + {ID: 2, Positive: 110, Recovered: 85, Deceased: 6}, + } + + mockService.On("GetNationalCasesPaginatedSorted", 10, 0, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, 2, nil) + + req, err := http.NewRequest("GET", "/api/v1/national?limit=10", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler.GetNationalCases(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response Response + err = json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "success", response.Status) + assert.NotNil(t, response.Data) + + // Check that it's a paginated response + paginatedData, ok := response.Data.(map[string]interface{}) + assert.True(t, ok) + assert.Contains(t, paginatedData, "data") + assert.Contains(t, paginatedData, "pagination") + + mockService.AssertExpectations(t) +} + +func TestCovidHandler_GetNationalCases_WithDateRangePaginated(t *testing.T) { + mockService := new(MockCovidService) + handler := NewCovidHandler(mockService, nil) + + expectedCases := []models.NationalCase{ + {ID: 1, Positive: 100, Date: time.Date(2020, 3, 15, 0, 0, 0, 0, time.UTC)}, + } + + mockService.On("GetNationalCasesByDateRangePaginatedSorted", "2020-03-01", "2020-03-31", 20, 10, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, 1, nil) + + req, err := http.NewRequest("GET", "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31&limit=20&offset=10", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler.GetNationalCases(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response Response + err = json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "success", response.Status) + assert.NotNil(t, response.Data) + + // Check that it's a paginated response + paginatedData, ok := response.Data.(map[string]interface{}) + assert.True(t, ok) + assert.Contains(t, paginatedData, "data") + assert.Contains(t, paginatedData, "pagination") + + mockService.AssertExpectations(t) +} + func TestCovidHandler_GetLatestNationalCase(t *testing.T) { mockService := new(MockCovidService) handler := NewCovidHandler(mockService, nil) diff --git a/internal/models/province_case.go b/internal/models/province_case.go index 7815fd4..4c59f04 100644 --- a/internal/models/province_case.go +++ b/internal/models/province_case.go @@ -9,17 +9,17 @@ type ProvinceCase struct { Positive int64 `json:"positive" db:"positive"` Recovered int64 `json:"recovered" db:"recovered"` Deceased int64 `json:"deceased" db:"deceased"` - PersonUnderObservation int64 `json:"person_under_observation" db:"person_under_observation"` - FinishedPersonUnderObservation int64 `json:"finished_person_under_observation" db:"finished_person_under_observation"` - PersonUnderSupervision int64 `json:"person_under_supervision" db:"person_under_supervision"` - FinishedPersonUnderSupervision int64 `json:"finished_person_under_supervision" db:"finished_person_under_supervision"` + PersonUnderObservation *int64 `json:"person_under_observation" db:"person_under_observation"` + FinishedPersonUnderObservation *int64 `json:"finished_person_under_observation" db:"finished_person_under_observation"` + PersonUnderSupervision *int64 `json:"person_under_supervision" db:"person_under_supervision"` + FinishedPersonUnderSupervision *int64 `json:"finished_person_under_supervision" db:"finished_person_under_supervision"` CumulativePositive int64 `json:"cumulative_positive" db:"cumulative_positive"` CumulativeRecovered int64 `json:"cumulative_recovered" db:"cumulative_recovered"` CumulativeDeceased int64 `json:"cumulative_deceased" db:"cumulative_deceased"` - CumulativePersonUnderObservation int64 `json:"cumulative_person_under_observation" db:"cumulative_person_under_observation"` - CumulativeFinishedPersonUnderObservation int64 `json:"cumulative_finished_person_under_observation" db:"cumulative_finished_person_under_observation"` - CumulativePersonUnderSupervision int64 `json:"cumulative_person_under_supervision" db:"cumulative_person_under_supervision"` - CumulativeFinishedPersonUnderSupervision int64 `json:"cumulative_finished_person_under_supervision" db:"cumulative_finished_person_under_supervision"` + CumulativePersonUnderObservation *int64 `json:"cumulative_person_under_observation" db:"cumulative_person_under_observation"` + CumulativeFinishedPersonUnderObservation *int64 `json:"cumulative_finished_person_under_observation" db:"cumulative_finished_person_under_observation"` + CumulativePersonUnderSupervision *int64 `json:"cumulative_person_under_supervision" db:"cumulative_person_under_supervision"` + CumulativeFinishedPersonUnderSupervision *int64 `json:"cumulative_finished_person_under_supervision" db:"cumulative_finished_person_under_supervision"` Rt *float64 `json:"rt" db:"rt"` RtUpper *float64 `json:"rt_upper" db:"rt_upper"` RtLower *float64 `json:"rt_lower" db:"rt_lower"` diff --git a/internal/models/province_case_response.go b/internal/models/province_case_response.go index 2cfe08c..da3ad08 100644 --- a/internal/models/province_case_response.go +++ b/internal/models/province_case_response.go @@ -70,9 +70,17 @@ func (pc *ProvinceCase) TransformToResponse(date time.Time) ProvinceCaseResponse dailyActive := pc.Positive - pc.Recovered - pc.Deceased cumulativeActive := pc.CumulativePositive - pc.CumulativeRecovered - pc.CumulativeDeceased - // Calculate active under observation and supervision - activePersonUnderObservation := pc.CumulativePersonUnderObservation - pc.CumulativeFinishedPersonUnderObservation - activePersonUnderSupervision := pc.CumulativePersonUnderSupervision - pc.CumulativeFinishedPersonUnderSupervision + // Helper function to safely get int64 value from pointer + safeInt64 := func(ptr *int64) int64 { + if ptr == nil { + return 0 + } + return *ptr + } + + // Calculate active under observation and supervision (with null safety) + activePersonUnderObservation := safeInt64(pc.CumulativePersonUnderObservation) - safeInt64(pc.CumulativeFinishedPersonUnderObservation) + activePersonUnderSupervision := safeInt64(pc.CumulativePersonUnderSupervision) - safeInt64(pc.CumulativeFinishedPersonUnderSupervision) // Build response response := ProvinceCaseResponse{ @@ -84,12 +92,12 @@ func (pc *ProvinceCase) TransformToResponse(date time.Time) ProvinceCaseResponse Deceased: pc.Deceased, Active: dailyActive, ODP: DailyObservationData{ - Active: pc.PersonUnderObservation - pc.FinishedPersonUnderObservation, - Finished: pc.FinishedPersonUnderObservation, + Active: safeInt64(pc.PersonUnderObservation) - safeInt64(pc.FinishedPersonUnderObservation), + Finished: safeInt64(pc.FinishedPersonUnderObservation), }, PDP: DailySupervisionData{ - Active: pc.PersonUnderSupervision - pc.FinishedPersonUnderSupervision, - Finished: pc.FinishedPersonUnderSupervision, + Active: safeInt64(pc.PersonUnderSupervision) - safeInt64(pc.FinishedPersonUnderSupervision), + Finished: safeInt64(pc.FinishedPersonUnderSupervision), }, }, Cumulative: ProvinceCumulativeCases{ @@ -99,13 +107,13 @@ func (pc *ProvinceCase) TransformToResponse(date time.Time) ProvinceCaseResponse Active: cumulativeActive, ODP: ObservationData{ Active: activePersonUnderObservation, - Finished: pc.CumulativeFinishedPersonUnderObservation, - Total: pc.CumulativePersonUnderObservation, + Finished: safeInt64(pc.CumulativeFinishedPersonUnderObservation), + Total: safeInt64(pc.CumulativePersonUnderObservation), }, PDP: SupervisionData{ Active: activePersonUnderSupervision, - Finished: pc.CumulativeFinishedPersonUnderSupervision, - Total: pc.CumulativePersonUnderSupervision, + Finished: safeInt64(pc.CumulativeFinishedPersonUnderSupervision), + Total: safeInt64(pc.CumulativePersonUnderSupervision), }, }, Statistics: ProvinceCaseStatistics{ diff --git a/internal/models/province_case_response_test.go b/internal/models/province_case_response_test.go index 88c832a..5cd00a8 100644 --- a/internal/models/province_case_response_test.go +++ b/internal/models/province_case_response_test.go @@ -13,6 +13,36 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { rtUpper := 1.8 rtLower := 1.2 + // Helper variables for nullable int64 fields (test case 1) + personUnderObservation1 := int64(25) + finishedPersonUnderObservation1 := int64(20) + personUnderSupervision1 := int64(30) + finishedPersonUnderSupervision1 := int64(25) + cumulativePersonUnderObservation1 := int64(800) + cumulativeFinishedPersonUnderObservation1 := int64(750) + cumulativePersonUnderSupervision1 := int64(600) + cumulativeFinishedPersonUnderSupervision1 := int64(580) + + // Helper variables for nullable int64 fields (test case 2) + personUnderObservation2 := int64(15) + finishedPersonUnderObservation2 := int64(10) + personUnderSupervision2 := int64(20) + finishedPersonUnderSupervision2 := int64(15) + cumulativePersonUnderObservation2 := int64(400) + cumulativeFinishedPersonUnderObservation2 := int64(350) + cumulativePersonUnderSupervision2 := int64(300) + cumulativeFinishedPersonUnderSupervision2 := int64(290) + + // Helper variables for nullable int64 fields (test case 3 - zeros) + personUnderObservation3 := int64(0) + finishedPersonUnderObservation3 := int64(0) + personUnderSupervision3 := int64(0) + finishedPersonUnderSupervision3 := int64(0) + cumulativePersonUnderObservation3 := int64(0) + cumulativeFinishedPersonUnderObservation3 := int64(0) + cumulativePersonUnderSupervision3 := int64(0) + cumulativeFinishedPersonUnderSupervision3 := int64(0) + tests := []struct { name string provinceCase ProvinceCase @@ -28,17 +58,17 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { Positive: 150, Recovered: 120, Deceased: 10, - PersonUnderObservation: 25, - FinishedPersonUnderObservation: 20, - PersonUnderSupervision: 30, - FinishedPersonUnderSupervision: 25, + PersonUnderObservation: &personUnderObservation1, + FinishedPersonUnderObservation: &finishedPersonUnderObservation1, + PersonUnderSupervision: &personUnderSupervision1, + FinishedPersonUnderSupervision: &finishedPersonUnderSupervision1, CumulativePositive: 5000, CumulativeRecovered: 4500, CumulativeDeceased: 300, - CumulativePersonUnderObservation: 800, - CumulativeFinishedPersonUnderObservation: 750, - CumulativePersonUnderSupervision: 600, - CumulativeFinishedPersonUnderSupervision: 580, + CumulativePersonUnderObservation: &cumulativePersonUnderObservation1, + CumulativeFinishedPersonUnderObservation: &cumulativeFinishedPersonUnderObservation1, + CumulativePersonUnderSupervision: &cumulativePersonUnderSupervision1, + CumulativeFinishedPersonUnderSupervision: &cumulativeFinishedPersonUnderSupervision1, Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -108,17 +138,17 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { Positive: 100, Recovered: 80, Deceased: 5, - PersonUnderObservation: 15, - FinishedPersonUnderObservation: 10, - PersonUnderSupervision: 20, - FinishedPersonUnderSupervision: 15, + PersonUnderObservation: &personUnderObservation2, + FinishedPersonUnderObservation: &finishedPersonUnderObservation2, + PersonUnderSupervision: &personUnderSupervision2, + FinishedPersonUnderSupervision: &finishedPersonUnderSupervision2, CumulativePositive: 2000, CumulativeRecovered: 1800, CumulativeDeceased: 100, - CumulativePersonUnderObservation: 400, - CumulativeFinishedPersonUnderObservation: 350, - CumulativePersonUnderSupervision: 300, - CumulativeFinishedPersonUnderSupervision: 290, + CumulativePersonUnderObservation: &cumulativePersonUnderObservation2, + CumulativeFinishedPersonUnderObservation: &cumulativeFinishedPersonUnderObservation2, + CumulativePersonUnderSupervision: &cumulativePersonUnderSupervision2, + CumulativeFinishedPersonUnderSupervision: &cumulativeFinishedPersonUnderSupervision2, Rt: nil, RtUpper: nil, RtLower: nil, @@ -188,17 +218,17 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { Positive: 0, Recovered: 0, Deceased: 0, - PersonUnderObservation: 0, - FinishedPersonUnderObservation: 0, - PersonUnderSupervision: 0, - FinishedPersonUnderSupervision: 0, + PersonUnderObservation: &personUnderObservation3, + FinishedPersonUnderObservation: &finishedPersonUnderObservation3, + PersonUnderSupervision: &personUnderSupervision3, + FinishedPersonUnderSupervision: &finishedPersonUnderSupervision3, CumulativePositive: 0, CumulativeRecovered: 0, CumulativeDeceased: 0, - CumulativePersonUnderObservation: 0, - CumulativeFinishedPersonUnderObservation: 0, - CumulativePersonUnderSupervision: 0, - CumulativeFinishedPersonUnderSupervision: 0, + CumulativePersonUnderObservation: &cumulativePersonUnderObservation3, + CumulativeFinishedPersonUnderObservation: &cumulativeFinishedPersonUnderObservation3, + CumulativePersonUnderSupervision: &cumulativePersonUnderSupervision3, + CumulativeFinishedPersonUnderSupervision: &cumulativeFinishedPersonUnderSupervision3, Rt: nil, RtUpper: nil, RtLower: nil, @@ -283,17 +313,17 @@ func TestProvinceCaseWithDate_TransformToResponse(t *testing.T) { Positive: 50, Recovered: 40, Deceased: 2, - PersonUnderObservation: 10, - FinishedPersonUnderObservation: 8, - PersonUnderSupervision: 12, - FinishedPersonUnderSupervision: 10, + PersonUnderObservation: &[]int64{10}[0], + FinishedPersonUnderObservation: &[]int64{8}[0], + PersonUnderSupervision: &[]int64{12}[0], + FinishedPersonUnderSupervision: &[]int64{10}[0], CumulativePositive: 3000, CumulativeRecovered: 2700, CumulativeDeceased: 200, - CumulativePersonUnderObservation: 500, - CumulativeFinishedPersonUnderObservation: 450, - CumulativePersonUnderSupervision: 350, - CumulativeFinishedPersonUnderSupervision: 320, + CumulativePersonUnderObservation: &[]int64{500}[0], + CumulativeFinishedPersonUnderObservation: &[]int64{450}[0], + CumulativePersonUnderSupervision: &[]int64{350}[0], + CumulativeFinishedPersonUnderSupervision: &[]int64{320}[0], Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -377,17 +407,17 @@ func TestTransformProvinceCaseSliceToResponse(t *testing.T) { Positive: 100, Recovered: 80, Deceased: 5, - PersonUnderObservation: 20, - FinishedPersonUnderObservation: 15, - PersonUnderSupervision: 25, - FinishedPersonUnderSupervision: 20, + PersonUnderObservation: &[]int64{20}[0], + FinishedPersonUnderObservation: &[]int64{15}[0], + PersonUnderSupervision: &[]int64{25}[0], + FinishedPersonUnderSupervision: &[]int64{20}[0], CumulativePositive: 1000, CumulativeRecovered: 800, CumulativeDeceased: 50, - CumulativePersonUnderObservation: 200, - CumulativeFinishedPersonUnderObservation: 180, - CumulativePersonUnderSupervision: 250, - CumulativeFinishedPersonUnderSupervision: 230, + CumulativePersonUnderObservation: &[]int64{200}[0], + CumulativeFinishedPersonUnderObservation: &[]int64{180}[0], + CumulativePersonUnderSupervision: &[]int64{250}[0], + CumulativeFinishedPersonUnderSupervision: &[]int64{230}[0], Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -406,17 +436,17 @@ func TestTransformProvinceCaseSliceToResponse(t *testing.T) { Positive: 50, Recovered: 45, Deceased: 2, - PersonUnderObservation: 10, - FinishedPersonUnderObservation: 8, - PersonUnderSupervision: 12, - FinishedPersonUnderSupervision: 10, + PersonUnderObservation: &[]int64{10}[0], + FinishedPersonUnderObservation: &[]int64{8}[0], + PersonUnderSupervision: &[]int64{12}[0], + FinishedPersonUnderSupervision: &[]int64{10}[0], CumulativePositive: 1050, CumulativeRecovered: 845, CumulativeDeceased: 52, - CumulativePersonUnderObservation: 210, - CumulativeFinishedPersonUnderObservation: 188, - CumulativePersonUnderSupervision: 262, - CumulativeFinishedPersonUnderSupervision: 240, + CumulativePersonUnderObservation: &[]int64{210}[0], + CumulativeFinishedPersonUnderObservation: &[]int64{188}[0], + CumulativePersonUnderSupervision: &[]int64{262}[0], + CumulativeFinishedPersonUnderSupervision: &[]int64{240}[0], Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -468,17 +498,17 @@ func TestProvinceCaseResponse_JSONStructure(t *testing.T) { Positive: 150, Recovered: 120, Deceased: 10, - PersonUnderObservation: 25, - FinishedPersonUnderObservation: 20, - PersonUnderSupervision: 30, - FinishedPersonUnderSupervision: 25, + PersonUnderObservation: &[]int64{25}[0], + FinishedPersonUnderObservation: &[]int64{20}[0], + PersonUnderSupervision: &[]int64{30}[0], + FinishedPersonUnderSupervision: &[]int64{25}[0], CumulativePositive: 5000, CumulativeRecovered: 4500, CumulativeDeceased: 300, - CumulativePersonUnderObservation: 800, - CumulativeFinishedPersonUnderObservation: 750, - CumulativePersonUnderSupervision: 600, - CumulativeFinishedPersonUnderSupervision: 580, + CumulativePersonUnderObservation: &[]int64{800}[0], + CumulativeFinishedPersonUnderObservation: &[]int64{750}[0], + CumulativePersonUnderSupervision: &[]int64{600}[0], + CumulativeFinishedPersonUnderSupervision: &[]int64{580}[0], Rt: &rt, RtUpper: &rt, RtLower: &rt, diff --git a/internal/models/province_case_test.go b/internal/models/province_case_test.go index b8071af..a5b477b 100644 --- a/internal/models/province_case_test.go +++ b/internal/models/province_case_test.go @@ -19,17 +19,17 @@ func TestProvinceCase_Structure(t *testing.T) { Positive: 50, Recovered: 40, Deceased: 2, - PersonUnderObservation: 10, - FinishedPersonUnderObservation: 8, - PersonUnderSupervision: 5, - FinishedPersonUnderSupervision: 3, + PersonUnderObservation: &[]int64{10}[0], + FinishedPersonUnderObservation: &[]int64{8}[0], + PersonUnderSupervision: &[]int64{5}[0], + FinishedPersonUnderSupervision: &[]int64{3}[0], CumulativePositive: 500, CumulativeRecovered: 400, CumulativeDeceased: 20, - CumulativePersonUnderObservation: 100, - CumulativeFinishedPersonUnderObservation: 80, - CumulativePersonUnderSupervision: 50, - CumulativeFinishedPersonUnderSupervision: 30, + CumulativePersonUnderObservation: &[]int64{100}[0], + CumulativeFinishedPersonUnderObservation: &[]int64{80}[0], + CumulativePersonUnderSupervision: &[]int64{50}[0], + CumulativeFinishedPersonUnderSupervision: &[]int64{30}[0], Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -42,10 +42,10 @@ func TestProvinceCase_Structure(t *testing.T) { assert.Equal(t, int64(50), provinceCase.Positive) assert.Equal(t, int64(40), provinceCase.Recovered) assert.Equal(t, int64(2), provinceCase.Deceased) - assert.Equal(t, int64(10), provinceCase.PersonUnderObservation) - assert.Equal(t, int64(8), provinceCase.FinishedPersonUnderObservation) - assert.Equal(t, int64(5), provinceCase.PersonUnderSupervision) - assert.Equal(t, int64(3), provinceCase.FinishedPersonUnderSupervision) + assert.Equal(t, int64(10), *provinceCase.PersonUnderObservation) + assert.Equal(t, int64(8), *provinceCase.FinishedPersonUnderObservation) + assert.Equal(t, int64(5), *provinceCase.PersonUnderSupervision) + assert.Equal(t, int64(3), *provinceCase.FinishedPersonUnderSupervision) assert.Equal(t, int64(500), provinceCase.CumulativePositive) assert.Equal(t, int64(400), provinceCase.CumulativeRecovered) assert.Equal(t, int64(20), provinceCase.CumulativeDeceased) diff --git a/internal/repository/national_case_repository.go b/internal/repository/national_case_repository.go index 52147b7..e21f5b4 100644 --- a/internal/repository/national_case_repository.go +++ b/internal/repository/national_case_repository.go @@ -13,8 +13,10 @@ import ( type NationalCaseRepository interface { GetAll() ([]models.NationalCase, error) GetAllSorted(sortParams utils.SortParams) ([]models.NationalCase, error) + GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) GetByDateRange(startDate, endDate time.Time) ([]models.NationalCase, error) GetByDateRangeSorted(startDate, endDate time.Time, sortParams utils.SortParams) ([]models.NationalCase, error) + GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) GetLatest() (*models.NationalCase, error) GetByDay(day int64) (*models.NationalCase, error) } @@ -150,3 +152,94 @@ func (r *nationalCaseRepository) GetByDay(day int64) (*models.NationalCase, erro return &c, nil } + +func (r *nationalCaseRepository) GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + // First get the total count + countQuery := `SELECT COUNT(*) FROM national_cases` + var total int + err := r.db.QueryRow(countQuery).Scan(&total) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get paginated data + query := `SELECT id, day, date, positive, recovered, deceased, + cumulative_positive, cumulative_recovered, cumulative_deceased, + rt, rt_upper, rt_lower + FROM national_cases + ORDER BY ` + sortParams.GetSQLOrderClause() + ` LIMIT ? OFFSET ?` + + rows, err := r.db.Query(query, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to query paginated national cases: %w", err) + } + defer func() { + if err := rows.Close(); err != nil { + fmt.Printf("Error closing rows: %v\n", err) + } + }() + + var cases []models.NationalCase + for rows.Next() { + var c models.NationalCase + err := rows.Scan(&c.ID, &c.Day, &c.Date, &c.Positive, &c.Recovered, &c.Deceased, + &c.CumulativePositive, &c.CumulativeRecovered, &c.CumulativeDeceased, + &c.Rt, &c.RtUpper, &c.RtLower) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan national case: %w", err) + } + cases = append(cases, c) + } + + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("row iteration error: %w", err) + } + + return cases, total, nil +} + +func (r *nationalCaseRepository) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + // First get the total count + countQuery := `SELECT COUNT(*) FROM national_cases WHERE date BETWEEN ? AND ?` + var total int + err := r.db.QueryRow(countQuery, startDate, endDate).Scan(&total) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get paginated data + query := `SELECT id, day, date, positive, recovered, deceased, + cumulative_positive, cumulative_recovered, cumulative_deceased, + rt, rt_upper, rt_lower + FROM national_cases + WHERE date BETWEEN ? AND ? + ORDER BY ` + sortParams.GetSQLOrderClause() + ` LIMIT ? OFFSET ?` + + rows, err := r.db.Query(query, startDate, endDate, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to query paginated national cases by date range: %w", err) + } + defer func() { + if err := rows.Close(); err != nil { + fmt.Printf("Error closing rows: %v\n", err) + } + }() + + var cases []models.NationalCase + for rows.Next() { + var c models.NationalCase + err := rows.Scan(&c.ID, &c.Day, &c.Date, &c.Positive, &c.Recovered, &c.Deceased, + &c.CumulativePositive, &c.CumulativeRecovered, &c.CumulativeDeceased, + &c.Rt, &c.RtUpper, &c.RtLower) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan national case: %w", err) + } + cases = append(cases, c) + } + + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("row iteration error: %w", err) + } + + return cases, total, nil +} diff --git a/internal/repository/province_case_repository.go b/internal/repository/province_case_repository.go index cb7b9f0..add40cc 100644 --- a/internal/repository/province_case_repository.go +++ b/internal/repository/province_case_repository.go @@ -72,7 +72,7 @@ func (r *provinceCaseRepository) GetAllPaginatedSorted(limit, offset int, sortPa var total int err := r.db.QueryRow(countQuery).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to count province cases: %w", err) + return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Get paginated data @@ -91,7 +91,7 @@ func (r *provinceCaseRepository) GetAllPaginatedSorted(limit, offset int, sortPa cases, err := r.queryProvinceCases(query, limit, offset) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) } return cases, total, nil @@ -123,7 +123,7 @@ func (r *provinceCaseRepository) GetByProvinceIDPaginated(provinceID string, lim var total int err := r.db.QueryRow(countQuery, provinceID).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to count province cases for province %s: %w", provinceID, err) + return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Get paginated data @@ -143,7 +143,7 @@ func (r *provinceCaseRepository) GetByProvinceIDPaginated(provinceID string, lim cases, err := r.queryProvinceCases(query, provinceID, limit, offset) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) } return cases, total, nil @@ -175,7 +175,7 @@ func (r *provinceCaseRepository) GetByProvinceIDAndDateRangePaginated(provinceID var total int err := r.db.QueryRow(countQuery, provinceID, startDate, endDate).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to count province cases for province %s in date range: %w", provinceID, err) + return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Get paginated data @@ -195,7 +195,7 @@ func (r *provinceCaseRepository) GetByProvinceIDAndDateRangePaginated(provinceID cases, err := r.queryProvinceCases(query, provinceID, startDate, endDate, limit, offset) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) } return cases, total, nil @@ -227,7 +227,7 @@ func (r *provinceCaseRepository) GetByDateRangePaginated(startDate, endDate time var total int err := r.db.QueryRow(countQuery, startDate, endDate).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to count province cases in date range: %w", err) + return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Get paginated data @@ -247,7 +247,7 @@ func (r *provinceCaseRepository) GetByDateRangePaginated(startDate, endDate time cases, err := r.queryProvinceCases(query, startDate, endDate, limit, offset) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) } return cases, total, nil @@ -357,27 +357,156 @@ func (r *provinceCaseRepository) buildOrderClause(sortParams utils.SortParams) s return dbField + " " + order } -// Stub implementations for other sorted methods - delegate to existing methods for now +// Sorted method implementations func (r *provinceCaseRepository) GetByProvinceIDSorted(provinceID string, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, error) { - return r.GetByProvinceID(provinceID) + query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, + pc.person_under_observation, pc.finished_person_under_observation, + pc.person_under_supervision, pc.finished_person_under_supervision, + pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, + pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, + pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, + pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name + FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + LEFT JOIN provinces p ON pc.province_id = p.id + WHERE pc.province_id = ? + ORDER BY ` + r.buildOrderClause(sortParams) + + return r.queryProvinceCases(query, provinceID) } func (r *provinceCaseRepository) GetByProvinceIDPaginatedSorted(provinceID string, limit, offset int, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, int, error) { - return r.GetByProvinceIDPaginated(provinceID, limit, offset) + // First get total count + countQuery := `SELECT COUNT(*) FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + WHERE pc.province_id = ?` + + var total int + err := r.db.QueryRow(countQuery, provinceID).Scan(&total) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get paginated data + query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, + pc.person_under_observation, pc.finished_person_under_observation, + pc.person_under_supervision, pc.finished_person_under_supervision, + pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, + pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, + pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, + pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name + FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + LEFT JOIN provinces p ON pc.province_id = p.id + WHERE pc.province_id = ? + ORDER BY ` + r.buildOrderClause(sortParams) + ` LIMIT ? OFFSET ?` + + cases, err := r.queryProvinceCases(query, provinceID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) + } + + return cases, total, nil } func (r *provinceCaseRepository) GetByProvinceIDAndDateRangeSorted(provinceID string, startDate, endDate time.Time, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, error) { - return r.GetByProvinceIDAndDateRange(provinceID, startDate, endDate) + query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, + pc.person_under_observation, pc.finished_person_under_observation, + pc.person_under_supervision, pc.finished_person_under_supervision, + pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, + pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, + pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, + pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name + FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + LEFT JOIN provinces p ON pc.province_id = p.id + WHERE pc.province_id = ? AND nc.date BETWEEN ? AND ? + ORDER BY ` + r.buildOrderClause(sortParams) + + return r.queryProvinceCases(query, provinceID, startDate, endDate) } func (r *provinceCaseRepository) GetByProvinceIDAndDateRangePaginatedSorted(provinceID string, startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, int, error) { - return r.GetByProvinceIDAndDateRangePaginated(provinceID, startDate, endDate, limit, offset) + // First get total count + countQuery := `SELECT COUNT(*) FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + WHERE pc.province_id = ? AND nc.date BETWEEN ? AND ?` + + var total int + err := r.db.QueryRow(countQuery, provinceID, startDate, endDate).Scan(&total) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get paginated data + query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, + pc.person_under_observation, pc.finished_person_under_observation, + pc.person_under_supervision, pc.finished_person_under_supervision, + pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, + pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, + pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, + pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name + FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + LEFT JOIN provinces p ON pc.province_id = p.id + WHERE pc.province_id = ? AND nc.date BETWEEN ? AND ? + ORDER BY ` + r.buildOrderClause(sortParams) + ` LIMIT ? OFFSET ?` + + cases, err := r.queryProvinceCases(query, provinceID, startDate, endDate, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) + } + + return cases, total, nil } func (r *provinceCaseRepository) GetByDateRangeSorted(startDate, endDate time.Time, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, error) { - return r.GetByDateRange(startDate, endDate) + query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, + pc.person_under_observation, pc.finished_person_under_observation, + pc.person_under_supervision, pc.finished_person_under_supervision, + pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, + pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, + pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, + pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name + FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + LEFT JOIN provinces p ON pc.province_id = p.id + WHERE nc.date BETWEEN ? AND ? + ORDER BY ` + r.buildOrderClause(sortParams) + + return r.queryProvinceCases(query, startDate, endDate) } func (r *provinceCaseRepository) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, int, error) { - return r.GetByDateRangePaginated(startDate, endDate, limit, offset) + // First get total count + countQuery := `SELECT COUNT(*) FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + WHERE nc.date BETWEEN ? AND ?` + + var total int + err := r.db.QueryRow(countQuery, startDate, endDate).Scan(&total) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get paginated data + query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, + pc.person_under_observation, pc.finished_person_under_observation, + pc.person_under_supervision, pc.finished_person_under_supervision, + pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, + pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, + pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, + pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name + FROM province_cases pc + JOIN national_cases nc ON pc.day = nc.id + LEFT JOIN provinces p ON pc.province_id = p.id + WHERE nc.date BETWEEN ? AND ? + ORDER BY ` + r.buildOrderClause(sortParams) + ` LIMIT ? OFFSET ?` + + cases, err := r.queryProvinceCases(query, startDate, endDate, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) + } + + return cases, total, nil } diff --git a/internal/service/covid_service.go b/internal/service/covid_service.go index 12db009..23fca3d 100644 --- a/internal/service/covid_service.go +++ b/internal/service/covid_service.go @@ -14,6 +14,8 @@ type CovidService interface { GetNationalCasesSorted(sortParams utils.SortParams) ([]models.NationalCase, error) GetNationalCasesByDateRange(startDate, endDate string) ([]models.NationalCase, error) GetNationalCasesByDateRangeSorted(startDate, endDate string, sortParams utils.SortParams) ([]models.NationalCase, error) + GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) + GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) GetLatestNationalCase() (*models.NationalCase, error) GetProvinces() ([]models.Province, error) GetProvincesWithLatestCase() ([]models.ProvinceWithLatestCase, error) @@ -358,3 +360,29 @@ func (s *covidService) GetProvinceCasesByDateRangePaginatedSorted(provinceID, st } return cases, total, nil } + +func (s *covidService) GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + cases, total, err := s.nationalCaseRepo.GetAllPaginatedSorted(limit, offset, sortParams) + if err != nil { + return nil, 0, fmt.Errorf("failed to get sorted national cases paginated: %w", err) + } + return cases, total, nil +} + +func (s *covidService) GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + start, err := time.Parse("2006-01-02", startDate) + if err != nil { + return nil, 0, fmt.Errorf("invalid start date format: %w", err) + } + + end, err := time.Parse("2006-01-02", endDate) + if err != nil { + return nil, 0, fmt.Errorf("invalid end date format: %w", err) + } + + cases, total, err := s.nationalCaseRepo.GetByDateRangePaginatedSorted(start, end, limit, offset, sortParams) + if err != nil { + return nil, 0, fmt.Errorf("failed to get sorted national cases by date range paginated: %w", err) + } + return cases, total, nil +} diff --git a/internal/service/covid_service_test.go b/internal/service/covid_service_test.go index 117352a..c1f6962 100644 --- a/internal/service/covid_service_test.go +++ b/internal/service/covid_service_test.go @@ -49,6 +49,16 @@ func (m *MockNationalCaseRepository) GetByDateRangeSorted(startDate, endDate tim return args.Get(0).([]models.NationalCase), args.Error(1) } +func (m *MockNationalCaseRepository) GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + args := m.Called(limit, offset, sortParams) + return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) +} + +func (m *MockNationalCaseRepository) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + args := m.Called(startDate, endDate, limit, offset, sortParams) + return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) +} + type MockProvinceRepository struct { mock.Mock } diff --git a/pkg/utils/query.go b/pkg/utils/query.go index 734e261..a9bd461 100644 --- a/pkg/utils/query.go +++ b/pkg/utils/query.go @@ -150,3 +150,23 @@ func ValidatePaginationParams(limit, offset int) (int, int) { return limit, offset } + +// ParsePaginationParams parses pagination parameters from request +// Supports both offset-based and page-based pagination +func ParsePaginationParams(r *http.Request) (limit, offset int) { + // Parse limit (records per page) + limit = ParseIntQueryParam(r, "limit", 50) + + // Check if page parameter is provided + page := ParseIntQueryParam(r, "page", 0) + if page > 0 { + // Page-based pagination (page starts from 1) + offset = (page - 1) * limit + } else { + // Offset-based pagination (fallback) + offset = ParseIntQueryParam(r, "offset", 0) + } + + // Validate and adjust parameters + return ValidatePaginationParams(limit, offset) +} diff --git a/test/integration/api_test.go b/test/integration/api_test.go index 3377975..2d28831 100644 --- a/test/integration/api_test.go +++ b/test/integration/api_test.go @@ -54,6 +54,16 @@ func (m *MockNationalCaseRepo) GetByDateRangeSorted(startDate, endDate time.Time return args.Get(0).([]models.NationalCase), args.Error(1) } +func (m *MockNationalCaseRepo) GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + args := m.Called(limit, offset, sortParams) + return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) +} + +func (m *MockNationalCaseRepo) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + args := m.Called(startDate, endDate, limit, offset, sortParams) + return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) +} + type MockProvinceRepo struct { mock.Mock } @@ -229,7 +239,7 @@ func TestAPI_GetNationalCases(t *testing.T) { mockNationalRepo.On("GetAllSorted", utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - resp, err := http.Get(server.URL + "/api/v1/national") + resp, err := http.Get(server.URL + "/api/v1/national?all=true") assert.NoError(t, err) defer func() { if err := resp.Body.Close(); err != nil { @@ -260,7 +270,45 @@ func TestAPI_GetNationalCasesWithDateRange(t *testing.T) { mockNationalRepo.On("GetByDateRangeSorted", startDate, endDate, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - resp, err := http.Get(server.URL + "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31") + resp, err := http.Get(server.URL + "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31&all=true") + assert.NoError(t, err) + defer func() { + if err := resp.Body.Close(); err != nil { + t.Logf("Error closing response body: %v", err) + } + }() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response handler.Response + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, "success", response.Status) + + mockNationalRepo.AssertExpectations(t) +} + +func TestAPI_GetNationalCasesPaginated(t *testing.T) { + server, mockNationalRepo, _, _ := setupTestServer() + defer server.Close() + + now := time.Now() + rt := 1.2 + expectedCases := []models.NationalCase{ + { + ID: 1, + Day: 1, + Date: now, + Positive: 100, + Recovered: 80, + Deceased: 5, + Rt: &rt, + }, + } + + mockNationalRepo.On("GetAllPaginatedSorted", 20, 0, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, 1, nil) + + resp, err := http.Get(server.URL + "/api/v1/national?limit=20") assert.NoError(t, err) defer func() { if err := resp.Body.Close(); err != nil { @@ -274,6 +322,13 @@ func TestAPI_GetNationalCasesWithDateRange(t *testing.T) { err = json.NewDecoder(resp.Body).Decode(&response) assert.NoError(t, err) assert.Equal(t, "success", response.Status) + assert.NotNil(t, response.Data) + + // Verify it's a paginated response + paginatedData, ok := response.Data.(map[string]interface{}) + assert.True(t, ok) + assert.Contains(t, paginatedData, "data") + assert.Contains(t, paginatedData, "pagination") mockNationalRepo.AssertExpectations(t) }