Skip to content
Merged
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
58 changes: 13 additions & 45 deletions lib/mobility-core/src/Kernel/External/Payout/Interface.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,39 +41,7 @@ createPayoutOrder serviceConfig req = case serviceConfig of
JuspayConfig cfg -> Juspay.createPayoutOrder cfg req
StripeConfig cfg -> do
connectedAccountId <- req.mConnectedAccountId & fromMaybeM (InvalidRequest "connectedAccountId required for Stripe payout")

-- Check Fleet VA available balance and compute adjusted transfer amount if needed.
-- If fleetAvail + transferAmount < payoutAmount, top up the transfer to cover the shortfall.
let computeAdjustedTransfer = do
fleetBalance <- Stripe.getBalance cfg (Just connectedAccountId)
let fleetAvail = Stripe.getAvailableForCurrency req.currency fleetBalance
if fleetAvail + req.transferAmount >= req.amount
then pure (req.transferAmount, Nothing)
else do
let topUp = req.amount - fleetAvail - req.transferAmount
adjustedAmt = req.transferAmount + topUp
-- Verify merchant (platform) VA can cover the increased transfer
merchantBalance <- Stripe.getBalance cfg Nothing
let merchantAvail = Stripe.getAvailableForCurrency req.currency merchantBalance
when (merchantAvail < adjustedAmt) $
throwError $
InvalidRequest $
"Merchant platform account has insufficient balance. Available: "
<> show merchantAvail
<> ", Required: "
<> show adjustedAmt
logInfo $
"Fleet VA balance insufficient (available: "
<> show fleetAvail
<> "). Topping up transfer by "
<> show topUp
<> " -> adjusted transfer: "
<> show adjustedAmt
pure (adjustedAmt, Just topUp)

(adjustedTransferAmount, merchantTopUpAmount) <- computeAdjustedTransfer

createTransferResp <- Stripe.createTransfer cfg (mkTransferReq connectedAccountId adjustedTransferAmount req)
createTransferResp <- Stripe.createTransfer cfg (mkTransferReq connectedAccountId req)
-- In case if external payout api call failed, we still need to store transferId and transferStatus
result <- withTryCatch "createExternalPayout" $ Stripe.createExternalPayout cfg req
createExternalPayoutResp <- case result of
Expand All @@ -92,20 +60,22 @@ createPayoutOrder serviceConfig req = case serviceConfig of
amount = req.amount,
customerId = Just req.customerId
}
pure $ mkCreatePayoutOrderResp merchantTopUpAmount createTransferResp createExternalPayoutResp
pure $ mkCreatePayoutOrderResp createTransferResp createExternalPayoutResp
where
mkTransferReq :: Text -> HighPrecMoney -> CreatePayoutOrderReq -> CreateTransferReq
mkTransferReq connectedAccountId adjustedTransferAmount CreatePayoutOrderReq {..} =
mkTransferReq :: Text -> CreatePayoutOrderReq -> CreateTransferReq
mkTransferReq connectedAccountId CreatePayoutOrderReq {..} = do
let senderAccountId = TransferPlatformAccount
destinationAccount = TransferConnectedAccount connectedAccountId
CreateTransferReq
{ amount = adjustedTransferAmount,
{ amount = transferAmount,
currency,
senderAccountId = TransferPlatformAccount,
destinationAccount = TransferConnectedAccount connectedAccountId,
senderAccountId,
destinationAccount,
description = Just remark
}

mkCreatePayoutOrderResp :: Maybe HighPrecMoney -> CreateTransferResp -> CreateExternalPayoutResp -> CreatePayoutOrderResp
mkCreatePayoutOrderResp merchantTopUpAmount CreateTransferResp {transferId, transferStatus} CreateExternalPayoutResp {..} =
mkCreatePayoutOrderResp :: CreateTransferResp -> CreateExternalPayoutResp -> CreatePayoutOrderResp
mkCreatePayoutOrderResp CreateTransferResp {transferId, transferStatus} CreateExternalPayoutResp {..} =
CreatePayoutOrderResp
{ orderId,
status,
Expand All @@ -122,8 +92,7 @@ createPayoutOrder serviceConfig req = case serviceConfig of
refunds = Nothing,
payments = Nothing,
fulfillments = Nothing,
customerId,
merchantTopUpAmount
customerId
}

payoutOrderStatus ::
Expand Down Expand Up @@ -159,8 +128,7 @@ payoutOrderStatus serviceConfig req = case serviceConfig of
refunds = Nothing,
payments = Nothing,
fulfillments = Nothing,
customerId,
merchantTopUpAmount = Nothing
customerId
}

