diff --git a/internal/adapters/inbound/httpapi/endpoints_test.go b/internal/adapters/inbound/httpapi/endpoints_test.go index 3cd0346..e633e3d 100644 --- a/internal/adapters/inbound/httpapi/endpoints_test.go +++ b/internal/adapters/inbound/httpapi/endpoints_test.go @@ -394,6 +394,51 @@ func TestForecastPrecisionTwo(t *testing.T) { } } +func TestForecastJSONOmitsLegacyDescriptionFields(t *testing.T) { + h := newHandler(t, &fakeService{ + forecast: &model.WeatherForecastRun{ + Product: model.ForecastProductHourly, + IssuedAt: time.Now().UTC(), + Periods: []model.WeatherForecastPeriod{{ + StartTime: time.Now().UTC(), + EndTime: time.Now().UTC().Add(time.Hour), + ConditionCode: model.WMOUnknown, + TextDescription: "Cloudy", + }}, + }, + }, "/forecast/hourly") + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/forecast/hourly", nil) + h.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + var payload struct { + Data struct { + Periods []map[string]any `json:"periods"` + } `json:"data"` + } + if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil { + t.Fatalf("decode forecast payload: %v", err) + } + if len(payload.Data.Periods) == 0 { + t.Fatalf("expected at least one period") + } + + period := payload.Data.Periods[0] + for _, key := range []string{"conditionText", "providerRawDescription", "detailedText", "iconUrl"} { + if _, ok := period[key]; ok { + t.Fatalf("unexpected legacy field %q in forecast response period: %#v", key, period) + } + } + if period["textDescription"] != "Cloudy" { + t.Fatalf("expected textDescription Cloudy, got %#v", period["textDescription"]) + } +} + func TestForecastTimezoneOffsetUppercaseTZConvertsAllTimes(t *testing.T) { issuedAt := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC) updatedAt := issuedAt.Add(30 * time.Minute) diff --git a/internal/adapters/inbound/httpapi/presenter/forecast.go b/internal/adapters/inbound/httpapi/presenter/forecast.go index b429906..3aaf31a 100644 --- a/internal/adapters/inbound/httpapi/presenter/forecast.go +++ b/internal/adapters/inbound/httpapi/presenter/forecast.go @@ -28,11 +28,7 @@ type WeatherForecastPeriodUS struct { Name string `json:"name,omitempty" xml:"name,omitempty"` IsDay *bool `json:"isDay,omitempty" xml:"isDay,omitempty"` ConditionCode model.WMOCode `json:"conditionCode" xml:"conditionCode"` - ConditionText string `json:"conditionText,omitempty" xml:"conditionText,omitempty"` - ProviderRawDescription string `json:"providerRawDescription,omitempty" xml:"providerRawDescription,omitempty"` TextDescription string `json:"textDescription,omitempty" xml:"textDescription,omitempty"` - DetailedText string `json:"detailedText,omitempty" xml:"detailedText,omitempty"` - IconURL string `json:"iconUrl,omitempty" xml:"iconUrl,omitempty"` TemperatureF *float64 `json:"temperatureF,omitempty" xml:"temperatureF,omitempty"` TemperatureFMin *float64 `json:"temperatureFMin,omitempty" xml:"temperatureFMin,omitempty"` TemperatureFMax *float64 `json:"temperatureFMax,omitempty" xml:"temperatureFMax,omitempty"` @@ -74,11 +70,7 @@ func ForecastPayload(run *model.WeatherForecastRun, units Units, precision int, Name: p.Name, IsDay: copyBoolPtr(p.IsDay), ConditionCode: p.ConditionCode, - ConditionText: p.ConditionText, - ProviderRawDescription: p.ProviderRawDescription, TextDescription: p.TextDescription, - DetailedText: p.DetailedText, - IconURL: p.IconURL, TemperatureF: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureC), precision), TemperatureFMin: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMin), precision), TemperatureFMax: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMax), precision), @@ -119,11 +111,7 @@ func ForecastPayload(run *model.WeatherForecastRun, units Units, precision int, Name: p.Name, IsDay: copyBoolPtr(p.IsDay), ConditionCode: p.ConditionCode, - ConditionText: p.ConditionText, - ProviderRawDescription: p.ProviderRawDescription, TextDescription: p.TextDescription, - DetailedText: p.DetailedText, - IconURL: p.IconURL, TemperatureC: roundedPtr(copyFloat64Ptr(p.TemperatureC), precision), TemperatureCMin: roundedPtr(copyFloat64Ptr(p.TemperatureCMin), precision), TemperatureCMax: roundedPtr(copyFloat64Ptr(p.TemperatureCMax), precision), diff --git a/internal/adapters/inbound/httpapi/presenter/payload_test.go b/internal/adapters/inbound/httpapi/presenter/payload_test.go index 1a711d8..23bf2a0 100644 --- a/internal/adapters/inbound/httpapi/presenter/payload_test.go +++ b/internal/adapters/inbound/httpapi/presenter/payload_test.go @@ -3,6 +3,7 @@ package presenter import ( + "encoding/json" "math" "testing" "time" @@ -83,6 +84,22 @@ func TestForecastPayloadUS(t *testing.T) { assertApprox(t, period.SnowfallDepthIn, 2.0, 0.0001) } +func TestForecastPayloadOmitsLegacyDescriptionFields(t *testing.T) { + run := &model.WeatherForecastRun{ + Product: model.ForecastProductHourly, + IssuedAt: time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC), + Periods: []model.WeatherForecastPeriod{{ + StartTime: time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC), + EndTime: time.Date(2026, 3, 20, 13, 0, 0, 0, time.UTC), + ConditionCode: model.WMOUnknown, + TextDescription: "Cloudy", + }}, + } + + assertForecastPayloadHasNoLegacyDescriptionFields(t, ForecastPayload(run, UnitsMetric, 0, nil)) + assertForecastPayloadHasNoLegacyDescriptionFields(t, ForecastPayload(run, UnitsUS, 0, nil)) +} + func TestForecastPayloadTimezoneConversionMetricAndUS(t *testing.T) { loc := time.FixedZone("UTC-05:00", -5*60*60) issuedAt := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC) @@ -308,3 +325,34 @@ func assertOffsetSeconds(t *testing.T, ts time.Time, want int) { t.Fatalf("expected offset %d, got %d for %s", want, got, ts.Format(time.RFC3339)) } } + +func assertForecastPayloadHasNoLegacyDescriptionFields(t *testing.T, payload any) { + t.Helper() + + b, err := json.Marshal(payload) + if err != nil { + t.Fatalf("json.Marshal(payload) error = %v", err) + } + + var root map[string]any + if err := json.Unmarshal(b, &root); err != nil { + t.Fatalf("json.Unmarshal(payload) error = %v", err) + } + periodsRaw, ok := root["periods"].([]any) + if !ok || len(periodsRaw) == 0 { + t.Fatalf("expected non-empty periods in payload: %#v", root["periods"]) + } + period, ok := periodsRaw[0].(map[string]any) + if !ok { + t.Fatalf("expected first period map, got %#v", periodsRaw[0]) + } + + for _, key := range []string{"conditionText", "providerRawDescription", "detailedText", "iconUrl"} { + if _, exists := period[key]; exists { + t.Fatalf("unexpected legacy field %q in payload period: %#v", key, period) + } + } + if period["textDescription"] != "Cloudy" { + t.Fatalf("expected textDescription Cloudy, got %#v", period["textDescription"]) + } +} diff --git a/internal/adapters/outbound/postgres/forecast_mapper.go b/internal/adapters/outbound/postgres/forecast_mapper.go index ba19cf6..9e720b3 100644 --- a/internal/adapters/outbound/postgres/forecast_mapper.go +++ b/internal/adapters/outbound/postgres/forecast_mapper.go @@ -24,11 +24,7 @@ func mapForecastPeriodRow(row forecastPeriodRow) model.WeatherForecastPeriod { Name: stringValue(row.Name), IsDay: boolPtr(row.IsDay), ConditionCode: model.WMOCode(row.ConditionCode), - ConditionText: stringValue(row.ConditionText), - ProviderRawDescription: stringValue(row.ProviderRawDescription), TextDescription: stringValue(row.TextDescription), - DetailedText: stringValue(row.DetailedText), - IconURL: stringValue(row.IconURL), TemperatureC: float64Ptr(row.TemperatureC), TemperatureCMin: float64Ptr(row.TemperatureCMin), TemperatureCMax: float64Ptr(row.TemperatureCMax), diff --git a/internal/adapters/outbound/postgres/forecast_queries.go b/internal/adapters/outbound/postgres/forecast_queries.go index 7790a30..205de4a 100644 --- a/internal/adapters/outbound/postgres/forecast_queries.go +++ b/internal/adapters/outbound/postgres/forecast_queries.go @@ -27,11 +27,7 @@ SELECT name, is_day, condition_code, - condition_text, - provider_raw_description, text_description, - detailed_text, - icon_url, temperature_c, temperature_c_min, temperature_c_max, diff --git a/internal/adapters/outbound/postgres/forecast_read.go b/internal/adapters/outbound/postgres/forecast_read.go index 0314991..6898b19 100644 --- a/internal/adapters/outbound/postgres/forecast_read.go +++ b/internal/adapters/outbound/postgres/forecast_read.go @@ -63,11 +63,7 @@ func (r *Repository) loadForecastPeriods(ctx context.Context, eventID string) ([ &row.Name, &row.IsDay, &row.ConditionCode, - &row.ConditionText, - &row.ProviderRawDescription, &row.TextDescription, - &row.DetailedText, - &row.IconURL, &row.TemperatureC, &row.TemperatureCMin, &row.TemperatureCMax, diff --git a/internal/adapters/outbound/postgres/forecast_rows.go b/internal/adapters/outbound/postgres/forecast_rows.go index 7ce9c43..2efe8bc 100644 --- a/internal/adapters/outbound/postgres/forecast_rows.go +++ b/internal/adapters/outbound/postgres/forecast_rows.go @@ -26,11 +26,7 @@ type forecastPeriodRow struct { Name sql.NullString IsDay sql.NullBool ConditionCode int - ConditionText sql.NullString - ProviderRawDescription sql.NullString TextDescription sql.NullString - DetailedText sql.NullString - IconURL sql.NullString TemperatureC sql.NullFloat64 TemperatureCMin sql.NullFloat64 TemperatureCMax sql.NullFloat64