Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/mobility-core/mobility-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ library
Kernel.External.EventTracking.Moengage.Flow
Kernel.External.EventTracking.Moengage.Types
Kernel.External.EventTracking.Types
Kernel.External.FleetEngine.Auth
Kernel.External.FleetEngine.Client
Kernel.External.FleetEngine.Config
Kernel.External.FleetEngine.Types
Kernel.External.GoogleTranslate.API
Kernel.External.GoogleTranslate.Client
Kernel.External.GoogleTranslate.Types
Expand Down
54 changes: 54 additions & 0 deletions lib/mobility-core/src/Kernel/External/FleetEngine/Auth.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{-
Copyright 2022-23, Juspay India Pvt Ltd

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License

as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is

distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS

FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero

General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
-}

module Kernel.External.FleetEngine.Auth
( TokenScope (..),
fleetEngineAudience,
parseServiceAccount,
mintFleetEngineToken,
)
where

import qualified Data.Aeson as A
import qualified Data.Text.Encoding as TE
import Kernel.Prelude
import qualified Kernel.Utils.JWT as JWT

-- | Audience for self-signed Fleet Engine JWTs (server, driver and consumer).
fleetEngineAudience :: Text
fleetEngineAudience = "https://fleetengine.googleapis.com/"

-- | What a minted token is scoped to. Consumer/driver tokens carry an
-- @authorization@ claim restricting them to a single trip/vehicle; server
-- tokens carry no restriction (the service-account role grants access).
data TokenScope
= -- | scoped to a tripId
ConsumerToken Text
| -- | scoped to a vehicleId
DriverToken Text
| ServerToken

-- | Decode the (decrypted) service-account JSON text.
parseServiceAccount :: Text -> Either String JWT.ServiceAccount
parseServiceAccount = A.eitherDecodeStrict . TE.encodeUtf8

-- | Mint a short-lived self-signed Fleet Engine JWT for the given scope.
mintFleetEngineToken :: JWT.ServiceAccount -> TokenScope -> Integer -> IO (Either String Text)
mintFleetEngineToken sa scope ttlSeconds =
JWT.createSignedJWTWithClaims sa fleetEngineAudience ttlSeconds (authClaims scope)
where
authClaims :: TokenScope -> [(Text, A.Value)]
authClaims (ConsumerToken tripId) = [("authorization", A.object ["tripid" A..= tripId])]
authClaims (DriverToken vehicleId) = [("authorization", A.object ["vehicleid" A..= vehicleId])]
authClaims ServerToken = []
148 changes: 148 additions & 0 deletions lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{-
Copyright 2022-23, Juspay India Pvt Ltd

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License

as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is

distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS

FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero

General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
-}

module Kernel.External.FleetEngine.Client
( defaultFleetEngineBaseUrl,
createTrip,
updateTrip,
updateTripStatus,
assignVehicleAndStart,
)
where

import qualified Data.Aeson as A
import qualified Data.Text as T
import EulerHS.Types (EulerClient, client)
import Kernel.External.FleetEngine.Types
import Kernel.Prelude
import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics)
import Kernel.Types.Common
import Kernel.Utils.Common
import Servant hiding (throwError)
import Servant.Client (Scheme (..))

type CreateTripAPI =
"v1"
:> "providers"
:> Capture "providerId" Text
:> "trips"
:> MandatoryQueryParam "tripId" Text
:> Header "Authorization" Text
:> ReqBody '[JSON] Trip
:> Post '[JSON] A.Value

type UpdateTripAPI =
"v1"
:> "providers"
:> Capture "providerId" Text
:> "trips"
:> Capture "tripId" Text
:> MandatoryQueryParam "updateMask" Text
:> Header "Authorization" Text
:> ReqBody '[JSON] Trip
:> Patch '[JSON] A.Value

defaultFleetEngineBaseUrl :: BaseUrl
defaultFleetEngineBaseUrl =
BaseUrl
{ baseUrlScheme = Https,
baseUrlHost = "fleetengine.googleapis.com",
baseUrlPort = 443,
baseUrlPath = ""
}

createTripClient :: Text -> Text -> Maybe Text -> Trip -> EulerClient A.Value
createTripClient = client (Proxy :: Proxy CreateTripAPI)

updateTripClient :: Text -> Text -> Text -> Maybe Text -> Trip -> EulerClient A.Value
updateTripClient = client (Proxy :: Proxy UpdateTripAPI)

bearer :: Text -> Maybe Text
bearer token = Just ("Bearer " <> token)

