Skip to content
Merged
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [v2.0.1] - 2025-09-07

### Hotfixes

- Ensure reproduction rate always appears in json response even when null - fix rt values to always be included in api responses for consistency - update reproductionrate struct to use pointer types for proper null handling - modify transformation logic to always include rt structure - update tests to handle new pointer-based rt values - bump version to 2.0.1 for hotfix release this ensures api consumers always receive the reproduction_rate object structure, with null values when data is not available.

### Fixed

- Correct database column typo and ensure rt fields always present in json response - fix database column name typo from 'cumulative_finished_persoon_under_observation' to 'cumulative_finished_person_under_observation' - remove 'omitempty' from rt fields (rt, rt_upper, rt_lower) to ensure they always appear in json responses even when null - update all sql queries and tests to use correct column name - critical production hotfix for database errors and missing rt data
- Correct [Unreleased] section format in CHANGELOG.md

### Added

- Add hotfix branch support to changelog generator

### Maintenance

- Bump version to 2.0.1 for hotfix release (version)

## [v2.0.0] - 2025-09-07

Expand Down
52 changes: 33 additions & 19 deletions generate-changelog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# - Cross-platform compatibility (Ruby vs. bash-specific features)
#
# Features:
# - Only runs from release branches (release/vx.x.x)
# - Only runs from release or hotfix branches (release/vx.x.x or hotfix/vx.x.x)
# - Categorizes commits by conventional commit types
# - Determines semantic version increment automatically
# - Updates CHANGELOG.md with proper formatting
Expand All @@ -38,6 +38,7 @@ class ChangelogGenerator
COMMIT_CATEGORIES = {
'feat' => { category: 'Added', breaking: false },
'fix' => { category: 'Fixed', breaking: false },
'hotfix' => { category: 'Hotfixes', breaking: false },
'docs' => { category: 'Documentation', breaking: false },
'style' => { category: 'Style', breaking: false },
'refactor' => { category: 'Changed', breaking: false },
Expand All @@ -49,8 +50,9 @@ class ChangelogGenerator
'revert' => { category: 'Reverted', breaking: false }
}.freeze

# Release branch pattern
# Release and hotfix branch patterns
RELEASE_BRANCH_PATTERN = /^release\/v(\d+)\.(\d+)\.(\d+)$/
HOTFIX_BRANCH_PATTERN = /^hotfix\/v(\d+)\.(\d+)\.(\d+)$/

# Changelog file path
CHANGELOG_PATH = 'CHANGELOG.md'
Expand Down Expand Up @@ -128,10 +130,13 @@ def get_current_branch
# Parse version information from the current branch name
#
# @return [Hash] Version components (major, minor, patch)
# @raise [RuntimeError] if not on a valid release branch
# @raise [RuntimeError] if not on a valid release or hotfix branch
def parse_version_from_branch
match = current_branch.match(RELEASE_BRANCH_PATTERN)
raise "Not on a release branch. Expected format: release/vX.Y.Z" unless match
release_match = current_branch.match(RELEASE_BRANCH_PATTERN)
hotfix_match = current_branch.match(HOTFIX_BRANCH_PATTERN)
match = release_match || hotfix_match

raise "Not on a release or hotfix branch. Expected format: release/vX.Y.Z or hotfix/vX.Y.Z" unless match

