Skip to content

backend/enhancement/added-mandate-status-check-support#1235

Open
dushyant-2002 wants to merge 1 commit into
mainfrom
backend/enhancement/added-mandate-status-check-support
Open

backend/enhancement/added-mandate-status-check-support#1235
dushyant-2002 wants to merge 1 commit into
mainfrom
backend/enhancement/added-mandate-status-check-support

Conversation

@dushyant-2002
Copy link
Copy Markdown
Contributor

@dushyant-2002 dushyant-2002 commented Apr 24, 2026

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates

Description

Additional Changes

  • This PR modifies the database schema (database migration added)
  • This PR modifies dhall configs/environment variables

Motivation and Context

How did you test it?

Checklist

  • I formatted the code and addressed linter errors ./dev/format-all-files.sh
  • I reviewed submitted code
  • I added unit tests for my changes where possible
  • I added a CHANGELOG entry if applicable

Summary by CodeRabbit

Release Notes

  • New Features
    • Added mandate status checking functionality, enabling users to query the current status, dates, and payment details of their payment mandates through the Juspay payment provider.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Walkthrough

This pull request adds mandate status lookup functionality for the Juspay payment provider. It introduces new request/response types, integrates a Juspay API endpoint, and provides router logic that delegates mandate status queries to Juspay while rejecting the operation for other providers.

Changes

Cohort / File(s) Summary
External Payment Interface Layer
lib/mobility-core/src/Kernel/External/Payment/Interface.hs, lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs, lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs
Added public getMandateStatus router function that dispatches to Juspay while failing fast for Stripe and PaytmEDC. Introduced MandateStatusReq type containing mandate ID. Implemented Juspay-specific handler with response mapping, strict parsing for status/frequency/dates, and UPI data extraction.
Juspay Integration Layer
lib/mobility-core/src/Kernel/External/Payment/Juspay/Flow.hs, lib/mobility-core/src/Kernel/External/Payment/Juspay/Types/Mandate.hs
Added Juspay Servant API type and mandateStatus function for direct API invocation with merchant/routing headers and basic auth. Defined data models for mandate status request and response, including nested payment/UPI info structures with JSON serialization.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Interface as Interface.getMandateStatus
    participant Juspay as Juspay.getMandateStatus
    participant Flow as Juspay.Flow.mandateStatus
    participant JuspayAPI as Juspay API
    
    Client->>Interface: getMandateStatus(config, routingId, req)
    Interface->>Interface: Pattern match on config
    alt JuspayConfig
        Interface->>Juspay: getMandateStatus(config, routingId, req)
        Juspay->>Juspay: Decrypt API key
        Juspay->>Flow: mandateStatus(baseUrl, apiKey, merchantId, routingId, mandateId)
        Flow->>Flow: Create BasicAuthData
        Flow->>JuspayAPI: POST /mandate/{mandateId}
        JuspayAPI-->>Flow: JuspayMandateStatusResp
        Flow-->>Juspay: Response
        Juspay->>Juspay: Parse & map to OrderStatusResp
        Juspay-->>Interface: OrderStatusResp
    else StripeConfig or PaytmEDCConfig
        Interface-->>Client: InternalError (not supported)
    end
    Interface-->>Client: OrderStatusResp
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A mandate's status now we seek,
Through Juspay's API, strong and sleek,
New types defined, requests take flight,
The routing layer gets it right,
Payment flows more clear and bright! 💳

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding mandate status check support for payment processing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend/enhancement/added-mandate-status-check-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
lib/mobility-core/src/Kernel/External/Payment/Juspay/Flow.hs (1)

486-494: Nit: reuse mkBasicAuthData helper.

This function re-implements the basic auth construction inline (lines 488-492). The module already defines mkBasicAuthData at line 277 which several other endpoints in this file use (e.g., createOrder, verifyVPA). Using the helper here would be consistent with newer wrappers and drop 5 lines. Not blocking — many existing mandate* functions in this file also inline it.

♻️ Proposed change
 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`)
+  callAPI url (eulerClient (Just merchantId) mRoutingId (mkBasicAuthData apiKey) mandateId req) "mandate-status" (Proxy `@MandateStatusAPI`)
     >>= fromEitherM (\err -> InternalError $ "Failed to call mandate status API: " <> show err)
🤖 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/Flow.hs` around lines
486 - 494, Replace the inline BasicAuthData construction in mandateStatus with
the existing mkBasicAuthData helper: in the mandateStatus function, remove the
manual BasicAuthData block and call mkBasicAuthData apiKey (or the appropriate
signature) to produce basicAuthData, then pass that into the existing callAPI
invocation (keeping the Euler.client (Proxy `@MandateStatusAPI`), merchantId,
mRoutingId, mandateId and req parameters intact); this reuses the shared helper
used by createOrder and verifyVPA and keeps behavior identical.
lib/mobility-core/src/Kernel/External/Payment/Juspay/Types/Mandate.hs (1)

154-157: Prefer newtype for single-field record.

MandateStatusReq has a single command field; a newtype is more efficient and consistent with MandateRevokeReq on line 124.

♻️ Proposed change
-data MandateStatusReq = MandateStatusReq
-  { command :: Text
-  }
-  deriving (Eq, Show, Generic, ToJSON, FromJSON, ToSchema, ToForm)
+newtype MandateStatusReq = MandateStatusReq
+  { command :: Text
+  }
+  deriving (Eq, Show, Generic, ToJSON, FromJSON, ToSchema, ToForm)
🤖 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 154 - 157, MandateStatusReq is defined as a single-field record; replace
the data declaration with a newtype to match MandateRevokeReq and gain
efficiency. Change the declaration of MandateStatusReq (and keep the field name
command for external compatibility) from a data record to a newtype and retain
the same deriving list (Eq, Show, Generic, ToJSON, FromJSON, ToSchema, ToForm),
adding any necessary LANGUAGE pragmas (e.g., GeneralizedNewtypeDeriving) if
required by the derived classes.
lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs (1)

602-602: Hard-coded "check_status" command — consider a named constant.

The command literal "check_status" is embedded at the interface layer. Other mandate operations in this file use the same pattern (e.g., "revoke", "pause", "resume"), so this is consistent, but if Juspay introduces additional commands for the status endpoint, a small constant at the top of the module would ease maintenance.

🤖 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` at line
602, Extract the literal "check_status" into a named top-level constant (e.g.,
mandateStatusCmd or cmdCheckStatus) in this module and replace the inline string
in the Juspay.mandateStatus call (and any other mandate command literals like
"revoke", "pause", "resume" if present) with that constant; update the
Juspay.mandateStatus invocation (the call site referencing juspayResp and
Juspay.MandateStatusReq) to use the new constant so future command additions are
centralized and easier to maintain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs`:
- Around line 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 "".

In `@lib/mobility-core/src/Kernel/External/Payment/Juspay/Types/Mandate.hs`:
- Around line 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.

---

Nitpick comments:
In `@lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs`:
- Line 602: Extract the literal "check_status" into a named top-level constant
(e.g., mandateStatusCmd or cmdCheckStatus) in this module and replace the inline
string in the Juspay.mandateStatus call (and any other mandate command literals
like "revoke", "pause", "resume" if present) with that constant; update the
Juspay.mandateStatus invocation (the call site referencing juspayResp and
Juspay.MandateStatusReq) to use the new constant so future command additions are
centralized and easier to maintain.

In `@lib/mobility-core/src/Kernel/External/Payment/Juspay/Flow.hs`:
- Around line 486-494: Replace the inline BasicAuthData construction in
mandateStatus with the existing mkBasicAuthData helper: in the mandateStatus
function, remove the manual BasicAuthData block and call mkBasicAuthData apiKey
(or the appropriate signature) to produce basicAuthData, then pass that into the
existing callAPI invocation (keeping the Euler.client (Proxy `@MandateStatusAPI`),
merchantId, mRoutingId, mandateId and req parameters intact); this reuses the
shared helper used by createOrder and verifyVPA and keeps behavior identical.

In `@lib/mobility-core/src/Kernel/External/Payment/Juspay/Types/Mandate.hs`:
- Around line 154-157: MandateStatusReq is defined as a single-field record;
replace the data declaration with a newtype to match MandateRevokeReq and gain
efficiency. Change the declaration of MandateStatusReq (and keep the field name
command for external compatibility) from a data record to a newtype and retain
the same deriving list (Eq, Show, Generic, ToJSON, FromJSON, ToSchema, ToForm),
adding any necessary LANGUAGE pragmas (e.g., GeneralizedNewtypeDeriving) if
required by the derived classes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5cd2022-fcb4-4721-b428-5b9ce905d1a7

📥 Commits

Reviewing files that changed from the base of the PR and between f8e71b7 and 8007efa.

📒 Files selected for processing (5)
  • lib/mobility-core/src/Kernel/External/Payment/Interface.hs
  • lib/mobility-core/src/Kernel/External/Payment/Interface/Juspay.hs
  • lib/mobility-core/src/Kernel/External/Payment/Interface/Types.hs
  • lib/mobility-core/src/Kernel/External/Payment/Juspay/Flow.hs
  • lib/mobility-core/src/Kernel/External/Payment/Juspay/Types/Mandate.hs

Comment on lines +613 to +622
{ 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))
}
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 "".

Comment on lines +159 to +169
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)
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant