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
15 changes: 15 additions & 0 deletions lib/mobility-core/src/Kernel/External/Payment/Interface.hs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ mandateExecution serviceConfig mRoutingId req = case serviceConfig of
StripeConfig _ -> throwError $ InternalError "Stripe Mandate Execution not supported."
PaytmEDCConfig _ -> throwError $ InternalError "PaytmEDC Mandate Execution not supported."

getMandateStatus ::
( EncFlow m r,
CoreMetrics m,
HasRequestId r,
MonadReader r m
) =>
PaymentServiceConfig ->
Maybe Text ->
MandateStatusReq ->
m OrderStatusResp
getMandateStatus serviceConfig mRoutingId req = case serviceConfig of
JuspayConfig cfg -> Juspay.getMandateStatus cfg mRoutingId req
StripeConfig _ -> throwError $ InternalError "Stripe Mandate Status not supported."
PaytmEDCConfig _ -> throwError $ InternalError "PaytmEDC Mandate Status not supported."

autoRefunds ::
( EncFlow m r,
CoreMetrics m,
Expand Down
41 changes: 41 additions & 0 deletions lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module Kernel.External.Payment.Interface.Juspay
mandateRevoke,
mandatePause,
mandateResume,
getMandateStatus,
autoRefund,
mandateNotificationStatus,
verifyVPA,
Expand Down Expand Up @@ -582,6 +583,46 @@ mkResumeReq req =
resume_date = show $ utcTimeToPOSIXSeconds req.resumeDate
}

getMandateStatus ::
( HasCallStack,
Metrics.CoreMetrics m,
EncFlow m r,
HasRequestId r,
MonadReader r m,
MonadThrow m
) =>
JuspayCfg ->
Maybe Text ->
MandateStatusReq ->
m OrderStatusResp
getMandateStatus config mRoutingId req = do
let url = config.url
merchantId = config.merchantId
apiKey <- decrypt config.apiKey
juspayResp <- Juspay.mandateStatus url apiKey merchantId mRoutingId req.mandateId Juspay.MandateStatusReq {command = "check_status"}
mkMandateStatusRes juspayResp

mkMandateStatusRes :: (MonadThrow m, Log m) => Juspay.JuspayMandateStatusResp -> m OrderStatusResp
mkMandateStatusRes Juspay.JuspayMandateStatusResp {..} = do
mandateStatusEnum <- (readMaybe (T.unpack status) :: Maybe MandateStatus) & fromMaybeM (InternalError $ "Invalid mandate status: " <> status)
frequencyEnum <- (readMaybe (T.unpack frequency) :: Maybe MandateFrequency) & fromMaybeM (InternalError $ "Invalid mandate frequency: " <> frequency)
startDateUTC <- (posixSecondsToUTCTime <$> (fromIntegral <$> (readMaybe (T.unpack start_date) :: Maybe Int))) & fromMaybeM (InternalError "Invalid start_date format")
endDateUTC <- (posixSecondsToUTCTime <$> (fromIntegral <$> (readMaybe (T.unpack end_date) :: Maybe Int))) & fromMaybeM (InternalError "Invalid end_date format")
return
MandateStatusResp
{ eventName = Nothing,
orderShortId = fromMaybe "" order_id,
status = mandateStatusEnum,
mandateStartDate = Just startDateUTC,
mandateEndDate = Just endDateUTC,
mandateId = mandate_id,
mandateFrequency = frequencyEnum,
mandateMaxAmount = realToFrac max_amount,
upi = mkUpi <$> (payment_info >>= (.upi))
}
Comment on lines +613 to +622
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

orderShortId silently defaults to empty string when order_id is absent.

order_id is Maybe Text in JuspayMandateStatusResp, and here it's coalesced to "". Downstream code that treats orderShortId as a required, non-empty identifier (e.g., for lookups/logging correlation) will silently misbehave. Consider either:

  • Logging at Info/Warn when order_id is missing, or
  • Making orderShortId on MandateStatusResp a Maybe Text, or
  • Failing with a clear InternalError if it's truly required.

