1439 lines
46 KiB
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
|
|
}
|