parcoursmob/core/application/solidarity-transport.go

1439 lines
46 KiB
Go

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