Given this is a mandate-status response (not necessarily tied to a single order), the middle option is typically the cleanest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs` around
lines 613 - 622, The code currently coerces JuspayMandateStatusResp.order_id to
an empty string when building MandateStatusResp.orderShortId (fromMaybe ""
order_id); instead change MandateStatusResp.orderShortId to a Maybe Text and
propagate the original order_id directly (i.e., assign orderShortId = order_id)
so absence is preserved; update the data type declaration for MandateStatusResp
and any places that construct or consume orderShortId (search for
MandateStatusResp, orderShortId usage) to handle Maybe Text accordingly, and add
minimal handling where a non-empty identifier is required (e.g., explicit error
or logging) rather than silently using "".

where
mkUpi Juspay.MandateUpiInfo {..} = Upi {payerApp = Nothing, payerAppName = Nothing, txnFlowType = Nothing, payerVpa = payer_vpa}

addDaysUtcTime :: UTCTime -> Integer -> UTCTime
addDaysUtcTime t x = t {utctDay = addDays x (utctDay t)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ data MandatePauseReq = MandatePauseReq {mandateId :: Text, pauseStartDate :: UTC

data MandateResumeReq = MandateResumeReq {mandateId :: Text, resumeDate :: UTCTime}

newtype MandateStatusReq = MandateStatusReq {mandateId :: Text}

newtype MandateRevokeReq = MandateRevokeReq {mandateId :: Text}

type MandateRevokeRes = APISuccess
Expand Down
32 changes: 32 additions & 0 deletions lib/mobility-core/src/Kernel/External/Payment/Juspay/Flow.hs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,38 @@ mandateResume url apiKey mandateId req = do
callAPI url (eulerClient mandateId basicAuthData req) "mandate-resume" (Proxy @MandateResumeAPI)
>>= fromEitherM (\err -> InternalError $ "Failed to call mandate resume API: " <> show err)

type MandateStatusAPI =
"mandates"
:> Header "x-merchantid" Text
:> Header "x-routing-id" Text
:> BasicAuth "username-password" BasicAuthData
:> Capture "mandateId" Text
:> ReqBody '[FormUrlEncoded] MandateStatusReq
:> Post '[JSON] JuspayMandateStatusResp

mandateStatus ::
( Metrics.CoreMetrics m,
MonadFlow m,
HasRequestId r,
MonadReader r m
) =>
BaseUrl ->
Text ->
Text ->
Maybe Text ->
Text ->
MandateStatusReq ->
m JuspayMandateStatusResp
mandateStatus url apiKey merchantId mRoutingId mandateId req = do
let eulerClient = Euler.client (Proxy @MandateStatusAPI)
let basicAuthData =
BasicAuthData
{ basicAuthUsername = DT.encodeUtf8 apiKey,
basicAuthPassword = ""
}
callAPI url (eulerClient (Just merchantId) mRoutingId basicAuthData mandateId req) "mandate-status" (Proxy @MandateStatusAPI)
>>= fromEitherM (\err -> InternalError $ "Failed to call mandate status API: " <> show err)

type AutoRefundAPI =
"orders"
:> Capture "orderId" Text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,32 @@ data MandateResumeReq = MandateResumeReq
resume_date :: Text
}
deriving (Eq, Show, Generic, ToJSON, FromJSON, ToSchema, ToForm)

--- For Mandate Status ---

data MandateStatusReq = MandateStatusReq
{ command :: Text
}
deriving (Eq, Show, Generic, ToJSON, FromJSON, ToSchema, ToForm)

data JuspayMandateStatusResp = JuspayMandateStatusResp
{ mandate_id :: Text,
status :: Text,
frequency :: Text,
start_date :: Text,
end_date :: Text,
max_amount :: Double,
order_id :: Maybe Text,
payment_info :: Maybe MandatePaymentInfo
}
deriving (Eq, Show, Generic, ToJSON, FromJSON, ToSchema)
Comment on lines +159 to +169
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

max_amount :: Double loses precision; use HighPrecMoney for consistency.

Other mandate payloads in this codebase model max_amount as HighPrecMoney (see the webhook path at lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs line 669, where justMandate.max_amount is assigned directly to mandateMaxAmount :: HighPrecMoney without conversion). Modeling the status response's max_amount as Double here introduces precision loss and diverges from the rest of the payment module. The downstream mapper at Interface/Juspay.hs line 620 compensates with realToFrac max_amount, which is unnecessary if the field is typed as HighPrecMoney to begin with.

♻️ Proposed change
 data JuspayMandateStatusResp = JuspayMandateStatusResp
   { mandate_id :: Text,
     status :: Text,
     frequency :: Text,
     start_date :: Text,
     end_date :: Text,
-    max_amount :: Double,
+    max_amount :: HighPrecMoney,
     order_id :: Maybe Text,
     payment_info :: Maybe MandatePaymentInfo
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mobility-core/src/Kernel/External/Payment/Juspay/Types/Mandate.hs` around
lines 159 - 169, The JuspayMandateStatusResp type declares max_amount :: Double
which causes precision loss; change the field to max_amount :: HighPrecMoney and
update its JSON instances if necessary so it deserializes as HighPrecMoney; then
remove the compensating realToFrac conversion in the downstream mapper (the code
that assigns justMandate.max_amount to mandateMaxAmount in Interface/Juspay.hs
and the mapping around line 620) so the value flows as HighPrecMoney end-to-end.


data MandatePaymentInfo = MandatePaymentInfo
{ upi :: Maybe MandateUpiInfo
}
deriving (Eq, Show, Generic, ToJSON, FromJSON, ToSchema)

data MandateUpiInfo = MandateUpiInfo
{ payer_vpa :: Maybe Text
}
deriving (Eq, Show, Generic, ToJSON, FromJSON, ToSchema)
Loading