Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
28 changes: 28 additions & 0 deletions .github/workflows/pr_checks.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
docker exec -i docker-postgres-1 psql -U crave_compass_admin -d cravecompass_dev < deploy/database/schema.sql
5 changes: 4 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions deploy/database/001_init_schema.sql → deploy/database/schema.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
-- Enable the PostGIS extension for spatial queries
CREATE EXTENSION IF NOT EXISTS postgis;

CREATE TABLE IF NOT EXISTS restaurants (
Expand All @@ -8,10 +7,17 @@ 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);
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;
14 changes: 10 additions & 4 deletions internal/application/create_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand All @@ -41,20 +45,18 @@ 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
}
}

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)
Expand All @@ -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
}
79 changes: 79 additions & 0 deletions internal/application/enrich_restaurants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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, details.Tags,
)

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
session.Pool[index].CuisineTags = append(session.Pool[index].CuisineTags, details.Tags...)

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.")
}
10 changes: 10 additions & 0 deletions internal/application/ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, extraTags []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{})
}
8 changes: 7 additions & 1 deletion internal/domain/restaurant.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
39 changes: 36 additions & 3 deletions internal/infrastructure/database/postgres_restaurant_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -146,3 +150,32 @@ 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, extraTags []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
cuisine_tags = array_cat(cuisine_tags, $8)
WHERE id = $1
`

_, err := r.db.Exec(
ctx,
query,
id,
googlePlaceID,
rating,
userRatingsTotal,
priceLevel,
photoReference,
formattedAddress,
extraTags,
)

return err
}
26 changes: 0 additions & 26 deletions internal/infrastructure/memory/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package memory

import (
"context"
"time"

"github.com/CraveCompass/api/internal/domain"
)
Expand All @@ -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
}
Expand Down
Loading
Loading