multimodal-routing/libs/carpool/rdex.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: &currency,
}
}
}
}
// 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
}