472 lines
12 KiB
Go
472 lines
12 KiB
Go
package carpool
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss"
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type RDEXCarpoolAPI struct {
|
|
OperatorId string
|
|
OperatorName string
|
|
PublicKey string
|
|
PrivateKey string
|
|
BaseURL string
|
|
}
|
|
|
|
func NewRDEXCarpoolAPI(operatorId string, operatorName string, apiKey string, baseURL string) (*RDEXCarpoolAPI, error) {
|
|
// apiKey is expected to be in format "publicKey:privateKey"
|
|
// For backward compatibility, if no colon, treat the whole string as private key
|
|
publicKey := ""
|
|
privateKey := apiKey
|
|
|
|
// Try to split if it contains colon
|
|
parts := []rune(apiKey)
|
|
colonIdx := -1
|
|
for i, r := range parts {
|
|
if r == ':' {
|
|
colonIdx = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if colonIdx > 0 {
|
|
publicKey = string(parts[:colonIdx])
|
|
privateKey = string(parts[colonIdx+1:])
|
|
}
|
|
|
|
return &RDEXCarpoolAPI{
|
|
OperatorId: operatorId,
|
|
OperatorName: operatorName,
|
|
PublicKey: publicKey,
|
|
PrivateKey: privateKey,
|
|
BaseURL: baseURL,
|
|
}, nil
|
|
}
|
|
|
|
// RDEX Journey response structures
|
|
type RDEXJourney struct {
|
|
UUID *int64 `json:"uuid"`
|
|
Operator *string `json:"operator"`
|
|
Origin *string `json:"origin"`
|
|
URL *string `json:"url"`
|
|
Driver *RDEXUser `json:"driver"`
|
|
From *RDEXWaypoint `json:"from"`
|
|
To *RDEXWaypoint `json:"to"`
|
|
Distance *int64 `json:"distance"`
|
|
Duration *int64 `json:"duration"`
|
|
Cost *RDEXCost `json:"cost"`
|
|
Outward *RDEXOutward `json:"outward"`
|
|
Type *string `json:"type"`
|
|
Frequency *string `json:"frequency"`
|
|
}
|
|
|
|
type RDEXUser struct {
|
|
UUID *int64 `json:"uuid"`
|
|
Alias *string `json:"alias"`
|
|
Image *string `json:"image"`
|
|
Gender *string `json:"gender"`
|
|
Seats *int64 `json:"seats"`
|
|
State *int `json:"state"`
|
|
}
|
|
|
|
type RDEXCar struct {
|
|
Model *string `json:"model"`
|
|
}
|
|
|
|
type RDEXWaypoint struct {
|
|
Address *string `json:"address"`
|
|
City *string `json:"city"`
|
|
PostalCode *string `json:"postalcode"`
|
|
Country *string `json:"country"`
|
|
Latitude *string `json:"latitude"`
|
|
Longitude *string `json:"longitude"`
|
|
}
|
|
|
|
type RDEXCost struct {
|
|
Variable *string `json:"variable"`
|
|
}
|
|
|
|
type RDEXOutward struct {
|
|
MinDate *string `json:"mindate"`
|
|
MaxDate *string `json:"maxdate"`
|
|
Monday *RDEXTimeRange `json:"monday"`
|
|
}
|
|
|
|
type RDEXTimeRange struct {
|
|
MinTime *string `json:"mintime"`
|
|
MaxTime *string `json:"maxtime"`
|
|
}
|
|
|
|
type RDEXJourneysResponse struct {
|
|
Journeys RDEXJourney `json:"journeys"`
|
|
}
|
|
|
|
func (api *RDEXCarpoolAPI) GetOperatorId() string {
|
|
return api.OperatorId
|
|
}
|
|
|
|
func (api *RDEXCarpoolAPI) GetDriverJourneys(
|
|
departureLat float64,
|
|
departureLng float64,
|
|
arrivalLat float64,
|
|
arrivalLng float64,
|
|
departureDate time.Time,
|
|
timeDelta *time.Duration,
|
|
departureRadius int64,
|
|
arrivalRadius int64,
|
|
count int64,
|
|
) ([]ocss.DriverJourney, error) {
|
|
td := 1 * time.Hour
|
|
if timeDelta != nil {
|
|
td = *timeDelta
|
|
}
|
|
|
|
minDate := departureDate.Add(-td)
|
|
maxDate := departureDate.Add(td)
|
|
|
|
log.Info().
|
|
Str("operator", api.OperatorId).
|
|
Float64("departure_lat", departureLat).
|
|
Float64("departure_lng", departureLng).
|
|
Float64("arrival_lat", arrivalLat).
|
|
Float64("arrival_lng", arrivalLng).
|
|
Time("departure_date", departureDate).
|
|
Time("min_date", minDate).
|
|
Time("max_date", maxDate).
|
|
Msg("RDEX GetDriverJourneys request")
|
|
|
|
results, err := rdexSearch(
|
|
api.BaseURL+"/rdex/journeys",
|
|
api.PublicKey,
|
|
api.PrivateKey,
|
|
departureLat,
|
|
departureLng,
|
|
arrivalLat,
|
|
arrivalLng,
|
|
minDate,
|
|
maxDate,
|
|
true, // driver journeys
|
|
)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("operator", api.OperatorId).Msg("error in rdexSearch")
|
|
return nil, err
|
|
}
|
|
|
|
log.Info().
|
|
Str("operator", api.OperatorId).
|
|
Int("raw_results_count", len(results)).
|
|
Msg("RDEX search returned results")
|
|
|
|
journeys := []ocss.DriverJourney{}
|
|
|
|
for _, r := range results {
|
|
// Skip if not a driver journey (check driver state)
|
|
if r.Driver == nil {
|
|
log.Debug().Msg("Skipping journey: Driver is nil")
|
|
continue
|
|
}
|
|
if r.Driver.State == nil {
|
|
log.Debug().Msg("Skipping journey: Driver.State is nil")
|
|
continue
|
|
}
|
|
if *r.Driver.State != 1 {
|
|
log.Debug().Int("state", *r.Driver.State).Msg("Skipping journey: Driver.State is not 1")
|
|
continue
|
|
}
|
|
|
|
// Parse price from cost.variable
|
|
var price *ocss.Price
|
|
if r.Cost != nil && r.Cost.Variable != nil {
|
|
// Parse the variable cost string (e.g., "0.060345") to float
|
|
if costFloat, err := strconv.ParseFloat(*r.Cost.Variable, 64); err == nil {
|
|
// Convert cost per km to total price (distance is in meters)
|
|
if r.Distance != nil {
|
|
totalPrice := costFloat * float64(*r.Distance) / 1000.0
|
|
paying := ocss.Paying
|
|
currency := "EUR"
|
|
price = &ocss.Price{
|
|
Type: &paying,
|
|
Amount: &totalPrice,
|
|
Currency: ¤cy,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse pickup datetime from outward
|
|
var pickupDatetime ocss.OCSSTime
|
|
if r.Outward != nil && r.Outward.MinDate != nil && r.Outward.Monday != nil && r.Outward.Monday.MinTime != nil {
|
|
dateTimeStr := fmt.Sprintf("%sT%s", *r.Outward.MinDate, *r.Outward.Monday.MinTime)
|
|
parsedTime, err := time.Parse("2006-01-02T15:04:05", dateTimeStr)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("datetime", dateTimeStr).Msg("error parsing RDEX datetime")
|
|
pickupDatetime = ocss.OCSSTime(departureDate)
|
|
} else {
|
|
pickupDatetime = ocss.OCSSTime(parsedTime)
|
|
}
|
|
} else {
|
|
pickupDatetime = ocss.OCSSTime(departureDate)
|
|
}
|
|
|
|
// Get driver alias
|
|
driverAlias := "Utilisateur RDEX"
|
|
if r.Driver != nil && r.Driver.Alias != nil {
|
|
driverAlias = *r.Driver.Alias
|
|
}
|
|
|
|
// Convert duration from seconds to duration
|
|
var duration *time.Duration
|
|
if r.Duration != nil {
|
|
d := time.Duration(*r.Duration) * time.Second
|
|
duration = &d
|
|
}
|
|
|
|
// Parse coordinates from string to float64
|
|
var fromLat, fromLng, toLat, toLng float64
|
|
if r.From != nil && r.From.Latitude != nil && r.From.Longitude != nil {
|
|
fromLat, _ = strconv.ParseFloat(*r.From.Latitude, 64)
|
|
fromLng, _ = strconv.ParseFloat(*r.From.Longitude, 64)
|
|
}
|
|
if r.To != nil && r.To.Latitude != nil && r.To.Longitude != nil {
|
|
toLat, _ = strconv.ParseFloat(*r.To.Latitude, 64)
|
|
toLng, _ = strconv.ParseFloat(*r.To.Longitude, 64)
|
|
}
|
|
|
|
// Build pickup and drop addresses from RDEX waypoint data
|
|
var pickupAddress, dropAddress *string
|
|
if r.From != nil {
|
|
if r.From.Address != nil {
|
|
pickupAddress = r.From.Address
|
|
} else if r.From.City != nil {
|
|
pickupAddress = r.From.City
|
|
}
|
|
}
|
|
if r.To != nil {
|
|
if r.To.Address != nil {
|
|
dropAddress = r.To.Address
|
|
} else if r.To.City != nil {
|
|
dropAddress = r.To.City
|
|
}
|
|
}
|
|
|
|
// Get available seats from driver
|
|
var availableSeats *int64
|
|
if r.Driver.Seats != nil {
|
|
availableSeats = r.Driver.Seats
|
|
}
|
|
|
|
// Convert UUID to string for ID
|
|
var uuidStr *string
|
|
if r.UUID != nil {
|
|
s := fmt.Sprintf("%d", *r.UUID)
|
|
uuidStr = &s
|
|
}
|
|
|
|
// Build the driver journey
|
|
driverJourney := ocss.DriverJourney{
|
|
DriverTrip: ocss.DriverTrip{
|
|
Driver: ocss.User{
|
|
ID: uuid.NewString(),
|
|
Operator: api.OperatorName,
|
|
Alias: driverAlias,
|
|
},
|
|
Trip: ocss.Trip{
|
|
Operator: api.OperatorName,
|
|
PassengerPickupLat: fromLat,
|
|
PassengerPickupLng: fromLng,
|
|
PassengerDropLat: toLat,
|
|
PassengerDropLng: toLng,
|
|
PassengerPickupAddress: pickupAddress,
|
|
PassengerDropAddress: dropAddress,
|
|
Duration: nilCheck(duration),
|
|
Distance: r.Distance,
|
|
// JourneyPolyline is not provided by RDEX standard
|
|
},
|
|
},
|
|
JourneySchedule: ocss.JourneySchedule{
|
|
ID: uuidStr,
|
|
PassengerPickupDate: pickupDatetime,
|
|
WebUrl: r.URL,
|
|
},
|
|
AvailableSteats: availableSeats,
|
|
Price: price,
|
|
}
|
|
journeys = append(journeys, driverJourney)
|
|
}
|
|
|
|
log.Info().
|
|
Str("operator", api.OperatorId).
|
|
Int("filtered_journeys_count", len(journeys)).
|
|
Msg("RDEX GetDriverJourneys completed")
|
|
|
|
return journeys, nil
|
|
}
|
|
|
|
func (api *RDEXCarpoolAPI) GetPassengerJourneys(
|
|
departureLat float64,
|
|
departureLng float64,
|
|
arrivalLat float64,
|
|
arrivalLng float64,
|
|
departureDate time.Time,
|
|
timeDelta *time.Duration,
|
|
departureRadius int64,
|
|
arrivalRadius int64,
|
|
count int64,
|
|
) ([]ocss.PassengerJourney, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func rdexSearch(baseURL string, publicKey string, privateKey string, departureLat float64, departureLng float64, arrivalLat float64, arrivalLng float64, minDate time.Time, maxDate time.Time, isDriver bool) ([]RDEXJourney, error) {
|
|
// Build query parameters
|
|
params := url.Values{}
|
|
|
|
// Set driver or passenger state based on search type
|
|
if isDriver {
|
|
params.Add("p[driver][state]", "1")
|
|
params.Add("p[passenger][state]", "0")
|
|
} else {
|
|
params.Add("p[driver][state]", "0")
|
|
params.Add("p[passenger][state]", "1")
|
|
}
|
|
|
|
// Add geographic coordinates
|
|
params.Add("p[from][latitude]", fmt.Sprintf("%f", departureLat))
|
|
params.Add("p[from][longitude]", fmt.Sprintf("%f", departureLng))
|
|
params.Add("p[to][latitude]", fmt.Sprintf("%f", arrivalLat))
|
|
params.Add("p[to][longitude]", fmt.Sprintf("%f", arrivalLng))
|
|
|
|
// Add date range
|
|
params.Add("p[outward][mindate]", minDate.Format("2006-01-02"))
|
|
params.Add("p[outward][maxdate]", maxDate.Format("2006-01-02"))
|
|
|
|
// Set to punctual frequency
|
|
params.Add("frequency", "punctual")
|
|
|
|
// Add timestamp
|
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
|
params.Add("timestamp", timestamp)
|
|
|
|
// Add API key
|
|
params.Add("apikey", publicKey)
|
|
|
|
// Calculate signature using HMAC-SHA256
|
|
signature := calculateRDEXSignature(params, privateKey)
|
|
params.Add("signature", signature)
|
|
|
|
// Build final URL
|
|
finalURL := baseURL + "?" + params.Encode()
|
|
|
|
log.Debug().
|
|
Str("url", finalURL).
|
|
Str("public_key", publicKey).
|
|
Bool("is_driver", isDriver).
|
|
Msg("RDEX API request")
|
|
|
|
req, err := http.NewRequest("GET", finalURL, nil)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("new request issue")
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("url", baseURL).Msg("error in RDEX request")
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
log.Debug().
|
|
Int("status_code", resp.StatusCode).
|
|
Str("status", resp.Status).
|
|
Msg("RDEX API response received")
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
log.Error().
|
|
Int("status", resp.StatusCode).
|
|
Bytes("body", body).
|
|
Str("url", baseURL).
|
|
Msg("RDEX API returned non-200 status")
|
|
return nil, fmt.Errorf("RDEX API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("error reading response body")
|
|
return nil, err
|
|
}
|
|
|
|
log.Debug().
|
|
Str("response_body", string(body)).
|
|
Msg("RDEX API raw response")
|
|
|
|
var wrappedJourneys []RDEXJourneysResponse
|
|
err = json.Unmarshal(body, &wrappedJourneys)
|
|
if err != nil {
|
|
log.Error().
|
|
Err(err).
|
|
Str("resp_body", string(body)).
|
|
Any("status", resp.Status).
|
|
Str("url", baseURL).
|
|
Msg("cannot parse json response from RDEX API")
|
|
return nil, err
|
|
}
|
|
|
|
// Unwrap the journeys from the response structure
|
|
journeys := make([]RDEXJourney, 0, len(wrappedJourneys))
|
|
for _, wrapped := range wrappedJourneys {
|
|
journeys = append(journeys, wrapped.Journeys)
|
|
}
|
|
|
|
log.Debug().
|
|
Int("journeys_count", len(journeys)).
|
|
Msg("RDEX API response parsed successfully")
|
|
|
|
return journeys, nil
|
|
}
|
|
|
|
// calculateRDEXSignature computes the HMAC-SHA256 signature for RDEX authentication
|
|
func calculateRDEXSignature(params url.Values, privateKey string) string {
|
|
// Get all parameter keys and sort them alphabetically
|
|
keys := make([]string, 0, len(params))
|
|
for key := range params {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
// Build the string to sign by concatenating sorted key=value pairs
|
|
var signatureData string
|
|
for _, key := range keys {
|
|
values := params[key]
|
|
for _, value := range values {
|
|
signatureData += key + "=" + value
|
|
}
|
|
}
|
|
|
|
log.Debug().
|
|
Str("signature_data", signatureData).
|
|
Msg("RDEX signature calculation")
|
|
|
|
// Calculate HMAC-SHA256
|
|
h := hmac.New(sha256.New, []byte(privateKey))
|
|
h.Write([]byte(signatureData))
|
|
signature := hex.EncodeToString(h.Sum(nil))
|
|
|
|
return signature
|
|
}
|