-- | Create a Fleet Engine trip. @tripId@ is the (1:1) BPP ride id, which makes
-- this idempotent: a re-issued CreateTrip for an existing trip returns
-- ALREADY_EXISTS and is treated as success.
createTrip ::
(CoreMetrics m, MonadFlow m, MonadReader r m, HasRequestId r) =>
BaseUrl ->
Text -> -- providerId
Text -> -- token (server JWT)
Text -> -- tripId
Trip ->
m ()
createTrip baseUrl providerId token tripId trip = do
result <-
callAPI
baseUrl
(createTripClient providerId tripId (bearer token) trip)
"fleetEngineCreateTrip"
(Proxy :: Proxy CreateTripAPI)
case result of
Right _ -> logInfo $ "FleetEngine: created trip " <> tripId
Left err
| "ALREADY_EXISTS" `T.isInfixOf` show err ->
logInfo $ "FleetEngine: trip already exists (idempotent no-op) " <> tripId
| otherwise -> logError $ "FleetEngine: createTrip failed for " <> tripId <> ": " <> show err
Comment on lines +92 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the actual error type returned by callAPI and existing structured handling patterns.
fd 'Common.hs' lib/mobility-core/src | xargs -r rg -n -C3 '\bcallAPI\b|FailureResponse|ClientError|ALREADY_EXISTS'
fd 'Client.hs' lib/mobility-core/src/Kernel/External | xargs -r rg -n -C3 'show err|ALREADY_EXISTS|FailureResponse'

Repository: nammayatri/shared-kernel

Length of output: 3618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate callAPI definition and error type handling.
fd -a 'Common.hs' lib/mobility-core/src
fd -a 'Client.hs' lib/mobility-core/src/Kernel/External/FleetEngine
fd -a 'Common.hs' lib/mobility-core/src | xargs -r sed -n '1,260p'
printf '\n==== FleetEngine Client slice ====\n'
sed -n '1,180p' lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs

printf '\n==== Search for error type definitions/usages ====\n'
rg -n -C3 'data .*ClientError|type .*ClientError|FailureResponse|ServantError|HttpException|callAPI\s*::|callAPI\s+\w+' lib/mobility-core/src

Repository: nammayatri/shared-kernel

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate callAPI definition and error type handling.
fd -a 'Common.hs' lib/mobility-core/src
fd -a 'Client.hs' lib/mobility-core/src/Kernel/External/FleetEngine

printf '\n==== Common.hs ====\n'
sed -n '1,260p' lib/mobility-core/src/Common.hs

printf '\n==== FleetEngine Client slice ====\n'
sed -n '1,180p' lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs

printf '\n==== Search for error type definitions/usages ====\n'
rg -n -C3 'data .*ClientError|type .*ClientError|FailureResponse|ServantError|HttpException|callAPI\s*::|callAPI\s+\w+' lib/mobility-core/src

Repository: nammayatri/shared-kernel

Length of output: 728


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '==== callAPI implementation ====\n'
sed -n '1,280p' lib/mobility-core/src/Kernel/Utils/Servant/Client.hs

printf '\n==== CallAPIError helper ====\n'
sed -n '1,220p' lib/mobility-core/src/Kernel/Types/Error/BaseError/HTTPError/CallAPIError.hs

printf '\n==== ALREADY_EXISTS search ====\n'
rg -n -C4 'ALREADY_EXISTS|already exists|FailureResponse _ \(Response' lib/mobility-core/src/Kernel/External lib/mobility-core/src/Kernel/Types lib/mobility-core/src/Kernel/Utils

Repository: nammayatri/shared-kernel

Length of output: 25771


Match Fleet Engine duplicate errors structurally
Use FailureResponse/response body for the idempotency check instead of show err; the current substring match is brittle and can flip duplicate creates into failures if the rendered exception text changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs` around lines 92
- 97, The idempotency check in FleetEngine createTrip handling is too brittle
because it relies on matching "ALREADY_EXISTS" in show err. Update the Left err
branch in Client.hs to inspect the structured FailureResponse/response body from
the FleetEngine error instead of stringifying the exception, and use that parsed
status/body to decide whether to log the idempotent no-op or fall through to
logError. Keep the existing logInfo/logError paths, but key the duplicate-create
detection off the response payload rather than the rendered exception text.


-- | PATCH a trip with the given field mask and body.
updateTrip ::
(CoreMetrics m, MonadFlow m, MonadReader r m, HasRequestId r) =>
BaseUrl ->
Text -> -- providerId
Text -> -- token (server JWT)
Text -> -- tripId
Text -> -- updateMask (comma-separated field paths)
Trip ->
m ()
updateTrip baseUrl providerId token tripId updateMask trip = do
result <-
callAPI
baseUrl
(updateTripClient providerId tripId updateMask (bearer token) trip)
"fleetEngineUpdateTrip"
(Proxy :: Proxy UpdateTripAPI)
case result of
Right _ -> logInfo $ "FleetEngine: updated trip " <> tripId <> " [" <> updateMask <> "]"
Left err -> logError $ "FleetEngine: updateTrip failed for " <> tripId <> ": " <> show err
Comment on lines +77 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Propagate Fleet Engine write failures instead of only logging them.

Both write helpers return m () and swallow every non-success response after logging. That makes upstream code proceed as if the remote trip state was updated, which can permanently desync local ride state from Fleet Engine. Fix this at the root by returning/throwing an error from createTrip and updateTrip, and keep the ALREADY_EXISTS branch as the only idempotent success case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/External/FleetEngine/Client.hs` around lines 77
- 118, Both Fleet Engine write helpers currently hide failures by returning m ()
after logging, so callers cannot react to remote write errors. Update createTrip
and updateTrip in Client.hs to propagate non-success responses from callAPI by
throwing/returning an error in the MonadFlow flow instead of just logging; keep
the ALREADY_EXISTS branch in createTrip as the only idempotent success path, and
ensure the change is applied where result is pattern-matched in these functions.


