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) }