createTransfer ::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ createPayoutOrder config req = do
transferStatus = Nothing,
transferId = Nothing,
idAssignedByServiceProvider = Nothing,
merchantTopUpAmount = Nothing,
..
}

Expand Down Expand Up @@ -151,7 +150,6 @@ payoutOrderStatus config req = do
transferStatus = Nothing,
transferId = Nothing,
idAssignedByServiceProvider = Nothing,
merchantTopUpAmount = Nothing,
..
}

Expand Down
22 changes: 0 additions & 22 deletions lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ module Kernel.External.Payout.Interface.Stripe
( createExternalPayout,
externalPayoutOrderStatus,
createTransfer,
getBalance,
getAvailableForCurrency,
payoutStripeServiceEventWebhook,
castPayoutStatus,
unPayoutId,
Expand Down Expand Up @@ -134,26 +132,6 @@ createTransfer config req = do
mkCreateTransferResp :: Stripe.TransferObject -> CreateTransferResp
mkCreateTransferResp Stripe.TransferObject {..} = CreateTransferResp {transferId = id, transferStatus = TRANSFERRED}

getBalance ::
( Metrics.CoreMetrics m,
EncFlow m r,
HasRequestId r,
MonadReader r m
) =>
StripeConfig ->
Maybe Text ->
m Stripe.BalanceResp
getBalance config mbConnectedAccountId = do
apiKey <- decrypt config.apiKey
Stripe.getBalance config.url apiKey mbConnectedAccountId

-- | Extract available balance for a specific currency from a Stripe BalanceResp.
-- Stripe amounts are in smallest currency unit (cents); converts to HighPrecMoney.
getAvailableForCurrency :: Currency -> Stripe.BalanceResp -> HighPrecMoney
getAvailableForCurrency currency Stripe.BalanceResp {available} =
let currencyText = T.toLower $ show currency
in sum [centsToUsd f.amount | f <- available, f.currency == currencyText]

payoutStripeServiceEventWebhook ::
( EncFlow m r,
HasRequestId r,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ data CreatePayoutOrderResp = CreatePayoutOrderResp
refunds :: Maybe [Text],
payments :: Maybe [Text],
fulfillments :: Maybe [Fulfillment],
customerId :: Maybe Text,
merchantTopUpAmount :: Maybe HighPrecMoney -- Stripe specific: extra amount transferred to cover Fleet VA shortfall
customerId :: Maybe Text
}
deriving (Show, Generic)
deriving anyclass (FromJSON, ToJSON, ToSchema)
Expand Down
40 changes: 0 additions & 40 deletions lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{-# LANGUAGE DerivingStrategies #-}

module Kernel.External.Payout.Stripe.Flow where

import qualified EulerHS.Types as Euler
Expand Down Expand Up @@ -141,41 +139,3 @@ createTransfer url apiKey connectedAccountId transferReq = do
let proxy = Proxy @CreateTransferAPI
eulerClient = Euler.client proxy (PaymentFlow.mkBasicAuthData apiKey) connectedAccountId transferReq
PaymentFlow.callStripeAPI url eulerClient "create-transfer" proxy

-- Balance types
data BalanceFund = BalanceFund
{ amount :: Int, -- Stripe amount in smallest currency unit (e.g. cents)
currency :: Text -- lowercase currency code, e.g. "eur"
}
deriving stock (Show, Generic)
deriving anyclass (FromJSON, ToJSON)

data BalanceResp = BalanceResp
{ available :: [BalanceFund],
pending :: [BalanceFund]
}
deriving stock (Show, Generic)
deriving anyclass (FromJSON, ToJSON)

-- GET /v1/balance
type GetBalanceAPI =
"v1"
:> "balance"
:> BasicAuth "secretkey-password" BasicAuthData
:> Header "Stripe-Account" Text -- Nothing = platform account, Just accountId = connected account
:> Get '[JSON] BalanceResp

getBalance ::
( Metrics.CoreMetrics m,
MonadFlow m,
HasRequestId r,
MonadReader r m
) =>
BaseUrl ->
Text ->
Maybe Text ->
m BalanceResp
getBalance url apiKey connectedAccountId = do
let proxy = Proxy @GetBalanceAPI
eulerClient = Euler.client proxy (PaymentFlow.mkBasicAuthData apiKey) connectedAccountId
PaymentFlow.callStripeAPI url eulerClient "get-balance" proxy
Loading