From 4b1fc830803f0f5122ef6bf8ad222d459ac30969 Mon Sep 17 00:00:00 2001 From: Illia Date: Thu, 4 Jun 2026 16:54:23 +0200 Subject: [PATCH 1/2] feat(resto): add detailed info and connect with google --- .env.example | 1 + cmd/server/main.go | 5 +- deploy/database/001_init_schema.sql | 10 +- internal/application/create_session.go | 14 +- internal/application/enrich_restaurants.go | 78 +++++++++ internal/application/ports.go | 10 ++ internal/domain/restaurant.go | 8 +- .../database/postgres_restaurant_repo.go | 37 ++++- .../infrastructure/memory/repositories.go | 26 --- .../places/google_places_client.go | 151 ++++++++++++++++++ internal/interfaces/ws/ws_handler.go | 2 +- 11 files changed, 305 insertions(+), 37 deletions(-) create mode 100644 internal/application/enrich_restaurants.go create mode 100644 internal/infrastructure/places/google_places_client.go diff --git a/.env.example b/.env.example index ca6f75e..b3a2045 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ POSTGRES_USER=crave_user POSTGRES_PASSWORD=your_secure_password POSTGRES_DB=cravecompass_dev DATABASE_URL=postgres://crave_user:your_secure_password@localhost:5432/cravecompass_dev +GOOGLE_PLACES_API_KEY= # Server Configuration PORT=8080 diff --git a/cmd/server/main.go b/cmd/server/main.go index 55fda80..592e6b6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,6 +10,7 @@ import ( "github.com/CraveCompass/api/internal/application" "github.com/CraveCompass/api/internal/infrastructure/database" "github.com/CraveCompass/api/internal/infrastructure/memory" + "github.com/CraveCompass/api/internal/infrastructure/places" apiHTTP "github.com/CraveCompass/api/internal/interfaces/http" "github.com/CraveCompass/api/internal/interfaces/ws" "github.com/jackc/pgx/v5/pgxpool" @@ -57,9 +58,11 @@ func main() { restaurantRepo := database.NewPostgresRestaurantRepo(dbPool) sessionRepo := memory.NewInMemorySessionRepo() + placesClient := places.NewGooglePlacesClient() wsHub := ws.NewHub() - createSessionUC := application.NewCreateSessionUseCase(restaurantRepo, sessionRepo) + enrichUC := application.NewEnrichRestaurantsUseCase(restaurantRepo, placesClient) + createSessionUC := application.NewCreateSessionUseCase(restaurantRepo, sessionRepo, enrichUC, wsHub) submitVoteUC := application.NewSubmitVoteUseCase(sessionRepo) sessionHandler := apiHTTP.NewSessionHandler(createSessionUC) diff --git a/deploy/database/001_init_schema.sql b/deploy/database/001_init_schema.sql index ee99f40..df8218d 100644 --- a/deploy/database/001_init_schema.sql +++ b/deploy/database/001_init_schema.sql @@ -14,4 +14,12 @@ CREATE TABLE IF NOT EXISTS restaurants ( -- Create a spatial index to make radius lookups blazingly fast CREATE INDEX IF NOT EXISTS idx_restaurants_location ON restaurants USING GIST (location); -CREATE INDEX IF NOT EXISTS idx_restaurants_tags ON restaurants USING GIN (cuisine_tags); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_restaurants_tags ON restaurants USING GIN (cuisine_tags); + +ALTER TABLE restaurants +ADD COLUMN IF NOT EXISTS google_place_id VARCHAR(255), +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 diff --git a/internal/application/create_session.go b/internal/application/create_session.go index 0eae96d..37c0bb2 100644 --- a/internal/application/create_session.go +++ b/internal/application/create_session.go @@ -21,12 +21,16 @@ type CreateSessionInput struct { type CreateSessionUseCase struct { restaurantRepo RestaurantRepository sessionRepo SessionRepository + enrichUC *EnrichRestaurantsUseCase + broadcaster SessionBroadcaster } -func NewCreateSessionUseCase(rr RestaurantRepository, sr SessionRepository) *CreateSessionUseCase { +func NewCreateSessionUseCase(rr RestaurantRepository, sr SessionRepository, enrichUC *EnrichRestaurantsUseCase, broadcaster SessionBroadcaster) *CreateSessionUseCase { return &CreateSessionUseCase{ restaurantRepo: rr, sessionRepo: sr, + enrichUC: enrichUC, + broadcaster: broadcaster, } } @@ -41,12 +45,10 @@ func (uc *CreateSessionUseCase) Execute(ctx context.Context, input CreateSession } if len(restaurants) < 5 { - log.Println("Not enough local restaurants found. Triggering OSM On-Demand Fetcher...") err := uc.restaurantRepo.FetchAndSaveFromOSM(ctx, input.Latitude, input.Longitude, input.RadiusMeters) if err != nil { log.Printf("Warning: OSM Fetch failed: %v", err) } - restaurants, err = uc.restaurantRepo.GetByLocation(ctx, input.Latitude, input.Longitude, input.RadiusMeters) if err != nil { return nil, err @@ -54,7 +56,7 @@ func (uc *CreateSessionUseCase) Execute(ctx context.Context, input CreateSession } if len(restaurants) == 0 { - return nil, errors.New("no restaurants found in this area, try increasing the radius") + return nil, errors.New("no restaurants found in this area") } bytes := make([]byte, 4) @@ -79,5 +81,9 @@ func (uc *CreateSessionUseCase) Execute(ctx context.Context, input CreateSession return nil, err } + if uc.enrichUC != nil { + go uc.enrichUC.ExecuteAsynchronously(session, uc.broadcaster) + } + return session, nil } diff --git a/internal/application/enrich_restaurants.go b/internal/application/enrich_restaurants.go new file mode 100644 index 0000000..75c7b7b --- /dev/null +++ b/internal/application/enrich_restaurants.go @@ -0,0 +1,78 @@ +package application + +import ( + "context" + "log" + "sync" + "time" + + "github.com/CraveCompass/api/internal/domain" +) + +type EnrichRestaurantsUseCase struct { + restaurantRepo RestaurantRepository + placesClient PlacesClient +} + +func NewEnrichRestaurantsUseCase(restaurantRepo RestaurantRepository, placesClient PlacesClient) *EnrichRestaurantsUseCase { + return &EnrichRestaurantsUseCase{ + restaurantRepo: restaurantRepo, + placesClient: placesClient, + } +} + +func (uc *EnrichRestaurantsUseCase) ExecuteAsynchronously(session *domain.Session, broadcaster SessionBroadcaster) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + var wg sync.WaitGroup + sem := make(chan struct{}, 2) + + for i := range session.Pool { + if session.Pool[i].GooglePlaceID != nil && *session.Pool[i].GooglePlaceID != "" { + continue + } + + wg.Add(1) + + go func(index int) { + defer wg.Done() + + sem <- struct{}{} + defer func() { <-sem }() + + time.Sleep(250 * time.Millisecond) + + rest := session.Pool[index] + + details, err := uc.placesClient.FetchRestaurantDetails(ctx, rest.Name, rest.Latitude, rest.Longitude) + if err != nil { + log.Printf("[Enrichment Worker] Failed to fetch details for %s: %v", rest.Name, err) + return + } + + _ = uc.restaurantRepo.UpdateGooglePlacesData( + ctx, rest.ID, &details.ID, details.Rating, details.UserRatingsTotal, details.PriceLevel, details.PhotoReference, details.FormattedAddress, // <-- Pass it here + ) + + session.Pool[index].GooglePlaceID = &details.ID + + session.Pool[index].Rating = details.Rating + session.Pool[index].UserRatingsTotal = details.UserRatingsTotal + session.Pool[index].PriceLevel = details.PriceLevel + session.Pool[index].PhotoReference = details.PhotoReference + session.Pool[index].FormattedAddress = details.FormattedAddress + + if broadcaster != nil { + broadcaster.Broadcast(session.ID, map[string]interface{}{ + "event": "SESSION_UPDATED", + "session": session, + "is_match": session.MatchedID != "", + }) + } + }(i) + } + + wg.Wait() + log.Println("[Enrichment Worker] Finished updating session pool.") +} diff --git a/internal/application/ports.go b/internal/application/ports.go index a5312f1..f8911ef 100644 --- a/internal/application/ports.go +++ b/internal/application/ports.go @@ -4,14 +4,24 @@ import ( "context" "github.com/CraveCompass/api/internal/domain" + "github.com/CraveCompass/api/internal/infrastructure/places" ) type RestaurantRepository interface { GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int) ([]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) error } type SessionRepository interface { Save(ctx context.Context, session *domain.Session) error GetByID(ctx context.Context, id string) (*domain.Session, error) } + +type PlacesClient interface { + FetchRestaurantDetails(ctx context.Context, name string, lat, lon float64) (*places.GooglePlaceResult, error) +} + +type SessionBroadcaster interface { + Broadcast(sessionID string, message interface{}) +} diff --git a/internal/domain/restaurant.go b/internal/domain/restaurant.go index 797a581..621bc43 100644 --- a/internal/domain/restaurant.go +++ b/internal/domain/restaurant.go @@ -10,6 +10,12 @@ type Restaurant struct { Longitude float64 `json:"longitude"` CuisineTags []string `json:"cuisine_tags"` PriceTier int `json:"price_tier"` - Rating float64 `json:"rating"` CreatedAt time.Time `json:"created_at"` + + GooglePlaceID *string `json:"google_place_id,omitempty"` + Rating *float64 `json:"rating,omitempty"` + UserRatingsTotal *int `json:"user_ratings_total,omitempty"` + PriceLevel *int `json:"price_level,omitempty"` + PhotoReference *string `json:"photo_reference,omitempty"` + FormattedAddress *string `json:"formatted_address,omitempty"` } diff --git a/internal/infrastructure/database/postgres_restaurant_repo.go b/internal/infrastructure/database/postgres_restaurant_repo.go index c76ccae..739f2ec 100644 --- a/internal/infrastructure/database/postgres_restaurant_repo.go +++ b/internal/infrastructure/database/postgres_restaurant_repo.go @@ -35,14 +35,14 @@ func NewPostgresRestaurantRepo(db *pgxpool.Pool) *PostgresRestaurantRepo { func (r *PostgresRestaurantRepo) GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int) ([]domain.Restaurant, error) { query := ` - SELECT id, osm_id, name, ST_Y(location::geometry) as lat, ST_X(location::geometry) as lon, cuisine_tags, price_tier, rating + 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 FROM restaurants WHERE ST_DWithin( location, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3 ) - LIMIT 30; -- Cap the deck size to prevent massive payloads + LIMIT 30; ` rows, err := r.db.Query(ctx, query, lon, lat, radiusMeters) @@ -62,8 +62,12 @@ func (r *PostgresRestaurantRepo) GetByLocation(ctx context.Context, lat, lon flo &rest.Latitude, &rest.Longitude, &rest.CuisineTags, - &rest.PriceTier, + &rest.GooglePlaceID, &rest.Rating, + &rest.UserRatingsTotal, + &rest.PriceLevel, + &rest.PhotoReference, + &rest.FormattedAddress, ); err != nil { return nil, fmt.Errorf("failed to scan restaurant row: %w", err) } @@ -146,3 +150,30 @@ func (r *PostgresRestaurantRepo) FetchAndSaveFromOSM(ctx context.Context, lat, l log.Printf("Successfully cached %d new restaurants to PostGIS!", insertedCount) return nil } + +func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id string, googlePlaceID *string, rating *float64, userRatingsTotal *int, priceLevel *int, photoReference *string, formattedAddress *string) error { + query := ` + UPDATE restaurants + SET google_place_id = $2, + rating = $3, + user_ratings_total = $4, + price_level = $5, + photo_reference = $6, + formatted_address = $7 + WHERE id = $1 + ` + + _, err := r.db.Exec( + ctx, + query, + id, + googlePlaceID, + rating, + userRatingsTotal, + priceLevel, + photoReference, + formattedAddress, + ) + + return err +} diff --git a/internal/infrastructure/memory/repositories.go b/internal/infrastructure/memory/repositories.go index 5d558ee..680f171 100644 --- a/internal/infrastructure/memory/repositories.go +++ b/internal/infrastructure/memory/repositories.go @@ -2,7 +2,6 @@ package memory import ( "context" - "time" "github.com/CraveCompass/api/internal/domain" ) @@ -13,31 +12,6 @@ func NewInMemoryRestaurantRepo() *InMemoryRestaurantRepo { return &InMemoryRestaurantRepo{} } -func (r *InMemoryRestaurantRepo) GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int) ([]domain.Restaurant, error) { - return []domain.Restaurant{ - { - ID: "rest_1", - Name: "Luigi's Pizza", - Latitude: lat + 0.001, - Longitude: lon + 0.001, - CuisineTags: []string{"Italian", "Pizza"}, - PriceTier: 2, - Rating: 4.8, - CreatedAt: time.Now(), - }, - { - ID: "rest_2", - Name: "Spicy Thai Street", - Latitude: lat - 0.002, - Longitude: lon + 0.001, - CuisineTags: []string{"Thai", "Spicy"}, - PriceTier: 1, - Rating: 4.5, - CreatedAt: time.Now(), - }, - }, nil -} - type InMemorySessionRepo struct { store map[string]*domain.Session } diff --git a/internal/infrastructure/places/google_places_client.go b/internal/infrastructure/places/google_places_client.go new file mode 100644 index 0000000..47f9c7f --- /dev/null +++ b/internal/infrastructure/places/google_places_client.go @@ -0,0 +1,151 @@ +package places + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" +) + +type GooglePlaceResult struct { + ID string + Rating *float64 + UserRatingsTotal *int + PriceLevel *int + PhotoReference *string + FormattedAddress *string +} + +type GooglePlacesClient struct { + apiKey string +} + +func NewGooglePlacesClient() *GooglePlacesClient { + return &GooglePlacesClient{ + apiKey: os.Getenv("GOOGLE_PLACES_API_KEY"), + } +} + +type searchTextRequest struct { + TextQuery string `json:"textQuery"` + LocationBias locationBias `json:"locationBias"` +} + +type locationBias struct { + Circle circle `json:"circle"` +} + +type circle struct { + Center center `json:"center"` + Radius float64 `json:"radius"` +} + +type center struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type searchTextResponse struct { + Places []place `json:"places"` +} + +type place struct { + Id string `json:"id"` + Rating float64 `json:"rating"` + UserRatingCount int `json:"userRatingCount"` + PriceLevel string `json:"priceLevel"` + FormattedAddress string `json:"formattedAddress"` + Photos []photo `json:"photos"` +} + +type photo struct { + Name string `json:"name"` +} + +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") + } + + url := "https://places.googleapis.com/v1/places:searchText" + + reqBody := searchTextRequest{ + TextQuery: name, + LocationBias: locationBias{ + Circle: circle{ + Center: center{ + Latitude: lat, + Longitude: lon, + }, + Radius: 100.0, + }, + }, + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + 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") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("google places API returned status: %d", resp.StatusCode) + } + + var resData searchTextResponse + if err := json.NewDecoder(resp.Body).Decode(&resData); err != nil { + return nil, err + } + + if len(resData.Places) == 0 { + return nil, fmt.Errorf("no places found for %s", name) + } + + p := resData.Places[0] + + result := &GooglePlaceResult{ + ID: p.Id, + Rating: &p.Rating, + UserRatingsTotal: &p.UserRatingCount, + } + + priceMap := map[string]int{ + "PRICE_LEVEL_INEXPENSIVE": 1, + "PRICE_LEVEL_MODERATE": 2, + "PRICE_LEVEL_EXPENSIVE": 3, + "PRICE_LEVEL_VERY_EXPENSIVE": 4, + } + + if val, ok := priceMap[p.PriceLevel]; ok { + priceLvl := val + result.PriceLevel = &priceLvl + } + + if len(p.Photos) > 0 { + result.PhotoReference = &p.Photos[0].Name + } + + if p.FormattedAddress != "" { + result.FormattedAddress = &p.FormattedAddress + } + + return result, nil +} diff --git a/internal/interfaces/ws/ws_handler.go b/internal/interfaces/ws/ws_handler.go index 6176a1a..392cf90 100644 --- a/internal/interfaces/ws/ws_handler.go +++ b/internal/interfaces/ws/ws_handler.go @@ -61,7 +61,7 @@ func (h *WSHandler) HandleConnection(w http.ResponseWriter, r *http.Request) { h.hub.AddClient(sessionID, conn) session, err := h.sessionRepo.GetByID(context.Background(), sessionID) - if err == nil { + if err == nil && session != nil { conn.WriteJSON(map[string]interface{}{ "event": "SESSION_UPDATED", "session": session, From 9c4314bbe407a0de2e1416cd59a2e04b1c2673c7 Mon Sep 17 00:00:00 2001 From: Illia Date: Fri, 5 Jun 2026 10:13:14 +0200 Subject: [PATCH 2/2] chore(places): improve tagging and location --- .github/workflows/deploy.yml | 3 - .github/workflows/pr_checks.yml | 28 +++++++++ Makefile | 2 +- .../{001_init_schema.sql => schema.sql} | 4 +- internal/application/enrich_restaurants.go | 3 +- internal/application/ports.go | 2 +- .../database/postgres_restaurant_repo.go | 4 +- .../places/google_places_client.go | 58 ++++++++++++------- 8 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/pr_checks.yml rename deploy/database/{001_init_schema.sql => schema.sql} (84%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0582aa1..12467b4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,6 +29,3 @@ jobs: context: . push: true tags: ghcr.io/cravecompass/cravecompass-api:latest - - - name: Trigger Dokploy Deployment - run: curl -s -X GET "${{ secrets.DOKPLOY_WEBHOOK_URL }}" diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml new file mode 100644 index 0000000..7224246 --- /dev/null +++ b/.github/workflows/pr_checks.yml @@ -0,0 +1,28 @@ +name: API PR Verification + +on: + pull_request: + branches: [main] + +jobs: + diagnostics-and-tests: + name: Backend Diagnostics & Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Go Environment + uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Go Format Check + run: go fmt ./... + + - name: Go Vet (Static Analysis) + run: go vet ./... + + - name: Run Backend Unit Tests + run: go test -v ./... diff --git a/Makefile b/Makefile index 2b51681..e5d4db3 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,4 @@ dev-down: docker compose -f deploy/docker/docker-compose.dev.yml down dev-migrate: - docker exec -i docker-postgres-1 psql -U crave_compass_admin -d cravecompass_dev < deploy/database/001_init_schema.sql \ No newline at end of file + docker exec -i docker-postgres-1 psql -U crave_compass_admin -d cravecompass_dev < deploy/database/schema.sql \ No newline at end of file diff --git a/deploy/database/001_init_schema.sql b/deploy/database/schema.sql similarity index 84% rename from deploy/database/001_init_schema.sql rename to deploy/database/schema.sql index df8218d..09c0d69 100644 --- a/deploy/database/001_init_schema.sql +++ b/deploy/database/schema.sql @@ -1,4 +1,3 @@ --- Enable the PostGIS extension for spatial queries CREATE EXTENSION IF NOT EXISTS postgis; CREATE TABLE IF NOT EXISTS restaurants ( @@ -8,11 +7,10 @@ CREATE TABLE IF NOT EXISTS restaurants ( location GEOGRAPHY(Point, 4326) NOT NULL, cuisine_tags TEXT[] NOT NULL DEFAULT '{}', price_tier INT DEFAULT 1, - rating NUMERIC(3, 2) DEFAULT 0.0, + rating NUMERIC(3, 2), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); --- Create a spatial index to make radius lookups blazingly fast CREATE INDEX IF NOT EXISTS idx_restaurants_location ON restaurants USING GIST (location); CREATE INDEX IF NOT EXISTS idx_restaurants_tags ON restaurants USING GIN (cuisine_tags); diff --git a/internal/application/enrich_restaurants.go b/internal/application/enrich_restaurants.go index 75c7b7b..ddf16dc 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, // <-- Pass it here + ctx, rest.ID, &details.ID, details.Rating, details.UserRatingsTotal, details.PriceLevel, details.PhotoReference, details.FormattedAddress, details.Tags, ) session.Pool[index].GooglePlaceID = &details.ID @@ -62,6 +62,7 @@ func (uc *EnrichRestaurantsUseCase) ExecuteAsynchronously(session *domain.Sessio session.Pool[index].PriceLevel = details.PriceLevel session.Pool[index].PhotoReference = details.PhotoReference session.Pool[index].FormattedAddress = details.FormattedAddress + session.Pool[index].CuisineTags = append(session.Pool[index].CuisineTags, details.Tags...) if broadcaster != nil { broadcaster.Broadcast(session.ID, map[string]interface{}{ diff --git a/internal/application/ports.go b/internal/application/ports.go index f8911ef..66ebe9f 100644 --- a/internal/application/ports.go +++ b/internal/application/ports.go @@ -10,7 +10,7 @@ import ( type RestaurantRepository interface { GetByLocation(ctx context.Context, lat, lon float64, radiusMeters int) ([]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) error + UpdateGooglePlacesData(ctx context.Context, id string, googlePlaceID *string, rating *float64, userRatingsTotal *int, priceLevel *int, photoReference *string, formattedAddress *string, extraTags []string) error } type SessionRepository interface { diff --git a/internal/infrastructure/database/postgres_restaurant_repo.go b/internal/infrastructure/database/postgres_restaurant_repo.go index 739f2ec..eb03ec6 100644 --- a/internal/infrastructure/database/postgres_restaurant_repo.go +++ b/internal/infrastructure/database/postgres_restaurant_repo.go @@ -151,7 +151,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) error { +func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id string, googlePlaceID *string, rating *float64, userRatingsTotal *int, priceLevel *int, photoReference *string, formattedAddress *string, extraTags []string) error { query := ` UPDATE restaurants SET google_place_id = $2, @@ -160,6 +160,7 @@ func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id price_level = $5, photo_reference = $6, formatted_address = $7 + cuisine_tags = array_cat(cuisine_tags, $8) WHERE id = $1 ` @@ -173,6 +174,7 @@ func (r *PostgresRestaurantRepo) UpdateGooglePlacesData(ctx context.Context, id priceLevel, photoReference, formattedAddress, + extraTags, ) return err diff --git a/internal/infrastructure/places/google_places_client.go b/internal/infrastructure/places/google_places_client.go index 47f9c7f..0daddc0 100644 --- a/internal/infrastructure/places/google_places_client.go +++ b/internal/infrastructure/places/google_places_client.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "os" + "strings" ) type GooglePlaceResult struct { @@ -16,6 +17,7 @@ type GooglePlaceResult struct { PriceLevel *int PhotoReference *string FormattedAddress *string + Tags []string } type GooglePlacesClient struct { @@ -29,20 +31,20 @@ func NewGooglePlacesClient() *GooglePlacesClient { } type searchTextRequest struct { - TextQuery string `json:"textQuery"` - LocationBias locationBias `json:"locationBias"` + TextQuery string `json:"textQuery"` + LocationRestriction locationRestriction `json:"locationRestriction"` } -type locationBias struct { - Circle circle `json:"circle"` +type locationRestriction struct { + Rectangle rectangle `json:"rectangle"` } -type circle struct { - Center center `json:"center"` - Radius float64 `json:"radius"` +type rectangle struct { + Low coordinates `json:"low"` + High coordinates `json:"high"` } -type center struct { +type coordinates struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` } @@ -52,12 +54,13 @@ 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"` - 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"` } type photo struct { @@ -71,15 +74,21 @@ func (c *GooglePlacesClient) FetchRestaurantDetails(ctx context.Context, name st url := "https://places.googleapis.com/v1/places:searchText" + latOffset := 0.005 + lonOffset := 0.005 + reqBody := searchTextRequest{ TextQuery: name, - LocationBias: locationBias{ - Circle: circle{ - Center: center{ - Latitude: lat, - Longitude: lon, + LocationRestriction: locationRestriction{ + Rectangle: rectangle{ + Low: coordinates{ + Latitude: lat - latOffset, + Longitude: lon - lonOffset, + }, + High: coordinates{ + Latitude: lat + latOffset, + Longitude: lon + lonOffset, }, - Radius: 100.0, }, }, } @@ -97,7 +106,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") + req.Header.Set("X-Goog-FieldMask", "places.id,places.rating,places.userRatingCount,places.priceLevel,places.photos,places.formattedAddress,places.types") client := &http.Client{} resp, err := client.Do(req) @@ -147,5 +156,12 @@ func (c *GooglePlacesClient) FetchRestaurantDetails(ctx context.Context, name st result.FormattedAddress = &p.FormattedAddress } + for _, t := range p.Types { + if t != "restaurant" && t != "food" && t != "point_of_interest" && t != "establishment" { + cleanTag := strings.ReplaceAll(t, "_", " ") + result.Tags = append(result.Tags, cleanTag) + } + } + return result, nil }