From 32ec0196168cbf098488c232fc23f11d320f5b74 Mon Sep 17 00:00:00 2001 From: Illia Date: Fri, 5 Jun 2026 16:51:34 +0200 Subject: [PATCH 1/2] feat(filters): add filters func and open hours in db --- cmd/server/main.go | 10 +++ deploy/database/schema.sql | 5 +- internal/application/create_session.go | 20 +++++- internal/application/enrich_restaurants.go | 3 +- internal/application/ports.go | 5 +- internal/domain/restaurant.go | 1 + internal/domain/session.go | 7 +++ .../database/postgres_restaurant_repo.go | 62 +++++++++++++++++-- .../places/google_places_client.go | 27 +++++--- 9 files changed, 119 insertions(+), 21 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 592e6b6..d137856 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "log" "net/http" @@ -74,6 +75,15 @@ func main() { })) http.HandleFunc("/sessions", enableCORS(sessionHandler.HandleCreateRoom)) http.HandleFunc("/ws", wsHandler.HandleConnection) + http.HandleFunc("/tags", enableCORS(func(w http.ResponseWriter, r *http.Request) { + tags, err := restaurantRepo.GetUniqueCuisines(context.Background()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(tags) + })) log.Fatal(http.ListenAndServe(":"+port, nil)) } diff --git a/deploy/database/schema.sql b/deploy/database/schema.sql index 09c0d69..d63e0f6 100644 --- a/deploy/database/schema.sql +++ b/deploy/database/schema.sql @@ -20,4 +20,7 @@ ADD COLUMN IF NOT EXISTS rating NUMERIC(3, 2), ADD COLUMN IF NOT EXISTS user_ratings_total INT, ADD COLUMN IF NOT EXISTS price_level INT, ADD COLUMN IF NOT EXISTS photo_reference TEXT, -ADD COLUMN IF NOT EXISTS formatted_address TEXT; \ No newline at end of file +ADD COLUMN IF NOT EXISTS formatted_address TEXT; + +ALTER TABLE restaurants +ADD COLUMN IF NOT EXISTS opening_hours TEXT[] DEFAULT '{}'; \ No newline at end of file diff --git a/internal/application/create_session.go b/internal/application/create_session.go index 37c0bb2..a39f251 100644 --- a/internal/application/create_session.go +++ b/internal/application/create_session.go @@ -16,6 +16,10 @@ type CreateSessionInput struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` RadiusMeters int `json:"radius_meters"` + + PriceTiers []int `json:"price_tiers"` + MinRating *float64 `json:"min_rating"` + Cuisines []string `json:"cuisines"` } type CreateSessionUseCase struct { @@ -39,7 +43,7 @@ func (uc *CreateSessionUseCase) Execute(ctx context.Context, input CreateSession return nil, errors.New("radius must be greater than zero") } - restaurants, err := uc.restaurantRepo.GetByLocation(ctx, input.Latitude, input.Longitude, input.RadiusMeters) + restaurants, err := uc.restaurantRepo.GetByLocation(ctx, input.Latitude, input.Longitude, input.RadiusMeters, input.PriceTiers, input.MinRating, input.Cuisines) if err != nil { return nil, err } @@ -49,7 +53,7 @@ func (uc *CreateSessionUseCase) Execute(ctx context.Context, input CreateSession if err != nil { log.Printf("Warning: OSM Fetch failed: %v", err) } - restaurants, err = uc.restaurantRepo.GetByLocation(ctx, input.Latitude, input.Longitude, input.RadiusMeters) + restaurants, err = uc.restaurantRepo.GetByLocation(ctx, input.Latitude, input.Longitude, input.RadiusMeters, input.PriceTiers, input.MinRating, input.Cuisines) if err != nil { return nil, err } @@ -65,6 +69,11 @@ func (uc *CreateSessionUseCase) Execute(ctx context.Context, input CreateSession } sessionID := hex.EncodeToString(bytes) + minRatingVal := 0.0 + if input.MinRating != nil { + minRatingVal = *input.MinRating + } + session := &domain.Session{ ID: sessionID, HostID: input.HostID, @@ -73,7 +82,12 @@ func (uc *CreateSessionUseCase) Execute(ctx context.Context, input CreateSession Participants: []domain.Participant{ {ID: input.HostID, Username: "Host"}, }, - Pool: restaurants, + Pool: restaurants, + Filters: domain.SessionFilters{ + PriceTiers: input.PriceTiers, + MinRating: minRatingVal, + Cuisines: input.Cuisines, + }, CreatedAt: time.Now(), } diff --git a/internal/application/enrich_restaurants.go b/internal/application/enrich_restaurants.go index ddf16dc..91ffcd7 100644 --- a/internal/application/enrich_restaurants.go +++ b/internal/application/enrich_restaurants.go @@ -52,7 +52,7 @@ func (uc *EnrichRestaurantsUseCase) ExecuteAsynchronously(session *domain.Sessio } _ = uc.restaurantRepo.UpdateGooglePlacesData( - ctx, rest.ID, &details.ID, details.Rating, details.UserRatingsTotal, details.PriceLevel, details.PhotoReference, details.FormattedAddress, details.Tags, + ctx, rest.ID, &details.ID, details.Rating, details.UserRatingsTotal, details.PriceLevel, details.PhotoReference, details.FormattedAddress, details.Tags, details.OpeningHours, ) session.Pool[index].GooglePlaceID = &details.ID @@ -63,6 +63,7 @@ func (uc *EnrichRestaurantsUseCase) ExecuteAsynchronously(session *domain.Sessio session.Pool[index].PhotoReference = details.PhotoReference session.Pool[index].FormattedAddress = details.FormattedAddress session.Pool[index].CuisineTags = append(session.Pool[index].CuisineTags, details.Tags...) + session.Pool[index].OpeningHours = details.OpeningHours if broadcaster != nil { broadcaster.Broadcast(session.ID, map[string]interface{}{ diff --git a/internal/application/ports.go b/internal/application/ports.go index 66ebe9f..3ebc886 100644 --- a/internal/application/ports.go +++ b/internal/application/ports.go @@ -8,9 +8,10 @@ import ( ) type RestaurantRepository interface { - GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int) ([]domain.Restaurant, error) + GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int, priceTiers []int, minRating *float64, cuisines []string) ([]domain.Restaurant, error) FetchAndSaveFromOSM(ctx context.Context, lat, lon float64, radiusMeters int) error - UpdateGooglePlacesData(ctx context.Context, id string, googlePlaceID *string, rating *float64, userRatingsTotal *int, priceLevel *int, photoReference *string, formattedAddress *string, extraTags []string) error + UpdateGooglePlacesData(ctx context.Context, id string, googlePlaceID *string, rating *float64, userRatingsTotal *int, priceLevel *int, photoReference *string, formattedAddress *string, extraTags []string, openingHours []string) error + GetUniqueCuisines(ctx context.Context) ([]string, error) } type SessionRepository interface { diff --git a/internal/domain/restaurant.go b/internal/domain/restaurant.go index 621bc43..d321790 100644 --- a/internal/domain/restaurant.go +++ b/internal/domain/restaurant.go @@ -18,4 +18,5 @@ type Restaurant struct { PriceLevel *int `json:"price_level,omitempty"` PhotoReference *string `json:"photo_reference,omitempty"` FormattedAddress *string `json:"formatted_address,omitempty"` + OpeningHours []string `json:"opening_hours,omitempty"` } diff --git a/internal/domain/session.go b/internal/domain/session.go index ff866fb..6f67d1d 100644 --- a/internal/domain/session.go +++ b/internal/domain/session.go @@ -27,6 +27,12 @@ type Participant struct { Username string `json:"username"` } +type SessionFilters struct { + PriceTiers []int `json:"price_tiers"` + MinRating float64 `json:"min_rating"` + Cuisines []string `json:"cuisines"` +} + type Session struct { ID string `json:"id"` HostID string `json:"host_id"` @@ -37,6 +43,7 @@ type Session struct { MatchedID string `json:"matched_id,omitempty"` Votes map[string]map[string]VoteType `json:"votes"` TiedIDs []string `json:"tied_ids,omitempty"` + Filters SessionFilters `json:"filters"` CreatedAt time.Time `json:"created_at"` } diff --git a/internal/infrastructure/database/postgres_restaurant_repo.go b/internal/infrastructure/database/postgres_restaurant_repo.go index eb03ec6..1c3eb35 100644 --- a/internal/infrastructure/database/postgres_restaurant_repo.go +++ b/internal/infrastructure/database/postgres_restaurant_repo.go @@ -33,19 +33,44 @@ func NewPostgresRestaurantRepo(db *pgxpool.Pool) *PostgresRestaurantRepo { } } -func (r *PostgresRestaurantRepo) GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int) ([]domain.Restaurant, error) { +func (r *PostgresRestaurantRepo) GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int, priceTiers []int, minRating *float64, cuisines []string) ([]domain.Restaurant, error) { query := ` - SELECT id, osm_id, name, ST_Y(location::geometry) as lat, ST_X(location::geometry) as lon, cuisine_tags, google_place_id, rating, user_ratings_total, price_level, photo_reference, formatted_address + SELECT id, osm_id, name, ST_Y(location::geometry) as lat, ST_X(location::geometry) as lon, cuisine_tags, google_place_id, rating, user_ratings_total, price_level, photo_reference, formatted_address, opening_hours FROM restaurants WHERE ST_DWithin( location, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3 ) - LIMIT 30; ` - rows, err := r.db.Query(ctx, query, lon, lat, radiusMeters) + args := []interface{}{lon, lat, radiusMeters} + argCount := 3 + + if len(priceTiers) > 0 { + argCount++ + query += fmt.Sprintf(" AND (price_level = ANY($%d) OR price_tier = ANY($%d) OR price_level IS NULL)", argCount, argCount) + args = append(args, priceTiers) + } + + if minRating != nil && *minRating > 0 { + argCount++ + query += fmt.Sprintf(" AND (rating >= $%d OR rating IS NULL)", argCount) + args = append(args, *minRating) + } + + if len(cuisines) > 0 { + argCount++ + query += fmt.Sprintf(` AND EXISTS ( + SELECT 1 FROM unnest(cuisine_tags) db_tag, unnest($%d::text[]) filter_tag + WHERE db_tag ILIKE '%%' || filter_tag || '%%' + )`, argCount) + args = append(args, cuisines) + } + + query += " LIMIT 30;" + + rows, err := r.db.Query(ctx, query, args...) if err != nil { return nil, fmt.Errorf("failed to query restaurants: %w", err) } @@ -68,6 +93,7 @@ func (r *PostgresRestaurantRepo) GetByLocation(ctx context.Context, lat, lon flo &rest.PriceLevel, &rest.PhotoReference, &rest.FormattedAddress, + &rest.OpeningHours, ); err != nil { return nil, fmt.Errorf("failed to scan restaurant row: %w", err) } @@ -151,7 +177,7 @@ func (r *PostgresRestaurantRepo) FetchAndSaveFromOSM(ctx context.Context, lat, l return nil } -func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id string, googlePlaceID *string, rating *float64, userRatingsTotal *int, priceLevel *int, photoReference *string, formattedAddress *string, extraTags []string) error { +func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id string, googlePlaceID *string, rating *float64, userRatingsTotal *int, priceLevel *int, photoReference *string, formattedAddress *string, extraTags []string, openingHours []string) error { query := ` UPDATE restaurants SET google_place_id = $2, @@ -161,6 +187,7 @@ func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id photo_reference = $6, formatted_address = $7 cuisine_tags = array_cat(cuisine_tags, $8) + opening_hours = $9 WHERE id = $1 ` @@ -175,7 +202,32 @@ func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id photoReference, formattedAddress, extraTags, + openingHours, ) return err } + +func (r *PostgresRestaurantRepo) GetUniqueCuisines(ctx context.Context) ([]string, error) { + query := ` + SELECT DISTINCT initcap(replace(tag, '_', ' ')) + FROM (SELECT unnest(cuisine_tags) AS tag FROM restaurants) sub + WHERE tag != '' + ORDER BY 1 + LIMIT 50; + ` + rows, err := r.db.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var tags []string + for rows.Next() { + var tag string + if err := rows.Scan(&tag); err == nil { + tags = append(tags, tag) + } + } + return tags, nil +} diff --git a/internal/infrastructure/places/google_places_client.go b/internal/infrastructure/places/google_places_client.go index 0daddc0..cf40ff3 100644 --- a/internal/infrastructure/places/google_places_client.go +++ b/internal/infrastructure/places/google_places_client.go @@ -18,6 +18,7 @@ type GooglePlaceResult struct { PhotoReference *string FormattedAddress *string Tags []string + OpeningHours []string } type GooglePlacesClient struct { @@ -54,19 +55,24 @@ type searchTextResponse struct { } type place struct { - Id string `json:"id"` - Rating float64 `json:"rating"` - UserRatingCount int `json:"userRatingCount"` - PriceLevel string `json:"priceLevel"` - FormattedAddress string `json:"formattedAddress"` - Types []string `json:"types"` - Photos []photo `json:"photos"` + Id string `json:"id"` + Rating float64 `json:"rating"` + UserRatingCount int `json:"userRatingCount"` + PriceLevel string `json:"priceLevel"` + FormattedAddress string `json:"formattedAddress"` + Types []string `json:"types"` + Photos []photo `json:"photos"` + RegularOpeningHours regularOpeningHours `json:"regularOpeningHours"` } type photo struct { Name string `json:"name"` } +type regularOpeningHours struct { + WeekdayDescriptions []string `json:"weekdayDescriptions"` +} + func (c *GooglePlacesClient) FetchRestaurantDetails(ctx context.Context, name string, lat, lon float64) (*GooglePlaceResult, error) { if c.apiKey == "" { return nil, fmt.Errorf("GOOGLE_PLACES_API_KEY is not set") @@ -106,8 +112,7 @@ func (c *GooglePlacesClient) FetchRestaurantDetails(ctx context.Context, name st req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Goog-Api-Key", c.apiKey) - req.Header.Set("X-Goog-FieldMask", "places.id,places.rating,places.userRatingCount,places.priceLevel,places.photos,places.formattedAddress,places.types") - + req.Header.Set("X-Goog-FieldMask", "places.id,places.rating,places.userRatingCount,places.priceLevel,places.photos,places.formattedAddress,places.types,places.regularOpeningHours.weekdayDescriptions") client := &http.Client{} resp, err := client.Do(req) if err != nil { @@ -163,5 +168,9 @@ func (c *GooglePlacesClient) FetchRestaurantDetails(ctx context.Context, name st } } + if len(p.RegularOpeningHours.WeekdayDescriptions) > 0 { + result.OpeningHours = p.RegularOpeningHours.WeekdayDescriptions + } + return result, nil } From 146f2ef5fccc9e812f1327b78ac0c70e48165d1c Mon Sep 17 00:00:00 2001 From: Illia Date: Fri, 5 Jun 2026 17:01:56 +0200 Subject: [PATCH 2/2] chore(tags): update possible nil --- internal/infrastructure/database/postgres_restaurant_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/infrastructure/database/postgres_restaurant_repo.go b/internal/infrastructure/database/postgres_restaurant_repo.go index 1c3eb35..0a5fd48 100644 --- a/internal/infrastructure/database/postgres_restaurant_repo.go +++ b/internal/infrastructure/database/postgres_restaurant_repo.go @@ -222,7 +222,7 @@ func (r *PostgresRestaurantRepo) GetUniqueCuisines(ctx context.Context) ([]strin } defer rows.Close() - var tags []string + tags := []string{} for rows.Next() { var tag string if err := rows.Scan(&tag); err == nil {