-- | Convenience: advance a trip's status.
updateTripStatus ::
(CoreMetrics m, MonadFlow m, MonadReader r m, HasRequestId r) =>
BaseUrl ->
Text ->
Text ->
Text ->
TripStatus ->
m ()
updateTripStatus baseUrl providerId token tripId status =
updateTrip baseUrl providerId token tripId "tripStatus" (emptyTrip {tripStatus = Just status})

-- | Convenience: assign the vehicle to the trip and move it to ENROUTE_TO_PICKUP.
assignVehicleAndStart ::
(CoreMetrics m, MonadFlow m, MonadReader r m, HasRequestId r) =>
BaseUrl ->
Text ->
Text ->
Text -> -- tripId
Text -> -- vehicleId
m ()
assignVehicleAndStart baseUrl providerId token tripId vehicleId =
updateTrip
baseUrl
providerId
token
tripId
"tripStatus,vehicleId"
(emptyTrip {tripStatus = Just ENROUTE_TO_PICKUP, vehicleId = Just vehicleId})
48 changes: 48 additions & 0 deletions lib/mobility-core/src/Kernel/External/FleetEngine/Config.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{-
Copyright 2022-23, Juspay India Pvt Ltd

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License

as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is

distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS

FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero

General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
-}

module Kernel.External.FleetEngine.Config where

import Kernel.External.Encryption
import Kernel.Prelude

