package breeds import ( _ "embed" "encoding/json" "fmt" "github.com/sundynix/pets-be/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" "reflect" ) //go:embed enrichment.json var enrichmentData []byte type Enrichment struct { SourceKey string `json:"source_key"` Name string `json:"name"` Fields map[string]string `json:"fields"` FieldSources map[string]string `json:"field_sources"` References []model.BreedReference `json:"references"` } func Enrichments() ([]Enrichment, error) { var rows []Enrichment err := json.Unmarshal(enrichmentData, &rows) return rows, err } // Reference facts remain available even when an editor locks the display content. func enrich(db *gorm.DB) error { rows, err := Enrichments() if err != nil { return fmt.Errorf("load breed sources: %w", err) } defaults, err := Defaults() if err != nil { return err } baseline := map[string]model.Breed{} for _, breed := range defaults { baseline[*breed.SourceKey] = breed } for _, item := range rows { var breed model.Breed if err := db.Where("source_key = ?", item.SourceKey).First(&breed).Error; err != nil { return err } for _, source := range item.References { source.BreedID = breed.ID // Source snapshots are read-only in admin; update only when factual metadata changes. if err := db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "breed_id"}, {Name: "provider"}}, DoUpdates: clause.AssignmentColumns([]string{"external_id", "url", "retrieved_at", "facts"}), Where: clause.Where{Exprs: []clause.Expression{clause.Expr{SQL: "sundynix_breed_reference.facts IS DISTINCT FROM EXCLUDED.facts OR sundynix_breed_reference.url IS DISTINCT FROM EXCLUDED.url OR sundynix_breed_reference.retrieved_at IS DISTINCT FROM EXCLUDED.retrieved_at"}}}}).Create(&source).Error; err != nil { return err } } if breed.EditorialLocked || breed.Name != item.Name { continue } original := baseline[item.SourceKey] updates := map[string]any{"english_name": original.EnglishName, "origin": original.Origin, "breed_group": original.BreedGroup, "fci_number": original.FCINumber, "height": original.Height, "weight": original.Weight, "summary": original.Summary, "temperament": original.Temperament, "traits": original.Traits, "care_notes": original.CareNotes, "life_span": "", "exercise": "", "grooming": ""} for field, value := range item.Fields { switch field { case "english_name", "origin", "breed_group", "fci_number", "height", "weight", "life_span", "exercise", "grooming", "temperament", "traits", "summary", "care_notes": updates[field] = value default: return fmt.Errorf("unsupported breed field: %s", field) } } origins, err := json.Marshal(item.FieldSources) if err != nil { return err } updates["field_sources"] = string(origins) latest := original.EncyclopediaUpdatedAt for _, ref := range item.References { if ref.RetrievedAt > latest { latest = ref.RetrievedAt } } updates["encyclopedia_updated_at"] = latest // Skip unchanged columns so ordinary starts do not rewrite every breed. current := map[string]string{"english_name": breed.EnglishName, "origin": breed.Origin, "breed_group": breed.BreedGroup, "fci_number": breed.FCINumber, "height": breed.Height, "weight": breed.Weight, "life_span": breed.LifeSpan, "exercise": breed.Exercise, "grooming": breed.Grooming, "temperament": breed.Temperament, "traits": breed.Traits, "summary": breed.Summary, "care_notes": breed.CareNotes, "encyclopedia_updated_at": breed.EncyclopediaUpdatedAt} for key, value := range updates { if key != "field_sources" && current[key] == value { delete(updates, key) } } if reflect.DeepEqual(breed.FieldSources, item.FieldSources) { delete(updates, "field_sources") } if len(updates) == 0 { continue } // UpdateColumns avoids advancing UpdatedAt on each application start. if err := db.Model(&breed).Where("editorial_locked = false AND name = ?", item.Name).UpdateColumns(updates).Error; err != nil { return err } } return nil }