package application import ( "context" "encoding/json" "fmt" "io" "slices" "sort" "strconv" "strings" "time" groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi" groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage" mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi" mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage" "git.coopgo.io/coopgo-platform/payments/pricing" "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen" solidaritytransformers "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers" solidaritytypes "git.coopgo.io/coopgo-platform/solidarity-transport/types" "github.com/google/uuid" "github.com/paulmach/orb" "github.com/paulmach/orb/geojson" "github.com/paulmach/orb/planar" "github.com/rs/zerolog/log" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators" filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage" ) type DriversForm struct { FirstName string `json:"first_name" validate:"required"` LastName string `json:"last_name" validate:"required"` Email string `json:"email" validate:"required,email"` Birthdate *time.Time `json:"birthdate" validate:"required"` PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"` Address any `json:"address,omitempty"` Gender string `json:"gender"` } const ( Sunday = iota Monday Tuesday Wednesday Thursday Friday Saturday ) type SolidarityTransportOverviewResult struct { Accounts []mobilityaccountsstorage.Account AccountsMap map[string]mobilityaccountsstorage.Account BeneficiariesMap map[string]mobilityaccountsstorage.Account Bookings []*solidaritytypes.Booking BookingsHistory []*solidaritytypes.Booking } // loadGeographyPolygon loads a single geography polygon for filtering func (h *ApplicationHandler) loadGeographyPolygon(layer, code string) ([]orb.Polygon, error) { if layer == "" || code == "" { return nil, nil } // Fetch geography from service geoFeature, err := h.services.Geography.Find(layer, code) if err != nil { return nil, fmt.Errorf("failed to load geography %s/%s: %w", layer, code, err) } polygons := []orb.Polygon{} // Extract polygon from the feature if geoFeature != nil && geoFeature.Geometry != nil { switch geom := geoFeature.Geometry.(type) { case orb.Polygon: polygons = append(polygons, geom) case orb.MultiPolygon: for _, poly := range geom { polygons = append(polygons, poly) } } } return polygons, nil } // isPointInGeographies checks if a point is within any of the geography polygons func isPointInGeographies(point orb.Point, polygons []orb.Polygon) bool { for _, poly := range polygons { if planar.PolygonContains(poly, point) { return true } } return false } // filterBookingsByPassengerAddressGeography filters bookings where passenger address is within geography func filterBookingsByPassengerAddressGeography(bookings []*solidaritytypes.Booking, beneficiariesMap map[string]mobilityaccountsstorage.Account, addressPolygons []orb.Polygon) []*solidaritytypes.Booking { if len(addressPolygons) == 0 { return bookings } filtered := []*solidaritytypes.Booking{} for _, booking := range bookings { passenger, ok := beneficiariesMap[booking.PassengerId] if !ok { continue } // Check if passenger has address with geometry if address, ok := passenger.Data["address"].(map[string]any); ok { if geometry, ok := address["geometry"].(map[string]any); ok { if geomType, ok := geometry["type"].(string); ok && geomType == "Point" { if coords, ok := geometry["coordinates"].([]any); ok && len(coords) == 2 { if lon, ok := coords[0].(float64); ok { if lat, ok := coords[1].(float64); ok { point := orb.Point{lon, lat} if isPointInGeographies(point, addressPolygons) { filtered = append(filtered, booking) } } } } } } } } return filtered } // filterBookingsByGeography filters bookings based on geography constraints func filterBookingsByGeography(bookings []*solidaritytypes.Booking, departurePolygons, destinationPolygons []orb.Polygon) []*solidaritytypes.Booking { // If no filters, return all bookings if len(departurePolygons) == 0 && len(destinationPolygons) == 0 { return bookings } filtered := []*solidaritytypes.Booking{} for _, booking := range bookings { if booking.Journey == nil { continue } includeBooking := true // Check departure filter if provided if len(departurePolygons) > 0 { departureMatch := false if booking.Journey.PassengerPickup != nil && booking.Journey.PassengerPickup.Geometry != nil { if point, ok := booking.Journey.PassengerPickup.Geometry.(orb.Point); ok { departureMatch = isPointInGeographies(point, departurePolygons) } } if !departureMatch { includeBooking = false } } // Check destination filter if provided if len(destinationPolygons) > 0 && includeBooking { destinationMatch := false if booking.Journey.PassengerDrop != nil && booking.Journey.PassengerDrop.Geometry != nil { if point, ok := booking.Journey.PassengerDrop.Geometry.(orb.Point); ok { destinationMatch = isPointInGeographies(point, destinationPolygons) } } if !destinationMatch { includeBooking = false } } if includeBooking { filtered = append(filtered, booking) } } return filtered } func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context, status, driverID, startDate, endDate, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode, histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeoLayer, histDepartureGeoCode, histDestinationGeoLayer, histDestinationGeoCode, histPassengerAddressGeoLayer, histPassengerAddressGeoCode string, archivedFilter bool, driverAddressGeoLayer, driverAddressGeoCode string) (*SolidarityTransportOverviewResult, error) { // Get ALL drivers for the accountsMap (used in bookings display) allDrivers, err := h.solidarityDrivers("", false) if err != nil { log.Error().Err(err).Msg("issue getting all solidarity drivers") allDrivers = []mobilityaccountsstorage.Account{} } // Build accountsMap with ALL drivers (for bookings to reference) accountsMap := map[string]mobilityaccountsstorage.Account{} for _, a := range allDrivers { accountsMap[a.ID] = a } // Get filtered drivers for the drivers tab display accounts, err := h.solidarityDrivers("", archivedFilter) if err != nil { log.Error().Err(err).Msg("issue getting solidarity drivers") accounts = []mobilityaccountsstorage.Account{} } // Apply driver address geography filtering only to the drivers tab list if driverAddressGeoLayer != "" && driverAddressGeoCode != "" { driverAddressPolygons, err := h.loadGeographyPolygon(driverAddressGeoLayer, driverAddressGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load driver address geography filter") } else { filtered := []mobilityaccountsstorage.Account{} for _, account := range accounts { // Check if driver has address - unmarshal as GeoJSON Feature if addr, ok := account.Data["address"]; ok { jsonAddr, err := json.Marshal(addr) if err == nil { addrGeojson, err := geojson.UnmarshalFeature(jsonAddr) if err == nil && addrGeojson.Geometry != nil { if point, ok := addrGeojson.Geometry.(orb.Point); ok { if isPointInGeographies(point, driverAddressPolygons) { filtered = append(filtered, account) } } } } } } accounts = filtered } } // Sort accounts by last name, then first name slices.SortFunc(accounts, func(a, b mobilityaccountsstorage.Account) int { lastNameA := strings.ToLower(a.Data["last_name"].(string)) lastNameB := strings.ToLower(b.Data["last_name"].(string)) if lastNameA != lastNameB { return strings.Compare(lastNameA, lastNameB) } firstNameA := strings.ToLower(a.Data["first_name"].(string)) firstNameB := strings.ToLower(b.Data["first_name"].(string)) return strings.Compare(firstNameA, firstNameB) }) beneficiariesMap, err := h.services.GetBeneficiariesMap() if err != nil { beneficiariesMap = map[string]mobilityaccountsstorage.Account{} } // Parse start date or use default var startdate time.Time if startDate != "" { if parsed, err := time.Parse("2006-01-02", startDate); err == nil { startdate = parsed } else { startdate = time.Now() } } else { startdate = time.Now() } // Parse end date or use default var enddate time.Time if endDate != "" { if parsed, err := time.Parse("2006-01-02", endDate); err == nil { enddate = parsed.Add(24 * time.Hour) // End of day } else { enddate = time.Now().Add(24 * 365 * time.Hour) } } else { enddate = time.Now().Add(24 * 365 * time.Hour) } request := &gen.GetSolidarityTransportBookingsRequest{ StartDate: timestamppb.New(startdate), EndDate: timestamppb.New(enddate), Status: status, Driverid: driverID, } resp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, request) if err != nil { log.Error().Err(err).Msg("issue getting solidarity transport bookings") return &SolidarityTransportOverviewResult{ Accounts: accounts, AccountsMap: accountsMap, BeneficiariesMap: beneficiariesMap, Bookings: []*solidaritytypes.Booking{}, BookingsHistory: []*solidaritytypes.Booking{}, }, nil } // Get bookings history with filters // Parse history start date or use default (1 month ago) var histStartdate time.Time if histStartDate != "" { if parsed, err := time.Parse("2006-01-02", histStartDate); err == nil { histStartdate = parsed } else { histStartdate = time.Now().Add(-30 * 24 * time.Hour) } } else { histStartdate = time.Now().Add(-30 * 24 * time.Hour) } // Parse history end date or use default (yesterday) var histEnddate time.Time if histEndDate != "" { if parsed, err := time.Parse("2006-01-02", histEndDate); err == nil { histEnddate = parsed.Add(24 * time.Hour) // End of day } else { histEnddate = time.Now().Add(-24 * time.Hour) } } else { histEnddate = time.Now().Add(-24 * time.Hour) } historyRequest := &gen.GetSolidarityTransportBookingsRequest{ StartDate: timestamppb.New(histStartdate), EndDate: timestamppb.New(histEnddate), Status: histStatus, Driverid: histDriverID, } historyResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, historyRequest) bookingsHistory := []*gen.SolidarityTransportBooking{} if err == nil { bookingsHistory = historyResp.Bookings } // Transform bookings to types transformedBookings := []*solidaritytypes.Booking{} for _, booking := range resp.Bookings { if transformed, err := solidaritytransformers.BookingProtoToType(booking); err == nil { transformedBookings = append(transformedBookings, transformed) } } // Apply geography filtering for current bookings var departurePolygons, destinationPolygons, passengerAddressPolygons []orb.Polygon if departureGeoLayer != "" && departureGeoCode != "" { departurePolygons, err = h.loadGeographyPolygon(departureGeoLayer, departureGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load departure geography filter") } } if destinationGeoLayer != "" && destinationGeoCode != "" { destinationPolygons, err = h.loadGeographyPolygon(destinationGeoLayer, destinationGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load destination geography filter") } } if passengerAddressGeoLayer != "" && passengerAddressGeoCode != "" { passengerAddressPolygons, err = h.loadGeographyPolygon(passengerAddressGeoLayer, passengerAddressGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load passenger address geography filter") } } transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons) transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons) // Sort upcoming bookings by date (ascending - earliest first) sort.Slice(transformedBookings, func(i, j int) bool { if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil { return transformedBookings[i].Journey.PassengerPickupDate.Before(transformedBookings[j].Journey.PassengerPickupDate) } return false }) transformedBookingsHistory := []*solidaritytypes.Booking{} for _, booking := range bookingsHistory { if transformed, err := solidaritytransformers.BookingProtoToType(booking); err == nil { transformedBookingsHistory = append(transformedBookingsHistory, transformed) } } // Apply geography filtering for history bookings var histDeparturePolygons, histDestinationPolygons, histPassengerAddressPolygons []orb.Polygon if histDepartureGeoLayer != "" && histDepartureGeoCode != "" { histDeparturePolygons, err = h.loadGeographyPolygon(histDepartureGeoLayer, histDepartureGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load history departure geography filter") } } if histDestinationGeoLayer != "" && histDestinationGeoCode != "" { histDestinationPolygons, err = h.loadGeographyPolygon(histDestinationGeoLayer, histDestinationGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load history destination geography filter") } } if histPassengerAddressGeoLayer != "" && histPassengerAddressGeoCode != "" { histPassengerAddressPolygons, err = h.loadGeographyPolygon(histPassengerAddressGeoLayer, histPassengerAddressGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load history passenger address geography filter") } } transformedBookingsHistory = filterBookingsByGeography(transformedBookingsHistory, histDeparturePolygons, histDestinationPolygons) transformedBookingsHistory = filterBookingsByPassengerAddressGeography(transformedBookingsHistory, beneficiariesMap, histPassengerAddressPolygons) // Sort history bookings by date (descending - most recent first) sort.Slice(transformedBookingsHistory, func(i, j int) bool { if transformedBookingsHistory[i].Journey != nil && transformedBookingsHistory[j].Journey != nil { return transformedBookingsHistory[i].Journey.PassengerPickupDate.After(transformedBookingsHistory[j].Journey.PassengerPickupDate) } return false }) return &SolidarityTransportOverviewResult{ Accounts: accounts, AccountsMap: accountsMap, BeneficiariesMap: beneficiariesMap, Bookings: transformedBookings, BookingsHistory: transformedBookingsHistory, }, nil } type SolidarityTransportBookingsResult struct { Bookings []*solidaritytypes.Booking DriversMap map[string]mobilityaccountsstorage.Account BeneficiariesMap map[string]mobilityaccountsstorage.Account } func (h *ApplicationHandler) GetSolidarityTransportBookings(ctx context.Context, startDate, endDate *time.Time, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode string) (*SolidarityTransportBookingsResult, error) { // Get all drivers drivers, err := h.solidarityDrivers("", false) if err != nil { log.Error().Err(err).Msg("issue getting solidarity drivers") drivers = []mobilityaccountsstorage.Account{} } driversMap := map[string]mobilityaccountsstorage.Account{} for _, d := range drivers { driversMap[d.ID] = d } // Get beneficiaries beneficiariesMap, err := h.services.GetBeneficiariesMap() if err != nil { beneficiariesMap = map[string]mobilityaccountsstorage.Account{} } // Determine date range var start, end time.Time if startDate != nil { start = *startDate } else { // Default: 1 year ago start = time.Now().Add(-365 * 24 * time.Hour) } if endDate != nil { end = *endDate } else { // Default: now end = time.Now() } // Get bookings request := &gen.GetSolidarityTransportBookingsRequest{ StartDate: timestamppb.New(start), EndDate: timestamppb.New(end), Status: status, Driverid: driverID, } resp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, request) if err != nil { log.Error().Err(err).Msg("issue getting solidarity transport bookings") return &SolidarityTransportBookingsResult{ Bookings: []*solidaritytypes.Booking{}, DriversMap: driversMap, BeneficiariesMap: beneficiariesMap, }, nil } // Transform bookings to types transformedBookings := []*solidaritytypes.Booking{} for _, booking := range resp.Bookings { if transformed, err := solidaritytransformers.BookingProtoToType(booking); err == nil { transformedBookings = append(transformedBookings, transformed) } } // Apply geography filtering var departurePolygons, destinationPolygons, passengerAddressPolygons []orb.Polygon if departureGeoLayer != "" && departureGeoCode != "" { departurePolygons, err = h.loadGeographyPolygon(departureGeoLayer, departureGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load departure geography filter for export") } } if destinationGeoLayer != "" && destinationGeoCode != "" { destinationPolygons, err = h.loadGeographyPolygon(destinationGeoLayer, destinationGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load destination geography filter for export") } } if passengerAddressGeoLayer != "" && passengerAddressGeoCode != "" { passengerAddressPolygons, err = h.loadGeographyPolygon(passengerAddressGeoLayer, passengerAddressGeoCode) if err != nil { log.Warn().Err(err).Msg("failed to load passenger address geography filter for export") } } transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons) transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons) // Sort bookings by date sort.Slice(transformedBookings, func(i, j int) bool { if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil { return transformedBookings[i].Journey.PassengerPickupDate.Before(transformedBookings[j].Journey.PassengerPickupDate) } return false }) return &SolidarityTransportBookingsResult{ Bookings: transformedBookings, DriversMap: driversMap, BeneficiariesMap: beneficiariesMap, }, nil } func (h *ApplicationHandler) CreateSolidarityTransportDriver(ctx context.Context, firstName, lastName, email string, birthdate *time.Time, phoneNumber string, address any, gender string, otherProperties any) (string, error) { dataMap := map[string]any{ "first_name": firstName, "last_name": lastName, "email": email, "phone_number": phoneNumber, "gender": gender, } // Convert birthdate to string for structpb compatibility if birthdate != nil { dataMap["birthdate"] = birthdate.Format(time.RFC3339) } if address != nil { dataMap["address"] = address } if otherProperties != nil { dataMap["other_properties"] = otherProperties } // Validate the data formData := DriversForm{ FirstName: firstName, LastName: lastName, Email: email, Birthdate: birthdate, PhoneNumber: phoneNumber, Address: address, Gender: gender, } validate := formvalidators.New() if err := validate.Struct(formData); err != nil { return "", err } data, err := structpb.NewValue(dataMap) if err != nil { return "", err } request := &mobilityaccounts.RegisterRequest{ Account: &mobilityaccounts.Account{ Namespace: "solidarity_drivers", Data: data.GetStructValue(), }, } resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, request) if err != nil { return "", err } return resp.Account.Id, nil } type SolidarityTransportDriverResult struct { Driver mobilityaccountsstorage.Account } func (h *ApplicationHandler) GetSolidarityTransportDriver(ctx context.Context, driverID string) (*SolidarityTransportDriverResult, error) { request := &mobilityaccounts.GetAccountRequest{ Id: driverID, } resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request) if err != nil { return nil, err } // Security check: ensure this is actually a solidarity transport driver account if resp.Account.Namespace != "solidarity_drivers" { return nil, fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, resp.Account.Namespace) } return &SolidarityTransportDriverResult{ Driver: resp.Account.ToStorageType(), }, nil } func (h *ApplicationHandler) UpdateSolidarityTransportDriver(ctx context.Context, driverID, firstName, lastName, email string, birthdate *time.Time, phoneNumber string, address any, gender string, otherProperties any) (string, error) { // Security check: verify the account exists and is a solidarity transport driver getRequest := &mobilityaccounts.GetAccountRequest{ Id: driverID, } getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest) if err != nil { return "", err } if getResp.Account.Namespace != "solidarity_drivers" { return "", fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace) } dataMap := map[string]any{ "first_name": firstName, "last_name": lastName, "email": email, "phone_number": phoneNumber, "gender": gender, } // Convert birthdate to string for structpb compatibility if birthdate != nil { dataMap["birthdate"] = birthdate.Format(time.RFC3339) } if address != nil { dataMap["address"] = address } if otherProperties != nil { dataMap["other_properties"] = otherProperties } // Validate the data formData := DriversForm{ FirstName: firstName, LastName: lastName, Email: email, Birthdate: birthdate, PhoneNumber: phoneNumber, Address: address, Gender: gender, } validate := formvalidators.New() if err := validate.Struct(formData); err != nil { return "", err } data, err := structpb.NewValue(dataMap) if err != nil { return "", err } request := &mobilityaccounts.UpdateDataRequest{ Account: &mobilityaccounts.Account{ Id: driverID, Namespace: "solidarity_drivers", Data: data.GetStructValue(), }, } resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request) if err != nil { return "", err } return resp.Account.Id, nil } type SolidarityTransportDriverDataResult struct { Driver mobilityaccountsstorage.Account Availabilities []*gen.DriverRegularAvailability Documents []filestorage.FileInfo Bookings []*solidaritytypes.Booking BeneficiariesMap map[string]mobilityaccountsstorage.Account Stats map[string]any WalletBalance float64 } func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Context, driverID string) (*SolidarityTransportDriverDataResult, error) { // Get driver account request := &mobilityaccounts.GetAccountRequest{ Id: driverID, } resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request) if err != nil { return nil, err } // Security check: ensure this is actually a solidarity transport driver account if resp.Account.Namespace != "solidarity_drivers" { return nil, fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, resp.Account.Namespace) } driver := resp.Account.ToStorageType() // Ensure other_properties exists to prevent template errors if driver.Data == nil { driver.Data = make(map[string]interface{}) } if driver.Data["other_properties"] == nil { driver.Data["other_properties"] = make(map[string]interface{}) } // Get availabilities availRequest := &gen.GetDriverRegularAvailabilitiesRequest{ DriverId: driverID, } availResp, err := h.services.GRPC.SolidarityTransport.GetDriverRegularAvailabilities(ctx, availRequest) if err != nil { return nil, err } // Get documents documents := h.filestorage.List(filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS + "/" + driverID) // Get driver bookings bookingsRequest := &gen.GetSolidarityTransportBookingsRequest{ StartDate: timestamppb.New(time.Now().Add(-365 * 24 * time.Hour)), EndDate: timestamppb.New(time.Now().Add(365 * 24 * time.Hour)), Driverid: driverID, } bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, bookingsRequest) protoBookings := []*gen.SolidarityTransportBooking{} if err == nil { protoBookings = bookingsResp.Bookings } // Convert proto bookings to types with geojson.Feature bookings := []*solidaritytypes.Booking{} for _, protoBooking := range protoBookings { booking, err := solidaritytransformers.BookingProtoToType(protoBooking) if err != nil { log.Error().Err(err).Msg("error converting booking proto to type") continue } bookings = append(bookings, booking) } // Collect unique passenger IDs passengerIDs := []string{} passengerIDsMap := make(map[string]bool) for _, booking := range bookings { if booking.PassengerId != "" { if !passengerIDsMap[booking.PassengerId] { passengerIDs = append(passengerIDs, booking.PassengerId) passengerIDsMap[booking.PassengerId] = true } } } // Get beneficiaries in batch beneficiariesMap := make(map[string]mobilityaccountsstorage.Account) if len(passengerIDs) > 0 { beneficiariesResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, &mobilityaccounts.GetAccountsBatchRequest{ Accountids: passengerIDs, }) if err == nil { for _, account := range beneficiariesResp.Accounts { a := account.ToStorageType() beneficiariesMap[a.ID] = a } } } // Calculate stats only for validated bookings validatedCount := 0 kmnb := 0 for _, booking := range bookings { if booking.Status == "VALIDATED" { validatedCount++ if booking.Journey != nil { kmnb += int(booking.Journey.DriverDistance) } } } stats := map[string]any{ "bookings": map[string]any{ "count": validatedCount, "km": kmnb, }, } // Calculate wallet balance like in original handler walletBalance := h.calculateWalletBalance(driver) return &SolidarityTransportDriverDataResult{ Driver: driver, Availabilities: availResp.Results, Documents: documents, Bookings: bookings, BeneficiariesMap: beneficiariesMap, Stats: stats, WalletBalance: walletBalance, }, nil } func (h *ApplicationHandler) ArchiveSolidarityTransportDriver(ctx context.Context, driverID string) error { // Security check: verify the account exists and is a solidarity transport driver getRequest := &mobilityaccounts.GetAccountRequest{ Id: driverID, } getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest) if err != nil { return err } if getResp.Account.Namespace != "solidarity_drivers" { return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace) } data, _ := structpb.NewValue(map[string]any{ "archived": true, }) request := &mobilityaccounts.UpdateDataRequest{ Account: &mobilityaccounts.Account{ Id: driverID, Namespace: "solidarity_drivers", Data: data.GetStructValue(), }, } _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request) return err } func (h *ApplicationHandler) UnarchiveSolidarityTransportDriver(ctx context.Context, driverID string) error { // Security check: verify the account exists and is a solidarity transport driver getRequest := &mobilityaccounts.GetAccountRequest{ Id: driverID, } getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest) if err != nil { return err } if getResp.Account.Namespace != "solidarity_drivers" { return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace) } data, _ := structpb.NewValue(map[string]any{ "archived": false, }) request := &mobilityaccounts.UpdateDataRequest{ Account: &mobilityaccounts.Account{ Id: driverID, Namespace: "solidarity_drivers", Data: data.GetStructValue(), }, } _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request) return err } func (h *ApplicationHandler) AddSolidarityTransportDriverDocument(ctx context.Context, driverID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error { // Security check: verify the account exists and is a solidarity transport driver getRequest := &mobilityaccounts.GetAccountRequest{ Id: driverID, } getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest) if err != nil { return err } if getResp.Account.Namespace != "solidarity_drivers" { return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace) } fileid := uuid.NewString() metadata := map[string]string{ "type": documentType, "name": documentName, } if err := h.filestorage.Put(file, filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS, fmt.Sprintf("%s/%s_%s", driverID, fileid, filename), fileSize, metadata); err != nil { return err } return nil } func (h *ApplicationHandler) GetSolidarityTransportDriverDocument(ctx context.Context, driverID, document string) (io.Reader, *filestorage.FileInfo, error) { return h.GetDocument(ctx, SolidarityDriverDocumentConfig, driverID, document) } func (h *ApplicationHandler) DeleteSolidarityTransportDriverDocument(ctx context.Context, driverID, document string) error { return h.DeleteDocument(ctx, SolidarityDriverDocumentConfig, driverID, document) } func (h *ApplicationHandler) AddSolidarityTransportAvailability(ctx context.Context, driverID, starttime, endtime string, address any, days map[string]bool) error { // Security check: verify the account exists and is a solidarity transport driver getRequest := &mobilityaccounts.GetAccountRequest{ Id: driverID, } getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest) if err != nil { return err } if getResp.Account.Namespace != "solidarity_drivers" { return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace) } availabilities := []*gen.DriverRegularAvailability{} // Convert address to JSON string for the GRPC call addressJSON := "" if address != nil { if addressBytes, err := json.Marshal(address); err == nil { addressJSON = string(addressBytes) } } for day, enabled := range days { if enabled { dayValue := h.getDayValue(day) a := &gen.DriverRegularAvailability{ DriverId: driverID, Day: dayValue, StartTime: starttime, EndTime: endtime, Address: &gen.GeoJsonFeature{ Serialized: addressJSON, }, } availabilities = append(availabilities, a) } } req := &gen.AddDriverRegularAvailabilitiesRequest{ Availabilities: availabilities, } _, err = h.services.GRPC.SolidarityTransport.AddDriverRegularAvailabilities(ctx, req) return err } func (h *ApplicationHandler) getDayValue(day string) int32 { switch day { case "sunday": return Sunday case "monday": return Monday case "tuesday": return Tuesday case "wednesday": return Wednesday case "thursday": return Thursday case "friday": return Friday case "saturday": return Saturday default: return Monday } } func (h *ApplicationHandler) DeleteSolidarityTransportAvailability(ctx context.Context, driverID, availabilityID string) error { req := &gen.DeleteDriverRegularAvailabilityRequest{ DriverId: driverID, AvailabilityId: availabilityID, } _, err := h.services.GRPC.SolidarityTransport.DeleteDriverRegularAvailability(ctx, req) return err } type SolidarityTransportJourneyDataResult struct { Journey *solidaritytypes.DriverJourney Driver mobilityaccountsstorage.Account Passenger mobilityaccountsstorage.Account Beneficiaries []mobilityaccountsstorage.Account PassengerWalletBalance float64 PricingResult map[string]pricing.Price } func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Context, driverID, journeyID, passengerID string, currentUserGroup groupstorage.Group) (*SolidarityTransportJourneyDataResult, error) { // Get journey using the correct API journeyRequest := &gen.GetDriverJourneyRequest{ DriverId: driverID, JourneyId: journeyID, } journeyResp, err := h.services.GRPC.SolidarityTransport.GetDriverJourney(ctx, journeyRequest) if err != nil { return nil, err } // Transform proto to type journey, err := solidaritytransformers.DriverJourneyProtoToType(journeyResp.DriverJourney) if err != nil { return nil, err } // Get driver account driverRequest := &mobilityaccounts.GetAccountRequest{ Id: driverID, } driverResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, driverRequest) if err != nil { return nil, err } // 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 } // Calculate pricing pricingResult, err := h.calculateSolidarityTransportPricing(ctx, journeyResp.DriverJourney, passengerID, passenger) if err != nil { log.Error().Err(err).Msg("error calculating pricing") pricingResult = map[string]pricing.Price{ "passenger": {Amount: 0.0, Currency: "EUR"}, "driver": {Amount: 0.0, Currency: "EUR"}, } } // Get beneficiaries in current user's group beneficiaries, err := h.services.GetBeneficiariesInGroup(currentUserGroup) if err != nil { return nil, fmt.Errorf("could not get beneficiaries: %w", err) } // Calculate passenger wallet balance like in original handler passengerWalletBalance := h.calculateWalletBalance(passenger) return &SolidarityTransportJourneyDataResult{ Journey: journey, Driver: driverResp.Account.ToStorageType(), Passenger: passenger, Beneficiaries: beneficiaries, PassengerWalletBalance: passengerWalletBalance, PricingResult: pricingResult, }, nil } func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context.Context, driverID, journeyID, passengerID, motivation, message string, doNotSend bool, returnWaitingTimeMinutes int) (string, error) { // Get journey for pricing calculation journeyRequest := &gen.GetDriverJourneyRequest{ DriverId: driverID, JourneyId: journeyID, } journeyResp, err := h.services.GRPC.SolidarityTransport.GetDriverJourney(ctx, journeyRequest) if err != nil { return "", err } // Get passenger account for pricing var passenger mobilityaccountsstorage.Account if passengerID != "" { passengerResp, err := h.services.GetAccount(passengerID) if err != nil { return "", fmt.Errorf("could not get passenger account: %w", err) } passenger = passengerResp } // Calculate pricing pricingResult, err := h.calculateSolidarityTransportPricing(ctx, journeyResp.DriverJourney, passengerID, passenger) priceAmount := float64(0) driverCompensation := float64(0) if err == nil { priceAmount = pricingResult["passenger"].Amount driverCompensation = pricingResult["driver"].Amount } // Convert return waiting time from minutes to nanoseconds (time.Duration is in nanoseconds) returnWaitingDuration := int64(returnWaitingTimeMinutes) * int64(time.Minute) // Create booking request bookingRequest := &gen.BookDriverJourneyRequest{ PassengerId: passengerID, DriverId: driverID, DriverJourneyId: journeyID, ReturnWaitingDuration: returnWaitingDuration, PriceAmount: priceAmount, PriceCurrency: "EUR", DriverCompensationAmount: driverCompensation, DriverCompensationCurrency: "EUR", Data: &structpb.Struct{ Fields: map[string]*structpb.Value{ "motivation": structpb.NewStringValue(motivation), "message": structpb.NewStringValue(message), "do_not_send": structpb.NewBoolValue(doNotSend), }, }, } resp, err := h.services.GRPC.SolidarityTransport.BookDriverJourney(ctx, bookingRequest) if err != nil { return "", err } // Send SMS if not disabled if !doNotSend && message != "" { send_message := strings.ReplaceAll(message, "{booking_id}", resp.Booking.Id) if err := h.GenerateSMS(driverID, send_message); err != nil { log.Error().Err(err).Msg("failed to send SMS") } } return resp.Booking.Id, nil } func (h *ApplicationHandler) ToggleSolidarityTransportJourneyNoreturn(ctx context.Context, driverID, journeyID string) error { // Toggle noreturn status updateRequest := &gen.ToggleSolidarityTransportNoreturnRequest{ JourneyId: journeyID, } _, err := h.services.GRPC.SolidarityTransport.ToggleSolidarityTransportNoreturn(ctx, updateRequest) return err } type SolidarityTransportBookingDataResult struct { Booking *solidaritytypes.Booking Driver mobilityaccountsstorage.Account Passenger mobilityaccountsstorage.Account Journey *solidaritytypes.DriverJourney PassengerWalletBalance float64 } func (h *ApplicationHandler) GetSolidarityTransportBookingData(ctx context.Context, bookingID string) (*SolidarityTransportBookingDataResult, error) { // Get booking bookingRequest := &gen.GetSolidarityTransportBookingRequest{ Id: bookingID, } bookingResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBooking(ctx, bookingRequest) if err != nil { return nil, err } booking := bookingResp.Booking // Get driver account driverRequest := &mobilityaccounts.GetAccountRequest{ Id: booking.DriverId, } driverResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, driverRequest) if err != nil { return nil, err } // Get passenger account passengerRequest := &mobilityaccounts.GetAccountRequest{ Id: booking.PassengerId, } passengerResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, passengerRequest) if err != nil { return nil, err } // Transform booking proto to type bookingType, err := solidaritytransformers.BookingProtoToType(booking) if err != nil { return nil, err } // Calculate passenger wallet balance like in original handler passengerWalletBalance := h.calculateWalletBalance(passengerResp.Account.ToStorageType()) return &SolidarityTransportBookingDataResult{ Booking: bookingType, Driver: driverResp.Account.ToStorageType(), Passenger: passengerResp.Account.ToStorageType(), Journey: bookingType.Journey, PassengerWalletBalance: passengerWalletBalance, }, nil } func (h *ApplicationHandler) solidarityDrivers(searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) { request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{"solidarity_drivers"}, } resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) if err != nil { return nil, err } // Create iterator that filters and transforms accounts filteredAccounts := func(yield func(mobilityaccountsstorage.Account) bool) { for _, account := range resp.Accounts { if h.filterSolidarityDriver(account, searchFilter, archivedFilter) { if !yield(account.ToStorageType()) { return } } } } return slices.Collect(filteredAccounts), nil } func (h *ApplicationHandler) filterSolidarityDriver(a *mobilityaccounts.Account, searchFilter string, archivedFilter bool) bool { // Search filter if searchFilter != "" { name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string) if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter)) { return false } } // Archived filter if archivedFilter { if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived { return true } return false } else { if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived { return false } } return true } func (h *ApplicationHandler) pricingGeography(loc *geojson.Feature) pricing.GeographyParams { if loc == nil { return pricing.GeographyParams{} } geo, err := h.services.Geography.GeoSearch(loc) if err != nil { log.Error().Err(err).Msg("issue in geosearch") return pricing.GeographyParams{} } return pricing.GeographyParams{ Location: loc, CityCode: geo["communes"].Properties.MustString("code"), IntercommunalityCode: geo["epci"].Properties.MustString("code"), RegionCode: geo["regions"].Properties.MustString("code"), DepartmentCode: geo["departements"].Properties.MustString("code"), } } func (h *ApplicationHandler) calculateSolidarityTransportPricing(ctx context.Context, journey *gen.SolidarityTransportDriverJourney, passengerID string, passenger mobilityaccountsstorage.Account) (map[string]pricing.Price, error) { // Transform proto to type for geography access journeyType, err := solidaritytransformers.DriverJourneyProtoToType(journey) if err != nil { return nil, err } benefParams := pricing.BeneficiaryParams{} if passengerID == "" { benefParams = pricing.BeneficiaryParams{ Address: h.pricingGeography(journeyType.PassengerPickup), History: 99, Priority: false, } } else { // Get solidarity transport history for passenger solidarity, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{ Passengerid: passengerID, StartDate: timestamppb.New(time.Now().Add(-12 * 730 * time.Hour)), EndDate: timestamppb.New(time.Now().Add(12 * 730 * time.Hour)), }) priority := false if a, ok := passenger.Data["other_properties"]; ok { if b, ok := a.(map[string]any); ok { if c, ok := b["status"]; ok { if p, ok := c.(string); ok { priority = (p == "Prioritaire") } } } } history := 0 if op, ok := passenger.Data["other_properties"]; ok { if op_map, ok := op.(map[string]any); ok { if pst, ok := op_map["previous_solidarity_transport"]; ok { if pst_str, ok := pst.(string); ok { if pst_str != "" { if n, err := strconv.Atoi(pst_str); err == nil { history = history + n } else { log.Error().Err(err).Str("n", pst_str).Msg("string to int conversion error") } } } } } } if err == nil { history = history + len(solidarity.Bookings) } var passengerGeo pricing.GeographyParams if pa, ok := passenger.Data["address"]; ok { jsonpa, err := json.Marshal(pa) if err == nil { passGeojson, err := geojson.UnmarshalFeature(jsonpa) if err == nil { passengerGeo = h.pricingGeography(passGeojson) } } } benefParams = pricing.BeneficiaryParams{ Address: passengerGeo, History: history, Priority: priority, } } pricingResult, err := h.services.Pricing.Prices(pricing.PricingParams{ MobilityType: "solidarity_transport", Beneficiary: benefParams, SharedMobility: pricing.SharedMobilityParams{ DriverDistance: journey.DriverDistance, PassengerDistance: journey.PassengerDistance, Departure: h.pricingGeography(journeyType.PassengerPickup), Destination: h.pricingGeography(journeyType.PassengerDrop), OutwardOnly: journey.Noreturn, }, }) if err != nil { log.Error().Err(err).Msg("error in pricing calculation") return nil, err } return pricingResult, nil } func (h *ApplicationHandler) UpdateSolidarityTransportBookingStatus(ctx context.Context, bookingID, action, reason, message string, notify bool) error { var status string switch action { case "confirm": status = "VALIDATED" case "cancel": status = "CANCELLED" case "waitconfirmation": status = "WAITING_CONFIRMATION" default: return fmt.Errorf("invalid action: %s", action) } // Get booking details BEFORE updating to capture previous status result, err := h.GetSolidarityTransportBookingData(ctx, bookingID) if err != nil { return err } booking := result.Booking driver := result.Driver passenger := result.Passenger previousStatus := booking.Status // Update booking status _, err = h.services.GRPC.SolidarityTransport.UpdateSolidarityTransportBookingStatus(ctx, &gen.UpdateSolidarityTransportBookingStatusRequest{ BookingId: bookingID, NewStatus: status, Reason: reason, }) if err != nil { return fmt.Errorf("update booking status issue: %w", err) } // Handle wallet operations based on status transitions // Credit driver / debit passenger when previous status was not VALIDATED and new status is VALIDATED if previousStatus != "VALIDATED" && status == "VALIDATED" { if message != "" { send_message := strings.ReplaceAll(message, "{booking_id}", bookingID) h.GenerateSMS(passenger.ID, send_message) } if err := h.CreditWallet(ctx, passenger.ID, -1*booking.Journey.Price.Amount, "Transport solidaire", "Débit transport solidaire"); err != nil { return fmt.Errorf("could not debit passenger wallet: %w", err) } if err := h.CreditWallet(ctx, driver.ID, booking.DriverCompensationAmount, "Transport solidaire", "Crédit transport solidaire"); err != nil { return fmt.Errorf("could not credit driver wallet: %w", err) } } // Credit passenger / debit driver when previous status was VALIDATED and new status is not VALIDATED anymore if previousStatus == "VALIDATED" && status != "VALIDATED" { if err := h.CreditWallet(ctx, passenger.ID, booking.Journey.Price.Amount, "Transport solidaire", "Remboursement annulation transport solidaire"); err != nil { return fmt.Errorf("could not credit passenger wallet: %w", err) } if err := h.CreditWallet(ctx, driver.ID, -1*booking.DriverCompensationAmount, "Transport solidaire", "Débit annulation transport solidaire"); err != nil { return fmt.Errorf("could not debit driver wallet: %w", err) } } // Handle notifications for cancelled status if status == "CANCELLED" && notify { // NOTIFY GROUP MEMBERS groupsrequest := &groupsmanagement.GetGroupsRequest{ Namespaces: []string{"parcoursmob_organizations"}, Member: booking.PassengerId, } groupsresp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, groupsrequest) if err != nil { log.Error().Err(err).Msg("") return nil // Don't fail the whole operation for notification issues } if len(groupsresp.Groups) > 0 { members, _, err := h.groupmembers(groupsresp.Groups[0].Id) if err != nil { log.Error().Err(err).Msg("could not retrieve groupe members") } else { for _, m := range members { if email, ok := m.Data["email"].(string); ok { h.emailing.Send("solidarity_transport.booking_driver_decline", email, map[string]string{ "bookingid": booking.Id, "baseUrl": h.config.GetString("base_url"), }) } } } } } return nil }