{
major: match[1].to_i,
Expand All @@ -148,6 +153,14 @@ def version_string
"v#{version_info[:major]}.#{version_info[:minor]}.#{version_info[:patch]}"
end

##
# Check if we're currently on a hotfix branch
#
# @return [Boolean] true if on hotfix branch, false if on release branch
def hotfix_branch?
current_branch.match?(HOTFIX_BRANCH_PATTERN)
end

##
# Validate the environment before proceeding
#
Expand Down Expand Up @@ -296,19 +309,20 @@ def should_skip_commit?(type, commit)
def category_priority(category)
priorities = {
'Breaking Changes' => 1,
'Added' => 2,
'Changed' => 3,
'Fixed' => 4,
'Deprecated' => 5,
'Removed' => 6,
'Security' => 7,
'Performance' => 8,
'Documentation' => 9,
'Tests' => 10,
'CI/CD' => 11,
'Build' => 12,
'Maintenance' => 13,
'Other' => 14
'Hotfixes' => 2,
'Added' => 3,
'Changed' => 4,
'Fixed' => 5,
'Deprecated' => 6,
'Removed' => 7,
'Security' => 8,
'Performance' => 9,
'Documentation' => 10,
'Tests' => 11,
'CI/CD' => 12,
'Build' => 13,
'Maintenance' => 14,
'Other' => 15
}
priorities[category] || 99
end
Expand Down Expand Up @@ -452,7 +466,7 @@ def self.run(args = ARGV)
opts.separator "following conventional commit format and Keep a Changelog style."
opts.separator ""
opts.separator "Requirements:"
opts.separator "- Must be run from a release branch (release/vX.Y.Z)"
opts.separator "- Must be run from a release or hotfix branch (release/vX.Y.Z or hotfix/vX.Y.Z)"
opts.separator "- Git repository with existing tags"
opts.separator "- CHANGELOG.md file with [Unreleased] section"
opts.separator ""
Expand Down
2 changes: 1 addition & 1 deletion internal/handler/covid_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (h *CovidHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
health := map[string]interface{}{
"status": "healthy",
"service": "COVID-19 API",
"version": "2.0.0",
"version": "2.0.1",
"timestamp": time.Now().UTC().Format(time.RFC3339),
}

Expand Down
2 changes: 1 addition & 1 deletion internal/handler/covid_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func TestCovidHandler_HealthCheck(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, "degraded", data["status"])
assert.Equal(t, "COVID-19 API", data["service"])
assert.Equal(t, "2.0.0", data["version"])
assert.Equal(t, "2.0.1", data["version"])
assert.Contains(t, data, "database")

dbData, ok := data["database"].(map[string]interface{})
Expand Down
6 changes: 3 additions & 3 deletions internal/models/national_case.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ type NationalCase struct {
CumulativePositive int64 `json:"cumulative_positive" db:"cumulative_positive"`
CumulativeRecovered int64 `json:"cumulative_recovered" db:"cumulative_recovered"`
CumulativeDeceased int64 `json:"cumulative_deceased" db:"cumulative_deceased"`
Rt *float64 `json:"rt,omitempty" db:"rt"`
RtUpper *float64 `json:"rt_upper,omitempty" db:"rt_upper"`
RtLower *float64 `json:"rt_lower,omitempty" db:"rt_lower"`
Rt *float64 `json:"rt" db:"rt"`
RtUpper *float64 `json:"rt_upper" db:"rt_upper"`
RtLower *float64 `json:"rt_lower" db:"rt_lower"`
}

type NullFloat64 struct {
Expand Down
18 changes: 8 additions & 10 deletions internal/models/national_case_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ type CasePercentages struct {

// ReproductionRate represents the R-value with confidence bounds
type ReproductionRate struct {
Value float64 `json:"value"`
UpperBound float64 `json:"upper_bound"`
LowerBound float64 `json:"lower_bound"`
Value *float64 `json:"value"`
UpperBound *float64 `json:"upper_bound"`
LowerBound *float64 `json:"lower_bound"`
}

// TransformToResponse converts a NationalCase model to the response format
Expand Down Expand Up @@ -74,13 +74,11 @@ func (nc *NationalCase) TransformToResponse() NationalCaseResponse {
},
}

// Add reproduction rate if available
if nc.Rt != nil && nc.RtUpper != nil && nc.RtLower != nil {
response.Statistics.ReproductionRate = &ReproductionRate{
Value: *nc.Rt,
UpperBound: *nc.RtUpper,
LowerBound: *nc.RtLower,
}
// Always include reproduction rate structure, even when values are null
response.Statistics.ReproductionRate = &ReproductionRate{
Value: nc.Rt,
UpperBound: nc.RtUpper,
LowerBound: nc.RtLower,
}

return response
Expand Down
26 changes: 18 additions & 8 deletions internal/models/national_case_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ func TestNationalCase_TransformToResponse(t *testing.T) {
if response.Statistics.ReproductionRate == nil {
t.Error("Expected ReproductionRate to be present")
} else {
if response.Statistics.ReproductionRate.Value != rtValue {
t.Errorf("Expected Rt.Value %f, got %f", rtValue, response.Statistics.ReproductionRate.Value)
if *response.Statistics.ReproductionRate.Value != rtValue {
t.Errorf("Expected Rt.Value %f, got %f", rtValue, *response.Statistics.ReproductionRate.Value)
}
if response.Statistics.ReproductionRate.UpperBound != rtUpper {
t.Errorf("Expected Rt.UpperBound %f, got %f", rtUpper, response.Statistics.ReproductionRate.UpperBound)
if *response.Statistics.ReproductionRate.UpperBound != rtUpper {
t.Errorf("Expected Rt.UpperBound %f, got %f", rtUpper, *response.Statistics.ReproductionRate.UpperBound)
}
if response.Statistics.ReproductionRate.LowerBound != rtLower {
t.Errorf("Expected Rt.LowerBound %f, got %f", rtLower, response.Statistics.ReproductionRate.LowerBound)
if *response.Statistics.ReproductionRate.LowerBound != rtLower {
t.Errorf("Expected Rt.LowerBound %f, got %f", rtLower, *response.Statistics.ReproductionRate.LowerBound)
}
}

Expand All @@ -103,8 +103,18 @@ func TestNationalCase_TransformToResponse_NoReproductionRate(t *testing.T) {

response := nc.TransformToResponse()

if response.Statistics.ReproductionRate != nil {
t.Error("Expected ReproductionRate to be nil when not provided")
if response.Statistics.ReproductionRate == nil {
t.Error("Expected ReproductionRate to always be present")
} else {
if response.Statistics.ReproductionRate.Value != nil {
t.Error("Expected Rt.Value to be nil when not provided")
}
if response.Statistics.ReproductionRate.UpperBound != nil {
t.Error("Expected Rt.UpperBound to be nil when not provided")
}
if response.Statistics.ReproductionRate.LowerBound != nil {
t.Error("Expected Rt.LowerBound to be nil when not provided")
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions internal/models/province_case.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ type ProvinceCase struct {
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_persoon_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,omitempty" db:"rt"`
RtUpper *float64 `json:"rt_upper,omitempty" db:"rt_upper"`
RtLower *float64 `json:"rt_lower,omitempty" db:"rt_lower"`
Rt *float64 `json:"rt" db:"rt"`
RtUpper *float64 `json:"rt_upper" db:"rt_upper"`
RtLower *float64 `json:"rt_lower" db:"rt_lower"`
Province *Province `json:"province,omitempty"`
}

Expand Down
14 changes: 6 additions & 8 deletions internal/models/province_case_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type ProvinceCumulativeCases struct {
// ProvinceCaseStatistics contains calculated statistics and metrics for province data
type ProvinceCaseStatistics struct {
Percentages CasePercentages `json:"percentages"`
ReproductionRate *ReproductionRate `json:"reproduction_rate,omitempty"`
ReproductionRate *ReproductionRate `json:"reproduction_rate"`
}

// TransformToResponse converts a ProvinceCase model to the response format
Expand Down Expand Up @@ -86,13 +86,11 @@ func (pc *ProvinceCase) TransformToResponse(date time.Time) ProvinceCaseResponse
Province: pc.Province,
}

// Add reproduction rate if available
if pc.Rt != nil && pc.RtUpper != nil && pc.RtLower != nil {
response.Statistics.ReproductionRate = &ReproductionRate{
Value: *pc.Rt,
UpperBound: *pc.RtUpper,
LowerBound: *pc.RtLower,
}
// Always include reproduction rate structure, even when values are null
response.Statistics.ReproductionRate = &ReproductionRate{
Value: pc.Rt,
UpperBound: pc.RtUpper,
LowerBound: pc.RtLower,
}

return response
Expand Down
24 changes: 16 additions & 8 deletions internal/models/province_case_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ func TestProvinceCase_TransformToResponse(t *testing.T) {
Deceased: 6.0, // (300 / 5000) * 100
},
ReproductionRate: &ReproductionRate{
Value: 1.5,
UpperBound: 1.8,
LowerBound: 1.2,
Value: &[]float64{1.5}[0],
UpperBound: &[]float64{1.8}[0],
LowerBound: &[]float64{1.2}[0],
},
},
Province: &Province{
Expand Down Expand Up @@ -151,7 +151,11 @@ func TestProvinceCase_TransformToResponse(t *testing.T) {
Recovered: 90.0, // (1800 / 2000) * 100
Deceased: 5.0, // (100 / 2000) * 100
},
ReproductionRate: nil,
ReproductionRate: &ReproductionRate{
Value: nil,
UpperBound: nil,
LowerBound: nil,
},
},
Province: &Province{
ID: "ID-JB",
Expand Down Expand Up @@ -219,7 +223,11 @@ func TestProvinceCase_TransformToResponse(t *testing.T) {
Recovered: 0.0,
Deceased: 0.0,
},
ReproductionRate: nil,
ReproductionRate: &ReproductionRate{
Value: nil,
UpperBound: nil,
LowerBound: nil,
},
},
Province: &Province{
ID: "ID-AC",
Expand Down Expand Up @@ -307,9 +315,9 @@ func TestProvinceCaseWithDate_TransformToResponse(t *testing.T) {
Deceased: 6.666666666666667, // (200 / 3000) * 100
},
ReproductionRate: &ReproductionRate{
Value: 1.2,
UpperBound: 1.5,
LowerBound: 0.9,
Value: &[]float64{1.2}[0],
UpperBound: &[]float64{1.5}[0],
LowerBound: &[]float64{0.9}[0],
},
},
Province: &Province{
Expand Down
10 changes: 5 additions & 5 deletions internal/repository/province_case_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (r *provinceCaseRepository) GetAll() ([]models.ProvinceCaseWithDate, error)
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_persoon_under_observation,
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
Expand All @@ -46,7 +46,7 @@ func (r *provinceCaseRepository) GetByProvinceID(provinceID string) ([]models.Pr
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_persoon_under_observation,
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
Expand All @@ -63,7 +63,7 @@ func (r *provinceCaseRepository) GetByProvinceIDAndDateRange(provinceID string,
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_persoon_under_observation,
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
Expand All @@ -80,7 +80,7 @@ func (r *provinceCaseRepository) GetByDateRange(startDate, endDate time.Time) ([
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_persoon_under_observation,
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
Expand All @@ -97,7 +97,7 @@ func (r *provinceCaseRepository) GetLatestByProvinceID(provinceID string) (*mode
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_persoon_under_observation,
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
Expand Down
Loading
Loading