From 793e50ce5247c7d8b6d20ffdeddeb17bf7d05c77 Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 5 Mar 2026 15:15:30 -0600 Subject: [PATCH] Add regional task templates API with climate zone lookup Adds a new endpoint GET /api/tasks/templates/by-region/?zip= that resolves ZIP codes to IECC climate regions and returns relevant home maintenance task templates. Includes climate region model, region lookup service with tests, seed data for all 8 climate zones with 50+ templates, and OpenAPI spec. Co-Authored-By: Claude Opus 4.6 --- docs/openapi.yaml | 33 ++ internal/database/database.go | 4 +- internal/dto/responses/task_template.go | 7 + internal/handlers/task_template_handler.go | 18 + internal/integration/kmp_contract_test.go | 2 + internal/models/climate_region.go | 28 ++ internal/models/task_template.go | 3 +- internal/repositories/task_template_repo.go | 14 + internal/router/router.go | 1 + internal/services/region_lookup.go | 157 +++++++ internal/services/region_lookup_test.go | 81 ++++ internal/services/task_template_service.go | 20 + seeds/004_climate_regions.sql | 21 + seeds/005_regional_templates.sql | 486 ++++++++++++++++++++ 14 files changed, 873 insertions(+), 2 deletions(-) create mode 100644 internal/models/climate_region.go create mode 100644 internal/services/region_lookup.go create mode 100644 internal/services/region_lookup_test.go create mode 100644 seeds/004_climate_regions.sql create mode 100644 seeds/005_regional_templates.sql diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 696e09f..3ae4cbd 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -523,6 +523,39 @@ paths: items: $ref: '#/components/schemas/TaskTemplateResponse' + /tasks/templates/by-region/: + get: + tags: [Static Data] + operationId: getTaskTemplatesByRegion + summary: Get task templates for a climate region by state or ZIP code + description: Returns templates matching the climate zone for a given US state abbreviation or ZIP code. At least one parameter is required. If both are provided, state takes priority. + parameters: + - name: state + in: query + required: false + schema: + type: string + example: MA + description: US state abbreviation (e.g., MA, FL, TX) + - name: zip + in: query + required: false + schema: + type: string + example: "02101" + description: US ZIP code (resolved to state on the server) + responses: + '200': + description: Regional templates for the climate zone + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TaskTemplateResponse' + '400': + $ref: '#/components/responses/BadRequest' + /tasks/templates/{id}/: get: tags: [Static Data] diff --git a/internal/database/database.go b/internal/database/database.go index 93cbb7c..e7431db 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -131,7 +131,9 @@ func Migrate() error { &models.TaskPriority{}, &models.TaskFrequency{}, &models.ContractorSpecialty{}, - &models.TaskTemplate{}, // Task templates reference category and frequency + &models.TaskTemplate{}, // Task templates reference category and frequency + &models.ClimateRegion{}, // IECC climate regions for regional templates + &models.ZipClimateRegion{}, // ZIP to climate region lookup // User and auth tables &models.User{}, diff --git a/internal/dto/responses/task_template.go b/internal/dto/responses/task_template.go index 6fddbe5..8daa279 100644 --- a/internal/dto/responses/task_template.go +++ b/internal/dto/responses/task_template.go @@ -21,6 +21,8 @@ type TaskTemplateResponse struct { Tags []string `json:"tags"` DisplayOrder int `json:"display_order"` IsActive bool `json:"is_active"` + RegionID *uint `json:"region_id,omitempty"` + RegionName string `json:"region_name,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -63,6 +65,11 @@ func NewTaskTemplateResponse(t *models.TaskTemplate) TaskTemplateResponse { resp.Frequency = NewTaskFrequencyResponse(t.Frequency) } + if len(t.Regions) > 0 { + resp.RegionID = &t.Regions[0].ID + resp.RegionName = t.Regions[0].Name + } + return resp } diff --git a/internal/handlers/task_template_handler.go b/internal/handlers/task_template_handler.go index 45032a3..4690eb8 100644 --- a/internal/handlers/task_template_handler.go +++ b/internal/handlers/task_template_handler.go @@ -80,6 +80,24 @@ func (h *TaskTemplateHandler) GetTemplatesByCategory(c echo.Context) error { return c.JSON(http.StatusOK, templates) } +// GetTemplatesByRegion handles GET /api/tasks/templates/by-region/?state=XX or ?zip=12345 +// Returns templates specific to the user's climate region based on state abbreviation or ZIP code +func (h *TaskTemplateHandler) GetTemplatesByRegion(c echo.Context) error { + state := c.QueryParam("state") + zip := c.QueryParam("zip") + + if state == "" && zip == "" { + return apperrors.BadRequest("error.state_or_zip_required") + } + + templates, err := h.templateService.GetByRegion(state, zip) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, templates) +} + // GetTemplate handles GET /api/tasks/templates/:id/ // Returns a single template by ID func (h *TaskTemplateHandler) GetTemplate(c echo.Context) error { diff --git a/internal/integration/kmp_contract_test.go b/internal/integration/kmp_contract_test.go index 2f6e83a..e0855d9 100644 --- a/internal/integration/kmp_contract_test.go +++ b/internal/integration/kmp_contract_test.go @@ -98,6 +98,8 @@ var specEndpointsKMPSkips = map[routeKey]bool{ {Method: "POST", Path: "/notifications/devices/"}: true, // KMP uses /notifications/devices/register/ {Method: "POST", Path: "/notifications/devices/unregister/"}: true, // KMP uses DELETE on device ID {Method: "PATCH", Path: "/notifications/preferences/"}: true, // KMP uses PUT + // Regional templates — not yet implemented in KMP (planned) + {Method: "GET", Path: "/tasks/templates/by-region/"}: true, // Stripe web-only and server-to-server endpoints — not implemented in mobile KMP {Method: "POST", Path: "/subscription/checkout/"}: true, // Web-only (Stripe Checkout) {Method: "POST", Path: "/subscription/portal/"}: true, // Web-only (Stripe Customer Portal) diff --git a/internal/models/climate_region.go b/internal/models/climate_region.go new file mode 100644 index 0000000..2cd8f52 --- /dev/null +++ b/internal/models/climate_region.go @@ -0,0 +1,28 @@ +package models + +// ClimateRegion represents an IECC climate zone for regional task templates +type ClimateRegion struct { + BaseModel + Name string `gorm:"column:name;size:100;not null;uniqueIndex" json:"name"` + ZoneNumber int `gorm:"column:zone_number;not null;index" json:"zone_number"` + Description string `gorm:"column:description;type:text" json:"description"` + IsActive bool `gorm:"column:is_active;default:true;index" json:"is_active"` +} + +// TableName returns the table name for GORM +func (ClimateRegion) TableName() string { + return "task_climateregion" +} + +// ZipClimateRegion maps ZIP codes to climate regions (static lookup) +type ZipClimateRegion struct { + BaseModel + ZipCode string `gorm:"column:zip_code;size:10;uniqueIndex;not null" json:"zip_code"` + ClimateRegionID uint `gorm:"column:climate_region_id;index;not null" json:"climate_region_id"` + ClimateRegion ClimateRegion `gorm:"foreignKey:ClimateRegionID" json:"climate_region,omitempty"` +} + +// TableName returns the table name for GORM +func (ZipClimateRegion) TableName() string { + return "task_zipclimateregion" +} diff --git a/internal/models/task_template.go b/internal/models/task_template.go index 1022506..65e1b83 100644 --- a/internal/models/task_template.go +++ b/internal/models/task_template.go @@ -13,7 +13,8 @@ type TaskTemplate struct { IconAndroid string `gorm:"column:icon_android;size:100" json:"icon_android"` Tags string `gorm:"column:tags;type:text" json:"tags"` // Comma-separated tags for search DisplayOrder int `gorm:"column:display_order;default:0" json:"display_order"` - IsActive bool `gorm:"column:is_active;default:true;index" json:"is_active"` + IsActive bool `gorm:"column:is_active;default:true;index" json:"is_active"` + Regions []ClimateRegion `gorm:"many2many:task_tasktemplate_regions;" json:"regions,omitempty"` } // TableName returns the table name for GORM diff --git a/internal/repositories/task_template_repo.go b/internal/repositories/task_template_repo.go index 672afcd..07f29bb 100644 --- a/internal/repositories/task_template_repo.go +++ b/internal/repositories/task_template_repo.go @@ -104,6 +104,20 @@ func (r *TaskTemplateRepository) Count() (int64, error) { return count, err } +// GetByRegion returns active templates associated with a specific climate region +func (r *TaskTemplateRepository) GetByRegion(regionID uint) ([]models.TaskTemplate, error) { + var templates []models.TaskTemplate + err := r.db. + Preload("Category"). + Preload("Frequency"). + Preload("Regions"). + Joins("JOIN task_tasktemplate_regions ON task_tasktemplate_regions.task_template_id = task_tasktemplate.id"). + Where("task_tasktemplate_regions.climate_region_id = ? AND task_tasktemplate.is_active = ?", regionID, true). + Order("task_tasktemplate.display_order ASC, task_tasktemplate.title ASC"). + Find(&templates).Error + return templates, err +} + // GetGroupedByCategory returns templates grouped by category name func (r *TaskTemplateRepository) GetGroupedByCategory() (map[string][]models.TaskTemplate, error) { templates, err := r.GetAll() diff --git a/internal/router/router.go b/internal/router/router.go index 94058f2..8cae61a 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -339,6 +339,7 @@ func setupPublicDataRoutes(api *echo.Group, residenceHandler *handlers.Residence templates.GET("/grouped/", taskTemplateHandler.GetTemplatesGrouped) templates.GET("/search/", taskTemplateHandler.SearchTemplates) templates.GET("/by-category/:category_id/", taskTemplateHandler.GetTemplatesByCategory) + templates.GET("/by-region/", taskTemplateHandler.GetTemplatesByRegion) templates.GET("/:id/", taskTemplateHandler.GetTemplate) } } diff --git a/internal/services/region_lookup.go b/internal/services/region_lookup.go new file mode 100644 index 0000000..37ea291 --- /dev/null +++ b/internal/services/region_lookup.go @@ -0,0 +1,157 @@ +package services + +import ( + "strconv" + "strings" +) + +// StateToClimateRegion maps US state abbreviations to IECC climate zone IDs. +// Some states span multiple zones — this uses the dominant zone for the most populated areas. +var StateToClimateRegion = map[string]uint{ + // Zone 1: Hot-Humid + "HI": 1, "FL": 1, "LA": 1, + // Zone 2: Hot-Dry / Hot-Humid mix + "TX": 2, "AZ": 2, "NV": 2, "NM": 2, + // Zone 3: Mixed-Humid + "GA": 3, "SC": 3, "AL": 3, "MS": 3, "AR": 3, "NC": 3, "TN": 3, "OK": 3, "CA": 3, + // Zone 4: Mixed + "VA": 4, "KY": 4, "MO": 4, "KS": 4, "DE": 4, "MD": 4, "DC": 4, "WV": 4, "OR": 4, + // Zone 5: Cold + "NJ": 5, "PA": 5, "CT": 5, "RI": 5, "MA": 5, "OH": 5, "IN": 5, "IL": 5, + "IA": 5, "NE": 5, "CO": 5, "UT": 5, "WA": 5, "ID": 5, "NY": 5, "MI": 5, + // Zone 6: Very Cold + "WI": 6, "MN": 6, "ND": 6, "SD": 6, "MT": 6, "WY": 6, "VT": 6, "NH": 6, "ME": 6, + // Zone 8: Arctic + "AK": 8, +} + +// GetClimateRegionIDByState returns the climate region ID for a US state abbreviation. +// Returns 0 if the state is not found. +func GetClimateRegionIDByState(state string) uint { + regionID, ok := StateToClimateRegion[strings.ToUpper(strings.TrimSpace(state))] + if !ok { + return 0 + } + return regionID +} + +// ZipToState maps a US ZIP code to a state abbreviation using the 3-digit ZIP prefix. +// Returns empty string if the ZIP is invalid or unrecognized. +func ZipToState(zip string) string { + zip = strings.TrimSpace(zip) + if len(zip) < 3 { + return "" + } + prefix, err := strconv.Atoi(zip[:3]) + if err != nil { + return "" + } + + // ZIP prefix → state mapping (USPS ranges) + switch { + case prefix >= 10 && prefix <= 27: + return "MA" + case prefix >= 28 && prefix <= 29: + return "RI" + case prefix >= 30 && prefix <= 38: + return "NH" + case prefix >= 39 && prefix <= 49: + return "ME" + case prefix >= 50 && prefix <= 59: + return "VT" + case prefix >= 60 && prefix <= 69: + return "CT" + case prefix >= 70 && prefix <= 89: + return "NJ" + case prefix >= 100 && prefix <= 149: + return "NY" + case prefix >= 150 && prefix <= 196: + return "PA" + case prefix >= 197 && prefix <= 199: + return "DE" + case prefix >= 200 && prefix <= 205: + return "DC" + case prefix >= 206 && prefix <= 219: + return "MD" + case prefix >= 220 && prefix <= 246: + return "VA" + case prefix >= 247 && prefix <= 268: + return "WV" + case prefix >= 270 && prefix <= 289: + return "NC" + case prefix >= 290 && prefix <= 299: + return "SC" + case prefix >= 300 && prefix <= 319: + return "GA" + case prefix >= 320 && prefix <= 349: + return "FL" + case prefix >= 350 && prefix <= 369: + return "AL" + case prefix >= 370 && prefix <= 385: + return "TN" + case prefix >= 386 && prefix <= 397: + return "MS" + case prefix >= 400 && prefix <= 427: + return "KY" + case prefix >= 430 && prefix <= 458: + return "OH" + case prefix >= 460 && prefix <= 479: + return "IN" + case prefix >= 480 && prefix <= 499: + return "MI" + case prefix >= 500 && prefix <= 528: + return "IA" + case prefix >= 530 && prefix <= 549: + return "WI" + case prefix >= 550 && prefix <= 567: + return "MN" + case prefix >= 570 && prefix <= 577: + return "SD" + case prefix >= 580 && prefix <= 588: + return "ND" + case prefix >= 590 && prefix <= 599: + return "MT" + case prefix >= 600 && prefix <= 629: + return "IL" + case prefix >= 630 && prefix <= 658: + return "MO" + case prefix >= 660 && prefix <= 679: + return "KS" + case prefix >= 680 && prefix <= 693: + return "NE" + case prefix >= 700 && prefix <= 714: + return "LA" + case prefix >= 716 && prefix <= 729: + return "AR" + case prefix >= 730 && prefix <= 749: + return "OK" + case prefix >= 750 && prefix <= 799: + return "TX" + case prefix >= 800 && prefix <= 816: + return "CO" + case prefix >= 820 && prefix <= 831: + return "WY" + case prefix >= 832 && prefix <= 838: + return "ID" + case prefix >= 840 && prefix <= 847: + return "UT" + case prefix >= 850 && prefix <= 865: + return "AZ" + case prefix >= 870 && prefix <= 884: + return "NM" + case prefix >= 889 && prefix <= 898: + return "NV" + case prefix >= 900 && prefix <= 966: + return "CA" + case prefix >= 967 && prefix <= 968: + return "HI" + case prefix >= 970 && prefix <= 979: + return "OR" + case prefix >= 980 && prefix <= 994: + return "WA" + case prefix >= 995 && prefix <= 999: + return "AK" + default: + return "" + } +} diff --git a/internal/services/region_lookup_test.go b/internal/services/region_lookup_test.go new file mode 100644 index 0000000..37127cb --- /dev/null +++ b/internal/services/region_lookup_test.go @@ -0,0 +1,81 @@ +package services + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetClimateRegionIDByState(t *testing.T) { + tests := []struct { + name string + state string + expected uint + }{ + {"Massachusetts → Cold (5)", "MA", 5}, + {"Florida → Hot-Humid (1)", "FL", 1}, + {"Arizona → Hot-Dry (2)", "AZ", 2}, + {"Alaska → Arctic (8)", "AK", 8}, + {"Minnesota → Very Cold (6)", "MN", 6}, + {"Tennessee → Mixed-Humid (3)", "TN", 3}, + {"Missouri → Mixed (4)", "MO", 4}, + {"Unknown state → 0", "XX", 0}, + {"Empty string → 0", "", 0}, + {"Lowercase input", "ma", 5}, + {"Whitespace trimmed", " TX ", 2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetClimateRegionIDByState(tt.state) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestZipToState(t *testing.T) { + tests := []struct { + name string + zip string + expected string + }{ + {"Boston MA", "02101", "MA"}, + {"Miami FL", "33101", "FL"}, + {"Phoenix AZ", "85001", "AZ"}, + {"Anchorage AK", "99501", "AK"}, + {"New York NY", "10001", "NY"}, + {"Chicago IL", "60601", "IL"}, + {"Houston TX", "77001", "TX"}, + {"Denver CO", "80201", "CO"}, + {"DC", "20001", "DC"}, + {"Too short", "12", ""}, + {"Empty", "", ""}, + {"Non-numeric", "abcde", ""}, + {"Unrecognized prefix", "00100", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ZipToState(tt.zip) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestStateToClimateRegion_AllStatesPresent(t *testing.T) { + // All 50 states + DC should be in the map + allStates := []string{ + "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", + "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", + "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", + "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", + "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY", + "DC", + } + + for _, state := range allStates { + t.Run(state, func(t *testing.T) { + regionID := GetClimateRegionIDByState(state) + assert.Greater(t, regionID, uint(0), "State %s should map to a region", state) + assert.LessOrEqual(t, regionID, uint(8), "State %s region should be 1-8, got %d", state, regionID) + }) + } +} diff --git a/internal/services/task_template_service.go b/internal/services/task_template_service.go index 9373896..91090b1 100644 --- a/internal/services/task_template_service.go +++ b/internal/services/task_template_service.go @@ -63,6 +63,26 @@ func (s *TaskTemplateService) GetByID(id uint) (*responses.TaskTemplateResponse, return &resp, nil } +// GetByRegion returns templates for a specific climate region. +// Accepts either a state abbreviation or ZIP code (state takes priority). +// ZIP codes are resolved to a state via the ZipToState lookup. +func (s *TaskTemplateService) GetByRegion(state, zip string) ([]responses.TaskTemplateResponse, error) { + // Resolve ZIP to state if no state provided + if state == "" && zip != "" { + state = ZipToState(zip) + } + + regionID := GetClimateRegionIDByState(state) + if regionID == 0 { + return []responses.TaskTemplateResponse{}, nil + } + templates, err := s.templateRepo.GetByRegion(regionID) + if err != nil { + return nil, err + } + return responses.NewTaskTemplateListResponse(templates), nil +} + // Count returns the total count of active templates func (s *TaskTemplateService) Count() (int64, error) { return s.templateRepo.Count() diff --git a/seeds/004_climate_regions.sql b/seeds/004_climate_regions.sql new file mode 100644 index 0000000..faf9d9e --- /dev/null +++ b/seeds/004_climate_regions.sql @@ -0,0 +1,21 @@ +-- Seed the 8 IECC climate regions for regional task templates +-- Run after 003_task_templates.sql + +INSERT INTO task_climateregion (id, created_at, updated_at, name, zone_number, description, is_active) +VALUES + (1, NOW(), NOW(), 'Hot-Humid', 1, 'South Florida, Hawaii, Gulf Coast lowlands', true), + (2, NOW(), NOW(), 'Hot-Dry', 2, 'Arizona, Southern Nevada, West Texas, Southern California deserts', true), + (3, NOW(), NOW(), 'Mixed-Humid', 3, 'Mid-Atlantic, North Carolina, Tennessee, Arkansas', true), + (4, NOW(), NOW(), 'Mixed-Dry', 4, 'Kansas, Missouri, Southern Illinois, Central Virginia', true), + (5, NOW(), NOW(), 'Cold', 5, 'Northern Illinois, Michigan, New York, Southern New England', true), + (6, NOW(), NOW(), 'Very Cold', 6, 'Buffalo NY, Northern Ohio, Northern Wisconsin, Minnesota', true), + (7, NOW(), NOW(), 'Subarctic', 7, 'Upper Peninsula MI, Northern Minnesota, Mountain regions', true), + (8, NOW(), NOW(), 'Arctic', 8, 'Alaska, Far Northern regions', true) +ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + zone_number = EXCLUDED.zone_number, + description = EXCLUDED.description, + is_active = EXCLUDED.is_active, + updated_at = NOW(); + +SELECT setval('task_climateregion_id_seq', (SELECT COALESCE(MAX(id), 0) + 1 FROM task_climateregion), false); diff --git a/seeds/005_regional_templates.sql b/seeds/005_regional_templates.sql new file mode 100644 index 0000000..690ad09 --- /dev/null +++ b/seeds/005_regional_templates.sql @@ -0,0 +1,486 @@ +-- Region-specific task templates +-- Run after 004_climate_regions.sql +-- These extend the existing 66 global templates (IDs 1-66) with region-specific tasks. +-- Existing global templates remain region-agnostic (shown to everyone by default). +-- +-- Category IDs (from 001_lookups.sql): +-- 1 = Appliances, 2 = Cleaning, 3 = Electrical, 4 = Exterior +-- 5 = General, 6 = HVAC, 7 = Interior, 8 = Pest Control, 9 = Plumbing, 10 = Safety +-- +-- Frequency IDs (from 001_lookups.sql): +-- 1 = Once, 2 = Daily, 3 = Weekly, 4 = Bi-Weekly, 5 = Monthly +-- 6 = Quarterly, 7 = Semi-Annually, 8 = Annually + +INSERT INTO task_tasktemplate (id, created_at, updated_at, title, description, category_id, frequency_id, icon_ios, icon_android, tags, display_order, is_active) +VALUES + -- ============================================= + -- ZONE 1: Hot-Humid (South FL, Hawaii, Gulf Coast) + -- ============================================= + (100, NOW(), NOW(), 'Hurricane Season Prep', + 'Install storm shutters, trim deadwood, secure outdoor furniture, photo-document exterior for insurance', + 10, 7, 'hurricane', 'Storm', 'hurricane,storm,shutters,prep,tropical', 100, true), + + (101, NOW(), NOW(), 'Inspect for Mold & Mildew', + 'Check under sinks, attic, crawl space, and bathrooms for mold growth. Address within 24 hours if found', + 2, 5, 'humidity.fill', 'WaterDamage', 'mold,mildew,humidity,inspection', 101, true), + + (102, NOW(), NOW(), 'Check AC Condensate Drain', + 'Clear the AC condensate drain line to prevent water backup and mold growth in humid climate', + 6, 5, 'drop.degreesign.fill', 'WaterDamage', 'ac,condensate,drain,humidity', 102, true), + + (103, NOW(), NOW(), 'Termite Inspection (Tropical)', + 'Professional inspection — tropical climates have year-round termite activity. Check wood structures, attic, crawl space', + 10, 7, 'ant.fill', 'BugReport', 'termite,tropical,pest,inspection', 103, true), + + (104, NOW(), NOW(), 'Dehumidifier Maintenance', + 'Clean filter, check drain, verify indoor humidity stays between 30-50%. Critical in humid climates', + 6, 5, 'humidity.fill', 'Air', 'dehumidifier,humidity,filter,maintenance', 104, true), + + (105, NOW(), NOW(), 'Storm Shutter Test', + 'Test all storm shutters for proper operation. Lubricate tracks, replace damaged hardware', + 10, 8, 'shield.lefthalf.filled', 'Shield', 'storm,shutter,hurricane,test', 105, true), + + -- ============================================= + -- ZONE 2: Hot-Dry (AZ, NV, TX, desert SW) + -- ============================================= + (110, NOW(), NOW(), 'Wildfire Defensible Space — Zone 0', + 'Clear combustible materials within 5 feet of house. Replace mulch with gravel, remove dead plants, clear roof and gutters of debris', + 4, 7, 'flame.fill', 'LocalFireDepartment', 'wildfire,defensible,fire,zone0', 110, true), + + (111, NOW(), NOW(), 'Wildfire Defensible Space — Zone 1', + 'Within 5-30 feet: space tree canopies 10 ft apart, prune branches 6 ft from ground, clear all dead vegetation', + 4, 7, 'flame.fill', 'LocalFireDepartment', 'wildfire,defensible,fire,zone1,trees', 111, true), + + (112, NOW(), NOW(), 'Inspect Roof for UV Damage', + 'Check shingles for cracks, brittleness, and curling caused by intense sun exposure. Desert sun ages roofs faster', + 4, 7, 'sun.max.fill', 'WbSunny', 'roof,uv,sun,shingles,damage', 112, true), + + (113, NOW(), NOW(), 'Check for Scorpions & Desert Pests', + 'Inspect around exterior lights, under mulch, and entry points for scorpions and spiders. Seal gaps in foundation and doors', + 10, 6, 'ant.fill', 'BugReport', 'scorpion,pest,desert,inspection', 113, true), + + (114, NOW(), NOW(), 'Dust AC Condenser Unit', + 'Clear accumulated dust and sand from outdoor AC unit. Clean condenser fins. Desert environments clog units faster', + 6, 5, 'fan', 'AcUnit', 'ac,dust,condenser,desert,sand', 114, true), + + (115, NOW(), NOW(), 'Check Foundation for Heat Cracks', + 'Inspect foundation for cracks caused by soil expansion and contraction from extreme temperature swings', + 4, 7, 'house.circle.fill', 'Foundation', 'foundation,cracks,heat,expansion', 115, true), + + (170, NOW(), NOW(), 'Service Evaporative Cooler (Swamp Cooler)', + 'Replace pads, clean water reservoir, oil pump and motor bearings. Prepare for cooling season', + 6, 8, 'fan.and.light.ceiling.fill', 'AcUnit', 'swamp cooler,evaporative,pads,service', 170, true), + + (171, NOW(), NOW(), 'Flush Water Heater (Hard Water)', + 'Drain and flush sediment from water heater tank. Desert regions have hard water that accelerates buildup', + 9, 7, 'drop.fill', 'WaterDamage', 'water heater,flush,sediment,hard water', 171, true), + + (172, NOW(), NOW(), 'Inspect Stucco & Exterior Walls', + 'Check stucco or siding for cracks and gaps caused by UV exposure and temperature swings. Patch before monsoon season', + 4, 7, 'sun.max.fill', 'WbSunny', 'stucco,exterior,cracks,uv,walls', 172, true), + + (173, NOW(), NOW(), 'Clean Solar Panels', + 'Remove dust, pollen, and debris from solar panels. Desert dust reduces efficiency by 15-25% if not cleaned regularly', + 3, 6, 'sun.max.fill', 'SolarPower', 'solar,panels,clean,dust,efficiency', 173, true), + + (174, NOW(), NOW(), 'Check Pool Equipment & Chemistry', + 'Inspect pool pump, filter, and chemical levels. High evaporation in hot climates requires more frequent balancing', + 4, 5, 'drop.circle.fill', 'Pool', 'pool,pump,filter,chemistry,evaporation', 174, true), + + (175, NOW(), NOW(), 'Inspect Irrigation System', + 'Check drip irrigation lines and sprinkler heads for leaks, clogs, and UV damage. Adjust watering schedule for season', + 4, 6, 'sprinkler.and.droplets.fill', 'Grass', 'irrigation,sprinkler,drip,water,landscape', 175, true), + + (176, NOW(), NOW(), 'Replace HVAC Air Filter', + 'Replace AC air filter. Desert dust and pollen clog filters faster — change monthly during peak summer', + 6, 5, 'aqi.medium', 'Air', 'hvac,filter,air,dust,replacement', 176, true), + + (177, NOW(), NOW(), 'Seal Windows & Doors for Heat', + 'Check and replace caulk and weather stripping around windows and doors. Prevents cool air loss and dust intrusion', + 7, 8, 'wind', 'Air', 'seal,windows,doors,caulk,weatherstrip,heat', 177, true), + + (178, NOW(), NOW(), 'Inspect Attic Ventilation', + 'Verify attic vents and ridge vents are clear. Proper ventilation prevents attic temps from exceeding 150°F and damaging roof sheathing', + 4, 7, 'thermometer.sun.fill', 'Thermostat', 'attic,ventilation,heat,ridge vent', 178, true), + + (179, NOW(), NOW(), 'Treat Wood Fence & Deck (UV Protection)', + 'Apply UV-resistant stain or sealant to wood fences, decks, and pergolas. Desert sun degrades untreated wood in 1-2 years', + 4, 8, 'sun.max.fill', 'Deck', 'fence,deck,wood,uv,stain,seal', 179, true), + + (180, NOW(), NOW(), 'Service Garage Door in Extreme Heat', + 'Lubricate springs, hinges, and rollers. Heat causes metal expansion — check alignment and weatherseal at bottom', + 5, 7, 'door.garage.open', 'Garage', 'garage,door,lubricate,heat,alignment', 180, true), + + (181, NOW(), NOW(), 'Test Smoke & CO Detectors', + 'Test all smoke and carbon monoxide detectors. Replace batteries. Extreme heat can reduce battery life faster', + 10, 7, 'smoke.fill', 'SmokeFree', 'smoke,detector,co,battery,safety', 181, true), + + -- ============================================= + -- ZONE 1 additional: Hot-Humid extras + -- ============================================= + (190, NOW(), NOW(), 'Service Whole-House Dehumidifier', + 'Clean evaporator coils, check refrigerant, empty and clean drain pan. Critical for preventing mold in humid climates', + 6, 6, 'humidity.fill', 'Air', 'dehumidifier,coils,service,humidity', 190, true), + + (191, NOW(), NOW(), 'Check Exterior Paint for Peeling', + 'Inspect exterior paint for peeling, bubbling, or mildew. Humidity causes paint failure faster than dry climates', + 4, 8, 'paintbrush.fill', 'FormatPaint', 'paint,exterior,peeling,humidity,mildew', 191, true), + + (192, NOW(), NOW(), 'Clean Dryer Vent', + 'Remove lint buildup from dryer vent duct and exterior flap. Humidity makes lint stick and accumulate faster', + 1, 7, 'wind', 'LocalLaundryService', 'dryer,vent,lint,fire,safety', 192, true), + + (193, NOW(), NOW(), 'Inspect Siding for Moisture Damage', + 'Check wood, vinyl, or fiber cement siding for swelling, warping, or rot. Look behind trim and at ground level', + 4, 8, 'house.fill', 'Home', 'siding,moisture,rot,inspection', 193, true), + + (194, NOW(), NOW(), 'Flush Water Heater', + 'Drain and flush sediment from water heater. Humid climates accelerate mineral buildup', + 9, 8, 'drop.fill', 'WaterDamage', 'water heater,flush,sediment,maintenance', 194, true), + + (195, NOW(), NOW(), 'Service Pool & Deck (Algae Prevention)', + 'Shock-treat pool, scrub deck for algae growth. Warm humid conditions cause rapid algae blooms', + 4, 5, 'drop.circle.fill', 'Pool', 'pool,deck,algae,shock,humidity', 195, true), + + (196, NOW(), NOW(), 'Inspect Attic for Moisture & Ventilation', + 'Check attic for moisture, ensure soffit vents and ridge vents are clear. Look for mold on roof sheathing', + 4, 7, 'humidity.fill', 'Roofing', 'attic,moisture,ventilation,mold', 196, true), + + (197, NOW(), NOW(), 'Test Sump Pump', + 'Test sump pump operation and check discharge line. Heavy tropical rains can overwhelm drainage', + 9, 6, 'drop.triangle.fill', 'WaterDamage', 'sump pump,test,flood,drainage', 197, true), + + -- ============================================= + -- ZONE 3 additional: Mixed-Humid extras + -- ============================================= + (200, NOW(), NOW(), 'Clean Dryer Vent', + 'Remove lint buildup from dryer vent and exterior flap. Reduce fire risk and improve drying efficiency', + 1, 7, 'wind', 'LocalLaundryService', 'dryer,vent,lint,fire,safety', 200, true), + + (201, NOW(), NOW(), 'Service Lawn Mower for Season', + 'Change oil, replace spark plug, sharpen blade, clean air filter. Prepare for growing season', + 5, 8, 'leaf.fill', 'Grass', 'lawn,mower,service,season,maintenance', 201, true), + + (202, NOW(), NOW(), 'Check Siding & Trim for Rot', + 'Inspect wood siding, fascia, and trim for soft spots and rot. Repair before rainy season', + 4, 8, 'house.fill', 'Home', 'siding,trim,rot,wood,repair', 202, true), + + (203, NOW(), NOW(), 'Replace HVAC Air Filter', + 'Replace furnace/AC air filter. Changed climate means both heating and cooling seasons — replace more often', + 6, 6, 'aqi.medium', 'Air', 'hvac,filter,air,replacement', 203, true), + + (204, NOW(), NOW(), 'Flush Water Heater', + 'Drain and flush sediment from water heater tank. Prevents efficiency loss and extends lifespan', + 9, 8, 'drop.fill', 'WaterDamage', 'water heater,flush,sediment,maintenance', 204, true), + + (205, NOW(), NOW(), 'Aerate & Overseed Lawn', + 'Core aerate lawn and overseed bare patches in early fall. Best recovery window for mixed-humid lawns', + 4, 8, 'leaf.fill', 'Grass', 'aerate,overseed,lawn,fall', 205, true), + + (206, NOW(), NOW(), 'Test Smoke & CO Detectors', + 'Test all smoke and carbon monoxide detectors. Replace batteries annually or when chirping', + 10, 7, 'smoke.fill', 'SmokeFree', 'smoke,detector,co,battery,safety', 206, true), + + -- ============================================= + -- ZONE 4 additional: Mixed-Dry extras + -- ============================================= + (210, NOW(), NOW(), 'Replace HVAC Air Filter', + 'Replace furnace/AC air filter. Dusty dry conditions and seasonal heating/cooling require frequent changes', + 6, 6, 'aqi.medium', 'Air', 'hvac,filter,air,dust,replacement', 210, true), + + (211, NOW(), NOW(), 'Clean Dryer Vent', + 'Remove lint buildup from dryer vent and exterior flap. Fire prevention and efficiency', + 1, 7, 'wind', 'LocalLaundryService', 'dryer,vent,lint,fire,safety', 211, true), + + (212, NOW(), NOW(), 'Flush Water Heater', + 'Drain and flush sediment from water heater. Hard water common in dry regions', + 9, 8, 'drop.fill', 'WaterDamage', 'water heater,flush,sediment,hard water', 212, true), + + (213, NOW(), NOW(), 'Service Lawn & Irrigation', + 'Check irrigation heads, adjust watering schedule for season, reseed bare patches', + 4, 6, 'sprinkler.and.droplets.fill', 'Grass', 'irrigation,lawn,water,sprinkler', 213, true), + + (214, NOW(), NOW(), 'Test Smoke & CO Detectors', + 'Test all smoke and carbon monoxide detectors. Replace batteries', + 10, 7, 'smoke.fill', 'SmokeFree', 'smoke,detector,co,battery,safety', 214, true), + + (215, NOW(), NOW(), 'Clean Gutters & Downspouts', + 'Clear leaves and debris from gutters. Ensure downspouts direct water 4-6 ft from foundation', + 4, 7, 'cloud.rain.fill', 'Water', 'gutters,downspouts,clean,drainage', 215, true), + + -- ============================================= + -- ZONE 5 additional: Cold extras + -- ============================================= + (220, NOW(), NOW(), 'Replace HVAC Air Filter', + 'Replace furnace air filter. Long heating season means more frequent changes', + 6, 6, 'aqi.medium', 'Air', 'hvac,filter,air,furnace,replacement', 220, true), + + (221, NOW(), NOW(), 'Clean Dryer Vent', + 'Remove lint buildup from dryer vent. Winter increases dryer use — clean more often', + 1, 7, 'wind', 'LocalLaundryService', 'dryer,vent,lint,fire,safety', 221, true), + + (222, NOW(), NOW(), 'Flush Water Heater', + 'Drain and flush sediment from water heater tank', + 9, 8, 'drop.fill', 'WaterDamage', 'water heater,flush,sediment', 222, true), + + (223, NOW(), NOW(), 'Test Smoke & CO Detectors', + 'Test all smoke and carbon monoxide detectors. Critical during heating season with furnace and fireplace use', + 10, 7, 'smoke.fill', 'SmokeFree', 'smoke,detector,co,battery,safety,heating', 223, true), + + (224, NOW(), NOW(), 'Inspect & Clean Chimney', + 'Professional chimney sweep and inspection before heating season. Check for creosote buildup and flue damage', + 10, 8, 'flame.fill', 'Fireplace', 'chimney,sweep,creosote,flue,fireplace', 224, true), + + -- ============================================= + -- ZONE 6 additional: Very Cold extras + -- ============================================= + (230, NOW(), NOW(), 'Flush Water Heater', + 'Drain and flush sediment from water heater. Extended heating season means heater works harder', + 9, 8, 'drop.fill', 'WaterDamage', 'water heater,flush,sediment', 230, true), + + (231, NOW(), NOW(), 'Test Smoke & CO Detectors', + 'Test all detectors monthly during heating season. Furnace and fireplace use increases CO risk', + 10, 5, 'smoke.fill', 'SmokeFree', 'smoke,detector,co,battery,safety,heating', 231, true), + + (232, NOW(), NOW(), 'Replace HVAC Air Filter', + 'Replace furnace filter. Extended heating season requires monthly changes November through April', + 6, 5, 'aqi.medium', 'Air', 'hvac,filter,air,furnace,winter', 232, true), + + (233, NOW(), NOW(), 'Inspect & Clean Chimney', + 'Professional sweep before heating season. Heavy use demands annual inspection for creosote and cracks', + 10, 8, 'flame.fill', 'Fireplace', 'chimney,sweep,creosote,flue', 233, true), + + -- ============================================= + -- ZONE 3: Mixed-Humid (Mid-Atlantic, mid-South) + -- ============================================= + (120, NOW(), NOW(), 'Blow Out Irrigation System', + 'Drain and blow out sprinkler lines before first frost to prevent pipe freeze damage', + 4, 8, 'sprinkler.and.droplets.fill', 'Grass', 'irrigation,winterize,sprinkler,frost', 120, true), + + (121, NOW(), NOW(), 'Insulate Exposed Pipes', + 'Add pipe insulation to exposed pipes under sinks, in crawl spaces, and in unheated areas before winter', + 9, 8, 'snowflake', 'AcUnit', 'pipes,insulation,freeze,crawlspace', 121, true), + + (122, NOW(), NOW(), 'Check Foundation for Moisture', + 'Inspect basement or crawl space for standing water, moisture, and cracks after heavy rain events', + 4, 6, 'house.circle.fill', 'Foundation', 'foundation,moisture,basement,cracks', 122, true), + + (123, NOW(), NOW(), 'Test Radon Level', + 'Use radon test kit or hire professional. Common concern in Mid-Atlantic region basements', + 10, 8, 'aqi.medium', 'Air', 'radon,test,safety,basement', 123, true), + + (124, NOW(), NOW(), 'Spring Flood Assessment', + 'After spring thaw, check for water damage, inspect foundation for standing water, clean and extend downspouts 4-6 ft from foundation', + 4, 8, 'cloud.rain.fill', 'Water', 'flood,spring,foundation,downspout', 124, true), + + -- ============================================= + -- ZONE 4: Mixed (Upper South, lower Midwest) + -- ============================================= + (130, NOW(), NOW(), 'Winterize Exterior Faucets & Hoses', + 'Disconnect all garden hoses, shut off water supply to exterior faucets, and drain the lines completely', + 9, 8, 'snowflake', 'AcUnit', 'winterize,faucet,hose,freeze', 130, true), + + (131, NOW(), NOW(), 'Check Attic Insulation Level', + 'Verify attic insulation is R-38 or higher. Add insulation if below threshold for energy efficiency and freeze prevention', + 7, 8, 'thermometer.snowflake', 'Thermostat', 'insulation,attic,r-value,energy', 131, true), + + (132, NOW(), NOW(), 'Inspect Weather Stripping & Caulk', + 'Check all exterior doors and windows for worn seals. Replace weather stripping and re-caulk gaps to prevent drafts', + 4, 8, 'wind', 'Air', 'weatherstrip,caulk,seal,draft', 132, true), + + (133, NOW(), NOW(), 'Seal Foundation Cracks', + 'Inspect foundation for cracks and seal with hydraulic cement. Prevents rodent entry and water infiltration', + 4, 8, 'house.circle.fill', 'Foundation', 'foundation,cracks,seal,rodent', 133, true), + + (134, NOW(), NOW(), 'Test Heating System Before Winter', + 'Run heating system before peak demand. Check for strange noises, smells, or uneven heating. Schedule service if needed', + 6, 8, 'flame.fill', 'LocalFireDepartment', 'heating,furnace,test,winter', 134, true), + + -- ============================================= + -- ZONE 5: Cold (Northeast, Great Lakes) + -- ============================================= + (140, NOW(), NOW(), 'Roof Snow Raking', + 'Rake snow from roof within 24 hours of 12+ inch accumulation to prevent ice dams and structural stress', + 4, 3, 'snowflake', 'AcUnit', 'snow,roof,rake,ice dam,winter', 140, true), + + (141, NOW(), NOW(), 'Ice Dam Prevention Check', + 'Inspect for ice dams forming at roof edge. Apply de-ice products if forming. Check attic ventilation is adequate', + 4, 3, 'snowflake.circle.fill', 'AcUnit', 'ice dam,roof,prevention,winter', 141, true), + + (142, NOW(), NOW(), 'Test Sump Pump & Backup', + 'Test sump pump operation. Verify battery backup is charged. Check discharge line is clear of ice and debris', + 9, 6, 'drop.triangle.fill', 'WaterDamage', 'sump pump,backup,basement,flood', 142, true), + + (143, NOW(), NOW(), 'Winter Gutter Maintenance', + 'Clear gutters of ice and snow buildup 2-3x weekly during snow season. Check downspout extensions are clear', + 4, 3, 'cloud.snow.fill', 'AcUnit', 'gutter,ice,snow,winter,maintenance', 143, true), + + (144, NOW(), NOW(), 'Check Foundation for Frost Heave', + 'Inspect basement and foundation for new cracks, settling, or water intrusion caused by freeze-thaw cycles', + 4, 8, 'house.circle.fill', 'Foundation', 'foundation,frost heave,cracks,settling', 144, true), + + (145, NOW(), NOW(), 'Winterize Entire Plumbing System', + 'Insulate all exterior pipes, disconnect hoses, shut off exterior water valves, open cabinet doors under sinks for warm air circulation', + 9, 8, 'snowflake', 'AcUnit', 'winterize,plumbing,pipes,insulate', 145, true), + + -- ============================================= + -- ZONE 6: Very Cold (Upstate NY, northern Midwest) + -- ============================================= + (150, NOW(), NOW(), 'Install/Check Heated Gutter System', + 'Verify heated gutter guards are operational before winter. Test heating elements and check for damage from previous season', + 4, 8, 'bolt.horizontal.fill', 'ElectricalServices', 'heated gutter,ice dam,winter,prevention', 150, true), + + (151, NOW(), NOW(), 'Verify Pipe Routing (Interior Walls)', + 'Confirm all water pipes run through heated interior walls. Plan to relocate any exterior-wall pipes before winter', + 9, 8, 'wrench.and.screwdriver.fill', 'Build', 'pipes,interior,freeze,prevention', 151, true), + + (152, NOW(), NOW(), 'Heavy Snow Roof Raking', + 'Mandatory after every 8+ inches of snow. Critical for preventing structural stress, ice dams, and roof collapse', + 4, 3, 'snowflake', 'AcUnit', 'snow,roof,rake,heavy,structural', 152, true), + + (153, NOW(), NOW(), 'Weekly Ice Dam & Gutter Inspection', + 'During winter months, inspect gutters and roof edge weekly for ice dam formation. Apply de-icing as needed', + 4, 3, 'eye.fill', 'Visibility', 'ice dam,gutter,weekly,inspection,winter', 153, true), + + -- ============================================= + -- ZONE 7-8: Subarctic / Arctic + -- ============================================= + (160, NOW(), NOW(), 'Test Backup Heating System', + 'Test backup/secondary heating system monthly. Dual furnaces are standard in extreme climates — both must be operational', + 6, 5, 'flame.fill', 'LocalFireDepartment', 'backup,heating,furnace,test', 160, true), + + (161, NOW(), NOW(), 'Maintain Basement Above 55°F', + 'Verify basement auxiliary heater maintains minimum 55°F at all times. Prevents pipe freeze and foundation damage', + 6, 5, 'thermometer.low', 'Thermostat', 'basement,heating,minimum,temperature', 161, true), + + (162, NOW(), NOW(), 'Inspect Heat-Traced Pipes', + 'Check heat tape on any exposed or vulnerable pipes. Replace damaged sections. Verify thermostat operation', + 9, 6, 'bolt.horizontal.fill', 'ElectricalServices', 'heat trace,pipes,tape,freeze', 162, true), + + (163, NOW(), NOW(), 'Verify Roof Structural Integrity for Snow Load', + 'Before winter, verify roof pitch supports heavy snow load (6:12 minimum). Check for sagging or previous damage', + 4, 8, 'house.fill', 'Roofing', 'roof,structure,snow load,pitch', 163, true) + +ON CONFLICT (id) DO UPDATE SET + title = EXCLUDED.title, + description = EXCLUDED.description, + category_id = EXCLUDED.category_id, + frequency_id = EXCLUDED.frequency_id, + icon_ios = EXCLUDED.icon_ios, + icon_android = EXCLUDED.icon_android, + tags = EXCLUDED.tags, + display_order = EXCLUDED.display_order, + is_active = EXCLUDED.is_active, + updated_at = NOW(); + +-- ============================================= +-- Associate templates with regions via join table +-- ============================================= + +-- Zone 1: Hot-Humid templates +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (100, 1), (101, 1), (102, 1), (103, 1), (104, 1), (105, 1), + (190, 1), (191, 1), (192, 1), (193, 1), (194, 1), (195, 1), (196, 1), (197, 1) +ON CONFLICT DO NOTHING; + +-- Zone 2: Hot-Dry templates +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (110, 2), (111, 2), (112, 2), (113, 2), (114, 2), (115, 2), + (170, 2), (171, 2), (172, 2), (173, 2), (174, 2), (175, 2), (176, 2), (177, 2), (178, 2), (179, 2), (180, 2), (181, 2) +ON CONFLICT DO NOTHING; + +-- Zone 3: Mixed-Humid templates +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (120, 3), (121, 3), (122, 3), (123, 3), (124, 3), + (200, 3), (201, 3), (202, 3), (203, 3), (204, 3), (205, 3), (206, 3) +ON CONFLICT DO NOTHING; + +-- Zone 4: Mixed templates +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (130, 4), (131, 4), (132, 4), (133, 4), (134, 4), + (210, 4), (211, 4), (212, 4), (213, 4), (214, 4), (215, 4) +ON CONFLICT DO NOTHING; + +-- Zone 5: Cold templates +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (140, 5), (141, 5), (142, 5), (143, 5), (144, 5), (145, 5), + (220, 5), (221, 5), (222, 5), (223, 5), (224, 5) +ON CONFLICT DO NOTHING; + +-- Zone 6: Very Cold templates (own + inherit zone 5 cold-weather tasks) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (150, 6), (151, 6), (152, 6), (153, 6), + (230, 6), (231, 6), (232, 6), (233, 6), + (140, 6), (141, 6), (142, 6), (143, 6), (144, 6), (145, 6), + (220, 6), (221, 6), (222, 6), (223, 6), (224, 6) +ON CONFLICT DO NOTHING; + +-- Zone 7: Subarctic (own + inherit zone 5 + 6 tasks) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (160, 7), (161, 7), (162, 7), (163, 7), + (150, 7), (151, 7), (152, 7), (153, 7), + (230, 7), (231, 7), (232, 7), (233, 7), + (140, 7), (141, 7), (142, 7), (143, 7), (144, 7), (145, 7), + (220, 7), (221, 7), (222, 7), (223, 7), (224, 7) +ON CONFLICT DO NOTHING; + +-- Zone 8: Arctic (own + inherit zone 5 + 6 + 7 tasks) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (160, 8), (161, 8), (162, 8), (163, 8), + (150, 8), (151, 8), (152, 8), (153, 8), + (230, 8), (231, 8), (232, 8), (233, 8), + (140, 8), (141, 8), (142, 8), (143, 8), (144, 8), (145, 8), + (220, 8), (221, 8), (222, 8), (223, 8), (224, 8) +ON CONFLICT DO NOTHING; + +-- ============================================= +-- Cross-tag existing global templates with regions where especially relevant +-- ============================================= + +-- "Winterize Outdoor Faucets" (ID 13) — Zones 3-8 (freeze risk) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (13, 3), (13, 4), (13, 5), (13, 6), (13, 7), (13, 8) +ON CONFLICT DO NOTHING; + +-- "Clean Gutters" (ID 57) — All zones (universal) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (57, 1), (57, 2), (57, 3), (57, 4), (57, 5), (57, 6), (57, 7), (57, 8) +ON CONFLICT DO NOTHING; + +-- "Inspect Roof" (ID 59) — All zones (universal) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (59, 1), (59, 2), (59, 3), (59, 4), (59, 5), (59, 6), (59, 7), (59, 8) +ON CONFLICT DO NOTHING; + +-- "Termite Inspection" (ID 23) — Zones 1-3 (warm/humid) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (23, 1), (23, 2), (23, 3) +ON CONFLICT DO NOTHING; + +-- "Pest Control Treatment" (ID 24) — All zones +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (24, 1), (24, 2), (24, 3), (24, 4), (24, 5), (24, 6), (24, 7), (24, 8) +ON CONFLICT DO NOTHING; + +-- "Schedule Chimney Cleaning" (ID 22) — Zones 4-8 (heating climates) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (22, 4), (22, 5), (22, 6), (22, 7), (22, 8) +ON CONFLICT DO NOTHING; + +-- "Service Sprinkler System" (ID 64) — Zones 3-6 (freeze winterization) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (64, 3), (64, 4), (64, 5), (64, 6) +ON CONFLICT DO NOTHING; + +-- "Inspect Weather Stripping" (ID 61) — Zones 4-8 (cold/energy efficiency) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (61, 4), (61, 5), (61, 6), (61, 7), (61, 8) +ON CONFLICT DO NOTHING; + +-- "Clean and Reverse Ceiling Fans" (ID 56) — All zones (seasonal direction change) +INSERT INTO task_tasktemplate_regions (task_template_id, climate_region_id) VALUES + (56, 1), (56, 2), (56, 3), (56, 4), (56, 5), (56, 6), (56, 7), (56, 8) +ON CONFLICT DO NOTHING; + +-- Reset template sequence +SELECT setval('task_tasktemplate_id_seq', (SELECT COALESCE(MAX(id), 0) + 1 FROM task_tasktemplate), false);