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",
 | 
									"listen":  "0.0.0.0:8080",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			"mcp": map[string]any{
 | 
								"mcp": map[string]any{
 | 
				
			||||||
				"enabled": false,
 | 
									"enabled": true,
 | 
				
			||||||
				"listen":  "0.0.0.0:8081",
 | 
									"listen":  "0.0.0.0:8081",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
| 
						 | 
					@ -123,6 +123,26 @@ func ReadConfig() (*viper.Viper, error) {
 | 
				
			||||||
			"journeys": map[string]any{
 | 
								"journeys": map[string]any{
 | 
				
			||||||
				"enabled":     true,
 | 
									"enabled":     true,
 | 
				
			||||||
				"search_view": "tabs",
 | 
									"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{
 | 
								"solidarity_transport": map[string]any{
 | 
				
			||||||
				"enabled": true,
 | 
									"enabled": true,
 | 
				
			||||||
| 
						 | 
					@ -226,8 +246,14 @@ func ReadConfig() (*viper.Viper, error) {
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"geo": map[string]any{
 | 
							"geo": map[string]any{
 | 
				
			||||||
 | 
								"type": "addok", // Options: "pelias", "addok"
 | 
				
			||||||
			"pelias": map[string]any{
 | 
								"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{
 | 
							"geography": map[string]any{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -282,6 +282,8 @@ func (h *ApplicationHandler) GetBeneficiaryData(ctx context.Context, beneficiary
 | 
				
			||||||
		solidarityTransportBookings = append(solidarityTransportBookings, booking)
 | 
							solidarityTransportBookings = append(solidarityTransportBookings, booking)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Don't filter out replaced bookings from beneficiary profile - show all bookings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Collect unique driver IDs
 | 
						// Collect unique driver IDs
 | 
				
			||||||
	driverIDs := []string{}
 | 
						driverIDs := []string{}
 | 
				
			||||||
	driverIDsMap := make(map[string]bool)
 | 
						driverIDsMap := make(map[string]bool)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,16 @@ type SearchJourneysResult struct {
 | 
				
			||||||
	KnowledgeBaseResults []any
 | 
						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
 | 
					// SearchJourneys performs the business logic for journey search
 | 
				
			||||||
func (h *ApplicationHandler) SearchJourneys(
 | 
					func (h *ApplicationHandler) SearchJourneys(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
| 
						 | 
					@ -46,6 +56,8 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
				
			||||||
	destinationGeo *geojson.Feature,
 | 
						destinationGeo *geojson.Feature,
 | 
				
			||||||
	passengerID string,
 | 
						passengerID string,
 | 
				
			||||||
	solidarityTransportExcludeDriver string,
 | 
						solidarityTransportExcludeDriver string,
 | 
				
			||||||
 | 
						solidarityExcludeGroupId string,
 | 
				
			||||||
 | 
						options *SearchJourneyOptions,
 | 
				
			||||||
) (*SearchJourneysResult, error) {
 | 
					) (*SearchJourneysResult, error) {
 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		// Results
 | 
							// Results
 | 
				
			||||||
| 
						 | 
					@ -64,6 +76,19 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
				
			||||||
	if departureGeo != nil && destinationGeo != nil && !departureDateTime.IsZero() {
 | 
						if departureGeo != nil && destinationGeo != nil && !departureDateTime.IsZero() {
 | 
				
			||||||
		searched = true
 | 
							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
 | 
							// SOLIDARITY TRANSPORT
 | 
				
			||||||
		var err error
 | 
							var err error
 | 
				
			||||||
		drivers, err = h.services.GetAccountsInNamespacesMap([]string{"solidarity_drivers", "organized_carpool_drivers"})
 | 
							drivers, err = h.services.GetAccountsInNamespacesMap([]string{"solidarity_drivers", "organized_carpool_drivers"})
 | 
				
			||||||
| 
						 | 
					@ -74,36 +99,58 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
				
			||||||
		protodep, _ := transformers.GeoJsonToProto(departureGeo)
 | 
							protodep, _ := transformers.GeoJsonToProto(departureGeo)
 | 
				
			||||||
		protodest, _ := transformers.GeoJsonToProto(destinationGeo)
 | 
							protodest, _ := transformers.GeoJsonToProto(destinationGeo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Debug().Time("departure time", departureDateTime).Msg("calling driver journeys with ...")
 | 
							// Get driver IDs to exclude based on group_id (drivers who already have bookings in this group)
 | 
				
			||||||
 | 
							excludedDriverIds := make(map[string]bool)
 | 
				
			||||||
		res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, &gen.GetDriverJourneysRequest{
 | 
							if solidarityExcludeGroupId != "" {
 | 
				
			||||||
			Departure:     protodep,
 | 
								bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{
 | 
				
			||||||
			Arrival:       protodest,
 | 
									StartDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)),
 | 
				
			||||||
			DepartureDate: timestamppb.New(departureDateTime),
 | 
									EndDate:   timestamppb.New(time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)),
 | 
				
			||||||
		})
 | 
								})
 | 
				
			||||||
		if err != nil {
 | 
								if err == nil {
 | 
				
			||||||
			log.Error().Err(err).Msg("error in grpc call to GetDriverJourneys")
 | 
									for _, booking := range bookingsResp.Bookings {
 | 
				
			||||||
		} else {
 | 
										if booking.GroupId == solidarityExcludeGroupId {
 | 
				
			||||||
			solidarityTransportResults = slices.Collect(func(yield func(*gen.SolidarityTransportDriverJourney) bool) {
 | 
											excludedDriverIds[booking.DriverId] = true
 | 
				
			||||||
				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
 | 
					 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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 {
 | 
								if err != nil {
 | 
				
			||||||
				return solidarityTransportResults[i].DriverDistance < solidarityTransportResults[j].DriverDistance
 | 
									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
 | 
							// Get departure and destination addresses from properties
 | 
				
			||||||
| 
						 | 
					@ -119,124 +166,134 @@ func (h *ApplicationHandler) SearchJourneys(
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		radius := float64(5)
 | 
					 | 
				
			||||||
		// ORGANIZED CARPOOL
 | 
							// ORGANIZED CARPOOL
 | 
				
			||||||
		organizedCarpoolResultsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(ctx, &carpoolproto.DriverJourneysRequest{
 | 
							if organizedCarpoolEnabled {
 | 
				
			||||||
			DepartureLat:     departureGeo.Point().Lat(),
 | 
								radius := float64(5)
 | 
				
			||||||
			DepartureLng:     departureGeo.Point().Lon(),
 | 
								organizedCarpoolResultsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(ctx, &carpoolproto.DriverJourneysRequest{
 | 
				
			||||||
			ArrivalLat:       destinationGeo.Point().Lat(),
 | 
									DepartureLat:     departureGeo.Point().Lat(),
 | 
				
			||||||
			ArrivalLng:       destinationGeo.Point().Lon(),
 | 
									DepartureLng:     departureGeo.Point().Lon(),
 | 
				
			||||||
			DepartureDate:    timestamppb.New(departureDateTime),
 | 
									ArrivalLat:       destinationGeo.Point().Lat(),
 | 
				
			||||||
			DepartureAddress: &departureAddress,
 | 
									ArrivalLng:       destinationGeo.Point().Lon(),
 | 
				
			||||||
			ArrivalAddress:   &destinationAddress,
 | 
									DepartureDate:    timestamppb.New(departureDateTime),
 | 
				
			||||||
			DepartureRadius:  &radius,
 | 
									DepartureAddress: &departureAddress,
 | 
				
			||||||
			ArrivalRadius:    &radius,
 | 
									ArrivalAddress:   &destinationAddress,
 | 
				
			||||||
		})
 | 
									DepartureRadius:  &radius,
 | 
				
			||||||
		if err != nil {
 | 
									ArrivalRadius:    &radius,
 | 
				
			||||||
			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 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
 | 
							var wg sync.WaitGroup
 | 
				
			||||||
		// CARPOOL OPERATORS
 | 
							// CARPOOL OPERATORS
 | 
				
			||||||
		carpools := make(chan *geojson.FeatureCollection)
 | 
							if carpoolOperatorsEnabled {
 | 
				
			||||||
		go h.services.InteropCarpool.Search(carpools, *departureGeo, *destinationGeo, departureDateTime)
 | 
								carpools := make(chan *geojson.FeatureCollection)
 | 
				
			||||||
		wg.Add(1)
 | 
								go h.services.InteropCarpool.Search(carpools, *departureGeo, *destinationGeo, departureDateTime)
 | 
				
			||||||
		go func() {
 | 
								wg.Add(1)
 | 
				
			||||||
			defer wg.Done()
 | 
								go func() {
 | 
				
			||||||
			for c := range carpools {
 | 
									defer wg.Done()
 | 
				
			||||||
				carpoolResults = append(carpoolResults, c)
 | 
									for c := range carpools {
 | 
				
			||||||
			}
 | 
										carpoolResults = append(carpoolResults, c)
 | 
				
			||||||
		}()
 | 
									}
 | 
				
			||||||
 | 
								}()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// TRANSIT
 | 
							// TRANSIT
 | 
				
			||||||
		transitch := make(chan *transitous.Itinerary)
 | 
							if transitEnabled {
 | 
				
			||||||
		go func(transitch chan *transitous.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
 | 
								transitch := make(chan *transitous.Itinerary)
 | 
				
			||||||
			defer close(transitch)
 | 
								go func(transitch chan *transitous.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
 | 
				
			||||||
			response, err := h.services.TransitRouting.PlanWithResponse(ctx, &transitous.PlanParams{
 | 
									defer close(transitch)
 | 
				
			||||||
				FromPlace: fmt.Sprintf("%f,%f", departure.Point().Lat(), departure.Point().Lon()),
 | 
									response, err := h.services.TransitRouting.PlanWithResponse(ctx, &transitous.PlanParams{
 | 
				
			||||||
				ToPlace:   fmt.Sprintf("%f,%f", destination.Point().Lat(), destination.Point().Lon()),
 | 
										FromPlace: fmt.Sprintf("%f,%f", departure.Point().Lat(), departure.Point().Lon()),
 | 
				
			||||||
				Time:      datetime,
 | 
										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")
 | 
									if err != nil {
 | 
				
			||||||
				return
 | 
										log.Error().Err(err).Msg("error retrieving transit data from Transitous server")
 | 
				
			||||||
			}
 | 
										return
 | 
				
			||||||
			for _, i := range response.Itineraries {
 | 
									}
 | 
				
			||||||
				transitch <- &i
 | 
									for _, i := range response.Itineraries {
 | 
				
			||||||
			}
 | 
										transitch <- &i
 | 
				
			||||||
		}(transitch, departureGeo, destinationGeo, &departureDateTime)
 | 
									}
 | 
				
			||||||
		wg.Add(1)
 | 
								}(transitch, departureGeo, destinationGeo, &departureDateTime)
 | 
				
			||||||
		go func() {
 | 
								wg.Add(1)
 | 
				
			||||||
			defer wg.Done()
 | 
								go func() {
 | 
				
			||||||
			paris, _ := time.LoadLocation("Europe/Paris")
 | 
									defer wg.Done()
 | 
				
			||||||
			requestedDay := departureDateTime.In(paris).Truncate(24 * time.Hour)
 | 
									paris, _ := time.LoadLocation("Europe/Paris")
 | 
				
			||||||
 | 
									requestedDay := departureDateTime.In(paris).Truncate(24 * time.Hour)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			for itinerary := range transitch {
 | 
									for itinerary := range transitch {
 | 
				
			||||||
				// Only include journeys that start on the requested day (in Paris timezone)
 | 
										// Only include journeys that start on the requested day (in Paris timezone)
 | 
				
			||||||
				if !itinerary.StartTime.IsZero() && !itinerary.EndTime.IsZero() {
 | 
										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 {
 | 
					 | 
				
			||||||
						log.Info().
 | 
											log.Info().
 | 
				
			||||||
							Str("requestedDay", requestedDay.Format("2006-01-02")).
 | 
												Time("startTime", itinerary.StartTime).
 | 
				
			||||||
							Str("startDay", startDay.Format("2006-01-02")).
 | 
												Time("endTime", itinerary.EndTime).
 | 
				
			||||||
							Msg("Journey search - filtered out transit journey (not on requested day)")
 | 
												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
 | 
							// VEHICLES
 | 
				
			||||||
		vehiclech := make(chan fleetsstorage.Vehicle)
 | 
							if fleetVehiclesEnabled {
 | 
				
			||||||
		go h.vehicleRequest(vehiclech, departureDateTime.Add(-24*time.Hour), departureDateTime.Add(168*time.Hour))
 | 
								vehiclech := make(chan fleetsstorage.Vehicle)
 | 
				
			||||||
		wg.Add(1)
 | 
								go h.vehicleRequest(vehiclech, departureDateTime.Add(-24*time.Hour), departureDateTime.Add(168*time.Hour))
 | 
				
			||||||
		go func() {
 | 
								wg.Add(1)
 | 
				
			||||||
			defer wg.Done()
 | 
								go func() {
 | 
				
			||||||
			for vehicle := range vehiclech {
 | 
									defer wg.Done()
 | 
				
			||||||
				vehicleResults = append(vehicleResults, vehicle)
 | 
									for vehicle := range vehiclech {
 | 
				
			||||||
			}
 | 
										vehicleResults = append(vehicleResults, vehicle)
 | 
				
			||||||
			slices.SortFunc(vehicleResults, sorting.VehiclesByDistanceFrom(*departureGeo))
 | 
									}
 | 
				
			||||||
		}()
 | 
									slices.SortFunc(vehicleResults, sorting.VehiclesByDistanceFrom(*departureGeo))
 | 
				
			||||||
 | 
								}()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		wg.Wait()
 | 
							wg.Wait()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// KNOWLEDGE BASE
 | 
							// KNOWLEDGE BASE
 | 
				
			||||||
		departureGeoSearch, _ := h.services.Geography.GeoSearch(departureGeo)
 | 
							if knowledgeBaseEnabled {
 | 
				
			||||||
		kbData := h.config.Get("knowledge_base")
 | 
								departureGeoSearch, _ := h.services.Geography.GeoSearch(departureGeo)
 | 
				
			||||||
		if kb, ok := kbData.([]any); ok {
 | 
								kbData := h.config.Get("knowledge_base")
 | 
				
			||||||
			for _, sol := range kb {
 | 
								if kb, ok := kbData.([]any); ok {
 | 
				
			||||||
				if solution, ok := sol.(map[string]any); ok {
 | 
									for _, sol := range kb {
 | 
				
			||||||
					if g, ok := solution["geography"]; ok {
 | 
										if solution, ok := sol.(map[string]any); ok {
 | 
				
			||||||
						if geography, ok := g.([]any); ok {
 | 
											if g, ok := solution["geography"]; ok {
 | 
				
			||||||
							for _, gg := range geography {
 | 
												if geography, ok := g.([]any); ok {
 | 
				
			||||||
								if geog, ok := gg.(map[string]any); ok {
 | 
													for _, gg := range geography {
 | 
				
			||||||
									if layer, ok := geog["layer"].(string); ok {
 | 
														if geog, ok := gg.(map[string]any); ok {
 | 
				
			||||||
										code := geog["code"]
 | 
															if layer, ok := geog["layer"].(string); ok {
 | 
				
			||||||
										geo, err := h.services.Geography.Find(layer, fmt.Sprintf("%v", code))
 | 
																code := geog["code"]
 | 
				
			||||||
										if err == nil {
 | 
																geo, err := h.services.Geography.Find(layer, fmt.Sprintf("%v", code))
 | 
				
			||||||
											geog["geography"] = geo
 | 
																if err == nil {
 | 
				
			||||||
											geog["name"] = geo.Properties.MustString("nom")
 | 
																	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)
 | 
																if strings.Compare(fmt.Sprintf("%v", code), departureGeoSearch[layer].Properties.MustString("code")) == 0 {
 | 
				
			||||||
											break
 | 
																	knowledgeBaseResults = append(knowledgeBaseResults, solution)
 | 
				
			||||||
 | 
																	break
 | 
				
			||||||
 | 
																}
 | 
				
			||||||
										}
 | 
															}
 | 
				
			||||||
									}
 | 
														}
 | 
				
			||||||
								}
 | 
													}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -358,6 +358,18 @@ func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context,
 | 
				
			||||||
	transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons)
 | 
						transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons)
 | 
				
			||||||
	transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons)
 | 
						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 upcoming bookings by date (ascending - earliest first)
 | 
				
			||||||
	sort.Slice(transformedBookings, func(i, j int) bool {
 | 
						sort.Slice(transformedBookings, func(i, j int) bool {
 | 
				
			||||||
		if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil {
 | 
							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 = filterBookingsByGeography(transformedBookingsHistory, histDeparturePolygons, histDestinationPolygons)
 | 
				
			||||||
	transformedBookingsHistory = filterBookingsByPassengerAddressGeography(transformedBookingsHistory, beneficiariesMap, histPassengerAddressPolygons)
 | 
						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 history bookings by date (descending - most recent first)
 | 
				
			||||||
	sort.Slice(transformedBookingsHistory, func(i, j int) bool {
 | 
						sort.Slice(transformedBookingsHistory, func(i, j int) bool {
 | 
				
			||||||
		if transformedBookingsHistory[i].Journey != nil && transformedBookingsHistory[j].Journey != nil {
 | 
							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 = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons)
 | 
				
			||||||
	transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons)
 | 
						transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Don't filter out replaced bookings for exports - include all bookings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Sort bookings by date
 | 
						// Sort bookings by date
 | 
				
			||||||
	sort.Slice(transformedBookings, func(i, j int) bool {
 | 
						sort.Slice(transformedBookings, func(i, j int) bool {
 | 
				
			||||||
		if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil {
 | 
							if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil {
 | 
				
			||||||
| 
						 | 
					@ -744,6 +760,8 @@ func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Contex
 | 
				
			||||||
		bookings = append(bookings, booking)
 | 
							bookings = append(bookings, booking)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Don't filter out replaced bookings from driver profile - show all bookings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Collect unique passenger IDs
 | 
						// Collect unique passenger IDs
 | 
				
			||||||
	passengerIDs := []string{}
 | 
						passengerIDs := []string{}
 | 
				
			||||||
	passengerIDsMap := make(map[string]bool)
 | 
						passengerIDsMap := make(map[string]bool)
 | 
				
			||||||
| 
						 | 
					@ -1048,7 +1066,19 @@ func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Conte
 | 
				
			||||||
	}, nil
 | 
						}, 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
 | 
						// Get journey for pricing calculation
 | 
				
			||||||
	journeyRequest := &gen.GetDriverJourneyRequest{
 | 
						journeyRequest := &gen.GetDriverJourneyRequest{
 | 
				
			||||||
		DriverId:  driverID,
 | 
							DriverId:  driverID,
 | 
				
			||||||
| 
						 | 
					@ -1083,6 +1113,12 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context
 | 
				
			||||||
	returnWaitingDuration := int64(returnWaitingTimeMinutes) * int64(time.Minute)
 | 
						returnWaitingDuration := int64(returnWaitingTimeMinutes) * int64(time.Minute)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create booking request
 | 
						// 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{
 | 
						bookingRequest := &gen.BookDriverJourneyRequest{
 | 
				
			||||||
		PassengerId:                passengerID,
 | 
							PassengerId:                passengerID,
 | 
				
			||||||
		DriverId:                   driverID,
 | 
							DriverId:                   driverID,
 | 
				
			||||||
| 
						 | 
					@ -1093,19 +1129,45 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context
 | 
				
			||||||
		DriverCompensationAmount:   driverCompensation,
 | 
							DriverCompensationAmount:   driverCompensation,
 | 
				
			||||||
		DriverCompensationCurrency: "EUR",
 | 
							DriverCompensationCurrency: "EUR",
 | 
				
			||||||
		Data: &structpb.Struct{
 | 
							Data: &structpb.Struct{
 | 
				
			||||||
			Fields: map[string]*structpb.Value{
 | 
								Fields: dataFields,
 | 
				
			||||||
				"motivation":  structpb.NewStringValue(motivation),
 | 
					 | 
				
			||||||
				"message":     structpb.NewStringValue(message),
 | 
					 | 
				
			||||||
				"do_not_send": structpb.NewBoolValue(doNotSend),
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set group_id if this is a replacement booking
 | 
				
			||||||
 | 
						if groupID != "" {
 | 
				
			||||||
 | 
							bookingRequest.GroupId = &groupID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := h.services.GRPC.SolidarityTransport.BookDriverJourney(ctx, bookingRequest)
 | 
						resp, err := h.services.GRPC.SolidarityTransport.BookDriverJourney(ctx, bookingRequest)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", err
 | 
							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
 | 
						// Send SMS if not disabled
 | 
				
			||||||
	if !doNotSend && message != "" {
 | 
						if !doNotSend && message != "" {
 | 
				
			||||||
		send_message := strings.ReplaceAll(message, "{booking_id}", resp.Booking.Id)
 | 
							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) {
 | 
					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
 | 
						// Transform proto to type for geography access
 | 
				
			||||||
	journeyType, err := solidaritytransformers.DriverJourneyProtoToType(journey)
 | 
						journeyType, err := solidaritytransformers.DriverJourneyProtoToType(journey)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,28 +1,29 @@
 | 
				
			||||||
package geo
 | 
					package geo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/paulmach/orb/geojson"
 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type GeoService struct {
 | 
					type GeoService struct {
 | 
				
			||||||
	peliasURL string
 | 
						geoType          string
 | 
				
			||||||
 | 
						baseURL          string
 | 
				
			||||||
 | 
						autocompleteURL  string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewGeoService(peliasURL string) *GeoService {
 | 
					func NewGeoService(geoType, baseURL, autocompleteEndpoint string) *GeoService {
 | 
				
			||||||
	return &GeoService{peliasURL: peliasURL}
 | 
						return &GeoService{
 | 
				
			||||||
 | 
							geoType:         geoType,
 | 
				
			||||||
 | 
							baseURL:         baseURL,
 | 
				
			||||||
 | 
							autocompleteURL: baseURL + autocompleteEndpoint,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type AutocompleteResult struct {
 | 
					func (s *GeoService) Autocomplete(text string) (*geojson.FeatureCollection, error) {
 | 
				
			||||||
	Features []any
 | 
						resp, err := http.Get(s.autocompleteURL + text)
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *GeoService) Autocomplete(text string) (*AutocompleteResult, error) {
 | 
					 | 
				
			||||||
	resp, err := http.Get(fmt.Sprintf("%s/autocomplete?text=%s", s.peliasURL, text))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -34,17 +35,11 @@ func (s *GeoService) Autocomplete(text string) (*AutocompleteResult, error) {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var response map[string]any
 | 
						featureCollection, err := geojson.UnmarshalFeatureCollection(body)
 | 
				
			||||||
	if err := json.Unmarshal(body, &response); err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error().Err(err).Msg("Failed to unmarshal feature collection")
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	features, ok := response["features"].([]any)
 | 
						return featureCollection, nil
 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		features = []any{}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &AutocompleteResult{
 | 
					 | 
				
			||||||
		Features: features,
 | 
					 | 
				
			||||||
	}, 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/routing-service v0.0.0-20250304234521-faabcc54f536
 | 
				
			||||||
	git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463
 | 
						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/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/arran4/golang-ical v0.3.1
 | 
				
			||||||
	github.com/coreos/go-oidc/v3 v3.11.0
 | 
						github.com/coreos/go-oidc/v3 v3.11.0
 | 
				
			||||||
	github.com/go-viper/mapstructure/v2 v2.4.0
 | 
						github.com/go-viper/mapstructure/v2 v2.4.0
 | 
				
			||||||
	github.com/gorilla/securecookie v1.1.1
 | 
						github.com/gorilla/securecookie v1.1.1
 | 
				
			||||||
	github.com/minio/minio-go/v7 v7.0.43
 | 
						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/paulmach/orb v0.12.0
 | 
				
			||||||
	github.com/rs/zerolog v1.34.0
 | 
						github.com/rs/zerolog v1.34.0
 | 
				
			||||||
	github.com/stretchr/objx v0.5.3
 | 
						github.com/stretchr/objx v0.5.3
 | 
				
			||||||
| 
						 | 
					@ -109,7 +110,6 @@ require (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	ariga.io/atlas v0.37.0 // indirect
 | 
						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/agext/levenshtein v1.2.3 // indirect
 | 
				
			||||||
	github.com/coreos/go-semver v0.3.0 // indirect
 | 
						github.com/coreos/go-semver v0.3.0 // indirect
 | 
				
			||||||
	github.com/coreos/go-systemd/v22 v22.5.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/protobuf v1.5.4 // indirect
 | 
				
			||||||
	github.com/golang/snappy v1.0.0 // indirect
 | 
						github.com/golang/snappy v1.0.0 // indirect
 | 
				
			||||||
	github.com/google/go-cmp v0.7.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/hashicorp/hcl/v2 v2.24.0 // indirect
 | 
				
			||||||
	github.com/json-iterator/go v1.1.12 // indirect
 | 
						github.com/json-iterator/go v1.1.12 // indirect
 | 
				
			||||||
	github.com/klauspost/compress v1.18.0 // 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/mitchellh/go-wordwrap v1.0.1 // indirect
 | 
				
			||||||
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
						github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
				
			||||||
	github.com/modern-go/reflect2 v1.0.2 // 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/montanaflynn/stats v0.7.1 // indirect
 | 
				
			||||||
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 | 
						github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 | 
				
			||||||
	github.com/pkg/errors v0.9.1 // 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/xdg-go/stringprep v1.0.4 // indirect
 | 
				
			||||||
	github.com/xuri/efp v0.0.1 // indirect
 | 
						github.com/xuri/efp v0.0.1 // indirect
 | 
				
			||||||
	github.com/xuri/nfp 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/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
 | 
				
			||||||
	github.com/zclconf/go-cty v1.17.0 // indirect
 | 
						github.com/zclconf/go-cty v1.17.0 // indirect
 | 
				
			||||||
	go.etcd.io/etcd/api/v3 v3.5.12 // 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/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 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/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 h1:4E0tbT8jj5oxaK66Ny61o7zqPaVc0qRN2cZG9IUR4Es=
 | 
				
			||||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858/go.mod h1:TbR3g1Awa8hpAe6LR1z1EQbv2IBVgN5JQ/FjXfKX4K0=
 | 
					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 h1:bY7PyrAgYY02f5IpDyf1WVfRqvWzivu31K6aEAYbWCw=
 | 
				
			||||||
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c/go.mod h1:lozSy6qlIIYhvKKXscZzz28HAtS0qBDUTv5nofLRmYA=
 | 
					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 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/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 h1:jo7fF7tLIAU110tUSIYXkMAvu30g8wHzZCLq3YomooQ=
 | 
				
			||||||
git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013140400-42fb40437ac3/go.mod h1:npYccQZcZj1WzTHhpLfFHFSBA3RiZOkO5R9x4uy1a9I=
 | 
					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 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/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=
 | 
					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/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 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/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-20251016152145-e0882db1bcbc h1:NLU5DUo5Kt3jkPhV3KkqQMahTHIrGildBvYlHwJ6JmM=
 | 
				
			||||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251008070137-723c12a6573d/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
 | 
					git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc/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=
 | 
					 | 
				
			||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 | 
					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.3.2/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 | 
				
			||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
 | 
					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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 | 
				
			||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 | 
					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/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.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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 | 
				
			||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
					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 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 | 
				
			||||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 | 
					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/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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 | 
				
			||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
					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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 | 
				
			||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 | 
					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.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 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
 | 
				
			||||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
 | 
					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 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
 | 
				
			||||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
 | 
					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.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 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
 | 
				
			||||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
 | 
					github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
 | 
				
			||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 | 
					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.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 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
 | 
				
			||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
 | 
					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 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
 | 
				
			||||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
 | 
					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 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
 | 
				
			||||||
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
 | 
					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 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
 | 
				
			||||||
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
 | 
					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-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 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
 | 
				
			||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
 | 
					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-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-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.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 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
 | 
				
			||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
 | 
					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 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
 | 
				
			||||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
 | 
					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=
 | 
					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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
				
			||||||
golang.org/x/mod v0.4.2/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.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 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
 | 
				
			||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
 | 
					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=
 | 
					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-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-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.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 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
 | 
				
			||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
 | 
					golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
 | 
				
			||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
 | 
					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-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-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.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 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
 | 
				
			||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 | 
					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=
 | 
					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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.6.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
 | 
					golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
 | 
				
			||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 | 
					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-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.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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
				
			||||||
golang.org/x/text v0.3.6/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
				
			||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 | 
					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 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
 | 
				
			||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
 | 
					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=
 | 
					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.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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
				
			||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 | 
					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 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
 | 
				
			||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
 | 
					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=
 | 
					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/core/application"
 | 
				
			||||||
	"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
 | 
						"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/servers/web"
 | 
				
			||||||
	"git.coopgo.io/coopgo-apps/parcoursmob/services"
 | 
						"git.coopgo.io/coopgo-apps/parcoursmob/services"
 | 
				
			||||||
	"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
 | 
						"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
 | 
				
			||||||
| 
						 | 
					@ -25,6 +26,7 @@ func main() {
 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		dev_env    = cfg.GetBool("dev_env")
 | 
							dev_env    = cfg.GetBool("dev_env")
 | 
				
			||||||
		webEnabled = cfg.GetBool("server.web.enabled")
 | 
							webEnabled = cfg.GetBool("server.web.enabled")
 | 
				
			||||||
 | 
							mcpEnabled = cfg.GetBool("server.mcp.enabled")
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if dev_env {
 | 
						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()
 | 
						wg.Wait()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,7 @@ func (renderer *Renderer) SolidarityTransportDriverDisplay(w http.ResponseWriter
 | 
				
			||||||
	renderer.Render("solidarity transport driver creation", w, r, files, state)
 | 
						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")
 | 
						files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.driver_journey.files")
 | 
				
			||||||
	bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
 | 
						bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
 | 
				
			||||||
	state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
 | 
						state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
 | 
				
			||||||
| 
						 | 
					@ -103,12 +103,13 @@ func (renderer *Renderer) SolidarityTransportDriverJourney(w http.ResponseWriter
 | 
				
			||||||
		"passenger_wallet_balance":  passengerWalletBalance,
 | 
							"passenger_wallet_balance":  passengerWalletBalance,
 | 
				
			||||||
		"pricing_result":            pricingResult,
 | 
							"pricing_result":            pricingResult,
 | 
				
			||||||
		"booking_motivations":       bookingMotivations,
 | 
							"booking_motivations":       bookingMotivations,
 | 
				
			||||||
 | 
							"replaces_booking_id":       replacesBookingID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	renderer.Render("solidarity transport driver creation", w, r, files, state)
 | 
						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")
 | 
						files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.booking_display.files")
 | 
				
			||||||
	bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
 | 
						bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
 | 
				
			||||||
	state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
 | 
						state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
 | 
				
			||||||
| 
						 | 
					@ -116,8 +117,13 @@ func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWrite
 | 
				
			||||||
		"driver":                   driver,
 | 
							"driver":                   driver,
 | 
				
			||||||
		"passenger":                passenger,
 | 
							"passenger":                passenger,
 | 
				
			||||||
		"booking":                  booking,
 | 
							"booking":                  booking,
 | 
				
			||||||
 | 
							"config":                   renderer.GlobalConfig,
 | 
				
			||||||
		"passenger_wallet_balance": passengerWalletBalance,
 | 
							"passenger_wallet_balance": passengerWalletBalance,
 | 
				
			||||||
		"booking_motivations":      bookingMotivations,
 | 
							"booking_motivations":      bookingMotivations,
 | 
				
			||||||
 | 
							"replacement_drivers":      replacementDrivers,
 | 
				
			||||||
 | 
							"replacement_drivers_map":  replacementDriversMap,
 | 
				
			||||||
 | 
							"replacement_pricing":      replacementPricing,
 | 
				
			||||||
 | 
							"replacement_locations":    replacementLocations,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	renderer.Render("booking display", w, r, files, state)
 | 
						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
 | 
						// Build headers dynamically based on configuration
 | 
				
			||||||
	headers := []string{
 | 
						headers := []string{
 | 
				
			||||||
		"ID Réservation",
 | 
							"ID Réservation",
 | 
				
			||||||
 | 
							"ID Groupe",
 | 
				
			||||||
		"Statut",
 | 
							"Statut",
 | 
				
			||||||
		"Motif de réservation",
 | 
							"Motif de réservation",
 | 
				
			||||||
		"Raison d'annulation",
 | 
							"Raison d'annulation",
 | 
				
			||||||
 | 
							"Remplacé par (ID)",
 | 
				
			||||||
		"Date de prise en charge",
 | 
							"Date de prise en charge",
 | 
				
			||||||
		"Heure de prise en charge",
 | 
							"Heure de prise en charge",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -102,6 +104,7 @@ func (r *XLSXRenderer) SolidarityTransportBookings(w http.ResponseWriter, result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Booking information
 | 
							// Booking information
 | 
				
			||||||
		row = append(row, booking.Id)
 | 
							row = append(row, booking.Id)
 | 
				
			||||||
 | 
							row = append(row, booking.GroupId)
 | 
				
			||||||
		row = append(row, booking.Status)
 | 
							row = append(row, booking.Status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Motivation (from booking.Data)
 | 
							// Motivation (from booking.Data)
 | 
				
			||||||
| 
						 | 
					@ -122,6 +125,15 @@ func (r *XLSXRenderer) SolidarityTransportBookings(w http.ResponseWriter, result
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		row = append(row, cancellationReason)
 | 
							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
 | 
							// Journey date and time
 | 
				
			||||||
		if booking.Journey != nil {
 | 
							if booking.Journey != nil {
 | 
				
			||||||
			row = append(row, booking.Journey.PassengerPickupDate.Format("2006-01-02"))
 | 
								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
 | 
					package api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,13 +13,13 @@ func (h *Handler) GeoAutocomplete(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	text := t[0]
 | 
						text := t[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	result, err := h.geoService.Autocomplete(text)
 | 
						featureCollection, err := h.geoService.Autocomplete(text)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	j, err := json.Marshal(result.Features)
 | 
						j, err := featureCollection.MarshalJSON()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,12 @@ type Handler struct {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, appHandler *application.ApplicationHandler, cacheHandler cacheStorage.CacheHandler) *Handler {
 | 
					func NewHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, appHandler *application.ApplicationHandler, cacheHandler cacheStorage.CacheHandler) *Handler {
 | 
				
			||||||
	cacheService := cache.NewCacheService(cacheHandler)
 | 
						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{
 | 
						return &Handler{
 | 
				
			||||||
		config:            cfg,
 | 
							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}/confirm", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("confirm"))
 | 
				
			||||||
	solidarityTransport.HandleFunc("/bookings/{bookingid}/cancel", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("cancel"))
 | 
						solidarityTransport.HandleFunc("/bookings/{bookingid}/cancel", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("cancel"))
 | 
				
			||||||
	solidarityTransport.HandleFunc("/bookings/{bookingid}/waitconfirmation", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("waitconfirmation"))
 | 
						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,
 | 
								destinationGeo,
 | 
				
			||||||
			passengerID,
 | 
								passengerID,
 | 
				
			||||||
			solidarityTransportExcludeDriver,
 | 
								solidarityTransportExcludeDriver,
 | 
				
			||||||
 | 
								"",  // solidarityExcludeGroupId - for modal search replacement only
 | 
				
			||||||
 | 
								nil, // options - use defaults
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Error().Err(err).Msg("error in journey search")
 | 
								log.Error().Err(err).Msg("error in journey search")
 | 
				
			||||||
| 
						 | 
					@ -389,6 +391,8 @@ func (h *Handler) JourneysSearchCompactHTTPHandler() http.HandlerFunc {
 | 
				
			||||||
			destinationGeo,
 | 
								destinationGeo,
 | 
				
			||||||
			passengerID,
 | 
								passengerID,
 | 
				
			||||||
			solidarityTransportExcludeDriver,
 | 
								solidarityTransportExcludeDriver,
 | 
				
			||||||
 | 
								"",  // solidarityExcludeGroupId - for modal search replacement only
 | 
				
			||||||
 | 
								nil, // options - use defaults
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Error().Err(err).Msg("error in journey search")
 | 
								log.Error().Err(err).Msg("error in journey search")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,8 @@ import (
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
 | 
						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"
 | 
						"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
 | 
				
			||||||
	"github.com/gorilla/mux"
 | 
						"github.com/gorilla/mux"
 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
| 
						 | 
					@ -520,6 +522,7 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
 | 
				
			||||||
		driverID := vars["driverid"]
 | 
							driverID := vars["driverid"]
 | 
				
			||||||
		journeyID := vars["journeyid"]
 | 
							journeyID := vars["journeyid"]
 | 
				
			||||||
		passengerID := r.URL.Query().Get("passengerid")
 | 
							passengerID := r.URL.Query().Get("passengerid")
 | 
				
			||||||
 | 
							replacesBookingID := r.URL.Query().Get("replaces_booking_id")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if r.Method == "POST" {
 | 
							if r.Method == "POST" {
 | 
				
			||||||
			// Parse form data
 | 
								// Parse form data
 | 
				
			||||||
| 
						 | 
					@ -531,7 +534,8 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
 | 
				
			||||||
				fmt.Sscanf(r.PostFormValue("return_waiting_time"), "%d", &returnWaitingTimeMinutes)
 | 
									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 {
 | 
								if err != nil {
 | 
				
			||||||
				log.Error().Err(err).Msg("error creating solidarity transport journey booking")
 | 
									log.Error().Err(err).Msg("error creating solidarity transport journey booking")
 | 
				
			||||||
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
									http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
				
			||||||
| 
						 | 
					@ -559,7 +563,7 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
 | 
				
			||||||
			return
 | 
								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
 | 
								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