diff --git a/lib/mobility-core/mobility-core.cabal b/lib/mobility-core/mobility-core.cabal index 669b114ce..b693fec69 100644 --- a/lib/mobility-core/mobility-core.cabal +++ b/lib/mobility-core/mobility-core.cabal @@ -221,6 +221,7 @@ library Kernel.External.Payout.Stripe.Types Kernel.External.Payout.Stripe.Types.Common Kernel.External.Payout.Stripe.Types.Payout + Kernel.External.Payout.Stripe.Types.Transfer Kernel.External.Payout.Types Kernel.External.Plasma Kernel.External.Plasma.Interface diff --git a/lib/mobility-core/src/Kernel/External/Payment/Interface.hs b/lib/mobility-core/src/Kernel/External/Payment/Interface.hs index 43a6df8d9..1750d39ac 100644 --- a/lib/mobility-core/src/Kernel/External/Payment/Interface.hs +++ b/lib/mobility-core/src/Kernel/External/Payment/Interface.hs @@ -450,9 +450,9 @@ createRefund :: PaymentServiceConfig -> CreateRefundReq -> m CreateRefundResp -createRefund config paymentIntentId = case config of +createRefund config req = case config of JuspayConfig _ -> throwError $ InternalError "Juspay Create Refund not supported." - StripeConfig cfg -> Stripe.createRefund cfg paymentIntentId + StripeConfig cfg -> Stripe.createRefund cfg req PaytmEDCConfig _ -> throwError $ InternalError "PaytmEDC Create Refund not supported." getRefund :: @@ -464,9 +464,9 @@ getRefund :: PaymentServiceConfig -> GetRefundReq -> m GetRefundResp -getRefund config paymentIntentId = case config of +getRefund config req = case config of JuspayConfig _ -> throwError $ InternalError "Juspay Get Refund not supported." - StripeConfig cfg -> Stripe.getRefund cfg paymentIntentId + StripeConfig cfg -> Stripe.getRefund cfg req PaytmEDCConfig _ -> throwError $ InternalError "PaytmEDC Get Refund not supported." cancelRefund :: @@ -478,9 +478,9 @@ cancelRefund :: PaymentServiceConfig -> CancelRefundReq -> m CancelRefundResp -cancelRefund config paymentIntentId = case config of +cancelRefund config req = case config of JuspayConfig _ -> throwError $ InternalError "Juspay Cancel Refund not supported." - StripeConfig cfg -> Stripe.cancelRefund cfg paymentIntentId + StripeConfig cfg -> Stripe.cancelRefund cfg req PaytmEDCConfig _ -> throwError $ InternalError "PaytmEDC Cancel Refund not supported." verifyVPA :: diff --git a/lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs b/lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs index 1ac7c5b87..95eeb4139 100644 --- a/lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs +++ b/lib/mobility-core/src/Kernel/External/Payment/Interface/Stripe.hs @@ -5,6 +5,7 @@ module Kernel.External.Payment.Interface.Stripe where import Control.Applicative ((<|>)) +import qualified Data.Text as T import Data.Time import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Kernel.External.Encryption @@ -325,7 +326,7 @@ createPaymentIntent config req = do ConnectedAccount -> createConnectedAccountCharge url apiKey req where -- Platform Charge: No cloning, no on_behalf_of - createPlatformCharge url apiKey CreatePaymentIntentReq {amount = amonutInUsd, ..} = do + createPlatformCharge url apiKey CreatePaymentIntentReq {amount = amountInUsd, ..} = do let paymentIntentReq = mkPlatformPaymentIntentReq paymentIntentResp <- Stripe.createPaymentIntent url apiKey paymentIntentReq let paymentIntentId = paymentIntentResp.id @@ -335,12 +336,13 @@ createPaymentIntent config req = do where mkPlatformPaymentIntentReq :: Stripe.PaymentIntentReq mkPlatformPaymentIntentReq = - let application_fee_amount = eurToCents applicationFeeAmount - amountInCents = eurToCents amonutInUsd + let amountInCents = eurToCents amountInUsd payment_method = paymentMethod -- Use original payment method (NO cloning) receipt_email = receiptEmail on_behalf_of = Nothing -- OMIT for platform charges - transfer_data = Stripe.TransferData {destination = driverAccountId} + (transfer_data, application_fee_amount) = case config.transferPaymentToConnectedAccount of + Just False -> (Nothing, Nothing) + _ -> (Just $ Stripe.TransferData {destination = driverAccountId}, Just $ eurToCents applicationFeeAmount) -- True is default confirm = True description = Nothing setup_future_usage = Just Stripe.FutureUsageOffSession -- off_session: enables SCA exemption for saved cards @@ -365,13 +367,13 @@ createPaymentIntent config req = do return $ CreatePaymentIntentResp {..} where mkPaymentIntentReq :: PaymentMethodId -> CreatePaymentIntentReq -> Stripe.PaymentIntentReq - mkPaymentIntentReq clonedPaymentMethodId CreatePaymentIntentReq {amount = amonutInUsd, ..} = do - let application_fee_amount = usdToCents applicationFeeAmount - let amountInCents = usdToCents amonutInUsd + mkPaymentIntentReq clonedPaymentMethodId CreatePaymentIntentReq {amount = amountInUsd, ..} = do + let application_fee_amount = Just $ usdToCents applicationFeeAmount + let amountInCents = usdToCents amountInUsd let payment_method = clonedPaymentMethodId let receipt_email = receiptEmail let on_behalf_of = Just driverAccountId - let transfer_data = Stripe.TransferData {destination = driverAccountId} + let transfer_data = Just $ Stripe.TransferData {destination = driverAccountId} let confirm = True let description = Nothing let setup_future_usage = Just Stripe.FutureUsageOffSession -- off_session: enables SCA exemption for saved cards @@ -739,10 +741,10 @@ createRefund config req = do mkRefundResp <$> Stripe.createRefund url apiKey (Just req.driverAccountId) refundReq mkRefundReq :: CreateRefundReq -> Maybe Bool -> Stripe.RefundReq - mkRefundReq CreateRefundReq {amount = amonutInUsd, ..} reverse_transfer = + mkRefundReq CreateRefundReq {amount = amountInUsd, ..} reverse_transfer = let charge = Nothing payment_intent = Just req.paymentIntentId - amountInCents = eurToCents <$> amonutInUsd + amountInCents = eurToCents <$> amountInUsd metadata = Metadata {order_short_id = Just orderShortId, order_id = Just orderId, refunds_id = Just refundsId} refund_application_fee = Just req.refundApplicationFee instructions_email = req.email @@ -802,7 +804,7 @@ mkGetRefundResp Stripe.RefundObject {..} = refundsId = metadata >>= (.refunds_id), paymentIntentId = payment_intent, amount = centsToUsd amount, - currency, + currency = readMaybe . T.unpack $ T.toUpper currency, status = castRefundStatus status, reverseTransferId = transfer_reversal, errorCode = failure_reason diff --git a/lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs b/lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs index d6acf6fa3..56fdac1fa 100644 --- a/lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs +++ b/lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs @@ -924,7 +924,7 @@ data GetRefundResp = GetRefundResp refundsId :: Maybe Text, paymentIntentId :: Maybe PaymentIntentId, amount :: HighPrecMoney, - currency :: Currency, + currency :: Maybe Currency, status :: RefundStatus, reverseTransferId :: Maybe Text, errorCode :: Maybe Text diff --git a/lib/mobility-core/src/Kernel/External/Payment/Stripe/Config.hs b/lib/mobility-core/src/Kernel/External/Payment/Stripe/Config.hs index 099a3b10f..c99836309 100644 --- a/lib/mobility-core/src/Kernel/External/Payment/Stripe/Config.hs +++ b/lib/mobility-core/src/Kernel/External/Payment/Stripe/Config.hs @@ -36,6 +36,7 @@ data StripeCfg = StripeCfg url :: BaseUrl, businessProfile :: Maybe BusinessProfile, chargeDestination :: ChargeDestination, + transferPaymentToConnectedAccount :: Maybe Bool, webhookEndpointSecret :: Maybe (EncryptedField 'AsEncrypted Text), webhookToleranceSeconds :: Maybe Seconds, serviceMode :: Maybe ServiceMode, diff --git a/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/PaymentIntent.hs b/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/PaymentIntent.hs index ee335396a..a11a7f570 100644 --- a/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/PaymentIntent.hs +++ b/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/PaymentIntent.hs @@ -35,13 +35,13 @@ data PaymentIntentReq = PaymentIntentReq payment_method :: PaymentMethodId, receipt_email :: Maybe Text, setup_future_usage :: Maybe SetupFutureUsage, - application_fee_amount :: Int, + application_fee_amount :: Maybe Int, capture_method :: CaptureMethod, confirmation_method :: ConfirmationMethod, on_behalf_of :: Maybe AccountId, use_stripe_sdk :: Bool, return_url :: Text, - transfer_data :: TransferData + transfer_data :: Maybe TransferData } deriving stock (Show, Eq, Generic, Read) deriving anyclass (FromJSON, ToJSON, ToSchema) @@ -62,9 +62,9 @@ instance ToForm PaymentIntentReq where ("capture_method", [toQueryParam capture_method]), ("confirmation_method", [toQueryParam confirmation_method]), ("use_stripe_sdk", [toQueryParam use_stripe_sdk]), - ("return_url", [toQueryParam return_url]), - ("transfer_data[destination]", [toQueryParam transfer_data.destination]) + ("return_url", [toQueryParam return_url]) ] + <> maybeToForm "transfer_data[destination]" (transfer_data <&> (.destination)) <> maybeToForm "metadata[order_short_id]" metadata.order_short_id <> maybeToForm "description" description <> maybeToForm "receipt_email" receipt_email diff --git a/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs b/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs index 3b2aa0ce2..c082cbdfc 100644 --- a/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs +++ b/lib/mobility-core/src/Kernel/External/Payment/Stripe/Types/Refund.hs @@ -9,7 +9,6 @@ import Data.OpenApi (ToSchema (declareNamedSchema), genericDeclareNamedSchema) import Kernel.External.Payment.Stripe.Types.Common import Kernel.Prelude import Kernel.Types.HideSecrets -import Kernel.Types.Price (Currency) import Kernel.Utils.JSON import qualified Kernel.Utils.Schema as S import Web.FormUrlEncoded @@ -56,7 +55,7 @@ data RefundObject = RefundObject balance_transaction :: Maybe Text, -- Balance transaction that describes the impact of this refund on your account balance charge :: Maybe Text, -- ID of the charge that was refunded created :: Int, -- Time at which the refund was created - currency :: Currency, -- Currency of the refund + currency :: Text, -- Currency of the refund metadata :: Maybe Metadata, -- Set of key-value pairs attached to the refund payment_intent :: Maybe Text, -- ID of the PaymentIntent that was refunded reason :: Maybe RefundReason, -- Reason for the refund diff --git a/lib/mobility-core/src/Kernel/External/Payout/Interface.hs b/lib/mobility-core/src/Kernel/External/Payout/Interface.hs index c739f38c1..c730c12ed 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Interface.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Interface.hs @@ -17,12 +17,14 @@ module Kernel.External.Payout.Interface ) where +import qualified Kernel.External.Payment.Interface as Payment import qualified Kernel.External.Payout.Interface.Juspay as Juspay import qualified Kernel.External.Payout.Interface.Stripe as Stripe import Kernel.External.Payout.Interface.Types as Reexport import Kernel.External.Payout.Types as Reexport import Kernel.Prelude import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics) +import Kernel.Types.Error import Kernel.Utils.Common createPayoutOrder :: @@ -37,7 +39,70 @@ createPayoutOrder :: m CreatePayoutOrderResp createPayoutOrder serviceConfig req = case serviceConfig of JuspayConfig cfg -> Juspay.createPayoutOrder cfg req - StripeConfig cfg -> Stripe.createPayoutOrder cfg req + StripeConfig cfg -> do + connectedAccountId <- req.mConnectedAccountId & fromMaybeM (InvalidRequest "connectedAccountId required for Stripe payout") + createTransferResp <- Stripe.createTransfer cfg (mkTransferReq connectedAccountId req) + + result <- withTryCatch "createExternalPayout" $ Stripe.createExternalPayout cfg req + createExternalPayoutResp <- case result of + Right resp -> pure resp + Left e -> do + let err = fromException @Payment.StripeError e + errorCode = err <&> toErrorCode + errorMessage = err >>= toMessage + logError $ "Error while create external payout : " <> show err <> "error code : " <> show errorCode <> "error message : " <> show errorMessage <> " orderId: " <> req.orderId + pure + CreateExternalPayoutResp + { orderId = req.orderId, + externalPayoutStatus = EXTERNAL_PAYOUT_FAILED, + orderType = Just req.orderType, + idAssignedByServiceProvider = Nothing, + externalPayoutAmount = req.externalPayoutAmount, + customerId = Just req.customerId + } + pure $ mkCreatePayoutOrderResp createTransferResp createExternalPayoutResp + where + mkTransferReq :: Text -> CreatePayoutOrderReq -> CreateTransferReq + mkTransferReq connectedAccountId CreatePayoutOrderReq {..} = do + let senderAccountId = TransferPlatformAccount + destinationAccount = TransferConnectedAccount connectedAccountId + CreateTransferReq + { amount, + currency, + senderAccountId, + destinationAccount, + description = Just remark + } + + mkCreatePayoutOrderResp :: CreateTransferResp -> CreateExternalPayoutResp -> CreatePayoutOrderResp + mkCreatePayoutOrderResp CreateTransferResp {transferId} CreateExternalPayoutResp {..} = + CreatePayoutOrderResp + { orderId, + status = castExternalPayoutStatusToStatus externalPayoutStatus, + externalPayoutStatus = Just externalPayoutStatus, + orderType, + transferId = Just transferId, + idAssignedByServiceProvider, + udf1 = Nothing, + udf2 = Nothing, + udf3 = Nothing, + udf4 = Nothing, + udf5 = Nothing, + amount = req.amount, + refunds = Nothing, + payments = Nothing, + fulfillments = Nothing, + customerId + } + +-- IMPORTANT: always consider TRANSFERRED or SUCCESS if transfer was successfull, to avoid multiple charges +castExternalPayoutStatusToStatus :: ExternalPayoutStatus -> PayoutOrderStatus +castExternalPayoutStatusToStatus = \case + EXTERNAL_PAYOUT_PAID -> SUCCESS + EXTERNAL_PAYOUT_PENDING -> TRANSFERRED + EXTERNAL_PAYOUT_IN_TRANSIT -> TRANSFERRED + EXTERNAL_PAYOUT_FAILED -> TRANSFERRED + EXTERNAL_PAYOUT_CANCELED -> TRANSFERRED payoutOrderStatus :: ( EncFlow m r, @@ -50,4 +115,40 @@ payoutOrderStatus :: m PayoutOrderStatusResp payoutOrderStatus serviceConfig req = case serviceConfig of JuspayConfig cfg -> Juspay.payoutOrderStatus cfg req - StripeConfig cfg -> Stripe.payoutOrderStatus cfg req + StripeConfig cfg -> do + resp <- Stripe.externalPayoutOrderStatus cfg req + pure $ mkPayoutOrderStatusResp resp + where + mkPayoutOrderStatusResp :: ExternalPayoutOrderStatusResp -> PayoutOrderStatusResp + mkPayoutOrderStatusResp CreateExternalPayoutResp {..} = + CreatePayoutOrderResp + { orderId, + status = if isJust req.transferId then castExternalPayoutStatusToStatus externalPayoutStatus else req.currentStatus, -- extra check for req.transferId + externalPayoutStatus = Just externalPayoutStatus, + orderType, + transferId = req.transferId, + idAssignedByServiceProvider, + udf1 = Nothing, + udf2 = Nothing, + udf3 = Nothing, + udf4 = Nothing, + udf5 = Nothing, + amount = req.amount, + refunds = Nothing, + payments = Nothing, + fulfillments = Nothing, + customerId + } + +createTransfer :: + ( CoreMetrics m, + EncFlow m r, + HasRequestId r, + MonadReader r m + ) => + PayoutServiceConfig -> + CreateTransferReq -> + m CreateTransferResp +createTransfer config req = case config of + JuspayConfig _ -> throwError $ InternalError "Juspay Create Transfer not supported." + StripeConfig cfg -> Stripe.createTransfer cfg req diff --git a/lib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hs b/lib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hs index b92c5b378..91866e1d5 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Interface/Juspay.hs @@ -35,7 +35,7 @@ import qualified Kernel.External.Payout.Juspay.Types.Payout as Payout import qualified Kernel.External.Payout.Juspay.Webhook as Juspay import Kernel.Prelude import qualified Kernel.Tools.Metrics.CoreMetrics as Metrics -import Kernel.Types.Common +import Kernel.Types.Common hiding (LogLevel (ERROR)) import Kernel.Types.Error import Kernel.Types.Field import Kernel.Utils.Error.Throwing (fromMaybeM) @@ -113,7 +113,10 @@ createPayoutOrder config req = do mkCreatePayoutOrderResp Payout.PayoutOrderResp {..} = do CreatePayoutOrderResp { amount = realToFrac amount, + externalPayoutStatus = Nothing, + transferId = Nothing, idAssignedByServiceProvider = Nothing, + status = castPayoutOrderStatus status, .. } @@ -146,7 +149,10 @@ payoutOrderStatus config req = do mkPayoutOrderStatusResp Payout.PayoutOrderResp {..} = do CreatePayoutOrderResp { amount = realToFrac amount, + externalPayoutStatus = Nothing, + transferId = Nothing, idAssignedByServiceProvider = Nothing, + status = castPayoutOrderStatus status, .. } @@ -171,7 +177,7 @@ mkWebhookOrderStatusPayoutResp payoutReq = case payoutReq.label of OrderStatusPayoutResp { payoutOrderId = orderId, idAssignedByServiceProvider = Nothing, - payoutStatus = payoutReq.info.status, + payoutStatus = castPayoutOrderStatus payoutReq.info.status, orderType = payoutReq.info._type, merchantCustomerId = payoutReq.info.merchantCustomerId, amount = realToFrac payoutReq.info.amount, @@ -180,3 +186,25 @@ mkWebhookOrderStatusPayoutResp payoutReq = case payoutReq.label of } _ -> BadStatusResp _ -> BadStatusResp + +castPayoutOrderStatus :: Juspay.PayoutOrderStatus -> PayoutOrderStatus +castPayoutOrderStatus status = case status of + Juspay.READY_FOR_FULFILLMENT -> READY_FOR_FULFILLMENT + Juspay.FULFILLMENTS_SCHEDULED -> FULFILLMENTS_SCHEDULED + Juspay.FULFILLMENTS_FAILURE -> FULFILLMENTS_FAILURE + Juspay.FULFILLMENTS_SUCCESSFUL -> FULFILLMENTS_SUCCESSFUL + Juspay.FULFILLMENTS_CANCELLED -> FULFILLMENTS_CANCELLED + Juspay.FULFILLMENTS_MANUAL_REVIEW -> FULFILLMENTS_MANUAL_REVIEW + Juspay.FULFILLED_PARTIALLY -> FULFILLED_PARTIALLY + Juspay.INITIATED -> INITIATED + Juspay.FAILURE -> FAILURE + Juspay.SUCCESS -> SUCCESS + Juspay.DISCARDED -> DISCARDED + Juspay.MANUAL_REVIEW -> MANUAL_REVIEW + Juspay.CANCELLED -> CANCELLED + Juspay.GATEWAY_SWITCHED -> GATEWAY_SWITCHED + Juspay.ERROR -> ERROR + Juspay.INVALID -> INVALID + Juspay.VALID -> VALID + Juspay.CONFLICTED -> CONFLICTED + Juspay.REVERSED -> REVERSED diff --git a/lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs b/lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs index 6dd6aee3d..e4885d72f 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Interface/Stripe.hs @@ -1,15 +1,15 @@ module Kernel.External.Payout.Interface.Stripe - ( createPayoutOrder, - payoutOrderStatus, + ( createExternalPayout, + externalPayoutOrderStatus, + createTransfer, ) where import Control.Applicative ((<|>)) import qualified Data.Text as T import Kernel.External.Encryption -import qualified Kernel.External.Payment.Interface.Stripe as PaymentStripe +import Kernel.External.Payment.Interface.Stripe (centsToUsd, eurToCents, usdToCents) import Kernel.External.Payout.Interface.Types -import qualified Kernel.External.Payout.Juspay.Types.Payout as Juspay import Kernel.External.Payout.Stripe.Config as Reexport import qualified Kernel.External.Payout.Stripe.Flow as Stripe import qualified Kernel.External.Payout.Stripe.Types as Stripe @@ -18,25 +18,24 @@ import qualified Kernel.Tools.Metrics.CoreMetrics as Metrics import Kernel.Types.Error import Kernel.Utils.Common -createPayoutOrder :: +createExternalPayout :: ( Metrics.CoreMetrics m, EncFlow m r, HasRequestId r, MonadReader r m ) => StripeConfig -> - CreatePayoutOrderReq -> - m CreatePayoutOrderResp -createPayoutOrder config req = do + CreateExternalPayoutReq -> + m CreateExternalPayoutResp +createExternalPayout config req = do apiKey <- decrypt config.apiKey let url = config.url stripeResp <- Stripe.createPayout url apiKey req.mConnectedAccountId (mkCreatePayoutReq req) - pure $ mkCreatePayoutOrderResp req.orderId (Just req) stripeResp + pure $ mkCreateExternalPayoutResp req.orderId (Just req) stripeResp where - -- Interface request is payout-order shaped (Juspay), so map to Stripe payout request. mkCreatePayoutReq CreatePayoutOrderReq {..} = Stripe.CreatePayoutReq - { amount = PaymentStripe.usdToCents amount, + { amount = usdToCents externalPayoutAmount, currency = T.toLower $ show currency, description = Just remark, destination = mExternalAccountId, @@ -52,48 +51,62 @@ createPayoutOrder config req = do } } -payoutOrderStatus :: +externalPayoutOrderStatus :: ( Metrics.CoreMetrics m, EncFlow m r, HasRequestId r, MonadReader r m ) => StripeConfig -> - PayoutOrderStatusReq -> - m PayoutOrderStatusResp -payoutOrderStatus config req = do + ExternalPayoutOrderStatusReq -> + m ExternalPayoutOrderStatusResp +externalPayoutOrderStatus config req = do apiKey <- decrypt config.apiKey let url = config.url payoutId <- req.idAssignedByServiceProvider & fromMaybeM (InvalidRequest "id assigned by service provider required for Stripe payout") stripeResp <- Stripe.getPayout url apiKey req.mConnectedAccountId (Stripe.PayoutId payoutId) - pure $ mkCreatePayoutOrderResp req.orderId Nothing stripeResp + pure $ mkCreateExternalPayoutResp req.orderId Nothing stripeResp -mkCreatePayoutOrderResp :: Text -> Maybe CreatePayoutOrderReq -> Stripe.PayoutObject -> CreatePayoutOrderResp -mkCreatePayoutOrderResp reqOrderId mbRequest stripeResp = - CreatePayoutOrderResp +mkCreateExternalPayoutResp :: Text -> Maybe CreateExternalPayoutReq -> Stripe.PayoutObject -> CreateExternalPayoutResp +mkCreateExternalPayoutResp reqOrderId mbRequest stripeResp = + CreateExternalPayoutResp { orderId = fromMaybe reqOrderId $ stripeResp.metadata >>= (.order_id), + externalPayoutStatus = stripeResp.status, idAssignedByServiceProvider = Just $ unPayoutId stripeResp.id, - status = castPayoutStatus stripeResp.status, orderType = (stripeResp.metadata >>= (.order_type)) <|> (mbRequest <&> (.orderType)), - udf1 = Nothing, - udf2 = Nothing, - udf3 = Nothing, - udf4 = Nothing, - udf5 = Nothing, - amount = PaymentStripe.centsToUsd stripeResp.amount, - refunds = Nothing, - payments = Nothing, - fulfillments = Nothing, + externalPayoutAmount = centsToUsd stripeResp.amount, customerId = (stripeResp.metadata >>= (.customer_id)) <|> (mbRequest <&> (.customerId)) } unPayoutId :: Stripe.PayoutId -> Text unPayoutId (Stripe.PayoutId payoutId) = payoutId -castPayoutStatus :: Stripe.PayoutStatus -> Juspay.PayoutOrderStatus -castPayoutStatus = \case - Stripe.PAYOUT_PENDING -> Juspay.INITIATED - Stripe.PAYOUT_IN_TRANSIT -> Juspay.INITIATED - Stripe.PAYOUT_PAID -> Juspay.SUCCESS - Stripe.PAYOUT_FAILED -> Juspay.FAILURE - Stripe.PAYOUT_CANCELED -> Juspay.CANCELLED +createTransfer :: + forall m r. + ( Metrics.CoreMetrics m, + EncFlow m r, + HasRequestId r, + MonadReader r m + ) => + StripeConfig -> + CreateTransferReq -> + m CreateTransferResp +createTransfer config req = do + let url = config.url + apiKey <- decrypt config.apiKey + transferReq <- buildCreateTransferReq req + let senderAccountId = case req.senderAccountId of + TransferConnectedAccount accountId -> Just accountId + TransferPlatformAccount -> Nothing + mkCreateTransferResp <$> Stripe.createTransfer url apiKey senderAccountId transferReq + where + buildCreateTransferReq :: CreateTransferReq -> m Stripe.TransferReq + buildCreateTransferReq CreateTransferReq {amount = amountInUsd, ..} = do + let amountInCents = eurToCents amountInUsd + destination <- case destinationAccount of + TransferConnectedAccount accountId -> pure accountId + TransferPlatformAccount -> config.platformAccountId & fromMaybeM (InternalError "STRIPE_PLATFORM_ACCOUNT_ID_NOT_FOUND") + pure Stripe.TransferReq {amount = amountInCents, metadata = Nothing, currency = T.toLower $ show currency, ..} + + mkCreateTransferResp :: Stripe.TransferObject -> CreateTransferResp + mkCreateTransferResp Stripe.TransferObject {..} = CreateTransferResp {transferId = id} diff --git a/lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs b/lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs index ac3a0bc02..bc83a8bcd 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs @@ -23,9 +23,11 @@ module Kernel.External.Payout.Interface.Types where import Kernel.Beam.Lib.UtilsTH (mkBeamInstancesForEnum) +import qualified Kernel.External.Payment.Stripe.Types as Stripe import qualified Kernel.External.Payout.Juspay.Config as Juspay -import Kernel.External.Payout.Juspay.Types as Reexport (Fulfillment (..), PayoutOrderStatus (..)) +import Kernel.External.Payout.Juspay.Types as Reexport (Fulfillment (..)) import qualified Kernel.External.Payout.Stripe.Config as Stripe +import Kernel.External.Payout.Stripe.Types as Reexport (ExternalPayoutStatus (..), TransferId (..)) import Kernel.Prelude import Kernel.Storage.Esqueleto (derivePersistField) import Kernel.Types.Common @@ -34,6 +36,31 @@ import Servant.API (ToHttpApiData (..)) data PayoutServiceConfig = JuspayConfig Juspay.JuspayConfig | StripeConfig Stripe.StripeConfig deriving (Show, Eq, Generic, ToJSON, FromJSON) +data PayoutOrderStatus + = READY_FOR_FULFILLMENT + | FULFILLMENTS_SCHEDULED + | FULFILLMENTS_FAILURE + | FULFILLMENTS_SUCCESSFUL + | FULFILLMENTS_CANCELLED + | FULFILLMENTS_MANUAL_REVIEW + | FULFILLED_PARTIALLY + | INITIATED + | FAILURE + | SUCCESS + | DISCARDED + | MANUAL_REVIEW + | CANCELLED + | GATEWAY_SWITCHED + | ERROR + | INVALID + | VALID + | CONFLICTED + | REVERSED + | TRANSFERRED -- Stripe specific + deriving (Show, Generic, Ord, Read, FromJSON, ToJSON, ToSchema, Eq) + +$(mkBeamInstancesForEnum ''PayoutOrderStatus) + data OrderStatusPayoutResp = OrderStatusPayoutResp { payoutOrderId :: Text, @@ -54,6 +81,7 @@ type AccountId = Text data CreatePayoutOrderReq = CreatePayoutOrderReq { orderId :: Text, amount :: HighPrecMoney, + externalPayoutAmount :: HighPrecMoney, currency :: Currency, customerPhone :: Text, customerEmail :: Text, @@ -64,7 +92,7 @@ data CreatePayoutOrderReq = CreatePayoutOrderReq customerVpa :: Maybe Text, -- Juspay specific isDynamicWebhookRequired :: Bool, mRoutingId :: Maybe Text, -- Juspay specific - mConnectedAccountId :: Maybe AccountId, -- Stripe specific + mConnectedAccountId :: Maybe Stripe.AccountId, -- Stripe specific mExternalAccountId :: Maybe Text -- Stripe specific, default will be used in case of Nothing } deriving stock (Show, Eq, Generic) @@ -72,9 +100,11 @@ data CreatePayoutOrderReq = CreatePayoutOrderReq data CreatePayoutOrderResp = CreatePayoutOrderResp { orderId :: Text, - idAssignedByServiceProvider :: Maybe Text, -- Stripe specific - status :: PayoutOrderStatus, + status :: PayoutOrderStatus, -- payout (transfer) status from platform to driver/fleet + externalPayoutStatus :: Maybe ExternalPayoutStatus, -- Stripe specific: payout status from driver/fleet connected account to driver/fleet bank account/card orderType :: Maybe Text, + transferId :: Maybe TransferId, -- Stripe specific + idAssignedByServiceProvider :: Maybe Text, -- Stripe specific udf1 :: Maybe Text, udf2 :: Maybe Text, udf3 :: Maybe Text, @@ -103,12 +133,44 @@ instance ToHttpApiData Expand where data PayoutOrderStatusReq = PayoutOrderStatusReq { orderId :: Text, + amount :: HighPrecMoney, idAssignedByServiceProvider :: Maybe Text, -- Stripe specific mbExpand :: Maybe Expand, -- Juspay specific mRoutingId :: Maybe Text, -- Juspay specific - mConnectedAccountId :: Maybe AccountId -- Stripe specific + mConnectedAccountId :: Maybe AccountId, -- Stripe specific + currentStatus :: PayoutOrderStatus, + transferId :: Maybe TransferId } deriving (Show, Generic) deriving anyclass (FromJSON, ToJSON) type PayoutOrderStatusResp = CreatePayoutOrderResp + +type CreateExternalPayoutReq = CreatePayoutOrderReq + +type ExternalPayoutOrderStatusReq = PayoutOrderStatusReq + +data CreateExternalPayoutResp = CreateExternalPayoutResp + { orderId :: Text, + externalPayoutStatus :: ExternalPayoutStatus, -- Stripe specific: payout status from driver/fleet connected account to driver/fleet bank account/card + orderType :: Maybe Text, + idAssignedByServiceProvider :: Maybe Text, -- Stripe specific + externalPayoutAmount :: HighPrecMoney, + customerId :: Maybe Text + } + +type ExternalPayoutOrderStatusResp = CreateExternalPayoutResp + +data TransferAccount = TransferConnectedAccount AccountId | TransferPlatformAccount + +data CreateTransferReq = CreateTransferReq + { amount :: HighPrecMoney, + currency :: Currency, + senderAccountId :: TransferAccount, + destinationAccount :: TransferAccount, + description :: Maybe Text + } + +newtype CreateTransferResp = CreateTransferResp + { transferId :: TransferId + } diff --git a/lib/mobility-core/src/Kernel/External/Payout/Juspay/Types/Payout.hs b/lib/mobility-core/src/Kernel/External/Payout/Juspay/Types/Payout.hs index 0ddd74429..22d12da5c 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Juspay/Types/Payout.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Juspay/Types/Payout.hs @@ -45,8 +45,6 @@ data PayoutOrderStatus | REVERSED deriving (Show, Generic, Ord, Read, FromJSON, ToJSON, ToSchema, Eq) -$(mkBeamInstancesForEnum ''PayoutOrderStatus) - data WebhookDetails = WebhookDetails { username :: Maybe Text, password :: Maybe Text, diff --git a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Config.hs b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Config.hs index b5741e0aa..824b7b5b9 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Config.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Config.hs @@ -16,6 +16,7 @@ module Kernel.External.Payout.Stripe.Config where import Data.Aeson import Kernel.External.Encryption import qualified Kernel.External.Payment.Stripe.Config as PaymentConfig +import Kernel.External.Payment.Stripe.Types.Common (AccountId) import Kernel.Prelude import Kernel.Types.Common @@ -24,7 +25,8 @@ data StripeConfig = StripeConfig url :: BaseUrl, webhookEndpointSecret :: Maybe (EncryptedField 'AsEncrypted Text), webhookToleranceSeconds :: Maybe Seconds, - serviceMode :: Maybe PaymentConfig.ServiceMode + serviceMode :: Maybe PaymentConfig.ServiceMode, + platformAccountId :: Maybe AccountId } deriving stock (Show, Eq, Generic) deriving anyclass (FromJSON, ToJSON) diff --git a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs index 7b82fe04d..32589ead9 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Flow.hs @@ -94,7 +94,7 @@ type ListPayoutsAPI = :> QueryParam "limit" Int :> QueryParam "starting_after" Text :> QueryParam "ending_before" Text - :> QueryParam "status" PayoutStatus + :> QueryParam "status" ExternalPayoutStatus :> Get '[JSON] PayoutList listPayouts :: @@ -109,9 +109,33 @@ listPayouts :: Maybe Int -> -- limit Maybe Text -> -- starting_after Maybe Text -> -- ending_before - Maybe PayoutStatus -> -- status filter + Maybe ExternalPayoutStatus -> -- status filter m PayoutList listPayouts url apiKey connectedAccountId limit startingAfter endingBefore status = do let proxy = Proxy @ListPayoutsAPI eulerClient = Euler.client proxy (PaymentFlow.mkBasicAuthData apiKey) connectedAccountId limit startingAfter endingBefore status PaymentFlow.callStripeAPI url eulerClient "list-payouts" proxy + +type CreateTransferAPI = + "v1" + :> "transfers" + :> BasicAuth "secretkey-password" BasicAuthData + :> Header "Stripe-Account" Text + :> ReqBody '[FormUrlEncoded] TransferReq + :> Post '[JSON] TransferObject + +createTransfer :: + ( Metrics.CoreMetrics m, + MonadFlow m, + HasRequestId r, + MonadReader r m + ) => + BaseUrl -> + Text -> + Maybe Text -> + TransferReq -> + m TransferObject +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 diff --git a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types.hs b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types.hs index ad3c0f25d..ac87f3c5f 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types.hs @@ -5,3 +5,4 @@ where import Kernel.External.Payout.Stripe.Types.Common as Reexport import Kernel.External.Payout.Stripe.Types.Payout as Reexport +import Kernel.External.Payout.Stripe.Types.Transfer as Reexport diff --git a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Payout.hs b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Payout.hs index c0e189d2c..6cd129097 100644 --- a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Payout.hs +++ b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Payout.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} module Kernel.External.Payout.Stripe.Types.Payout where @@ -7,6 +8,7 @@ import Data.Aeson import qualified Data.HashMap.Strict as HM import Data.OpenApi (ToSchema (declareNamedSchema), genericDeclareNamedSchema) import Data.Time.Clock.POSIX (POSIXTime) +import Kernel.Beam.Lib.UtilsTH (mkBeamInstancesForEnum) import Kernel.External.Payout.Stripe.Types.Common import Kernel.Prelude import Kernel.Utils.JSON @@ -19,46 +21,49 @@ newtype PayoutId = PayoutId Text deriving stock (Generic, Show, Eq) deriving newtype (FromJSON, ToJSON, ToSchema, FromHttpApiData, ToHttpApiData) -data PayoutStatus - = PAYOUT_PENDING - | PAYOUT_IN_TRANSIT - | PAYOUT_PAID - | PAYOUT_FAILED - | PAYOUT_CANCELED - deriving stock (Show, Eq, Generic) +-- Payout status from driver/fleet connected account to driver/fleet card or bank account +data ExternalPayoutStatus + = EXTERNAL_PAYOUT_PENDING + | EXTERNAL_PAYOUT_IN_TRANSIT + | EXTERNAL_PAYOUT_PAID + | EXTERNAL_PAYOUT_FAILED + | EXTERNAL_PAYOUT_CANCELED + deriving stock (Show, Read, Eq, Ord, Generic) deriving anyclass (ToSchema) -instance FromJSON PayoutStatus where - parseJSON = withText "PayoutStatus" $ \case - "pending" -> pure PAYOUT_PENDING - "in_transit" -> pure PAYOUT_IN_TRANSIT - "paid" -> pure PAYOUT_PAID - "failed" -> pure PAYOUT_FAILED - "canceled" -> pure PAYOUT_CANCELED +instance FromJSON ExternalPayoutStatus where + parseJSON = withText "ExternalPayoutStatus" $ \case + "pending" -> pure EXTERNAL_PAYOUT_PENDING + "in_transit" -> pure EXTERNAL_PAYOUT_IN_TRANSIT + "paid" -> pure EXTERNAL_PAYOUT_PAID + "failed" -> pure EXTERNAL_PAYOUT_FAILED + "canceled" -> pure EXTERNAL_PAYOUT_CANCELED _ -> fail "Invalid payout status" -instance ToJSON PayoutStatus where - toJSON PAYOUT_PENDING = String "pending" - toJSON PAYOUT_IN_TRANSIT = String "in_transit" - toJSON PAYOUT_PAID = String "paid" - toJSON PAYOUT_FAILED = String "failed" - toJSON PAYOUT_CANCELED = String "canceled" - -instance ToHttpApiData PayoutStatus where - toQueryParam PAYOUT_PENDING = "pending" - toQueryParam PAYOUT_IN_TRANSIT = "in_transit" - toQueryParam PAYOUT_PAID = "paid" - toQueryParam PAYOUT_FAILED = "failed" - toQueryParam PAYOUT_CANCELED = "canceled" - -instance FromHttpApiData PayoutStatus where - parseQueryParam "pending" = Right PAYOUT_PENDING - parseQueryParam "in_transit" = Right PAYOUT_IN_TRANSIT - parseQueryParam "paid" = Right PAYOUT_PAID - parseQueryParam "failed" = Right PAYOUT_FAILED - parseQueryParam "canceled" = Right PAYOUT_CANCELED +instance ToJSON ExternalPayoutStatus where + toJSON EXTERNAL_PAYOUT_PENDING = String "pending" + toJSON EXTERNAL_PAYOUT_IN_TRANSIT = String "in_transit" + toJSON EXTERNAL_PAYOUT_PAID = String "paid" + toJSON EXTERNAL_PAYOUT_FAILED = String "failed" + toJSON EXTERNAL_PAYOUT_CANCELED = String "canceled" + +instance ToHttpApiData ExternalPayoutStatus where + toQueryParam EXTERNAL_PAYOUT_PENDING = "pending" + toQueryParam EXTERNAL_PAYOUT_IN_TRANSIT = "in_transit" + toQueryParam EXTERNAL_PAYOUT_PAID = "paid" + toQueryParam EXTERNAL_PAYOUT_FAILED = "failed" + toQueryParam EXTERNAL_PAYOUT_CANCELED = "canceled" + +instance FromHttpApiData ExternalPayoutStatus where + parseQueryParam "pending" = Right EXTERNAL_PAYOUT_PENDING + parseQueryParam "in_transit" = Right EXTERNAL_PAYOUT_IN_TRANSIT + parseQueryParam "paid" = Right EXTERNAL_PAYOUT_PAID + parseQueryParam "failed" = Right EXTERNAL_PAYOUT_FAILED + parseQueryParam "canceled" = Right EXTERNAL_PAYOUT_CANCELED parseQueryParam _ = Left "Invalid payout status" +$(mkBeamInstancesForEnum ''ExternalPayoutStatus) + data PayoutType = Card | BankAccount deriving stock (Show, Eq, Generic) deriving anyclass (ToSchema) @@ -131,7 +136,7 @@ data PayoutObject = PayoutObject { id :: PayoutId, amount :: Int, currency :: Text, - status :: PayoutStatus, + status :: ExternalPayoutStatus, _type :: PayoutType, method :: PayoutMethod, description :: Maybe Text, diff --git a/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs new file mode 100644 index 000000000..f4e4ab357 --- /dev/null +++ b/lib/mobility-core/src/Kernel/External/Payout/Stripe/Types/Transfer.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} + +module Kernel.External.Payout.Stripe.Types.Transfer where + +import Data.Aeson +import qualified Data.HashMap.Strict as HM +import Data.Time.Clock.POSIX (POSIXTime) +import Kernel.External.Payment.Stripe.Types.Common +import Kernel.Prelude +import Kernel.Utils.JSON +import Web.FormUrlEncoded +import Web.HttpApiData (ToHttpApiData (..)) + +newtype TransferId = TransferId {getTransferId :: Text} + deriving stock (Generic, Show, Eq) + deriving newtype (FromJSON, ToJSON, ToSchema) + +data TransferReq = TransferReq + { amount :: Int, + currency :: Text, + destination :: AccountId, + metadata :: Maybe Metadata, + description :: Maybe Text + } + deriving stock (Show, Generic) + +instance ToForm TransferReq where + toForm TransferReq {..} = + Form $ + HM.fromList $ + catMaybes + [ Just . ("amount",) . pure $ toQueryParam amount, + Just . ("currency",) . pure $ toQueryParam currency, + Just . ("destination",) . pure $ toQueryParam destination, + ("description",) . pure . toQueryParam <$> description + ] + +-- TODO webhook transfer.created, transfer.reversed, transfer.updated +-- Currently transfer api would throw error instead of transfer object in case of failure, so there is no status field +data TransferObject = TransferObject + { id :: TransferId, + _object :: Text, + amount :: Int, + created :: POSIXTime, + currency :: Text, + destination :: AccountId + } + deriving stock (Show, Generic) + +instance FromJSON TransferObject where + parseJSON = genericParseJSON stripPrefixUnderscoreIfAny + +instance ToJSON TransferObject where + toJSON = genericToJSON stripPrefixUnderscoreIfAny