Files
gooseek/backend/internal/travel/travelpayouts.go
home 08bd41e75c feat: travel service with 2GIS routing, POI, hotels + finance providers + UI overhaul
- Add travel-svc microservice (Amadeus, TravelPayouts, 2GIS, OpenRouteService)
- Add travel orchestrator with parallel collectors (events, POI, hotels, flights)
- Add 2GIS road routing with transport cost calculation (car/bus/taxi)
- Add TravelMap (2GIS MapGL) and TravelWidgets components
- Add useTravelChat hook for streaming travel agent responses
- Add finance heatmap providers refactor
- Add SearXNG settings, API proxy routes, Docker compose updates
- Update Dockerfiles, config, types, and all UI pages for consistency

Made-with: Cursor
2026-03-01 21:58:32 +03:00

277 lines
7.2 KiB
Go

package travel
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
type TravelPayoutsClient struct {
token string
marker string
baseURL string
httpClient *http.Client
}
type TravelPayoutsConfig struct {
Token string
Marker string
BaseURL string
}
func NewTravelPayoutsClient(cfg TravelPayoutsConfig) *TravelPayoutsClient {
baseURL := cfg.BaseURL
if baseURL == "" {
baseURL = "https://api.travelpayouts.com"
}
return &TravelPayoutsClient{
token: cfg.Token,
marker: cfg.Marker,
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *TravelPayoutsClient) doRequest(ctx context.Context, path string, query url.Values) ([]byte, error) {
if query == nil {
query = url.Values{}
}
query.Set("token", c.token)
if c.marker != "" {
query.Set("marker", c.marker)
}
fullURL := c.baseURL + path + "?" + query.Encode()
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
}
return body, nil
}
func (c *TravelPayoutsClient) SearchFlights(ctx context.Context, req FlightSearchRequest) ([]FlightOffer, error) {
query := url.Values{}
query.Set("origin", req.Origin)
query.Set("destination", req.Destination)
query.Set("depart_date", req.DepartureDate)
if req.ReturnDate != "" {
query.Set("return_date", req.ReturnDate)
}
query.Set("adults", fmt.Sprintf("%d", req.Adults))
if req.Currency != "" {
query.Set("currency", req.Currency)
} else {
query.Set("currency", "rub")
}
query.Set("limit", "10")
body, err := c.doRequest(ctx, "/aviasales/v3/prices_for_dates", query)
if err != nil {
return nil, err
}
var response struct {
Success bool `json:"success"`
Data []struct {
Origin string `json:"origin"`
Destination string `json:"destination"`
OriginAirport string `json:"origin_airport"`
DestAirport string `json:"destination_airport"`
Price float64 `json:"price"`
Airline string `json:"airline"`
FlightNumber string `json:"flight_number"`
DepartureAt string `json:"departure_at"`
ReturnAt string `json:"return_at"`
Transfers int `json:"transfers"`
ReturnTransfers int `json:"return_transfers"`
Duration int `json:"duration"`
Link string `json:"link"`
} `json:"data"`
}
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("unmarshal flights: %w", err)
}
offers := make([]FlightOffer, 0, len(response.Data))
for _, d := range response.Data {
offer := FlightOffer{
ID: fmt.Sprintf("%s-%s-%s", d.Origin, d.Destination, d.DepartureAt),
Airline: d.Airline,
FlightNumber: d.FlightNumber,
DepartureAirport: d.OriginAirport,
DepartureCity: d.Origin,
DepartureTime: d.DepartureAt,
ArrivalAirport: d.DestAirport,
ArrivalCity: d.Destination,
ArrivalTime: d.ReturnAt,
Duration: d.Duration,
Stops: d.Transfers,
Price: d.Price,
Currency: req.Currency,
BookingURL: "https://www.aviasales.ru" + d.Link,
}
offers = append(offers, offer)
}
return offers, nil
}
func (c *TravelPayoutsClient) GetCheapestPrices(ctx context.Context, origin, destination string, currency string) ([]FlightOffer, error) {
query := url.Values{}
query.Set("origin", origin)
query.Set("destination", destination)
if currency != "" {
query.Set("currency", currency)
} else {
query.Set("currency", "rub")
}
body, err := c.doRequest(ctx, "/aviasales/v3/prices_for_dates", query)
if err != nil {
return nil, err
}
var response struct {
Success bool `json:"success"`
Data []struct {
DepartDate string `json:"depart_date"`
ReturnDate string `json:"return_date"`
Origin string `json:"origin"`
Destination string `json:"destination"`
Price float64 `json:"price"`
Airline string `json:"airline"`
Transfers int `json:"number_of_changes"`
Link string `json:"link"`
} `json:"data"`
}
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("unmarshal prices: %w", err)
}
offers := make([]FlightOffer, 0, len(response.Data))
for _, d := range response.Data {
offer := FlightOffer{
ID: fmt.Sprintf("%s-%s-%s", d.Origin, d.Destination, d.DepartDate),
Airline: d.Airline,
DepartureCity: d.Origin,
DepartureTime: d.DepartDate,
ArrivalCity: d.Destination,
ArrivalTime: d.ReturnDate,
Stops: d.Transfers,
Price: d.Price,
Currency: currency,
BookingURL: "https://www.aviasales.ru" + d.Link,
}
offers = append(offers, offer)
}
return offers, nil
}
func (c *TravelPayoutsClient) GetPopularDestinations(ctx context.Context, origin string) ([]TravelSuggestion, error) {
query := url.Values{}
query.Set("origin", origin)
query.Set("currency", "rub")
body, err := c.doRequest(ctx, "/aviasales/v3/city_directions", query)
if err != nil {
return nil, err
}
var response struct {
Success bool `json:"success"`
Data map[string]struct {
Origin string `json:"origin"`
Destination string `json:"destination"`
Price float64 `json:"price"`
Transfers int `json:"transfers"`
Airline string `json:"airline"`
DepartDate string `json:"departure_at"`
ReturnDate string `json:"return_at"`
} `json:"data"`
}
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("unmarshal destinations: %w", err)
}
suggestions := make([]TravelSuggestion, 0)
for dest, d := range response.Data {
suggestion := TravelSuggestion{
ID: dest,
Type: "destination",
Title: dest,
Description: fmt.Sprintf("от %s, %d пересадок", d.Airline, d.Transfers),
Price: d.Price,
Currency: "RUB",
}
suggestions = append(suggestions, suggestion)
}
return suggestions, nil
}
func (c *TravelPayoutsClient) GetAirportByIATA(ctx context.Context, iata string) (*GeoLocation, error) {
query := url.Values{}
query.Set("code", iata)
query.Set("locale", "ru")
body, err := c.doRequest(ctx, "/data/ru/airports.json", query)
if err != nil {
return nil, err
}
var airports []struct {
Code string `json:"code"`
Name string `json:"name"`
Coordinates []float64 `json:"coordinates"`
Country string `json:"country_code"`
City string `json:"city_code"`
}
if err := json.Unmarshal(body, &airports); err != nil {
return nil, fmt.Errorf("unmarshal airports: %w", err)
}
for _, a := range airports {
if a.Code == iata && len(a.Coordinates) >= 2 {
return &GeoLocation{
Lng: a.Coordinates[0],
Lat: a.Coordinates[1],
Name: a.Name,
Country: a.Country,
}, nil
}
}
return nil, fmt.Errorf("airport not found: %s", iata)
}