package service import ( "context" "fmt" "time" "git.coopgo.io/coopgo-platform/saved-search/data/storage" "git.coopgo.io/coopgo-platform/saved-search/data/types" "github.com/google/uuid" "github.com/paulmach/orb/geojson" "github.com/rs/zerolog/log" ) // SavedSearchService handles the business logic for saved searches type SavedSearchService struct { storage storage.Storage } // NewSavedSearchService creates a new SavedSearchService instance func NewSavedSearchService(storage storage.Storage) *SavedSearchService { return &SavedSearchService{ storage: storage, } } // CreateSavedSearchParams represents the parameters for creating a saved search type CreateSavedSearchParams struct { OwnerID string `json:"owner_id"` Departure *geojson.Feature `json:"departure"` Destination *geojson.Feature `json:"destination"` DateTime time.Time `json:"datetime"` Data map[string]interface{} `json:"data"` } // UpdateSavedSearchParams represents the parameters for updating a saved search type UpdateSavedSearchParams struct { ID string `json:"id"` OwnerID string `json:"owner_id"` Departure *geojson.Feature `json:"departure"` Destination *geojson.Feature `json:"destination"` DateTime time.Time `json:"datetime"` Data map[string]interface{} `json:"data"` } // ListSavedSearchesParams represents the parameters for listing saved searches type ListSavedSearchesParams struct { OwnerID string `json:"owner_id"` Limit int `json:"limit"` Offset int `json:"offset"` } // ListSavedSearchesResult represents the result of listing saved searches type ListSavedSearchesResult struct { SavedSearches []*types.SavedSearch `json:"saved_searches"` Total int64 `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` } // CreateSavedSearch creates a new saved search func (s *SavedSearchService) CreateSavedSearch(ctx context.Context, params CreateSavedSearchParams) (*types.SavedSearch, error) { // Validate required fields if params.OwnerID == "" { return nil, fmt.Errorf("owner_id is required") } if params.Departure == nil { return nil, fmt.Errorf("departure is required") } if params.Destination == nil { return nil, fmt.Errorf("destination is required") } if params.DateTime.IsZero() { return nil, fmt.Errorf("datetime is required") } // Ensure data is not nil if params.Data == nil { params.Data = make(map[string]interface{}) } // Create saved search search := types.SavedSearch{ ID: uuid.NewString(), OwnerID: params.OwnerID, Departure: params.Departure, Destination: params.Destination, DateTime: params.DateTime, Data: params.Data, } if err := s.storage.CreateSavedSearch(ctx, search); err != nil { log.Error().Err(err).Str("owner_id", params.OwnerID).Msg("failed to create saved search") return nil, fmt.Errorf("failed to create saved search: %w", err) } log.Info().Str("id", search.ID).Str("owner_id", search.OwnerID).Msg("saved search created successfully") return &search, nil } // GetSavedSearch retrieves a saved search by ID func (s *SavedSearchService) GetSavedSearch(ctx context.Context, id string) (*types.SavedSearch, error) { if id == "" { return nil, fmt.Errorf("id is required") } search, err := s.storage.GetSavedSearch(ctx, id) if err != nil { log.Error().Err(err).Str("id", id).Msg("failed to get saved search") return nil, fmt.Errorf("failed to get saved search: %w", err) } return search, nil } // GetSavedSearchesByOwner retrieves all saved searches for a specific owner func (s *SavedSearchService) GetSavedSearchesByOwner(ctx context.Context, ownerID string) ([]*types.SavedSearch, error) { if ownerID == "" { return nil, fmt.Errorf("owner_id is required") } searches, err := s.storage.GetSavedSearchesByOwner(ctx, ownerID) if err != nil { log.Error().Err(err).Str("owner_id", ownerID).Msg("failed to get saved searches by owner") return nil, fmt.Errorf("failed to get saved searches: %w", err) } return searches, nil } // UpdateSavedSearch updates an existing saved search func (s *SavedSearchService) UpdateSavedSearch(ctx context.Context, params UpdateSavedSearchParams) (*types.SavedSearch, error) { // Validate required fields if params.ID == "" { return nil, fmt.Errorf("id is required") } if params.OwnerID == "" { return nil, fmt.Errorf("owner_id is required") } if params.Departure == nil { return nil, fmt.Errorf("departure is required") } if params.Destination == nil { return nil, fmt.Errorf("destination is required") } if params.DateTime.IsZero() { return nil, fmt.Errorf("datetime is required") } // Check if the saved search exists and belongs to the owner existing, err := s.storage.GetSavedSearch(ctx, params.ID) if err != nil { return nil, fmt.Errorf("saved search not found: %w", err) } if existing.OwnerID != params.OwnerID { return nil, fmt.Errorf("access denied: saved search belongs to different owner") } // Ensure data is not nil if params.Data == nil { params.Data = make(map[string]interface{}) } // Update saved search search := types.SavedSearch{ ID: params.ID, OwnerID: params.OwnerID, Departure: params.Departure, Destination: params.Destination, DateTime: params.DateTime, Data: params.Data, CreatedAt: existing.CreatedAt, // Preserve creation time } if err := s.storage.UpdateSavedSearch(ctx, search); err != nil { log.Error().Err(err).Str("id", params.ID).Msg("failed to update saved search") return nil, fmt.Errorf("failed to update saved search: %w", err) } log.Info().Str("id", search.ID).Str("owner_id", search.OwnerID).Msg("saved search updated successfully") return &search, nil } // DeleteSavedSearch deletes a saved search by ID, with owner verification func (s *SavedSearchService) DeleteSavedSearch(ctx context.Context, id, ownerID string) error { if id == "" { return fmt.Errorf("id is required") } if ownerID == "" { return fmt.Errorf("owner_id is required") } // Check if the saved search exists and belongs to the owner existing, err := s.storage.GetSavedSearch(ctx, id) if err != nil { return fmt.Errorf("saved search not found: %w", err) } if existing.OwnerID != ownerID { return fmt.Errorf("access denied: saved search belongs to different owner") } if err := s.storage.DeleteSavedSearch(ctx, id); err != nil { log.Error().Err(err).Str("id", id).Msg("failed to delete saved search") return fmt.Errorf("failed to delete saved search: %w", err) } log.Info().Str("id", id).Str("owner_id", ownerID).Msg("saved search deleted successfully") return nil } // ListSavedSearches retrieves paginated saved searches with optional filtering func (s *SavedSearchService) ListSavedSearches(ctx context.Context, params ListSavedSearchesParams) (*ListSavedSearchesResult, error) { // Set default pagination values if params.Limit <= 0 { params.Limit = 10 } if params.Limit > 100 { params.Limit = 100 // Max limit } if params.Offset < 0 { params.Offset = 0 } searches, total, err := s.storage.ListSavedSearches(ctx, params.OwnerID, params.Limit, params.Offset) if err != nil { log.Error().Err(err).Str("owner_id", params.OwnerID).Msg("failed to list saved searches") return nil, fmt.Errorf("failed to list saved searches: %w", err) } return &ListSavedSearchesResult{ SavedSearches: searches, Total: total, Limit: params.Limit, Offset: params.Offset, }, nil }