package feeds import ( "strings" "time" "github.com/google/uuid" ) // PresentFeed maps an input_feeds row to the public/legacy feed DTO. // Legacy fields (item_path, is_active, last_synced, product_count) are always set; // v2 fields (feed_type, sync_interval_minutes, last_synced_at, options) are included for dual-support. func PresentFeed(feed map[string]any) map[string]any { if feed == nil { return nil } opts, _ := feed["options"].(map[string]any) if opts == nil { opts = map[string]any{} } itemPath, _ := opts["item_path"].(string) if itemPath == "" { itemPath, _ = feed["item_path"].(string) } status, _ := feed["status"].(string) lastSynced := formatAPITime(feed["last_synced_at"]) productsUpdated := formatAPITime(feed["products_updated_at"]) productCount := intFromAny(feed["product_count"]) mappingFieldCount := intFromAny(feed["mapping_field_count"]) hasMappings := mappingFieldCount > 0 if v, ok := feed["has_mappings"].(bool); ok { hasMappings = v || mappingFieldCount > 0 } mappingIncomplete := !hasMappings if v, ok := feed["mapping_incomplete"].(bool); ok { mappingIncomplete = v } // last_data_at: live sync timestamp when present, else latest raw product update // (covers MySQL→Postgres imports where last_synced_at was never set). lastDataAt := lastSynced if lastDataAt == nil { lastDataAt = productsUpdated } out := map[string]any{ "id": stringifyID(feed["id"]), "name": feed["name"], "url": nullish(feed["url"]), "item_path": itemPath, "is_active": strings.EqualFold(status, "active"), "product_count": productCount, "mapping_field_count": mappingFieldCount, "has_mappings": hasMappings, "mapping_incomplete": mappingIncomplete, "status": status, "last_synced": lastSynced, "created_at": formatAPITime(feed["created_at"]), "updated_at": formatAPITime(feed["updated_at"]), "feed_type": feed["feed_type"], "sync_interval_minutes": feed["sync_interval_minutes"], "last_synced_at": lastSynced, "products_updated_at": productsUpdated, "last_data_at": lastDataAt, "options": opts, } if deltas, ok := opts["last_sync_deltas"].(map[string]any); ok && deltas != nil { out["last_sync_deltas"] = deltas } return out } func intFromAny(v any) int { switch t := v.(type) { case int: return t case int32: return int(t) case int64: return int(t) case float64: return int(t) default: return 0 } } // PresentFeeds maps a page of feed rows through PresentFeed. func PresentFeeds(items []map[string]any) []map[string]any { out := make([]map[string]any, 0, len(items)) for _, item := range items { out = append(out, PresentFeed(item)) } return out } func stringifyID(v any) any { switch t := v.(type) { case uuid.UUID: return t.String() case [16]byte: return uuid.UUID(t).String() default: return v } } func nullish(v any) any { if v == nil { return nil } if s, ok := v.(string); ok && s == "" { return nil } return v } func formatAPITime(v any) any { if v == nil { return nil } switch t := v.(type) { case time.Time: if t.IsZero() { return nil } return t.UTC().Format(time.RFC3339) case *time.Time: if t == nil || t.IsZero() { return nil } return t.UTC().Format(time.RFC3339) case string: if t == "" { return nil } return t default: return v } }