package application import ( "cmp" "context" "encoding/json" "fmt" "io" "slices" "strconv" "strings" "time" formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators" filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage" "git.coopgo.io/coopgo-platform/carpool-service/servers/grpc/proto" 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" "github.com/google/uuid" "github.com/paulmach/orb" "github.com/paulmach/orb/geojson" "github.com/rs/zerolog/log" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) type OrganizedCarpoolDriversForm 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"` FileNumber string `json:"file_number"` Address any `json:"address,omitempty"` AddressDestination any `json:"address_destination,omitempty"` Gender string `json:"gender"` } type OrganizedCarpoolOverviewResult struct { Accounts []mobilityaccountsstorage.Account AccountsMap map[string]mobilityaccountsstorage.Account BeneficiariesMap map[string]mobilityaccountsstorage.Account Bookings []*proto.CarpoolServiceBooking BookingsHistory []*proto.CarpoolServiceBooking } func (h *ApplicationHandler) getOrganizedCarpoolDrivers(ctx context.Context, searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) { request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{"organized_carpool_drivers"}, } resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, 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.filterOrganizedCarpoolDriver(account, searchFilter, archivedFilter) { if !yield(account.ToStorageType()) { return } } } } return slices.Collect(filteredAccounts), nil } func (h *ApplicationHandler) filterOrganizedCarpoolDriver(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 } // filterOrganizedCarpoolBookingsByGeography filters bookings by departure and destination geography func filterOrganizedCarpoolBookingsByGeography(bookings []*proto.CarpoolServiceBooking, departurePolygons, destinationPolygons []orb.Polygon) []*proto.CarpoolServiceBooking { if len(departurePolygons) == 0 && len(destinationPolygons) == 0 { return bookings } filtered := []*proto.CarpoolServiceBooking{} for _, booking := range bookings { includeBooking := true // Check departure filter if provided if len(departurePolygons) > 0 { departureMatch := false pickupPoint := orb.Point{booking.PassengerPickupLng, booking.PassengerPickupLat} departureMatch = isPointInGeographies(pickupPoint, departurePolygons) if !departureMatch { includeBooking = false } } // Check destination filter if provided if len(destinationPolygons) > 0 && includeBooking { destinationMatch := false dropPoint := orb.Point{booking.PassengerDropLng, booking.PassengerDropLat} destinationMatch = isPointInGeographies(dropPoint, destinationPolygons) if !destinationMatch { includeBooking = false } } if includeBooking { filtered = append(filtered, booking) } } return filtered } // filterOrganizedCarpoolBookingsByPassengerAddressGeography filters bookings where passenger address is within geography func filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookings []*proto.CarpoolServiceBooking, beneficiariesMap map[string]mobilityaccountsstorage.Account, addressPolygons []orb.Polygon) []*proto.CarpoolServiceBooking { if len(addressPolygons) == 0 { return bookings } filtered := []*proto.CarpoolServiceBooking{} for _, booking := range bookings { passenger, ok := beneficiariesMap[booking.Passenger.Id] if !ok { continue } // Check if passenger has address - unmarshal as GeoJSON Feature if pa, ok := passenger.Data["address"]; ok { jsonpa, err := json.Marshal(pa) if err == nil { passGeojson, err := geojson.UnmarshalFeature(jsonpa) if err == nil && passGeojson.Geometry != nil { if point, ok := passGeojson.Geometry.(orb.Point); ok { if isPointInGeographies(point, addressPolygons) { filtered = append(filtered, booking) } } } } } } return filtered } func (h *ApplicationHandler) GetOrganizedCarpoolOverview(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) (*OrganizedCarpoolOverviewResult, error) { // Get ALL drivers for the accountsMap (used in bookings display) allDrivers, err := h.getOrganizedCarpoolDrivers(ctx, "", false) if err != nil { log.Error().Err(err).Msg("issue getting all organized carpool 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.getOrganizedCarpoolDrivers(ctx, "", archivedFilter) if err != nil { log.Error().Err(err).Msg("issue getting organized carpool 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 } } 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) } bookingsproto, err := h.services.GRPC.CarpoolService.GetCarpoolBookings(ctx, &proto.GetCarpoolBookingsRequest{ MinDate: timestamppb.New(startdate), MaxDate: timestamppb.New(enddate), }) if err != nil { log.Error().Err(err).Msg("issue retrieving bookings") } bookings := []*proto.CarpoolServiceBooking{} if err == nil { for _, b := range bookingsproto.Bookings { // Apply driver filter if specified if driverID != "" && b.Driver.Id != driverID { continue } // Apply status filter if specified if status != "" && b.Status.String() != status { continue } bookings = append(bookings, b) } } // 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") } } bookings = filterOrganizedCarpoolBookingsByGeography(bookings, departurePolygons, destinationPolygons) bookings = filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookings, beneficiariesMap, passengerAddressPolygons) // 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) } historyBookingsproto, err := h.services.GRPC.CarpoolService.GetCarpoolBookings(ctx, &proto.GetCarpoolBookingsRequest{ MinDate: timestamppb.New(histStartdate), MaxDate: timestamppb.New(histEnddate), }) bookingsHistory := []*proto.CarpoolServiceBooking{} if err == nil { for _, b := range historyBookingsproto.Bookings { // Apply driver filter if specified if histDriverID != "" && b.Driver.Id != histDriverID { continue } // Apply status filter if specified if histStatus != "" && b.Status.String() != histStatus { continue } bookingsHistory = append(bookingsHistory, b) } } // 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") } } bookingsHistory = filterOrganizedCarpoolBookingsByGeography(bookingsHistory, histDeparturePolygons, histDestinationPolygons) bookingsHistory = filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookingsHistory, beneficiariesMap, histPassengerAddressPolygons) // 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) }) slices.SortFunc(bookings, func(a, b *proto.CarpoolServiceBooking) int { return cmp.Compare(a.PassengerPickupDate.AsTime().Unix(), b.PassengerPickupDate.AsTime().Unix()) }) // Sort history bookings by date (most recent first) slices.SortFunc(bookingsHistory, func(a, b *proto.CarpoolServiceBooking) int { return cmp.Compare(b.PassengerPickupDate.AsTime().Unix(), a.PassengerPickupDate.AsTime().Unix()) }) return &OrganizedCarpoolOverviewResult{ Accounts: accounts, AccountsMap: accountsMap, BeneficiariesMap: beneficiariesMap, Bookings: bookings, BookingsHistory: bookingsHistory, }, nil } type OrganizedCarpoolBookingDataResult struct { Booking *proto.CarpoolServiceBooking Driver mobilityaccountsstorage.Account Passenger mobilityaccountsstorage.Account DriverDepartureAddress string DriverArrivalAddress string } func (h *ApplicationHandler) GetOrganizedCarpoolBookingData(ctx context.Context, bookingID string) (*OrganizedCarpoolBookingDataResult, error) { resp, err := h.services.GRPC.CarpoolService.GetBooking(ctx, &proto.GetCarpoolBookingRequest{ BookingId: bookingID, }) if err != nil { return nil, fmt.Errorf("could not get carpool booking: %w", err) } if resp.Booking == nil { return nil, fmt.Errorf("carpool booking not found") } driver, err := h.services.GetAccount(resp.Booking.Driver.Id) if err != nil { return nil, fmt.Errorf("driver retrieval issue: %w", err) } passenger, err := h.services.GetAccount(resp.Booking.Passenger.Id) if err != nil { return nil, fmt.Errorf("passenger retrieval issue: %w", err) } // Extract driver departure and arrival addresses from DriverRoute GeoJSON var driverDepartureAddress, driverArrivalAddress string if resp.Booking.DriverRoute != nil && resp.Booking.DriverRoute.Serialized != "" { fc, err := geojson.UnmarshalFeatureCollection([]byte(resp.Booking.DriverRoute.Serialized)) if err != nil { log.Error().Err(err).Msg("could not unmarshal driver route geojson") } else { // Extract departure address (first feature) if len(fc.Features) > 0 { if addr, ok := fc.Features[0].Properties["label"]; ok { if addrStr, ok := addr.(string); ok { driverDepartureAddress = addrStr } } } // Extract arrival address (last feature) if len(fc.Features) > 1 { if addr, ok := fc.Features[1].Properties["label"]; ok { if addrStr, ok := addr.(string); ok { driverArrivalAddress = addrStr } } } } } return &OrganizedCarpoolBookingDataResult{ Booking: resp.Booking, Driver: driver, Passenger: passenger, DriverDepartureAddress: driverDepartureAddress, DriverArrivalAddress: driverArrivalAddress, }, nil } func (h *ApplicationHandler) UpdateOrganizedCarpoolBookingStatus(ctx context.Context, bookingID, action string) error { var status proto.CarpoolServiceBookingStatus if action == "confirm" { status = proto.CarpoolServiceBookingStatus_CONFIRMED } else if action == "cancel" { status = proto.CarpoolServiceBookingStatus_CANCELLED } else if action == "waitconfirmation" { status = proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION } else { return fmt.Errorf("unknown booking action: %s", action) } // Get booking details BEFORE updating to capture previous status result, err := h.GetOrganizedCarpoolBookingData(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.CarpoolService.UpdateBooking(ctx, &proto.UpdateCarpoolBookingRequest{ BookingId: bookingID, Status: status, }) if err != nil { return fmt.Errorf("update carpool booking status issue: %w", err) } // Handle wallet operations based on status transitions // Credit driver / debit passenger when previous status was not CONFIRMED and new status is CONFIRMED if previousStatus != proto.CarpoolServiceBookingStatus_CONFIRMED && status == proto.CarpoolServiceBookingStatus_CONFIRMED { if booking.Price != nil && booking.Price.Amount != nil { if err := h.CreditWallet(ctx, passenger.ID, -1*(*booking.Price.Amount), "Covoiturage solidaire", "Débit covoiturage solidaire"); err != nil { return fmt.Errorf("could not debit passenger wallet: %w", err) } } if booking.DriverCompensationAmount != nil && *booking.DriverCompensationAmount > 0 { if err := h.CreditWallet(ctx, driver.ID, *booking.DriverCompensationAmount, "Covoiturage solidaire", "Crédit covoiturage solidaire"); err != nil { return fmt.Errorf("could not credit driver wallet: %w", err) } } } // Credit passenger / debit driver when previous status was CONFIRMED and new status is not CONFIRMED anymore if previousStatus == proto.CarpoolServiceBookingStatus_CONFIRMED && status != proto.CarpoolServiceBookingStatus_CONFIRMED { if booking.Price != nil && booking.Price.Amount != nil { if err := h.CreditWallet(ctx, passenger.ID, *booking.Price.Amount, "Covoiturage solidaire", "Remboursement annulation covoiturage solidaire"); err != nil { return fmt.Errorf("could not credit passenger wallet: %w", err) } } if booking.DriverCompensationAmount != nil && *booking.DriverCompensationAmount > 0 { if err := h.CreditWallet(ctx, driver.ID, -1*(*booking.DriverCompensationAmount), "Covoiturage solidaire", "Débit annulation covoiturage solidaire"); err != nil { return fmt.Errorf("could not debit driver wallet: %w", err) } } } return nil } func (h *ApplicationHandler) CreateOrganizedCarpoolDriver(ctx context.Context, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address, addressDestination any, gender string) (string, error) { dataMap := map[string]any{ "first_name": firstName, "last_name": lastName, "email": email, "phone_number": phoneNumber, "file_number": fileNumber, "gender": gender, } // Convert birthdate to string format for structpb compatibility if birthdate != nil { dataMap["birthdate"] = birthdate.Format("2006-01-02") } if address != nil { dataMap["address"] = address } if addressDestination != nil { dataMap["address_destination"] = addressDestination } // Validate the data formData := OrganizedCarpoolDriversForm{ FirstName: firstName, LastName: lastName, Email: email, Birthdate: birthdate, PhoneNumber: phoneNumber, FileNumber: fileNumber, Address: address, AddressDestination: addressDestination, 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: "organized_carpool_drivers", Data: data.GetStructValue(), }, } resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, request) if err != nil { return "", err } return resp.Account.Id, nil } type OrganizedCarpoolDriverDataResult struct { Driver mobilityaccountsstorage.Account Trips []*geojson.FeatureCollection Documents []filestorage.FileInfo Bookings any BeneficiariesMap map[string]mobilityaccountsstorage.Account Stats map[string]any WalletBalance float64 } func (h *ApplicationHandler) GetOrganizedCarpoolDriverData(ctx context.Context, driverID string) (*OrganizedCarpoolDriverDataResult, error) { documents := h.filestorage.List(filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS + "/" + driverID) driver, err := h.services.GetAccount(driverID) if err != nil { return nil, fmt.Errorf("issue retrieving driver account: %w", err) } // Security check: ensure this is actually an organized carpool driver account if driver.Namespace != "organized_carpool_drivers" { return nil, fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace) } trips := []*geojson.FeatureCollection{} resp, err := h.services.GRPC.CarpoolService.GetRegularRoutes(ctx, &proto.GetRegularRoutesRequest{ UserId: driverID, }) for _, r := range resp.Routes { t, err := geojson.UnmarshalFeatureCollection([]byte(r.Serialized)) if err != nil { log.Error().Err(err).Msg("could not unmarshall feature collection") continue } trips = append(trips, t) } // Get driver bookings bookingsRequest := &proto.GetUserBookingsRequest{ UserId: driverID, MinDate: timestamppb.New(time.Now().Add(-365 * 24 * time.Hour)), MaxDate: timestamppb.New(time.Now().Add(365 * 24 * time.Hour)), } bookingsResp, err := h.services.GRPC.CarpoolService.GetUserBookings(ctx, bookingsRequest) bookings := []*proto.CarpoolServiceBooking{} if err == nil { bookings = bookingsResp.Bookings } // Collect unique passenger IDs passengerIDs := []string{} passengerIDsMap := make(map[string]bool) for _, booking := range bookings { if booking.Passenger != nil && booking.Passenger.Id != "" { if !passengerIDsMap[booking.Passenger.Id] { passengerIDs = append(passengerIDs, booking.Passenger.Id) passengerIDsMap[booking.Passenger.Id] = 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 confirmedCount := 0 kmnb := 0 for _, booking := range bookings { if booking.Status.String() == "CONFIRMED" { confirmedCount++ if booking.Distance != nil { kmnb += int(*booking.Distance) } } } stats := map[string]any{ "bookings": map[string]any{ "count": len(bookings), "confirmed": confirmedCount, "km": kmnb, }, } // Calculate wallet balance walletBalance := h.calculateWalletBalance(driver) return &OrganizedCarpoolDriverDataResult{ Driver: driver, Trips: trips, Documents: documents, Bookings: bookings, BeneficiariesMap: beneficiariesMap, Stats: stats, WalletBalance: walletBalance, }, nil } type OrganizedCarpoolDriverResult struct { Driver mobilityaccountsstorage.Account } func (h *ApplicationHandler) GetOrganizedCarpoolDriver(ctx context.Context, driverID string) (*OrganizedCarpoolDriverResult, error) { driver, err := h.services.GetAccount(driverID) if err != nil { return nil, fmt.Errorf("issue retrieving driver account: %w", err) } // Security check: ensure this is actually an organized carpool driver account if driver.Namespace != "organized_carpool_drivers" { return nil, fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace) } return &OrganizedCarpoolDriverResult{ Driver: driver, }, nil } func (h *ApplicationHandler) UpdateOrganizedCarpoolDriver(ctx context.Context, driverID, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address, addressDestination any, gender, otherProperties string) (string, error) { // Security check: verify the account exists and is an organized carpool driver driver, err := h.services.GetAccount(driverID) if err != nil { return "", fmt.Errorf("issue retrieving driver account: %w", err) } if driver.Namespace != "organized_carpool_drivers" { return "", fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace) } dataMap := map[string]any{ "first_name": firstName, "last_name": lastName, "email": email, "phone_number": phoneNumber, "file_number": fileNumber, "gender": gender, } // Convert birthdate to string format for structpb compatibility if birthdate != nil { dataMap["birthdate"] = birthdate.Format("2006-01-02") } if address != nil { dataMap["address"] = address } if addressDestination != nil { dataMap["address_destination"] = addressDestination } // Handle other_properties for update form if otherProperties != "" { var otherProps map[string]any if err := json.Unmarshal([]byte(otherProperties), &otherProps); err == nil { if dataMap["other_properties"] == nil { dataMap["other_properties"] = make(map[string]any) } for k, v := range otherProps { dataMap["other_properties"].(map[string]any)[k] = v } } } // Validate the data formData := OrganizedCarpoolDriversForm{ FirstName: firstName, LastName: lastName, Email: email, Birthdate: birthdate, PhoneNumber: phoneNumber, FileNumber: fileNumber, Address: address, AddressDestination: addressDestination, 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: "organized_carpool_drivers", Data: data.GetStructValue(), }, } resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request) if err != nil { return "", err } return resp.Account.Id, nil } func (h *ApplicationHandler) ArchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error { // Security check: verify the account exists and is an organized carpool driver driver, err := h.services.GetAccount(driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } if driver.Namespace != "organized_carpool_drivers" { return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace) } data, err := structpb.NewValue(map[string]any{ "archived": true, }) if err != nil { return err } request := &mobilityaccounts.UpdateDataRequest{ Account: &mobilityaccounts.Account{ Id: driverID, Namespace: "organized_carpool_drivers", Data: data.GetStructValue(), }, } _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request) return err } func (h *ApplicationHandler) UnarchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error { // Security check: verify the account exists and is an organized carpool driver driver, err := h.services.GetAccount(driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } if driver.Namespace != "organized_carpool_drivers" { return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace) } data, err := structpb.NewValue(map[string]any{ "archived": false, }) if err != nil { return err } request := &mobilityaccounts.UpdateDataRequest{ Account: &mobilityaccounts.Account{ Id: driverID, Namespace: "organized_carpool_drivers", Data: data.GetStructValue(), }, } _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request) return err } func (h *ApplicationHandler) AddOrganizedCarpoolDriverDocument(ctx context.Context, driverID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error { // Security check: verify the account exists and is an organized carpool driver driver, err := h.services.GetAccount(driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } if driver.Namespace != "organized_carpool_drivers" { return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace) } fileid := uuid.NewString() metadata := map[string]string{ "type": documentType, "name": documentName, } if err := h.filestorage.Put(file, filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS, fmt.Sprintf("%s/%s_%s", driverID, fileid, filename), fileSize, metadata); err != nil { return err } return nil } func (h *ApplicationHandler) GetOrganizedCarpoolDriverDocument(ctx context.Context, driverID, document string) (io.Reader, *filestorage.FileInfo, error) { return h.GetDocument(ctx, OrganizedCarpoolDriverDocumentConfig, driverID, document) } func (h *ApplicationHandler) DeleteOrganizedCarpoolDriverDocument(ctx context.Context, driverID, document string) error { return h.DeleteDocument(ctx, OrganizedCarpoolDriverDocumentConfig, driverID, document) } func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driverID, outwardtime, returntime string, departure, destination *geojson.Feature, days map[string]bool) error { // Security check: verify the account exists and is an organized carpool driver driver, err := h.services.GetAccount(driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } if driver.Namespace != "organized_carpool_drivers" { return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace) } trips := []*proto.CarpoolFeatureCollection{} outwardroute, err := h.services.Routing.Route([]orb.Point{departure.Point(), destination.Point()}) if err != nil { return fmt.Errorf("failed calling route search: %w", err) } returnroute, err := h.services.Routing.Route([]orb.Point{destination.Point(), departure.Point()}) if err != nil { return fmt.Errorf("failed calling route search: %w", err) } outwardschedules := []map[string]any{} returnschedules := []map[string]any{} dayMap := map[string]string{ "monday": "MON", "tuesday": "TUE", "wednesday": "WED", "thursday": "THU", "friday": "FRI", "saturday": "SAT", "sunday": "SUN", } for day, enabled := range days { if enabled { dayCode := dayMap[day] outwardschedules = append(outwardschedules, map[string]any{ "day": dayCode, "time_of_day": outwardtime, }) returnschedules = append(returnschedules, map[string]any{ "day": dayCode, "time_of_day": returntime, }) } } outward_fc := geojson.NewFeatureCollection() outward_fc.Append(departure) outward_fc.Append(destination) outward_fc.ExtraMembers = geojson.Properties{} outward_fc.ExtraMembers["properties"] = map[string]any{ "is_driver": true, "is_passenger": false, "user": mobilityaccountsstorage.Account{ ID: driverID, }, "polyline": outwardroute.Summary.Polyline, "schedules": outwardschedules, "driver_options": map[string]any{}, "passenger_options": map[string]any{}, } outwardtrip, err := outward_fc.MarshalJSON() if err != nil { return fmt.Errorf("failed marshaling outward geojson: %w", err) } return_fc := geojson.NewFeatureCollection() return_fc.Append(destination) return_fc.Append(departure) return_fc.ExtraMembers = geojson.Properties{} return_fc.ExtraMembers["properties"] = map[string]any{ "is_driver": true, "is_passenger": false, "user": mobilityaccountsstorage.Account{ ID: driverID, }, "polyline": returnroute.Summary.Polyline, "schedules": returnschedules, "driver_options": map[string]any{}, "passenger_options": map[string]any{}, } returntrip, err := return_fc.MarshalJSON() if err != nil { return fmt.Errorf("failed marshaling return geojson: %w", err) } trips = append(trips, &proto.CarpoolFeatureCollection{ Serialized: string(outwardtrip), }) trips = append(trips, &proto.CarpoolFeatureCollection{ Serialized: string(returntrip), }) req := &proto.CreateRegularRoutesRequest{ Routes: trips, } _, err = h.services.GRPC.CarpoolService.CreateRegularRoutes(ctx, req) if err != nil { return fmt.Errorf("could not create regular routes: %w", err) } return nil } func (h *ApplicationHandler) DeleteOrganizedCarpoolTrip(ctx context.Context, tripID string) error { req := &proto.DeleteRegularRoutesRequest{ Ids: []string{tripID}, } _, err := h.services.GRPC.CarpoolService.DeleteRegularRoutes(ctx, req) if err != nil { return fmt.Errorf("could not delete regular routes: %w", err) } return nil } type OrganizedCarpoolJourneyDataResult struct { Journey *geojson.FeatureCollection Driver mobilityaccountsstorage.Account Passenger mobilityaccountsstorage.Account Beneficiaries []mobilityaccountsstorage.Account PassengerWalletBalance float64 PricingResult map[string]pricing.Price } func (h *ApplicationHandler) GetOrganizedCarpoolJourneyData(ctx context.Context, driverID, journeyID, passengerID string, currentUserGroup groupstorage.Group) (*OrganizedCarpoolJourneyDataResult, error) { // Get the planned trip data journeyResp, err := h.services.GRPC.CarpoolService.GetPlannedTrip(ctx, &proto.GetPlannedTripRequest{ Id: journeyID, }) if err != nil { return nil, fmt.Errorf("could not get carpool journey: %w", err) } journey, err := geojson.UnmarshalFeatureCollection([]byte(journeyResp.PlannedTrip.Serialized)) if err != nil { return nil, fmt.Errorf("could not unmarshal carpool journey: %w", err) } driver, err := h.services.GetAccount(driverID) if err != nil { return nil, fmt.Errorf("could not get driver: %w", err) } var passenger mobilityaccountsstorage.Account if passengerID != "" { passenger, err = h.services.GetAccount(passengerID) if err != nil { return nil, fmt.Errorf("could not get passenger account: %w", err) } } // 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 pricing pricingResult, err := h.calculateOrganizedCarpoolPricing(ctx, journey, passengerID, passenger) if err != nil { log.Error().Err(err).Msg("error calculating organized carpool pricing") pricingResult = map[string]pricing.Price{ "passenger": {Amount: 0.0, Currency: "EUR"}, "driver": {Amount: 0.0, Currency: "EUR"}, } } // Calculate passenger wallet balance passengerWalletBalance := h.calculateWalletBalance(passenger) return &OrganizedCarpoolJourneyDataResult{ Journey: journey, Driver: driver, Passenger: passenger, Beneficiaries: beneficiaries, PassengerWalletBalance: passengerWalletBalance, PricingResult: pricingResult, }, nil } func (h *ApplicationHandler) CreateOrganizedCarpoolJourneyBooking(ctx context.Context, driverID, journeyID, passengerID, motivation, message string, doNotSend bool) (string, error) { if passengerID == "" { return "", fmt.Errorf("missing passenger ID for carpool booking") } // Get the planned trip data journeyResp, err := h.services.GRPC.CarpoolService.GetPlannedTrip(ctx, &proto.GetPlannedTripRequest{ Id: journeyID, }) if err != nil { return "", fmt.Errorf("could not get carpool journey: %w", err) } journey, err := geojson.UnmarshalFeatureCollection([]byte(journeyResp.PlannedTrip.Serialized)) if err != nil { return "", fmt.Errorf("could not unmarshal carpool journey: %w", err) } departureDate := journey.ExtraMembers["departure_date"] var departureTime *timestamppb.Timestamp if departureDate != nil { if dd, ok := departureDate.(string); ok { dt, _ := time.Parse("2006-01-02 15:04", dd) departureTime = timestamppb.New(dt) } } // Extract operator from journey data var operatorID string if operator, ok := journey.ExtraMembers["operator"]; ok { if op, ok := operator.(string); ok { operatorID = op } } if operatorID == "" { operatorID = "example.coopgo.fr" // fallback to default } if departureTime == nil { // Fallback to current time if we can't extract departure time departureTime = timestamppb.New(time.Now()) } // Extract journey properties from the geojson data var journeyProps map[string]any if props, ok := journey.ExtraMembers["properties"]; ok { if propsMap, ok := props.(map[string]any); ok { journeyProps = propsMap } } if journeyProps == nil { return "", fmt.Errorf("could not extract journey properties") } // Extract departure date from journey ExtraMembers if depDate, ok := journey.ExtraMembers["departure_date"]; ok { if depDateStr, ok := depDate.(string); ok { if parsedTime, err := time.Parse("2006-01-02T15:04:05Z", depDateStr); err == nil { departureTime = timestamppb.New(parsedTime) } else if parsedTime, err := time.Parse("2006-01-02", depDateStr); err == nil { departureTime = timestamppb.New(parsedTime) } else { log.Warn().Str("departure_date", depDateStr).Msg("could not parse departure date, using current time") } } } // Extract passenger pickup/drop coordinates and addresses from stored passenger data var pickupLat, pickupLng, dropLat, dropLng float64 var pickupAddress, dropAddress string // Check if we have passenger pickup and drop features in the journey's extra members if pickupData, ok := journey.ExtraMembers["passenger_pickup"]; ok { if pickupMap, ok := pickupData.(map[string]interface{}); ok { if geometry, ok := pickupMap["geometry"].(map[string]interface{}); ok { if coords, ok := geometry["coordinates"].([]interface{}); ok && len(coords) >= 2 { if lng, ok := coords[0].(float64); ok { pickupLng = lng } if lat, ok := coords[1].(float64); ok { pickupLat = lat } } } if properties, ok := pickupMap["properties"].(map[string]interface{}); ok { if label, ok := properties["label"].(string); ok { pickupAddress = label } } } } if dropData, ok := journey.ExtraMembers["passenger_drop"]; ok { if dropMap, ok := dropData.(map[string]interface{}); ok { if geometry, ok := dropMap["geometry"].(map[string]interface{}); ok { if coords, ok := geometry["coordinates"].([]interface{}); ok && len(coords) >= 2 { if lng, ok := coords[0].(float64); ok { dropLng = lng } if lat, ok := coords[1].(float64); ok { dropLat = lat } } } if properties, ok := dropMap["properties"].(map[string]interface{}); ok { if label, ok := properties["label"].(string); ok { dropAddress = label } } } } // Extract time from schedules if available and no specific departure_date was found if departureTime.AsTime().Equal(time.Now().Truncate(time.Second)) { if schedules, ok := journeyProps["schedules"]; ok { if schedulesList, ok := schedules.([]any); ok && len(schedulesList) > 0 { if schedule, ok := schedulesList[0].(map[string]any); ok { if timeOfDay, ok := schedule["time_of_day"].(string); ok { // Parse time and combine with current date now := time.Now() timeStr := fmt.Sprintf("%s %s", now.Format("2006-01-02"), timeOfDay) if depTime, err := time.Parse("2006-01-02 15:04", timeStr); err == nil { departureTime = timestamppb.New(depTime) } } } } } } // Get passenger account and calculate pricing var passenger mobilityaccountsstorage.Account if passengerID != "" { passenger, err = h.services.GetAccount(passengerID) if err != nil { return "", fmt.Errorf("could not get passenger account: %w", err) } } pricingResult, err := h.calculateOrganizedCarpoolPricing(ctx, journey, passengerID, passenger) if err != nil { log.Error().Err(err).Msg("error calculating organized carpool pricing") pricingResult = map[string]pricing.Price{ "passenger": {Amount: 0.0, Currency: "EUR"}, "driver": {Amount: 0.0, Currency: "EUR"}, } } // Extract price values priceAmount := pricingResult["passenger"].Amount priceCurrency := pricingResult["passenger"].Currency driverCompensationAmount := pricingResult["driver"].Amount driverCompensationCurrency := pricingResult["driver"].Currency // Determine price type priceType := proto.CarpoolServicePriceType_PAYING if priceAmount == 0 { priceType = proto.CarpoolServicePriceType_FREE } // Extract passenger distance from journey var passengerDistance *int64 if dist, ok := journey.ExtraMembers["passenger_distance"].(float64); ok { distInt := int64(dist) passengerDistance = &distInt } // Create carpool booking using extracted journey data booking := &proto.CarpoolServiceBooking{ Id: uuid.NewString(), Driver: &proto.CarpoolServiceUser{ Id: driverID, Operator: operatorID, }, Passenger: &proto.CarpoolServiceUser{ Id: passengerID, Operator: operatorID, }, PassengerPickupDate: departureTime, PassengerPickupLat: pickupLat, PassengerPickupLng: pickupLng, PassengerDropLat: dropLat, PassengerDropLng: dropLng, PassengerPickupAddress: &pickupAddress, PassengerDropAddress: &dropAddress, Status: proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION, Distance: passengerDistance, DriverJourneyId: journeyID, Price: &proto.CarpoolServicePrice{ Type: &priceType, Amount: &priceAmount, Currency: &priceCurrency, }, DriverCompensationAmount: &driverCompensationAmount, DriverCompensationCurrency: &driverCompensationCurrency, Motivation: &motivation, } bookingRes, err := h.services.GRPC.CarpoolService.CreateBooking(ctx, &proto.CreateCarpoolBookingRequest{ Booking: booking, }) if err != nil { return "", fmt.Errorf("cannot create carpool booking: %w", err) } // Send SMS notification if requested if message != "" && !doNotSend { send_message := strings.ReplaceAll(message, "{booking_id}", bookingRes.Booking.Id) log.Debug().Str("message", send_message).Msg("Carpool booking created: sending message") h.GenerateSMS(driverID, send_message) } return bookingRes.Booking.Id, nil } func (h *ApplicationHandler) calculateOrganizedCarpoolPricing(ctx context.Context, tripGeoJSON *geojson.FeatureCollection, passengerID string, passenger mobilityaccountsstorage.Account) (map[string]pricing.Price, error) { // For organized carpool, use simple pricing based on distance // Extract distance from journey features if available var passengerDistance int64 = 0 var driverDistance int64 = 0 // Try to extract distance from journey extra members if tripGeoJSON != nil && tripGeoJSON.ExtraMembers != nil { if dist, ok := tripGeoJSON.ExtraMembers["passenger_distance"].(float64); ok { passengerDistance = int64(dist) } if dist, ok := tripGeoJSON.ExtraMembers["driver_distance"].(float64); ok { driverDistance = int64(dist) } } benefParams := pricing.BeneficiaryParams{} if passengerID == "" { benefParams = pricing.BeneficiaryParams{ History: 99, Priority: false, } } else { // Get organized carpool history for passenger carpoolBookings, err := h.services.GRPC.CarpoolService.GetUserBookings(ctx, &proto.GetUserBookingsRequest{ UserId: passengerID, }) // Check priority status 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") } } } } // Calculate history from previous bookings and stored value history := 0 if op, ok := passenger.Data["other_properties"]; ok { if op_map, ok := op.(map[string]any); ok { if poc, ok := op_map["previous_organized_carpool"]; ok { if poc_str, ok := poc.(string); ok { if poc_str != "" { if n, err := strconv.Atoi(poc_str); err == nil { history = history + n } else { log.Error().Err(err).Str("n", poc_str).Msg("string to int conversion error") } } } } } } // Count only WAITING_DRIVER_CONFIRMATION, CONFIRMED and VALIDATED bookings if err == nil { for _, booking := range carpoolBookings.Bookings { if booking.Status == proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION || booking.Status == proto.CarpoolServiceBookingStatus_CONFIRMED || booking.Status == proto.CarpoolServiceBookingStatus_VALIDATED { history++ } } } benefParams = pricing.BeneficiaryParams{ History: history, Priority: priority, } } pricingParams := pricing.PricingParams{ MobilityType: "organized_carpool", Beneficiary: benefParams, SharedMobility: pricing.SharedMobilityParams{ DriverDistance: driverDistance, PassengerDistance: passengerDistance, }, } log.Info(). Str("mobility_type", pricingParams.MobilityType). Int("beneficiary_history", pricingParams.Beneficiary.History). Bool("beneficiary_priority", pricingParams.Beneficiary.Priority). Int64("driver_distance", pricingParams.SharedMobility.DriverDistance). Int64("passenger_distance", pricingParams.SharedMobility.PassengerDistance). Str("passenger_id", passengerID). Msg("calling pricing service for organized carpool") pricingResult, err := h.services.Pricing.Prices(pricingParams) if err != nil { log.Error().Err(err).Msg("pricing service returned error") return nil, err } log.Info(). Float64("passenger_price", pricingResult["passenger"].Amount). Str("passenger_currency", pricingResult["passenger"].Currency). Float64("driver_price", pricingResult["driver"].Amount). Str("driver_currency", pricingResult["driver"].Currency). Msg("pricing service result for organized carpool") return pricingResult, nil } type OrganizedCarpoolBookingsResult struct { Bookings []*proto.CarpoolServiceBooking DriversMap map[string]mobilityaccountsstorage.Account BeneficiariesMap map[string]mobilityaccountsstorage.Account } func (h *ApplicationHandler) GetOrganizedCarpoolBookings(ctx context.Context, startDate, endDate *time.Time, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode string) (*OrganizedCarpoolBookingsResult, error) { // Get all drivers drivers, err := h.getOrganizedCarpoolDrivers(ctx, "", false) if err != nil { log.Error().Err(err).Msg("issue getting organized carpool 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 { start = time.Now().Add(-365 * 24 * time.Hour) } if endDate != nil { end = *endDate } else { end = time.Now() } // Get bookings from gRPC service request := &proto.GetCarpoolBookingsRequest{ MinDate: timestamppb.New(start), MaxDate: timestamppb.New(end), } resp, err := h.services.GRPC.CarpoolService.GetCarpoolBookings(ctx, request) if err != nil { return nil, err } bookings := []*proto.CarpoolServiceBooking{} for _, b := range resp.Bookings { // Apply driver filter if specified if driverID != "" && b.Driver.Id != driverID { continue } // Apply status filter if specified if status != "" && b.Status.String() != status { continue } bookings = append(bookings, b) } // 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") } } 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") } } bookings = filterOrganizedCarpoolBookingsByGeography(bookings, departurePolygons, destinationPolygons) bookings = filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookings, beneficiariesMap, passengerAddressPolygons) // Sort by date (most recent first) slices.SortFunc(bookings, func(a, b *proto.CarpoolServiceBooking) int { return cmp.Compare(b.PassengerPickupDate.AsTime().Unix(), a.PassengerPickupDate.AsTime().Unix()) }) return &OrganizedCarpoolBookingsResult{ Bookings: bookings, DriversMap: driversMap, BeneficiariesMap: beneficiariesMap, }, nil }