1482 lines
49 KiB
Go
1482 lines
49 KiB
Go
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
|
|
}
|