From 723c12a6573d710fd71a385ae5dc44c8b4545477 Mon Sep 17 00:00:00 2001 From: Arnaud Delcasse Date: Wed, 8 Oct 2025 09:01:37 +0200 Subject: [PATCH] improvements --- .gitignore | 1 + go.mod | 8 +- go.sum | 6 +- handler/bookings.go | 20 +- handler/journeys_test.go | 335 ++++++++++++++++++ .../gen/solidarity-transport-types.pb.go | 44 ++- .../grpc/proto/gen/solidarity-transport.pb.go | 42 ++- .../proto/solidarity-transport-types.proto | 2 + servers/grpc/proto/solidarity-transport.proto | 2 + servers/grpc/server/bookings.go | 4 +- servers/grpc/transformers/bookings.go | 36 +- storage/mock.go | 279 +++++++++++++++ storage/storage.go | 2 + types/booking.go | 16 +- 14 files changed, 733 insertions(+), 64 deletions(-) create mode 100644 handler/journeys_test.go create mode 100644 storage/mock.go diff --git a/.gitignore b/.gitignore index 5b6b072..4ad042c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ config.yaml +solidarity-transport diff --git a/go.mod b/go.mod index ae7a1ba..3462a02 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,19 @@ go 1.23.3 //replace git.coopgo.io/coopgo-platform/routing-service => ../../coopgo-platform/routing-service/ require ( + git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536 github.com/google/uuid v1.6.0 + github.com/paulmach/orb v0.11.1 github.com/rs/zerolog v1.33.0 github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 go.mongodb.org/mongo-driver/v2 v2.1.0 google.golang.org/grpc v1.70.0 google.golang.org/protobuf v1.36.6 ) require ( - git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -23,14 +26,15 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/twpayne/go-polyline v1.1.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/go.sum b/go.sum index a04a0c1..2b75df2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -git.coopgo.io/coopgo-platform/routing-service v0.0.0-20240919052617-d03cd410081c h1:I0pJtlpW7Eloiro+VXVRzdQYJW9AMmjNIczBlROCX9Y= -git.coopgo.io/coopgo-platform/routing-service v0.0.0-20240919052617-d03cd410081c/go.mod h1:Nh7o15LlV0OuO9zxvJIs9FlelpeAaLYkXtFdgIkFrgg= git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536 h1:SllXX1VJXulfhNi+Pd0R9chksm8zO6gkWcTQ/uSMsdc= git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536/go.mod h1:Nh7o15LlV0OuO9zxvJIs9FlelpeAaLYkXtFdgIkFrgg= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -86,6 +84,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -97,6 +96,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/twpayne/go-polyline v1.1.1 h1:/tSF1BR7rN4HWj4XKqvRUNrCiYVMCvywxTFVofvDV0w= github.com/twpayne/go-polyline v1.1.1/go.mod h1:ybd9IWWivW/rlXPXuuckeKUyF3yrIim+iqA7kSl4NFY= @@ -197,8 +197,6 @@ google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/handler/bookings.go b/handler/bookings.go index 80e5abf..401be8d 100644 --- a/handler/bookings.go +++ b/handler/bookings.go @@ -9,7 +9,7 @@ import ( "github.com/rs/zerolog/log" ) -func (h Handler) BookDriverJourney(passengerid string, driverid string, journeyid string, returnWaitingDuration time.Duration, priceAmount float64, priceCurrency string, data map[string]any) (*types.Booking, error) { +func (h Handler) BookDriverJourney(passengerid string, driverid string, journeyid string, returnWaitingDuration time.Duration, priceAmount float64, priceCurrency string, driverCompensationAmount float64, driverCompensationCurrency string, data map[string]any) (*types.Booking, error) { journey, err := h.Storage.GetDriverJourney(journeyid) if err != nil { log.Error().Err(err).Msg("could not find driver journey") @@ -26,14 +26,16 @@ func (h Handler) BookDriverJourney(passengerid string, driverid string, journeyi log.Debug().Float64("Price", priceAmount).Any("journey", journey.Price).Msg("store booking") booking := types.Booking{ - Id: uuid.NewString(), - GroupId: uuid.NewString(), - Status: "WAITING_CONFIRMATION", - PassengerId: passengerid, - DriverId: driverid, - Journey: journey, - ReturnWaitingDuration: returnWaitingDuration, - Data: data, + Id: uuid.NewString(), + GroupId: uuid.NewString(), + Status: "WAITING_CONFIRMATION", + PassengerId: passengerid, + DriverId: driverid, + Journey: journey, + ReturnWaitingDuration: returnWaitingDuration, + Data: data, + DriverCompensationAmount: driverCompensationAmount, + DriverCompensationCurrency: driverCompensationCurrency, } if err := h.Storage.CreateBooking(booking); err != nil { diff --git a/handler/journeys_test.go b/handler/journeys_test.go new file mode 100644 index 0000000..1f57785 --- /dev/null +++ b/handler/journeys_test.go @@ -0,0 +1,335 @@ +package handler + +import ( + "testing" + "time" + + "git.coopgo.io/coopgo-platform/routing-service" + "git.coopgo.io/coopgo-platform/solidarity-transport/storage" + "git.coopgo.io/coopgo-platform/solidarity-transport/types" + "github.com/google/uuid" + "github.com/paulmach/orb" + "github.com/paulmach/orb/geojson" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type MockRoutingService struct { + mock.Mock +} + +func (m *MockRoutingService) Route(points []orb.Point) (*routing.Route, error) { + args := m.Called(points) + return args.Get(0).(*routing.Route), args.Error(1) +} + +func createTestConfig() *viper.Viper { + cfg := viper.New() + cfg.Set("storage.db.type", "mock") + cfg.Set("parameters.limits.distance.min", 1000) + cfg.Set("parameters.limits.distance.max", 50000) + return cfg +} + +func createTestGeoJSONFeature(lat, lng float64, label string) *geojson.Feature { + feature := geojson.NewFeature(orb.Point{lng, lat}) + feature.Properties = make(map[string]interface{}) + feature.Properties["label"] = label + return feature +} + +func createTestRoute() *routing.Route { + return createTestRouteWithDistance(6000) // Default route with 6km passenger distance +} + +func createTestRouteNoReturn(passengerDistance float64) *routing.Route { + // No-return: 3 legs (driver→pickup, pickup→dropoff, dropoff→driver) + return &routing.Route{ + Summary: routing.Summary{ + Distance: 4000 + passengerDistance, + Duration: 15 * time.Minute, + Polyline: "test_polyline", + }, + Legs: []routing.RouteLeg{ + {Summary: routing.Summary{Distance: 2000, Duration: 5 * time.Minute}}, // driver to pickup + {Summary: routing.Summary{Distance: passengerDistance, Duration: 8 * time.Minute}}, // pickup to dropoff + {Summary: routing.Summary{Distance: 2000, Duration: 5 * time.Minute}}, // dropoff to driver destination + }, + } +} + +func createTestRouteReturn(passengerDistance float64) *routing.Route { + // Return: 4+ legs (driver→pickup, pickup→dropoff, dropoff→pickup, pickup→driver) + // For return journeys: passenger distance = legs[1] + legs[2] + singleLegDistance := passengerDistance / 2 + + return &routing.Route{ + Summary: routing.Summary{ + Distance: 4000 + passengerDistance, + Duration: 18 * time.Minute, + Polyline: "test_polyline", + }, + Legs: []routing.RouteLeg{ + {Summary: routing.Summary{Distance: 2000, Duration: 5 * time.Minute}}, // driver to pickup + {Summary: routing.Summary{Distance: singleLegDistance, Duration: 8 * time.Minute}}, // pickup to dropoff + {Summary: routing.Summary{Distance: singleLegDistance, Duration: 2 * time.Minute}}, // dropoff to pickup (return) + {Summary: routing.Summary{Distance: 2000, Duration: 3 * time.Minute}}, // pickup to driver destination + }, + } +} + +func createTestRouteWithDistance(passengerDistance float64) *routing.Route { + // Default to return journey for backward compatibility + return createTestRouteReturn(passengerDistance) +} + +func createTestAvailability(driverID string, day int, startTime, endTime string) *types.DriverRegularAvailability { + address := createTestGeoJSONFeature(45.7640, 4.8357, "Driver Address") + return &types.DriverRegularAvailability{ + ID: uuid.New().String(), + DriverId: driverID, + Day: day, + StartTime: startTime, + EndTime: endTime, + Address: address, + } +} + +func TestHandler_GetDriverJourneys(t *testing.T) { + tests := []struct { + name string + departureDate time.Time + noreturn bool + availabilities []*types.DriverRegularAvailability + routingResponse *routing.Route + expectedJourneys int + expectedDriverIDs []string + description string + }{ + { + name: "journey within distance limits", + departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC), + noreturn: false, + availabilities: []*types.DriverRegularAvailability{ + createTestAvailability("driver-1", 1, "07:00", "09:00"), + }, + routingResponse: createTestRouteWithDistance(6000), // 6km - within limits (1km to 50km) + expectedJourneys: 1, + expectedDriverIDs: []string{"driver-1"}, + description: "6km journey should be within distance limits", + }, + { + name: "journey too short - below minimum distance", + departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC), + noreturn: false, + availabilities: []*types.DriverRegularAvailability{ + createTestAvailability("driver-1", 1, "07:00", "09:00"), + }, + routingResponse: createTestRouteWithDistance(500), // 500m - below 1km minimum + expectedJourneys: 0, + expectedDriverIDs: []string{}, + description: "500m journey should be rejected (below minimum distance)", + }, + { + name: "journey too long - above maximum distance", + departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC), + noreturn: false, + availabilities: []*types.DriverRegularAvailability{ + createTestAvailability("driver-1", 1, "07:00", "09:00"), + }, + routingResponse: createTestRouteWithDistance(60000), // 60km - above 50km maximum + expectedJourneys: 0, + expectedDriverIDs: []string{}, + description: "60km journey should be rejected (above maximum distance)", + }, + { + name: "journey at minimum distance boundary", + departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC), + noreturn: false, + availabilities: []*types.DriverRegularAvailability{ + createTestAvailability("driver-1", 1, "07:00", "09:00"), + }, + routingResponse: createTestRouteWithDistance(1000), // Exactly 1km minimum + expectedJourneys: 1, + expectedDriverIDs: []string{"driver-1"}, + description: "1km journey should be accepted (at minimum boundary)", + }, + { + name: "journey at maximum distance boundary", + departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC), + noreturn: false, + availabilities: []*types.DriverRegularAvailability{ + createTestAvailability("driver-1", 1, "07:00", "09:00"), + }, + routingResponse: createTestRouteWithDistance(50000), // Exactly 50km maximum + expectedJourneys: 1, + expectedDriverIDs: []string{"driver-1"}, + description: "50km journey should be accepted (at maximum boundary)", + }, + { + name: "wrong day no availabilities", + departureDate: time.Date(2024, 1, 16, 9, 0, 0, 0, time.UTC), + noreturn: false, + availabilities: []*types.DriverRegularAvailability{ + createTestAvailability("driver-1", 1, "08:00", "10:00"), + }, + expectedJourneys: 0, + expectedDriverIDs: []string{}, + description: "Tuesday departure should not match Monday availability", + }, + } + + departure := createTestGeoJSONFeature(45.7578, 4.8320, "Departure") + arrival := createTestGeoJSONFeature(45.7485, 4.8467, "Arrival") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := createTestConfig() + mockStorage := storage.NewMockStorage(cfg) + mockRouting := &MockRoutingService{} + + for _, availability := range tt.availabilities { + err := mockStorage.CreateDriverRegularAvailability(*availability) + require.NoError(t, err) + } + + routeResponse := tt.routingResponse + if routeResponse == nil { + routeResponse = createTestRoute() // Default route + } + mockRouting.On("Route", mock.AnythingOfType("[]orb.Point")).Return(routeResponse, nil) + + handler := &Handler{ + Config: cfg, + Storage: mockStorage, + Routing: mockRouting, + } + + journeys, err := handler.GetDriverJourneys(departure, arrival, tt.departureDate, tt.noreturn) + + assert.NoError(t, err, tt.description) + assert.Len(t, journeys, tt.expectedJourneys, tt.description) + + if tt.expectedJourneys > 0 { + actualDriverIDs := make([]string, len(journeys)) + for i, journey := range journeys { + assert.NotEmpty(t, journey.Id) + actualDriverIDs[i] = journey.DriverId + } + assert.ElementsMatch(t, tt.expectedDriverIDs, actualDriverIDs, tt.description) + } + }) + } +} + +func TestHandler_GetDriverJourney(t *testing.T) { + cfg := createTestConfig() + mockStorage := storage.NewMockStorage(cfg) + + testJourney := &types.DriverJourney{ + Id: uuid.New().String(), + DriverId: "driver-1", + } + err := mockStorage.PushDriverJourneys([]*types.DriverJourney{testJourney}) + require.NoError(t, err) + + handler := &Handler{ + Config: cfg, + Storage: mockStorage, + } + + tests := []struct { + name string + driverID string + journeyID string + expectedError bool + errorMessage string + }{ + { + name: "successful retrieval", + driverID: "driver-1", + journeyID: testJourney.Id, + expectedError: false, + }, + { + name: "driver mismatch", + driverID: "driver-2", + journeyID: testJourney.Id, + expectedError: true, + errorMessage: "not allowed, driver id mismatch", + }, + { + name: "journey not found", + driverID: "driver-1", + journeyID: "non-existent", + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + journey, err := handler.GetDriverJourney(tt.driverID, tt.journeyID) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorMessage != "" { + assert.Contains(t, err.Error(), tt.errorMessage) + } + assert.Nil(t, journey) + } else { + assert.NoError(t, err) + assert.NotNil(t, journey) + assert.Equal(t, tt.journeyID, journey.Id) + assert.Equal(t, tt.driverID, journey.DriverId) + } + }) + } +} + +func TestHandler_ToggleDriverJourneyNoreturn(t *testing.T) { + cfg := createTestConfig() + mockStorage := storage.NewMockStorage(cfg) + mockRouting := &MockRoutingService{} + + departure := createTestGeoJSONFeature(45.7578, 4.8320, "Departure") + arrival := createTestGeoJSONFeature(45.7485, 4.8467, "Arrival") + driverAddress := createTestGeoJSONFeature(45.7640, 4.8357, "Driver Address") + + testJourney := &types.DriverJourney{ + Id: uuid.New().String(), + DriverId: "driver-1", + PassengerPickup: departure, + PassengerDrop: arrival, + DriverDeparture: driverAddress, + Noreturn: false, + } + + err := mockStorage.PushDriverJourneys([]*types.DriverJourney{testJourney}) + require.NoError(t, err) + + mockRouting.On("Route", mock.AnythingOfType("[]orb.Point")).Return(createTestRoute(), nil) + + handler := &Handler{ + Config: cfg, + Storage: mockStorage, + Routing: mockRouting, + } + + err = handler.ToggleDriverJourneyNoreturn(testJourney.Id) + assert.NoError(t, err) + + updatedJourney, err := mockStorage.GetDriverJourney(testJourney.Id) + assert.NoError(t, err) + assert.True(t, updatedJourney.Noreturn) + + err = handler.ToggleDriverJourneyNoreturn(testJourney.Id) + assert.NoError(t, err) + + updatedJourney, err = mockStorage.GetDriverJourney(testJourney.Id) + assert.NoError(t, err) + assert.False(t, updatedJourney.Noreturn) + + mockRouting.AssertExpectations(t) +} \ No newline at end of file diff --git a/servers/grpc/proto/gen/solidarity-transport-types.pb.go b/servers/grpc/proto/gen/solidarity-transport-types.pb.go index 6e4e2c2..e494baa 100644 --- a/servers/grpc/proto/gen/solidarity-transport-types.pb.go +++ b/servers/grpc/proto/gen/solidarity-transport-types.pb.go @@ -396,17 +396,19 @@ func (x *SolidarityTransportPrice) GetCurrency() string { } type SolidarityTransportBooking struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - GroupId string `protobuf:"bytes,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` - DriverId string `protobuf:"bytes,3,opt,name=driver_id,json=driverId,proto3" json:"driver_id,omitempty"` - PassengerId string `protobuf:"bytes,4,opt,name=passenger_id,json=passengerId,proto3" json:"passenger_id,omitempty"` - Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` - ReturnWaitingDuration int64 `protobuf:"varint,6,opt,name=return_waiting_duration,json=returnWaitingDuration,proto3" json:"return_waiting_duration,omitempty"` - Data *structpb.Struct `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` - Journey *SolidarityTransportDriverJourney `protobuf:"bytes,10,opt,name=journey,proto3" json:"journey,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + GroupId string `protobuf:"bytes,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + DriverId string `protobuf:"bytes,3,opt,name=driver_id,json=driverId,proto3" json:"driver_id,omitempty"` + PassengerId string `protobuf:"bytes,4,opt,name=passenger_id,json=passengerId,proto3" json:"passenger_id,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + ReturnWaitingDuration int64 `protobuf:"varint,6,opt,name=return_waiting_duration,json=returnWaitingDuration,proto3" json:"return_waiting_duration,omitempty"` + Data *structpb.Struct `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` + DriverCompensationAmount float64 `protobuf:"fixed64,8,opt,name=driver_compensation_amount,json=driverCompensationAmount,proto3" json:"driver_compensation_amount,omitempty"` + DriverCompensationCurrency string `protobuf:"bytes,9,opt,name=driver_compensation_currency,json=driverCompensationCurrency,proto3" json:"driver_compensation_currency,omitempty"` + Journey *SolidarityTransportDriverJourney `protobuf:"bytes,10,opt,name=journey,proto3" json:"journey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SolidarityTransportBooking) Reset() { @@ -488,6 +490,20 @@ func (x *SolidarityTransportBooking) GetData() *structpb.Struct { return nil } +func (x *SolidarityTransportBooking) GetDriverCompensationAmount() float64 { + if x != nil { + return x.DriverCompensationAmount + } + return 0 +} + +func (x *SolidarityTransportBooking) GetDriverCompensationCurrency() string { + if x != nil { + return x.DriverCompensationCurrency + } + return "" +} + func (x *SolidarityTransportBooking) GetJourney() *SolidarityTransportDriverJourney { if x != nil { return x.Journey @@ -537,7 +553,7 @@ const file_solidarity_transport_types_proto_rawDesc = "" + "\x06_price\"N\n" + "\x18SolidarityTransportPrice\x12\x16\n" + "\x06amount\x18\x01 \x01(\x01R\x06amount\x12\x1a\n" + - "\bcurrency\x18\x02 \x01(\tR\bcurrency\"\xc1\x02\n" + + "\bcurrency\x18\x02 \x01(\tR\bcurrency\"\xc1\x03\n" + "\x1aSolidarityTransportBooking\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x19\n" + "\bgroup_id\x18\x02 \x01(\tR\agroupId\x12\x1b\n" + @@ -545,7 +561,9 @@ const file_solidarity_transport_types_proto_rawDesc = "" + "\fpassenger_id\x18\x04 \x01(\tR\vpassengerId\x12\x16\n" + "\x06status\x18\x05 \x01(\tR\x06status\x126\n" + "\x17return_waiting_duration\x18\x06 \x01(\x03R\x15returnWaitingDuration\x12+\n" + - "\x04data\x18\a \x01(\v2\x17.google.protobuf.StructR\x04data\x12;\n" + + "\x04data\x18\a \x01(\v2\x17.google.protobuf.StructR\x04data\x12<\n" + + "\x1adriver_compensation_amount\x18\b \x01(\x01R\x18driverCompensationAmount\x12@\n" + + "\x1cdriver_compensation_currency\x18\t \x01(\tR\x1adriverCompensationCurrency\x12;\n" + "\ajourney\x18\n" + " \x01(\v2!.SolidarityTransportDriverJourneyR\ajourneyBKZIgit.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/genb\x06proto3" diff --git a/servers/grpc/proto/gen/solidarity-transport.pb.go b/servers/grpc/proto/gen/solidarity-transport.pb.go index 81fa8a6..0861e94 100644 --- a/servers/grpc/proto/gen/solidarity-transport.pb.go +++ b/servers/grpc/proto/gen/solidarity-transport.pb.go @@ -608,16 +608,18 @@ func (x *GetDriverJourneyResponse) GetDriverJourney() *SolidarityTransportDriver } type BookDriverJourneyRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - PassengerId string `protobuf:"bytes,1,opt,name=passenger_id,json=passengerId,proto3" json:"passenger_id,omitempty"` - DriverId string `protobuf:"bytes,2,opt,name=driver_id,json=driverId,proto3" json:"driver_id,omitempty"` - DriverJourneyId string `protobuf:"bytes,3,opt,name=driver_journey_id,json=driverJourneyId,proto3" json:"driver_journey_id,omitempty"` - ReturnWaitingDuration int64 `protobuf:"varint,4,opt,name=return_waiting_duration,json=returnWaitingDuration,proto3" json:"return_waiting_duration,omitempty"` - PriceAmount float64 `protobuf:"fixed64,5,opt,name=price_amount,json=priceAmount,proto3" json:"price_amount,omitempty"` - PriceCurrency string `protobuf:"bytes,6,opt,name=price_currency,json=priceCurrency,proto3" json:"price_currency,omitempty"` - Data *structpb.Struct `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + PassengerId string `protobuf:"bytes,1,opt,name=passenger_id,json=passengerId,proto3" json:"passenger_id,omitempty"` + DriverId string `protobuf:"bytes,2,opt,name=driver_id,json=driverId,proto3" json:"driver_id,omitempty"` + DriverJourneyId string `protobuf:"bytes,3,opt,name=driver_journey_id,json=driverJourneyId,proto3" json:"driver_journey_id,omitempty"` + ReturnWaitingDuration int64 `protobuf:"varint,4,opt,name=return_waiting_duration,json=returnWaitingDuration,proto3" json:"return_waiting_duration,omitempty"` + PriceAmount float64 `protobuf:"fixed64,5,opt,name=price_amount,json=priceAmount,proto3" json:"price_amount,omitempty"` + PriceCurrency string `protobuf:"bytes,6,opt,name=price_currency,json=priceCurrency,proto3" json:"price_currency,omitempty"` + Data *structpb.Struct `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` + DriverCompensationAmount float64 `protobuf:"fixed64,8,opt,name=driver_compensation_amount,json=driverCompensationAmount,proto3" json:"driver_compensation_amount,omitempty"` + DriverCompensationCurrency string `protobuf:"bytes,9,opt,name=driver_compensation_currency,json=driverCompensationCurrency,proto3" json:"driver_compensation_currency,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BookDriverJourneyRequest) Reset() { @@ -699,6 +701,20 @@ func (x *BookDriverJourneyRequest) GetData() *structpb.Struct { return nil } +func (x *BookDriverJourneyRequest) GetDriverCompensationAmount() float64 { + if x != nil { + return x.DriverCompensationAmount + } + return 0 +} + +func (x *BookDriverJourneyRequest) GetDriverCompensationCurrency() string { + if x != nil { + return x.DriverCompensationCurrency + } + return "" +} + type BookDriverJourneyResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Booking *SolidarityTransportBooking `protobuf:"bytes,1,opt,name=booking,proto3" json:"booking,omitempty"` @@ -1246,7 +1262,7 @@ const file_solidarity_transport_proto_rawDesc = "" + "\n" + "journey_id\x18\x02 \x01(\tR\tjourneyId\"d\n" + "\x18GetDriverJourneyResponse\x12H\n" + - "\x0edriver_journey\x18\x01 \x01(\v2!.SolidarityTransportDriverJourneyR\rdriverJourney\"\xb5\x02\n" + + "\x0edriver_journey\x18\x01 \x01(\v2!.SolidarityTransportDriverJourneyR\rdriverJourney\"\xb5\x03\n" + "\x18BookDriverJourneyRequest\x12!\n" + "\fpassenger_id\x18\x01 \x01(\tR\vpassengerId\x12\x1b\n" + "\tdriver_id\x18\x02 \x01(\tR\bdriverId\x12*\n" + @@ -1254,7 +1270,9 @@ const file_solidarity_transport_proto_rawDesc = "" + "\x17return_waiting_duration\x18\x04 \x01(\x03R\x15returnWaitingDuration\x12!\n" + "\fprice_amount\x18\x05 \x01(\x01R\vpriceAmount\x12%\n" + "\x0eprice_currency\x18\x06 \x01(\tR\rpriceCurrency\x12+\n" + - "\x04data\x18\a \x01(\v2\x17.google.protobuf.StructR\x04data\"R\n" + + "\x04data\x18\a \x01(\v2\x17.google.protobuf.StructR\x04data\x12<\n" + + "\x1adriver_compensation_amount\x18\b \x01(\x01R\x18driverCompensationAmount\x12@\n" + + "\x1cdriver_compensation_currency\x18\t \x01(\tR\x1adriverCompensationCurrency\"R\n" + "\x19BookDriverJourneyResponse\x125\n" + "\abooking\x18\x01 \x01(\v2\x1b.SolidarityTransportBookingR\abooking\"\xef\x01\n" + "%GetSolidarityTransportBookingsRequest\x129\n" + diff --git a/servers/grpc/proto/solidarity-transport-types.proto b/servers/grpc/proto/solidarity-transport-types.proto index a9e7f0b..fbe1f0f 100644 --- a/servers/grpc/proto/solidarity-transport-types.proto +++ b/servers/grpc/proto/solidarity-transport-types.proto @@ -51,6 +51,8 @@ message SolidarityTransportBooking { string status = 5; int64 return_waiting_duration = 6; google.protobuf.Struct data = 7; + double driver_compensation_amount = 8; + string driver_compensation_currency = 9; SolidarityTransportDriverJourney journey = 10; } diff --git a/servers/grpc/proto/solidarity-transport.proto b/servers/grpc/proto/solidarity-transport.proto index d0f927e..739c7b0 100644 --- a/servers/grpc/proto/solidarity-transport.proto +++ b/servers/grpc/proto/solidarity-transport.proto @@ -88,6 +88,8 @@ message BookDriverJourneyRequest { double price_amount = 5; string price_currency = 6; google.protobuf.Struct data = 7; + double driver_compensation_amount = 8; + string driver_compensation_currency = 9; } message BookDriverJourneyResponse { diff --git a/servers/grpc/server/bookings.go b/servers/grpc/server/bookings.go index 88b9ca5..d310fb3 100644 --- a/servers/grpc/server/bookings.go +++ b/servers/grpc/server/bookings.go @@ -17,10 +17,12 @@ func (s SolidarityTransportServerImpl) BookDriverJourney(ctx context.Context, re journeyId := req.DriverJourneyId priceAmount := req.PriceAmount priceCurrency := req.PriceCurrency + driverCompensationAmount := req.DriverCompensationAmount + driverCompensationCurrency := req.DriverCompensationCurrency returnWaitingDuration := time.Duration(req.ReturnWaitingDuration) data := req.Data.AsMap() - booking, err := s.Handler.BookDriverJourney(passengerId, driverId, journeyId, returnWaitingDuration, priceAmount, priceCurrency, data) + booking, err := s.Handler.BookDriverJourney(passengerId, driverId, journeyId, returnWaitingDuration, priceAmount, priceCurrency, driverCompensationAmount, driverCompensationCurrency, data) if err != nil { log.Error().Err(err).Msg("issue in BookDriverJourney handler") return nil, status.Errorf(codes.Internal, "could not create booking : %v", err) diff --git a/servers/grpc/transformers/bookings.go b/servers/grpc/transformers/bookings.go index dda3961..073197b 100644 --- a/servers/grpc/transformers/bookings.go +++ b/servers/grpc/transformers/bookings.go @@ -20,14 +20,16 @@ func BookingTypeToProto(booking *types.Booking) (*gen.SolidarityTransportBooking } return &gen.SolidarityTransportBooking{ - Id: booking.Id, - GroupId: booking.GroupId, - DriverId: booking.DriverId, - PassengerId: booking.PassengerId, - Status: booking.Status, - ReturnWaitingDuration: int64(booking.ReturnWaitingDuration), - Journey: journey, - Data: data, + Id: booking.Id, + GroupId: booking.GroupId, + DriverId: booking.DriverId, + PassengerId: booking.PassengerId, + Status: booking.Status, + ReturnWaitingDuration: int64(booking.ReturnWaitingDuration), + Journey: journey, + Data: data, + DriverCompensationAmount: booking.DriverCompensationAmount, + DriverCompensationCurrency: booking.DriverCompensationCurrency, }, nil } @@ -37,13 +39,15 @@ func BookingProtoToType(booking *gen.SolidarityTransportBooking) (*types.Booking return nil, err } return &types.Booking{ - Id: booking.Id, - GroupId: booking.GroupId, - DriverId: booking.DriverId, - PassengerId: booking.PassengerId, - Status: booking.Status, - ReturnWaitingDuration: time.Duration(booking.ReturnWaitingDuration), - Journey: journey, - Data: booking.Data.AsMap(), + Id: booking.Id, + GroupId: booking.GroupId, + DriverId: booking.DriverId, + PassengerId: booking.PassengerId, + Status: booking.Status, + ReturnWaitingDuration: time.Duration(booking.ReturnWaitingDuration), + Journey: journey, + Data: booking.Data.AsMap(), + DriverCompensationAmount: booking.DriverCompensationAmount, + DriverCompensationCurrency: booking.DriverCompensationCurrency, }, nil } diff --git a/storage/mock.go b/storage/mock.go new file mode 100644 index 0000000..6ee4f9d --- /dev/null +++ b/storage/mock.go @@ -0,0 +1,279 @@ +package storage + +import ( + "fmt" + "strings" + "sync" + + "git.coopgo.io/coopgo-platform/solidarity-transport/types" + "github.com/google/uuid" + "github.com/spf13/viper" +) + +// MockStorage implements the Storage interface for testing purposes +type MockStorage struct { + config *viper.Viper + mu sync.RWMutex + availabilities map[string]*types.DriverRegularAvailability + journeys map[string]*types.DriverJourney + bookings map[string]*types.Booking + availabilitiesByDriver map[string][]*types.DriverRegularAvailability +} + +// NewMockStorage creates a new mock storage instance +func NewMockStorage(cfg *viper.Viper) Storage { + return &MockStorage{ + config: cfg, + availabilities: make(map[string]*types.DriverRegularAvailability), + journeys: make(map[string]*types.DriverJourney), + bookings: make(map[string]*types.Booking), + availabilitiesByDriver: make(map[string][]*types.DriverRegularAvailability), + } +} + +// Driver Regular Availability methods + +func (m *MockStorage) CreateDriverRegularAvailability(availability types.DriverRegularAvailability) error { + m.mu.Lock() + defer m.mu.Unlock() + + if availability.ID == "" { + availability.ID = uuid.New().String() + } + + m.availabilities[availability.ID] = &availability + + // Update driver index + m.availabilitiesByDriver[availability.DriverId] = append( + m.availabilitiesByDriver[availability.DriverId], + &availability, + ) + + return nil +} + +func (m *MockStorage) CreateDriverRegularAvailabilities(availabilities []*types.DriverRegularAvailability) error { + for _, availability := range availabilities { + if err := m.CreateDriverRegularAvailability(*availability); err != nil { + return err + } + } + return nil +} + +func (m *MockStorage) DeleteDriverRegularAvailability(driverid string, availabilityid string) error { + m.mu.Lock() + defer m.mu.Unlock() + + availability, exists := m.availabilities[availabilityid] + if !exists { + return fmt.Errorf("availability %s not found", availabilityid) + } + + if availability.DriverId != driverid { + return fmt.Errorf("availability %s does not belong to driver %s", availabilityid, driverid) + } + + delete(m.availabilities, availabilityid) + + // Update driver index + driverAvailabilities := m.availabilitiesByDriver[driverid] + for i, a := range driverAvailabilities { + if a.ID == availabilityid { + m.availabilitiesByDriver[driverid] = append(driverAvailabilities[:i], driverAvailabilities[i+1:]...) + break + } + } + + return nil +} + +func (m *MockStorage) GetDriverRegularAvailability(driverid string, availabilityid string) (*types.DriverRegularAvailability, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + availability, exists := m.availabilities[availabilityid] + if !exists { + return nil, fmt.Errorf("availability %s not found", availabilityid) + } + + if availability.DriverId != driverid { + return nil, fmt.Errorf("availability %s does not belong to driver %s", availabilityid, driverid) + } + + return availability, nil +} + +func (m *MockStorage) GetDriverRegularAvailabilities(driverid string) ([]*types.DriverRegularAvailability, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.availabilitiesByDriver[driverid], nil +} + +func (m *MockStorage) GetRegularAvailabilities(day int, timeInDay string) ([]*types.DriverRegularAvailability, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + var result []*types.DriverRegularAvailability + + for _, availability := range m.availabilities { + // Match day + if availability.Day != day { + continue + } + + // For testing, just return all availabilities for the day + // Let the handler do the time filtering logic + result = append(result, availability) + } + + return result, nil +} + +// timeMatches is a simple time matching function for testing +func (m *MockStorage) timeMatches(availabilityTime, requestedTime string) bool { + // Simple string matching for testing - real implementation would parse times + return strings.HasPrefix(availabilityTime, requestedTime[:2]) // Match hour +} + +// Driver Journey methods + +func (m *MockStorage) PushDriverJourneys(journeys []*types.DriverJourney) error { + m.mu.Lock() + defer m.mu.Unlock() + + for _, journey := range journeys { + if journey.Id == "" { + journey.Id = uuid.New().String() + } + m.journeys[journey.Id] = journey + } + + return nil +} + +func (m *MockStorage) GetDriverJourney(id string) (*types.DriverJourney, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + journey, exists := m.journeys[id] + if !exists { + return nil, fmt.Errorf("journey %s not found", id) + } + + return journey, nil +} + +func (m *MockStorage) UpdateDriverJourney(journey types.DriverJourney) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.journeys[journey.Id]; !exists { + return fmt.Errorf("journey %s not found", journey.Id) + } + + m.journeys[journey.Id] = &journey + return nil +} + +// Booking methods + +func (m *MockStorage) CreateBooking(booking types.Booking) error { + m.mu.Lock() + defer m.mu.Unlock() + + if booking.Id == "" { + booking.Id = uuid.New().String() + } + + m.bookings[booking.Id] = &booking + return nil +} + +func (m *MockStorage) GetAllBookings() ([]*types.Booking, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + var result []*types.Booking + for _, booking := range m.bookings { + result = append(result, booking) + } + + return result, nil +} + +func (m *MockStorage) GetBooking(id string) (*types.Booking, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + booking, exists := m.bookings[id] + if !exists { + return nil, fmt.Errorf("booking %s not found", id) + } + + return booking, nil +} + +func (m *MockStorage) UpdateBooking(booking types.Booking) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.bookings[booking.Id]; !exists { + return fmt.Errorf("booking %s not found", booking.Id) + } + + m.bookings[booking.Id] = &booking + return nil +} + +func (m *MockStorage) UpdateBookingStatus(bookingid string, newStatus string, reason string) error { + m.mu.Lock() + defer m.mu.Unlock() + + booking, exists := m.bookings[bookingid] + if !exists { + return fmt.Errorf("booking %s not found", bookingid) + } + + booking.Status = newStatus + // In a real implementation, we might store the reason in a separate field + return nil +} + +// Helper methods for testing + +// Reset clears all data - useful for test setup +func (m *MockStorage) Reset() { + m.mu.Lock() + defer m.mu.Unlock() + + m.availabilities = make(map[string]*types.DriverRegularAvailability) + m.journeys = make(map[string]*types.DriverJourney) + m.bookings = make(map[string]*types.Booking) + m.availabilitiesByDriver = make(map[string][]*types.DriverRegularAvailability) +} + +// GetAllJourneys returns all stored journeys - useful for testing +func (m *MockStorage) GetAllJourneys() []*types.DriverJourney { + m.mu.RLock() + defer m.mu.RUnlock() + + var result []*types.DriverJourney + for _, journey := range m.journeys { + result = append(result, journey) + } + return result +} + +// GetAllAvailabilities returns all stored availabilities - useful for testing +func (m *MockStorage) GetAllAvailabilities() []*types.DriverRegularAvailability { + m.mu.RLock() + defer m.mu.RUnlock() + + var result []*types.DriverRegularAvailability + for _, availability := range m.availabilities { + result = append(result, availability) + } + return result +} \ No newline at end of file diff --git a/storage/storage.go b/storage/storage.go index d5ce6da..55b905a 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -35,6 +35,8 @@ func NewStorage(cfg *viper.Viper) (Storage, error) { return NewMongoDBStorage(cfg) case "memory": return NewMemoryStorage(cfg) + case "mock": + return NewMockStorage(cfg), nil default: return nil, fmt.Errorf("storage type %v is not supported", storage_type) diff --git a/types/booking.go b/types/booking.go index ce2321e..1265a8b 100644 --- a/types/booking.go +++ b/types/booking.go @@ -3,13 +3,15 @@ package types import "time" type Booking struct { - Id string `json:"id" bson:"_id"` - GroupId string `json:"group_id" bson:"group_id"` - DriverId string `json:"driver_id" bson:"driver_id"` - PassengerId string `json:"passenger_id" bson:"passenger_id"` - Status string `json:"status" bson:"status"` - ReturnWaitingDuration time.Duration `json:"return_waiting_duration" bson:"return_waiting_duration"` - Data map[string]any `json:"data" bson:"data"` + Id string `json:"id" bson:"_id"` + GroupId string `json:"group_id" bson:"group_id"` + DriverId string `json:"driver_id" bson:"driver_id"` + PassengerId string `json:"passenger_id" bson:"passenger_id"` + Status string `json:"status" bson:"status"` + ReturnWaitingDuration time.Duration `json:"return_waiting_duration" bson:"return_waiting_duration"` + Data map[string]any `json:"data" bson:"data"` + DriverCompensationAmount float64 `json:"driver_compensation_amount" bson:"driver_compensation_amount"` + DriverCompensationCurrency string `json:"driver_compensation_currency" bson:"driver_compensation_currency"` Journey *DriverJourney `json:"journey,omitempty" bson:"journey,omitempty"` }