Add MCP server
This commit is contained in:
		
							parent
							
								
									d992a7984f
								
							
						
					
					
						commit
						52de8d363e
					
				
							
								
								
									
										28
									
								
								config.go
								
								
								
								
							
							
						
						
									
										28
									
								
								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",
 | 
			
		||||
				"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,6 +99,23 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
		protodep, _ := transformers.GeoJsonToProto(departureGeo)
 | 
			
		||||
		protodest, _ := transformers.GeoJsonToProto(destinationGeo)
 | 
			
		||||
 | 
			
		||||
		// 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{
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +138,10 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
						if dj.DriverId == solidarityTransportExcludeDriver {
 | 
			
		||||
							continue
 | 
			
		||||
						}
 | 
			
		||||
						// Skip drivers who already have bookings in the same group
 | 
			
		||||
						if excludedDriverIds[dj.DriverId] {
 | 
			
		||||
							continue
 | 
			
		||||
						}
 | 
			
		||||
						if !yield(dj) {
 | 
			
		||||
							return
 | 
			
		||||
						}
 | 
			
		||||
| 
						 | 
				
			
			@ -105,6 +151,7 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
					return solidarityTransportResults[i].DriverDistance < solidarityTransportResults[j].DriverDistance
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Get departure and destination addresses from properties
 | 
			
		||||
		var departureAddress, destinationAddress string
 | 
			
		||||
| 
						 | 
				
			
			@ -119,8 +166,9 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		radius := float64(5)
 | 
			
		||||
		// ORGANIZED CARPOOL
 | 
			
		||||
		if organizedCarpoolEnabled {
 | 
			
		||||
			radius := float64(5)
 | 
			
		||||
			organizedCarpoolResultsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(ctx, &carpoolproto.DriverJourneysRequest{
 | 
			
		||||
				DepartureLat:     departureGeo.Point().Lat(),
 | 
			
		||||
				DepartureLng:     departureGeo.Point().Lon(),
 | 
			
		||||
| 
						 | 
				
			
			@ -140,9 +188,11 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
					return *organizedCarpoolResults[i].Distance < *organizedCarpoolResults[j].Distance
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var wg sync.WaitGroup
 | 
			
		||||
		// CARPOOL OPERATORS
 | 
			
		||||
		if carpoolOperatorsEnabled {
 | 
			
		||||
			carpools := make(chan *geojson.FeatureCollection)
 | 
			
		||||
			go h.services.InteropCarpool.Search(carpools, *departureGeo, *destinationGeo, departureDateTime)
 | 
			
		||||
			wg.Add(1)
 | 
			
		||||
| 
						 | 
				
			
			@ -152,8 +202,10 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
					carpoolResults = append(carpoolResults, c)
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TRANSIT
 | 
			
		||||
		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)
 | 
			
		||||
| 
						 | 
				
			
			@ -203,8 +255,10 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// VEHICLES
 | 
			
		||||
		if fleetVehiclesEnabled {
 | 
			
		||||
			vehiclech := make(chan fleetsstorage.Vehicle)
 | 
			
		||||
			go h.vehicleRequest(vehiclech, departureDateTime.Add(-24*time.Hour), departureDateTime.Add(168*time.Hour))
 | 
			
		||||
			wg.Add(1)
 | 
			
		||||
| 
						 | 
				
			
			@ -215,9 +269,11 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
				}
 | 
			
		||||
				slices.SortFunc(vehicleResults, sorting.VehiclesByDistanceFrom(*departureGeo))
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
		wg.Wait()
 | 
			
		||||
 | 
			
		||||
		// KNOWLEDGE BASE
 | 
			
		||||
		if knowledgeBaseEnabled {
 | 
			
		||||
			departureGeoSearch, _ := h.services.Geography.GeoSearch(departureGeo)
 | 
			
		||||
			kbData := h.config.Get("knowledge_base")
 | 
			
		||||
			if kb, ok := kbData.([]any); ok {
 | 
			
		||||
| 
						 | 
				
			
			@ -247,6 +303,7 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &SearchJourneysResult{
 | 
			
		||||
		CarpoolResults:       carpoolResults,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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