Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
SOFTWARE.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
@echo " help - Show this help message"

2 changes: 1 addition & 1 deletion generate-changelog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -752,4 +752,4 @@ def self.run(args = ARGV)
# Run the CLI if this file is executed directly
if __FILE__ == $0
CLI.run
end
end
65 changes: 51 additions & 14 deletions internal/handler/covid_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)"
Expand All @@ -160,18 +201,14 @@ 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")

// 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 {
Expand Down
83 changes: 80 additions & 3 deletions internal/handler/covid_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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)
Expand Down
16 changes: 8 additions & 8 deletions internal/models/province_case.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
30 changes: 19 additions & 11 deletions internal/models/province_case_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand All @@ -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{
Expand Down
Loading
Loading