-- | Per-merchant-operating-city Fleet Engine (On-demand Rides & Deliveries)
-- configuration. Modelled on 'Kernel.External.Maps.Google.Config.GoogleCfg':
-- the service-account JSON is stored encrypted and decrypted server-side only
-- (it is never shipped to apps; the backend mints short-lived scoped JWTs from
-- it). Absence of this config in a city is treated as "feature off" by callers.
data FleetEngineCfg = FleetEngineCfg
{ -- | GCP project id that owns the Fleet Engine provider (the @providers/{id}@ path segment)
providerId :: Text,
-- | Fleet Engine REST host; defaults to https://fleetengine.googleapis.com when unset
fleetEngineUrl :: Maybe BaseUrl,
-- | Service-account JSON (raw JSON text), encrypted at rest
serviceAccountJson :: EncryptedField 'AsEncrypted Text,
-- | TTL (seconds) for consumer (rider) SDK tokens; defaults to 'defaultConsumerTokenTtl'
consumerTokenTtlSeconds :: Maybe Integer,
-- | TTL (seconds) for driver SDK tokens; defaults to 'defaultDriverTokenTtl'
driverTokenTtlSeconds :: Maybe Integer,
-- | TTL (seconds) for server-to-server tokens; defaults to 'defaultServerTokenTtl'
serverTokenTtlSeconds :: Maybe Integer
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
deriving (Generic, Show, Eq, ToJSON, FromJSON)

defaultConsumerTokenTtl :: Integer
defaultConsumerTokenTtl = 15 * 60

defaultDriverTokenTtl :: Integer
defaultDriverTokenTtl = 60 * 60

defaultServerTokenTtl :: Integer
defaultServerTokenTtl = 60 * 60
96 changes: 96 additions & 0 deletions lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{-
Copyright 2022-23, Juspay India Pvt Ltd

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License

as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is

distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS

FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero

General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
-}

module Kernel.External.FleetEngine.Types where

import qualified Data.Aeson as A
import Kernel.Prelude

-- | Lifecycle of a Fleet Engine trip. JSON values must match the Fleet Engine
-- REST enum spelling exactly (the constructor names are used verbatim).
data TripStatus
= UNKNOWN_TRIP_STATUS
| NEW
| ENROUTE_TO_PICKUP
| ARRIVED_AT_PICKUP
| ENROUTE_TO_INTERMEDIATE_DESTINATION
| ARRIVED_AT_INTERMEDIATE_DESTINATION
| ENROUTE_TO_DROPOFF
| COMPLETE
| CANCELED
deriving (Show, Eq, Generic, ToJSON, FromJSON)

data TripType
= UNKNOWN_TRIP_TYPE
| SHARED
| EXCLUSIVE
deriving (Show, Eq, Generic, ToJSON, FromJSON)

data LatLng = LatLng
{ latitude :: Double,
longitude :: Double
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | A Fleet Engine TerminalLocation (only the @point@ is required for our use).
newtype TerminalLocation = TerminalLocation
{ point :: LatLng
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | The subset of the Fleet Engine @Trip@ resource we read/write. All fields are
-- optional so the same record serves as both the CreateTrip body and the
-- (masked) UpdateTrip body. @Nothing@ fields are omitted from the JSON so they
-- never clobber server state on a PATCH.
data Trip = Trip
{ tripType :: Maybe TripType,
tripStatus :: Maybe TripStatus,
vehicleId :: Maybe Text,
numberOfPassengers :: Maybe Int,
pickupPoint :: Maybe TerminalLocation,
dropoffPoint :: Maybe TerminalLocation
}
deriving (Show, Eq, Generic)

tripJSONOptions :: A.Options
tripJSONOptions = A.defaultOptions {A.omitNothingFields = True}

instance ToJSON Trip where
toJSON = A.genericToJSON tripJSONOptions

instance FromJSON Trip where
parseJSON = A.genericParseJSON tripJSONOptions

emptyTrip :: Trip
emptyTrip =
Trip
{ tripType = Nothing,
tripStatus = Nothing,
vehicleId = Nothing,
numberOfPassengers = Nothing,
pickupPoint = Nothing,
dropoffPoint = Nothing
}

-- | Build the CreateTrip body. Fleet Engine requires @tripType@; pickup/dropoff
-- are optional but improve ETA quality.
mkCreateTripBody :: TripType -> Maybe LatLng -> Maybe LatLng -> Maybe Int -> Trip
mkCreateTripBody tType mbPickup mbDropoff mbPassengers =
emptyTrip
{ tripType = Just tType,
tripStatus = Just NEW,
pickupPoint = TerminalLocation <$> mbPickup,
dropoffPoint = TerminalLocation <$> mbDropoff,
numberOfPassengers = mbPassengers
}
Comment on lines +86 to +96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File outline ==\n'
ast-grep outline lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs --view expanded || true

printf '\n== Relevant snippet ==\n'
sed -n '1,180p' lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs | cat -n

printf '\n== mkCreateTripBody usages ==\n'
rg -n "mkCreateTripBody" -S .

printf '\n== Trip / pickupPoint definitions ==\n'
rg -n "data Trip|pickupPoint|tripStatus|tripType|newtype Trip|type Trip" lib/mobility-core/src -S

Repository: nammayatri/shared-kernel

Length of output: 6240


🌐 Web query:

Fleet Engine CreateTrip trip_type pickup_point required tripStatus create request docs

💡 Result:

To create a trip in Fleet Engine using the CreateTrip request, you must provide a parent string (the project ID in the format providers/{provider}) and a unique trip_id [1][2]. Within the Trip object included in the request, the following fields are strictly required: 1. trip_type: Specifies the type of trip (e.g., EXCLUSIVE or SHARED) [1][3][4]. 2. pickup_point: Defines the point of origin for the trip [1][3][4]. Regarding the tripStatus field: it is not required in the creation request because all new trips are automatically assigned a trip_status of NEW by the Fleet Engine system [3][4]. Any value you provide for trip_status during the creation request is ignored [3][4]. Other optional fields that can be provided include dropoff_point, number_of_passengers, vehicle_id, and intermediate_destinations [3][4].

Citations:


Make pickupPoint required in mkCreateTripBody. Fleet Engine create trips require both trip_type and pickup_point; tripStatus is ignored on create and can be omitted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/External/FleetEngine/Types.hs` around lines 86 -
96, Make mkCreateTripBody require a pickup point instead of accepting it as
optional, since Fleet Engine create-trip requests need pickup_point and
trip_type. Update the mkCreateTripBody signature and its use of pickupPoint so
it is always set from a provided LatLng, and remove tripStatus from the create
body because it is ignored on create. Keep the rest of the Trip construction in
emptyTrip unchanged.

Loading
Loading