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 }