Add MCP server
This commit is contained in:
parent
d992a7984f
commit
52de8d363e
30
config.go
30
config.go
|
|
@ -20,7 +20,7 @@ func ReadConfig() (*viper.Viper, error) {
|
|||
"listen": "0.0.0.0:8080",
|
||||
},
|
||||
"mcp": map[string]any{
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"listen": "0.0.0.0:8081",
|
||||
},
|
||||
},
|
||||
|
|
@ -123,6 +123,26 @@ func ReadConfig() (*viper.Viper, error) {
|
|||
"journeys": map[string]any{
|
||||
"enabled": true,
|
||||
"search_view": "tabs",
|
||||
"solutions": map[string]any{
|
||||
"solidarity_transport": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
"organized_carpool": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
"carpool_operators": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
"transit": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
"fleet_vehicles": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
"knowledge_base": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"solidarity_transport": map[string]any{
|
||||
"enabled": true,
|
||||
|
|
@ -226,8 +246,14 @@ func ReadConfig() (*viper.Viper, error) {
|
|||
},
|
||||
},
|
||||
"geo": map[string]any{
|
||||
"type": "addok", // Options: "pelias", "addok"
|
||||
"pelias": map[string]any{
|
||||
"url": "https://geocode.ridygo.fr",
|
||||
"url": "https://geocode.ridygo.fr",
|
||||
"autocomplete": "/autocomplete?text=",
|
||||
},
|
||||
"addok": map[string]any{
|
||||
"url": "https://api-adresse.data.gouv.fr",
|
||||
"autocomplete": "/search/?q=",
|
||||
},
|
||||
},
|
||||
"geography": map[string]any{
|
||||
|
|
|
|||
|
|
@ -282,6 +282,8 @@ func (h *ApplicationHandler) GetBeneficiaryData(ctx context.Context, beneficiary
|
|||
solidarityTransportBookings = append(solidarityTransportBookings, booking)
|
||||
}
|
||||
|
||||
// Don't filter out replaced bookings from beneficiary profile - show all bookings
|
||||
|
||||
// Collect unique driver IDs
|
||||
driverIDs := []string{}
|
||||
driverIDsMap := make(map[string]bool)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ type SearchJourneysResult struct {
|
|||
KnowledgeBaseResults []any
|
||||
}
|
||||
|
||||
// SearchJourneyOptions contains per-request options for journey search
|
||||
type SearchJourneyOptions struct {
|
||||
DisableSolidarityTransport bool
|
||||
DisableOrganizedCarpool bool
|
||||
DisableCarpoolOperators bool
|
||||
DisableTransit bool
|
||||
DisableFleetVehicles bool
|
||||
DisableKnowledgeBase bool
|
||||
}
|
||||
|
||||
// SearchJourneys performs the business logic for journey search
|
||||
func (h *ApplicationHandler) SearchJourneys(
|
||||
ctx context.Context,
|
||||
|
|
@ -46,6 +56,8 @@ func (h *ApplicationHandler) SearchJourneys(
|
|||
destinationGeo *geojson.Feature,
|
||||
passengerID string,
|
||||
solidarityTransportExcludeDriver string,
|
||||
solidarityExcludeGroupId string,
|
||||
options *SearchJourneyOptions,
|
||||
) (*SearchJourneysResult, error) {
|
||||
var (
|
||||
// Results
|
||||
|
|
@ -64,6 +76,19 @@ func (h *ApplicationHandler) SearchJourneys(
|
|||
if departureGeo != nil && destinationGeo != nil && !departureDateTime.IsZero() {
|
||||
searched = true
|
||||
|
||||
// Default options if not provided
|
||||
if options == nil {
|
||||
options = &SearchJourneyOptions{}
|
||||
}
|
||||
|
||||
// Check solution type configurations (global config AND per-request options)
|
||||
solidarityTransportEnabled := h.config.GetBool("modules.journeys.solutions.solidarity_transport.enabled") && !options.DisableSolidarityTransport
|
||||
organizedCarpoolEnabled := h.config.GetBool("modules.journeys.solutions.organized_carpool.enabled") && !options.DisableOrganizedCarpool
|
||||
carpoolOperatorsEnabled := h.config.GetBool("modules.journeys.solutions.carpool_operators.enabled") && !options.DisableCarpoolOperators
|
||||
transitEnabled := h.config.GetBool("modules.journeys.solutions.transit.enabled") && !options.DisableTransit
|
||||
fleetVehiclesEnabled := h.config.GetBool("modules.journeys.solutions.fleet_vehicles.enabled") && !options.DisableFleetVehicles
|
||||
knowledgeBaseEnabled := h.config.GetBool("modules.journeys.solutions.knowledge_base.enabled") && !options.DisableKnowledgeBase
|
||||
|
||||
// SOLIDARITY TRANSPORT
|
||||
var err error
|
||||
drivers, err = h.services.GetAccountsInNamespacesMap([]string{"solidarity_drivers", "organized_carpool_drivers"})
|
||||
|
|
@ -74,36 +99,58 @@ func (h *ApplicationHandler) SearchJourneys(
|
|||
protodep, _ := transformers.GeoJsonToProto(departureGeo)
|
||||
protodest, _ := transformers.GeoJsonToProto(destinationGeo)
|
||||
|
||||
log.Debug().Time("departure time", departureDateTime).Msg("calling driver journeys with ...")
|
||||
|
||||
res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, &gen.GetDriverJourneysRequest{
|
||||
Departure: protodep,
|
||||
Arrival: protodest,
|
||||
DepartureDate: timestamppb.New(departureDateTime),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error in grpc call to GetDriverJourneys")
|
||||
} else {
|
||||
solidarityTransportResults = slices.Collect(func(yield func(*gen.SolidarityTransportDriverJourney) bool) {
|
||||
for _, dj := range res.DriverJourneys {
|
||||
if a, ok := drivers[dj.DriverId].Data["archived"]; ok {
|
||||
if archived, ok := a.(bool); ok {
|
||||
if archived {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if dj.DriverId == solidarityTransportExcludeDriver {
|
||||
continue
|
||||
}
|
||||
if !yield(dj) {
|
||||
return
|
||||
// Get driver IDs to exclude based on group_id (drivers who already have bookings in this group)
|
||||
excludedDriverIds := make(map[string]bool)
|
||||
if solidarityExcludeGroupId != "" {
|
||||
bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{
|
||||
StartDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
EndDate: timestamppb.New(time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)),
|
||||
})
|
||||
if err == nil {
|
||||
for _, booking := range bookingsResp.Bookings {
|
||||
if booking.GroupId == solidarityExcludeGroupId {
|
||||
excludedDriverIds[booking.DriverId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if solidarityTransportEnabled {
|
||||
log.Debug().Time("departure time", departureDateTime).Msg("calling driver journeys with ...")
|
||||
|
||||
res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, &gen.GetDriverJourneysRequest{
|
||||
Departure: protodep,
|
||||
Arrival: protodest,
|
||||
DepartureDate: timestamppb.New(departureDateTime),
|
||||
})
|
||||
sort.Slice(solidarityTransportResults, func(i, j int) bool {
|
||||
return solidarityTransportResults[i].DriverDistance < solidarityTransportResults[j].DriverDistance
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error in grpc call to GetDriverJourneys")
|
||||
} else {
|
||||
solidarityTransportResults = slices.Collect(func(yield func(*gen.SolidarityTransportDriverJourney) bool) {
|
||||
for _, dj := range res.DriverJourneys {
|
||||
if a, ok := drivers[dj.DriverId].Data["archived"]; ok {
|
||||
if archived, ok := a.(bool); ok {
|
||||
if archived {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if dj.DriverId == solidarityTransportExcludeDriver {
|
||||
continue
|
||||
}
|
||||
// Skip drivers who already have bookings in the same group
|
||||
if excludedDriverIds[dj.DriverId] {
|
||||
continue
|
||||
}
|
||||
if !yield(dj) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
sort.Slice(solidarityTransportResults, func(i, j int) bool {
|
||||
return solidarityTransportResults[i].DriverDistance < solidarityTransportResults[j].DriverDistance
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Get departure and destination addresses from properties
|
||||
|
|
@ -119,124 +166,134 @@ func (h *ApplicationHandler) SearchJourneys(
|
|||
}
|
||||
}
|
||||
|
||||
radius := float64(5)
|
||||
// ORGANIZED CARPOOL
|
||||
organizedCarpoolResultsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(ctx, &carpoolproto.DriverJourneysRequest{
|
||||
DepartureLat: departureGeo.Point().Lat(),
|
||||
DepartureLng: departureGeo.Point().Lon(),
|
||||
ArrivalLat: destinationGeo.Point().Lat(),
|
||||
ArrivalLng: destinationGeo.Point().Lon(),
|
||||
DepartureDate: timestamppb.New(departureDateTime),
|
||||
DepartureAddress: &departureAddress,
|
||||
ArrivalAddress: &destinationAddress,
|
||||
DepartureRadius: &radius,
|
||||
ArrivalRadius: &radius,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving organized carpools")
|
||||
} else {
|
||||
organizedCarpoolResults = organizedCarpoolResultsRes.DriverJourneys
|
||||
sort.Slice(organizedCarpoolResults, func(i, j int) bool {
|
||||
return *organizedCarpoolResults[i].Distance < *organizedCarpoolResults[j].Distance
|
||||
if organizedCarpoolEnabled {
|
||||
radius := float64(5)
|
||||
organizedCarpoolResultsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(ctx, &carpoolproto.DriverJourneysRequest{
|
||||
DepartureLat: departureGeo.Point().Lat(),
|
||||
DepartureLng: departureGeo.Point().Lon(),
|
||||
ArrivalLat: destinationGeo.Point().Lat(),
|
||||
ArrivalLng: destinationGeo.Point().Lon(),
|
||||
DepartureDate: timestamppb.New(departureDateTime),
|
||||
DepartureAddress: &departureAddress,
|
||||
ArrivalAddress: &destinationAddress,
|
||||
DepartureRadius: &radius,
|
||||
ArrivalRadius: &radius,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving organized carpools")
|
||||
} else {
|
||||
organizedCarpoolResults = organizedCarpoolResultsRes.DriverJourneys
|
||||
sort.Slice(organizedCarpoolResults, func(i, j int) bool {
|
||||
return *organizedCarpoolResults[i].Distance < *organizedCarpoolResults[j].Distance
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// CARPOOL OPERATORS
|
||||
carpools := make(chan *geojson.FeatureCollection)
|
||||
go h.services.InteropCarpool.Search(carpools, *departureGeo, *destinationGeo, departureDateTime)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for c := range carpools {
|
||||
carpoolResults = append(carpoolResults, c)
|
||||
}
|
||||
}()
|
||||
if carpoolOperatorsEnabled {
|
||||
carpools := make(chan *geojson.FeatureCollection)
|
||||
go h.services.InteropCarpool.Search(carpools, *departureGeo, *destinationGeo, departureDateTime)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for c := range carpools {
|
||||
carpoolResults = append(carpoolResults, c)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// TRANSIT
|
||||
transitch := make(chan *transitous.Itinerary)
|
||||
go func(transitch chan *transitous.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
|
||||
defer close(transitch)
|
||||
response, err := h.services.TransitRouting.PlanWithResponse(ctx, &transitous.PlanParams{
|
||||
FromPlace: fmt.Sprintf("%f,%f", departure.Point().Lat(), departure.Point().Lon()),
|
||||
ToPlace: fmt.Sprintf("%f,%f", destination.Point().Lat(), destination.Point().Lon()),
|
||||
Time: datetime,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving transit data from Transitous server")
|
||||
return
|
||||
}
|
||||
for _, i := range response.Itineraries {
|
||||
transitch <- &i
|
||||
}
|
||||
}(transitch, departureGeo, destinationGeo, &departureDateTime)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
paris, _ := time.LoadLocation("Europe/Paris")
|
||||
requestedDay := departureDateTime.In(paris).Truncate(24 * time.Hour)
|
||||
if transitEnabled {
|
||||
transitch := make(chan *transitous.Itinerary)
|
||||
go func(transitch chan *transitous.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
|
||||
defer close(transitch)
|
||||
response, err := h.services.TransitRouting.PlanWithResponse(ctx, &transitous.PlanParams{
|
||||
FromPlace: fmt.Sprintf("%f,%f", departure.Point().Lat(), departure.Point().Lon()),
|
||||
ToPlace: fmt.Sprintf("%f,%f", destination.Point().Lat(), destination.Point().Lon()),
|
||||
Time: datetime,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving transit data from Transitous server")
|
||||
return
|
||||
}
|
||||
for _, i := range response.Itineraries {
|
||||
transitch <- &i
|
||||
}
|
||||
}(transitch, departureGeo, destinationGeo, &departureDateTime)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
paris, _ := time.LoadLocation("Europe/Paris")
|
||||
requestedDay := departureDateTime.In(paris).Truncate(24 * time.Hour)
|
||||
|
||||
for itinerary := range transitch {
|
||||
// Only include journeys that start on the requested day (in Paris timezone)
|
||||
if !itinerary.StartTime.IsZero() && !itinerary.EndTime.IsZero() {
|
||||
log.Info().
|
||||
Time("startTime", itinerary.StartTime).
|
||||
Time("endTime", itinerary.EndTime).
|
||||
Str("startTimezone", itinerary.StartTime.Location().String()).
|
||||
Str("endTimezone", itinerary.EndTime.Location().String()).
|
||||
Str("startTimeRFC3339", itinerary.StartTime.Format(time.RFC3339)).
|
||||
Str("endTimeRFC3339", itinerary.EndTime.Format(time.RFC3339)).
|
||||
Msg("Journey search - received transit itinerary from Transitous")
|
||||
|
||||
startInParis := itinerary.StartTime.In(paris)
|
||||
startDay := startInParis.Truncate(24 * time.Hour)
|
||||
|
||||
// Check if journey starts on the requested day
|
||||
if startDay.Equal(requestedDay) {
|
||||
transitResults = append(transitResults, itinerary)
|
||||
} else {
|
||||
for itinerary := range transitch {
|
||||
// Only include journeys that start on the requested day (in Paris timezone)
|
||||
if !itinerary.StartTime.IsZero() && !itinerary.EndTime.IsZero() {
|
||||
log.Info().
|
||||
Str("requestedDay", requestedDay.Format("2006-01-02")).
|
||||
Str("startDay", startDay.Format("2006-01-02")).
|
||||
Msg("Journey search - filtered out transit journey (not on requested day)")
|
||||
Time("startTime", itinerary.StartTime).
|
||||
Time("endTime", itinerary.EndTime).
|
||||
Str("startTimezone", itinerary.StartTime.Location().String()).
|
||||
Str("endTimezone", itinerary.EndTime.Location().String()).
|
||||
Str("startTimeRFC3339", itinerary.StartTime.Format(time.RFC3339)).
|
||||
Str("endTimeRFC3339", itinerary.EndTime.Format(time.RFC3339)).
|
||||
Msg("Journey search - received transit itinerary from Transitous")
|
||||
|
||||
startInParis := itinerary.StartTime.In(paris)
|
||||
startDay := startInParis.Truncate(24 * time.Hour)
|
||||
|
||||
// Check if journey starts on the requested day
|
||||
if startDay.Equal(requestedDay) {
|
||||
transitResults = append(transitResults, itinerary)
|
||||
} else {
|
||||
log.Info().
|
||||
Str("requestedDay", requestedDay.Format("2006-01-02")).
|
||||
Str("startDay", startDay.Format("2006-01-02")).
|
||||
Msg("Journey search - filtered out transit journey (not on requested day)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}()
|
||||
}
|
||||
|
||||
// VEHICLES
|
||||
vehiclech := make(chan fleetsstorage.Vehicle)
|
||||
go h.vehicleRequest(vehiclech, departureDateTime.Add(-24*time.Hour), departureDateTime.Add(168*time.Hour))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for vehicle := range vehiclech {
|
||||
vehicleResults = append(vehicleResults, vehicle)
|
||||
}
|
||||
slices.SortFunc(vehicleResults, sorting.VehiclesByDistanceFrom(*departureGeo))
|
||||
}()
|
||||
if fleetVehiclesEnabled {
|
||||
vehiclech := make(chan fleetsstorage.Vehicle)
|
||||
go h.vehicleRequest(vehiclech, departureDateTime.Add(-24*time.Hour), departureDateTime.Add(168*time.Hour))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for vehicle := range vehiclech {
|
||||
vehicleResults = append(vehicleResults, vehicle)
|
||||
}
|
||||
slices.SortFunc(vehicleResults, sorting.VehiclesByDistanceFrom(*departureGeo))
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// KNOWLEDGE BASE
|
||||
departureGeoSearch, _ := h.services.Geography.GeoSearch(departureGeo)
|
||||
kbData := h.config.Get("knowledge_base")
|
||||
if kb, ok := kbData.([]any); ok {
|
||||
for _, sol := range kb {
|
||||
if solution, ok := sol.(map[string]any); ok {
|
||||
if g, ok := solution["geography"]; ok {
|
||||
if geography, ok := g.([]any); ok {
|
||||
for _, gg := range geography {
|
||||
if geog, ok := gg.(map[string]any); ok {
|
||||
if layer, ok := geog["layer"].(string); ok {
|
||||
code := geog["code"]
|
||||
geo, err := h.services.Geography.Find(layer, fmt.Sprintf("%v", code))
|
||||
if err == nil {
|
||||
geog["geography"] = geo
|
||||
geog["name"] = geo.Properties.MustString("nom")
|
||||
}
|
||||
if strings.Compare(fmt.Sprintf("%v", code), departureGeoSearch[layer].Properties.MustString("code")) == 0 {
|
||||
knowledgeBaseResults = append(knowledgeBaseResults, solution)
|
||||
break
|
||||
if knowledgeBaseEnabled {
|
||||
departureGeoSearch, _ := h.services.Geography.GeoSearch(departureGeo)
|
||||
kbData := h.config.Get("knowledge_base")
|
||||
if kb, ok := kbData.([]any); ok {
|
||||
for _, sol := range kb {
|
||||
if solution, ok := sol.(map[string]any); ok {
|
||||
if g, ok := solution["geography"]; ok {
|
||||
if geography, ok := g.([]any); ok {
|
||||
for _, gg := range geography {
|
||||
if geog, ok := gg.(map[string]any); ok {
|
||||
if layer, ok := geog["layer"].(string); ok {
|
||||
code := geog["code"]
|
||||
geo, err := h.services.Geography.Find(layer, fmt.Sprintf("%v", code))
|
||||
if err == nil {
|
||||
geog["geography"] = geo
|
||||
geog["name"] = geo.Properties.MustString("nom")
|
||||
}
|
||||
if strings.Compare(fmt.Sprintf("%v", code), departureGeoSearch[layer].Properties.MustString("code")) == 0 {
|
||||
knowledgeBaseResults = append(knowledgeBaseResults, solution)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,6 +358,18 @@ func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context,
|
|||
transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons)
|
||||
transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons)
|
||||
|
||||
// Filter out replaced bookings from upcoming bookings list
|
||||
filteredBookings := []*solidaritytypes.Booking{}
|
||||
for _, booking := range transformedBookings {
|
||||
if booking.Data != nil {
|
||||
if _, hasReplacedBy := booking.Data["replaced_by"]; hasReplacedBy {
|
||||
continue // Skip bookings that have been replaced
|
||||
}
|
||||
}
|
||||
filteredBookings = append(filteredBookings, booking)
|
||||
}
|
||||
transformedBookings = filteredBookings
|
||||
|
||||
// Sort upcoming bookings by date (ascending - earliest first)
|
||||
sort.Slice(transformedBookings, func(i, j int) bool {
|
||||
if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil {
|
||||
|
|
@ -396,6 +408,8 @@ func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context,
|
|||
transformedBookingsHistory = filterBookingsByGeography(transformedBookingsHistory, histDeparturePolygons, histDestinationPolygons)
|
||||
transformedBookingsHistory = filterBookingsByPassengerAddressGeography(transformedBookingsHistory, beneficiariesMap, histPassengerAddressPolygons)
|
||||
|
||||
// Don't filter out replaced bookings from history - we want to see them in history
|
||||
|
||||
// Sort history bookings by date (descending - most recent first)
|
||||
sort.Slice(transformedBookingsHistory, func(i, j int) bool {
|
||||
if transformedBookingsHistory[i].Journey != nil && transformedBookingsHistory[j].Journey != nil {
|
||||
|
|
@ -503,6 +517,8 @@ func (h *ApplicationHandler) GetSolidarityTransportBookings(ctx context.Context,
|
|||
transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons)
|
||||
transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons)
|
||||
|
||||
// Don't filter out replaced bookings for exports - include all bookings
|
||||
|
||||
// Sort bookings by date
|
||||
sort.Slice(transformedBookings, func(i, j int) bool {
|
||||
if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil {
|
||||
|
|
@ -744,6 +760,8 @@ func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Contex
|
|||
bookings = append(bookings, booking)
|
||||
}
|
||||
|
||||
// Don't filter out replaced bookings from driver profile - show all bookings
|
||||
|
||||
// Collect unique passenger IDs
|
||||
passengerIDs := []string{}
|
||||
passengerIDsMap := make(map[string]bool)
|
||||
|
|
@ -1048,7 +1066,19 @@ func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Conte
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context.Context, driverID, journeyID, passengerID, motivation, message string, doNotSend bool, returnWaitingTimeMinutes int) (string, error) {
|
||||
func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context.Context, driverID, journeyID, passengerID, motivation, message string, doNotSend bool, returnWaitingTimeMinutes int, replacesBookingID string) (string, error) {
|
||||
// If this is a replacement booking, get the old booking's group_id
|
||||
var groupID string
|
||||
if replacesBookingID != "" {
|
||||
oldBookingResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBooking(ctx, &gen.GetSolidarityTransportBookingRequest{
|
||||
Id: replacesBookingID,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not get booking to replace: %w", err)
|
||||
}
|
||||
groupID = oldBookingResp.Booking.GroupId
|
||||
}
|
||||
|
||||
// Get journey for pricing calculation
|
||||
journeyRequest := &gen.GetDriverJourneyRequest{
|
||||
DriverId: driverID,
|
||||
|
|
@ -1083,6 +1113,12 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context
|
|||
returnWaitingDuration := int64(returnWaitingTimeMinutes) * int64(time.Minute)
|
||||
|
||||
// Create booking request
|
||||
dataFields := map[string]*structpb.Value{
|
||||
"motivation": structpb.NewStringValue(motivation),
|
||||
"message": structpb.NewStringValue(message),
|
||||
"do_not_send": structpb.NewBoolValue(doNotSend),
|
||||
}
|
||||
|
||||
bookingRequest := &gen.BookDriverJourneyRequest{
|
||||
PassengerId: passengerID,
|
||||
DriverId: driverID,
|
||||
|
|
@ -1093,19 +1129,45 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context
|
|||
DriverCompensationAmount: driverCompensation,
|
||||
DriverCompensationCurrency: "EUR",
|
||||
Data: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"motivation": structpb.NewStringValue(motivation),
|
||||
"message": structpb.NewStringValue(message),
|
||||
"do_not_send": structpb.NewBoolValue(doNotSend),
|
||||
},
|
||||
Fields: dataFields,
|
||||
},
|
||||
}
|
||||
|
||||
// Set group_id if this is a replacement booking
|
||||
if groupID != "" {
|
||||
bookingRequest.GroupId = &groupID
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.SolidarityTransport.BookDriverJourney(ctx, bookingRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If this is a replacement booking, update the old booking to mark it as replaced
|
||||
if replacesBookingID != "" {
|
||||
oldBookingResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBooking(ctx, &gen.GetSolidarityTransportBookingRequest{
|
||||
Id: replacesBookingID,
|
||||
})
|
||||
if err == nil {
|
||||
oldBooking, err := solidaritytransformers.BookingProtoToType(oldBookingResp.Booking)
|
||||
if err == nil && oldBooking != nil {
|
||||
if oldBooking.Data == nil {
|
||||
oldBooking.Data = make(map[string]any)
|
||||
}
|
||||
oldBooking.Data["replaced_by"] = resp.Booking.Id
|
||||
|
||||
// Update the booking
|
||||
oldBookingProto, _ := solidaritytransformers.BookingTypeToProto(oldBooking)
|
||||
_, err = h.services.GRPC.SolidarityTransport.UpdateSolidarityTransportBooking(ctx, &gen.UpdateSolidarityTransportBookingRequest{
|
||||
Booking: oldBookingProto,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("old_booking_id", replacesBookingID).Str("new_booking_id", resp.Booking.Id).Msg("failed to mark old booking as replaced")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send SMS if not disabled
|
||||
if !doNotSend && message != "" {
|
||||
send_message := strings.ReplaceAll(message, "{booking_id}", resp.Booking.Id)
|
||||
|
|
@ -1253,6 +1315,20 @@ func (h *ApplicationHandler) pricingGeography(loc *geojson.Feature) pricing.Geog
|
|||
}
|
||||
}
|
||||
|
||||
// CalculateSolidarityTransportPricing is the exported wrapper for calculateSolidarityTransportPricing
|
||||
func (h *ApplicationHandler) CalculateSolidarityTransportPricing(ctx context.Context, journey *gen.SolidarityTransportDriverJourney, passengerID string) (map[string]pricing.Price, error) {
|
||||
// Get passenger account
|
||||
var passenger mobilityaccountsstorage.Account
|
||||
if passengerID != "" {
|
||||
passengerResp, err := h.services.GetAccount(passengerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get passenger account: %w", err)
|
||||
}
|
||||
passenger = passengerResp
|
||||
}
|
||||
return h.calculateSolidarityTransportPricing(ctx, journey, passengerID, passenger)
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) calculateSolidarityTransportPricing(ctx context.Context, journey *gen.SolidarityTransportDriverJourney, passengerID string, passenger mobilityaccountsstorage.Account) (map[string]pricing.Price, error) {
|
||||
// Transform proto to type for geography access
|
||||
journeyType, err := solidaritytransformers.DriverJourneyProtoToType(journey)
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
package geo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/paulmach/orb/geojson"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GeoService struct {
|
||||
peliasURL string
|
||||
geoType string
|
||||
baseURL string
|
||||
autocompleteURL string
|
||||
}
|
||||
|
||||
func NewGeoService(peliasURL string) *GeoService {
|
||||
return &GeoService{peliasURL: peliasURL}
|
||||
func NewGeoService(geoType, baseURL, autocompleteEndpoint string) *GeoService {
|
||||
return &GeoService{
|
||||
geoType: geoType,
|
||||
baseURL: baseURL,
|
||||
autocompleteURL: baseURL + autocompleteEndpoint,
|
||||
}
|
||||
}
|
||||
|
||||
type AutocompleteResult struct {
|
||||
Features []any
|
||||
}
|
||||
|
||||
func (s *GeoService) Autocomplete(text string) (*AutocompleteResult, error) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/autocomplete?text=%s", s.peliasURL, text))
|
||||
func (s *GeoService) Autocomplete(text string) (*geojson.FeatureCollection, error) {
|
||||
resp, err := http.Get(s.autocompleteURL + text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -34,17 +35,11 @@ func (s *GeoService) Autocomplete(text string) (*AutocompleteResult, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
featureCollection, err := geojson.UnmarshalFeatureCollection(body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to unmarshal feature collection")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
features, ok := response["features"].([]any)
|
||||
if !ok {
|
||||
features = []any{}
|
||||
}
|
||||
|
||||
return &AutocompleteResult{
|
||||
Features: features,
|
||||
}, nil
|
||||
return featureCollection, nil
|
||||
}
|
||||
7
go.mod
7
go.mod
|
|
@ -57,12 +57,13 @@ require (
|
|||
git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536
|
||||
git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463
|
||||
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251008070137-723c12a6573d
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc
|
||||
github.com/arran4/golang-ical v0.3.1
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/minio/minio-go/v7 v7.0.43
|
||||
github.com/modelcontextprotocol/go-sdk v1.0.0
|
||||
github.com/paulmach/orb v0.12.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/objx v0.5.3
|
||||
|
|
@ -109,7 +110,6 @@ require (
|
|||
|
||||
require (
|
||||
ariga.io/atlas v0.37.0 // indirect
|
||||
github.com/AlexJarrah/go-ods v1.0.7 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
|
|
@ -124,6 +124,7 @@ require (
|
|||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.24.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
|
|
@ -138,7 +139,6 @@ require (
|
|||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
|
|
@ -158,6 +158,7 @@ require (
|
|||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.1 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.12 // indirect
|
||||
|
|
|
|||
49
go.sum
49
go.sum
|
|
@ -10,22 +10,14 @@ git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260 h1:Li3
|
|||
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260/go.mod h1:6cvvjv0RLSwBthIQ4TiuZoXFGvQXZ55hNSJchWXAgB4=
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.0 h1:pfW/K3fWfap54yNfkLzBXjvOjjoTaEGFEqS/+VkHv7s=
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.0/go.mod h1:nuK2mi1M2+DdntinqK/8C4ttW4WWyKCCY/xD1D7XjkE=
|
||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20250616160304-0285c9494673 h1:cth7a8Mnx1C6C6F5rv7SoKVMHYpI/CioFubyi0xB+Dw=
|
||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20250616160304-0285c9494673/go.mod h1:TbR3g1Awa8hpAe6LR1z1EQbv2IBVgN5JQ/FjXfKX4K0=
|
||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858 h1:4E0tbT8jj5oxaK66Ny61o7zqPaVc0qRN2cZG9IUR4Es=
|
||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858/go.mod h1:TbR3g1Awa8hpAe6LR1z1EQbv2IBVgN5JQ/FjXfKX4K0=
|
||||
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c h1:bY7PyrAgYY02f5IpDyf1WVfRqvWzivu31K6aEAYbWCw=
|
||||
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c/go.mod h1:lozSy6qlIIYhvKKXscZzz28HAtS0qBDUTv5nofLRmYA=
|
||||
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386 h1:v1JUdx8sknw2YYhFGz5cOAa1dEWNIBKvyiOpKr3RR+s=
|
||||
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386/go.mod h1:1typNYtO+PQT6KG77vs/PUv0fO60/nbeSGZL2tt1LLg=
|
||||
git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251010131127-a2c82a1a5c8e h1:c0iCczcVxDbzbaQY04zzFpMXgHTRGcYOJ8LqYk9UYuo=
|
||||
git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251010131127-a2c82a1a5c8e/go.mod h1:npYccQZcZj1WzTHhpLfFHFSBA3RiZOkO5R9x4uy1a9I=
|
||||
git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013125457-ab53f677d9cb h1:NotnSudZYn4cLAXJvtYor1XLkS5HXXNEPNgHy0Hw3Qs=
|
||||
git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013125457-ab53f677d9cb/go.mod h1:npYccQZcZj1WzTHhpLfFHFSBA3RiZOkO5R9x4uy1a9I=
|
||||
git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013140400-42fb40437ac3 h1:jo7fF7tLIAU110tUSIYXkMAvu30g8wHzZCLq3YomooQ=
|
||||
git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013140400-42fb40437ac3/go.mod h1:npYccQZcZj1WzTHhpLfFHFSBA3RiZOkO5R9x4uy1a9I=
|
||||
git.coopgo.io/coopgo-platform/payments v0.0.0-20251008125601-e36cd6e557da h1:5D2B1WkRolbXFUHsqPHnYtTeweSZ+iCHlx6S2KKxmxQ=
|
||||
git.coopgo.io/coopgo-platform/payments v0.0.0-20251008125601-e36cd6e557da/go.mod h1:gSAH2Tr9x8K8QC0vsUMwSWLrQOlsG+v64ACrjYw4BL0=
|
||||
git.coopgo.io/coopgo-platform/payments v0.0.0-20251013175712-75d0288d2d4f h1:B/+AP+rLFx8AojO2bKV3R93kMU84g8Dhy7DNVoT8xCY=
|
||||
git.coopgo.io/coopgo-platform/payments v0.0.0-20251013175712-75d0288d2d4f/go.mod h1:gSAH2Tr9x8K8QC0vsUMwSWLrQOlsG+v64ACrjYw4BL0=
|
||||
git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536 h1:SllXX1VJXulfhNi+Pd0R9chksm8zO6gkWcTQ/uSMsdc=
|
||||
|
|
@ -34,10 +26,8 @@ git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463 h1
|
|||
git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463/go.mod h1:0fuGuYub5CBy9NB6YMqxawE0HoBaxPb9gmSw1gjfDy0=
|
||||
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af h1:KxHim1dFcOVbFhRqelec8cJ65QBD2cma6eytW8llgYY=
|
||||
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af/go.mod h1:mad9D+WICDdpJzB+8H/wEVVbllK2mU6VLVByrppc9x0=
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251008070137-723c12a6573d h1:eBzRP50PXlXlLhgZjFhjTuoxIuQ3N/+5A6RIZyZEMAs=
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251008070137-723c12a6573d/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
|
||||
github.com/AlexJarrah/go-ods v1.0.7 h1:QxhYKncbsgf59BNNOcc4XB7wxKvOrSwtC0fpf6/gtsM=
|
||||
github.com/AlexJarrah/go-ods v1.0.7/go.mod h1:tifLS6QTLIRhFV4zSjZ59700fZOGeqqQD8KBBOb/F3w=
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc h1:NLU5DUo5Kt3jkPhV3KkqQMahTHIrGildBvYlHwJ6JmM=
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.2/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
|
|
@ -168,6 +158,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
|
@ -232,13 +224,13 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW
|
|||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74=
|
||||
github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
|
|
@ -266,8 +258,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
|||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
|
||||
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
|
|
@ -336,18 +326,14 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k
|
|||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
|
||||
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.7.1 h1:gm8q0UCAyaTt3MEF5wWMjVdmthm2EHAWesGSKS9tdVI=
|
||||
github.com/xuri/excelize/v2 v2.7.1/go.mod h1:qc0+2j4TvAUrBw36ATtcTeC1VCM0fFdAXZOmcF4nTpY=
|
||||
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
|
||||
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
|
|
@ -404,13 +390,8 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
|
|
@ -418,7 +399,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
|
@ -430,10 +410,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
|
|
@ -443,7 +419,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -464,24 +439,17 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -491,7 +459,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
|||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
10
main.go
10
main.go
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/servers/mcp"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/servers/web"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/services"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
|
|
@ -25,6 +26,7 @@ func main() {
|
|||
var (
|
||||
dev_env = cfg.GetBool("dev_env")
|
||||
webEnabled = cfg.GetBool("server.web.enabled")
|
||||
mcpEnabled = cfg.GetBool("server.mcp.enabled")
|
||||
)
|
||||
|
||||
if dev_env {
|
||||
|
|
@ -70,5 +72,13 @@ func main() {
|
|||
}()
|
||||
}
|
||||
|
||||
if mcpEnabled {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
mcp.Run(cfg, svc, applicationHandler, kv, filestorage)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ func (renderer *Renderer) SolidarityTransportDriverDisplay(w http.ResponseWriter
|
|||
renderer.Render("solidarity transport driver creation", w, r, files, state)
|
||||
}
|
||||
|
||||
func (renderer *Renderer) SolidarityTransportDriverJourney(w http.ResponseWriter, r *http.Request, driverJourney any, driver any, passenger any, beneficiaries any, passengerWalletBalance float64, pricingResult map[string]pricing.Price) {
|
||||
func (renderer *Renderer) SolidarityTransportDriverJourney(w http.ResponseWriter, r *http.Request, driverJourney any, driver any, passenger any, beneficiaries any, passengerWalletBalance float64, pricingResult map[string]pricing.Price, replacesBookingID string) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.driver_journey.files")
|
||||
bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
|
||||
state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
|
||||
|
|
@ -103,12 +103,13 @@ func (renderer *Renderer) SolidarityTransportDriverJourney(w http.ResponseWriter
|
|||
"passenger_wallet_balance": passengerWalletBalance,
|
||||
"pricing_result": pricingResult,
|
||||
"booking_motivations": bookingMotivations,
|
||||
"replaces_booking_id": replacesBookingID,
|
||||
}
|
||||
|
||||
renderer.Render("solidarity transport driver creation", w, r, files, state)
|
||||
}
|
||||
|
||||
func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, passengerWalletBalance float64) {
|
||||
func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, passengerWalletBalance float64, replacementDrivers any, replacementDriversMap any, replacementPricing any, replacementLocations any) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.booking_display.files")
|
||||
bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
|
||||
state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
|
||||
|
|
@ -116,8 +117,13 @@ func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWrite
|
|||
"driver": driver,
|
||||
"passenger": passenger,
|
||||
"booking": booking,
|
||||
"config": renderer.GlobalConfig,
|
||||
"passenger_wallet_balance": passengerWalletBalance,
|
||||
"booking_motivations": bookingMotivations,
|
||||
"replacement_drivers": replacementDrivers,
|
||||
"replacement_drivers_map": replacementDriversMap,
|
||||
"replacement_pricing": replacementPricing,
|
||||
"replacement_locations": replacementLocations,
|
||||
}
|
||||
|
||||
renderer.Render("booking display", w, r, files, state)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ func (r *XLSXRenderer) SolidarityTransportBookings(w http.ResponseWriter, result
|
|||
// Build headers dynamically based on configuration
|
||||
headers := []string{
|
||||
"ID Réservation",
|
||||
"ID Groupe",
|
||||
"Statut",
|
||||
"Motif de réservation",
|
||||
"Raison d'annulation",
|
||||
"Remplacé par (ID)",
|
||||
"Date de prise en charge",
|
||||
"Heure de prise en charge",
|
||||
}
|
||||
|
|
@ -102,6 +104,7 @@ func (r *XLSXRenderer) SolidarityTransportBookings(w http.ResponseWriter, result
|
|||
|
||||
// Booking information
|
||||
row = append(row, booking.Id)
|
||||
row = append(row, booking.GroupId)
|
||||
row = append(row, booking.Status)
|
||||
|
||||
// Motivation (from booking.Data)
|
||||
|
|
@ -122,6 +125,15 @@ func (r *XLSXRenderer) SolidarityTransportBookings(w http.ResponseWriter, result
|
|||
}
|
||||
row = append(row, cancellationReason)
|
||||
|
||||
// Replaced by (from booking.Data)
|
||||
replacedBy := ""
|
||||
if booking.Data != nil {
|
||||
if replacedByVal, ok := booking.Data["replaced_by"]; ok && replacedByVal != nil {
|
||||
replacedBy = fmt.Sprint(replacedByVal)
|
||||
}
|
||||
}
|
||||
row = append(row, replacedBy)
|
||||
|
||||
// Journey date and time
|
||||
if booking.Journey != nil {
|
||||
row = append(row, booking.Journey.PassengerPickupDate.Format("2006-01-02"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
# MCP Server for PARCOURSMOB
|
||||
|
||||
This package implements a Model Context Protocol (MCP) HTTP server for the PARCOURSMOB application, exposing journey search functionality as an MCP tool.
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP server provides a standardized interface for AI assistants to search for multimodal journeys, including:
|
||||
- Public transit routes
|
||||
- Carpooling solutions (via operators like Mobicoop)
|
||||
- Solidarity transport
|
||||
- Organized carpools
|
||||
- Fleet vehicles
|
||||
- Local knowledge base solutions
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable the MCP server in your config file:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
mcp:
|
||||
enabled: true
|
||||
listen: "0.0.0.0:8081"
|
||||
```
|
||||
|
||||
Or via environment variables:
|
||||
```bash
|
||||
export SERVER_MCP_ENABLED=true
|
||||
export SERVER_MCP_LISTEN="0.0.0.0:8081"
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Initialize
|
||||
```http
|
||||
POST /mcp/v1/initialize
|
||||
```
|
||||
|
||||
Returns server capabilities and protocol version.
|
||||
|
||||
### List Tools
|
||||
```http
|
||||
GET /mcp/v1/tools/list
|
||||
```
|
||||
|
||||
Returns available MCP tools.
|
||||
|
||||
### Call Tool
|
||||
```http
|
||||
POST /mcp/v1/tools/call
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "search_journeys",
|
||||
"arguments": {
|
||||
"departure": "123 Main St, Paris, France",
|
||||
"destination": "456 Oak Ave, Lyon, France",
|
||||
"departure_date": "2025-01-20",
|
||||
"departure_time": "14:30"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Health Check
|
||||
```http
|
||||
GET /health
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### search_journeys
|
||||
|
||||
Searches for multimodal journey options between two locations.
|
||||
|
||||
**Parameters:**
|
||||
- `departure` (string, required): Departure address as text
|
||||
- `destination` (string, required): Destination address as text
|
||||
- `departure_date` (string, required): Date in YYYY-MM-DD format
|
||||
- `departure_time` (string, required): Time in HH:MM format (24-hour)
|
||||
- `passenger_id` (string, optional): Passenger ID to retrieve address from account
|
||||
- `exclude_driver_ids` (array, optional): List of driver IDs to exclude from solidarity transport results
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8081/mcp/v1/tools/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "search_journeys",
|
||||
"arguments": {
|
||||
"departure": "Gare de Lyon, Paris",
|
||||
"destination": "Part-Dieu, Lyon",
|
||||
"departure_date": "2025-01-20",
|
||||
"departure_time": "09:00"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
"search_parameters": {
|
||||
"departure": {
|
||||
"label": "Gare de Lyon, Paris, France",
|
||||
"coordinates": { "type": "Point", "coordinates": [2.3736, 48.8443] }
|
||||
},
|
||||
"destination": {
|
||||
"label": "Part-Dieu, Lyon, France",
|
||||
"coordinates": { "type": "Point", "coordinates": [4.8575, 45.7605] }
|
||||
},
|
||||
"departure_date": "2025-01-20",
|
||||
"departure_time": "09:00"
|
||||
},
|
||||
"results": {
|
||||
"CarpoolResults": [...],
|
||||
"TransitResults": [...],
|
||||
"VehicleResults": [...],
|
||||
"DriverJourneys": [...],
|
||||
"OrganizedCarpools": [...],
|
||||
"KnowledgeBaseResults": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Package Structure
|
||||
|
||||
- `mcp.go`: HTTP server and request routing
|
||||
- `tools.go`: Tool registration and execution
|
||||
- `journey_search.go`: Journey search tool implementation
|
||||
|
||||
### Flow
|
||||
|
||||
1. HTTP request received at MCP endpoint
|
||||
2. Tool name and arguments extracted
|
||||
3. Addresses geocoded using Pelias geocoding service
|
||||
4. Journey search executed via ApplicationHandler
|
||||
5. Results formatted and returned as JSON
|
||||
|
||||
### Dependencies
|
||||
|
||||
The MCP server uses:
|
||||
- Pelias geocoding service (configured via `geo.pelias.url`)
|
||||
- ApplicationHandler for journey search business logic
|
||||
- All backend services (GRPC): solidarity transport, carpool service, transit routing, fleets, etc.
|
||||
|
||||
## Integration with AI Assistants
|
||||
|
||||
The MCP server follows the Model Context Protocol specification, making it compatible with AI assistants that support MCP, such as Claude Desktop or other MCP-enabled tools.
|
||||
|
||||
Example Claude Desktop configuration:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"parcoursmob": {
|
||||
"url": "http://localhost:8081/mcp/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Testing
|
||||
|
||||
Test the health endpoint:
|
||||
```bash
|
||||
curl http://localhost:8081/health
|
||||
```
|
||||
|
||||
List available tools:
|
||||
```bash
|
||||
curl http://localhost:8081/mcp/v1/tools/list
|
||||
```
|
||||
|
||||
Test journey search:
|
||||
```bash
|
||||
curl -X POST http://localhost:8081/mcp/v1/tools/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "search_journeys",
|
||||
"arguments": {
|
||||
"departure": "Paris",
|
||||
"destination": "Lyon",
|
||||
"departure_date": "2025-01-20",
|
||||
"departure_time": "10:00"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
To add a new MCP tool:
|
||||
|
||||
1. Define the tool in `tools.go`:
|
||||
```go
|
||||
func (h *ToolsHandler) registerNewTool() {
|
||||
tool := &Tool{
|
||||
Name: "tool_name",
|
||||
Description: "Tool description",
|
||||
InputSchema: map[string]any{...},
|
||||
}
|
||||
h.tools["tool_name"] = tool
|
||||
}
|
||||
```
|
||||
|
||||
2. Implement the handler:
|
||||
```go
|
||||
func (h *ToolsHandler) handleNewTool(ctx context.Context, arguments map[string]any) (any, error) {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
3. Add to CallTool switch statement in `tools.go`
|
||||
|
||||
## Notes
|
||||
|
||||
- All times are handled in Europe/Paris timezone
|
||||
- Geocoding uses the first result from Pelias
|
||||
- Journey search runs multiple transport mode queries in parallel
|
||||
- Results include all available transport options for the requested route
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/paulmach/orb/geojson"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// JourneySearchInput represents the input for journey search
|
||||
type JourneySearchInput struct {
|
||||
Departure string `json:"departure" jsonschema:"Departure address as text (e.g. Paris or Gare de Lyon Paris)"`
|
||||
Destination string `json:"destination" jsonschema:"Destination address as text (e.g. Lyon or Part-Dieu Lyon)"`
|
||||
DepartureDate string `json:"departure_date" jsonschema:"Departure date in YYYY-MM-DD format"`
|
||||
DepartureTime string `json:"departure_time" jsonschema:"Departure time in HH:MM format (24-hour)"`
|
||||
PassengerID string `json:"passenger_id,omitempty" jsonschema:"Optional passenger ID to retrieve address from account"`
|
||||
ExcludeDriverIDs []string `json:"exclude_driver_ids,omitempty" jsonschema:"Optional list of driver IDs to exclude from solidarity transport results"`
|
||||
}
|
||||
|
||||
// JourneySearchOutput represents the output of journey search
|
||||
type JourneySearchOutput struct {
|
||||
SearchParameters map[string]any `json:"search_parameters"`
|
||||
Results any `json:"results"`
|
||||
}
|
||||
|
||||
// registerJourneySearchTool registers the journey search tool with the MCP server
|
||||
func (s *MCPServer) registerJourneySearchTool() {
|
||||
mcpsdk.AddTool(
|
||||
s.mcpServer,
|
||||
&mcpsdk.Tool{
|
||||
Name: "search_journeys",
|
||||
Description: "Search for multimodal journeys including transit, carpooling, solidarity transport, organized carpool, and local solutions. Accepts departure and destination as text addresses that will be geocoded automatically.",
|
||||
},
|
||||
s.handleJourneySearch,
|
||||
)
|
||||
}
|
||||
|
||||
// handleJourneySearch handles the journey search tool execution
|
||||
func (s *MCPServer) handleJourneySearch(ctx context.Context, req *mcpsdk.CallToolRequest, input JourneySearchInput) (*mcpsdk.CallToolResult, *JourneySearchOutput, error) {
|
||||
// Geocode departure address using French government API
|
||||
log.Info().Str("address", input.Departure).Msg("Geocoding departure address")
|
||||
departureFeature, err := s.geocodeAddress(input.Departure)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to geocode departure address")
|
||||
return nil, nil, fmt.Errorf("failed to geocode departure address: %w", err)
|
||||
}
|
||||
|
||||
// Geocode destination address using French government API
|
||||
log.Info().Str("address", input.Destination).Msg("Geocoding destination address")
|
||||
destinationFeature, err := s.geocodeAddress(input.Destination)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to geocode destination address")
|
||||
return nil, nil, fmt.Errorf("failed to geocode destination address: %w", err)
|
||||
}
|
||||
|
||||
// Parse date and time
|
||||
parisLoc, err := time.LoadLocation("Europe/Paris")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load Paris timezone: %w", err)
|
||||
}
|
||||
|
||||
departureDateTime, err := time.ParseInLocation("2006-01-02 15:04", fmt.Sprintf("%s %s", input.DepartureDate, input.DepartureTime), parisLoc)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to parse date/time")
|
||||
return nil, nil, fmt.Errorf("failed to parse departure date/time: %w", err)
|
||||
}
|
||||
|
||||
// Convert to UTC for the search
|
||||
departureDateTime = departureDateTime.UTC()
|
||||
|
||||
log.Info().
|
||||
Str("departure", input.Departure).
|
||||
Str("destination", input.Destination).
|
||||
Time("departure_datetime", departureDateTime).
|
||||
Msg("Executing journey search")
|
||||
|
||||
// Prepare exclude driver ID (only first one if provided)
|
||||
excludeDriverID := ""
|
||||
if len(input.ExcludeDriverIDs) > 0 {
|
||||
excludeDriverID = input.ExcludeDriverIDs[0]
|
||||
}
|
||||
|
||||
// Prepare search options - disable transit for MCP requests
|
||||
searchOptions := &application.SearchJourneyOptions{
|
||||
DisableTransit: true,
|
||||
}
|
||||
|
||||
// Call the journey search from application handler
|
||||
searchResult, err := s.applicationHandler.SearchJourneys(
|
||||
ctx,
|
||||
departureDateTime,
|
||||
departureFeature,
|
||||
destinationFeature,
|
||||
input.PassengerID,
|
||||
excludeDriverID,
|
||||
"", // solidarityExcludeGroupId - not used in MCP context
|
||||
searchOptions,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("journey search failed: %w", err)
|
||||
}
|
||||
|
||||
// Format the results for MCP response
|
||||
response := &JourneySearchOutput{
|
||||
SearchParameters: map[string]any{
|
||||
"departure": map[string]any{
|
||||
"label": getFeatureLabel(departureFeature),
|
||||
"coordinates": departureFeature.Geometry,
|
||||
},
|
||||
"destination": map[string]any{
|
||||
"label": getFeatureLabel(destinationFeature),
|
||||
"coordinates": destinationFeature.Geometry,
|
||||
},
|
||||
"departure_date": input.DepartureDate,
|
||||
"departure_time": input.DepartureTime,
|
||||
},
|
||||
Results: searchResult,
|
||||
}
|
||||
|
||||
return &mcpsdk.CallToolResult{}, response, nil
|
||||
}
|
||||
|
||||
// geocodeAddress uses the geo service helper to geocode an address
|
||||
func (s *MCPServer) geocodeAddress(address string) (*geojson.Feature, error) {
|
||||
// Use the geo service to get autocomplete results
|
||||
featureCollection, err := s.geoService.Autocomplete(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocoding request failed: %w", err)
|
||||
}
|
||||
|
||||
if len(featureCollection.Features) == 0 {
|
||||
return nil, fmt.Errorf("no results found for address: %s", address)
|
||||
}
|
||||
|
||||
// Return the first feature directly
|
||||
return featureCollection.Features[0], nil
|
||||
}
|
||||
|
||||
// getFeatureLabel extracts a human-readable label from a GeoJSON Feature
|
||||
func getFeatureLabel(feature *geojson.Feature) string {
|
||||
if feature.Properties == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try common label fields
|
||||
if label, ok := feature.Properties["label"].(string); ok {
|
||||
return label
|
||||
}
|
||||
if name, ok := feature.Properties["name"].(string); ok {
|
||||
return name
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package mcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/geo"
|
||||
cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/services"
|
||||
)
|
||||
|
||||
// MCPServer represents the MCP HTTP server
|
||||
type MCPServer struct {
|
||||
cfg *viper.Viper
|
||||
services *services.ServicesHandler
|
||||
kv cache.KVHandler
|
||||
filestorage cache.FileStorage
|
||||
applicationHandler *application.ApplicationHandler
|
||||
mcpServer *mcpsdk.Server
|
||||
geoService *geo.GeoService
|
||||
}
|
||||
|
||||
// NewMCPServer creates a new MCP server instance
|
||||
func NewMCPServer(
|
||||
cfg *viper.Viper,
|
||||
svc *services.ServicesHandler,
|
||||
applicationHandler *application.ApplicationHandler,
|
||||
kv cache.KVHandler,
|
||||
filestorage cache.FileStorage,
|
||||
) *MCPServer {
|
||||
// Initialize geocoding service
|
||||
geoType := cfg.GetString("geo.type")
|
||||
baseURL := cfg.GetString("geo." + geoType + ".url")
|
||||
autocompleteEndpoint := cfg.GetString("geo." + geoType + ".autocomplete")
|
||||
geoService := geo.NewGeoService(geoType, baseURL, autocompleteEndpoint)
|
||||
|
||||
server := &MCPServer{
|
||||
cfg: cfg,
|
||||
services: svc,
|
||||
kv: kv,
|
||||
filestorage: filestorage,
|
||||
applicationHandler: applicationHandler,
|
||||
geoService: geoService,
|
||||
}
|
||||
|
||||
// Create MCP server with implementation info
|
||||
server.mcpServer = mcpsdk.NewServer(&mcpsdk.Implementation{
|
||||
Name: "parcoursmob-mcp-server",
|
||||
Version: "1.0.0",
|
||||
}, nil)
|
||||
|
||||
// Register journey search tool
|
||||
server.registerJourneySearchTool()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// Run starts the MCP HTTP server with SSE transport
|
||||
func Run(
|
||||
cfg *viper.Viper,
|
||||
svc *services.ServicesHandler,
|
||||
applicationHandler *application.ApplicationHandler,
|
||||
kv cache.KVHandler,
|
||||
filestorage cache.FileStorage,
|
||||
) {
|
||||
address := cfg.GetString("server.mcp.listen")
|
||||
service_name := cfg.GetString("service_name")
|
||||
|
||||
mcpServer := NewMCPServer(cfg, svc, applicationHandler, kv, filestorage)
|
||||
|
||||
// Create HTTP server with SSE transport
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Health check endpoint
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"healthy"}`))
|
||||
})
|
||||
|
||||
// MCP Streamable HTTP endpoint (preferred over SSE as of 2025-03-26 spec)
|
||||
streamHandler := mcpsdk.NewStreamableHTTPHandler(func(r *http.Request) *mcpsdk.Server {
|
||||
return mcpServer.mcpServer
|
||||
}, nil)
|
||||
mux.Handle("/", streamHandler)
|
||||
|
||||
// Also support legacy SSE endpoint for backwards compatibility
|
||||
sseHandler := mcpsdk.NewSSEHandler(func(r *http.Request) *mcpsdk.Server {
|
||||
return mcpServer.mcpServer
|
||||
}, nil)
|
||||
mux.Handle("/sse", sseHandler)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: mux,
|
||||
Addr: address,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
log.Info().Str("service_name", service_name).Str("address", address).Msg("Running MCP HTTP server with SSE transport")
|
||||
|
||||
err := srv.ListenAndServe()
|
||||
log.Error().Err(err).Msg("MCP server error")
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
|
@ -14,13 +13,13 @@ func (h *Handler) GeoAutocomplete(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
text := t[0]
|
||||
|
||||
result, err := h.geoService.Autocomplete(text)
|
||||
featureCollection, err := h.geoService.Autocomplete(text)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
j, err := json.Marshal(result.Features)
|
||||
j, err := featureCollection.MarshalJSON()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ type Handler struct {
|
|||
|
||||
func NewHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, appHandler *application.ApplicationHandler, cacheHandler cacheStorage.CacheHandler) *Handler {
|
||||
cacheService := cache.NewCacheService(cacheHandler)
|
||||
geoService := geo.NewGeoService(cfg.GetString("geo.pelias.url"))
|
||||
|
||||
// Get geocoding configuration
|
||||
geoType := cfg.GetString("geo.type")
|
||||
baseURL := cfg.GetString("geo." + geoType + ".url")
|
||||
autocompleteEndpoint := cfg.GetString("geo." + geoType + ".autocomplete")
|
||||
geoService := geo.NewGeoService(geoType, baseURL, autocompleteEndpoint)
|
||||
|
||||
return &Handler{
|
||||
config: cfg,
|
||||
|
|
|
|||
|
|
@ -27,4 +27,5 @@ func (ws *WebServer) setupSolidarityTransportRoutes(appRouter *mux.Router) {
|
|||
solidarityTransport.HandleFunc("/bookings/{bookingid}/confirm", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("confirm"))
|
||||
solidarityTransport.HandleFunc("/bookings/{bookingid}/cancel", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("cancel"))
|
||||
solidarityTransport.HandleFunc("/bookings/{bookingid}/waitconfirmation", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("waitconfirmation"))
|
||||
solidarityTransport.HandleFunc("/bookings/{bookingid}/create-replacement", ws.appHandler.SolidarityTransportCreateReplacementBookingHTTPHandler())
|
||||
}
|
||||
|
|
@ -105,6 +105,8 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
|
|||
destinationGeo,
|
||||
passengerID,
|
||||
solidarityTransportExcludeDriver,
|
||||
"", // solidarityExcludeGroupId - for modal search replacement only
|
||||
nil, // options - use defaults
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error in journey search")
|
||||
|
|
@ -389,6 +391,8 @@ func (h *Handler) JourneysSearchCompactHTTPHandler() http.HandlerFunc {
|
|||
destinationGeo,
|
||||
passengerID,
|
||||
solidarityTransportExcludeDriver,
|
||||
"", // solidarityExcludeGroupId - for modal search replacement only
|
||||
nil, // options - use defaults
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error in journey search")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"time"
|
||||
|
||||
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
gen "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -520,6 +522,7 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
|
|||
driverID := vars["driverid"]
|
||||
journeyID := vars["journeyid"]
|
||||
passengerID := r.URL.Query().Get("passengerid")
|
||||
replacesBookingID := r.URL.Query().Get("replaces_booking_id")
|
||||
|
||||
if r.Method == "POST" {
|
||||
// Parse form data
|
||||
|
|
@ -531,7 +534,8 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
|
|||
fmt.Sscanf(r.PostFormValue("return_waiting_time"), "%d", &returnWaitingTimeMinutes)
|
||||
}
|
||||
|
||||
bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(r.Context(), driverID, journeyID, passengerID, motivation, message, doNotSend, returnWaitingTimeMinutes)
|
||||
replacesBookingID := r.PostFormValue("replaces_booking_id")
|
||||
bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(r.Context(), driverID, journeyID, passengerID, motivation, message, doNotSend, returnWaitingTimeMinutes, replacesBookingID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error creating solidarity transport journey booking")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
|
|
@ -559,7 +563,7 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
|
|||
return
|
||||
}
|
||||
|
||||
h.renderer.SolidarityTransportDriverJourney(w, r, result.Journey, result.Driver, result.Passenger, result.Beneficiaries, result.PassengerWalletBalance, result.PricingResult)
|
||||
h.renderer.SolidarityTransportDriverJourney(w, r, result.Journey, result.Driver, result.Passenger, result.Beneficiaries, result.PassengerWalletBalance, result.PricingResult, replacesBookingID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -592,7 +596,141 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
|
|||
return
|
||||
}
|
||||
|
||||
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance)
|
||||
// If booking is cancelled, search for replacement drivers
|
||||
var replacementDrivers any
|
||||
var replacementDriversMap map[string]mobilityaccountsstorage.Account
|
||||
var replacementPricing map[string]map[string]interface{}
|
||||
var replacementLocations map[string]string
|
||||
if result.Booking.Status == "CANCELLED" {
|
||||
// Initialize maps to avoid nil pointer in template
|
||||
replacementDriversMap = make(map[string]mobilityaccountsstorage.Account)
|
||||
replacementPricing = make(map[string]map[string]interface{})
|
||||
replacementLocations = make(map[string]string)
|
||||
|
||||
searchResult, err := h.applicationHandler.SearchJourneys(
|
||||
r.Context(),
|
||||
result.Booking.Journey.PassengerPickupDate,
|
||||
result.Booking.Journey.PassengerPickup,
|
||||
result.Booking.Journey.PassengerDrop,
|
||||
result.Booking.PassengerId,
|
||||
result.Booking.DriverId, // Exclude the original driver
|
||||
result.Booking.GroupId, // Exclude drivers with bookings in this group
|
||||
nil, // options - use defaults
|
||||
)
|
||||
if err == nil {
|
||||
replacementDrivers = searchResult.DriverJourneys
|
||||
replacementDriversMap = searchResult.Drivers
|
||||
|
||||
// Calculate pricing for each replacement driver journey
|
||||
for _, dj := range searchResult.DriverJourneys {
|
||||
// Extract driver departure location
|
||||
if dj.DriverDeparture != nil && dj.DriverDeparture.Serialized != "" {
|
||||
var feature map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(dj.DriverDeparture.Serialized), &feature); err == nil {
|
||||
if props, ok := feature["properties"].(map[string]interface{}); ok {
|
||||
if name, ok := props["name"].(string); ok {
|
||||
replacementLocations[dj.Id] = name
|
||||
} else if label, ok := props["label"].(string); ok {
|
||||
replacementLocations[dj.Id] = label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pricingResult, err := h.applicationHandler.CalculateSolidarityTransportPricing(r.Context(), dj, result.Booking.PassengerId)
|
||||
if err == nil {
|
||||
pricing := map[string]interface{}{
|
||||
"passenger": map[string]interface{}{
|
||||
"amount": pricingResult["passenger"].Amount,
|
||||
"currency": pricingResult["passenger"].Currency,
|
||||
},
|
||||
"driver": map[string]interface{}{
|
||||
"amount": pricingResult["driver"].Amount,
|
||||
"currency": pricingResult["driver"].Currency,
|
||||
},
|
||||
}
|
||||
replacementPricing[dj.Id] = pricing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance, replacementDrivers, replacementDriversMap, replacementPricing, replacementLocations)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) SolidarityTransportCreateReplacementBookingHTTPHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
oldBookingID := vars["bookingid"]
|
||||
|
||||
// Get the old booking to retrieve its data
|
||||
oldBookingResult, err := h.applicationHandler.GetSolidarityTransportBookingData(r.Context(), oldBookingID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving old booking")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse form data for new driver/journey
|
||||
driverID := r.PostFormValue("driver_id")
|
||||
journeyID := r.PostFormValue("journey_id")
|
||||
message := r.PostFormValue("message")
|
||||
doNotSend := r.PostFormValue("do_not_send") == "on"
|
||||
|
||||
// Use old booking's data
|
||||
passengerID := oldBookingResult.Booking.PassengerId
|
||||
motivation := ""
|
||||
if oldBookingResult.Booking.Data != nil {
|
||||
if m, ok := oldBookingResult.Booking.Data["motivation"].(string); ok {
|
||||
motivation = m
|
||||
}
|
||||
}
|
||||
|
||||
// Get the new driver journey to retrieve journey information
|
||||
driverJourneyResp, err := h.services.GRPC.SolidarityTransport.GetDriverJourney(r.Context(), &gen.GetDriverJourneyRequest{
|
||||
DriverId: driverID,
|
||||
JourneyId: journeyID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving new driver journey")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate return waiting time based on journey type
|
||||
returnWaitingTimeMinutes := 30 // Default for round trips
|
||||
if driverJourneyResp.DriverJourney.Noreturn {
|
||||
returnWaitingTimeMinutes = 0
|
||||
}
|
||||
|
||||
// Create the replacement booking with pricing calculated in CreateSolidarityTransportJourneyBooking
|
||||
bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(
|
||||
r.Context(),
|
||||
driverID,
|
||||
journeyID,
|
||||
passengerID,
|
||||
motivation,
|
||||
message, // message from form
|
||||
doNotSend, // doNotSend from form checkbox
|
||||
returnWaitingTimeMinutes,
|
||||
oldBookingID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error creating replacement booking")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("booking_id", bookingID).Str("replaces", oldBookingID).Msg("Replacement booking created successfully")
|
||||
|
||||
// Redirect to the new booking
|
||||
http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/bookings/%s", bookingID), http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue