Skip to content

Added challan service provider#1327

Open
RitoBose wants to merge 1 commit into
mainfrom
feat/challan/signzy
Open

Added challan service provider#1327
RitoBose wants to merge 1 commit into
mainfrom
feat/challan/signzy

Conversation

@RitoBose

@RitoBose RitoBose commented Jun 3, 2026

Copy link
Copy Markdown

Type of Change

  • Bugfix
  • [ x] New feature
  • Enhancement
  • Refactoring
  • Dependency updates

Description

Added Challan Service Provider

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

  • New Features
    • Vehicle challan search via Signzy: query by vehicle number to retrieve pending challan counts.
    • Public request/response types and a service config (includes encrypted API key) added.
    • Signzy API client and integration added for calling the endpoint.
    • Response filtering to count pending challans, plus improved logging and clearer error mapping.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a Signzy challan-search integration: service enum and domain types, Signzy config, Servant API client, interface types, Signzy implementation that filters pending challans, a dispatcher, and cabal exports.

Changes

Challan Search Service

Layer / File(s) Summary
Service types, domain types, and configuration
lib/mobility-core/src/Kernel/External/ChallanSearch/Types.hs, lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs, lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Config.hs
ChallanSearchService enum defines Signzy. Signzy domain types model request/response/result/detail structures. SignzyChallanSearchCfg holds url and encrypted apiKey.
Interface types and public contract
lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Types.hs
Adds ChallanSearchServiceConfig (wraps Signzy cfg), PendingChallanReq { vehicleNumber :: Text }, and PendingChallanResp { pendingChallanCount :: Int }.
Signzy API client
lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/API.hs
Servant API type for api/v3/vehicle/challan-search with Authorization header, typed client challanSearchClient, and challanSearch wrapper converting failures to ExternalAPICallError.
Service implementation and dispatcher
lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs, lib/mobility-core/src/Kernel/External/ChallanSearch/Interface.hs
signzyChallanSearch decrypts API key, builds request from vehicleNumber (paymentCheck = True), calls Signzy API, filters challanDetails where challanStatus (lowercased) == "pending", and returns count. getPendingChallanCount dispatches on ChallanSearchServiceConfig.
Module exports
lib/mobility-core/mobility-core.cabal
Adds seven modules to exposed-modules: interface, interface types, Signzy API/config/types, and shared types.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant getPendingChallanCount
  participant signzyChallanSearch
  participant challanSearchClient
  participant SignzyService
  Client->>getPendingChallanCount: ChallanSearchServiceConfig, PendingChallanReq
  getPendingChallanCount->>signzyChallanSearch: SignzyChallanSearchCfg, PendingChallanReq
  signzyChallanSearch->>signzyChallanSearch: Decrypt apiKey
  signzyChallanSearch->>challanSearchClient: Build ChallanSearchReq (vehicleNumber, paymentCheck=True)
  challanSearchClient->>SignzyService: HTTP POST with Authorization header
  SignzyService-->>challanSearchClient: ChallanSearchResp
  signzyChallanSearch->>signzyChallanSearch: Filter challanDetails where challanStatus == "pending"
  signzyChallanSearch-->>getPendingChallanCount: PendingChallanResp
  getPendingChallanCount-->>Client: PendingChallanResp
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • khuzema786
  • Vignesh-772
  • bytedex

Poem

🐰 I hopped through types and keys today,
Decrypted secrets on the Signzy way,
I called the endpoint, filtered slow,
Counted pending fines in a row,
A tiny rabbit, with code to show.

🚥 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 'Added challan service provider' accurately captures the main change: introducing a new Signzy challan search service provider integration with modules, types, and API wrappers.
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 feat/challan/signzy

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.

@coderabbitai coderabbitai Bot requested review from Vignesh-772 and khuzema786 June 3, 2026 08:17
@RitoBose RitoBose force-pushed the feat/challan/signzy branch from cdbae4c to 1299767 Compare June 3, 2026 08:26
@RitoBose

RitoBose commented Jun 3, 2026

Copy link
Copy Markdown
Author

@ClaudeWeb review this PR real quick.

@khuzema786

Copy link
Copy Markdown
Contributor

Let me fetch the PR diff, description, and comments in parallel.The gh CLI has a token scope issue. Let me try the GitHub MCP tool instead.Let me look at how similar service providers are structured in the codebase for comparison.Let me look at a comparable existing service (like RC/DL search) for pattern comparison, and also check the semgrep tools for security scanning.I have all the context needed. Here's the full review:


PR Review: Added Challan Service Provider (#1327)

Verdict: Request Changes. The integration structure is clean and follows repo conventions, but there's a likely runtime JSON parsing bug and a few other issues worth addressing before merge.


🔴 Critical

1. Snake_case fields will break JSON deserialization at runtime

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs

ChallanSearchResult mixes snake_case and camelCase field names:

data ChallanSearchResult = ChallanSearchResult
  { regNo             :: Text,         -- camelCase
    message           :: Text,
    status_code       :: Int,          -- ← snake_case
    challanDetails    :: [ChallanDetail],
    successfulSources :: [Text],
    failedSources     :: [Text]
  }
  deriving (Show, Generic, ToJSON, FromJSON)

data ChallanDetail = ChallanDetail
  { ...
    court_challan  :: Maybe Text,      -- ← snake_case
    ...
  }

With deriving (Generic, FromJSON), Aeson uses the Haskell field names verbatim as JSON keys. Since every other field in these records is camelCase (regNo, challanDetails, successfulSources), the Signzy API almost certainly returns "statusCode" and "courtChallan" — not "status_code" / "court_challan". status_code :: Int is non-Maybe, so a key mismatch causes the entire response to fail to parse, surfacing as an InternalError at runtime.

Fix: Rename to camelCase, or add explicit Aeson options if the API genuinely uses snake_case:

-- Option A: rename to match the API's actual casing
data ChallanSearchResult = ChallanSearchResult
  { ...
    statusCode :: Int,
    ...
  }

-- Option B: if the API truly mixes cases, use explicit field mapping
instance FromJSON ChallanSearchResult where
  parseJSON = withObject "ChallanSearchResult" $ \v ->
    ChallanSearchResult
      <$> v .: "regNo"
      <*> v .: "message"
      <*> v .: "status_code"
      ...

🟡 Warning

2. Case-sensitive "Pending" match may silently return 0

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs:20

isPending :: CST.ChallanDetail -> Bool
isPending detail = detail.challanStatus == Just "Pending"

This is an exact, case-sensitive match. If the Signzy API returns "PENDING", "pending", or a variant like "Pending Payment", every challan will be silently dropped and pendingChallanCount will always be 0. This kind of silent data loss is hard to debug in production.

Fix: Either document the exact values the API returns (with a source link/comment) or use case-insensitive comparison:

isPending detail = fmap (Text.toLower) detail.challanStatus == Just "pending"

3. ChallanSearchResp doesn't handle API-level error responses

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs:10

newtype ChallanSearchResp = ChallanSearchResp
  { result :: ChallanSearchResult
  }

result is non-optional. If Signzy returns an error envelope (e.g., {"error": "...", "status": 400}) without a result key, the FromJSON parse will fail and bubble up as a generic InternalError. The error message from API.hs will obscure the actual Signzy error details.

Consider wrapping result in Maybe and checking status_code (once fixed), or adding an error-case constructor.

4. challanDetails list missing error source context

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs:16

failedSources :: [Text] is collected in the response but Interface/Signzy.hs ignores it entirely. If some sources failed, the count could be incomplete — callers get no signal that the result is partial. At minimum this should be logged:

signzyChallanSearch cfg req = do
  ...
  unless (null signzyResp.result.failedSources) $
    logWarning $ "Challan search had failed sources: " <> show signzyResp.result.failedSources
  pure $ CIT.PendingChallanResp {pendingChallanCount = length pendingChallans}

🔵 Suggestion

5. ChallanSearch/Types.hs is disconnected from the rest of the module

Kernel.External.ChallanSearch.Types exports ChallanSearchService = Signzy with Beam instances, but nothing in the new files imports or uses it. This is fine if consuming code (in the main app repo) will reference it — but it would help to add a note or ensure it's actually wired up before merging to avoid dead module exports.

6. PR checklist is entirely unchecked

None of the checklist items (format, self-review, tests, CHANGELOG) are checked. Even if no unit tests are feasible for an external API integration, the formatting and self-review boxes should be checked before requesting review.

@RitoBose RitoBose force-pushed the feat/challan/signzy branch from 1299767 to 60604b4 Compare June 3, 2026 12:13
@coderabbitai coderabbitai Bot requested a review from bytedex June 3, 2026 12:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Types.hs`:
- Line 10: This interface type is deriving JSON instances (FromJSON, ToJSON)
which couples the interface to a JSON contract; remove FromJSON and ToJSON from
the deriving clause so the type only derives Show, Eq, Generic (i.e., delete the
ToJSON/FromJSON symbols from the deriving list in
lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Types.hs). Repeat
the same removal for the other interface types flagged (the other deriving
clauses at the same file locations referenced in the comment) to keep interface
types decoupled from JSON serialization.
🪄 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: 9226b87b-aba1-411a-9b10-dc0725100f87

📥 Commits

Reviewing files that changed from the base of the PR and between cdbae4c and 60604b4.

📒 Files selected for processing (8)
  • lib/mobility-core/mobility-core.cabal
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Types.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/API.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Config.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Types.hs
🚧 Files skipped from review as they are similar to previous changes (7)
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface.hs
  • lib/mobility-core/mobility-core.cabal
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Types.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Config.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/API.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs

import Kernel.Prelude

data ChallanSearchServiceConfig = SignzyChallanSearch Signzy.SignzyChallanSearchCfg
deriving (Show, Eq, Generic, FromJSON, ToJSON)

@coderabbitai coderabbitai Bot Jun 3, 2026

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 | ⚡ Quick win

Avoid JSON instances on interface boundary types.

These interface types should stay decoupled from JSON contracts to avoid accidental UI/API coupling and backward-compatibility lock-in. Please remove ToJSON/FromJSON derivations here.

Suggested change
 data ChallanSearchServiceConfig = SignzyChallanSearch Signzy.SignzyChallanSearchCfg
-  deriving (Show, Eq, Generic, FromJSON, ToJSON)
+  deriving (Show, Eq, Generic)

 newtype PendingChallanReq = PendingChallanReq
   { vehicleNumber :: Text
   }
-  deriving (Show, Generic, ToJSON, FromJSON)
+  deriving (Show, Generic)

 newtype PendingChallanResp = PendingChallanResp
   { pendingChallanCount :: Int
   }
-  deriving (Show, Generic, ToJSON, FromJSON)
+  deriving (Show, Generic)

Based on learnings, in lib/mobility-core/src/Kernel/External/Payout/Interface/Types.hs interface types intentionally do not derive ToJSON/FromJSON to prevent accidental UI usage and compatibility issues.

Also applies to: 15-15, 20-20

🤖 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/ChallanSearch/Interface/Types.hs` at
line 10, This interface type is deriving JSON instances (FromJSON, ToJSON) which
couples the interface to a JSON contract; remove FromJSON and ToJSON from the
deriving clause so the type only derives Show, Eq, Generic (i.e., delete the
ToJSON/FromJSON symbols from the deriving list in
lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Types.hs). Repeat
the same removal for the other interface types flagged (the other deriving
clauses at the same file locations referenced in the comment) to keep interface
types decoupled from JSON serialization.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@RitoBose Did you check this ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@RitoBose RitoBose force-pushed the feat/challan/signzy branch from 60604b4 to c703e08 Compare June 3, 2026 13:36
signzyChallanSearch :: (EncFlow m r, CoreMetrics m, Log m, HasRequestId r, MonadReader r m) => CSC.SignzyChallanSearchCfg -> CIT.PendingChallanReq -> m CIT.PendingChallanResp
signzyChallanSearch cfg req = do
apiKey <- decrypt cfg.apiKey
let signzyReq = CST.ChallanSearchReq {vehicleNumber = req.vehicleNumber, paymentCheck = True}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

paymentCheck was not required to pass.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes I tried that previously but the signzy API returns an error : {
"error": {
"name": "error",
"message": "Error in getting data",
"status": 409,
"reason": "CONFLICT",
"type": "Conflict",
"statusCode": 409
}
}
when we don't pass paymentCheck=true.

let pendingChallans = filter isPending signzyResp.result.challanDetails
pure $ CIT.PendingChallanResp {pendingChallanCount = length pendingChallans}
where
isPending :: CST.ChallanDetail -> Bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add toLowerCase and then compare it with "pending" ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Resolved

rto :: Maybe Text,
accusedName :: Maybe Text,
amount :: Maybe Text,
challanStatus :: Maybe Text,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is mandatory for us.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Resolved


data ChallanDetail = ChallanDetail
{ number :: Maybe Int,
challanNumber :: Maybe Text,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This also

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest of the fields can be removed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Resolved

@RitikaHanish

Copy link
Copy Markdown
Member

@RitoBose Handle the error case properly.

@RitoBose RitoBose force-pushed the feat/challan/signzy branch from c703e08 to 64597eb Compare June 4, 2026 09:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs`:
- Around line 16-24: The type ChallanSearchResult currently derives
ToJSON/FromJSON via Generic which leaks Haskell field names; replace this with
explicit Aeson instances: implement a FromJSON and ToJSON for
ChallanSearchResult (or use Aeson.withObject and object builders) that map each
Haskell field (regNo, message, status_code, challanDetails, successfulSources,
failedSources) to the exact Signzy JSON keys expected by the provider (verify
and use their canonical keys — e.g. "regNo" vs "reg_no" or "regno", "statusCode"
vs "status_code", "challanDetails" spelling), avoid relying on Generic or
default Aeson fieldLabelModifier, and mirror the same key mapping in both
FromJSON and ToJSON to keep round-trip stability.
🪄 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: 802dd7b7-4412-42f5-8bec-d4286c3b5b26

📥 Commits

Reviewing files that changed from the base of the PR and between c703e08 and 64597eb.

📒 Files selected for processing (8)
  • lib/mobility-core/mobility-core.cabal
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Types.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/API.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Config.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Types.hs
🚧 Files skipped from review as they are similar to previous changes (6)
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/API.hs
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Types.hs
  • lib/mobility-core/mobility-core.cabal
  • lib/mobility-core/src/Kernel/External/ChallanSearch/Types.hs

Comment on lines +16 to +24
data ChallanSearchResult = ChallanSearchResult
{ regNo :: Text,
message :: Text,
status_code :: Int,
challanDetails :: [ChallanDetail],
successfulSources :: [Text],
failedSources :: [Text]
}
deriving (Show, Generic, ToJSON, FromJSON)

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 | ⚡ Quick win

Make Signzy JSON key mapping explicit (don’t rely on Generic field-label casing).

ChallanSearchResult in lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs derives ToJSON/FromJSON via Generic, so Aeson keys are derived from the Haskell record labels (regNo, status_code, etc.). This is brittle for a third-party provider where casing may differ. Implement explicit FromJSON/ToJSON instances with stable Signzy field names (and avoid relying on the proposed diff’s key casing/typos like chalanDetails unless they match the actual provider payload).

🤖 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/ChallanSearch/Signzy/Types.hs` around
lines 16 - 24, The type ChallanSearchResult currently derives ToJSON/FromJSON
via Generic which leaks Haskell field names; replace this with explicit Aeson
instances: implement a FromJSON and ToJSON for ChallanSearchResult (or use
Aeson.withObject and object builders) that map each Haskell field (regNo,
message, status_code, challanDetails, successfulSources, failedSources) to the
exact Signzy JSON keys expected by the provider (verify and use their canonical
keys — e.g. "regNo" vs "reg_no" or "regno", "statusCode" vs "status_code",
"challanDetails" spelling), avoid relying on Generic or default Aeson
fieldLabelModifier, and mirror the same key mapping in both FromJSON and ToJSON
to keep round-trip stability.

@RitoBose RitoBose force-pushed the feat/challan/signzy branch 2 times, most recently from 1cdaa34 to 1a8983b Compare June 5, 2026 06:25
@RitikaHanish

Copy link
Copy Markdown
Member

@ClaudeWeb Can you review this PR by making sure, it's implemented as per proper design principles and proper error handling is happening.

@khuzema786

Copy link
Copy Markdown
Contributor

Let me pull up existing similar service implementations for comparison.Let me look at comparable existing service implementations to benchmark the design.Let me look at a closely comparable existing service (Verification with Signzy) to benchmark design patterns.Let me read the actual content of the Insurance interface and a comparable API file to benchmark patterns.Let me read the Insurance interface Types and Acko implementation to see the full design pattern.Now let me check the Acko Types to see how they handle snake_case API fields — the key design question.Now I have the full picture. Here's the design-focused review:


Design & Error Handling Review — PR #1327 (Challan Service Provider)

Verdict: Request Changes. The module structure is correct but deviates from the established codebase pattern in three ways that matter: the adapter layer skips the conversion-function contract, the Types layer has a likely parse bug, and error handling is thin throughout.

I benchmarked this against the most comparable service — Kernel.External.Insurance (specifically Acko) — since it was recently added, follows the same three-tier design, and also uses a Signzy-family API with encryption.


🔴 Critical — Runtime Bug

1. Mixed snake_case/camelCase in Signzy/Types.hs will break JSON parsing

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs

The established pattern in this codebase is: provider-specific types use the exact field names the API sends, verbatim (see Insurance/Acko/Types.hs where policy_id, reference_id, plan_type are all snake_case because Acko's API sends them that way).

The problem here is inconsistency within the same record:

data ChallanSearchResult = ChallanSearchResult
  { regNo             :: Text,          -- camelCase
    message           :: Text,
    status_code       :: Int,           -- ← snake_case (will parse as "status_code")
    challanDetails    :: [ChallanDetail], -- camelCase
    successfulSources :: [Text],
    failedSources     :: [Text]
  }

data ChallanDetail = ChallanDetail
  { challanNumber  :: Maybe Text,       -- camelCase
    ...
    court_challan  :: Maybe Text,       -- ← snake_case
    ...
  }

With deriving (Generic, FromJSON), Aeson uses field names verbatim. Every other field here is camelCase, which means Signzy's API almost certainly sends "statusCode" and "courtChallan" — not "status_code" / "court_challan".

  • status_code :: Int is non-Maybe, so a key mismatch causes the entire response to fail to parse, surfacing as an InternalError at runtime — not a missing value, a total failure.

Fix: Rename to match the actual API response, keeping the same convention as all other fields:

statusCode   :: Int,
courtChallan :: Maybe Text,

🟠 Design Principle Violations

2. Adapter layer is missing explicit conversion functions (confidence 90)

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs

The established pattern (Insurance/Interface/Acko.hs) requires named conversion functions between domain types and provider types. Compare:

Existing pattern:

-- Insurance/Interface/Acko.hs
toAckoInsuranceRequest :: InsuranceRequest -> AckoTypes.AckoInsuranceRequest
toAckoInsuranceRequest req = AckoTypes.AckoInsuranceRequest { ... }

fromAckoInsuranceResponse :: AckoTypes.AckoInsuranceResponse -> InsuranceResponse
fromAckoInsuranceResponse resp = InsuranceResponse { ... }

This PR's approach:

signzyChallanSearch cfg req = do
  let signzyReq = CST.ChallanSearchReq {vehicleNumber = req.vehicleNumber}  -- inline, unnamed
  ...
  pure $ CIT.PendingChallanResp {pendingChallanCount = length pendingChallans}  -- inline, unnamed

This is acceptable for a simple 1-to-1 today, but it violates the contract of this layer. When the Signzy API adds fields (which it will — this is v3, and challans have rich data), there's no natural place to evolve the mapping. The pattern exists precisely to keep provider complexity contained.

Fix:

signzyChallanSearch cfg req = do
  apiKey <- decrypt cfg.apiKey
  signzyResp <- CSA.challanSearch cfg.url apiKey (toSignzyReq req)
  pure $ fromSignzyResp signzyResp

toSignzyReq :: CIT.PendingChallanReq -> CST.ChallanSearchReq
toSignzyReq req = CST.ChallanSearchReq {vehicleNumber = req.vehicleNumber}

fromSignzyResp :: CST.ChallanSearchResp -> CIT.PendingChallanResp
fromSignzyResp resp =
  CIT.PendingChallanResp {pendingChallanCount = length (filter isPending resp.result.challanDetails)}
  where
    isPending d = d.challanStatus == Just "Pending"

3. ChallanSearch/Types.hs uses generic JSON derivation instead of custom instances (confidence 85)

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Types.hs

The Insurance/Types.hs pattern uses explicit FromJSON/ToJSON instances with proper error messages:

-- Insurance/Types.hs (existing)
instance FromJSON InsuranceService where
  parseJSON (String "Acko")      = pure Acko
  parseJSON (String "IffcoTokio") = pure IffcoTokio
  parseJSON (String _)           = parseFail "Expected \"Acko\" or \"IffcoTokio\""
  parseJSON e                    = typeMismatch "String" e

The PR uses:

-- ChallanSearch/Types.hs (new)
data ChallanSearchService = Signzy
  deriving (Show, Read, Eq, Ord, Generic, ToJSON, FromJSON)

Generic derivation produces {"tag":"Signzy"} by default, which differs from the "Signzy" string format used consistently across all service enums in the codebase. A config mismatch will fail silently with a confusing error.

Fix: Match the established pattern:

instance FromJSON ChallanSearchService where
  parseJSON (String "Signzy") = pure Signzy
  parseJSON (String _)        = parseFail "Expected \"Signzy\""
  parseJSON e                 = typeMismatch "String" e

instance ToJSON ChallanSearchService where
  toJSON = String . show

4. Missing derivePersistField (confidence 75 — worth checking)

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Types.hs

Insurance/Types.hs includes derivePersistField "InsuranceService" alongside the Beam TH splice. If the application layer intends to store the ChallanSearchService type in a Persistent/Esqueleto column, this is missing. Worth confirming with the author.


🟡 Error Handling Gaps

5. isPending is a silent data sink — case-sensitive, no fallback logging

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs:20

isPending detail = detail.challanStatus == Just "Pending"

Two issues:

  • Case-sensitive: If Signzy sends "PENDING" or "pending", every challan is silently dropped and pendingChallanCount = 0 — indistinguishable from "no challans" to callers.
  • No logging: When the filter produces 0 from a non-empty list, there's no diagnostic trace.

Fix:

signzyChallanSearch cfg req = do
  ...
  let allChallans    = signzyResp.result.challanDetails
      pendingChallans = filter isPending allChallans
  logDebug $ "Challan search: total=" <> show (length allChallans)
           <> " pending=" <> show (length pendingChallans)
  pure $ CIT.PendingChallanResp {pendingChallanCount = length pendingChallans}
  where
    isPending d = fmap (Text.toLower) d.challanStatus == Just "pending"

6. failedSources is silently ignored — callers get no signal of partial results

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Interface/Signzy.hs

ChallanSearchResult has failedSources :: [Text] from the Signzy response. The code ignores it entirely. If failedSources is non-empty, the returned pendingChallanCount could be understated and the caller has no way to know.

unless (null signzyResp.result.failedSources) $
  logWarning $ "Signzy challan search partial failure — sources: "
             <> show signzyResp.result.failedSources

At minimum, log a warning. Depending on SLAs, this might warrant returning an error instead.

7. ChallanSearchResp has no error envelope handling

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs

newtype ChallanSearchResp = ChallanSearchResp
  { result :: ChallanSearchResult  -- non-Maybe, required
  }

Signzy's API (v3) returns error envelopes like {"error": "...", "statusCode": 400} when requests fail. Since result is non-Maybe, Aeson parse failure produces a generic InternalError "Failed to call Signzy Challan Search API: ..." which swallows the actual Signzy error detail.

Fix: Wrap result in Maybe and check status_code (once field naming is fixed), or add an explicit error constructor:

data ChallanSearchResp
  = ChallanSearchSuccess { result :: ChallanSearchResult }
  | ChallanSearchError   { errorMessage :: Text, statusCode :: Int }
  deriving (Show, Generic)

instance FromJSON ChallanSearchResp where
  parseJSON v = (ChallanSearchSuccess <$> parseJSON v)
            <|> (ChallanSearchError   <$> parseJSON v)

Summary Table

# File Issue Severity
1 Signzy/Types.hs status_code/court_challan snake_case breaks JSON parse 🔴 Critical
2 Interface/Signzy.hs No named conversion functions — deviates from adapter pattern 🟠 Design
3 ChallanSearch/Types.hs Generic JSON on enum — wrong format, no error messages 🟠 Design
4 ChallanSearch/Types.hs Missing derivePersistField (check intent) 🟠 Design
5 Interface/Signzy.hs isPending case-sensitive, no diagnostic logging 🟡 Error Handling
6 Interface/Signzy.hs failedSources silently ignored 🟡 Error Handling
7 Signzy/Types.hs No error envelope in response type 🟡 Error Handling

@RitikaHanish

Copy link
Copy Markdown
Member

@ClaudeWeb Can you pull latest code and then review ? Just flag is any critical thing needs to be fixed.
Other modifications give as suggestion.

@khuzema786

Copy link
Copy Markdown
Contributor

Fetching all the latest file contents from the PR branch in parallel.Got the latest code. Good news — several previous concerns are already addressed. Here's the focused review:


Review of Latest Branch (feat/challan/signzy)

Verdict: Almost there. One critical item remains; everything else is a suggestion.

What's been fixed since last review ✅

  • court_challan snake_case field removed
  • isPending is now case-insensitive (T.toLower)
  • logInfo added for pending vs. total count diagnostics
  • Error handling in API.hs upgraded to ExternalAPICallError with a named error code — good

🔴 Critical — Must Fix

status_code :: Int is still snake_case and will break JSON parsing at runtime

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs

data ChallanSearchResult = ChallanSearchResult
  { regNo             :: Text,    -- camelCase
    message           :: Text,    -- camelCase
    status_code       :: Int,     -- ← snake_case  ⚠️
    challanDetails    :: [ChallanDetail],
    successfulSources :: [Text],
    failedSources     :: [Text]
  }
  deriving (Show, Generic, ToJSON, FromJSON)

With deriving (Generic, FromJSON), Aeson uses field names verbatim as JSON keys. Every other field here is camelCase — which means Signzy's API sends "statusCode", not "status_code". Since this field is Int (not Maybe Int), a key mismatch causes the entire ChallanSearchResult to fail to parse, making every API call throw a runtime error.

Fix — rename to match the other fields:

statusCode :: Int,

💡 Suggestions (non-blocking)

1. paymentCheck = True is hardcoded with no documentation

File: Interface/Signzy.hs

let signzyReq = CST.ChallanSearchReq {vehicleNumber = req.vehicleNumber, paymentCheck = True}

If True is always the correct value here, add a comment explaining why (e.g., -- required to get challan payment status from Signzy). If it ever needs to vary per caller, it belongs in PendingChallanReq or SignzyChallanSearchCfg.

2. failedSources is still silently ignored

File: Interface/Signzy.hs

The logInfo added is great. But if failedSources is non-empty, the count could be understated and the caller has no idea. A one-liner would cover this:

unless (null signzyResp.result.failedSources) $
  logWarning $ "Signzy challan search partial: failed sources " <> show signzyResp.result.failedSources

3. ChallanSearchResp.result is non-optional — API errors swallowed

File: Signzy/Types.hs

If Signzy returns an error envelope (e.g. {"error": "invalid vehicle number"}), the FromJSON for result fails silently and produces a generic error. Wrapping in Maybe or handling the error case would surface Signzy's actual message.


One-line summary: Fix status_codestatusCode to unblock runtime; the rest are polish.

@RitikaHanish RitikaHanish left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One-line summary: Fix status_code → statusCode to unblock runtime; the rest are polish.

@RitoBose

RitoBose commented Jun 9, 2026

Copy link
Copy Markdown
Author

@ClaudeWeb the critical issue u mentioned "🔴 Critical — Must Fix
status_code :: Int is still snake_case and will break JSON parsing at runtime
File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs

data ChallanSearchResult = ChallanSearchResult
{ regNo :: Text, -- camelCase
message :: Text, -- camelCase
status_code :: Int, -- ← snake_case ⚠️
challanDetails :: [ChallanDetail],
successfulSources :: [Text],
failedSources :: [Text]
}
deriving (Show, Generic, ToJSON, FromJSON)
With deriving (Generic, FromJSON), Aeson uses field names verbatim as JSON keys. Every other field here is camelCase — which means Signzy's API sends "statusCode", not "status_code". Since this field is Int (not Maybe Int), a key mismatch causes the entire ChallanSearchResult to fail to parse, making every API call throw a runtime error.

Fix — rename to match the other fields:

statusCode :: Int,
"

is perhaps not applicable as the 3rd party API itself returns this response {
"result": {
"regNo": "xxxxxxxxx",
"message": "Challan Details Found",
"status_code": 200,
"challanDetails": [
{
"number": 1,
"challanNumber": "xxxxxxxxxxxxx",
"offenseDetails": "122/177 Leaving Vehicles In Such A Position, To Cause Or Likely To Cause Danger,Obstruction Or Undue Inconvenience To Any Road Users",
"challanPlace": " xxxxxxxxxxx",
"challanDate": "xxxxxxxxx",
"state": "WB",
"rto": "Bidhannagar",
"accusedName": "xxxxxxxxxxx",
"amount": "500",
"challanStatus": "Pending",
"court_challan": "0",
"challanSource": "ECHALLAN",
"paymentSource": "https://echallan.parivahan.gov.in/"
},
{
"number": 2,
"challanNumber": "xxxxxxxxxxxxxxxx",
"offenseDetails": "122/177MVA No Parking #",
"challanPlace": "xxxxxxx",
"challanDate": "xxxxxx",
"state": "WB",
"rto": "Bidhannagar",
"accusedName": "xxxxxxxx",
"amount": "500",
"challanStatus": "Pending",
"court_challan": "0",
"challanSource": "ECHALLAN",
"paymentSource": "https://echallan.parivahan.gov.in/"
}
],
"successfulSources": [
"ECHALLAN",
"WB"
],
"failedSources": []
}
}

@khuzema786

Copy link
Copy Markdown
Contributor

Fair correction — the API genuinely sends status_code as snake_case (a quirk of Signzy's own response format). That finding is withdrawn.

However, looking at the actual API response you shared, there's a new concern worth flagging:


🟡 New Suggestion — ChallanDetail non-Maybe fields may cause silent parse failures

File: lib/mobility-core/src/Kernel/External/ChallanSearch/Signzy/Types.hs

The current ChallanDetail has several non-Maybe required fields:

data ChallanDetail = ChallanDetail
  { number       :: Int,    -- non-Maybe
    challanNumber :: Text,  -- non-Maybe
    rto           :: Text,  -- non-Maybe ⚠️
    accusedName   :: Text,  -- non-Maybe ⚠️
    amount        :: Text,  -- non-Maybe ⚠️
    challanStatus :: Text
  }

But the API response shows other fields like offenseDetails, challanDate, state, court_challan etc. — implying Signzy aggregates challans from multiple state sources (you can see successfulSources: ["ECHALLAN", "WB"]). Different state sources may return null or omit rto, accusedName, or amount for some challans. Since these are non-Maybe, a single malformed challan in the array will cause the entire response to fail parsing, meaning getPendingChallanCount throws instead of returning a partial count.

Suggested fix — make the variable fields optional:

data ChallanDetail = ChallanDetail
  { number       :: Int,
    challanNumber :: Text,
    rto           :: Maybe Text,
    accusedName   :: Maybe Text,
    amount        :: Maybe Text,
    challanStatus :: Text         -- keep required; this is what we filter on
  }

Everything else from the previous review stands as suggestions (not blockers). The status_code snake_case is confirmed correct — my mistake.

@RitoBose RitoBose force-pushed the feat/challan/signzy branch from 40861bc to d16b6b4 Compare June 9, 2026 13:57
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.

3 participants